diff --git a/NEWS.md b/NEWS.md index 3cbf6ba2a..bba3a03c7 100644 --- a/NEWS.md +++ b/NEWS.md @@ -35,7 +35,8 @@ Many bugs have been fixed, and numerous features that were only partially implem * Custom event scripts can now be enabled or disabled with a menu option * Help system updated and expanded to the complete application (previously it was only partially implemented) * Improved input device configuration, and default keyboard mappings are now applied if the keyboard has not been configured by the user -* GUI-configurable option to sort favorite games on the top of the game lists (favorites marked with stars) +* GUI-configurable option to sort favorite games above non-favorite games (favorites marked with stars) +* GUI-configurable option to sort folders on top of the gamelists * Added new component GuiComplexTextEditPopup to handle changes to configuration file entries and similar * Speed improvements and optimizations, the application now starts faster and feels more responsive * Moved all resources to a subdirectory structure and enabled the CMake install prefix variable to generate the resources search path @@ -65,7 +66,7 @@ Many bugs have been fixed, and numerous features that were only partially implem * The random system selection did not consider the currently selected system * The random game selection did not consider the currently selected game * The random game selection traversed folders, i.e. a game could be selected inside a subdirectory and vice versa -* Deleting a game did not delete the game media files or its entry in the gamelist.xml file +* Deleting a game from the metadata editor did not delete the game media files or the entry in the gamelist.xml file * SystemView didn't properly loop the systems if only two systems were available * Hidden files still showed up if they had a gamelist.xml entry * On Unix, adding a hidden folder with a game in it crashed the application on startup diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index a2bd8270b..ad20f6654 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -37,6 +37,7 @@ FileData::FileData( mEnvData(envData), mSourceFileData(nullptr), mParent(nullptr), + mOnlyFolders(false), mDeletionFlag(false), // Metadata is REALLY set in the constructor! metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) @@ -368,6 +369,10 @@ void FileData::removeChild(FileData* file) void FileData::sort(ComparisonFunction& comparator, bool ascending) { mFirstLetterIndex.clear(); + mOnlyFolders = false; + bool foldersOnTop = Settings::getInstance()->getBool("FoldersOnTop"); + std::vector mChildrenFolders; + std::vector mChildrenOthers; // Only run this section of code if the setting to show hidden games has been disabled, // in order to avoid unnecessary processing. @@ -386,32 +391,69 @@ void FileData::sort(ComparisonFunction& comparator, bool ascending) mChildren.insert(mChildren.end(), mChildrenShown.begin(), mChildrenShown.end()); } - std::stable_sort(mChildren.begin(), mChildren.end(), comparator); + if (foldersOnTop) { + for (unsigned int i = 0; i < mChildren.size(); i++) { + if (mChildren[i]->getType() == FOLDER) + mChildrenFolders.push_back(mChildren[i]); + else + mChildrenOthers.push_back(mChildren[i]); + } + + std::stable_sort(mChildrenFolders.begin(), mChildrenFolders.end(), comparator); + std::stable_sort(mChildrenOthers.begin(), mChildrenOthers.end(), comparator); + + if (!ascending) { + std::reverse(mChildrenFolders.begin(), mChildrenFolders.end()); + std::reverse(mChildrenOthers.begin(), mChildrenOthers.end()); + } + + mChildren.erase(mChildren.begin(), mChildren.end()); + mChildren.reserve(mChildrenFolders.size() + mChildrenOthers.size()); + mChildren.insert(mChildren.end(), mChildrenFolders.begin(), mChildrenFolders.end()); + mChildren.insert(mChildren.end(), mChildrenOthers.begin(), mChildrenOthers.end()); + } + else { + std::stable_sort(mChildren.begin(), mChildren.end(), comparator); + if (!ascending) + std::reverse(mChildren.begin(), mChildren.end()); + } for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) { - // Build mFirstLetterIndex. - const char firstChar = toupper((*it)->getSortName().front()); - mFirstLetterIndex.push_back(std::string(1, firstChar)); + if (!(foldersOnTop && (*it)->getType() == FOLDER)) { + // Build mFirstLetterIndex. + const char firstChar = toupper((*it)->getSortName().front()); + mFirstLetterIndex.push_back(std::string(1, firstChar)); + } // Iterate through any child folders. if ((*it)->getChildren().size() > 0) (*it)->sort(comparator, ascending); } + // If there are only folders in the gamelist, then it makes sense to still + // generate a letter index. + if (mChildrenOthers.size() == 0 && mChildrenFolders.size() > 0) { + for (unsigned int i = 0; i < mChildrenFolders.size(); i++) { + const char firstChar = toupper(mChildrenFolders[i]->getSortName().front()); + mFirstLetterIndex.push_back(std::string(1, firstChar)); + } + mOnlyFolders = true; + } + // Sort and make each entry unique in mFirstLetterIndex. std::sort(mFirstLetterIndex.begin(), mFirstLetterIndex.end()); auto last = std::unique(mFirstLetterIndex.begin(), mFirstLetterIndex.end()); mFirstLetterIndex.erase(last, mFirstLetterIndex.end()); - - if (!ascending) - std::reverse(mChildren.begin(), mChildren.end()); } void FileData::sortFavoritesOnTop(ComparisonFunction& comparator, bool ascending) { mFirstLetterIndex.clear(); + mOnlyFolders = false; + std::vector mChildrenFolders; std::vector mChildrenFavorites; std::vector mChildrenOthers; bool showHiddenGames = Settings::getInstance()->getBool("ShowHiddenGames"); + bool foldersOnTop = Settings::getInstance()->getBool("FoldersOnTop"); for (unsigned int i = 0; i < mChildren.size(); i++) { // Exclude game if it's marked as hidden and the hide setting has been set. @@ -421,7 +463,10 @@ void FileData::sortFavoritesOnTop(ComparisonFunction& comparator, bool ascending continue; } - if (mChildren[i]->getFavorite()) { + if (foldersOnTop && mChildren[i]->getType() == FOLDER) { + mChildrenFolders.push_back(mChildren[i]); + } + else if (mChildren[i]->getFavorite()) { mChildrenFavorites.push_back(mChildren[i]); } else { @@ -438,9 +483,24 @@ void FileData::sortFavoritesOnTop(ComparisonFunction& comparator, bool ascending // probably faster than building a redundant index for all gamelists during sorting. if (mChildrenOthers.size() == 0 && mChildrenFavorites.size() > 0) { for (unsigned int i = 0; i < mChildren.size(); i++) { - const char firstChar = toupper(mChildren[i]->getSortName().front()); + if (foldersOnTop && mChildren[i]->getType() == FOLDER) { + continue; + } + else { + const char firstChar = toupper(mChildren[i]->getSortName().front()); + mFirstLetterIndex.push_back(std::string(1, firstChar)); + } + } + } + // If there are only folders in the gamelist, then it also makes sense to generate + // a letter index. + else if (mChildrenOthers.size() == 0 && mChildrenFavorites.size() == 0 && + mChildrenFolders.size() > 0) { + for (unsigned int i = 0; i < mChildrenFolders.size(); i++) { + const char firstChar = toupper(mChildrenFolders[i]->getSortName().front()); mFirstLetterIndex.push_back(std::string(1, firstChar)); } + mOnlyFolders = true; } // Sort and make each entry unique in mFirstLetterIndex. @@ -454,9 +514,16 @@ void FileData::sortFavoritesOnTop(ComparisonFunction& comparator, bool ascending mFirstLetterIndex.insert(mFirstLetterIndex.begin(), FAVORITE_CHAR); // Sort favorite games and the other games separately. + std::stable_sort(mChildrenFolders.begin(), mChildrenFolders.end(), comparator); std::stable_sort(mChildrenFavorites.begin(), mChildrenFavorites.end(), comparator); std::stable_sort(mChildrenOthers.begin(), mChildrenOthers.end(), comparator); + // Iterate through any child folders. + for (auto it = mChildrenFolders.cbegin(); it != mChildrenFolders.cend(); it++) { + if ((*it)->getChildren().size() > 0) + (*it)->sortFavoritesOnTop(comparator, ascending); + } + // Iterate through any child folders. for (auto it = mChildrenFavorites.cbegin(); it != mChildrenFavorites.cend(); it++) { if ((*it)->getChildren().size() > 0) @@ -470,13 +537,15 @@ void FileData::sortFavoritesOnTop(ComparisonFunction& comparator, bool ascending } if (!ascending) { + std::reverse(mChildrenFolders.begin(), mChildrenFolders.end()); std::reverse(mChildrenFavorites.begin(), mChildrenFavorites.end()); std::reverse(mChildrenOthers.begin(), mChildrenOthers.end()); } // Combine the individually sorted favorite games and other games vectors. mChildren.erase(mChildren.begin(), mChildren.end()); - mChildren.reserve(mChildrenFavorites.size() + mChildrenOthers.size()); + mChildren.reserve(mChildrenFolders.size() + mChildrenFavorites.size() + mChildrenOthers.size()); + mChildren.insert(mChildren.end(), mChildrenFolders.begin(), mChildrenFolders.end()); mChildren.insert(mChildren.end(), mChildrenFavorites.begin(), mChildrenFavorites.end()); mChildren.insert(mChildren.end(), mChildrenOthers.begin(), mChildrenOthers.end()); } diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index c42308118..f09a52323 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -59,6 +59,7 @@ public: inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } const std::vector& getFirstLetterIndex() const { return mFirstLetterIndex; }; + const bool getOnlyFoldersFlag() { return mOnlyFolders; } static const std::string getROMDirectory(); static const std::string getMediaDirectory(); const std::string getMediafilePath(std::string subdirectory, std::string mediatype) const; @@ -137,6 +138,7 @@ private: std::vector mChildren; std::vector mFilteredChildren; std::vector mFirstLetterIndex; + bool mOnlyFolders; // Used for flagging a game for deletion from its gamelist.xml file. bool mDeletionFlag; diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 7624fa40e..221f032c5 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -39,6 +39,11 @@ GuiGamelistOptions::GuiGamelistOptions( fromPlaceholder = file->isPlaceHolder(); ComponentListRow row; + // Read the setting for whether folders are sorted on top of the gamelists. + // Also check if the gamelist only contains folders, as generated by the FileData sorting. + mFoldersOnTop = Settings::getInstance()->getBool("FoldersOnTop"); + mOnlyHasFolders = file->getParent()->getOnlyFoldersFlag(); + // Read the applicable favorite sorting setting depending on whether the // system is a custom collection or not. if (CollectionSystemManager::get()->getIsCustomCollection(file->getSystem())) @@ -53,11 +58,21 @@ GuiGamelistOptions::GuiGamelistOptions( // The letter index is generated in FileData during gamelist sorting. mFirstLetterIndex = file->getParent()->getFirstLetterIndex(); - // Set the quick selector to the first character of the selected game. - if (mFavoritesSorting && file->getFavorite() && mFirstLetterIndex.front() == FAVORITE_CHAR) - mCurrentFirstCharacter = FAVORITE_CHAR; - else - mCurrentFirstCharacter = toupper(file->getSortName().front()); + // Don't include the folder name starting characters if folders are sorted on top + // unless the list only contains folders. + if (!mOnlyHasFolders && mFoldersOnTop && file->getType() == FOLDER) { + mCurrentFirstCharacter = mFirstLetterIndex.front(); + } + else { + // Set the quick selector to the first character of the selected game. + if (mFavoritesSorting && file->getFavorite() && + mFirstLetterIndex.front() == FAVORITE_CHAR) { + mCurrentFirstCharacter = FAVORITE_CHAR; + } + else { + mCurrentFirstCharacter = toupper(file->getSortName().front()); + } + } mJumpToLetterList = std::make_shared(mWindow, getHelpStyle(), "JUMP TO...", false); @@ -178,7 +193,8 @@ GuiGamelistOptions::~GuiGamelistOptions() } // Has the user changed the letter using the quick selector? - if (mCurrentFirstCharacter != mJumpToLetterList->getSelected()) { + if ((mFoldersOnTop && getGamelist()->getCursor()->getType() == FOLDER) || + mCurrentFirstCharacter != mJumpToLetterList->getSelected()) { if (mJumpToLetterList->getSelected() == FAVORITE_CHAR) jumpToFirstRow(); else @@ -275,14 +291,24 @@ void GuiGamelistOptions::jumpToLetter() if (mFavoritesSorting && mFirstLetterIndex.front() == FAVORITE_CHAR) { if ((char)toupper(files.at(i)->getSortName().front()) == letter && !files.at(i)->getFavorite()) { - getGamelist()->setCursor(files.at(i)); - break; + if (mFoldersOnTop && files.at(i)->getType() == FOLDER) { + continue; + } + else { + getGamelist()->setCursor(files.at(i)); + break; + } } } else { if ((char)toupper(files.at(i)->getSortName().front()) == letter) { - getGamelist()->setCursor(files.at(i)); - break; + if (!mOnlyHasFolders && mFoldersOnTop && files.at(i)->getType() == FOLDER) { + continue; + } + else { + getGamelist()->setCursor(files.at(i)); + break; + } } } } @@ -290,8 +316,22 @@ void GuiGamelistOptions::jumpToLetter() void GuiGamelistOptions::jumpToFirstRow() { - // Get first row of the gamelist. - getGamelist()->setCursor(getGamelist()->getFirstEntry()); + if (mFoldersOnTop) { + // Get the gamelist. + const std::vector& files = getGamelist()->getCursor()-> + getParent()->getChildrenListToDisplay(); + // Select the first game that is not a folder. + for (auto it = files.cbegin(); it != files.cend(); it++) { + if ((*it)->getType() == GAME) { + getGamelist()->setCursor(*it); + break; + } + } + } + else { + // Get first row of the gamelist. + getGamelist()->setCursor(getGamelist()->getFirstEntry()); + } } bool GuiGamelistOptions::input(InputConfig* config, Input input) diff --git a/es-app/src/guis/GuiGamelistOptions.h b/es-app/src/guis/GuiGamelistOptions.h index 44c7b136e..b8558e107 100644 --- a/es-app/src/guis/GuiGamelistOptions.h +++ b/es-app/src/guis/GuiGamelistOptions.h @@ -50,7 +50,9 @@ private: SystemData* mSystem; IGameListView* getGamelist(); + bool mFoldersOnTop; bool mFavoritesSorting; + bool mOnlyHasFolders; bool fromPlaceholder; bool mFiltersChanged; bool mCancelled; diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index f590d06eb..6699a36f1 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -388,12 +388,35 @@ void GuiMenu::openUISettings() } }); - // Sort favorites on top of the gamelists. - auto favoritesFirstSwitch = std::make_shared(mWindow); - favoritesFirstSwitch->setState(Settings::getInstance()->getBool("FavoritesFirst")); - s->addWithLabel("SORT FAVORITES ON TOP OF GAMELISTS", favoritesFirstSwitch); - s->addSaveFunc([favoritesFirstSwitch] { - if (Settings::getInstance()->setBool("FavoritesFirst", favoritesFirstSwitch->getState())) + // Sort folders on top of the gamelists. + auto folders_on_top = std::make_shared(mWindow); + folders_on_top->setState(Settings::getInstance()->getBool("FoldersOnTop")); + s->addWithLabel("SORT FOLDERS ON TOP OF GAMELISTS", folders_on_top); + s->addSaveFunc([folders_on_top] { + if (Settings::getInstance()->setBool("FoldersOnTop", folders_on_top->getState())) + for (auto it = SystemData::sSystemVector.cbegin(); it != + SystemData::sSystemVector.cend(); it++) { + + if ((*it)->isCollection()) + continue; + + FileData* rootFolder = (*it)->getRootFolder(); + rootFolder->sort(getSortTypeFromString(rootFolder->getSortTypeString()), + Settings::getInstance()->getBool("FavoritesFirst")); + ViewController::get()->reloadGameListView(*it); + + // Jump to the first row of the gamelist. + IGameListView* gameList = ViewController::get()->getGameListView((*it)).get(); + gameList->setCursor(gameList->getFirstEntry()); + } + }); + + // Sort favorites on top of non-favorites in the gamelists. + auto favorites_first = std::make_shared(mWindow); + favorites_first->setState(Settings::getInstance()->getBool("FavoritesFirst")); + s->addWithLabel("SORT FAVORITE GAMES ABOVE NON-FAVORITES", favorites_first); + s->addSaveFunc([favorites_first] { + if (Settings::getInstance()->setBool("FavoritesFirst", favorites_first->getState())) for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { // The favorites and recent gamelists never sort favorites on top. diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 1fcf544f5..cec5b0347 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -84,6 +84,7 @@ void Settings::setDefaults() mStringMap["ThemeSet"] = ""; mStringMap["UIMode"] = "full"; mStringMap["DefaultSortOrder"] = "filename, ascending"; + mBoolMap["FoldersOnTop"] = true; mBoolMap["FavoritesFirst"] = true; mBoolMap["ForceDisableFilters"] = false; mBoolMap["QuickSystemSelect"] = true;