File indexing completed on 2024-04-21 07:52:37

0001 /*
0002     SPDX-FileCopyrightText: 1997 Mathias Mueller <in5y158@public.uni-hamburg.de>
0003     SPDX-FileCopyrightText: 2006 Mauricio Piacentini <mauricio@tabuleiro.com>
0004 
0005     SPDX-License-Identifier: GPL-2.0-or-later
0006 */
0007 
0008 // own
0009 #include "kmahjonggtileset.h"
0010 
0011 // STL
0012 #include <cstdlib>
0013 
0014 // Qt
0015 #include <QFile>
0016 #include <QGuiApplication>
0017 #include <QPainter>
0018 #include <QPixmapCache>
0019 #include <QStandardPaths>
0020 #include <QSvgRenderer>
0021 
0022 // KF
0023 #include <KConfig>
0024 #include <KConfigGroup>
0025 #include <KLocalizedString>
0026 
0027 // LibKMahjongg
0028 #include "libkmahjongg_debug.h"
0029 
0030 class KMahjonggTilesetMetricsData
0031 {
0032 public:
0033     short lvloffx = 0; // used for 3D indentation, x value
0034     short lvloffy = 0; // used for 3D indentation, y value
0035     short w = 0; // tile width ( +border +shadow)
0036     short h = 0; // tile height ( +border +shadow)
0037     short fw = 0; // face width
0038     short fh = 0; // face height
0039 
0040     KMahjonggTilesetMetricsData() = default;
0041 };
0042 
0043 class KMahjonggTilesetPrivate
0044 {
0045 public:
0046     KMahjonggTilesetPrivate() = default;
0047 
0048     void updateScaleInfo(short tilew, short tileh);
0049     void buildElementIdTable();
0050     QString pixmapCacheNameFromElementId(const QString &elementid, short width, short height) const;
0051     QPixmap renderElement(short width, short height, const QString &elementid) const;
0052 
0053 public:
0054     QList<QString> elementIdTable;
0055 
0056     QString name;
0057     QString description;
0058     QString license;
0059     QString copyrightText;
0060     QString version;
0061     QString website;
0062     QString bugReportUrl;
0063     QString authorName;
0064     QString authorEmailAddress;
0065 
0066     KMahjonggTilesetMetricsData originaldata;
0067     KMahjonggTilesetMetricsData scaleddata;
0068     QString filename; // cache the last file loaded to save reloading it
0069     QString graphicspath;
0070 
0071     mutable QSvgRenderer svg; // render() is non-const
0072     bool isSVG = false;
0073     bool graphicsLoaded = false;
0074 };
0075 
0076 // ---------------------------------------------------------
0077 
0078 KMahjonggTileset::KMahjonggTileset()
0079     : d_ptr(new KMahjonggTilesetPrivate)
0080 {
0081     Q_D(KMahjonggTileset);
0082 
0083     d->buildElementIdTable();
0084 }
0085 
0086 // ---------------------------------------------------------
0087 
0088 KMahjonggTileset::~KMahjonggTileset() = default;
0089 
0090 void KMahjonggTilesetPrivate::updateScaleInfo(short tilew, short tileh)
0091 {
0092     scaleddata.w = tilew;
0093     scaleddata.h = tileh;
0094     const double ratio = (static_cast<qreal>(scaleddata.w)) / (static_cast<qreal>(originaldata.w));
0095     scaleddata.lvloffx = static_cast<short>(originaldata.lvloffx * ratio);
0096     scaleddata.lvloffy = static_cast<short>(originaldata.lvloffy * ratio);
0097     scaleddata.fw = static_cast<short>(originaldata.fw * ratio);
0098     scaleddata.fh = static_cast<short>(originaldata.fh * ratio);
0099 }
0100 
0101 QSize KMahjonggTileset::preferredTileSize(QSize boardsize, int horizontalCells, int verticalCells) const
0102 {
0103     Q_D(const KMahjonggTileset);
0104 
0105     // calculate our best tile size to fit the boardsize passed to us
0106     qreal newtilew, newtileh, aspectratio;
0107     qreal bw = boardsize.width();
0108     qreal bh = boardsize.height();
0109 
0110     // use tileface for calculation, with one complete tile in the sum for extra margin
0111     qreal fullh = (d->originaldata.fh * verticalCells) + d->originaldata.h;
0112     qreal fullw = (d->originaldata.fw * horizontalCells) + d->originaldata.w;
0113     qreal floatw = d->originaldata.w;
0114     qreal floath = d->originaldata.h;
0115 
0116     if ((fullw / fullh) > (bw / bh)) {
0117         // space will be left on height, use width as limit
0118         aspectratio = bw / fullw;
0119     } else {
0120         aspectratio = bh / fullh;
0121     }
0122     newtilew = aspectratio * floatw;
0123     newtileh = aspectratio * floath;
0124     return QSize(static_cast<short>(newtilew), static_cast<short>(newtileh));
0125 }
0126 
0127 bool KMahjonggTileset::loadDefault()
0128 {
0129     Q_D(KMahjonggTileset);
0130 
0131     const QString tilesetPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kmahjongglib/tilesets/default.desktop"));
0132     qCDebug(LIBKMAHJONGG_LOG) << "Inside LoadDefault(), located path at" << tilesetPath;
0133     if (tilesetPath.isEmpty()) {
0134         return false;
0135     }
0136     return loadTileset(tilesetPath);
0137 }
0138 
0139 QString KMahjonggTileset::name() const
0140 {
0141     Q_D(const KMahjonggTileset);
0142 
0143     return d->name;
0144 }
0145 
0146 QString KMahjonggTileset::description() const
0147 {
0148     Q_D(const KMahjonggTileset);
0149 
0150     return d->description;
0151 }
0152 
0153 QString KMahjonggTileset::license() const
0154 {
0155     Q_D(const KMahjonggTileset);
0156 
0157     return d->license;
0158 }
0159 
0160 QString KMahjonggTileset::copyrightText() const
0161 {
0162     Q_D(const KMahjonggTileset);
0163 
0164     return d->copyrightText;
0165 }
0166 
0167 QString KMahjonggTileset::version() const
0168 {
0169     Q_D(const KMahjonggTileset);
0170 
0171     return d->version;
0172 }
0173 
0174 QString KMahjonggTileset::website() const
0175 {
0176     Q_D(const KMahjonggTileset);
0177 
0178     return d->website;
0179 }
0180 
0181 QString KMahjonggTileset::bugReportUrl() const
0182 {
0183     Q_D(const KMahjonggTileset);
0184 
0185     return d->bugReportUrl;
0186 }
0187 
0188 QString KMahjonggTileset::authorName() const
0189 {
0190     Q_D(const KMahjonggTileset);
0191 
0192     return d->authorName;
0193 }
0194 
0195 QString KMahjonggTileset::authorEmailAddress() const
0196 {
0197     Q_D(const KMahjonggTileset);
0198 
0199     return d->authorEmailAddress;
0200 }
0201 
0202 short KMahjonggTileset::width() const
0203 {
0204     Q_D(const KMahjonggTileset);
0205 
0206     return d->scaleddata.w;
0207 }
0208 
0209 short KMahjonggTileset::height() const
0210 {
0211     Q_D(const KMahjonggTileset);
0212 
0213     return d->scaleddata.h;
0214 }
0215 
0216 short KMahjonggTileset::levelOffsetX() const
0217 {
0218     Q_D(const KMahjonggTileset);
0219 
0220     return d->scaleddata.lvloffx;
0221 }
0222 
0223 short KMahjonggTileset::levelOffsetY() const
0224 {
0225     Q_D(const KMahjonggTileset);
0226 
0227     return d->scaleddata.lvloffy;
0228 }
0229 
0230 short KMahjonggTileset::qWidth() const
0231 {
0232     Q_D(const KMahjonggTileset);
0233 
0234     return static_cast<short>(d->scaleddata.fw / 2.0);
0235 }
0236 
0237 short KMahjonggTileset::qHeight() const
0238 {
0239     Q_D(const KMahjonggTileset);
0240 
0241     return static_cast<short>(d->scaleddata.fh / 2.0);
0242 }
0243 
0244 QString KMahjonggTileset::path() const
0245 {
0246     Q_D(const KMahjonggTileset);
0247 
0248     return d->filename;
0249 }
0250 
0251 #define kTilesetVersionFormat 1
0252 
0253 // ---------------------------------------------------------
0254 bool KMahjonggTileset::loadTileset(const QString &tilesetPath)
0255 {
0256     Q_D(KMahjonggTileset);
0257 
0258     // qCDebug(LIBKMAHJONGG_LOG) << "Attempting to load .desktop at" << tilesetPath;
0259 
0260     // verify if it is a valid file first and if we can open it
0261     QFile tilesetfile(tilesetPath);
0262     if (!tilesetfile.open(QIODevice::ReadOnly)) {
0263         d->name.clear();
0264         d->description.clear();
0265         d->license.clear();
0266         d->copyrightText.clear();
0267         d->version.clear();
0268         d->website.clear();
0269         d->bugReportUrl.clear();
0270         d->authorName.clear();
0271         d->authorEmailAddress.clear();
0272         return false;
0273     }
0274     tilesetfile.close();
0275 
0276     KConfig tileconfig(tilesetPath, KConfig::SimpleConfig);
0277     KConfigGroup group = tileconfig.group(QStringLiteral("KMahjonggTileset"));
0278 
0279     d->name = group.readEntry("Name"); // Returns translated data
0280     d->description = group.readEntry("Description");
0281     d->license = group.readEntry("License");
0282     d->copyrightText = group.readEntry("Copyright");
0283     d->version = group.readEntry("Version");
0284     d->website = group.readEntry("Website");
0285     d->bugReportUrl = group.readEntry("BugReportUrl");
0286     d->authorName = group.readEntry("Author");
0287     d->authorEmailAddress = group.readEntry("AuthorEmail");
0288 
0289     // Version control
0290     int tileversion = group.readEntry("VersionFormat", 0);
0291     // Format is increased when we have incompatible changes, meaning that older clients are not able to use the remaining information safely
0292     if (tileversion > kTilesetVersionFormat) {
0293         return false;
0294     }
0295 
0296     QString graphName = group.readEntry("FileName");
0297 
0298     d->graphicspath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kmahjongglib/tilesets/") + graphName);
0299     // qCDebug(LIBKMAHJONGG_LOG) << "Using tileset at" << d->graphicspath;
0300 
0301     // only SVG for now
0302     d->isSVG = true;
0303     if (d->graphicspath.isEmpty()) {
0304         return false;
0305     }
0306 
0307     d->originaldata.w = group.readEntry("TileWidth", 30);
0308     d->originaldata.h = group.readEntry("TileHeight", 50);
0309     d->originaldata.fw = group.readEntry("TileFaceWidth", 30);
0310     d->originaldata.fh = group.readEntry("TileFaceHeight", 50);
0311     d->originaldata.lvloffx = group.readEntry("LevelOffsetX", 10);
0312     d->originaldata.lvloffy = group.readEntry("LevelOffsetY", 10);
0313 
0314     // client application needs to call loadGraphics()
0315     d->graphicsLoaded = false;
0316     d->filename = tilesetPath;
0317 
0318     return true;
0319 }
0320 
0321 // ---------------------------------------------------------
0322 bool KMahjonggTileset::loadGraphics()
0323 {
0324     Q_D(KMahjonggTileset);
0325 
0326     if (d->graphicsLoaded) {
0327         return true;
0328     }
0329     if (d->isSVG) {
0330         // really?
0331         d->svg.load(d->graphicspath);
0332         if (d->svg.isValid()) {
0333             // invalidate our global cache
0334             QPixmapCache::clear();
0335             d->graphicsLoaded = true;
0336             reloadTileset(QSize(d->originaldata.w, d->originaldata.h));
0337         } else {
0338             return false;
0339         }
0340     } else {
0341         // TODO add support for png??
0342         return false;
0343     }
0344 
0345     return true;
0346 }
0347 
0348 // ---------------------------------------------------------
0349 bool KMahjonggTileset::reloadTileset(QSize newTilesize)
0350 {
0351     Q_D(KMahjonggTileset);
0352 
0353     if (QSize(d->scaleddata.w, d->scaleddata.h) == newTilesize) {
0354         return false;
0355     }
0356 
0357     if (d->isSVG) {
0358         if (d->svg.isValid()) {
0359             d->updateScaleInfo(newTilesize.width(), newTilesize.height());
0360             // rendering will be done when needed, automatically using the global cache
0361         } else {
0362             return false;
0363         }
0364     } else {
0365         // TODO add support for png???
0366         return false;
0367     }
0368 
0369     return true;
0370 }
0371 
0372 void KMahjonggTilesetPrivate::buildElementIdTable()
0373 {
0374     constexpr int tileCount = 4;
0375     constexpr int characterCount = 9;
0376     constexpr int bambooCount = 9;
0377     constexpr int rodCount = 9;
0378     constexpr int seasonCount = 4;
0379     constexpr int windCount = 4;
0380     constexpr int dragonCount = 3;
0381     constexpr int flowerCount = 4;
0382 
0383     elementIdTable.reserve(2 * tileCount + characterCount + bambooCount + rodCount + seasonCount + windCount + dragonCount + flowerCount);
0384 
0385     // Build a list for faster lookup of element ids, mapped to the enumeration used by GameData and BoardWidget
0386     // Unselected tiles
0387     for (short idx = 1; idx <= tileCount; idx++) {
0388         elementIdTable.append(QStringLiteral("TILE_%1").arg(idx));
0389     }
0390     // Selected tiles
0391     for (short idx = 1; idx <= tileCount; idx++) {
0392         elementIdTable.append(QStringLiteral("TILE_%1_SEL").arg(idx));
0393     }
0394     // now faces
0395     for (short idx = 1; idx <= characterCount; idx++) {
0396         elementIdTable.append(QStringLiteral("CHARACTER_%1").arg(idx));
0397     }
0398     for (short idx = 1; idx <= bambooCount; idx++) {
0399         elementIdTable.append(QStringLiteral("BAMBOO_%1").arg(idx));
0400     }
0401     for (short idx = 1; idx <= rodCount; idx++) {
0402         elementIdTable.append(QStringLiteral("ROD_%1").arg(idx));
0403     }
0404     for (short idx = 1; idx <= seasonCount; idx++) {
0405         elementIdTable.append(QStringLiteral("SEASON_%1").arg(idx));
0406     }
0407     for (short idx = 1; idx <= windCount; idx++) {
0408         elementIdTable.append(QStringLiteral("WIND_%1").arg(idx));
0409     }
0410     for (short idx = 1; idx <= dragonCount; idx++) {
0411         elementIdTable.append(QStringLiteral("DRAGON_%1").arg(idx));
0412     }
0413     for (short idx = 1; idx <= flowerCount; idx++) {
0414         elementIdTable.append(QStringLiteral("FLOWER_%1").arg(idx));
0415     }
0416 }
0417 
0418 QString KMahjonggTilesetPrivate::pixmapCacheNameFromElementId(const QString &elementid, short width, short height) const
0419 {
0420     return name + elementid + QStringLiteral("W%1H%2").arg(width).arg(height);
0421 }
0422 
0423 QPixmap KMahjonggTilesetPrivate::renderElement(short width, short height, const QString &elementid) const
0424 {
0425     // qCDebug(LIBKMAHJONGG_LOG) << "render element" << elementid << width << height;
0426     QPixmap qiRend(width, height);
0427     qiRend.fill(Qt::transparent);
0428 
0429     if (svg.isValid()) {
0430         QPainter p(&qiRend);
0431         svg.render(&p, elementid);
0432     }
0433     return qiRend;
0434 }
0435 
0436 QPixmap KMahjonggTileset::selectedTile(int num) const
0437 {
0438     Q_D(const KMahjonggTileset);
0439 
0440     QPixmap pm;
0441 
0442     const qreal dpr = qApp->devicePixelRatio();
0443     // use tile size
0444     const short width = d->scaleddata.w * dpr;
0445     const short height = d->scaleddata.h * dpr;
0446     QString elemId = d->elementIdTable.at(num + 4); // selected offset in our idtable;
0447     // using raw pixmap size with cache id, as the rendering is done dpr-ignorant
0448     const  QString pixmapCacheName = d->pixmapCacheNameFromElementId(elemId, width, height);
0449     if (!QPixmapCache::find(pixmapCacheName, &pm)) {
0450         pm = d->renderElement(width, height, elemId);
0451         pm.setDevicePixelRatio(dpr);
0452         QPixmapCache::insert(pixmapCacheName, pm);
0453     }
0454     return pm;
0455 }
0456 
0457 QPixmap KMahjonggTileset::unselectedTile(int num) const
0458 {
0459     Q_D(const KMahjonggTileset);
0460 
0461     QPixmap pm;
0462 
0463     const qreal dpr = qApp->devicePixelRatio();
0464     // use tile size
0465     const short width = d->scaleddata.w * dpr;
0466     const short height = d->scaleddata.h * dpr;
0467     QString elemId = d->elementIdTable.at(num);
0468     // using raw pixmap size with cache id, as the rendering is done dpr-ignorant
0469     const  QString pixmapCacheName = d->pixmapCacheNameFromElementId(elemId, width, height);
0470     if (!QPixmapCache::find(pixmapCacheName, &pm)) {
0471         pm = d->renderElement(width, height, elemId);
0472         pm.setDevicePixelRatio(dpr);
0473         QPixmapCache::insert(pixmapCacheName, pm);
0474     }
0475     return pm;
0476 }
0477 
0478 QPixmap KMahjonggTileset::tileface(int num) const
0479 {
0480     Q_D(const KMahjonggTileset);
0481 
0482     QPixmap pm;
0483     if ((num + 8) >= d->elementIdTable.count()) {
0484         // qCDebug(LIBKMAHJONGG_LOG) << "Client asked for invalid tileface id";
0485         return pm;
0486     }
0487 
0488     const qreal dpr = qApp->devicePixelRatio();
0489     // use face size
0490     const short width = d->scaleddata.fw * dpr;
0491     const short height = d->scaleddata.fh * dpr;
0492     QString elemId = d->elementIdTable.at(num + 8); // tileface offset in our idtable;
0493     // using raw pixmap size with cache id, as the rendering is done dpr-ignorant
0494     const  QString pixmapCacheName = d->pixmapCacheNameFromElementId(elemId, width, height);
0495     if (!QPixmapCache::find(pixmapCacheName, &pm)) {
0496         pm = d->renderElement(width, height, elemId);
0497         pm.setDevicePixelRatio(dpr);
0498         QPixmapCache::insert(pixmapCacheName, pm);
0499     }
0500     return pm;
0501 }