ES-DE/es-core/src/ThemeData.cpp

1932 lines
79 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>
// 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::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},
{"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},
{"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},
{"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},
{"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},
{"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},
{"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 for legacy theme version.
if (root.child("formatVersion") != nullptr)
throw error << ": Legacy <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();
}
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);
parseIncludes(root);
parseViews(root);
if (root.child("feature") != nullptr)
throw error << ": Legacy <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.
const std::string defaultUserThemeDir {Utils::FileSystem::getHomePath() +
"/.emulationstation/themes"};
std::string userThemeDirSetting {Utils::FileSystem::expandHomePath(
Settings::getInstance()->getString("UserThemeDirectory"))};
#if defined(_WIN64)
userThemeDirSetting = Utils::String::replace(userThemeDirSetting, "\\", "/");
#endif
std::string userThemeDirectory;
if (userThemeDirSetting == "") {
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;
}
#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
userThemeDirectory
};
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)) {
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" : "") << ", "
<< 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 slate-es-de
// instead, and if that's also missing then pick the first available one.
bool defaultSetFound {true};
theme = sThemes.find("slate-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::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;
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 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);
}
}
}
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)
{
ThemeException error;
error << "ThemeData::parseIncludes(): ";
error.setFiles(mPaths);
// Check for legacy theme version.
if (root.child("formatVersion") != nullptr)
throw error << ": Legacy <formatVersion> tag found";
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")))) {
#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
"");
}
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";
parseTransitions(theme);
parseVariables(theme);
parseColorSchemes(theme);
parseIncludes(theme);
parseViews(theme);
if (theme.child("feature") != nullptr)
throw error << ": Legacy <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);
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::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);
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 << ": Legacy \"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();
}
}
}
}