// 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 #define MINIMUM_LEGACY_THEME_FORMAT_VERSION 3 // 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::sLegacySupportedViews { {"all"}, {"system"}, {"basic"}, {"detailed"}, {"grid"}, {"video"}}; std::vector ThemeData::sLegacySupportedFeatures { {"navigationsounds"}, {"video"}, {"carousel"}, {"z-index"}, {"visible"}}; std::vector ThemeData::sLegacyProperties { {"showSnapshotNoVideo"}, {"showSnapshotDelay"}, {"forceUppercase"}, {"alignment"}, {"defaultLogo"}, {"logoSize"}, {"logoScale"}, {"logoRotation"}, {"logoRotationOrigin"}, {"logoAlignment"}, {"maxLogoCount"}, {"selectorOffsetY"}}; 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}, {"maxItemCount", FLOAT}, {"maxLogoCount", FLOAT}, // For backward compatibility with legacy themes. {"itemsBeforeCenter", UNSIGNED_INTEGER}, {"itemsAfterCenter", UNSIGNED_INTEGER}, {"itemStacking", STRING}, {"selectedItemMargins", NORMALIZED_PAIR}, {"itemSize", NORMALIZED_PAIR}, {"itemScale", FLOAT}, {"itemRotation", FLOAT}, {"itemRotationOrigin", NORMALIZED_PAIR}, {"itemAxisHorizontal", BOOLEAN}, {"imageInterpolation", STRING}, {"imageColor", COLOR}, {"imageColorEnd", COLOR}, {"imageGradientType", STRING}, {"imageBrightness", FLOAT}, {"imageSaturation", FLOAT}, {"itemTransitions", STRING}, {"itemHorizontalAlignment", STRING}, {"itemVerticalAlignment", STRING}, {"wheelHorizontalAlignment", STRING}, {"horizontalOffset", FLOAT}, {"verticalOffset", FLOAT}, {"reflections", BOOLEAN}, {"reflectionsOpacity", FLOAT}, {"reflectionsFalloff", FLOAT}, {"unfocusedItemOpacity", FLOAT}, {"defaultLogo", PATH}, // For backward compatibility with legacy themes. {"logoSize", NORMALIZED_PAIR}, // For backward compatibility with legacy themes. {"logoScale", FLOAT}, // For backward compatibility with legacy themes. {"logoRotation", FLOAT}, // For backward compatibility with legacy themes. {"logoRotationOrigin", NORMALIZED_PAIR}, // For backward compatibility with legacy themes. {"logoAlignment", STRING}, // For backward compatibility with legacy themes. {"color", COLOR}, {"colorEnd", COLOR}, {"gradientType", STRING}, {"text", STRING}, {"textColor", COLOR}, {"textBackgroundColor", COLOR}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"letterCase", STRING}, {"letterCaseCollections", STRING}, {"letterCaseGroupedCollections", STRING}, {"lineSpacing", FLOAT}, {"fadeAbovePrimary", BOOLEAN}, {"zIndex", FLOAT}, {"legacyZIndexMode", STRING}}}, // For backward compatibility with legacy themes. {"grid", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"staticImage", PATH}, {"imageType", STRING}, {"defaultImage", PATH}, {"itemSize", NORMALIZED_PAIR}, {"itemScale", FLOAT}, {"itemSpacing", NORMALIZED_PAIR}, {"fractionalRows", BOOLEAN}, {"itemTransitions", STRING}, {"rowTransitions", STRING}, {"unfocusedItemOpacity", FLOAT}, {"imageFit", STRING}, {"imageRelativeScale", FLOAT}, {"imageColor", COLOR}, {"imageColorEnd", COLOR}, {"imageGradientType", STRING}, {"imageBrightness", FLOAT}, {"imageSaturation", FLOAT}, {"backgroundImage", PATH}, {"backgroundRelativeScale", FLOAT}, {"backgroundColor", COLOR}, {"backgroundColorEnd", COLOR}, {"backgroundGradientType", STRING}, {"selectorImage", PATH}, {"selectorRelativeScale", FLOAT}, {"selectorLayer", STRING}, {"selectorColor", COLOR}, {"selectorColorEnd", COLOR}, {"selectorGradientType", STRING}, {"text", STRING}, {"textRelativeScale", FLOAT}, {"textColor", COLOR}, {"textBackgroundColor", COLOR}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"letterCase", STRING}, {"letterCaseCollections", STRING}, {"letterCaseGroupedCollections", STRING}, {"lineSpacing", FLOAT}, {"fadeAbovePrimary", BOOLEAN}, {"zIndex", FLOAT}}}, {"textlist", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"selectorHeight", FLOAT}, {"selectorHorizontalOffset", FLOAT}, {"selectorVerticalOffset", FLOAT}, {"selectorOffsetY", FLOAT}, // For backward compatibility with legacy themes. {"selectorColor", COLOR}, {"selectorColorEnd", COLOR}, {"selectorGradientType", STRING}, {"selectorImagePath", PATH}, {"selectorImageTile", BOOLEAN}, {"primaryColor", COLOR}, {"secondaryColor", COLOR}, {"selectedColor", COLOR}, {"selectedSecondaryColor", COLOR}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"scrollSound", PATH}, // For backward compatibility with legacy themes. {"horizontalAlignment", STRING}, {"alignment", STRING}, // For backward compatibility with legacy themes. {"horizontalMargin", FLOAT}, {"letterCase", STRING}, {"letterCaseCollections", STRING}, {"letterCaseGroupedCollections", STRING}, {"forceUppercase", BOOLEAN}, // For backward compatibility with legacy themes. {"lineSpacing", FLOAT}, {"indicators", STRING}, {"collectionIndicators", STRING}, {"fadeAbovePrimary", BOOLEAN}, {"zIndex", FLOAT}}}, {"image", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"maxSize", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"path", PATH}, {"default", PATH}, {"imageType", STRING}, {"metadataElement", BOOLEAN}, {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"tile", BOOLEAN}, {"tileSize", NORMALIZED_PAIR}, {"tileHorizontalAlignment", STRING}, {"tileVerticalAlignment", STRING}, {"interpolation", STRING}, {"color", COLOR}, {"colorEnd", COLOR}, {"gradientType", STRING}, {"scrollFadeIn", BOOLEAN}, {"brightness", FLOAT}, {"opacity", FLOAT}, {"saturation", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"video", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"maxSize", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"path", PATH}, {"default", PATH}, {"defaultImage", PATH}, {"imageType", STRING}, {"metadataElement", BOOLEAN}, {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"audio", BOOLEAN}, {"interpolation", STRING}, {"color", COLOR}, {"colorEnd", COLOR}, {"gradientType", STRING}, {"pillarboxes", BOOLEAN}, {"pillarboxThreshold", NORMALIZED_PAIR}, {"scanlines", BOOLEAN}, {"delay", FLOAT}, {"fadeInTime", FLOAT}, {"scrollFadeIn", BOOLEAN}, {"brightness", FLOAT}, {"opacity", FLOAT}, {"saturation", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}, {"showSnapshotNoVideo", BOOLEAN}, // For backward compatibility with legacy themes. {"showSnapshotDelay", BOOLEAN}}}, // For backward compatibility with legacy themes. {"animation", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"metadataElement", BOOLEAN}, {"path", PATH}, {"speed", FLOAT}, {"direction", STRING}, {"keepAspectRatio", BOOLEAN}, {"interpolation", STRING}, {"brightness", FLOAT}, {"opacity", FLOAT}, {"saturation", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"badges", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"horizontalAlignment", STRING}, {"alignment", STRING}, // For backward compatibility with legacy themes. {"direction", STRING}, {"lines", UNSIGNED_INTEGER}, {"itemsPerLine", UNSIGNED_INTEGER}, {"itemMargin", NORMALIZED_PAIR}, {"slots", STRING}, {"controllerPos", NORMALIZED_PAIR}, {"controllerSize", FLOAT}, {"customBadgeIcon", PATH}, {"customControllerIcon", PATH}, {"folderLinkPos", NORMALIZED_PAIR}, {"folderLinkSize", FLOAT}, {"customFolderLinkIcon", PATH}, {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"text", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"text", STRING}, {"systemdata", STRING}, {"metadata", STRING}, {"metadataElement", BOOLEAN}, {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"container", BOOLEAN}, {"containerVerticalSnap", BOOLEAN}, {"containerScrollSpeed", FLOAT}, {"containerStartDelay", FLOAT}, {"containerResetDelay", FLOAT}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"horizontalAlignment", STRING}, {"verticalAlignment", STRING}, {"alignment", STRING}, // For backward compatibility with legacy themes. {"color", COLOR}, {"backgroundColor", COLOR}, {"letterCase", STRING}, {"forceUppercase", BOOLEAN}, // For backward compatibility with legacy themes. {"lineSpacing", FLOAT}, {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"datetime", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"metadata", STRING}, {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"horizontalAlignment", STRING}, {"verticalAlignment", STRING}, {"alignment", STRING}, // For backward compatibility with legacy themes. {"color", COLOR}, {"backgroundColor", COLOR}, {"letterCase", STRING}, {"forceUppercase", BOOLEAN}, // For backward compatibility with legacy themes. {"lineSpacing", FLOAT}, {"format", STRING}, {"displayRelative", BOOLEAN}, {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"gamelistinfo", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"horizontalAlignment", STRING}, {"verticalAlignment", STRING}, {"alignment", STRING}, // For backward compatibility with legacy themes. {"color", COLOR}, {"backgroundColor", COLOR}, {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"rating", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, {"gameselector", STRING}, {"gameselectorEntry", UNSIGNED_INTEGER}, {"interpolation", STRING}, {"color", COLOR}, {"filledPath", PATH}, {"unfilledPath", PATH}, {"overlay", BOOLEAN}, {"opacity", FLOAT}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, {"gameselector", {{"selection", STRING}, {"gameCount", UNSIGNED_INTEGER}}}, {"helpsystem", {{"pos", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"textColor", COLOR}, {"textColorDimmed", COLOR}, {"iconColor", COLOR}, {"iconColorDimmed", COLOR}, {"fontPath", PATH}, {"fontSize", FLOAT}, {"entrySpacing", FLOAT}, {"iconTextSpacing", FLOAT}, {"letterCase", STRING}, {"textStyle", STRING}, // For backward compatibility with legacy themes. {"opacity", FLOAT}, {"customButtonIcon", PATH}}}, {"navigationsounds", {{"systembrowseSound", PATH}, {"quicksysselectSound", PATH}, {"selectSound", PATH}, {"backSound", PATH}, {"scrollSound", PATH}, {"favoriteSound", PATH}, {"launchSound", PATH}}}, // Legacy components below, not in use any longer but needed for backward compatibility. {"sound", {{"path", PATH}}}, {"imagegrid", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"margin", NORMALIZED_PAIR}, {"padding", NORMALIZED_RECT}, {"autoLayout", NORMALIZED_PAIR}, {"autoLayoutSelectedZoom", FLOAT}, {"gameImage", PATH}, {"folderImage", PATH}, {"imageSource", STRING}, {"scrollDirection", STRING}, {"centerSelection", BOOLEAN}, {"scrollLoop", BOOLEAN}, {"animate", BOOLEAN}, {"zIndex", FLOAT}}}, {"gridtile", {{"size", NORMALIZED_PAIR}, {"padding", NORMALIZED_PAIR}, {"imageColor", COLOR}, {"backgroundImage", PATH}, {"backgroundCornerSize", NORMALIZED_PAIR}, {"backgroundColor", COLOR}, {"backgroundCenterColor", COLOR}, {"backgroundEdgeColor", COLOR}}}, {"ninepatch", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"path", PATH}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}}; // clang-format on ThemeData::ThemeData() : mLegacyTheme {false} , mCustomCollection {false} { mCurrentThemeSet = mThemeSets.find(Settings::getInstance()->getString("ThemeSet")); mVariantDefinedTransitions = ""; } 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"; if (mCurrentThemeSet != mThemeSets.cend()) mLegacyTheme = mCurrentThemeSet->second.capabilities.legacyTheme; // The resolution tag introduced in RetroPie EmulationStation in 2020 is a very bad idea // as it changes sizing of components from relative values to absolute pixel values. // So themes using it will simply not get loaded at all. if (root.child("resolution") != nullptr) throw error << ": tag not supported"; // Check for legacy theme version. int legacyVersion {root.child("formatVersion").text().as_int(-1)}; if (mLegacyTheme) { if (legacyVersion == -1) throw error << ": tag missing for legacy theme set"; if (legacyVersion < MINIMUM_LEGACY_THEME_FORMAT_VERSION) throw error << ": Defined legacy format version " << legacyVersion << " is less than the minimum supported version " << MINIMUM_LEGACY_THEME_FORMAT_VERSION; } else if (legacyVersion != -1) { throw error << ": Legacy tag found for non-legacy theme set"; } if (!mLegacyTheme) { if (mCurrentThemeSet->second.capabilities.variants.size() > 0) { for (auto& variant : mCurrentThemeSet->second.capabilities.variants) mVariants.emplace_back(variant.name); if (std::find(mVariants.cbegin(), mVariants.cend(), Settings::getInstance()->getString("ThemeVariant")) != mVariants.cend()) mSelectedVariant = Settings::getInstance()->getString("ThemeVariant"); else mSelectedVariant = mVariants.front(); // Special shortcut variant to apply configuration to all defined variants. mVariants.emplace_back("all"); if (trigger != ThemeTriggers::TriggerType::NONE) { auto overrides = getCurrentThemeSetSelectedVariantOverrides(); if (overrides.find(trigger) != overrides.end()) mOverrideVariant = overrides.at(trigger).first; } } if (mCurrentThemeSet->second.capabilities.colorSchemes.size() > 0) { for (auto& colorScheme : mCurrentThemeSet->second.capabilities.colorSchemes) mColorSchemes.emplace_back(colorScheme.name); if (std::find(mColorSchemes.cbegin(), mColorSchemes.cend(), Settings::getInstance()->getString("ThemeColorScheme")) != mColorSchemes.cend()) mSelectedColorScheme = Settings::getInstance()->getString("ThemeColorScheme"); else mSelectedColorScheme = mColorSchemes.front(); } if (mCurrentThemeSet->second.capabilities.aspectRatios.size() > 0) { if (std::find(mCurrentThemeSet->second.capabilities.aspectRatios.cbegin(), mCurrentThemeSet->second.capabilities.aspectRatios.cend(), Settings::getInstance()->getString("ThemeAspectRatio")) != mCurrentThemeSet->second.capabilities.aspectRatios.cend()) mSelectedAspectRatio = Settings::getInstance()->getString("ThemeAspectRatio"); else mSelectedAspectRatio = mCurrentThemeSet->second.capabilities.aspectRatios.front(); if (mSelectedAspectRatio == "automatic") { // Auto-detect the closest aspect ratio based on what's available in the theme set. mSelectedAspectRatio = "16:9"; const float screenAspectRatio {Renderer::getScreenAspectRatio()}; float diff {std::fabs(sAspectRatioMap["16:9"] - screenAspectRatio)}; for (auto& aspectRatio : mCurrentThemeSet->second.capabilities.aspectRatios) { if (aspectRatio == "automatic") continue; if (sAspectRatioMap.find(aspectRatio) != sAspectRatioMap.end()) { const float newDiff { std::fabs(sAspectRatioMap[aspectRatio] - screenAspectRatio)}; if (newDiff < diff) { diff = newDiff; mSelectedAspectRatio = aspectRatio; } } } } } } parseVariables(root); if (!mLegacyTheme) parseColorSchemes(root); parseIncludes(root); parseViews(root); // For non-legacy themes this will simply check for the presence of a feature tag and throw // an error if it's found. parseFeatures(root); if (!mLegacyTheme) { parseVariants(root); parseAspectRatios(root); } } bool ThemeData::hasView(const std::string& view) { auto viewIt = mViews.find(view); return (viewIt != mViews.cend()); } std::vector ThemeData::makeExtras(const std::shared_ptr& theme, const std::string& view) { std::vector comps; auto viewIt = theme->mViews.find(view); if (viewIt == theme->mViews.cend()) return comps; for (auto it = viewIt->second.legacyOrderedKeys.cbegin(); // Line break. it != viewIt->second.legacyOrderedKeys.cend(); ++it) { ThemeElement& elem {viewIt->second.elements.at(*it)}; if (elem.extra) { GuiComponent* comp {nullptr}; const std::string& t {elem.type}; if (t == "image") comp = new ImageComponent; else if (t == "text") comp = new TextComponent; if (comp) { comp->setDefaultZIndex(10.0f); comp->applyTheme(theme, view, *it, ThemeFlags::ALL); comps.push_back(comp); } } } return comps; } const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const { auto viewIt = mViews.find(view); if (viewIt == mViews.cend()) return nullptr; // Not found. auto elemIt = viewIt->second.elements.find(element); if (elemIt == viewIt->second.elements.cend()) return nullptr; // If expectedType is an empty string, then skip type checking. if (elemIt->second.type != expectedType && !expectedType.empty()) { LOG(LogWarning) << "ThemeData::getElement(): Requested element \"" << view << "." << element << "\" has the wrong type, expected \"" << expectedType << "\", got \"" << elemIt->second.type << "\""; return nullptr; } return &elemIt->second; } void ThemeData::populateThemeSets() { assert(mThemeSets.empty()); LOG(LogInfo) << "Checking for available theme sets..."; // Check for themes first under the home directory, then under the data installation // directory (Unix only) and last under the ES-DE binary directory. #if defined(__unix__) || defined(__APPLE__) #if defined(APPIMAGE_BUILD) static const size_t pathCount {2}; #else static const size_t pathCount {3}; #endif #else static const size_t pathCount {2}; #endif std::string paths[pathCount] = { Utils::FileSystem::getExePath() + "/themes", #if defined(__APPLE__) Utils::FileSystem::getExePath() + "/../Resources/themes", #elif defined(__unix__) && !defined(APPIMAGE_BUILD) Utils::FileSystem::getProgramDataPath() + "/themes", #endif Utils::FileSystem::getHomePath() + "/.emulationstation/themes" }; for (size_t i = 0; i < pathCount; ++i) { if (!Utils::FileSystem::isDirectory(paths[i])) continue; Utils::FileSystem::StringList dirContent {Utils::FileSystem::getDirContent(paths[i])}; for (Utils::FileSystem::StringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) { if (Utils::FileSystem::isDirectory(*it)) { #if defined(_WIN64) LOG(LogDebug) << "Loading theme set capabilities for \"" << Utils::String::replace(*it, "/", "\\") << "\"..."; #else LOG(LogDebug) << "Loading theme set capabilities for \"" << *it << "\"..."; #endif ThemeCapability capabilities {parseThemeCapabilities(*it)}; #if defined(_WIN64) LOG(LogInfo) << "Added" << (capabilities.legacyTheme ? " legacy" : "") << " theme set \"" << Utils::String::replace(*it, "/", "\\") << "\""; #else LOG(LogInfo) << "Added" << (capabilities.legacyTheme ? " legacy" : "") << " theme set \"" << *it << "\""; #endif if (!capabilities.legacyTheme) { LOG(LogDebug) << "Theme set includes support for " << capabilities.variants.size() << " variant" << (capabilities.variants.size() != 1 ? "s" : "") << ", " << capabilities.colorSchemes.size() << " color scheme" << (capabilities.colorSchemes.size() != 1 ? "s" : "") << " and " << capabilities.aspectRatios.size() << " aspect ratio" << (capabilities.aspectRatios.size() != 1 ? "s" : ""); } ThemeSet set {*it, capabilities}; mThemeSets[set.getName()] = set; } } } if (mThemeSets.empty()) { LOG(LogWarning) << "Couldn't find any theme sets, creating dummy entry"; ThemeSet set {"no-theme-sets", ThemeCapability()}; mThemeSets[set.getName()] = set; mCurrentThemeSet = mThemeSets.begin(); } } const std::string ThemeData::getThemeFromCurrentSet(const std::string& system) { if (mThemeSets.empty()) getThemeSets(); if (mThemeSets.empty()) // No theme sets available. return ""; std::map::const_iterator set { mThemeSets.find(Settings::getInstance()->getString("ThemeSet"))}; if (set == mThemeSets.cend()) { // Currently configured theme set is missing, attempt to load the default theme set // slate-DE instead, and if that's also missing then pick the first available set. bool defaultSetFound {true}; set = mThemeSets.find("slate-DE"); if (set == mThemeSets.cend()) { set = mThemeSets.cbegin(); defaultSetFound = false; } LOG(LogWarning) << "Configured theme set \"" << Settings::getInstance()->getString("ThemeSet") << "\" does not exist, loading" << (defaultSetFound ? " default " : " ") << "theme set \"" << set->first << "\" instead"; Settings::getInstance()->setString("ThemeSet", set->first); mCurrentThemeSet = mThemeSets.find(Settings::getInstance()->getString("ThemeSet")); } return set->second.getThemePath(system); } const std::string ThemeData::getAspectRatioLabel(const std::string& aspectRatio) { auto it = std::find_if(sSupportedAspectRatios.cbegin(), sSupportedAspectRatios.cend(), [&aspectRatio](const std::pair& 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); if (mCurrentThemeSet->second.capabilities.legacyTheme) { const std::string& legacyTransitionSetting { Settings::getInstance()->getString("LegacyTransitionAnimations")}; if (legacyTransitionSetting == "builtin-slide") transitionAnim = static_cast(ViewTransitionAnimation::SLIDE); else if (legacyTransitionSetting == "builtin-fade") transitionAnim = static_cast(ViewTransitionAnimation::FADE); setTransitionsFunc(transitionAnim); } else { const std::string& transitionSetting { Settings::getInstance()->getString("ThemeTransitionAnimations")}; std::string profile; size_t profileEntry {0}; if (transitionSetting == "automatic") { if (mVariantDefinedTransitions != "") profile = mVariantDefinedTransitions; else if (!mCurrentThemeSet->second.capabilities.transitions.empty()) profile = mCurrentThemeSet->second.capabilities.transitions.front().name; } else { profile = transitionSetting; } auto it = std::find_if( mCurrentThemeSet->second.capabilities.transitions.cbegin(), mCurrentThemeSet->second.capabilities.transitions.cend(), [&profile](const ThemeTransitions transitions) { return transitions.name == profile; }); if (it != mCurrentThemeSet->second.capabilities.transitions.cend()) profileEntry = static_cast( std::distance(mCurrentThemeSet->second.capabilities.transitions.cbegin(), it) + 1); if (profileEntry != 0 && mCurrentThemeSet->second.capabilities.transitions.size() > profileEntry - 1) { auto transitionMap = mCurrentThemeSet->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 (transitionSetting == "builtin-slide" || transitionSetting == "builtin-fade") { if (std::find( mCurrentThemeSet->second.capabilities.suppressedTransitionProfiles.cbegin(), mCurrentThemeSet->second.capabilities.suppressedTransitionProfiles.cend(), transitionSetting) == mCurrentThemeSet->second.capabilities.suppressedTransitionProfiles.cend()) { if (transitionSetting == "builtin-slide") { transitionAnim = static_cast(ViewTransitionAnimation::SLIDE); } else if (transitionSetting == "builtin-fade") { transitionAnim = static_cast(ViewTransitionAnimation::FADE); } setTransitionsFunc(transitionAnim); } } } } const std::map>> ThemeData::getCurrentThemeSetSelectedVariantOverrides() { const auto variantIter = std::find_if( mCurrentThemeSet->second.capabilities.variants.cbegin(), mCurrentThemeSet->second.capabilities.variants.cend(), [this](ThemeVariant currVariant) { return currVariant.name == mSelectedVariant; }); if (variantIter != mCurrentThemeSet->second.capabilities.variants.cend() && !(*variantIter).overrides.empty()) return (*variantIter).overrides; else return ThemeVariant().overrides; } 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.legacyTheme = false; 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; } 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