GCC Code Coverage Report
Directory: src/ Exec Total Coverage
File: src/gui/theme.cpp Lines: 391 465 84.1 %
Date: 2017-11-29 Branches: 643 1350 47.6 %

Line Branch Exec Source
1
/*
2
 *  The ManaPlus Client
3
 *  Copyright (C) 2008  The Legend of Mazzeroth Development Team
4
 *  Copyright (C) 2009  Aethyra Development Team
5
 *  Copyright (C) 2009  The Mana World Development Team
6
 *  Copyright (C) 2009-2010  The Mana Developers
7
 *  Copyright (C) 2011-2017  The ManaPlus Developers
8
 *
9
 *  This file is part of The ManaPlus Client.
10
 *
11
 *  This program is free software; you can redistribute it and/or modify
12
 *  it under the terms of the GNU General Public License as published by
13
 *  the Free Software Foundation; either version 2 of the License, or
14
 *  any later version.
15
 *
16
 *  This program is distributed in the hope that it will be useful,
17
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
 *  GNU General Public License for more details.
20
 *
21
 *  You should have received a copy of the GNU General Public License
22
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
23
 */
24
25
#include "gui/theme.h"
26
27
#include "configuration.h"
28
#include "graphicsmanager.h"
29
30
#include "const/gui/theme.h"
31
32
#include "fs/virtfs/fs.h"
33
#include "fs/virtfs/list.h"
34
35
#include "gui/skin.h"
36
#include "gui/themeinfo.h"
37
38
#include "resources/imagerect.h"
39
40
#include "resources/dye/dyepalette.h"
41
42
#include "resources/image/image.h"
43
44
#include "resources/loaders/imageloader.h"
45
#include "resources/loaders/imagesetloader.h"
46
#include "resources/loaders/subimageloader.h"
47
#include "resources/loaders/subimagesetloader.h"
48
#include "resources/loaders/xmlloader.h"
49
50
#include "utils/dtor.h"
51
#include "utils/foreach.h"
52
53
#include "debug.h"
54
55
2
static std::string defaultThemePath;
56
57
2
std::string Theme::mThemePath;
58
2
std::string Theme::mThemeName;
59
2
std::string Theme::mScreenDensity;
60
61
Theme *theme = nullptr;
62
63
// Set the theme path...
64
816
static void initDefaultThemePath()
65
{
66

4080
    defaultThemePath = branding.getStringValue("guiThemePath");
67
68
1632
    logger->log("defaultThemePath: " + defaultThemePath);
69

3264
    if (!defaultThemePath.empty() &&
70

1632
        VirtFs::isDirectory(defaultThemePath))
71
    {
72
        return;
73
    }
74
    defaultThemePath = "themes/";
75
}
76
77
408
Theme::Theme() :
78
    Palette(CAST_S32(ThemeColorId::THEME_COLORS_END) * THEME_PALETTES),
79
    mSkins(),
80
    mMinimumOpacity(-1.0F),
81
    mProgressColors(ProgressColors(CAST_SIZE(
82
2040
                    ProgressColorId::THEME_PROG_END)))
83
{
84
408
    initDefaultThemePath();
85
86

1632
    config.addListener("guialpha", this);
87
88
408
    mColors[CAST_SIZE(ThemeColorId::HIGHLIGHT)].ch = 'H';
89
408
    mColors[CAST_SIZE(ThemeColorId::CHAT)].ch = 'C';
90
408
    mColors[CAST_SIZE(ThemeColorId::GM)].ch = 'G';
91
408
    mColors[CAST_SIZE(ThemeColorId::GLOBAL)].ch = 'g';
92
408
    mColors[CAST_SIZE(ThemeColorId::PLAYER)].ch = 'Y';
93
408
    mColors[CAST_SIZE(ThemeColorId::WHISPER_TAB)].ch = 'W';
94
408
    mColors[CAST_SIZE(ThemeColorId::WHISPER_TAB_OFFLINE)].ch = 'w';
95
408
    mColors[CAST_SIZE(ThemeColorId::IS)].ch = 'I';
96
408
    mColors[CAST_SIZE(ThemeColorId::PARTY_CHAT_TAB)].ch = 'P';
97
408
    mColors[CAST_SIZE(ThemeColorId::GUILD_CHAT_TAB)].ch = 'U';
98
408
    mColors[CAST_SIZE(ThemeColorId::SERVER)].ch = 'S';
99
408
    mColors[CAST_SIZE(ThemeColorId::LOGGER)].ch = 'L';
100
408
    mColors[CAST_SIZE(ThemeColorId::HYPERLINK)].ch = '<';
101
408
    mColors[CAST_SIZE(ThemeColorId::SELFNICK)].ch = 's';
102
408
    mColors[CAST_SIZE(ThemeColorId::OLDCHAT)].ch = 'o';
103
408
    mColors[CAST_SIZE(ThemeColorId::AWAYCHAT)].ch = 'a';
104
408
    mCharColors['H'] = CAST_S32(ThemeColorId::HIGHLIGHT);
105
408
    mCharColors['C'] = CAST_S32(ThemeColorId::CHAT);
106
408
    mCharColors['G'] = CAST_S32(ThemeColorId::GM);
107
408
    mCharColors['g'] = CAST_S32(ThemeColorId::GLOBAL);
108
408
    mCharColors['Y'] = CAST_S32(ThemeColorId::PLAYER);
109
408
    mCharColors['W'] = CAST_S32(ThemeColorId::WHISPER_TAB);
110
408
    mCharColors['w'] = CAST_S32(ThemeColorId::WHISPER_TAB_OFFLINE);
111
408
    mCharColors['I'] = CAST_S32(ThemeColorId::IS);
112
408
    mCharColors['P'] = CAST_S32(ThemeColorId::PARTY_CHAT_TAB);
113
408
    mCharColors['U'] = CAST_S32(ThemeColorId::GUILD_CHAT_TAB);
114
408
    mCharColors['S'] = CAST_S32(ThemeColorId::SERVER);
115
408
    mCharColors['L'] = CAST_S32(ThemeColorId::LOGGER);
116
408
    mCharColors['<'] = CAST_S32(ThemeColorId::HYPERLINK);
117
408
    mCharColors['s'] = CAST_S32(ThemeColorId::SELFNICK);
118
408
    mCharColors['o'] = CAST_S32(ThemeColorId::OLDCHAT);
119
408
    mCharColors['a'] = CAST_S32(ThemeColorId::AWAYCHAT);
120
121
    // here need use outlined colors
122
816
    mCharColors['H' | 0x80]
123
408
        = CAST_S32(ThemeColorId::HIGHLIGHT_OUTLINE);
124
408
    mCharColors['C' | 0x80] = CAST_S32(ThemeColorId::CHAT_OUTLINE);
125
408
    mCharColors['G' | 0x80] = CAST_S32(ThemeColorId::GM_OUTLINE);
126
408
    mCharColors['g' | 0x80] = CAST_S32(ThemeColorId::GLOBAL_OUTLINE);
127
408
    mCharColors['Y' | 0x80] = CAST_S32(ThemeColorId::PLAYER_OUTLINE);
128
816
    mCharColors['W' | 0x80]
129
408
        = CAST_S32(ThemeColorId::WHISPER_TAB_OUTLINE);
130
816
    mCharColors['w' | 0x80]
131
408
        = CAST_S32(ThemeColorId::WHISPER_TAB_OFFLINE_OUTLINE);
132
408
    mCharColors['I' | 0x80] = CAST_S32(ThemeColorId::IS_OUTLINE);
133
816
    mCharColors['P' | 0x80]
134
408
        = CAST_S32(ThemeColorId::PARTY_CHAT_TAB_OUTLINE);
135
816
    mCharColors['U' | 0x80]
136
408
        = CAST_S32(ThemeColorId::GUILD_CHAT_TAB_OUTLINE);
137
408
    mCharColors['S' | 0x80] = CAST_S32(ThemeColorId::SERVER_OUTLINE);
138
408
    mCharColors['L' | 0x80] = CAST_S32(ThemeColorId::LOGGER_OUTLINE);
139
816
    mCharColors['<' | 0x80]
140
408
        = CAST_S32(ThemeColorId::HYPERLINK_OUTLINE);
141
408
    mCharColors['s' | 0x80] = CAST_S32(ThemeColorId::SELFNICK_OUTLINE);
142
408
    mCharColors['o' | 0x80] = CAST_S32(ThemeColorId::OLDCHAT_OUTLINE);
143
408
    mCharColors['a' | 0x80] = CAST_S32(ThemeColorId::AWAYCHAT_OUTLINE);
144
408
}
145
146
2292
Theme::~Theme()
147
{
148
764
    delete_all(mSkins);
149
1528
    config.removeListener("guialpha", this);
150
    CHECKLISTENERS
151
764
    delete_all(mProgressColors);
152
764
}
153
154
48
Color Theme::getProgressColor(const ProgressColorIdT type,
155
                              const float progress)
