Added variant trigger support.

This commit is contained in:
Leon Styhre 2023-01-04 19:01:41 +01:00
parent 549d78dfed
commit fa67018b72
5 changed files with 312 additions and 94 deletions

View file

@ -239,7 +239,7 @@ SystemData::SystemData(const std::string& name,
mPlaceholder = new FileData(PLACEHOLDER, "<No Entries Found>", getSystemEnvData(), this);
setIsGameSystemStatus();
loadTheme();
loadTheme(ThemeTriggers::TriggerType::NONE);
}
SystemData::~SystemData()
@ -1304,7 +1304,7 @@ std::pair<unsigned int, unsigned int> SystemData::getDisplayedGameCount() const
return mRootFolder->getGameCount();
}
void SystemData::loadTheme()
void SystemData::loadTheme(ThemeTriggers::TriggerType trigger)
{
mTheme = std::make_shared<ThemeData>();
@ -1354,7 +1354,7 @@ void SystemData::loadTheme()
sysData.insert(std::pair<std::string, std::string>("system.theme.collections", "\b"));
}
mTheme->loadFile(sysData, path, isCustomCollection());
mTheme->loadFile(sysData, path, trigger, isCustomCollection());
}
catch (ThemeException& e) {
LOG(LogError) << e.what() << " (system \"" << mName << "\", theme \"" << mThemeFolder

View file

@ -13,6 +13,7 @@
#define ES_APP_SYSTEM_DATA_H
#include "PlatformId.h"
#include "ThemeData.h"
#include <algorithm>
#include <map>
@ -134,8 +135,8 @@ public:
void sortSystem(bool reloadGamelist = true, bool jumpToFirstRow = false);
// Load or re-load theme.
void loadTheme();
// Load or reload theme.
void loadTheme(ThemeTriggers::TriggerType trigger);
FileFilterIndex* getIndex() { return mFilterIndex; }
void onMetaDataSavePoint();

View file

@ -739,12 +739,13 @@ std::shared_ptr<GamelistView> ViewController::getGamelistView(SystemData* system
// If there's no entry, then create it and return it.
std::shared_ptr<GamelistView> view;
bool themeHasVideoView {system->getTheme()->hasView("video")};
if (system->getTheme()->isLegacyTheme()) {
const bool themeHasVideoView {system->getTheme()->hasView("video")};
// Decide which view style to use.
GamelistViewStyle selectedViewStyle {AUTOMATIC};
std::string viewPreference {Settings::getInstance()->getString("GamelistViewStyle")};
const std::string& viewPreference {Settings::getInstance()->getString("GamelistViewStyle")};
if (viewPreference == "basic")
selectedViewStyle = BASIC;
else if (viewPreference == "detailed")
@ -752,9 +753,8 @@ std::shared_ptr<GamelistView> ViewController::getGamelistView(SystemData* system
else if (viewPreference == "video")
selectedViewStyle = VIDEO;
if (system->getTheme()->isLegacyTheme()) {
if (selectedViewStyle == AUTOMATIC) {
std::vector<FileData*> files {
const std::vector<FileData*> files {
system->getRootFolder()->getFilesRecursive(GAME | FOLDER)};
for (auto it = files.cbegin(); it != files.cend(); ++it) {
@ -768,8 +768,6 @@ std::shared_ptr<GamelistView> ViewController::getGamelistView(SystemData* system
}
}
}
}
// Create the view.
switch (selectedViewStyle) {
case VIDEO: {
@ -788,9 +786,107 @@ std::shared_ptr<GamelistView> ViewController::getGamelistView(SystemData* system
break;
}
}
}
else {
// Variant triggers.
const auto overrides = system->getTheme()->getCurrentThemeSetSelectedVariantOverrides();
if (!overrides.empty()) {
ThemeTriggers::TriggerType noVideosTriggerType {ThemeTriggers::TriggerType::NONE};
ThemeTriggers::TriggerType noMediaTriggerType {ThemeTriggers::TriggerType::NONE};
const std::vector<FileData*> files {
system->getRootFolder()->getFilesRecursive(GAME | FOLDER)};
if (overrides.find(ThemeTriggers::TriggerType::NO_VIDEOS) != overrides.end()) {
noVideosTriggerType = ThemeTriggers::TriggerType::NO_VIDEOS;
for (auto it = files.cbegin(); it != files.cend(); ++it) {
if (!(*it)->getVideoPath().empty()) {
noVideosTriggerType = ThemeTriggers::TriggerType::NONE;
break;
}
}
}
if (overrides.find(ThemeTriggers::TriggerType::NO_MEDIA) != overrides.end()) {
noMediaTriggerType = ThemeTriggers::TriggerType::NO_MEDIA;
for (auto& imageType : overrides.at(ThemeTriggers::TriggerType::NO_MEDIA).second) {
for (auto it = files.cbegin(); it != files.cend(); ++it) {
if (imageType == "miximage") {
if (!(*it)->getMiximagePath().empty()) {
noMediaTriggerType = ThemeTriggers::TriggerType::NONE;
goto BREAK;
}
}
else if (imageType == "marquee") {
if (!(*it)->getMarqueePath().empty()) {
noMediaTriggerType = ThemeTriggers::TriggerType::NONE;
goto BREAK;
}
}
else if (imageType == "screenshot") {
if (!(*it)->getScreenshotPath().empty()) {
noMediaTriggerType = ThemeTriggers::TriggerType::NONE;
goto BREAK;
}
}
else if (imageType == "titlescreen") {
if (!(*it)->getTitleScreenPath().empty()) {
noMediaTriggerType = ThemeTriggers::TriggerType::NONE;
goto BREAK;
}
}
else if (imageType == "cover") {
if (!(*it)->getCoverPath().empty()) {
noMediaTriggerType = ThemeTriggers::TriggerType::NONE;
goto BREAK;
}
}
else if (imageType == "backcover") {
if (!(*it)->getBackCoverPath().empty()) {
noMediaTriggerType = ThemeTriggers::TriggerType::NONE;
goto BREAK;
}
}
else if (imageType == "3dbox") {
if (!(*it)->get3DBoxPath().empty()) {
noMediaTriggerType = ThemeTriggers::TriggerType::NONE;
goto BREAK;
}
}
else if (imageType == "physicalmedia") {
if (!(*it)->getPhysicalMediaPath().empty()) {
noMediaTriggerType = ThemeTriggers::TriggerType::NONE;
goto BREAK;
}
}
else if (imageType == "fanart") {
if (!(*it)->getFanArtPath().empty()) {
noMediaTriggerType = ThemeTriggers::TriggerType::NONE;
goto BREAK;
}
}
else if (imageType == "video") {
if (!(*it)->getVideoPath().empty()) {
noMediaTriggerType = ThemeTriggers::TriggerType::NONE;
goto BREAK;
}
}
}
}
}
BREAK:
// noMedia takes precedence over the noVideos trigger.
if (noMediaTriggerType == ThemeTriggers::TriggerType::NO_MEDIA)
system->loadTheme(noMediaTriggerType);
else
system->loadTheme(noVideosTriggerType);
}
}
view = std::shared_ptr<GamelistView>(new GamelistView(system->getRootFolder()));
view->setTheme(system->getTheme());
std::vector<SystemData*>& sysVec = SystemData::sSystemVector;
@ -970,7 +1066,7 @@ void ViewController::preload()
// Load navigation sounds, either from the theme if it supports it, or otherwise from
// the bundled fallback sound files.
bool themeSoundSupport = false;
bool themeSoundSupport {false};
for (SystemData* system : SystemData::sSystemVector) {
if (system->getTheme()->hasView("all")) {
NavigationSounds::getInstance().loadThemeNavigationSounds(system->getTheme().get());
@ -1000,7 +1096,7 @@ void ViewController::reloadGamelistView(GamelistView* view, bool reloadTheme)
mCurrentView = nullptr;
if (reloadTheme)
system->loadTheme();
system->loadTheme(ThemeTriggers::TriggerType::NONE);
system->getIndex()->setKidModeFilters();
std::shared_ptr<GamelistView> newView {getGamelistView(system)};
@ -1047,15 +1143,18 @@ void ViewController::reloadAll()
// Load themes, create GamelistViews and reset filters.
for (auto it = cursorMap.cbegin(); it != cursorMap.cend(); ++it) {
it->first->loadTheme();
it->first->loadTheme(ThemeTriggers::TriggerType::NONE);
it->first->getIndex()->resetFilters();
getGamelistView(it->first)->setCursor(it->second);
}
// Rebuild SystemListView.
mSystemListView.reset();
getSystemListView();
// Restore cursor positions for all systems.
for (auto it = cursorMap.cbegin(); it != cursorMap.cend(); ++it)
getGamelistView(it->first)->setCursor(it->second);
// Update mCurrentView since the pointers changed.
if (mState.viewing == GAMELIST) {
mCurrentView = getGamelistView(mState.getSystem());
@ -1073,7 +1172,7 @@ void ViewController::reloadAll()
// Load navigation sounds, either from the theme if it supports it, or otherwise from
// the bundled fallback sound files.
NavigationSounds::getInstance().deinit();
bool themeSoundSupport = false;
bool themeSoundSupport {false};
for (SystemData* system : SystemData::sSystemVector) {
if (system->getTheme()->hasView("all")) {
NavigationSounds::getInstance().loadThemeNavigationSounds(system->getTheme().get());

View file

@ -28,6 +28,18 @@ std::vector<std::string> ThemeData::sSupportedViews {
{"system"},
{"gamelist"}};
std::vector<std::string> ThemeData::sSupportedMediaTypes {
{"miximage"},
{"marquee"},
{"screenshot"},
{"titlescreen"},
{"cover"},
{"backcover"},
{"3dbox"},
{"physicalmedia"},
{"fanart"},
{"video"}};
std::vector<std::string> ThemeData::sLegacySupportedViews {
{"all"},
{"system"},
@ -497,9 +509,11 @@ ThemeData::ThemeData()
void ThemeData::loadFile(const std::map<std::string, std::string>& sysDataMap,
const std::string& path,
const ThemeTriggers::TriggerType trigger,
const bool customCollection)
{
mCustomCollection = customCollection;
mOverrideVariant = "";
mPaths.push_back(path);
@ -565,6 +579,12 @@ void ThemeData::loadFile(const std::map<std::string, std::string>& sysDataMap,
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) {
@ -809,6 +829,21 @@ const std::string ThemeData::getAspectRatioLabel(const std::string& aspectRatio)
return "invalid ratio";
}
const std::map<ThemeTriggers::TriggerType, std::pair<std::string, std::vector<std::string>>>
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;
@ -816,20 +851,20 @@ unsigned int ThemeData::getHexColor(const std::string& str)
if (str == "")
throw error << "Empty color property";
size_t len {str.size()};
if (len != 6 && len != 8)
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 val;
unsigned int value;
std::stringstream ss;
ss << str;
ss >> std::hex >> val;
ss >> std::hex >> value;
if (len == 6)
val = (val << 8) | 0xFF;
if (length == 6)
value = (value << 8) | 0xFF;
return val;
return value;
}
std::string ThemeData::resolvePlaceholders(const std::string& in)
@ -854,18 +889,19 @@ ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string&
{
ThemeCapability capabilities;
std::vector<std::string> aspectRatiosTemp;
bool hasTriggers {false};
std::string capFile {path + "/capabilities.xml"};
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)
pugi::xml_parse_result res {
const pugi::xml_parse_result& res {
doc.load_file(Utils::String::stringToWideString(capFile).c_str())};
#else
pugi::xml_parse_result res {doc.load_file(capFile.c_str())};
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";
@ -874,7 +910,7 @@ ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string&
LOG(LogError) << "Couldn't parse capabilities.xml: " << res.description();
return capabilities;
}
pugi::xml_node themeCapabilities {doc.child("themeCapabilities")};
const pugi::xml_node& themeCapabilities {doc.child("themeCapabilities")};
if (!themeCapabilities) {
LOG(LogError) << "Missing <themeCapabilities> tag in capabilities.xml";
return capabilities;
@ -882,7 +918,7 @@ ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string&
for (pugi::xml_node aspectRatio {themeCapabilities.child("aspectRatio")}; aspectRatio;
aspectRatio = aspectRatio.next_sibling("aspectRatio")) {
std::string value {aspectRatio.text().get()};
const std::string& value {aspectRatio.text().get()};
if (std::find_if(sSupportedAspectRatios.cbegin(), sSupportedAspectRatios.cend(),
[&value](const std::pair<std::string, std::string>& entry) {
return entry.first == value;
@ -906,7 +942,7 @@ ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string&
for (pugi::xml_node variant {themeCapabilities.child("variant")}; variant;
variant = variant.next_sibling("variant")) {
ThemeVariant readVariant;
std::string name {variant.attribute("name").as_string()};
const std::string& name {variant.attribute("name").as_string()};
if (name.empty()) {
LOG(LogWarning)
<< "Found <variant> tag without name attribute, ignoring entry in \"" << capFile
@ -921,7 +957,7 @@ ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string&
readVariant.name = name;
}
pugi::xml_node labelTag {variant.child("label")};
const pugi::xml_node& labelTag {variant.child("label")};
if (labelTag == nullptr) {
LOG(LogDebug)
<< "No variant <label> tag found, setting label value to the variant name \""
@ -929,7 +965,7 @@ ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string&
readVariant.label = name;
}
else {
std::string labelValue {labelTag.text().as_string()};
const std::string& labelValue {labelTag.text().as_string()};
if (labelValue == "") {
LOG(LogWarning) << "No variant <label> value defined, setting value to "
"the variant name \""
@ -941,9 +977,9 @@ ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string&
}
}
pugi::xml_node selectableTag {variant.child("selectable")};
const pugi::xml_node& selectableTag {variant.child("selectable")};
if (selectableTag != nullptr) {
std::string value {selectableTag.text().as_string()};
const std::string& value {selectableTag.text().as_string()};
if (value.front() == '0' || value.front() == 'f' || value.front() == 'F' ||
value.front() == 'n' || value.front() == 'N')
readVariant.selectable = false;
@ -951,20 +987,52 @@ ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string&
readVariant.selectable = true;
}
pugi::xml_node overrideTag {variant.child("override")};
for (pugi::xml_node overrideTag {variant.child("override")}; overrideTag;
overrideTag = overrideTag.next_sibling("override")) {
if (overrideTag != nullptr) {
pugi::xml_node triggerTag {overrideTag.child("trigger")};
std::vector<std::string> mediaTypes;
const pugi::xml_node& mediaTypeTag {overrideTag.child("mediaType")};
if (mediaTypeTag != nullptr) {
std::string mediaTypeValue {mediaTypeTag.text().as_string()};
for (auto& character : mediaTypeValue) {
if (std::isspace(character))
character = ',';
}
mediaTypeValue = Utils::String::replace(mediaTypeValue, ",,", ",");
mediaTypes = Utils::String::delimitedStringToVector(mediaTypeValue, ",");
for (std::string& type : mediaTypes) {
if (std::find(sSupportedMediaTypes.cbegin(),
sSupportedMediaTypes.cend(),
type) == sSupportedMediaTypes.cend()) {
LOG(LogError) << "ThemeData::parseThemeCapabilities(): Invalid "
"override configuration, unsupported "
"\"mediaType\" value \""
<< type << "\"";
mediaTypes.clear();
break;
}
}
}
const pugi::xml_node& triggerTag {overrideTag.child("trigger")};
if (triggerTag != nullptr) {
std::string triggerValue {triggerTag.text().as_string()};
const std::string& triggerValue {triggerTag.text().as_string()};
if (triggerValue == "") {
LOG(LogWarning)
<< "No <trigger> tag value defined for variant \"" << readVariant.name
LOG(LogWarning) << "No <trigger> tag value defined for variant \""
<< readVariant.name << "\", ignoring entry in \""
<< capFile << "\"";
}
else if (triggerValue != "noVideos" && triggerValue != "noMedia") {
LOG(LogWarning) << "Invalid <useVariant> tag value \"" << triggerValue
<< "\" defined for variant \"" << readVariant.name
<< "\", ignoring entry in \"" << capFile << "\"";
}
else {
pugi::xml_node useVariantTag {overrideTag.child("useVariant")};
const pugi::xml_node& useVariantTag {overrideTag.child("useVariant")};
if (useVariantTag != nullptr) {
std::string useVariantValue {useVariantTag.text().as_string()};
const std::string& useVariantValue {
useVariantTag.text().as_string()};
if (useVariantValue == "") {
LOG(LogWarning)
<< "No <useVariant> tag value defined for variant \""
@ -972,19 +1040,32 @@ ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string&
<< "\"";
}
else {
readVariant.override = true;
readVariant.overrideTrigger = triggerValue;
readVariant.overrideVariant = useVariantValue;
hasTriggers = true;
if (triggerValue == "noVideos") {
readVariant
.overrides[ThemeTriggers::TriggerType::NO_VIDEOS] =
std::make_pair(useVariantValue,
std::vector<std::string>());
}
else if (triggerValue == "noMedia") {
if (mediaTypes.empty())
mediaTypes.emplace_back("miximage");
readVariant
.overrides[ThemeTriggers::TriggerType::NO_MEDIA] =
std::make_pair(useVariantValue, mediaTypes);
}
}
}
else {
LOG(LogWarning) << "Found an <override> tag without a corresponding "
LOG(LogWarning)
<< "Found an <override> tag without a corresponding "
"<useVariant> tag, "
<< "ignoring entry for variant \"" << readVariant.name
<< "\" in \"" << capFile << "\"";
}
}
}
}
else {
LOG(LogWarning)
<< "Found an <override> tag without a corresponding <trigger> tag, "
@ -1012,7 +1093,7 @@ ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string&
for (pugi::xml_node colorScheme {themeCapabilities.child("colorScheme")}; colorScheme;
colorScheme = colorScheme.next_sibling("colorScheme")) {
ThemeColorScheme readColorScheme;
std::string name {colorScheme.attribute("name").as_string()};
const std::string& name {colorScheme.attribute("name").as_string()};
if (name.empty()) {
LOG(LogWarning)
<< "Found <colorScheme> tag without name attribute, ignoring entry in \""
@ -1022,7 +1103,7 @@ ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string&
readColorScheme.name = name;
}
pugi::xml_node labelTag {colorScheme.child("label")};
const pugi::xml_node& labelTag {colorScheme.child("label")};
if (labelTag == nullptr) {
LOG(LogDebug) << "No colorScheme <label> tag found, setting label value to the "
"color scheme name \""
@ -1030,7 +1111,7 @@ ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string&
readColorScheme.label = name;
}
else {
std::string labelValue {labelTag.text().as_string()};
const std::string& labelValue {labelTag.text().as_string()};
if (labelValue == "") {
LOG(LogWarning) << "No colorScheme <label> value defined, setting value to "
"the color scheme name \""
@ -1077,6 +1158,28 @@ ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string&
}
}
if (hasTriggers) {
for (auto& variant : capabilities.variants) {
for (auto it = variant.overrides.begin(); it != variant.overrides.end();) {
const auto variantIter =
std::find_if(capabilities.variants.begin(), capabilities.variants.end(),
[it](ThemeVariant currVariant) {
return currVariant.name == (*it).second.first;
});
if (variantIter == capabilities.variants.end()) {
LOG(LogWarning)
<< "The <useVariant> tag value \"" << (*it).second.first
<< "\" does not match any defined variants, ignoring entry in \"" << capFile
<< "\"";
it = variant.overrides.erase(it);
}
else {
++it;
}
}
}
}
return capabilities;
}
@ -1214,7 +1317,10 @@ void ThemeData::parseVariants(const pugi::xml_node& root)
<< "\" is not defined in capabilities.xml";
}
if (mSelectedVariant == viewKey || viewKey == "all") {
const std::string variant {mOverrideVariant.empty() ? mSelectedVariant :
mOverrideVariant};
if (variant == viewKey || viewKey == "all") {
parseVariables(node);
parseColorSchemes(node);
parseIncludes(node);

View file

@ -66,6 +66,15 @@ namespace ThemeFlags
// clang-format on
} // namespace ThemeFlags
namespace ThemeTriggers
{
enum class TriggerType {
NONE,
NO_VIDEOS,
NO_MEDIA
};
} // namespace ThemeTriggers
class ThemeException : public std::exception
{
public:
@ -175,13 +184,11 @@ public:
std::string name;
std::string label;
bool selectable;
bool override;
std::string overrideTrigger;
std::string overrideVariant;
std::map<ThemeTriggers::TriggerType, std::pair<std::string, std::vector<std::string>>>
overrides;
ThemeVariant()
: selectable {false}
, override {false}
{
}
};
@ -218,6 +225,7 @@ public:
void loadFile(const std::map<std::string, std::string>& sysDataMap,
const std::string& path,
const ThemeTriggers::TriggerType trigger,
const bool customCollection);
bool hasView(const std::string& view);
ThemeView& getViewElements(std::string view) { return mViews[view]; }
@ -239,6 +247,8 @@ public:
const static std::string getCurrentThemeSetName() { return mCurrentThemeSet->first; }
const bool isLegacyTheme() { return mLegacyTheme; }
const std::map<ThemeTriggers::TriggerType, std::pair<std::string, std::vector<std::string>>>
getCurrentThemeSetSelectedVariantOverrides();
enum ElementPropertyType {
NORMALIZED_RECT,
@ -280,6 +290,7 @@ private:
const LegacyWorkaround legacyWorkaround);
static std::vector<std::string> sSupportedViews;
static std::vector<std::string> sSupportedMediaTypes;
static std::vector<std::string> sLegacySupportedViews;
static std::vector<std::string> sLegacySupportedFeatures;
static std::vector<std::string> sLegacyProperties;
@ -298,6 +309,7 @@ private:
std::vector<std::string> mVariants;
std::vector<std::string> mColorSchemes;
std::string mSelectedVariant;
std::string mOverrideVariant;
std::string mSelectedColorScheme;
std::string mSelectedAspectRatio;
bool mLegacyTheme;