From 7f84eeea9459ddd61ed0b63d0c495d159b0ccfc9 Mon Sep 17 00:00:00 2001 From: pjft Date: Sat, 18 Mar 2017 17:54:39 +0000 Subject: [PATCH] Adding generic gamelist filter funcionality for ES, specifically genre, players, ratings and publisher/developer --- es-app/CMakeLists.txt | 4 + es-app/src/FileData.cpp | 31 +- es-app/src/FileData.h | 9 +- es-app/src/FileFilterIndex.cpp | 377 ++++++++++++++++++ es-app/src/FileFilterIndex.h | 79 ++++ es-app/src/Gamelist.cpp | 8 + es-app/src/SystemData.cpp | 8 + es-app/src/SystemData.h | 6 + es-app/src/guis/GuiGamelistFilter.cpp | 112 ++++++ es-app/src/guis/GuiGamelistFilter.h | 34 ++ es-app/src/guis/GuiGamelistOptions.cpp | 127 ++++-- es-app/src/guis/GuiGamelistOptions.h | 4 + es-app/src/guis/GuiMetaDataEd.cpp | 6 + es-app/src/views/ViewController.cpp | 14 +- .../src/views/gamelist/BasicGameListView.cpp | 25 +- .../src/views/gamelist/GridGameListView.cpp | 4 +- .../views/gamelist/ISimpleGameListView.cpp | 13 +- .../src/views/gamelist/VideoGameListView.cpp | 2 + es-core/src/components/ComponentList.cpp | 5 +- es-core/src/components/OptionListComponent.h | 18 + 20 files changed, 817 insertions(+), 69 deletions(-) create mode 100644 es-app/src/FileFilterIndex.cpp create mode 100644 es-app/src/FileFilterIndex.h create mode 100644 es-app/src/guis/GuiGamelistFilter.cpp create mode 100644 es-app/src/guis/GuiGamelistFilter.h diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 04cbf509c..1ae077006 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -10,6 +10,7 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.h ${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Gamelist.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/FileFilterIndex.h # GuiComponents ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.h @@ -26,6 +27,7 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSettings.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.h # Scrapers ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.h @@ -57,6 +59,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Gamelist.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/FileFilterIndex.cpp # GuiComponents ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.cpp @@ -72,6 +75,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSettings.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.cpp # Scrapers ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.cpp diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 798909ea3..53cae33a3 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -81,6 +81,26 @@ const std::string& FileData::getThumbnailPath() const return metadata.get("image"); } +const std::vector& FileData::getChildrenListToDisplay() { + + FileFilterIndex* idx = mSystem->getIndex(); + if (idx->isFiltered()) { + mFilteredChildren.clear(); + for(auto it = mChildren.begin(); it != mChildren.end(); it++) + { + if (idx->showFile((*it))) { + mFilteredChildren.push_back(*it); + } + } + + return mFilteredChildren; + } + else + { + return mChildren; + } +} + const std::string& FileData::getVideoPath() const { return metadata.get("video"); @@ -91,19 +111,22 @@ const std::string& FileData::getMarqueePath() const return metadata.get("marquee"); } - -std::vector FileData::getFilesRecursive(unsigned int typeMask) const +std::vector FileData::getFilesRecursive(unsigned int typeMask, bool displayedOnly) const { std::vector out; + FileFilterIndex* idx = mSystem->getIndex(); for(auto it = mChildren.begin(); it != mChildren.end(); it++) { if((*it)->getType() & typeMask) - out.push_back(*it); + { + if (!displayedOnly || !idx->isFiltered() || idx->showFile(*it)) + out.push_back(*it); + } if((*it)->getChildren().size() > 0) { - std::vector subchildren = (*it)->getFilesRecursive(typeMask); + std::vector subchildren = (*it)->getFilesRecursive(typeMask, displayedOnly); out.insert(out.end(), subchildren.cbegin(), subchildren.cend()); } } diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index bd59ac071..3dd3e059c 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -11,7 +11,8 @@ class SystemData; enum FileType { GAME = 1, // Cannot have children. - FOLDER = 2 + FOLDER = 2, + PLACEHOLDER = 3 }; enum FileChangeType @@ -48,11 +49,14 @@ public: virtual const std::string& getVideoPath() const; virtual const std::string& getMarqueePath() const; - std::vector getFilesRecursive(unsigned int typeMask) const; + const std::vector& getChildrenListToDisplay(); + std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; void addChild(FileData* file); // Error if mType != FOLDER void removeChild(FileData* file); //Error if mType != FOLDER + inline bool isPlaceHolder() { return mType == PLACEHOLDER; }; + // Returns our best guess at the "real" name for this file (will attempt to perform MAME name translation) std::string getDisplayName() const; @@ -82,4 +86,5 @@ private: FileData* mParent; std::unordered_map mChildrenByFilename; std::vector mChildren; + std::vector mFilteredChildren; }; diff --git a/es-app/src/FileFilterIndex.cpp b/es-app/src/FileFilterIndex.cpp new file mode 100644 index 000000000..a02e22d18 --- /dev/null +++ b/es-app/src/FileFilterIndex.cpp @@ -0,0 +1,377 @@ +#include "FileFilterIndex.h" + +#define UNKNOWN_LABEL "UNKNOWN" +#define INCLUDE_UNKNOWN false; + +FileFilterIndex::FileFilterIndex() + : filterByGenre(false), filterByPlayers(false), filterByPubDev(false), filterByRatings(false) +{ + FilterDataDecl filterDecls[] = { + //type //allKeys //filteredBy //filteredKeys //primaryKey //hasSecondaryKey //secondaryKey //menuLabel + { GENRE_FILTER, &genreIndexAllKeys, &filterByGenre, &genreIndexFilteredKeys, "genre", true, "genre", "GENRE" }, + { PLAYER_FILTER, &playersIndexAllKeys, &filterByPlayers, &playersIndexFilteredKeys, "players", false, "", "PLAYERS" }, + { PUBDEV_FILTER, &pubDevIndexAllKeys, &filterByPubDev, &pubDevIndexFilteredKeys, "developer", true, "publisher", "PUBLISHER / DEVELOPER" }, + { RATINGS_FILTER, &ratingsIndexAllKeys, &filterByRatings, &ratingsIndexFilteredKeys, "rating", false, "", "RATING" } + }; + + filterDataDecl = std::vector(filterDecls, filterDecls + sizeof(filterDecls) / sizeof(filterDecls[0])); +} + +FileFilterIndex::~FileFilterIndex() +{ + clearIndex(genreIndexAllKeys); + clearIndex(playersIndexAllKeys); + clearIndex(pubDevIndexAllKeys); + clearIndex(ratingsIndexAllKeys); + +} + +std::vector& FileFilterIndex::getFilterDataDecls() +{ + return filterDataDecl; +} + +std::string FileFilterIndex::getIndexableKey(FileData* game, FilterIndexType type, bool getSecondary) +{ + std::string key = ""; + switch(type) + { + case GENRE_FILTER: + { + key = strToUpper(game->metadata.get("genre")); + boost::trim(key); + if (getSecondary && !key.empty()) { + std::istringstream f(key); + std::string newKey; + getline(f, newKey, '/'); + if (!newKey.empty() && newKey != key) + { + key = newKey; + } + else + { + key = std::string(); + } + } + break; + } + case PLAYER_FILTER: + { + if (getSecondary) + break; + + key = game->metadata.get("players"); + break; + } + case PUBDEV_FILTER: + { + key = strToUpper(game->metadata.get("publisher")); + boost::trim(key); + + if ((getSecondary && !key.empty()) || (!getSecondary && key.empty())) + key = strToUpper(game->metadata.get("developer")); + else + key = strToUpper(game->metadata.get("publisher")); + break; + } + case RATINGS_FILTER: + { + int ratingNumber = 0; + if (!getSecondary) + { + std::string ratingString = game->metadata.get("rating"); + if (!ratingString.empty()) { + try { + ratingNumber = boost::math::iround(std::stod(ratingString)*5); + if (ratingNumber < 0) + ratingNumber = 0; + + key = std::to_string(ratingNumber) + " STARS"; + } + catch (int e) + { + LOG(LogError) << "Error parsing Rating (invalid value, expected decimal): " << ratingString; + } + } + } + break; + } + } + boost::trim(key); + if (key.empty() || (type == RATINGS_FILTER && key == "0 STARS")) { + key = UNKNOWN_LABEL; + } + return key; +} + +void FileFilterIndex::addToIndex(FileData* game) +{ + manageGenreEntryInIndex(game); + managePlayerEntryInIndex(game); + managePubDevEntryInIndex(game); + manageRatingsEntryInIndex(game); +} + +void FileFilterIndex::removeFromIndex(FileData* game) +{ + manageGenreEntryInIndex(game, true); + managePlayerEntryInIndex(game, true); + managePubDevEntryInIndex(game, true); + manageRatingsEntryInIndex(game, true); +} + +void FileFilterIndex::setFilter(FilterIndexType type, std::vector* values) +{ + // test if it exists before setting + if(type == NONE) + { + clearAllFilters(); + } + else + { + for (std::vector::iterator it = filterDataDecl.begin(); it != filterDataDecl.end(); ++it ) { + if ((*it).type == type) + { + FilterDataDecl filterData = (*it); + *(filterData.filteredByRef) = values->size() > 0; + filterData.currentFilteredKeys->clear(); + for (std::vector::iterator vit = values->begin(); vit != values->end(); ++vit ) { + // check if exists + if (filterData.allIndexKeys->find(*vit) != filterData.allIndexKeys->end()) { + filterData.currentFilteredKeys->push_back(std::string(*vit)); + } + } + } + } + } + return; +} + +void FileFilterIndex::clearAllFilters() +{ + for (std::vector::iterator it = filterDataDecl.begin(); it != filterDataDecl.end(); ++it ) { + FilterDataDecl filterData = (*it); + *(filterData.filteredByRef) = false; + filterData.currentFilteredKeys->clear(); + } + return; +} + +void FileFilterIndex::debugPrintIndexes() +{ + LOG(LogInfo) << "Printing Indexes..."; + for (auto x: playersIndexAllKeys) { + LOG(LogInfo) << "Multiplayer Index: " << x.first << ": " << x.second; + } + for (auto x: genreIndexAllKeys) { + LOG(LogInfo) << "Genre Index: " << x.first << ": " << x.second; + } + for (auto x: ratingsIndexAllKeys) { + LOG(LogInfo) << "Ratings Index: " << x.first << ": " << x.second; + } + for (auto x: pubDevIndexAllKeys) { + LOG(LogInfo) << "PubDev Index: " << x.first << ": " << x.second; + } +} + +bool FileFilterIndex::showFile(FileData* game) +{ + // this shouldn't happen, but just in case let's get it out of the way + if (!isFiltered()) + return true; + + // if folder, needs further inspection - i.e. see if folder contains at least one element + // that should be shown + if (game->getType() == FOLDER) { + std::vector children = game->getChildren(); + // iterate through all of the children, until there's a match + + for (std::vector::iterator it = children.begin(); it != children.end(); ++it ) { + if (showFile(*it)) + { + return true; + } + } + return false; + } + + bool keepGoing = false; + + for (std::vector::iterator it = filterDataDecl.begin(); it != filterDataDecl.end(); ++it ) { + FilterDataDecl filterData = (*it); + if(*(filterData.filteredByRef)) { + // try to find a match + std::string key = getIndexableKey(game, filterData.type, false); + keepGoing = isKeyBeingFilteredBy(key, filterData.type); + + // if we didn't find a match, try for secondary keys - i.e. publisher and dev, or first genre + if (!keepGoing) { + if (!filterData.hasSecondaryKey) + { + return false; + } + std::string secKey = getIndexableKey(game, filterData.type, true); + if (secKey != UNKNOWN_LABEL) + { + keepGoing = isKeyBeingFilteredBy(secKey, filterData.type); + } + } + // if still nothing, then it's not a match + if (!keepGoing) + return false; + + } + + } + + return keepGoing; +} + +bool FileFilterIndex::isKeyBeingFilteredBy(std::string key, FilterIndexType type) { + const FilterIndexType filterTypes[4] = { PLAYER_FILTER, RATINGS_FILTER, GENRE_FILTER, PUBDEV_FILTER }; + std::vector filterKeysList[4] = { playersIndexFilteredKeys, ratingsIndexFilteredKeys, genreIndexFilteredKeys, pubDevIndexFilteredKeys }; + + for (int i = 0; i < 4; i++) { + if (filterTypes[i] == type) { + for (std::vector::iterator it = filterKeysList[i].begin(); it != filterKeysList[i].end(); ++it ) { + if (key == (*it)) + { + return true; + } + } + return false; + } + } + + return false; +} + +void FileFilterIndex::manageGenreEntryInIndex(FileData* game, bool remove) +{ + + std::string key = getIndexableKey(game, GENRE_FILTER, false); + + // flag for including unknowns + bool includeUnknown = INCLUDE_UNKNOWN; + + // only add unknown in pubdev IF both dev and pub are empty + if (!includeUnknown && (key == UNKNOWN_LABEL || key == "BIOS")) { + // no valid genre info found + return; + } + + manageIndexEntry(&genreIndexAllKeys, key, remove); + + key = getIndexableKey(game, GENRE_FILTER, true); + if (!includeUnknown && key == UNKNOWN_LABEL) + { + manageIndexEntry(&genreIndexAllKeys, key, remove); + } +} + +void FileFilterIndex::managePlayerEntryInIndex(FileData* game, bool remove) +{ + // flag for including unknowns + bool includeUnknown = INCLUDE_UNKNOWN; + std::string key = getIndexableKey(game, PLAYER_FILTER, false); + + // only add unknown in pubdev IF both dev and pub are empty + if (!includeUnknown && key == UNKNOWN_LABEL) { + // no valid player info found + return; + } + + manageIndexEntry(&playersIndexAllKeys, key, remove); +} + +void FileFilterIndex::managePubDevEntryInIndex(FileData* game, bool remove) +{ + std::string pub = getIndexableKey(game, PUBDEV_FILTER, false); + std::string dev = getIndexableKey(game, PUBDEV_FILTER, true); + + // flag for including unknowns + bool includeUnknown = INCLUDE_UNKNOWN; + bool unknownPub = false; + bool unknownDev = false; + + if (pub == UNKNOWN_LABEL) { + unknownPub = true; + } + if (dev == UNKNOWN_LABEL) { + unknownDev = true; + } + + if (!includeUnknown && unknownDev && unknownPub) { + // no valid rating info found + return; + } + + if (unknownDev && unknownPub) { + // if no info at all + manageIndexEntry(&pubDevIndexAllKeys, pub, remove); + } + else + { + if (!unknownDev) { + // if no info at all + manageIndexEntry(&pubDevIndexAllKeys, dev, remove); + } + if (!unknownPub) { + // if no info at all + manageIndexEntry(&pubDevIndexAllKeys, pub, remove); + } + } +} + +void FileFilterIndex::manageRatingsEntryInIndex(FileData* game, bool remove) +{ + std::string key = getIndexableKey(game, RATINGS_FILTER, false); + + // flag for including unknowns + bool includeUnknown = INCLUDE_UNKNOWN; + + if (!includeUnknown && key == UNKNOWN_LABEL) { + // no valid rating info found + return; + } + + manageIndexEntry(&ratingsIndexAllKeys, key, remove); +} + +void FileFilterIndex::manageIndexEntry(std::map* index, std::string key, bool remove) { + bool includeUnknown = INCLUDE_UNKNOWN; + if (!includeUnknown && key == UNKNOWN_LABEL) + return; + if (remove) { + // removing entry + if (index->find(key) == index->end()) + { + // this shouldn't happen + LOG(LogError) << "Couldn't find entry in index! " << key; + } + else + { + (index->at(key))--; + if(index->at(key) <= 0) { + index->erase(key); + } + } + } + else + { + // adding entry + if (index->find(key) == index->end()) + { + (*index)[key] = 1; + } + else + { + (index->at(key))++; + } + } +} + +void FileFilterIndex::clearIndex(std::map indexMap) +{ + indexMap.clear(); +} \ No newline at end of file diff --git a/es-app/src/FileFilterIndex.h b/es-app/src/FileFilterIndex.h new file mode 100644 index 000000000..fffdaccf2 --- /dev/null +++ b/es-app/src/FileFilterIndex.h @@ -0,0 +1,79 @@ +#pragma once + +#include +#include "FileData.h" +#include "Log.h" +#include +#include +#include +#include +#include "Util.h" + +enum FilterIndexType +{ + NONE, + GENRE_FILTER, + PLAYER_FILTER, + PUBDEV_FILTER, + RATINGS_FILTER +}; + +struct FilterDataDecl +{ + FilterIndexType type; // type of filter + std::map* allIndexKeys; // all possible filters for this type + bool* filteredByRef; // is it filtered by this type + std::vector* currentFilteredKeys; // current keys being filtered for + std::string primaryKey; // primary key in metadata + bool hasSecondaryKey; // has secondary key for comparison + std::string secondaryKey; // what's the secondary key + std::string menuLabel; // text to show in menu +}; + +class FileFilterIndex +{ +public: + FileFilterIndex(); + ~FileFilterIndex(); + void addToIndex(FileData* game); + void removeFromIndex(FileData* game); + void setFilter(FilterIndexType type, std::vector* values); + void clearAllFilters(); + void debugPrintIndexes(); + bool showFile(FileData* game); + bool isFiltered() { return (filterByGenre || filterByPlayers || filterByPubDev || filterByRatings); }; + bool isKeyBeingFilteredBy(std::string key, FilterIndexType type); + std::map* getGenreAllIndexedKeys() { return &genreIndexAllKeys; }; + std::vector* getGenreFilteredKeys() { return &genreIndexFilteredKeys; }; + std::vector& getFilterDataDecls(); +private: + std::vector filterDataDecl; + std::string getIndexableKey(FileData* game, FilterIndexType type, bool getSecondary); + + void manageGenreEntryInIndex(FileData* game, bool remove = false); + void managePlayerEntryInIndex(FileData* game, bool remove = false); + void managePubDevEntryInIndex(FileData* game, bool remove = false); + void manageRatingsEntryInIndex(FileData* game, bool remove = false); + + void manageIndexEntry(std::map* index, std::string key, bool remove); + + void clearIndex(std::map indexMap); + + bool filterByGenre; + bool filterByPlayers; + bool filterByPubDev; + bool filterByRatings; + + std::map genreIndexAllKeys; + std::map playersIndexAllKeys; + std::map pubDevIndexAllKeys; + std::map ratingsIndexAllKeys; + + std::vector genreIndexFilteredKeys; + std::vector playersIndexFilteredKeys; + std::vector pubDevIndexFilteredKeys; + std::vector ratingsIndexFilteredKeys; + + FileData* mRootFolder; + +}; \ No newline at end of file diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index e5bc7da3b..99ecad3b1 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -141,6 +141,14 @@ void parseGamelist(SystemData* system) file->metadata.set("name", defaultName); file->metadata.resetChangedFlag(); + + // index if it's a game! + if(type == GAME) + { + FileFilterIndex* index = system->getIndex(); + index->addToIndex(file); + } + } } } diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index cb46f1c47..a5f642989 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -36,6 +36,8 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, con mPlatformIds = platformIds; mThemeFolder = themeFolder; + mFilterIndex = new FileFilterIndex(); + mRootFolder = new FileData(FOLDER, mStartPath, this); mRootFolder->metadata.set("name", mFullName); @@ -59,6 +61,7 @@ SystemData::~SystemData() } delete mRootFolder; + delete mFilterIndex; } @@ -429,6 +432,11 @@ unsigned int SystemData::getGameCount() const return mRootFolder->getFilesRecursive(GAME).size(); } +unsigned int SystemData::getDisplayedGameCount() const +{ + return mRootFolder->getFilesRecursive(GAME, true).size(); +} + void SystemData::loadTheme() { mTheme = std::make_shared(); diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 2a0ac8188..2e332f9ad 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -7,6 +7,7 @@ #include "MetaData.h" #include "PlatformId.h" #include "ThemeData.h" +#include "FileFilterIndex.h" class SystemData { @@ -32,6 +33,7 @@ public: std::string getThemePath() const; unsigned int getGameCount() const; + unsigned int getDisplayedGameCount() const; void launchGame(Window* window, FileData* game); @@ -64,6 +66,8 @@ public: // Load or re-load theme. void loadTheme(); + FileFilterIndex* getIndex() { return mFilterIndex; }; + private: std::string mName; std::string mFullName; @@ -76,5 +80,7 @@ private: void populateFolder(FileData* folder); + FileFilterIndex* mFilterIndex; + FileData* mRootFolder; }; diff --git a/es-app/src/guis/GuiGamelistFilter.cpp b/es-app/src/guis/GuiGamelistFilter.cpp new file mode 100644 index 000000000..1112b2826 --- /dev/null +++ b/es-app/src/guis/GuiGamelistFilter.cpp @@ -0,0 +1,112 @@ +#include "guis/GuiGamelistFilter.h" +#include "guis/GuiMsgBox.h" +#include "views/ViewController.h" + +#include "components/TextComponent.h" +#include "components/OptionListComponent.h" + +GuiGamelistFilter::GuiGamelistFilter(Window* window, SystemData* system) : GuiComponent(window), mMenu(window, "FILTER GAMELIST BY"), mSystem(system) +{ + initializeMenu(); +} + +void GuiGamelistFilter::initializeMenu() +{ + addChild(&mMenu); + + // get filters from system + + mFilterIndex = mSystem->getIndex(); + + ComponentListRow row; + + // show filtered menu + row.elements.clear(); + row.addElement(std::make_shared(mWindow, "RESET ALL FILTERS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.makeAcceptInputHandler(std::bind(&GuiGamelistFilter::resetAllFilters, this)); + mMenu.addRow(row); + row.elements.clear(); + + addFiltersToMenu(); + + mMenu.addButton("BACK", "back", std::bind(&GuiGamelistFilter::applyFilters, this)); + + mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); +} + +void GuiGamelistFilter::resetAllFilters() +{ + mFilterIndex->clearAllFilters(); + for (std::map >>::iterator it = mFilterOptions.begin(); it != mFilterOptions.end(); ++it ) { + std::shared_ptr< OptionListComponent > optionList = it->second; + optionList->selectNone(); + } +} + +GuiGamelistFilter::~GuiGamelistFilter() +{ + mFilterOptions.clear(); +} + +void GuiGamelistFilter::addFiltersToMenu() +{ + std::vector decls = mFilterIndex->getFilterDataDecls(); + for (std::vector::iterator it = decls.begin(); it != decls.end(); ++it ) { + + FilterIndexType type = (*it).type; // type of filter + std::map* allKeys = (*it).allIndexKeys; // all possible filters for this type + std::vector* allFilteredKeys = (*it).currentFilteredKeys; // current keys being filtered for + std::string menuLabel = (*it).menuLabel; // text to show in menu + std::shared_ptr< OptionListComponent > optionList; + + + // add filters (with first one selected) + ComponentListRow row; + + // add genres + optionList = std::make_shared< OptionListComponent >(mWindow, menuLabel, true); + for(auto it: *allKeys) + { + optionList->add(it.first, it.first, mFilterIndex->isKeyBeingFilteredBy(it.first, type)); + } + if (allKeys->size() > 0) + mMenu.addWithLabel(menuLabel, optionList); + + mFilterOptions[type] = optionList; + } +} + +void GuiGamelistFilter::applyFilters() +{ + std::vector decls = mFilterIndex->getFilterDataDecls(); + for (std::map >>::iterator it = mFilterOptions.begin(); it != mFilterOptions.end(); ++it ) { + std::shared_ptr< OptionListComponent > optionList = it->second; + std::vector filters = optionList->getSelectedObjects(); + mFilterIndex->setFilter(it->first, &filters); + } + + delete this; + +} + +bool GuiGamelistFilter::input(InputConfig* config, Input input) +{ + bool consumed = GuiComponent::input(config, input); + if(consumed) + return true; + + if(config->isMappedTo("b", input) && input.value != 0) + { + applyFilters(); + } + + + return false; +} + +std::vector GuiGamelistFilter::getHelpPrompts() +{ + std::vector prompts = mMenu.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", "back")); + return prompts; +} diff --git a/es-app/src/guis/GuiGamelistFilter.h b/es-app/src/guis/GuiGamelistFilter.h new file mode 100644 index 000000000..8462b0400 --- /dev/null +++ b/es-app/src/guis/GuiGamelistFilter.h @@ -0,0 +1,34 @@ +#pragma once + +#include "GuiComponent.h" +#include "SystemData.h" +#include "components/MenuComponent.h" +#include "FileFilterIndex.h" +#include "Log.h" + + +template +class OptionListComponent; + + +class GuiGamelistFilter : public GuiComponent +{ +public: + GuiGamelistFilter(Window* window, SystemData* system); + ~GuiGamelistFilter(); + bool input(InputConfig* config, Input input) override; + + virtual std::vector getHelpPrompts() override; + +private: + void initializeMenu(); + void applyFilters(); + void resetAllFilters(); + void addFiltersToMenu(); + + std::map >> mFilterOptions; + + MenuComponent mMenu; + SystemData* mSystem; + FileFilterIndex* mFilterIndex; +}; diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 22548c200..4c65b9dfb 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -4,35 +4,21 @@ #include "views/ViewController.h" GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : GuiComponent(window), - mSystem(system), - mMenu(window, "OPTIONS") + mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false) { addChild(&mMenu); - // jump to letter - char curChar = toupper(getGamelist()->getCursor()->getName()[0]); - if(curChar < 'A' || curChar > 'Z') - curChar = 'A'; - - mJumpToLetterList = std::make_shared(mWindow, "JUMP TO LETTER", false); - for(char c = 'A'; c <= 'Z'; c++) - mJumpToLetterList->add(std::string(1, c), c, c == curChar); - + // check it's not a placeholder folder - if it is, only show "Filter Options" + FileData* file = getGamelist()->getCursor(); + fromPlaceholder = file->isPlaceHolder(); + bool isFiltered = system->getIndex()->isFiltered(); ComponentListRow row; - row.addElement(std::make_shared(mWindow, "JUMP TO LETTER", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(mJumpToLetterList, false); - row.input_handler = [&](InputConfig* config, Input input) { - if(config->isMappedTo("a", input) && input.value) - { - jumpToLetter(); - return true; - } - else if(mJumpToLetterList->input(config, input)) - { - return true; - } - return false; - }; + + // show filtered menu + 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); row.elements.clear(); @@ -48,23 +34,53 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui }; mMenu.addRow(row); - // sort list by - mListSort = std::make_shared(mWindow, "SORT GAMES BY", false); - for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) - { - const FileData::SortType& sort = FileSorts::SortTypes.at(i); - mListSort->add(sort.description, &sort, i == 0); // TODO - actually make the sort type persistent + if (!fromPlaceholder) { + + if (!isFiltered) { + // jump to letter + row.elements.clear(); + char curChar = toupper(getGamelist()->getCursor()->getName()[0]); + if(curChar < 'A' || curChar > 'Z') + curChar = 'A'; + + mJumpToLetterList = std::make_shared(mWindow, "JUMP TO LETTER", false); + for(char c = 'A'; c <= 'Z'; c++) + mJumpToLetterList->add(std::string(1, c), c, c == curChar); + + row.addElement(std::make_shared(mWindow, "JUMP TO LETTER", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(mJumpToLetterList, false); + row.input_handler = [&](InputConfig* config, Input input) { + if(config->isMappedTo("a", input) && input.value) + { + jumpToLetter(); + return true; + } + else if(mJumpToLetterList->input(config, input)) + { + return true; + } + return false; + }; + mMenu.addRow(row); + } + + // sort list by + mListSort = std::make_shared(mWindow, "SORT GAMES BY", false); + for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) + { + const FileData::SortType& sort = FileSorts::SortTypes.at(i); + mListSort->add(sort.description, &sort, i == 0); // TODO - actually make the sort type persistent + } + + mMenu.addWithLabel("SORT GAMES BY", mListSort); + + 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); } - mMenu.addWithLabel("SORT GAMES BY", mListSort); - - // edit game metadata - 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); - // center the menu setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); @@ -73,13 +89,36 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui GuiGamelistOptions::~GuiGamelistOptions() { // apply sort - FileData* root = getGamelist()->getCursor()->getSystem()->getRootFolder(); - root->sort(*mListSort->getSelected()); // will also recursively sort children + if (!fromPlaceholder) { + FileData* root = getGamelist()->getCursor()->getSystem()->getRootFolder(); + root->sort(*mListSort->getSelected()); // will also recursively sort children - // notify that the root folder was sorted - getGamelist()->onFileChanged(root, FILE_SORTED); + // notify that the root folder was sorted + getGamelist()->onFileChanged(root, FILE_SORTED); + } + if (mFiltersChanged) + { + if (!fromPlaceholder) { + FileData* root = getGamelist()->getCursor()->getSystem()->getRootFolder(); + getGamelist()->onFileChanged(root, FILE_SORTED); + } + else + { + // 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); + } + } } +void GuiGamelistOptions::openGamelistFilter() +{ + mFiltersChanged = true; + GuiGamelistFilter* ggf = new GuiGamelistFilter(mWindow, mSystem); + mWindow->pushGui(ggf); +} + void GuiGamelistOptions::openMetaDataEd() { // open metadata editor diff --git a/es-app/src/guis/GuiGamelistOptions.h b/es-app/src/guis/GuiGamelistOptions.h index 2ce685c9e..151f4ab6c 100644 --- a/es-app/src/guis/GuiGamelistOptions.h +++ b/es-app/src/guis/GuiGamelistOptions.h @@ -1,6 +1,7 @@ #include "GuiComponent.h" #include "components/MenuComponent.h" #include "components/OptionListComponent.h" +#include "GuiGamelistFilter.h" #include "FileSorts.h" class IGameListView; @@ -15,6 +16,7 @@ public: virtual std::vector getHelpPrompts() override; private: + void openGamelistFilter(); void openMetaDataEd(); void jumpToLetter(); @@ -28,4 +30,6 @@ private: SystemData* mSystem; IGameListView* getGamelist(); + bool fromPlaceholder; + bool mFiltersChanged; }; diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 11367c2ad..7630cb6a5 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -170,6 +170,9 @@ void GuiMetaDataEd::onSizeChanged() void GuiMetaDataEd::save() { + // remove game from index + mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); + for(unsigned int i = 0; i < mEditors.size(); i++) { if(mMetaDataDecl.at(i).isStatistic) @@ -178,6 +181,9 @@ void GuiMetaDataEd::save() mMetaData->set(mMetaDataDecl.at(i).key, mEditors.at(i)->getValue()); } + // enter game in index + mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); + if(mSavedCallback) mSavedCallback(); } diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index de35e614e..15d50043f 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -125,7 +125,7 @@ void ViewController::goToRandomGame() for(auto it = SystemData::sSystemVector.begin(); it != SystemData::sSystemVector.end(); it++) { if ((*it)->getName() != "retropie") - total += (*it)->getGameCount(); + total += (*it)->getDisplayedGameCount(); } // get random number in range @@ -135,14 +135,14 @@ void ViewController::goToRandomGame() { if ((*it)->getName() != "retropie") { - if ((target - (int)(*it)->getGameCount()) >= 0) + if ((target - (int)(*it)->getDisplayedGameCount()) >= 0) { - target -= (int)(*it)->getGameCount(); + target -= (int)(*it)->getDisplayedGameCount(); } else { goToGameList(*it); - std::vector list = (*it)->getRootFolder()->getFilesRecursive(GAME); + std::vector list = (*it)->getRootFolder()->getFilesRecursive(GAME, true); getGameListView(*it)->setCursor(list.at(target)); return; } @@ -401,7 +401,11 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) system->loadTheme(); std::shared_ptr newView = getGameListView(system); - newView->setCursor(cursor); + + // to counter having come from a placeholder + if (!cursor->isPlaceHolder()) { + newView->setCursor(cursor); + } if(isCurrent) mCurrentView = newView; diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 0cd5b023c..23a39ead3 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -5,6 +5,7 @@ #include "ThemeData.h" #include "SystemData.h" #include "Settings.h" +#include "FileFilterIndex.h" BasicGameListView::BasicGameListView(Window* window, FileData* root) : ISimpleGameListView(window, root), mList(window) @@ -13,7 +14,7 @@ BasicGameListView::BasicGameListView(Window* window, FileData* root) mList.setPosition(0, mSize.y() * 0.2f); addChild(&mList); - populateList(root->getChildren()); + populateList(root->getChildrenListToDisplay()); } void BasicGameListView::onThemeChanged(const std::shared_ptr& theme) @@ -38,12 +39,20 @@ void BasicGameListView::onFileChanged(FileData* file, FileChangeType change) void BasicGameListView::populateList(const std::vector& files) { mList.clear(); - - mHeaderText.setText(files.at(0)->getSystem()->getFullName()); - - for(auto it = files.begin(); it != files.end(); it++) + if (files.size() > 0) { - mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + mHeaderText.setText(files.at(0)->getSystem()->getFullName()); + + for(auto it = files.begin(); it != files.end(); it++) + { + mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + } + } + else + { + // empty list - add a placeholder + FileData* placeholder = new FileData(PLACEHOLDER, "", this->mRoot->getSystem()); + mList.add(placeholder->getName(), placeholder, (placeholder->getType() == PLACEHOLDER)); } } @@ -54,9 +63,11 @@ FileData* BasicGameListView::getCursor() void BasicGameListView::setCursor(FileData* cursor) { + if (cursor->isPlaceHolder()) + return; if(!mList.setCursor(cursor)) { - populateList(cursor->getParent()->getChildren()); + populateList(cursor->getParent()->getChildrenListToDisplay()); mList.setCursor(cursor); // update our cursor stack in case our cursor just got set to some folder we weren't in before diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index c34b4b8ba..77f9a6f86 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -10,7 +10,7 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : ISimpleGame mGrid.setSize(mSize.x(), mSize.y() * 0.8f); addChild(&mGrid); - populateList(root->getChildren()); + populateList(root->getChildrenListToDisplay()); } FileData* GridGameListView::getCursor() @@ -22,7 +22,7 @@ void GridGameListView::setCursor(FileData* file) { if(!mGrid.setCursor(file)) { - populateList(file->getParent()->getChildren()); + populateList(file->getParent()->getChildrenListToDisplay()); mGrid.setCursor(file); } } diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 6fae30059..fa9b6944b 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -47,8 +47,15 @@ void ISimpleGameListView::onFileChanged(FileData* file, FileChangeType change) // we could be tricky here to be efficient; // but this shouldn't happen very often so we'll just always repopulate FileData* cursor = getCursor(); - populateList(cursor->getParent()->getChildren()); - setCursor(cursor); + if (!cursor->isPlaceHolder()) { + populateList(cursor->getParent()->getChildrenListToDisplay()); + setCursor(cursor); + } + else + { + populateList(mRoot->getChildrenListToDisplay()); + setCursor(cursor); + } } bool ISimpleGameListView::input(InputConfig* config, Input input) @@ -67,7 +74,7 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) if(cursor->getChildren().size() > 0) { mCursorStack.push(cursor); - populateList(cursor->getChildren()); + populateList(cursor->getChildrenListToDisplay()); } } diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index bb4bf3b5b..266225231 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -248,7 +248,9 @@ void VideoGameListView::updateInfoPanel() thumbnail_path.insert(0, getHomePath()); } if (!mVideo.setVideo(video_path)) + { mVideo.setDefaultVideo(); + } mVideoPlaying = true; mVideo.setImage(thumbnail_path); diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index a16bcb097..f4a313c4d 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -81,11 +81,12 @@ bool ComponentList::input(InputConfig* config, Input input) }else if(config->isMappedTo("down", input)) { return listInput(input.value != 0 ? 1 : 0); + }else if(config->isMappedTo("pageup", input)) { - return listInput(input.value != 0 ? -7 : 0); + return listInput(input.value != 0 ? -6 : 0); }else if(config->isMappedTo("pagedown", input)){ - return listInput(input.value != 0 ? 7 : 0); + return listInput(input.value != 0 ? 6 : 0); } return false; diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index 40ff34fff..45d12f336 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -251,6 +251,24 @@ public: onSelectedChanged(); } + void selectAll() + { + for(unsigned int i = 0; i < mEntries.size(); i++) + { + mEntries.at(i).selected = true; + } + onSelectedChanged(); + } + + void selectNone() + { + for(unsigned int i = 0; i < mEntries.size(); i++) + { + mEntries.at(i).selected = false; + } + onSelectedChanged(); + } + private: unsigned int getSelectedId() {