From c35a297d9ab55e97720576e8c7cd5f3301ee4acf Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Sat, 29 Jan 2022 18:41:22 +0100 Subject: [PATCH] Large update to get the new theme engine up and running. --- es-app/src/main.cpp | 1 + es-app/src/views/GamelistBase.h | 1 + es-app/src/views/GamelistLegacy.h | 171 ++-- es-app/src/views/GamelistView.cpp | 585 +++++++++++- es-app/src/views/GamelistView.h | 6 +- es-app/src/views/SystemView.cpp | 22 +- es-core/src/HelpStyle.cpp | 132 +-- es-core/src/Settings.cpp | 2 + es-core/src/Sound.cpp | 36 +- es-core/src/ThemeData.cpp | 837 +++++++++++++----- es-core/src/ThemeData.h | 71 +- es-core/src/components/BadgeComponent.cpp | 11 +- es-core/src/components/BadgeComponent.h | 2 +- es-core/src/components/DateTimeComponent.cpp | 1 - es-core/src/components/ImageComponent.cpp | 4 +- es-core/src/components/LottieComponent.cpp | 1 - es-core/src/components/LottieComponent.h | 4 +- es-core/src/components/RatingComponent.cpp | 1 - es-core/src/components/TextComponent.cpp | 8 +- es-core/src/components/VideoComponent.cpp | 19 +- es-core/src/components/VideoComponent.h | 7 +- .../src/components/VideoFFmpegComponent.cpp | 3 +- 22 files changed, 1436 insertions(+), 489 deletions(-) diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 205ecde65..fbeedaa43 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -642,6 +642,7 @@ int main(int argc, char* argv[]) AudioManager::getInstance(); MameNames::getInstance(); + ThemeData::getThemeSets(); loadSystemsReturnCode loadSystemsStatus = loadSystemConfigFile(); if (loadSystemsStatus) { diff --git a/es-app/src/views/GamelistBase.h b/es-app/src/views/GamelistBase.h index 89d55b494..572690af4 100644 --- a/es-app/src/views/GamelistBase.h +++ b/es-app/src/views/GamelistBase.h @@ -16,6 +16,7 @@ #include "Window.h" #include "components/BadgeComponent.h" #include "components/DateTimeComponent.h" +#include "components/LottieComponent.h" #include "components/RatingComponent.h" #include "components/ScrollableContainer.h" #include "components/TextComponent.h" diff --git a/es-app/src/views/GamelistLegacy.h b/es-app/src/views/GamelistLegacy.h index ac0e424f1..e7ec84c66 100644 --- a/es-app/src/views/GamelistLegacy.h +++ b/es-app/src/views/GamelistLegacy.h @@ -45,7 +45,7 @@ void GamelistView::legacyPopulateFields() // Thumbnails. mImageComponents.push_back(std::make_unique()); - mImageComponents.back()->setMetadataField("md_thumbnail"); + mImageComponents.back()->setMetadataField("image_md_thumbnail"); mImageComponents.back()->setOrigin(0.5f, 0.5f); mImageComponents.back()->setVisible(false); mImageComponents.back()->setMaxSize(mSize.x * (0.25f - 2.0f * padding), mSize.y * 0.10f); @@ -54,7 +54,7 @@ void GamelistView::legacyPopulateFields() // Marquee. mImageComponents.push_back(std::make_unique()); - mImageComponents.back()->setMetadataField("md_marquee"); + mImageComponents.back()->setMetadataField("image_md_marquee"); mImageComponents.back()->setOrigin(0.5f, 0.5f); mImageComponents.back()->setVisible(false); mImageComponents.back()->setMaxSize(mSize.x * (0.5f - 2.0f * padding), mSize.y * 0.18f); @@ -63,7 +63,7 @@ void GamelistView::legacyPopulateFields() // Image. mImageComponents.push_back(std::make_unique()); - mImageComponents.back()->setMetadataField("md_image"); + mImageComponents.back()->setMetadataField("image_md_image"); mImageComponents.back()->setOrigin(0.5f, 0.5f); mImageComponents.back()->setPosition(mSize.x * 0.25f, mList.getPosition().y + mSize.y * 0.2125f); @@ -74,9 +74,10 @@ void GamelistView::legacyPopulateFields() if (mViewStyle == ViewController::VIDEO) { // Video. mVideoComponents.push_back(std::make_unique()); - mVideoComponents.back()->setMetadataField("md_video"); + mVideoComponents.back()->setMetadataField("video_md_video"); mVideoComponents.back()->setOrigin(0.5f, 0.5f); - mVideoComponents.back()->setPosition(mSize.x * 0.25f, mSize.y * 0.4f); + mVideoComponents.back()->setPosition(mSize.x * 0.25f, + mList.getPosition().y + mSize.y * 0.2125f); mVideoComponents.back()->setSize(mSize.x * (0.5f - 2.0f * padding), mSize.y * 0.4f); mVideoComponents.back()->setDefaultZIndex(30.0f); addChild(mVideoComponents.back().get()); @@ -85,84 +86,85 @@ void GamelistView::legacyPopulateFields() mList.setPosition(mSize.x * (0.50f + padding), mList.getPosition().y); mList.setSize(mSize.x * (0.50f - padding), mList.getSize().y); mList.setAlignment(TextListComponent::ALIGN_LEFT); - mList.setCursorChangedCallback([&](const CursorState& /*state*/) { legacyUpdateInfoPanel(); }); + mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); // Metadata labels + values. mTextComponents.push_back(std::make_unique()); mTextComponents.back()->setText("Rating: ", false); - mTextComponents.back()->setMetadataField("md_lbl_rating"); + mTextComponents.back()->setMetadataField("text_md_lbl_rating"); addChild(mTextComponents.back().get()); mTextComponents.push_back(std::make_unique()); mTextComponents.back()->setText("Released: ", false); - mTextComponents.back()->setMetadataField("md_lbl_releasedate"); + mTextComponents.back()->setMetadataField("text_md_lbl_releasedate"); addChild(mTextComponents.back().get()); mTextComponents.push_back(std::make_unique()); mTextComponents.back()->setText("Developer: ", false); - mTextComponents.back()->setMetadataField("md_lbl_developer"); + mTextComponents.back()->setMetadataField("text_md_lbl_developer"); addChild(mTextComponents.back().get()); mTextComponents.push_back(std::make_unique()); mTextComponents.back()->setText("Publisher: ", false); - mTextComponents.back()->setMetadataField("md_lbl_publisher"); + mTextComponents.back()->setMetadataField("text_md_lbl_publisher"); addChild(mTextComponents.back().get()); mTextComponents.push_back(std::make_unique()); mTextComponents.back()->setText("Genre: ", false); - mTextComponents.back()->setMetadataField("md_lbl_genre"); + mTextComponents.back()->setMetadataField("text_md_lbl_genre"); addChild(mTextComponents.back().get()); mTextComponents.push_back(std::make_unique()); mTextComponents.back()->setText("Players: ", false); - mTextComponents.back()->setMetadataField("md_lbl_players"); + mTextComponents.back()->setMetadataField("text_md_lbl_players"); addChild(mTextComponents.back().get()); mTextComponents.push_back(std::make_unique()); mTextComponents.back()->setText("Last played: ", false); - mTextComponents.back()->setMetadataField("md_lbl_lastplayed"); + mTextComponents.back()->setMetadataField("text_md_lbl_lastplayed"); addChild(mTextComponents.back().get()); mTextComponents.push_back(std::make_unique()); mTextComponents.back()->setText("Times played: ", false); - mTextComponents.back()->setMetadataField("md_lbl_playcount"); + mTextComponents.back()->setMetadataField("text_md_lbl_playcount"); addChild(mTextComponents.back().get()); mRatingComponents.push_back(std::make_unique()); - mRatingComponents.back()->setMetadataField("md_rating"); + mRatingComponents.back()->setMetadataField("rating_md_rating"); mRatingComponents.back()->setDefaultZIndex(40.0f); addChild(mRatingComponents.back().get()); mDateTimeComponents.push_back(std::make_unique()); - mDateTimeComponents.back()->setMetadataField("md_releasedate"); + mDateTimeComponents.back()->setMetadataField("datetime_md_releasedate"); addChild(mDateTimeComponents.back().get()); mTextComponents.push_back(std::make_unique()); - mTextComponents.back()->setMetadataField("md_developer"); + mTextComponents.back()->setMetadataField("text_md_developer"); addChild(mTextComponents.back().get()); mTextComponents.push_back(std::make_unique()); - mTextComponents.back()->setMetadataField("md_publisher"); + mTextComponents.back()->setMetadataField("text_md_publisher"); addChild(mTextComponents.back().get()); mTextComponents.push_back(std::make_unique()); - mTextComponents.back()->setMetadataField("md_genre"); + mTextComponents.back()->setMetadataField("text_md_genre"); addChild(mTextComponents.back().get()); mTextComponents.push_back(std::make_unique()); - mTextComponents.back()->setMetadataField("md_players"); + mTextComponents.back()->setMetadataField("text_md_players"); addChild(mTextComponents.back().get()); mDateTimeComponents.push_back(std::make_unique()); - mDateTimeComponents.back()->setMetadataField("md_lastplayed"); + mDateTimeComponents.back()->setMetadataField("datetime_md_lastplayed"); + mDateTimeComponents.back()->setDisplayRelative(true); addChild(mDateTimeComponents.back().get()); mTextComponents.push_back(std::make_unique()); - mTextComponents.back()->setMetadataField("md_playcount"); + mTextComponents.back()->setMetadataField("text_md_playcount"); addChild(mTextComponents.back().get()); mTextComponents.push_back(std::make_unique()); - mTextComponents.back()->setMetadataField("md_name"); + mTextComponents.back()->setMetadataField("text_md_name"); mTextComponents.back()->setPosition(mSize.x, mSize.y); mTextComponents.back()->setFont(Font::get(FONT_SIZE_MEDIUM)); mTextComponents.back()->setHorizontalAlignment(ALIGN_CENTER); @@ -172,7 +174,7 @@ void GamelistView::legacyPopulateFields() // Badges. mBadgeComponents.push_back(std::make_unique()); - mBadgeComponents.back()->setMetadataField("md_badges"); + mBadgeComponents.back()->setMetadataField("badges_md_badges"); mBadgeComponents.back()->setOrigin(0.5f, 0.5f); mBadgeComponents.back()->setPosition(mSize.x * 0.8f, mSize.y * 0.7f); mBadgeComponents.back()->setSize(mSize.x * 0.15f, mSize.y * 0.2f); @@ -180,22 +182,21 @@ void GamelistView::legacyPopulateFields() addChild(mBadgeComponents.back().get()); // Scrollable container (game description). - mScrollableContainerComponents.push_back(std::make_unique()); - mScrollableContainerComponents.back()->setMetadataField("md_description"); - mScrollableContainerComponents.back()->setSize( - mSize.x * (0.50f - 2.0f * padding), - mSize.y - mScrollableContainerComponents.back()->getPosition().y); - mScrollableContainerComponents.back()->setAutoScroll(true); - mScrollableContainerComponents.back()->setDefaultZIndex(40.0f); - addChild(mScrollableContainerComponents.back().get()); + mContainerComponents.push_back(std::make_unique()); + mContainerComponents.back()->setMetadataField("text_md_description"); + mContainerComponents.back()->setSize(mSize.x * (0.50f - 2.0f * padding), + mSize.y - mContainerComponents.back()->getPosition().y); + mContainerComponents.back()->setAutoScroll(true); + mContainerComponents.back()->setDefaultZIndex(40.0f); + addChild(mContainerComponents.back().get()); mTextComponents.push_back(std::make_unique()); mTextComponents.back()->setFont(Font::get(FONT_SIZE_SMALL)); - mTextComponents.back()->setSize(mScrollableContainerComponents.back()->getSize().x, 0.0f); - mScrollableContainerComponents.back()->addChild(mTextComponents.back().get()); + mTextComponents.back()->setSize(mContainerComponents.back()->getSize().x, 0.0f); + mContainerComponents.back()->addChild(mTextComponents.back().get()); mGamelistInfoComponents.push_back(std::make_unique()); - mGamelistInfoComponents.back()->setMetadataField("gamelistInfo"); + mGamelistInfoComponents.back()->setMetadataField("text_gamelistInfo"); mGamelistInfoComponents.back()->setOrigin(0.5f, 0.5f); mGamelistInfoComponents.back()->setFont(Font::get(FONT_SIZE_SMALL)); mGamelistInfoComponents.back()->setDefaultZIndex(50.0f); @@ -209,9 +210,9 @@ void GamelistView::legacyOnThemeChanged(const std::shared_ptr& theme) using namespace ThemeFlags; - mTextComponents[LOGOTEXT]->applyTheme(theme, getName(), "logoText", ALL); - mImageComponents[LOGO]->applyTheme(theme, getName(), "logo", ALL); - mImageComponents[BACKGROUND]->applyTheme(theme, getName(), "background", ALL); + mTextComponents[LOGOTEXT]->applyTheme(theme, getName(), "text_logoText", ALL); + mImageComponents[LOGO]->applyTheme(theme, getName(), "image_logo", ALL); + mImageComponents[BACKGROUND]->applyTheme(theme, getName(), "image_background", ALL); // Remove old theme extras. for (auto extra : mThemeExtras) { @@ -225,12 +226,13 @@ void GamelistView::legacyOnThemeChanged(const std::shared_ptr& theme) for (auto extra : mThemeExtras) addChild(extra); - mList.applyTheme(theme, getName(), "gamelist", ALL); + mList.applyTheme(theme, getName(), "textlist_gamelist", ALL); mImageComponents[LegacyImage::MD_THUMBNAIL]->applyTheme( theme, getName(), mImageComponents[LegacyImage::MD_THUMBNAIL]->getMetadataField(), ALL); - mImageComponents[LegacyImage::MD_MARQUEE]->applyTheme( - theme, getName(), "md_marquee", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION | VISIBLE); + mImageComponents[LegacyImage::MD_MARQUEE]->applyTheme(theme, getName(), "image_md_marquee", + POSITION | ThemeFlags::SIZE | Z_INDEX | + ROTATION | VISIBLE); if (mViewStyle == ViewController::DETAILED) { mImageComponents[LegacyImage::MD_IMAGE]->applyTheme( @@ -276,15 +278,15 @@ void GamelistView::legacyOnThemeChanged(const std::shared_ptr& theme) ALL); } - for (auto& container : mScrollableContainerComponents) { + for (auto& container : mContainerComponents) { container->applyTheme(theme, getName(), container->getMetadataField(), POSITION | ThemeFlags::SIZE | Z_INDEX | VISIBLE); } - mTextComponents[LegacyText::MD_DESCRIPTION]->setSize( - mScrollableContainerComponents.front()->getSize().x, 0.0f); + mTextComponents[LegacyText::MD_DESCRIPTION]->setSize(mContainerComponents.front()->getSize().x, + 0.0f); mTextComponents[LegacyText::MD_DESCRIPTION]->applyTheme( - theme, getName(), "md_description", + theme, getName(), "text_md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION)); for (auto& gamelistInfo : mGamelistInfoComponents) @@ -299,10 +301,10 @@ void GamelistView::legacyOnThemeChanged(const std::shared_ptr& theme) // Hide some components if we're in Basic mode. if (mViewStyle == ViewController::BASIC) { - if (mTheme->getElement(getName(), "logoText", "text") == nullptr) + if (mTheme->getElement(getName(), "text_logoText", "text") == nullptr) mTextComponents[LegacyText::LOGOTEXT]->setVisible(false); mImageComponents[LegacyImage::MD_IMAGE]->setVisible(false); - for (auto& container : mScrollableContainerComponents) + for (auto& container : mContainerComponents) container->setVisible(false); } @@ -339,11 +341,12 @@ void GamelistView::legacyUpdateInfoPanel() // If we're scrolling, hide the metadata fields if the last game had this options set, // or if we're in the grouped custom collection view. - if (mList.isScrolling()) + if (mList.isScrolling()) { if ((mLastUpdated && mLastUpdated->metadata.get("hidemetadata") == "true") || (mLastUpdated->getSystem()->isCustomCollection() && mLastUpdated->getPath() == mLastUpdated->getSystem()->getName())) hideMetaDataFields = true; + } if (hideMetaDataFields || mViewStyle == ViewController::BASIC) { for (size_t i = LegacyText::MD_LBL_RATING; i < LegacyText::MD_DESCRIPTION; ++i) @@ -484,7 +487,7 @@ void GamelistView::legacyUpdateInfoPanel() } mTextComponents[LegacyText::MD_DESCRIPTION]->setText(file->metadata.get("desc")); - for (auto& container : mScrollableContainerComponents) + for (auto& container : mContainerComponents) container->reset(); for (auto& rating : mRatingComponents) @@ -540,66 +543,6 @@ void GamelistView::legacyUpdateInfoPanel() } } - for (auto& text : mTextComponents) { - if (text->getValue() != "") - continue; - - std::string metadata = text->getMetadataField(); - if (metadata == "") - continue; - - if (metadata == "md_controller") { - std::string controller = - BadgeComponent::getDisplayName(file->metadata.get("controller")); - text->setValue(controller == "unknown" ? "" : controller); - continue; - } - - if (metadata == "md_name") - text->setValue(file->metadata.get("name")); - else if (metadata == "md_rating") - text->setValue(mRatingComponents.front()->getRatingValue()); - else if (metadata == "md_developer") - text->setValue(file->metadata.get("developer")); - else if (metadata == "md_publisher") - text->setValue(file->metadata.get("publisher")); - else if (metadata == "md_genre") - text->setValue(file->metadata.get("genre")); - else if (metadata == "md_players") - text->setValue(file->metadata.get("players")); - else if (metadata == "md_favorite") - text->setValue(file->metadata.get("favorite") == "true" ? "yes" : "no"); - else if (metadata == "md_completed") - text->setValue(file->metadata.get("completed") == "true" ? "yes" : "no"); - else if (metadata == "md_kidgame") - text->setValue(file->metadata.get("kidgame") == "true" ? "yes" : "no"); - else if (metadata == "md_broken") - text->setValue(file->metadata.get("broken") == "true" ? "yes" : "no"); - else if (metadata == "md_playcount") - text->setValue(file->metadata.get("playcount")); - else if (metadata == "md_altemulator") - text->setValue(file->metadata.get("altemulator")); - else - text->setValue(metadata); - } - - for (auto& date : mDateTimeComponents) { - std::string metadata = date->getMetadataField(); - if (metadata == "") - continue; - - if (metadata == "md_releasedate") { - date->setValue(file->metadata.get("releasedate")); - } - else if (metadata == "md_lastplayed") { - date->setValue(file->metadata.get("lastplayed")); - date->setDisplayRelative(true); - } - else { - date->setValue("19700101T000000"); - } - } - fadingOut = false; } @@ -724,9 +667,9 @@ void GamelistView::legacyInitMDValues() } mDateTimeComponents[LegacyDateTime::MD_RELEASEDATE]->setFont(defaultFont); - // mDateTimeComponents[LegacyDateTime::MD_RELEASEDATE]->setColor(0xFFFFFFFF); + // mDateTimeComponents[LegacyDateTime::MD_RELEASEDATE]->setColor(0xFFFFFFFF); mDateTimeComponents[LegacyDateTime::MD_LASTPLAYED]->setFont(defaultFont); - // mDateTimeComponents[LegacyDateTime::MD_LASTPLAYED]->setColor(0xFFFFFFFF); + // mDateTimeComponents[LegacyDateTime::MD_LASTPLAYED]->setColor(0xFFFFFFFF); values.emplace_back(mRatingComponents.front().get()); values.emplace_back(mDateTimeComponents[LegacyDateTime::MD_RELEASEDATE].get()); @@ -756,10 +699,10 @@ void GamelistView::legacyInitMDValues() mRatingComponents.front()->setPosition(Renderer::getScreenWidth() * 2.0f, Renderer::getScreenHeight() * 2.0f); - mScrollableContainerComponents.front()->setPosition(Renderer::getScreenWidth() * 2.0f, - Renderer::getScreenHeight() * 2.0f); + mContainerComponents.front()->setPosition(Renderer::getScreenWidth() * 2.0f, + Renderer::getScreenHeight() * 2.0f); - for (auto& container : mScrollableContainerComponents) { + for (auto& container : mContainerComponents) { container->setPosition(container->getPosition().x, bottom + mSize.y * 0.01f); container->setSize(container->getSize().x, mSize.y - container->getPosition().y); } diff --git a/es-app/src/views/GamelistView.cpp b/es-app/src/views/GamelistView.cpp index 63589bb75..6e7cabaac 100644 --- a/es-app/src/views/GamelistView.cpp +++ b/es-app/src/views/GamelistView.cpp @@ -13,17 +13,14 @@ #include "UIModeController.h" #include "animations/LambdaAnimation.h" -// #define FADE_IN_START_OPACITY 0.5f -// #define FADE_IN_TIME 325 +#define FADE_IN_START_OPACITY 0.5f +#define FADE_IN_TIME 325 -GamelistView::GamelistView(FileData* root, bool legacyMode) +GamelistView::GamelistView(FileData* root) : GamelistBase {root} - , mLegacyMode {legacyMode} + , mLegacyMode {false} , mViewStyle {ViewController::BASIC} { - // TEMPORARY - mLegacyMode = true; - mViewStyle = ViewController::getInstance()->getState().viewstyle; if (mLegacyMode) @@ -71,8 +68,14 @@ void GamelistView::onFileChanged(FileData* file, bool reloadGamelist) void GamelistView::onShow() { // Reset any Lottie animations. - for (auto extra : mThemeExtras) - extra->resetFileAnimation(); + for (auto& animation : mLottieAnimComponents) + animation->resetFileAnimation(); + + // Reset any Lottie animations. + if (mLegacyMode) { + for (auto extra : mThemeExtras) + extra->resetFileAnimation(); + } mLastUpdated = nullptr; GuiComponent::onShow(); @@ -84,6 +87,8 @@ void GamelistView::onShow() void GamelistView::onThemeChanged(const std::shared_ptr& theme) { + mLegacyMode = mTheme->isLegacyTheme(); + if (mLegacyMode) { legacyOnThemeChanged(theme); return; @@ -91,21 +96,94 @@ void GamelistView::onThemeChanged(const std::shared_ptr& theme) using namespace ThemeFlags; - // Remove old theme extras. - for (auto extra : mThemeExtras) { - removeChild(extra); - delete extra; + if (mTheme->hasView("gamelist")) { + for (auto& element : mTheme->getViewElements("gamelist").elements) { + if (element.second.type == "image") { + mImageComponents.push_back(std::make_unique()); + mImageComponents.back()->setDefaultZIndex(30.0f); + mImageComponents.back()->applyTheme(theme, "gamelist", element.first, ALL); + if (mImageComponents.back()->getMetadataField() != "") + mImageComponents.back()->setScrollHide(true); + addChild(mImageComponents.back().get()); + } + else if (element.second.type == "video") { + mVideoComponents.push_back(std::make_unique()); + mVideoComponents.back()->setDefaultZIndex(30.0f); + addChild(mVideoComponents.back().get()); + mVideoComponents.back()->applyTheme(theme, "gamelist", element.first, ALL); + if (mVideoComponents.back()->getMetadataField() != "") + mVideoComponents.back()->setScrollHide(true); + } + else if (element.second.type == "animation") { + mLottieAnimComponents.push_back(std::make_unique()); + mLottieAnimComponents.back()->setDefaultZIndex(35.0f); + mLottieAnimComponents.back()->applyTheme(theme, "gamelist", element.first, ALL); + addChild(mLottieAnimComponents.back().get()); + } + else if (element.second.type == "badges") { + mBadgeComponents.push_back(std::make_unique()); + mBadgeComponents.back()->setDefaultZIndex(35.0f); + mBadgeComponents.back()->applyTheme(theme, "gamelist", element.first, ALL); + mBadgeComponents.back()->setScrollHide(true); + addChild(mBadgeComponents.back().get()); + } + else if (element.second.type == "text") { + if (element.second.has("container") && element.second.get("container")) { + mContainerTextComponents.push_back(std::make_unique()); + mContainerTextComponents.back()->setDefaultZIndex(40.0f); + mContainerComponents.push_back(std::make_unique()); + mContainerComponents.back()->setAutoScroll(true); + mContainerComponents.back()->addChild(mContainerTextComponents.back().get()); + mContainerComponents.back()->applyTheme(theme, "gamelist", element.first, + POSITION | ThemeFlags::SIZE | Z_INDEX | + VISIBLE); + mContainerTextComponents.back()->applyTheme(theme, "gamelist", element.first, + ALL ^ POSITION ^ Z_INDEX ^ + ThemeFlags::SIZE ^ VISIBLE); + mContainerTextComponents.back()->setSize( + mContainerComponents.back()->getSize().x, 0.0f); + mContainerComponents.back()->setDefaultZIndex( + mContainerTextComponents.back()->getDefaultZIndex()); + mContainerComponents.back()->setZIndex( + mContainerTextComponents.back()->getZIndex()); + mContainerComponents.back()->setScrollHide(true); + addChild(mContainerComponents.back().get()); + } + else { + mTextComponents.push_back(std::make_unique()); + mTextComponents.back()->setDefaultZIndex(40.0f); + mTextComponents.back()->applyTheme(theme, "gamelist", element.first, ALL); + if (mTextComponents.back()->getMetadataField() != "") + mTextComponents.back()->setScrollHide(true); + addChild(mTextComponents.back().get()); + } + } + else if (element.second.type == "datetime") { + mDateTimeComponents.push_back(std::make_unique()); + mDateTimeComponents.back()->setDefaultZIndex(40.0f); + mDateTimeComponents.back()->applyTheme(theme, "gamelist", element.first, ALL); + if (mDateTimeComponents.back()->getMetadataField() != "") + mDateTimeComponents.back()->setScrollHide(true); + addChild(mDateTimeComponents.back().get()); + } + else if (element.second.type == "gamelistinfo") { + mGamelistInfoComponents.push_back(std::make_unique()); + mGamelistInfoComponents.back()->setDefaultZIndex(45.0f); + mGamelistInfoComponents.back()->applyTheme(theme, "gamelist", element.first, ALL); + addChild(mGamelistInfoComponents.back().get()); + } + else if (element.second.type == "rating") { + mRatingComponents.push_back(std::make_unique()); + mRatingComponents.back()->setDefaultZIndex(45.0f); + mRatingComponents.back()->applyTheme(theme, "gamelist", element.first, ALL); + mRatingComponents.back()->setScrollHide(true); + addChild(mRatingComponents.back().get()); + } + } } - mThemeExtras.clear(); - // Add new theme extras. - mThemeExtras = ThemeData::makeExtras(theme, getName()); - for (auto extra : mThemeExtras) - addChild(extra); - - mList.applyTheme(theme, getName(), "gamelist", ALL); - - // TODO: Implement logic to populate component vectors. + mList.setDefaultZIndex(50.0f); + mList.applyTheme(theme, "gamelist", "textlist_gamelist", ALL); sortChildren(); } @@ -117,6 +195,23 @@ void GamelistView::update(int deltaTime) return; } + if (ViewController::getInstance()->getGameLaunchTriggered()) { + for (auto& image : mImageComponents) { + if (image->isAnimationPlaying(0)) + image->finishAnimation(0); + } + } + + for (auto& video : mVideoComponents) { + if (!mVideoPlaying) + video->onHide(); + else if (mVideoPlaying && !video->isVideoPaused() && !mWindow->isScreensaverActive()) + video->onShow(); + + if (ViewController::getInstance()->getGameLaunchTriggered() && video->isAnimationPlaying(0)) + video->finishAnimation(0); + } + updateChildren(deltaTime); } @@ -140,7 +235,10 @@ void GamelistView::render(const glm::mat4& parentTrans) HelpStyle GamelistView::getHelpStyle() { HelpStyle style; - style.applyTheme(mTheme, getName()); + if (mLegacyMode) + style.applyTheme(mTheme, getName()); + else + style.applyTheme(mTheme, "gamelist"); return style; } @@ -193,6 +291,11 @@ std::vector GamelistView::getHelpPrompts() void GamelistView::updateInfoPanel() { + if (mLegacyMode) { + legacyUpdateInfoPanel(); + return; + } + FileData* file {(mList.size() == 0 || mList.isScrolling()) ? nullptr : mList.getSelected()}; // If the game data has already been rendered to the info panel, then skip it this time. @@ -219,8 +322,438 @@ void GamelistView::updateInfoPanel() } } - if (hideMetaDataFields) - ; + // If we're scrolling, hide the metadata fields if the last game had this options set, + // or if we're in the grouped custom collection view. + if (mList.isScrolling()) { + if ((mLastUpdated && mLastUpdated->metadata.get("hidemetadata") == "true") || + (mLastUpdated->getSystem()->isCustomCollection() && + mLastUpdated->getPath() == mLastUpdated->getSystem()->getName())) + hideMetaDataFields = true; + } - // TODO: Implement gamelist logic. + if (hideMetaDataFields) { + for (auto& text : mTextComponents) { + if (text->getMetadataField() != "") + text->setVisible(false); + } + for (auto& date : mDateTimeComponents) + date->setVisible(false); + for (auto& badge : mBadgeComponents) + badge->setVisible(false); + for (auto& rating : mRatingComponents) + rating->setVisible(false); + for (auto& cText : mContainerTextComponents) { + if (cText->getMetadataField() != "md_description") + cText->setVisible(false); + } + } + else { + for (auto& text : mTextComponents) { + if (text->getMetadataField() != "") + text->setVisible(true); + } + for (auto& date : mDateTimeComponents) + date->setVisible(true); + for (auto& badge : mBadgeComponents) + badge->setVisible(true); + for (auto& rating : mRatingComponents) + rating->setVisible(true); + for (auto& cText : mContainerTextComponents) { + if (cText->getMetadataField() != "md_description") + cText->setVisible(true); + } + } + + bool fadingOut = false; + if (file == nullptr) { + mVideoPlaying = false; + fadingOut = true; + } + else { + // If we're browsing a grouped custom collection, then update the folder metadata + // which will generate a description of three random games and return a pointer to + // the first of these so that we can display its game media. + if (file->getSystem()->isCustomCollection() && + file->getPath() == file->getSystem()->getName()) { + mRandomGame = CollectionSystemsManager::getInstance()->updateCollectionFolderMetadata( + file->getSystem()); + if (mRandomGame) { + for (auto& image : mImageComponents) { + if (image->getMetadataField() == "md_image") + image->setImage(mRandomGame->getImagePath()); + else if (image->getMetadataField() == "md_miximage") + image->setImage(mRandomGame->getMiximagePath()); + else if (image->getMetadataField() == "md_marquee") + image->setImage(mRandomGame->getMarqueePath(), false, true); + else if (image->getMetadataField() == "md_screenshot") + image->setImage(mRandomGame->getScreenshotPath()); + else if (image->getMetadataField() == "md_titlescreen") + image->setImage(mRandomGame->getTitleScreenPath()); + else if (image->getMetadataField() == "md_cover") + image->setImage(mRandomGame->getCoverPath()); + else if (image->getMetadataField() == "md_backcover") + image->setImage(mRandomGame->getBackCoverPath()); + else if (image->getMetadataField() == "md_3dbox") + image->setImage(mRandomGame->get3DBoxPath()); + else if (image->getMetadataField() == "md_fanart") + image->setImage(mRandomGame->getFanArtPath()); + else if (image->getMetadataField() == "md_thumbnail") + image->setImage(mRandomGame->getThumbnailPath()); + } + + for (auto& video : mVideoComponents) { + if (video->getMetadataField() == "md_image") + video->setImage(mRandomGame->getImagePath()); + else if (video->getMetadataField() == "md_miximage") + video->setImage(mRandomGame->getMiximagePath()); + else if (video->getMetadataField() == "md_marquee") + video->setImage(mRandomGame->getMarqueePath(), false, true); + else if (video->getMetadataField() == "md_screenshot") + video->setImage(mRandomGame->getScreenshotPath()); + else if (video->getMetadataField() == "md_titlescreen") + video->setImage(mRandomGame->getTitleScreenPath()); + else if (video->getMetadataField() == "md_cover") + video->setImage(mRandomGame->getCoverPath()); + else if (video->getMetadataField() == "md_backcover") + video->setImage(mRandomGame->getBackCoverPath()); + else if (video->getMetadataField() == "md_3dbox") + video->setImage(mRandomGame->get3DBoxPath()); + else if (video->getMetadataField() == "md_fanart") + video->setImage(mRandomGame->getFanArtPath()); + else if (video->getMetadataField() == "md_thumbnail") + video->setImage(mRandomGame->getThumbnailPath()); + + // Always stop the video before setting a new video as it will otherwise + // continue to play if it has the same path (i.e. it is the same physical + // video file) as the previously set video. That may happen when entering a + // folder with the same name as the first game file inside, or as in this + // case, when entering a custom collection. + video->onHide(); + + if (video->hasStaticVideo()) + video->setStaticVideo(); + else if (!video->setVideo(mRandomGame->getVideoPath())) + video->setDefaultVideo(); + } + } + else { + for (auto& image : mImageComponents) { + if (image->getMetadataField() != "") + image->setImage(""); + } + + for (auto& video : mVideoComponents) { + video->setImage(""); + video->setVideo(""); + if (video->hasStaticVideo()) { + video->onStopVideo(); + video->setStaticVideo(); + } + else { + video->setDefaultVideo(); + } + } + } + } + else { + for (auto& image : mImageComponents) { + if (image->getMetadataField() == "md_image") + image->setImage(file->getImagePath()); + else if (image->getMetadataField() == "md_miximage") + image->setImage(file->getMiximagePath()); + else if (image->getMetadataField() == "md_marquee") + image->setImage(file->getMarqueePath(), false, true); + else if (image->getMetadataField() == "md_screenshot") + image->setImage(file->getScreenshotPath()); + else if (image->getMetadataField() == "md_titlescreen") + image->setImage(file->getTitleScreenPath()); + else if (image->getMetadataField() == "md_cover") + image->setImage(file->getCoverPath()); + else if (image->getMetadataField() == "md_backcover") + image->setImage(file->getBackCoverPath()); + else if (image->getMetadataField() == "md_3dbox") + image->setImage(file->get3DBoxPath()); + else if (image->getMetadataField() == "md_fanart") + image->setImage(file->getFanArtPath()); + else if (image->getMetadataField() == "md_thumbnail") + image->setImage(file->getThumbnailPath()); + } + + for (auto& video : mVideoComponents) { + if (video->getMetadataField() == "md_image") + video->setImage(file->getImagePath()); + else if (video->getMetadataField() == "md_miximage") + video->setImage(file->getMiximagePath()); + else if (video->getMetadataField() == "md_marquee") + video->setImage(file->getMarqueePath(), false, true); + else if (video->getMetadataField() == "md_screenshot") + video->setImage(file->getScreenshotPath()); + else if (video->getMetadataField() == "md_titlescreen") + video->setImage(file->getTitleScreenPath()); + else if (video->getMetadataField() == "md_cover") + video->setImage(file->getCoverPath()); + else if (video->getMetadataField() == "md_backcover") + video->setImage(file->getBackCoverPath()); + else if (video->getMetadataField() == "md_3dbox") + video->setImage(file->get3DBoxPath()); + else if (video->getMetadataField() == "md_fanart") + video->setImage(file->getFanArtPath()); + else if (video->getMetadataField() == "md_thumbnail") + video->setImage(file->getThumbnailPath()); + + video->onHide(); + + if (video->hasStaticVideo()) + video->setStaticVideo(); + else if (!video->setVideo(file->getVideoPath())) + video->setDefaultVideo(); + } + } + + mVideoPlaying = true; + + // Populate the gamelistInfo field which shows an icon if a folder has been entered + // as well as the game count for the entire system (total and favorites separately). + // If a filter has been applied, then the number of filtered and total games replaces + // the game counter. + for (auto& gamelistInfo : mGamelistInfoComponents) { + std::string gamelistInfoString; + Alignment infoAlign = gamelistInfo->getHorizontalAlignment(); + + if (mIsFolder && infoAlign == ALIGN_RIGHT) + gamelistInfoString = ViewController::FOLDER_CHAR + " "; + + if (mIsFiltered) { + if (mFilteredGameCountAll == mFilteredGameCount) + gamelistInfoString += ViewController::FILTER_CHAR + " " + + std::to_string(mFilteredGameCount) + " / " + + std::to_string(mGameCount); + else + gamelistInfoString += + ViewController::FILTER_CHAR + " " + std::to_string(mFilteredGameCount) + + " + " + std::to_string(mFilteredGameCountAll - mFilteredGameCount) + " / " + + std::to_string(mGameCount); + } + else { + gamelistInfoString += + ViewController::CONTROLLER_CHAR + " " + std::to_string(mGameCount); + if (!(file->getSystem()->isCollection() && + file->getSystem()->getFullName() == "favorites")) + gamelistInfoString += " " + ViewController::FAVORITE_CHAR + " " + + std::to_string(mFavoritesGameCount); + } + + if (mIsFolder && infoAlign != ALIGN_RIGHT) + gamelistInfoString += " " + ViewController::FOLDER_CHAR; + + gamelistInfo->setValue(gamelistInfoString); + } + + // Fade in the game image. + for (auto& image : mImageComponents) { + if (image->getMetadataField() == "md_image") { + auto func = [&image](float t) { + image->setOpacity(static_cast( + glm::mix(static_cast(FADE_IN_START_OPACITY), 1.0f, t) * 255)); + }; + image->setAnimation(new LambdaAnimation(func, FADE_IN_TIME), 0, nullptr, false); + } + } + + // Fade in the static image. + for (auto& video : mVideoComponents) { + if (video->getMetadataField() == "md_video") { + auto func = [&video](float t) { + video->setOpacity(static_cast( + glm::mix(static_cast(FADE_IN_START_OPACITY), 1.0f, t) * 255)); + }; + video->setAnimation(new LambdaAnimation(func, FADE_IN_TIME), 0, nullptr, false); + } + } + + for (auto& container : mContainerComponents) + container->reset(); + + for (auto& rating : mRatingComponents) + rating->setValue(file->metadata.get("rating")); + + // Populate the badge slots based on game metadata. + std::vector badgeSlots; + for (auto& badgeComponent : mBadgeComponents) { + for (auto& badge : badgeComponent->getBadgeTypes()) { + BadgeComponent::BadgeInfo badgeInfo; + badgeInfo.badgeType = badge; + if (badge == "controller") { + if (file->metadata.get("controller").compare("") != 0) { + badgeInfo.gameController = file->metadata.get("controller"); + badgeSlots.push_back(badgeInfo); + } + } + else if (badge == "altemulator") { + if (file->metadata.get(badge).compare("") != 0) + badgeSlots.push_back(badgeInfo); + } + else { + if (file->metadata.get(badge).compare("true") == 0) + badgeSlots.push_back(badgeInfo); + } + } + badgeComponent->setBadges(badgeSlots); + } + + for (auto& text : mTextComponents) { + if (text->getMetadataField() == "md_name") + text->setText(file->metadata.get("name")); + } + + if (file->getType() == GAME) { + if (!hideMetaDataFields) { + for (auto& date : mDateTimeComponents) { + if (date->getMetadataField() == "md_lastplayed") + date->setValue(file->metadata.get("lastplayed")); + else if (date->getMetadataField() == "md_playcount") + date->setValue(file->metadata.get("playcount")); + } + } + else if (file->getType() == FOLDER) { + if (!hideMetaDataFields) { + for (auto& date : mDateTimeComponents) { + if (date->getMetadataField() == "md_lastplayed") { + date->setValue(file->metadata.get("lastplayed")); + date->setVisible(false); + date->setVisible(false); + } + } + } + } + } + + std::string metadata; + + auto getMetadataValue = [&file, &metadata]() -> std::string { + if (metadata == "md_name") + return file->metadata.get("name"); + else if (metadata == "md_description") + return file->metadata.get("desc"); + else if (metadata == "md_developer") + return file->metadata.get("developer"); + else if (metadata == "md_publisher") + return file->metadata.get("publisher"); + else if (metadata == "md_genre") + return file->metadata.get("genre"); + else if (metadata == "md_players") + return file->metadata.get("players"); + else if (metadata == "md_favorite") + return file->metadata.get("favorite") == "true" ? "Yes" : "No"; + else if (metadata == "md_completed") + return file->metadata.get("completed") == "true" ? "Yes" : "No"; + else if (metadata == "md_kidgame") + return file->metadata.get("kidgame") == "true" ? "Yes" : "No"; + else if (metadata == "md_broken") + return file->metadata.get("broken") == "true" ? "Yes" : "No"; + else if (metadata == "md_playcount") + return file->metadata.get("playcount"); + else if (metadata == "md_altemulator") + return file->metadata.get("altemulator"); + else + return metadata; + }; + + for (auto& text : mContainerTextComponents) { + metadata = text->getMetadataField(); + if (metadata == "") + continue; + + if (metadata == "md_rating") { + text->setValue(mRatingComponents.front()->getRatingValue()); + continue; + } + else if (metadata == "md_controller") { + std::string controller = + BadgeComponent::getDisplayName(file->metadata.get("controller")); + text->setValue(controller == "unknown" ? "" : controller); + continue; + } + + text->setValue(getMetadataValue()); + } + + for (auto& text : mTextComponents) { + metadata = text->getMetadataField(); + if (metadata == "") + continue; + + if (metadata == "md_rating") { + text->setValue(mRatingComponents.front()->getRatingValue()); + continue; + } + else if (metadata == "md_controller") { + std::string controller = + BadgeComponent::getDisplayName(file->metadata.get("controller")); + text->setValue(controller == "unknown" ? "" : controller); + continue; + } + + text->setValue(getMetadataValue()); + } + + for (auto& date : mDateTimeComponents) { + std::string metadata = date->getMetadataField(); + if (metadata == "") + continue; + + if (metadata == "md_releasedate") { + date->setValue(file->metadata.get("releasedate")); + } + else if (metadata == "md_lastplayed") { + date->setValue(file->metadata.get("lastplayed")); + date->setDisplayRelative(true); + } + else { + date->setValue("19700101T000000"); + } + } + + fadingOut = false; + } + + std::vector comps; + + for (auto& text : mTextComponents) { + if (text->getScrollHide()) + comps.emplace_back(text.get()); + } + for (auto& date : mDateTimeComponents) { + if (date->getScrollHide()) + comps.emplace_back(date.get()); + } + for (auto& image : mImageComponents) { + if (image->getScrollHide()) + comps.emplace_back(image.get()); + } + for (auto& badge : mBadgeComponents) { + if (badge->getScrollHide()) + comps.emplace_back(badge.get()); + } + for (auto& rating : mRatingComponents) { + if (rating->getScrollHide()) + comps.emplace_back(rating.get()); + } + for (auto& container : mContainerComponents) { + if (container->getScrollHide()) + comps.emplace_back(container.get()); + } + + for (auto it = comps.cbegin(); it != comps.cend(); ++it) { + GuiComponent* comp = *it; + // An animation is playing, then animate if reverse != fadingOut. + // An animation is not playing, then animate if opacity != our target opacity. + if ((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) || + (!comp->isAnimationPlaying(0) && comp->getOpacity() != (fadingOut ? 0 : 255))) { + auto func = [comp](float t) { + comp->setOpacity(static_cast(glm::mix(0.0f, 1.0f, t) * 255)); + }; + comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut); + } + } } diff --git a/es-app/src/views/GamelistView.h b/es-app/src/views/GamelistView.h index 9a92343a3..340637ca1 100644 --- a/es-app/src/views/GamelistView.h +++ b/es-app/src/views/GamelistView.h @@ -17,7 +17,7 @@ class GamelistView : public GamelistBase { public: - GamelistView(FileData* root, bool legacyMode = false); + GamelistView(FileData* root); ~GamelistView(); // Called when a FileData* is added, has its metadata changed, or is removed. @@ -76,9 +76,11 @@ private: std::vector> mDateTimeComponents; std::vector> mImageComponents; std::vector> mVideoComponents; + std::vector> mLottieAnimComponents; std::vector> mBadgeComponents; std::vector> mRatingComponents; - std::vector> mScrollableContainerComponents; + std::vector> mContainerComponents; + std::vector> mContainerTextComponents; std::vector> mGamelistInfoComponents; enum LegacyText { diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 2ce73d1d2..d714e1a2d 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -76,7 +76,8 @@ void SystemView::populate() glm::vec3 offsetLogoPlaceholderText = {0.0f, 0.0f, 0.0f}; // Make logo. - const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); + const ThemeData::ThemeElement* logoElem = + theme->getElement("system", "image_logo", "image"); if (logoElem) { auto path = logoElem->get("path"); std::string defaultPath = @@ -86,7 +87,8 @@ void SystemView::populate() ResourceManager::getInstance().fileExists(defaultPath))) { auto* logo = new ImageComponent(false, false); logo->setMaxSize(glm::round(mCarousel.logoSize * mCarousel.logoScale)); - logo->applyTheme(theme, "system", "logo", ThemeFlags::PATH | ThemeFlags::COLOR); + logo->applyTheme(theme, "system", "image_logo", + ThemeFlags::PATH | ThemeFlags::COLOR); logo->setRotateByTargetSize(true); e.data.logo = std::shared_ptr(logo); } @@ -100,7 +102,7 @@ void SystemView::populate() glm::vec3 center {resolution.x / 2.0f, resolution.y / 2.0f, 1.0f}; // Placeholder Image. - logoElem = theme->getElement("system", "logoPlaceholderImage", "image"); + logoElem = theme->getElement("system", "image_logoPlaceholderImage", "image"); if (logoElem) { auto path = logoElem->get("path"); std::string defaultPath = @@ -109,7 +111,8 @@ void SystemView::populate() (!defaultPath.empty() && ResourceManager::getInstance().fileExists(defaultPath))) { auto* logo = new ImageComponent(false, false); - logo->applyTheme(theme, "system", "logoPlaceholderImage", ThemeFlags::ALL); + logo->applyTheme(theme, "system", "image_logoPlaceholderImage", + ThemeFlags::ALL); if (!logoElem->has("size")) logo->setMaxSize(mCarousel.logoSize * mCarousel.logoScale); offsetLogo = logo->getPosition() - center; @@ -120,7 +123,7 @@ void SystemView::populate() // Placeholder Text. const ThemeData::ThemeElement* logoPlaceholderText = - theme->getElement("system", "logoPlaceholderText", "text"); + theme->getElement("system", "text_logoPlaceholderText", "text"); if (logoPlaceholderText) { // Element 'logoPlaceholderText' found in theme: place text auto* text = new TextComponent(it->getName(), Font::get(FONT_SIZE_LARGE), @@ -134,7 +137,7 @@ void SystemView::populate() text->setHorizontalAlignment(ALIGN_CENTER); text->setVerticalAlignment(mCarousel.logoAlignment); } - text->applyTheme(it->getTheme(), "system", "logoPlaceholderText", + text->applyTheme(it->getTheme(), "system", "text_logoPlaceholderText", ThemeFlags::ALL); if (!e.data.logo) { e.data.logo = std::shared_ptr(text); @@ -541,15 +544,16 @@ void SystemView::getViewElements(const std::shared_ptr& theme) return; const ThemeData::ThemeElement* carouselElem = - theme->getElement("system", "systemcarousel", "carousel"); + theme->getElement("system", "carousel_systemcarousel", "carousel"); if (carouselElem) getCarouselFromTheme(carouselElem); - const ThemeData::ThemeElement* sysInfoElem = theme->getElement("system", "systemInfo", "text"); + const ThemeData::ThemeElement* sysInfoElem = + theme->getElement("system", "text_systemInfo", "text"); if (sysInfoElem) - mSystemInfo.applyTheme(theme, "system", "systemInfo", ThemeFlags::ALL); + mSystemInfo.applyTheme(theme, "system", "text_systemInfo", ThemeFlags::ALL); mViewNeedsReload = false; } diff --git a/es-core/src/HelpStyle.cpp b/es-core/src/HelpStyle.cpp index fd3815b79..b5d9f732e 100644 --- a/es-core/src/HelpStyle.cpp +++ b/es-core/src/HelpStyle.cpp @@ -11,6 +11,8 @@ #include "resources/Font.h" +#define PREFIX "button_" + HelpStyle::HelpStyle() { position = @@ -32,7 +34,7 @@ HelpStyle::HelpStyle() void HelpStyle::applyTheme(const std::shared_ptr& theme, const std::string& view) { - auto elem = theme->getElement(view, "help", "helpsystem"); + auto elem = theme->getElement(view, "helpsystem_help", "helpsystem"); if (!elem) return; @@ -73,74 +75,76 @@ void HelpStyle::applyTheme(const std::shared_ptr& theme, const std::s textStyle = elem->get("textStyle"); // Load custom button icons. + // The names may look a bit strange when combined with the PREFIX string "button_" but it's + // because ThemeData adds this prefix to avoid name collisions when using XML attributes. // General. - if (elem->has("dpad_updown")) - mCustomButtons.dpad_updown = elem->get("dpad_updown"); - if (elem->has("dpad_leftright")) - mCustomButtons.dpad_leftright = elem->get("dpad_leftright"); - if (elem->has("dpad_all")) - mCustomButtons.dpad_all = elem->get("dpad_all"); - if (elem->has("thumbstick_click")) - mCustomButtons.thumbstick_click = elem->get("thumbstick_click"); - if (elem->has("button_l")) - mCustomButtons.button_l = elem->get("button_l"); - if (elem->has("button_r")) - mCustomButtons.button_r = elem->get("button_r"); - if (elem->has("button_lr")) - mCustomButtons.button_lr = elem->get("button_lr"); - if (elem->has("button_lt")) - mCustomButtons.button_lt = elem->get("button_lt"); - if (elem->has("button_rt")) - mCustomButtons.button_rt = elem->get("button_rt"); + if (elem->has(PREFIX "dpad_updown")) + mCustomButtons.dpad_updown = elem->get(PREFIX "dpad_updown"); + if (elem->has(PREFIX "dpad_leftright")) + mCustomButtons.dpad_leftright = elem->get(PREFIX "dpad_leftright"); + if (elem->has(PREFIX "dpad_all")) + mCustomButtons.dpad_all = elem->get(PREFIX "dpad_all"); + if (elem->has(PREFIX "thumbstick_click")) + mCustomButtons.thumbstick_click = elem->get(PREFIX "thumbstick_click"); + if (elem->has(PREFIX "button_l")) + mCustomButtons.button_l = elem->get(PREFIX "button_l"); + if (elem->has(PREFIX "button_r")) + mCustomButtons.button_r = elem->get(PREFIX "button_r"); + if (elem->has(PREFIX "button_lr")) + mCustomButtons.button_lr = elem->get(PREFIX "button_lr"); + if (elem->has(PREFIX "button_lt")) + mCustomButtons.button_lt = elem->get(PREFIX "button_lt"); + if (elem->has(PREFIX "button_rt")) + mCustomButtons.button_rt = elem->get(PREFIX "button_rt"); // SNES. - if (elem->has("button_a_SNES")) - mCustomButtons.button_a_SNES = elem->get("button_a_SNES"); - if (elem->has("button_b_SNES")) - mCustomButtons.button_b_SNES = elem->get("button_b_SNES"); - if (elem->has("button_x_SNES")) - mCustomButtons.button_x_SNES = elem->get("button_x_SNES"); - if (elem->has("button_y_SNES")) - mCustomButtons.button_y_SNES = elem->get("button_y_SNES"); - if (elem->has("button_start_SNES")) - mCustomButtons.button_start_SNES = elem->get("button_start_SNES"); - if (elem->has("button_back_SNES")) - mCustomButtons.button_back_SNES = elem->get("button_back_SNES"); + if (elem->has(PREFIX "button_a_SNES")) + mCustomButtons.button_a_SNES = elem->get(PREFIX "button_a_SNES"); + if (elem->has(PREFIX "button_b_SNES")) + mCustomButtons.button_b_SNES = elem->get(PREFIX "button_b_SNES"); + if (elem->has(PREFIX "button_x_SNES")) + mCustomButtons.button_x_SNES = elem->get(PREFIX "button_x_SNES"); + if (elem->has(PREFIX "button_y_SNES")) + mCustomButtons.button_y_SNES = elem->get(PREFIX "button_y_SNES"); + if (elem->has(PREFIX "button_start_SNES")) + mCustomButtons.button_start_SNES = elem->get(PREFIX "button_start_SNES"); + if (elem->has(PREFIX "button_back_SNES")) + mCustomButtons.button_back_SNES = elem->get(PREFIX "button_back_SNES"); - // PS. - if (elem->has("button_a_PS")) - mCustomButtons.button_a_PS = elem->get("button_a_PS"); - if (elem->has("button_b_PS")) - mCustomButtons.button_b_PS = elem->get("button_b_PS"); - if (elem->has("button_x_PS")) - mCustomButtons.button_x_PS = elem->get("button_x_PS"); - if (elem->has("button_y_PS")) - mCustomButtons.button_y_PS = elem->get("button_y_PS"); - if (elem->has("button_start_PS4")) - mCustomButtons.button_start_PS4 = elem->get("button_start_PS4"); - if (elem->has("button_back_PS4")) - mCustomButtons.button_back_PS4 = elem->get("button_back_PS4"); - if (elem->has("button_start_PS5")) - mCustomButtons.button_start_PS5 = elem->get("button_start_PS5"); - if (elem->has("button_back_PS5")) - mCustomButtons.button_back_PS5 = elem->get("button_back_PS5"); + // PlayStation. + if (elem->has(PREFIX "button_a_PS")) + mCustomButtons.button_a_PS = elem->get(PREFIX "button_a_PS"); + if (elem->has(PREFIX "button_b_PS")) + mCustomButtons.button_b_PS = elem->get(PREFIX "button_b_PS"); + if (elem->has(PREFIX "button_x_PS")) + mCustomButtons.button_x_PS = elem->get(PREFIX "button_x_PS"); + if (elem->has(PREFIX "button_y_PS")) + mCustomButtons.button_y_PS = elem->get(PREFIX "button_y_PS"); + if (elem->has(PREFIX "button_start_PS4")) + mCustomButtons.button_start_PS4 = elem->get(PREFIX "button_start_PS4"); + if (elem->has(PREFIX "button_back_PS4")) + mCustomButtons.button_back_PS4 = elem->get(PREFIX "button_back_PS4"); + if (elem->has(PREFIX "button_start_PS5")) + mCustomButtons.button_start_PS5 = elem->get(PREFIX "button_start_PS5"); + if (elem->has(PREFIX "button_back_PS5")) + mCustomButtons.button_back_PS5 = elem->get(PREFIX "button_back_PS5"); // XBOX. - if (elem->has("button_a_XBOX")) - mCustomButtons.button_a_XBOX = elem->get("button_a_XBOX"); - if (elem->has("button_b_XBOX")) - mCustomButtons.button_b_XBOX = elem->get("button_b_XBOX"); - if (elem->has("button_x_XBOX")) - mCustomButtons.button_x_XBOX = elem->get("button_x_XBOX"); - if (elem->has("button_y_XBOX")) - mCustomButtons.button_y_XBOX = elem->get("button_y_XBOX"); - if (elem->has("button_start_XBOX")) - mCustomButtons.button_start_XBOX = elem->get("button_start_XBOX"); - if (elem->has("button_back_XBOX")) - mCustomButtons.button_back_XBOX = elem->get("button_back_XBOX"); - if (elem->has("button_start_XBOX360")) - mCustomButtons.button_start_XBOX360 = elem->get("button_start_XBOX360"); - if (elem->has("button_back_XBOX360")) - mCustomButtons.button_back_XBOX360 = elem->get("button_back_XBOX360"); + if (elem->has(PREFIX "button_a_XBOX")) + mCustomButtons.button_a_XBOX = elem->get(PREFIX "button_a_XBOX"); + if (elem->has(PREFIX "button_b_XBOX")) + mCustomButtons.button_b_XBOX = elem->get(PREFIX "button_b_XBOX"); + if (elem->has(PREFIX "button_x_XBOX")) + mCustomButtons.button_x_XBOX = elem->get(PREFIX "button_x_XBOX"); + if (elem->has(PREFIX "button_y_XBOX")) + mCustomButtons.button_y_XBOX = elem->get(PREFIX "button_y_XBOX"); + if (elem->has(PREFIX "button_start_XBOX")) + mCustomButtons.button_start_XBOX = elem->get(PREFIX "button_start_XBOX"); + if (elem->has(PREFIX "button_back_XBOX")) + mCustomButtons.button_back_XBOX = elem->get(PREFIX "button_back_XBOX"); + if (elem->has(PREFIX "button_start_XBOX360")) + mCustomButtons.button_start_XBOX360 = elem->get(PREFIX "button_start_XBOX360"); + if (elem->has(PREFIX "button_back_XBOX360")) + mCustomButtons.button_back_XBOX360 = elem->get(PREFIX "button_back_XBOX360"); } diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index e19b7a7d3..f97ae75bc 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -133,6 +133,8 @@ void Settings::setDefaults() mStringMap["GamelistViewStyle"] = {"automatic", "automatic"}; mStringMap["TransitionStyle"] = {"slide", "slide"}; mStringMap["ThemeSet"] = {"rbsimple-DE", "rbsimple-DE"}; + mStringMap["ThemeVariant"] = {"", ""}; + mStringMap["ThemeAspectRatio"] = {"", ""}; mStringMap["UIMode"] = {"full", "full"}; mStringMap["DefaultSortOrder"] = {"filename, ascending", "filename, ascending"}; mStringMap["MenuOpeningEffect"] = {"scale-up", "scale-up"}; diff --git a/es-core/src/Sound.cpp b/es-core/src/Sound.cpp index 0ee7251ff..1c5248184 100644 --- a/es-core/src/Sound.cpp +++ b/es-core/src/Sound.cpp @@ -33,18 +33,20 @@ std::shared_ptr Sound::getFromTheme(ThemeData* const theme, const std::string& view, const std::string& element) { + std::string elemName {element.substr(6, std::string::npos)}; + if (theme == nullptr) { - LOG(LogDebug) << "Sound::getFromTheme(): Using fallback sound file for \"" << element + LOG(LogDebug) << "Sound::getFromTheme(): Using fallback sound file for \"" << elemName << "\""; - return get(ResourceManager::getInstance().getResourcePath(":/sounds/" + element + ".wav")); + return get(ResourceManager::getInstance().getResourcePath(":/sounds/" + elemName + ".wav")); } - LOG(LogDebug) << "Sound::getFromTheme(): Looking for tag "; + LOG(LogDebug) << "Sound::getFromTheme(): Looking for tag "; const ThemeData::ThemeElement* elem = theme->getElement(view, element, "sound"); if (!elem || !elem->has("path")) { LOG(LogDebug) << "Sound::getFromTheme(): Tag not found, using fallback sound file"; - return get(ResourceManager::getInstance().getResourcePath(":/sounds/" + element + ".wav")); + return get(ResourceManager::getInstance().getResourcePath(":/sounds/" + elemName + ".wav")); } LOG(LogDebug) << "Sound::getFromTheme(): Tag found, ready to load theme sound file"; @@ -79,8 +81,7 @@ void Sound::init() Uint8* data = nullptr; Uint32 dlen = 0; if (SDL_LoadWAV(mPath.c_str(), &wave, &data, &dlen) == nullptr) { - LOG(LogError) << "Failed to load theme navigation sound file:"; - LOG(LogError) << SDL_GetError(); + LOG(LogError) << "Failed to load theme navigation sound file: " << SDL_GetError(); return; } @@ -90,14 +91,12 @@ void Sound::init() AudioManager::sAudioFormat.channels, AudioManager::sAudioFormat.freq); if (conversionStream == nullptr) { - LOG(LogError) << "Failed to create sample conversion stream:"; - LOG(LogError) << SDL_GetError(); + LOG(LogError) << "Failed to create sample conversion stream: " << SDL_GetError(); return; } if (SDL_AudioStreamPut(conversionStream, data, dlen) == -1) { - LOG(LogError) << "Failed to put samples in the conversion stream:"; - LOG(LogError) << SDL_GetError(); + LOG(LogError) << "Failed to put samples in the conversion stream: " << SDL_GetError(); SDL_FreeAudioStream(conversionStream); return; } @@ -106,8 +105,7 @@ void Sound::init() Uint8* converted = new Uint8[sampleLength]; if (SDL_AudioStreamGet(conversionStream, converted, sampleLength) == -1) { - LOG(LogError) << "Failed to convert sound file '" << mPath << "':"; - LOG(LogError) << SDL_GetError(); + LOG(LogError) << "Failed to convert sound file '" << mPath << "': " << SDL_GetError(); SDL_FreeAudioStream(conversionStream); delete[] converted; return; @@ -209,13 +207,13 @@ void NavigationSounds::loadThemeNavigationSounds(ThemeData* const theme) "Theme set does not include navigation sound support, using fallback sounds"; } - mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "systembrowse")); - mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "quicksysselect")); - mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "select")); - mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "back")); - mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "scroll")); - mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "favorite")); - mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "launch")); + mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "sound_systembrowse")); + mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "sound_quicksysselect")); + mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "sound_select")); + mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "sound_back")); + mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "sound_scroll")); + mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "sound_favorite")); + mNavigationSounds.push_back(Sound::getFromTheme(theme, "all", "sound_launch")); } void NavigationSounds::playThemeNavigationSound(NavigationSoundsID soundID) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 4ec61aaa8..df7d999ba 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -3,9 +3,9 @@ // EmulationStation Desktop Edition // ThemeData.cpp // -// Finds available themes on the file system and loads these, -// including the parsing of individual theme components -// (includes, features, variables, views, elements). +// 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" @@ -21,16 +21,57 @@ #include #include -#define MINIMUM_THEME_FORMAT_VERSION 3 +#define MINIMUM_LEGACY_THEME_FORMAT_VERSION 3 -std::vector ThemeData::sSupportedViews {{"all"}, {"system"}, {"basic"}, - {"detailed"}, {"grid"}, {"video"}}; -std::vector ThemeData::sSupportedFeatures { - {"navigationsounds"}, {"video"}, {"carousel"}, {"z-index"}, {"visible"}}; +// clang-format off +std::vector ThemeData::sSupportedViews { + {"all"}, + {"system"}, + {"gamelist"}}; + +std::vector ThemeData::sLegacySupportedViews { + {"all"}, + {"system"}, + {"basic"}, + {"detailed"}, + {"grid"}, + {"video"}}; + +std::vector ThemeData::sLegacySupportedFeatures { + {"navigationsounds"}, + {"video"}, + {"carousel"}, + {"z-index"}, + {"visible"}}; + +std::vector ThemeData::sSupportedAspectRatios { + {"16:9"}, + {"16:9_vertical"}, + {"16:10"}, + {"16:10_vertical"}, + {"3:2"}, + {"3:2_vertical"}, + {"4:3"}, + {"4:3_vertical"}, + {"5:4"}, + {"5:4_vertical"}, + {"12:5"}, + {"43:18"}, + {"64:27"}}; + +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 // Line break. - {{"image", + ThemeData::sElementMap { + {"image", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, {"maxSize", NORMALIZED_PAIR}, @@ -40,120 +81,27 @@ std::map> {"path", PATH}, {"default", PATH}, {"tile", BOOLEAN}, + {"metadata", STRING}, {"color", COLOR}, {"colorEnd", COLOR}, {"gradientType", STRING}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, - {"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}}}, - {"text", + {"video", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, + {"maxSize", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, - {"text", STRING}, - {"metadata", STRING}, - {"backgroundColor", COLOR}, - {"fontPath", PATH}, - {"fontSize", FLOAT}, - {"color", COLOR}, - {"alignment", STRING}, - {"forceUppercase", BOOLEAN}, - {"lineSpacing", FLOAT}, - {"value", STRING}, - {"visible", BOOLEAN}, - {"zIndex", FLOAT}}}, - {"textlist", - {{"pos", NORMALIZED_PAIR}, - {"size", NORMALIZED_PAIR}, - {"origin", NORMALIZED_PAIR}, - {"selectorHeight", FLOAT}, - {"selectorOffsetY", FLOAT}, - {"selectorColor", COLOR}, - {"selectorColorEnd", COLOR}, - {"selectorGradientType", STRING}, - {"selectorImagePath", PATH}, - {"selectorImageTile", BOOLEAN}, - {"selectedColor", COLOR}, - {"primaryColor", COLOR}, - {"secondaryColor", COLOR}, - {"fontPath", PATH}, - {"fontSize", FLOAT}, - {"scrollHide", BOOLEAN}, - {"scrollSound", PATH}, // For backward compatibility with old themes. - {"alignment", STRING}, - {"horizontalMargin", FLOAT}, - {"forceUppercase", BOOLEAN}, - {"lineSpacing", FLOAT}, - {"zIndex", FLOAT}}}, - {"container", - {{"pos", NORMALIZED_PAIR}, - {"size", NORMALIZED_PAIR}, - {"origin", NORMALIZED_PAIR}, - {"text", STRING}, - {"metadata", STRING}, - {"visible", BOOLEAN}, - {"zIndex", FLOAT}}}, - {"ninepatch", - {{"pos", NORMALIZED_PAIR}, - {"size", NORMALIZED_PAIR}, {"path", PATH}, + {"default", PATH}, + {"imageMetadata", STRING}, + {"delay", FLOAT}, {"visible", BOOLEAN}, - {"zIndex", FLOAT}}}, - {"datetime", - {{"pos", NORMALIZED_PAIR}, - {"size", NORMALIZED_PAIR}, - {"origin", NORMALIZED_PAIR}, - {"rotation", FLOAT}, - {"rotationOrigin", NORMALIZED_PAIR}, - {"metadata", STRING}, - {"backgroundColor", COLOR}, - {"fontPath", PATH}, - {"fontSize", FLOAT}, - {"color", COLOR}, - {"alignment", STRING}, - {"forceUppercase", BOOLEAN}, - {"lineSpacing", FLOAT}, - {"value", STRING}, - {"format", STRING}, - {"displayRelative", BOOLEAN}, - {"visible", BOOLEAN}, - {"zIndex", FLOAT}}}, - {"rating", - {{"pos", NORMALIZED_PAIR}, - {"size", NORMALIZED_PAIR}, - {"origin", NORMALIZED_PAIR}, - {"rotation", FLOAT}, - {"rotationOrigin", NORMALIZED_PAIR}, - {"color", COLOR}, - {"filledPath", PATH}, - {"unfilledPath", PATH}, - {"visible", BOOLEAN}, - {"zIndex", FLOAT}}}, + {"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}, @@ -184,41 +132,65 @@ std::map> {"customControllerIcon", PATH}, {"visible", BOOLEAN}, {"zIndex", FLOAT}}}, - {"sound", {{"path", PATH}}}, - {"helpsystem", - {{"pos", NORMALIZED_PAIR}, - {"origin", NORMALIZED_PAIR}, - {"textColor", COLOR}, - {"textColorDimmed", COLOR}, - {"iconColor", COLOR}, - {"iconColorDimmed", COLOR}, - {"fontPath", PATH}, - {"fontSize", FLOAT}, - {"entrySpacing", FLOAT}, - {"iconTextSpacing", FLOAT}, - {"textStyle", STRING}, - {"customButtonIcon", PATH}}}, - {"navigationsounds", - {{"systembrowseSound", PATH}, - {"quicksysselectSound", PATH}, - {"selectSound", PATH}, - {"backSound", PATH}, - {"scrollSound", PATH}, - {"favoriteSound", PATH}, - {"launchSound", PATH}}}, - {"video", + {"text", {{"pos", NORMALIZED_PAIR}, {"size", NORMALIZED_PAIR}, - {"maxSize", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, {"rotation", FLOAT}, {"rotationOrigin", NORMALIZED_PAIR}, - {"default", PATH}, - {"delay", FLOAT}, + {"text", STRING}, + {"metadata", STRING}, + {"container", BOOLEAN}, + {"fontPath", PATH}, + {"fontSize", FLOAT}, + {"alignment", STRING}, + {"color", COLOR}, + {"backgroundColor", COLOR}, + {"forceUppercase", BOOLEAN}, + {"lineSpacing", FLOAT}, {"visible", BOOLEAN}, - {"zIndex", FLOAT}, - {"showSnapshotNoVideo", BOOLEAN}, - {"showSnapshotDelay", BOOLEAN}}}, + {"zIndex", FLOAT}}}, + {"datetime", + {{"pos", NORMALIZED_PAIR}, + {"size", NORMALIZED_PAIR}, + {"origin", NORMALIZED_PAIR}, + {"rotation", FLOAT}, + {"rotationOrigin", NORMALIZED_PAIR}, + {"metadata", STRING}, + {"fontPath", PATH}, + {"fontSize", FLOAT}, + {"alignment", STRING}, + {"color", COLOR}, + {"backgroundColor", COLOR}, + {"forceUppercase", BOOLEAN}, + {"lineSpacing", FLOAT}, + {"format", STRING}, + {"displayRelative", BOOLEAN}, + {"visible", BOOLEAN}, + {"zIndex", FLOAT}}}, + {"gamelistinfo", + {{"pos", NORMALIZED_PAIR}, + {"size", NORMALIZED_PAIR}, + {"origin", NORMALIZED_PAIR}, + {"rotation", FLOAT}, + {"rotationOrigin", NORMALIZED_PAIR}, + {"fontPath", PATH}, + {"fontSize", FLOAT}, + {"color", COLOR}, + {"backgroundColor", COLOR}, + {"alignment", STRING}, + {"visible", BOOLEAN}, + {"zIndex", FLOAT}}}, + {"rating", + {{"pos", NORMALIZED_PAIR}, + {"size", NORMALIZED_PAIR}, + {"origin", NORMALIZED_PAIR}, + {"rotation", FLOAT}, + {"rotationOrigin", NORMALIZED_PAIR}, + {"color", COLOR}, + {"filledPath", PATH}, + {"unfilledPath", PATH}, + {"zIndex", FLOAT}}}, {"carousel", {{"type", STRING}, {"size", NORMALIZED_PAIR}, @@ -234,10 +206,89 @@ std::map> {"logoAlignment", STRING}, {"maxLogoCount", FLOAT}, {"zIndex", FLOAT}, - {"legacyZIndexMode", STRING}}}}; + {"legacyZIndexMode", STRING}}}, + {"textlist", + {{"pos", NORMALIZED_PAIR}, + {"size", NORMALIZED_PAIR}, + {"origin", NORMALIZED_PAIR}, + {"selectorHeight", FLOAT}, + {"selectorOffsetY", FLOAT}, + {"selectorColor", COLOR}, + {"selectorColorEnd", COLOR}, + {"selectorGradientType", STRING}, + {"selectorImagePath", PATH}, + {"selectorImageTile", BOOLEAN}, + {"selectedColor", COLOR}, + {"primaryColor", COLOR}, + {"secondaryColor", COLOR}, + {"fontPath", PATH}, + {"fontSize", FLOAT}, + {"scrollHide", BOOLEAN}, + {"scrollSound", PATH}, // For backward compatibility with legacy themes. + {"alignment", STRING}, + {"horizontalMargin", FLOAT}, + {"forceUppercase", BOOLEAN}, + {"lineSpacing", FLOAT}, + {"zIndex", FLOAT}}}, + {"helpsystem", + {{"pos", NORMALIZED_PAIR}, + {"origin", NORMALIZED_PAIR}, + {"textColor", COLOR}, + {"textColorDimmed", COLOR}, + {"iconColor", COLOR}, + {"iconColorDimmed", COLOR}, + {"fontPath", PATH}, + {"fontSize", FLOAT}, + {"entrySpacing", FLOAT}, + {"iconTextSpacing", FLOAT}, + {"textStyle", STRING}, + {"customButtonIcon", PATH}}}, + {"sound", + {{"path", 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. + {"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() - : mVersion {0} // The version will be loaded from the theme set. + : mCurrentThemeSet {} + , mLegacyTheme {false} { } @@ -253,7 +304,6 @@ void ThemeData::loadFile(const std::map& sysDataMap, if (!Utils::FileSystem::exists(path)) throw error << "File does not exist"; - mVersion = 0; mViews.clear(); mVariables.clear(); @@ -272,20 +322,67 @@ void ThemeData::loadFile(const std::map& sysDataMap, if (!root) throw error << ": Missing tag"; - // Parse version. - mVersion = root.child("formatVersion").text().as_float(-404); - if (mVersion == -404) - throw error << ": tag missing"; + mCurrentThemeSet = mThemeSets.find(Settings::getInstance()->getString("ThemeSet")); + if (mCurrentThemeSet != mThemeSets.cend()) + mLegacyTheme = mCurrentThemeSet->second.capabilities.legacyTheme; - if (mVersion < MINIMUM_THEME_FORMAT_VERSION) - throw error << ": Defined format version " << mVersion - << " is less than the minimum supported version " - << MINIMUM_THEME_FORMAT_VERSION; + // 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(); + } + + if (mCurrentThemeSet->second.capabilities.aspectRatios.size() > 0) { + for (auto& aspectRatio : sSupportedAspectRatios) { + if (std::find(mCurrentThemeSet->second.capabilities.aspectRatios.cbegin(), + mCurrentThemeSet->second.capabilities.aspectRatios.cend(), + aspectRatio) != + mCurrentThemeSet->second.capabilities.aspectRatios.cend()) + mAspectRatios.emplace_back(aspectRatio); + } + + if (std::find(mAspectRatios.cbegin(), mAspectRatios.cend(), + Settings::getInstance()->getString("ThemeAspectRatio")) != + mAspectRatios.cend()) + mSelectedAspectRatio = Settings::getInstance()->getString("ThemeAspectRatio"); + else + mSelectedAspectRatio = mAspectRatios.front(); + } + } parseVariables(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) @@ -303,8 +400,8 @@ std::vector ThemeData::makeExtras(const std::shared_ptrmViews.cend()) return comps; - for (auto it = viewIt->second.orderedKeys.cbegin(); // Line break. - it != viewIt->second.orderedKeys.cend(); ++it) { + 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}; @@ -339,6 +436,7 @@ const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, 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) << " requested mismatched theme type for [" << view << "." << element << "] - expected \"" << expectedType << "\", got \"" << elemIt->second.type @@ -349,9 +447,12 @@ const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, return &elemIt->second; } -std::map ThemeData::getThemeSets() +std::map& ThemeData::getThemeSets() { - std::map sets; + if (!mThemeSets.empty()) + return mThemeSets; + + 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. @@ -380,33 +481,47 @@ std::map ThemeData::getThemeSets() for (Utils::FileSystem::StringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) { if (Utils::FileSystem::isDirectory(*it)) { - ThemeSet set = {*it}; - sets[set.getName()] = set; + LOG(LogDebug) << "Loading theme set capabilities for \"" << *it << "\"..."; + ThemeCapability capabilities {parseThemeCapabilities(*it)}; + + LOG(LogInfo) << "Added" << (capabilities.legacyTheme ? " legacy" : "") + << " theme set \"" << *it << "\""; + if (!capabilities.legacyTheme) { + LOG(LogDebug) << "Theme set includes support for " + << capabilities.variants.size() << " variant" + << (capabilities.variants.size() != 1 ? "s" : "") << " and " + << capabilities.aspectRatios.size() << " aspect ratio" + << (capabilities.aspectRatios.size() != 1 ? "s" : ""); + } + ThemeSet set {*it, capabilities}; + mThemeSets[set.getName()] = set; } } } - return sets; + return mThemeSets; } std::string ThemeData::getThemeFromCurrentSet(const std::string& system) { - std::map themeSets {ThemeData::getThemeSets()}; - if (themeSets.empty()) + if (mThemeSets.empty()) + getThemeSets(); + + if (mThemeSets.empty()) // No theme sets available. return ""; - std::map::const_iterator set = - themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if (set == themeSets.cend()) { + 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 // rbsimple-DE instead, and if that's also missing then pick the first available set. bool defaultSetFound {true}; - set = themeSets.find("rbsimple-DE"); + set = mThemeSets.find("rbsimple-DE"); - if (set == themeSets.cend()) { - set = themeSets.cbegin(); + if (set == mThemeSets.cend()) { + set = mThemeSets.cbegin(); defaultSetFound = false; } @@ -485,12 +600,175 @@ std::string ThemeData::resolvePlaceholders(const std::string& in) return prefix + mVariables[replace] + suffix; } +ThemeData::ThemeCapability ThemeData::parseThemeCapabilities(const std::string& path) +{ + ThemeCapability capabilities; + + 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 = + doc.load_file(Utils::String::stringToWideString(capFile).c_str()); +#else + 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; + } + 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")) { + std::string value = aspectRatio.text().get(); + if (std::find(sSupportedAspectRatios.cbegin(), sSupportedAspectRatios.cend(), value) == + sSupportedAspectRatios.cend()) { + LOG(LogWarning) << "Declared aspect ratio \"" << value + << "\" is not supported, ignoring entry in \"" << capFile << "\""; + } + else { + if (std::find(capabilities.aspectRatios.cbegin(), capabilities.aspectRatios.cend(), + value) != capabilities.aspectRatios.cend()) { + LOG(LogWarning) + << "Aspect ratio \"" << value + << "\" is declared multiple times, ignoring entry in \"" << capFile << "\""; + } + else { + capabilities.aspectRatios.emplace_back(value); + } + } + } + for (pugi::xml_node variant = themeCapabilities.child("variant"); variant; + variant = variant.next_sibling("variant")) { + ThemeVariant readVariant; + std::string name {variant.attribute("name").as_string()}; + if (name.empty()) { + LOG(LogWarning) + << "Found tag without name attribute, skipping entry in \"" << capFile + << "\""; + } + else { + readVariant.name = name; + } + + pugi::xml_node labelTag {variant.child("label")}; + if (labelTag == nullptr) { + LOG(LogDebug) + << "No variant