// SPDX-License-Identifier: MIT // // EmulationStation Desktop Edition // ThemeData.cpp // // Finds available themes on the file system and loads and parses these. // Basic error checking for valid elements and data types is done here, // with additional validation handled by the individual components. // #include "ThemeData.h" #include "Log.h" #include "Settings.h" #include "components/ImageComponent.h" #include "components/TextComponent.h" #include "utils/FileSystemUtil.h" #include "utils/StringUtil.h" #include #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::sSupportedAspectRatios { {"automatic", "automatic"}, {"16:9", "16:9"}, {"16:9_vertical", "16:9 vertical"}, {"16:10", "16:10"}, {"16:10_vertical", "16:10 vertical"}, {"3:2", "3:2"}, {"3:2_vertical", "3:2 vertical"}, {"4:3", "4:3"}, {"4:3_vertical", "4:3 vertical"}, {"5:4", "5:4"}, {"5:4_vertical", "5:4 vertical"}, {"21:9", "21:9"}, {"21:9_vertical", "21:9 vertical"}, {"32:9", "32:0"}, {"32:9_vertical", "32:9 vertical"}}; std::map ThemeData::sAspectRatioMap { {"16:9", 1.7777f}, {"16:9_vertical", 0.5625f}, {"16:10", 1.6f}, {"16:10_vertical", 0.625f}, {"3:2", 1.5f}, {"3:2_vertical", 0.6667f}, {"4:3", 1.3333f}, {"4:3_vertical", 0.75f}, {"5:4", 1.25f}, {"5:4_vertical", 0.8f}, {"21:9", 2.3703f}, {"21:9_vertical", 0.4219f}, {"32:9", 3.5555f}, {"32:9_vertical", 0.2813f}}; std::map> 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}, {"itemSize", NORMALIZED_PAIR}, {"itemScale", FLOAT}, {"itemRotation", FLOAT}, {"itemRotationOrigin", NORMALIZED_PAIR}, {"itemAxisHorizontal", BOOLEAN}, {"itemAxisRotation", FLOAT}, {"imageFit", STRING}, {"imageInterpolation", STRING}, {"imageCornerRadius", FLOAT}, {"imageColor", COLOR}, {"imageColorEnd", COLOR}, {"imageGradientType", STRING}, {"imageSelectedColor", COLOR}, {"imageSelectedColorEnd", COLOR}, {"imageSelectedGradientType", STRING}, {"imageBrightness", FLOAT}, {"imageSaturation", FLOAT}, {"itemTransitions", STRING}, {"itemDiagonalOffset", FLOAT}, {"itemHorizontalAlignment", STRING}, {"itemVerticalAlignment", STRING}, {"wheelHorizontalAlignment", STRING}, {"wheelVerticalAlignment", STRING}, {"horizontalOffset", FLOAT}, {"verticalOffset", FLOAT}, {"reflections", BOOLEAN}, {"reflectionsOpacity", FLOAT}, {"reflectionsFalloff", FLOAT}, {"unfocusedItemOpacity", FLOAT}, {"unfocusedItemSaturation", FLOAT}, {"unfocusedItemDimming", FLOAT}, {"fastScrolling", BOOLEAN}, {"color", COLOR}, {"colorEnd", COLOR}, {"gradientType", STRING}, {"text", STRING}, {"textRelativeScale", FLOAT}, {"textColor", COLOR}, {"textBackgroundColor", COLOR}, {"textSelectedColor", COLOR}, {"textSelectedBackgroundColor", COLOR}, {"textHorizontalScrolling", BOOLEAN}, {"textHorizontalScrollSpeed", FLOAT}, {"textHorizontalScrollDelay", FLOAT}, {"textHorizontalScrollGap", FLOAT}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"letterCase", STRING}, {"letterCaseAutoCollections", STRING}, {"letterCaseCustomCollections", STRING}, {"lineSpacing", FLOAT}, {"systemNameSuffix", BOOLEAN}, {"letterCaseSystemNameSuffix", STRING}, {"fadeAbovePrimary", BOOLEAN}, {"zIndex", FLOAT}}}, {"grid", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"staticImage", PATH}, {"imageType", STRING}, {"defaultImage", PATH}, {"defaultFolderImage", PATH}, {"itemSize", NORMALIZED_PAIR}, {"itemScale", FLOAT}, {"itemSpacing", NORMALIZED_PAIR}, {"scaleInwards", BOOLEAN}, {"fractionalRows", BOOLEAN}, {"itemTransitions", STRING}, {"rowTransitions", STRING}, {"unfocusedItemOpacity", FLOAT}, {"unfocusedItemSaturation", FLOAT}, {"unfocusedItemDimming", FLOAT}, {"imageFit", STRING}, {"imageInterpolation", STRING}, {"imageRelativeScale", FLOAT}, {"imageCornerRadius", FLOAT}, {"imageColor", COLOR}, {"imageColorEnd", COLOR}, {"imageGradientType", STRING}, {"imageSelectedColor", COLOR}, {"imageSelectedColorEnd", COLOR}, {"imageSelectedGradientType", STRING}, {"imageBrightness", FLOAT}, {"imageSaturation", FLOAT}, {"backgroundImage", PATH}, {"backgroundRelativeScale", FLOAT}, {"backgroundCornerRadius", FLOAT}, {"backgroundColor", COLOR}, {"backgroundColorEnd", COLOR}, {"backgroundGradientType", STRING}, {"selectorImage", PATH}, {"selectorRelativeScale", FLOAT}, {"selectorCornerRadius", FLOAT}, {"selectorLayer", STRING}, {"selectorColor", COLOR}, {"selectorColorEnd", COLOR}, {"selectorGradientType", STRING}, {"text", STRING}, {"textRelativeScale", FLOAT}, {"textColor", COLOR}, {"textBackgroundColor", COLOR}, {"textSelectedColor", COLOR}, {"textSelectedBackgroundColor", COLOR}, {"textHorizontalScrolling", BOOLEAN}, {"textHorizontalScrollSpeed", FLOAT}, {"textHorizontalScrollDelay", FLOAT}, {"textHorizontalScrollGap", FLOAT}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"letterCase", STRING}, {"letterCaseAutoCollections", STRING}, {"letterCaseCustomCollections", STRING}, {"lineSpacing", FLOAT}, {"systemNameSuffix", BOOLEAN}, {"letterCaseSystemNameSuffix", STRING}, {"fadeAbovePrimary", BOOLEAN}, {"zIndex", FLOAT}}}, {"textlist", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"selectorHeight", FLOAT}, {"selectorHorizontalOffset", FLOAT}, {"selectorVerticalOffset", FLOAT}, {"selectorColor", COLOR}, {"selectorColorEnd", COLOR}, {"selectorGradientType", STRING}, {"selectorImagePath", PATH}, {"selectorImageTile", BOOLEAN}, {"primaryColor", COLOR}, {"secondaryColor", COLOR}, {"selectedColor", COLOR}, {"selectedSecondaryColor", COLOR}, {"selectedBackgroundColor", COLOR}, {"selectedSecondaryBackgroundColor", COLOR}, {"textHorizontalScrolling", BOOLEAN}, {"textHorizontalScrollSpeed", FLOAT}, {"textHorizontalScrollDelay", FLOAT}, {"textHorizontalScrollGap", FLOAT}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"horizontalAlignment", STRING}, {"horizontalMargin", FLOAT}, {"letterCase", STRING}, {"letterCaseAutoCollections", STRING}, {"letterCaseCustomCollections", STRING}, {"lineSpacing", FLOAT}, {"indicators", STRING}, {"collectionIndicators", STRING}, {"systemNameSuffix", BOOLEAN}, {"letterCaseSystemNameSuffix", STRING}, {"fadeAbovePrimary", BOOLEAN}, {"zIndex", FLOAT}}}, {"image", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"cropSize", NORMALIZED_PAIR}, {"maxSize", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"stationary", STRING}, {"flipHorizontal", BOOLEAN}, {"flipVertical", BOOLEAN}, {"path", PATH}, {"gameOverridePath", PATH}, {"default", PATH}, {"imageType", STRING}, {"metadataElement", BOOLEAN}, {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"tile", BOOLEAN}, {"tileSize", NORMALIZED_PAIR}, {"tileHorizontalAlignment", STRING}, {"tileVerticalAlignment", STRING}, {"interpolation", STRING}, {"cornerRadius", FLOAT}, {"color", COLOR}, {"colorEnd", COLOR}, {"gradientType", STRING}, {"scrollFadeIn", BOOLEAN}, {"brightness", FLOAT}, {"opacity", FLOAT}, {"saturation", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"video", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"cropSize", NORMALIZED_PAIR}, {"maxSize", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"stationary", STRING}, {"path", PATH}, {"default", PATH}, {"defaultImage", PATH}, {"imageType", STRING}, {"metadataElement", BOOLEAN}, {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"iterationCount", UNSIGNED_INTEGER}, {"onIterationsDone", STRING}, {"audio", BOOLEAN}, {"interpolation", STRING}, {"imageCornerRadius", FLOAT}, {"videoCornerRadius", FLOAT}, {"color", COLOR}, {"colorEnd", COLOR}, {"gradientType", STRING}, {"pillarboxes", BOOLEAN}, {"pillarboxThreshold", NORMALIZED_PAIR}, {"scanlines", BOOLEAN}, {"delay", FLOAT}, {"fadeInTime", FLOAT}, {"scrollFadeIn", BOOLEAN}, {"brightness", FLOAT}, {"opacity", FLOAT}, {"saturation", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"animation", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"maxSize", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"stationary", STRING}, {"metadataElement", BOOLEAN}, {"path", PATH}, {"speed", FLOAT}, {"direction", STRING}, {"iterationCount", UNSIGNED_INTEGER}, {"interpolation", STRING}, {"cornerRadius", FLOAT}, {"color", COLOR}, {"colorEnd", COLOR}, {"gradientType", STRING}, {"brightness", FLOAT}, {"opacity", FLOAT}, {"saturation", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"badges", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"stationary", STRING}, {"horizontalAlignment", STRING}, {"direction", STRING}, {"lines", UNSIGNED_INTEGER}, {"itemsPerLine", UNSIGNED_INTEGER}, {"itemMargin", NORMALIZED_PAIR}, {"slots", STRING}, {"controllerPos", NORMALIZED_PAIR}, {"controllerSize", FLOAT}, {"customBadgeIcon", PATH}, {"customControllerIcon", PATH}, {"folderLinkPos", NORMALIZED_PAIR}, {"folderLinkSize", FLOAT}, {"customFolderLinkIcon", PATH}, {"badgeIconColor", COLOR}, {"badgeIconColorEnd", COLOR}, {"badgeIconGradientType", STRING}, {"controllerIconColor", COLOR}, {"controllerIconColorEnd", COLOR}, {"controllerIconGradientType", STRING}, {"folderLinkIconColor", COLOR}, {"folderLinkIconColorEnd", COLOR}, {"folderLinkIconGradientType", STRING}, {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"text", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"stationary", STRING}, {"text", STRING}, {"systemdata", STRING}, {"metadata", STRING}, {"defaultValue", STRING}, {"systemNameSuffix", BOOLEAN}, {"letterCaseSystemNameSuffix", STRING}, {"metadataElement", BOOLEAN}, {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"container", BOOLEAN}, {"containerType", STRING}, {"containerVerticalSnap", BOOLEAN}, {"containerScrollSpeed", FLOAT}, {"containerStartDelay", FLOAT}, {"containerResetDelay", FLOAT}, {"containerScrollGap", FLOAT}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"horizontalAlignment", STRING}, {"verticalAlignment", STRING}, {"color", COLOR}, {"backgroundColor", COLOR}, {"letterCase", STRING}, {"lineSpacing", FLOAT}, {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"datetime", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"stationary", STRING}, {"metadata", STRING}, {"defaultValue", STRING}, {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"horizontalAlignment", STRING}, {"verticalAlignment", STRING}, {"color", COLOR}, {"backgroundColor", COLOR}, {"letterCase", STRING}, {"lineSpacing", FLOAT}, {"format", STRING}, {"displayRelative", BOOLEAN}, {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"gamelistinfo", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"stationary", STRING}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"horizontalAlignment", STRING}, {"verticalAlignment", STRING}, {"color", COLOR}, {"backgroundColor", COLOR}, {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"rating", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"stationary", STRING}, {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"interpolation", STRING}, {"color", COLOR}, {"filledPath", PATH}, {"unfilledPath", PATH}, {"overlay", BOOLEAN}, {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"gameselector", {{"selection", STRING}, {"gameCount", UNSIGNED_INTEGER}, {"allowDuplicates", BOOLEAN}}}, {"helpsystem", {{"pos", NORMALIZED_PAIR}, {"posDimmed", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"originDimmed", NORMALIZED_PAIR}, {"textColor", COLOR}, {"textColorDimmed", COLOR}, {"iconColor", COLOR}, {"iconColorDimmed", COLOR}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"fontSizeDimmed", FLOAT}, {"entrySpacing", FLOAT}, {"entrySpacingDimmed", FLOAT}, {"iconTextSpacing", FLOAT}, {"iconTextSpacingDimmed", FLOAT}, {"letterCase", STRING}, {"opacity", FLOAT}, {"opacityDimmed", FLOAT}, {"customButtonIcon", PATH}}}, {"sound", {{"path", PATH}}}}; // clang-format on ThemeData::ThemeData() : mCustomCollection {false} { sCurrentTheme = sThemes.find(Settings::getInstance()->getString("Theme")); sVariantDefinedTransitions = ""; } void ThemeData::loadFile(const std::map& 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 for legacy theme version. if (root.child("formatVersion") != nullptr) throw error << ": Legacy tag found"; if (sCurrentTheme->second.capabilities.variants.size() > 0) { for (auto& variant : sCurrentTheme->second.capabilities.variants) mVariants.emplace_back(variant.name); if (std::find(mVariants.cbegin(), mVariants.cend(), Settings::getInstance()->getString("ThemeVariant")) != mVariants.cend()) mSelectedVariant = Settings::getInstance()->getString("ThemeVariant"); else mSelectedVariant = mVariants.front(); // Special shortcut variant to apply configuration to all defined variants. mVariants.emplace_back("all"); if (trigger != ThemeTriggers::TriggerType::NONE) { auto overrides = getCurrentThemeSelectedVariantOverrides(); if (overrides.find(trigger) != overrides.end()) mOverrideVariant = overrides.at(trigger).first; } } if (sCurrentTheme->second.capabilities.colorSchemes.size() > 0) { for (auto& colorScheme : sCurrentTheme->second.capabilities.colorSchemes) mColorSchemes.emplace_back(colorScheme.name); if (std::find(mColorSchemes.cbegin(), mColorSchemes.cend(), Settings::getInstance()->getString("ThemeColorScheme")) != mColorSchemes.cend()) mSelectedColorScheme = Settings::getInstance()->getString("ThemeColorScheme"); else mSelectedColorScheme = mColorSchemes.front(); } sAspectRatioMatch = false; if (sCurrentTheme->second.capabilities.aspectRatios.size() > 0) { if (std::find(sCurrentTheme->second.capabilities.aspectRatios.cbegin(), sCurrentTheme->second.capabilities.aspectRatios.cend(), Settings::getInstance()->getString("ThemeAspectRatio")) != sCurrentTheme->second.capabilities.aspectRatios.cend()) sSelectedAspectRatio = Settings::getInstance()->getString("ThemeAspectRatio"); else sSelectedAspectRatio = sCurrentTheme->second.capabilities.aspectRatios.front(); if (sSelectedAspectRatio == "automatic") { // Auto-detect the closest aspect ratio based on what's available in the theme config. sSelectedAspectRatio = "16:9"; const float screenAspectRatio {Renderer::getScreenAspectRatio()}; float diff {std::fabs(sAspectRatioMap["16:9"] - screenAspectRatio)}; for (auto& aspectRatio : sCurrentTheme->second.capabilities.aspectRatios) { if (aspectRatio == "automatic") continue; if (sAspectRatioMap.find(aspectRatio) != sAspectRatioMap.end()) { const float newDiff { std::fabs(sAspectRatioMap[aspectRatio] - screenAspectRatio)}; if (newDiff < 0.01f) sAspectRatioMatch = true; if (newDiff < diff) { diff = newDiff; sSelectedAspectRatio = aspectRatio; } } } } } parseVariables(root); parseColorSchemes(root); parseIncludes(root); parseViews(root); if (root.child("feature") != nullptr) throw error << ": Legacy tag found"; parseVariants(root); parseAspectRatios(root); } bool ThemeData::hasView(const std::string& view) { auto viewIt = mViews.find(view); return (viewIt != mViews.cend()); } const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const { auto viewIt = mViews.find(view); if (viewIt == mViews.cend()) return nullptr; // Not found. auto elemIt = viewIt->second.elements.find(element); if (elemIt == viewIt->second.elements.cend()) return nullptr; // If expectedType is an empty string, then skip type checking. if (elemIt->second.type != expectedType && !expectedType.empty()) { LOG(LogWarning) << "ThemeData::getElement(): Requested element \"" << view << "." << element << "\" has the wrong type, expected \"" << expectedType << "\", got \"" << elemIt->second.type << "\""; return nullptr; } return &elemIt->second; } void ThemeData::populateThemes() { sThemes.clear(); LOG(LogInfo) << "Checking for available themes..."; // Check for themes first under the user theme directory (which is in the ES-DE home directory // by default), then under the data installation directory (Unix only) and last under the ES-DE // binary directory. const std::string defaultUserThemeDir {Utils::FileSystem::getHomePath() + "/.emulationstation/themes"}; std::string userThemeDirSetting {Utils::FileSystem::expandHomePath( Settings::getInstance()->getString("UserThemeDirectory"))}; #if defined(_WIN64) userThemeDirSetting = Utils::String::replace(userThemeDirSetting, "\\", "/"); #endif std::string userThemeDirectory; if (userThemeDirSetting == "") { userThemeDirectory = defaultUserThemeDir; } else if (Utils::FileSystem::isDirectory(userThemeDirSetting) || Utils::FileSystem::isSymlink(userThemeDirSetting)) { userThemeDirectory = userThemeDirSetting; #if defined(_WIN64) LOG(LogInfo) << "Setting user theme directory to \"" << Utils::String::replace(userThemeDirectory, "/", "\\") << "\""; #else LOG(LogInfo) << "Setting user theme directory to \"" << userThemeDirectory << "\""; #endif } else { LOG(LogWarning) << "Requested user theme directory \"" << userThemeDirSetting << "\" does not exist or is not a directory, reverting to \"" << defaultUserThemeDir << "\""; userThemeDirectory = defaultUserThemeDir; } #if defined(__unix__) || defined(__APPLE__) #if defined(APPIMAGE_BUILD) static const size_t pathCount {2}; #else static const size_t pathCount {3}; #endif #else static const size_t pathCount {2}; #endif std::string paths[pathCount] = { Utils::FileSystem::getExePath() + "/themes", #if defined(__APPLE__) Utils::FileSystem::getExePath() + "/../Resources/themes", #elif defined(__unix__) && !defined(APPIMAGE_BUILD) Utils::FileSystem::getProgramDataPath() + "/themes", #endif userThemeDirectory }; for (size_t i {0}; i < pathCount; ++i) { if (!Utils::FileSystem::isDirectory(paths[i])) continue; Utils::FileSystem::StringList dirContent {Utils::FileSystem::getDirContent(paths[i])}; for (Utils::FileSystem::StringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) { if (Utils::FileSystem::isDirectory(*it)) { const std::string themeDirName {Utils::FileSystem::getFileName(*it)}; if (themeDirName == "themes-list" || (themeDirName.length() >= 8 && Utils::String::toLower(themeDirName.substr(themeDirName.length() - 8, 8)) == "disabled")) continue; #if defined(_WIN64) LOG(LogDebug) << "Loading theme capabilities for \"" << Utils::String::replace(*it, "/", "\\") << "\"..."; #else LOG(LogDebug) << "Loading theme capabilities for \"" << *it << "\"..."; #endif ThemeCapability capabilities {parseThemeCapabilities(*it)}; if (!capabilities.validTheme) continue; std::string themeName; if (capabilities.themeName != "") { themeName.append(" (\"").append(capabilities.themeName).append("\")"); } #if defined(_WIN64) LOG(LogInfo) << "Added theme \"" << Utils::String::replace(*it, "/", "\\") << "\"" << themeName; #else LOG(LogInfo) << "Added theme \"" << *it << "\"" << themeName; #endif int aspectRatios {0}; if (capabilities.aspectRatios.size() > 0) aspectRatios = static_cast(capabilities.aspectRatios.size()) - 1; LOG(LogDebug) << "Theme includes support for " << capabilities.variants.size() << " variant" << (capabilities.variants.size() != 1 ? "s" : "") << ", " << capabilities.colorSchemes.size() << " color scheme" << (capabilities.colorSchemes.size() != 1 ? "s" : "") << ", " << aspectRatios << " aspect ratio" << (aspectRatios != 1 ? "s" : "") << " and " << capabilities.transitions.size() << " transition" << (capabilities.transitions.size() != 1 ? "s" : ""); Theme theme {*it, capabilities}; sThemes[theme.getName()] = theme; } } } if (sThemes.empty()) { LOG(LogWarning) << "Couldn't find any themes, creating dummy entry"; Theme theme {"no-themes", ThemeCapability()}; sThemes[theme.getName()] = theme; sCurrentTheme = sThemes.begin(); } } const std::string ThemeData::getSystemThemeFile(const std::string& system) { if (sThemes.empty()) getThemes(); if (sThemes.empty()) return ""; std::map::const_iterator theme { sThemes.find(Settings::getInstance()->getString("Theme"))}; if (theme == sThemes.cend()) { // Currently configured theme is missing, attempt to load the default theme slate-es-de // instead, and if that's also missing then pick the first available one. bool defaultSetFound {true}; theme = sThemes.find("slate-es-de"); if (theme == sThemes.cend()) { theme = sThemes.cbegin(); defaultSetFound = false; } LOG(LogWarning) << "Configured theme \"" << Settings::getInstance()->getString("Theme") << "\" does not exist, loading" << (defaultSetFound ? " default " : " ") << "theme \"" << theme->first << "\" instead"; Settings::getInstance()->setString("Theme", theme->first); sCurrentTheme = sThemes.find(Settings::getInstance()->getString("Theme")); } return theme->second.getThemePath(system); } const std::string ThemeData::getAspectRatioLabel(const std::string& aspectRatio) { auto it = std::find_if(sSupportedAspectRatios.cbegin(), sSupportedAspectRatios.cend(), [&aspectRatio](const std::pair& entry) { return entry.first == aspectRatio; }); if (it != sSupportedAspectRatios.cend()) return it->second; else return "invalid ratio"; } void ThemeData::setThemeTransitions() { auto setTransitionsFunc = [](int transitionAnim) { Settings::getInstance()->setInt("TransitionsSystemToSystem", transitionAnim); Settings::getInstance()->setInt("TransitionsSystemToGamelist", transitionAnim); Settings::getInstance()->setInt("TransitionsGamelistToGamelist", transitionAnim); Settings::getInstance()->setInt("TransitionsGamelistToSystem", transitionAnim); Settings::getInstance()->setInt("TransitionsStartupToSystem", transitionAnim); Settings::getInstance()->setInt("TransitionsStartupToGamelist", transitionAnim); }; int transitionAnim {ViewTransitionAnimation::INSTANT}; setTransitionsFunc(transitionAnim); const std::string& transitionsSetting {Settings::getInstance()->getString("ThemeTransitions")}; std::string profile; size_t profileEntry {0}; if (transitionsSetting == "automatic") { if (sVariantDefinedTransitions != "") profile = sVariantDefinedTransitions; else if (!sCurrentTheme->second.capabilities.transitions.empty()) profile = sCurrentTheme->second.capabilities.transitions.front().name; } else { profile = transitionsSetting; } auto it = std::find_if( sCurrentTheme->second.capabilities.transitions.cbegin(), sCurrentTheme->second.capabilities.transitions.cend(), [&profile](const ThemeTransitions transitions) { return transitions.name == profile; }); if (it != sCurrentTheme->second.capabilities.transitions.cend()) profileEntry = static_cast( 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, "_", " ") << "\""; } } 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; 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 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; } const pugi::xml_node& labelTag {variant.child("label")}; if (labelTag == nullptr) { LOG(LogDebug) << "No variant