From ec0a7ad2f14f1ad23f6e055659237bef89659a83 Mon Sep 17 00:00:00 2001 From: Leon Styhre Date: Tue, 18 Jan 2022 17:14:17 +0100 Subject: [PATCH] Added the preliminary GamelistBase and GamelistView classes. --- es-app/CMakeLists.txt | 4 + es-app/src/views/GamelistBase.cpp | 795 ++++++++++++++++++++++++++++++ es-app/src/views/GamelistBase.h | 101 ++++ es-app/src/views/GamelistView.cpp | 558 +++++++++++++++++++++ es-app/src/views/GamelistView.h | 91 ++++ 5 files changed, 1549 insertions(+) create mode 100644 es-app/src/views/GamelistBase.cpp create mode 100644 es-app/src/views/GamelistBase.h create mode 100644 es-app/src/views/GamelistView.cpp create mode 100644 es-app/src/views/GamelistView.h diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 56a0e0c03..a976736d6 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -55,6 +55,8 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/IGamelistView.h ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/ISimpleGamelistView.h ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/VideoGamelistView.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/views/GamelistBase.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/views/GamelistView.h ${CMAKE_CURRENT_SOURCE_DIR}/src/views/SystemView.h ${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.h ) @@ -105,6 +107,8 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/IGamelistView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/ISimpleGamelistView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/VideoGamelistView.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/views/GamelistBase.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/views/GamelistView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/views/SystemView.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.cpp ) diff --git a/es-app/src/views/GamelistBase.cpp b/es-app/src/views/GamelistBase.cpp new file mode 100644 index 000000000..07ac1a555 --- /dev/null +++ b/es-app/src/views/GamelistBase.cpp @@ -0,0 +1,795 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GamelistBase.cpp +// +// Gamelist base class with utility functions and other low-level logic. +// + +#include "views/GamelistBase.h" + +#include "CollectionSystemsManager.h" +#include "FileFilterIndex.h" +#include "UIModeController.h" +#include "guis/GuiGamelistOptions.h" +#include "views/ViewController.h" + +GamelistBase::GamelistBase(Window* window, FileData* root) + : GuiComponent {window} + , mRoot {root} + , mList {window} + , mRandomGame {nullptr} + , mLastUpdated(nullptr) +{ +} + +GamelistBase::~GamelistBase() +{ + // +} + +void GamelistBase::setCursor(FileData* cursor) +{ + if (!mList.setCursor(cursor) && (!cursor->isPlaceHolder())) { + populateList(cursor->getParent()->getChildrenListToDisplay(), cursor->getParent()); + mList.setCursor(cursor); + + // Update our cursor stack in case our cursor just got set to some folder + // we weren't in before. + if (mCursorStack.empty() || mCursorStack.top() != cursor->getParent()) { + std::stack tmp; + FileData* ptr = cursor->getParent(); + + while (ptr && ptr != mRoot) { + tmp.push(ptr); + ptr = ptr->getParent(); + } + + // Flip the stack and put it in mCursorStack. + mCursorStack = std::stack(); + while (!tmp.empty()) { + mCursorStack.push(tmp.top()); + tmp.pop(); + } + } + } +} + +bool GamelistBase::input(InputConfig* config, Input input) +{ + if (input.value != 0) { + if (config->isMappedTo("a", input)) { + FileData* cursor = getCursor(); + if (cursor->getType() == GAME) { + onPauseVideo(); + ViewController::getInstance()->cancelViewTransitions(); + stopListScrolling(); + launch(cursor); + } + else { + // It's a folder. + if (cursor->getChildren().size() > 0) { + ViewController::getInstance()->cancelViewTransitions(); + NavigationSounds::getInstance().playThemeNavigationSound(SELECTSOUND); + mCursorStack.push(cursor); + populateList(cursor->getChildrenListToDisplay(), cursor); + + FileData* newCursor = nullptr; + std::vector listEntries = cursor->getChildrenListToDisplay(); + // Check if there is an entry in the cursor stack history matching any entry + // in the currect folder. If so, select that entry. + for (auto it = mCursorStackHistory.begin(); // Line break. + it != mCursorStackHistory.end(); ++it) { + if (std::find(listEntries.begin(), listEntries.end(), *it) != + listEntries.end()) { + newCursor = *it; + mCursorStackHistory.erase(it); + break; + } + } + + // If there was no match in the cursor history, simply select the first entry. + if (!newCursor) + newCursor = getCursor(); + setCursor(newCursor); + if (mRoot->getSystem()->getThemeFolder() == "custom-collections") + updateHelpPrompts(); + } + else { + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + } + } + + return true; + } + else if (config->isMappedTo("b", input)) { + ViewController::getInstance()->cancelViewTransitions(); + if (mCursorStack.size()) { + // Save the position to the cursor stack history. + mCursorStackHistory.push_back(getCursor()); + NavigationSounds::getInstance().playThemeNavigationSound(BACKSOUND); + populateList(mCursorStack.top()->getParent()->getChildrenListToDisplay(), + mCursorStack.top()->getParent()); + setCursor(mCursorStack.top()); + if (mCursorStack.size() > 0) + mCursorStack.pop(); + if (mRoot->getSystem()->getThemeFolder() == "custom-collections") + updateHelpPrompts(); + } + else { + NavigationSounds::getInstance().playThemeNavigationSound(BACKSOUND); + onPauseVideo(); + onFocusLost(); + stopListScrolling(); + SystemData* systemToView = getCursor()->getSystem(); + if (systemToView->isCustomCollection() && + systemToView->getRootFolder()->getParent()) + ViewController::getInstance()->goToSystemView( + systemToView->getRootFolder()->getParent()->getSystem(), true); + else + ViewController::getInstance()->goToSystemView(systemToView, true); + } + + return true; + } + else if (config->isMappedTo("x", input)) { + if (getCursor()->getType() == PLACEHOLDER) { + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + return true; + } + else if (config->isMappedTo("x", input) && + mRoot->getSystem()->getThemeFolder() == "custom-collections" && + mCursorStack.empty() && + ViewController::getInstance()->getState().viewing == + ViewController::GAMELIST) { + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + // Jump to the randomly selected game. + if (mRandomGame) { + stopListScrolling(); + ViewController::getInstance()->cancelViewTransitions(); + mWindow->startMediaViewer(mRandomGame); + return true; + } + } + else if (mRoot->getSystem()->isGameSystem()) { + stopListScrolling(); + ViewController::getInstance()->cancelViewTransitions(); + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + mWindow->startMediaViewer(getCursor()); + return true; + } + } + else if (config->isMappedLike(getQuickSystemSelectRightButton(), input)) { + if (Settings::getInstance()->getBool("QuickSystemSelect") && + SystemData::sSystemVector.size() > 1) { + onPauseVideo(); + onFocusLost(); + stopListScrolling(); + ViewController::getInstance()->goToNextGamelist(); + return true; + } + } + else if (config->isMappedLike(getQuickSystemSelectLeftButton(), input)) { + if (Settings::getInstance()->getBool("QuickSystemSelect") && + SystemData::sSystemVector.size() > 1) { + onPauseVideo(); + onFocusLost(); + stopListScrolling(); + ViewController::getInstance()->goToPrevGamelist(); + return true; + } + } + else if (Settings::getInstance()->getBool("RandomAddButton") && + (config->isMappedTo("leftthumbstickclick", input) || + config->isMappedTo("rightthumbstickclick", input))) { + if (mRoot->getSystem()->isGameSystem() && getCursor()->getType() != PLACEHOLDER) { + stopListScrolling(); + // Jump to a random game. + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + FileData* randomGame = getCursor()->getSystem()->getRandomGame(getCursor()); + if (randomGame) + setCursor(randomGame); + return true; + } + } + else if (config->isMappedTo("y", input) && + mRoot->getSystem()->getThemeFolder() == "custom-collections" && + !CollectionSystemsManager::getInstance()->isEditing() && mCursorStack.empty() && + ViewController::getInstance()->getState().viewing == ViewController::GAMELIST) { + // Jump to the randomly selected game. + if (mRandomGame) { + NavigationSounds::getInstance().playThemeNavigationSound(SELECTSOUND); + // If there is already an mCursorStackHistory entry for the collection, then + // remove it so we don't get multiple entries. + std::vector listEntries = + mRandomGame->getSystem()->getRootFolder()->getChildrenListToDisplay(); + for (auto it = mCursorStackHistory.begin(); it != mCursorStackHistory.end(); ++it) { + if (std::find(listEntries.begin(), listEntries.end(), *it) != + listEntries.end()) { + mCursorStackHistory.erase(it); + break; + } + } + setCursor(mRandomGame); + updateHelpPrompts(); + } + else { + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + } + } + else if (config->isMappedTo("y", input) && + !Settings::getInstance()->getBool("FavoritesAddButton") && + !CollectionSystemsManager::getInstance()->isEditing()) { + return true; + } + else if (config->isMappedTo("y", input) && + !UIModeController::getInstance()->isUIModeKid() && + !UIModeController::getInstance()->isUIModeKiosk()) { + // Notify the user if attempting to add a custom collection to a custom collection. + if (CollectionSystemsManager::getInstance()->isEditing() && + mRoot->getSystem()->isGameSystem() && getCursor()->getType() != PLACEHOLDER && + getCursor()->getParent()->getPath() == "collections") { + NavigationSounds::getInstance().playThemeNavigationSound(FAVORITESOUND); + mWindow->queueInfoPopup("CAN'T ADD CUSTOM COLLECTIONS TO CUSTOM COLLECTIONS", 4000); + } + // Notify the user if attempting to add a placeholder to a custom collection. + if (CollectionSystemsManager::getInstance()->isEditing() && + mRoot->getSystem()->isGameSystem() && getCursor()->getType() == PLACEHOLDER) { + NavigationSounds::getInstance().playThemeNavigationSound(FAVORITESOUND); + mWindow->queueInfoPopup("CAN'T ADD PLACEHOLDERS TO CUSTOM COLLECTIONS", 4000); + } + else if (mRoot->getSystem()->isGameSystem() && getCursor()->getType() != PLACEHOLDER && + getCursor()->getParent()->getPath() != "collections") { + if (getCursor()->getType() == GAME || getCursor()->getType() == FOLDER) + NavigationSounds::getInstance().playThemeNavigationSound(FAVORITESOUND); + // When marking or unmarking a game as favorite, don't jump to the new position + // it gets after the gamelist sorting. Instead retain the cursor position in the + // list using the logic below. + FileData* entryToUpdate = getCursor(); + SystemData* system = getCursor()->getSystem(); + bool favoritesSorting; + bool removedLastFavorite = false; + bool selectLastEntry = false; + bool isEditing = CollectionSystemsManager::getInstance()->isEditing(); + bool foldersOnTop = Settings::getInstance()->getBool("FoldersOnTop"); + // If the current list only contains folders, then treat it as if the folders + // are not sorted on top, this way the logic should work exactly as for mixed + // lists or files-only lists. + if (getCursor()->getType() == FOLDER && foldersOnTop == true) + foldersOnTop = !getCursor()->getParent()->getOnlyFoldersFlag(); + + if (mRoot->getSystem()->isCustomCollection() || + mRoot->getSystem()->getThemeFolder() == "custom-collections") + favoritesSorting = Settings::getInstance()->getBool("FavFirstCustom"); + else + favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst"); + + if (favoritesSorting && mRoot->getSystem()->getName() != "recent" && !isEditing) { + FileData* entryToSelect; + // Add favorite flag. + if (!getCursor()->getFavorite()) { + // If it's a folder and folders are sorted on top, select the current entry. + if (foldersOnTop && getCursor()->getType() == FOLDER) { + entryToSelect = getCursor(); + } + // If it's the first entry to be marked as favorite, select the next entry. + else if (getCursor() == getFirstEntry()) { + entryToSelect = getNextEntry(); + } + else if (getCursor() == getLastEntry() && + getPreviousEntry()->getFavorite()) { + entryToSelect = getLastEntry(); + selectLastEntry = true; + } + // If we are on the favorite marking boundary, select the next entry. + else if (getCursor()->getFavorite() != getPreviousEntry()->getFavorite()) { + entryToSelect = getNextEntry(); + } + // If we mark the second entry as favorite and the first entry is not a + // favorite, then select this entry if they are of the same type. + else if (getPreviousEntry() == getFirstEntry() && + getCursor()->getType() == getPreviousEntry()->getType()) { + entryToSelect = getPreviousEntry(); + } + // For all other scenarios try to select the next entry, and if it doesn't + // exist, select the previous entry. + else { + entryToSelect = + getCursor() != getNextEntry() ? getNextEntry() : getPreviousEntry(); + } + } + // Remove favorite flag. + else { + // If it's a folder and folders are sorted on top, select the current entry. + if (foldersOnTop && getCursor()->getType() == FOLDER) { + entryToSelect = getCursor(); + } + // If it's the last entry, select the previous entry. + else if (getCursor() == getLastEntry()) { + entryToSelect = getPreviousEntry(); + } + // If we are on the favorite marking boundary, select the previous entry, + // unless folders are sorted on top and the previous entry is a folder. + else if (foldersOnTop && + getCursor()->getFavorite() != getNextEntry()->getFavorite()) { + entryToSelect = getPreviousEntry()->getType() == FOLDER ? + getCursor() : + getPreviousEntry(); + } + // If we are on the favorite marking boundary, select the previous entry. + else if (getCursor()->getFavorite() != getNextEntry()->getFavorite()) { + entryToSelect = getPreviousEntry(); + } + // For all other scenarios try to select the next entry, and if it doesn't + // exist, select the previous entry. + else { + entryToSelect = + getCursor() != getNextEntry() ? getNextEntry() : getPreviousEntry(); + } + + // If we removed the last favorite marking, set the flag to jump to the + // first list entry after the sorting has been performed. + if (foldersOnTop && getCursor() == getFirstGameEntry() && + !getNextEntry()->getFavorite()) + removedLastFavorite = true; + else if (getCursor() == getFirstEntry() && !getNextEntry()->getFavorite()) + removedLastFavorite = true; + } + + setCursor(entryToSelect); + system = entryToUpdate->getSystem(); + } + + // Marking folders as favorites don't make them part of any collections, + // so it makes more sense to handle it here than to add the function to + // CollectionSystemsManager. + if (entryToUpdate->getType() == FOLDER) { + if (isEditing) { + mWindow->queueInfoPopup("CAN'T ADD FOLDERS TO CUSTOM COLLECTIONS", 4000); + } + else { + MetaDataList* md = &entryToUpdate->getSourceFileData()->metadata; + if (md->get("favorite") == "false") { + md->set("favorite", "true"); + mWindow->queueInfoPopup( + "MARKED FOLDER '" + + Utils::String::toUpper(Utils::String::removeParenthesis( + entryToUpdate->getName())) + + "' AS FAVORITE", + 4000); + } + else { + md->set("favorite", "false"); + mWindow->queueInfoPopup( + "REMOVED FAVORITE MARKING FOR FOLDER '" + + Utils::String::toUpper(Utils::String::removeParenthesis( + entryToUpdate->getName())) + + "'", + 4000); + } + } + + entryToUpdate->getSourceFileData()->getSystem()->onMetaDataSavePoint(); + + getCursor()->getParent()->sort( + mRoot->getSortTypeFromString(mRoot->getSortTypeString()), + Settings::getInstance()->getBool("FavoritesFirst")); + + ViewController::getInstance()->onFileChanged(getCursor(), false); + + // Always jump to the first entry in the gamelist if the last favorite + // was unmarked. We couldn't do this earlier as we didn't have the list + // sorted yet. + if (removedLastFavorite) { + // TEMPORARY + // ViewController::getInstance() + // ->getGamelistView(entryToUpdate->getSystem()) + // ->setCursor(ViewController::getInstance() + // ->getGamelistView(entryToUpdate->getSystem()) + // ->getFirstEntry()); + } + return true; + } + else if (isEditing && entryToUpdate->metadata.get("nogamecount") == "true") { + mWindow->queueInfoPopup("CAN'T ADD ENTRIES THAT ARE NOT COUNTED " + "AS GAMES TO CUSTOM COLLECTIONS", + 4000); + } + else if (CollectionSystemsManager::getInstance()->toggleGameInCollection( + entryToUpdate)) { + // As the toggling of the game destroyed this object, we need to get the view + // from ViewController instead of using the reference that existed before the + // destruction. Otherwise we get random crashes. + // TEMPORARY + // IGamelistView* view = + // ViewController::getInstance()->getGamelistView(system).get(); + // Jump to the first entry in the gamelist if the last favorite was unmarked. + if (foldersOnTop && removedLastFavorite && + !entryToUpdate->getSystem()->isCustomCollection()) { + // TEMPORARY + // ViewController::getInstance() + // ->getGamelistView(entryToUpdate->getSystem()) + // ->setCursor(ViewController::getInstance() + // ->getGamelistView(entryToUpdate->getSystem()) + // ->getFirstGameEntry()); + } + else if (removedLastFavorite && + !entryToUpdate->getSystem()->isCustomCollection()) { + setCursor(getFirstEntry()); + // view->setCursor(view->getFirstEntry()); + } + else if (selectLastEntry) { + setCursor(getLastEntry()); + // view->setCursor(view->getLastEntry()); + } + // Display the indication icons which show what games are part of the + // custom collection currently being edited. This is done cheaply using + // onFileChanged() which will trigger populateList(). + if (isEditing) { + for (auto it = SystemData::sSystemVector.begin(); + it != SystemData::sSystemVector.end(); ++it) { + // TEMPORARY + // ViewController::getInstance()->getGamelistView((*it))->onFileChanged( + // ViewController::getInstance()->getGamelistView((*it))->getCursor(), + // false); + } + } + return true; + } + } + else if (config->isMappedTo("y", input) && getCursor()->isPlaceHolder()) { + NavigationSounds::getInstance().playThemeNavigationSound(SCROLLSOUND); + } + } + } + + // return IGamelistView::input(config, input); + // Select button opens GuiGamelistOptions. + if (!UIModeController::getInstance()->isUIModeKid() && // Line break. + config->isMappedTo("back", input) && input.value) { + ViewController::getInstance()->cancelViewTransitions(); + stopListScrolling(); + mWindow->pushGui(new GuiGamelistOptions(mWindow, this->mRoot->getSystem())); + return true; + } + + // Ctrl-R reloads the view when debugging. + else if (Settings::getInstance()->getBool("Debug") && + config->getDeviceId() == DEVICE_KEYBOARD && + (SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL)) && input.id == SDLK_r && + input.value != 0) { + LOG(LogDebug) << "IGamelistView::input(): Reloading view"; + // TEMPORARY + // ViewController::getInstance()->reloadGamelistView(this, true); + return true; + } + + return GuiComponent::input(config, input); +} + +void GamelistBase::populateList(const std::vector& files, FileData* firstEntry) +{ + mFirstGameEntry = nullptr; + bool favoriteStar {true}; + bool isEditing {false}; + std::string editingCollection; + std::string inCollectionPrefix; + + if (CollectionSystemsManager::getInstance()->isEditing()) { + editingCollection = CollectionSystemsManager::getInstance()->getEditingCollection(); + isEditing = true; + } + + // Read the settings that control whether a unicode star character should be added + // as a prefix to the game name. + if (files.size() > 0) { + if (files.front()->getSystem()->isCustomCollection()) + favoriteStar = Settings::getInstance()->getBool("FavStarCustom"); + else + favoriteStar = Settings::getInstance()->getBool("FavoritesStar"); + } + + mList.clear(); + + if (files.size() > 0) { + for (auto it = files.cbegin(); it != files.cend(); ++it) { + if (!mFirstGameEntry && (*it)->getType() == GAME) + mFirstGameEntry = (*it); + // Add a leading tick mark icon to the game name if it's part of the custom collection + // currently being edited. + if (isEditing && (*it)->getType() == GAME) { + if (CollectionSystemsManager::getInstance()->inCustomCollection(editingCollection, + (*it))) { + if (Settings::getInstance()->getBool("SpecialCharsASCII")) + inCollectionPrefix = "! "; + else + inCollectionPrefix = ViewController::TICKMARK_CHAR + " "; + } + else { + inCollectionPrefix = ""; + } + } + + if ((*it)->getFavorite() && favoriteStar && + mRoot->getSystem()->getName() != "favorites") { + if (Settings::getInstance()->getBool("SpecialCharsASCII")) + mList.add(inCollectionPrefix + "* " + (*it)->getName(), *it, + ((*it)->getType() == FOLDER)); + else + mList.add(inCollectionPrefix + ViewController::FAVORITE_CHAR + " " + + (*it)->getName(), + *it, ((*it)->getType() == FOLDER)); + } + else if ((*it)->getType() == FOLDER && mRoot->getSystem()->getName() != "collections") { + if (Settings::getInstance()->getBool("SpecialCharsASCII")) + mList.add("# " + (*it)->getName(), *it, true); + else + mList.add(ViewController::FOLDER_CHAR + " " + (*it)->getName(), *it, true); + } + else { + mList.add(inCollectionPrefix + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + } + } + } + else { + addPlaceholder(firstEntry); + } + + generateGamelistInfo(getCursor(), firstEntry); + generateFirstLetterIndex(files); +} + +void GamelistBase::addPlaceholder(FileData* firstEntry) +{ + // Empty list, add a placeholder. + FileData* placeholder; + + if (firstEntry && firstEntry->getSystem()->isGroupedCustomCollection()) + placeholder = firstEntry->getSystem()->getPlaceholder(); + else + placeholder = this->mRoot->getSystem()->getPlaceholder(); + + mList.add(placeholder->getName(), placeholder, (placeholder->getType() == PLACEHOLDER)); +} + +void GamelistBase::generateFirstLetterIndex(const std::vector& files) +{ + std::string firstChar; + + bool onlyFavorites {true}; + bool onlyFolders {true}; + bool hasFavorites {false}; + bool hasFolders {false}; + bool favoritesSorting {false}; + + mFirstLetterIndex.clear(); + + if (files.size() > 0 && files.front()->getSystem()->isCustomCollection()) + favoritesSorting = Settings::getInstance()->getBool("FavFirstCustom"); + else + favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst"); + + bool foldersOnTop {Settings::getInstance()->getBool("FoldersOnTop")}; + + // Find out if there are only favorites and/or only folders in the list. + for (auto it = files.begin(); it != files.end(); ++it) { + if (!((*it)->getFavorite())) + onlyFavorites = false; + if (!((*it)->getType() == FOLDER)) + onlyFolders = false; + } + + // Build the index. + for (auto it = files.begin(); it != files.end(); ++it) { + if ((*it)->getType() == FOLDER && (*it)->getFavorite() && favoritesSorting && + !onlyFavorites) { + hasFavorites = true; + } + else if ((*it)->getType() == FOLDER && foldersOnTop && !onlyFolders) { + hasFolders = true; + } + else if ((*it)->getType() == GAME && (*it)->getFavorite() && favoritesSorting && + !onlyFavorites) { + hasFavorites = true; + } + else { + mFirstLetterIndex.push_back(Utils::String::getFirstCharacter((*it)->getSortName())); + } + } + + // Sort and make each entry unique. + std::sort(mFirstLetterIndex.begin(), mFirstLetterIndex.end()); + auto last = std::unique(mFirstLetterIndex.begin(), mFirstLetterIndex.end()); + mFirstLetterIndex.erase(last, mFirstLetterIndex.end()); + + // If there are any favorites and/or folders in the list, insert their respective + // Unicode characters at the beginning of the vector. + if (hasFavorites) + mFirstLetterIndex.insert(mFirstLetterIndex.begin(), ViewController::FAVORITE_CHAR); + + if (hasFolders) + mFirstLetterIndex.insert(mFirstLetterIndex.begin(), ViewController::FOLDER_CHAR); +} + +void GamelistBase::generateGamelistInfo(FileData* cursor, FileData* firstEntry) +{ + // Generate data needed for the gamelistInfo field, which is displayed from the + // gamelist interfaces (Detailed/Video/Grid). + mIsFiltered = false; + mIsFolder = false; + FileData* rootFolder {firstEntry->getSystem()->getRootFolder()}; + + std::pair gameCount; + FileFilterIndex* idx {rootFolder->getSystem()->getIndex()}; + + // For the 'recent' collection we need to recount the games as the collection was + // trimmed down to 50 items. If we don't do this, the game count will not be correct + // as it would include all the games prior to trimming. + if (mRoot->getPath() == "recent") + mRoot->countGames(gameCount); + + gameCount = rootFolder->getGameCount(); + + mGameCount = gameCount.first; + mFavoritesGameCount = gameCount.second; + mFilteredGameCount = 0; + mFilteredGameCountAll = 0; + + if (idx->isFiltered()) { + mIsFiltered = true; + mFilteredGameCount = + static_cast(rootFolder->getFilesRecursive(GAME, true, false).size()); + // Also count the games that are set to not be counted as games, as the filter may + // apply to such entries as well and this will be indicated with a separate '+ XX' + // in the GamelistInfo field. + mFilteredGameCountAll = + static_cast(rootFolder->getFilesRecursive(GAME, true, true).size()); + } + + if (firstEntry->getParent() && firstEntry->getParent()->getType() == FOLDER) + mIsFolder = true; +} + +void GamelistBase::remove(FileData* game, bool deleteFile) +{ + // Delete the game file on the filesystem. + if (deleteFile) + Utils::FileSystem::removeFile(game->getPath()); + + FileData* parent {game->getParent()}; + // Select next element in list, or previous if none. + if (getCursor() == game) { + std::vector siblings {parent->getChildrenListToDisplay()}; + auto gameIter = std::find(siblings.cbegin(), siblings.cend(), game); + unsigned int gamePos { + static_cast(std::distance(siblings.cbegin(), gameIter))}; + if (gameIter != siblings.cend()) { + if ((gamePos + 1) < siblings.size()) + setCursor(siblings.at(gamePos + 1)); + else if (gamePos > 1) + setCursor(siblings.at(gamePos - 1)); + } + } + mList.remove(game); + + if (mList.size() == 0) + addPlaceholder(nullptr); + + // If a game has been deleted, immediately remove the entry from gamelist.xml + // regardless of the value of the setting SaveGamelistsMode. + game->setDeletionFlag(true); + parent->getSystem()->writeMetaData(); + + // Remove before repopulating (removes from parent), then update the view. + delete game; + + if (deleteFile) { + parent->sort(parent->getSortTypeFromString(parent->getSortTypeString()), + Settings::getInstance()->getBool("FavoritesFirst")); + onFileChanged(parent, false); + } +} + +void GamelistBase::removeMedia(FileData* game) +{ + std::string systemMediaDir {FileData::getMediaDirectory() + game->getSystem()->getName()}; + std::string mediaType; + std::string path; + + // Stop the video player, especially important on Windows as the file would otherwise be locked. + onStopVideo(); + + // If there are no media files left in the directory after the deletion, then remove + // the directory too. Remove any empty parent directories as well. + auto removeEmptyDirFunc = [](std::string systemMediaDir, std::string mediaType, + std::string path) { + std::string parentPath {Utils::FileSystem::getParent(path)}; + while (parentPath != systemMediaDir + "/" + mediaType) { + if (Utils::FileSystem::getDirContent(parentPath).size() == 0) { + Utils::FileSystem::removeDirectory(parentPath); + parentPath = Utils::FileSystem::getParent(parentPath); + } + else { + break; + } + } + }; + + // Remove all game media files on the filesystem. + while (Utils::FileSystem::exists(game->getVideoPath())) { + mediaType = "videos"; + path = game->getVideoPath(); + Utils::FileSystem::removeFile(path); + removeEmptyDirFunc(systemMediaDir, mediaType, path); + } + + while (Utils::FileSystem::exists(game->getMiximagePath())) { + mediaType = "miximages"; + path = game->getMiximagePath(); + Utils::FileSystem::removeFile(path); + removeEmptyDirFunc(systemMediaDir, mediaType, path); + } + + while (Utils::FileSystem::exists(game->getScreenshotPath())) { + mediaType = "screenshots"; + path = game->getScreenshotPath(); + Utils::FileSystem::removeFile(path); + removeEmptyDirFunc(systemMediaDir, mediaType, path); + } + + while (Utils::FileSystem::exists(game->getTitleScreenPath())) { + mediaType = "titlescreens"; + path = game->getTitleScreenPath(); + Utils::FileSystem::removeFile(path); + removeEmptyDirFunc(systemMediaDir, mediaType, path); + } + + while (Utils::FileSystem::exists(game->getCoverPath())) { + mediaType = "covers"; + path = game->getCoverPath(); + Utils::FileSystem::removeFile(path); + removeEmptyDirFunc(systemMediaDir, mediaType, path); + } + + while (Utils::FileSystem::exists(game->getBackCoverPath())) { + mediaType = "backcovers"; + path = game->getBackCoverPath(); + Utils::FileSystem::removeFile(path); + removeEmptyDirFunc(systemMediaDir, mediaType, path); + } + + while (Utils::FileSystem::exists(game->getFanArtPath())) { + mediaType = "fanart"; + path = game->getFanArtPath(); + Utils::FileSystem::removeFile(path); + removeEmptyDirFunc(systemMediaDir, mediaType, path); + } + + while (Utils::FileSystem::exists(game->getMarqueePath())) { + mediaType = "marquees"; + path = game->getMarqueePath(); + Utils::FileSystem::removeFile(path); + removeEmptyDirFunc(systemMediaDir, mediaType, path); + } + + while (Utils::FileSystem::exists(game->get3DBoxPath())) { + mediaType = "3dboxes"; + path = game->get3DBoxPath(); + Utils::FileSystem::removeFile(path); + removeEmptyDirFunc(systemMediaDir, mediaType, path); + } + + while (Utils::FileSystem::exists(game->getPhysicalMediaPath())) { + mediaType = "physicalmedia"; + path = game->getPhysicalMediaPath(); + Utils::FileSystem::removeFile(path); + removeEmptyDirFunc(systemMediaDir, mediaType, path); + } + + while (Utils::FileSystem::exists(game->getThumbnailPath())) { + mediaType = "thumbnails"; + path = game->getThumbnailPath(); + Utils::FileSystem::removeFile(path); + removeEmptyDirFunc(systemMediaDir, mediaType, path); + } +} diff --git a/es-app/src/views/GamelistBase.h b/es-app/src/views/GamelistBase.h new file mode 100644 index 000000000..c48098415 --- /dev/null +++ b/es-app/src/views/GamelistBase.h @@ -0,0 +1,101 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GamelistBase.h +// +// Gamelist base class with utility functions and other low-level logic. +// + +#ifndef ES_APP_VIEWS_GAMELIST_BASE_H +#define ES_APP_VIEWS_GAMELIST_BASE_H + +#include "FileData.h" +#include "GuiComponent.h" +#include "SystemData.h" +#include "ThemeData.h" +#include "Window.h" +#include "components/BadgeComponent.h" +#include "components/DateTimeComponent.h" +#include "components/RatingComponent.h" +#include "components/ScrollableContainer.h" +#include "components/TextComponent.h" +#include "components/TextListComponent.h" + +#include + +class GamelistBase : public GuiComponent +{ +public: + FileData* getCursor() { return mList.getSelected(); } + void setCursor(FileData*); + + bool input(InputConfig* config, Input input) override; + + FileData* getNextEntry() { return mList.getNext(); } + FileData* getPreviousEntry() { return mList.getPrevious(); } + FileData* getFirstEntry() { return mList.getFirst(); } + FileData* getLastEntry() { return mList.getLast(); } + FileData* getFirstGameEntry() { return mFirstGameEntry; } + +protected: + GamelistBase(Window* window, FileData* root); + ~GamelistBase(); + + // Called when a FileData* is added, has its metadata changed, or is removed. + virtual void onFileChanged(FileData* file, bool reloadGamelist) = 0; + + void populateList(const std::vector& files, FileData* firstEntry); + void addPlaceholder(FileData*); + + void generateFirstLetterIndex(const std::vector& files); + void generateGamelistInfo(FileData* cursor, FileData* firstEntry); + + void remove(FileData* game, bool deleteFile); + void removeMedia(FileData* game); + + virtual void launch(FileData* game) = 0; + + bool isListScrolling() override { return mList.isScrolling(); } + void stopListScrolling() override { mList.stopScrolling(); } + + const std::vector& getFirstLetterIndex() { return mFirstLetterIndex; } + std::string getQuickSystemSelectRightButton() { return "right"; } + std::string getQuickSystemSelectLeftButton() { return "left"; } + + // These functions are used to retain the folder cursor history, for instance + // during a view reload. The calling function stores the history temporarily. + void copyCursorHistory(std::vector& cursorHistory) + { + cursorHistory = mCursorStackHistory; + } + void populateCursorHistory(std::vector& cursorHistory) + { + mCursorStackHistory = cursorHistory; + } + + FileData* mRoot; + TextListComponent mList; + + // Points to the first game in the list, i.e. the first entry which is of the type "GAME". + FileData* mFirstGameEntry; + + // This game is randomly selected in the grouped custom collections view. + FileData* mRandomGame; + FileData* mLastUpdated; + + std::stack mCursorStack; + std::vector mCursorStackHistory; + + std::vector mFirstLetterIndex; + + unsigned int mGameCount; + unsigned int mFavoritesGameCount; + unsigned int mFilteredGameCount; + unsigned int mFilteredGameCountAll; + bool mIsFiltered; + bool mIsFolder; + +private: +}; + +#endif // ES_APP_VIEWS_GAMELIST_BASE_H diff --git a/es-app/src/views/GamelistView.cpp b/es-app/src/views/GamelistView.cpp new file mode 100644 index 000000000..9b329c173 --- /dev/null +++ b/es-app/src/views/GamelistView.cpp @@ -0,0 +1,558 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GamelistView.cpp +// +// Main gamelist logic. +// + +#include "views/GamelistView.h" + +#include "CollectionSystemsManager.h" +#include "UIModeController.h" +#include "animations/LambdaAnimation.h" + +#define FADE_IN_START_OPACITY 0.5f +#define FADE_IN_TIME 650 + +GamelistView::GamelistView(Window* window, FileData* root) + : GamelistBase {window, root} + , mHeaderText {window} + , mHeaderImage {window} + , mBackground {window} + , mThumbnail {window} + , mMarquee {window} + , mImage {window} + , mLblRating {window} + , mLblReleaseDate {window} + , mLblDeveloper {window} + , mLblPublisher {window} + , mLblGenre {window} + , mLblPlayers {window} + , mLblLastPlayed {window} + , mLblPlayCount {window} + , mRating {window} + , mReleaseDate {window} + , mDeveloper {window} + , mPublisher {window} + , mGenre {window} + , mPlayers {window} + , mLastPlayed {window} + , mPlayCount {window} + , mName {window} + , mBadges {window} + , mDescContainer {window} + , mDescription {window} + , mGamelistInfo {window} +{ + mHeaderText.setText(mRoot->getSystem()->getFullName()); +} + +GamelistView::~GamelistView() +{ + // +} + +void GamelistView::onFileChanged(FileData* file, bool reloadGamelist) +{ + if (reloadGamelist) { + // Might switch to a detailed view. + // TEMPORARY. + // ViewController::getInstance()->reloadGamelistView(this); + return; + } + + // We could be tricky here to be efficient; + // but this shouldn't happen very often so we'll just always repopulate. + FileData* cursor {getCursor()}; + if (!cursor->isPlaceHolder()) { + populateList(cursor->getParent()->getChildrenListToDisplay(), cursor->getParent()); + setCursor(cursor); + } + else { + populateList(mRoot->getChildrenListToDisplay(), mRoot); + setCursor(cursor); + } +} + +void GamelistView::onShow() +{ + // Reset any Lottie animations. + for (auto extra : mThemeExtras) + extra->resetFileAnimation(); + + mLastUpdated = nullptr; + GuiComponent::onShow(); + updateInfoPanel(); +} + +void GamelistView::onThemeChanged(const std::shared_ptr& theme) +{ + using namespace ThemeFlags; + mBackground.applyTheme(theme, getName(), "background", ALL); + mHeaderImage.applyTheme(theme, getName(), "logo", ALL); + mHeaderText.applyTheme(theme, getName(), "logoText", ALL); + + // Remove old theme extras. + for (auto extra : mThemeExtras) { + removeChild(extra); + delete extra; + } + mThemeExtras.clear(); + + // Add new theme extras. + mThemeExtras = ThemeData::makeExtras(theme, getName(), mWindow); + for (auto extra : mThemeExtras) + addChild(extra); + + if (mHeaderImage.hasImage()) { + removeChild(&mHeaderText); + addChild(&mHeaderImage); + } + else { + addChild(&mHeaderText); + removeChild(&mHeaderImage); + } + + mList.applyTheme(theme, getName(), "gamelist", ALL); + + mThumbnail.applyTheme(theme, getName(), "md_thumbnail", + POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION | VISIBLE); + mMarquee.applyTheme(theme, getName(), "md_marquee", + POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION | VISIBLE); + mImage.applyTheme(theme, getName(), "md_image", + POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION | VISIBLE); + mName.applyTheme(theme, getName(), "md_name", ALL); + mBadges.applyTheme(theme, getName(), "md_badges", ALL); + + initMDLabels(); + std::vector labels {getMDLabels()}; + assert(labels.size() == 8); + std::vector lblElements = { + "md_lbl_rating", "md_lbl_releasedate", "md_lbl_developer", "md_lbl_publisher", + "md_lbl_genre", "md_lbl_players", "md_lbl_lastplayed", "md_lbl_playcount"}; + + for (unsigned int i = 0; i < labels.size(); ++i) + labels[i]->applyTheme(theme, getName(), lblElements[i], ALL); + + initMDValues(); + std::vector values {getMDValues()}; + assert(values.size() == 8); + std::vector valElements = {"md_rating", "md_releasedate", "md_developer", + "md_publisher", "md_genre", "md_players", + "md_lastplayed", "md_playcount"}; + + for (unsigned int i = 0; i < values.size(); ++i) + values[i]->applyTheme(theme, getName(), valElements[i], ALL ^ ThemeFlags::TEXT); + + mDescContainer.applyTheme(theme, getName(), "md_description", + POSITION | ThemeFlags::SIZE | Z_INDEX | VISIBLE); + mDescription.setSize(mDescContainer.getSize().x, 0.0f); + mDescription.applyTheme( + theme, getName(), "md_description", + ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION)); + + mGamelistInfo.applyTheme(theme, getName(), "gamelistInfo", ALL ^ ThemeFlags::TEXT); + // If there is no position defined in the theme for gamelistInfo, then hide it. + if (mGamelistInfo.getPosition() == glm::vec3 {}) + mGamelistInfo.setVisible(false); + else + mGamelistInfo.setVisible(true); + + sortChildren(); +} + +void GamelistView::update(int deltaTime) +{ + // TEMPORARY + // BasicGamelistView::update(deltaTime); + mImage.update(deltaTime); + + if (ViewController::getInstance()->getGameLaunchTriggered() && mImage.isAnimationPlaying(0)) + mImage.finishAnimation(0); +} + +void GamelistView::render(const glm::mat4& parentTrans) +{ + glm::mat4 trans {parentTrans * getTransform()}; + + float scaleX {trans[0].x}; + float scaleY {trans[1].y}; + + glm::ivec2 pos {static_cast(std::round(trans[3].x)), + static_cast(std::round(trans[3].y))}; + glm::ivec2 size {static_cast(std::round(mSize.x * scaleX)), + static_cast(std::round(mSize.y * scaleY))}; + + Renderer::pushClipRect(pos, size); + renderChildren(trans); + Renderer::popClipRect(); +} + +HelpStyle GamelistView::getHelpStyle() +{ + HelpStyle style; + style.applyTheme(mTheme, getName()); + return style; +} + +std::vector GamelistView::getHelpPrompts() +{ + std::vector prompts; + + if (Settings::getInstance()->getBool("QuickSystemSelect") && + SystemData::sSystemVector.size() > 1) + prompts.push_back(HelpPrompt("left/right", "system")); + + if (mRoot->getSystem()->getThemeFolder() == "custom-collections" && mCursorStack.empty() && + ViewController::getInstance()->getState().viewing == ViewController::GAMELIST) + prompts.push_back(HelpPrompt("a", "enter")); + else + prompts.push_back(HelpPrompt("a", "launch")); + + prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("x", "view media")); + + if (!UIModeController::getInstance()->isUIModeKid()) + prompts.push_back(HelpPrompt("back", "options")); + if (mRoot->getSystem()->isGameSystem() && Settings::getInstance()->getBool("RandomAddButton")) + prompts.push_back(HelpPrompt("thumbstickclick", "random")); + + if (mRoot->getSystem()->getThemeFolder() == "custom-collections" && + !CollectionSystemsManager::getInstance()->isEditing() && mCursorStack.empty() && + ViewController::getInstance()->getState().viewing == ViewController::GAMELIST && + ViewController::getInstance()->getState().viewstyle != ViewController::BASIC) { + prompts.push_back(HelpPrompt("y", "jump to game")); + } + else if (mRoot->getSystem()->isGameSystem() && + (mRoot->getSystem()->getThemeFolder() != "custom-collections" || + !mCursorStack.empty()) && + !UIModeController::getInstance()->isUIModeKid() && + !UIModeController::getInstance()->isUIModeKiosk() && + (Settings::getInstance()->getBool("FavoritesAddButton") || + CollectionSystemsManager::getInstance()->isEditing())) { + std::string prompt = CollectionSystemsManager::getInstance()->getEditingCollection(); + prompts.push_back(HelpPrompt("y", prompt)); + } + else if (mRoot->getSystem()->isGameSystem() && + mRoot->getSystem()->getThemeFolder() == "custom-collections" && + CollectionSystemsManager::getInstance()->isEditing()) { + std::string prompt = CollectionSystemsManager::getInstance()->getEditingCollection(); + prompts.push_back(HelpPrompt("y", prompt)); + } + return prompts; +} + +void GamelistView::updateInfoPanel() +{ + 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. + if (file == mLastUpdated) + return; + + if (!mList.isScrolling()) + mLastUpdated = file; + + bool hideMetaDataFields {false}; + + if (file) { + // Always hide the metadata fields if browsing grouped custom collections. + if (file->getSystem()->isCustomCollection() && + file->getPath() == file->getSystem()->getName()) + hideMetaDataFields = true; + else + hideMetaDataFields = (file->metadata.get("hidemetadata") == "true"); + + // Always hide the metadata fields for placeholders as well. + if (file->getType() == PLACEHOLDER) { + hideMetaDataFields = true; + mLastUpdated = nullptr; + } + } + + // 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; + + if (hideMetaDataFields) { + mLblRating.setVisible(false); + mRating.setVisible(false); + mLblReleaseDate.setVisible(false); + mReleaseDate.setVisible(false); + mLblDeveloper.setVisible(false); + mDeveloper.setVisible(false); + mLblPublisher.setVisible(false); + mPublisher.setVisible(false); + mLblGenre.setVisible(false); + mGenre.setVisible(false); + mLblPlayers.setVisible(false); + mPlayers.setVisible(false); + mLblLastPlayed.setVisible(false); + mLastPlayed.setVisible(false); + mLblPlayCount.setVisible(false); + mPlayCount.setVisible(false); + mBadges.setVisible(false); + } + else { + mLblRating.setVisible(true); + mRating.setVisible(true); + mLblReleaseDate.setVisible(true); + mReleaseDate.setVisible(true); + mLblDeveloper.setVisible(true); + mDeveloper.setVisible(true); + mLblPublisher.setVisible(true); + mPublisher.setVisible(true); + mLblGenre.setVisible(true); + mGenre.setVisible(true); + mLblPlayers.setVisible(true); + mPlayers.setVisible(true); + mLblLastPlayed.setVisible(true); + mLastPlayed.setVisible(true); + mLblPlayCount.setVisible(true); + mPlayCount.setVisible(true); + mBadges.setVisible(true); + } + + bool fadingOut = false; + if (file == nullptr) { + 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) { + mThumbnail.setImage(mRandomGame->getThumbnailPath()); + mMarquee.setImage(mRandomGame->getMarqueePath(), false, true); + mImage.setImage(mRandomGame->getImagePath()); + } + else { + mThumbnail.setImage(""); + mMarquee.setImage(""); + mImage.setImage(""); + } + } + else { + mThumbnail.setImage(file->getThumbnailPath()); + mMarquee.setImage(file->getMarqueePath(), false, true); + mImage.setImage(file->getImagePath()); + } + + // 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. + std::string gamelistInfoString; + Alignment infoAlign = mGamelistInfo.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; + + mGamelistInfo.setValue(gamelistInfoString); + + // Fade in the game image. + auto func = [this](float t) { + mImage.setOpacity(static_cast( + glm::mix(static_cast(FADE_IN_START_OPACITY), 1.0f, t) * 255)); + }; + mImage.setAnimation(new LambdaAnimation(func, FADE_IN_TIME), 0, nullptr, false); + + mDescription.setText(file->metadata.get("desc")); + mDescContainer.reset(); + + mRating.setValue(file->metadata.get("rating")); + mReleaseDate.setValue(file->metadata.get("releasedate")); + mDeveloper.setValue(file->metadata.get("developer")); + mPublisher.setValue(file->metadata.get("publisher")); + mGenre.setValue(file->metadata.get("genre")); + mPlayers.setValue(file->metadata.get("players")); + + // Populate the badge slots based on game metadata. + std::vector badgeSlots; + for (auto badge : mBadges.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); + } + } + mBadges.setBadges(badgeSlots); + + mName.setValue(file->metadata.get("name")); + + if (file->getType() == GAME) { + if (!hideMetaDataFields) { + mLastPlayed.setValue(file->metadata.get("lastplayed")); + mPlayCount.setValue(file->metadata.get("playcount")); + } + } + else if (file->getType() == FOLDER) { + if (!hideMetaDataFields) { + mLastPlayed.setValue(file->metadata.get("lastplayed")); + mLblPlayCount.setVisible(false); + mPlayCount.setVisible(false); + } + } + + fadingOut = false; + } + + std::vector comps = getMDValues(); + comps.push_back(&mThumbnail); + comps.push_back(&mMarquee); + comps.push_back(&mImage); + comps.push_back(&mDescription); + comps.push_back(&mName); + comps.push_back(&mBadges); + std::vector labels = getMDLabels(); + comps.insert(comps.cend(), labels.cbegin(), labels.cend()); + + 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); + } + } +} + +void GamelistView::initMDLabels() +{ + std::vector components {getMDLabels()}; + + const unsigned int colCount {2}; + const unsigned int rowCount {static_cast(components.size() / 2)}; + + glm::vec3 start {mSize.x * 0.01f, mSize.y * 0.625f, 0.0f}; + + const float colSize {(mSize.x * 0.48f) / colCount}; + const float rowPadding {0.01f * mSize.y}; + + for (unsigned int i = 0; i < components.size(); ++i) { + const unsigned int row = i % rowCount; + glm::vec3 pos {}; + if (row == 0) { + pos = start + glm::vec3 {colSize * (i / rowCount), 0.0f, 0.0f}; + } + else { + // Work from the last component. + GuiComponent* lc {components[i - 1]}; + pos = lc->getPosition() + glm::vec3 {0.0f, lc->getSize().y + rowPadding, 0.0f}; + } + + components[i]->setFont(Font::get(FONT_SIZE_SMALL)); + components[i]->setPosition(pos); + components[i]->setDefaultZIndex(40.0f); + } +} + +void GamelistView::initMDValues() +{ + std::vector labels {getMDLabels()}; + std::vector values {getMDValues()}; + + std::shared_ptr defaultFont {Font::get(FONT_SIZE_SMALL)}; + mRating.setSize(defaultFont->getHeight() * 5.0f, static_cast(defaultFont->getHeight())); + mReleaseDate.setFont(defaultFont); + mDeveloper.setFont(defaultFont); + mPublisher.setFont(defaultFont); + mGenre.setFont(defaultFont); + mPlayers.setFont(defaultFont); + mLastPlayed.setFont(defaultFont); + mPlayCount.setFont(defaultFont); + + float bottom {0.0f}; + + const float colSize {(mSize.x * 0.48f) / 2.0f}; + for (unsigned int i = 0; i < labels.size(); ++i) { + const float heightDiff = (labels[i]->getSize().y - values[i]->getSize().y) / 2.0f; + values[i]->setPosition(labels[i]->getPosition() + + glm::vec3 {labels[i]->getSize().x, heightDiff, 0.0f}); + values[i]->setSize(colSize - labels[i]->getSize().x, values[i]->getSize().y); + values[i]->setDefaultZIndex(40.0f); + + float testBot = values[i]->getPosition().y + values[i]->getSize().y; + + if (testBot > bottom) + bottom = testBot; + } + + mDescContainer.setPosition(mDescContainer.getPosition().x, bottom + mSize.y * 0.01f); + mDescContainer.setSize(mDescContainer.getSize().x, mSize.y - mDescContainer.getPosition().y); +} + +std::vector GamelistView::getMDLabels() +{ + std::vector ret; + ret.push_back(&mLblRating); + ret.push_back(&mLblReleaseDate); + ret.push_back(&mLblDeveloper); + ret.push_back(&mLblPublisher); + ret.push_back(&mLblGenre); + ret.push_back(&mLblPlayers); + ret.push_back(&mLblLastPlayed); + ret.push_back(&mLblPlayCount); + return ret; +} + +std::vector GamelistView::getMDValues() +{ + std::vector ret; + ret.push_back(&mRating); + ret.push_back(&mReleaseDate); + ret.push_back(&mDeveloper); + ret.push_back(&mPublisher); + ret.push_back(&mGenre); + ret.push_back(&mPlayers); + ret.push_back(&mLastPlayed); + ret.push_back(&mPlayCount); + return ret; +} diff --git a/es-app/src/views/GamelistView.h b/es-app/src/views/GamelistView.h new file mode 100644 index 000000000..ef34de3bc --- /dev/null +++ b/es-app/src/views/GamelistView.h @@ -0,0 +1,91 @@ +// SPDX-License-Identifier: MIT +// +// EmulationStation Desktop Edition +// GamelistView.h +// +// Main gamelist logic. +// + +#ifndef ES_APP_VIEWS_GAMELIST_VIEW_H +#define ES_APP_VIEWS_GAMELIST_VIEW_H + +#include "views/GamelistBase.h" + +#include "renderers/Renderer.h" +#include "views/ViewController.h" + +class GamelistView : public GamelistBase +{ +public: + GamelistView(Window* window, FileData* root); + ~GamelistView(); + + // Called when a FileData* is added, has its metadata changed, or is removed. + void onFileChanged(FileData* file, bool reloadGamelist) override; + void onShow() override; + + void preloadGamelist() { updateInfoPanel(); } + void launch(FileData* game) override { ViewController::getInstance()->triggerGameLaunch(game); } + + std::string getName() const { return "DEPRECATED FUNCTION"; } + + const std::shared_ptr getTheme() const { return mTheme; } + void setTheme(const std::shared_ptr& theme) + { + mTheme = theme; + onThemeChanged(theme); + } + void onThemeChanged(const std::shared_ptr& theme); + + void update(int deltaTime) override; + void render(const glm::mat4& parentTrans) override; + + HelpStyle getHelpStyle() override; + std::vector getHelpPrompts() override; + +private: + void updateInfoPanel(); + + void initMDLabels(); + void initMDValues(); + + std::vector getMDLabels(); + std::vector getMDValues(); + + std::shared_ptr mTheme; + std::vector mThemeExtras; + + TextComponent mHeaderText; + ImageComponent mHeaderImage; + ImageComponent mBackground; + + ImageComponent mThumbnail; + ImageComponent mMarquee; + ImageComponent mImage; + + TextComponent mLblRating; + TextComponent mLblReleaseDate; + TextComponent mLblDeveloper; + TextComponent mLblPublisher; + TextComponent mLblGenre; + TextComponent mLblPlayers; + TextComponent mLblLastPlayed; + TextComponent mLblPlayCount; + + RatingComponent mRating; + DateTimeComponent mReleaseDate; + TextComponent mDeveloper; + TextComponent mPublisher; + TextComponent mGenre; + TextComponent mPlayers; + DateTimeComponent mLastPlayed; + TextComponent mPlayCount; + TextComponent mName; + BadgeComponent mBadges; + + ScrollableContainer mDescContainer; + TextComponent mDescription; + TextComponent mGamelistInfo; +}; + +#endif // ES_APP_VIEWS_GAMELIST_VIEW_H