156
{
157
48
    int color[3] = {0, 0, 0};
158
159
48
    if (theme != nullptr)
160
    {
161
        const DyePalette *const dye
162
96
            = theme->mProgressColors[CAST_SIZE(type)];
163
164
48
        if (dye != nullptr)
165
        {
166
48
            dye->getColor(progress, color);
167
        }
168
        else
169
        {
170
            logger->log("color not found: "
171
                + toString(CAST_S32(type)));
172
        }
173
    }
174
175
48
    return Color(color[0], color[1], color[2]);
176
}
177
178
2038
Skin *Theme::load(const std::string &filename,
179
                  const std::string &filename2,
180
                  const bool full,
181
                  const std::string &restrict defaultPath)
182
{
183
    // Check if this skin was already loaded
184
185
4076
    const SkinIterator skinIterator = mSkins.find(filename);
186
4076
    if (mSkins.end() != skinIterator)
187
    {
188
396
        if (skinIterator->second != nullptr)
189
396
            skinIterator->second->instances++;
190
        return skinIterator->second;
191
    }
192
193
1642
    Skin *skin = nullptr;
194
1642
    if (mScreenDensity.empty())
195
    {   // if no density detected
196
1642
        skin = readSkin(filename, full);
197

1814
        if ((skin == nullptr) && !filename2.empty() && filename2 != filename)
198
172
            skin = readSkin(filename2, full);
199

1644
        if ((skin == nullptr) && filename2 != "window.xml")
200

8
            skin = readSkin("window.xml", full);
201
    }
202
    else
203
    {   // first use correct density images
204
        const std::string endStr("_" + mScreenDensity + ".xml");
205
        std::string name = filename;
206
        if (findCutLast(name, ".xml"))
207
            skin = readSkin(name + endStr, full);
208
        if (skin == nullptr)
209
            skin = readSkin(filename, full);
210
        if ((skin == nullptr) && !filename2.empty() && filename2 != filename)
211
        {
212
            name = filename2;
213
            if (findCutLast(name, ".xml"))
214
                skin = readSkin(name + endStr, full);
215
            if (skin == nullptr)
216
                skin = readSkin(filename2, full);
217
        }
218
        if ((skin == nullptr) && filename2 != "window.xml")
219
        {
220
            skin = readSkin("window" + endStr, full);
221
            if (skin == nullptr)
222
                skin = readSkin("window.xml", full);
223
        }
224
    }
225
226
1642
    if (skin == nullptr)
227
    {
228
        // Try falling back on the defaultPath if this makes sense
229
        if (filename != defaultPath)
230
        {
231
            logger->log("Error loading skin '%s', falling back on default.",
232
                        filename.c_str());
233
234
            skin = readSkin(defaultPath, full);
235
        }
236
237
        if (skin == nullptr)
238
        {
239
            logger->log(strprintf("Error: Loading default skin '%s' failed. "
240
                                  "Make sure the skin file is valid.",
241
                                  defaultPath.c_str()));
242
        }
243
    }
244
245
1642
    mSkins[filename] = skin;
246
1642
    return skin;
247
}
248
249
1994
void Theme::unload(Skin *const skin)
250
{
251
1994
    if (skin == nullptr)
252
        return;
253
1994
    skin->instances --;
254
1994
    if (skin->instances == 0)
255
    {
256
3260
        SkinIterator it = mSkins.begin();
257
3260
        const SkinIterator it_end = mSkins.end();
258
6470
        while (it != it_end)
259
        {
260
6470
            if (it->second == skin)
261
            {
262
1630
                mSkins.erase(it);
263
                break;
264
            }
265
            ++ it;
266
        }
267
1630
        delete skin;
268
    }
269
}
270
271
void Theme::setMinimumOpacity(const float minimumOpacity)
272
{
273
    if (minimumOpacity > 1.0F)
274
        return;
275
276
    mMinimumOpacity = minimumOpacity;
277
    updateAlpha();
278
}
279
280
5200
void Theme::updateAlpha()
281
{
282
15600
    FOR_EACH (SkinIterator, iter, mSkins)
283
    {
284
        Skin *const skin = iter->second;
285
        if (skin != nullptr)
286
            skin->updateAlpha(mMinimumOpacity);
287
    }
288
5200
}
289
290
5200
void Theme::optionChanged(const std::string &name A_UNUSED)
291
{
292
5200
    updateAlpha();
293
5200
}
294
295
156
struct SkinParameter final
296
{
297
    A_DEFAULT_COPY(SkinParameter)
298
    int index;
299
    std::string name;
300
};
301
302
6
static const SkinParameter skinParam[] =
303
{
304
    {0, "top-left-corner"},
305
    {0, "standart"},
306
    {0, "up"},
307
    {0, "hstart"},
308
    {0, "in"},
309
    {0, "normal"},
310
    {1, "top-edge"},
311
    {1, "highlighted"},
312
    {1, "down"},
313
    {1, "hmiddle"},
314
    {1, "in-highlighted"},
315
    {1, "checked"},
316
    {2, "top-right-corner"},
317
    {2, "pressed"},
318
    {2, "left"},
319
    {2, "hend"},
320
    {2, "out"},
321
    {2, "disabled"},
322
    {3, "left-edge"},
323
    {3, "disabled"},
324
    {3, "right"},
325
    {3, "hgrip"},
326
    {3, "out-highlighted"},
327
    {3, "disabled-checked"},
328
    {4, "bg-quad"},
329
    {4, "vstart"},
330
    {4, "normal-highlighted"},
331
    {5, "right-edge"},
332
    {5, "vmiddle"},
333
    {5, "checked-highlighted"},
334
    {6, "bottom-left-corner"},
335
    {6, "vend"},
336
    {7, "bottom-edge"},
337
    {7, "vgrip"},
338
    {8, "bottom-right-corner"},
339


















142
};
340
341
6
static const SkinParameter imageParam[] =
342
{
343
    {0, "closeImage"},
344
    {1, "closeImageHighlighted"},
345
    {2, "stickyImageUp"},
346
    {3, "stickyImageDown"},
347


18
};
348
349
3284
struct SkinHelper final
350
{
351
1642
    SkinHelper() :
352
        partType(),
353
        xPos(),
354
        yPos(),
355
        width(),
356
        height(),
357
        rect(),
358
        node(),
359
3284
        image()
360
    {
361
    }
362
363
    A_DELETE_COPY(SkinHelper)
364
365
    std::string partType;
366
    int xPos;
367
    int yPos;
368
    int width;
369
    int height;
370
    ImageRect *rect;
371
    XmlNodePtr *node;
372
    Image *image;
373
374
10042
    bool loadList(const SkinParameter *const params,
375
                  const size_t size) A_NONNULL(2)
376
    {
377
202138
        for (size_t f = 0; f < size; f ++)
378
        {
379
201734
            const SkinParameter &param = params[f];
380
211372
            if (partType == param.name)
381
            {
382
9638
                rect->grid[param.index] = Loader::getSubImage(
383
                    image,
384
                    xPos, yPos,
385
                    width, height);
386
9638
                return true;
387
            }
388
        }
389
        return false;
390
    }
391
};
392
393
1816
Skin *Theme::readSkin(const std::string &filename, const bool full)
394
{
395
1816
    if (filename.empty())
396
        return nullptr;
397
398
1806
    const std::string path = resolveThemePath(filename);
399

3612
    if (!VirtFs::exists(path))
400
        return nullptr;
401
    XML::Document *const doc = Loader::getXml(path,
402
        UseVirtFs_true,
403
1642
        SkipError_true);
404
1642
    if (doc == nullptr)
405
        return nullptr;
406
1642
    XmlNodeConstPtr rootNode = doc->rootNode();
407

1642
    if ((rootNode == nullptr) || !xmlNameEqual(rootNode, "skinset"))
408
    {
409
        doc->decRef();
410
        return nullptr;
411
    }
412
413

6568
    const std::string skinSetImage = XML::getProperty(rootNode, "image", "");
414
415
1642
    if (skinSetImage.empty())
416
    {
417
        logger->log1("Theme::readSkin(): Skinset does not define an image!");
418
        doc->decRef();
419
        return nullptr;
420
    }
421
422
1642
    Image *const dBorders = Theme::getImageFromTheme(skinSetImage);
423
3284
    ImageRect *const border = new ImageRect;
424
3284
    ImageRect *const images = new ImageRect;
425
1642
    int padding = 3;
426
1642
    int titlePadding = 4;
427
1642
    int titlebarHeight = 0;
428
1642
    int titlebarHeightRelative = 0;
429
1642
    int closePadding = 3;
430
1642
    int stickySpacing = 3;
431
1642
    int stickyPadding = 3;
432
1642
    int resizePadding = 2;
433
3284
    StringIntMap *const mOptions = new StringIntMap;
434
435
    // iterate <widget>'s
436
6568
    for_each_xml_child_node(widgetNode, rootNode)
437
    {
438

4926
        if (!xmlNameEqual(widgetNode, "widget"))
439
3284
            continue;
440
441
        const std::string widgetType =
442

8210
                XML::getProperty(widgetNode, "type", "unknown");
443
1642
        if (widgetType == "Window")
444
        {
445
3284
            SkinHelper helper;
446
1642
            const int globalXPos = XML::getProperty(widgetNode, "xpos", 0);
447
1642
            const int globalYPos = XML::getProperty(widgetNode, "ypos", 0);
448
35984
            for_each_xml_child_node(partNode, widgetNode)
449
            {
450

34342
                if (xmlNameEqual(partNode, "part"))
451
                {
452

53160
                    helper.partType = XML::getProperty(
453
                        partNode, "type", "unknown");
454
21264
                    helper.xPos = XML::getProperty(
455
10632
                        partNode, "xpos", 0) + globalXPos;
456
21264
                    helper.yPos = XML::getProperty(
457
10632
                        partNode, "ypos", 0) + globalYPos;
458
10632
                    helper.width = XML::getProperty(partNode, "width", 0);
459
10632
                    helper.height = XML::getProperty(partNode, "height", 0);
460

10632
                    if ((helper.width == 0) || (helper.height == 0))
461
994
                        continue;
462
9638
                    helper.image = dBorders;
463
464
9638
                    helper.rect = border;
465

9638
                    if (!helper.loadList(skinParam,
466
                        sizeof(skinParam) / sizeof(SkinParameter)))
467
                    {
468
404
                        helper.rect = images;
469
404
                        helper.loadList(imageParam,
470
                            sizeof(imageParam) / sizeof(SkinParameter));
471
                    }
472
                }
473

23710
                else if (full && xmlNameEqual(partNode, "option"))
474
                {
475
                    const std::string name = XML::getProperty(
476

12060
                        partNode, "name", "");
477
2412
                    if (name == "padding")
478
                    {
479
906
                        padding = XML::getProperty(partNode, "value", 3);
480
                    }
481
1506
                    else if (name == "titlePadding")
482
                    {
483
                        titlePadding = XML::getProperty(partNode, "value", 4);
484
                    }
485
1506
                    else if (name == "closePadding")
486
                    {
487
                        closePadding = XML::getProperty(partNode, "value", 3);
488
                    }
489
1506
                    else if (name == "stickySpacing")
490
                    {
491
84
                        stickySpacing = XML::getProperty(partNode, "value", 3);
492
                    }
493
1422
                    else if (name == "stickyPadding")
494
                    {
495
                        stickyPadding = XML::getProperty(partNode, "value", 3);
496
                    }
497
1422
                    else if (name == "titlebarHeight")
498
                    {
499
116
                        titlebarHeight = XML::getProperty(
500
                            partNode, "value", 0);
501
                    }
502
1306
                    else if (name == "titlebarHeightRelative")
503
                    {
504
                        titlebarHeightRelative = XML::getProperty(
505
                            partNode, "value", 0);
506
                    }
507
1306
                    else if (name == "resizePadding")
508
                    {
509
                        resizePadding = XML::getProperty(
510
                            partNode, "value", 2);
511
                    }
512
                    else
513
                    {
514

1306
                        (*mOptions)[name] = XML::getProperty(
515
                            partNode, "value", 0);
516
                    }
517
                }
518
            }
519
        }
520
        else
521
        {
522
            logger->log("Theme::readSkin(): Unknown widget type '%s'",
523
                        widgetType.c_str());
524
        }
525
    }
526
527
1642
    if (dBorders != nullptr)
528
1642
        dBorders->decRef();
529
530

6568
    (*mOptions)["closePadding"] = closePadding;
531

6568
    (*mOptions)["stickyPadding"] = stickyPadding;
532

6568
    (*mOptions)["stickySpacing"] = stickySpacing;
533

6568
    (*mOptions)["titlebarHeight"] = titlebarHeight;
534

6568
    (*mOptions)["titlebarHeightRelative"] = titlebarHeightRelative;
535

6568
    (*mOptions)["resizePadding"] = resizePadding;
536
537
    Skin *const skin = new Skin(border, images, filename, "", padding,
538

6568
        titlePadding, mOptions);
539
1642
    delete images;
540
1642
    skin->updateAlpha(mMinimumOpacity);
541
1642
    doc->decRef();
542
    return skin;
543
}
544
545
408
bool Theme::tryThemePath(const std::string &themeName)
546
{
547
408
    if (!themeName.empty())
548
    {
549
408
        const std::string path = defaultThemePath + themeName;
550

816
        if (VirtFs::exists(path))
551
        {
552
408
            mThemePath = path;
553
408
            mThemeName = themeName;
554
408
            if (theme != nullptr)
555

1632
                theme->loadColors("");
556
816
            return true;
557
        }
558
    }
559
560
    return false;
561
}
562
563
4
void Theme::fillSkinsList(StringVect &list)
564
{
565

20
    VirtFs::getDirs(branding.getStringValue("guiThemePath"), list);
566
8
    std::sort(list.begin(), list.end());
567
4
}
568
569
4
void Theme::fillFontsList(StringVect &list)
570
{
571
4
    VirtFs::permitLinks(true);
572

20
    VirtFs::getFiles(branding.getStringValue("fontsPath"), list);
573
8
    std::sort(list.begin(), list.end());
574
4
    VirtFs::permitLinks(false);
575
4
}
576
577
4
void Theme::fillSoundsList(StringVect &list)
578
{
579
    VirtFs::List *const skins = VirtFs::enumerateFiles(
580

20
        branding.getStringValue("systemsounds"));
581
582
112
    FOR_EACH (StringVectCIter, i, skins->names)
583
    {
584


552
        if (!VirtFs::isDirectory((branding.getStringValue(
585
276
            "systemsounds") + *i)))
586
        {
587
276
            std::string str = *i;
588

368
            if (findCutLast(str, ".ogg"))
589
76
                list.push_back(str);
590
        }
591
    }
592
593
4
    VirtFs::freeList(skins);
594
8
    std::sort(list.begin(), list.end());
595
4
}
596
597
408
void Theme::selectSkin()
598
{
599
408
    prepareThemePath();
600
816
    mScreenDensity = graphicsManager.getDensityString();
601
408
}
602
603
408
void Theme::prepareThemePath()
604
{
605
408
    initDefaultThemePath();
606
607
408
    mThemePath.clear();
608
408
    mThemeName.clear();
609
610
    // Try theme from settings
611


2040
    if (tryThemePath(config.getStringValue("theme")))
612
        return;
613
614
    // Try theme from branding
615
    if (tryThemePath(branding.getStringValue("theme")))
616
        return;
617
618
    if (mThemePath.empty())
619
        mThemePath = "graphics/gui";
620
621
    theme->loadColors(mThemePath);
622
623
    logger->log("Selected Theme: " + mThemePath);
624
}
625
626
6464
std::string Theme::resolveThemePath(const std::string &path)
627
{
628
    // Need to strip off any dye info for the existence tests
629
6464
    const int pos = CAST_S32(path.find('|'));
630
12928
    std::string file;
631
6464
    if (pos > 0)
632
        file = path.substr(0, pos);
633
    else
634
        file = path;
635
636
    // File with path
637
6464
    if (file.find('/') != std::string::npos)
638
    {
639
        // Might be a valid path already
640
        if (VirtFs::exists(file))
641
            return path;
642
    }
643
644
    // Try the theme
645
19392
    file = pathJoin(getThemePath(), file);
646
647
6464
    if (VirtFs::exists(file))
648
7624
        return pathJoin(getThemePath(), path);
649
650
    // Backup
651

13260
    return pathJoin(branding.getStringValue("guiPath"), path);
652
}
653
654
1654
Image *Theme::getImageFromTheme(const std::string &path)
655
{
656
3308
    return Loader::getImage(resolveThemePath(path));
657
}
658
659
292
ImageSet *Theme::getImageSetFromTheme(const std::string &path,
660
                                      const int w, const int h)
