From cc8123f5a6092b95745474a50c98eca4ae18c8da Mon Sep 17 00:00:00 2001 From: Leon Styhre <leon@leonstyhre.com> Date: Sun, 13 Feb 2022 20:03:34 +0100 Subject: [PATCH] Added a GameSelectorComponent for displaying game media in SystemView. --- es-app/src/CollectionSystemsManager.cpp | 4 + es-app/src/FileData.cpp | 38 +++++- es-app/src/FileData.h | 14 +- es-core/CMakeLists.txt | 1 + es-core/src/ThemeData.cpp | 3 + .../src/components/GameSelectorComponent.h | 124 ++++++++++++++++++ 6 files changed, 180 insertions(+), 4 deletions(-) create mode 100644 es-core/src/components/GameSelectorComponent.h diff --git a/es-app/src/CollectionSystemsManager.cpp b/es-app/src/CollectionSystemsManager.cpp index a5f1bb3ba..16fd70a29 100644 --- a/es-app/src/CollectionSystemsManager.cpp +++ b/es-app/src/CollectionSystemsManager.cpp @@ -1443,6 +1443,10 @@ void CollectionSystemsManager::trimCollectionCount(FileData* rootFolder, int lim (CollectionFileData*)rootFolder->getChildrenListToDisplay().back(); ViewController::getInstance()->getGamelistView(curSys).get()->remove(gameToRemove, false); } + // Also update the lists of last played and most played games as these could otherwise + // contain dangling pointers. + rootFolder->updateLastPlayedList(); + rootFolder->updateMostPlayedList(); } const bool CollectionSystemsManager::themeFolderExists(const std::string& folder) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index b68258d94..f811de823 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -39,6 +39,8 @@ FileData::FileData(FileType type, , mEnvData {envData} , mSystem {system} , mOnlyFolders {false} + , mUpdateChildrenLastPlayed {false} + , mUpdateChildrenMostPlayed {false} , mDeletionFlag {false} { // Metadata needs at least a name field (since that's what getName() will return). @@ -736,6 +738,9 @@ void FileData::sort(const SortType& type, bool mFavoritesOnTop) sortFavoritesOnTop(*type.comparisonFunction, mGameCount); else sort(*type.comparisonFunction, mGameCount); + + updateLastPlayedList(); + updateMostPlayedList(); } void FileData::countGames(std::pair<unsigned int, unsigned int>& gameCount) @@ -743,9 +748,6 @@ void FileData::countGames(std::pair<unsigned int, unsigned int>& gameCount) bool isKidMode = (Settings::getInstance()->getString("UIMode") == "kid" || Settings::getInstance()->getBool("ForceKid")); - (Settings::getInstance()->getString("UIMode") == "kid" || - Settings::getInstance()->getBool("ForceKid")); - for (unsigned int i = 0; i < mChildren.size(); ++i) { if (mChildren[i]->getType() == GAME && mChildren[i]->getCountAsGame()) { if (!isKidMode || (isKidMode && mChildren[i]->getKidgame())) { @@ -761,6 +763,36 @@ void FileData::countGames(std::pair<unsigned int, unsigned int>& gameCount) mGameCount = gameCount; } +void FileData::updateLastPlayedList() +{ + if (!mUpdateChildrenLastPlayed) + return; + + mChildrenLastPlayed.clear(); + mChildrenLastPlayed = getChildrenRecursive(); + + std::stable_sort(mChildrenLastPlayed.begin(), mChildrenLastPlayed.end()); + std::sort(std::begin(mChildrenLastPlayed), std::end(mChildrenLastPlayed), + [](FileData* a, FileData* b) { + return a->metadata.get("lastplayed") > b->metadata.get("lastplayed"); + }); +} + +void FileData::updateMostPlayedList() +{ + if (!mUpdateChildrenMostPlayed) + return; + + mChildrenMostPlayed.clear(); + mChildrenMostPlayed = getChildrenRecursive(); + + std::stable_sort(mChildrenMostPlayed.begin(), mChildrenMostPlayed.end()); + std::sort(std::begin(mChildrenMostPlayed), std::end(mChildrenMostPlayed), + [](FileData* a, FileData* b) { + return a->metadata.getInt("playcount") > b->metadata.getInt("playcount"); + }); +} + const FileData::SortType& FileData::getSortTypeFromString(const std::string& desc) const { std::vector<FileData::SortType> SortTypes = FileSorts::SortTypes; diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 73332be6f..b40659000 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -56,6 +56,13 @@ public: const std::vector<FileData*>& getChildren() const { return mChildren; } SystemData* getSystem() const { return mSystem; } SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } + + // These functions are used by GameSelectorComponent. + const std::vector<FileData*>& getChildrenLastPlayed() const { return mChildrenLastPlayed; } + const std::vector<FileData*>& getChildrenMostPlayed() const { return mChildrenMostPlayed; } + void setUpdateChildrenLastPlayed(bool state) { mUpdateChildrenLastPlayed = state; } + void setUpdateChildrenMostPlayed(bool state) { mUpdateChildrenMostPlayed = state; } + const bool getOnlyFoldersFlag() const { return mOnlyFolders; } const bool getHasFoldersFlag() const { return mHasFolders; } static const std::string getROMDirectory(); @@ -127,7 +134,8 @@ public: MetaDataList metadata; // Only count the games, a cheaper alternative to a full sort when that is not required. void countGames(std::pair<unsigned int, unsigned int>& gameCount); - + void updateLastPlayedList(); + void updateMostPlayedList(); void setSortTypeString(std::string typestring) { mSortTypeString = typestring; } const std::string& getSortTypeString() const { return mSortTypeString; } const FileData::SortType& getSortTypeFromString(const std::string& desc) const; @@ -146,10 +154,14 @@ private: std::unordered_map<std::string, FileData*> mChildrenByFilename; std::vector<FileData*> mChildren; std::vector<FileData*> mFilteredChildren; + std::vector<FileData*> mChildrenLastPlayed; + std::vector<FileData*> mChildrenMostPlayed; // The pair includes all games, and favorite games. std::pair<unsigned int, unsigned int> mGameCount; bool mOnlyFolders; bool mHasFolders; + bool mUpdateChildrenLastPlayed; + bool mUpdateChildrenMostPlayed; // Used for flagging a game for deletion from its gamelist.xml file. bool mDeletionFlag; }; diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index ddbde8dc3..779cd75f2 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -42,6 +42,7 @@ set(CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/components/DateTimeComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/DateTimeEditComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/FlexboxComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GameSelectorComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GridTileComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/HelpComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/IList.h diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 2ead841c9..d383deb92 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -279,6 +279,9 @@ std::map<std::string, std::map<std::string, ThemeData::ElementPropertyType>> {"forceUppercase", BOOLEAN}, // For backward compatibility with legacy themes. {"lineSpacing", FLOAT}, {"zIndex", FLOAT}}}, + {"gameselector", + {{"selection", STRING}, + {"count", UNSIGNED_INTEGER}}}, {"helpsystem", {{"pos", NORMALIZED_PAIR}, {"origin", NORMALIZED_PAIR}, diff --git a/es-core/src/components/GameSelectorComponent.h b/es-core/src/components/GameSelectorComponent.h new file mode 100644 index 000000000..26f72041a --- /dev/null +++ b/es-core/src/components/GameSelectorComponent.h @@ -0,0 +1,124 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GameSelectorComponent.h +// +// Makes a selection of games based on theme-controlled criteria. +// + +#ifndef ES_CORE_COMPONENTS_GAME_SELECTOR_COMPONENT_H +#define ES_CORE_COMPONENTS_GAME_SELECTOR_COMPONENT_H + +#include "GuiComponent.h" +#include "Log.h" +#include "ThemeData.h" + +class GameSelectorComponent : public GuiComponent +{ +public: + GameSelectorComponent(SystemData* system) + : mSystem {system} + , mGameSelection {GameSelection::RANDOM} + , mGameCount {1} + { + } + + const std::vector<FileData*>& getGames() const { return mGames; } + + void refreshGames() + { + mGames.clear(); + + bool isKidMode {(Settings::getInstance()->getString("UIMode") == "kid" || + Settings::getInstance()->getBool("ForceKid"))}; + + if (mGameSelection == GameSelection::RANDOM) { + for (int i = 0; i < mGameCount; ++i) { + FileData* randomGame {mSystem->getRandomGame()}; + if (randomGame != nullptr) + mGames.emplace_back(randomGame); + } + } + else if (mGameSelection == GameSelection::LAST_PLAYED) { + for (auto& child : mSystem->getRootFolder()->getChildrenLastPlayed()) { + if (child->getType() != GAME) + continue; + if (!child->getCountAsGame()) + continue; + if (isKidMode && !child->getKidgame()) + continue; + if (child->metadata.get("lastplayed") == "0") + continue; + mGames.emplace_back(child); + if (static_cast<int>(mGames.size()) == mGameCount) + break; + } + } + else if (mGameSelection == GameSelection::MOST_PLAYED) { + for (auto& child : mSystem->getRootFolder()->getChildrenMostPlayed()) { + if (child->getType() != GAME) + continue; + if (!child->getCountAsGame()) + continue; + if (isKidMode && !child->getKidgame()) + continue; + if (child->metadata.get("playcount") == "0") + continue; + mGames.emplace_back(child); + if (static_cast<int>(mGames.size()) == mGameCount) + break; + } + } + } + + void applyTheme(const std::shared_ptr<ThemeData>& theme, + const std::string& view, + const std::string& element, + unsigned int properties) + { + const ThemeData::ThemeElement* elem {theme->getElement(view, element, "gameselector")}; + if (!elem) + return; + + if (elem->has("selection")) { + const std::string selection {elem->get<std::string>("selection")}; + if (selection == "random") { + mGameSelection = GameSelection::RANDOM; + } + else if (selection == "lastplayed") { + mGameSelection = GameSelection::LAST_PLAYED; + mSystem->getRootFolder()->setUpdateChildrenLastPlayed(true); + mSystem->getRootFolder()->updateLastPlayedList(); + } + else if (selection == "mostplayed") { + mGameSelection = GameSelection::MOST_PLAYED; + mSystem->getRootFolder()->setUpdateChildrenMostPlayed(true); + mSystem->getRootFolder()->updateMostPlayedList(); + } + else { + mGameSelection = GameSelection::RANDOM; + LOG(LogWarning) << "GameSelectorComponent: Invalid theme configuration, property " + "<selection> set to \"" + << selection << "\""; + } + } + + if (elem->has("count")) + mGameCount = glm::clamp(static_cast<int>(elem->get<unsigned int>("count")), 1, 30); + } + +private: + enum class GameSelection { + RANDOM, // Replace with AllowShortEnumsOnASingleLine: false (clang-format >=11.0). + LAST_PLAYED, + MOST_PLAYED + }; + + SystemData* mSystem; + std::vector<FileData*> mGames; + + GameSelection mGameSelection; + int mGameCount; +}; + +#endif // ES_CORE_COMPONENTS_GAME_SELECTOR_COMPONENT_H