// 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 #include // clang-format off std::vector ThemeData::sSupportedViews { {"all"}, {"system"}, {"gamelist"}}; std::vector ThemeData::sSupportedMediaTypes { {"miximage"}, {"marquee"}, {"screenshot"}, {"titlescreen"}, {"cover"}, {"backcover"}, {"3dbox"}, {"physicalmedia"}, {"fanart"}, {"video"}}; std::vector ThemeData::sSupportedTransitions { {"systemToSystem"}, {"systemToGamelist"}, {"gamelistToGamelist"}, {"gamelistToSystem"}, {"startupToSystem"}, {"startupToGamelist"}}; std::vector ThemeData::sSupportedTransitionAnimations { {"builtin-instant"}, {"builtin-slide"}, {"builtin-fade"}}; std::vector> ThemeData::sSupportedFontSizes { {"medium", "medium"}, {"large", "large"}, {"small", "small"}, {"x-large", "extra large"}, {"x-small", "extra small"}}; std::vector> 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 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> ThemeData::sSupportedLanguages { {"automatic", "automatic"}, {"en_US", "ENGLISH (UNITED STATES)"}, {"en_GB", "ENGLISH (UNITED KINGDOM)"}, {"ca_ES", "CATALÀ"}, {"de_DE", "DEUTSCH"}, {"es_ES", "ESPAÑOL (ESPAÑA)"}, {"fr_FR", "FRANÇAIS"}, {"it_IT", "ITALIANO"}, {"nl_NL", "NEDERLANDS"}, {"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> ThemeData::sPropertyAttributeMap // The data type is defined by the parent property. { {"badges", {{"customBadgeIcon", "badge"}, {"customControllerIcon", "controller"}}}, {"helpsystem", {{"customButtonIcon", "button"}}}, }; std::map> 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}, {"textBackgroundCornerRadius", 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}, {"textBackgroundCornerRadius", 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& 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 tag"; // Check if there's an unsupported theme version tag. if (root.child("formatVersion") != nullptr) throw error << ": Unsupported 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 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 themePaths {Utils::FileSystem::getProgramDataPath() + "/themes", Utils::FileSystem::getAppDataDirectory() + "/themes", userThemeDirectory}; #elif defined(__APPLE__) const std::vector themePaths { Utils::FileSystem::getExePath() + "/themes", Utils::FileSystem::getExePath() + "/../Resources/themes", userThemeDirectory}; #elif defined(_WIN64) || defined(APPIMAGE_BUILD) const std::vector themePaths {Utils::FileSystem::getExePath() + "/themes", userThemeDirectory}; #else const std::vector 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(capabilities.aspectRatios.size()) - 1; if (capabilities.languages.size() > 0) languages = static_cast(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::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& 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& 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& 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( 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(ViewTransitionAnimation::SLIDE); } else if (transitionsSetting == "builtin-fade") { transitionAnim = static_cast(ViewTransitionAnimation::FADE); } setTransitionsFunc(transitionAnim); } } } const std::map>> 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 aspectRatiosTemp; std::vector fontSizesTemp; std::vector 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 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& 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& 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 tag without name attribute, ignoring entry in \"" << capFile << "\""; } else if (name == "all") { LOG(LogWarning) << "Found tag using reserved name \"all\", ignoring entry in \"" << capFile << "\""; } else { readVariant.name = name; } if (variant.child("label") == nullptr) { LOG(LogDebug) << "No variant