diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 8ce59c048..413868626 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -1,4 +1,6 @@ +// SPDX-License-Identifier: MIT // +// EmulationStation Desktop Edition // BasicGameListView.cpp // // Interface that defines a GameListView of the type 'Basic'. @@ -46,10 +48,15 @@ void BasicGameListView::onFileChanged(FileData* file, FileChangeType change) void BasicGameListView::populateList(const std::vector& files) { + firstGameEntry = nullptr; + mList.clear(); mHeaderText.setText(mRoot->getSystem()->getFullName()); if (files.size() > 0) { for (auto it = files.cbegin(); it != files.cend(); it++) { + if (!firstGameEntry && (*it)->getType() == GAME) + firstGameEntry = (*it); + if ((*it)->getFavorite() && mRoot->getSystem()->getName() != "favorites") { mList.add(FAVORITE_GAME_CHAR + " " + (*it)->getName(), @@ -100,6 +107,16 @@ void BasicGameListView::setCursor(FileData* cursor) } } +FileData* BasicGameListView::getNextEntry() +{ + return mList.getNext(); +} + +FileData* BasicGameListView::getPreviousEntry() +{ + return mList.getPrevious(); +} + FileData* BasicGameListView::getFirstEntry() { return mList.getFirst(); @@ -110,6 +127,11 @@ FileData* BasicGameListView::getLastEntry() return mList.getLast(); } +FileData* BasicGameListView::getFirstGameEntry() +{ + return firstGameEntry; +} + void BasicGameListView::addPlaceholder() { // Empty list - add a placeholder. diff --git a/es-app/src/views/gamelist/BasicGameListView.h b/es-app/src/views/gamelist/BasicGameListView.h index f3138d059..55477753f 100644 --- a/es-app/src/views/gamelist/BasicGameListView.h +++ b/es-app/src/views/gamelist/BasicGameListView.h @@ -1,4 +1,6 @@ +// SPDX-License-Identifier: MIT // +// EmulationStation Desktop Edition // BasicGameListView.h // // Interface that defines a GameListView of the type 'basic'. @@ -22,8 +24,11 @@ public: virtual FileData* getCursor() override; virtual void setCursor(FileData* file) override; + virtual FileData* getNextEntry() override; + virtual FileData* getPreviousEntry() override; virtual FileData* getFirstEntry() override; virtual FileData* getLastEntry() override; + virtual FileData* getFirstGameEntry() override; virtual const char* getName() const override { return "basic"; } @@ -42,6 +47,8 @@ protected: virtual void addPlaceholder(); TextListComponent mList; + // Points to the first game in the list, i.e. the first entry which is of the type 'GAME'. + FileData* firstGameEntry; const std::string FAVORITE_GAME_CHAR = "\uF005"; const std::string FAVORITE_FOLDER_CHAR = "\uF07C"; diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 1ac245f77..1cbdf8712 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -1,4 +1,6 @@ +// SPDX-License-Identifier: MIT // +// EmulationStation Desktop Edition // GridGameListView.cpp // // Interface that defines a GameListView of the type 'grid'. @@ -11,8 +13,8 @@ #include "views/ViewController.h" #include "CollectionSystemManager.h" #include "Settings.h" -#include "SystemData.h" #include "Sound.h" +#include "SystemData.h" #if defined(_RPI_) #include "components/VideoPlayerComponent.h" #endif @@ -159,6 +161,16 @@ void GridGameListView::setCursor(FileData* file) } } +FileData* GridGameListView::getNextEntry() +{ + return mGrid.getNext();; +} + +FileData* GridGameListView::getPreviousEntry() +{ + return mGrid.getPrevious(); +} + FileData* GridGameListView::getFirstEntry() { return mGrid.getFirst();; @@ -169,6 +181,11 @@ FileData* GridGameListView::getLastEntry() return mGrid.getLast(); } +FileData* GridGameListView::getFirstGameEntry() +{ + return firstGameEntry; +} + std::string GridGameListView::getQuickSystemSelectRightButton() { return "rightshoulder"; @@ -222,11 +239,16 @@ const std::string GridGameListView::getImagePath(FileData* file) void GridGameListView::populateList(const std::vector& files) { + firstGameEntry = nullptr; + mGrid.clear(); mHeaderText.setText(mRoot->getSystem()->getFullName()); if (files.size() > 0) { - for (auto it = files.cbegin(); it != files.cend(); it++) + for (auto it = files.cbegin(); it != files.cend(); it++) { + if (!firstGameEntry && (*it)->getType() == GAME) + firstGameEntry = (*it); mGrid.add((*it)->getName(), getImagePath(*it), *it); + } } else { addPlaceholder(); diff --git a/es-app/src/views/gamelist/GridGameListView.h b/es-app/src/views/gamelist/GridGameListView.h index ce52019b4..6499a77c6 100644 --- a/es-app/src/views/gamelist/GridGameListView.h +++ b/es-app/src/views/gamelist/GridGameListView.h @@ -1,17 +1,18 @@ +// SPDX-License-Identifier: MIT // +// EmulationStation Desktop Edition // GridGameListView.h // // Interface that defines a GameListView of the type 'grid'. // -#pragma once #ifndef ES_APP_VIEWS_GAME_LIST_GRID_GAME_LIST_VIEW_H #define ES_APP_VIEWS_GAME_LIST_GRID_GAME_LIST_VIEW_H #include "components/DateTimeComponent.h" +#include "components/ImageGridComponent.h" #include "components/RatingComponent.h" #include "components/ScrollableContainer.h" -#include "components/ImageGridComponent.h" #include "components/VideoComponent.h" #include "views/gamelist/ISimpleGameListView.h" @@ -27,8 +28,11 @@ public: virtual FileData* getCursor() override; virtual void setCursor(FileData*) override; + virtual FileData* getNextEntry() override; + virtual FileData* getPreviousEntry() override; virtual FileData* getFirstEntry() override; virtual FileData* getLastEntry() override; + virtual FileData* getFirstGameEntry() override; virtual bool input(InputConfig* config, Input input) override; @@ -47,6 +51,8 @@ protected: virtual void addPlaceholder(); ImageGridComponent mGrid; + // Points to the first game in the list, i.e. the first entry which is of the type 'GAME'. + FileData* firstGameEntry; private: void updateInfoPanel(); diff --git a/es-app/src/views/gamelist/IGameListView.h b/es-app/src/views/gamelist/IGameListView.h index 99c6eda1b..d77088ae4 100644 --- a/es-app/src/views/gamelist/IGameListView.h +++ b/es-app/src/views/gamelist/IGameListView.h @@ -44,8 +44,11 @@ public: virtual FileData* getCursor() = 0; virtual void setCursor(FileData*) = 0; + virtual FileData* getNextEntry() = 0; + virtual FileData* getPreviousEntry() = 0; virtual FileData* getFirstEntry() = 0; virtual FileData* getLastEntry() = 0; + virtual FileData* getFirstGameEntry() = 0; virtual bool input(InputConfig* config, Input input) override; virtual void remove(FileData* game, bool deleteFile) = 0; diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index b01dd0541..e6af0b047 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -182,36 +182,118 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) if (mRoot->getSystem()->isGameSystem()) { 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(); + bool favoritesSorting; + bool removedLastFavorite = false; + bool foldersOnTop = Settings::getInstance()->getBool("FoldersOnTop"); + + if (CollectionSystemManager::get()->getIsCustomCollection(mRoot->getSystem())) + favoritesSorting = Settings::getInstance()->getBool("FavFirstCustom"); + else + favoritesSorting = Settings::getInstance()->getBool("FavoritesFirst"); + + if (favoritesSorting && static_cast(getName()) != "recent") { + 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(); + // If we are on the favorite marking boundary, select the next entry. + else if (getCursor()->getFavorite() != getPreviousEntry()->getFavorite()) + entryToSelect = getNextEntry(); + // 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); + } + // Marking folders as favorites is only cosmetic as they're not sorted // differently and they're not part of any collections. So it makes more // sense to do it here than to add the function to CollectionSystemManager. - if (getCursor()->getType() == FOLDER) { + if (entryToUpdate->getType() == FOLDER) { GuiInfoPopup* s; - MetaDataList* md = &getCursor()->getSourceFileData()->metadata; + MetaDataList* md = &entryToUpdate->getSourceFileData()->metadata; if (md->get("favorite") == "false") { md->set("favorite", "true"); s = new GuiInfoPopup(mWindow, "Marked folder '" + - Utils::String::removeParenthesis(getCursor()->getName()) + + Utils::String::removeParenthesis(entryToUpdate->getName()) + "' as favorite", 4000); } else { md->set("favorite", "false"); s = new GuiInfoPopup(mWindow, "Removed favorite marking for folder '" + - Utils::String::removeParenthesis(getCursor()->getName()) + + Utils::String::removeParenthesis(entryToUpdate->getName()) + "'", 4000); } mWindow->setInfoPopup(s); - getCursor()->getSourceFileData()->getSystem()->onMetaDataSavePoint(); + entryToUpdate->getSourceFileData()->getSystem()->onMetaDataSavePoint(); if (!Settings::getInstance()->getBool("FoldersOnTop")) mRoot->sort(mRoot->getSortTypeFromString(mRoot->getSortTypeString()), Settings::getInstance()->getBool("FavoritesFirst")); ViewController::get()->onFileChanged(getCursor(), FILE_METADATA_CHANGED); + + // 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) { + ViewController::get()->getGameListView(mRoot->getSystem())->setCursor( + ViewController::get()->getGameListView(mRoot->getSystem())-> + getFirstEntry()); + } + return true; } - else if (CollectionSystemManager::get()->toggleGameInCollection(getCursor())) { + else if (CollectionSystemManager::get()->toggleGameInCollection(entryToUpdate)) { + // Jump to the first entry in the gamelist if the last favorite was unmarked. + if (foldersOnTop && removedLastFavorite) + setCursor(getFirstGameEntry()); + else if (removedLastFavorite) + setCursor(getFirstEntry()); return true; } } diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index c2b330cde..4cadefcd4 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -144,6 +144,24 @@ public: return mEntries.at(mCursor).object; } + inline const UserData& getNext() const + { + // If there is a next entry, then return it, otherwise return the current entry. + if (mCursor + 1 < mEntries.size()) + return mEntries.at(mCursor+1).object; + else + return mEntries.at(mCursor).object; + } + + inline const UserData& getPrevious() const + { + // If there is a previous entry, then return it, otherwise return the current entry. + if (mCursor != 0) + return mEntries.at(mCursor-1).object; + else + return mEntries.at(mCursor).object; + } + inline const UserData& getFirst() const { assert(size() > 0);