1 /*
2     This file is part of the KDE libraries
3     SPDX-FileCopyrightText: 2000 Kurt Granroth <granroth@kde.org>
4     SPDX-FileCopyrightText: 2006 Hamish Rodda <rodda@kde.org>
5     SPDX-FileCopyrightText: 2008 Pino Toscano <pino@kde.org>
6 
7     SPDX-License-Identifier: LGPL-2.0-only
8 */
9 
10 #include <kanimatedbutton.h>
11 
12 #include <QImageReader>
13 #include <QMovie>
14 #include <QPainter>
15 #include <QPixmap>
16 #include <QTimer>
17 
18 class KAnimatedButtonPrivate
19 {
20 public:
KAnimatedButtonPrivate(KAnimatedButton * qq)21     KAnimatedButtonPrivate(KAnimatedButton *qq)
22         : q(qq)
23     {
24     }
25 
26     void updateIcons();
27     void updateCurrentIcon();
28     void movieFrameChanged(int number);
29     void movieFinished();
30     void timerUpdate();
31 
32     KAnimatedButton *const q;
33     QMovie *movie = nullptr;
34 
35     int frames;
36     int current_frame;
37     QPixmap pixmap;
38     QTimer timer;
39     QString icon_path;
40     QVector<QPixmap *> framesCache; // We keep copies of each frame so that
41     // the icon code can properly cache them in QPixmapCache,
42     // and not fill it up with dead copies
43 };
44 
KAnimatedButton(QWidget * parent)45 KAnimatedButton::KAnimatedButton(QWidget *parent)
46     : QToolButton(parent)
47     , d(new KAnimatedButtonPrivate(this))
48 {
49     connect(&d->timer, &QTimer::timeout, this, [this]() {
50         d->timerUpdate();
51     });
52 }
53 
~KAnimatedButton()54 KAnimatedButton::~KAnimatedButton()
55 {
56     d->timer.stop();
57     qDeleteAll(d->framesCache);
58     delete d->movie;
59 }
60 
start()61 void KAnimatedButton::start()
62 {
63     if (d->movie) {
64         d->movie->start();
65     } else {
66         d->current_frame = 0;
67         d->timer.start(50);
68     }
69 }
70 
stop()71 void KAnimatedButton::stop()
72 {
73     if (d->movie) {
74         d->movie->stop();
75         d->movie->jumpToFrame(0);
76         d->movieFrameChanged(0);
77     } else {
78         d->current_frame = 0;
79         d->timer.stop();
80         d->updateCurrentIcon();
81     }
82 }
83 
setAnimationPath(const QString & path)84 void KAnimatedButton::setAnimationPath(const QString &path)
85 {
86     if (d->icon_path == path) {
87         return;
88     }
89 
90     d->timer.stop();
91     d->icon_path = path;
92     d->updateIcons();
93 }
94 
animationPath() const95 QString KAnimatedButton::animationPath() const
96 {
97     return d->icon_path;
98 }
99 
timerUpdate()100 void KAnimatedButtonPrivate::timerUpdate()
101 {
102     if (!q->isVisible()) {
103         return;
104     }
105 
106     current_frame++;
107     if (current_frame == frames) {
108         current_frame = 0;
109     }
110 
111     updateCurrentIcon();
112 }
113 
updateCurrentIcon()114 void KAnimatedButtonPrivate::updateCurrentIcon()
115 {
116     if (pixmap.isNull()) {
117         return;
118     }
119 
120     QPixmap *frame = framesCache[current_frame];
121     if (!frame) {
122         const int icon_size = qMin(pixmap.width(), pixmap.height());
123         const int row_size = pixmap.width() / icon_size;
124         const int row = current_frame / row_size;
125         const int column = current_frame % row_size;
126         frame = new QPixmap(icon_size, icon_size);
127         frame->fill(Qt::transparent);
128         QPainter p(frame);
129         p.drawPixmap(QPoint(0, 0), pixmap, QRect(column * icon_size, row * icon_size, icon_size, icon_size));
130         p.end();
131         framesCache[current_frame] = frame;
132     }
133 
134     q->setIcon(QIcon(*frame));
135 }
136 
movieFrameChanged(int number)137 void KAnimatedButtonPrivate::movieFrameChanged(int number)
138 {
139     Q_UNUSED(number);
140     q->setIcon(QIcon(movie->currentPixmap()));
141 }
142 
movieFinished()143 void KAnimatedButtonPrivate::movieFinished()
144 {
145     // if not running, make it loop
146     if (movie->state() == QMovie::NotRunning) {
147         movie->start();
148     }
149 }
150 
updateIcons()151 void KAnimatedButtonPrivate::updateIcons()
152 {
153     pixmap = QPixmap();
154     QMovie *newMovie = nullptr;
155     QImageReader reader(icon_path);
156     if (QMovie::supportedFormats().contains(reader.format())) {
157         newMovie = new QMovie(icon_path);
158         frames = 0;
159         newMovie->setCacheMode(QMovie::CacheAll);
160         QObject::connect(newMovie, &QMovie::frameChanged, q, [this](int value) {
161             movieFrameChanged(value);
162         });
163         QObject::connect(newMovie, &QMovie::finished, q, [this] {
164             movieFinished();
165         });
166     } else {
167         const QPixmap pix(icon_path);
168         if (pix.isNull()) {
169             return;
170         }
171 
172         const int icon_size = qMin(pix.width(), pix.height());
173         if ((pix.height() % icon_size != 0) || (pix.width() % icon_size != 0)) {
174             return;
175         }
176 
177         frames = (pix.height() / icon_size) * (pix.width() / icon_size);
178         pixmap = pix;
179     }
180 
181     current_frame = 0;
182     qDeleteAll(framesCache);
183     framesCache.fill(nullptr);
184     framesCache.resize(frames);
185     delete movie;
186     movie = newMovie;
187 
188     if (movie) {
189         movie->jumpToFrame(0);
190         movieFrameChanged(0);
191     } else {
192         updateCurrentIcon();
193     }
194 }
195 
196 #include "moc_kanimatedbutton.cpp"
197