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