// SPDX-License-Identifier: MIT // // EmulationStation Desktop Edition // GuiGamelistOptions.cpp // // Gamelist options menu for the 'Jump to...' quick selector, // game sorting, game filters, and metadata edit. // // The filter interface is covered by GuiGamelistFilter and the // metadata edit interface is covered by GuiMetaDataEd. // #include "GuiGamelistOptions.h" #include "guis/GuiGamelistFilter.h" #include "scrapers/Scraper.h" #include "views/gamelist/IGameListView.h" #include "views/UIModeController.h" #include "views/ViewController.h" #include "CollectionSystemManager.h" #include "FileFilterIndex.h" #include "FileSorts.h" #include "GuiMetaDataEd.h" #include "Sound.h" #include "SystemData.h" GuiGamelistOptions::GuiGamelistOptions( Window* window, SystemData* system) : GuiComponent(window), mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false), mCancelled(false), isCustomCollection(false), isCustomCollectionGroup(false) { addChild(&mMenu); // Check that it's not a placeholder folder - if it is, only show "Filter Options". FileData* file = getGamelist()->getCursor(); FAVORITE_CHAR = file->FAVORITE_CHAR; FOLDER_CHAR = file->FOLDER_CHAR; fromPlaceholder = file->isPlaceHolder(); ComponentListRow row; // There is some special logic required for custom collections. if (file->getSystem()->isCustomCollection() && file->getPath() != file->getSystem()->getName()) isCustomCollection = true; else if (file->getSystem()->isCustomCollection() && file->getPath() == file->getSystem()->getName()) isCustomCollectionGroup = true; // 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"); if (file->getType() != PLACEHOLDER) mOnlyHasFolders = file->getParent()->getOnlyFoldersFlag(); // Read the applicable favorite sorting setting depending on whether the // system is a custom collection or not. if (isCustomCollection) mFavoritesSorting = Settings::getInstance()->getBool("FavFirstCustom"); else mFavoritesSorting = Settings::getInstance()->getBool("FavoritesFirst"); if (!fromPlaceholder) { // Jump to letter quick selector. row.elements.clear(); // The letter index is generated in FileData during gamelist sorting. mFirstLetterIndex = getGamelist()->getFirstLetterIndex(); // 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 = FOLDER_CHAR; } else { // Check if the currently selected game is a favorite. bool isFavorite = false; if (mFirstLetterIndex.size() == 1 && mFirstLetterIndex.front() == FAVORITE_CHAR) isFavorite = true; else if (mFirstLetterIndex.size() > 1 && (mFirstLetterIndex.front() == FAVORITE_CHAR || mFirstLetterIndex[1] == FAVORITE_CHAR)) isFavorite = true; if (mFavoritesSorting && file->getFavorite() && isFavorite) mCurrentFirstCharacter = FAVORITE_CHAR; else mCurrentFirstCharacter = toupper(file->getSortName().front()); } mJumpToLetterList = std::make_shared(mWindow, getHelpStyle(), "JUMP TO...", false); // Populate the quick selector. for (unsigned int i = 0; i < mFirstLetterIndex.size(); i++) { mJumpToLetterList->add(mFirstLetterIndex[i], mFirstLetterIndex[i], 0); if (mFirstLetterIndex[i] == mCurrentFirstCharacter) mJumpToLetterList->selectEntry(i); } if (system->getName() != "recent") mMenu.addWithLabel("JUMP TO..", mJumpToLetterList); // Add the sorting entry, unless this is the grouped custom collections list. if (!isCustomCollectionGroup) { // Sort list by selected sort type (persistent throughout the program session). mListSort = std::make_shared(mWindow, getHelpStyle(), "SORT GAMES BY", false); FileData* root; if (isCustomCollection) root = getGamelist()->getCursor()->getSystem()->getRootFolder(); else root = mSystem->getRootFolder(); std::string sortType = root->getSortTypeString(); for (unsigned int i = 0; i add(sort.description, &sort, 1); else mListSort->add(sort.description, &sort, 0); } // Don't show the sort type option if the gamelist type is recent/last played. if (system->getName() != "recent") mMenu.addWithLabel("SORT GAMES BY", mListSort); } } // Add the filters entry, unless this is the grouped custom collections list. if (!isCustomCollectionGroup) { if (system->getName() != "recent" && Settings::getInstance()->getBool("GamelistFilters")) { row.elements.clear(); row.addElement(std::make_shared (mWindow, "FILTER GAMELIST", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); mMenu.addRow(row); } } std::string customSystem; if (Settings::getInstance()->getBool("UseCustomCollectionsSystem")) customSystem = file->getSystem()->getName(); else customSystem = system->getName(); if (UIModeController::getInstance()->isUIModeFull() && (isCustomCollection || isCustomCollectionGroup)) { if (CollectionSystemManager::get()->getEditingCollection() != customSystem) { row.elements.clear(); row.addElement(std::make_shared( mWindow, "ADD/REMOVE GAMES TO THIS GAME COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::startEditMode, this)); mMenu.addRow(row); } } if (UIModeController::getInstance()->isUIModeFull() && CollectionSystemManager::get()->isEditing()) { row.elements.clear(); row.addElement(std::make_shared( mWindow, "FINISH EDITING '" + Utils::String::toUpper( CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION",Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::exitEditMode, this)); mMenu.addRow(row); } if (file->getType() == FOLDER) { if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) { row.elements.clear(); row.addElement(std::make_shared(mWindow, "EDIT THIS FOLDER'S METADATA", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openMetaDataEd, this)); mMenu.addRow(row); } } else { if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) { row.elements.clear(); row.addElement(std::make_shared(mWindow, "EDIT THIS GAME'S METADATA", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openMetaDataEd, this)); mMenu.addRow(row); } } // Buttons. Logic to apply or cancel settings are handled by the destructor. mMenu.addButton("APPLY", "apply", [&] { delete this; }); mMenu.addButton("CANCEL", "cancel", [&] { mCancelled = true; delete this; }); // Center the menu. setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); } GuiGamelistOptions::~GuiGamelistOptions() { // This is required for the situation where scrolling started just before the menu // was openened. Without this, the scrolling would run until manually stopped after // the menu has been closed. ViewController::get()->stopScrolling(); if (mCancelled) return; if (!fromPlaceholder) { FileData* root; if (isCustomCollection) root = getGamelist()->getCursor()->getSystem()->getRootFolder(); else root = mSystem->getRootFolder(); // If a new sorting type was selected, then sort and update mSortTypeString for the system. if (!isCustomCollectionGroup && (*mListSort->getSelected()).description != root->getSortTypeString()) { // This will also recursively sort children. root->sort(*mListSort->getSelected(), mFavoritesSorting); root->setSortTypeString((*mListSort->getSelected()).description); // Notify that the root folder was sorted (refresh). getGamelist()->onFileChanged(root, false); } // Has the user changed the letter using the quick selector? if (mCurrentFirstCharacter != mJumpToLetterList->getSelected()) { if (mJumpToLetterList->getSelected() == FAVORITE_CHAR || mJumpToLetterList->getSelected() == FOLDER_CHAR) jumpToFirstRow(); else jumpToLetter(); } } if (mFiltersChanged) { // Only reload full view if we came from a placeholder as we need to // re-display the remaining elements for whatever new game is selected. ViewController::get()->reloadGameListView(mSystem); } NavigationSounds::getInstance()->playThemeNavigationSound(SCROLLSOUND); } void GuiGamelistOptions::openGamelistFilter() { GuiGamelistFilter* ggf; mFiltersChanged = true; if (isCustomCollection) ggf = new GuiGamelistFilter(mWindow, getGamelist()->getCursor()->getSystem()); else ggf = new GuiGamelistFilter(mWindow, mSystem); mWindow->pushGui(ggf); } void GuiGamelistOptions::startEditMode() { std::string editingSystem = mSystem->getName(); // Need to check if we're editing the collections bundle, // as we will want to edit the selected collection within. if (editingSystem == CollectionSystemManager::get()->getCustomCollectionsBundle()->getName()) { FileData* file = getGamelist()->getCursor(); // Do we have the cursor on a specific collection?. if (file->getType() == FOLDER) editingSystem = file->getName(); else // We are inside a specific collection. We want to edit that one. editingSystem = file->getSystem()->getName(); } CollectionSystemManager::get()->setEditMode(editingSystem); // 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(). for (auto it = SystemData::sSystemVector.begin(); it != SystemData::sSystemVector.end(); it++) { ViewController::get()->getGameListView((*it))->onFileChanged( ViewController::get()->getGameListView((*it))->getCursor(), false); } delete this; } void GuiGamelistOptions::exitEditMode() { CollectionSystemManager::get()->exitEditMode(); for (auto it = SystemData::sSystemVector.begin(); it != SystemData::sSystemVector.end(); it++) { ViewController::get()->getGameListView((*it))->onFileChanged( ViewController::get()->getGameListView((*it))->getCursor(), false); } delete this; } void GuiGamelistOptions::openMetaDataEd() { // Open metadata editor. // Get the FileData that holds the original metadata. FileData* file = getGamelist()->getCursor()->getSourceFileData(); ScraperSearchParams p; p.game = file; p.system = file->getSystem(); std::function clearGameBtnFunc; std::function deleteGameBtnFunc; clearGameBtnFunc = [this, file] { if (file->getType() == FOLDER) { LOG(LogInfo) << "Deleting the media files and gamelist.xml entry for the folder \"" << file->getFullPath() << "\""; } else { LOG(LogInfo) << "Deleting the media files and gamelist.xml entry for the file \"" << file->getFullPath() << "\""; } ViewController::get()->getGameListView(file->getSystem()).get()->removeMedia(file); // Manually reset all the metadata values, set the name to the actual file/folder name. const std::vector& mdd = file->metadata.getMDD(); for (auto it = mdd.cbegin(); it != mdd.cend(); it++) { if (it->key == "name") { file->metadata.set(it->key, file->getDisplayName()); continue; } file->metadata.set(it->key, it->defaultValue); } file->getSystem()->sortSystem(); mWindow->invalidateCachedBackground(); // Remove the folder entry from the gamelist.xml file. file->setDeletionFlag(); file->getParent()->getSystem()->writeMetaData(); }; deleteGameBtnFunc = [this, file] { LOG(LogInfo) << "Deleting the game file \"" << file->getFullPath() << "\", all its media files and its gamelist.xml entry."; CollectionSystemManager::get()->deleteCollectionFiles(file); ViewController::get()->getGameListView( file->getSystem()).get()->removeMedia(file); ViewController::get()->getGameListView( file->getSystem()).get()->remove(file, true); mWindow->invalidateCachedBackground(); }; if (file->getType() == FOLDER) { mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(FOLDER_METADATA), p, Utils::FileSystem::getFileName(file->getPath()), std::bind( &IGameListView::onFileChanged, ViewController::get()->getGameListView( file->getSystem()).get(), file, true), clearGameBtnFunc, deleteGameBtnFunc)); } else { mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(GAME_METADATA), p, Utils::FileSystem::getFileName(file->getPath()), std::bind( &IGameListView::onFileChanged, ViewController::get()->getGameListView( file->getSystem()).get(), file, true), clearGameBtnFunc, deleteGameBtnFunc)); } } void GuiGamelistOptions::jumpToLetter() { char letter = mJumpToLetterList->getSelected().front(); // Get the gamelist. const std::vector& files = getGamelist()->getCursor()-> getParent()->getChildrenListToDisplay(); for (unsigned int i = 0; i < files.size(); i++) { if (mFavoritesSorting && (mFirstLetterIndex.front() == FAVORITE_CHAR || mFirstLetterIndex.front() == FOLDER_CHAR)) { if ((char)toupper(files.at(i)->getSortName().front()) == letter && !files.at(i)->getFavorite()) { if (!mOnlyHasFolders && mFoldersOnTop && files.at(i)->getType() == FOLDER) { continue; } else { getGamelist()->setCursor(files.at(i)); break; } } } else { if ((char)toupper(files.at(i)->getSortName().front()) == letter) { if (!mOnlyHasFolders && mFoldersOnTop && files.at(i)->getType() == FOLDER) { continue; } else { getGamelist()->setCursor(files.at(i)); break; } } } } } void GuiGamelistOptions::jumpToFirstRow() { if (mFoldersOnTop && mJumpToLetterList->getSelected() == FAVORITE_CHAR) { // Get the gamelist. const std::vector& files = getGamelist()->getCursor()-> getParent()->getChildrenListToDisplay(); // Select the first game that is not a folder, unless it's a folder-only list in // which case the first line overall is selected. for (auto it = files.cbegin(); it != files.cend(); it++) { if (!mOnlyHasFolders && mFoldersOnTop && (*it)->getType() == FOLDER) { continue; } else { getGamelist()->setCursor(*it); break; } } } else { // Get first row of the gamelist. getGamelist()->setCursor(getGamelist()->getFirstEntry()); } } bool GuiGamelistOptions::input(InputConfig* config, Input input) { if (input.value != 0 && config->isMappedTo("select", input)) mCancelled = true; if (input.value != 0 && (config->isMappedTo("b", input) || config->isMappedTo("select", input))) { delete this; return true; } return mMenu.input(config, input); } HelpStyle GuiGamelistOptions::getHelpStyle() { HelpStyle style = HelpStyle(); style.applyTheme(mSystem->getTheme(), "system"); return style; } std::vector GuiGamelistOptions::getHelpPrompts() { auto prompts = mMenu.getHelpPrompts(); prompts.push_back(HelpPrompt("a", "select")); prompts.push_back(HelpPrompt("b", "close (apply)")); prompts.push_back(HelpPrompt("select", "close (cancel)")); return prompts; } IGameListView* GuiGamelistOptions::getGamelist() { return ViewController::get()->getGameListView(mSystem).get(); }