1 /*
2 SPDX-FileCopyrightText: 1997 Mathias Mueller <in5y158@public.uni-hamburg.de>
3 SPDX-FileCopyrightText: 2006 Mauricio Piacentini <mauricio@tabuleiro.com>
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8 // own
9 #include "kmahjonggtileset.h"
10
11 // STL
12 #include <cstdlib>
13
14 // Qt
15 #include <QFile>
16 #include <QGuiApplication>
17 #include <QImage>
18 #include <QMap>
19 #include <QPainter>
20 #include <QPixmapCache>
21 #include <QStandardPaths>
22 #include <QSvgRenderer>
23
24 // KF
25 #include <KConfig>
26 #include <KConfigGroup>
27 #include <KLocalizedString>
28
29 // LibKMahjongg
30 #include "libkmahjongg_debug.h"
31
32 class KMahjonggTilesetMetricsData
33 {
34 public:
35 short lvloffx; // used for 3D indentation, x value
36 short lvloffy; // used for 3D indentation, y value
37 short w; // tile width ( +border +shadow)
38 short h; // tile height ( +border +shadow)
39 short fw; // face width
40 short fh; // face height
41
KMahjonggTilesetMetricsData()42 KMahjonggTilesetMetricsData()
43 : lvloffx(0)
44 , lvloffy(0)
45 , w(0)
46 , h(0)
47 , fw(0)
48 , fh(0)
49 {
50 }
51 };
52
53 class KMahjonggTilesetPrivate
54 {
55 public:
KMahjonggTilesetPrivate()56 KMahjonggTilesetPrivate()
57 : isSVG(false)
58 , graphicsLoaded(false)
59 {
60 }
61 QList<QString> elementIdTable;
62 QMap<QString, QString> authorproperties;
63
64 KMahjonggTilesetMetricsData originaldata;
65 KMahjonggTilesetMetricsData scaleddata;
66 QString filename; // cache the last file loaded to save reloading it
67 QString graphicspath;
68
69 QSvgRenderer svg;
70 bool isSVG;
71 bool graphicsLoaded;
72 };
73
74 // ---------------------------------------------------------
75
KMahjonggTileset()76 KMahjonggTileset::KMahjonggTileset()
77 : d(new KMahjonggTilesetPrivate)
78 {
79 buildElementIdTable();
80
81 static bool _inited = false;
82 if (_inited) {
83 return;
84 }
85 _inited = true;
86 }
87
88 // ---------------------------------------------------------
89
90 KMahjonggTileset::~KMahjonggTileset() = default;
91
updateScaleInfo(short tilew,short tileh)92 void KMahjonggTileset::updateScaleInfo(short tilew, short tileh)
93 {
94 d->scaleddata.w = tilew;
95 d->scaleddata.h = tileh;
96 double ratio = (static_cast<qreal>(d->scaleddata.w)) / (static_cast<qreal>(d->originaldata.w));
97 d->scaleddata.lvloffx = static_cast<short>(d->originaldata.lvloffx * ratio);
98 d->scaleddata.lvloffy = static_cast<short>(d->originaldata.lvloffy * ratio);
99 d->scaleddata.fw = static_cast<short>(d->originaldata.fw * ratio);
100 d->scaleddata.fh = static_cast<short>(d->originaldata.fh * ratio);
101 }
102
preferredTileSize(const QSize & boardsize,int horizontalCells,int verticalCells)103 QSize KMahjonggTileset::preferredTileSize(const QSize & boardsize, int horizontalCells, int verticalCells)
104 {
105 //calculate our best tile size to fit the boardsize passed to us
106 qreal newtilew, newtileh, aspectratio;
107 qreal bw = boardsize.width();
108 qreal bh = boardsize.height();
109
110 //use tileface for calculation, with one complete tile in the sum for extra margin
111 qreal fullh = (d->originaldata.fh * verticalCells) + d->originaldata.h;
112 qreal fullw = (d->originaldata.fw * horizontalCells) + d->originaldata.w;
113 qreal floatw = d->originaldata.w;
114 qreal floath = d->originaldata.h;
115
116 if ((fullw / fullh) > (bw / bh)) {
117 //space will be left on height, use width as limit
118 aspectratio = bw / fullw;
119 } else {
120 aspectratio = bh / fullh;
121 }
122 newtilew = aspectratio * floatw;
123 newtileh = aspectratio * floath;
124 return QSize(static_cast<short>(newtilew), static_cast<short>(newtileh));
125 }
126
loadDefault()127 bool KMahjonggTileset::loadDefault()
128 {
129 QString idx = QStringLiteral("default.desktop");
130
131 QString tilesetPath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kmahjongglib/tilesets/") + idx);
132 qCDebug(LIBKMAHJONGG_LOG) << "Inside LoadDefault(), located path at" << tilesetPath;
133 if (tilesetPath.isEmpty()) {
134 return false;
135 }
136 return loadTileset(tilesetPath);
137 }
138
authorProperty(const QString & key) const139 QString KMahjonggTileset::authorProperty(const QString & key) const
140 {
141 return d->authorproperties[key];
142 }
143
width() const144 short KMahjonggTileset::width() const
145 {
146 return d->scaleddata.w;
147 }
148
height() const149 short KMahjonggTileset::height() const
150 {
151 return d->scaleddata.h;
152 }
153
levelOffsetX() const154 short KMahjonggTileset::levelOffsetX() const
155 {
156 return d->scaleddata.lvloffx;
157 }
158
levelOffsetY() const159 short KMahjonggTileset::levelOffsetY() const
160 {
161 return d->scaleddata.lvloffy;
162 }
163
qWidth() const164 short KMahjonggTileset::qWidth() const
165 {
166 return static_cast<short>(d->scaleddata.fw / 2.0);
167 }
168
qHeight() const169 short KMahjonggTileset::qHeight() const
170 {
171 return static_cast<short>(d->scaleddata.fh / 2.0);
172 }
173
path() const174 QString KMahjonggTileset::path() const
175 {
176 return d->filename;
177 }
178
179 #define kTilesetVersionFormat 1
180
181 // ---------------------------------------------------------
loadTileset(const QString & tilesetPath)182 bool KMahjonggTileset::loadTileset(const QString & tilesetPath)
183 {
184 //qCDebug(LIBKMAHJONGG_LOG) << "Attempting to load .desktop at" << tilesetPath;
185
186 //clear our properties map
187 d->authorproperties.clear();
188
189 // verify if it is a valid file first and if we can open it
190 QFile tilesetfile(tilesetPath);
191 if (!tilesetfile.open(QIODevice::ReadOnly)) {
192 return false;
193 }
194 tilesetfile.close();
195
196 KConfig tileconfig(tilesetPath, KConfig::SimpleConfig);
197 KConfigGroup group = tileconfig.group("KMahjonggTileset");
198
199 d->authorproperties.insert(QStringLiteral("Name"), group.readEntry("Name")); // Returns translated data
200 d->authorproperties.insert(QStringLiteral("Author"), group.readEntry("Author"));
201 d->authorproperties.insert(QStringLiteral("Description"), group.readEntry("Description"));
202 d->authorproperties.insert(QStringLiteral("AuthorEmail"), group.readEntry("AuthorEmail"));
203
204 //Version control
205 int tileversion = group.readEntry("VersionFormat", 0);
206 //Format is increased when we have incompatible changes, meaning that older clients are not able to use the remaining information safely
207 if (tileversion > kTilesetVersionFormat) {
208 return false;
209 }
210
211 QString graphName = group.readEntry("FileName");
212
213 d->graphicspath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QStringLiteral("kmahjongglib/tilesets/") + graphName);
214 //qCDebug(LIBKMAHJONGG_LOG) << "Using tileset at" << d->graphicspath;
215
216 //only SVG for now
217 d->isSVG = true;
218 if (d->graphicspath.isEmpty()) {
219 return false;
220 }
221
222 d->originaldata.w = group.readEntry("TileWidth", 30);
223 d->originaldata.h = group.readEntry("TileHeight", 50);
224 d->originaldata.fw = group.readEntry("TileFaceWidth", 30);
225 d->originaldata.fh = group.readEntry("TileFaceHeight", 50);
226 d->originaldata.lvloffx = group.readEntry("LevelOffsetX", 10);
227 d->originaldata.lvloffy = group.readEntry("LevelOffsetY", 10);
228
229 //client application needs to call loadGraphics()
230 d->graphicsLoaded = false;
231 d->filename = tilesetPath;
232
233 return true;
234 }
235
236 // ---------------------------------------------------------
loadGraphics()237 bool KMahjonggTileset::loadGraphics()
238 {
239 if (d->graphicsLoaded) {
240 return true;
241 }
242 if (d->isSVG) {
243 //really?
244 d->svg.load(d->graphicspath);
245 if (d->svg.isValid()) {
246 //invalidate our global cache
247 QPixmapCache::clear();
248 d->graphicsLoaded = true;
249 reloadTileset(QSize(d->originaldata.w, d->originaldata.h));
250 } else {
251 return false;
252 }
253 } else {
254 //TODO add support for png??
255 return false;
256 }
257
258 return true;
259 }
260
261 // ---------------------------------------------------------
reloadTileset(const QSize & newTilesize)262 bool KMahjonggTileset::reloadTileset(const QSize & newTilesize)
263 {
264 if (QSize(d->scaleddata.w, d->scaleddata.h) == newTilesize) {
265 return false;
266 }
267
268 if (d->isSVG) {
269 if (d->svg.isValid()) {
270 updateScaleInfo(newTilesize.width(), newTilesize.height());
271 //rendering will be done when needed, automatically using the global cache
272 } else {
273 return false;
274 }
275 } else {
276 //TODO add support for png???
277 return false;
278 }
279
280 return true;
281 }
282
buildElementIdTable()283 void KMahjonggTileset::buildElementIdTable()
284 {
285 //Build a list for faster lookup of element ids, mapped to the enumeration used by GameData and BoardWidget
286 //Unselected tiles
287 for (short idx = 1; idx <= 4; idx++) {
288 d->elementIdTable.append(QStringLiteral("TILE_%1").arg(idx));
289 }
290 //Selected tiles
291 for (short idx = 1; idx <= 4; idx++) {
292 d->elementIdTable.append(QStringLiteral("TILE_%1_SEL").arg(idx));
293 }
294 //now faces
295 for (short idx = 1; idx <= 9; idx++) {
296 d->elementIdTable.append(QStringLiteral("CHARACTER_%1").arg(idx));
297 }
298 for (short idx = 1; idx <= 9; idx++) {
299 d->elementIdTable.append(QStringLiteral("BAMBOO_%1").arg(idx));
300 }
301 for (short idx = 1; idx <= 9; idx++) {
302 d->elementIdTable.append(QStringLiteral("ROD_%1").arg(idx));
303 }
304 for (short idx = 1; idx <= 4; idx++) {
305 d->elementIdTable.append(QStringLiteral("SEASON_%1").arg(idx));
306 }
307 for (short idx = 1; idx <= 4; idx++) {
308 d->elementIdTable.append(QStringLiteral("WIND_%1").arg(idx));
309 }
310 for (short idx = 1; idx <= 3; idx++) {
311 d->elementIdTable.append(QStringLiteral("DRAGON_%1").arg(idx));
312 }
313 for (short idx = 1; idx <= 4; idx++) {
314 d->elementIdTable.append(QStringLiteral("FLOWER_%1").arg(idx));
315 }
316 }
317
pixmapCacheNameFromElementId(const QString & elementid)318 QString KMahjonggTileset::pixmapCacheNameFromElementId(const QString & elementid)
319 {
320 return authorProperty(QStringLiteral("Name")) + elementid + QStringLiteral("W%1H%2").arg(d->scaleddata.w).arg(d->scaleddata.h);
321 }
322
renderElement(short width,short height,const QString & elementid)323 QPixmap KMahjonggTileset::renderElement(short width, short height, const QString & elementid)
324 {
325 //qCDebug(LIBKMAHJONGG_LOG) << "render element" << elementid << width << height;
326 const qreal dpr = qApp->devicePixelRatio();
327 width = width * dpr;
328 height = height * dpr;
329 QImage qiRend(QSize(width, height), QImage::Format_ARGB32_Premultiplied);
330 qiRend.fill(0);
331
332 if (d->svg.isValid()) {
333 QPainter p(&qiRend);
334 d->svg.render(&p, elementid);
335 }
336 qiRend.setDevicePixelRatio(dpr);
337 return QPixmap::fromImage(qiRend);
338 }
339
selectedTile(int num)340 QPixmap KMahjonggTileset::selectedTile(int num)
341 {
342 QPixmap pm;
343 QString elemId = d->elementIdTable.at(num + 4); //selected offset in our idtable;
344 if (!QPixmapCache::find(pixmapCacheNameFromElementId(elemId), &pm)) {
345 //use tile size
346 pm = renderElement(d->scaleddata.w, d->scaleddata.h, elemId);
347 QPixmapCache::insert(pixmapCacheNameFromElementId(elemId), pm);
348 }
349 return pm;
350 }
351
unselectedTile(int num)352 QPixmap KMahjonggTileset::unselectedTile(int num)
353 {
354 QPixmap pm;
355 QString elemId = d->elementIdTable.at(num);
356 if (!QPixmapCache::find(pixmapCacheNameFromElementId(elemId), &pm)) {
357 //use tile size
358 pm = renderElement(d->scaleddata.w, d->scaleddata.h, elemId);
359 QPixmapCache::insert(pixmapCacheNameFromElementId(elemId), pm);
360 }
361 return pm;
362 }
363
tileface(int num)364 QPixmap KMahjonggTileset::tileface(int num)
365 {
366 QPixmap pm;
367 if ((num + 8) >= d->elementIdTable.count()) {
368 //qCDebug(LIBKMAHJONGG_LOG) << "Client asked for invalid tileface id";
369 return pm;
370 }
371
372 QString elemId = d->elementIdTable.at(num + 8); //tileface offset in our idtable;
373 if (!QPixmapCache::find(pixmapCacheNameFromElementId(elemId), &pm)) {
374 //use face size
375 pm = renderElement(d->scaleddata.fw, d->scaleddata.fh, elemId);
376 QPixmapCache::insert(pixmapCacheNameFromElementId(elemId), pm);
377 }
378 return pm;
379 }
380