Merge lp:~aacid/unity8/suspend_screenshoting into lp:unity8
- suspend_screenshoting
- Merge into trunk
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 |
Related bugs: |
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
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1743
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
Albert Astals Cid (aacid) wrote : | # |
> Is the screenshot unloaded if app has a drawn-to surface?
Yes, see
}
}
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1744
http://
Executed test runs:
FAILURE: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
FAILURE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Albert Astals Cid (aacid) wrote : | # |
> Generate qmltypes please.
Done
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.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1747
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
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:
Utils/windowsta
> 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/
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.defaultScreen
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?
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1748
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Michał Sawicz (saviq) wrote : | # |
>> Why not QStandardPaths:
> Utils/windowsta
Yeah, but at least it writes in ~/.cache/*unity8*/, not in ~/.cache/ directly ;)
Also, there's no transition when screenshot is loaded over splashscreen.
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1750
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1752
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Daniel d'Andrada (dandrader) wrote : | # |
"""
added file 'plugins/
"""
Anything against using CamelCase for the file names? eg: SessionGrabber.cpp
What do we gain from making it all lower-case?
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
Albert Astals Cid (aacid) wrote : | # |
> """
> added file 'plugins/
> """
>
> 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?
Andrea Cimitan (cimi) : | # |
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
Andrea Cimitan (cimi) : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1752
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
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
Michael Zanetti (mzanetti) wrote : | # |
Needs a merge
Albert Astals Cid (aacid) wrote : | # |
Merged
Michael Zanetti (mzanetti) wrote : | # |
Doesn't compile any more after merging with trunk: https:/
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1753
http://
Executed test runs:
FAILURE: http://
FAILURE: http://
FAILURE: http://
FAILURE: http://
Click here to trigger a rebuild:
http://
- 1754. By Albert Astals Cid
-
Merge unity8
- 1755. By Albert Astals Cid
-
Build++
Albert Astals Cid (aacid) wrote : | # |
> Doesn't compile any more after merging with trunk:
> https:/
> wily-i386.
Fixed.
Michael Zanetti (mzanetti) : | # |
PS Jenkins bot (ps-jenkins) wrote : | # |
FAILED: Continuous integration, rev:1755
http://
Executed test runs:
UNSTABLE: http://
SUCCESS: http://
SUCCESS: http://
UNSTABLE: http://
SUCCESS: http://
deb: http://
SUCCESS: http://
Click here to trigger a rebuild:
http://
Preview Diff
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 | } |
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.