Merge pull request #115 from pjft/RetroPie-master-filter-rebase

Adding generic gamelist filter funcionality for ES
This commit is contained in:
Jools Wills 2017-05-04 16:25:57 +01:00 committed by GitHub
commit a909f10b2d
20 changed files with 817 additions and 69 deletions

View file

@ -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

View file

@ -81,6 +81,26 @@ const std::string& FileData::getThumbnailPath() const
return metadata.get("image");
}
const std::vector<FileData*>& 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*> FileData::getFilesRecursive(unsigned int typeMask) const
std::vector<FileData*> FileData::getFilesRecursive(unsigned int typeMask, bool displayedOnly) const
{
std::vector<FileData*> 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<FileData*> subchildren = (*it)->getFilesRecursive(typeMask);
std::vector<FileData*> subchildren = (*it)->getFilesRecursive(typeMask, displayedOnly);
out.insert(out.end(), subchildren.cbegin(), subchildren.cend());
}
}

View file

@ -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<FileData*> getFilesRecursive(unsigned int typeMask) const;
const std::vector<FileData*>& getChildrenListToDisplay();
std::vector<FileData*> 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<std::string,FileData*> mChildrenByFilename;
std::vector<FileData*> mChildren;
std::vector<FileData*> mFilteredChildren;
};

View file

@ -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<FilterDataDecl>(filterDecls, filterDecls + sizeof(filterDecls) / sizeof(filterDecls[0]));
}
FileFilterIndex::~FileFilterIndex()
{
clearIndex(genreIndexAllKeys);
clearIndex(playersIndexAllKeys);
clearIndex(pubDevIndexAllKeys);
clearIndex(ratingsIndexAllKeys);
}
std::vector<FilterDataDecl>& 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<std::string>* values)
{
// test if it exists before setting
if(type == NONE)
{
clearAllFilters();
}
else
{
for (std::vector<FilterDataDecl>::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<std::string>::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<FilterDataDecl>::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<FileData*> children = game->getChildren();
// iterate through all of the children, until there's a match
for (std::vector<FileData*>::iterator it = children.begin(); it != children.end(); ++it ) {
if (showFile(*it))
{
return true;
}
}
return false;
}
bool keepGoing = false;
for (std::vector<FilterDataDecl>::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<std::string> filterKeysList[4] = { playersIndexFilteredKeys, ratingsIndexFilteredKeys, genreIndexFilteredKeys, pubDevIndexFilteredKeys };
for (int i = 0; i < 4; i++) {
if (filterTypes[i] == type) {
for (std::vector<std::string>::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<std::string, int>* 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<std::string, int> indexMap)
{
indexMap.clear();
}

View file

@ -0,0 +1,79 @@
#pragma once
#include <map>
#include "FileData.h"
#include "Log.h"
#include <boost/math/special_functions/round.hpp>
#include <boost/algorithm/string.hpp>
#include <sstream>
#include <iostream>
#include "Util.h"
enum FilterIndexType
{
NONE,
GENRE_FILTER,
PLAYER_FILTER,
PUBDEV_FILTER,
RATINGS_FILTER
};
struct FilterDataDecl
{
FilterIndexType type; // type of filter
std::map<std::string, int>* allIndexKeys; // all possible filters for this type
bool* filteredByRef; // is it filtered by this type
std::vector<std::string>* 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<std::string>* 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<std::string, int>* getGenreAllIndexedKeys() { return &genreIndexAllKeys; };
std::vector<std::string>* getGenreFilteredKeys() { return &genreIndexFilteredKeys; };
std::vector<FilterDataDecl>& getFilterDataDecls();
private:
std::vector<FilterDataDecl> 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<std::string, int>* index, std::string key, bool remove);
void clearIndex(std::map<std::string, int> indexMap);
bool filterByGenre;
bool filterByPlayers;
bool filterByPubDev;
bool filterByRatings;
std::map<std::string, int> genreIndexAllKeys;
std::map<std::string, int> playersIndexAllKeys;
std::map<std::string, int> pubDevIndexAllKeys;
std::map<std::string, int> ratingsIndexAllKeys;
std::vector<std::string> genreIndexFilteredKeys;
std::vector<std::string> playersIndexFilteredKeys;
std::vector<std::string> pubDevIndexFilteredKeys;
std::vector<std::string> ratingsIndexFilteredKeys;
FileData* mRootFolder;
};

View file

@ -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);
}
}
}
}

