2020-09-21 17:17:34 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
2020-06-21 12:25:28 +00:00
|
|
|
//
|
2024-06-02 20:30:41 +00:00
|
|
|
// ES-DE Frontend
|
2020-06-21 12:25:28 +00:00
|
|
|
// ThemeData.cpp
|
|
|
|
//
|
2022-01-29 17:41:22 +00:00
|
|
|
// Finds available themes on the file system and loads and parses these.
|
|
|
|
// Basic error checking for valid elements and data types is done here,
|
|
|
|
// with additional validation handled by the individual components.
|
2020-06-21 12:25:28 +00:00
|
|
|
//
|
|
|
|
|
2013-11-12 23:28:15 +00:00
|
|
|
#include "ThemeData.h"
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
#include "Log.h"
|
|
|
|
#include "Settings.h"
|
2014-01-03 14:26:39 +00:00
|
|
|
#include "components/ImageComponent.h"
|
|
|
|
#include "components/TextComponent.h"
|
2018-01-09 22:55:09 +00:00
|
|
|
#include "utils/FileSystemUtil.h"
|
2019-07-06 14:50:50 +00:00
|
|
|
#include "utils/StringUtil.h"
|
2020-09-21 17:17:34 +00:00
|
|
|
|
2018-01-29 22:50:10 +00:00
|
|
|
#include <algorithm>
|
2020-09-21 17:17:34 +00:00
|
|
|
#include <pugixml.hpp>
|
2014-01-26 22:20:21 +00:00
|
|
|
|
2022-01-29 17:41:22 +00:00
|
|
|
// clang-format off
|
|
|
|
std::vector<std::string> ThemeData::sSupportedViews {
|
|
|
|
{"all"},
|
|
|
|
{"system"},
|
|
|
|
{"gamelist"}};
|
|
|
|
|
2023-01-04 18:01:41 +00:00
|
|
|
std::vector<std::string> ThemeData::sSupportedMediaTypes {
|
|
|
|
{"miximage"},
|
|
|
|
{"marquee"},
|
|
|
|
{"screenshot"},
|
|
|
|
{"titlescreen"},
|
|
|
|
{"cover"},
|
|
|
|
{"backcover"},
|
|
|
|
{"3dbox"},
|
|
|
|
{"physicalmedia"},
|
|
|
|
{"fanart"},
|
|
|
|
{"video"}};
|
|
|
|
|
2023-01-08 16:00:36 +00:00
|
|
|
std::vector<std::string> ThemeData::sSupportedTransitions {
|
|
|
|
{"systemToSystem"},
|
|
|
|
{"systemToGamelist"},
|
|
|
|
{"gamelistToGamelist"},
|
|
|
|
{"gamelistToSystem"},
|
|
|
|
{"startupToSystem"},
|
|
|
|
{"startupToGamelist"}};
|
|
|
|
|
|
|
|
std::vector<std::string> ThemeData::sSupportedTransitionAnimations {
|
|
|
|
{"builtin-instant"},
|
|
|
|
{"builtin-slide"},
|
|
|
|
{"builtin-fade"}};
|
|
|
|
|
2023-12-20 20:58:40 +00:00
|
|
|
std::vector<std::pair<std::string, std::string>> ThemeData::sSupportedFontSizes {
|
|
|
|
{"medium", "medium"},
|
|
|
|
{"large", "large"},
|
|
|
|
{"small", "small"},
|
|
|
|
{"x-large", "extra large"},
|
|
|
|
{"x-small", "extra small"}};
|
|
|
|
|
2022-01-31 22:22:42 +00:00
|
|
|
std::vector<std::pair<std::string, std::string>> ThemeData::sSupportedAspectRatios {
|
2022-11-01 16:08:51 +00:00
|
|
|
{"automatic", "automatic"},
|
2022-01-31 22:22:42 +00:00
|
|
|
{"16:9", "16:9"},
|
|
|
|
{"16:9_vertical", "16:9 vertical"},
|
|
|
|
{"16:10", "16:10"},
|
|
|
|
{"16:10_vertical", "16:10 vertical"},
|
|
|
|
{"3:2", "3:2"},
|
|
|
|
{"3:2_vertical", "3:2 vertical"},
|
|
|
|
{"4:3", "4:3"},
|
|
|
|
{"4:3_vertical", "4:3 vertical"},
|
|
|
|
{"5:4", "5:4"},
|
|
|
|
{"5:4_vertical", "5:4 vertical"},
|
2023-12-19 17:48:12 +00:00
|
|
|
{"19.5:9", "19.5:9"},
|
2023-12-19 18:00:10 +00:00
|
|
|
{"19.5:9_vertical", "19.5:9 vertical"},
|
|
|
|
{"20:9", "20:9"},
|
|
|
|
{"20:9_vertical", "20:9 vertical"},
|
2022-01-31 22:22:42 +00:00
|
|
|
{"21:9", "21:9"},
|
|
|
|
{"21:9_vertical", "21:9 vertical"},
|
|
|
|
{"32:9", "32:0"},
|
2023-12-17 20:45:42 +00:00
|
|
|
{"32:9_vertical", "32:9 vertical"},
|
|
|
|
{"1:1", "1:1"}};
|
2022-01-29 17:41:22 +00:00
|
|
|
|
2022-11-01 16:08:51 +00:00
|
|
|
std::map<std::string, float> ThemeData::sAspectRatioMap {
|
|
|
|
{"16:9", 1.7777f},
|
|
|
|
{"16:9_vertical", 0.5625f},
|
|
|
|
{"16:10", 1.6f},
|
|
|
|
{"16:10_vertical", 0.625f},
|
|
|
|
{"3:2", 1.5f},
|
|
|
|
{"3:2_vertical", 0.6667f},
|
|
|
|
{"4:3", 1.3333f},
|
|
|
|
{"4:3_vertical", 0.75f},
|
|
|
|
{"5:4", 1.25f},
|
|
|
|
{"5:4_vertical", 0.8f},
|
2023-12-19 17:48:12 +00:00
|
|
|
{"19.5:9", 2.1667f},
|
|
|
|
{"19.5:9_vertical", 0.4615f},
|
2023-12-19 18:00:10 +00:00
|
|
|
{"20:9", 2.2222f},
|
|
|
|
{"20:9_vertical", 0.45f},
|
2022-11-01 16:08:51 +00:00
|
|
|
{"21:9", 2.3703f},
|
|
|
|
{"21:9_vertical", 0.4219f},
|
|
|
|
{"32:9", 3.5555f},
|
2023-12-17 20:45:42 +00:00
|
|
|
{"32:9_vertical", 0.2813f},
|
|
|
|
{"1:1", 1.0f}};
|
2022-11-01 16:08:51 +00:00
|
|
|
|
2022-01-29 17:41:22 +00:00
|
|
|
std::map<std::string, std::map<std::string, std::string>> ThemeData::sPropertyAttributeMap
|
|
|
|
// The data type is defined by the parent property.
|
|
|
|
{
|
|
|
|
{"badges",
|
|
|
|
{{"customBadgeIcon", "badge"},
|
|
|
|
{"customControllerIcon", "controller"}}},
|
|
|
|
{"helpsystem",
|
|
|
|
{{"customButtonIcon", "button"}}},
|
|
|
|
};
|
2021-08-17 16:41:45 +00:00
|
|
|
|
2022-01-16 11:09:55 +00:00
|
|
|
std::map<std::string, std::map<std::string, ThemeData::ElementPropertyType>>
|
2022-01-29 17:41:22 +00:00
|
|
|
ThemeData::sElementMap {
|
2022-11-13 18:59:26 +00:00
|
|
|
{"carousel",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"type", STRING},
|
2022-12-11 10:22:08 +00:00
|
|
|
{"staticImage", PATH},
|
|
|
|
{"imageType", STRING},
|
|
|
|
{"defaultImage", PATH},
|
2023-02-13 20:38:52 +00:00
|
|
|
{"defaultFolderImage", PATH},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"maxItemCount", FLOAT},
|
|
|
|
{"itemsBeforeCenter", UNSIGNED_INTEGER},
|
|
|
|
{"itemsAfterCenter", UNSIGNED_INTEGER},
|
2022-12-12 16:45:07 +00:00
|
|
|
{"itemStacking", STRING},
|
2023-01-05 21:10:45 +00:00
|
|
|
{"selectedItemMargins", NORMALIZED_PAIR},
|
2024-06-02 15:01:38 +00:00
|
|
|
{"selectedItemOffset", NORMALIZED_PAIR},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"itemSize", NORMALIZED_PAIR},
|
|
|
|
{"itemScale", FLOAT},
|
|
|
|
{"itemRotation", FLOAT},
|
|
|
|
{"itemRotationOrigin", NORMALIZED_PAIR},
|
|
|
|
{"itemAxisHorizontal", BOOLEAN},
|
2023-01-11 22:33:50 +00:00
|
|
|
{"itemAxisRotation", FLOAT},
|
2023-03-01 20:19:20 +00:00
|
|
|
{"imageFit", STRING},
|
2022-12-11 10:22:08 +00:00
|
|
|
{"imageInterpolation", STRING},
|
2023-08-20 17:41:07 +00:00
|
|
|
{"imageCornerRadius", FLOAT},
|
2022-12-11 10:22:08 +00:00
|
|
|
{"imageColor", COLOR},
|
|
|
|
{"imageColorEnd", COLOR},
|
|
|
|
{"imageGradientType", STRING},
|
2023-02-13 19:30:03 +00:00
|
|
|
{"imageSelectedColor", COLOR},
|
|
|
|
{"imageSelectedColorEnd", COLOR},
|
|
|
|
{"imageSelectedGradientType", STRING},
|
2022-12-14 19:17:41 +00:00
|
|
|
{"imageBrightness", FLOAT},
|
2022-12-13 20:35:21 +00:00
|
|
|
{"imageSaturation", FLOAT},
|
2022-12-09 18:27:48 +00:00
|
|
|
{"itemTransitions", STRING},
|
2023-02-14 16:56:27 +00:00
|
|
|
{"itemDiagonalOffset", FLOAT},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"itemHorizontalAlignment", STRING},
|
|
|
|
{"itemVerticalAlignment", STRING},
|
|
|
|
{"wheelHorizontalAlignment", STRING},
|
2023-01-11 21:29:30 +00:00
|
|
|
{"wheelVerticalAlignment", STRING},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"horizontalOffset", FLOAT},
|
|
|
|
{"verticalOffset", FLOAT},
|
|
|
|
{"reflections", BOOLEAN},
|
|
|
|
{"reflectionsOpacity", FLOAT},
|
|
|
|
{"reflectionsFalloff", FLOAT},
|
|
|
|
{"unfocusedItemOpacity", FLOAT},
|
2023-03-03 21:41:53 +00:00
|
|
|
{"unfocusedItemSaturation", FLOAT},
|
|
|
|
{"unfocusedItemDimming", FLOAT},
|
2023-02-23 16:08:21 +00:00
|
|
|
{"fastScrolling", BOOLEAN},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"color", COLOR},
|
|
|
|
{"colorEnd", COLOR},
|
|
|
|
{"gradientType", STRING},
|
|
|
|
{"text", STRING},
|
2023-08-09 16:57:23 +00:00
|
|
|
{"textRelativeScale", FLOAT},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"textColor", COLOR},
|
|
|
|
{"textBackgroundColor", COLOR},
|
2023-02-13 19:30:03 +00:00
|
|
|
{"textSelectedColor", COLOR},
|
|
|
|
{"textSelectedBackgroundColor", COLOR},
|
2023-08-09 16:57:23 +00:00
|
|
|
{"textHorizontalScrolling", BOOLEAN},
|
|
|
|
{"textHorizontalScrollSpeed", FLOAT},
|
|
|
|
{"textHorizontalScrollDelay", FLOAT},
|
2023-08-09 18:02:21 +00:00
|
|
|
{"textHorizontalScrollGap", FLOAT},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"fontPath", PATH},
|
|
|
|
{"fontSize", FLOAT},
|
|
|
|
{"letterCase", STRING},
|
2023-01-13 10:03:23 +00:00
|
|
|
{"letterCaseAutoCollections", STRING},
|
|
|
|
{"letterCaseCustomCollections", STRING},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"lineSpacing", FLOAT},
|
2023-01-14 13:05:24 +00:00
|
|
|
{"systemNameSuffix", BOOLEAN},
|
|
|
|
{"letterCaseSystemNameSuffix", STRING},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"fadeAbovePrimary", BOOLEAN},
|
2023-07-30 16:17:27 +00:00
|
|
|
{"zIndex", FLOAT}}},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"grid",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
2022-12-11 10:22:08 +00:00
|
|
|
{"staticImage", PATH},
|
|
|
|
{"imageType", STRING},
|
|
|
|
{"defaultImage", PATH},
|
2023-02-13 20:38:52 +00:00
|
|
|
{"defaultFolderImage", PATH},
|
2022-12-11 10:22:08 +00:00
|
|
|
{"itemSize", NORMALIZED_PAIR},
|
|
|
|
{"itemScale", FLOAT},
|
|
|
|
{"itemSpacing", NORMALIZED_PAIR},
|
2023-04-12 21:06:13 +00:00
|
|
|
{"scaleInwards", BOOLEAN},
|
2022-12-11 10:22:08 +00:00
|
|
|
{"fractionalRows", BOOLEAN},
|
|
|
|
{"itemTransitions", STRING},
|
|
|
|
{"rowTransitions", STRING},
|
|
|
|
{"unfocusedItemOpacity", FLOAT},
|
2023-03-03 22:51:42 +00:00
|
|
|
{"unfocusedItemSaturation", FLOAT},
|
|
|
|
{"unfocusedItemDimming", FLOAT},
|
2022-12-11 10:22:08 +00:00
|
|
|
{"imageFit", STRING},
|
2023-09-22 15:30:34 +00:00
|
|
|
{"imageInterpolation", STRING},
|
2022-12-11 16:48:56 +00:00
|
|
|
{"imageRelativeScale", FLOAT},
|
2023-08-20 17:41:07 +00:00
|
|
|
{"imageCornerRadius", FLOAT},
|
2022-12-11 10:22:08 +00:00
|
|
|
{"imageColor", COLOR},
|
|
|
|
{"imageColorEnd", COLOR},
|
|
|
|
{"imageGradientType", STRING},
|
2023-01-28 13:14:30 +00:00
|
|
|
{"imageSelectedColor", COLOR},
|
|
|
|
{"imageSelectedColorEnd", COLOR},
|
|
|
|
{"imageSelectedGradientType", STRING},
|
2022-12-14 19:17:41 +00:00
|
|
|
{"imageBrightness", FLOAT},
|
2022-12-13 20:35:21 +00:00
|
|
|
{"imageSaturation", FLOAT},
|
2022-12-09 17:49:32 +00:00
|
|
|
{"backgroundImage", PATH},
|
|
|
|
{"backgroundRelativeScale", FLOAT},
|
2023-08-20 17:41:07 +00:00
|
|
|
{"backgroundCornerRadius", FLOAT},
|
2022-12-09 17:49:32 +00:00
|
|
|
{"backgroundColor", COLOR},
|
|
|
|
{"backgroundColorEnd", COLOR},
|
|
|
|
{"backgroundGradientType", STRING},
|
|
|
|
{"selectorImage", PATH},
|
|
|
|
{"selectorRelativeScale", FLOAT},
|
2023-08-20 17:41:07 +00:00
|
|
|
{"selectorCornerRadius", FLOAT},
|
2022-12-11 11:10:08 +00:00
|
|
|
{"selectorLayer", STRING},
|
2022-12-09 17:49:32 +00:00
|
|
|
{"selectorColor", COLOR},
|
|
|
|
{"selectorColorEnd", COLOR},
|
|
|
|
{"selectorGradientType", STRING},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"text", STRING},
|
2022-12-09 17:49:32 +00:00
|
|
|
{"textRelativeScale", FLOAT},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"textColor", COLOR},
|
|
|
|
{"textBackgroundColor", COLOR},
|
2023-01-28 13:14:30 +00:00
|
|
|
{"textSelectedColor", COLOR},
|
|
|
|
{"textSelectedBackgroundColor", COLOR},
|
2023-08-10 17:20:44 +00:00
|
|
|
{"textHorizontalScrolling", BOOLEAN},
|
|
|
|
{"textHorizontalScrollSpeed", FLOAT},
|
|
|
|
{"textHorizontalScrollDelay", FLOAT},
|
|
|
|
{"textHorizontalScrollGap", FLOAT},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"fontPath", PATH},
|
|
|
|
{"fontSize", FLOAT},
|
|
|
|
{"letterCase", STRING},
|
2023-01-13 10:03:23 +00:00
|
|
|
{"letterCaseAutoCollections", STRING},
|
|
|
|
{"letterCaseCustomCollections", STRING},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"lineSpacing", FLOAT},
|
2023-01-14 13:05:24 +00:00
|
|
|
{"systemNameSuffix", BOOLEAN},
|
|
|
|
{"letterCaseSystemNameSuffix", STRING},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"fadeAbovePrimary", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
|
|
|
{"textlist",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
2024-01-27 13:50:32 +00:00
|
|
|
{"selectorWidth", FLOAT},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"selectorHeight", FLOAT},
|
2022-12-18 11:02:50 +00:00
|
|
|
{"selectorHorizontalOffset", FLOAT},
|
|
|
|
{"selectorVerticalOffset", FLOAT},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"selectorColor", COLOR},
|
|
|
|
{"selectorColorEnd", COLOR},
|
|
|
|
{"selectorGradientType", STRING},
|
|
|
|
{"selectorImagePath", PATH},
|
|
|
|
{"selectorImageTile", BOOLEAN},
|
|
|
|
{"primaryColor", COLOR},
|
|
|
|
{"secondaryColor", COLOR},
|
|
|
|
{"selectedColor", COLOR},
|
|
|
|
{"selectedSecondaryColor", COLOR},
|
2023-02-01 18:55:24 +00:00
|
|
|
{"selectedBackgroundColor", COLOR},
|
|
|
|
{"selectedSecondaryBackgroundColor", COLOR},
|
2024-06-02 20:36:52 +00:00
|
|
|
{"selectedBackgroundMargins", NORMALIZED_PAIR},
|
2024-06-03 15:34:07 +00:00
|
|
|
{"selectedBackgroundCornerRadius", FLOAT},
|
2023-08-08 17:35:20 +00:00
|
|
|
{"textHorizontalScrolling", BOOLEAN},
|
|
|
|
{"textHorizontalScrollSpeed", FLOAT},
|
|
|
|
{"textHorizontalScrollDelay", FLOAT},
|
2023-08-09 18:02:21 +00:00
|
|
|
{"textHorizontalScrollGap", FLOAT},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"fontPath", PATH},
|
|
|
|
{"fontSize", FLOAT},
|
|
|
|
{"horizontalAlignment", STRING},
|
|
|
|
{"horizontalMargin", FLOAT},
|
|
|
|
{"letterCase", STRING},
|
2023-01-13 10:03:23 +00:00
|
|
|
{"letterCaseAutoCollections", STRING},
|
|
|
|
{"letterCaseCustomCollections", STRING},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"lineSpacing", FLOAT},
|
|
|
|
{"indicators", STRING},
|
|
|
|
{"collectionIndicators", STRING},
|
2023-01-14 13:05:24 +00:00
|
|
|
{"systemNameSuffix", BOOLEAN},
|
|
|
|
{"letterCaseSystemNameSuffix", STRING},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"fadeAbovePrimary", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"image",
|
2022-01-16 11:09:55 +00:00
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"maxSize", NORMALIZED_PAIR},
|
2024-06-05 16:43:36 +00:00
|
|
|
{"cropSize", NORMALIZED_PAIR},
|
|
|
|
{"cropPos", NORMALIZED_PAIR},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"rotation", FLOAT},
|
|
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
2023-08-06 21:51:53 +00:00
|
|
|
{"stationary", STRING},
|
2023-12-18 00:16:32 +00:00
|
|
|
{"renderDuringTransitions", BOOLEAN},
|
2023-02-23 16:10:55 +00:00
|
|
|
{"flipHorizontal", BOOLEAN},
|
|
|
|
{"flipVertical", BOOLEAN},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"path", PATH},
|
2023-04-10 18:16:19 +00:00
|
|
|
{"gameOverridePath", PATH},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"default", PATH},
|
2022-02-13 10:45:06 +00:00
|
|
|
{"imageType", STRING},
|
2022-08-18 20:51:21 +00:00
|
|
|
{"metadataElement", BOOLEAN},
|
2022-02-14 18:32:07 +00:00
|
|
|
{"gameselector", STRING},
|
2022-12-15 17:23:48 +00:00
|
|
|
{"gameselectorEntry", UNSIGNED_INTEGER},
|
2022-02-13 14:01:55 +00:00
|
|
|
{"tile", BOOLEAN},
|
2022-09-06 19:33:50 +00:00
|
|
|
{"tileSize", NORMALIZED_PAIR},
|
2022-09-07 18:18:15 +00:00
|
|
|
{"tileHorizontalAlignment", STRING},
|
|
|
|
{"tileVerticalAlignment", STRING},
|
2022-02-13 10:45:06 +00:00
|
|
|
{"interpolation", STRING},
|
2023-08-20 17:41:07 +00:00
|
|
|
{"cornerRadius", FLOAT},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"color", COLOR},
|
|
|
|
{"colorEnd", COLOR},
|
|
|
|
{"gradientType", STRING},
|
2022-01-30 18:30:38 +00:00
|
|
|
{"scrollFadeIn", BOOLEAN},
|
2022-12-14 19:17:41 +00:00
|
|
|
{"brightness", FLOAT},
|
2022-02-12 16:38:55 +00:00
|
|
|
{"opacity", FLOAT},
|
2022-03-17 18:33:09 +00:00
|
|
|
{"saturation", FLOAT},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"visible", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"video",
|
2022-01-16 11:09:55 +00:00
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"maxSize", NORMALIZED_PAIR},
|
2024-06-05 16:46:27 +00:00
|
|
|
{"cropSize", NORMALIZED_PAIR},
|
|
|
|
{"cropPos", NORMALIZED_PAIR},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"origin", NORMALIZED_PAIR},
|
2023-09-30 10:12:32 +00:00
|
|
|
{"rotation", FLOAT},
|
|
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
2023-08-06 21:51:53 +00:00
|
|
|
{"stationary", STRING},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"path", PATH},
|
|
|
|
{"default", PATH},
|
2022-01-30 18:30:38 +00:00
|
|
|
{"defaultImage", PATH},
|
2022-02-13 10:45:06 +00:00
|
|
|
{"imageType", STRING},
|
2022-09-16 17:43:36 +00:00
|
|
|
{"metadataElement", BOOLEAN},
|
2022-02-19 20:22:46 +00:00
|
|
|
{"gameselector", STRING},
|
2022-12-15 17:23:48 +00:00
|
|
|
{"gameselectorEntry", UNSIGNED_INTEGER},
|
2023-08-13 12:48:00 +00:00
|
|
|
{"iterationCount", UNSIGNED_INTEGER},
|
|
|
|
{"onIterationsDone", STRING},
|
2022-02-19 20:45:31 +00:00
|
|
|
{"audio", BOOLEAN},
|
2022-02-13 10:45:06 +00:00
|
|
|
{"interpolation", STRING},
|
2023-08-20 17:41:07 +00:00
|
|
|
{"imageCornerRadius", FLOAT},
|
|
|
|
{"videoCornerRadius", FLOAT},
|
2022-12-12 20:51:27 +00:00
|
|
|
{"color", COLOR},
|
|
|
|
{"colorEnd", COLOR},
|
|
|
|
{"gradientType", STRING},
|
2022-02-12 16:38:55 +00:00
|
|
|
{"pillarboxes", BOOLEAN},
|
2022-10-16 14:40:52 +00:00
|
|
|
{"pillarboxThreshold", NORMALIZED_PAIR},
|
2022-02-12 16:38:55 +00:00
|
|
|
{"scanlines", BOOLEAN},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"delay", FLOAT},
|
2022-02-15 20:26:40 +00:00
|
|
|
{"fadeInTime", FLOAT},
|
2022-01-30 18:30:38 +00:00
|
|
|
{"scrollFadeIn", BOOLEAN},
|
2022-12-14 19:17:41 +00:00
|
|
|
{"brightness", FLOAT},
|
2022-02-12 16:38:55 +00:00
|
|
|
{"opacity", FLOAT},
|
2022-03-17 18:33:09 +00:00
|
|
|
{"saturation", FLOAT},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"visible", BOOLEAN},
|
2023-07-30 16:17:27 +00:00
|
|
|
{"zIndex", FLOAT}}},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"animation",
|
2022-01-16 11:09:55 +00:00
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
2023-01-17 17:35:46 +00:00
|
|
|
{"maxSize", NORMALIZED_PAIR},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"rotation", FLOAT},
|
|
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
2023-08-06 21:51:53 +00:00
|
|
|
{"stationary", STRING},
|
2022-08-18 20:51:21 +00:00
|
|
|
{"metadataElement", BOOLEAN},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"path", PATH},
|
|
|
|
{"speed", FLOAT},
|
|
|
|
{"direction", STRING},
|
2023-03-04 19:36:49 +00:00
|
|
|
{"iterationCount", UNSIGNED_INTEGER},
|
2022-03-05 19:56:47 +00:00
|
|
|
{"interpolation", STRING},
|
2023-08-20 17:41:07 +00:00
|
|
|
{"cornerRadius", FLOAT},
|
2023-03-04 09:28:43 +00:00
|
|
|
{"color", COLOR},
|
|
|
|
{"colorEnd", COLOR},
|
|
|
|
{"gradientType", STRING},
|
2022-12-14 19:17:41 +00:00
|
|
|
{"brightness", FLOAT},
|
2022-02-12 16:38:55 +00:00
|
|
|
{"opacity", FLOAT},
|
2022-03-17 18:33:09 +00:00
|
|
|
{"saturation", FLOAT},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"visible", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"badges",
|
2022-01-16 11:09:55 +00:00
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"rotation", FLOAT},
|
|
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
2023-08-06 21:51:53 +00:00
|
|
|
{"stationary", STRING},
|
2022-02-10 23:19:08 +00:00
|
|
|
{"horizontalAlignment", STRING},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"direction", STRING},
|
2022-02-13 14:01:55 +00:00
|
|
|
{"lines", UNSIGNED_INTEGER},
|
|
|
|
{"itemsPerLine", UNSIGNED_INTEGER},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"itemMargin", NORMALIZED_PAIR},
|
|
|
|
{"slots", STRING},
|
|
|
|
{"controllerPos", NORMALIZED_PAIR},
|
|
|
|
{"controllerSize", FLOAT},
|
|
|
|
{"customBadgeIcon", PATH},
|
|
|
|
{"customControllerIcon", PATH},
|
2022-04-13 14:53:28 +00:00
|
|
|
{"folderLinkPos", NORMALIZED_PAIR},
|
|
|
|
{"folderLinkSize", FLOAT},
|
|
|
|
{"customFolderLinkIcon", PATH},
|
2023-01-28 10:27:05 +00:00
|
|
|
{"badgeIconColor", COLOR},
|
|
|
|
{"badgeIconColorEnd", COLOR},
|
|
|
|
{"badgeIconGradientType", STRING},
|
|
|
|
{"controllerIconColor", COLOR},
|
|
|
|
{"controllerIconColorEnd", COLOR},
|
|
|
|
{"controllerIconGradientType", STRING},
|
|
|
|
{"folderLinkIconColor", COLOR},
|
|
|
|
{"folderLinkIconColorEnd", COLOR},
|
|
|
|
{"folderLinkIconGradientType", STRING},
|
2023-09-30 09:36:10 +00:00
|
|
|
{"interpolation", STRING},
|
2022-02-12 16:38:55 +00:00
|
|
|
{"opacity", FLOAT},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"visible", BOOLEAN},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"zIndex", FLOAT}}},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"text",
|
2022-01-16 11:09:55 +00:00
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"rotation", FLOAT},
|
|
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
2023-08-06 21:51:53 +00:00
|
|
|
{"stationary", STRING},
|
2022-01-22 20:21:13 +00:00
|
|
|
{"text", STRING},
|
2022-02-13 10:45:06 +00:00
|
|
|
{"systemdata", STRING},
|
2022-01-22 20:21:13 +00:00
|
|
|
{"metadata", STRING},
|
2023-01-10 21:20:00 +00:00
|
|
|
{"defaultValue", STRING},
|
2023-01-14 13:05:24 +00:00
|
|
|
{"systemNameSuffix", BOOLEAN},
|
|
|
|
{"letterCaseSystemNameSuffix", STRING},
|
2022-08-18 20:51:21 +00:00
|
|
|
{"metadataElement", BOOLEAN},
|
2022-02-14 18:32:07 +00:00
|
|
|
{"gameselector", STRING},
|
2022-12-15 17:23:48 +00:00
|
|
|
{"gameselectorEntry", UNSIGNED_INTEGER},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"container", BOOLEAN},
|
2023-08-07 20:58:35 +00:00
|
|
|
{"containerType", STRING},
|
2022-09-16 16:51:36 +00:00
|
|
|
{"containerVerticalSnap", BOOLEAN},
|
2022-02-10 19:02:56 +00:00
|
|
|
{"containerScrollSpeed", FLOAT},
|
|
|
|
{"containerStartDelay", FLOAT},
|
|
|
|
{"containerResetDelay", FLOAT},
|
2023-08-10 17:22:46 +00:00
|
|
|
{"containerScrollGap", FLOAT},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"fontPath", PATH},
|
|
|
|
{"fontSize", FLOAT},
|
2022-02-10 23:19:08 +00:00
|
|
|
{"horizontalAlignment", STRING},
|
|
|
|
{"verticalAlignment", STRING},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"color", COLOR},
|
|
|
|
{"backgroundColor", COLOR},
|
2024-06-02 20:30:41 +00:00
|
|
|
{"backgroundMargins", NORMALIZED_PAIR},
|
2024-06-03 15:27:00 +00:00
|
|
|
{"backgroundCornerRadius", FLOAT},
|
2022-02-09 19:44:22 +00:00
|
|
|
{"letterCase", STRING},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"lineSpacing", FLOAT},
|
2022-02-12 16:38:55 +00:00
|
|
|
{"opacity", FLOAT},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"visible", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
|
|
|
{"datetime",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"rotation", FLOAT},
|
|
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
2023-08-06 21:51:53 +00:00
|
|
|
{"stationary", STRING},
|
2022-01-22 20:21:13 +00:00
|
|
|
{"metadata", STRING},
|
2023-01-10 21:20:00 +00:00
|
|
|
{"defaultValue", STRING},
|
2022-02-20 14:49:32 +00:00
|
|
|
{"gameselector", STRING},
|
2022-12-15 17:23:48 +00:00
|
|
|
{"gameselectorEntry", UNSIGNED_INTEGER},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"fontPath", PATH},
|
|
|
|
{"fontSize", FLOAT},
|
2022-02-10 23:19:08 +00:00
|
|
|
{"horizontalAlignment", STRING},
|
|
|
|
{"verticalAlignment", STRING},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"color", COLOR},
|
|
|
|
{"backgroundColor", COLOR},
|
2022-02-09 19:44:22 +00:00
|
|
|
{"letterCase", STRING},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"lineSpacing", FLOAT},
|
|
|
|
{"format", STRING},
|
|
|
|
{"displayRelative", BOOLEAN},
|
2022-02-12 16:38:55 +00:00
|
|
|
{"opacity", FLOAT},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"visible", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"gamelistinfo",
|
2022-01-16 11:09:55 +00:00
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"rotation", FLOAT},
|
|
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
2023-08-06 21:51:53 +00:00
|
|
|
{"stationary", STRING},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"fontPath", PATH},
|
|
|
|
{"fontSize", FLOAT},
|
2022-02-10 23:19:08 +00:00
|
|
|
{"horizontalAlignment", STRING},
|
|
|
|
{"verticalAlignment", STRING},
|
2022-10-31 19:12:42 +00:00
|
|
|
{"color", COLOR},
|
|
|
|
{"backgroundColor", COLOR},
|
2022-02-12 16:38:55 +00:00
|
|
|
{"opacity", FLOAT},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"visible", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"rating",
|
2022-01-16 11:09:55 +00:00
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"rotation", FLOAT},
|
|
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
2023-08-06 21:51:53 +00:00
|
|
|
{"stationary", STRING},
|
2024-01-27 12:32:58 +00:00
|
|
|
{"hideIfZero", BOOLEAN},
|
2022-03-18 21:16:53 +00:00
|
|
|
{"gameselector", STRING},
|
2022-12-15 17:23:48 +00:00
|
|
|
{"gameselectorEntry", UNSIGNED_INTEGER},
|
2022-08-28 18:45:04 +00:00
|
|
|
{"interpolation", STRING},
|
2022-01-29 17:41:22 +00:00
|
|
|
{"color", COLOR},
|
|
|
|
{"filledPath", PATH},
|
|
|
|
{"unfilledPath", PATH},
|
2022-09-01 15:40:29 +00:00
|
|
|
{"overlay", BOOLEAN},
|
2022-02-12 16:38:55 +00:00
|
|
|
{"opacity", FLOAT},
|
|
|
|
{"visible", BOOLEAN},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"zIndex", FLOAT}}},
|
2022-02-13 19:03:34 +00:00
|
|
|
{"gameselector",
|
|
|
|
{{"selection", STRING},
|
2023-01-31 18:11:58 +00:00
|
|
|
{"gameCount", UNSIGNED_INTEGER},
|
|
|
|
{"allowDuplicates", BOOLEAN}}},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"helpsystem",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
2023-04-09 10:47:44 +00:00
|
|
|
{"posDimmed", NORMALIZED_PAIR},
|
2022-03-20 18:07:52 +00:00
|
|
|
{"origin", NORMALIZED_PAIR},
|
2023-04-09 10:47:44 +00:00
|
|
|
{"originDimmed", NORMALIZED_PAIR},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"textColor", COLOR},
|
|
|
|
{"textColorDimmed", COLOR},
|
|
|
|
{"iconColor", COLOR},
|
|
|
|
{"iconColorDimmed", COLOR},
|
|
|
|
{"fontPath", PATH},
|
|
|
|
{"fontSize", FLOAT},
|
2023-04-09 10:47:44 +00:00
|
|
|
{"fontSizeDimmed", FLOAT},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"entrySpacing", FLOAT},
|
2023-04-09 10:47:44 +00:00
|
|
|
{"entrySpacingDimmed", FLOAT},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"iconTextSpacing", FLOAT},
|
2023-04-09 10:47:44 +00:00
|
|
|
{"iconTextSpacingDimmed", FLOAT},
|
2022-02-09 19:44:22 +00:00
|
|
|
{"letterCase", STRING},
|
2022-02-13 11:18:33 +00:00
|
|
|
{"opacity", FLOAT},
|
2023-04-09 10:47:44 +00:00
|
|
|
{"opacityDimmed", FLOAT},
|
2022-01-16 11:09:55 +00:00
|
|
|
{"customButtonIcon", PATH}}},
|
2022-11-13 18:59:26 +00:00
|
|
|
{"sound",
|
2023-07-30 16:17:27 +00:00
|
|
|
{{"path", PATH}}}};
|
2022-01-29 17:41:22 +00:00
|
|
|
// clang-format on
|
2021-07-07 18:31:46 +00:00
|
|
|
|
2013-12-30 23:23:34 +00:00
|
|
|
ThemeData::ThemeData()
|
2023-07-30 16:17:27 +00:00
|
|
|
: mCustomCollection {false}
|
2013-11-12 23:28:15 +00:00
|
|
|
{
|
2023-08-14 20:40:32 +00:00
|
|
|
sCurrentTheme = sThemes.find(Settings::getInstance()->getString("Theme"));
|
2023-02-09 23:34:24 +00:00
|
|
|
sVariantDefinedTransitions = "";
|
2013-11-12 23:28:15 +00:00
|
|
|
}
|
|
|
|
|
2022-01-23 16:50:51 +00:00
|
|
|
void ThemeData::loadFile(const std::map<std::string, std::string>& sysDataMap,
|
2022-10-29 11:04:00 +00:00
|
|
|
const std::string& path,
|
2023-01-04 18:01:41 +00:00
|
|
|
const ThemeTriggers::TriggerType trigger,
|
2022-10-29 11:04:00 +00:00
|
|
|
const bool customCollection)
|
2013-11-12 23:28:15 +00:00
|
|
|
{
|
2022-10-29 11:04:00 +00:00
|
|
|
mCustomCollection = customCollection;
|
2023-01-04 18:01:41 +00:00
|
|
|
mOverrideVariant = "";
|
2022-10-29 11:04:00 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
mPaths.push_back(path);
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
ThemeException error;
|
2022-01-23 16:50:51 +00:00
|
|
|
error << "ThemeData::loadFile(): ";
|
2020-06-21 12:25:28 +00:00
|
|
|
error.setFiles(mPaths);
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
if (!Utils::FileSystem::exists(path))
|
2021-01-17 21:33:02 +00:00
|
|
|
throw error << "File does not exist";
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
mViews.clear();
|
|
|
|
mVariables.clear();
|
2017-05-14 04:07:28 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend());
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
pugi::xml_document doc;
|
2021-07-07 18:31:46 +00:00
|
|
|
#if defined(_WIN64)
|
2022-01-23 16:50:51 +00:00
|
|
|
pugi::xml_parse_result res {doc.load_file(Utils::String::stringToWideString(path).c_str())};
|
2021-07-07 18:31:46 +00:00
|
|
|
#else
|
2022-01-23 16:50:51 +00:00
|
|
|
pugi::xml_parse_result res {doc.load_file(path.c_str())};
|
2021-07-07 18:31:46 +00:00
|
|
|
#endif
|
2020-06-21 12:25:28 +00:00
|
|
|
if (!res)
|
2022-01-23 16:50:51 +00:00
|
|
|
throw error << ": XML parsing error: " << res.description();
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2022-01-23 16:50:51 +00:00
|
|
|
pugi::xml_node root {doc.child("theme")};
|
2020-06-21 12:25:28 +00:00
|
|
|
if (!root)
|
2022-01-23 16:50:51 +00:00
|
|
|
throw error << ": Missing <theme> tag";
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2024-01-21 12:27:39 +00:00
|
|
|
// Check if there's an unsupported theme version tag.
|
2023-07-30 16:17:27 +00:00
|
|
|
if (root.child("formatVersion") != nullptr)
|
2024-01-21 12:22:26 +00:00
|
|
|
throw error << ": Unsupported <formatVersion> tag found";
|
2023-07-30 16:17:27 +00:00
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
if (sCurrentTheme->second.capabilities.variants.size() > 0) {
|
|
|
|
for (auto& variant : sCurrentTheme->second.capabilities.variants)
|
2023-07-30 16:17:27 +00:00
|
|
|
mVariants.emplace_back(variant.name);
|
|
|
|
|
|
|
|
if (std::find(mVariants.cbegin(), mVariants.cend(),
|
|
|
|
Settings::getInstance()->getString("ThemeVariant")) != mVariants.cend())
|
|
|
|
mSelectedVariant = Settings::getInstance()->getString("ThemeVariant");
|
|
|
|
else
|
|
|
|
mSelectedVariant = mVariants.front();
|
|
|
|
// Special shortcut variant to apply configuration to all defined variants.
|
|
|
|
mVariants.emplace_back("all");
|
|
|
|
|
|
|
|
if (trigger != ThemeTriggers::TriggerType::NONE) {
|
2023-08-14 20:40:32 +00:00
|
|
|
auto overrides = getCurrentThemeSelectedVariantOverrides();
|
2023-07-30 16:17:27 +00:00
|
|
|
if (overrides.find(trigger) != overrides.end())
|
|
|
|
mOverrideVariant = overrides.at(trigger).first;
|
2022-01-29 17:41:22 +00:00
|
|
|
}
|
2023-07-30 16:17:27 +00:00
|
|
|
}
|
2022-01-29 17:41:22 +00:00
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
if (sCurrentTheme->second.capabilities.colorSchemes.size() > 0) {
|
|
|
|
for (auto& colorScheme : sCurrentTheme->second.capabilities.colorSchemes)
|
2023-07-30 16:17:27 +00:00
|
|
|
mColorSchemes.emplace_back(colorScheme.name);
|
2022-10-31 18:32:13 +00:00
|
|
|
|
2023-07-30 16:17:27 +00:00
|
|
|
if (std::find(mColorSchemes.cbegin(), mColorSchemes.cend(),
|
|
|
|
Settings::getInstance()->getString("ThemeColorScheme")) !=
|
|
|
|
mColorSchemes.cend())
|
|
|
|
mSelectedColorScheme = Settings::getInstance()->getString("ThemeColorScheme");
|
|
|
|
else
|
|
|
|
mSelectedColorScheme = mColorSchemes.front();
|
|
|
|
}
|
2022-10-31 18:32:13 +00:00
|
|
|
|
2023-12-20 20:58:40 +00:00
|
|
|
if (sCurrentTheme->second.capabilities.fontSizes.size() > 0) {
|
|
|
|
for (auto& fontSize : sCurrentTheme->second.capabilities.fontSizes)
|
|
|
|
mFontSizes.emplace_back(fontSize);
|
|
|
|
|
|
|
|
if (std::find(mFontSizes.cbegin(), mFontSizes.cend(),
|
|
|
|
Settings::getInstance()->getString("ThemeFontSize")) != mFontSizes.cend())
|
|
|
|
mSelectedFontSize = Settings::getInstance()->getString("ThemeFontSize");
|
|
|
|
else
|
|
|
|
mSelectedFontSize = mFontSizes.front();
|
|
|
|
}
|
|
|
|
|
2023-07-30 16:17:27 +00:00
|
|
|
sAspectRatioMatch = false;
|
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
if (sCurrentTheme->second.capabilities.aspectRatios.size() > 0) {
|
|
|
|
if (std::find(sCurrentTheme->second.capabilities.aspectRatios.cbegin(),
|
|
|
|
sCurrentTheme->second.capabilities.aspectRatios.cend(),
|
2023-07-30 16:17:27 +00:00
|
|
|
Settings::getInstance()->getString("ThemeAspectRatio")) !=
|
2023-08-14 20:40:32 +00:00
|
|
|
sCurrentTheme->second.capabilities.aspectRatios.cend())
|
2023-07-30 16:17:27 +00:00
|
|
|
sSelectedAspectRatio = Settings::getInstance()->getString("ThemeAspectRatio");
|
|
|
|
else
|
2023-08-14 20:40:32 +00:00
|
|
|
sSelectedAspectRatio = sCurrentTheme->second.capabilities.aspectRatios.front();
|
2023-07-30 16:17:27 +00:00
|
|
|
|
|
|
|
if (sSelectedAspectRatio == "automatic") {
|
2023-08-14 20:40:32 +00:00
|
|
|
// Auto-detect the closest aspect ratio based on what's available in the theme config.
|
2023-07-30 16:17:27 +00:00
|
|
|
sSelectedAspectRatio = "16:9";
|
|
|
|
const float screenAspectRatio {Renderer::getScreenAspectRatio()};
|
|
|
|
float diff {std::fabs(sAspectRatioMap["16:9"] - screenAspectRatio)};
|
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
for (auto& aspectRatio : sCurrentTheme->second.capabilities.aspectRatios) {
|
2023-07-30 16:17:27 +00:00
|
|
|
if (aspectRatio == "automatic")
|
|
|
|
continue;
|
2023-02-09 23:34:24 +00:00
|
|
|
|
2023-07-30 16:17:27 +00:00
|
|
|
if (sAspectRatioMap.find(aspectRatio) != sAspectRatioMap.end()) {
|
|
|
|
const float newDiff {
|
|
|
|
std::fabs(sAspectRatioMap[aspectRatio] - screenAspectRatio)};
|
|
|
|
if (newDiff < 0.01f)
|
|
|
|
sAspectRatioMatch = true;
|
|
|
|
if (newDiff < diff) {
|
|
|
|
diff = newDiff;
|
|
|
|
sSelectedAspectRatio = aspectRatio;
|
2022-11-01 16:08:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-01-29 17:41:22 +00:00
|
|
|
}
|
|
|
|
}
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
parseVariables(root);
|
2023-07-30 16:17:27 +00:00
|
|
|
parseColorSchemes(root);
|
2023-12-20 20:58:40 +00:00
|
|
|
parseFontSizes(root);
|
2020-06-21 12:25:28 +00:00
|
|
|
parseIncludes(root);
|
|
|
|
parseViews(root);
|
2023-07-30 16:17:27 +00:00
|
|
|
if (root.child("feature") != nullptr)
|
2024-01-21 12:27:39 +00:00
|
|
|
throw error << ": Unsupported <feature> tag found";
|
2023-07-30 16:17:27 +00:00
|
|
|
parseVariants(root);
|
|
|
|
parseAspectRatios(root);
|
2013-11-12 23:28:15 +00:00
|
|
|
}
|
|
|
|
|
2022-01-23 19:03:50 +00:00
|
|
|
bool ThemeData::hasView(const std::string& view)
|
|
|
|
{
|
|
|
|
auto viewIt = mViews.find(view);
|
|
|
|
return (viewIt != mViews.cend());
|
|
|
|
}
|
|
|
|
|
|
|
|
const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view,
|
|
|
|
const std::string& element,
|
|
|
|
const std::string& expectedType) const
|
|
|
|
{
|
|
|
|
auto viewIt = mViews.find(view);
|
|
|
|
if (viewIt == mViews.cend())
|
|
|
|
return nullptr; // Not found.
|
|
|
|
|
|
|
|
auto elemIt = viewIt->second.elements.find(element);
|
|
|
|
if (elemIt == viewIt->second.elements.cend())
|
|
|
|
return nullptr;
|
|
|
|
|
2022-01-29 17:41:22 +00:00
|
|
|
// If expectedType is an empty string, then skip type checking.
|
2022-01-23 19:03:50 +00:00
|
|
|
if (elemIt->second.type != expectedType && !expectedType.empty()) {
|
2022-09-15 15:29:34 +00:00
|
|
|
LOG(LogWarning) << "ThemeData::getElement(): Requested element \"" << view << "." << element
|
|
|
|
<< "\" has the wrong type, expected \"" << expectedType << "\", got \""
|
|
|
|
<< elemIt->second.type << "\"";
|
2022-01-23 19:03:50 +00:00
|
|
|
return nullptr;
|
|
|
|
}
|
|
|
|
|
|
|
|
return &elemIt->second;
|
|
|
|
}
|
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
void ThemeData::populateThemes()
|
2022-01-23 19:03:50 +00:00
|
|
|
{
|
2023-08-14 20:40:32 +00:00
|
|
|
sThemes.clear();
|
|
|
|
LOG(LogInfo) << "Checking for available themes...";
|
2022-01-23 19:03:50 +00:00
|
|
|
|
2023-04-06 09:40:32 +00:00
|
|
|
// Check for themes first under the user theme directory (which is in the ES-DE home directory
|
|
|
|
// by default), then under the data installation directory (Unix only) and last under the ES-DE
|
|
|
|
// binary directory.
|
2023-12-15 17:33:02 +00:00
|
|
|
#if defined(__ANDROID__)
|
2023-12-19 16:35:58 +00:00
|
|
|
const std::string userThemeDirectory {Utils::FileSystem::getInternalAppDataDirectory() +
|
|
|
|
"/themes"};
|
2023-12-15 17:33:02 +00:00
|
|
|
#else
|
2023-12-19 16:35:58 +00:00
|
|
|
const std::string defaultUserThemeDir {Utils::FileSystem::getAppDataDirectory() + "/themes"};
|
|
|
|
const std::string userThemeDirSetting {Utils::FileSystem::expandHomePath(
|
2023-04-06 09:40:32 +00:00
|
|
|
Settings::getInstance()->getString("UserThemeDirectory"))};
|
2023-12-19 16:35:58 +00:00
|
|
|
std::string userThemeDirectory;
|
2023-04-06 09:40:32 +00:00
|
|
|
|
2023-12-13 23:27:44 +00:00
|
|
|
if (userThemeDirSetting.empty()) {
|
2023-04-06 09:40:32 +00:00
|
|
|
userThemeDirectory = defaultUserThemeDir;
|
|
|
|
}
|
2023-12-19 16:35:58 +00:00
|
|
|
else if (Utils::FileSystem::isDirectory(userThemeDirSetting) ||
|
|
|
|
Utils::FileSystem::isSymlink(userThemeDirSetting)) {
|
2023-04-06 09:40:32 +00:00
|
|
|
userThemeDirectory = userThemeDirSetting;
|
2023-12-19 16:35:58 +00:00
|
|
|
#if defined(_WIN64)
|
|
|
|
LOG(LogInfo) << "Setting user theme directory to \""
|
|
|
|
<< Utils::String::replace(userThemeDirectory, "/", "\\") << "\"";
|
|
|
|
#else
|
|
|
|
LOG(LogInfo) << "Setting user theme directory to \"" << userThemeDirectory << "\"";
|
|
|
|
#endif
|
2023-04-06 09:40:32 +00:00
|
|
|
}
|
|
|
|
else {
|
2023-12-19 16:35:58 +00:00
|
|
|
LOG(LogWarning) << "Requested user theme directory \"" << userThemeDirSetting
|
2023-04-06 09:40:32 +00:00
|
|
|
<< "\" does not exist or is not a directory, reverting to \""
|
2023-12-19 16:35:58 +00:00
|
|
|
<< defaultUserThemeDir << "\"";
|
2023-04-06 09:40:32 +00:00
|
|
|
userThemeDirectory = defaultUserThemeDir;
|
|
|
|
}
|
2023-12-15 17:33:02 +00:00
|
|
|
#endif
|
2022-01-23 19:03:50 +00:00
|
|
|
|
2023-12-12 22:21:27 +00:00
|
|
|
#if defined(__ANDROID__)
|
2023-12-23 22:41:51 +00:00
|
|
|
const std::vector<std::string> themePaths {Utils::FileSystem::getProgramDataPath() + "/themes",
|
|
|
|
Utils::FileSystem::getAppDataDirectory() + "/themes",
|
|
|
|
userThemeDirectory};
|
2023-12-12 22:21:27 +00:00
|
|
|
#elif defined(__APPLE__)
|
2023-12-19 16:35:58 +00:00
|
|
|
const std::vector<std::string> themePaths {
|
|
|
|
Utils::FileSystem::getExePath() + "/themes",
|
|
|
|
Utils::FileSystem::getExePath() + "/../Resources/themes", userThemeDirectory};
|
2023-12-12 22:21:27 +00:00
|
|
|
#elif defined(_WIN64) || defined(APPIMAGE_BUILD)
|
2023-12-19 16:35:58 +00:00
|
|
|
const std::vector<std::string> themePaths {Utils::FileSystem::getExePath() + "/themes",
|
|
|
|
userThemeDirectory};
|
2023-12-12 22:21:27 +00:00
|
|
|
#else
|
2023-12-19 16:35:58 +00:00
|
|
|
const std::vector<std::string> themePaths {Utils::FileSystem::getExePath() + "/themes",
|
|
|
|
Utils::FileSystem::getProgramDataPath() + "/themes",
|
|
|
|
userThemeDirectory};
|
2022-01-23 19:03:50 +00:00
|
|
|
#endif
|
|
|
|
|
2023-12-12 22:21:27 +00:00
|
|
|
for (auto path : themePaths) {
|
2023-12-19 16:35:58 +00:00
|
|
|
if (!Utils::FileSystem::isDirectory(path))
|
2022-01-23 19:03:50 +00:00
|
|
|
continue;
|
|
|
|
|
2023-12-19 16:35:58 +00:00
|
|
|
Utils::FileSystem::StringList dirContent {Utils::FileSystem::getDirContent(path)};
|
2022-01-23 19:03:50 +00:00
|
|
|
|
2023-12-19 16:35:58 +00:00
|
|
|
for (Utils::FileSystem::StringList::const_iterator it = dirContent.cbegin();
|
2022-01-23 19:03:50 +00:00
|
|
|
it != dirContent.cend(); ++it) {
|
2023-12-19 16:35:58 +00:00
|
|
|
if (Utils::FileSystem::isDirectory(*it)) {
|
|
|
|
const std::string themeDirName {Utils::FileSystem::getFileName(*it)};
|
2023-03-21 16:22:17 +00:00
|
|
|
if (themeDirName == "themes-list" ||
|
|
|
|
(themeDirName.length() >= 8 &&
|
|
|
|
Utils::String::toLower(themeDirName.substr(themeDirName.length() - 8, 8)) ==
|
|
|
|
"disabled"))
|
|
|
|
continue;
|
2023-12-19 16:35:58 +00:00
|
|
|
#if defined(_WIN64)
|
|
|
|
LOG(LogDebug) << "Loading theme capabilities for \""
|
|
|
|
<< Utils::String::replace(*it, "/", "\\") << "\"...";
|
|
|
|
#else
|
|
|
|
LOG(LogDebug) << "Loading theme capabilities for \"" << *it << "\"...";
|
|
|
|
#endif
|
|
|
|
ThemeCapability capabilities {parseThemeCapabilities((*it))};
|
2022-01-29 17:41:22 +00:00
|
|
|
|
2023-07-30 16:17:27 +00:00
|
|
|
if (!capabilities.validTheme)
|
|
|
|
continue;
|
|
|
|
|
2023-01-20 17:37:32 +00:00
|
|
|
std::string themeName;
|
2023-12-12 22:21:27 +00:00
|
|
|
if (capabilities.themeName != "")
|
2023-08-14 20:40:32 +00:00
|
|
|
themeName.append(" (\"").append(capabilities.themeName).append("\")");
|
2023-01-20 17:37:32 +00:00
|
|
|
|
2023-12-19 16:35:58 +00:00
|
|
|
#if defined(_WIN64)
|
|
|
|
LOG(LogInfo) << "Added theme \"" << Utils::String::replace(*it, "/", "\\") << "\""
|
|
|
|
<< themeName;
|
|
|
|
#else
|
|
|
|
LOG(LogInfo) << "Added theme \"" << *it << "\"" << themeName;
|
|
|
|
#endif
|
2023-07-30 16:17:27 +00:00
|
|
|
int aspectRatios {0};
|
|
|
|
if (capabilities.aspectRatios.size() > 0)
|
|
|
|
aspectRatios = static_cast<int>(capabilities.aspectRatios.size()) - 1;
|
2023-08-14 20:40:32 +00:00
|
|
|
LOG(LogDebug) << "Theme includes support for " << capabilities.variants.size()
|
2023-07-30 16:17:27 +00:00
|
|
|
<< " variant" << (capabilities.variants.size() != 1 ? "s" : "")
|
|
|
|
<< ", " << capabilities.colorSchemes.size() << " color scheme"
|
|
|
|
<< (capabilities.colorSchemes.size() != 1 ? "s" : "") << ", "
|
2023-12-20 20:58:40 +00:00
|
|
|
<< capabilities.fontSizes.size() << " font size"
|
|
|
|
<< (capabilities.fontSizes.size() != 1 ? "s" : "") << ", "
|
2023-07-30 16:17:27 +00:00
|
|
|
<< aspectRatios << " aspect ratio" << (aspectRatios != 1 ? "s" : "")
|
|
|
|
<< " and " << capabilities.transitions.size() << " transition"
|
|
|
|
<< (capabilities.transitions.size() != 1 ? "s" : "");
|
|
|
|
|
2023-12-19 16:35:58 +00:00
|
|
|
Theme theme {*it, capabilities};
|
2023-08-14 20:40:32 +00:00
|
|
|
sThemes[theme.getName()] = theme;
|
2022-01-23 19:03:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-06-11 16:35:44 +00:00
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
if (sThemes.empty()) {
|
|
|
|
LOG(LogWarning) << "Couldn't find any themes, creating dummy entry";
|
|
|
|
Theme theme {"no-themes", ThemeCapability()};
|
|
|
|
sThemes[theme.getName()] = theme;
|
|
|
|
sCurrentTheme = sThemes.begin();
|
2022-06-11 16:35:44 +00:00
|
|
|
}
|
2022-01-23 19:03:50 +00:00
|
|
|
}
|
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
const std::string ThemeData::getSystemThemeFile(const std::string& system)
|
2022-01-23 19:03:50 +00:00
|
|
|
{
|
2023-08-14 20:40:32 +00:00
|
|
|
if (sThemes.empty())
|
|
|
|
getThemes();
|
2022-01-29 17:41:22 +00:00
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
if (sThemes.empty())
|
2022-01-23 19:03:50 +00:00
|
|
|
return "";
|
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
std::map<std::string, Theme, StringComparator>::const_iterator theme {
|
|
|
|
sThemes.find(Settings::getInstance()->getString("Theme"))};
|
|
|
|
if (theme == sThemes.cend()) {
|
2024-01-28 17:50:26 +00:00
|
|
|
// Currently configured theme is missing, attempt to load the default theme linear-es-de
|
2023-08-14 20:40:32 +00:00
|
|
|
// instead, and if that's also missing then pick the first available one.
|
2022-01-23 19:03:50 +00:00
|
|
|
bool defaultSetFound {true};
|
|
|
|
|
2024-01-28 17:50:26 +00:00
|
|
|
theme = sThemes.find("linear-es-de");
|
2022-01-23 19:03:50 +00:00
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
if (theme == sThemes.cend()) {
|
|
|
|
theme = sThemes.cbegin();
|
2022-01-23 19:03:50 +00:00
|
|
|
defaultSetFound = false;
|
|
|
|
}
|
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
LOG(LogWarning) << "Configured theme \"" << Settings::getInstance()->getString("Theme")
|
2022-01-23 19:03:50 +00:00
|
|
|
<< "\" does not exist, loading" << (defaultSetFound ? " default " : " ")
|
2023-08-14 20:40:32 +00:00
|
|
|
<< "theme \"" << theme->first << "\" instead";
|
2022-01-23 19:03:50 +00:00
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
Settings::getInstance()->setString("Theme", theme->first);
|
|
|
|
sCurrentTheme = sThemes.find(Settings::getInstance()->getString("Theme"));
|
2022-01-23 19:03:50 +00:00
|
|
|
}
|
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
return theme->second.getThemePath(system);
|
2022-01-23 19:03:50 +00:00
|
|
|
}
|
|
|
|
|
2023-12-20 20:58:40 +00:00
|
|
|
const std::string ThemeData::getFontSizeLabel(const std::string& fontSize)
|
|
|
|
{
|
|
|
|
auto it = std::find_if(sSupportedFontSizes.cbegin(), sSupportedFontSizes.cend(),
|
|
|
|
[&fontSize](const std::pair<std::string, std::string>& entry) {
|
|
|
|
return entry.first == fontSize;
|
|
|
|
});
|
|
|
|
if (it != sSupportedFontSizes.cend())
|
|
|
|
return it->second;
|
|
|
|
else
|
|
|
|
return "invalid font size";
|
|
|
|
}
|
|
|
|
|
2022-01-31 22:22:42 +00:00
|
|
|
const std::string ThemeData::getAspectRatioLabel(const std::string& aspectRatio)
|
|
|
|
{
|
|
|
|
auto it = std::find_if(sSupportedAspectRatios.cbegin(), sSupportedAspectRatios.cend(),
|
|
|
|
[&aspectRatio](const std::pair<std::string, std::string>& entry) {
|
|
|
|
return entry.first == aspectRatio;
|
|
|
|
});
|
|
|
|
if (it != sSupportedAspectRatios.cend())
|
|
|
|
return it->second;
|
|
|
|
else
|
|
|
|
return "invalid ratio";
|
|
|
|
}
|
|
|
|
|
2023-01-08 16:00:36 +00:00
|
|
|
void ThemeData::setThemeTransitions()
|
|
|
|
{
|
|
|
|
auto setTransitionsFunc = [](int transitionAnim) {
|
|
|
|
Settings::getInstance()->setInt("TransitionsSystemToSystem", transitionAnim);
|
|
|
|
Settings::getInstance()->setInt("TransitionsSystemToGamelist", transitionAnim);
|
|
|
|
Settings::getInstance()->setInt("TransitionsGamelistToGamelist", transitionAnim);
|
|
|
|
Settings::getInstance()->setInt("TransitionsGamelistToSystem", transitionAnim);
|
|
|
|
Settings::getInstance()->setInt("TransitionsStartupToSystem", transitionAnim);
|
|
|
|
Settings::getInstance()->setInt("TransitionsStartupToGamelist", transitionAnim);
|
|
|
|
};
|
|
|
|
|
|
|
|
int transitionAnim {ViewTransitionAnimation::INSTANT};
|
|
|
|
setTransitionsFunc(transitionAnim);
|
|
|
|
|
2023-07-30 16:17:27 +00:00
|
|
|
const std::string& transitionsSetting {Settings::getInstance()->getString("ThemeTransitions")};
|
|
|
|
std::string profile;
|
|
|
|
size_t profileEntry {0};
|
|
|
|
|
|
|
|
if (transitionsSetting == "automatic") {
|
|
|
|
if (sVariantDefinedTransitions != "")
|
|
|
|
profile = sVariantDefinedTransitions;
|
2023-08-14 20:40:32 +00:00
|
|
|
else if (!sCurrentTheme->second.capabilities.transitions.empty())
|
|
|
|
profile = sCurrentTheme->second.capabilities.transitions.front().name;
|
2023-01-08 16:00:36 +00:00
|
|
|
}
|
|
|
|
else {
|
2023-07-30 16:17:27 +00:00
|
|
|
profile = transitionsSetting;
|
|
|
|
}
|
2023-01-08 16:00:36 +00:00
|
|
|
|
2023-07-30 16:17:27 +00:00
|
|
|
auto it = std::find_if(
|
2023-08-14 20:40:32 +00:00
|
|
|
sCurrentTheme->second.capabilities.transitions.cbegin(),
|
|
|
|
sCurrentTheme->second.capabilities.transitions.cend(),
|
2023-07-30 16:17:27 +00:00
|
|
|
[&profile](const ThemeTransitions transitions) { return transitions.name == profile; });
|
2023-08-14 20:40:32 +00:00
|
|
|
if (it != sCurrentTheme->second.capabilities.transitions.cend())
|
2023-07-30 16:17:27 +00:00
|
|
|
profileEntry = static_cast<size_t>(
|
2023-08-14 20:40:32 +00:00
|
|
|
std::distance(sCurrentTheme->second.capabilities.transitions.cbegin(), it) + 1);
|
2023-07-30 16:17:27 +00:00
|
|
|
|
|
|
|
if (profileEntry != 0 &&
|
2023-08-14 20:40:32 +00:00
|
|
|
sCurrentTheme->second.capabilities.transitions.size() > profileEntry - 1) {
|
2023-07-30 16:17:27 +00:00
|
|
|
auto transitionMap =
|
2023-08-14 20:40:32 +00:00
|
|
|
sCurrentTheme->second.capabilities.transitions[profileEntry - 1].animations;
|
2023-07-30 16:17:27 +00:00
|
|
|
if (transitionMap.find(ViewTransition::SYSTEM_TO_SYSTEM) != transitionMap.end())
|
|
|
|
Settings::getInstance()->setInt("TransitionsSystemToSystem",
|
|
|
|
transitionMap[ViewTransition::SYSTEM_TO_SYSTEM]);
|
|
|
|
if (transitionMap.find(ViewTransition::SYSTEM_TO_GAMELIST) != transitionMap.end())
|
|
|
|
Settings::getInstance()->setInt("TransitionsSystemToGamelist",
|
|
|
|
transitionMap[ViewTransition::SYSTEM_TO_GAMELIST]);
|
|
|
|
if (transitionMap.find(ViewTransition::GAMELIST_TO_GAMELIST) != transitionMap.end())
|
|
|
|
Settings::getInstance()->setInt("TransitionsGamelistToGamelist",
|
|
|
|
transitionMap[ViewTransition::GAMELIST_TO_GAMELIST]);
|
|
|
|
if (transitionMap.find(ViewTransition::GAMELIST_TO_SYSTEM) != transitionMap.end())
|
|
|
|
Settings::getInstance()->setInt("TransitionsGamelistToSystem",
|
|
|
|
transitionMap[ViewTransition::GAMELIST_TO_SYSTEM]);
|
|
|
|
if (transitionMap.find(ViewTransition::STARTUP_TO_SYSTEM) != transitionMap.end())
|
|
|
|
Settings::getInstance()->setInt("TransitionsStartupToSystem",
|
|
|
|
transitionMap[ViewTransition::STARTUP_TO_SYSTEM]);
|
|
|
|
if (transitionMap.find(ViewTransition::STARTUP_TO_GAMELIST) != transitionMap.end())
|
|
|
|
Settings::getInstance()->setInt("TransitionsStartupToGamelist",
|
|
|
|
transitionMap[ViewTransition::STARTUP_TO_GAMELIST]);
|
|
|
|
}
|
|
|
|
else if (transitionsSetting == "builtin-slide" || transitionsSetting == "builtin-fade") {
|
2023-08-14 20:40:32 +00:00
|
|
|
if (std::find(sCurrentTheme->second.capabilities.suppressedTransitionProfiles.cbegin(),
|
|
|
|
sCurrentTheme->second.capabilities.suppressedTransitionProfiles.cend(),
|
2023-07-30 16:17:27 +00:00
|
|
|
transitionsSetting) ==
|
2023-08-14 20:40:32 +00:00
|
|
|
sCurrentTheme->second.capabilities.suppressedTransitionProfiles.cend()) {
|
2023-07-30 16:17:27 +00:00
|
|
|
if (transitionsSetting == "builtin-slide") {
|
|
|
|
transitionAnim = static_cast<int>(ViewTransitionAnimation::SLIDE);
|
|
|
|
}
|
|
|
|
else if (transitionsSetting == "builtin-fade") {
|
|
|
|
transitionAnim = static_cast<int>(ViewTransitionAnimation::FADE);
|
2023-01-08 16:00:36 +00:00
|
|
|
}
|
2023-07-30 16:17:27 +00:00
|
|
|
setTransitionsFunc(transitionAnim);
|
2023-01-08 16:00:36 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-04 18:01:41 +00:00
|
|
|
const std::map<ThemeTriggers::TriggerType, std::pair<std::string, std::vector<std::string>>>
|
2023-08-14 20:40:32 +00:00
|
|
|
ThemeData::getCurrentThemeSelectedVariantOverrides()
|
2023-01-04 18:01:41 +00:00
|
|
|
{
|
|
|
|
const auto variantIter = std::find_if(
|
2023-08-14 20:40:32 +00:00
|
|
|
sCurrentTheme->second.capabilities.variants.cbegin(),
|
|
|
|
sCurrentTheme->second.capabilities.variants.cend(),
|
2023-01-04 18:01:41 +00:00
|
|
|
[this](ThemeVariant currVariant) { return currVariant.name == mSelectedVariant; });
|
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
if (variantIter != sCurrentTheme->second.capabilities.variants.cend() &&
|
2023-01-04 18:01:41 +00:00
|
|
|
!(*variantIter).overrides.empty())
|
|
|
|
return (*variantIter).overrides;
|
|
|
|
else
|
|
|
|
return ThemeVariant().overrides;
|
|
|
|
}
|
|
|
|
|
2023-02-09 23:34:24 +00:00
|
|
|
const void ThemeData::themeLoadedLogOutput()
|
|
|
|
{
|
2023-08-14 20:40:32 +00:00
|
|
|
LOG(LogInfo) << "Finished loading theme \"" << sCurrentTheme->first << "\"";
|
2023-07-30 16:17:27 +00:00
|
|
|
if (sSelectedAspectRatio != "") {
|
|
|
|
const bool autoDetect {Settings::getInstance()->getString("ThemeAspectRatio") ==
|
|
|
|
"automatic"};
|
|
|
|
const std::string match {sAspectRatioMatch ? "exact match " : "closest match "};
|
|
|
|
|
|
|
|
LOG(LogInfo) << "Aspect ratio " << (autoDetect ? "automatically " : "manually ")
|
|
|
|
<< "set to " << (autoDetect ? match : "") << "\""
|
|
|
|
<< Utils::String::replace(sSelectedAspectRatio, "_", " ") << "\"";
|
2023-02-09 23:34:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-23 19:03:50 +00:00
|
|
|
unsigned int ThemeData::getHexColor(const std::string& str)
|
|
|
|
{
|
|
|
|
ThemeException error;
|
|
|
|
|
|
|
|
if (str == "")
|
|
|
|
throw error << "Empty color property";
|
|
|
|
|
2023-01-04 18:01:41 +00:00
|
|
|
const size_t length {str.size()};
|
|
|
|
if (length != 6 && length != 8)
|
2022-01-23 19:03:50 +00:00
|
|
|
throw error << "Invalid color property \"" << str
|
|
|
|
<< "\" (must be 6 or 8 characters in length)";
|
|
|
|
|
2023-01-04 18:01:41 +00:00
|
|
|
unsigned int value;
|
2022-01-23 19:03:50 +00:00
|
|
|
std::stringstream ss;
|
|
|
|
ss << str;
|
2023-01-04 18:01:41 +00:00
|
|
|
ss >> std::hex >> value;
|
2022-01-23 19:03:50 +00:00
|
|
|
|
2023-01-04 18:01:41 +00:00
|
|
|
if (length == 6)
|
|
|
|
value = (value << 8) | 0xFF;
|
2022-01-23 19:03:50 +00:00
|
|
|
|
2023-01-04 18:01:41 +00:00
|
|
|
return value;
|
2022-01-23 19:03:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
std::string ThemeData::resolvePlaceholders(const std::string& in)
|
|
|
|
{
|
|
|
|
if (in.empty())
|
|
|
|
return in;
|
|
|
|
|
|
|
|
const size_t variableBegin {in.find("${")};
|
|
|
|
const size_t variableEnd {in.find("}", variableBegin)};
|
|
|
|
|
|
|
|
if ((variableBegin == std::string::npos) || (variableEnd == std::string::npos))
|
|
|
|
return in;
|
|
|
|
|
|
|
|
std::string prefix {in.substr(0, variableBegin)};
|
|
|
|
std::string replace {in.substr(variableBegin + 2, variableEnd - (variableBegin + 2))};
|
|
|
|
std::string suffix {resolvePlaceholders(in.substr(variableEnd + 1).c_str())};
|
|
|
|
|
|
|
|
return prefix + mVariables[replace] + suffix;
|
|
|
|
}
|
|
|
|
|
2022-01-29 17:41:22 +00:00
|
|
|
ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string& path)
|
|
|
|
{
|
|
|
|
ThemeCapability capabilities;
|
2022-01-31 22:22:42 +00:00
|
|
|
std::vector<std::string> aspectRatiosTemp;
|
2023-12-20 20:58:40 +00:00
|
|
|
std::vector<std::string> fontSizesTemp;
|
2023-01-04 18:01:41 +00:00
|
|
|
bool hasTriggers {false};
|
2022-01-29 17:41:22 +00:00
|
|
|
|
2023-01-04 18:01:41 +00:00
|
|
|
const std::string capFile {path + "/capabilities.xml"};
|
2022-01-29 17:41:22 +00:00
|
|
|
|
|
|
|
if (Utils::FileSystem::isRegularFile(capFile) || Utils::FileSystem::isSymlink(capFile)) {
|
2023-07-30 16:17:27 +00:00
|
|
|
capabilities.validTheme = true;
|
2022-01-29 17:41:22 +00:00
|
|
|
|
|
|
|
pugi::xml_document doc;
|
|
|
|
#if defined(_WIN64)
|
2023-01-04 18:01:41 +00:00
|
|
|
const pugi::xml_parse_result& res {
|
2022-10-31 18:32:13 +00:00
|
|
|
doc.load_file(Utils::String::stringToWideString(capFile).c_str())};
|
2022-01-29 17:41:22 +00:00
|
|
|
#else
|
2023-01-04 18:01:41 +00:00
|
|
|
const pugi::xml_parse_result& res {doc.load_file(capFile.c_str())};
|
2022-01-29 17:41:22 +00:00
|
|
|
#endif
|
|
|
|
if (res.status == pugi::status_no_document_element) {
|
|
|
|
LOG(LogDebug) << "Found a capabilities.xml file with no configuration";
|
|
|
|
}
|
|
|
|
else if (!res) {
|
|
|
|
LOG(LogError) << "Couldn't parse capabilities.xml: " << res.description();
|
|
|
|
return capabilities;
|
|
|
|
}
|
2023-01-04 18:01:41 +00:00
|
|
|
const pugi::xml_node& themeCapabilities {doc.child("themeCapabilities")};
|
2022-01-29 17:41:22 +00:00
|
|
|
if (!themeCapabilities) {
|
|
|
|
LOG(LogError) << "Missing <themeCapabilities> tag in capabilities.xml";
|
|
|
|
return capabilities;
|
|
|
|
}
|
|
|
|
|
2023-01-20 17:37:32 +00:00
|
|
|
const pugi::xml_node& themeName {themeCapabilities.child("themeName")};
|
|
|
|
if (themeName != nullptr)
|
|
|
|
capabilities.themeName = themeName.text().get();
|
|
|
|
|
2022-10-31 18:32:13 +00:00
|
|
|
for (pugi::xml_node aspectRatio {themeCapabilities.child("aspectRatio")}; aspectRatio;
|
2022-01-29 17:41:22 +00:00
|
|
|
aspectRatio = aspectRatio.next_sibling("aspectRatio")) {
|
2023-01-04 18:01:41 +00:00
|
|
|
const std::string& value {aspectRatio.text().get()};
|
2022-01-31 22:22:42 +00:00
|
|
|
if (std::find_if(sSupportedAspectRatios.cbegin(), sSupportedAspectRatios.cend(),
|
|
|
|
[&value](const std::pair<std::string, std::string>& entry) {
|
|
|
|
return entry.first == value;
|
|
|
|
}) == sSupportedAspectRatios.cend()) {
|
2022-01-29 17:41:22 +00:00
|
|
|
LOG(LogWarning) << "Declared aspect ratio \"" << value
|
|
|
|
<< "\" is not supported, ignoring entry in \"" << capFile << "\"";
|
|
|
|
}
|
|
|
|
else {
|
2022-01-31 22:22:42 +00:00
|
|
|
if (std::find(aspectRatiosTemp.cbegin(), aspectRatiosTemp.cend(), value) !=
|
|
|
|
aspectRatiosTemp.cend()) {
|
2022-01-29 17:41:22 +00:00
|
|
|
LOG(LogWarning)
|
|
|
|
<< "Aspect ratio \"" << value
|
|
|
|
<< "\" is declared multiple times, ignoring entry in \"" << capFile << "\"";
|
|
|
|
}
|
|
|
|
else {
|
2022-01-31 22:22:42 +00:00
|
|
|
aspectRatiosTemp.emplace_back(value);
|
2022-01-29 17:41:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2022-01-31 22:22:42 +00:00
|
|
|
|
2023-12-20 20:58:40 +00:00
|
|
|
for (pugi::xml_node fontSize {themeCapabilities.child("fontSize")}; fontSize;
|
|
|
|
fontSize = fontSize.next_sibling("fontSize")) {
|
|
|
|
const std::string& value {fontSize.text().get()};
|
|
|
|
if (std::find_if(sSupportedFontSizes.cbegin(), sSupportedFontSizes.cend(),
|
|
|
|
[&value](const std::pair<std::string, std::string>& entry) {
|
|
|
|
return entry.first == value;
|
|
|
|
}) == sSupportedFontSizes.cend()) {
|
|
|
|
LOG(LogWarning) << "Declared font size \"" << value
|
|
|
|
<< "\" is not supported, ignoring entry in \"" << capFile << "\"";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (std::find(fontSizesTemp.cbegin(), fontSizesTemp.cend(), value) !=
|
|
|
|
fontSizesTemp.cend()) {
|
|
|
|
LOG(LogWarning)
|
|
|
|
<< "Font size \"" << value
|
|
|
|
<< "\" is declared multiple times, ignoring entry in \"" << capFile << "\"";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
fontSizesTemp.emplace_back(value);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-31 18:32:13 +00:00
|
|
|
for (pugi::xml_node variant {themeCapabilities.child("variant")}; variant;
|
2022-01-29 17:41:22 +00:00
|
|
|
variant = variant.next_sibling("variant")) {
|
|
|
|
ThemeVariant readVariant;
|
2023-01-04 18:01:41 +00:00
|
|
|
const std::string& name {variant.attribute("name").as_string()};
|
2022-01-29 17:41:22 +00:00
|
|
|
if (name.empty()) {
|
|
|
|
LOG(LogWarning)
|
2022-01-30 20:35:39 +00:00
|
|
|
<< "Found <variant> tag without name attribute, ignoring entry in \"" << capFile
|
2022-01-29 17:41:22 +00:00
|
|
|
<< "\"";
|
|
|
|
}
|
2022-10-28 19:08:44 +00:00
|
|
|
else if (name == "all") {
|
|
|
|
LOG(LogWarning)
|
|
|
|
<< "Found <variant> tag using reserved name \"all\", ignoring entry in \""
|
|
|
|
<< capFile << "\"";
|
|
|
|
}
|
2022-01-29 17:41:22 +00:00
|
|
|
else {
|
|
|
|
readVariant.name = name;
|
|
|
|
}
|
|
|
|
|
2023-01-04 18:01:41 +00:00
|
|
|
const pugi::xml_node& labelTag {variant.child("label")};
|
2022-01-29 17:41:22 +00:00
|
|
|
if (labelTag == nullptr) {
|
|
|
|
LOG(LogDebug)
|
|
|
|
<< "No variant <label> tag found, setting label value to the variant name \""
|
|
|
|
<< name << "\" for \"" << capFile << "\"";
|
|
|
|
readVariant.label = name;
|
|
|
|
}
|
|
|
|
else {
|
2023-01-04 18:01:41 +00:00
|
|
|
const std::string& labelValue {labelTag.text().as_string()};
|
2022-01-29 17:41:22 +00:00
|
|
|
if (labelValue == "") {
|
|
|
|
LOG(LogWarning) << "No variant <label> value defined, setting value to "
|
|
|
|
"the variant name \""
|
|
|
|
<< name << "\" for \"" << capFile << "\"";
|
|
|
|
readVariant.label = name;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
readVariant.label = labelValue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-04 18:01:41 +00:00
|
|
|
const pugi::xml_node& selectableTag {variant.child("selectable")};
|
2022-01-29 17:41:22 +00:00
|
|
|
if (selectableTag != nullptr) {
|
2023-01-04 18:01:41 +00:00
|
|
|
const std::string& value {selectableTag.text().as_string()};
|
2022-01-29 17:41:22 +00:00
|
|
|
if (value.front() == '0' || value.front() == 'f' || value.front() == 'F' ||
|
|
|
|
value.front() == 'n' || value.front() == 'N')
|
|
|
|
readVariant.selectable = false;
|
|
|
|
else
|
|
|
|
readVariant.selectable = true;
|
|
|
|
}
|
|
|
|
|
2023-01-04 18:01:41 +00:00
|
|
|
for (pugi::xml_node overrideTag {variant.child("override")}; overrideTag;
|
|
|
|
overrideTag = overrideTag.next_sibling("override")) {
|
|
|
|
if (overrideTag != nullptr) {
|
|
|
|
std::vector<std::string> mediaTypes;
|
|
|
|
const pugi::xml_node& mediaTypeTag {overrideTag.child("mediaType")};
|
|
|
|
if (mediaTypeTag != nullptr) {
|
|
|
|
std::string mediaTypeValue {mediaTypeTag.text().as_string()};
|
|
|
|
for (auto& character : mediaTypeValue) {
|
|
|
|
if (std::isspace(character))
|
|
|
|
character = ',';
|
|
|
|
}
|
|
|
|
mediaTypeValue = Utils::String::replace(mediaTypeValue, ",,", ",");
|
|
|
|
mediaTypes = Utils::String::delimitedStringToVector(mediaTypeValue, ",");
|
|
|
|
|
|
|
|
for (std::string& type : mediaTypes) {
|
|
|
|
if (std::find(sSupportedMediaTypes.cbegin(),
|
|
|
|
sSupportedMediaTypes.cend(),
|
|
|
|
type) == sSupportedMediaTypes.cend()) {
|
|
|
|
LOG(LogError) << "ThemeData::parseThemeCapabilities(): Invalid "
|
|
|
|
"override configuration, unsupported "
|
|
|
|
"\"mediaType\" value \""
|
|
|
|
<< type << "\"";
|
|
|
|
mediaTypes.clear();
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2022-01-29 17:41:22 +00:00
|
|
|
}
|
2023-01-04 18:01:41 +00:00
|
|
|
|
|
|
|
const pugi::xml_node& triggerTag {overrideTag.child("trigger")};
|
|
|
|
if (triggerTag != nullptr) {
|
|
|
|
const std::string& triggerValue {triggerTag.text().as_string()};
|
|
|
|
if (triggerValue == "") {
|
|
|
|
LOG(LogWarning) << "No <trigger> tag value defined for variant \""
|
|
|
|
<< readVariant.name << "\", ignoring entry in \""
|
|
|
|
<< capFile << "\"";
|
|
|
|
}
|
|
|
|
else if (triggerValue != "noVideos" && triggerValue != "noMedia") {
|
|
|
|
LOG(LogWarning) << "Invalid <useVariant> tag value \"" << triggerValue
|
|
|
|
<< "\" defined for variant \"" << readVariant.name
|
|
|
|
<< "\", ignoring entry in \"" << capFile << "\"";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
const pugi::xml_node& useVariantTag {overrideTag.child("useVariant")};
|
|
|
|
if (useVariantTag != nullptr) {
|
|
|
|
const std::string& useVariantValue {
|
|
|
|
useVariantTag.text().as_string()};
|
|
|
|
if (useVariantValue == "") {
|
|
|
|
LOG(LogWarning)
|
|
|
|
<< "No <useVariant> tag value defined for variant \""
|
|
|
|
<< readVariant.name << "\", ignoring entry in \"" << capFile
|
|
|
|
<< "\"";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
hasTriggers = true;
|
|
|
|
if (triggerValue == "noVideos") {
|
|
|
|
readVariant
|
|
|
|
.overrides[ThemeTriggers::TriggerType::NO_VIDEOS] =
|
|
|
|
std::make_pair(useVariantValue,
|
|
|
|
std::vector<std::string>());
|
|
|
|
}
|
|
|
|
else if (triggerValue == "noMedia") {
|
|
|
|
if (mediaTypes.empty())
|
|
|
|
mediaTypes.emplace_back("miximage");
|
|
|
|
readVariant
|
|
|
|
.overrides[ThemeTriggers::TriggerType::NO_MEDIA] =
|
|
|
|
std::make_pair(useVariantValue, mediaTypes);
|
|
|
|
}
|
|
|
|
}
|
2022-01-29 17:41:22 +00:00
|
|
|
}
|
|
|
|
else {
|
2023-01-04 18:01:41 +00:00
|
|
|
LOG(LogWarning)
|
|
|
|
<< "Found an <override> tag without a corresponding "
|
|
|
|
"<useVariant> tag, "
|
|
|
|
<< "ignoring entry for variant \"" << readVariant.name
|
|
|
|
<< "\" in \"" << capFile << "\"";
|
2022-01-29 17:41:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
LOG(LogWarning)
|
|
|
|
<< "Found an <override> tag without a corresponding <trigger> tag, "
|
2022-01-30 20:35:39 +00:00
|
|
|
<< "ignoring entry for variant \"" << readVariant.name << "\" in \""
|
2022-01-29 17:41:22 +00:00
|
|
|
<< capFile << "\"";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (readVariant.name != "") {
|
|
|
|
bool duplicate {false};
|
|
|
|
for (auto& variant : capabilities.variants) {
|
|
|
|
if (variant.name == readVariant.name) {
|
|
|
|
LOG(LogWarning) << "Variant \"" << readVariant.name
|
|
|
|
<< "\" is declared multiple times, ignoring entry in \""
|
|
|
|
<< capFile << "\"";
|
|
|
|
duplicate = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!duplicate)
|
|
|
|
capabilities.variants.emplace_back(readVariant);
|
|
|
|
}
|
|
|
|
}
|
2022-10-31 18:32:13 +00:00
|
|
|
|
|
|
|
for (pugi::xml_node colorScheme {themeCapabilities.child("colorScheme")}; colorScheme;
|
|
|
|
colorScheme = colorScheme.next_sibling("colorScheme")) {
|
|
|
|
ThemeColorScheme readColorScheme;
|
2023-01-04 18:01:41 +00:00
|
|
|
const std::string& name {colorScheme.attribute("name").as_string()};
|
2022-10-31 18:32:13 +00:00
|
|
|
if (name.empty()) {
|
|
|
|
LOG(LogWarning)
|
|
|
|
<< "Found <colorScheme> tag without name attribute, ignoring entry in \""
|
|
|
|
<< capFile << "\"";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
readColorScheme.name = name;
|
|
|
|
}
|
|
|
|
|
2023-01-04 18:01:41 +00:00
|
|
|
const pugi::xml_node& labelTag {colorScheme.child("label")};
|
2022-10-31 18:32:13 +00:00
|
|
|
if (labelTag == nullptr) {
|
|
|
|
LOG(LogDebug) << "No colorScheme <label> tag found, setting label value to the "
|
|
|
|
"color scheme name \""
|
|
|
|
<< name << "\" for \"" << capFile << "\"";
|
|
|
|
readColorScheme.label = name;
|
|
|
|
}
|
|
|
|
else {
|
2023-01-04 18:01:41 +00:00
|
|
|
const std::string& labelValue {labelTag.text().as_string()};
|
2022-10-31 18:32:13 +00:00
|
|
|
if (labelValue == "") {
|
|
|
|
LOG(LogWarning) << "No colorScheme <label> value defined, setting value to "
|
|
|
|
"the color scheme name \""
|
|
|
|
<< name << "\" for \"" << capFile << "\"";
|
|
|
|
readColorScheme.label = name;
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
readColorScheme.label = labelValue;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (readColorScheme.name != "") {
|
|
|
|
bool duplicate {false};
|
|
|
|
for (auto& colorScheme : capabilities.colorSchemes) {
|
|
|
|
if (colorScheme.name == readColorScheme.name) {
|
|
|
|
LOG(LogWarning) << "Color scheme \"" << readColorScheme.name
|
|
|
|
<< "\" is declared multiple times, ignoring entry in \""
|
|
|
|
<< capFile << "\"";
|
|
|
|
duplicate = true;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!duplicate)
|
|
|
|
capabilities.colorSchemes.emplace_back(readColorScheme);
|
|
|
|
}
|
|
|
|
}
|
2023-01-08 16:00:36 +00:00
|
|
|
|
|
|
|
for (pugi::xml_node transitions {themeCapabilities.child("transitions")}; transitions;
|
|
|
|
transitions = transitions.next_sibling("transitions")) {
|
|
|
|
std::map<ViewTransition, ViewTransitionAnimation> readTransitions;
|
|
|
|
std::string name {transitions.attribute("name").as_string()};
|
|
|
|
std::string label;
|
|
|
|
bool selectable {true};
|
|
|
|
|
|
|
|
if (name.empty()) {
|
|
|
|
LOG(LogWarning)
|
|
|
|
<< "Found <transitions> tag without name attribute, ignoring entry in \""
|
|
|
|
<< capFile << "\"";
|
|
|
|
name.clear();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (std::find(sSupportedTransitionAnimations.cbegin(),
|
|
|
|
sSupportedTransitionAnimations.cend(),
|
|
|
|
name) != sSupportedTransitionAnimations.cend()) {
|
|
|
|
LOG(LogWarning)
|
|
|
|
<< "Found <transitions> tag using reserved name attribute value \"" << name
|
|
|
|
<< "\", ignoring entry in \"" << capFile << "\"";
|
|
|
|
name.clear();
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
for (auto& transitionEntry : capabilities.transitions) {
|
|
|
|
if (transitionEntry.name == name) {
|
|
|
|
LOG(LogWarning)
|
|
|
|
<< "Found <transitions> tag with previously used name attribute "
|
|
|
|
"value \""
|
|
|
|
<< name << "\", ignoring entry in \"" << capFile << "\"";
|
|
|
|
name.clear();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (name == "")
|
|
|
|
continue;
|
|
|
|
|
|
|
|
const pugi::xml_node& labelTag {transitions.child("label")};
|
|
|
|
if (labelTag != nullptr)
|
|
|
|
label = labelTag.text().as_string();
|
|
|
|
|
|
|
|
const pugi::xml_node& selectableTag {transitions.child("selectable")};
|
|
|
|
if (selectableTag != nullptr) {
|
|
|
|
const std::string& value {selectableTag.text().as_string()};
|
|
|
|
if (value.front() == '0' || value.front() == 'f' || value.front() == 'F' ||
|
|
|
|
value.front() == 'n' || value.front() == 'N')
|
|
|
|
selectable = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (auto& currTransition : sSupportedTransitions) {
|
|
|
|
const pugi::xml_node& transitionTag {transitions.child(currTransition.c_str())};
|
|
|
|
if (transitionTag != nullptr) {
|
|
|
|
const std::string& transitionValue {transitionTag.text().as_string()};
|
|
|
|
if (transitionValue.empty()) {
|
|
|
|
LOG(LogWarning) << "Found <" << currTransition
|
|
|
|
<< "> transition tag without any value, "
|
|
|
|
"ignoring entry in \""
|
|
|
|
<< capFile << "\"";
|
|
|
|
}
|
|
|
|
else if (std::find(sSupportedTransitionAnimations.cbegin(),
|
|
|
|
sSupportedTransitionAnimations.cend(),
|
|
|
|
currTransition) != sSupportedTransitionAnimations.cend()) {
|
|
|
|
LOG(LogWarning)
|
|
|
|
<< "Invalid <" << currTransition << "> transition tag value \""
|
|
|
|
<< transitionValue << "\", ignoring entry in \"" << capFile << "\"";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
ViewTransitionAnimation transitionAnim {ViewTransitionAnimation::INSTANT};
|
|
|
|
if (transitionValue == "slide")
|
|
|
|
transitionAnim = ViewTransitionAnimation::SLIDE;
|
|
|
|
else if (transitionValue == "fade")
|
|
|
|
transitionAnim = ViewTransitionAnimation::FADE;
|
|
|
|
|
|
|
|
if (currTransition == "systemToSystem")
|
|
|
|
readTransitions[ViewTransition::SYSTEM_TO_SYSTEM] = transitionAnim;
|
|
|
|
else if (currTransition == "systemToGamelist")
|
|
|
|
readTransitions[ViewTransition::SYSTEM_TO_GAMELIST] = transitionAnim;
|
|
|
|
else if (currTransition == "gamelistToGamelist")
|
|
|
|
readTransitions[ViewTransition::GAMELIST_TO_GAMELIST] = transitionAnim;
|
|
|
|
else if (currTransition == "gamelistToSystem")
|
|
|
|
readTransitions[ViewTransition::GAMELIST_TO_SYSTEM] = transitionAnim;
|
|
|
|
else if (currTransition == "startupToSystem")
|
|
|
|
readTransitions[ViewTransition::STARTUP_TO_SYSTEM] = transitionAnim;
|
|
|
|
else if (currTransition == "startupToGamelist")
|
|
|
|
readTransitions[ViewTransition::STARTUP_TO_GAMELIST] = transitionAnim;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!readTransitions.empty()) {
|
2023-01-08 16:14:32 +00:00
|
|
|
// If startupToSystem and startupToGamelist are not defined, then set them
|
|
|
|
// to the same values as systemToSystem and gamelistToGamelist respectively,
|
|
|
|
// assuming those transitions have been defined.
|
|
|
|
if (readTransitions.find(ViewTransition::STARTUP_TO_SYSTEM) ==
|
|
|
|
readTransitions.cend()) {
|
|
|
|
if (readTransitions.find(ViewTransition::SYSTEM_TO_SYSTEM) !=
|
|
|
|
readTransitions.cend())
|
|
|
|
readTransitions[ViewTransition::STARTUP_TO_SYSTEM] =
|
|
|
|
readTransitions[ViewTransition::SYSTEM_TO_SYSTEM];
|
|
|
|
}
|
|
|
|
if (readTransitions.find(ViewTransition::STARTUP_TO_GAMELIST) ==
|
|
|
|
readTransitions.cend()) {
|
|
|
|
if (readTransitions.find(ViewTransition::GAMELIST_TO_GAMELIST) !=
|
|
|
|
readTransitions.cend())
|
|
|
|
readTransitions[ViewTransition::STARTUP_TO_GAMELIST] =
|
|
|
|
readTransitions[ViewTransition::GAMELIST_TO_GAMELIST];
|
|
|
|
}
|
|
|
|
|
2023-01-08 16:00:36 +00:00
|
|
|
ThemeTransitions transition;
|
|
|
|
transition.name = name;
|
|
|
|
transition.label = label;
|
|
|
|
transition.selectable = selectable;
|
|
|
|
transition.animations = std::move(readTransitions);
|
|
|
|
capabilities.transitions.emplace_back(std::move(transition));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-08 18:30:16 +00:00
|
|
|
for (pugi::xml_node suppressTransitionProfiles {
|
|
|
|
themeCapabilities.child("suppressTransitionProfiles")};
|
|
|
|
suppressTransitionProfiles;
|
|
|
|
suppressTransitionProfiles =
|
|
|
|
suppressTransitionProfiles.next_sibling("suppressTransitionProfiles")) {
|
|
|
|
std::vector<std::string> readSuppressProfiles;
|
2023-01-08 16:00:36 +00:00
|
|
|
|
2023-01-08 18:30:16 +00:00
|
|
|
for (pugi::xml_node entries {suppressTransitionProfiles.child("entry")}; entries;
|
2023-01-08 16:00:36 +00:00
|
|
|
entries = entries.next_sibling("entry")) {
|
|
|
|
const std::string& entryValue {entries.text().as_string()};
|
|
|
|
|
|
|
|
if (std::find(sSupportedTransitionAnimations.cbegin(),
|
|
|
|
sSupportedTransitionAnimations.cend(),
|
|
|
|
entryValue) != sSupportedTransitionAnimations.cend()) {
|
2023-01-08 18:30:16 +00:00
|
|
|
capabilities.suppressedTransitionProfiles.emplace_back(entryValue);
|
2023-01-08 16:00:36 +00:00
|
|
|
}
|
|
|
|
else {
|
|
|
|
LOG(LogWarning)
|
2023-01-08 18:30:16 +00:00
|
|
|
<< "Found suppressTransitionProfiles <entry> tag with invalid value \""
|
2023-01-08 16:00:36 +00:00
|
|
|
<< entryValue << "\", ignoring entry in \"" << capFile << "\"";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// Sort and remove any duplicates.
|
2023-01-08 18:30:16 +00:00
|
|
|
if (capabilities.suppressedTransitionProfiles.size() > 1) {
|
|
|
|
std::sort(capabilities.suppressedTransitionProfiles.begin(),
|
|
|
|
capabilities.suppressedTransitionProfiles.end());
|
|
|
|
auto last = std::unique(capabilities.suppressedTransitionProfiles.begin(),
|
|
|
|
capabilities.suppressedTransitionProfiles.end());
|
|
|
|
capabilities.suppressedTransitionProfiles.erase(
|
|
|
|
last, capabilities.suppressedTransitionProfiles.end());
|
2023-01-08 16:00:36 +00:00
|
|
|
}
|
|
|
|
}
|
2022-01-29 17:41:22 +00:00
|
|
|
}
|
|
|
|
else {
|
2023-07-30 16:17:27 +00:00
|
|
|
capabilities.validTheme = false;
|
|
|
|
LOG(LogWarning)
|
2023-08-14 20:40:32 +00:00
|
|
|
<< "No capabilities.xml file found, this does not appear to be a valid theme: \""
|
2023-07-30 16:36:38 +00:00
|
|
|
#if defined(_WIN64)
|
|
|
|
<< Utils::String::replace(path, "/", "\\") << "\"";
|
|
|
|
#else
|
2023-07-30 16:17:27 +00:00
|
|
|
<< path << "\"";
|
2023-07-30 16:36:38 +00:00
|
|
|
#endif
|
2022-01-29 17:41:22 +00:00
|
|
|
}
|
|
|
|
|
2022-01-31 22:22:42 +00:00
|
|
|
// Add the aspect ratios in the order they are defined in sSupportedAspectRatios so they
|
|
|
|
// always show up in the same order in the UI Settings menu.
|
|
|
|
if (!aspectRatiosTemp.empty()) {
|
2022-12-17 18:47:37 +00:00
|
|
|
// Add the "automatic" aspect ratio if there is at least one entry.
|
|
|
|
if (!aspectRatiosTemp.empty())
|
2022-11-01 16:08:51 +00:00
|
|
|
capabilities.aspectRatios.emplace_back(sSupportedAspectRatios.front().first);
|
2022-01-31 22:22:42 +00:00
|
|
|
for (auto& aspectRatio : sSupportedAspectRatios) {
|
|
|
|
if (std::find(aspectRatiosTemp.cbegin(), aspectRatiosTemp.cend(), aspectRatio.first) !=
|
|
|
|
aspectRatiosTemp.cend()) {
|
|
|
|
capabilities.aspectRatios.emplace_back(aspectRatio.first);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-20 20:58:40 +00:00
|
|
|
// Add the font sizes in the order they are defined in sSupportedFontSizes so they always
|
|
|
|
// show up in the same order in the UI Settings menu.
|
|
|
|
if (!fontSizesTemp.empty()) {
|
|
|
|
for (auto& fontSize : sSupportedFontSizes) {
|
|
|
|
if (std::find(fontSizesTemp.cbegin(), fontSizesTemp.cend(), fontSize.first) !=
|
|
|
|
fontSizesTemp.cend()) {
|
|
|
|
capabilities.fontSizes.emplace_back(fontSize.first);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-04 18:01:41 +00:00
|
|
|
if (hasTriggers) {
|
|
|
|
for (auto& variant : capabilities.variants) {
|
|
|
|
for (auto it = variant.overrides.begin(); it != variant.overrides.end();) {
|
|
|
|
const auto variantIter =
|
|
|
|
std::find_if(capabilities.variants.begin(), capabilities.variants.end(),
|
|
|
|
[it](ThemeVariant currVariant) {
|
|
|
|
return currVariant.name == (*it).second.first;
|
|
|
|
});
|
|
|
|
if (variantIter == capabilities.variants.end()) {
|
|
|
|
LOG(LogWarning)
|
|
|
|
<< "The <useVariant> tag value \"" << (*it).second.first
|
|
|
|
<< "\" does not match any defined variants, ignoring entry in \"" << capFile
|
|
|
|
<< "\"";
|
|
|
|
it = variant.overrides.erase(it);
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
++it;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-29 17:41:22 +00:00
|
|
|
return capabilities;
|
|
|
|
}
|
|
|
|
|
2013-12-31 03:48:28 +00:00
|
|
|
void ThemeData::parseIncludes(const pugi::xml_node& root)
|
2013-11-12 23:28:15 +00:00
|
|
|
{
|
2024-01-21 13:22:49 +00:00
|
|
|
for (pugi::xml_node node {root.child("include")}; node; node = node.next_sibling("include")) {
|
|
|
|
ThemeException error;
|
|
|
|
error << "ThemeData::parseIncludes(): ";
|
|
|
|
error.setFiles(mPaths);
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2024-01-21 13:22:49 +00:00
|
|
|
// Check if there's an unsupported theme version tag.
|
|
|
|
if (root.child("formatVersion") != nullptr)
|
|
|
|
throw error << ": Unsupported <formatVersion> tag found";
|
2022-01-29 17:41:22 +00:00
|
|
|
|
2022-01-23 16:50:51 +00:00
|
|
|
std::string relPath {resolvePlaceholders(node.text().as_string())};
|
|
|
|
std::string path {Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true)};
|
2022-10-29 11:04:00 +00:00
|
|
|
|
|
|
|
if (!ResourceManager::getInstance().fileExists(path)) {
|
|
|
|
// For explicit paths, throw an error if the file couldn't be found, but only
|
|
|
|
// print a debug message if it was set using a variable.
|
|
|
|
if (relPath == node.text().get()) {
|
|
|
|
throw error << " -> \"" << relPath << "\" not found (resolved to \"" << path
|
|
|
|
<< "\")";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
if (!(Settings::getInstance()->getBool("DebugSkipMissingThemeFiles") ||
|
|
|
|
(mCustomCollection && Settings::getInstance()->getBool(
|
|
|
|
"DebugSkipMissingThemeFilesCustomCollections")))) {
|
2022-12-19 19:38:41 +00:00
|
|
|
#if defined(_WIN64)
|
|
|
|
LOG(LogDebug) << Utils::String::replace(error.message, "/", "\\")
|
|
|
|
<< ": Couldn't find file \"" << node.text().get() << "\" "
|
|
|
|
<< ((node.text().get() != path) ?
|
|
|
|
"which resolves to \"" +
|
|
|
|
Utils::String::replace(path, "/", "\\") + "\"" :
|
|
|
|
#else
|
2022-10-29 11:04:00 +00:00
|
|
|
LOG(LogDebug) << error.message << ": Couldn't find file \"" << node.text().get()
|
|
|
|
<< "\" "
|
|
|
|
<< ((node.text().get() != path) ?
|
|
|
|
"which resolves to \"" + path + "\"" :
|
2022-12-19 19:38:41 +00:00
|
|
|
#endif
|
2022-10-29 11:04:00 +00:00
|
|
|
"");
|
|
|
|
}
|
2023-10-31 18:25:36 +00:00
|
|
|
continue;
|
2022-10-29 11:04:00 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-02-08 19:53:39 +00:00
|
|
|
error << " -> \"" << relPath << "\"";
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
mPaths.push_back(path);
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
pugi::xml_document includeDoc;
|
2021-07-07 18:31:46 +00:00
|
|
|
#if defined(_WIN64)
|
2022-01-23 16:50:51 +00:00
|
|
|
pugi::xml_parse_result result {
|
|
|
|
includeDoc.load_file(Utils::String::stringToWideString(path).c_str())};
|
2021-07-07 18:31:46 +00:00
|
|
|
#else
|
2022-01-23 16:50:51 +00:00
|
|
|
pugi::xml_parse_result result {includeDoc.load_file(path.c_str())};
|
2021-07-07 18:31:46 +00:00
|
|
|
#endif
|
2020-06-21 12:25:28 +00:00
|
|
|
if (!result)
|
2021-02-08 19:53:39 +00:00
|
|
|
throw error << ": Error parsing file: " << result.description();
|
2013-12-31 03:48:28 +00:00
|
|
|
|
2022-01-23 16:50:51 +00:00
|
|
|
pugi::xml_node theme {includeDoc.child("theme")};
|
2020-06-21 12:25:28 +00:00
|
|
|
if (!theme)
|
2022-01-23 16:50:51 +00:00
|
|
|
throw error << ": Missing <theme> tag";
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2023-07-30 16:17:27 +00:00
|
|
|
parseTransitions(theme);
|
2020-06-21 12:25:28 +00:00
|
|
|
parseVariables(theme);
|
2023-07-30 16:17:27 +00:00
|
|
|
parseColorSchemes(theme);
|
2023-12-20 20:58:40 +00:00
|
|
|
parseFontSizes(theme);
|
2020-06-21 12:25:28 +00:00
|
|
|
parseIncludes(theme);
|
|
|
|
parseViews(theme);
|
2023-07-30 16:17:27 +00:00
|
|
|
if (theme.child("feature") != nullptr)
|
2024-01-21 12:27:39 +00:00
|
|
|
throw error << ": Unsupported <feature> tag found";
|
2023-07-30 16:17:27 +00:00
|
|
|
parseVariants(theme);
|
|
|
|
parseAspectRatios(theme);
|
2022-01-29 17:41:22 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
mPaths.pop_back();
|
|
|
|
}
|
2013-11-12 23:28:15 +00:00
|
|
|
}
|
|
|
|
|
2022-01-29 17:41:22 +00:00
|
|
|
void ThemeData::parseVariants(const pugi::xml_node& root)
|
|
|
|
{
|
2023-08-14 20:40:32 +00:00
|
|
|
if (sCurrentTheme == sThemes.end())
|
2022-01-29 17:41:22 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
if (mSelectedVariant == "")
|
|
|
|
return;
|
|
|
|
|
|
|
|
ThemeException error;
|
|
|
|
error << "ThemeData::parseVariants(): ";
|
|
|
|
error.setFiles(mPaths);
|
|
|
|
|
2022-10-31 18:32:13 +00:00
|
|
|
for (pugi::xml_node node {root.child("variant")}; node; node = node.next_sibling("variant")) {
|
2022-01-29 17:41:22 +00:00
|
|
|
if (!node.attribute("name"))
|
|
|
|
throw error << ": <variant> tag missing \"name\" attribute";
|
|
|
|
|
|
|
|
const std::string delim {" \t\r\n,"};
|
|
|
|
const std::string nameAttr {node.attribute("name").as_string()};
|
|
|
|
size_t prevOff {nameAttr.find_first_not_of(delim, 0)};
|
|
|
|
size_t off {nameAttr.find_first_of(delim, prevOff)};
|
|
|
|
std::string viewKey;
|
|
|
|
while (off != std::string::npos || prevOff != std::string::npos) {
|
|
|
|
viewKey = nameAttr.substr(prevOff, off - prevOff);
|
|
|
|
prevOff = nameAttr.find_first_not_of(delim, off);
|
|
|
|
off = nameAttr.find_first_of(delim, prevOff);
|
|
|
|
|
|
|
|
if (std::find(mVariants.cbegin(), mVariants.cend(), viewKey) == mVariants.cend()) {
|
|
|
|
throw error << ": <variant> value \"" << viewKey
|
|
|
|
<< "\" is not defined in capabilities.xml";
|
|
|
|
}
|
|
|
|
|
2023-01-04 18:01:41 +00:00
|
|
|
const std::string variant {mOverrideVariant.empty() ? mSelectedVariant :
|
|
|
|
mOverrideVariant};
|
|
|
|
|
|
|
|
if (variant == viewKey || viewKey == "all") {
|
2023-01-15 17:24:08 +00:00
|
|
|
parseTransitions(node);
|
2022-10-28 19:06:01 +00:00
|
|
|
parseVariables(node);
|
2022-11-01 16:13:37 +00:00
|
|
|
parseColorSchemes(node);
|
2023-12-20 20:58:40 +00:00
|
|
|
parseFontSizes(node);
|
2022-01-29 17:41:22 +00:00
|
|
|
parseIncludes(node);
|
|
|
|
parseViews(node);
|
2022-11-01 16:13:37 +00:00
|
|
|
parseAspectRatios(node);
|
2022-01-29 17:41:22 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-10-31 18:32:13 +00:00
|
|
|
void ThemeData::parseColorSchemes(const pugi::xml_node& root)
|
|
|
|
{
|
2023-08-14 20:40:32 +00:00
|
|
|
if (sCurrentTheme == sThemes.end())
|
2022-10-31 18:32:13 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
if (mSelectedColorScheme == "")
|
|
|
|
return;
|
|
|
|
|
|
|
|
ThemeException error;
|
|
|
|
error << "ThemeData::parseColorSchemes(): ";
|
|
|
|
error.setFiles(mPaths);
|
|
|
|
|
|
|
|
for (pugi::xml_node node {root.child("colorScheme")}; node;
|
|
|
|
node = node.next_sibling("colorScheme")) {
|
|
|
|
if (!node.attribute("name"))
|
|
|
|
throw error << ": <colorScheme> tag missing \"name\" attribute";
|
|
|
|
|
|
|
|
const std::string delim {" \t\r\n,"};
|
|
|
|
const std::string nameAttr {node.attribute("name").as_string()};
|
|
|
|
size_t prevOff {nameAttr.find_first_not_of(delim, 0)};
|
|
|
|
size_t off {nameAttr.find_first_of(delim, prevOff)};
|
|
|
|
std::string viewKey;
|
|
|
|
while (off != std::string::npos || prevOff != std::string::npos) {
|
|
|
|
viewKey = nameAttr.substr(prevOff, off - prevOff);
|
|
|
|
prevOff = nameAttr.find_first_not_of(delim, off);
|
|
|
|
off = nameAttr.find_first_of(delim, prevOff);
|
|
|
|
|
|
|
|
if (std::find(mColorSchemes.cbegin(), mColorSchemes.cend(), viewKey) ==
|
|
|
|
mColorSchemes.cend()) {
|
|
|
|
throw error << ": <colorScheme> value \"" << viewKey
|
|
|
|
<< "\" is not defined in capabilities.xml";
|
|
|
|
}
|
|
|
|
|
2022-10-31 20:50:54 +00:00
|
|
|
if (mSelectedColorScheme == viewKey)
|
2022-10-31 18:32:13 +00:00
|
|
|
parseVariables(node);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-12-20 20:58:40 +00:00
|
|
|
void ThemeData::parseFontSizes(const pugi::xml_node& root)
|
|
|
|
{
|
|
|
|
if (sCurrentTheme == sThemes.end())
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (mSelectedFontSize == "")
|
|
|
|
return;
|
|
|
|
|
|
|
|
ThemeException error;
|
|
|
|
error << "ThemeData::parseFontSizes(): ";
|
|
|
|
error.setFiles(mPaths);
|
|
|
|
|
|
|
|
for (pugi::xml_node node {root.child("fontSize")}; node; node = node.next_sibling("fontSize")) {
|
|
|
|
if (!node.attribute("name"))
|
|
|
|
throw error << ": <fontSize> tag missing \"name\" attribute";
|
|
|
|
|
|
|
|
const std::string delim {" \t\r\n,"};
|
|
|
|
const std::string nameAttr {node.attribute("name").as_string()};
|
|
|
|
size_t prevOff {nameAttr.find_first_not_of(delim, 0)};
|
|
|
|
size_t off {nameAttr.find_first_of(delim, prevOff)};
|
|
|
|
std::string viewKey;
|
|
|
|
while (off != std::string::npos || prevOff != std::string::npos) {
|
|
|
|
viewKey = nameAttr.substr(prevOff, off - prevOff);
|
|
|
|
prevOff = nameAttr.find_first_not_of(delim, off);
|
|
|
|
off = nameAttr.find_first_of(delim, prevOff);
|
|
|
|
|
|
|
|
if (std::find(mFontSizes.cbegin(), mFontSizes.cend(), viewKey) == mFontSizes.cend()) {
|
|
|
|
throw error << ": <fontSize> value \"" << viewKey
|
|
|
|
<< "\" is not defined in capabilities.xml";
|
|
|
|
}
|
|
|
|
|
|
|
|
if (mSelectedFontSize == viewKey)
|
|
|
|
parseVariables(node);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-01-29 17:41:22 +00:00
|
|
|
void ThemeData::parseAspectRatios(const pugi::xml_node& root)
|
|
|
|
{
|
2023-08-14 20:40:32 +00:00
|
|
|
if (sCurrentTheme == sThemes.end())
|
2022-01-29 17:41:22 +00:00
|
|
|
return;
|
|
|
|
|
2023-02-09 23:34:24 +00:00
|
|
|
if (sSelectedAspectRatio == "")
|
2022-01-29 17:41:22 +00:00
|
|
|
return;
|
|
|
|
|
|
|
|
ThemeException error;
|
|
|
|
error << "ThemeData::parseAspectRatios(): ";
|
|
|
|
error.setFiles(mPaths);
|
|
|
|
|
2022-10-31 18:32:13 +00:00
|
|
|
for (pugi::xml_node node {root.child("aspectRatio")}; node;
|
2022-01-29 17:41:22 +00:00
|
|
|
node = node.next_sibling("aspectRatio")) {
|
|
|
|
if (!node.attribute("name"))
|
|
|
|
throw error << ": <aspectRatio> tag missing \"name\" attribute";
|
|
|
|
|
|
|
|
const std::string delim {" \t\r\n,"};
|
|
|
|
const std::string nameAttr {node.attribute("name").as_string()};
|
|
|
|
size_t prevOff {nameAttr.find_first_not_of(delim, 0)};
|
|
|
|
size_t off {nameAttr.find_first_of(delim, prevOff)};
|
|
|
|
std::string viewKey;
|
|
|
|
while (off != std::string::npos || prevOff != std::string::npos) {
|
|
|
|
viewKey = nameAttr.substr(prevOff, off - prevOff);
|
|
|
|
prevOff = nameAttr.find_first_not_of(delim, off);
|
|
|
|
off = nameAttr.find_first_of(delim, prevOff);
|
|
|
|
|
2023-08-14 20:40:32 +00:00
|
|
|
if (std::find(sCurrentTheme->second.capabilities.aspectRatios.cbegin(),
|
|
|
|
sCurrentTheme->second.capabilities.aspectRatios.cend(),
|
|
|
|
viewKey) == sCurrentTheme->second.capabilities.aspectRatios.cend()) {
|
2022-10-31 18:32:13 +00:00
|
|
|
throw error << ": <aspectRatio> value \"" << viewKey
|
2022-01-29 17:41:22 +00:00
|
|
|
<< "\" is not defined in capabilities.xml";
|
|
|
|
}
|
|
|
|
|
2023-02-09 23:34:24 +00:00
|
|
|
if (sSelectedAspectRatio == viewKey) {
|
2022-11-01 16:13:37 +00:00
|
|
|
parseVariables(node);
|
|
|
|
parseColorSchemes(node);
|
2023-12-20 20:58:40 +00:00
|
|
|
parseFontSizes(node);
|
2022-01-29 17:41:22 +00:00
|
|
|
parseIncludes(node);
|
|
|
|
parseViews(node);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-01-15 17:24:08 +00:00
|
|
|
void ThemeData::parseTransitions(const pugi::xml_node& root)
|
|
|
|
{
|
|
|
|
ThemeException error;
|
|
|
|
error << "ThemeData::parseTransitions(): ";
|
|
|
|
error.setFiles(mPaths);
|
|
|
|
|
|
|
|
const pugi::xml_node& transitions {root.child("transitions")};
|
|
|
|
if (transitions != nullptr) {
|
|
|
|
const std::string& transitionsValue {transitions.text().as_string()};
|
2023-08-14 20:40:32 +00:00
|
|
|
if (std::find_if(sCurrentTheme->second.capabilities.transitions.cbegin(),
|
|
|
|
sCurrentTheme->second.capabilities.transitions.cend(),
|
2023-01-15 17:24:08 +00:00
|
|
|
[&transitionsValue](const ThemeTransitions transitions) {
|
|
|
|
return transitions.name == transitionsValue;
|
2023-08-14 20:40:32 +00:00
|
|
|
}) == sCurrentTheme->second.capabilities.transitions.cend()) {
|
2023-01-15 17:24:08 +00:00
|
|
|
throw error << ": <transitions> value \"" << transitionsValue
|
|
|
|
<< "\" is not matching any defined transitions";
|
|
|
|
}
|
2023-02-09 23:34:24 +00:00
|
|
|
sVariantDefinedTransitions = transitionsValue;
|
2023-01-15 17:24:08 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2017-05-14 04:07:28 +00:00
|
|
|
void ThemeData::parseVariables(const pugi::xml_node& root)
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
ThemeException error;
|
|
|
|
error.setFiles(mPaths);
|
2019-08-25 15:23:02 +00:00
|
|
|
|
2022-10-31 18:32:13 +00:00
|
|
|
for (pugi::xml_node node {root.child("variables")}; node;
|
2022-10-28 19:06:01 +00:00
|
|
|
node = node.next_sibling("variables")) {
|
2017-05-14 04:07:28 +00:00
|
|
|
|
2022-10-28 19:06:01 +00:00
|
|
|
for (pugi::xml_node_iterator it = node.begin(); it != node.end(); ++it) {
|
|
|
|
const std::string key {it->name()};
|
|
|
|
const std::string val {resolvePlaceholders(it->text().as_string())};
|
2017-05-14 04:07:28 +00:00
|
|
|
|
2022-10-28 19:06:01 +00:00
|
|
|
if (!val.empty()) {
|
2023-07-30 16:17:27 +00:00
|
|
|
if (mVariables.find(key) != mVariables.end())
|
2022-10-28 19:06:01 +00:00
|
|
|
mVariables[key] = val;
|
|
|
|
else
|
|
|
|
mVariables.insert(std::pair<std::string, std::string>(key, val));
|
|
|
|
}
|
|
|
|
}
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
2017-05-14 04:07:28 +00:00
|
|
|
}
|
|
|
|
|
2013-12-31 03:48:28 +00:00
|
|
|
void ThemeData::parseViews(const pugi::xml_node& root)
|
2013-11-12 23:28:15 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
ThemeException error;
|
2022-01-23 16:50:51 +00:00
|
|
|
error << "ThemeData::parseViews(): ";
|
2020-06-21 12:25:28 +00:00
|
|
|
error.setFiles(mPaths);
|
|
|
|
|
|
|
|
// Parse views.
|
2022-10-31 18:32:13 +00:00
|
|
|
for (pugi::xml_node node {root.child("view")}; node; node = node.next_sibling("view")) {
|
2020-06-21 12:25:28 +00:00
|
|
|
if (!node.attribute("name"))
|
2022-01-23 16:50:51 +00:00
|
|
|
throw error << ": View missing \"name\" attribute";
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2022-01-23 16:50:51 +00:00
|
|
|
const std::string delim {" \t\r\n,"};
|
|
|
|
const std::string nameAttr {node.attribute("name").as_string()};
|
|
|
|
size_t prevOff {nameAttr.find_first_not_of(delim, 0)};
|
|
|
|
size_t off {nameAttr.find_first_of(delim, prevOff)};
|
2020-06-21 12:25:28 +00:00
|
|
|
std::string viewKey;
|
2020-07-13 18:58:25 +00:00
|
|
|
while (off != std::string::npos || prevOff != std::string::npos) {
|
2020-06-21 12:25:28 +00:00
|
|
|
viewKey = nameAttr.substr(prevOff, off - prevOff);
|
|
|
|
prevOff = nameAttr.find_first_not_of(delim, off);
|
|
|
|
off = nameAttr.find_first_of(delim, prevOff);
|
|
|
|
|
2023-07-30 16:17:27 +00:00
|
|
|
if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) !=
|
|
|
|
sSupportedViews.cend()) {
|
|
|
|
ThemeView& view {
|
|
|
|
mViews.insert(std::pair<std::string, ThemeView>(viewKey, ThemeView()))
|
|
|
|
.first->second};
|
|
|
|
parseView(node, view);
|
2022-01-29 17:41:22 +00:00
|
|
|
}
|
|
|
|
else {
|
2023-07-30 16:17:27 +00:00
|
|
|
throw error << ": Unsupported \"" << viewKey << "\" view style defined";
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2013-11-12 23:28:15 +00:00
|
|
|
}
|
|
|
|
|
2013-12-31 03:48:28 +00:00
|
|
|
void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view)
|
2013-11-12 23:28:15 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
ThemeException error;
|
2022-01-23 16:50:51 +00:00
|
|
|
error << "ThemeData::parseView(): ";
|
2020-06-21 12:25:28 +00:00
|
|
|
error.setFiles(mPaths);
|
|
|
|
|
2022-10-31 18:32:13 +00:00
|
|
|
for (pugi::xml_node node {root.first_child()}; node; node = node.next_sibling()) {
|
2020-06-21 12:25:28 +00:00
|
|
|
if (!node.attribute("name"))
|
2022-01-23 16:50:51 +00:00
|
|
|
throw error << ": Element of type \"" << node.name() << "\" missing \"name\" attribute";
|
2020-06-21 12:25:28 +00:00
|
|
|
|
|
|
|
auto elemTypeIt = sElementMap.find(node.name());
|
|
|
|
if (elemTypeIt == sElementMap.cend())
|
2022-01-23 16:50:51 +00:00
|
|
|
throw error << ": Unknown element type \"" << node.name() << "\"";
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2022-01-23 16:50:51 +00:00
|
|
|
const std::string delim {" \t\r\n,"};
|
|
|
|
const std::string nameAttr {node.attribute("name").as_string()};
|
|
|
|
size_t prevOff {nameAttr.find_first_not_of(delim, 0)};
|
|
|
|
size_t off {nameAttr.find_first_of(delim, prevOff)};
|
2020-06-21 12:25:28 +00:00
|
|
|
while (off != std::string::npos || prevOff != std::string::npos) {
|
2022-01-23 16:50:51 +00:00
|
|
|
std::string elemKey {nameAttr.substr(prevOff, off - prevOff)};
|
2020-06-21 12:25:28 +00:00
|
|
|
prevOff = nameAttr.find_first_not_of(delim, off);
|
|
|
|
off = nameAttr.find_first_of(delim, prevOff);
|
|
|
|
|
2022-01-29 17:41:22 +00:00
|
|
|
// Add the element type as a prefix to avoid name collisions between different
|
2023-07-30 16:17:27 +00:00
|
|
|
// component types.
|
|
|
|
elemKey = std::string {node.name()} + "_" + elemKey;
|
2022-01-29 17:41:22 +00:00
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
parseElement(
|
|
|
|
node, elemTypeIt->second,
|
|
|
|
view.elements.insert(std::pair<std::string, ThemeElement>(elemKey, ThemeElement()))
|
2023-07-30 16:17:27 +00:00
|
|
|
.first->second);
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
}
|
2013-11-12 23:28:15 +00:00
|
|
|
}
|
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
void ThemeData::parseElement(const pugi::xml_node& root,
|
2021-07-07 18:31:46 +00:00
|
|
|
const std::map<std::string, ElementPropertyType>& typeMap,
|
2023-07-30 16:17:27 +00:00
|
|
|
ThemeElement& element)
|
2013-12-30 23:23:34 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
ThemeException error;
|
2022-01-23 16:50:51 +00:00
|
|
|
error << "ThemeData::parseElement(): ";
|
2020-06-21 12:25:28 +00:00
|
|
|
error.setFiles(mPaths);
|
2023-07-30 16:17:27 +00:00
|
|
|
element.type = root.name();
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2023-07-30 16:17:27 +00:00
|
|
|
if (root.attribute("extra") != nullptr)
|
2024-01-21 12:27:39 +00:00
|
|
|
throw error << ": Unsupported \"extra\" attribute found for element of type \""
|
|
|
|
<< element.type << "\"";
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2022-10-31 18:32:13 +00:00
|
|
|
for (pugi::xml_node node {root.first_child()}; node; node = node.next_sibling()) {
|
2020-06-21 12:25:28 +00:00
|
|
|
auto typeIt = typeMap.find(node.name());
|
|
|
|
if (typeIt == typeMap.cend())
|
2022-01-23 16:50:51 +00:00
|
|
|
throw error << ": Unknown property type \"" << node.name()
|
|
|
|
<< "\" for element of type \"" << root.name() << "\"";
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2022-01-23 16:50:51 +00:00
|
|
|
std::string str {resolvePlaceholders(node.text().as_string())};
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2022-10-17 20:21:52 +00:00
|
|
|
// Handle the special case with mutually exclusive system variables, for example
|
2023-01-13 10:03:23 +00:00
|
|
|
// system.fullName.autoCollections and system.fullName.noCollections which can never
|
2022-10-17 20:21:52 +00:00
|
|
|
// exist at the same time. A backspace is assigned in SystemData to flag the
|
|
|
|
// variables that do not apply and if it's encountered here we simply skip the
|
|
|
|
// property.
|
2023-07-30 16:17:27 +00:00
|
|
|
if (str == "\b")
|
2022-10-17 20:21:52 +00:00
|
|
|
continue;
|
|
|
|
|
2023-07-30 16:17:27 +00:00
|
|
|
// Strictly enforce that there are no blank values in the theme configuration.
|
|
|
|
if (str == "")
|
2022-01-29 17:41:22 +00:00
|
|
|
throw error << ": Property \"" << typeIt->first << "\" for element \"" << element.type
|
|
|
|
<< "\" has no value defined";
|
|
|
|
|
2023-07-30 16:17:27 +00:00
|
|
|
std::string nodeName {node.name()};
|
2022-01-29 17:41:22 +00:00
|
|
|
|
|
|
|
// If an attribute exists, then replace nodeName with its name.
|
|
|
|
auto attributeEntry = sPropertyAttributeMap.find(element.type);
|
|
|
|
if (attributeEntry != sPropertyAttributeMap.end()) {
|
|
|
|
auto attribute = attributeEntry->second.find(typeIt->first);
|
|
|
|
if (attribute != attributeEntry->second.end()) {
|
|
|
|
if (node.attribute(attribute->second.c_str()) == nullptr) {
|
|
|
|
throw error << ": Unknown attribute \"" << node.first_attribute().name()
|
|
|
|
<< "\" for property \"" << typeIt->first << "\" (element \""
|
|
|
|
<< attributeEntry->first << "\")";
|
|
|
|
}
|
|
|
|
else {
|
|
|
|
// Add the attribute name as a prefix to avoid potential name collisions.
|
|
|
|
nodeName = attribute->second + "_" +
|
|
|
|
node.attribute(attribute->second.c_str()).as_string("");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
switch (typeIt->second) {
|
2021-07-07 18:31:46 +00:00
|
|
|
case NORMALIZED_RECT: {
|
2021-08-15 20:03:17 +00:00
|
|
|
glm::vec4 val;
|
2021-07-07 18:31:46 +00:00
|
|
|
|
|
|
|
auto splits = Utils::String::delimitedStringToVector(str, " ");
|
|
|
|
if (splits.size() == 2) {
|
2022-01-16 11:09:55 +00:00
|
|
|
val = glm::vec4 {static_cast<float>(atof(splits.at(0).c_str())),
|
|
|
|
static_cast<float>(atof(splits.at(1).c_str())),
|
|
|
|
static_cast<float>(atof(splits.at(0).c_str())),
|
|
|
|
static_cast<float>(atof(splits.at(1).c_str()))};
|
2021-07-07 18:31:46 +00:00
|
|
|
}
|
|
|
|
else if (splits.size() == 4) {
|
2022-01-16 11:09:55 +00:00
|
|
|
val = glm::vec4 {static_cast<float>(atof(splits.at(0).c_str())),
|
|
|
|
static_cast<float>(atof(splits.at(1).c_str())),
|
|
|
|
static_cast<float>(atof(splits.at(2).c_str())),
|
|
|
|
static_cast<float>(atof(splits.at(3).c_str()))};
|
2021-07-07 18:31:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
element.properties[node.name()] = val;
|
|
|
|
break;
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
2021-07-07 18:31:46 +00:00
|
|
|
case NORMALIZED_PAIR: {
|
|
|
|
size_t divider = str.find(' ');
|
|
|
|
if (divider == std::string::npos)
|
2022-01-23 16:50:51 +00:00
|
|
|
throw error << ": Invalid normalized pair value \"" << str.c_str()
|
|
|
|
<< "\" for property \"" << node.name() << "\"";
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2022-01-16 11:09:55 +00:00
|
|
|
std::string first {str.substr(0, divider)};
|
|
|
|
std::string second {str.substr(divider, std::string::npos)};
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2022-01-16 11:09:55 +00:00
|
|
|
glm::vec2 val {static_cast<float>(atof(first.c_str())),
|
|
|
|
static_cast<float>(atof(second.c_str()))};
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
element.properties[node.name()] = val;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case STRING: {
|
|
|
|
element.properties[node.name()] = str;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case PATH: {
|
2022-01-29 17:41:22 +00:00
|
|
|
std::string path;
|
|
|
|
|
|
|
|
if (!str.empty() && str.front() == ':')
|
|
|
|
path = ResourceManager::getInstance().getResourcePath(str);
|
|
|
|
else
|
|
|
|
path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true);
|
|
|
|
|
2022-01-04 20:21:26 +00:00
|
|
|
if (!ResourceManager::getInstance().fileExists(path)) {
|
2021-07-07 18:31:46 +00:00
|
|
|
std::stringstream ss;
|
2022-10-29 11:04:00 +00:00
|
|
|
// For explicit paths, print a warning if the file couldn't be found, but
|
2022-02-09 17:45:03 +00:00
|
|
|
// only print a debug message if it was set using a variable.
|
|
|
|
if (str == node.text().as_string()) {
|
2022-12-19 19:38:41 +00:00
|
|
|
#if defined(_WIN64)
|
|
|
|
LOG(LogWarning) << Utils::String::replace(error.message, "/", "\\")
|
|
|
|
<< ": Couldn't find file \"" << node.text().get() << "\" "
|
|
|
|
<< ((node.text().get() != path) ?
|
|
|
|
"which resolves to \"" +
|
|
|
|
Utils::String::replace(path, "/", "\\") + "\"" :
|
|
|
|
#else
|
2022-02-09 17:45:03 +00:00
|
|
|
LOG(LogWarning)
|
|
|
|
<< error.message << ": Couldn't find file \"" << node.text().get()
|
|
|
|
<< "\" "
|
|
|
|
<< ((node.text().get() != path) ? "which resolves to \"" + path + "\"" :
|
2022-12-19 19:38:41 +00:00
|
|
|
#endif
|
|
|
|
"")
|
|
|
|
<< " (element type \"" << element.type << "\", name \""
|
|
|
|
<< root.attribute("name").as_string() << "\", property \""
|
|
|
|
<< nodeName << "\")";
|
2022-02-09 17:45:03 +00:00
|
|
|
}
|
2022-10-29 11:04:00 +00:00
|
|
|
else if (!(Settings::getInstance()->getBool("DebugSkipMissingThemeFiles") ||
|
|
|
|
(mCustomCollection &&
|
|
|
|
Settings::getInstance()->getBool(
|
|
|
|
"DebugSkipMissingThemeFilesCustomCollections")))) {
|
2022-12-19 19:38:41 +00:00
|
|
|
#if defined(_WIN64)
|
|
|
|
LOG(LogDebug) << Utils::String::replace(error.message, "/", "\\")
|
|
|
|
<< ": Couldn't find file \"" << node.text().get() << "\" "
|
|
|
|
<< ((node.text().get() != path) ?
|
|
|
|
"which resolves to \"" +
|
|
|
|
Utils::String::replace(path, "/", "\\") + "\"" :
|
|
|
|
#else
|
2022-02-09 17:45:03 +00:00
|
|
|
LOG(LogDebug)
|
|
|
|
<< error.message << ": Couldn't find file \"" << node.text().get()
|
|
|
|
<< "\" "
|
|
|
|
<< ((node.text().get() != path) ? "which resolves to \"" + path + "\"" :
|
2022-12-19 19:38:41 +00:00
|
|
|
#endif
|
|
|
|
"")
|
|
|
|
<< " (element type \"" << element.type << "\", name \""
|
|
|
|
<< root.attribute("name").as_string() << "\", property \""
|
|
|
|
<< nodeName << "\")";
|
2022-02-09 17:45:03 +00:00
|
|
|
}
|
2021-07-07 18:31:46 +00:00
|
|
|
}
|
2022-01-29 17:41:22 +00:00
|
|
|
element.properties[nodeName] = path;
|
2021-07-07 18:31:46 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case COLOR: {
|
2022-01-23 16:50:51 +00:00
|
|
|
try {
|
|
|
|
element.properties[node.name()] = getHexColor(str);
|
|
|
|
}
|
|
|
|
catch (ThemeException& e) {
|
|
|
|
throw error << ": " << e.what();
|
|
|
|
}
|
2021-07-07 18:31:46 +00:00
|
|
|
break;
|
|
|
|
}
|
2022-02-13 14:01:55 +00:00
|
|
|
case UNSIGNED_INTEGER: {
|
|
|
|
unsigned int integerVal {static_cast<unsigned int>(strtoul(str.c_str(), 0, 0))};
|
|
|
|
element.properties[node.name()] = integerVal;
|
|
|
|
break;
|
|
|
|
}
|
2021-07-07 18:31:46 +00:00
|
|
|
case FLOAT: {
|
2022-01-23 16:50:51 +00:00
|
|
|
float floatVal {static_cast<float>(strtod(str.c_str(), 0))};
|
2021-07-07 18:31:46 +00:00
|
|
|
element.properties[node.name()] = floatVal;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case BOOLEAN: {
|
2022-01-29 17:41:22 +00:00
|
|
|
bool boolVal = false;
|
|
|
|
// Only look at the first character.
|
2022-02-09 22:12:12 +00:00
|
|
|
if (str.size() > 0) {
|
|
|
|
if (str.front() == '1' || str.front() == 't' || str.front() == 'T' ||
|
|
|
|
str.front() == 'y' || str.front() == 'Y')
|
|
|
|
boolVal = true;
|
|
|
|
}
|
2021-07-07 18:31:46 +00:00
|
|
|
|
|
|
|
element.properties[node.name()] = boolVal;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
default: {
|
2022-01-23 16:50:51 +00:00
|
|
|
throw error << ": Unknown ElementPropertyType for \""
|
2021-07-07 18:31:46 +00:00
|
|
|
<< root.attribute("name").as_string() << "\", property " << node.name();
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2013-11-12 23:28:15 +00:00
|
|
|
}
|