661
{
662
584
    return Loader::getImageSet(resolveThemePath(path), w, h);
663
}
664
665
305592
static int readColorType(const std::string &type)
666
{
667
    static const std::string colors[CAST_SIZE(
668
        ThemeColorId::THEME_COLORS_END)] =
669
    {
670
        "BROWSERBOX",
671
        "BROWSERBOX_OUTLINE",
672
        "SELFNICK",
673
        "SELFNICK_OUTLINE",
674
        "TEXT",
675
        "TEXT_OUTLINE",
676
        "CARET",
677
        "SHADOW",
678
        "OUTLINE",
679
        "BORDER",
680
        "PROGRESS_BAR",
681
        "PROGRESS_BAR_OUTLINE",
682
        "BUTTON",
683
        "BUTTON_OUTLINE",
684
        "BUTTON_DISABLED",
685
        "BUTTON_DISABLED_OUTLINE",
686
        "BUTTON_HIGHLIGHTED",
687
        "BUTTON_HIGHLIGHTED_OUTLINE",
688
        "BUTTON_PRESSED",
689
        "BUTTON_PRESSED_OUTLINE",
690
        "CHECKBOX",
691
        "CHECKBOX_OUTLINE",
692
        "DROPDOWN",
693
        "DROPDOWN_OUTLINE",
694
        "LABEL",
695
        "LABEL_OUTLINE",
696
        "LISTBOX",
697
        "LISTBOX_OUTLINE",
698
        "LISTBOX_SELECTED",
699
        "LISTBOX_SELECTED_OUTLINE",
700
        "RADIOBUTTON",
701
        "RADIOBUTTON_OUTLINE",
702
        "POPUP",
703
        "POPUP_OUTLINE",
704
        "TAB",
705
        "TAB_OUTLINE",
706
        "TAB_HIGHLIGHTED",
707
        "TAB_HIGHLIGHTED_OUTLINE",
708
        "TAB_SELECTED",
709
        "TAB_SELECTED_OUTLINE",
710
        "TEXTBOX",
711
        "TEXTFIELD",
712
        "TEXTFIELD_OUTLINE",
713
        "WINDOW",
714
        "WINDOW_OUTLINE",
715
        "BATTLE_CHAT_TAB",
716
        "BATTLE_CHAT_TAB_OUTLINE",
717
        "CHANNEL_CHAT_TAB",
718
        "CHANNEL_CHAT_TAB_OUTLINE",
719
        "PARTY_CHAT_TAB",
720
        "PARTY_CHAT_TAB_OUTLINE",
721
        "PARTY_SOCIAL_TAB",
722
        "PARTY_SOCIAL_TAB_OUTLINE",
723
        "GUILD_CHAT_TAB",
724
        "GUILD_CHAT_TAB_OUTLINE",
725
        "GUILD_SOCIAL_TAB",
726
        "GUILD_SOCIAL_TAB_OUTLINE",
727
        "GM_CHAT_TAB",
728
        "GM_CHAT_TAB_OUTLINE",
729
        "BATTLE_CHAT_TAB_HIGHLIGHTED",
730
        "BATTLE_CHAT_TAB_HIGHLIGHTED_OUTLINE",
731
        "CHANNEL_CHAT_TAB_HIGHLIGHTED",
732
        "CHANNEL_CHAT_TAB_HIGHLIGHTED_OUTLINE",
733
        "PARTY_CHAT_TAB_HIGHLIGHTED",
734
        "PARTY_CHAT_TAB_HIGHLIGHTED_OUTLINE",
735
        "PARTY_SOCIAL_TAB_HIGHLIGHTED",
736
        "PARTY_SOCIAL_TAB_HIGHLIGHTED_OUTLINE",
737
        "GUILD_CHAT_TAB_HIGHLIGHTED",
738
        "GUILD_CHAT_TAB_HIGHLIGHTED_OUTLINE",
739
        "GUILD_SOCIAL_TAB_HIGHLIGHTED",
740
        "GUILD_SOCIAL_TAB_HIGHLIGHTED_OUTLINE",
741
        "GM_CHAT_TAB_HIGHLIGHTED",
742
        "GM_CHAT_TAB_HIGHLIGHTED_OUTLINE",
743
        "BATTLE_CHAT_TAB_SELECTED",
744
        "BATTLE_CHAT_TAB_SELECTED_OUTLINE",
745
        "CHANNEL_CHAT_TAB_SELECTED",
746
        "CHANNEL_CHAT_TAB_SELECTED_OUTLINE",
747
        "PARTY_CHAT_TAB_SELECTED",
748
        "PARTY_CHAT_TAB_SELECTED_OUTLINE",
749
        "PARTY_SOCIAL_TAB_SELECTED",
750
        "PARTY_SOCIAL_TAB_SELECTED_OUTLINE",
751
        "GUILD_CHAT_TAB_SELECTED",
752
        "GUILD_CHAT_TAB_SELECTED_OUTLINE",
753
        "GUILD_SOCIAL_TAB_SELECTED",
754
        "GUILD_SOCIAL_TAB_SELECTED_OUTLINE",
755
        "GM_CHAT_TAB_SELECTED",
756
        "GM_CHAT_TAB_SELECTED_OUTLINE",
757
        "BACKGROUND",
758
        "BACKGROUND_GRAY",
759
        "SCROLLBAR_GRAY",
760
        "DROPDOWN_SHADOW",
761
        "HIGHLIGHT",
762
        "HIGHLIGHT_OUTLINE",
763
        "TAB_FLASH",
764
        "TAB_FLASH_OUTLINE",
765
        "TAB_PLAYER_FLASH",
766
        "TAB_PLAYER_FLASH_OUTLINE",
767
        "SHOP_WARNING",
768
        "ITEM_EQUIPPED",
769
        "ITEM_EQUIPPED_OUTLINE",
770
        "ITEM_NOT_EQUIPPED",
771
        "ITEM_NOT_EQUIPPED_OUTLINE",
772
        "CHAT",
773
        "CHAT_OUTLINE",
774
        "GM",
775
        "GM_OUTLINE",
776
        "GLOBAL",
777
        "GLOBAL_OUTLINE",
778
        "PLAYER",
779
        "PLAYER_OUTLINE",
780
        "WHISPER_TAB",
781
        "WHISPER_TAB_OUTLINE",
782
        "WHISPER_TAB_OFFLINE",
783
        "WHISPER_TAB_OFFLINE_OUTLINE",
784
        "WHISPER_TAB_HIGHLIGHTED",
785
        "WHISPER_TAB_HIGHLIGHTED_OUTLINE",
786
        "WHISPER_TAB_OFFLINE_HIGHLIGHTED",
787
        "WHISPER_TAB_OFFLINE_HIGHLIGHTED_OUTLINE",
788
        "WHISPER_TAB_SELECTED",
789
        "WHISPER_TAB_SELECTED_OUTLINE",
790
        "WHISPER_TAB_OFFLINE_SELECTED",
791
        "WHISPER_TAB_OFFLINE_SELECTED_OUTLINE",
792
        "IS",
793
        "IS_OUTLINE",
794
        "SERVER",
795
        "SERVER_OUTLINE",
796
        "LOGGER",
797
        "LOGGER_OUTLINE",
798
        "HYPERLINK",
799
        "HYPERLINK_OUTLINE",
800
        "UNKNOWN_ITEM",
801
        "UNKNOWN_ITEM_OUTLINE",
802
        "GENERIC",
803
        "GENERIC_OUTLINE",
804
        "HEAD",
805
        "HEAD_OUTLINE",
806
        "USABLE",
807
        "USABLE_OUTLINE",
808
        "TORSO",
809
        "TORSO_OUTLINE",
810
        "ONEHAND",
811
        "ONEHAND_OUTLINE",
812
        "LEGS",
813
        "LEGS_OUTLINE",
814
        "FEET",
815
        "FEET_OUTLINE",
816
        "TWOHAND",
817
        "TWOHAND_OUTLINE",
818
        "SHIELD",
819
        "SHIELD_OUTLINE",
820
        "RING",
821
        "RING_OUTLINE",
822
        "NECKLACE",
823
        "NECKLACE_OUTLINE",
824
        "ARMS",
825
        "ARMS_OUTLINE",
826
        "AMMO",
827
        "AMMO_OUTLINE",
828
        "SERVER_VERSION_NOT_SUPPORTED",
829
        "SERVER_VERSION_NOT_SUPPORTED_OUTLINE",
830
        "WARNING",
831
        "WARNING_OUTLINE",
832
        "CHARM",
833
        "CHARM_OUTLINE",
834
        "CARD",
835
        "CARD_OUTLINE",
836
        "PLAYER_ADVANCED",
837
        "PLAYER_ADVANCED_OUTLINE",
838
        "BUBBLE_NAME",
839
        "BUBBLE_NAME_OUTLINE",
840
        "BUBBLE_TEXT",
841
        "BUBBLE_TEXT_OUTLINE",
842
        "BLACK",
843
        "BLACK_OUTLINE",
844
        "RED",
845
        "RED_OUTLINE",
846
        "GREEN",
847
        "GREEN_OUTLINE",
848
        "BLUE",
849
        "BLUE_OUTLINE",
850
        "ORANGE",
851
        "ORANGE_OUTLINE",
852
        "YELLOW",
853
        "YELLOW_OUTLINE",
854
        "PINK",
855
        "PINK_OUTLINE",
856
        "PURPLE",
857
        "PURPLE_OUTLINE",
858
        "GRAY",
859
        "GRAY_OUTLINE",
860
        "BROWN",
861
        "BROWN_OUTLINE",
862
        "STATUSBAR_ON",
863
        "STATUSBAR_OFF",
864
        "TABLE_BACKGROUND",
865
        "SLOTS_BAR",
866
        "SLOTS_BAR_OUTLINE",
867
        "HP_BAR",
868
        "HP_BAR_OUTLINE",
869
        "MP_BAR",
870
        "MP_BAR_OUTLINE",
871
        "NO_MP_BAR",
872
        "NO_MP_BAR_OUTLINE",
873
        "XP_BAR",
874
        "XP_BAR_OUTLINE",
875
        "WEIGHT_BAR",
876
        "WEIGHT_BAR_OUTLINE",
877
        "MONEY_BAR",
878
        "MONEY_BAR_OUTLINE",
879
        "ARROWS_BAR",
880
        "ARROWS_BAR_OUTLINE",
881
        "STATUS_BAR",
882
        "STATUS_BAR_OUTLINE",
883
        "JOB_BAR",
884
        "JOB_BAR_OUTLINE",
885
        "OLDCHAT",
886
        "OLDCHAT_OUTLINE",
887
        "AWAYCHAT",
888
        "AWAYCHAT_OUTLINE",
889
        "SKILL_COOLDOWN",
890
        "TEXT_DISABLED",
891
        "TEXT_DISABLED_OUTLINE"
892

















































































































306484
    };
893
894
305592
    if (type.empty())
895
        return -1;
896
897
73536696
    for (int i = 0; i < CAST_S32(ThemeColorId::THEME_COLORS_END); i++)
898
    {
899
36904824
        if (compareStrI(type, colors[i]) == 0)
900
            return i;
901
    }
902
903
    return -1;
904
}
905
906
179520
static Color readColor(const std::string &description)
907
{
908
179520
    const int size = static_cast<int>(description.length());
909

359040
    if (size < 7 || description[0] != '#')
910
    {
911
        logger->log("Error, invalid theme color palette: %s",
912
                    description.c_str());
913
        return Palette::BLACK;
914
    }
915
916
    unsigned int v = 0;
917
2333760
    for (int i = 1; i < 7; ++i)
918
    {
919
2154240
        signed const char c = description[i];
920
        int n;
921
922
1077120
        if ('0' <= c && c <= '9')
923
        {
924
632400
            n = c - '0';
925
        }
926
444720
        else if ('A' <= c && c <= 'F')
927
        {
928
4896
            n = c - 'A' + 10;
929
        }
930
439824
        else if ('a' <= c && c <= 'f')
931
        {
932
439824
            n = c - 'a' + 10;
933
        }
934
        else
935
        {
936
            logger->log("Error, invalid theme color palette: %s",
937
                        description.c_str());
938
            return Palette::BLACK;
939
        }
940
941
1077120
        v = (v << 4) | n;
942
    }
943
944
179520
    return Color(v);
945
}
946
947
179520
static GradientTypeT readColorGradient(const std::string &grad)
948
{
949
    static const std::string grads[] =
950
    {
951
        "STATIC",
952
        "PULSE",
953
        "SPECTRUM",
954
        "RAINBOW"
955




179540
    };
956
957
179520
    if (grad.empty())
958
        return GradientType::STATIC;
959
960
1632
    for (int i = 0; i < 4; i++)
961
    {
962
1632
        if (compareStrI(grad, grads[i]) != 0)
963
            return static_cast<GradientTypeT>(i);
964
    }
965
966
    return GradientType::STATIC;
967
}
968
969
4896
static int readProgressType(const std::string &type)
970
{
971
    static const std::string colors[CAST_SIZE(
972
        ProgressColorId::THEME_PROG_END)] =
973
    {
974
        "HP",
975
        "HP_POISON",
976
        "MP",
977
        "NO_MP",
978
        "EXP",
979
        "INVY_SLOTS",
980
        "WEIGHT",
981
        "JOB",
982
        "UPDATE",
983
        "MONEY",
984
        "ARROWS",
985
        "STATUS"
986








4948
    };
987
988
4896
    if (type.empty())
989
        return -1;
990
991
58752
    for (int i = 0; i < CAST_S32(ProgressColorId::THEME_PROG_END); i++)
992
    {
993
31824
        if (compareStrI(type, colors[i]) == 0)
994
            return i;
995
    }
996
997
    return -1;
998
}
999
1000
408
void Theme::loadColors(std::string file)
1001
{
1002
408
    if (file.empty())
1003
        file = "colors.xml";
1004
    else
1005
        file = pathJoin(file, "colors.xml");
1006
1007
816
    XML::Document *const doc = Loader::getXml(resolveThemePath(file),
1008
        UseVirtFs_true,
1009
408
        SkipError_false);
1010
408
    if (doc == nullptr)
1011
        return;
1012
408
    XmlNodeConstPtrConst root = doc->rootNode();
1013
1014

408
    if ((root == nullptr) || !xmlNameEqual(root, "colors"))
1015
    {
1016
        logger->log("Error loading colors file: %s", file.c_str());
1017
        doc->decRef();
1018
        return;
1019
    }
1020
1021
816
    logger->log("Loading colors file: %s", file.c_str());
1022
1023
13056
    for_each_xml_child_node(paletteNode, root)
1024
    {
1025
12648
        if (xmlNameEqual(paletteNode, "progressbar"))
1026
        {
1027

24480
            const int type = readProgressType(XML::getProperty(
1028
4896
                paletteNode, "id", ""));
1029
4896
            if (type < 0)
1030
                continue;
1031
1032

34272
            mProgressColors[type] = new DyePalette(XML::getProperty(
1033

4896
                paletteNode, "color", ""), 6);
1034
        }
1035
7752
        else if (!xmlNameEqual(paletteNode, "palette"))
1036
        {
1037
6528
            continue;
1038
        }
1039
1040
6120
        const int paletteId = XML::getProperty(paletteNode, "id", 1);
1041
6120
        if (paletteId < 0 || paletteId >= THEME_PALETTES)
1042
            continue;
1043
1044
366384
        for_each_xml_child_node(node, paletteNode)
1045
        {
1046
360264
            if (xmlNameEqual(node, "color"))
1047
            {
1048

881280
                const std::string id = XML::getProperty(node, "id", "");
1049
179520
                const int type = readColorType(id);
1050
179520
                if (type < 0)
1051
16320
                    continue;
1052
1053

881280
                const std::string temp = XML::getProperty(node, "color", "");
1054
179520
                if (temp.empty())
1055
                    continue;
1056
1057
179520
                const Color color = readColor(temp);
1058
                const GradientTypeT grad = readColorGradient(
1059

897600
                    XML::getProperty(node, "effect", ""));
1060
179520
                mColors[paletteId * CAST_SIZE(
1061
718080
                    ThemeColorId::THEME_COLORS_END) + type].set(
1062
                    type, color, grad, 10);
1063
1064

718080
                if (!findLast(id, "_OUTLINE"))
1065
                {
1066

252144
                    const int type2 = readColorType(id + "_OUTLINE");
1067
126072
                    if (type2 < 0)
1068
16320
                        continue;
1069
109752
                    const int idx = paletteId
1070
                        * CAST_S32(ThemeColorId::THEME_COLORS_END);
1071
329256
                    mColors[idx + type2] = mColors[idx + type];
1072
                }
1073
            }
1074
        }
1075
    }
1076
408
    doc->decRef();
1077
}
1078
1079
#define loadGrid() \
1080
    { \
1081
        const ImageRect &rect = skin->getBorder(); \
1082
        for (int f = start; f <= end; f ++) \
1083
        { \
1084
            if (rect.grid[f]) \
1085
            { \
1086
                image.grid[f] = rect.grid[f]; \
1087
                image.grid[f]->incRef(); \
1088
            } \
1089
        } \
1090
    }
