Merge lp:~aacid/unity8/suspend_screenshoting into lp:unity8

Proposed by Albert Astals Cid
Status: Merged
Approved by: Michael Zanetti
Approved revision: 1755
Merged at revision: 1841
Proposed branch: lp:~aacid/unity8/suspend_screenshoting
Merge into: lp:unity8
Diff against target: 735 lines (+524/-22)
17 files modified
debian/unity8-private.install (+1/-0)
plugins/CMakeLists.txt (+1/-0)
plugins/SessionGrabber/CMakeLists.txt (+8/-0)
plugins/SessionGrabber/SessionGrabber.qmltypes (+23/-0)
plugins/SessionGrabber/plugin.cpp (+27/-0)
plugins/SessionGrabber/plugin.h (+32/-0)
plugins/SessionGrabber/qmldir (+3/-0)
plugins/SessionGrabber/sessiongrabber.cpp (+135/-0)
plugins/SessionGrabber/sessiongrabber.h (+85/-0)
qml/Stages/ApplicationWindow.qml (+46/-22)
qml/Stages/Splash.qml (+2/-0)
qml/Stages/SpreadDelegate.qml (+1/-0)
tests/plugins/CMakeLists.txt (+1/-0)
tests/plugins/SessionGrabber/CMakeLists.txt (+14/-0)
tests/plugins/SessionGrabber/sessiongrabbertest.cpp (+79/-0)
tests/plugins/SessionGrabber/sessiongrabbertest.qml (+22/-0)
tests/qmltests/Stages/tst_ApplicationWindow.qml (+44/-0)
To merge this branch: bzr merge lp:~aacid/unity8/suspend_screenshoting
Reviewer Review Type Date Requested Status
PS Jenkins bot (community) continuous-integration Needs Fixing
Michael Zanetti (community) Abstain
Andrea Cimitan (community) Abstain
Michał Sawicz Approve
Daniel d'Andrada (community) Needs Information
Review via email: mp+257119@code.launchpad.net

Commit message

Save screenshots of suspended apps to disk

Description of the change

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

 * Did you perform an exploratory manual test run of your code change and any related functionality?
Not yet :D

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

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

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

To post a comment you must log in.
Revision history for this message
Michał Sawicz (saviq) wrote :

As mentioned, SessionScreenshoter could use a better name... For one, it could be SessionShotter (note double-t), as it's not shooting the screen, just the session ;). SessionGrabber? We already have a ScreenGrabber!

Generate qmltypes please.

Is the screenshot unloaded if app has a drawn-to surface?

Some clarification about the activity indicator - we do want it to run over either screenshot or splash screen *if* the app is active/focused, just not if it's in the background.

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Albert Astals Cid (aacid) wrote :

> Is the screenshot unloaded if app has a drawn-to surface?
Yes, see

           Transition {
                from: "screenshot"; to: "surface"
                SequentialAnimation {
                    ...
                    PropertyAction { target: screenshotImage; property: "source"; value: "" }
                }
            }

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Albert Astals Cid (aacid) wrote :

> Generate qmltypes please.

Done

Revision history for this message
Albert Astals Cid (aacid) wrote :

> Some clarification about the activity indicator - we do want it to run over
> either screenshot or splash screen *if* the app is active/focused, just not if
> it's in the background.

This should be there now.

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Albert Astals Cid (aacid) wrote :

> I think we're unnecessarily complicating this by using the intermediate SOURCES variable, not sure why we started it ;)
Fixed

> Why not QStandardPaths::CacheLocation/app_shots/?
Utils/windowstatestorage.cpp is not using it so i did the same pattern.

> We tend to put the comma after newline.
Fixed, We need a bot to do this :D

> Any reason why not if (image.save(path))?
I think at some point i was returning b, changed it

> If you s/Screenshoter/Grabber/, likely s/take/grab/ here, too.
Done

> Maybe ::drop() or ::clear()... or just ::remove()? It feels weird to have a nice and short ::take() and then the verbose removeScreenshot().
Honestly i'd prefer not to, the thing is that we have "SessionGrabber", so it's quite clear what "grab" does, but "remove/drop/clear" seems like it may clear the grabber (i.e. for example reset parameters), not actually remove the screenshot.

> Third time, please factor out, maybe even cachePath(appId)?
Sure

> /me not totally familiar with QtConcurrent, can you explain why we need the QSharedPointer here?
You mean for the watcher or for the grab result?

