File indexing completed on 2024-04-28 07:39:31

0001 /*
0002     SPDX-FileCopyrightText: 2010 Daniel Laidig <laidig@kde.org>
0003     SPDX-License-Identifier: GPL-2.0-or-later
0004 */
0005 
0006 #include "themedbackgroundrenderer.h"
0007 
0008 #include "settings/kgametheme/kgametheme.h"
0009 #include <QDebug>
0010 #include <QStandardPaths>
0011 
0012 #include <QApplication>
0013 #include <QMargins>
0014 #include <QPainter>
0015 #include <QPalette>
0016 #include <QRegularExpression>
0017 #include <QtConcurrentRun>
0018 
0019 using namespace Practice;
0020 
0021 ThemedBackgroundRenderer::ThemedBackgroundRenderer(QObject *parent, const QString &cacheFilename)
0022     : QObject(parent)
0023     , m_haveCache(true)
0024     , m_queuedRequest(false)
0025     , m_isFastScaledRender(true)
0026 {
0027     m_theme = new KGameTheme();
0028     m_cache.setSaveFilename(QStandardPaths::writableLocation(QStandardPaths::CacheLocation) + '/' + cacheFilename);
0029     m_timer.setSingleShot(true);
0030     m_timer.setInterval(1000);
0031     connect(&m_timer, &QTimer::timeout, this, &ThemedBackgroundRenderer::updateBackgroundTimeout);
0032     connect(&m_watcher, &QFutureWatcherBase::finished, this, &ThemedBackgroundRenderer::renderingFinished);
0033 }
0034 
0035 ThemedBackgroundRenderer::~ThemedBackgroundRenderer()
0036 {
0037     if (m_future.isRunning()) {
0038         qDebug() << "Waiting for rendering to finish";
0039         m_future.waitForFinished();
0040     }
0041     m_cache.saveCache();
0042     delete m_theme;
0043 }
0044 
0045 void ThemedBackgroundRenderer::setTheme(const QString &theme)
0046 {
0047     if (!m_theme->load(theme)) {
0048         qDebug() << "could not load theme" << theme;
0049     }
0050     m_renderer.load(m_theme->graphics());
0051     m_cache.setFilenames(QStringList(m_theme->graphics()) << m_theme->path());
0052     m_haveCache = !m_cache.isEmpty();
0053     m_lastScaledRenderRects.clear();
0054     m_lastFullRenderRects.clear();
0055     m_rectMappings.clear();
0056 }
0057 
0058 void ThemedBackgroundRenderer::clearRects()
0059 {
0060     m_rects.clear();
0061     m_rectMappings.clear();
0062 }
0063 
0064 void ThemedBackgroundRenderer::addRect(const QString &name, const QRect &rect)
0065 {
0066     m_rects.append(QPair<QString, QRect>(name, rect));
0067     if (!m_rectMappings.contains(name)) {
0068         QString mapped = m_theme->property("X-Parley-" + name);
0069         m_rectMappings[name] = mapped.isEmpty() ? name : mapped;
0070     }
0071 }
0072 
0073 QPixmap ThemedBackgroundRenderer::getScaledBackground()
0074 {
0075     if (m_rects.isEmpty() || m_rects[0].second.isEmpty()) {
0076         return QPixmap();
0077     }
0078     if (m_future.isRunning() || m_future.resultCount()) {
0079         return QPixmap();
0080     }
0081     if (m_cache.isEmpty()) {
0082         m_timer.start(0);
0083         return QPixmap();
0084     }
0085     if (m_lastScaledRenderRects == m_rects) {
0086         // we already renderered an image with that exact sizing, no need to waste resources on it again
0087         return QPixmap();
0088     }
0089 
0090     QFutureWatcher<QImage> watcher;
0091     m_future = QtConcurrent::run([this]() {
0092         return renderBackground(true);
0093     });
0094     watcher.setFuture(m_future);
0095     watcher.waitForFinished();
0096 
0097     QPixmap result = QPixmap::fromImage(m_future.result());
0098     m_future = QFuture<QImage>();
0099     m_lastScaledRenderRects = m_rects;
0100     return result;
0101 }
0102 
0103 QColor ThemedBackgroundRenderer::fontColor(const QString &context, const QColor &fallback)
0104 {
0105     QString text = m_theme->property("X-Parley-Font-Color-" + context).toLower();
0106     if (text.length() == 6 && text.contains(QRegularExpression(QStringLiteral("[0-9a-f]{6}")))) {
0107         return QColor(text.mid(0, 2).toInt(nullptr, 16), text.mid(2, 2).toInt(nullptr, 16), text.mid(4, 2).toInt(nullptr, 16));
0108     }
0109 
0110     return fallback;
0111 }
0112 
0113 void ThemedBackgroundRenderer::updateBackground()
0114 {
0115     if (m_rects.isEmpty() || m_rects[0].second.isEmpty()) {
0116         return;
0117     }
0118     m_timer.start();
0119 }
0120 
0121 void ThemedBackgroundRenderer::updateBackgroundTimeout()
0122 {
0123     bool fastScale = false;
0124     if (m_future.isRunning()) {
0125         m_timer.start(); // restart the timer again
0126         return;
0127     }
0128     if (m_lastFullRenderRects == m_rects && m_lastScaledRenderRects == m_rects) {
0129         // we already renderered an image with that exact sizing, no need to waste resources on it again
0130         return;
0131     }
0132     m_future = QtConcurrent::run([this, fastScale]() {
0133         return renderBackground(fastScale);
0134     });
0135     m_watcher.setFuture(m_future);
0136     m_lastFullRenderRects = m_rects;
0137 }
0138 
0139 void ThemedBackgroundRenderer::renderingFinished()
0140 {
0141     if (!m_future.resultCount()) {
0142         // qDebug() << "there is no image!";
0143         return;
0144     }
0145     Q_EMIT backgroundChanged(QPixmap::fromImage(m_future.result()));
0146     m_future = QFuture<QImage>();
0147 }
0148 
0149 QSizeF ThemedBackgroundRenderer::getSizeForId(const QString &id)
0150 {
0151     if (!m_renderer.elementExists(id))
0152         return QSizeF();
0153     return m_renderer.boundsOnElement(id).size();
0154 }
0155 
0156 QRectF ThemedBackgroundRenderer::getRectForId(const QString &id)
0157 {
0158     if (!m_renderer.elementExists(id))
0159         return QRectF();
0160     return m_renderer.boundsOnElement(id);
0161 }
0162 
0163 QPixmap ThemedBackgroundRenderer::getPixmapForId(const QString &id, QSize size)
0164 {
0165     if (!m_renderer.elementExists(id))
0166         return QPixmap();
0167     QRectF itemRect = m_renderer.boundsOnElement(id);
0168     if (itemRect.isNull())
0169         return QPixmap();
0170     if (size.isEmpty())
0171         size = itemRect.size().toSize();
0172 
0173     if (m_cache.imageSize(id) != size) {
0174         QImage image(size, QImage::Format_ARGB32_Premultiplied);
0175         image.fill(QColor(Qt::transparent).rgba());
0176         QPainter p(&image);
0177         m_renderer.render(&p, id, QRectF(QPointF(0, 0), size));
0178         m_cache.updateImage(id, image);
0179         return QPixmap::fromImage(image);
0180     } else {
0181         return QPixmap::fromImage(m_cache.getImage(id));
0182     }
0183 }
0184 
0185 QMargins ThemedBackgroundRenderer::contentMargins()
0186 {
0187     QString rect;
0188     if (!m_rects.empty()) {
0189         rect = m_rects.at(0).first;
0190     }
0191     if (m_rectMappings.contains(rect)) {
0192         rect = m_rectMappings.value(rect);
0193     }
0194     QMargins margins;
0195     if (m_renderer.elementExists(rect + "-border-topleft"))
0196         margins.setTop(m_renderer.boundsOnElement(rect + "-border-topleft").toAlignedRect().height());
0197     if (m_renderer.elementExists(rect + "-border-bottomleft"))
0198         margins.setBottom(m_renderer.boundsOnElement(rect + "-border-bottomleft").toAlignedRect().height());
0199     if (m_renderer.elementExists(rect + "-border-topleft"))
0200         margins.setLeft(m_renderer.boundsOnElement(rect + "-border-topleft").toAlignedRect().width());
0201     if (m_renderer.elementExists(rect + "-border-topright"))
0202         margins.setRight(m_renderer.boundsOnElement(rect + "-border-topright").toAlignedRect().width());
0203     return margins;
0204 }
0205 
0206 QImage ThemedBackgroundRenderer::renderBackground(bool fastScale)
0207 {
0208     m_isFastScaledRender = false;
0209 
0210     QImage image(m_rects[0].second.size(), QImage::Format_ARGB32_Premultiplied);
0211     image.fill(QColor(Qt::transparent).rgba());
0212     QPainter p(&image);
0213 
0214     for (QPair<QString, QRect> rect : qAsConst(m_rects)) {
0215         if (!m_rects.isEmpty() && rect == m_rects[0]) {
0216             QMargins margins = contentMargins();
0217             rect.second =
0218                 QRect(QPoint(margins.left(), margins.top()), rect.second.size() - QSize(margins.right() + margins.left(), margins.bottom() + margins.top()));
0219         }
0220         renderRect(rect.first, rect.second, &p, fastScale);
0221     }
0222 
0223     // qDebug() << "image rendered, time:" << t.elapsed();
0224     return image;
0225 }
0226 
0227 void ThemedBackgroundRenderer::renderRect(const QString &name, const QRect &rect, QPainter *p, bool fastScale)
0228 {
0229     renderItem(name, QStringLiteral("center"), rect, p, fastScale, Rect, Qt::IgnoreAspectRatio, Center, Centered, true);
0230     renderItem(name, QStringLiteral("center-ratio"), rect, p, fastScale, Rect, Qt::IgnoreAspectRatio, Center, Centered, true);
0231     renderItem(name, QStringLiteral("center-noscale"), rect, p, fastScale, NoScale, Qt::IgnoreAspectRatio, Center, Centered, true);
0232 
0233     renderItem(name, QStringLiteral("border-topleft"), rect, p, fastScale, NoScale, Qt::IgnoreAspectRatio, Top, Corner, false);
0234     renderItem(name, QStringLiteral("border-topright"), rect, p, fastScale, NoScale, Qt::IgnoreAspectRatio, Right, Corner, false);
0235     renderItem(name, QStringLiteral("border-bottomleft"), rect, p, fastScale, NoScale, Qt::IgnoreAspectRatio, Left, Corner, false);
0236     renderItem(name, QStringLiteral("border-bottomright"), rect, p, fastScale, NoScale, Qt::IgnoreAspectRatio, Bottom, Corner, false);
0237 
0238     QStringList edges;
0239     edges << QStringLiteral("top") << QStringLiteral("bottom") << QStringLiteral("left") << QStringLiteral("right");
0240     for (const QString &edge : qAsConst(edges)) {
0241         ScaleBase scaleBase;
0242         Edge alignEdge;
0243         if (edge == QLatin1String("top")) {
0244             alignEdge = Top;
0245             scaleBase = Horizontal;
0246         } else if (edge == QLatin1String("bottom")) {
0247             alignEdge = Bottom;
0248             scaleBase = Horizontal;
0249         } else if (edge == QLatin1String("right")) {
0250             alignEdge = Right;
0251             scaleBase = Vertical;
0252         } else {
0253             alignEdge = Left;
0254             scaleBase = Vertical;
0255         }
0256         for (int inside = 1; inside >= 0; inside--) {
0257             renderItem(name,
0258                        QString(inside ? "inside" : "border") + '-' + edge,
0259                        rect,
0260                        p,
0261                        fastScale,
0262                        scaleBase,
0263                        Qt::IgnoreAspectRatio,
0264                        alignEdge,
0265                        Centered,
0266                        inside);
0267             renderItem(name,
0268                        QString(inside ? "inside" : "border") + '-' + edge + "-ratio",
0269                        rect,
0270                        p,
0271                        fastScale,
0272                        scaleBase,
0273                        Qt::KeepAspectRatio,
0274                        alignEdge,
0275                        Centered,
0276                        inside);
0277             renderItem(name,
0278                        QString(inside ? "inside" : "border") + '-' + edge + "-noscale",
0279                        rect,
0280                        p,
0281                        fastScale,
0282                        NoScale,
0283                        Qt::IgnoreAspectRatio,
0284                        alignEdge,
0285                        Centered,
0286                        inside);
0287             renderItem(name,
0288                        QString(inside ? "inside" : "border") + '-' + edge + "-repeat",
0289                        rect,
0290                        p,
0291                        fastScale,
0292                        scaleBase,
0293                        Qt::IgnoreAspectRatio,
0294                        alignEdge,
0295                        Repeated,
0296                        inside);
0297             renderItem(name,
0298                        QString(inside ? "inside" : "border") + '-' + edge + '-' + (scaleBase == Vertical ? "top" : "left"),
0299                        rect,
0300                        p,
0301                        fastScale,
0302                        NoScale,
0303                        Qt::IgnoreAspectRatio,
0304                        alignEdge,
0305                        LeftTop,
0306                        inside);
0307             renderItem(name,
0308                        QString(inside ? "inside" : "border") + '-' + edge + '-' + (scaleBase == Vertical ? "bottom" : "right"),
0309                        rect,
0310                        p,
0311                        fastScale,
0312                        NoScale,
0313                        Qt::IgnoreAspectRatio,
0314                        alignEdge,
0315                        RightBottom,
0316                        inside);
0317         }
0318     }
0319 }
0320 
0321 void ThemedBackgroundRenderer::renderItem(const QString &idBase,
0322                                           const QString &idSuffix,
0323                                           const QRect &rect,
0324                                           QPainter *p,
0325                                           bool fastScale,
0326                                           ScaleBase scaleBase,
0327                                           Qt::AspectRatioMode aspectRatio,
0328                                           Edge edge,
0329                                           Align align,
0330                                           bool inside)
0331 {
0332     // the id without the mapping, which we need to use for caching
0333     // (otherwise, images could share a place in the cache which makes it useless if they have different sizes)
0334     QString id = idBase + '-' + idSuffix;
0335     // the id according to the mapping specified in the desktop file
0336     QString mappedId = m_rectMappings.contains(idBase) ? m_rectMappings.value(idBase) + '-' + idSuffix : id;
0337 
0338     if (!m_renderer.elementExists(mappedId))
0339         return;
0340     QRectF itemRectF = m_renderer.boundsOnElement(mappedId);
0341     if (itemRectF.isNull() || rect.isNull())
0342         return;
0343 
0344     // qDebug() << "draw item" << id;
0345     //    qDebug() << "original item rect:" << itemRect << m_renderer.boundsOnElement(id);
0346     QRect itemRect = scaleRect(itemRectF, rect, scaleBase, aspectRatio);
0347     //    qDebug() << "scaled" << itemRect;
0348     itemRect = alignRect(itemRect, rect, edge, align, inside);
0349     //    qDebug() << "aligned" << itemRect;
0350 
0351     QImage image;
0352     if (m_cache.imageSize(id) == itemRect.size()) {
0353         // qDebug() << "found in cache:" << id;
0354         image = m_cache.getImage(id);
0355     } else if (fastScale && !m_cache.imageSize(id).isEmpty()) {
0356         // qDebug() << "FAST SCALE for:" << id;
0357         image = m_cache.getImage(id).scaled(itemRect.size(), Qt::IgnoreAspectRatio, Qt::FastTransformation);
0358         m_isFastScaledRender = true;
0359     } else {
0360         // qDebug() << "NOT IN CACHE, render svg:" << id;
0361         image = QImage(itemRect.size(), QImage::Format_ARGB32_Premultiplied);
0362         image.fill(QColor(Qt::transparent).rgba());
0363         QPainter painter(&image);
0364         if (align == Repeated) {
0365             QImage tile(itemRectF.toRect().size(), QImage::Format_ARGB32_Premultiplied);
0366             tile.fill(QColor(Qt::transparent).rgba());
0367             QPainter tilePainter(&tile);
0368             m_renderer.render(&tilePainter, mappedId, QRect(QPoint(0, 0), tile.size()));
0369             painter.fillRect(image.rect(), QBrush(tile));
0370         } else if (aspectRatio == Qt::KeepAspectRatioByExpanding) {
0371             m_renderer.render(&painter, mappedId, QRect(QPoint(0, 0), itemRect.size()));
0372             painter.end();
0373             QRect croppedRect = rect;
0374             croppedRect.moveCenter(itemRect.center());
0375             image = image.copy(croppedRect);
0376         } else {
0377             m_renderer.render(&painter, mappedId, QRect(QPoint(0, 0), itemRect.size()));
0378         }
0379         m_cache.updateImage(id, image);
0380         m_haveCache = true;
0381     }
0382     p->drawImage(itemRect.topLeft(), image);
0383 }
0384 
0385 QRect ThemedBackgroundRenderer::scaleRect(QRectF itemRect, const QRect &baseRect, ScaleBase scaleBase, Qt::AspectRatioMode aspectRatio)
0386 {
0387     qreal verticalFactor = 0;
0388     qreal horizontalFactor = 0;
0389     switch (scaleBase) {
0390     case NoScale:
0391         return itemRect.toRect();
0392     case Horizontal:
0393         switch (aspectRatio) {
0394         case Qt::IgnoreAspectRatio:
0395             itemRect.setWidth(baseRect.width());
0396             return itemRect.toRect();
0397         case Qt::KeepAspectRatio:
0398             horizontalFactor = baseRect.width() / itemRect.width();
0399             itemRect.setWidth(baseRect.width());
0400             itemRect.setHeight(itemRect.height() * horizontalFactor);
0401             return itemRect.toRect();
0402         case Qt::KeepAspectRatioByExpanding:
0403             qWarning() << "KeepAspectRatioByExpanding only works for the center";
0404             return itemRect.toRect();
0405         }
0406         break;
0407     case Vertical:
0408         switch (aspectRatio) {
0409         case Qt::IgnoreAspectRatio:
0410             itemRect.setHeight(baseRect.height());
0411             return itemRect.toRect();
0412         case Qt::KeepAspectRatio:
0413             verticalFactor = baseRect.height() / itemRect.height();
0414             itemRect.setHeight(baseRect.height());
0415             itemRect.setWidth(itemRect.width() * verticalFactor);
0416             return itemRect.toRect();
0417         case Qt::KeepAspectRatioByExpanding:
0418             qWarning() << "KeepAspectRatioByExpanding only works for the center";
0419             return itemRect.toRect();
0420         }
0421         break;
0422     case Rect:
0423         switch (aspectRatio) {
0424         case Qt::IgnoreAspectRatio:
0425             itemRect.setWidth(baseRect.width());
0426             itemRect.setHeight(baseRect.height());
0427             return itemRect.toRect();
0428         case Qt::KeepAspectRatio:
0429             horizontalFactor = baseRect.width() / itemRect.width();
0430             verticalFactor = baseRect.height() / itemRect.height();
0431             if (verticalFactor < horizontalFactor) {
0432                 itemRect.setHeight(baseRect.height());
0433                 itemRect.setWidth(itemRect.width() * verticalFactor);
0434             } else {
0435                 itemRect.setWidth(baseRect.width());
0436                 itemRect.setHeight(itemRect.height() * horizontalFactor);
0437             }
0438             return itemRect.toRect();
0439         case Qt::KeepAspectRatioByExpanding:
0440             horizontalFactor = baseRect.width() / itemRect.width();
0441             verticalFactor = baseRect.height() / itemRect.height();
0442             if (verticalFactor > horizontalFactor) {
0443                 itemRect.setHeight(baseRect.height());
0444                 itemRect.setWidth(itemRect.width() * verticalFactor);
0445             } else {
0446                 itemRect.setWidth(baseRect.width());
0447                 itemRect.setHeight(itemRect.height() * horizontalFactor);
0448             }
0449             return itemRect.toRect();
0450         }
0451         break;
0452     }
0453     // qDebug() << "unhandled scaling option";
0454     return itemRect.toRect();
0455 }
0456 
0457 QRect ThemedBackgroundRenderer::alignRect(QRect itemRect, const QRect &baseRect, Edge edge, Align align, bool inside)
0458 {
0459     if (edge == Center) {
0460         int x = baseRect.x() + (baseRect.width() - itemRect.width()) / 2;
0461         int y = baseRect.y() + (baseRect.height() - itemRect.height()) / 2;
0462         itemRect.moveTo(x, y);
0463         return itemRect;
0464     }
0465 
0466     if (edge == Top || edge == Bottom) {
0467         // set x coordinate
0468         int x = 0;
0469         switch (align) {
0470         case Corner:
0471             if (edge == Top) {
0472                 x = baseRect.x() - itemRect.width();
0473             } else {
0474                 x = baseRect.x() + baseRect.width();
0475             }
0476             break;
0477         case LeftTop:
0478             x = baseRect.x();
0479             break;
0480         case Centered:
0481         case Repeated:
0482             x = baseRect.x() + (baseRect.width() - itemRect.width()) / 2;
0483             break;
0484         case RightBottom:
0485             x = baseRect.x() + baseRect.width() - itemRect.width();
0486             break;
0487         }
0488         // set y coordinate
0489         int y = baseRect.y();
0490         if (edge == Bottom) {
0491             y += baseRect.height() - itemRect.height();
0492         }
0493         if ((!inside) && edge == Top) {
0494             y -= itemRect.height();
0495         } else if (!inside) {
0496             y += itemRect.height();
0497         }
0498         itemRect.moveTo(x, y);
0499         return itemRect;
0500     } else if (edge == Left || edge == Right) {
0501         // set y coordinate
0502         int y = 0;
0503         switch (align) {
0504         case Corner:
0505             if (edge == Right) {
0506                 y = baseRect.y() - itemRect.height();
0507             } else {
0508                 y = baseRect.y() + baseRect.height();
0509             }
0510             break;
0511         case LeftTop:
0512             y = baseRect.y();
0513             break;
0514         case Centered:
0515         case Repeated:
0516             y = baseRect.y() + (baseRect.height() - itemRect.height()) / 2;
0517             break;
0518         case RightBottom:
0519             y = baseRect.y() + baseRect.height() - itemRect.height();
0520             break;
0521         }
0522         // set x coordinate
0523         int x = baseRect.x();
0524         if (edge == Right) {
0525             x += baseRect.width() - itemRect.width();
0526         }
0527         if ((!inside) && edge == Left) {
0528             x -= itemRect.width();
0529         } else if (!inside) {
0530             x += itemRect.width();
0531         }
0532         itemRect.moveTo(x, y);
0533         return itemRect;
0534     }
0535     // qDebug() << "unhandled alignment option";
0536     return itemRect;
0537 }
0538 
0539 #include "moc_themedbackgroundrenderer.cpp"