1091
1092
418
void Theme::loadRect(ImageRect &image,
1093
                     const std::string &name,
1094
                     const std::string &name2,
1095
                     const int start,
1096
                     const int end)
1097
{
1098
836
    Skin *const skin = load(name, name2, false);
1099
418
    if (skin != nullptr)
1100
    {
1101

418
        loadGrid();
1102
418
        unload(skin);
1103
    }
1104
418
}
1105
1106
86
Skin *Theme::loadSkinRect(ImageRect &image,
1107
                          const std::string &name,
1108
                          const std::string &name2,
1109
                          const int start,
1110
                          const int end)
1111
{
1112
172
    Skin *const skin = load(name, name2);
1113
86
    if (skin != nullptr)
1114

86
        loadGrid();
1115
86
    return skin;
1116
}
1117
1118
522
void Theme::unloadRect(const ImageRect &rect,
1119
                       const int start,
1120
                       const int end)
1121
{
1122
5220
    for (int f = start; f <= end; f ++)
1123
    {
1124
4698
        if (rect.grid[f] != nullptr)
1125
2554
            rect.grid[f]->decRef();
1126
    }
1127
522
}
1128
1129
116
Image *Theme::getImageFromThemeXml(const std::string &name,
1130
                                   const std::string &name2)
1131
{
1132
116
    if (theme == nullptr)
1133
        return nullptr;
1134
1135
232
    Skin *const skin = theme->load(name, name2, false);
1136
116
    if (skin != nullptr)
1137
    {
1138
116
        const ImageRect &rect = skin->getBorder();
1139
116
        if (rect.grid[0] != nullptr)
1140
        {
1141
104
            Image *const image = rect.grid[0];
1142
104
            image->incRef();
1143
104
            theme->unload(skin);
1144
104
            return image;
1145
        }
1146
12
        theme->unload(skin);
1147
    }
1148
    return nullptr;
1149
}
1150
1151
2
ImageSet *Theme::getImageSetFromThemeXml(const std::string &name,
1152
                                         const std::string &name2,
1153
                                         const int w, const int h)
