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"