View file

@ -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<ThemeData>();

View file

@ -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;
};

View file

@ -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<TextComponent>(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<FilterIndexType, std::shared_ptr< OptionListComponent<std::string> >>::iterator it = mFilterOptions.begin(); it != mFilterOptions.end(); ++it ) {
std::shared_ptr< OptionListComponent<std::string> > optionList = it->second;
optionList->selectNone();
}
}
GuiGamelistFilter::~GuiGamelistFilter()
{
mFilterOptions.clear();
}
void GuiGamelistFilter::addFiltersToMenu()
{
std::vector<FilterDataDecl> decls = mFilterIndex->getFilterDataDecls();
for (std::vector<FilterDataDecl>::iterator it = decls.begin(); it != decls.end(); ++it ) {
FilterIndexType type = (*it).type; // type of filter
std::map<std::string, int>* allKeys = (*it).allIndexKeys; // all possible filters for this type
std::vector<std::string>* allFilteredKeys = (*it).currentFilteredKeys; // current keys being filtered for
std::string menuLabel = (*it).menuLabel; // text to show in menu
std::shared_ptr< OptionListComponent<std::string> > optionList;
// add filters (with first one selected)
ComponentListRow row;
// add genres
optionList = std::make_shared< OptionListComponent<std::string> >(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<FilterDataDecl> decls = mFilterIndex->getFilterDataDecls();
for (std::map<FilterIndexType, std::shared_ptr< OptionListComponent<std::string> >>::iterator it = mFilterOptions.begin(); it != mFilterOptions.end(); ++it ) {
std::shared_ptr< OptionListComponent<std::string> > optionList = it->second;
std::vector<std::string> 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<HelpPrompt> GuiGamelistFilter::getHelpPrompts()
{
std::vector<HelpPrompt> prompts = mMenu.getHelpPrompts();
prompts.push_back(HelpPrompt("b", "back"));
return prompts;
}

View file

@ -0,0 +1,34 @@
#pragma once
#include "GuiComponent.h"
#include "SystemData.h"
#include "components/MenuComponent.h"
#include "FileFilterIndex.h"
#include "Log.h"
template<typename T>
class OptionListComponent;
class GuiGamelistFilter : public GuiComponent
{
public:
GuiGamelistFilter(Window* window, SystemData* system);
~GuiGamelistFilter();
bool input(InputConfig* config, Input input) override;
virtual std::vector<HelpPrompt> getHelpPrompts() override;
private:
void initializeMenu();
void applyFilters();
void resetAllFilters();
void addFiltersToMenu();
std::map<FilterIndexType, std::shared_ptr< OptionListComponent<std::string> >> mFilterOptions;
MenuComponent mMenu;
SystemData* mSystem;
FileFilterIndex* mFilterIndex;
};

View file

@ -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<LetterList>(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<TextComponent>(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<TextComponent>(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<SortList>(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<LetterList>(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<TextComponent>(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<SortList>(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<TextComponent>(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<TextComponent>(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

View file

@ -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<HelpPrompt> getHelpPrompts() override;
private:
void openGamelistFilter();
void openMetaDataEd();
void jumpToLetter();
@ -28,4 +30,6 @@ private:
SystemData* mSystem;
IGameListView* getGamelist();
bool fromPlaceholder;
bool mFiltersChanged;
};

View file

@ -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();
}

View file

@ -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<FileData*> list = (*it)->getRootFolder()->getFilesRecursive(GAME);
std::vector<FileData*> 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<IGameListView> newView = getGameListView(system);
newView->setCursor(cursor);
// to counter having come from a placeholder
if (!cursor->isPlaceHolder()) {
newView->setCursor(cursor);
}
if(isCurrent)
mCurrentView = newView;

View file

@ -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<ThemeData>& theme)
@ -38,12 +39,20 @@ void BasicGameListView::onFileChanged(FileData* file, FileChangeType change)
void BasicGameListView::populateList(const std::vector<FileData*>& 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, "<No Results Found for Current Filter Criteria>", 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

View file

@ -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);
}
}

View file

@ -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());
}
}

View file

@ -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);

View file

@ -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;

View file

@ -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()
{