1 /*
2     SPDX-FileCopyrightText: 2009 Daniel Laidig <d.laidig@gmx.de>
3     SPDX-License-Identifier: GPL-2.0-or-later
4 */
5 
6 #include "imagewidget.h"
7 #include <config-parley.h>
8 
9 #include <QPaintEngine>
10 #include <QPainter>
11 #include <QTimeLine>
12 #include <QTimer>
13 
14 #include <QDebug>
15 
16 #if defined(Q_WS_X11)
17 #include <QX11Info>
18 #include <X11/Xlib.h>
19 #include <X11/extensions/Xrender.h>
20 #undef KeyPress
21 #undef FocusOut
22 #endif
23 
24 using namespace Practice;
25 
26 // The functions centerPixmaps() and transition() are copied from kdelibs/plasma/paintutils.cpp, revision 1133527
27 // License: LGPLv2+
28 // SPDX-FileCopyrightText: 2005 Aaron Seigo <aseigo@kde.org>
29 // SPDX-FileCopyrightText: 2008 Andrew Lake <jamboarder@yahoo.com>
30 // Don't just modify the code here, if there are issues they should probably also be fixed in libplasma.
31 
centerPixmaps(QPixmap & from,QPixmap & to)32 void centerPixmaps(QPixmap &from, QPixmap &to)
33 {
34     if (from.size() == to.size() && from.hasAlphaChannel() && to.hasAlphaChannel()) {
35         return;
36     }
37     QRect fromRect(from.rect());
38     QRect toRect(to.rect());
39 
40     QRect actualRect = QRect(QPoint(0, 0), fromRect.size().expandedTo(toRect.size()));
41     fromRect.moveCenter(actualRect.center());
42     toRect.moveCenter(actualRect.center());
43 
44     if (from.size() != actualRect.size() || !from.hasAlphaChannel()) {
45         QPixmap result(actualRect.size());
46         result.fill(Qt::transparent);
47         QPainter p(&result);
48         p.setCompositionMode(QPainter::CompositionMode_Source);
49         p.drawPixmap(fromRect.topLeft(), from);
50         p.end();
51         from = result;
52     }
53 
54     if (to.size() != actualRect.size() || !to.hasAlphaChannel()) {
55         QPixmap result(actualRect.size());
56         result.fill(Qt::transparent);
57         QPainter p(&result);
58         p.setCompositionMode(QPainter::CompositionMode_Source);
59         p.drawPixmap(toRect.topLeft(), to);
60         p.end();
61         to = result;
62     }
63 }
64 
transition(const QPixmap & from,const QPixmap & to,qreal amount)65 QPixmap transition(const QPixmap &from, const QPixmap &to, qreal amount)
66 {
67     if (from.isNull() && to.isNull()) {
68         return from;
69     }
70 
71     QPixmap startPixmap(from);
72     QPixmap targetPixmap(to);
73 
74     if (from.size() != to.size() || !from.hasAlphaChannel() || !to.hasAlphaChannel()) {
75         centerPixmaps(startPixmap, targetPixmap);
76     }
77 
78     // paint to in the center of from
79     QRect toRect = to.rect();
80 
81     QColor color;
82     color.setAlphaF(amount);
83 
84     // If the native paint engine supports Porter/Duff compositing and CompositionMode_Plus
85     QPaintEngine *paintEngine = from.paintEngine();
86     if (paintEngine && paintEngine->hasFeature(QPaintEngine::PorterDuff) && paintEngine->hasFeature(QPaintEngine::BlendModes)) {
87         QPainter p;
88         p.begin(&targetPixmap);
89         p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
90         p.fillRect(targetPixmap.rect(), color);
91         p.end();
92 
93         p.begin(&startPixmap);
94         p.setCompositionMode(QPainter::CompositionMode_DestinationOut);
95         p.fillRect(startPixmap.rect(), color);
96         p.setCompositionMode(QPainter::CompositionMode_Plus);
97         p.drawPixmap(toRect.topLeft(), targetPixmap);
98         p.end();
99 
100         return startPixmap;
101     }
102 #if defined(Q_WS_X11)
103     // We have Xrender support
104     else if (paintEngine && paintEngine->hasFeature(QPaintEngine::PorterDuff)) {
105         // QX11PaintEngine doesn't implement CompositionMode_Plus in Qt 4.3,
106         // which we need to be able to do a transition from one pixmap to
107         // another.
108         //
109         // In order to avoid the overhead of converting the pixmaps to images
110         // and doing the operation entirely in software, this function has a
111         // specialized path for X11 that uses Xrender directly to do the
112         // transition. This operation can be fully accelerated in HW.
113         //
114         // This specialization can be removed when QX11PaintEngine supports
115         // CompositionMode_Plus.
116         QPixmap source(targetPixmap), destination(startPixmap);
117 
118         source.detach();
119         destination.detach();
120 
121         Display *dpy = QX11Info::display();
122 
123         XRenderPictFormat *format = XRenderFindStandardFormat(dpy, PictStandardA8);
124         XRenderPictureAttributes pa;
125         pa.repeat = 1; // RepeatNormal
126 
127         // Create a 1x1 8 bit repeating alpha picture
128         Pixmap pixmap = XCreatePixmap(dpy, destination.handle(), 1, 1, 8);
129         Picture alpha = XRenderCreatePicture(dpy, pixmap, format, CPRepeat, &pa);
130         XFreePixmap(dpy, pixmap);
131 
132         // Fill the alpha picture with the opacity value
133         XRenderColor xcolor;
134         xcolor.alpha = quint16(0xffff * amount);
135         XRenderFillRectangle(dpy, PictOpSrc, alpha, &xcolor, 0, 0, 1, 1);
136 
137         // Reduce the alpha of the destination with 1 - opacity
138         XRenderComposite(dpy, PictOpOutReverse, alpha, None, destination.x11PictureHandle(), 0, 0, 0, 0, 0, 0, destination.width(), destination.height());
139 
140         // Add source * opacity to the destination
141         XRenderComposite(dpy,
142                          PictOpAdd,
143                          source.x11PictureHandle(),
144                          alpha,
145                          destination.x11PictureHandle(),
146                          toRect.x(),
147                          toRect.y(),
148                          0,
149                          0,
150                          0,
151                          0,
152                          destination.width(),
153                          destination.height());
154 
155         XRenderFreePicture(dpy, alpha);
156         return destination;
157     }
158 #endif
159     else {
160         // Fall back to using QRasterPaintEngine to do the transition.
161         QImage under = startPixmap.toImage();
162         QImage over = targetPixmap.toImage();
163 
164         QPainter p;
165         p.begin(&over);
166         p.setCompositionMode(QPainter::CompositionMode_DestinationIn);
167         p.fillRect(over.rect(), color);
168         p.end();
169 
170         p.begin(&under);
171         p.setCompositionMode(QPainter::CompositionMode_DestinationOut);
172         p.fillRect(under.rect(), color);
173         p.setCompositionMode(QPainter::CompositionMode_Plus);
174         p.drawImage(toRect.topLeft(), over);
175         p.end();
176 
177         return QPixmap::fromImage(under);
178     }
179 }
180 
ImageWidget(QWidget * parent)181 ImageWidget::ImageWidget(QWidget *parent)
182     : QWidget(parent)
183 {
184     m_scaleTimer = new QTimer(this);
185     m_scaleTimer->setSingleShot(true);
186     m_scaleTimer->setInterval(500);
187 
188     m_animation = new QTimeLine(300, this);
189 
190     m_scaledPixmapOutOfDate = false;
191     connect(m_scaleTimer, SIGNAL(timeout()), this, SLOT(scalePixmap()));
192     connect(m_animation, SIGNAL(valueChanged(qreal)), this, SLOT(update()));
193     connect(m_animation, &QTimeLine::finished, this, &ImageWidget::animationFinished);
194 }
195 
setPixmap(const QPixmap & pixmap)196 void ImageWidget::setPixmap(const QPixmap &pixmap)
197 {
198     // qDebug() << "set new pixmap, size:" << pixmap.size();
199     if (m_animation->state() == QTimeLine::Running) {
200         m_scaledPixmap = transition(m_animationPixmap, m_scaledPixmap, m_animation->currentValue());
201         m_animation->stop();
202         animationFinished();
203     }
204 
205     m_animationPixmap = m_scaledPixmap;
206     m_originalPixmap = pixmap;
207     m_scaledPixmap = QPixmap();
208     m_scaledBackupPixmap = QPixmap();
209     m_scaledPixmapOutOfDate = true;
210     if (!m_scaling) {
211         m_scaledPixmap = pixmap;
212     }
213     scalePixmap(true);
214     if (m_fading) {
215         m_animation->start();
216     }
217     update();
218 }
219 
setScalingEnabled(bool scaling,bool onlyDownscaling)220 void ImageWidget::setScalingEnabled(bool scaling, bool onlyDownscaling)
221 {
222     m_scaling = scaling;
223     m_onlyDownscaling = onlyDownscaling;
224 }
225 
setKeepAspectRatio(Qt::AspectRatioMode mode)226 void ImageWidget::setKeepAspectRatio(Qt::AspectRatioMode mode)
227 {
228     m_keepAspectRatio = mode;
229 }
230 
setFadingEnabled(bool fading)231 void ImageWidget::setFadingEnabled(bool fading)
232 {
233     m_fading = fading;
234 }
235 
setAlignment(Qt::Alignment alignment)236 void ImageWidget::setAlignment(Qt::Alignment alignment)
237 {
238     m_alignment = alignment;
239 }
240 
paintEvent(QPaintEvent * e)241 void ImageWidget::paintEvent(QPaintEvent *e)
242 {
243     QWidget::paintEvent(e);
244     QPainter painter(this);
245     if (m_scaling && m_scaledPixmapOutOfDate) {
246         m_scaleTimer->start();
247         scalePixmap(false);
248     }
249     QPixmap pm = m_scaledPixmap;
250     if (m_animation->state() == QTimeLine::Running) {
251         pm = transition(m_animationPixmap, m_scaledPixmap, m_animation->currentValue());
252     }
253 
254     int x = (size().width() - pm.width()) / 2;
255     if (m_alignment.testFlag(Qt::AlignLeft)) {
256         x = 0;
257     } else if (m_alignment.testFlag(Qt::AlignRight)) {
258         x = size().width() - pm.width();
259     }
260     int y = (size().height() - pm.height()) / 2;
261     if (m_alignment.testFlag(Qt::AlignTop)) {
262         y = 0;
263     } else if (m_alignment.testFlag(Qt::AlignBottom)) {
264         y = size().height() - pm.height();
265     }
266     painter.drawPixmap(x, y, pm);
267 }
268 
resizeEvent(QResizeEvent * e)269 void ImageWidget::resizeEvent(QResizeEvent *e)
270 {
271     if (!m_scaledPixmapOutOfDate) {
272         m_scaledBackupPixmap = m_scaledPixmap;
273     }
274     // stop animations when resizing
275     if (m_animation->state() == QTimeLine::Running) {
276         m_animation->stop();
277         animationFinished();
278     }
279     m_scaledPixmapOutOfDate = true;
280     QWidget::resizeEvent(e);
281     emit sizeChanged();
282 }
283 
scalePixmap(bool smooth)284 void ImageWidget::scalePixmap(bool smooth)
285 {
286     bool scaleUp = m_originalPixmap.width() <= size().width() && m_originalPixmap.height() <= size().height();
287     if ((m_onlyDownscaling && scaleUp) || m_originalPixmap.size() == size()) {
288         // qDebug() << "no need to scale pixmap";
289         m_scaledPixmapOutOfDate = false;
290         m_scaledPixmap = m_originalPixmap;
291         m_scaledBackupPixmap = QPixmap();
292     } else if (smooth) {
293         // qDebug() << "smooth scaling to" << size();
294         if (m_originalPixmap.isNull() || size().isEmpty()) {
295             m_scaledPixmapOutOfDate = false;
296             m_scaledPixmap = QPixmap();
297             update();
298             return;
299         }
300         m_scaledPixmap = m_originalPixmap.scaled(size(), m_keepAspectRatio, Qt::SmoothTransformation);
301         m_scaledBackupPixmap = QPixmap();
302         m_scaledPixmapOutOfDate = false;
303         update();
304     } else {
305         // qDebug() << "fast scaling to" << size();
306         // Try to find out if it makes sense to use the scaled backup pixmap.
307         // If the scaled backup gets too small, we use the original image.
308         float ratio = 0;
309         if (!size().isEmpty()) {
310             ratio = qMin(float(m_scaledBackupPixmap.width()) / size().width(), float(m_scaledBackupPixmap.height()) / size().height());
311         }
312         if (ratio > 0.4 && !m_scaledBackupPixmap.isNull()) {
313             m_scaledPixmap = m_scaledBackupPixmap.scaled(size(), m_keepAspectRatio, Qt::FastTransformation);
314         } else {
315             if (m_originalPixmap.isNull() || size().isEmpty()) {
316                 m_scaledPixmap = QPixmap();
317                 return;
318             }
319             // use the original pixmap
320             m_scaledPixmap = m_originalPixmap.scaled(size(), m_keepAspectRatio, Qt::FastTransformation);
321             m_scaledBackupPixmap = m_scaledPixmap;
322         }
323         m_scaledPixmapOutOfDate = true;
324     }
325 }
326 
animationFinished()327 void ImageWidget::animationFinished()
328 {
329     m_animationPixmap = QPixmap();
330 }
331