1 | /**************************************************************************** |
2 | ** |
3 | ** Copyright (C) 2016 The Qt Company Ltd. |
4 | ** Contact: https://www.qt.io/licensing/ |
5 | ** |
6 | ** This file is part of the QtQuick module of the Qt Toolkit. |
7 | ** |
8 | ** $QT_BEGIN_LICENSE:LGPL$ |
9 | ** Commercial License Usage |
10 | ** Licensees holding valid commercial Qt licenses may use this file in |
11 | ** accordance with the commercial license agreement provided with the |
12 | ** Software or, alternatively, in accordance with the terms contained in |
13 | ** a written agreement between you and The Qt Company. For licensing terms |
14 | ** and conditions see https://www.qt.io/terms-conditions. For further |
15 | ** information use the contact form at https://www.qt.io/contact-us. |
16 | ** |
17 | ** GNU Lesser General Public License Usage |
18 | ** Alternatively, this file may be used under the terms of the GNU Lesser |
19 | ** General Public License version 3 as published by the Free Software |
20 | ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
21 | ** packaging of this file. Please review the following information to |
22 | ** ensure the GNU Lesser General Public License version 3 requirements |
23 | ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
24 | ** |
25 | ** GNU General Public License Usage |
26 | ** Alternatively, this file may be used under the terms of the GNU |
27 | ** General Public License version 2.0 or (at your option) the GNU General |
28 | ** Public license version 3 or any later version approved by the KDE Free |
29 | ** Qt Foundation. The licenses are as published by the Free Software |
30 | ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
31 | ** included in the packaging of this file. Please review the following |
32 | ** information to ensure the GNU General Public License requirements will |
33 | ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
34 | ** https://www.gnu.org/licenses/gpl-3.0.html. |
35 | ** |
36 | ** $QT_END_LICENSE$ |
37 | ** |
38 | ****************************************************************************/ |
39 | |
40 | #include "qquickanimatedimage_p.h" |
41 | #include "qquickanimatedimage_p_p.h" |
42 | |
43 | #include <QtGui/qguiapplication.h> |
44 | #include <QtQml/qqmlinfo.h> |
45 | #include <QtQml/qqmlfile.h> |
46 | #include <QtQml/qqmlengine.h> |
47 | #include <QtGui/qmovie.h> |
48 | #if QT_CONFIG(qml_network) |
49 | #include <QtNetwork/qnetworkrequest.h> |
50 | #include <QtNetwork/qnetworkreply.h> |
51 | #endif |
52 | |
53 | QT_BEGIN_NAMESPACE |
54 | |
55 | QQuickPixmap* QQuickAnimatedImagePrivate::infoForCurrentFrame(QQmlEngine *engine) |
56 | { |
57 | if (!movie) |
58 | return nullptr; |
59 | |
60 | int current = movie->currentFrameNumber(); |
61 | if (!frameMap.contains(key: current)) { |
62 | QUrl requestedUrl; |
63 | QQuickPixmap *pixmap = nullptr; |
64 | if (engine && !movie->fileName().isEmpty()) { |
65 | requestedUrl.setUrl(url: QString::fromUtf8(str: "quickanimatedimage://%1#%2" ) |
66 | .arg(a: movie->fileName()) |
67 | .arg(a: current)); |
68 | } |
69 | if (!requestedUrl.isEmpty()) { |
70 | if (QQuickPixmap::isCached(url: requestedUrl, requestRegion: QRect(), requestSize: QSize(), frame: 0, options: QQuickImageProviderOptions())) |
71 | pixmap = new QQuickPixmap(engine, requestedUrl); |
72 | else |
73 | pixmap = new QQuickPixmap(requestedUrl, movie->currentImage()); |
74 | } else { |
75 | pixmap = new QQuickPixmap; |
76 | pixmap->setImage(movie->currentImage()); |
77 | } |
78 | frameMap.insert(key: current, value: pixmap); |
79 | } |
80 | |
81 | return frameMap.value(akey: current); |
82 | } |
83 | |
84 | /*! |
85 | \qmltype AnimatedImage |
86 | \instantiates QQuickAnimatedImage |
87 | \inqmlmodule QtQuick |
88 | \inherits Image |
89 | \brief Plays animations stored as a series of images. |
90 | \ingroup qtquick-visual |
91 | |
92 | The AnimatedImage type extends the features of the \l Image type, providing |
93 | a way to play animations stored as images containing a series of frames, |
94 | such as those stored in GIF files. |
95 | |
96 | Information about the current frame and total length of the animation can be |
97 | obtained using the \l currentFrame and \l frameCount properties. You can |
98 | start, pause and stop the animation by changing the values of the \l playing |
99 | and \l paused properties. |
100 | |
101 | The full list of supported formats can be determined with QMovie::supportedFormats(). |
102 | |
103 | \section1 Example Usage |
104 | |
105 | \beginfloatleft |
106 | \image animatedimageitem.gif |
107 | \endfloat |
108 | |
109 | The following QML shows how to display an animated image and obtain information |
110 | about its state, such as the current frame and total number of frames. |
111 | The result is an animated image with a simple progress indicator underneath it. |
112 | |
113 | \b Note: When animated images are cached, every frame of the animation will be cached. |
114 | |
115 | Set cache to false if you are playing a long or large animation and you |
116 | want to conserve memory. |
117 | |
118 | If the image data comes from a sequential device (e.g. a socket), |
119 | AnimatedImage can only loop if cache is set to true. |
120 | |
121 | \clearfloat |
122 | \snippet qml/animatedimage.qml document |
123 | |
124 | \sa BorderImage, Image |
125 | */ |
126 | |
127 | /*! |
128 | \qmlproperty url QtQuick::AnimatedImage::source |
129 | |
130 | This property holds the URL that refers to the source image. |
131 | |
132 | AnimatedImage can handle any image format supported by Qt, loaded |
133 | from any URL scheme supported by Qt. It is however not compatible |
134 | with QQuickImageProvider. |
135 | */ |
136 | |
137 | QQuickAnimatedImage::QQuickAnimatedImage(QQuickItem *parent) |
138 | : QQuickImage(*(new QQuickAnimatedImagePrivate), parent) |
139 | { |
140 | connect(sender: this, signal: &QQuickImageBase::cacheChanged, receiver: this, slot: &QQuickAnimatedImage::onCacheChanged); |
141 | connect(sender: this, signal: &QQuickImageBase::currentFrameChanged, receiver: this, slot: &QQuickAnimatedImage::frameChanged); |
142 | connect(sender: this, signal: &QQuickImageBase::currentFrameChanged, receiver: this, slot: &QQuickAnimatedImage::currentFrameChanged); |
143 | connect(sender: this, signal: &QQuickImageBase::frameCountChanged, receiver: this, slot: &QQuickAnimatedImage::frameCountChanged); |
144 | } |
145 | |
146 | QQuickAnimatedImage::~QQuickAnimatedImage() |
147 | { |
148 | Q_D(QQuickAnimatedImage); |
149 | #if QT_CONFIG(qml_network) |
150 | if (d->reply) |
151 | d->reply->deleteLater(); |
152 | #endif |
153 | delete d->movie; |
154 | qDeleteAll(c: d->frameMap); |
155 | d->frameMap.clear(); |
156 | } |
157 | |
158 | /*! |
159 | \qmlproperty bool QtQuick::AnimatedImage::paused |
160 | This property holds whether the animated image is paused. |
161 | |
162 | By default, this property is false. Set it to true when you want to pause |
163 | the animation. |
164 | */ |
165 | |
166 | bool QQuickAnimatedImage::isPaused() const |
167 | { |
168 | Q_D(const QQuickAnimatedImage); |
169 | if (!d->movie) |
170 | return d->paused; |
171 | return d->movie->state()==QMovie::Paused; |
172 | } |
173 | |
174 | void QQuickAnimatedImage::setPaused(bool pause) |
175 | { |
176 | Q_D(QQuickAnimatedImage); |
177 | if (pause == d->paused) |
178 | return; |
179 | if (!d->movie) { |
180 | d->paused = pause; |
181 | emit pausedChanged(); |
182 | } else { |
183 | d->movie->setPaused(pause); |
184 | } |
185 | } |
186 | |
187 | /*! |
188 | \qmlproperty bool QtQuick::AnimatedImage::playing |
189 | This property holds whether the animated image is playing. |
190 | |
191 | By default, this property is true, meaning that the animation |
192 | will start playing immediately. |
193 | |
194 | \b Note: this property is affected by changes to the actual playing |
195 | state of AnimatedImage. If non-animated images are used, \a playing |
196 | will need to be manually set to \a true in order to animate |
197 | following images. |
198 | \qml |
199 | AnimatedImage { |
200 | onStatusChanged: playing = (status == AnimatedImage.Ready) |
201 | } |
202 | \endqml |
203 | */ |
204 | |
205 | bool QQuickAnimatedImage::isPlaying() const |
206 | { |
207 | Q_D(const QQuickAnimatedImage); |
208 | if (!d->movie) |
209 | return d->playing; |
210 | return d->movie->state()!=QMovie::NotRunning; |
211 | } |
212 | |
213 | void QQuickAnimatedImage::setPlaying(bool play) |
214 | { |
215 | Q_D(QQuickAnimatedImage); |
216 | if (play == d->playing) |
217 | return; |
218 | if (!d->movie) { |
219 | d->playing = play; |
220 | emit playingChanged(); |
221 | return; |
222 | } |
223 | if (play) |
224 | d->movie->start(); |
225 | else |
226 | d->movie->stop(); |
227 | } |
228 | |
229 | /*! |
230 | \qmlproperty int QtQuick::AnimatedImage::currentFrame |
231 | \qmlproperty int QtQuick::AnimatedImage::frameCount |
232 | |
233 | currentFrame is the frame that is currently visible. By monitoring this property |
234 | for changes, you can animate other items at the same time as the image. |
235 | |
236 | frameCount is the number of frames in the animation. For some animation formats, |
237 | frameCount is unknown and has a value of zero. |
238 | */ |
239 | int QQuickAnimatedImage::currentFrame() const |
240 | { |
241 | Q_D(const QQuickAnimatedImage); |
242 | if (!d->movie) |
243 | return d->presetCurrentFrame; |
244 | return d->movie->currentFrameNumber(); |
245 | } |
246 | |
247 | void QQuickAnimatedImage::setCurrentFrame(int frame) |
248 | { |
249 | Q_D(QQuickAnimatedImage); |
250 | if (!d->movie) { |
251 | d->presetCurrentFrame = frame; |
252 | return; |
253 | } |
254 | d->movie->jumpToFrame(frameNumber: frame); |
255 | } |
256 | |
257 | int QQuickAnimatedImage::frameCount() const |
258 | { |
259 | Q_D(const QQuickAnimatedImage); |
260 | if (!d->movie) |
261 | return 0; |
262 | return d->movie->frameCount(); |
263 | } |
264 | |
265 | /*! |
266 | \qmlproperty real QtQuick::AnimatedImage::speed |
267 | \since QtQuick 2.11 |
268 | |
269 | This property holds the speed of the animation. |
270 | |
271 | The speed is measured in percentage of the original animated image speed. |
272 | The default speed is 1.0 (original speed). |
273 | */ |
274 | qreal QQuickAnimatedImage::speed() const |
275 | { |
276 | Q_D(const QQuickAnimatedImage); |
277 | return d->speed; |
278 | } |
279 | |
280 | void QQuickAnimatedImage::setSpeed(qreal speed) |
281 | { |
282 | Q_D(QQuickAnimatedImage); |
283 | if (d->speed != speed) { |
284 | d->speed = speed; |
285 | if (d->movie) |
286 | d->movie->setSpeed(qRound(d: speed * 100.0)); |
287 | emit speedChanged(); |
288 | } |
289 | } |
290 | |
291 | void QQuickAnimatedImage::setSource(const QUrl &url) |
292 | { |
293 | Q_D(QQuickAnimatedImage); |
294 | if (url == d->url) |
295 | return; |
296 | |
297 | #if QT_CONFIG(qml_network) |
298 | if (d->reply) { |
299 | d->reply->deleteLater(); |
300 | d->reply = nullptr; |
301 | } |
302 | #endif |
303 | |
304 | d->setImage(QImage()); |
305 | qDeleteAll(c: d->frameMap); |
306 | d->frameMap.clear(); |
307 | |
308 | d->oldPlaying = isPlaying(); |
309 | d->setMovie(nullptr); |
310 | d->url = url; |
311 | emit sourceChanged(d->url); |
312 | |
313 | if (isComponentComplete()) |
314 | load(); |
315 | } |
316 | |
317 | void QQuickAnimatedImage::load() |
318 | { |
319 | Q_D(QQuickAnimatedImage); |
320 | |
321 | if (d->url.isEmpty()) { |
322 | if (d->progress != 0) { |
323 | d->progress = 0; |
324 | emit progressChanged(progress: d->progress); |
325 | } |
326 | |
327 | d->setImage(QImage()); |
328 | d->status = Null; |
329 | emit statusChanged(d->status); |
330 | |
331 | d->currentSourceSize = QSize(0, 0); |
332 | if (d->currentSourceSize != d->oldSourceSize) { |
333 | d->oldSourceSize = d->currentSourceSize; |
334 | emit sourceSizeChanged(); |
335 | } |
336 | if (isPlaying() != d->oldPlaying) |
337 | emit playingChanged(); |
338 | } else { |
339 | const qreal targetDevicePixelRatio = (window() ? window()->effectiveDevicePixelRatio() : qApp->devicePixelRatio()); |
340 | d->devicePixelRatio = 1.0; |
341 | |
342 | QUrl loadUrl = d->url; |
343 | resolve2xLocalFile(url: d->url, targetDevicePixelRatio, sourceUrl: &loadUrl, sourceDevicePixelRatio: &d->devicePixelRatio); |
344 | QString lf = QQmlFile::urlToLocalFileOrQrc(loadUrl); |
345 | |
346 | if (!lf.isEmpty()) { |
347 | d->setMovie(new QMovie(lf)); |
348 | movieRequestFinished(); |
349 | } else { |
350 | #if QT_CONFIG(qml_network) |
351 | if (d->status != Loading) { |
352 | d->status = Loading; |
353 | emit statusChanged(d->status); |
354 | } |
355 | if (d->progress != 0) { |
356 | d->progress = 0; |
357 | emit progressChanged(progress: d->progress); |
358 | } |
359 | QNetworkRequest req(d->url); |
360 | req.setAttribute(code: QNetworkRequest::HttpPipeliningAllowedAttribute, value: true); |
361 | |
362 | d->reply = qmlEngine(this)->networkAccessManager()->get(request: req); |
363 | connect(sender: d->reply, signal: &QNetworkReply::finished, receiver: this, slot: &QQuickAnimatedImage::movieRequestFinished); |
364 | connect(sender: d->reply, SIGNAL(downloadProgress(qint64,qint64)), receiver: this, SLOT(requestProgress(qint64,qint64))); |
365 | #endif |
366 | } |
367 | } |
368 | } |
369 | |
370 | #define ANIMATEDIMAGE_MAXIMUM_REDIRECT_RECURSION 16 |
371 | |
372 | void QQuickAnimatedImage::movieRequestFinished() |
373 | { |
374 | |
375 | Q_D(QQuickAnimatedImage); |
376 | |
377 | #if QT_CONFIG(qml_network) |
378 | if (d->reply) { |
379 | d->redirectCount++; |
380 | if (d->redirectCount < ANIMATEDIMAGE_MAXIMUM_REDIRECT_RECURSION) { |
381 | QVariant redirect = d->reply->attribute(code: QNetworkRequest::RedirectionTargetAttribute); |
382 | if (redirect.isValid()) { |
383 | QUrl url = d->reply->url().resolved(relative: redirect.toUrl()); |
384 | d->reply->deleteLater(); |
385 | setSource(url); |
386 | return; |
387 | } |
388 | } |
389 | |
390 | d->redirectCount=0; |
391 | d->setMovie(new QMovie(d->reply)); |
392 | } |
393 | #endif |
394 | |
395 | if (!d->movie || !d->movie->isValid()) { |
396 | qmlWarning(me: this) << "Error Reading Animated Image File " << d->url.toString(); |
397 | d->setMovie(nullptr); |
398 | d->setImage(QImage()); |
399 | if (d->progress != 0) { |
400 | d->progress = 0; |
401 | emit progressChanged(progress: d->progress); |
402 | } |
403 | d->status = Error; |
404 | emit statusChanged(d->status); |
405 | |
406 | d->currentSourceSize = QSize(0, 0); |
407 | if (d->currentSourceSize != d->oldSourceSize) { |
408 | d->oldSourceSize = d->currentSourceSize; |
409 | emit sourceSizeChanged(); |
410 | } |
411 | if (isPlaying() != d->oldPlaying) |
412 | emit playingChanged(); |
413 | return; |
414 | } |
415 | |
416 | connect(sender: d->movie, signal: &QMovie::stateChanged, receiver: this, slot: &QQuickAnimatedImage::playingStatusChanged); |
417 | connect(sender: d->movie, signal: &QMovie::frameChanged, receiver: this, slot: &QQuickAnimatedImage::movieUpdate); |
418 | if (d->cache) |
419 | d->movie->setCacheMode(QMovie::CacheAll); |
420 | d->movie->setSpeed(qRound(d: d->speed * 100.0)); |
421 | |
422 | d->status = Ready; |
423 | emit statusChanged(d->status); |
424 | |
425 | if (d->progress != 1.0) { |
426 | d->progress = 1.0; |
427 | emit progressChanged(progress: d->progress); |
428 | } |
429 | |
430 | bool pausedAtStart = d->paused; |
431 | if (d->movie && d->playing) |
432 | d->movie->start(); |
433 | if (d->movie && pausedAtStart) |
434 | d->movie->setPaused(true); |
435 | if (d->movie && (d->paused || !d->playing)) { |
436 | d->movie->jumpToFrame(frameNumber: d->presetCurrentFrame); |
437 | d->presetCurrentFrame = 0; |
438 | } |
439 | |
440 | QQuickPixmap *pixmap = d->infoForCurrentFrame(engine: qmlEngine(this)); |
441 | if (pixmap) |
442 | d->setPixmap(*pixmap); |
443 | |
444 | if (isPlaying() != d->oldPlaying) |
445 | emit playingChanged(); |
446 | |
447 | if (d->movie) |
448 | d->currentSourceSize = d->movie->currentPixmap().size(); |
449 | else |
450 | d->currentSourceSize = QSize(0, 0); |
451 | |
452 | if (d->currentSourceSize != d->oldSourceSize) { |
453 | d->oldSourceSize = d->currentSourceSize; |
454 | emit sourceSizeChanged(); |
455 | } |
456 | } |
457 | |
458 | void QQuickAnimatedImage::movieUpdate() |
459 | { |
460 | Q_D(QQuickAnimatedImage); |
461 | |
462 | if (!d->cache) { |
463 | qDeleteAll(c: d->frameMap); |
464 | d->frameMap.clear(); |
465 | } |
466 | |
467 | if (d->movie) { |
468 | d->setPixmap(*d->infoForCurrentFrame(engine: qmlEngine(this))); |
469 | emit QQuickImageBase::currentFrameChanged(); |
470 | } |
471 | } |
472 | |
473 | void QQuickAnimatedImage::playingStatusChanged() |
474 | { |
475 | Q_D(QQuickAnimatedImage); |
476 | |
477 | if ((d->movie->state() != QMovie::NotRunning) != d->playing) { |
478 | d->playing = (d->movie->state() != QMovie::NotRunning); |
479 | emit playingChanged(); |
480 | } |
481 | if ((d->movie->state() == QMovie::Paused) != d->paused) { |
482 | d->paused = (d->movie->state() == QMovie::Paused); |
483 | emit pausedChanged(); |
484 | } |
485 | } |
486 | |
487 | void QQuickAnimatedImage::onCacheChanged() |
488 | { |
489 | Q_D(QQuickAnimatedImage); |
490 | if (!cache()) { |
491 | qDeleteAll(c: d->frameMap); |
492 | d->frameMap.clear(); |
493 | if (d->movie) |
494 | d->movie->setCacheMode(QMovie::CacheNone); |
495 | } else { |
496 | if (d->movie) |
497 | d->movie->setCacheMode(QMovie::CacheAll); |
498 | } |
499 | } |
500 | |
501 | QSize QQuickAnimatedImage::sourceSize() |
502 | { |
503 | Q_D(QQuickAnimatedImage); |
504 | return d->currentSourceSize; |
505 | } |
506 | |
507 | void QQuickAnimatedImage::componentComplete() |
508 | { |
509 | QQuickItem::componentComplete(); // NOT QQuickImage |
510 | load(); |
511 | } |
512 | |
513 | void QQuickAnimatedImagePrivate::setMovie(QMovie *m) |
514 | { |
515 | if (movie == m) |
516 | return; |
517 | Q_Q(QQuickAnimatedImage); |
518 | const int oldFrameCount = q->frameCount(); |
519 | |
520 | if (movie) { |
521 | movie->disconnect(); |
522 | movie->deleteLater(); |
523 | } |
524 | movie = m; |
525 | |
526 | if (oldFrameCount != q->frameCount()) |
527 | emit q->frameCountChanged(); |
528 | } |
529 | |
530 | QT_END_NAMESPACE |
531 | |
532 | #include "moc_qquickanimatedimage_p.cpp" |
533 | |