1154
{
1155
2
    if (theme == nullptr)
1156
        return nullptr;
1157
1158
4
    Skin *const skin = theme->load(name, name2, false);
1159
2
    if (skin != nullptr)
1160
    {
1161
2
        const ImageRect &rect = skin->getBorder();
1162
2
        if (rect.grid[0] != nullptr)
1163
        {
1164
            Image *const image = rect.grid[0];
1165
            const SDL_Rect &rect2 = image->mBounds;
1166
            if ((rect2.w != 0u) && (rect2.h != 0u))
1167
            {
1168
                ImageSet *const imageSet = Loader::getSubImageSet(
1169
                    image, w, h);
1170
                theme->unload(skin);
1171
                return imageSet;
1172
            }
1173
        }
1174
2
        theme->unload(skin);
1175
    }
1176
    return nullptr;
1177
}
1178
1179
#define readValue(name) \
1180
        { \
1181
            tmpData = reinterpret_cast<XmlChar*>( \
1182
                XmlNodeGetContent(infoNode)); \
1183
            info->name = tmpData; \
1184
            XmlFree(tmpData); \
1185
        }
1186
1187
#define readIntValue(name) \
1188
        { \
1189
            tmpData = reinterpret_cast<XmlChar*>( \
1190
                XmlNodeGetContent(infoNode)); \
1191
            info->name = atoi(tmpData); \
1192
            XmlFree(tmpData); \
1193
        }