> Typo "AppdId"
Fixed

> Typotoo.
Fixed

> As you rename... grabbed()?
Done

> If you rename ::removeScreenshot, likely rename this too, clearSessionGrab or so?
Not done since i didn't change the other one

> I'd probably use screenshoter.path || d.defaultScreenshot.
Ok, changed, not that it makes a difference

> Maybe add a comment why that?
Done

> Spinner active if running?
If the splash is still around (it'll go away as soon as the first frame is drawn), yes

> No spinner if stopped? Should be if stopped and !active?
How would one be stopped and active at the same time?

Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Michał Sawicz (saviq) wrote :

>> Why not QStandardPaths::CacheLocation/app_shots/?
> Utils/windowstatestorage.cpp is not using it so i did the same pattern.

Yeah, but at least it writes in ~/.cache/*unity8*/, not in ~/.cache/ directly ;)

Also, there's no transition when screenshot is loaded over splashscreen.

review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Daniel d'Andrada (dandrader) wrote :

"""
added file 'plugins/SessionGrabber/sessiongrabber.cpp'
"""

Anything against using CamelCase for the file names? eg: SessionGrabber.cpp
What do we gain from making it all lower-case?

review: Needs Information
Revision history for this message
Michał Sawicz (saviq) wrote :

 * Did you perform an exploratory manual test run of the code change and any related functionality?
Y
 * Did CI run pass? If not, please explain why.
Some flakiness, will run again before top-approving.
 * Did you make sure that the branch does not contain spurious tags?
Y

review: Approve
Revision history for this message
Albert Astals Cid (aacid) wrote :

> """
> added file 'plugins/SessionGrabber/sessiongrabber.cpp'
> """
>
> Anything against using CamelCase for the file names? eg: SessionGrabber.cpp
> What do we gain from making it all lower-case?

Having to press the shift key fewer times :D

Honestly we have half the plugins that use lowercase and half that use CamelCase, at some points we should decide what we want to use, but not sure this time should be now. Maybe on Dallas?

Revision history for this message
Andrea Cimitan (cimi) :
review: Needs Information
Revision history for this message
Andrea Cimitan (cimi) wrote :

After discussing with Gerry, eventually we might decide to have full resolution screenshots (maybe in black and white to indicate that is suspended), but we will fix it in a different branch

Revision history for this message
Andrea Cimitan (cimi) :
review: Abstain
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
Revision history for this message
Albert Astals Cid (aacid) wrote :

> After discussing with Gerry, eventually we might decide to have full
> resolution screenshots (maybe in black and white to indicate that is
> suspended), but we will fix it in a different branch

FWIW the screenshot in disk is full resolution, it's the showing only that is not full resolution so won't lose resolution or have to re-take screenshots when changing it later

Revision history for this message
Michael Zanetti (mzanetti) wrote :

Needs a merge

review: Needs Fixing
Revision history for this message
Albert Astals Cid (aacid) wrote :

Merged

Revision history for this message
Michael Zanetti (mzanetti) wrote :
review: Needs Fixing
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)
1754. By Albert Astals Cid

Merge unity8

1755. By Albert Astals Cid

Build++

Revision history for this message
Albert Astals Cid (aacid) wrote :

> Doesn't compile any more after merging with trunk:
> https://launchpadlibrarian.net/209888034/buildlog_ubuntu-
> wily-i386.unity8_8.10%2B15.10.20150624-0ubuntu1_BUILDING.txt.gz

Fixed.

