// SPDX-License-Identifier: MIT // // ES-DE Frontend // 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/LocalizationUtil.h" #include "utils/StringUtil.h" #include <algorithm> #include <pugixml.hpp> // clang-format off std::vector<std::string> ThemeData::sSupportedViews { {"all"}, {"system"}, {"gamelist"}}; std::vector<std::string> ThemeData::sSupportedMediaTypes { {"miximage"}, {"marquee"}, {"screenshot"}, {"titlescreen"}, {"cover"}, {"backcover"}, {"3dbox"}, {"physicalmedia"}, {"fanart"}, {"video"}}; std::vector<std::string> ThemeData::sSupportedTransitions { {"systemToSystem"}, {"systemToGamelist"}, {"gamelistToGamelist"}, {"gamelistToSystem"}, {"startupToSystem"}, {"startupToGamelist"}}; std::vector<std::string> ThemeData::sSupportedTransitionAnimations { {"builtin-instant"}, {"builtin-slide"}, {"builtin-fade"}}; std::vector<std::pair<std::string, std::string>> ThemeData::sSupportedFontSizes { {"medium", "medium"}, {"large", "large"}, {"small", "small"}, {"x-large", "extra large"}, {"x-small", "extra small"}}; std::vector<std::pair<std::string, std::string>> ThemeData::sSupportedAspectRatios { {"automatic", "automatic"}, {"16:9", "16:9"}, {"16:9_vertical", "16:9 vertical"}, {"16:10", "16:10"}, {"16:10_vertical", "16:10 vertical"}, {"3:2", "3:2"}, {"3:2_vertical", "3:2 vertical"}, {"4:3", "4:3"}, {"4:3_vertical", "4:3 vertical"}, {"5:4", "5:4"}, {"5:4_vertical", "5:4 vertical"}, {"19.5:9", "19.5:9"}, {"19.5:9_vertical", "19.5:9 vertical"}, {"20:9", "20:9"}, {"20:9_vertical", "20:9 vertical"}, {"21:9", "21:9"}, {"21:9_vertical", "21:9 vertical"}, {"32:9", "32:9"}, {"32:9_vertical", "32:9 vertical"}, {"1:1", "1:1"}}; std::map<std::string, float> ThemeData::sAspectRatioMap { {"16:9", 1.7777f}, {"16:9_vertical", 0.5625f}, {"16:10", 1.6f}, {"16:10_vertical", 0.625f}, {"3:2", 1.5f}, {"3:2_vertical", 0.6667f}, {"4:3", 1.3333f}, {"4:3_vertical", 0.75f}, {"5:4", 1.25f}, {"5:4_vertical", 0.8f}, {"19.5:9", 2.1667f}, {"19.5:9_vertical", 0.4615f}, {"20:9", 2.2222f}, {"20:9_vertical", 0.45f}, {"21:9", 2.3703f}, {"21:9_vertical", 0.4219f}, {"32:9", 3.5555f}, {"32:9_vertical", 0.2813f}, {"1:1", 1.0f}}; std::vector<std::pair<std::string, std::string>> ThemeData::sSupportedLanguages { {"automatic", "automatic"}, {"en_US", "ENGLISH (UNITED STATES)"}, {"en_GB", "ENGLISH (UNITED KINGDOM)"}, {"de_DE", "DEUTSCH"}, {"es_ES", "ESPAÑOL (ESPAÑA)"}, {"fr_FR", "FRANÇAIS"}, {"it_IT", "ITALIANO"}, {"pl_PL", "POLSKI"}, {"pt_BR", "PORTUGUÊS (BRASIL)"}, {"ro_RO", "ROMÂNĂ"}, {"ru_RU", "РУССКИЙ"}, {"sv_SE", "SVENSKA"}, {"ja_JP", "日本語"}, {"ko_KR", "한국어"}, {"zh_CN", "简体中文"}}; 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}, {"selectedItemOffset", NORMALIZED_PAIR}, {"itemSize", NORMALIZED_PAIR}, {"itemScale", FLOAT}, {"itemRotation", FLOAT}, {"itemRotationOrigin", NORMALIZED_PAIR}, {"itemAxisHorizontal", BOOLEAN}, {"itemAxisRotation", FLOAT}, {"imageFit", STRING}, {"imageCropPos", NORMALIZED_PAIR}, {"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}, {"imageCropPos", NORMALIZED_PAIR}, {"imageInterpolation", STRING}, {"imageRelativeScale", FLOAT}, {"imageCornerRadius", FLOAT}, {"imageColor", COLOR}, {"imageColorEnd", COLOR}, {"imageGradientType", STRING}, {"imageSelectedColor", COLOR}, {"imageSelectedColorEnd", COLOR}, {"imageSelectedGradientType", STRING}, {"imageBrightness", FLOAT}, {"imageSaturation", FLOAT}, {"backgroundImage", PATH}, {"backgroundRelativeScale", FLOAT}, {"backgroundCornerRadius", FLOAT}, {"backgroundColor", COLOR}, {"backgroundColorEnd", COLOR}, {"backgroundGradientType", STRING}, {"selectorImage", PATH}, {"selectorRelativeScale", FLOAT}, {"selectorCornerRadius", FLOAT}, {"selectorLayer", STRING}, {"selectorColor", COLOR}, {"selectorColorEnd", COLOR}, {"selectorGradientType", STRING}, {"text", STRING}, {"textRelativeScale", FLOAT}, {"textColor", COLOR}, {"textBackgroundColor", COLOR}, {"textSelectedColor", COLOR}, {"textSelectedBackgroundColor", COLOR}, {"textHorizontalScrolling", BOOLEAN}, {"textHorizontalScrollSpeed", FLOAT}, {"textHorizontalScrollDelay", FLOAT}, {"textHorizontalScrollGap", FLOAT}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"letterCase", STRING}, {"letterCaseAutoCollections", STRING}, {"letterCaseCustomCollections", STRING}, {"lineSpacing", FLOAT}, {"systemNameSuffix", BOOLEAN}, {"letterCaseSystemNameSuffix", STRING}, {"fadeAbovePrimary", BOOLEAN}, {"zIndex", FLOAT}}}, {"textlist", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"selectorWidth", FLOAT}, {"selectorHeight", FLOAT}, {"selectorHorizontalOffset", FLOAT}, {"selectorVerticalOffset", FLOAT}, {"selectorColor", COLOR}, {"selectorColorEnd", COLOR}, {"selectorGradientType", STRING}, {"selectorImagePath", PATH}, {"selectorImageTile", BOOLEAN}, {"primaryColor", COLOR}, {"secondaryColor", COLOR}, {"selectedColor", COLOR}, {"selectedSecondaryColor", COLOR}, {"selectedBackgroundColor", COLOR}, {"selectedSecondaryBackgroundColor", COLOR}, {"selectedBackgroundMargins", NORMALIZED_PAIR}, {"selectedBackgroundCornerRadius", FLOAT}, {"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}, {"maxSize", NORMALIZED_PAIR}, {"cropSize", NORMALIZED_PAIR}, {"cropPos", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"stationary", STRING}, {"renderDuringTransitions", BOOLEAN}, {"flipHorizontal", BOOLEAN}, {"flipVertical", BOOLEAN}, {"path", PATH}, {"gameOverridePath", PATH}, {"default", PATH}, {"imageType", STRING}, {"metadataElement", BOOLEAN}, {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"tile", BOOLEAN}, {"tileSize", NORMALIZED_PAIR}, {"tileHorizontalAlignment", STRING}, {"tileVerticalAlignment", STRING}, {"interpolation", STRING}, {"cornerRadius", FLOAT}, {"color", COLOR}, {"colorEnd", COLOR}, {"gradientType", STRING}, {"scrollFadeIn", BOOLEAN}, {"brightness", FLOAT}, {"opacity", FLOAT}, {"saturation", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"video", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"maxSize", NORMALIZED_PAIR}, {"cropSize", NORMALIZED_PAIR}, {"cropPos", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"stationary", STRING}, {"path", PATH}, {"default", PATH}, {"defaultImage", PATH}, {"imageType", STRING}, {"metadataElement", BOOLEAN}, {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"iterationCount", UNSIGNED_INTEGER}, {"onIterationsDone", STRING}, {"audio", BOOLEAN}, {"interpolation", STRING}, {"imageCornerRadius", FLOAT}, {"videoCornerRadius", FLOAT}, {"color", COLOR}, {"colorEnd", COLOR}, {"gradientType", STRING}, {"pillarboxes", BOOLEAN}, {"pillarboxThreshold", NORMALIZED_PAIR}, {"scanlines", BOOLEAN}, {"delay", FLOAT}, {"fadeInTime", FLOAT}, {"scrollFadeIn", BOOLEAN}, {"brightness", FLOAT}, {"opacity", FLOAT}, {"saturation", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"animation", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"maxSize", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"stationary", STRING}, {"metadataElement", BOOLEAN}, {"path", PATH}, {"speed", FLOAT}, {"direction", STRING}, {"iterationCount", UNSIGNED_INTEGER}, {"interpolation", STRING}, {"cornerRadius", FLOAT}, {"color", COLOR}, {"colorEnd", COLOR}, {"gradientType", STRING}, {"brightness", FLOAT}, {"opacity", FLOAT}, {"saturation", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"badges", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"stationary", STRING}, {"horizontalAlignment", STRING}, {"direction", STRING}, {"lines", UNSIGNED_INTEGER}, {"itemsPerLine", UNSIGNED_INTEGER}, {"itemMargin", NORMALIZED_PAIR}, {"slots", STRING}, {"controllerPos", NORMALIZED_PAIR}, {"controllerSize", FLOAT}, {"customBadgeIcon", PATH}, {"customControllerIcon", PATH}, {"folderLinkPos", NORMALIZED_PAIR}, {"folderLinkSize", FLOAT}, {"customFolderLinkIcon", PATH}, {"badgeIconColor", COLOR}, {"badgeIconColorEnd", COLOR}, {"badgeIconGradientType", STRING}, {"controllerIconColor", COLOR}, {"controllerIconColorEnd", COLOR}, {"controllerIconGradientType", STRING}, {"folderLinkIconColor", COLOR}, {"folderLinkIconColorEnd", COLOR}, {"folderLinkIconGradientType", STRING}, {"interpolation", STRING}, {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"text", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"stationary", STRING}, {"text", STRING}, {"systemdata", STRING}, {"metadata", STRING}, {"defaultValue", STRING}, {"systemNameSuffix", BOOLEAN}, {"letterCaseSystemNameSuffix", STRING}, {"metadataElement", BOOLEAN}, {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"container", BOOLEAN}, {"containerType", STRING}, {"containerVerticalSnap", BOOLEAN}, {"containerScrollSpeed", FLOAT}, {"containerStartDelay", FLOAT}, {"containerResetDelay", FLOAT}, {"containerScrollGap", FLOAT}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"horizontalAlignment", STRING}, {"verticalAlignment", STRING}, {"color", COLOR}, {"backgroundColor", COLOR}, {"backgroundMargins", NORMALIZED_PAIR}, {"backgroundCornerRadius", FLOAT}, {"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}, {"backgroundMargins", NORMALIZED_PAIR}, {"backgroundCornerRadius", FLOAT}, {"letterCase", STRING}, {"lineSpacing", FLOAT}, {"format", STRING}, {"displayRelative", BOOLEAN}, {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"gamelistinfo", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"stationary", STRING}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"horizontalAlignment", STRING}, {"verticalAlignment", STRING}, {"color", COLOR}, {"backgroundColor", COLOR}, {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"rating", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"stationary", STRING}, {"hideIfZero", BOOLEAN}, {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"interpolation", STRING}, {"color", COLOR}, {"filledPath", PATH}, {"unfilledPath", PATH}, {"overlay", BOOLEAN}, {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"gameselector", {{"selection", STRING}, {"gameCount", UNSIGNED_INTEGER}, {"allowDuplicates", BOOLEAN}}}, {"helpsystem", {{"pos", NORMALIZED_PAIR}, {"posDimmed", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"originDimmed", NORMALIZED_PAIR}, {"textColor", COLOR}, {"textColorDimmed", COLOR}, {"iconColor", COLOR}, {"iconColorDimmed", COLOR}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"fontSizeDimmed", FLOAT}, {"entrySpacing", FLOAT}, {"entrySpacingDimmed", FLOAT}, {"iconTextSpacing", FLOAT}, {"iconTextSpacingDimmed", FLOAT}, {"letterCase", STRING}, {"opacity", FLOAT}, {"opacityDimmed", FLOAT}, {"customButtonIcon", PATH}}}, {"sound", {{"path", PATH}}}}; // clang-format on ThemeData::ThemeData() : mCustomCollection {false} { sCurrentTheme = sThemes.find(Settings::getInstance()->getString("Theme")); sVariantDefinedTransitions = ""; } void ThemeData::loadFile(const std::map<std::string, std::string>& sysDataMap, const std::string& path, const ThemeTriggers::TriggerType trigger, const bool customCollection) { mCustomCollection = customCollection; mOverrideVariant = ""; mPaths.push_back(path); ThemeException error; error << "ThemeData::loadFile(): "; error.setFiles(mPaths); if (!Utils::FileSystem::exists(path)) throw error << "File does not exist"; mViews.clear(); mVariables.clear(); mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); pugi::xml_document doc; #if defined(_WIN64) pugi::xml_parse_result res {doc.load_file(Utils::String::stringToWideString(path).c_str())}; #else pugi::xml_parse_result res {doc.load_file(path.c_str())}; #endif if (!res) throw error << ": XML parsing error: " << res.description(); pugi::xml_node root {doc.child("theme")}; if (!root) throw error << ": Missing <theme> tag"; // Check if there's an unsupported theme version tag. if (root.child("formatVersion") != nullptr) throw error << ": Unsupported <formatVersion> tag found"; if (sCurrentTheme->second.capabilities.variants.size() > 0) { for (auto& variant : sCurrentTheme->second.capabilities.variants) mVariants.emplace_back(variant.name); if (std::find(mVariants.cbegin(), mVariants.cend(), Settings::getInstance()->getString("ThemeVariant")) != mVariants.cend()) mSelectedVariant = Settings::getInstance()->getString("ThemeVariant"); else mSelectedVariant = mVariants.front(); // Special shortcut variant to apply configuration to all defined variants. mVariants.emplace_back("all"); if (trigger != ThemeTriggers::TriggerType::NONE) { auto overrides = getCurrentThemeSelectedVariantOverrides(); if (overrides.find(trigger) != overrides.end()) mOverrideVariant = overrides.at(trigger).first; } } if (sCurrentTheme->second.capabilities.colorSchemes.size() > 0) { for (auto& colorScheme : sCurrentTheme->second.capabilities.colorSchemes) mColorSchemes.emplace_back(colorScheme.name); if (std::find(mColorSchemes.cbegin(), mColorSchemes.cend(), Settings::getInstance()->getString("ThemeColorScheme")) != mColorSchemes.cend()) mSelectedColorScheme = Settings::getInstance()->getString("ThemeColorScheme"); else mSelectedColorScheme = mColorSchemes.front(); } if (sCurrentTheme->second.capabilities.fontSizes.size() > 0) { for (auto& fontSize : sCurrentTheme->second.capabilities.fontSizes) mFontSizes.emplace_back(fontSize); if (std::find(mFontSizes.cbegin(), mFontSizes.cend(), Settings::getInstance()->getString("ThemeFontSize")) != mFontSizes.cend()) mSelectedFontSize = Settings::getInstance()->getString("ThemeFontSize"); else mSelectedFontSize = mFontSizes.front(); } sAspectRatioMatch = false; sThemeLanguage = ""; 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; } } } } } if (sCurrentTheme->second.capabilities.languages.size() > 0) { for (auto& language : sCurrentTheme->second.capabilities.languages) mLanguages.emplace_back(language); std::string langSetting {Settings::getInstance()->getString("ThemeLanguage")}; if (langSetting == "automatic") langSetting = Utils::Localization::sCurrentLocale; // Check if there is an exact match. if (std::find(sCurrentTheme->second.capabilities.languages.cbegin(), sCurrentTheme->second.capabilities.languages.cend(), langSetting) != sCurrentTheme->second.capabilities.languages.cend()) { sThemeLanguage = langSetting; } else { // We assume all locales are in the correct format. const std::string currLanguage {langSetting.substr(0, 2)}; // Select the closest matching locale (i.e. same language but possibly for a // different country). for (const auto& lang : sCurrentTheme->second.capabilities.languages) { if (lang.substr(0, 2) == currLanguage) { sThemeLanguage = lang; break; } } // If there is no match then fall back to the default language en_US, which is // mandatory for all themes that provide language support. if (sThemeLanguage == "") sThemeLanguage = "en_US"; } } parseVariables(root); parseColorSchemes(root); parseFontSizes(root); parseLanguages(root); parseIncludes(root); parseViews(root); if (root.child("feature") != nullptr) throw error << ": Unsupported <feature> tag found"; parseVariants(root); parseAspectRatios(root); } bool ThemeData::hasView(const std::string& view) { auto viewIt = mViews.find(view); return (viewIt != mViews.cend()); } const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const { auto viewIt = mViews.find(view); if (viewIt == mViews.cend()) return nullptr; // Not found. auto elemIt = viewIt->second.elements.find(element); if (elemIt == viewIt->second.elements.cend()) return nullptr; // If expectedType is an empty string, then skip type checking. if (elemIt->second.type != expectedType && !expectedType.empty()) { LOG(LogWarning) << "ThemeData::getElement(): Requested element \"" << view << "." << element << "\" has the wrong type, expected \"" << expectedType << "\", got \"" << elemIt->second.type << "\""; return nullptr; } return &elemIt->second; } void ThemeData::populateThemes() { sThemes.clear(); LOG(LogInfo) << "Checking for available themes..."; // Check for themes first under the user theme directory (which is in the ES-DE home directory // by default), then under the data installation directory (Unix only) and last under the ES-DE // binary directory. #if defined(__ANDROID__) const std::string userThemeDirectory {Utils::FileSystem::getInternalAppDataDirectory() + "/themes"}; #else const std::string defaultUserThemeDir {Utils::FileSystem::getAppDataDirectory() + "/themes"}; const std::string userThemeDirSetting {Utils::FileSystem::expandHomePath( Settings::getInstance()->getString("UserThemeDirectory"))}; std::string userThemeDirectory; if (userThemeDirSetting.empty()) { userThemeDirectory = defaultUserThemeDir; } else if (Utils::FileSystem::isDirectory(userThemeDirSetting) || Utils::FileSystem::isSymlink(userThemeDirSetting)) { userThemeDirectory = userThemeDirSetting; #if defined(_WIN64) LOG(LogInfo) << "Setting user theme directory to \"" << Utils::String::replace(userThemeDirectory, "/", "\\") << "\""; #else LOG(LogInfo) << "Setting user theme directory to \"" << userThemeDirectory << "\""; #endif } else { LOG(LogWarning) << "Requested user theme directory \"" << userThemeDirSetting << "\" does not exist or is not a directory, reverting to \"" << defaultUserThemeDir << "\""; userThemeDirectory = defaultUserThemeDir; } #endif #if defined(__ANDROID__) const std::vector<std::string> themePaths {Utils::FileSystem::getProgramDataPath() + "/themes", Utils::FileSystem::getAppDataDirectory() + "/themes", userThemeDirectory}; #elif defined(__APPLE__) const std::vector<std::string> themePaths { Utils::FileSystem::getExePath() + "/themes", Utils::FileSystem::getExePath() + "/../Resources/themes", userThemeDirectory}; #elif defined(_WIN64) || defined(APPIMAGE_BUILD) const std::vector<std::string> themePaths {Utils::FileSystem::getExePath() + "/themes", userThemeDirectory}; #else const std::vector<std::string> themePaths {Utils::FileSystem::getExePath() + "/themes", Utils::FileSystem::getProgramDataPath() + "/themes", userThemeDirectory}; #endif for (auto path : themePaths) { if (!Utils::FileSystem::isDirectory(path)) continue; Utils::FileSystem::StringList dirContent {Utils::FileSystem::getDirContent(path)}; for (Utils::FileSystem::StringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) { if (Utils::FileSystem::isDirectory(*it)) { const std::string themeDirName {Utils::FileSystem::getFileName(*it)}; if (themeDirName == "themes-list" || (themeDirName.length() >= 8 && Utils::String::toLower(themeDirName.substr(themeDirName.length() - 8, 8)) == "disabled")) continue; #if defined(_WIN64) LOG(LogDebug) << "Loading theme capabilities for \"" << Utils::String::replace(*it, "/", "\\") << "\"..."; #else LOG(LogDebug) << "Loading theme capabilities for \"" << *it << "\"..."; #endif ThemeCapability capabilities {parseThemeCapabilities((*it))}; if (!capabilities.validTheme) continue; std::string themeName; if (capabilities.themeName != "") themeName.append(" (\"").append(capabilities.themeName).append("\")"); #if defined(_WIN64) LOG(LogInfo) << "Added theme \"" << Utils::String::replace(*it, "/", "\\") << "\"" << themeName; #else LOG(LogInfo) << "Added theme \"" << *it << "\"" << themeName; #endif int aspectRatios {0}; int languages {0}; if (capabilities.aspectRatios.size() > 0) aspectRatios = static_cast<int>(capabilities.aspectRatios.size()) - 1; if (capabilities.languages.size() > 0) languages = static_cast<int>(capabilities.languages.size()) - 1; LOG(LogDebug) << "Theme includes support for " << capabilities.variants.size() << " variant" << (capabilities.variants.size() != 1 ? "s" : "") << ", " << capabilities.colorSchemes.size() << " color scheme" << (capabilities.colorSchemes.size() != 1 ? "s" : "") << ", " << capabilities.fontSizes.size() << " font size" << (capabilities.fontSizes.size() != 1 ? "s" : "") << ", " << aspectRatios << " aspect ratio" << (aspectRatios != 1 ? "s" : "") << ", " << capabilities.transitions.size() << " transition" << (capabilities.transitions.size() != 1 ? "s" : "") << " and " << languages << " language" << (languages != 1 ? "s" : ""); Theme theme {*it, capabilities}; sThemes[theme.getName()] = theme; } } } if (sThemes.empty()) { LOG(LogWarning) << "Couldn't find any themes, creating dummy entry"; Theme theme {"no-themes", ThemeCapability()}; sThemes[theme.getName()] = theme; sCurrentTheme = sThemes.begin(); } } const std::string ThemeData::getSystemThemeFile(const std::string& system) { if (sThemes.empty()) getThemes(); if (sThemes.empty()) return ""; std::map<std::string, Theme, StringComparator>::const_iterator theme { sThemes.find(Settings::getInstance()->getString("Theme"))}; if (theme == sThemes.cend()) { // Currently configured theme is missing, attempt to load the default theme linear-es-de // instead, and if that's also missing then pick the first available one. bool defaultSetFound {true}; theme = sThemes.find("linear-es-de"); if (theme == sThemes.cend()) { theme = sThemes.cbegin(); defaultSetFound = false; } LOG(LogWarning) << "Configured theme \"" << Settings::getInstance()->getString("Theme") << "\" does not exist, loading" << (defaultSetFound ? " default " : " ") << "theme \"" << theme->first << "\" instead"; Settings::getInstance()->setString("Theme", theme->first); sCurrentTheme = sThemes.find(Settings::getInstance()->getString("Theme")); } return theme->second.getThemePath(system); } const std::string ThemeData::getFontSizeLabel(const std::string& fontSize) { auto it = std::find_if(sSupportedFontSizes.cbegin(), sSupportedFontSizes.cend(), [&fontSize](const std::pair<std::string, std::string>& entry) { return entry.first == fontSize; }); if (it != sSupportedFontSizes.cend()) return it->second; else return "invalid font size"; } const std::string ThemeData::getAspectRatioLabel(const std::string& aspectRatio) { auto it = std::find_if(sSupportedAspectRatios.cbegin(), sSupportedAspectRatios.cend(), [&aspectRatio](const std::pair<std::string, std::string>& entry) { return entry.first == aspectRatio; }); if (it != sSupportedAspectRatios.cend()) return it->second; else return "invalid ratio"; } const std::string ThemeData::getLanguageLabel(const std::string& language) { auto it = std::find_if(sSupportedLanguages.cbegin(), sSupportedLanguages.cend(), [&language](const std::pair<std::string, std::string>& entry) { return entry.first == language; }); if (it != sSupportedLanguages.cend()) return it->second; else return "invalid language"; } 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, "_", " ") << "\""; } if (sThemeLanguage != "") { LOG(LogInfo) << "Theme language set to \"" << sThemeLanguage << "\""; } else { LOG(LogInfo) << "Theme does not have multilingual support"; } } unsigned int ThemeData::getHexColor(const std::string& str) { ThemeException error; if (str == "") throw error << "Empty color property"; const size_t length {str.size()}; if (length != 6 && length != 8) throw error << "Invalid color property \"" << str << "\" (must be 6 or 8 characters in length)"; unsigned int value; std::stringstream ss; ss << str; ss >> std::hex >> value; if (length == 6) value = (value << 8) | 0xFF; return value; } std::string ThemeData::resolvePlaceholders(const std::string& in) { if (in.empty()) return in; const size_t variableBegin {in.find("${")}; const size_t variableEnd {in.find("}", variableBegin)}; if ((variableBegin == std::string::npos) || (variableEnd == std::string::npos)) return in; std::string prefix {in.substr(0, variableBegin)}; std::string replace {in.substr(variableBegin + 2, variableEnd - (variableBegin + 2))}; std::string suffix {resolvePlaceholders(in.substr(variableEnd + 1).c_str())}; return prefix + mVariables[replace] + suffix; } ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string& path) { ThemeCapability capabilities; std::vector<std::string> aspectRatiosTemp; std::vector<std::string> fontSizesTemp; std::vector<std::string> languagesTemp; bool hasTriggers {false}; const std::string capFile {path + "/capabilities.xml"}; if (Utils::FileSystem::isRegularFile(capFile) || Utils::FileSystem::isSymlink(capFile)) { capabilities.validTheme = true; pugi::xml_document doc; #if defined(_WIN64) const pugi::xml_parse_result& res { doc.load_file(Utils::String::stringToWideString(capFile).c_str())}; #else const pugi::xml_parse_result& res {doc.load_file(capFile.c_str())}; #endif if (res.status == pugi::status_no_document_element) { LOG(LogDebug) << "Found a capabilities.xml file with no configuration"; } else if (!res) { LOG(LogError) << "Couldn't parse capabilities.xml: " << res.description(); return capabilities; } const pugi::xml_node& themeCapabilities {doc.child("themeCapabilities")}; if (!themeCapabilities) { LOG(LogError) << "Missing <themeCapabilities> tag in capabilities.xml"; return capabilities; } const pugi::xml_node& themeName {themeCapabilities.child("themeName")}; if (themeName != nullptr) capabilities.themeName = themeName.text().get(); for (pugi::xml_node aspectRatio {themeCapabilities.child("aspectRatio")}; aspectRatio; aspectRatio = aspectRatio.next_sibling("aspectRatio")) { const std::string& value {aspectRatio.text().get()}; if (std::find_if(sSupportedAspectRatios.cbegin(), sSupportedAspectRatios.cend(), [&value](const std::pair<std::string, std::string>& entry) { return entry.first == value; }) == sSupportedAspectRatios.cend()) { LOG(LogWarning) << "Declared aspect ratio \"" << value << "\" is not supported, ignoring entry in \"" << capFile << "\""; } else { if (std::find(aspectRatiosTemp.cbegin(), aspectRatiosTemp.cend(), value) != aspectRatiosTemp.cend()) { LOG(LogWarning) << "Aspect ratio \"" << value << "\" is declared multiple times, ignoring entry in \"" << capFile << "\""; } else { aspectRatiosTemp.emplace_back(value); } } } for (pugi::xml_node fontSize {themeCapabilities.child("fontSize")}; fontSize; fontSize = fontSize.next_sibling("fontSize")) { const std::string& value {fontSize.text().get()}; if (std::find_if(sSupportedFontSizes.cbegin(), sSupportedFontSizes.cend(), [&value](const std::pair<std::string, std::string>& entry) { return entry.first == value; }) == sSupportedFontSizes.cend()) { LOG(LogWarning) << "Declared font size \"" << value << "\" is not supported, ignoring entry in \"" << capFile << "\""; } else { if (std::find(fontSizesTemp.cbegin(), fontSizesTemp.cend(), value) != fontSizesTemp.cend()) { LOG(LogWarning) << "Font size \"" << value << "\" is declared multiple times, ignoring entry in \"" << capFile << "\""; } else { fontSizesTemp.emplace_back(value); } } } for (pugi::xml_node variant {themeCapabilities.child("variant")}; variant; variant = variant.next_sibling("variant")) { ThemeVariant readVariant; const std::string& name {variant.attribute("name").as_string()}; if (name.empty()) { LOG(LogWarning) << "Found <variant> tag without name attribute, ignoring entry in \"" << capFile << "\""; } else if (name == "all") { LOG(LogWarning) << "Found <variant> tag using reserved name \"all\", ignoring entry in \"" << capFile << "\""; } else { readVariant.name = name; } if (variant.child("label") == nullptr) { LOG(LogDebug) << "No variant <label> tags found, setting label value to the variant name \"" << name << "\" for \"" << capFile << "\""; readVariant.labels.emplace_back(std::make_pair("en_US", name)); } else { std::vector<std::pair<std::string, std::string>> labels; for (pugi::xml_node labelTag {variant.child("label")}; labelTag; labelTag = labelTag.next_sibling("label")) { std::string language {labelTag.attribute("language").as_string()}; if (language == "") language = "en_US"; else if (std::find_if(sSupportedLanguages.cbegin(), sSupportedLanguages.cend(), [language](std::pair<std::string, std::string> currLang) { return currLang.first == language; }) == sSupportedLanguages.cend()) { LOG(LogWarning) << "Declared language \"" << language << "\" not supported, setting label language to \"en_US\"" " for variant name \"" << name << "\" in \"" << capFile << "\""; language = "en_US"; } 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 << "\""; labelValue = name; } labels.emplace_back(std::make_pair(language, labelValue)); } if (!labels.empty()) { // Add the label languages in the order they are defined in sSupportedLanguages. for (auto& language : sSupportedLanguages) { if (language.first == "automatic") continue; const auto it = std::find_if(labels.cbegin(), labels.cend(), [language](std::pair<std::string, std::string> currLabel) { return currLabel.first == language.first; }); if (it != labels.cend()) { readVariant.labels.emplace_back( std::make_pair((*it).first, (*it).second)); } } } } 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; } if (colorScheme.child("label") == nullptr) { LOG(LogDebug) << "No colorScheme <label> tag found, setting label value to the " "color scheme name \"" << name << "\" for \"" << capFile << "\""; readColorScheme.labels.emplace_back(std::make_pair("en_US", name)); } else { std::vector<std::pair<std::string, std::string>> labels; for (pugi::xml_node labelTag {colorScheme.child("label")}; labelTag; labelTag = labelTag.next_sibling("label")) { std::string language {labelTag.attribute("language").as_string()}; if (language == "") language = "en_US"; else if (std::find_if(sSupportedLanguages.cbegin(), sSupportedLanguages.cend(), [language](std::pair<std::string, std::string> currLang) { return currLang.first == language; }) == sSupportedLanguages.cend()) { LOG(LogWarning) << "Declared language \"" << language << "\" not supported, setting label language to \"en_US\"" " for color scheme name \"" << name << "\" in \"" << capFile << "\""; language = "en_US"; } 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 << "\""; labelValue = name; } labels.emplace_back(std::make_pair(language, labelValue)); } if (!labels.empty()) { // Add the label languages in the order they are defined in sSupportedLanguages. for (auto& language : sSupportedLanguages) { if (language.first == "automatic") continue; const auto it = std::find_if(labels.cbegin(), labels.cend(), [language](std::pair<std::string, std::string> currLabel) { return currLabel.first == language.first; }); if (it != labels.cend()) { readColorScheme.labels.emplace_back( std::make_pair((*it).first, (*it).second)); } } } } 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 language {themeCapabilities.child("language")}; language; language = language.next_sibling("language")) { const std::string& value {language.text().get()}; if (std::find_if(sSupportedLanguages.cbegin(), sSupportedLanguages.cend(), [&value](const std::pair<std::string, std::string>& entry) { return entry.first == value; }) == sSupportedLanguages.cend()) { LOG(LogWarning) << "Declared language \"" << value << "\" is not supported, ignoring entry in \"" << capFile << "\""; } else { if (std::find(languagesTemp.cbegin(), languagesTemp.cend(), value) != languagesTemp.cend()) { LOG(LogWarning) << "Language \"" << value << "\" is declared multiple times, ignoring entry in \"" << capFile << "\""; } else { languagesTemp.emplace_back(value); } } } if (languagesTemp.size() > 0 && std::find(languagesTemp.cbegin(), languagesTemp.cend(), "en_US") == languagesTemp.cend()) { LOG(LogError) << "Theme has declared language support but is missing mandatory " << "\"en_US\" entry in \"" << capFile << "\""; languagesTemp.clear(); } 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& 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; std::vector<std::pair<std::string, std::string>> labels; for (pugi::xml_node labelTag {transitions.child("label")}; labelTag; labelTag = labelTag.next_sibling("label")) { std::string language {labelTag.attribute("language").as_string()}; if (language == "") language = "en_US"; else if (std::find_if(sSupportedLanguages.cbegin(), sSupportedLanguages.cend(), [language](std::pair<std::string, std::string> currLang) { return currLang.first == language; }) == sSupportedLanguages.cend()) { LOG(LogWarning) << "Declared language \"" << language << "\" not supported, setting label language to \"en_US\"" " for transition name \"" << name << "\" in \"" << capFile << "\""; language = "en_US"; } std::string labelValue {labelTag.text().as_string()}; if (labelValue != "") labels.emplace_back(std::make_pair(language, labelValue)); } if (!labels.empty()) { // Add the label languages in the order they are defined in sSupportedLanguages. for (auto& language : sSupportedLanguages) { if (language.first == "automatic") continue; const auto it = std::find_if(labels.cbegin(), labels.cend(), [language](std::pair<std::string, std::string> currLabel) { return currLabel.first == language.first; }); if (it != labels.cend()) { transition.labels.emplace_back( std::make_pair((*it).first, (*it).second)); } } } 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. capabilities.aspectRatios.emplace_back(sSupportedAspectRatios.front().first); for (auto& aspectRatio : sSupportedAspectRatios) { if (std::find(aspectRatiosTemp.cbegin(), aspectRatiosTemp.cend(), aspectRatio.first) != aspectRatiosTemp.cend()) { capabilities.aspectRatios.emplace_back(aspectRatio.first); } } } // Add the languages in the order they are defined in sSupportedLanguages so they always // show up in the same order in the UI Settings menu. if (!languagesTemp.empty()) { // Add the "automatic" language if there is at least one entry. capabilities.languages.emplace_back(sSupportedLanguages.front().first); for (auto& language : sSupportedLanguages) { if (std::find(languagesTemp.cbegin(), languagesTemp.cend(), language.first) != languagesTemp.cend()) { capabilities.languages.emplace_back(language.first); } } } // Add the font sizes in the order they are defined in sSupportedFontSizes so they always // show up in the same order in the UI Settings menu. if (!fontSizesTemp.empty()) { for (auto& fontSize : sSupportedFontSizes) { if (std::find(fontSizesTemp.cbegin(), fontSizesTemp.cend(), fontSize.first) != fontSizesTemp.cend()) { capabilities.fontSizes.emplace_back(fontSize.first); } } } if (hasTriggers) { for (auto& variant : capabilities.variants) { for (auto it = variant.overrides.begin(); it != variant.overrides.end();) { const auto variantIter = std::find_if(capabilities.variants.begin(), capabilities.variants.end(), [it](ThemeVariant currVariant) { return currVariant.name == (*it).second.first; }); if (variantIter == capabilities.variants.end()) { LOG(LogWarning) << "The <useVariant> tag value \"" << (*it).second.first << "\" does not match any defined variants, ignoring entry in \"" << capFile << "\""; it = variant.overrides.erase(it); } else { ++it; } } } } return capabilities; } void ThemeData::parseIncludes(const pugi::xml_node& root) { for (pugi::xml_node node {root.child("include")}; node; node = node.next_sibling("include")) { ThemeException error; error << "ThemeData::parseIncludes(): "; error.setFiles(mPaths); // Check if there's an unsupported theme version tag. if (root.child("formatVersion") != nullptr) throw error << ": Unsupported <formatVersion> tag found"; std::string relPath {resolvePlaceholders(node.text().as_string())}; std::string path {Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true)}; if (!ResourceManager::getInstance().fileExists(path)) { // For explicit paths, throw an error if the file couldn't be found, but only // print a debug message if it was set using a variable. if (relPath == node.text().get()) { throw error << " -> \"" << relPath << "\" not found (resolved to \"" << path << "\")"; } else { if (!(Settings::getInstance()->getBool("DebugSkipMissingThemeFiles") || (mCustomCollection && Settings::getInstance()->getBool( "DebugSkipMissingThemeFilesCustomCollections")))) { #if defined(_WIN64) LOG(LogDebug) << Utils::String::replace(error.message, "/", "\\") << ": Couldn't find file \"" << node.text().get() << "\" " << ((node.text().get() != path) ? "which resolves to \"" + Utils::String::replace(path, "/", "\\") + "\"" : #else LOG(LogDebug) << error.message << ": Couldn't find file \"" << node.text().get() << "\" " << ((node.text().get() != path) ? "which resolves to \"" + path + "\"" : #endif ""); } continue; } } error << " -> \"" << relPath << "\""; mPaths.push_back(path); pugi::xml_document includeDoc; #if defined(_WIN64) pugi::xml_parse_result result { includeDoc.load_file(Utils::String::stringToWideString(path).c_str())}; #else pugi::xml_parse_result result {includeDoc.load_file(path.c_str())}; #endif if (!result) throw error << ": Error parsing file: " << result.description(); pugi::xml_node theme {includeDoc.child("theme")}; if (!theme) throw error << ": Missing <theme> tag"; parseTransitions(theme); parseVariables(theme); parseColorSchemes(theme); parseFontSizes(theme); parseLanguages(theme); parseIncludes(theme); parseViews(theme); if (theme.child("feature") != nullptr) throw error << ": Unsupported <feature> tag found"; parseVariants(theme); parseAspectRatios(theme); mPaths.pop_back(); } } void ThemeData::parseVariants(const pugi::xml_node& root) { if (sCurrentTheme == sThemes.end()) return; if (mSelectedVariant == "") return; ThemeException error; error << "ThemeData::parseVariants(): "; error.setFiles(mPaths); for (pugi::xml_node node {root.child("variant")}; node; node = node.next_sibling("variant")) { if (!node.attribute("name")) throw error << ": <variant> tag missing \"name\" attribute"; const std::string delim {" \t\r\n,"}; const std::string nameAttr {node.attribute("name").as_string()}; size_t prevOff {nameAttr.find_first_not_of(delim, 0)}; size_t off {nameAttr.find_first_of(delim, prevOff)}; std::string viewKey; while (off != std::string::npos || prevOff != std::string::npos) { viewKey = nameAttr.substr(prevOff, off - prevOff); prevOff = nameAttr.find_first_not_of(delim, off); off = nameAttr.find_first_of(delim, prevOff); if (std::find(mVariants.cbegin(), mVariants.cend(), viewKey) == mVariants.cend()) { throw error << ": <variant> value \"" << viewKey << "\" is not defined in capabilities.xml"; } const std::string variant {mOverrideVariant.empty() ? mSelectedVariant : mOverrideVariant}; if (variant == viewKey || viewKey == "all") { parseTransitions(node); parseVariables(node); parseColorSchemes(node); parseFontSizes(node); parseLanguages(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); parseIncludes(node); } } } } void ThemeData::parseFontSizes(const pugi::xml_node& root) { if (sCurrentTheme == sThemes.end()) return; if (mSelectedFontSize == "") return; ThemeException error; error << "ThemeData::parseFontSizes(): "; error.setFiles(mPaths); for (pugi::xml_node node {root.child("fontSize")}; node; node = node.next_sibling("fontSize")) { if (!node.attribute("name")) throw error << ": <fontSize> tag missing \"name\" attribute"; const std::string delim {" \t\r\n,"}; const std::string nameAttr {node.attribute("name").as_string()}; size_t prevOff {nameAttr.find_first_not_of(delim, 0)}; size_t off {nameAttr.find_first_of(delim, prevOff)}; std::string viewKey; while (off != std::string::npos || prevOff != std::string::npos) { viewKey = nameAttr.substr(prevOff, off - prevOff); prevOff = nameAttr.find_first_not_of(delim, off); off = nameAttr.find_first_of(delim, prevOff); if (std::find(mFontSizes.cbegin(), mFontSizes.cend(), viewKey) == mFontSizes.cend()) { throw error << ": <fontSize> value \"" << viewKey << "\" is not defined in capabilities.xml"; } if (mSelectedFontSize == viewKey) { parseVariables(node); parseIncludes(node); } } } } void ThemeData::parseAspectRatios(const pugi::xml_node& root) { if (sCurrentTheme == sThemes.end()) return; if (sSelectedAspectRatio == "") return; ThemeException error; error << "ThemeData::parseAspectRatios(): "; error.setFiles(mPaths); for (pugi::xml_node node {root.child("aspectRatio")}; node; node = node.next_sibling("aspectRatio")) { if (!node.attribute("name")) throw error << ": <aspectRatio> tag missing \"name\" attribute"; const std::string delim {" \t\r\n,"}; const std::string nameAttr {node.attribute("name").as_string()}; size_t prevOff {nameAttr.find_first_not_of(delim, 0)}; size_t off {nameAttr.find_first_of(delim, prevOff)}; std::string viewKey; while (off != std::string::npos || prevOff != std::string::npos) { viewKey = nameAttr.substr(prevOff, off - prevOff); prevOff = nameAttr.find_first_not_of(delim, off); off = nameAttr.find_first_of(delim, prevOff); if (std::find(sCurrentTheme->second.capabilities.aspectRatios.cbegin(), sCurrentTheme->second.capabilities.aspectRatios.cend(), viewKey) == sCurrentTheme->second.capabilities.aspectRatios.cend()) { throw error << ": <aspectRatio> value \"" << viewKey << "\" is not defined in capabilities.xml"; } if (sSelectedAspectRatio == viewKey) { parseVariables(node); parseColorSchemes(node); parseFontSizes(node); parseLanguages(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::parseLanguages(const pugi::xml_node& root) { if (sCurrentTheme == sThemes.end()) return; if (sThemeLanguage == "") return; ThemeException error; error << "ThemeData::parseLanguages(): "; error.setFiles(mPaths); for (pugi::xml_node node {root.child("language")}; node; node = node.next_sibling("language")) { if (!node.attribute("name")) throw error << ": <language> 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(mLanguages.cbegin(), mLanguages.cend(), viewKey) == mLanguages.cend()) { throw error << ": <language> value \"" << viewKey << "\" is not defined in capabilities.xml"; } if (sThemeLanguage == viewKey) { parseVariables(node); parseIncludes(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()) { if (mVariables.find(key) != mVariables.end()) mVariables[key] = val; else mVariables.insert(std::pair<std::string, std::string>(key, val)); } } } } void ThemeData::parseViews(const pugi::xml_node& root) { ThemeException error; error << "ThemeData::parseViews(): "; error.setFiles(mPaths); // Parse views. for (pugi::xml_node node {root.child("view")}; node; node = node.next_sibling("view")) { if (!node.attribute("name")) throw error << ": View missing \"name\" attribute"; const std::string delim {" \t\r\n,"}; const std::string nameAttr {node.attribute("name").as_string()}; size_t prevOff {nameAttr.find_first_not_of(delim, 0)}; size_t off {nameAttr.find_first_of(delim, prevOff)}; std::string viewKey; while (off != std::string::npos || prevOff != std::string::npos) { viewKey = nameAttr.substr(prevOff, off - prevOff); prevOff = nameAttr.find_first_not_of(delim, off); off = nameAttr.find_first_of(delim, prevOff); if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) { ThemeView& view { mViews.insert(std::pair<std::string, ThemeView>(viewKey, ThemeView())) .first->second}; parseView(node, view); } else { throw error << ": Unsupported \"" << viewKey << "\" view style defined"; } } } } void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) { ThemeException error; error << "ThemeData::parseView(): "; error.setFiles(mPaths); for (pugi::xml_node node {root.first_child()}; node; node = node.next_sibling()) { if (!node.attribute("name")) throw error << ": Element of type \"" << node.name() << "\" missing \"name\" attribute"; auto elemTypeIt = sElementMap.find(node.name()); if (elemTypeIt == sElementMap.cend()) throw error << ": Unknown element type \"" << node.name() << "\""; const std::string delim {" \t\r\n,"}; const std::string nameAttr {node.attribute("name").as_string()}; size_t prevOff {nameAttr.find_first_not_of(delim, 0)}; size_t off {nameAttr.find_first_of(delim, prevOff)}; while (off != std::string::npos || prevOff != std::string::npos) { std::string elemKey {nameAttr.substr(prevOff, off - prevOff)}; prevOff = nameAttr.find_first_not_of(delim, off); off = nameAttr.find_first_of(delim, prevOff); // Add the element type as a prefix to avoid name collisions between different // component types. elemKey = std::string {node.name()} + "_" + elemKey; parseElement( node, elemTypeIt->second, view.elements.insert(std::pair<std::string, ThemeElement>(elemKey, ThemeElement())) .first->second); } } } void ThemeData::parseElement(const pugi::xml_node& root, const std::map<std::string, ElementPropertyType>& typeMap, ThemeElement& element) { ThemeException error; error << "ThemeData::parseElement(): "; error.setFiles(mPaths); element.type = root.name(); if (root.attribute("extra") != nullptr) throw error << ": Unsupported \"extra\" attribute found for element of type \"" << element.type << "\""; for (pugi::xml_node node {root.first_child()}; node; node = node.next_sibling()) { auto typeIt = typeMap.find(node.name()); if (typeIt == typeMap.cend()) throw error << ": Unknown property type \"" << node.name() << "\" for element of type \"" << root.name() << "\""; std::string str {resolvePlaceholders(node.text().as_string())}; // Handle the special case with mutually exclusive system variables, for example // system.fullName.autoCollections and system.fullName.noCollections which can never // exist at the same time. A backspace is assigned in SystemData to flag the // variables that do not apply and if it's encountered here we simply skip the // property. if (str == "\b") continue; // Strictly enforce that there are no blank values in the theme configuration. if (str == "") throw error << ": Property \"" << typeIt->first << "\" for element \"" << element.type << "\" has no value defined"; std::string nodeName {node.name()}; // If an attribute exists, then replace nodeName with its name. auto attributeEntry = sPropertyAttributeMap.find(element.type); if (attributeEntry != sPropertyAttributeMap.end()) { auto attribute = attributeEntry->second.find(typeIt->first); if (attribute != attributeEntry->second.end()) { if (node.attribute(attribute->second.c_str()) == nullptr) { throw error << ": Unknown attribute \"" << node.first_attribute().name() << "\" for property \"" << typeIt->first << "\" (element \"" << attributeEntry->first << "\")"; } else { // Add the attribute name as a prefix to avoid potential name collisions. nodeName = attribute->second + "_" + node.attribute(attribute->second.c_str()).as_string(""); } } } switch (typeIt->second) { case NORMALIZED_RECT: { glm::vec4 val; auto splits = Utils::String::delimitedStringToVector(str, " "); if (splits.size() == 2) { val = glm::vec4 {static_cast<float>(atof(splits.at(0).c_str())), static_cast<float>(atof(splits.at(1).c_str())), static_cast<float>(atof(splits.at(0).c_str())), static_cast<float>(atof(splits.at(1).c_str()))}; } else if (splits.size() == 4) { val = glm::vec4 {static_cast<float>(atof(splits.at(0).c_str())), static_cast<float>(atof(splits.at(1).c_str())), static_cast<float>(atof(splits.at(2).c_str())), static_cast<float>(atof(splits.at(3).c_str()))}; } element.properties[node.name()] = val; break; } case NORMALIZED_PAIR: { size_t divider = str.find(' '); if (divider == std::string::npos) throw error << ": Invalid normalized pair value \"" << str.c_str() << "\" for property \"" << node.name() << "\""; std::string first {str.substr(0, divider)}; std::string second {str.substr(divider, std::string::npos)}; glm::vec2 val {static_cast<float>(atof(first.c_str())), static_cast<float>(atof(second.c_str()))}; element.properties[node.name()] = val; break; } case STRING: { element.properties[node.name()] = str; break; } case PATH: { std::string path; if (!str.empty() && str.front() == ':') path = ResourceManager::getInstance().getResourcePath(str); else path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true); if (!ResourceManager::getInstance().fileExists(path)) { std::stringstream ss; // For explicit paths, print a warning if the file couldn't be found, but // only print a debug message if it was set using a variable. if (str == node.text().as_string()) { #if defined(_WIN64) LOG(LogWarning) << Utils::String::replace(error.message, "/", "\\") << ": Couldn't find file \"" << node.text().get() << "\" " << ((node.text().get() != path) ? "which resolves to \"" + Utils::String::replace(path, "/", "\\") + "\"" : #else LOG(LogWarning) << error.message << ": Couldn't find file \"" << node.text().get() << "\" " << ((node.text().get() != path) ? "which resolves to \"" + path + "\"" : #endif "") << " (element type \"" << element.type << "\", name \"" << root.attribute("name").as_string() << "\", property \"" << nodeName << "\")"; } else if (!(Settings::getInstance()->getBool("DebugSkipMissingThemeFiles") || (mCustomCollection && Settings::getInstance()->getBool( "DebugSkipMissingThemeFilesCustomCollections")))) { #if defined(_WIN64) LOG(LogDebug) << Utils::String::replace(error.message, "/", "\\") << ": Couldn't find file \"" << node.text().get() << "\" " << ((node.text().get() != path) ? "which resolves to \"" + Utils::String::replace(path, "/", "\\") + "\"" : #else LOG(LogDebug) << error.message << ": Couldn't find file \"" << node.text().get() << "\" " << ((node.text().get() != path) ? "which resolves to \"" + path + "\"" : #endif "") << " (element type \"" << element.type << "\", name \"" << root.attribute("name").as_string() << "\", property \"" << nodeName << "\")"; } } element.properties[nodeName] = path; break; } case COLOR: { try { element.properties[node.name()] = getHexColor(str); } catch (ThemeException& e) { throw error << ": " << e.what(); } break; } case UNSIGNED_INTEGER: { unsigned int integerVal {static_cast<unsigned int>(strtoul(str.c_str(), 0, 0))}; element.properties[node.name()] = integerVal; break; } case FLOAT: { float floatVal {static_cast<float>(strtod(str.c_str(), 0))}; element.properties[node.name()] = floatVal; break; } case BOOLEAN: { bool boolVal = false; // Only look at the first character. if (str.size() > 0) { if (str.front() == '1' || str.front() == 't' || str.front() == 'T' || str.front() == 'y' || str.front() == 'Y') boolVal = true; } element.properties[node.name()] = boolVal; break; } default: { throw error << ": Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name(); } } } } #if defined(GETTEXT_DUMMY_ENTRIES) void ThemeData::gettextMessageCatalogEntries() { // sSupportedFontSizes _("medium"); _("large"); _("small"); _("extra small"); _("extra large"); // sSupportedAspectRatios _("automatic"); _("16:9 vertical"); _("16:10 vertical"); _("3:2 vertical"); _("4:3 vertical"); _("5:4 vertical"); _("19.5:9 vertical"); _("20:9 vertical"); _("21:9 vertical"); _("32:9 vertical"); } #endif