1194
1195
#define readFloatValue(name) \
1196
        { \
1197
            tmpData = reinterpret_cast<XmlChar*>( \
1198
                XmlNodeGetContent(infoNode)); \
1199
            info->name = static_cast<float>(atof(tmpData)); \
1200
            XmlFree(tmpData); \
1201
        }
1202
1203
8
ThemeInfo *Theme::loadInfo(const std::string &themeName)
1204
{
1205
16
    std::string path;
1206
8
    if (themeName.empty())
1207
    {
1208
        path = "graphics/gui/info.xml";
1209
    }
1210
    else
1211
    {
1212

56
        path = pathJoin(defaultThemePath,
1213
            themeName,
1214
            "info.xml");
1215
    }
1216

16
    logger->log("loading: " + path);
1217
    XML::Document *const doc = Loader::getXml(path,
1218
        UseVirtFs_true,
1219
8
        SkipError_false);
1220
8
    if (doc == nullptr)
1221
        return nullptr;
1222
8
    XmlNodeConstPtrConst rootNode = doc->rootNode();
1223
1224

8
    if ((rootNode == nullptr) || !xmlNameEqual(rootNode, "info"))
1225
    {
1226
        doc->decRef();
1227
        return nullptr;
1228
    }
1229
1230
8
    ThemeInfo *const info = new ThemeInfo;
1231
1232
8
    const std::string fontSize2("fontSize_" + mScreenDensity);
1233
16
    const std::string npcfontSize2("npcfontSize_" + mScreenDensity);
1234
8
    XmlChar *tmpData = nullptr;
1235
64
    for_each_xml_child_node(infoNode, rootNode)
1236
    {
1237

56
        if (xmlNameEqual(infoNode, "name"))
1238

16
            readValue(name)
1239

48
        else if (xmlNameEqual(infoNode, "copyright"))
1240

16
            readValue(copyright)
1241

40
        else if (xmlNameEqual(infoNode, "font"))
1242
            readValue(font)
1243

40
        else if (xmlNameEqual(infoNode, "boldFont"))
1244
            readValue(boldFont)
1245

40
        else if (xmlNameEqual(infoNode, "particleFont"))
1246
            readValue(particleFont)
1247

40
        else if (xmlNameEqual(infoNode, "helpFont"))
1248
            readValue(helpFont)
1249

40
        else if (xmlNameEqual(infoNode, "secureFont"))
1250
            readValue(secureFont)
1251

40
        else if (xmlNameEqual(infoNode, "npcFont"))
1252
            readValue(npcFont)
1253

40
        else if (xmlNameEqual(infoNode, "japanFont"))
1254
            readValue(japanFont)
1255

40
        else if (xmlNameEqual(infoNode, "chinaFont"))
1256
            readValue(chinaFont)
1257

40
        else if (xmlNameEqual(infoNode, "fontSize"))
1258
            readIntValue(fontSize)
1259

40
        else if (xmlNameEqual(infoNode, "npcfontSize"))
1260
            readIntValue(npcfontSize)
1261

40
        else if (xmlNameEqual(infoNode, "guialpha"))
1262

16
            readFloatValue(guiAlpha)
1263

32
        else if (xmlNameEqual(infoNode, fontSize2.c_str()))
1264
            readIntValue(fontSize)
1265

32
        else if (xmlNameEqual(infoNode, npcfontSize2.c_str()))
1266
            readIntValue(npcfontSize)
1267
    }
1268
8
    doc->decRef();
1269
8
    return info;
1270
}
1271
1272
8
ThemeColorIdT Theme::getIdByChar(const signed char c, bool &valid) const
1273
{
1274
16
    const CharColors::const_iterator it = mCharColors.find(c);
1275
16
    if (it != mCharColors.end())
1276
    {
1277
4
        valid = true;
1278
4
        return static_cast<ThemeColorIdT>((*it).second);
1279
    }
1280
1281
4
    valid = false;
1282
4
    return ThemeColorId::BROWSERBOX;
1283

6
}