1 /*
2     SPDX-FileCopyrightText: 2010 Daniel Laidig <laidig@kde.org>
3     SPDX-License-Identifier: GPL-2.0-or-later
4 */
5 
6 #include "themedbackgroundrenderer.h"
7 
8 #include "settings/kgametheme/kgametheme.h"
9 #include <QDebug>
10 #include <QStandardPaths>
11 
12 #include <QApplication>
13 #include <QMargins>
14 #include <QPainter>
15 #include <QPalette>
16 #include <QtConcurrentRun>
17 
18 using namespace Practice;
19 
ThemedBackgroundRenderer(QObject * parent,const QString & cacheFilename)20 ThemedBackgroundRenderer::ThemedBackgroundRenderer(QObject *parent, const QString &cacheFilename)
21     : QObject(parent)
22     , m_haveCache(true)
23     , m_queuedRequest(false)
24     , m_isFastScaledRender(true)
25 {
26     m_theme = new KGameTheme();
27     m_cache.setSaveFilename(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + '/' + cacheFilename);
28     m_timer.setSingleShot(true);
29     m_timer.setInterval(1000);
30     connect(&m_timer, &QTimer::timeout, this, &ThemedBackgroundRenderer::updateBackgroundTimeout);
31     connect(&m_watcher, &QFutureWatcherBase::finished, this, &ThemedBackgroundRenderer::renderingFinished);
32 }
33 
~ThemedBackgroundRenderer()34 ThemedBackgroundRenderer::~ThemedBackgroundRenderer()
35 {
36     if (m_future.isRunning()) {
37         qDebug() << "Waiting for rendering to finish";
38         m_future.waitForFinished();
39     }
40     m_cache.saveCache();
41     delete m_theme;
42 }
43 
setTheme(const QString & theme)44 void ThemedBackgroundRenderer::setTheme(const QString &theme)
45 {
46     if (!m_theme->load(theme)) {
47         qDebug() << "could not load theme" << theme;
48     }
49     m_renderer.load(m_theme->graphics());
50     m_cache.setFilenames(QStringList(m_theme->graphics()) << m_theme->path());
51     m_haveCache = !m_cache.isEmpty();
52     m_lastScaledRenderRects.clear();
53     m_lastFullRenderRects.clear();
54     m_rectMappings.clear();
55 }
56 
clearRects()57 void ThemedBackgroundRenderer::clearRects()
58 {
59     m_rects.clear();
60     m_rectMappings.clear();
61 }
62 
addRect(const QString & name,const QRect & rect)63 void ThemedBackgroundRenderer::addRect(const QString &name, const QRect &rect)
64 {
65     m_rects.append(qMakePair<QString, QRect>(name, rect));
66     if (!m_rectMappings.contains(name)) {
67         QString mapped = m_theme->property("X-Parley-" + name);
68         m_rectMappings[name] = mapped.isEmpty() ? name : mapped;
69     }
70 }
71 
getScaledBackground()72 QPixmap ThemedBackgroundRenderer::getScaledBackground()
73 {
74     if (m_rects.isEmpty() || m_rects[0].second.isEmpty()) {
75         return QPixmap();
76     }
77     if (m_future.isRunning() || m_future.resultCount()) {
78         return QPixmap();
79     }
80     if (m_cache.isEmpty()) {
81         m_timer.start(0);
82         return QPixmap();
83     }
84     if (m_lastScaledRenderRects == m_rects) {
85         // we already renderered an image with that exact sizing, no need to waste resources on it again
86         return QPixmap();
87     }
88 
89     QFutureWatcher<QImage> watcher;
90     m_future = QtConcurrent::run(this, &ThemedBackgroundRenderer::renderBackground, true);
91     watcher.setFuture(m_future);
92     watcher.waitForFinished();
93 
94     QPixmap result = QPixmap::fromImage(m_future.result());
95     m_future = QFuture<QImage>();
96     m_lastScaledRenderRects = m_rects;
97     return result;
98 }
99 
fontColor(const QString & context,const QColor & fallback)100 QColor ThemedBackgroundRenderer::fontColor(const QString &context, const QColor &fallback)
101 {
102     QString text = m_theme->property("X-Parley-Font-Color-" + context).toLower();
103     if (text.length() == 6 && text.contains(QRegExp(QStringLiteral("[0-9a-f]{6}")))) {
104         return QColor(text.midRef(0, 2).toInt(0, 16), text.midRef(2, 2).toInt(0, 16), text.midRef(4, 2).toInt(0, 16));
105     }
106 
107     return fallback;
108 }
109 
updateBackground()110 void ThemedBackgroundRenderer::updateBackground()
111 {
112     if (m_rects.isEmpty() || m_rects[0].second.isEmpty()) {
113         return;
114     }
115     m_timer.start();
116 }
117 
updateBackgroundTimeout()118 void ThemedBackgroundRenderer::updateBackgroundTimeout()
119 {
120     bool fastScale = false;
121     if (m_future.isRunning()) {
122         m_timer.start(); // restart the timer again
123         return;
124     }
125     if (m_lastFullRenderRects == m_rects && m_lastScaledRenderRects == m_rects) {
126         // we already renderered an image with that exact sizing, no need to waste resources on it again
127         return;
128     }
129     m_future = QtConcurrent::run(this, &ThemedBackgroundRenderer::renderBackground, fastScale);
130     m_watcher.setFuture(m_future);
131     m_lastFullRenderRects = m_rects;
132 }
133 
renderingFinished()134 void ThemedBackgroundRenderer::renderingFinished()
135 {
136     if (!m_future.resultCount()) {
137         // qDebug() << "there is no image!";
138         return;
139     }
140     emit backgroundChanged(QPixmap::fromImage(m_future.result()));
141     m_future = QFuture<QImage>();
142 }
143 
getSizeForId(const QString & id)144 QSizeF ThemedBackgroundRenderer::getSizeForId(const QString &id)
145 {
146     if (!m_renderer.elementExists(id))
147         return QSizeF();
148     return m_renderer.boundsOnElement(id).size();
149 }
150 
getRectForId(const QString & id)151 QRectF ThemedBackgroundRenderer::getRectForId(const QString &id)
152 {
153     if (!m_renderer.elementExists(id))
154         return QRectF();
155     return m_renderer.boundsOnElement(id);
156 }
157 
getPixmapForId(const QString & id,QSize size)158 QPixmap ThemedBackgroundRenderer::getPixmapForId(const QString &id, QSize size)
159 {
160     if (!m_renderer.elementExists(id))
161         return QPixmap();
162     QRectF itemRect = m_renderer.boundsOnElement(id);
163     if (itemRect.isNull())
164         return QPixmap();
165     if (size.isEmpty())
166         size = itemRect.size().toSize();
167 
168     if (m_cache.imageSize(id) != size) {
169         QImage image(size, QImage::Format_ARGB32_Premultiplied);
170         image.fill(QColor(Qt::transparent).rgba());
171         QPainter p(&image);
172         m_renderer.render(&p, id, QRectF(QPointF(0, 0), size));
173         m_cache.updateImage(id, image);
174         return QPixmap::fromImage(image);
175     } else {
176         return QPixmap::fromImage(m_cache.getImage(id));
177     }
178 }
179 
contentMargins()180 QMargins ThemedBackgroundRenderer::contentMargins()
181 {
182     QString rect;
183     if (!m_rects.empty()) {
184         rect = m_rects.at(0).first;
185     }
186     if (m_rectMappings.contains(rect)) {
187         rect = m_rectMappings.value(rect);
188     }
189     QMargins margins;
190     if (m_renderer.elementExists(rect + "-border-topleft"))
191         margins.setTop(m_renderer.boundsOnElement(rect + "-border-topleft").toAlignedRect().height());
192     if (m_renderer.elementExists(rect + "-border-bottomleft"))
193         margins.setBottom(m_renderer.boundsOnElement(rect + "-border-bottomleft").toAlignedRect().height());
194     if (m_renderer.elementExists(rect + "-border-topleft"))
195         margins.setLeft(m_renderer.boundsOnElement(rect + "-border-topleft").toAlignedRect().width());
196     if (m_renderer.elementExists(rect + "-border-topright"))
197         margins.setRight(m_renderer.boundsOnElement(rect + "-border-topright").toAlignedRect().width());
198     return margins;
199 }
200 
renderBackground(bool fastScale)201 QImage ThemedBackgroundRenderer::renderBackground(bool fastScale)
202 {
203     m_isFastScaledRender = false;
204 
205     QImage image(m_rects[0].second.size(), QImage::Format_ARGB32_Premultiplied);
206     image.fill(QColor(Qt::transparent).rgba());
207     QPainter p(&image);
208 
209     for (QPair<QString, QRect> rect : qAsConst(m_rects)) {
210         if (!m_rects.isEmpty() && rect == m_rects[0]) {
211             QMargins margins = contentMargins();
212             rect.second =
213                 QRect(QPoint(margins.left(), margins.top()), rect.second.size() - QSize(margins.right() + margins.left(), margins.bottom() + margins.top()));
214         }
215         renderRect(rect.first, rect.second, &p, fastScale);
216     }
217 
218     // qDebug() << "image rendered, time:" << t.elapsed();
219     return image;
220 }
221 
renderRect(const QString & name,const QRect & rect,QPainter * p,bool fastScale)222 void ThemedBackgroundRenderer::renderRect(const QString &name, const QRect &rect, QPainter *p, bool fastScale)
223 {
224     renderItem(name, QStringLiteral("center"), rect, p, fastScale, Rect, Qt::IgnoreAspectRatio, Center, Centered, true);
225     renderItem(name, QStringLiteral("center-ratio"), rect, p, fastScale, Rect, Qt::IgnoreAspectRatio, Center, Centered, true);
226     renderItem(name, QStringLiteral("center-noscale"), rect, p, fastScale, NoScale, Qt::IgnoreAspectRatio, Center, Centered, true);
227 
228     renderItem(name, QStringLiteral("border-topleft"), rect, p, fastScale, NoScale, Qt::IgnoreAspectRatio, Top, Corner, false);
229     renderItem(name, QStringLiteral("border-topright"), rect, p, fastScale, NoScale, Qt::IgnoreAspectRatio, Right, Corner, false);
230     renderItem(name, QStringLiteral("border-bottomleft"), rect, p, fastScale, NoScale, Qt::IgnoreAspectRatio, Left, Corner, false);
231     renderItem(name, QStringLiteral("border-bottomright"), rect, p, fastScale, NoScale, Qt::IgnoreAspectRatio, Bottom, Corner, false);
232 
233     QStringList edges;
234     edges << QStringLiteral("top") << QStringLiteral("bottom") << QStringLiteral("left") << QStringLiteral("right");
235     for (const QString &edge : qAsConst(edges)) {
236         ScaleBase scaleBase;
237         Edge alignEdge;
238         if (edge == QLatin1String("top")) {
239             alignEdge = Top;
240             scaleBase = Horizontal;
241         } else if (edge == QLatin1String("bottom")) {
242             alignEdge = Bottom;
243             scaleBase = Horizontal;
244         } else if (edge == QLatin1String("right")) {
245             alignEdge = Right;
246             scaleBase = Vertical;
247         } else {
248             alignEdge = Left;
249             scaleBase = Vertical;
250         }
251         for (int inside = 1; inside >= 0; inside--) {
252             renderItem(name,
253                        QString(inside ? "inside" : "border") + '-' + edge,
254                        rect,
255                        p,
256                        fastScale,
257                        scaleBase,
258                        Qt::IgnoreAspectRatio,
259                        alignEdge,
260                        Centered,
261                        inside);
262             renderItem(name,
263                        QString(inside ? "inside" : "border") + '-' + edge + "-ratio",
264                        rect,
265                        p,
266                        fastScale,
267                        scaleBase,
268                        Qt::KeepAspectRatio,
269                        alignEdge,
270                        Centered,
271                        inside);
272             renderItem(name,
273                        QString(inside ? "inside" : "border") + '-' + edge + "-noscale",
274                        rect,
275                        p,
276                        fastScale,
277                        NoScale,
278                        Qt::IgnoreAspectRatio,
279                        alignEdge,
280                        Centered,
281                        inside);
282             renderItem(name,
283                        QString(inside ? "inside" : "border") + '-' + edge + "-repeat",
284                        rect,
285                        p,
286                        fastScale,
287                        scaleBase,
288                        Qt::IgnoreAspectRatio,
289                        alignEdge,
290                        Repeated,
291                        inside);
292             renderItem(name,
293                        QString(inside ? "inside" : "border") + '-' + edge + '-' + (scaleBase == Vertical ? "top" : "left"),
294                        rect,
295                        p,
296                        fastScale,
297                        NoScale,
298                        Qt::IgnoreAspectRatio,
299                        alignEdge,
300                        LeftTop,
301                        inside);
302             renderItem(name,
303                        QString(inside ? "inside" : "border") + '-' + edge + '-' + (scaleBase == Vertical ? "bottom" : "right"),
304                        rect,
305                        p,
306                        fastScale,
307                        NoScale,
308                        Qt::IgnoreAspectRatio,
309                        alignEdge,
310                        RightBottom,
311                        inside);
312         }
313     }
314 }
315 
renderItem(const QString & idBase,const QString & idSuffix,const QRect & rect,QPainter * p,bool fastScale,ScaleBase scaleBase,Qt::AspectRatioMode aspectRatio,Edge edge,Align align,bool inside)316 void ThemedBackgroundRenderer::renderItem(const QString &idBase,
317                                           const QString &idSuffix,
318                                           const QRect &rect,
319                                           QPainter *p,
320                                           bool fastScale,
321                                           ScaleBase scaleBase,
322                                           Qt::AspectRatioMode aspectRatio,
323                                           Edge edge,
324                                           Align align,
325                                           bool inside)
326 {
327     // the id without the mapping, which we need to use for caching
328     // (otherwise, images could share a place in the cache which makes it useless if they have different sizes)
329     QString id = idBase + '-' + idSuffix;
330     // the id according to the mapping specified in the desktop file
331     QString mappedId = m_rectMappings.contains(idBase) ? m_rectMappings.value(idBase) + '-' + idSuffix : id;
332 
333     if (!m_renderer.elementExists(mappedId))
334         return;
335     QRectF itemRectF = m_renderer.boundsOnElement(mappedId);
336     if (itemRectF.isNull() || rect.isNull())
337         return;
338 
339     // qDebug() << "draw item" << id;
340     //    qDebug() << "original item rect:" << itemRect << m_renderer.boundsOnElement(id);
341     QRect itemRect = scaleRect(itemRectF, rect, scaleBase, aspectRatio);
342     //    qDebug() << "scaled" << itemRect;
343     itemRect = alignRect(itemRect, rect, edge, align, inside);
344     //    qDebug() << "aligned" << itemRect;
345 
346     QImage image;
347     if (m_cache.imageSize(id) == itemRect.size()) {
348         // qDebug() << "found in cache:" << id;
349         image = m_cache.getImage(id);
350     } else if (fastScale && !m_cache.imageSize(id).isEmpty()) {
351         // qDebug() << "FAST SCALE for:" << id;
352         image = m_cache.getImage(id).scaled(itemRect.size(), Qt::IgnoreAspectRatio, Qt::FastTransformation);
353         m_isFastScaledRender = true;
354     } else {
355         // qDebug() << "NOT IN CACHE, render svg:" << id;
356         image = QImage(itemRect.size(), QImage::Format_ARGB32_Premultiplied);
357         image.fill(QColor(Qt::transparent).rgba());
358         QPainter painter(&image);
359         if (align == Repeated) {
360             QImage tile(itemRectF.toRect().size(), QImage::Format_ARGB32_Premultiplied);
361             tile.fill(QColor(Qt::transparent).rgba());
362             QPainter tilePainter(&tile);
363             m_renderer.render(&tilePainter, mappedId, QRect(QPoint(0, 0), tile.size()));
364             painter.fillRect(image.rect(), QBrush(tile));
365         } else if (aspectRatio == Qt::KeepAspectRatioByExpanding) {
366             m_renderer.render(&painter, mappedId, QRect(QPoint(0, 0), itemRect.size()));
367             painter.end();
368             QRect croppedRect = rect;
369             croppedRect.moveCenter(itemRect.center());
370             image = image.copy(croppedRect);
371         } else {
372             m_renderer.render(&painter, mappedId, QRect(QPoint(0, 0), itemRect.size()));
373         }
374         m_cache.updateImage(id, image);
375         m_haveCache = true;
376     }
377     p->drawImage(itemRect.topLeft(), image);
378 }
379 
scaleRect(QRectF itemRect,const QRect & baseRect,ScaleBase scaleBase,Qt::AspectRatioMode aspectRatio)380 QRect ThemedBackgroundRenderer::scaleRect(QRectF itemRect, const QRect &baseRect, ScaleBase scaleBase, Qt::AspectRatioMode aspectRatio)
381 {
382     qreal verticalFactor = 0;
383     qreal horizontalFactor = 0;
384     switch (scaleBase) {
385     case NoScale:
386         return itemRect.toRect();
387     case Horizontal:
388         switch (aspectRatio) {
389         case Qt::IgnoreAspectRatio:
390             itemRect.setWidth(baseRect.width());
391             return itemRect.toRect();
392         case Qt::KeepAspectRatio:
393             horizontalFactor = baseRect.width() / itemRect.width();
394             itemRect.setWidth(baseRect.width());
395             itemRect.setHeight(itemRect.height() * horizontalFactor);
396             return itemRect.toRect();
397         case Qt::KeepAspectRatioByExpanding:
398             qWarning() << "KeepAspectRatioByExpanding only works for the center";
399             return itemRect.toRect();
400         }
401         break;
402     case Vertical:
403         switch (aspectRatio) {
404         case Qt::IgnoreAspectRatio:
405             itemRect.setHeight(baseRect.height());
406             return itemRect.toRect();
407         case Qt::KeepAspectRatio:
408             verticalFactor = baseRect.height() / itemRect.height();
409             itemRect.setHeight(baseRect.height());
410             itemRect.setWidth(itemRect.width() * verticalFactor);
411             return itemRect.toRect();
412         case Qt::KeepAspectRatioByExpanding:
413             qWarning() << "KeepAspectRatioByExpanding only works for the center";
414             return itemRect.toRect();
415         }
416         break;
417     case Rect:
418         switch (aspectRatio) {
419         case Qt::IgnoreAspectRatio:
420             itemRect.setWidth(baseRect.width());
421             itemRect.setHeight(baseRect.height());
422             return itemRect.toRect();
423         case Qt::KeepAspectRatio:
424             horizontalFactor = baseRect.width() / itemRect.width();
425             verticalFactor = baseRect.height() / itemRect.height();
426             if (verticalFactor < horizontalFactor) {
427                 itemRect.setHeight(baseRect.height());
428                 itemRect.setWidth(itemRect.width() * verticalFactor);
429             } else {
430                 itemRect.setWidth(baseRect.width());
431                 itemRect.setHeight(itemRect.height() * horizontalFactor);
432             }
433             return itemRect.toRect();
434         case Qt::KeepAspectRatioByExpanding:
435             horizontalFactor = baseRect.width() / itemRect.width();
436             verticalFactor = baseRect.height() / itemRect.height();
437             if (verticalFactor > horizontalFactor) {
438                 itemRect.setHeight(baseRect.height());
439                 itemRect.setWidth(itemRect.width() * verticalFactor);
440             } else {
441                 itemRect.setWidth(baseRect.width());
442                 itemRect.setHeight(itemRect.height() * horizontalFactor);
443             }
444             return itemRect.toRect();
445         }
446         break;
447     }
448     // qDebug() << "unhandled scaling option";
449     return itemRect.toRect();
450 }
451 
alignRect(QRect itemRect,const QRect & baseRect,Edge edge,Align align,bool inside)452 QRect ThemedBackgroundRenderer::alignRect(QRect itemRect, const QRect &baseRect, Edge edge, Align align, bool inside)
453 {
454     if (edge == Center) {
455         int x = baseRect.x() + (baseRect.width() - itemRect.width()) / 2;
456         int y = baseRect.y() + (baseRect.height() - itemRect.height()) / 2;
457         itemRect.moveTo(x, y);
458         return itemRect;
459     }
460 
461     if (edge == Top || edge == Bottom) {
462         // set x coordinate
463         int x = 0;
464         switch (align) {
465         case Corner:
466             if (edge == Top) {
467                 x = baseRect.x() - itemRect.width();
468             } else {
469                 x = baseRect.x() + baseRect.width();
470             }
471             break;
472         case LeftTop:
473             x = baseRect.x();
474             break;
475         case Centered:
476         case Repeated:
477             x = baseRect.x() + (baseRect.width() - itemRect.width()) / 2;
478             break;
479         case RightBottom:
480             x = baseRect.x() + baseRect.width() - itemRect.width();
481             break;
482         }
483         // set y coordinate
484         int y = baseRect.y();
485         if (edge == Bottom) {
486             y += baseRect.height() - itemRect.height();
487         }
488         if ((!inside) && edge == Top) {
489             y -= itemRect.height();
490         } else if (!inside) {
491             y += itemRect.height();
492         }
493         itemRect.moveTo(x, y);
494         return itemRect;
495     } else if (edge == Left || edge == Right) {
496         // set y coordinate
497         int y = 0;
498         switch (align) {
499         case Corner:
500             if (edge == Right) {
501                 y = baseRect.y() - itemRect.height();
502             } else {
503                 y = baseRect.y() + baseRect.height();
504             }
505             break;
506         case LeftTop:
507             y = baseRect.y();
508             break;
509         case Centered:
510         case Repeated:
511             y = baseRect.y() + (baseRect.height() - itemRect.height()) / 2;
512             break;
513         case RightBottom:
514             y = baseRect.y() + baseRect.height() - itemRect.height();
515             break;
516         }
517         // set x coordinate
518         int x = baseRect.x();
519         if (edge == Right) {
520             x += baseRect.width() - itemRect.width();
521         }
522         if ((!inside) && edge == Left) {
523             x -= itemRect.width();
524         } else if (!inside) {
525             x += itemRect.width();
526         }
527         itemRect.moveTo(x, y);
528         return itemRect;
529     }
530     // qDebug() << "unhandled alignment option";
531     return itemRect;
532 }
533