File indexing completed on 2024-06-09 04:03:33

0001 /*
0002  *  Copyright (C) 2009-2010 Parker Coates <coates@kde.org>
0003  *
0004  *  Original card caching:
0005  *  Copyright (C) 2008 Andreas Pakulat <apaku@gmx.de>
0006  *
0007  *  This program is free software; you can redistribute it and/or
0008  *  modify it under the terms of the GNU General Public License as
0009  *  published by the Free Software Foundation; either version 2 of
0010  *  the License, or (at your option) any later version.
0011  *
0012  *  This program is distributed in the hope that it will be useful,
0013  *  but WITHOUT ANY WARRANTY; without even the implied warranty of
0014  *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
0015  *  GNU General Public License for more details.
0016  *
0017  *  You should have received a copy of the GNU General Public License
0018  *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
0019  *
0020  */
0021 
0022 #include "kabstractcarddeck_p.h"
0023 
0024 // own
0025 #include "common.h"
0026 #include "kcardpile.h"
0027 #include "libkcardgame_debug.h"
0028 // KF
0029 #include <KImageCache>
0030 // Qt
0031 #include <QApplication>
0032 #include <QGraphicsScene>
0033 #include <QPainter>
0034 #include <QSvgRenderer>
0035 #include <QTimer>
0036 
0037 namespace
0038 {
0039 const QString cacheNameTemplate(QStringLiteral("libkcardgame-themes/%1"));
0040 const QString unscaledSizeKey(QStringLiteral("libkcardgame_unscaledsize"));
0041 const QString lastUsedSizeKey(QStringLiteral("libkcardgame_lastusedsize"));
0042 
0043 QString keyForPixmap(const QString &element, const QSize &s)
0044 {
0045     return element + QLatin1Char('@') + QString::number(s.width()) + QLatin1Char('x') + QString::number(s.height());
0046 }
0047 }
0048 
0049 RenderingThread::RenderingThread(KAbstractCardDeckPrivate *d, QSize size, const QStringList &elements)
0050     : d(d)
0051     , m_size(size)
0052     , m_elementsToRender(elements)
0053     , m_haltFlag(false)
0054 {
0055     connect(this, &RenderingThread::renderingDone, d, &KAbstractCardDeckPrivate::submitRendering, Qt::QueuedConnection);
0056 }
0057 
0058 void RenderingThread::halt()
0059 {
0060     m_haltFlag = true;
0061     wait();
0062 }
0063 
0064 void RenderingThread::run()
0065 {
0066     {
0067         // Load the renderer even if we don't have any elements to render.
0068         QMutexLocker l(&(d->rendererMutex));
0069         d->renderer();
0070     }
0071 
0072     const auto size = m_size * qApp->devicePixelRatio();
0073     for (const QString &element : std::as_const(m_elementsToRender)) {
0074         if (m_haltFlag)
0075             return;
0076 
0077         const QImage img = d->renderCard(element, size);
0078         Q_EMIT renderingDone(element, img);
0079     }
0080 }
0081 
0082 KAbstractCardDeckPrivate::KAbstractCardDeckPrivate(KAbstractCardDeck *q)
0083     : QObject(q)
0084     , q(q)
0085     , animationCheckTimer(new QTimer(this))
0086     , cache(nullptr)
0087     , svgRenderer(nullptr)
0088     , thread(nullptr)
0089 
0090 {
0091     animationCheckTimer->setSingleShot(true);
0092     animationCheckTimer->setInterval(0);
0093     connect(animationCheckTimer, &QTimer::timeout, this, &KAbstractCardDeckPrivate::checkIfAnimationIsDone);
0094 }
0095 
0096 KAbstractCardDeckPrivate::~KAbstractCardDeckPrivate()
0097 {
0098     deleteThread();
0099     delete cache;
0100     delete svgRenderer;
0101 }
0102 
0103 // Note that rendererMutex MUST be locked before calling this function.
0104 QSvgRenderer *KAbstractCardDeckPrivate::renderer()
0105 {
0106     if (!svgRenderer) {
0107         QString thread = (qApp->thread() == QThread::currentThread()) ? QStringLiteral("main") : QStringLiteral("rendering");
0108         // qCDebug(LIBKCARDGAME_LOG) << QString("Loading card deck SVG in %1 thread").arg( thread );
0109 
0110         svgRenderer = new QSvgRenderer(theme.graphicsFilePath());
0111     }
0112     return svgRenderer;
0113 }
0114 
0115 QImage KAbstractCardDeckPrivate::renderCard(const QString &element, const QSize &size)
0116 {
0117     // Note that we don't use Format_ARGB32_Premultiplied as it sacrifices some
0118     // colour accuracy at low opacities for performance. Normally this wouldn't
0119     // be an issue, but in card games we often will have, say, 52 pixmaps
0120     // stacked on top of one another, which causes these colour inaccuracies to
0121     // add up to the point that they're very visible.
0122     QImage img(size, QImage::Format_ARGB32);
0123     img.fill(Qt::transparent);
0124     QPainter p(&img);
0125     {
0126         QMutexLocker l(&rendererMutex);
0127         if (renderer()->elementExists(element)) {
0128             renderer()->render(&p, element);
0129         } else {
0130             qCWarning(LIBKCARDGAME_LOG) << "Could not find" << element << "in SVG.";
0131             p.fillRect(QRect(0, 0, img.width(), img.height()), Qt::white);
0132             p.setPen(Qt::red);
0133             p.drawLine(0, 0, img.width(), img.height());
0134             p.drawLine(img.width(), 0, 0, img.height());
0135             p.end();
0136         }
0137     }
0138     p.end();
0139 
0140     return img;
0141 }
0142 
0143 QSizeF KAbstractCardDeckPrivate::unscaledCardSize()
0144 {
0145     QSizeF size;
0146 
0147     if (!theme.isValid())
0148         return size;
0149 
0150     if (!cacheFind(cache, unscaledSizeKey, &size)) {
0151         {
0152             QMutexLocker l(&rendererMutex);
0153             size = renderer()->boundsOnElement(QStringLiteral("back")).size();
0154         }
0155         cacheInsert(cache, unscaledSizeKey, size);
0156     }
0157 
0158     return size;
0159 }
0160 
0161 QPixmap KAbstractCardDeckPrivate::requestPixmap(quint32 id, bool faceUp)
0162 {
0163     if (!theme.isValid() || !currentCardSize.isValid())
0164         return QPixmap();
0165 
0166     QString elementId = q->elementName(id, faceUp);
0167     QHash<QString, CardElementData> &index = faceUp ? frontIndex : backIndex;
0168 
0169     QHash<QString, CardElementData>::iterator it = index.find(elementId);
0170     if (it == index.end())
0171         return QPixmap();
0172 
0173     QPixmap &stored = it.value().cardPixmap;
0174     const auto dpr = qApp->devicePixelRatio();
0175     QSize requestedCardSize = currentCardSize * dpr;
0176     if (stored.size() != requestedCardSize) {
0177         QString key = keyForPixmap(elementId, requestedCardSize);
0178         if (!cache->findPixmap(key, &stored)) {
0179             if (stored.isNull()) {
0180                 // qCDebug(LIBKCARDGAME_LOG) << "Renderering" << key << "in main thread.";
0181                 QImage img = renderCard(elementId, requestedCardSize);
0182                 cache->insertImage(key, img);
0183                 stored = QPixmap::fromImage(img);
0184             } else {
0185                 stored = stored.scaled(requestedCardSize);
0186             }
0187         }
0188         Q_ASSERT(stored.size() == requestedCardSize);
0189         stored.setDevicePixelRatio(dpr);
0190     }
0191     return stored;
0192 }
0193 
0194 void KAbstractCardDeckPrivate::deleteThread()
0195 {
0196     if (thread && thread->isRunning())
0197         thread->halt();
0198     delete thread;
0199     thread = nullptr;
0200 }
0201 
0202 void KAbstractCardDeckPrivate::submitRendering(const QString &elementId, const QImage &image)
0203 {
0204     // If the currentCardSize has changed since the rendering was performed,
0205     // we sadly just have to throw it away.
0206     const auto dpr = qApp->devicePixelRatio();
0207     if (image.size() != currentCardSize * dpr)
0208         return;
0209 
0210     cache->insertImage(keyForPixmap(elementId, currentCardSize * dpr), image);
0211     QPixmap pix = QPixmap::fromImage(image);
0212 
0213     pix.setDevicePixelRatio(dpr);
0214 
0215     QHash<QString, CardElementData>::iterator it;
0216     it = frontIndex.find(elementId);
0217     if (it != frontIndex.end()) {
0218         it.value().cardPixmap = pix;
0219         const auto cards = it.value().cardUsers;
0220         for (KCard *c : cards)
0221             c->setFrontPixmap(pix);
0222     }
0223 
0224     it = backIndex.find(elementId);
0225     if (it != backIndex.end()) {
0226         it.value().cardPixmap = pix;
0227         const auto cards = it.value().cardUsers;
0228         for (KCard *c : cards)
0229             c->setBackPixmap(pix);
0230     }
0231 }
0232 
0233 void KAbstractCardDeckPrivate::cardStartedAnimation(KCard *card)
0234 {
0235     Q_ASSERT(!cardsWaitedFor.contains(card));
0236     cardsWaitedFor.insert(card);
0237 }
0238 
0239 void KAbstractCardDeckPrivate::cardStoppedAnimation(KCard *card)
0240 {
0241     Q_ASSERT(cardsWaitedFor.contains(card));
0242     cardsWaitedFor.remove(card);
0243 
0244     if (cardsWaitedFor.isEmpty())
0245         animationCheckTimer->start();
0246 }
0247 
0248 void KAbstractCardDeckPrivate::checkIfAnimationIsDone()
0249 {
0250     if (cardsWaitedFor.isEmpty())
0251         Q_EMIT q->cardAnimationDone();
0252 }
0253 
0254 KAbstractCardDeck::KAbstractCardDeck(const KCardTheme &theme, QObject *parent)
0255     : QObject(parent)
0256     , d(new KAbstractCardDeckPrivate(this))
0257 {
0258     setTheme(theme);
0259 }
0260 
0261 KAbstractCardDeck::~KAbstractCardDeck()
0262 {
0263     for (KCard *c : std::as_const(d->cards))
0264         delete c;
0265     d->cards.clear();
0266 }
0267 
0268 void KAbstractCardDeck::setDeckContents(const QList<quint32> &ids)
0269 {
0270     for (KCard *c : std::as_const(d->cards))
0271         delete c;
0272     d->cards.clear();
0273     d->cardsWaitedFor.clear();
0274 
0275     QHash<QString, CardElementData> oldFrontIndex = d->frontIndex;
0276     d->frontIndex.clear();
0277 
0278     QHash<QString, CardElementData> oldBackIndex = d->backIndex;
0279     d->backIndex.clear();
0280 
0281     for (quint32 id : ids) {
0282         KCard *c = new KCard(id, this);
0283 
0284         c->setObjectName(elementName(c->id()));
0285 
0286         connect(c, &KCard::animationStarted, d, &KAbstractCardDeckPrivate::cardStartedAnimation);
0287         connect(c, &KCard::animationStopped, d, &KAbstractCardDeckPrivate::cardStoppedAnimation);
0288 
0289         QString elementId = elementName(id, true);
0290         d->frontIndex[elementId].cardUsers.append(c);
0291 
0292         elementId = elementName(id, false);
0293         d->backIndex[elementId].cardUsers.append(c);
0294 
0295         d->cards << c;
0296     }
0297 
0298     QHash<QString, CardElementData>::iterator it;
0299     QHash<QString, CardElementData>::iterator end;
0300     QHash<QString, CardElementData>::const_iterator it2;
0301     QHash<QString, CardElementData>::const_iterator end2;
0302 
0303     end = d->frontIndex.end();
0304     end2 = oldFrontIndex.constEnd();
0305     for (it = d->frontIndex.begin(); it != end; ++it) {
0306         it2 = oldFrontIndex.constFind(it.key());
0307         if (it2 != end2)
0308             it.value().cardPixmap = it2.value().cardPixmap;
0309     }
0310 
0311     end = d->backIndex.end();
0312     end2 = oldBackIndex.constEnd();
0313     for (it = d->backIndex.begin(); it != end; ++it) {
0314         it2 = oldBackIndex.constFind(it.key());
0315         if (it2 != end2)
0316             it.value().cardPixmap = it2.value().cardPixmap;
0317     }
0318 }
0319 
0320 QList<KCard *> KAbstractCardDeck::cards() const
0321 {
0322     return d->cards;
0323 }
0324 
0325 int KAbstractCardDeck::rankFromId(quint32 id) const
0326 {
0327     Q_UNUSED(id);
0328     return -1;
0329 }
0330 
0331 int KAbstractCardDeck::suitFromId(quint32 id) const
0332 {
0333     Q_UNUSED(id);
0334     return -1;
0335 }
0336 
0337 int KAbstractCardDeck::colorFromId(quint32 id) const
0338 {
0339     Q_UNUSED(id);
0340     return -1;
0341 }
0342 
0343 void KAbstractCardDeck::setCardWidth(int width)
0344 {
0345     if (width < 20)
0346         return;
0347 
0348     int height = width * d->originalCardSize.height() / d->originalCardSize.width();
0349     QSize newSize(width, height);
0350 
0351     if (newSize != d->currentCardSize) {
0352         d->deleteThread();
0353 
0354         d->currentCardSize = newSize;
0355 
0356         if (!d->theme.isValid())
0357             return;
0358 
0359         cacheInsert(d->cache, lastUsedSizeKey, d->currentCardSize);
0360 
0361         QStringList elementsToRender = d->frontIndex.keys() + d->backIndex.keys();
0362         d->thread = new RenderingThread(d, d->currentCardSize, elementsToRender);
0363         d->thread->start();
0364     }
0365 }
0366 
0367 int KAbstractCardDeck::cardWidth() const
0368 {
0369     return d->currentCardSize.width();
0370 }
0371 
0372 void KAbstractCardDeck::setCardHeight(int height)
0373 {
0374     setCardWidth(height * d->originalCardSize.width() / d->originalCardSize.height());
0375 }
0376 
0377 int KAbstractCardDeck::cardHeight() const
0378 {
0379     return d->currentCardSize.height();
0380 }
0381 
0382 QSize KAbstractCardDeck::cardSize() const
0383 {
0384     return d->currentCardSize;
0385 }
0386 
0387 void KAbstractCardDeck::setTheme(const KCardTheme &theme)
0388 {
0389     if (theme != d->theme && theme.isValid()) {
0390         d->deleteThread();
0391 
0392         d->theme = theme;
0393 
0394         {
0395             QMutexLocker l(&(d->rendererMutex));
0396             delete d->svgRenderer;
0397             d->svgRenderer = nullptr;
0398         }
0399 
0400         delete d->cache;
0401 
0402         QString cacheName = QString(cacheNameTemplate).arg(theme.dirName());
0403         d->cache = new KImageCache(cacheName, 3 * 1024 * 1024);
0404         d->cache->setEvictionPolicy(KSharedDataCache::EvictLeastRecentlyUsed);
0405 
0406         // Enabling the pixmap cache has caused issues: we were getting back
0407         // different pixmaps than we had inserted. We keep a partial cache of the
0408         // pixmaps in KAbstractCardDeck already, so the builtin pixmap caching
0409         // doesn't really add that much benefit anyway.
0410         d->cache->setPixmapCaching(false);
0411 
0412         if (d->cache->timestamp() < theme.lastModified().toSecsSinceEpoch()) {
0413             d->cache->clear();
0414             d->cache->setTimestamp(theme.lastModified().toSecsSinceEpoch());
0415         }
0416 
0417         d->originalCardSize = d->unscaledCardSize();
0418         Q_ASSERT(!d->originalCardSize.isNull());
0419 
0420         if (!cacheFind(d->cache, lastUsedSizeKey, &(d->currentCardSize))) {
0421             qreal ratio = d->originalCardSize.height() / d->originalCardSize.width();
0422             d->currentCardSize = QSize(10, 10 * ratio);
0423         }
0424     }
0425 }
0426 
0427 KCardTheme KAbstractCardDeck::theme() const
0428 {
0429     return d->theme;
0430 }
0431 
0432 bool KAbstractCardDeck::hasAnimatedCards() const
0433 {
0434     return !d->cardsWaitedFor.isEmpty();
0435 }
0436 
0437 void KAbstractCardDeck::stopAnimations()
0438 {
0439     const auto currentCardsWaitedFor = d->cardsWaitedFor;
0440     for (KCard *c : currentCardsWaitedFor)
0441         c->stopAnimation();
0442     Q_ASSERT(d->cardsWaitedFor.isEmpty());
0443     d->cardsWaitedFor.clear();
0444 }
0445 
0446 QPixmap KAbstractCardDeck::cardPixmap(quint32 id, bool faceUp)
0447 {
0448     return d->requestPixmap(id, faceUp);
0449 }
0450 
0451 #include "moc_kabstractcarddeck.cpp"
0452 #include "moc_kabstractcarddeck_p.cpp"