ES-DE/es-core/src/ThemeData.cpp
Leon Styhre a7005f22e9 Renamed the textlist property selectorOffsetY to selectorVerticalOffset and added a selectorHorizontalOffset property.
Also enforced minimum and maximum values for a few textlist properties.
2022-12-18 12:02:50 +01:00

1651 lines
64 KiB
C++

// SPDX-License-Identifier: MIT
//
// EmulationStation Desktop Edition
// 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>
#define MINIMUM_LEGACY_THEME_FORMAT_VERSION 3
// clang-format off
std::vector<std::string> ThemeData::sSupportedViews {
{"all"},
{"system"},
{"gamelist"}};
std::vector<std::string> ThemeData::sLegacySupportedViews {
{"all"},
{"system"},
{"basic"},
{"detailed"},
{"grid"},
{"video"}};
std::vector<std::string> ThemeData::sLegacySupportedFeatures {
{"navigationsounds"},
{"video"},
{"carousel"},
{"z-index"},
{"visible"}};
std::vector<std::string> ThemeData::sLegacyProperties {
{"showSnapshotNoVideo"},
{"showSnapshotDelay"},
{"forceUppercase"},
{"alignment"},
{"defaultLogo"},
{"logoSize"},
{"logoScale"},
{"logoRotation"},
{"logoRotationOrigin"},
{"logoAlignment"},
{"maxLogoCount"}};
std::vector<std::string> ThemeData::sDeprecatedProperties {
{"selectorOffsetY"},
{"staticItem"},
{"itemType"},
{"defaultItem"},
{"itemInterpolation"}};
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"},
{"21:9", "21:9"},
{"21:9_vertical", "21:9 vertical"},
{"32:9", "32:0"},
{"32:9_vertical", "32:9 vertical"}};
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},
{"21:9", 2.3703f},
{"21:9_vertical", 0.4219f},
{"32:9", 3.5555f},
{"32:9_vertical", 0.2813f}};
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},
{"staticItem", PATH}, // TEMPORARY: For backward compatibility.
{"itemType", STRING}, // TEMPORARY: For backward compatibility.
{"defaultItem", PATH}, // TEMPORARY: For backward compatibility.
{"maxItemCount", FLOAT},
{"maxLogoCount", FLOAT}, // For backward compatibility with legacy themes.
{"itemsBeforeCenter", UNSIGNED_INTEGER},
{"itemsAfterCenter", UNSIGNED_INTEGER},
{"itemStacking", STRING},
{"itemSize", NORMALIZED_PAIR},
{"itemScale", FLOAT},
{"itemRotation", FLOAT},
{"itemRotationOrigin", NORMALIZED_PAIR},
{"itemAxisHorizontal", BOOLEAN},
{"itemInterpolation", STRING}, // TEMPORARY: For backward compatibility.
{"imageInterpolation", STRING},
{"imageColor", COLOR},
{"imageColorEnd", COLOR},
{"imageGradientType", STRING},
{"imageBrightness", FLOAT},
{"imageSaturation", FLOAT},
{"itemTransitions", STRING},
{"itemHorizontalAlignment", STRING},
{"itemVerticalAlignment", STRING},
{"wheelHorizontalAlignment", STRING},
{"horizontalOffset", FLOAT},
{"verticalOffset", FLOAT},
{"reflections", BOOLEAN},
{"reflectionsOpacity", FLOAT},
{"reflectionsFalloff", FLOAT},
{"unfocusedItemOpacity", FLOAT},
{"defaultLogo", PATH}, // For backward compatibility with legacy themes.
{"logoSize", NORMALIZED_PAIR}, // For backward compatibility with legacy themes.
{"logoScale", FLOAT}, // For backward compatibility with legacy themes.
{"logoRotation", FLOAT}, // For backward compatibility with legacy themes.
{"logoRotationOrigin", NORMALIZED_PAIR}, // For backward compatibility with legacy themes.
{"logoAlignment", STRING}, // For backward compatibility with legacy themes.
{"color", COLOR},
{"colorEnd", COLOR},
{"gradientType", STRING},
{"text", STRING},
{"textColor", COLOR},
{"textBackgroundColor", COLOR},
{"fontPath", PATH},
{"fontSize", FLOAT},
{"letterCase", STRING},
{"letterCaseCollections", STRING},
{"letterCaseGroupedCollections", STRING},
{"lineSpacing", FLOAT},
{"fadeAbovePrimary", BOOLEAN},
{"zIndex", FLOAT},
{"legacyZIndexMode", STRING}}}, // For backward compatibility with legacy themes.
{"grid",
{{"pos", NORMALIZED_PAIR},
{"size", NORMALIZED_PAIR},
{"origin", NORMALIZED_PAIR},
{"staticImage", PATH},
{"imageType", STRING},
{"defaultImage", PATH},
{"itemSize", NORMALIZED_PAIR},
{"itemScale", FLOAT},
{"itemSpacing", NORMALIZED_PAIR},
{"fractionalRows", BOOLEAN},
{"itemTransitions", STRING},
{"rowTransitions", STRING},
{"unfocusedItemOpacity", FLOAT},
{"edgeScaleInwards", BOOLEAN}, // TODO
{"imageFit", STRING},
{"imageRelativeScale", FLOAT},
{"imageColor", COLOR},
{"imageColorEnd", COLOR},
{"imageGradientType", STRING},
{"imageBrightness", FLOAT},
{"imageSaturation", FLOAT},
{"backgroundImage", PATH},
{"backgroundRelativeScale", FLOAT},
{"backgroundColor", COLOR},
{"backgroundColorEnd", COLOR},
{"backgroundGradientType", STRING},
{"selectorImage", PATH},
{"selectorRelativeScale", FLOAT},
{"selectorLayer", STRING},
{"selectorColor", COLOR},
{"selectorColorEnd", COLOR},
{"selectorGradientType", STRING},
{"text", STRING},
{"textRelativeScale", FLOAT},
{"textColor", COLOR},
{"textBackgroundColor", COLOR},
{"fontPath", PATH},
{"fontSize", FLOAT},
{"letterCase", STRING},
{"letterCaseCollections", STRING},
{"letterCaseGroupedCollections", STRING},
{"lineSpacing", FLOAT},
{"fadeAbovePrimary", BOOLEAN},
{"zIndex", FLOAT}}},
{"textlist",
{{"pos", NORMALIZED_PAIR},
{"size", NORMALIZED_PAIR},
{"origin", NORMALIZED_PAIR},
{"selectorHeight", FLOAT},
{"selectorHorizontalOffset", FLOAT},
{"selectorVerticalOffset", FLOAT},
{"selectorOffsetY", FLOAT}, // For backward compatibility with legacy themes.
{"selectorColor", COLOR},
{"selectorColorEnd", COLOR},
{"selectorGradientType", STRING},
{"selectorImagePath", PATH},
{"selectorImageTile", BOOLEAN},
{"primaryColor", COLOR},
{"secondaryColor", COLOR},
{"selectedColor", COLOR},
{"selectedSecondaryColor", COLOR},
{"fontPath", PATH},
{"fontSize", FLOAT},
{"scrollSound", PATH}, // For backward compatibility with legacy themes.
{"horizontalAlignment", STRING},
{"alignment", STRING}, // For backward compatibility with legacy themes.
{"horizontalMargin", FLOAT},
{"letterCase", STRING},
{"letterCaseCollections", STRING},
{"letterCaseGroupedCollections", STRING},
{"forceUppercase", BOOLEAN}, // For backward compatibility with legacy themes.
{"lineSpacing", FLOAT},
{"indicators", STRING},
{"collectionIndicators", STRING},
{"fadeAbovePrimary", BOOLEAN},
{"zIndex", FLOAT}}},
{"image",
{{"pos", NORMALIZED_PAIR},
{"size", NORMALIZED_PAIR},
{"maxSize", NORMALIZED_PAIR},
{"origin", NORMALIZED_PAIR},
{"rotation", FLOAT},
{"rotationOrigin", NORMALIZED_PAIR},
{"path", PATH},
{"default", PATH},
{"imageType", STRING},
{"metadataElement", BOOLEAN},
{"gameselector", STRING},
{"gameselectorEntry", UNSIGNED_INTEGER},
{"tile", BOOLEAN},
{"tileSize", NORMALIZED_PAIR},
{"tileHorizontalAlignment", STRING},
{"tileVerticalAlignment", STRING},
{"interpolation", STRING},
{"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},
{"maxSize", NORMALIZED_PAIR},
{"origin", NORMALIZED_PAIR},
{"path", PATH},
{"default", PATH},
{"defaultImage", PATH},
{"imageType", STRING},
{"metadataElement", BOOLEAN},
{"gameselector", STRING},
{"gameselectorEntry", UNSIGNED_INTEGER},
{"audio", BOOLEAN},
{"interpolation", STRING},
{"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},
{"showSnapshotNoVideo", BOOLEAN}, // For backward compatibility with legacy themes.
{"showSnapshotDelay", BOOLEAN}}}, // For backward compatibility with legacy themes.
{"animation",
{{"pos", NORMALIZED_PAIR},
{"size", NORMALIZED_PAIR},
{"origin", NORMALIZED_PAIR},
{"rotation", FLOAT},
{"rotationOrigin", NORMALIZED_PAIR},
{"metadataElement", BOOLEAN},
{"path", PATH},
{"speed", FLOAT},
{"direction", STRING},
{"keepAspectRatio", BOOLEAN},
{"interpolation", 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},
{"horizontalAlignment", STRING},
{"alignment", STRING}, // For backward compatibility with legacy themes.
{"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},
{"opacity", FLOAT},
{"visible", BOOLEAN},
{"zIndex", FLOAT}}},
{"text",
{{"pos", NORMALIZED_PAIR},
{"size", NORMALIZED_PAIR},
{"origin", NORMALIZED_PAIR},
{"rotation", FLOAT},
{"rotationOrigin", NORMALIZED_PAIR},
{"text", STRING},
{"systemdata", STRING},
{"metadata", STRING},
{"metadataElement", BOOLEAN},
{"gameselector", STRING},
{"gameselectorEntry", UNSIGNED_INTEGER},
{"container", BOOLEAN},
{"containerVerticalSnap", BOOLEAN},
{"containerScrollSpeed", FLOAT},
{"containerStartDelay", FLOAT},
{"containerResetDelay", FLOAT},
{"fontPath", PATH},
{"fontSize", FLOAT},
{"horizontalAlignment", STRING},
{"verticalAlignment", STRING},
{"alignment", STRING}, // For backward compatibility with legacy themes.
{"color", COLOR},
{"backgroundColor", COLOR},
{"letterCase", STRING},
{"forceUppercase", BOOLEAN}, // For backward compatibility with legacy themes.
{"lineSpacing", FLOAT},
{"opacity", FLOAT},
{"visible", BOOLEAN},
{"zIndex", FLOAT}}},
{"datetime",
{{"pos", NORMALIZED_PAIR},
{"size", NORMALIZED_PAIR},
{"origin", NORMALIZED_PAIR},
{"rotation", FLOAT},
{"rotationOrigin", NORMALIZED_PAIR},
{"metadata", STRING},
{"gameselector", STRING},
{"gameselectorEntry", UNSIGNED_INTEGER},
{"fontPath", PATH},
{"fontSize", FLOAT},
{"horizontalAlignment", STRING},
{"verticalAlignment", STRING},
{"alignment", STRING}, // For backward compatibility with legacy themes.
{"color", COLOR},
{"backgroundColor", COLOR},
{"letterCase", STRING},
{"forceUppercase", BOOLEAN}, // For backward compatibility with legacy themes.
{"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},
{"fontPath", PATH},
{"fontSize", FLOAT},
{"horizontalAlignment", STRING},
{"verticalAlignment", STRING},
{"alignment", STRING}, // For backward compatibility with legacy themes.
{"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},
{"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}}},
{"helpsystem",
{{"pos", NORMALIZED_PAIR},
{"origin", NORMALIZED_PAIR},
{"textColor", COLOR},
{"textColorDimmed", COLOR},
{"iconColor", COLOR},
{"iconColorDimmed", COLOR},
{"fontPath", PATH},
{"fontSize", FLOAT},
{"entrySpacing", FLOAT},
{"iconTextSpacing", FLOAT},
{"letterCase", STRING},
{"textStyle", STRING}, // For backward compatibility with legacy themes.
{"opacity", FLOAT},
{"customButtonIcon", PATH}}},
{"navigationsounds",
{{"systembrowseSound", PATH},
{"quicksysselectSound", PATH},
{"selectSound", PATH},
{"backSound", PATH},
{"scrollSound", PATH},
{"favoriteSound", PATH},
{"launchSound", PATH}}},
// Legacy components below, not in use any longer but needed for backward compatibility.
{"sound",
{{"path", PATH}}},
{"imagegrid",
{{"pos", NORMALIZED_PAIR},
{"size", NORMALIZED_PAIR},
{"margin", NORMALIZED_PAIR},
{"padding", NORMALIZED_RECT},
{"autoLayout", NORMALIZED_PAIR},
{"autoLayoutSelectedZoom", FLOAT},
{"gameImage", PATH},
{"folderImage", PATH},
{"imageSource", STRING},
{"scrollDirection", STRING},
{"centerSelection", BOOLEAN},
{"scrollLoop", BOOLEAN},
{"animate", BOOLEAN},
{"zIndex", FLOAT}}},
{"gridtile",
{{"size", NORMALIZED_PAIR},
{"padding", NORMALIZED_PAIR},
{"imageColor", COLOR},
{"backgroundImage", PATH},
{"backgroundCornerSize", NORMALIZED_PAIR},
{"backgroundColor", COLOR},
{"backgroundCenterColor", COLOR},
{"backgroundEdgeColor", COLOR}}},
{"ninepatch",
{{"pos", NORMALIZED_PAIR},
{"size", NORMALIZED_PAIR},
{"path", PATH},
{"visible", BOOLEAN},
{"zIndex", FLOAT}}}};
// clang-format on
ThemeData::ThemeData()
: mLegacyTheme {false}
, mCustomCollection {false}
{
mCurrentThemeSet = mThemeSets.find(Settings::getInstance()->getString("ThemeSet"));
}
void ThemeData::loadFile(const std::map<std::string, std::string>& sysDataMap,
const std::string& path,
const bool customCollection)
{
mCustomCollection = customCollection;
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";
if (mCurrentThemeSet != mThemeSets.cend())
mLegacyTheme = mCurrentThemeSet->second.capabilities.legacyTheme;
// The resolution tag introduced in RetroPie EmulationStation in 2020 is a very bad idea
// as it changes sizing of components from relative values to absolute pixel values.
// So themes using it will simply not get loaded at all.
if (root.child("resolution") != nullptr)
throw error << ": <resolution> tag not supported";
// Check for legacy theme version.
int legacyVersion {root.child("formatVersion").text().as_int(-1)};
if (mLegacyTheme) {
if (legacyVersion == -1)
throw error << ": <formatVersion> tag missing for legacy theme set";
if (legacyVersion < MINIMUM_LEGACY_THEME_FORMAT_VERSION)
throw error << ": Defined legacy format version " << legacyVersion
<< " is less than the minimum supported version "
<< MINIMUM_LEGACY_THEME_FORMAT_VERSION;
}
else if (legacyVersion != -1) {
throw error << ": Legacy <formatVersion> tag found for non-legacy theme set";
}
if (!mLegacyTheme) {
if (mCurrentThemeSet->second.capabilities.variants.size() > 0) {
for (auto& variant : mCurrentThemeSet->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 (mCurrentThemeSet->second.capabilities.colorSchemes.size() > 0) {
for (auto& colorScheme : mCurrentThemeSet->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 (mCurrentThemeSet->second.capabilities.aspectRatios.size() > 0) {
if (std::find(mCurrentThemeSet->second.capabilities.aspectRatios.cbegin(),
mCurrentThemeSet->second.capabilities.aspectRatios.cend(),
Settings::getInstance()->getString("ThemeAspectRatio")) !=
mCurrentThemeSet->second.capabilities.aspectRatios.cend())
mSelectedAspectRatio = Settings::getInstance()->getString("ThemeAspectRatio");
else
mSelectedAspectRatio = mCurrentThemeSet->second.capabilities.aspectRatios.front();
if (mSelectedAspectRatio == "automatic") {
// Auto-detect the closest aspect ratio based on what's available in the theme set.
mSelectedAspectRatio = "16:9";
const float screenAspectRatio {Renderer::getScreenAspectRatio()};
float diff {std::fabs(sAspectRatioMap["16:9"] - screenAspectRatio)};
for (auto& aspectRatio : mCurrentThemeSet->second.capabilities.aspectRatios) {
if (aspectRatio == "automatic")
continue;
if (sAspectRatioMap.find(aspectRatio) != sAspectRatioMap.end()) {
const float newDiff {
std::fabs(sAspectRatioMap[aspectRatio] - screenAspectRatio)};
if (newDiff < diff) {
diff = newDiff;
mSelectedAspectRatio = aspectRatio;
}
}
}
}
}
}
parseVariables(root);
if (!mLegacyTheme)
parseColorSchemes(root);
parseIncludes(root);
parseViews(root);
// For non-legacy themes this will simply check for the presence of a feature tag and throw
// an error if it's found.
parseFeatures(root);
if (!mLegacyTheme) {
parseVariants(root);
parseAspectRatios(root);
}
}
bool ThemeData::hasView(const std::string& view)
{
auto viewIt = mViews.find(view);
return (viewIt != mViews.cend());
}
std::vector<GuiComponent*> ThemeData::makeExtras(const std::shared_ptr<ThemeData>& theme,
const std::string& view)
{
std::vector<GuiComponent*> comps;
auto viewIt = theme->mViews.find(view);
if (viewIt == theme->mViews.cend())
return comps;
for (auto it = viewIt->second.legacyOrderedKeys.cbegin(); // Line break.
it != viewIt->second.legacyOrderedKeys.cend(); ++it) {
ThemeElement& elem {viewIt->second.elements.at(*it)};
if (elem.extra) {
GuiComponent* comp {nullptr};
const std::string& t {elem.type};
if (t == "image")
comp = new ImageComponent;
else if (t == "text")
comp = new TextComponent;
if (comp) {
comp->setDefaultZIndex(10.0f);
comp->applyTheme(theme, view, *it, ThemeFlags::ALL);
comps.push_back(comp);
}
}
}
return comps;
}
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::populateThemeSets()
{
assert(mThemeSets.empty());
LOG(LogInfo) << "Checking for available theme sets...";
// Check for themes first under the home directory, then under the data installation
// directory (Unix only) and last under the ES-DE binary directory.
#if defined(__unix__) || defined(__APPLE__)
#if defined(APPIMAGE_BUILD)
static const size_t pathCount {2};
#else
static const size_t pathCount {3};
#endif
#else
static const size_t pathCount {2};
#endif
std::string paths[pathCount] = {
Utils::FileSystem::getExePath() + "/themes",
#if defined(__APPLE__)
Utils::FileSystem::getExePath() + "/../Resources/themes",
#elif defined(__unix__) && !defined(APPIMAGE_BUILD)
Utils::FileSystem::getProgramDataPath() + "/themes",
#endif
Utils::FileSystem::getHomePath() + "/.emulationstation/themes"
};
for (size_t i = 0; i < pathCount; ++i) {
if (!Utils::FileSystem::isDirectory(paths[i]))
continue;
Utils::FileSystem::StringList dirContent {Utils::FileSystem::getDirContent(paths[i])};
for (Utils::FileSystem::StringList::const_iterator it = dirContent.cbegin();
it != dirContent.cend(); ++it) {
if (Utils::FileSystem::isDirectory(*it)) {
#if defined(_WIN64)
LOG(LogDebug) << "Loading theme set capabilities for \""
<< Utils::String::replace(*it, "/", "\\") << "\"...";
#else
LOG(LogDebug) << "Loading theme set capabilities for \"" << *it << "\"...";
#endif
ThemeCapability capabilities {parseThemeCapabilities(*it)};
#if defined(_WIN64)
LOG(LogInfo) << "Added" << (capabilities.legacyTheme ? " legacy" : "")
<< " theme set \"" << Utils::String::replace(*it, "/", "\\") << "\"";
#else
LOG(LogInfo) << "Added" << (capabilities.legacyTheme ? " legacy" : "")
<< " theme set \"" << *it << "\"";
#endif
if (!capabilities.legacyTheme) {
LOG(LogDebug) << "Theme set includes support for "
<< capabilities.variants.size() << " variant"
<< (capabilities.variants.size() != 1 ? "s" : "") << ", "
<< capabilities.colorSchemes.size() << " color scheme"
<< (capabilities.colorSchemes.size() != 1 ? "s" : "") << " and "
<< capabilities.aspectRatios.size() << " aspect ratio"
<< (capabilities.aspectRatios.size() != 1 ? "s" : "");
}
ThemeSet set {*it, capabilities};
mThemeSets[set.getName()] = set;
}
}
}
if (mThemeSets.empty()) {
LOG(LogWarning) << "Couldn't find any theme sets, creating dummy entry";
ThemeSet set {"no-theme-sets", ThemeCapability()};
mThemeSets[set.getName()] = set;
mCurrentThemeSet = mThemeSets.begin();
}
}
const std::string ThemeData::getThemeFromCurrentSet(const std::string& system)
{
if (mThemeSets.empty())
getThemeSets();
if (mThemeSets.empty())
// No theme sets available.
return "";
std::map<std::string, ThemeSet, StringComparator>::const_iterator set {
mThemeSets.find(Settings::getInstance()->getString("ThemeSet"))};
if (set == mThemeSets.cend()) {
// Currently configured theme set is missing, attempt to load the default theme set
// slate-DE instead, and if that's also missing then pick the first available set.
bool defaultSetFound {true};
set = mThemeSets.find("slate-DE");
if (set == mThemeSets.cend()) {
set = mThemeSets.cbegin();
defaultSetFound = false;
}
LOG(LogWarning) << "Configured theme set \""
<< Settings::getInstance()->getString("ThemeSet")
<< "\" does not exist, loading" << (defaultSetFound ? " default " : " ")
<< "theme set \"" << set->first << "\" instead";
Settings::getInstance()->setString("ThemeSet", set->first);
mCurrentThemeSet = mThemeSets.find(Settings::getInstance()->getString("ThemeSet"));
}
return set->second.getThemePath(system);
}
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";
}
unsigned int ThemeData::getHexColor(const std::string& str)
{
ThemeException error;
if (str == "")
throw error << "Empty color property";
size_t len {str.size()};
if (len != 6 && len != 8)
throw error << "Invalid color property \"" << str
<< "\" (must be 6 or 8 characters in length)";
unsigned int val;
std::stringstream ss;
ss << str;
ss >> std::hex >> val;
if (len == 6)
val = (val << 8) | 0xFF;
return val;
}
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::string capFile {path + "/capabilities.xml"};
if (Utils::FileSystem::isRegularFile(capFile) || Utils::FileSystem::isSymlink(capFile)) {
capabilities.legacyTheme = false;
pugi::xml_document doc;
#if defined(_WIN64)
pugi::xml_parse_result res {
doc.load_file(Utils::String::stringToWideString(capFile).c_str())};
#else
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;
}
pugi::xml_node themeCapabilities {doc.child("themeCapabilities")};
if (!themeCapabilities) {
LOG(LogError) << "Missing <themeCapabilities> tag in capabilities.xml";
return capabilities;
}
for (pugi::xml_node aspectRatio {themeCapabilities.child("aspectRatio")}; aspectRatio;
aspectRatio = aspectRatio.next_sibling("aspectRatio")) {
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 variant {themeCapabilities.child("variant")}; variant;
variant = variant.next_sibling("variant")) {
ThemeVariant readVariant;
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;
}
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 {
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;
}
}
pugi::xml_node selectableTag {variant.child("selectable")};
if (selectableTag != nullptr) {
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;
}
pugi::xml_node overrideTag {variant.child("override")};
if (overrideTag != nullptr) {
pugi::xml_node triggerTag {overrideTag.child("trigger")};
if (triggerTag != nullptr) {
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 {
pugi::xml_node useVariantTag {overrideTag.child("useVariant")};
if (useVariantTag != nullptr) {
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 {
readVariant.override = true;
readVariant.overrideTrigger = triggerValue;
readVariant.overrideVariant = useVariantValue;
}
}
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;
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;
}
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 {
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);
}
}
}
else {
LOG(LogDebug) << "No capabilities.xml file found, flagging as legacy theme set";
capabilities.legacyTheme = true;
}
// 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);
}
}
}
return capabilities;
}
void ThemeData::parseIncludes(const pugi::xml_node& root)
{
ThemeException error;
error << "ThemeData::parseIncludes(): ";
error.setFiles(mPaths);
if (!mLegacyTheme) {
if (root.child("formatVersion").text().as_int(-1) != -1)
throw error << ": Legacy <formatVersion> tag found for non-legacy theme set";
}
for (pugi::xml_node node {root.child("include")}; node; node = node.next_sibling("include")) {
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")))) {
LOG(LogDebug) << error.message << ": Couldn't find file \"" << node.text().get()
<< "\" "
<< ((node.text().get() != path) ?
"which resolves to \"" + path + "\"" :
"");
}
return;
}
}
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";
parseVariables(theme);
if (!mLegacyTheme)
parseColorSchemes(theme);
parseIncludes(theme);
parseViews(theme);
// For non-legacy themes this will simply check for the presence of a feature tag and throw
// an error if it's found.
parseFeatures(theme);
if (!mLegacyTheme) {
parseVariants(theme);
parseAspectRatios(theme);
}
mPaths.pop_back();
}
}
void ThemeData::parseFeatures(const pugi::xml_node& root)
{
ThemeException error;
error << "ThemeData::parseFeatures(): ";
error.setFiles(mPaths);
if (!mLegacyTheme && root.child("feature") != nullptr)
throw error << ": Legacy <feature> tag found for non-legacy theme set";
for (pugi::xml_node node {root.child("feature")}; node; node = node.next_sibling("feature")) {
if (!node.attribute("supported"))
throw error << ": Feature missing \"supported\" attribute";
const std::string supportedAttr {node.attribute("supported").as_string()};
if (std::find(sLegacySupportedFeatures.cbegin(), sLegacySupportedFeatures.cend(),
supportedAttr) != sLegacySupportedFeatures.cend()) {
parseViews(node);
}
}
}
void ThemeData::parseVariants(const pugi::xml_node& root)
{
if (mCurrentThemeSet == mThemeSets.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";
}
if (mSelectedVariant == viewKey || viewKey == "all") {
parseVariables(node);
parseColorSchemes(node);
parseIncludes(node);
parseViews(node);
parseAspectRatios(node);
}
}
}
}
void ThemeData::parseColorSchemes(const pugi::xml_node& root)
{
if (mCurrentThemeSet == mThemeSets.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::parseAspectRatios(const pugi::xml_node& root)
{
if (mCurrentThemeSet == mThemeSets.end())
return;
if (mSelectedAspectRatio == "")
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(mCurrentThemeSet->second.capabilities.aspectRatios.cbegin(),
mCurrentThemeSet->second.capabilities.aspectRatios.cend(),
viewKey) == mCurrentThemeSet->second.capabilities.aspectRatios.cend()) {
throw error << ": <aspectRatio> value \"" << viewKey
<< "\" is not defined in capabilities.xml";
}
if (mSelectedAspectRatio == viewKey) {
parseVariables(node);
parseColorSchemes(node);
parseIncludes(node);
parseViews(node);
}
}
}
}
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()) {
// Overriding existing variables is not allowed for legacy themes.
if (!mLegacyTheme && 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 (mLegacyTheme) {
if (std::find(sLegacySupportedViews.cbegin(), sLegacySupportedViews.cend(),
viewKey) != sLegacySupportedViews.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";
}
}
else {
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. Also include workarounds for legacy theme sets for when the
// fixed labels have been defined with the wrong element type.
const std::string elementType {node.name()};
LegacyWorkaround legacyWorkaround {LegacyWorkaround::NONE};
if (mLegacyTheme && elementType == "text" &&
(elemKey == "md_releasedate" || elemKey == "md_lastplayed")) {
LOG(LogDebug) << "ThemeData::parseView(): Element type for \"" << elemKey
<< "\" incorrectly set to \"text\" "
"instead of \"datetime\", applying workaround";
legacyWorkaround = LegacyWorkaround::DATETIME;
elemKey = "datetime_" + elemKey;
}
else if (mLegacyTheme && elementType == "datetime" &&
(elemKey == "md_lbl_releasedate" || elemKey == "md_lbl_lastplayed")) {
LOG(LogDebug) << "ThemeData::parseView(): Element type for \"" << elemKey
<< "\" incorrectly set to \"datetime\" "
"instead of \"text\", applying workaround";
legacyWorkaround = LegacyWorkaround::TEXT;
elemKey = "text_" + elemKey;
}
else if (mLegacyTheme && elementType == "text" && elemKey == "md_rating") {
LOG(LogDebug) << "ThemeData::parseView(): Element type for \"" << elemKey
<< "\" incorrectly set to \"text\" "
"instead of \"rating\", applying workaround";
legacyWorkaround = LegacyWorkaround::RATING;
elemKey = "rating_" + elemKey;
}
else {
elemKey = elementType + "_" + elemKey;
}
parseElement(
node, elemTypeIt->second,
view.elements.insert(std::pair<std::string, ThemeElement>(elemKey, ThemeElement()))
.first->second,
legacyWorkaround);
if (mLegacyTheme &&
std::find(view.legacyOrderedKeys.cbegin(), view.legacyOrderedKeys.cend(),
elemKey) == view.legacyOrderedKeys.cend())
view.legacyOrderedKeys.push_back(elemKey);
}
}
}
void ThemeData::parseElement(const pugi::xml_node& root,
const std::map<std::string, ElementPropertyType>& typeMap,
ThemeElement& element,
const LegacyWorkaround legacyWorkaround)
{
ThemeException error;
error << "ThemeData::parseElement(): ";
error.setFiles(mPaths);
if (legacyWorkaround == LegacyWorkaround::DATETIME)
element.type = "datetime";
else if (legacyWorkaround == LegacyWorkaround::TEXT)
element.type = "text";
else if (legacyWorkaround == LegacyWorkaround::RATING)
element.type = "rating";
else
element.type = root.name();
element.extra = root.attribute("extra").as_bool(false);
if (mLegacyTheme)
element.extra = root.attribute("extra").as_bool(false);
else if (!mLegacyTheme && std::string(root.attribute("extra").as_string("")) != "")
throw error << ": Legacy \"extra\" attribute found for non-legacy theme set";
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.collections 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 (!mLegacyTheme && str == "\b")
continue;
// Skip this check for legacy themes to not break backward compatibility with some
// themes sets that include empty property values.
if (!mLegacyTheme && str == "")
throw error << ": Property \"" << typeIt->first << "\" for element \"" << element.type
<< "\" has no value defined";
std::string nodeName = node.name();
// Strictly enforce removal of legacy properties for non-legacy theme sets by creating
// an unthemed system if they're present in the configuration.
if (!mLegacyTheme) {
for (auto& legacyProperty : sLegacyProperties) {
if (nodeName == legacyProperty) {
throw error << ": Legacy <" << nodeName
<< "> property found for non-legacy theme set";
}
}
}
// Print a warning if a deprecated property is used for a non-legacy theme set.
if (!mLegacyTheme) {
for (auto& deprecatedProperty : sDeprecatedProperties) {
if (nodeName == deprecatedProperty) {
LOG(LogWarning)
<< "ThemeData::parseElement(): Property \"" << deprecatedProperty
<< "\" is deprecated and support for it will be removed in a future "
"version";
}
}
}
// 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()) {
LOG(LogWarning)
<< error.message << ": Couldn't find file \"" << node.text().get()
<< "\" "
<< ((node.text().get() != path) ? "which resolves to \"" + path + "\"" :
"")
<< " (element type \"" << element.type << "\", name \""
<< root.attribute("name").as_string() << "\", property \"" << nodeName
<< "\")";
}
else if (!(Settings::getInstance()->getBool("DebugSkipMissingThemeFiles") ||
(mCustomCollection &&
Settings::getInstance()->getBool(
"DebugSkipMissingThemeFilesCustomCollections")))) {
LOG(LogDebug)
<< error.message << ": Couldn't find file \"" << node.text().get()
<< "\" "
<< ((node.text().get() != path) ? "which resolves to \"" + path + "\"" :
"")
<< " (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();
}
}
}
}