Revision history for this message
Michael Zanetti (mzanetti) :
review: Abstain
Revision history for this message
PS Jenkins bot (ps-jenkins) wrote :
review: Needs Fixing (continuous-integration)

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'debian/unity8-private.install'
2--- debian/unity8-private.install 2015-04-21 15:22:53 +0000
3+++ debian/unity8-private.install 2015-06-30 11:41:39 +0000
4@@ -8,6 +8,7 @@
5 usr/lib/*/unity8/qml/Powerd
6 usr/lib/*/unity8/qml/ScreenGrabber
7 usr/lib/*/unity8/qml/SessionBroadcast
8+usr/lib/*/unity8/qml/SessionGrabber
9 usr/lib/*/unity8/qml/Ubuntu
10 usr/lib/*/unity8/qml/Unity
11 usr/lib/*/unity8/qml/Utils
12
13=== modified file 'plugins/CMakeLists.txt'
14--- plugins/CMakeLists.txt 2014-12-12 10:00:31 +0000
15+++ plugins/CMakeLists.txt 2015-06-30 11:41:39 +0000
16@@ -18,6 +18,7 @@
17 add_subdirectory(Dash)
18 add_subdirectory(Powerd)
19 add_subdirectory(SessionBroadcast)
20+add_subdirectory(SessionGrabber)
21 add_subdirectory(ScreenGrabber)
22 add_subdirectory(Ubuntu)
23 add_subdirectory(Unity)
24
25=== added directory 'plugins/SessionGrabber'
26=== added file 'plugins/SessionGrabber/CMakeLists.txt'
27--- plugins/SessionGrabber/CMakeLists.txt 1970-01-01 00:00:00 +0000
28+++ plugins/SessionGrabber/CMakeLists.txt 2015-06-30 11:41:39 +0000
29@@ -0,0 +1,8 @@
30+include_directories(
31+ ${CMAKE_CURRENT_BINARY_DIR}
32+)
33+
34+add_library(SessionGrabber-qml MODULE sessiongrabber.cpp plugin.cpp)
35+qt5_use_modules(SessionGrabber-qml Qml Gui Quick Concurrent)
36+
37+add_unity8_plugin(SessionGrabber 0.1 SessionGrabber TARGETS SessionGrabber-qml)
38
39=== added file 'plugins/SessionGrabber/SessionGrabber.qmltypes'
40--- plugins/SessionGrabber/SessionGrabber.qmltypes 1970-01-01 00:00:00 +0000
41+++ plugins/SessionGrabber/SessionGrabber.qmltypes 2015-06-30 11:41:39 +0000
42@@ -0,0 +1,23 @@
43+import QtQuick.tooling 1.1
44+
45+// This file describes the plugin-supplied types contained in the library.
46+// It is used for QML tooling purposes only.
47+//
48+// This file was auto-generated by:
49+// 'qmlplugindump -notrelocatable SessionGrabber 0.1 plugins'
50+
51+Module {
52+ Component {
53+ name: "SessionGrabber"
54+ prototype: "QObject"
55+ exports: ["SessionGrabber/SessionGrabber 0.1"]
56+ exportMetaObjectRevisions: [0]
57+ Property { name: "appId"; type: "string" }
58+ Property { name: "path"; type: "string"; isReadonly: true }
59+ Property { name: "target"; type: "QQuickItem"; isPointer: true }
60+ Signal { name: "appIdChanged" }
61+ Signal { name: "screenshotTaken" }
62+ Method { name: "take" }
63+ Method { name: "removeScreenshot" }
64+ }
65+}
66
67=== added file 'plugins/SessionGrabber/plugin.cpp'
68--- plugins/SessionGrabber/plugin.cpp 1970-01-01 00:00:00 +0000
69+++ plugins/SessionGrabber/plugin.cpp 2015-06-30 11:41:39 +0000
70@@ -0,0 +1,27 @@
71+/*
72+ * Copyright (C) 2015 Canonical, Ltd.
73+ *
74+ * This program is free software; you can redistribute it and/or modify
75+ * it under the terms of the GNU General Public License as published by
76+ * the Free Software Foundation; version 3.
77+ *
78+ * This program is distributed in the hope that it will be useful,
79+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
80+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
81+ * GNU General Public License for more details.
82+ *
83+ * You should have received a copy of the GNU General Public License
84+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
85+ *
86+ */
87+
88+#include "plugin.h"
89+#include "sessiongrabber.h"
90+
91+#include <QtQml/qqml.h>
92+
93+void SessionGrabberPlugin::registerTypes(const char *uri)
94+{
95+ Q_ASSERT(uri == QLatin1String("SessionGrabber"));
96+ qmlRegisterType<SessionGrabber>(uri, 0, 1, "SessionGrabber");
97+}
98
99=== added file 'plugins/SessionGrabber/plugin.h'
100--- plugins/SessionGrabber/plugin.h 1970-01-01 00:00:00 +0000
101+++ plugins/SessionGrabber/plugin.h 2015-06-30 11:41:39 +0000
102@@ -0,0 +1,32 @@
103+/*
104+ * Copyright (C) 2015 Canonical, Ltd.
105+ *
106+ * This program is free software; you can redistribute it and/or modify
107+ * it under the terms of the GNU General Public License as published by
108+ * the Free Software Foundation; version 3.
109+ *
110+ * This program is distributed in the hope that it will be useful,
111+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
112+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
113+ * GNU General Public License for more details.
114+ *
115+ * You should have received a copy of the GNU General Public License
116+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
117+ *
118+ */
119+
120+#ifndef SESSIONGRABBER_PLUGIN_H
121+#define SESSIONGRABBER_PLUGIN_H
122+
123+#include <QtQml/QQmlExtensionPlugin>
124+
125+class SessionGrabberPlugin : public QQmlExtensionPlugin
126+{
127+ Q_OBJECT
128+ Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QQmlExtensionInterface")
129+
130+public:
131+ void registerTypes(const char *uri);
132+};
133+
134+#endif
135
136=== added file 'plugins/SessionGrabber/qmldir'
137--- plugins/SessionGrabber/qmldir 1970-01-01 00:00:00 +0000
138+++ plugins/SessionGrabber/qmldir 2015-06-30 11:41:39 +0000
139@@ -0,0 +1,3 @@
140+module SessionGrabber
141+plugin SessionGrabber-qml
142+typeinfo SessionGrabber.qmltypes
143
144=== added file 'plugins/SessionGrabber/sessiongrabber.cpp'
145--- plugins/SessionGrabber/sessiongrabber.cpp 1970-01-01 00:00:00 +0000
146+++ plugins/SessionGrabber/sessiongrabber.cpp 2015-06-30 11:41:39 +0000
147@@ -0,0 +1,135 @@
148+/*
149+ * Copyright (C) 2015 Canonical, Ltd.
150+ *
151+ * This program is free software; you can redistribute it and/or modify
152+ * it under the terms of the GNU General Public License as published by
153+ * the Free Software Foundation; version 3.
154+ *
155+ * This program is distributed in the hope that it will be useful,
156+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
157+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
158+ * GNU General Public License for more details.
159+ *
160+ * You should have received a copy of the GNU General Public License
161+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
162+ *
163+ */
164+
165+#include "sessiongrabber.h"
166+
167+#include <QDebug>
168+#include <QQuickItem>
169+#include <QQuickItemGrabResult>
170+#include <QtConcurrent>
171+
172+static QString cacheFolder()
173+{
174+ return QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + "/app_shots/";
175+}
176+
177+static QString cachePath(const QString &appId)
178+{
179+ return cacheFolder() + appId + ".png";
180+}
181+
182+SessionGrabber::SessionGrabber(QObject *parent)
183+ : QObject(parent)
184+ , m_target(nullptr)
185+ , m_watcher(nullptr)
186+{
187+}
188+
189+QString SessionGrabber::appId() const
190+{
191+ return m_appId;
192+}
193+
194+void SessionGrabber::setAppId(const QString &appId)
195+{
196+ if (appId != m_appId) {
197+ m_appId = appId;
198+ const QString path = cachePath(appId);
199+ if (QFile::exists(path)) {
200+ setPath(path);
201+ }
202+ }
203+}
204+
205+QQuickItem *SessionGrabber::target() const
206+{
207+ return m_target;
208+}
209+
210+void SessionGrabber::setTarget(QQuickItem *target)
211+{
212+ if (target != m_target) {
213+ m_target = target;
214+ Q_EMIT targetChanged();
215+ }
216+}
217+
218+QString SessionGrabber::path() const
219+{
220+ return m_path;
221+}
222+
223+void SessionGrabber::setPath(const QString &path)
224+{
225+ if (path != m_path) {
226+ m_path = path;
227+ Q_EMIT pathChanged();
228+ }
229+}
230+
231+static QString saveScreenshot(const QString &appId, const QSharedPointer<QQuickItemGrabResult> grabResult)
232+{
233+ const QImage image = grabResult->image();
234+ const QString cPath = cacheFolder();
235+ QDir d;
236+ d.mkpath(cPath);
237+ const QString path = cachePath(appId);
238+ if (image.save(path)) {
239+ return path;
240+ } else {
241+ return QString();
242+ }
243+}
244+
245+void SessionGrabber::grab()
246+{
247+ if (!m_grabResult.isNull()) {
248+ qWarning() << "Asked to grab a screenshot when there's one already being grabbed, ignoring";
249+ return;
250+ }
251+
252+ if (m_target && !m_appId.isEmpty()) {
253+ // grabToImage runs in a separate thread
254+ m_grabResult = m_target->grabToImage();
255+ connect(m_grabResult.data(), &QQuickItemGrabResult::ready, this, &SessionGrabber::grabReady);
256+ } else {
257+ qWarning() << "Can't grab screnshot: appId:" << m_appId << " target:" << m_target;
258+ }
259+}
260+
261+void SessionGrabber::removeScreenshot()
262+{
263+ const QString path = cachePath(m_appId);
264+ QFile::remove(path);
265+}
266+
267+void SessionGrabber::grabReady()
268+{
269+ QFuture<QString> f = QtConcurrent::run(saveScreenshot, m_appId, m_grabResult);
270+ m_watcher = new QFutureWatcher<QString>(this);
271+ m_watcher->setFuture(f);
272+ connect(m_watcher, &QFutureWatcher<QString>::finished, this, &SessionGrabber::saveFinished);
273+}
274+
275+void SessionGrabber::saveFinished()
276+{
277+ setPath(m_watcher->future().result());
278+ delete m_watcher;
279+ m_watcher = nullptr;
280+ m_grabResult.clear();
281+ Q_EMIT screenshotGrabbed();
282+}
283
284=== added file 'plugins/SessionGrabber/sessiongrabber.h'
285--- plugins/SessionGrabber/sessiongrabber.h 1970-01-01 00:00:00 +0000
286+++ plugins/SessionGrabber/sessiongrabber.h 2015-06-30 11:41:39 +0000
287@@ -0,0 +1,85 @@
288+/*
289+ * Copyright (C) 2015 Canonical, Ltd.
290+ *
291+ * This program is free software; you can redistribute it and/or modify
292+ * it under the terms of the GNU General Public License as published by
293+ * the Free Software Foundation; version 3.
294+ *
295+ * This program is distributed in the hope that it will be useful,
296+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
297+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
298+ * GNU General Public License for more details.
299+ *
300+ * You should have received a copy of the GNU General Public License
301+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
302+ *
303+ */
304+
305+#ifndef SESSIONGRABBER_H
306+#define SESSIONGRABBER_H
307+
308+#include <QObject>
309+#include <QSharedPointer>
310+#include <QFutureWatcher>
311+
312+class QQuickItem;
313+class QQuickItemGrabResult;
314+
315+/**
316+ * SessionGrabber saves to disk screenshots of the given item.
317+ * Images are saved into $HOME/.cache/app_shots/appId.png
318+ * It also handles giving back the screenshot path if it already exists (e.g. because a reboot)
319+ */
320+class SessionGrabber : public QObject
321+{
322+ Q_OBJECT
323+
324+ /// appId is the key of the screenshot name.
325+ Q_PROPERTY(QString appId READ appId WRITE setAppId NOTIFY appIdChanged)
326+
327+ /// path where the screenshot is saved, can be empty if no screenshot has been grabbed yet.
328+ Q_PROPERTY(QString path READ path NOTIFY pathChanged)
329+
330+ /// target item for the screenshot grabbing.
331+ Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged)
332+
333+public:
334+ explicit SessionGrabber(QObject *parent = 0);
335+
336+ QString appId() const;
337+ void setAppId(const QString &appId);
338+
339+ QQuickItem *target() const;
340+ void setTarget(QQuickItem *target);
341+
342+ QString path() const;
343+
344+ /// Starts grabbing a screenshot. Emits screenshotGrabbed when ready.
345+ Q_INVOKABLE void grab();
346+
347+ /// Removes the existing screenshot
348+ Q_INVOKABLE void removeScreenshot();
349+
350+Q_SIGNALS:
351+ void appIdChanged();
352+ void targetChanged();
353+ void pathChanged();
354+
355+ /// Signals screenshot grabbing has finished.
356+ void screenshotGrabbed();
357+
358+private Q_SLOTS:
359+ void grabReady();
360+ void saveFinished();
361+
362+private:
363+ void setPath(const QString &path);
364+
365+ QString m_appId;
366+ QString m_path;
367+ QQuickItem *m_target;
368+ QFutureWatcher<QString> *m_watcher;
369+ QSharedPointer<QQuickItemGrabResult> m_grabResult;
370+};
371+
372+#endif
373
374=== modified file 'qml/Stages/ApplicationWindow.qml'
375--- qml/Stages/ApplicationWindow.qml 2015-04-10 15:10:51 +0000
376+++ qml/Stages/ApplicationWindow.qml 2015-06-30 11:41:39 +0000
377@@ -17,6 +17,7 @@
378 import QtQuick 2.0
379 import Ubuntu.Components 1.1
380 import Unity.Application 0.1
381+import SessionGrabber 0.1
382
383 FocusScope {
384 id: root
385@@ -30,11 +31,16 @@
386 property QtObject application
387 property int surfaceOrientationAngle
388
389+ function removeScreenshot() {
390+ sessionGrabber.removeScreenshot();
391+ }
392+
393 QtObject {
394 id: d
395
396 // helpers so that we don't have to check for the existence of an application everywhere
397 // (in order to avoid breaking qml binding due to a javascript exception)
398+ readonly property string appId: root.application ? root.application.appId : ""
399 readonly property string name: root.application ? root.application.name : ""
400 readonly property url icon: root.application ? root.application.icon : ""
401 readonly property int applicationState: root.application ? root.application.state : -1
402@@ -50,11 +56,11 @@
403 property bool hadSurface: sessionContainer.surfaceContainer.hadSurface
404
405 property bool needToTakeScreenshot:
406- sessionContainer.surface && d.surfaceInitialized && screenshotImage.status === Image.Null
407- && d.applicationState === ApplicationInfoInterface.Stopped
408+ sessionContainer.surface && d.surfaceInitialized
409+ && (d.applicationState === ApplicationInfoInterface.Stopped || d.applicationState === ApplicationInfoInterface.Suspended)
410 onNeedToTakeScreenshotChanged: {
411 if (needToTakeScreenshot) {
412- screenshotImage.take();
413+ sessionGrabber.grab();
414 }
415 }
416
417@@ -86,27 +92,9 @@
418 onTriggered: { if (stateGroup.state === "surface") { d.surfaceOldEnoughToBeResized = true; } }
419 }
420
421- Image {
422- id: screenshotImage
423- objectName: "screenshotImage"
424- source: d.defaultScreenshot
425- anchors.fill: parent
426- antialiasing: !root.interactive
427-
428- function take() {
429- // Format: "image://application/$APP_ID/$CURRENT_TIME_MS"
430- // eg: "image://application/calculator-app/123456"
431- var timeMs = new Date().getTime();
432- source = "image://application/" + root.application.appId + "/" + timeMs;
433- }
434-
435- // Save memory by using a half-resolution (thus quarter size) screenshot
436- sourceSize.width: root.width / 2
437- sourceSize.height: root.height / 2
438- }
439-
440 Loader {
441 id: splashLoader
442+ objectName: "splashLoader"
443 visible: active
444 active: false
445 anchors.fill: parent
446@@ -120,10 +108,46 @@
447 backgroundColor: d.splashColor
448 headerColor: d.splashColorHeader
449 footerColor: d.splashColorFooter
450+ activeSpinner: d.applicationState === ApplicationInfoInterface.Starting ||
451+ d.applicationState === ApplicationInfoInterface.Running
452 }
453 }
454 }
455
456+ Image {
457+ id: screenshotImage
458+ objectName: "screenshotImage"
459+ source: sessionGrabber.path || d.defaultScreenshot
460+ anchors.fill: parent
461+ antialiasing: !root.interactive
462+ cache: false
463+ asynchronous: true
464+
465+ // Save memory by using a half-resolution (thus quarter size) screenshot
466+ sourceSize.width: root.width / 2
467+ sourceSize.height: root.height / 2
468+
469+ ActivityIndicator {
470+ id: activityIndicator
471+ anchors.centerIn: parent
472+ visible: running
473+ running: stateGroup.state == "screenshot" &&
474+ (d.applicationState === ApplicationInfoInterface.Starting || d.applicationState === ApplicationInfoInterface.Running)
475+ }
476+ }
477+
478+ SessionGrabber {
479+ id: sessionGrabber
480+ appId: d.appId
481+ target: root
482+
483+ onScreenshotGrabbed: {
484+ // Need to reset to "" and back since it may be the same path with new content
485+ screenshotImage.source = "";
486+ screenshotImage.source = sessionGrabber.path;
487+ }
488+ }
489+
490 SessionContainer {
491 id: sessionContainer
492 // A fake application might not even have a session property.
493
494=== modified file 'qml/Stages/Splash.qml'
495--- qml/Stages/Splash.qml 2014-09-16 13:48:40 +0000
496+++ qml/Stages/Splash.qml 2015-06-30 11:41:39 +0000
497@@ -31,6 +31,7 @@
498 property url icon
499 property alias title: header.title
500 property alias showHeader: header.visible
501+ property alias activeSpinner: activityIndicator.running
502
503 Ambiance.Palette {
504 id: ambiancePalette
505@@ -144,6 +145,7 @@
506 }
507
508 ActivityIndicator {
509+ id: activityIndicator
510 anchors.centerIn: header.visible ? parent : undefined
511 anchors.verticalCenterOffset: header.visible ? header.height / 2 : 0
512
513
514=== modified file 'qml/Stages/SpreadDelegate.qml'
515--- qml/Stages/SpreadDelegate.qml 2015-05-11 14:36:03 +0000
516+++ qml/Stages/SpreadDelegate.qml 2015-06-30 11:41:39 +0000
517@@ -397,6 +397,7 @@
518 if (!running) {
519 dragArea.moving = false;
520 if (requestClose) {
521+ appWindow.removeScreenshot();
522 root.closed();
523 } else {
524 dragArea.distance = 0;
525
526=== modified file 'tests/plugins/CMakeLists.txt'
527--- tests/plugins/CMakeLists.txt 2014-12-12 10:00:31 +0000
528+++ tests/plugins/CMakeLists.txt 2015-06-30 11:41:39 +0000
529@@ -2,6 +2,7 @@
530 add_subdirectory(Greeter)
531 add_subdirectory(LightDM)
532 add_subdirectory(Dash)
533+add_subdirectory(SessionGrabber)
534 add_subdirectory(Ubuntu)
535 add_subdirectory(Unity)
536 add_subdirectory(Utils)
537
538=== added directory 'tests/plugins/SessionGrabber'
539=== added file 'tests/plugins/SessionGrabber/CMakeLists.txt'
540--- tests/plugins/SessionGrabber/CMakeLists.txt 1970-01-01 00:00:00 +0000
541+++ tests/plugins/SessionGrabber/CMakeLists.txt 2015-06-30 11:41:39 +0000
542@@ -0,0 +1,14 @@
543+add_executable(SessionGrabberTestExec
544+ sessiongrabbertest.cpp
545+ ${CMAKE_SOURCE_DIR}/plugins/SessionGrabber/sessiongrabber.cpp
546+ )
547+qt5_use_modules(SessionGrabberTestExec Core Quick Test Concurrent)
548+
549+add_definitions(-DCURRENT_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}")
550+
551+include_directories(
552+ ${CMAKE_CURRENT_BINARY_DIR}
553+ ${CMAKE_SOURCE_DIR}/plugins/SessionGrabber
554+ )
555+
556+add_unity8_uitest(SessionGrabber SessionGrabberTestExec)
557
558=== added file 'tests/plugins/SessionGrabber/sessiongrabbertest.cpp'
559--- tests/plugins/SessionGrabber/sessiongrabbertest.cpp 1970-01-01 00:00:00 +0000
560+++ tests/plugins/SessionGrabber/sessiongrabbertest.cpp 2015-06-30 11:41:39 +0000
561@@ -0,0 +1,79 @@
562+/*
563+ * Copyright (C) 2015 Canonical, Ltd.
564+ *
565+ * This program is free software; you can redistribute it and/or modify
566+ * it under the terms of the GNU General Public License as published by
567+ * the Free Software Foundation; version 3.
568+ *
569+ * This program is distributed in the hope that it will be useful,
570+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
571+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
572+ * GNU General Public License for more details.
573+ *
574+ * You should have received a copy of the GNU General Public License
575+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
576+ */
577+
578+#include <QtTestGui>
579+#include <QQuickItem>
580+#include <QQuickView>
581+#include <QSignalSpy>
582+
583+#include "sessiongrabber.h"
584+
585+class SessionGrabberTest : public QObject
586+{
587+ Q_OBJECT
588+
589+private Q_SLOTS:
590+
591+ void init()
592+ {
593+ view = new QQuickView();
594+ view->setSource(QUrl::fromLocalFile(CURRENT_SOURCE_DIR "/sessiongrabbertest.qml"));
595+ view->show();
596+ QTest::qWaitForWindowExposed(view);
597+ }
598+
599+ void testSessionGrabber()
600+ {
601+ SessionGrabber s;
602+ s.setAppId("test-app-id");
603+ s.setTarget(view->rootObject());
604+ QSignalSpy spy(&s, &SessionGrabber::screenshotGrabbed);
605+
606+ QVERIFY(!QFile::exists(s.path()));
607+
608+ view->rootObject()->setProperty("color", "red");
609+ s.grab();
610+ spy.wait();
611+
612+ QVERIFY(QFile::exists(s.path()));
613+ QImage image(s.path());
614+ QCOMPARE(image.pixel(0, 0), qRgb(255, 0, 0));
615+
616+ view->rootObject()->setProperty("color", "blue");
617+ s.grab();
618+ spy.wait();
619+
620+ QVERIFY(QFile::exists(s.path()));
621+ image = QImage(s.path());
622+ QCOMPARE(image.pixel(0, 0), qRgb(0, 0, 255));
623+
624+ s.removeScreenshot();
625+
626+ QVERIFY(!QFile::exists(s.path()));
627+ }
628+
629+ void cleanup()
630+ {
631+ delete view;
632+ }
633+
634+private:
635+ QQuickView *view;
636+};
637+
638+QTEST_MAIN(SessionGrabberTest)
639+
640+#include "sessiongrabbertest.moc"
641
642=== added file 'tests/plugins/SessionGrabber/sessiongrabbertest.qml'
643--- tests/plugins/SessionGrabber/sessiongrabbertest.qml 1970-01-01 00:00:00 +0000
644+++ tests/plugins/SessionGrabber/sessiongrabbertest.qml 2015-06-30 11:41:39 +0000
645@@ -0,0 +1,22 @@
646+/*
647+ * Copyright (C) 2015 Canonical, Ltd.
648+ *
649+ * This program is free software; you can redistribute it and/or modify
650+ * it under the terms of the GNU General Public License as published by
651+ * the Free Software Foundation; version 3.
652+ *
653+ * This program is distributed in the hope that it will be useful,
654+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
655+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
656+ * GNU General Public License for more details.
657+ *
658+ * You should have received a copy of the GNU General Public License
659+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
660+ */
661+
662+import QtQuick 2.4
663+
664+Rectangle {
665+ width: 100
666+ height: 100
667+}
668
669=== modified file 'tests/qmltests/Stages/tst_ApplicationWindow.qml'
670--- tests/qmltests/Stages/tst_ApplicationWindow.qml 2015-03-06 04:44:11 +0000
671+++ tests/qmltests/Stages/tst_ApplicationWindow.qml 2015-06-30 11:41:39 +0000
672@@ -35,6 +35,9 @@
673 root.fakeApplication.manualSurfaceCreation = true;
674 root.fakeApplication.setState(ApplicationInfoInterface.Starting);
675 }
676+ Component.onDestruction: {
677+ applicationWindowLoader.item.removeScreenshot();
678+ }
679 property QtObject fakeApplication: null
680 readonly property var fakeSession: fakeApplication ? fakeApplication.session : null
681
682@@ -331,6 +334,40 @@
683 tryCompare(screenshotImage, "status", Image.Null);
684 }
685
686+ function test_suspendrestartApp() {
687+ var screenshotImage = findChild(applicationWindowLoader.item, "screenshotImage");
688+
689+ initSession();
690+ setApplicationState(appRunning);
691+ tryCompare(stateGroup, "state", "surface");
692+ waitUntilTransitionsEnd();
693+
694+ setApplicationState(appSuspended);
695+
696+ cleanupSession();
697+
698+ tryCompare(stateGroup, "state", "screenshot");
699+ waitUntilTransitionsEnd();
700+ tryCompare(fakeApplication, "session", null);
701+
702+ // and restart it
703+ setApplicationState(appStarting);
704+
705+ waitUntilTransitionsEnd();
706+ verify(stateGroup.state === "screenshot");
707+ verify(fakeSession === null);
708+
709+ setApplicationState(appRunning);
710+
711+ waitUntilTransitionsEnd();
712+ verify(stateGroup.state === "screenshot");
713+
714+ initSession();
715+
716+ tryCompare(stateGroup, "state", "surface");
717+ tryCompare(screenshotImage, "status", Image.Null);
718+ }
719+
720 function test_appCrashed() {
721 initSession();
722 setApplicationState(appRunning);
723@@ -408,5 +445,12 @@
724 applicationWindowLoader.item.interactive = true;
725 compare(fakeSession.surface.activeFocus, true);
726 }
727+
728+ function test_no_spinner_if_stopped() {
729+ setApplicationState(appStopped);
730+ tryCompare(stateGroup, "state", "splashScreen");
731+ var splashLoader = findChild(applicationWindowLoader.item, "splashLoader");
732+ verify(!splashLoader.item.activeSpinner);
733+ }
734 }
735 }

Subscribers

People subscribed via source and target branches