mirror of
https://github.com/RetroDECK/ES-DE.git
synced 2024-11-30 01:55:39 +00:00
2053 lines
84 KiB
C++
2053 lines
84 KiB
C++
// SPDX-License-Identifier: MIT
|
|
//
|
|
// ES-DE
|
|
// ThemeData.cpp
|
|
//
|
|
// 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.
|
|
//
|
|
|
|
#include "ThemeData.h"
|
|
|
|
#include "Log.h"
|
|
#include "Settings.h"
|
|
#include "components/ImageComponent.h"
|
|
#include "components/TextComponent.h"
|
|
#include "utils/FileSystemUtil.h"
|
|
#include "utils/StringUtil.h"
|
|
|
|
#include <algorithm>
|
|
#include <pugixml.hpp>
|
|
|
|
// clang-format off
|
|
std::vector<std::string> ThemeData::sSupportedViews {
|
|
{"all"},
|
|
{"system"},
|
|
{"gamelist"}};
|
|
|
|
std::vector<std::string> ThemeData::sSupportedMediaTypes {
|
|
{"miximage"},
|
|
{"marquee"},
|
|
{"screenshot"},
|
|
{"titlescreen"},
|
|
{"cover"},
|
|
{"backcover"},
|
|
{"3dbox"},
|
|
{"physicalmedia"},
|
|
{"fanart"},
|
|
{"video"}};
|
|
|
|
std::vector<std::string> ThemeData::sSupportedTransitions {
|
|
{"systemToSystem"},
|
|
{"systemToGamelist"},
|
|
{"gamelistToGamelist"},
|
|
{"gamelistToSystem"},
|
|
{"startupToSystem"},
|
|
{"startupToGamelist"}};
|
|
|
|
std::vector<std::string> ThemeData::sSupportedTransitionAnimations {
|
|
{"builtin-instant"},
|
|
{"builtin-slide"},
|
|
{"builtin-fade"}};
|
|
|
|
std::vector<std::pair<std::string, std::string>> ThemeData::sSupportedFontSizes {
|
|
{"medium", "medium"},
|
|
{"large", "large"},
|
|
{"small", "small"},
|
|
{"x-large", "extra large"},
|
|
{"x-small", "extra small"}};
|
|
|
|
std::vector<std::pair<std::string, std::string>> ThemeData::sSupportedAspectRatios {
|
|
{"automatic", "automatic"},
|
|
{"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"},
|
|
{"19.5:9", "19.5:9"},
|
|
{"19.5:9_vertical", "19.5:9 vertical"},
|
|
{"20:9", "20:9"},
|
|
{"20:9_vertical", "20:9 vertical"},
|
|
{"21:9", "21:9"},
|
|
{"21:9_vertical", "21:9 vertical"},
|
|
{"32:9", "32:0"},
|
|
{"32:9_vertical", "32:9 vertical"},
|
|
{"1:1", "1:1"}};
|
|
|
|
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},
|
|
{"19.5:9", 2.1667f},
|
|
{"19.5:9_vertical", 0.4615f},
|
|
{"20:9", 2.2222f},
|
|
{"20:9_vertical", 0.45f},
|
|
{"21:9", 2.3703f},
|
|
{"21:9_vertical", 0.4219f},
|
|
{"32:9", 3.5555f},
|
|
{"32:9_vertical", 0.2813f},
|
|
{"1:1", 1.0f}};
|
|
|
|
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"}}},
|
|
};
|
|
|
|
std::map<std::string, std::map<std::string, ThemeData::ElementPropertyType>>
|
|
ThemeData::sElementMap {
|
|
{"carousel",
|
|
{{"pos", NORMALIZED_PAIR},
|
|
{"size", NORMALIZED_PAIR},
|
|
{"origin", NORMALIZED_PAIR},
|
|
{"type", STRING},
|
|
{"staticImage", PATH},
|
|
{"imageType", STRING},
|
|
{"defaultImage", PATH},
|
|
{"defaultFolderImage", PATH},
|
|
{"maxItemCount", FLOAT},
|
|
{"itemsBeforeCenter", UNSIGNED_INTEGER},
|
|
{"itemsAfterCenter", UNSIGNED_INTEGER},
|
|
{"itemStacking", STRING},
|
|
{"selectedItemMargins", NORMALIZED_PAIR},
|
|
{"itemSize", NORMALIZED_PAIR},
|
|
{"itemScale", FLOAT},
|
|
{"itemRotation", FLOAT},
|
|
{"itemRotationOrigin", NORMALIZED_PAIR},
|
|
{"itemAxisHorizontal", BOOLEAN},
|
|
{"itemAxisRotation", FLOAT},
|
|
{"imageFit", STRING},
|
|
{"imageInterpolation", STRING},
|
|
{"imageCornerRadius", FLOAT},
|
|
{"imageColor", COLOR},
|
|
{"imageColorEnd", COLOR},
|
|
{"imageGradientType", STRING},
|
|
{"imageSelectedColor", COLOR},
|
|
{"imageSelectedColorEnd", COLOR},
|
|
{"imageSelectedGradientType", STRING},
|
|
{"imageBrightness", FLOAT},
|
|
{"imageSaturation", FLOAT},
|
|
{"itemTransitions", STRING},
|
|
{"itemDiagonalOffset", FLOAT},
|
|
{"itemHorizontalAlignment", STRING},
|
|
{"itemVerticalAlignment", STRING},
|
|
{"wheelHorizontalAlignment", STRING},
|
|
{"wheelVerticalAlignment", STRING},
|
|
{"horizontalOffset", FLOAT},
|
|
{"verticalOffset", FLOAT},
|
|
{"reflections", BOOLEAN},
|
|
{"reflectionsOpacity", FLOAT},
|
|
{"reflectionsFalloff", FLOAT},
|
|
{"unfocusedItemOpacity", FLOAT},
|
|
{"unfocusedItemSaturation", FLOAT},
|
|
{"unfocusedItemDimming", FLOAT},
|
|
{"fastScrolling", BOOLEAN},
|
|
{"color", COLOR},
|
|
{"colorEnd", COLOR},
|
|
{"gradientType", STRING},
|
|
{"text", STRING},
|
|
{"textRelativeScale", FLOAT},
|
|
{"textColor", COLOR},
|
|
{"textBackgroundColor", COLOR},
|
|
{"textSelectedColor", COLOR},
|
|
{"textSelectedBackgroundColor", COLOR},
|
|
{"textHorizontalScrolling", BOOLEAN},
|
|
{"textHorizontalScrollSpeed", FLOAT},
|
|
{"textHorizontalScrollDelay", FLOAT},
|
|
{"textHorizontalScrollGap", FLOAT},
|
|
{"fontPath", PATH},
|
|
{"fontSize", FLOAT},
|
|
{"letterCase", STRING},
|
|
{"letterCaseAutoCollections", STRING},
|
|
{"letterCaseCustomCollections", STRING},
|
|
{"lineSpacing", FLOAT},
|
|
{"systemNameSuffix", BOOLEAN},
|
|
{"letterCaseSystemNameSuffix", STRING},
|
|
{"fadeAbovePrimary", BOOLEAN},
|
|
{"zIndex", FLOAT}}},
|
|
{"grid",
|
|
{{"pos", NORMALIZED_PAIR},
|
|
{"size", NORMALIZED_PAIR},
|
|
{"origin", NORMALIZED_PAIR},
|
|
{"staticImage", PATH},
|
|
{"imageType", STRING},
|
|
{"defaultImage", PATH},
|
|
{"defaultFolderImage", PATH},
|
|
{"itemSize", NORMALIZED_PAIR},
|
|
{"itemScale", FLOAT},
|
|
{"itemSpacing", NORMALIZED_PAIR},
|
|
{"scaleInwards", BOOLEAN},
|
|
{"fractionalRows", BOOLEAN},
|
|
{"itemTransitions", STRING},
|
|
{"rowTransitions", STRING},
|
|
{"unfocusedItemOpacity", FLOAT},
|
|
{"unfocusedItemSaturation", FLOAT},
|
|
{"unfocusedItemDimming", FLOAT},
|
|
{"imageFit", STRING},
|
|
{"imageInterpolation", STRING},
|
|
{"imageRelativeScale", FLOAT},
|
|
{"imageCornerRadius", FLOAT},
|
|
{"imageColor", COLOR},
|
|
{"imageColorEnd", COLOR},
|
|
{"imageGradientType", STRING},
|
|
{"imageSelectedColor", COLOR},
|
|
{"imageSelectedColorEnd", COLOR},
|
|
{"imageSelectedGradientType", STRING},
|
|
{"imageBrightness", FLOAT},
|
|
{"imageSaturation", FLOAT},
|
|
{"backgroundImage", PATH},
|
|
{"backgroundRelativeScale", FLOAT},
|
|
{"backgroundCornerRadius", FLOAT},
|
|
{"backgroundColor", COLOR},
|
|
{"backgroundColorEnd", COLOR},
|
|
{"backgroundGradientType", STRING},
|
|
{"selectorImage", PATH},
|
|
{"selectorRelativeScale", FLOAT},
|
|
{"selectorCornerRadius", FLOAT},
|
|
{"selectorLayer", STRING},
|
|
{"selectorColor", COLOR},
|
|
{"selectorColorEnd", COLOR},
|
|
{"selectorGradientType", STRING},
|
|
{"text", STRING},
|
|
{"textRelativeScale", FLOAT},
|
|
{"textColor", COLOR},
|
|
{"textBackgroundColor", COLOR},
|
|
{"textSelectedColor", COLOR},
|
|
{"textSelectedBackgroundColor", COLOR},
|
|
{"textHorizontalScrolling", BOOLEAN},
|
|
{"textHorizontalScrollSpeed", FLOAT},
|
|
{"textHorizontalScrollDelay", FLOAT},
|
|
{"textHorizontalScrollGap", FLOAT},
|
|
{"fontPath", PATH},
|
|
{"fontSize", FLOAT},
|
|
{"letterCase", STRING},
|
|
{"letterCaseAutoCollections", STRING},
|
|
{"letterCaseCustomCollections", STRING},
|
|
{"lineSpacing", FLOAT},
|
|
{"systemNameSuffix", BOOLEAN},
|
|
{"letterCaseSystemNameSuffix", STRING},
|
|
{"fadeAbovePrimary", BOOLEAN},
|
|
{"zIndex", FLOAT}}},
|
|
{"textlist",
|
|
{{"pos", NORMALIZED_PAIR},
|
|
{"size", NORMALIZED_PAIR},
|
|
{"origin", NORMALIZED_PAIR},
|
|
{"selectorWidth", FLOAT},
|
|
{"selectorHeight", FLOAT},
|
|
{"selectorHorizontalOffset", FLOAT},
|
|
{"selectorVerticalOffset", FLOAT},
|
|
{"selectorColor", COLOR},
|
|
{"selectorColorEnd", COLOR},
|
|
{"selectorGradientType", STRING},
|
|
{"selectorImagePath", PATH},
|
|
{"selectorImageTile", BOOLEAN},
|
|
{"primaryColor", COLOR},
|
|
{"secondaryColor", COLOR},
|
|
{"selectedColor", COLOR},
|
|
{"selectedSecondaryColor", COLOR},
|
|
{"selectedBackgroundColor", COLOR},
|
|
{"selectedSecondaryBackgroundColor", COLOR},
|
|
{"textHorizontalScrolling", BOOLEAN},
|
|
{"textHorizontalScrollSpeed", FLOAT},
|
|
{"textHorizontalScrollDelay", FLOAT},
|
|
{"textHorizontalScrollGap", FLOAT},
|
|
{"fontPath", PATH},
|
|
{"fontSize", FLOAT},
|
|
{"horizontalAlignment", STRING},
|
|
{"horizontalMargin", FLOAT},
|
|
{"letterCase", STRING},
|
|
{"letterCaseAutoCollections", STRING},
|
|
{"letterCaseCustomCollections", STRING},
|
|
{"lineSpacing", FLOAT},
|
|
{"indicators", STRING},
|
|
{"collectionIndicators", STRING},
|
|
{"systemNameSuffix", BOOLEAN},
|
|
{"letterCaseSystemNameSuffix", STRING},
|
|
{"fadeAbovePrimary", BOOLEAN},
|
|
{"zIndex", FLOAT}}},
|
|
{"image",
|
|
{{"pos", NORMALIZED_PAIR},
|
|
{"size", NORMALIZED_PAIR},
|
|
{"cropSize", NORMALIZED_PAIR},
|
|
{"maxSize", NORMALIZED_PAIR},
|
|
{"origin", NORMALIZED_PAIR},
|
|
{"rotation", FLOAT},
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
|
{"stationary", STRING},
|
|
{"renderDuringTransitions", BOOLEAN},
|
|
{"flipHorizontal", BOOLEAN},
|
|
{"flipVertical", BOOLEAN},
|
|
{"path", PATH},
|
|
{"gameOverridePath", PATH},
|
|
{"default", PATH},
|
|
{"imageType", STRING},
|
|
{"metadataElement", BOOLEAN},
|
|
{"gameselector", STRING},
|
|
{"gameselectorEntry", UNSIGNED_INTEGER},
|
|
{"tile", BOOLEAN},
|
|
{"tileSize", NORMALIZED_PAIR},
|
|
{"tileHorizontalAlignment", STRING},
|
|
{"tileVerticalAlignment", STRING},
|
|
{"interpolation", STRING},
|
|
{"cornerRadius", FLOAT},
|
|
{"color", COLOR},
|
|
{"colorEnd", COLOR},
|
|
{"gradientType", STRING},
|
|
{"scrollFadeIn", BOOLEAN},
|
|
{"brightness", FLOAT},
|
|
{"opacity", FLOAT},
|
|
{"saturation", FLOAT},
|
|
{"visible", BOOLEAN},
|
|
{"zIndex", FLOAT}}},
|
|
{"video",
|
|
{{"pos", NORMALIZED_PAIR},
|
|
{"size", NORMALIZED_PAIR},
|
|
{"cropSize", NORMALIZED_PAIR},
|
|
{"maxSize", NORMALIZED_PAIR},
|
|
{"origin", NORMALIZED_PAIR},
|
|
{"rotation", FLOAT},
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
|
{"stationary", STRING},
|
|
{"path", PATH},
|
|
{"default", PATH},
|
|
{"defaultImage", PATH},
|
|
{"imageType", STRING},
|
|
{"metadataElement", BOOLEAN},
|
|
{"gameselector", STRING},
|
|
{"gameselectorEntry", UNSIGNED_INTEGER},
|
|
{"iterationCount", UNSIGNED_INTEGER},
|
|
{"onIterationsDone", STRING},
|
|
{"audio", BOOLEAN},
|
|
{"interpolation", STRING},
|
|
{"imageCornerRadius", FLOAT},
|
|
{"videoCornerRadius", FLOAT},
|
|
{"color", COLOR},
|
|
{"colorEnd", COLOR},
|
|
{"gradientType", STRING},
|
|
{"pillarboxes", BOOLEAN},
|
|
{"pillarboxThreshold", NORMALIZED_PAIR},
|
|
{"scanlines", BOOLEAN},
|
|
{"delay", FLOAT},
|
|
{"fadeInTime", FLOAT},
|
|
{"scrollFadeIn", BOOLEAN},
|
|
{"brightness", FLOAT},
|
|
{"opacity", FLOAT},
|
|
{"saturation", FLOAT},
|
|
{"visible", BOOLEAN},
|
|
{"zIndex", FLOAT}}},
|
|
{"animation",
|
|
{{"pos", NORMALIZED_PAIR},
|
|
{"size", NORMALIZED_PAIR},
|
|
{"maxSize", NORMALIZED_PAIR},
|
|
{"origin", NORMALIZED_PAIR},
|
|
{"rotation", FLOAT},
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
|
{"stationary", STRING},
|
|
{"metadataElement", BOOLEAN},
|
|
{"path", PATH},
|
|
{"speed", FLOAT},
|
|
{"direction", STRING},
|
|
{"iterationCount", UNSIGNED_INTEGER},
|
|
{"interpolation", STRING},
|
|
{"cornerRadius", FLOAT},
|
|
{"color", COLOR},
|
|
{"colorEnd", COLOR},
|
|
{"gradientType", STRING},
|
|
{"brightness", FLOAT},
|
|
{"opacity", FLOAT},
|
|
{"saturation", FLOAT},
|
|
{"visible", BOOLEAN},
|
|
{"zIndex", FLOAT}}},
|
|
{"badges",
|
|
{{"pos", NORMALIZED_PAIR},
|
|
{"size", NORMALIZED_PAIR},
|
|
{"origin", NORMALIZED_PAIR},
|
|
{"rotation", FLOAT},
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
|
{"stationary", STRING},
|
|
{"horizontalAlignment", STRING},
|
|
{"direction", STRING},
|
|
{"lines", UNSIGNED_INTEGER},
|
|
{"itemsPerLine", UNSIGNED_INTEGER},
|
|
{"itemMargin", NORMALIZED_PAIR},
|
|
{"slots", STRING},
|
|
{"controllerPos", NORMALIZED_PAIR},
|
|
{"controllerSize", FLOAT},
|
|
{"customBadgeIcon", PATH},
|
|
{"customControllerIcon", PATH},
|
|
{"folderLinkPos", NORMALIZED_PAIR},
|
|
{"folderLinkSize", FLOAT},
|
|
{"customFolderLinkIcon", PATH},
|
|
{"badgeIconColor", COLOR},
|
|
{"badgeIconColorEnd", COLOR},
|
|
{"badgeIconGradientType", STRING},
|
|
{"controllerIconColor", COLOR},
|
|
{"controllerIconColorEnd", COLOR},
|
|
{"controllerIconGradientType", STRING},
|
|
{"folderLinkIconColor", COLOR},
|
|
{"folderLinkIconColorEnd", COLOR},
|
|
{"folderLinkIconGradientType", STRING},
|
|
{"interpolation", STRING},
|
|
{"opacity", FLOAT},
|
|
{"visible", BOOLEAN},
|
|
{"zIndex", FLOAT}}},
|
|
{"text",
|
|
{{"pos", NORMALIZED_PAIR},
|
|
{"size", NORMALIZED_PAIR},
|
|
{"origin", NORMALIZED_PAIR},
|
|
{"rotation", FLOAT},
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
|
{"stationary", STRING},
|
|
{"text", STRING},
|
|
{"systemdata", STRING},
|
|
{"metadata", STRING},
|
|
{"defaultValue", STRING},
|
|
{"systemNameSuffix", BOOLEAN},
|
|
{"letterCaseSystemNameSuffix", STRING},
|
|
{"metadataElement", BOOLEAN},
|
|
{"gameselector", STRING},
|
|
{"gameselectorEntry", UNSIGNED_INTEGER},
|
|
{"container", BOOLEAN},
|
|
{"containerType", STRING},
|
|
{"containerVerticalSnap", BOOLEAN},
|
|
{"containerScrollSpeed", FLOAT},
|
|
{"containerStartDelay", FLOAT},
|
|
{"containerResetDelay", FLOAT},
|
|
{"containerScrollGap", FLOAT},
|
|
{"fontPath", PATH},
|
|
{"fontSize", FLOAT},
|
|
{"horizontalAlignment", STRING},
|
|
{"verticalAlignment", STRING},
|
|
{"color", COLOR},
|
|
{"backgroundColor", COLOR},
|
|
{"letterCase", STRING},
|
|
{"lineSpacing", FLOAT},
|
|
{"opacity", FLOAT},
|
|
{"visible", BOOLEAN},
|
|
{"zIndex", FLOAT}}},
|
|
{"datetime",
|
|
{{"pos", NORMALIZED_PAIR},
|
|
{"size", NORMALIZED_PAIR},
|
|
{"origin", NORMALIZED_PAIR},
|
|
{"rotation", FLOAT},
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
|
{"stationary", STRING},
|
|
{"metadata", STRING},
|
|
{"defaultValue", STRING},
|
|
{"gameselector", STRING},
|
|
{"gameselectorEntry", UNSIGNED_INTEGER},
|
|
{"fontPath", PATH},
|
|
{"fontSize", FLOAT},
|
|
{"horizontalAlignment", STRING},
|
|
{"verticalAlignment", STRING},
|
|
{"color", COLOR},
|
|
{"backgroundColor", COLOR},
|
|
{"letterCase", STRING},
|
|
{"lineSpacing", FLOAT},
|
|
{"format", STRING},
|
|
{"displayRelative", BOOLEAN},
|
|
{"opacity", FLOAT},
|
|
{"visible", BOOLEAN},
|
|
{"zIndex", FLOAT}}},
|
|
{"gamelistinfo",
|
|
{{"pos", NORMALIZED_PAIR},
|
|
{"size", NORMALIZED_PAIR},
|
|
{"origin", NORMALIZED_PAIR},
|
|
{"rotation", FLOAT},
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
|
{"stationary", STRING},
|
|
{"fontPath", PATH},
|
|
{"fontSize", FLOAT},
|
|
{"horizontalAlignment", STRING},
|
|
{"verticalAlignment", STRING},
|
|
{"color", COLOR},
|
|
{"backgroundColor", COLOR},
|
|
{"opacity", FLOAT},
|
|
{"visible", BOOLEAN},
|
|
{"zIndex", FLOAT}}},
|
|
{"rating",
|
|
{{"pos", NORMALIZED_PAIR},
|
|
{"size", NORMALIZED_PAIR},
|
|
{"origin", NORMALIZED_PAIR},
|
|
{"rotation", FLOAT},
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
|
{"stationary", STRING},
|
|
{"hideIfZero", BOOLEAN},
|
|
{"gameselector", STRING},
|
|
{"gameselectorEntry", UNSIGNED_INTEGER},
|
|
{"interpolation", STRING},
|
|
{"color", COLOR},
|
|
{"filledPath", PATH},
|
|
{"unfilledPath", PATH},
|
|
{"overlay", BOOLEAN},
|
|
{"opacity", FLOAT},
|
|
{"visible", BOOLEAN},
|
|
{"zIndex", FLOAT}}},
|
|
{"gameselector",
|
|
{{"selection", STRING},
|
|
{"gameCount", UNSIGNED_INTEGER},
|
|
{"allowDuplicates", BOOLEAN}}},
|
|
{"helpsystem",
|
|
{{"pos", NORMALIZED_PAIR},
|
|
{"posDimmed", NORMALIZED_PAIR},
|
|
{"origin", NORMALIZED_PAIR},
|
|
{"originDimmed", NORMALIZED_PAIR},
|
|
{"textColor", COLOR},
|
|
{"textColorDimmed", COLOR},
|
|
{"iconColor", COLOR},
|
|
{"iconColorDimmed", COLOR},
|
|
{"fontPath", PATH},
|
|
{"fontSize", FLOAT},
|
|
{"fontSizeDimmed", FLOAT},
|
|
{"entrySpacing", FLOAT},
|
|
{"entrySpacingDimmed", FLOAT},
|
|
{"iconTextSpacing", FLOAT},
|
|
{"iconTextSpacingDimmed", FLOAT},
|
|
{"letterCase", STRING},
|
|
{"opacity", FLOAT},
|
|
{"opacityDimmed", FLOAT},
|
|
{"customButtonIcon", PATH}}},
|
|
{"sound",
|
|
{{"path", PATH}}}};
|
|
// clang-format on
|
|
|
|
ThemeData::ThemeData()
|
|
: mCustomCollection {false}
|
|
{
|
|
sCurrentTheme = sThemes.find(Settings::getInstance()->getString("Theme"));
|
|
sVariantDefinedTransitions = "";
|
|
}
|
|
|
|
void ThemeData::loadFile(const std::map<std::string, std::string>& sysDataMap,
|
|
const std::string& path,
|
|
const ThemeTriggers::TriggerType trigger,
|
|
const bool customCollection)
|
|
{
|
|
mCustomCollection = customCollection;
|
|
mOverrideVariant = "";
|
|
|
|
mPaths.push_back(path);
|
|
|
|
ThemeException error;
|
|
error << "ThemeData::loadFile(): ";
|
|
error.setFiles(mPaths);
|
|
|
|
if (!Utils::FileSystem::exists(path))
|
|
throw error << "File does not exist";
|
|
|
|
mViews.clear();
|
|
mVariables.clear();
|
|
|
|
mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend());
|
|
|
|
pugi::xml_document doc;
|
|
#if defined(_WIN64)
|
|
pugi::xml_parse_result res {doc.load_file(Utils::String::stringToWideString(path).c_str())};
|
|
#else
|
|
pugi::xml_parse_result res {doc.load_file(path.c_str())};
|
|
#endif
|
|
if (!res)
|
|
throw error << ": XML parsing error: " << res.description();
|
|
|
|
pugi::xml_node root {doc.child("theme")};
|
|
if (!root)
|
|
throw error << ": Missing <theme> tag";
|
|
|
|
// Check if there's an unsupported theme version tag.
|
|
if (root.child("formatVersion") != nullptr)
|
|
throw error << ": Unsupported <formatVersion> tag found";
|
|
|
|
if (sCurrentTheme->second.capabilities.variants.size() > 0) {
|
|
for (auto& variant : sCurrentTheme->second.capabilities.variants)
|
|
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) {
|
|
auto overrides = getCurrentThemeSelectedVariantOverrides();
|
|
if (overrides.find(trigger) != overrides.end())
|
|
mOverrideVariant = overrides.at(trigger).first;
|
|
}
|
|
}
|
|
|
|
if (sCurrentTheme->second.capabilities.colorSchemes.size() > 0) {
|
|
for (auto& colorScheme : sCurrentTheme->second.capabilities.colorSchemes)
|
|
mColorSchemes.emplace_back(colorScheme.name);
|
|
|
|
if (std::find(mColorSchemes.cbegin(), mColorSchemes.cend(),
|
|
Settings::getInstance()->getString("ThemeColorScheme")) !=
|
|
mColorSchemes.cend())
|
|
mSelectedColorScheme = Settings::getInstance()->getString("ThemeColorScheme");
|
|
else
|
|
mSelectedColorScheme = mColorSchemes.front();
|
|
}
|
|
|
|
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();
|
|
}
|
|
|
|
sAspectRatioMatch = false;
|
|
|
|
if (sCurrentTheme->second.capabilities.aspectRatios.size() > 0) {
|
|
if (std::find(sCurrentTheme->second.capabilities.aspectRatios.cbegin(),
|
|
sCurrentTheme->second.capabilities.aspectRatios.cend(),
|
|
Settings::getInstance()->getString("ThemeAspectRatio")) !=
|
|
sCurrentTheme->second.capabilities.aspectRatios.cend())
|
|
sSelectedAspectRatio = Settings::getInstance()->getString("ThemeAspectRatio");
|
|
else
|
|
sSelectedAspectRatio = sCurrentTheme->second.capabilities.aspectRatios.front();
|
|
|
|
if (sSelectedAspectRatio == "automatic") {
|
|
// Auto-detect the closest aspect ratio based on what's available in the theme config.
|
|
sSelectedAspectRatio = "16:9";
|
|
const float screenAspectRatio {Renderer::getScreenAspectRatio()};
|
|
float diff {std::fabs(sAspectRatioMap["16:9"] - screenAspectRatio)};
|
|
|
|
for (auto& aspectRatio : sCurrentTheme->second.capabilities.aspectRatios) {
|
|
if (aspectRatio == "automatic")
|
|
continue;
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
parseVariables(root);
|
|
parseColorSchemes(root);
|
|
parseFontSizes(root);
|
|
parseIncludes(root);
|
|
parseViews(root);
|
|
if (root.child("feature") != nullptr)
|
|
throw error << ": Unsupported <feature> tag found";
|
|
parseVariants(root);
|
|
parseAspectRatios(root);
|
|
}
|
|
|
|
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;
|
|
|
|
// If expectedType is an empty string, then skip type checking.
|
|
if (elemIt->second.type != expectedType && !expectedType.empty()) {
|
|
LOG(LogWarning) << "ThemeData::getElement(): Requested element \"" << view << "." << element
|
|
<< "\" has the wrong type, expected \"" << expectedType << "\", got \""
|
|
<< elemIt->second.type << "\"";
|
|
return nullptr;
|
|
}
|
|
|
|
return &elemIt->second;
|
|
}
|
|
|
|
void ThemeData::populateThemes()
|
|
{
|
|
sThemes.clear();
|
|
LOG(LogInfo) << "Checking for available themes...";
|
|
|
|
// 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.
|
|
#if defined(__ANDROID__)
|
|
const std::string userThemeDirectory {Utils::FileSystem::getInternalAppDataDirectory() +
|
|
"/themes"};
|
|
#else
|
|
const std::string defaultUserThemeDir {Utils::FileSystem::getAppDataDirectory() + "/themes"};
|
|
const std::string userThemeDirSetting {Utils::FileSystem::expandHomePath(
|
|
Settings::getInstance()->getString("UserThemeDirectory"))};
|
|
std::string userThemeDirectory;
|
|
|
|
if (userThemeDirSetting.empty()) {
|
|
userThemeDirectory = defaultUserThemeDir;
|
|
}
|
|
else if (Utils::FileSystem::isDirectory(userThemeDirSetting) ||
|
|
Utils::FileSystem::isSymlink(userThemeDirSetting)) {
|
|
userThemeDirectory = userThemeDirSetting;
|
|
#if defined(_WIN64)
|
|
LOG(LogInfo) << "Setting user theme directory to \""
|
|
<< Utils::String::replace(userThemeDirectory, "/", "\\") << "\"";
|
|
#else
|
|
LOG(LogInfo) << "Setting user theme directory to \"" << userThemeDirectory << "\"";
|
|
#endif
|
|
}
|
|
else {
|
|
LOG(LogWarning) << "Requested user theme directory \"" << userThemeDirSetting
|
|
<< "\" does not exist or is not a directory, reverting to \""
|
|
<< defaultUserThemeDir << "\"";
|
|
userThemeDirectory = defaultUserThemeDir;
|
|
}
|
|
#endif
|
|
|
|
#if defined(__ANDROID__)
|
|
const std::vector<std::string> themePaths {Utils::FileSystem::getProgramDataPath() + "/themes",
|
|
Utils::FileSystem::getAppDataDirectory() + "/themes",
|
|
userThemeDirectory};
|
|
#elif defined(__APPLE__)
|
|
const std::vector<std::string> themePaths {
|
|
Utils::FileSystem::getExePath() + "/themes",
|
|
Utils::FileSystem::getExePath() + "/../Resources/themes", userThemeDirectory};
|
|
#elif defined(_WIN64) || defined(APPIMAGE_BUILD)
|
|
const std::vector<std::string> themePaths {Utils::FileSystem::getExePath() + "/themes",
|
|
userThemeDirectory};
|
|
#else
|
|
const std::vector<std::string> themePaths {Utils::FileSystem::getExePath() + "/themes",
|
|
Utils::FileSystem::getProgramDataPath() + "/themes",
|
|
userThemeDirectory};
|
|
#endif
|
|
|
|
for (auto path : themePaths) {
|
|
if (!Utils::FileSystem::isDirectory(path))
|
|
continue;
|
|
|
|
Utils::FileSystem::StringList dirContent {Utils::FileSystem::getDirContent(path)};
|
|
|
|
for (Utils::FileSystem::StringList::const_iterator it = dirContent.cbegin();
|
|
it != dirContent.cend(); ++it) {
|
|
if (Utils::FileSystem::isDirectory(*it)) {
|
|
const std::string themeDirName {Utils::FileSystem::getFileName(*it)};
|
|
if (themeDirName == "themes-list" ||
|
|
(themeDirName.length() >= 8 &&
|
|
Utils::String::toLower(themeDirName.substr(themeDirName.length() - 8, 8)) ==
|
|
"disabled"))
|
|
continue;
|
|
#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))};
|
|
|
|
if (!capabilities.validTheme)
|
|
continue;
|
|
|
|
std::string themeName;
|
|
if (capabilities.themeName != "")
|
|
themeName.append(" (\"").append(capabilities.themeName).append("\")");
|
|
|
|
#if defined(_WIN64)
|
|
LOG(LogInfo) << "Added theme \"" << Utils::String::replace(*it, "/", "\\") << "\""
|
|
<< themeName;
|
|
#else
|
|
LOG(LogInfo) << "Added theme \"" << *it << "\"" << themeName;
|
|
#endif
|
|
int aspectRatios {0};
|
|
if (capabilities.aspectRatios.size() > 0)
|
|
aspectRatios = static_cast<int>(capabilities.aspectRatios.size()) - 1;
|
|
LOG(LogDebug) << "Theme includes support for " << capabilities.variants.size()
|
|
<< " variant" << (capabilities.variants.size() != 1 ? "s" : "")
|
|
<< ", " << capabilities.colorSchemes.size() << " color scheme"
|
|
<< (capabilities.colorSchemes.size() != 1 ? "s" : "") << ", "
|
|
<< capabilities.fontSizes.size() << " font size"
|
|
<< (capabilities.fontSizes.size() != 1 ? "s" : "") << ", "
|
|
<< aspectRatios << " aspect ratio" << (aspectRatios != 1 ? "s" : "")
|
|
<< " and " << capabilities.transitions.size() << " transition"
|
|
<< (capabilities.transitions.size() != 1 ? "s" : "");
|
|
|
|
Theme theme {*it, capabilities};
|
|
sThemes[theme.getName()] = theme;
|
|
}
|
|
}
|
|
}
|
|
|
|
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();
|
|
}
|
|
}
|
|
|
|
const std::string ThemeData::getSystemThemeFile(const std::string& system)
|
|
{
|
|
if (sThemes.empty())
|
|
getThemes();
|
|
|
|
if (sThemes.empty())
|
|
return "";
|
|
|
|
std::map<std::string, Theme, StringComparator>::const_iterator theme {
|
|
sThemes.find(Settings::getInstance()->getString("Theme"))};
|
|
if (theme == sThemes.cend()) {
|
|
// Currently configured theme is missing, attempt to load the default theme linear-es-de
|
|
// instead, and if that's also missing then pick the first available one.
|
|
bool defaultSetFound {true};
|
|
|
|
theme = sThemes.find("linear-es-de");
|
|
|
|
if (theme == sThemes.cend()) {
|
|
theme = sThemes.cbegin();
|
|
defaultSetFound = false;
|
|
}
|
|
|
|
LOG(LogWarning) << "Configured theme \"" << Settings::getInstance()->getString("Theme")
|
|
<< "\" does not exist, loading" << (defaultSetFound ? " default " : " ")
|
|
<< "theme \"" << theme->first << "\" instead";
|
|
|
|
Settings::getInstance()->setString("Theme", theme->first);
|
|
sCurrentTheme = sThemes.find(Settings::getInstance()->getString("Theme"));
|
|
}
|
|
|
|
return theme->second.getThemePath(system);
|
|
}
|
|
|
|
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";
|
|
}
|
|
|
|
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";
|
|
}
|
|
|
|
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);
|
|
|
|
const std::string& transitionsSetting {Settings::getInstance()->getString("ThemeTransitions")};
|
|
std::string profile;
|
|
size_t profileEntry {0};
|
|
|
|
if (transitionsSetting == "automatic") {
|
|
if (sVariantDefinedTransitions != "")
|
|
profile = sVariantDefinedTransitions;
|
|
else if (!sCurrentTheme->second.capabilities.transitions.empty())
|
|
profile = sCurrentTheme->second.capabilities.transitions.front().name;
|
|
}
|
|
else {
|
|
profile = transitionsSetting;
|
|
}
|
|
|
|
auto it = std::find_if(
|
|
sCurrentTheme->second.capabilities.transitions.cbegin(),
|
|
sCurrentTheme->second.capabilities.transitions.cend(),
|
|
[&profile](const ThemeTransitions transitions) { return transitions.name == profile; });
|
|
if (it != sCurrentTheme->second.capabilities.transitions.cend())
|
|
profileEntry = static_cast<size_t>(
|
|
std::distance(sCurrentTheme->second.capabilities.transitions.cbegin(), it) + 1);
|
|
|
|
if (profileEntry != 0 &&
|
|
sCurrentTheme->second.capabilities.transitions.size() > profileEntry - 1) {
|
|
auto transitionMap =
|
|
sCurrentTheme->second.capabilities.transitions[profileEntry - 1].animations;
|
|
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") {
|
|
if (std::find(sCurrentTheme->second.capabilities.suppressedTransitionProfiles.cbegin(),
|
|
sCurrentTheme->second.capabilities.suppressedTransitionProfiles.cend(),
|
|
transitionsSetting) ==
|
|
sCurrentTheme->second.capabilities.suppressedTransitionProfiles.cend()) {
|
|
if (transitionsSetting == "builtin-slide") {
|
|
transitionAnim = static_cast<int>(ViewTransitionAnimation::SLIDE);
|
|
}
|
|
else if (transitionsSetting == "builtin-fade") {
|
|
transitionAnim = static_cast<int>(ViewTransitionAnimation::FADE);
|
|
}
|
|
setTransitionsFunc(transitionAnim);
|
|
}
|
|
}
|
|
}
|
|
|
|
const std::map<ThemeTriggers::TriggerType, std::pair<std::string, std::vector<std::string>>>
|
|
ThemeData::getCurrentThemeSelectedVariantOverrides()
|
|
{
|
|
const auto variantIter = std::find_if(
|
|
sCurrentTheme->second.capabilities.variants.cbegin(),
|
|
sCurrentTheme->second.capabilities.variants.cend(),
|
|
[this](ThemeVariant currVariant) { return currVariant.name == mSelectedVariant; });
|
|
|
|
if (variantIter != sCurrentTheme->second.capabilities.variants.cend() &&
|
|
!(*variantIter).overrides.empty())
|
|
return (*variantIter).overrides;
|
|
else
|
|
return ThemeVariant().overrides;
|
|
}
|
|
|
|
const void ThemeData::themeLoadedLogOutput()
|
|
{
|
|
LOG(LogInfo) << "Finished loading theme \"" << sCurrentTheme->first << "\"";
|
|
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, "_", " ") << "\"";
|
|
}
|
|
}
|
|
|
|
unsigned int ThemeData::getHexColor(const std::string& str)
|
|
{
|
|
ThemeException error;
|
|
|
|
if (str == "")
|
|
throw error << "Empty color property";
|
|
|
|
const size_t length {str.size()};
|
|
if (length != 6 && length != 8)
|
|
throw error << "Invalid color property \"" << str
|
|
<< "\" (must be 6 or 8 characters in length)";
|
|
|
|
unsigned int value;
|
|
std::stringstream ss;
|
|
ss << str;
|
|
ss >> std::hex >> value;
|
|
|
|
if (length == 6)
|
|
value = (value << 8) | 0xFF;
|
|
|
|
return value;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string& path)
|
|
{
|
|
ThemeCapability capabilities;
|
|
std::vector<std::string> aspectRatiosTemp;
|
|
std::vector<std::string> fontSizesTemp;
|
|
bool hasTriggers {false};
|
|
|
|
const std::string capFile {path + "/capabilities.xml"};
|
|
|
|
if (Utils::FileSystem::isRegularFile(capFile) || Utils::FileSystem::isSymlink(capFile)) {
|
|
capabilities.validTheme = true;
|
|
|
|
pugi::xml_document doc;
|
|
#if defined(_WIN64)
|
|
const pugi::xml_parse_result& res {
|
|
doc.load_file(Utils::String::stringToWideString(capFile).c_str())};
|
|
#else
|
|
const pugi::xml_parse_result& res {doc.load_file(capFile.c_str())};
|
|
#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;
|
|
}
|
|
const pugi::xml_node& themeCapabilities {doc.child("themeCapabilities")};
|
|
if (!themeCapabilities) {
|
|
LOG(LogError) << "Missing <themeCapabilities> tag in capabilities.xml";
|
|
return capabilities;
|
|
}
|
|
|
|
const pugi::xml_node& themeName {themeCapabilities.child("themeName")};
|
|
if (themeName != nullptr)
|
|
capabilities.themeName = themeName.text().get();
|
|
|
|
for (pugi::xml_node aspectRatio {themeCapabilities.child("aspectRatio")}; aspectRatio;
|
|
aspectRatio = aspectRatio.next_sibling("aspectRatio")) {
|
|
const std::string& value {aspectRatio.text().get()};
|
|
if (std::find_if(sSupportedAspectRatios.cbegin(), sSupportedAspectRatios.cend(),
|
|
[&value](const std::pair<std::string, std::string>& entry) {
|
|
return entry.first == value;
|
|
}) == sSupportedAspectRatios.cend()) {
|
|
LOG(LogWarning) << "Declared aspect ratio \"" << value
|
|
<< "\" is not supported, ignoring entry in \"" << capFile << "\"";
|
|
}
|
|
else {
|
|
if (std::find(aspectRatiosTemp.cbegin(), aspectRatiosTemp.cend(), value) !=
|
|
aspectRatiosTemp.cend()) {
|
|
LOG(LogWarning)
|
|
<< "Aspect ratio \"" << value
|
|
<< "\" is declared multiple times, ignoring entry in \"" << capFile << "\"";
|
|
}
|
|
else {
|
|
aspectRatiosTemp.emplace_back(value);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
for (pugi::xml_node variant {themeCapabilities.child("variant")}; variant;
|
|
variant = variant.next_sibling("variant")) {
|
|
ThemeVariant readVariant;
|
|
const std::string& name {variant.attribute("name").as_string()};
|
|
if (name.empty()) {
|
|
LOG(LogWarning)
|
|
<< "Found <variant> tag without name attribute, ignoring entry in \"" << capFile
|
|
<< "\"";
|
|
}
|
|
else if (name == "all") {
|
|
LOG(LogWarning)
|
|
<< "Found <variant> tag using reserved name \"all\", ignoring entry in \""
|
|
<< capFile << "\"";
|
|
}
|
|
else {
|
|
readVariant.name = name;
|
|
}
|
|
|
|
const pugi::xml_node& labelTag {variant.child("label")};
|
|
if (labelTag == nullptr) {
|
|
LOG(LogDebug)
|
|
<< "No variant <label> tag found, setting label value to the variant name \""
|
|
<< name << "\" for \"" << capFile << "\"";
|
|
readVariant.label = name;
|
|
}
|
|
else {
|
|
const std::string& labelValue {labelTag.text().as_string()};
|
|
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;
|
|
}
|
|
}
|
|
|
|
const pugi::xml_node& selectableTag {variant.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')
|
|
readVariant.selectable = false;
|
|
else
|
|
readVariant.selectable = true;
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
LOG(LogWarning)
|
|
<< "Found an <override> tag without a corresponding "
|
|
"<useVariant> tag, "
|
|
<< "ignoring entry for variant \"" << readVariant.name
|
|
<< "\" in \"" << capFile << "\"";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
LOG(LogWarning)
|
|
<< "Found an <override> tag without a corresponding <trigger> tag, "
|
|
<< "ignoring entry for variant \"" << readVariant.name << "\" in \""
|
|
<< 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);
|
|
}
|
|
}
|
|
|
|
for (pugi::xml_node colorScheme {themeCapabilities.child("colorScheme")}; colorScheme;
|
|
colorScheme = colorScheme.next_sibling("colorScheme")) {
|
|
ThemeColorScheme readColorScheme;
|
|
const std::string& name {colorScheme.attribute("name").as_string()};
|
|
if (name.empty()) {
|
|
LOG(LogWarning)
|
|
<< "Found <colorScheme> tag without name attribute, ignoring entry in \""
|
|
<< capFile << "\"";
|
|
}
|
|
else {
|
|
readColorScheme.name = name;
|
|
}
|
|
|
|
const pugi::xml_node& labelTag {colorScheme.child("label")};
|
|
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 {
|
|
const std::string& labelValue {labelTag.text().as_string()};
|
|
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);
|
|
}
|
|
}
|
|
|
|
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()) {
|
|
// 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];
|
|
}
|
|
|
|
ThemeTransitions transition;
|
|
transition.name = name;
|
|
transition.label = label;
|
|
transition.selectable = selectable;
|
|
transition.animations = std::move(readTransitions);
|
|
capabilities.transitions.emplace_back(std::move(transition));
|
|
}
|
|
}
|
|
|
|
for (pugi::xml_node suppressTransitionProfiles {
|
|
themeCapabilities.child("suppressTransitionProfiles")};
|
|
suppressTransitionProfiles;
|
|
suppressTransitionProfiles =
|
|
suppressTransitionProfiles.next_sibling("suppressTransitionProfiles")) {
|
|
std::vector<std::string> readSuppressProfiles;
|
|
|
|
for (pugi::xml_node entries {suppressTransitionProfiles.child("entry")}; entries;
|
|
entries = entries.next_sibling("entry")) {
|
|
const std::string& entryValue {entries.text().as_string()};
|
|
|
|
if (std::find(sSupportedTransitionAnimations.cbegin(),
|
|
sSupportedTransitionAnimations.cend(),
|
|
entryValue) != sSupportedTransitionAnimations.cend()) {
|
|
capabilities.suppressedTransitionProfiles.emplace_back(entryValue);
|
|
}
|
|
else {
|
|
LOG(LogWarning)
|
|
<< "Found suppressTransitionProfiles <entry> tag with invalid value \""
|
|
<< entryValue << "\", ignoring entry in \"" << capFile << "\"";
|
|
}
|
|
}
|
|
|
|
// Sort and remove any duplicates.
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
capabilities.validTheme = false;
|
|
LOG(LogWarning)
|
|
<< "No capabilities.xml file found, this does not appear to be a valid theme: \""
|
|
#if defined(_WIN64)
|
|
<< Utils::String::replace(path, "/", "\\") << "\"";
|
|
#else
|
|
<< path << "\"";
|
|
#endif
|
|
}
|
|
|
|
// 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()) {
|
|
// Add the "automatic" aspect ratio if there is at least one entry.
|
|
if (!aspectRatiosTemp.empty())
|
|
capabilities.aspectRatios.emplace_back(sSupportedAspectRatios.front().first);
|
|
for (auto& aspectRatio : sSupportedAspectRatios) {
|
|
if (std::find(aspectRatiosTemp.cbegin(), aspectRatiosTemp.cend(), aspectRatio.first) !=
|
|
aspectRatiosTemp.cend()) {
|
|
capabilities.aspectRatios.emplace_back(aspectRatio.first);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return capabilities;
|
|
}
|
|
|
|
void ThemeData::parseIncludes(const pugi::xml_node& root)
|
|
{
|
|
for (pugi::xml_node node {root.child("include")}; node; node = node.next_sibling("include")) {
|
|
ThemeException error;
|
|
error << "ThemeData::parseIncludes(): ";
|
|
error.setFiles(mPaths);
|
|
|
|
// Check if there's an unsupported theme version tag.
|
|
if (root.child("formatVersion") != nullptr)
|
|
throw error << ": Unsupported <formatVersion> tag found";
|
|
|
|
std::string relPath {resolvePlaceholders(node.text().as_string())};
|
|
std::string path {Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true)};
|
|
|
|
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")))) {
|
|
#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
|
|
LOG(LogDebug) << error.message << ": Couldn't find file \"" << node.text().get()
|
|
<< "\" "
|
|
<< ((node.text().get() != path) ?
|
|
"which resolves to \"" + path + "\"" :
|
|
#endif
|
|
"");
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
|
|
error << " -> \"" << relPath << "\"";
|
|
|
|
mPaths.push_back(path);
|
|
|
|
pugi::xml_document includeDoc;
|
|
#if defined(_WIN64)
|
|
pugi::xml_parse_result result {
|
|
includeDoc.load_file(Utils::String::stringToWideString(path).c_str())};
|
|
#else
|
|
pugi::xml_parse_result result {includeDoc.load_file(path.c_str())};
|
|
#endif
|
|
if (!result)
|
|
throw error << ": Error parsing file: " << result.description();
|
|
|
|
pugi::xml_node theme {includeDoc.child("theme")};
|
|
if (!theme)
|
|
throw error << ": Missing <theme> tag";
|
|
|
|
parseTransitions(theme);
|
|
parseVariables(theme);
|
|
parseColorSchemes(theme);
|
|
parseFontSizes(theme);
|
|
parseIncludes(theme);
|
|
parseViews(theme);
|
|
if (theme.child("feature") != nullptr)
|
|
throw error << ": Unsupported <feature> tag found";
|
|
parseVariants(theme);
|
|
parseAspectRatios(theme);
|
|
|
|
mPaths.pop_back();
|
|
}
|
|
}
|
|
|
|
void ThemeData::parseVariants(const pugi::xml_node& root)
|
|
{
|
|
if (sCurrentTheme == sThemes.end())
|
|
return;
|
|
|
|
if (mSelectedVariant == "")
|
|
return;
|
|
|
|
ThemeException error;
|
|
error << "ThemeData::parseVariants(): ";
|
|
error.setFiles(mPaths);
|
|
|
|
for (pugi::xml_node node {root.child("variant")}; node; node = node.next_sibling("variant")) {
|
|
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";
|
|
}
|
|
|
|
const std::string variant {mOverrideVariant.empty() ? mSelectedVariant :
|
|
mOverrideVariant};
|
|
|
|
if (variant == viewKey || viewKey == "all") {
|
|
parseTransitions(node);
|
|
parseVariables(node);
|
|
parseColorSchemes(node);
|
|
parseFontSizes(node);
|
|
parseIncludes(node);
|
|
parseViews(node);
|
|
parseAspectRatios(node);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ThemeData::parseColorSchemes(const pugi::xml_node& root)
|
|
{
|
|
if (sCurrentTheme == sThemes.end())
|
|
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";
|
|
}
|
|
|
|
if (mSelectedColorScheme == viewKey)
|
|
parseVariables(node);
|
|
}
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ThemeData::parseAspectRatios(const pugi::xml_node& root)
|
|
{
|
|
if (sCurrentTheme == sThemes.end())
|
|
return;
|
|
|
|
if (sSelectedAspectRatio == "")
|
|
return;
|
|
|
|
ThemeException error;
|
|
error << "ThemeData::parseAspectRatios(): ";
|
|
error.setFiles(mPaths);
|
|
|
|
for (pugi::xml_node node {root.child("aspectRatio")}; node;
|
|
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);
|
|
|
|
if (std::find(sCurrentTheme->second.capabilities.aspectRatios.cbegin(),
|
|
sCurrentTheme->second.capabilities.aspectRatios.cend(),
|
|
viewKey) == sCurrentTheme->second.capabilities.aspectRatios.cend()) {
|
|
throw error << ": <aspectRatio> value \"" << viewKey
|
|
<< "\" is not defined in capabilities.xml";
|
|
}
|
|
|
|
if (sSelectedAspectRatio == viewKey) {
|
|
parseVariables(node);
|
|
parseColorSchemes(node);
|
|
parseFontSizes(node);
|
|
parseIncludes(node);
|
|
parseViews(node);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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()};
|
|
if (std::find_if(sCurrentTheme->second.capabilities.transitions.cbegin(),
|
|
sCurrentTheme->second.capabilities.transitions.cend(),
|
|
[&transitionsValue](const ThemeTransitions transitions) {
|
|
return transitions.name == transitionsValue;
|
|
}) == sCurrentTheme->second.capabilities.transitions.cend()) {
|
|
throw error << ": <transitions> value \"" << transitionsValue
|
|
<< "\" is not matching any defined transitions";
|
|
}
|
|
sVariantDefinedTransitions = transitionsValue;
|
|
}
|
|
}
|
|
|
|
void ThemeData::parseVariables(const pugi::xml_node& root)
|
|
{
|
|
ThemeException error;
|
|
error.setFiles(mPaths);
|
|
|
|
for (pugi::xml_node node {root.child("variables")}; node;
|
|
node = node.next_sibling("variables")) {
|
|
|
|
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())};
|
|
|
|
if (!val.empty()) {
|
|
if (mVariables.find(key) != mVariables.end())
|
|
mVariables[key] = val;
|
|
else
|
|
mVariables.insert(std::pair<std::string, std::string>(key, val));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ThemeData::parseViews(const pugi::xml_node& root)
|
|
{
|
|
ThemeException error;
|
|
error << "ThemeData::parseViews(): ";
|
|
error.setFiles(mPaths);
|
|
|
|
// Parse views.
|
|
for (pugi::xml_node node {root.child("view")}; node; node = node.next_sibling("view")) {
|
|
if (!node.attribute("name"))
|
|
throw error << ": View 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(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) !=
|
|
sSupportedViews.cend()) {
|
|
ThemeView& view {
|
|
mViews.insert(std::pair<std::string, ThemeView>(viewKey, ThemeView()))
|
|
.first->second};
|
|
parseView(node, view);
|
|
}
|
|
else {
|
|
throw error << ": Unsupported \"" << viewKey << "\" view style defined";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view)
|
|
{
|
|
ThemeException error;
|
|
error << "ThemeData::parseView(): ";
|
|
error.setFiles(mPaths);
|
|
|
|
for (pugi::xml_node node {root.first_child()}; node; node = node.next_sibling()) {
|
|
if (!node.attribute("name"))
|
|
throw error << ": Element of type \"" << node.name() << "\" missing \"name\" attribute";
|
|
|
|
auto elemTypeIt = sElementMap.find(node.name());
|
|
if (elemTypeIt == sElementMap.cend())
|
|
throw error << ": Unknown element type \"" << node.name() << "\"";
|
|
|
|
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)};
|
|
while (off != std::string::npos || prevOff != std::string::npos) {
|
|
std::string elemKey {nameAttr.substr(prevOff, off - prevOff)};
|
|
prevOff = nameAttr.find_first_not_of(delim, off);
|
|
off = nameAttr.find_first_of(delim, prevOff);
|
|
|
|
// Add the element type as a prefix to avoid name collisions between different
|
|
// component types.
|
|
elemKey = std::string {node.name()} + "_" + elemKey;
|
|
|
|
parseElement(
|
|
node, elemTypeIt->second,
|
|
view.elements.insert(std::pair<std::string, ThemeElement>(elemKey, ThemeElement()))
|
|
.first->second);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ThemeData::parseElement(const pugi::xml_node& root,
|
|
const std::map<std::string, ElementPropertyType>& typeMap,
|
|
ThemeElement& element)
|
|
{
|
|
ThemeException error;
|
|
error << "ThemeData::parseElement(): ";
|
|
error.setFiles(mPaths);
|
|
element.type = root.name();
|
|
|
|
if (root.attribute("extra") != nullptr)
|
|
throw error << ": Unsupported \"extra\" attribute found for element of type \""
|
|
<< element.type << "\"";
|
|
|
|
for (pugi::xml_node node {root.first_child()}; node; node = node.next_sibling()) {
|
|
auto typeIt = typeMap.find(node.name());
|
|
if (typeIt == typeMap.cend())
|
|
throw error << ": Unknown property type \"" << node.name()
|
|
<< "\" for element of type \"" << root.name() << "\"";
|
|
|
|
std::string str {resolvePlaceholders(node.text().as_string())};
|
|
|
|
// Handle the special case with mutually exclusive system variables, for example
|
|
// system.fullName.autoCollections and system.fullName.noCollections which can never
|
|
// 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.
|
|
if (str == "\b")
|
|
continue;
|
|
|
|
// Strictly enforce that there are no blank values in the theme configuration.
|
|
if (str == "")
|
|
throw error << ": Property \"" << typeIt->first << "\" for element \"" << element.type
|
|
<< "\" has no value defined";
|
|
|
|
std::string nodeName {node.name()};
|
|
|
|
// 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("");
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (typeIt->second) {
|
|
case NORMALIZED_RECT: {
|
|
glm::vec4 val;
|
|
|
|
auto splits = Utils::String::delimitedStringToVector(str, " ");
|
|
if (splits.size() == 2) {
|
|
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()))};
|
|
}
|
|
else if (splits.size() == 4) {
|
|
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()))};
|
|
}
|
|
|
|
element.properties[node.name()] = val;
|
|
break;
|
|
}
|
|
case NORMALIZED_PAIR: {
|
|
size_t divider = str.find(' ');
|
|
if (divider == std::string::npos)
|
|
throw error << ": Invalid normalized pair value \"" << str.c_str()
|
|
<< "\" for property \"" << node.name() << "\"";
|
|
|
|
std::string first {str.substr(0, divider)};
|
|
std::string second {str.substr(divider, std::string::npos)};
|
|
|
|
glm::vec2 val {static_cast<float>(atof(first.c_str())),
|
|
static_cast<float>(atof(second.c_str()))};
|
|
|
|
element.properties[node.name()] = val;
|
|
break;
|
|
}
|
|
case STRING: {
|
|
element.properties[node.name()] = str;
|
|
break;
|
|
}
|
|
case PATH: {
|
|
std::string path;
|
|
|
|
if (!str.empty() && str.front() == ':')
|
|
path = ResourceManager::getInstance().getResourcePath(str);
|
|
else
|
|
path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true);
|
|
|
|
if (!ResourceManager::getInstance().fileExists(path)) {
|
|
std::stringstream ss;
|
|
// For explicit paths, print a warning if the file couldn't be found, but
|
|
// only print a debug message if it was set using a variable.
|
|
if (str == node.text().as_string()) {
|
|
#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
|
|
LOG(LogWarning)
|
|
<< error.message << ": Couldn't find file \"" << node.text().get()
|
|
<< "\" "
|
|
<< ((node.text().get() != path) ? "which resolves to \"" + path + "\"" :
|
|
#endif
|
|
"")
|
|
<< " (element type \"" << element.type << "\", name \""
|
|
<< root.attribute("name").as_string() << "\", property \""
|
|
<< nodeName << "\")";
|
|
}
|
|
else if (!(Settings::getInstance()->getBool("DebugSkipMissingThemeFiles") ||
|
|
(mCustomCollection &&
|
|
Settings::getInstance()->getBool(
|
|
"DebugSkipMissingThemeFilesCustomCollections")))) {
|
|
#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
|
|
LOG(LogDebug)
|
|
<< error.message << ": Couldn't find file \"" << node.text().get()
|
|
<< "\" "
|
|
<< ((node.text().get() != path) ? "which resolves to \"" + path + "\"" :
|
|
#endif
|
|
"")
|
|
<< " (element type \"" << element.type << "\", name \""
|
|
<< root.attribute("name").as_string() << "\", property \""
|
|
<< nodeName << "\")";
|
|
}
|
|
}
|
|
element.properties[nodeName] = path;
|
|
break;
|
|
}
|
|
case COLOR: {
|
|
try {
|
|
element.properties[node.name()] = getHexColor(str);
|
|
}
|
|
catch (ThemeException& e) {
|
|
throw error << ": " << e.what();
|
|
}
|
|
break;
|
|
}
|
|
case UNSIGNED_INTEGER: {
|
|
unsigned int integerVal {static_cast<unsigned int>(strtoul(str.c_str(), 0, 0))};
|
|
element.properties[node.name()] = integerVal;
|
|
break;
|
|
}
|
|
case FLOAT: {
|
|
float floatVal {static_cast<float>(strtod(str.c_str(), 0))};
|
|
element.properties[node.name()] = floatVal;
|
|
break;
|
|
}
|
|
case BOOLEAN: {
|
|
bool boolVal = false;
|
|
// Only look at the first character.
|
|
if (str.size() > 0) {
|
|
if (str.front() == '1' || str.front() == 't' || str.front() == 'T' ||
|
|
str.front() == 'y' || str.front() == 'Y')
|
|
boolVal = true;
|
|
}
|
|
|
|
element.properties[node.name()] = boolVal;
|
|
break;
|
|
}
|
|
default: {
|
|
throw error << ": Unknown ElementPropertyType for \""
|
|
<< root.attribute("name").as_string() << "\", property " << node.name();
|
|
}
|
|
}
|
|
}
|
|
}
|