Changed 'emulationstation' folder to 'es-app' because I forgot executables don't have extensions on Linux

Half of the ES code has been missing for 5 days because I am incompetent
This commit is contained in:
Aloshi 2014-06-25 11:29:58 -05:00
parent dbdbcde6cd
commit bc72990f39
68 changed files with 37935 additions and 1 deletions

View file

@ -185,4 +185,4 @@ set(LIBRARY_OUTPUT_PATH ${dir} CACHE PATH "Build directory" FORCE)
add_subdirectory("external") add_subdirectory("external")
add_subdirectory("es-core") add_subdirectory("es-core")
add_subdirectory("emulationstation") add_subdirectory("es-app")

149
es-app/CMakeLists.txt Normal file
View file

@ -0,0 +1,149 @@
project("emulationstation")
set(ES_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/src/EmulationStation.h
${CMAKE_CURRENT_SOURCE_DIR}/src/FileData.h
${CMAKE_CURRENT_SOURCE_DIR}/src/FileSorts.h
${CMAKE_CURRENT_SOURCE_DIR}/src/MetaData.h
${CMAKE_CURRENT_SOURCE_DIR}/src/PlatformId.h
${CMAKE_CURRENT_SOURCE_DIR}/src/ScraperCmdLine.h
${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.h
${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Gamelist.h
# GuiComponents
${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/RatingComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ScraperSearchComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextListComponent.h
# Guis
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiFastSelect.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMetaDataEd.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGameScraper.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistOptions.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMenu.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSettings.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.h
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.h
# Scrapers
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.h
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBScraper.h
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/TheArchiveScraper.h
# Views
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/BasicGameListView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/DetailedGameListView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/IGameListView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/ISimpleGameListView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/GridGameListView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/SystemView.h
${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.h
# Animations
${CMAKE_CURRENT_SOURCE_DIR}/src/animations/LaunchAnimation.h
${CMAKE_CURRENT_SOURCE_DIR}/src/animations/MoveCameraAnimation.h
)
set(ES_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/FileData.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/FileSorts.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/main.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/MameNameMap.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/MetaData.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/PlatformId.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/ScraperCmdLine.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Gamelist.cpp
# GuiComponents
${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/RatingComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ScraperSearchComponent.cpp
# Guis
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiFastSelect.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMetaDataEd.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGameScraper.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistOptions.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMenu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiSettings.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.cpp
# Scrapers
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBScraper.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/TheArchiveScraper.cpp
# Views
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/BasicGameListView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/DetailedGameListView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/IGameListView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/ISimpleGameListView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/GridGameListView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/SystemView.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.cpp
)
#-------------------------------------------------------------------------------
# define OS specific sources and headers
if(MSVC)
LIST(APPEND ES_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/EmulationStation.rc
)
endif()
#-------------------------------------------------------------------------------
# define target
include_directories(${COMMON_INCLUDE_DIRS} ${CMAKE_CURRENT_SOURCE_DIR}/src)
add_executable(emulationstation ${ES_SOURCES} ${ES_HEADERS})
target_link_libraries(emulationstation ${COMMON_LIBRARIES} es-core)
# special properties for Windows builds
if(MSVC)
#show console in debug builds, but not in proper release builds
#Note that up to CMake 2.8.10 this feature is broken: http://public.kitware.com/Bug/view.php?id=12566
set_target_properties(emulationstation PROPERTIES LINK_FLAGS_DEBUG "/SUBSYSTEM:CONSOLE")
set_target_properties(emulationstation PROPERTIES COMPILE_DEFINITIONS_DEBUG "_CONSOLE")
set_target_properties(emulationstation PROPERTIES LINK_FLAGS_RELWITHDEBINFO "/SUBSYSTEM:CONSOLE")
set_target_properties(emulationstation PROPERTIES COMPILE_DEFINITIONS_RELWITHDEBINFO "_CONSOLE")
set_target_properties(emulationstation PROPERTIES LINK_FLAGS_RELEASE "/SUBSYSTEM:WINDOWS")
set_target_properties(emulationstation PROPERTIES LINK_FLAGS_MINSIZEREL "/SUBSYSTEM:WINDOWS")
endif()
#-------------------------------------------------------------------------------
# set up CPack install stuff so `make install` does something useful
install(TARGETS emulationstation
RUNTIME
DESTINATION bin)
INCLUDE(InstallRequiredSystemLibraries)
SET(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A flexible graphical emulator front-end")
SET(CPACK_PACKAGE_DESCRIPTION "EmulationStation is a flexible, graphical front-end designed for keyboardless navigation of your multi-platform retro game collection.")
SET(CPACK_RESOURCE_FILE LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE.md")
SET(CPACK_RESOURCE_FILE README "${CMAKE_CURRENT_SOURCE_DIR}/README.md")
SET(CPACK_DEBIAN_PACKAGE_MAINTAINER "Alec Lofquist <allofquist@yahoo.com>")
SET(CPACK_DEBIAN_PACKAGE_SECTION "misc")
SET(CPACK_DEBIAN_PACKAGE_PRIORITY "extra")
SET(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libsdl2-2.0-0, libboost-system1.54.0, libboost-filesystem1.54.0, libfreeimage3, libfreetype6, libcurl3, libasound2")
SET(CPACK_DEBIAN_PACKAGE_BUILDS_DEPENDS "debhelper (>= 8.0.0), cmake, g++ (>= 4.8), libsdl2-dev, libboost-system-dev, libboost-filesystem-dev, libboost-date-time-dev, libfreeimage-dev, libfreetype6-dev, libeigen3-dev, libcurl4-openssl-dev, libasound2-dev, libgl1-mesa-dev")
SET(CPACK_PACKAGE_VENDOR "emulationstation.org")
SET(CPACK_PACKAGE_VERSION "2.0.0~rc1")
SET(CPACK_PACKAGE_VERSION_MAJOR "2")
SET(CPACK_PACKAGE_VERSION_MINOR "0")
SET(CPACK_PACKAGE_VERSION_PATCH "0")
SET(CPACK_PACKAGE_INSTALL_DIRECTORY "emulationstation_${CMAKE_PACKAGE_VERSION}")
SET(CPACK_PACKAGE_EXECUTABLES "emulationstation" "emulationstation")
SET(CPACK_GENERATOR "TGZ;DEB")
INCLUDE(CPack)

Binary file not shown.

View file

@ -0,0 +1,13 @@
#pragma once
// These numbers and strings need to be manually updated for a new version.
// Do this version number update as the very last commit for the new release version.
#define PROGRAM_VERSION_MAJOR 2
#define PROGRAM_VERSION_MINOR 0
#define PROGRAM_VERSION_MAINTENANCE 0
#define PROGRAM_VERSION_STRING "2.0.0-rc1"
#define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__
#define RESOURCE_VERSION_STRING "2,0,0\0"
#define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE

View file

@ -0,0 +1,39 @@
#include "EmulationStation.h"
#include "windows.h"
VS_VERSION_INFO VERSIONINFO
FILEVERSION RESOURCE_VERSION
PRODUCTVERSION RESOURCE_VERSION
FILEFLAGSMASK 0x3fL
#ifdef _DEBUG
FILEFLAGS 0x1L
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904E4"
BEGIN
VALUE "Comments", "\0"
VALUE "FileDescription", "EmulationStation - emulator frontend\0"
VALUE "FileVersion", RESOURCE_VERSION_STRING
VALUE "InternalName", "emulationstation.exe\0"
VALUE "LegalCopyright", "\0"
VALUE "LegalTrademarks", "\0"
VALUE "OriginalFilename", "emulationstation.exe\0"
VALUE "ProductName", "EmulationStation\0"
VALUE "ProductVersion", PROGRAM_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1252
END
END
IDI_ES_LOGO ICON DISCARDABLE "../data/es_icon.ico"

144
es-app/src/FileData.cpp Normal file
View file

@ -0,0 +1,144 @@
#include "FileData.h"
#include "SystemData.h"
namespace fs = boost::filesystem;
std::string removeParenthesis(const std::string& str)
{
// remove anything in parenthesis or brackets
// should be roughly equivalent to the regex replace "\((.*)\)|\[(.*)\]" with ""
// I would love to just use regex, but it's not worth pulling in another boost lib for one function that is used once
std::string ret = str;
size_t start, end;
static const int NUM_TO_REPLACE = 2;
static const char toReplace[NUM_TO_REPLACE*2] = { '(', ')', '[', ']' };
bool done = false;
while(!done)
{
done = true;
for(int i = 0; i < NUM_TO_REPLACE; i++)
{
end = ret.find_first_of(toReplace[i*2+1]);
start = ret.find_last_of(toReplace[i*2], end);
if(start != std::string::npos && end != std::string::npos)
{
ret.erase(start, end - start + 1);
done = false;
}
}
}
// also strip whitespace
end = ret.find_last_not_of(' ');
if(end != std::string::npos)
end++;
ret = ret.substr(0, end);
return ret;
}
FileData::FileData(FileType type, const fs::path& path, SystemData* system)
: mType(type), mPath(path), mSystem(system), mParent(NULL), metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor!
{
// metadata needs at least a name field (since that's what getName() will return)
if(metadata.get("name").empty())
metadata.set("name", getCleanName());
}
FileData::~FileData()
{
if(mParent)
mParent->removeChild(this);
while(mChildren.size())
delete mChildren.back();
}
std::string FileData::getCleanName() const
{
std::string stem = mPath.stem().generic_string();
if(mSystem && mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO))
stem = PlatformIds::getCleanMameName(stem.c_str());
return removeParenthesis(stem);
}
const std::string& FileData::getThumbnailPath() const
{
if(!metadata.get("thumbnail").empty())
return metadata.get("thumbnail");
else
return metadata.get("image");
}
std::vector<FileData*> FileData::getFilesRecursive(unsigned int typeMask) const
{
std::vector<FileData*> out;
for(auto it = mChildren.begin(); it != mChildren.end(); it++)
{
if((*it)->getType() & typeMask)
out.push_back(*it);
if((*it)->getChildren().size() > 0)
{
std::vector<FileData*> subchildren = (*it)->getFilesRecursive(typeMask);
out.insert(out.end(), subchildren.cbegin(), subchildren.cend());
}
}
return out;
}
void FileData::addChild(FileData* file)
{
assert(mType == FOLDER);
assert(file->getParent() == NULL);
mChildren.push_back(file);
file->mParent = this;
}
void FileData::removeChild(FileData* file)
{
assert(mType == FOLDER);
assert(file->getParent() == this);
for(auto it = mChildren.begin(); it != mChildren.end(); it++)
{
if(*it == file)
{
mChildren.erase(it);
return;
}
}
// File somehow wasn't in our children.
assert(false);
}
void FileData::sort(ComparisonFunction& comparator, bool ascending)
{
std::sort(mChildren.begin(), mChildren.end(), comparator);
for(auto it = mChildren.begin(); it != mChildren.end(); it++)
{
if((*it)->getChildren().size() > 0)
(*it)->sort(comparator, ascending);
}
if(!ascending)
std::reverse(mChildren.begin(), mChildren.end());
}
void FileData::sort(const SortType& type)
{
sort(*type.comparisonFunction, type.ascending);
}

77
es-app/src/FileData.h Normal file
View file

@ -0,0 +1,77 @@
#pragma once
#include <vector>
#include <string>
#include <boost/filesystem.hpp>
#include "MetaData.h"
class SystemData;
enum FileType
{
GAME = 1, // Cannot have children.
FOLDER = 2
};
enum FileChangeType
{
FILE_ADDED,
FILE_METADATA_CHANGED,
FILE_REMOVED,
FILE_SORTED
};
// Used for loading/saving gamelist.xml.
const char* fileTypeToString(FileType type);
FileType stringToFileType(const char* str);
// Remove (.*) and [.*] from str
std::string removeParenthesis(const std::string& str);
// A tree node that holds information for a file.
class FileData
{
public:
FileData(FileType type, const boost::filesystem::path& path, SystemData* system);
virtual ~FileData();
inline const std::string& getName() const { return metadata.get("name"); }
inline FileType getType() const { return mType; }
inline const boost::filesystem::path& getPath() const { return mPath; }
inline FileData* getParent() const { return mParent; }
inline const std::vector<FileData*>& getChildren() const { return mChildren; }
inline SystemData* getSystem() const { return mSystem; }
virtual const std::string& getThumbnailPath() const;
std::vector<FileData*> getFilesRecursive(unsigned int typeMask) const;
void addChild(FileData* file); // Error if mType != FOLDER
void removeChild(FileData* file); //Error if mType != FOLDER
// Returns our best guess at the "real" name for this file (will strip parenthesis and attempt to perform MAME name translation)
std::string getCleanName() const;
typedef bool ComparisonFunction(const FileData* a, const FileData* b);
struct SortType
{
ComparisonFunction* comparisonFunction;
bool ascending;
std::string description;
SortType(ComparisonFunction* sortFunction, bool sortAscending, const std::string & sortDescription)
: comparisonFunction(sortFunction), ascending(sortAscending), description(sortDescription) {}
};
void sort(ComparisonFunction& comparator, bool ascending = true);
void sort(const SortType& type);
MetaDataList metadata;
private:
FileType mType;
boost::filesystem::path mPath;
SystemData* mSystem;
FileData* mParent;
std::vector<FileData*> mChildren;
};

72
es-app/src/FileSorts.cpp Normal file
View file

@ -0,0 +1,72 @@
#include "FileSorts.h"
namespace FileSorts
{
const FileData::SortType typesArr[] = {
FileData::SortType(&compareFileName, true, "filename, ascending"),
FileData::SortType(&compareFileName, false, "filename, descending"),
FileData::SortType(&compareRating, true, "rating, ascending"),
FileData::SortType(&compareRating, false, "rating, descending"),
FileData::SortType(&compareTimesPlayed, true, "times played, ascending"),
FileData::SortType(&compareTimesPlayed, false, "times played, descending"),
FileData::SortType(&compareLastPlayed, true, "last played, ascending"),
FileData::SortType(&compareLastPlayed, false, "last played, descending")
};
const std::vector<FileData::SortType> SortTypes(typesArr, typesArr + sizeof(typesArr)/sizeof(typesArr[0]));
//returns if file1 should come before file2
bool compareFileName(const FileData* file1, const FileData* file2)
{
std::string name1 = file1->getName();
std::string name2 = file2->getName();
//min of name1/name2 .length()s
unsigned int count = name1.length() > name2.length() ? name2.length() : name1.length();
for(unsigned int i = 0; i < count; i++)
{
if(toupper(name1[i]) != toupper(name2[i]))
{
return toupper(name1[i]) < toupper(name2[i]);
}
}
return name1.length() < name2.length();
}
bool compareRating(const FileData* file1, const FileData* file2)
{
//only games have rating metadata
if(file1->metadata.getType() == GAME_METADATA && file2->metadata.getType() == GAME_METADATA)
{
return file1->metadata.getFloat("rating") < file2->metadata.getFloat("rating");
}
return false;
}
bool compareTimesPlayed(const FileData* file1, const FileData* file2)
{
//only games have playcount metadata
if(file1->metadata.getType() == GAME_METADATA && file2->metadata.getType() == GAME_METADATA)
{
return (file1)->metadata.getInt("playcount") < (file2)->metadata.getInt("playcount");
}
return false;
}
bool compareLastPlayed(const FileData* file1, const FileData* file2)
{
//only games have lastplayed metadata
if(file1->metadata.getType() == GAME_METADATA && file2->metadata.getType() == GAME_METADATA)
{
return (file1)->metadata.getTime("lastplayed") < (file2)->metadata.getTime("lastplayed");
}
return false;
}
};

14
es-app/src/FileSorts.h Normal file
View file

@ -0,0 +1,14 @@
#pragma once
#include <vector>
#include "FileData.h"
namespace FileSorts
{
bool compareFileName(const FileData* file1, const FileData* file2);
bool compareRating(const FileData* file1, const FileData* file2);
bool compareTimesPlayed(const FileData* file1, const FileData* fil2);
bool compareLastPlayed(const FileData* file1, const FileData* file2);
extern const std::vector<FileData::SortType> SortTypes;
};

252
es-app/src/Gamelist.cpp Normal file
View file

@ -0,0 +1,252 @@
#include "Gamelist.h"
#include "SystemData.h"
#include "pugixml/pugixml.hpp"
#include <boost/filesystem.hpp>
#include "Log.h"
#include "Settings.h"
#include "Util.h"
namespace fs = boost::filesystem;
FileData* findOrCreateFile(SystemData* system, const boost::filesystem::path& path, FileType type)
{
// first, verify that path is within the system's root folder
FileData* root = system->getRootFolder();
bool contains = false;
fs::path relative = removeCommonPath(path, root->getPath(), contains);
if(!contains)
{
LOG(LogError) << "File path \"" << path << "\" is outside system path \"" << system->getStartPath() << "\"";
return NULL;
}
auto path_it = relative.begin();
FileData* treeNode = root;
bool found = false;
while(path_it != relative.end())
{
const std::vector<FileData*>& children = treeNode->getChildren();
found = false;
for(auto child_it = children.begin(); child_it != children.end(); child_it++)
{
if((*child_it)->getPath().filename() == *path_it)
{
treeNode = *child_it;
found = true;
break;
}
}
// this is the end
if(path_it == --relative.end())
{
if(found)
return treeNode;
if(type == FOLDER)
{
LOG(LogWarning) << "gameList: folder doesn't already exist, won't create";
return NULL;
}
FileData* file = new FileData(type, path, system);
treeNode->addChild(file);
return file;
}
if(!found)
{
// don't create folders unless it's leading up to a game
// if type is a folder it's gonna be empty, so don't bother
if(type == FOLDER)
{
LOG(LogWarning) << "gameList: folder doesn't already exist, won't create";
return NULL;
}
// create missing folder
FileData* folder = new FileData(FOLDER, treeNode->getPath().stem() / *path_it, system);
treeNode->addChild(folder);
treeNode = folder;
}
path_it++;
}
return NULL;
}
void parseGamelist(SystemData* system)
{
std::string xmlpath = system->getGamelistPath(false);
if(!boost::filesystem::exists(xmlpath))
return;
LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"...";
pugi::xml_document doc;
pugi::xml_parse_result result = doc.load_file(xmlpath.c_str());
if(!result)
{
LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " << result.description();
return;
}
pugi::xml_node root = doc.child("gameList");
if(!root)
{
LOG(LogError) << "Could not find <gameList> node in gamelist \"" << xmlpath << "\"!";
return;
}
fs::path relativeTo = system->getStartPath();
const char* tagList[2] = { "game", "folder" };
FileType typeList[2] = { GAME, FOLDER };
for(int i = 0; i < 2; i++)
{
const char* tag = tagList[i];
FileType type = typeList[i];
for(pugi::xml_node fileNode = root.child(tag); fileNode; fileNode = fileNode.next_sibling(tag))
{
fs::path path = resolvePath(fileNode.child("path").text().get(), relativeTo, false);
if(!boost::filesystem::exists(path))
{
LOG(LogWarning) << "File \"" << path << "\" does not exist! Ignoring.";
continue;
}
FileData* file = findOrCreateFile(system, path, type);
if(!file)
{
LOG(LogError) << "Error finding/creating FileData for \"" << path << "\", skipping.";
continue;
}
//load the metadata
std::string defaultName = file->metadata.get("name");
file->metadata = MetaDataList::createFromXML(GAME_METADATA, fileNode, relativeTo);
//make sure name gets set if one didn't exist
if(file->metadata.get("name").empty())
file->metadata.set("name", defaultName);
}
}
}
void addFileDataNode(pugi::xml_node& parent, const FileData* file, const char* tag, SystemData* system)
{
//create game and add to parent node
pugi::xml_node newNode = parent.append_child(tag);
//write metadata
file->metadata.appendToXML(newNode, true, system->getStartPath());
if(newNode.children().begin() == newNode.child("name") //first element is name
&& ++newNode.children().begin() == newNode.children().end() //theres only one element
&& newNode.child("name").text().get() == file->getCleanName()) //the name is the default
{
//if the only info is the default name, don't bother with this node
//delete it and ultimately do nothing
parent.remove_child(newNode);
}else{
//there's something useful in there so we'll keep the node, add the path
// try and make the path relative if we can so things still work if we change the rom folder location in the future
newNode.prepend_child("path").text().set(makeRelativePath(file->getPath(), system->getStartPath(), false).generic_string().c_str());
}
}
void updateGamelist(SystemData* system)
{
//We do this by reading the XML again, adding changes and then writing it back,
//because there might be information missing in our systemdata which would then miss in the new XML.
//We have the complete information for every game though, so we can simply remove a game
//we already have in the system from the XML, and then add it back from its GameData information...
if(Settings::getInstance()->getBool("IgnoreGamelist"))
return;
pugi::xml_document doc;
pugi::xml_node root;
std::string xmlReadPath = system->getGamelistPath(false);
if(boost::filesystem::exists(xmlReadPath))
{
//parse an existing file first
pugi::xml_parse_result result = doc.load_file(xmlReadPath.c_str());
if(!result)
{
LOG(LogError) << "Error parsing XML file \"" << xmlReadPath << "\"!\n " << result.description();
return;
}
root = doc.child("gameList");
if(!root)
{
LOG(LogError) << "Could not find <gameList> node in gamelist \"" << xmlReadPath << "\"!";
return;
}
}else{
//set up an empty gamelist to append to
root = doc.append_child("gameList");
}
//now we have all the information from the XML. now iterate through all our games and add information from there
FileData* rootFolder = system->getRootFolder();
if (rootFolder != nullptr)
{
//get only files, no folders
std::vector<FileData*> files = rootFolder->getFilesRecursive(GAME | FOLDER);
//iterate through all files, checking if they're already in the XML
std::vector<FileData*>::const_iterator fit = files.cbegin();
while(fit != files.cend())
{
const char* tag = ((*fit)->getType() == GAME) ? "game" : "folder";
// check if the file already exists in the XML
// if it does, remove it before adding
for(pugi::xml_node fileNode = root.child(tag); fileNode; fileNode = fileNode.next_sibling(tag))
{
pugi::xml_node pathNode = fileNode.child("path");
if(!pathNode)
{
LOG(LogError) << "<" << tag << "> node contains no <path> child!";
continue;
}
fs::path nodePath = resolvePath(pathNode.text().get(), system->getStartPath(), true);
fs::path gamePath((*fit)->getPath());
if(nodePath == gamePath || (fs::exists(nodePath) && fs::exists(gamePath) && fs::equivalent(nodePath, gamePath)))
{
// found it
root.remove_child(fileNode);
break;
}
}
// it was either removed or never existed to begin with; either way, we can add it now
addFileDataNode(root, *fit, tag, system);
++fit;
}
//now write the file
//make sure the folders leading up to this path exist (or the write will fail)
boost::filesystem::path xmlWritePath(system->getGamelistPath(true));
boost::filesystem::create_directories(xmlWritePath.parent_path());
if (!doc.save_file(xmlWritePath.c_str())) {
LOG(LogError) << "Error saving gamelist.xml to \"" << xmlWritePath << "\" (for system " << system->getName() << ")!";
}
}else{
LOG(LogError) << "Found no root folder for system \"" << system->getName() << "\"!";
}
}

9
es-app/src/Gamelist.h Normal file
View file

@ -0,0 +1,9 @@
#pragma once
class SystemData;
// Loads gamelist.xml data into a SystemData.
void parseGamelist(SystemData* system);
// Writes currently loaded metadata for a SystemData to gamelist.xml.
void updateGamelist(SystemData* system);

30048
es-app/src/MameNameMap.cpp Normal file

File diff suppressed because it is too large Load diff

135
es-app/src/MetaData.cpp Normal file
View file

@ -0,0 +1,135 @@
#include "MetaData.h"
#include "components/TextComponent.h"
#include "Log.h"
#include "Util.h"
namespace fs = boost::filesystem;
MetaDataDecl gameDecls[] = {
// key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd
{"name", MD_STRING, "", false, "name", "enter game name"},
{"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"},
{"image", MD_IMAGE_PATH, "", false, "image", "enter path to image"},
{"thumbnail", MD_IMAGE_PATH, "", false, "thumbnail", "enter path to thumbnail"},
{"rating", MD_RATING, "0.000000", false, "rating", "enter rating"},
{"releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date"},
{"developer", MD_STRING, "unknown", false, "developer", "enter game developer"},
{"publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher"},
{"genre", MD_STRING, "unknown", false, "genre", "enter game genre"},
{"players", MD_INT, "1", false, "players", "enter number of players"},
{"playcount", MD_INT, "0", true, "play count", "enter number of times played"},
{"lastplayed", MD_TIME, "0", true, "last played", "enter last played date"}
};
const std::vector<MetaDataDecl> gameMDD(gameDecls, gameDecls + sizeof(gameDecls) / sizeof(gameDecls[0]));
MetaDataDecl folderDecls[] = {
{"name", MD_STRING, "", false},
{"desc", MD_MULTILINE_STRING, "", false},
{"image", MD_IMAGE_PATH, "", false},
{"thumbnail", MD_IMAGE_PATH, "", false},
};
const std::vector<MetaDataDecl> folderMDD(folderDecls, folderDecls + sizeof(folderDecls) / sizeof(folderDecls[0]));
const std::vector<MetaDataDecl>& getMDDByType(MetaDataListType type)
{
switch(type)
{
case GAME_METADATA:
return gameMDD;
case FOLDER_METADATA:
return folderMDD;
}
LOG(LogError) << "Invalid MDD type";
return gameMDD;
}
MetaDataList::MetaDataList(MetaDataListType type)
: mType(type)
{
const std::vector<MetaDataDecl>& mdd = getMDD();
for(auto iter = mdd.begin(); iter != mdd.end(); iter++)
set(iter->key, iter->defaultValue);
}
MetaDataList MetaDataList::createFromXML(MetaDataListType type, pugi::xml_node node, const fs::path& relativeTo)
{
MetaDataList mdl(type);
const std::vector<MetaDataDecl>& mdd = mdl.getMDD();
for(auto iter = mdd.begin(); iter != mdd.end(); iter++)
{
pugi::xml_node md = node.child(iter->key.c_str());
if(md)
{
// if it's a path, resolve relative paths
std::string value = md.text().get();
if(iter->type == MD_IMAGE_PATH)
value = resolvePath(value, relativeTo, true).generic_string();
mdl.set(iter->key, value);
}else{
mdl.set(iter->key, iter->defaultValue);
}
}
return mdl;
}
void MetaDataList::appendToXML(pugi::xml_node parent, bool ignoreDefaults, const fs::path& relativeTo) const
{
const std::vector<MetaDataDecl>& mdd = getMDD();
for(auto mddIter = mdd.begin(); mddIter != mdd.end(); mddIter++)
{
auto mapIter = mMap.find(mddIter->key);
if(mapIter != mMap.end())
{
// we have this value!
// if it's just the default (and we ignore defaults), don't write it
if(ignoreDefaults && mapIter->second == mddIter->defaultValue)
continue;
// try and make paths relative if we can
std::string value = mapIter->second;
if(mddIter->type == MD_IMAGE_PATH)
value = makeRelativePath(value, relativeTo, true).generic_string();
parent.append_child(mapIter->first.c_str()).text().set(value.c_str());
}
}
}
void MetaDataList::set(const std::string& key, const std::string& value)
{
mMap[key] = value;
}
void MetaDataList::setTime(const std::string& key, const boost::posix_time::ptime& time)
{
mMap[key] = boost::posix_time::to_iso_string(time);
}
const std::string& MetaDataList::get(const std::string& key) const
{
return mMap.at(key);
}
int MetaDataList::getInt(const std::string& key) const
{
return atoi(get(key).c_str());
}
float MetaDataList::getFloat(const std::string& key) const
{
return (float)atof(get(key).c_str());
}
boost::posix_time::ptime MetaDataList::getTime(const std::string& key) const
{
return string_to_ptime(get(key), "%Y%m%dT%H%M%S%F%q");
}

65
es-app/src/MetaData.h Normal file
View file

@ -0,0 +1,65 @@
#pragma once
#include "pugixml/pugixml.hpp"
#include <string>
#include <map>
#include "GuiComponent.h"
#include <boost/date_time.hpp>
#include <boost/filesystem.hpp>
enum MetaDataType
{
//generic types
MD_STRING,
MD_INT,
MD_FLOAT,
//specialized types
MD_MULTILINE_STRING,
MD_IMAGE_PATH,
MD_RATING,
MD_DATE,
MD_TIME //used for lastplayed
};
struct MetaDataDecl
{
std::string key;
MetaDataType type;
std::string defaultValue;
bool isStatistic; //if true, ignore scraper values for this metadata
std::string displayName; // displayed as this in editors
std::string displayPrompt; // phrase displayed in editors when prompted to enter value (currently only for strings)
};
enum MetaDataListType
{
GAME_METADATA,
FOLDER_METADATA
};
const std::vector<MetaDataDecl>& getMDDByType(MetaDataListType type);
class MetaDataList
{
public:
static MetaDataList createFromXML(MetaDataListType type, pugi::xml_node node, const boost::filesystem::path& relativeTo);
void appendToXML(pugi::xml_node parent, bool ignoreDefaults, const boost::filesystem::path& relativeTo) const;
MetaDataList(MetaDataListType type);
void set(const std::string& key, const std::string& value);
void setTime(const std::string& key, const boost::posix_time::ptime& time); //times are internally stored as ISO strings (e.g. boost::posix_time::to_iso_string(ptime))
const std::string& get(const std::string& key) const;
int getInt(const std::string& key) const;
float getFloat(const std::string& key) const;
boost::posix_time::ptime getTime(const std::string& key) const;
inline MetaDataListType getType() const { return mType; }
inline const std::vector<MetaDataDecl>& getMDD() const { return getMDDByType(getType()); }
private:
MetaDataListType mType;
std::map<std::string, std::string> mMap;
};

100
es-app/src/PlatformId.cpp Normal file
View file

@ -0,0 +1,100 @@
#include "PlatformId.h"
#include <string.h>
extern const char* mameNameToRealName[];
namespace PlatformIds
{
const char* PlatformNames[PLATFORM_COUNT + 1] = {
"unknown", // nothing set
"3do",
"amiga",
"amstradcpc",
"apple2",
"arcade",
"atari800",
"atari2600",
"atari5200",
"atari7800",
"atarilynx",
"atarist",
"atarijaguar",
"atarijaguarcd",
"atarixe",
"colecovision",
"c64", // commodore 64
"intellivision",
"macintosh",
"xbox",
"xbox360",
"neogeo",
"ngp", // neo geo pocket
"ngpc", // neo geo pocket color
"n3ds", // nintendo 3DS
"n64", // nintendo 64
"nds", // nintendo DS
"nes", // nintendo entertainment system
"gb", // game boy
"gba", // game boy advance
"gbc", // game boy color
"gc", // gamecube
"wii",
"wiiu",
"pc",
"sega32x",
"segacd",
"dreamcast",
"gamegear",
"genesis", // sega genesis
"mastersystem", // sega master system
"megadrive", // sega megadrive
"saturn", // sega saturn
"psx",
"ps2",
"ps3",
"ps4",
"psvita",
"psp", // playstation portable
"snes", // super nintendo entertainment system
"pcengine", // turbografx-16/pcengine
"wonderswan",
"wonderswancolor",
"zxspectrum",
"ignore", // do not allow scraping for this system
"invalid"
};
PlatformId getPlatformId(const char* str)
{
if(str == NULL)
return PLATFORM_UNKNOWN;
for(unsigned int i = 1; i < PLATFORM_COUNT; i++)
{
if(strcmp(PlatformNames[i], str) == 0)
return (PlatformId)i;
}
return PLATFORM_UNKNOWN;
}
const char* getPlatformName(PlatformId id)
{
return PlatformNames[id];
}
const char* getCleanMameName(const char* from)
{
const char** mameNames = mameNameToRealName;
while(*mameNames != NULL && strcmp(from, *mameNames) != 0)
mameNames += 2;
if(*mameNames)
return *(mameNames + 1);
return from;
}
}

73
es-app/src/PlatformId.h Normal file
View file

@ -0,0 +1,73 @@
#pragma once
#include <map>
namespace PlatformIds
{
enum PlatformId : unsigned int
{
PLATFORM_UNKNOWN = 0,
THREEDO, // name can't start with a constant
AMIGA,
AMSTRAD_CPC,
APPLE_II,
ARCADE,
ATARI_800,
ATARI_2600,
ATARI_5200,
ATARI_7800,
ATARI_LYNX,
ATARI_ST, // Atari ST/STE/Falcon
ATARI_JAGUAR,
ATARI_JAGUAR_CD,
ATARI_XE,
COLECOVISION,
COMMODORE_64,
INTELLIVISION,
MAC_OS,
XBOX,
XBOX_360,
NEOGEO,
NEOGEO_POCKET,
NEOGEO_POCKET_COLOR,
NINTENDO_3DS,
NINTENDO_64,
NINTENDO_DS,
NINTENDO_ENTERTAINMENT_SYSTEM,
GAME_BOY,
GAME_BOY_ADVANCE,
GAME_BOY_COLOR,
NINTENDO_GAMECUBE,
NINTENDO_WII,
NINTENDO_WII_U,
PC,
SEGA_32X,
SEGA_CD,
SEGA_DREAMCAST,
SEGA_GAME_GEAR,
SEGA_GENESIS,
SEGA_MASTER_SYSTEM,
SEGA_MEGA_DRIVE,
SEGA_SATURN,
PLAYSTATION,
PLAYSTATION_2,
PLAYSTATION_3,
PLAYSTATION_4,
PLAYSTATION_VITA,
PLAYSTATION_PORTABLE,
SUPER_NINTENDO,
TURBOGRAFX_16, // (also PC Engine)
WONDERSWAN,
WONDERSWAN_COLOR,
ZX_SPECTRUM,
PLATFORM_IGNORE, // do not allow scraping for this system
PLATFORM_COUNT
};
PlatformId getPlatformId(const char* str);
const char* getPlatformName(PlatformId id);
const char* getCleanMameName(const char* from);
}

View file

@ -0,0 +1,285 @@
#include "ScraperCmdLine.h"
#include <iostream>
#include <vector>
#include "SystemData.h"
#include "Settings.h"
#include <signal.h>
#include "Log.h"
std::ostream& out = std::cout;
void handle_interrupt_signal(int p)
{
sleep(50);
LOG(LogInfo) << "Interrupt received during scrape...";
SystemData::deleteSystems();
exit(1);
}
int run_scraper_cmdline()
{
out << "EmulationStation scraper\n";
out << "========================\n";
out << "\n";
signal(SIGINT, handle_interrupt_signal);
//==================================================================================
//filter
//==================================================================================
enum FilterChoice
{
FILTER_MISSING_IMAGES,
FILTER_ALL
};
int filter_choice;
do {
out << "Select filter for games to be scraped:\n";
out << FILTER_MISSING_IMAGES << " - games missing images\n";
out << FILTER_ALL << " - all games period, can overwrite existing metadata\n";
std::cin >> filter_choice;
std::cin.ignore(1, '\n'); //skip the unconsumed newline
} while(filter_choice < FILTER_MISSING_IMAGES || filter_choice > FILTER_ALL);
out << "\n";
//==================================================================================
//platforms
//==================================================================================
std::vector<SystemData*> systems;
out << "You can scrape only specific platforms, or scrape all of them.\n";
out << "Would you like to scrape all platforms? (y/n)\n";
std::string system_choice;
std::getline(std::cin, system_choice);
if(system_choice == "y" || system_choice == "Y")
{
out << "Will scrape all platforms.\n";
for(auto i = SystemData::sSystemVector.begin(); i != SystemData::sSystemVector.end(); i++)
{
out << " " << (*i)->getName() << " (" << (*i)->getGameCount() << " games)\n";
systems.push_back(*i);
}
}else{
std::string sys_name;
out << "Enter the names of the platforms you would like to scrape, one at a time.\n";
out << "Type nothing and press enter when you are ready to continue.\n";
do {
for(auto i = SystemData::sSystemVector.begin(); i != SystemData::sSystemVector.end(); i++)
{
if(std::find(systems.begin(), systems.end(), (*i)) != systems.end())
out << " C ";
else
out << " ";
out << "\"" << (*i)->getName() << "\" (" << (*i)->getGameCount() << " games)\n";
}
std::getline(std::cin, sys_name);
if(sys_name.empty())
break;
bool found = false;
for(auto i = SystemData::sSystemVector.begin(); i != SystemData::sSystemVector.end(); i++)
{
if((*i)->getName() == sys_name)
{
systems.push_back(*i);
found = true;
break;
}
}
if(!found)
out << "System not found.\n";
} while(true);
}
//==================================================================================
//manual mode
//==================================================================================
out << "\n";
out << "You can let the scraper try to automatically choose the best result, or\n";
out << "you can manually approve each result. This \"manual mode\" is much more accurate.\n";
out << "It is highly recommended you use manual mode unless you have a very large collection.\n";
out << "Scrape in manual mode? (y/n)\n";
std::string manual_mode_str;
std::getline(std::cin, manual_mode_str);
bool manual_mode = false;
if(manual_mode_str == "y" || manual_mode_str == "Y")
{
manual_mode = true;
out << "Scraping in manual mode!\n";
}else{
out << "Scraping in automatic mode!\n";
}
//==================================================================================
//scraping
//==================================================================================
out << "\n";
out << "Alright, let's do this thing!\n";
out << "=============================\n";
/*
std::shared_ptr<Scraper> scraper = Settings::getInstance()->getScraper();
for(auto sysIt = systems.begin(); sysIt != systems.end(); sysIt++)
{
std::vector<FileData*> files = (*sysIt)->getRootFolder()->getFilesRecursive(GAME);
ScraperSearchParams params;
params.system = (*sysIt);
for(auto gameIt = files.begin(); gameIt != files.end(); gameIt++)
{
params.nameOverride = "";
params.game = *gameIt;
//print original search term
out << getCleanFileName(params.game->getPath()) << "...\n";
//need to take into account filter_choice
if(filter_choice == FILTER_MISSING_IMAGES)
{
if(!params.game->metadata.get("image").empty()) //maybe should also check if the image file exists/is a URL
{
out << " Skipping, metadata \"image\" entry is not empty.\n";
continue;
}
}
//actually get some results
do {
std::vector<MetaDataList> mdls = scraper->getResults(params);
//no results
if(mdls.size() == 0)
{
if(manual_mode)
{
//in manual mode let the user enter a custom search
out << " NO RESULTS FOUND! Enter a new name to search for, or nothing to skip.\n";
std::getline(std::cin, params.nameOverride);
if(params.nameOverride.empty())
{
out << " Skipping...\n";
break;
}
continue;
}else{
out << " NO RESULTS FOUND! Skipping...\n";
break;
}
}
//some results
if(manual_mode)
{
//print list of choices
for(unsigned int i = 0; i < mdls.size(); i++)
{
out << " " << i << " - " << mdls.at(i).get("name") << "\n";
}
int choice = -1;
std::string choice_str;
out << "Your choice: ";
std::getline(std::cin, choice_str);
std::stringstream choice_buff(choice_str); //convert to int
choice_buff >> choice;
if(choice >= 0 && choice < (int)mdls.size())
{
params.game->metadata = mdls.at(choice);
break;
}else{
out << "Invalid choice.\n";
continue;
}
}else{
//automatic mode
//always choose the first choice
out << " name -> " << mdls.at(0).get("name") << "\n";
params.game->metadata = mdls.at(0);
break;
}
} while(true);
out << "===================\n";
}
}
out << "\n\n";
out << "Downloading boxart...\n";
for(auto sysIt = systems.begin(); sysIt != systems.end(); sysIt++)
{
std::vector<FileData*> files = (*sysIt)->getRootFolder()->getFilesRecursive(GAME);
for(auto gameIt = files.begin(); gameIt != files.end(); gameIt++)
{
FileData* game = *gameIt;
const std::vector<MetaDataDecl>& mdd = game->metadata.getMDD();
for(auto i = mdd.begin(); i != mdd.end(); i++)
{
std::string key = i->key;
std::string url = game->metadata.get(key);
if(i->type == MD_IMAGE_PATH && HttpReq::isUrl(url))
{
std::string urlShort = url.substr(0, url.length() > 35 ? 35 : url.length());
if(url.length() != urlShort.length()) urlShort += "...";
out << " " << game->metadata.get("name") << " [from: " << urlShort << "]...\n";
ScraperSearchParams p;
p.game = game;
p.system = *sysIt;
game->metadata.set(key, downloadImage(url, getSaveAsPath(p, key, url)));
if(game->metadata.get(key).empty())
{
out << " FAILED! Skipping.\n";
game->metadata.set(key, url); //result URL to what it was if download failed, retry some other time
}
}
}
}
}
out << "\n\n";
out << "==============================\n";
out << "SCRAPE COMPLETE!\n";
out << "==============================\n";
*/
out << "\n\n";
out << "ACTUALLY THIS IS STILL TODO\n";
return 0;
}

View file

@ -0,0 +1,3 @@
#pragma once
int run_scraper_cmdline();

444
es-app/src/SystemData.cpp Normal file
View file

@ -0,0 +1,444 @@
#include "SystemData.h"
#include "Gamelist.h"
#include <boost/filesystem.hpp>
#include <fstream>
#include <stdlib.h>
#include <SDL_joystick.h>
#include "Renderer.h"
#include "AudioManager.h"
#include "VolumeControl.h"
#include "Log.h"
#include "InputManager.h"
#include <iostream>
#include "Settings.h"
#include "FileSorts.h"
std::vector<SystemData*> SystemData::sSystemVector;
namespace fs = boost::filesystem;
SystemData::SystemData(const std::string& name, const std::string& fullName, const std::string& startPath, const std::vector<std::string>& extensions,
const std::string& command, const std::vector<PlatformIds::PlatformId>& platformIds, const std::string& themeFolder)
{
mName = name;
mFullName = fullName;
mStartPath = startPath;
//expand home symbol if the startpath contains ~
if(mStartPath[0] == '~')
{
mStartPath.erase(0, 1);
mStartPath.insert(0, getHomePath());
}
mSearchExtensions = extensions;
mLaunchCommand = command;
mPlatformIds = platformIds;
mThemeFolder = themeFolder;
mRootFolder = new FileData(FOLDER, mStartPath, this);
mRootFolder->metadata.set("name", mFullName);
if(!Settings::getInstance()->getBool("ParseGamelistOnly"))
populateFolder(mRootFolder);
if(!Settings::getInstance()->getBool("IgnoreGamelist"))
parseGamelist(this);
mRootFolder->sort(FileSorts::SortTypes.at(0));
loadTheme();
}
SystemData::~SystemData()
{
//save changed game data back to xml
if(!Settings::getInstance()->getBool("IgnoreGamelist"))
{
updateGamelist(this);
}
delete mRootFolder;
}
std::string strreplace(std::string& str, std::string replace, std::string with)
{
size_t pos = str.find(replace);
if(pos != std::string::npos)
return str.replace(pos, replace.length(), with.c_str(), with.length());
else
return str;
}
std::string escapePath(const boost::filesystem::path& path)
{
// a quick and dirty way to insert a backslash before most characters that would mess up a bash path;
// someone with regex knowledge should make this into a one-liner
std::string pathStr = path.generic_string();
const char* invalidChars = " '\"\\!$^&*(){}[]?;<>";
for(unsigned int i = 0; i < pathStr.length(); i++)
{
char c;
unsigned int charNum = 0;
do {
c = invalidChars[charNum];
if(pathStr[i] == c)
{
pathStr.insert(i, "\\");
i++;
break;
}
charNum++;
} while(c != '\0');
}
return pathStr;
}
void SystemData::launchGame(Window* window, FileData* game)
{
LOG(LogInfo) << "Attempting to launch game...";
AudioManager::getInstance()->deinit();
VolumeControl::getInstance()->deinit();
window->deinit();
std::string command = mLaunchCommand;
const std::string rom = escapePath(game->getPath());
const std::string basename = game->getPath().stem().string();
const std::string rom_raw = game->getPath().string();
command = strreplace(command, "%ROM%", rom);
command = strreplace(command, "%BASENAME%", basename);
command = strreplace(command, "%ROM_RAW%", rom_raw);
LOG(LogInfo) << " " << command;
std::cout << "==============================================\n";
int exitCode = system(command.c_str());
std::cout << "==============================================\n";
if(exitCode != 0)
{
LOG(LogWarning) << "...launch terminated with nonzero exit code " << exitCode << "!";
}
window->init();
VolumeControl::getInstance()->init();
AudioManager::getInstance()->init();
window->normalizeNextUpdate();
//update number of times the game has been launched
int timesPlayed = game->metadata.getInt("playcount") + 1;
game->metadata.set("playcount", std::to_string(static_cast<long long>(timesPlayed)));
//update last played time
boost::posix_time::ptime time = boost::posix_time::second_clock::universal_time();
game->metadata.setTime("lastplayed", time);
}
void SystemData::populateFolder(FileData* folder)
{
const fs::path& folderPath = folder->getPath();
if(!fs::is_directory(folderPath))
{
LOG(LogWarning) << "Error - folder with path \"" << folderPath << "\" is not a directory!";
return;
}
const std::string folderStr = folderPath.generic_string();
//make sure that this isn't a symlink to a thing we already have
if(fs::is_symlink(folderPath))
{
//if this symlink resolves to somewhere that's at the beginning of our path, it's gonna recurse
if(folderStr.find(fs::canonical(folderPath).generic_string()) == 0)
{
LOG(LogWarning) << "Skipping infinitely recursive symlink \"" << folderPath << "\"";
return;
}
}
fs::path filePath;
std::string extension;
bool isGame;
for(fs::directory_iterator end, dir(folderPath); dir != end; ++dir)
{
filePath = (*dir).path();
if(filePath.stem().empty())
continue;
//this is a little complicated because we allow a list of extensions to be defined (delimited with a space)
//we first get the extension of the file itself:
extension = filePath.extension().string();
//fyi, folders *can* also match the extension and be added as games - this is mostly just to support higan
//see issue #75: https://github.com/Aloshi/EmulationStation/issues/75
isGame = false;
if(std::find(mSearchExtensions.begin(), mSearchExtensions.end(), extension) != mSearchExtensions.end())
{
FileData* newGame = new FileData(GAME, filePath.generic_string(), this);
folder->addChild(newGame);
isGame = true;
}
//add directories that also do not match an extension as folders
if(!isGame && fs::is_directory(filePath))
{
FileData* newFolder = new FileData(FOLDER, filePath.generic_string(), this);
populateFolder(newFolder);
//ignore folders that do not contain games
if(newFolder->getChildren().size() == 0)
delete newFolder;
else
folder->addChild(newFolder);
}
}
}
std::vector<std::string> readList(const std::string& str, const char* delims = " \t\r\n,")
{
std::vector<std::string> ret;
size_t prevOff = str.find_first_not_of(delims, 0);
size_t off = str.find_first_of(delims, prevOff);
while(off != std::string::npos || prevOff != std::string::npos)
{
ret.push_back(str.substr(prevOff, off - prevOff));
prevOff = str.find_first_not_of(delims, off);
off = str.find_first_of(delims, prevOff);
}
return ret;
}
//creates systems from information located in a config file
bool SystemData::loadConfig()
{
deleteSystems();
std::string path = getConfigPath(false);
LOG(LogInfo) << "Loading system config file " << path << "...";
if(!fs::exists(path))
{
LOG(LogError) << "es_systems.cfg file does not exist!";
writeExampleConfig(getConfigPath(true));
return false;
}
pugi::xml_document doc;
pugi::xml_parse_result res = doc.load_file(path.c_str());
if(!res)
{
LOG(LogError) << "Could not parse es_systems.cfg file!";
LOG(LogError) << res.description();
return false;
}
//actually read the file
pugi::xml_node systemList = doc.child("systemList");
if(!systemList)
{
LOG(LogError) << "es_systems.cfg is missing the <systemList> tag!";
return false;
}
for(pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system"))
{
std::string name, fullname, path, cmd, themeFolder;
PlatformIds::PlatformId platformId = PlatformIds::PLATFORM_UNKNOWN;
name = system.child("name").text().get();
fullname = system.child("fullname").text().get();
path = system.child("path").text().get();
// convert extensions list from a string into a vector of strings
std::vector<std::string> extensions = readList(system.child("extension").text().get());
cmd = system.child("command").text().get();
// platform id list
const char* platformList = system.child("platform").text().get();
std::vector<std::string> platformStrs = readList(platformList);
std::vector<PlatformIds::PlatformId> platformIds;
for(auto it = platformStrs.begin(); it != platformStrs.end(); it++)
{
const char* str = it->c_str();
PlatformIds::PlatformId platformId = PlatformIds::getPlatformId(str);
if(platformId == PlatformIds::PLATFORM_IGNORE)
{
// when platform is ignore, do not allow other platforms
platformIds.clear();
platformIds.push_back(platformId);
break;
}
// if there appears to be an actual platform ID supplied but it didn't match the list, warn
if(str != NULL && str[0] != '\0' && platformId == PlatformIds::PLATFORM_UNKNOWN)
LOG(LogWarning) << " Unknown platform for system \"" << name << "\" (platform \"" << str << "\" from list \"" << platformList << "\")";
else if(platformId != PlatformIds::PLATFORM_UNKNOWN)
platformIds.push_back(platformId);
}
// theme folder
themeFolder = system.child("theme").text().as_string(name.c_str());
//validate
if(name.empty() || path.empty() || extensions.empty() || cmd.empty())
{
LOG(LogError) << "System \"" << name << "\" is missing name, path, extension, or command!";
continue;
}
//convert path to generic directory seperators
boost::filesystem::path genericPath(path);
path = genericPath.generic_string();
SystemData* newSys = new SystemData(name, fullname, path, extensions, cmd, platformIds, themeFolder);
if(newSys->getRootFolder()->getChildren().size() == 0)
{
LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it.";
delete newSys;
}else{
sSystemVector.push_back(newSys);
}
}
return true;
}
void SystemData::writeExampleConfig(const std::string& path)
{
std::ofstream file(path.c_str());
file << "<!-- This is the EmulationStation Systems configuration file.\n"
"All systems must be contained within the <systemList> tag.-->\n"
"\n"
"<systemList>\n"
" <!-- Here's an example system to get you started. -->\n"
" <system>\n"
"\n"
" <!-- A short name, used internally. Traditionally lower-case. -->\n"
" <name>nes</name>\n"
"\n"
" <!-- A \"pretty\" name, displayed in menus and such. -->\n"
" <fullname>Nintendo Entertainment System</fullname>\n"
"\n"
" <!-- The path to start searching for ROMs in. '~' will be expanded to $HOME on Linux or %HOMEPATH% on Windows. -->\n"
" <path>~/roms/nes</path>\n"
"\n"
" <!-- A list of extensions to search for, delimited by any of the whitespace characters (\", \\r\\n\\t\").\n"
" You MUST include the period at the start of the extension! It's also case sensitive. -->\n"
" <extension>.nes .NES</extension>\n"
"\n"
" <!-- The shell command executed when a game is selected. A few special tags are replaced if found in a command:\n"
" %ROM% is replaced by a bash-special-character-escaped absolute path to the ROM.\n"
" %BASENAME% is replaced by the \"base\" name of the ROM. For example, \"/foo/bar.rom\" would have a basename of \"bar\". Useful for MAME.\n"
" %ROM_RAW% is the raw, unescaped path to the ROM. -->\n"
" <command>retroarch -L ~/cores/libretro-fceumm.so %ROM%</command>\n"
"\n"
" <!-- The platform to use when scraping. You can see the full list of accepted platforms in src/PlatformIds.cpp.\n"
" It's case sensitive, but everything is lowercase. This tag is optional.\n"
" You can use multiple platforms too, delimited with any of the whitespace characters (\", \\r\\n\\t\"), eg: \"genesis, megadrive\" -->\n"
" <platform>nes</platform>\n"
"\n"
" <!-- The theme to load from the current theme set. See THEMES.md for more information.\n"
" This tag is optional. If not set, it will default to the value of <name>. -->\n"
" <theme>nes</theme>\n"
" </system>\n"
"</systemList>\n";
file.close();
LOG(LogError) << "Example config written! Go read it at \"" << path << "\"!";
}
void SystemData::deleteSystems()
{
for(unsigned int i = 0; i < sSystemVector.size(); i++)
{
delete sSystemVector.at(i);
}
sSystemVector.clear();
}
std::string SystemData::getConfigPath(bool forWrite)
{
fs::path path = getHomePath() + "/.emulationstation/es_systems.cfg";
if(forWrite || fs::exists(path))
return path.generic_string();
return "/etc/emulationstation/es_systems.cfg";
}
std::string SystemData::getGamelistPath(bool forWrite) const
{
fs::path filePath;
filePath = mRootFolder->getPath() / "gamelist.xml";
if(fs::exists(filePath))
return filePath.generic_string();
filePath = getHomePath() + "/.emulationstation/gamelists/" + mName + "/gamelist.xml";
if(forWrite) // make sure the directory exists if we're going to write to it, or crashes will happen
fs::create_directories(filePath.parent_path());
if(forWrite || fs::exists(filePath))
return filePath.generic_string();
return "/etc/emulationstation/gamelists/" + mName + "/gamelist.xml";
}
std::string SystemData::getThemePath() const
{
// where we check for themes, in order:
// 1. [SYSTEM_PATH]/theme.xml
// 2. currently selected theme set
// first, check game folder
fs::path localThemePath = mRootFolder->getPath() / "theme.xml";
if(fs::exists(localThemePath))
return localThemePath.generic_string();
// not in game folder, try theme sets
return ThemeData::getThemeFromCurrentSet(mThemeFolder).generic_string();
}
bool SystemData::hasGamelist() const
{
return (fs::exists(getGamelistPath(false)));
}
unsigned int SystemData::getGameCount() const
{
return mRootFolder->getFilesRecursive(GAME).size();
}
void SystemData::loadTheme()
{
mTheme = std::make_shared<ThemeData>();
std::string path = getThemePath();
if(!fs::exists(path)) // no theme available for this platform
return;
try
{
mTheme->loadFile(path);
} catch(ThemeException& e)
{
LOG(LogError) << e.what();
mTheme = std::make_shared<ThemeData>(); // reset to empty
}
}

80
es-app/src/SystemData.h Normal file
View file

@ -0,0 +1,80 @@
#pragma once
#include <vector>
#include <string>
#include "FileData.h"
#include "Window.h"
#include "MetaData.h"
#include "PlatformId.h"
#include "ThemeData.h"
class SystemData
{
public:
SystemData(const std::string& name, const std::string& fullName, const std::string& startPath, const std::vector<std::string>& extensions,
const std::string& command, const std::vector<PlatformIds::PlatformId>& platformIds, const std::string& themeFolder);
~SystemData();
inline FileData* getRootFolder() const { return mRootFolder; };
inline const std::string& getName() const { return mName; }
inline const std::string& getFullName() const { return mFullName; }
inline const std::string& getStartPath() const { return mStartPath; }
inline const std::vector<std::string>& getExtensions() const { return mSearchExtensions; }
inline const std::string& getThemeFolder() const { return mThemeFolder; }
inline const std::vector<PlatformIds::PlatformId>& getPlatformIds() const { return mPlatformIds; }
inline bool hasPlatformId(PlatformIds::PlatformId id) { return std::find(mPlatformIds.begin(), mPlatformIds.end(), id) != mPlatformIds.end(); }
inline const std::shared_ptr<ThemeData>& getTheme() const { return mTheme; }
std::string getGamelistPath(bool forWrite) const;
bool hasGamelist() const;
std::string getThemePath() const;
unsigned int getGameCount() const;
void launchGame(Window* window, FileData* game);
static void deleteSystems();
static bool loadConfig(); //Load the system config file at getConfigPath(). Returns true if no errors were encountered. An example will be written if the file doesn't exist.
static void writeExampleConfig(const std::string& path);
static std::string getConfigPath(bool forWrite); // if forWrite, will only return ~/.emulationstation/es_systems.cfg, never /etc/emulationstation/es_systems.cfg
static std::vector<SystemData*> sSystemVector;
inline std::vector<SystemData*>::const_iterator getIterator() const { return std::find(sSystemVector.begin(), sSystemVector.end(), this); };
inline std::vector<SystemData*>::const_reverse_iterator getRevIterator() const { return std::find(sSystemVector.rbegin(), sSystemVector.rend(), this); };
inline SystemData* getNext() const
{
auto it = getIterator();
it++;
if(it == sSystemVector.end()) it = sSystemVector.begin();
return *it;
}
inline SystemData* getPrev() const
{
auto it = getRevIterator();
it++;
if(it == sSystemVector.rend()) it = sSystemVector.rbegin();
return *it;
}
// Load or re-load theme.
void loadTheme();
private:
std::string mName;
std::string mFullName;
std::string mStartPath;
std::vector<std::string> mSearchExtensions;
std::string mLaunchCommand;
std::vector<PlatformIds::PlatformId> mPlatformIds;
std::string mThemeFolder;
std::shared_ptr<ThemeData> mTheme;
void populateFolder(FileData* folder);
FileData* mRootFolder;
};

View file

@ -0,0 +1,388 @@
#include "VolumeControl.h"
#include "Log.h"
#if defined(__linux__)
#ifdef _RPI_
const char * VolumeControl::mixerName = "PCM";
#else
const char * VolumeControl::mixerName = "Master";
#endif
const char * VolumeControl::mixerCard = "default";
#endif
std::weak_ptr<VolumeControl> VolumeControl::sInstance;
VolumeControl::VolumeControl()
: originalVolume(0), internalVolume(0)
#if defined (__APPLE__)
#error TODO: Not implemented for MacOS yet!!!
#elif defined(__linux__)
, mixerIndex(0), mixerHandle(nullptr), mixerElem(nullptr), mixerSelemId(nullptr)
#elif defined(WIN32) || defined(_WIN32)
, mixerHandle(nullptr), endpointVolume(nullptr)
#endif
{
init();
//get original volume levels for system
originalVolume = getVolume();
}
VolumeControl::VolumeControl(const VolumeControl & right)
{
sInstance = right.sInstance;
}
VolumeControl & VolumeControl::operator=(const VolumeControl & right)
{
if (this != &right) {
sInstance = right.sInstance;
}
return *this;
}
VolumeControl::~VolumeControl()
{
//set original volume levels for system
setVolume(originalVolume);
deinit();
}
std::shared_ptr<VolumeControl> & VolumeControl::getInstance()
{
//check if an VolumeControl instance is already created, if not create one
static std::shared_ptr<VolumeControl> sharedInstance = sInstance.lock();
if (sharedInstance == nullptr) {
sharedInstance.reset(new VolumeControl);
sInstance = sharedInstance;
}
return sharedInstance;
}
void VolumeControl::init()
{
//initialize audio mixer interface
#if defined (__APPLE__)
#error TODO: Not implemented for MacOS yet!!!
#elif defined(__linux__)
//try to open mixer device
if (mixerHandle == nullptr)
{
snd_mixer_selem_id_alloca(&mixerSelemId);
//sets simple-mixer index and name
snd_mixer_selem_id_set_index(mixerSelemId, mixerIndex);
snd_mixer_selem_id_set_name(mixerSelemId, mixerName);
//open mixer
if (snd_mixer_open(&mixerHandle, 0) >= 0)
{
LOG(LogDebug) << "VolumeControl::init() - Opened ALSA mixer";
//ok. attach to defualt card
if (snd_mixer_attach(mixerHandle, mixerCard) >= 0)
{
LOG(LogDebug) << "VolumeControl::init() - Attached to default card";
//ok. register simple element class
if (snd_mixer_selem_register(mixerHandle, NULL, NULL) >= 0)
{
LOG(LogDebug) << "VolumeControl::init() - Registered simple element class";
//ok. load registered elements
if (snd_mixer_load(mixerHandle) >= 0)
{
LOG(LogDebug) << "VolumeControl::init() - Loaded mixer elements";
//ok. find elements now
mixerElem = snd_mixer_find_selem(mixerHandle, mixerSelemId);
if (mixerElem != nullptr)
{
//wohoo. good to go...
LOG(LogDebug) << "VolumeControl::init() - Mixer initialized";
}
else
{
LOG(LogError) << "VolumeControl::init() - Failed to find mixer elements!";
snd_mixer_close(mixerHandle);
mixerHandle = nullptr;
}
}
else
{
LOG(LogError) << "VolumeControl::init() - Failed to load mixer elements!";
snd_mixer_close(mixerHandle);
mixerHandle = nullptr;
}
}
else
{
LOG(LogError) << "VolumeControl::init() - Failed to register simple element class!";
snd_mixer_close(mixerHandle);
mixerHandle = nullptr;
}
}
else
{
LOG(LogError) << "VolumeControl::init() - Failed to attach to default card!";
snd_mixer_close(mixerHandle);
mixerHandle = nullptr;
}
}
else
{
LOG(LogError) << "VolumeControl::init() - Failed to open ALSA mixer!";
}
}
#elif defined(WIN32) || defined(_WIN32)
//get windows version information
OSVERSIONINFOEXA osVer = {sizeof(OSVERSIONINFO)};
::GetVersionExA(reinterpret_cast<LPOSVERSIONINFOA>(&osVer));
//check windows version
if(osVer.dwMajorVersion < 6)
{
//Windows older than Vista. use mixer API. open default mixer
if (mixerHandle == nullptr)
{
if (mixerOpen(&mixerHandle, 0, NULL, 0, 0) == MMSYSERR_NOERROR)
{
//retrieve info on the volume slider control for the "Speaker Out" line
MIXERLINECONTROLS mixerLineControls;
mixerLineControls.cbStruct = sizeof(MIXERLINECONTROLS);
mixerLineControls.dwLineID = 0xFFFF0000; //Id of "Speaker Out" line
mixerLineControls.cControls = 1;
//mixerLineControls.dwControlID = 0x00000000; //Id of "Speaker Out" line's volume slider
mixerLineControls.dwControlType = MIXERCONTROL_CONTROLTYPE_VOLUME; //Get volume control
mixerLineControls.pamxctrl = &mixerControl;
mixerLineControls.cbmxctrl = sizeof(MIXERCONTROL);
if (mixerGetLineControls((HMIXEROBJ)mixerHandle, &mixerLineControls, MIXER_GETLINECONTROLSF_ONEBYTYPE) != MMSYSERR_NOERROR)
{
LOG(LogError) << "VolumeControl::getVolume() - Failed to get mixer volume control!";
mixerClose(mixerHandle);
mixerHandle = nullptr;
}
}
else
{
LOG(LogError) << "VolumeControl::init() - Failed to open mixer!";
}
}
}
else
{
//Windows Vista or above. use EndpointVolume API. get device enumerator
if (endpointVolume == nullptr)
{
CoInitialize(nullptr);
IMMDeviceEnumerator * deviceEnumerator = nullptr;
CoCreateInstance(__uuidof(MMDeviceEnumerator), nullptr, CLSCTX_INPROC_SERVER, __uuidof(IMMDeviceEnumerator), (LPVOID *)&deviceEnumerator);
if (deviceEnumerator != nullptr)
{
//get default endpoint
IMMDevice * defaultDevice = nullptr;
deviceEnumerator->GetDefaultAudioEndpoint(eRender, eConsole, &defaultDevice);
if (defaultDevice != nullptr)
{
//retrieve endpoint volume
defaultDevice->Activate(__uuidof(IAudioEndpointVolume), CLSCTX_INPROC_SERVER, nullptr, (LPVOID *)&endpointVolume);
if (endpointVolume == nullptr)
{
LOG(LogError) << "VolumeControl::init() - Failed to get default audio endpoint volume!";
}
//release default device. we don't need it anymore
defaultDevice->Release();
}
else
{
LOG(LogError) << "VolumeControl::init() - Failed to get default audio endpoint!";
}
//release device enumerator. we don't need it anymore
deviceEnumerator->Release();
}
else
{
LOG(LogError) << "VolumeControl::init() - Failed to get audio endpoint enumerator!";
CoUninitialize();
}
}
}
#endif
}
void VolumeControl::deinit()
{
//deinitialize audio mixer interface
#if defined (__APPLE__)
#error TODO: Not implemented for MacOS yet!!!
#elif defined(__linux__)
if (mixerHandle != nullptr) {
snd_mixer_detach(mixerHandle, mixerCard);
snd_mixer_free(mixerHandle);
snd_mixer_close(mixerHandle);
mixerHandle = nullptr;
mixerElem = nullptr;
}
#elif defined(WIN32) || defined(_WIN32)
if (mixerHandle != nullptr) {
mixerClose(mixerHandle);
mixerHandle = nullptr;
}
else if (endpointVolume != nullptr) {
endpointVolume->Release();
endpointVolume = nullptr;
CoUninitialize();
}
#endif
}
int VolumeControl::getVolume() const
{
int volume = 0;
#if defined (__APPLE__)
#error TODO: Not implemented for MacOS yet!!!
#elif defined(__linux__)
if (mixerElem != nullptr)
{
//get volume range
long minVolume;
long maxVolume;
if (snd_mixer_selem_get_playback_volume_range(mixerElem, &minVolume, &maxVolume) == 0)
{
//ok. now get volume
long rawVolume;
if (snd_mixer_selem_get_playback_volume(mixerElem, SND_MIXER_SCHN_MONO, &rawVolume) == 0)
{
//worked. bring into range 0-100
rawVolume -= minVolume;
if (rawVolume > 0)
{
volume = (rawVolume * 100) / (maxVolume - minVolume);
}
//else volume = 0;
}
else
{
LOG(LogError) << "VolumeControl::getVolume() - Failed to get mixer volume!";
}
}
else
{
LOG(LogError) << "VolumeControl::getVolume() - Failed to get volume range!";
}
}
#elif defined(WIN32) || defined(_WIN32)
if (mixerHandle != nullptr)
{
//Windows older than Vista. use mixer API. get volume from line control
MIXERCONTROLDETAILS_UNSIGNED value;
MIXERCONTROLDETAILS mixerControlDetails;
mixerControlDetails.cbStruct = sizeof(MIXERCONTROLDETAILS);
mixerControlDetails.dwControlID = mixerControl.dwControlID;
mixerControlDetails.cChannels = 1; //always 1 for a MIXERCONTROL_CONTROLF_UNIFORM control
mixerControlDetails.cMultipleItems = 0; //always 0 except for a MIXERCONTROL_CONTROLF_MULTIPLE control
mixerControlDetails.paDetails = &value;
mixerControlDetails.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED);
if (mixerGetControlDetails((HMIXEROBJ)mixerHandle, &mixerControlDetails, MIXER_GETCONTROLDETAILSF_VALUE) == MMSYSERR_NOERROR)
{
volume = (uint8_t)((value.dwValue * 100) / 65535);
}
else
{
LOG(LogError) << "VolumeControl::getVolume() - Failed to get mixer volume!";
}
}
else if (endpointVolume != nullptr)
{
//Windows Vista or above. use EndpointVolume API
float floatVolume = 0.0f; //0-1
if (endpointVolume->GetMasterVolumeLevelScalar(&floatVolume) == S_OK)
{
volume = (uint8_t)(floatVolume * 100.0f);
}
else
{
LOG(LogError) << "VolumeControl::getVolume() - Failed to get master volume!";
}
}
#endif
//clamp to 0-100 range
if (volume < 0)
{
volume = 0;
}
if (volume > 100)
{
volume = 100;
}
return volume;
}
void VolumeControl::setVolume(int volume)
{
//clamp to 0-100 range
if (volume < 0)
{
volume = 0;
}
if (volume > 100)
{
volume = 100;
}
//store values in internal variables
internalVolume = volume;
#if defined (__APPLE__)
#error TODO: Not implemented for MacOS yet!!!
#elif defined(__linux__)
if (mixerElem != nullptr)
{
//get volume range
long minVolume;
long maxVolume;
if (snd_mixer_selem_get_playback_volume_range(mixerElem, &minVolume, &maxVolume) == 0)
{
//ok. bring into minVolume-maxVolume range and set
long rawVolume = (volume * (maxVolume - minVolume) / 100) + minVolume;
if (snd_mixer_selem_set_playback_volume(mixerElem, SND_MIXER_SCHN_FRONT_LEFT, rawVolume) < 0
|| snd_mixer_selem_set_playback_volume(mixerElem, SND_MIXER_SCHN_FRONT_RIGHT, rawVolume) < 0)
{
LOG(LogError) << "VolumeControl::getVolume() - Failed to set mixer volume!";
}
}
else
{
LOG(LogError) << "VolumeControl::getVolume() - Failed to get volume range!";
}
}
#elif defined(WIN32) || defined(_WIN32)
if (mixerHandle != nullptr)
{
//Windows older than Vista. use mixer API. get volume from line control
MIXERCONTROLDETAILS_UNSIGNED value;
value.dwValue = (volume * 65535) / 100;
MIXERCONTROLDETAILS mixerControlDetails;
mixerControlDetails.cbStruct = sizeof(MIXERCONTROLDETAILS);
mixerControlDetails.dwControlID = mixerControl.dwControlID;
mixerControlDetails.cChannels = 1; //always 1 for a MIXERCONTROL_CONTROLF_UNIFORM control
mixerControlDetails.cMultipleItems = 0; //always 0 except for a MIXERCONTROL_CONTROLF_MULTIPLE control
mixerControlDetails.paDetails = &value;
mixerControlDetails.cbDetails = sizeof(MIXERCONTROLDETAILS_UNSIGNED);
if (mixerSetControlDetails((HMIXEROBJ)mixerHandle, &mixerControlDetails, MIXER_SETCONTROLDETAILSF_VALUE) != MMSYSERR_NOERROR)
{
LOG(LogError) << "VolumeControl::setVolume() - Failed to set mixer volume!";
}
}
else if (endpointVolume != nullptr)
{
//Windows Vista or above. use EndpointVolume API
float floatVolume = 0.0f; //0-1
if (volume > 0) {
floatVolume = (float)volume / 100.0f;
}
if (endpointVolume->SetMasterVolumeLevelScalar(floatVolume, nullptr) != S_OK)
{
LOG(LogError) << "VolumeControl::setVolume() - Failed to set master volume!";
}
}
#endif
}

View file

@ -0,0 +1,58 @@
#pragma once
#include <memory>
#include <stdint.h>
#if defined (__APPLE__)
#error TODO: Not implemented for MacOS yet!!!
#elif defined(__linux__)
#include <unistd.h>
#include <fcntl.h>
#include <alsa/asoundlib.h>
#elif defined(WIN32) || defined(_WIN32)
#include <Windows.h>
#include <MMSystem.h>
#include <mmdeviceapi.h>
#include <endpointvolume.h>
#endif
/*!
Singleton pattern. Call getInstance() to get an object.
*/
class VolumeControl
{
#if defined (__APPLE__)
#error TODO: Not implemented for MacOS yet!!!
#elif defined(__linux__)
static const char * mixerName;
static const char * mixerCard;
int mixerIndex;
snd_mixer_t* mixerHandle;
snd_mixer_elem_t* mixerElem;
snd_mixer_selem_id_t* mixerSelemId;
#elif defined(WIN32) || defined(_WIN32)
HMIXER mixerHandle;
MIXERCONTROL mixerControl;
IAudioEndpointVolume * endpointVolume;
#endif
int originalVolume;
int internalVolume;
static std::weak_ptr<VolumeControl> sInstance;
VolumeControl();
VolumeControl(const VolumeControl & right);
VolumeControl & operator=(const VolumeControl & right);
public:
static std::shared_ptr<VolumeControl> & getInstance();
void init();
void deinit();
int getVolume() const;
void setVolume(int volume);
~VolumeControl();
};

View file

@ -0,0 +1,64 @@
#pragma once
#include "animations/Animation.h"
#include "Log.h"
// let's look at the game launch effect:
// -move camera to center on point P (interpolation method: linear)
// -zoom camera to factor Z via matrix scale (interpolation method: exponential)
// -fade screen to black at rate F (interpolation method: exponential)
// How the animation gets constructed from the example of implementing the game launch effect:
// 1. Current parameters are retrieved through a get() call
// ugliness:
// -have to have a way to get a reference to the animation
// -requires additional implementation in some parent object, potentially AnimationController
// 2. ViewController is passed in LaunchAnimation constructor, applies state through setters
// ugliness:
// -effect only works for ViewController
// 3. Pass references to ViewController variables - LaunchAnimation(mCameraPos, mFadePerc, target)
// ugliness:
// -what if ViewController is deleted? --> AnimationController class handles that
// -no callbacks for changes...but that works well with this style of update, so I think it's okay
// 4. Use callbacks to set variables
// ugliness:
// -boilerplate as hell every time
// #3 wins
// can't wait to see how this one bites me in the ass
class LaunchAnimation : public Animation
{
public:
//Target is a centerpoint
LaunchAnimation(Eigen::Affine3f& camera, float& fade, const Eigen::Vector3f& target, int duration) :
mCameraStart(camera), mTarget(target), mDuration(duration), cameraOut(camera), fadeOut(fade) {}
int getDuration() const override { return mDuration; }
void apply(float t) override
{
cameraOut = Eigen::Affine3f::Identity();
float zoom = lerp<float>(1.0, 4.25f, t*t);
cameraOut.scale(Eigen::Vector3f(zoom, zoom, 1));
const float sw = (float)Renderer::getScreenWidth() / zoom;
const float sh = (float)Renderer::getScreenHeight() / zoom;
Eigen::Vector3f centerPoint = lerp<Eigen::Vector3f>(-mCameraStart.translation() + Eigen::Vector3f(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0),
mTarget, smoothStep(0.0, 1.0, t));
cameraOut.translate(Eigen::Vector3f((sw / 2) - centerPoint.x(), (sh / 2) - centerPoint.y(), 0));
fadeOut = lerp<float>(0.0, 1.0, t*t);
}
private:
Eigen::Affine3f mCameraStart;
Eigen::Vector3f mTarget;
int mDuration;
Eigen::Affine3f& cameraOut;
float& fadeOut;
};

View file

@ -0,0 +1,24 @@
#pragma once
#include "animations/Animation.h"
class MoveCameraAnimation : public Animation
{
public:
MoveCameraAnimation(Eigen::Affine3f& camera, const Eigen::Vector3f& target) : mCameraStart(camera), mTarget(target), cameraOut(camera) {}
int getDuration() const override { return 400; }
void apply(float t) override
{
// cubic ease out
t -= 1;
cameraOut.translation() = -lerp<Eigen::Vector3f>(-mCameraStart.translation(), mTarget, t*t*t + 1);
}
private:
Eigen::Affine3f mCameraStart;
Eigen::Vector3f mTarget;
Eigen::Affine3f& cameraOut;
};

View file

@ -0,0 +1,51 @@
#include "components/AsyncReqComponent.h"
#include "Renderer.h"
AsyncReqComponent::AsyncReqComponent(Window* window, std::shared_ptr<HttpReq> req, std::function<void(std::shared_ptr<HttpReq>)> onSuccess, std::function<void()> onCancel)
: GuiComponent(window),
mSuccessFunc(onSuccess), mCancelFunc(onCancel), mTime(0), mRequest(req)
{
}
bool AsyncReqComponent::input(InputConfig* config, Input input)
{
if(input.value != 0 && config->isMappedTo("b", input))
{
if(mCancelFunc)
mCancelFunc();
delete this;
}
return true;
}
void AsyncReqComponent::update(int deltaTime)
{
if(mRequest->status() != HttpReq::REQ_IN_PROGRESS)
{
mSuccessFunc(mRequest);
delete this;
return;
}
mTime += deltaTime;
}
void AsyncReqComponent::render(const Eigen::Affine3f& parentTrans)
{
Eigen::Affine3f trans = Eigen::Affine3f::Identity();
trans = trans.translate(Eigen::Vector3f(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0));
Renderer::setMatrix(trans);
Eigen::Vector3f point(cos(mTime * 0.01f) * 12, sin(mTime * 0.01f) * 12, 0);
Renderer::drawRect((int)point.x(), (int)point.y(), 8, 8, 0x0000FFFF);
}
std::vector<HelpPrompt> AsyncReqComponent::getHelpPrompts()
{
std::vector<HelpPrompt> prompts;
prompts.push_back(HelpPrompt("b", "cancel"));
return prompts;
}

View file

@ -0,0 +1,45 @@
#pragma once
#include "GuiComponent.h"
#include "HttpReq.h"
#include <functional>
#include <memory>
/*
Used to asynchronously run an HTTP request.
Displays a simple animation on the UI to show the application hasn't frozen. Can be canceled by the user pressing B.
Usage example:
std::shared_ptr<HttpReq> httpreq = std::make_shared<HttpReq>("cdn.garcya.us", "/wp-content/uploads/2010/04/TD250.jpg");
AsyncReqComponent* req = new AsyncReqComponent(mWindow, httpreq,
[] (std::shared_ptr<HttpReq> r)
{
LOG(LogInfo) << "Request completed";
LOG(LogInfo) << " error, if any: " << r->getErrorMsg();
}, [] ()
{
LOG(LogInfo) << "Request canceled";
});
mWindow->pushGui(req);
//we can forget about req, since it will always delete itself
*/
class AsyncReqComponent : public GuiComponent
{
public:
AsyncReqComponent(Window* window, std::shared_ptr<HttpReq> req, std::function<void(std::shared_ptr<HttpReq>)> onSuccess, std::function<void()> onCancel = nullptr);
bool input(InputConfig* config, Input input) override;
void update(int deltaTime) override;
void render(const Eigen::Affine3f& parentTrans) override;
virtual std::vector<HelpPrompt> getHelpPrompts() override;
private:
std::function<void(std::shared_ptr<HttpReq>)> mSuccessFunc;
std::function<void()> mCancelFunc;
unsigned int mTime;
std::shared_ptr<HttpReq> mRequest;
};

View file

@ -0,0 +1,169 @@
#include "components/RatingComponent.h"
#include "Renderer.h"
#include "Window.h"
#include "Util.h"
#include "resources/SVGResource.h"
RatingComponent::RatingComponent(Window* window) : GuiComponent(window)
{
mFilledTexture = TextureResource::get(":/star_filled.svg", true);
mUnfilledTexture = TextureResource::get(":/star_unfilled.svg", true);
mValue = 0.5f;
mSize << 64 * NUM_RATING_STARS, 64;
updateVertices();
}
void RatingComponent::setValue(const std::string& value)
{
if(value.empty())
{
mValue = 0.0f;
}else{
mValue = stof(value);
if(mValue > 1.0f)
mValue = 1.0f;
else if(mValue < 0.0f)
mValue = 0.0f;
}
updateVertices();
}
std::string RatingComponent::getValue() const
{
return std::to_string((long double)mValue);
}
void RatingComponent::onSizeChanged()
{
if(mSize.y() == 0)
mSize[1] = mSize.x() / NUM_RATING_STARS;
else if(mSize.x() == 0)
mSize[0] = mSize.y() * NUM_RATING_STARS;
auto filledSVG = dynamic_cast<SVGResource*>(mFilledTexture.get());
auto unfilledSVG = dynamic_cast<SVGResource*>(mUnfilledTexture.get());
if(mSize.y() > 0)
{
size_t heightPx = (size_t)round(mSize.y());
if(filledSVG)
filledSVG->rasterizeAt(heightPx, heightPx);
if(unfilledSVG)
unfilledSVG->rasterizeAt(heightPx, heightPx);
}
updateVertices();
}
void RatingComponent::updateVertices()
{
const float numStars = NUM_RATING_STARS;
const float h = round(getSize().y()); // is the same as a single star's width
const float w = round(h * mValue * numStars);
const float fw = round(h * numStars);
mVertices[0].pos << 0.0f, 0.0f;
mVertices[0].tex << 0.0f, 1.0f;
mVertices[1].pos << w, h;
mVertices[1].tex << mValue * numStars, 0.0f;
mVertices[2].pos << 0.0f, h;
mVertices[2].tex << 0.0f, 0.0f;
mVertices[3] = mVertices[0];
mVertices[4].pos << w, 0.0f;
mVertices[4].tex << mValue * numStars, 1.0f;
mVertices[5] = mVertices[1];
mVertices[6] = mVertices[4];
mVertices[7].pos << fw, h;
mVertices[7].tex << numStars, 0.0f;
mVertices[8] = mVertices[1];
mVertices[9] = mVertices[6];
mVertices[10].pos << fw, 0.0f;
mVertices[10].tex << numStars, 1.0f;
mVertices[11] = mVertices[7];
}
void RatingComponent::render(const Eigen::Affine3f& parentTrans)
{
Eigen::Affine3f trans = roundMatrix(parentTrans * getTransform());
Renderer::setMatrix(trans);
glEnable(GL_TEXTURE_2D);
glEnable(GL_BLEND);
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
glColor4ub(255, 255, 255, getOpacity());
glEnableClientState(GL_VERTEX_ARRAY);
glEnableClientState(GL_TEXTURE_COORD_ARRAY);
glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].pos);
glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].tex);
mFilledTexture->bind();
glDrawArrays(GL_TRIANGLES, 0, 6);
mUnfilledTexture->bind();
glDrawArrays(GL_TRIANGLES, 6, 6);
glDisableClientState(GL_VERTEX_ARRAY);
glDisableClientState(GL_TEXTURE_COORD_ARRAY);
glDisable(GL_TEXTURE_2D);
glDisable(GL_BLEND);
glColor4ub(255, 255, 255, 255);
renderChildren(trans);
}
bool RatingComponent::input(InputConfig* config, Input input)
{
if(config->isMappedTo("a", input) && input.value != 0)
{
mValue += 1.f / NUM_RATING_STARS;
if(mValue > 1.0f)
mValue = 0.0f;
updateVertices();
}
return GuiComponent::input(config, input);
}
void RatingComponent::applyTheme(const std::shared_ptr<ThemeData>& theme, const std::string& view, const std::string& element, unsigned int properties)
{
GuiComponent::applyTheme(theme, view, element, properties);
using namespace ThemeFlags;
const ThemeData::ThemeElement* elem = theme->getElement(view, element, "rating");
if(!elem)
return;
bool imgChanged = false;
if(properties & PATH && elem->has("filledPath"))
{
mFilledTexture = TextureResource::get(elem->get<std::string>("filledPath"), true);
imgChanged = true;
}
if(properties & PATH && elem->has("unfilledPath"))
{
mUnfilledTexture = TextureResource::get(elem->get<std::string>("unfilledPath"), true);
imgChanged = true;
}
if(imgChanged)
onSizeChanged();
}
std::vector<HelpPrompt> RatingComponent::getHelpPrompts()
{
std::vector<HelpPrompt> prompts;
prompts.push_back(HelpPrompt("a", "add star"));
return prompts;
}

View file

@ -0,0 +1,44 @@
#pragma once
#include "GuiComponent.h"
#include "resources/TextureResource.h"
#define NUM_RATING_STARS 5
// Used to visually display/edit some sort of "score" - e.g. 5/10, 3/5, etc.
// setSize(x, y) works a little differently than you might expect:
// * (0, y != 0) - x will be automatically calculated (5*y).
// * (x != 0, 0) - y will be automatically calculated (x/5).
// * (x != 0, y != 0) - you better be sure x = y*5
class RatingComponent : public GuiComponent
{
public:
RatingComponent(Window* window);
std::string getValue() const override;
void setValue(const std::string& value) override; // Should be a normalized float (in the range [0..1]) - if it's not, it will be clamped.
bool input(InputConfig* config, Input input) override;
void render(const Eigen::Affine3f& parentTrans);
void onSizeChanged() override;
virtual void applyTheme(const std::shared_ptr<ThemeData>& theme, const std::string& view, const std::string& element, unsigned int properties) override;
virtual std::vector<HelpPrompt> getHelpPrompts() override;
private:
void updateVertices();
float mValue;
struct Vertex
{
Eigen::Vector2f pos;
Eigen::Vector2f tex;
} mVertices[12];
std::shared_ptr<TextureResource> mFilledTexture;
std::shared_ptr<TextureResource> mUnfilledTexture;
};

View file

@ -0,0 +1,476 @@
#include "components/ScraperSearchComponent.h"
#include "guis/GuiMsgBox.h"
#include "components/TextComponent.h"
#include "components/ScrollableContainer.h"
#include "components/ImageComponent.h"
#include "components/RatingComponent.h"
#include "components/DateTimeComponent.h"
#include "components/AnimatedImageComponent.h"
#include "components/ComponentList.h"
#include "HttpReq.h"
#include "Settings.h"
#include "Log.h"
#include "Util.h"
#include "guis/GuiTextEditPopup.h"
ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) : GuiComponent(window),
mGrid(window, Eigen::Vector2i(4, 3)), mBusyAnim(window),
mSearchType(type)
{
addChild(&mGrid);
mBlockAccept = false;
using namespace Eigen;
// left spacer (empty component, needed for borders)
mGrid.setEntry(std::make_shared<GuiComponent>(mWindow), Vector2i(0, 0), false, false, Vector2i(1, 3), GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM);
// selected result name
mResultName = std::make_shared<TextComponent>(mWindow, "Result name", Font::get(FONT_SIZE_MEDIUM), 0x777777FF);
// selected result thumbnail
mResultThumbnail = std::make_shared<ImageComponent>(mWindow);
mGrid.setEntry(mResultThumbnail, Vector2i(1, 1), false, false, Vector2i(1, 1));
// selected result desc + container
mDescContainer = std::make_shared<ScrollableContainer>(mWindow);
mResultDesc = std::make_shared<TextComponent>(mWindow, "Result desc", Font::get(FONT_SIZE_SMALL), 0x777777FF);
mDescContainer->addChild(mResultDesc.get());
mDescContainer->setAutoScroll(true);
// metadata
auto font = Font::get(FONT_SIZE_SMALL); // this gets replaced in onSizeChanged() so its just a placeholder
const unsigned int mdColor = 0x777777FF;
const unsigned int mdLblColor = 0x666666FF;
mMD_Rating = std::make_shared<RatingComponent>(mWindow);
mMD_ReleaseDate = std::make_shared<DateTimeComponent>(mWindow);
mMD_ReleaseDate->setColor(mdColor);
mMD_Developer = std::make_shared<TextComponent>(mWindow, "", font, mdColor);
mMD_Publisher = std::make_shared<TextComponent>(mWindow, "", font, mdColor);
mMD_Genre = std::make_shared<TextComponent>(mWindow, "", font, mdColor);
mMD_Players = std::make_shared<TextComponent>(mWindow, "", font, mdColor);
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "RATING:", font, mdLblColor), mMD_Rating, false));
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "RELEASED:", font, mdLblColor), mMD_ReleaseDate));
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "DEVELOPER:", font, mdLblColor), mMD_Developer));
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "PUBLISHER:", font, mdLblColor), mMD_Publisher));
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "GENRE:", font, mdLblColor), mMD_Genre));
mMD_Pairs.push_back(MetaDataPair(std::make_shared<TextComponent>(mWindow, "PLAYERS:", font, mdLblColor), mMD_Players));
mMD_Grid = std::make_shared<ComponentGrid>(mWindow, Vector2i(2, mMD_Pairs.size()*2 - 1));
unsigned int i = 0;
for(auto it = mMD_Pairs.begin(); it != mMD_Pairs.end(); it++)
{
mMD_Grid->setEntry(it->first, Vector2i(0, i), false, true);
mMD_Grid->setEntry(it->second, Vector2i(1, i), false, it->resize);
i += 2;
}
mGrid.setEntry(mMD_Grid, Vector2i(2, 1), false, false);
// result list
mResultList = std::make_shared<ComponentList>(mWindow);
mResultList->setCursorChangedCallback([this](CursorState state) { if(state == CURSOR_STOPPED) updateInfoPane(); });
updateViewStyle();
}
void ScraperSearchComponent::onSizeChanged()
{
mGrid.setSize(mSize);
if(mSize.x() == 0 || mSize.y() == 0)
return;
// column widths
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT)
mGrid.setColWidthPerc(0, 0.02f); // looks better when this is higher in auto mode
else
mGrid.setColWidthPerc(0, 0.01f);
mGrid.setColWidthPerc(1, 0.25f);
mGrid.setColWidthPerc(2, 0.25f);
// row heights
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT) // show name
mGrid.setRowHeightPerc(0, (mResultName->getFont()->getHeight() * 1.6f) / mGrid.getSize().y()); // result name
else
mGrid.setRowHeightPerc(0, 0.0825f); // hide name but do padding
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT)
{
mGrid.setRowHeightPerc(2, 0.2f);
}else{
mGrid.setRowHeightPerc(1, 0.505f);
}
const float boxartCellScale = 0.9f;
// limit thumbnail size using setMaxHeight - we do this instead of letting mGrid call setSize because it maintains the aspect ratio
// we also pad a little so it doesn't rub up against the metadata labels
mResultThumbnail->setMaxSize(mGrid.getColWidth(1) * boxartCellScale, mGrid.getRowHeight(1));
// metadata
resizeMetadata();
if(mSearchType != ALWAYS_ACCEPT_FIRST_RESULT)
mDescContainer->setSize(mGrid.getColWidth(1)*boxartCellScale + mGrid.getColWidth(2), mResultDesc->getFont()->getHeight() * 3);
else
mDescContainer->setSize(mGrid.getColWidth(3)*boxartCellScale, mResultDesc->getFont()->getHeight() * 8);
mResultDesc->setSize(mDescContainer->getSize().x(), 0); // make desc text wrap at edge of container
mGrid.onSizeChanged();
mBusyAnim.setSize(mSize);
}
void ScraperSearchComponent::resizeMetadata()
{
mMD_Grid->setSize(mGrid.getColWidth(2), mGrid.getRowHeight(1));
if(mMD_Grid->getSize().y() > mMD_Pairs.size())
{
const int fontHeight = (int)(mMD_Grid->getSize().y() / mMD_Pairs.size() * 0.8f);
auto fontLbl = Font::get(fontHeight, FONT_PATH_REGULAR);
auto fontComp = Font::get(fontHeight, FONT_PATH_LIGHT);
// update label fonts
float maxLblWidth = 0;
for(auto it = mMD_Pairs.begin(); it != mMD_Pairs.end(); it++)
{
it->first->setFont(fontLbl);
it->first->setSize(0, 0);
if(it->first->getSize().x() > maxLblWidth)
maxLblWidth = it->first->getSize().x() + 6;
}
for(unsigned int i = 0; i < mMD_Pairs.size(); i++)
{
mMD_Grid->setRowHeightPerc(i*2, (fontLbl->getLetterHeight() + 2) / mMD_Grid->getSize().y());
}
// update component fonts
mMD_ReleaseDate->setFont(fontComp);
mMD_Developer->setFont(fontComp);
mMD_Publisher->setFont(fontComp);
mMD_Genre->setFont(fontComp);
mMD_Players->setFont(fontComp);
mMD_Grid->setColWidthPerc(0, maxLblWidth / mMD_Grid->getSize().x());
// rating is manually sized
mMD_Rating->setSize(mMD_Grid->getColWidth(1), fontLbl->getHeight() * 0.65f);
mMD_Grid->onSizeChanged();
// make result font follow label font
mResultDesc->setFont(Font::get(fontHeight, FONT_PATH_REGULAR));
}
}
void ScraperSearchComponent::updateViewStyle()
{
using namespace Eigen;
// unlink description and result list and result name
mGrid.removeEntry(mResultName);
mGrid.removeEntry(mResultDesc);
mGrid.removeEntry(mResultList);
// add them back depending on search type
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT)
{
// show name
mGrid.setEntry(mResultName, Vector2i(1, 0), false, true, Vector2i(2, 1), GridFlags::BORDER_TOP);
// need a border on the bottom left
mGrid.setEntry(std::make_shared<GuiComponent>(mWindow), Vector2i(0, 2), false, false, Vector2i(3, 1), GridFlags::BORDER_BOTTOM);
// show description on the right
mGrid.setEntry(mDescContainer, Vector2i(3, 0), false, false, Vector2i(1, 3), GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM);
mResultDesc->setSize(mDescContainer->getSize().x(), 0); // make desc text wrap at edge of container
}else{
// fake row where name would be
mGrid.setEntry(std::make_shared<GuiComponent>(mWindow), Vector2i(1, 0), false, true, Vector2i(2, 1), GridFlags::BORDER_TOP);
// show result list on the right
mGrid.setEntry(mResultList, Vector2i(3, 0), true, true, Vector2i(1, 3), GridFlags::BORDER_LEFT | GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM);
// show description under image/info
mGrid.setEntry(mDescContainer, Vector2i(1, 2), false, false, Vector2i(2, 1), GridFlags::BORDER_BOTTOM);
mResultDesc->setSize(mDescContainer->getSize().x(), 0); // make desc text wrap at edge of container
}
}
void ScraperSearchComponent::search(const ScraperSearchParams& params)
{
mResultList->clear();
mScraperResults.clear();
mThumbnailReq.reset();
mMDResolveHandle.reset();
updateInfoPane();
mLastSearch = params;
mSearchHandle = startScraperSearch(params);
}
void ScraperSearchComponent::stop()
{
mThumbnailReq.reset();
mSearchHandle.reset();
mMDResolveHandle.reset();
mBlockAccept = false;
}
void ScraperSearchComponent::onSearchDone(const std::vector<ScraperSearchResult>& results)
{
mResultList->clear();
mScraperResults = results;
const int end = results.size() > MAX_SCRAPER_RESULTS ? MAX_SCRAPER_RESULTS : results.size(); // at max display 5
auto font = Font::get(FONT_SIZE_MEDIUM);
unsigned int color = 0x777777FF;
if(end == 0)
{
ComponentListRow row;
row.addElement(std::make_shared<TextComponent>(mWindow, "NO GAMES FOUND - SKIP", font, color), true);
if(mSkipCallback)
row.makeAcceptInputHandler(mSkipCallback);
mResultList->addRow(row);
mGrid.resetCursor();
}else{
ComponentListRow row;
for(int i = 0; i < end; i++)
{
row.elements.clear();
row.addElement(std::make_shared<TextComponent>(mWindow, strToUpper(results.at(i).mdl.get("name")), font, color), true);
row.makeAcceptInputHandler([this, i] { returnResult(mScraperResults.at(i)); });
mResultList->addRow(row);
}
mGrid.resetCursor();
}
mBlockAccept = false;
updateInfoPane();
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT)
{
if(mScraperResults.size() == 0)
mSkipCallback();
else
returnResult(mScraperResults.front());
}else if(mSearchType == ALWAYS_ACCEPT_MATCHING_CRC)
{
// TODO
}
}
void ScraperSearchComponent::onSearchError(const std::string& error)
{
LOG(LogInfo) << "ScraperSearchComponent search error: " << error;
mWindow->pushGui(new GuiMsgBox(mWindow, strToUpper(error),
"RETRY", std::bind(&ScraperSearchComponent::search, this, mLastSearch),
"SKIP", mSkipCallback,
"CANCEL", mCancelCallback));
}
int ScraperSearchComponent::getSelectedIndex()
{
if(!mScraperResults.size() || mGrid.getSelectedComponent() != mResultList)
return -1;
return mResultList->getCursorId();
}
void ScraperSearchComponent::updateInfoPane()
{
int i = getSelectedIndex();
if(mSearchType == ALWAYS_ACCEPT_FIRST_RESULT && mScraperResults.size())
{
i = 0;
}
if(i != -1 && (int)mScraperResults.size() > i)
{
ScraperSearchResult& res = mScraperResults.at(i);
mResultName->setText(strToUpper(res.mdl.get("name")));
mResultDesc->setText(strToUpper(res.mdl.get("desc")));
mDescContainer->reset();
mResultThumbnail->setImage("");
const std::string& thumb = res.thumbnailUrl.empty() ? res.imageUrl : res.thumbnailUrl;
if(!thumb.empty())
{
mThumbnailReq = std::unique_ptr<HttpReq>(new HttpReq(thumb));
}else{
mThumbnailReq.reset();
}
// metadata
mMD_Rating->setValue(strToUpper(res.mdl.get("rating")));
mMD_ReleaseDate->setValue(strToUpper(res.mdl.get("releasedate")));
mMD_Developer->setText(strToUpper(res.mdl.get("developer")));
mMD_Publisher->setText(strToUpper(res.mdl.get("publisher")));
mMD_Genre->setText(strToUpper(res.mdl.get("genre")));
mMD_Players->setText(strToUpper(res.mdl.get("players")));
mGrid.onSizeChanged();
}else{
mResultName->setText("");
mResultDesc->setText("");
mResultThumbnail->setImage("");
// metadata
mMD_Rating->setValue("");
mMD_ReleaseDate->setValue("");
mMD_Developer->setText("");
mMD_Publisher->setText("");
mMD_Genre->setText("");
mMD_Players->setText("");
}
}
bool ScraperSearchComponent::input(InputConfig* config, Input input)
{
if(config->isMappedTo("a", input) && input.value != 0)
{
if(mBlockAccept)
return true;
}
return GuiComponent::input(config, input);
}
void ScraperSearchComponent::render(const Eigen::Affine3f& parentTrans)
{
Eigen::Affine3f trans = parentTrans * getTransform();
renderChildren(trans);
if(mBlockAccept)
{
Renderer::setMatrix(trans);
Renderer::drawRect(0.f, 0.f, mSize.x(), mSize.y(), 0x00000011);
//Renderer::drawRect((int)mResultList->getPosition().x(), (int)mResultList->getPosition().y(),
// (int)mResultList->getSize().x(), (int)mResultList->getSize().y(), 0x00000011);
mBusyAnim.render(trans);
}
}
void ScraperSearchComponent::returnResult(ScraperSearchResult result)
{
mBlockAccept = true;
// resolve metadata image before returning
if(!result.imageUrl.empty())
{
mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch);
return;
}
mAcceptCallback(result);
}
void ScraperSearchComponent::update(int deltaTime)
{
GuiComponent::update(deltaTime);
if(mBlockAccept)
{
mBusyAnim.update(deltaTime);
}
if(mThumbnailReq && mThumbnailReq->status() != HttpReq::REQ_IN_PROGRESS)
{
updateThumbnail();
}
if(mSearchHandle && mSearchHandle->status() != ASYNC_IN_PROGRESS)
{
auto status = mSearchHandle->status();
auto results = mSearchHandle->getResults();
auto statusString = mSearchHandle->getStatusString();
// we reset here because onSearchDone in auto mode can call mSkipCallback() which can call
// another search() which will set our mSearchHandle to something important
mSearchHandle.reset();
if(status == ASYNC_DONE)
{
onSearchDone(results);
}else if(status == ASYNC_ERROR)
{
onSearchError(statusString);
}
}
if(mMDResolveHandle && mMDResolveHandle->status() != ASYNC_IN_PROGRESS)
{
if(mMDResolveHandle->status() == ASYNC_DONE)
{
ScraperSearchResult result = mMDResolveHandle->getResult();
mMDResolveHandle.reset();
// this might end in us being deleted, depending on mAcceptCallback - so make sure this is the last thing we do in update()
returnResult(result);
}else if(mMDResolveHandle->status() == ASYNC_ERROR)
{
onSearchError(mMDResolveHandle->getStatusString());
mMDResolveHandle.reset();
}
}
}
void ScraperSearchComponent::updateThumbnail()
{
if(mThumbnailReq && mThumbnailReq->status() == HttpReq::REQ_SUCCESS)
{
std::string content = mThumbnailReq->getContent();
mResultThumbnail->setImage(content.data(), content.length());
mGrid.onSizeChanged(); // a hack to fix the thumbnail position since its size changed
}else{
LOG(LogWarning) << "thumbnail req failed: " << mThumbnailReq->getErrorMsg();
mResultThumbnail->setImage("");
}
mThumbnailReq.reset();
}
void ScraperSearchComponent::openInputScreen(ScraperSearchParams& params)
{
auto searchForFunc = [&](const std::string& name)
{
params.nameOverride = name;
search(params);
};
stop();
mWindow->pushGui(new GuiTextEditPopup(mWindow, "SEARCH FOR",
// initial value is last search if there was one, otherwise the clean path name
params.nameOverride.empty() ? params.game->getCleanName() : params.nameOverride,
searchForFunc, false, "SEARCH"));
}
std::vector<HelpPrompt> ScraperSearchComponent::getHelpPrompts()
{
std::vector<HelpPrompt> prompts = mGrid.getHelpPrompts();
if(getSelectedIndex() != -1)
prompts.push_back(HelpPrompt("a", "accept result"));
return prompts;
}
void ScraperSearchComponent::onFocusGained()
{
mGrid.onFocusGained();
}
void ScraperSearchComponent::onFocusLost()
{
mGrid.onFocusLost();
}

View file

@ -0,0 +1,104 @@
#pragma once
#include "GuiComponent.h"
#include "scrapers/Scraper.h"
#include "components/ComponentGrid.h"
#include "components/BusyComponent.h"
#include <functional>
class ComponentList;
class ImageComponent;
class RatingComponent;
class TextComponent;
class DateTimeComponent;
class ScrollableContainer;
class HttpReq;
class AnimatedImageComponent;
class ScraperSearchComponent : public GuiComponent
{
public:
enum SearchType
{
ALWAYS_ACCEPT_FIRST_RESULT,
ALWAYS_ACCEPT_MATCHING_CRC,
NEVER_AUTO_ACCEPT
};
ScraperSearchComponent(Window* window, SearchType searchType = NEVER_AUTO_ACCEPT);
void search(const ScraperSearchParams& params);
void openInputScreen(ScraperSearchParams& from);
void stop();
inline SearchType getSearchType() const { return mSearchType; }
// Metadata assets will be resolved before calling the accept callback (e.g. result.mdl's "image" is automatically downloaded and properly set).
inline void setAcceptCallback(const std::function<void(const ScraperSearchResult&)>& acceptCallback) { mAcceptCallback = acceptCallback; }
inline void setSkipCallback(const std::function<void()>& skipCallback) { mSkipCallback = skipCallback; };
inline void setCancelCallback(const std::function<void()>& cancelCallback) { mCancelCallback = cancelCallback; }
bool input(InputConfig* config, Input input) override;
void update(int deltaTime) override;
void render(const Eigen::Affine3f& parentTrans) override;
std::vector<HelpPrompt> getHelpPrompts() override;
void onSizeChanged() override;
void onFocusGained() override;
void onFocusLost() override;
private:
void updateViewStyle();
void updateThumbnail();
void updateInfoPane();
void resizeMetadata();
void onSearchError(const std::string& error);
void onSearchDone(const std::vector<ScraperSearchResult>& results);
int getSelectedIndex();
// resolve any metadata assets that need to be downloaded and return
void returnResult(ScraperSearchResult result);
ComponentGrid mGrid;
std::shared_ptr<TextComponent> mResultName;
std::shared_ptr<ScrollableContainer> mDescContainer;
std::shared_ptr<TextComponent> mResultDesc;
std::shared_ptr<ImageComponent> mResultThumbnail;
std::shared_ptr<ComponentList> mResultList;
std::shared_ptr<ComponentGrid> mMD_Grid;
std::shared_ptr<RatingComponent> mMD_Rating;
std::shared_ptr<DateTimeComponent> mMD_ReleaseDate;
std::shared_ptr<TextComponent> mMD_Developer;
std::shared_ptr<TextComponent> mMD_Publisher;
std::shared_ptr<TextComponent> mMD_Genre;
std::shared_ptr<TextComponent> mMD_Players;
// label-component pair
struct MetaDataPair
{
std::shared_ptr<TextComponent> first;
std::shared_ptr<GuiComponent> second;
bool resize;
MetaDataPair(const std::shared_ptr<TextComponent>& f, const std::shared_ptr<GuiComponent>& s, bool r = true) : first(f), second(s), resize(r) {};
};
std::vector<MetaDataPair> mMD_Pairs;
SearchType mSearchType;
ScraperSearchParams mLastSearch;
std::function<void(const ScraperSearchResult&)> mAcceptCallback;
std::function<void()> mSkipCallback;
std::function<void()> mCancelCallback;
bool mBlockAccept;
std::unique_ptr<ScraperSearchHandle> mSearchHandle;
std::unique_ptr<MDResolveHandle> mMDResolveHandle;
std::vector<ScraperSearchResult> mScraperResults;
std::unique_ptr<HttpReq> mThumbnailReq;
BusyComponent mBusyAnim;
};

View file

@ -0,0 +1,369 @@
#pragma once
#include "components/IList.h"
#include "Renderer.h"
#include "resources/Font.h"
#include "InputManager.h"
#include "Sound.h"
#include "Log.h"
#include "ThemeData.h"
#include "Util.h"
#include <vector>
#include <string>
#include <memory>
#include <functional>
struct TextListData
{
unsigned int colorId;
std::shared_ptr<TextCache> textCache;
};
//A graphical list. Supports multiple colors for rows and scrolling.
template <typename T>
class TextListComponent : public IList<TextListData, T>
{
protected:
using IList<TextListData, T>::mEntries;
using IList<TextListData, T>::listUpdate;
using IList<TextListData, T>::listInput;
using IList<TextListData, T>::listRenderTitleOverlay;
using IList<TextListData, T>::getTransform;
using IList<TextListData, T>::mSize;
using IList<TextListData, T>::mCursor;
using IList<TextListData, T>::Entry;
public:
using IList<TextListData, T>::size;
using IList<TextListData, T>::isScrolling;
using IList<TextListData, T>::stopScrolling;
TextListComponent(Window* window);
bool input(InputConfig* config, Input input) override;
void update(int deltaTime) override;
void render(const Eigen::Affine3f& parentTrans) override;
void applyTheme(const std::shared_ptr<ThemeData>& theme, const std::string& view, const std::string& element, unsigned int properties) override;
void add(const std::string& name, const T& obj, unsigned int colorId);
enum Alignment
{
ALIGN_LEFT,
ALIGN_CENTER,
ALIGN_RIGHT
};
inline void setAlignment(Alignment align) { mAlignment = align; }
inline void setCursorChangedCallback(const std::function<void(CursorState state)>& func) { mCursorChangedCallback = func; }
inline void setFont(const std::shared_ptr<Font>& font)
{
mFont = font;
for(auto it = mEntries.begin(); it != mEntries.end(); it++)
it->data.textCache.reset();
}
inline void setUppercase(bool uppercase)
{
mUppercase = true;
for(auto it = mEntries.begin(); it != mEntries.end(); it++)
it->data.textCache.reset();
}
inline void setSelectorColor(unsigned int color) { mSelectorColor = color; }
inline void setSelectedColor(unsigned int color) { mSelectedColor = color; }
inline void setScrollSound(const std::shared_ptr<Sound>& sound) { mScrollSound = sound; }
inline void setColor(unsigned int id, unsigned int color) { mColors[id] = color; }
inline void setSound(const std::shared_ptr<Sound>& sound) { mScrollSound = sound; }
inline void setLineSpacing(float lineSpacing) { mLineSpacing = lineSpacing; }
protected:
virtual void onScroll(int amt) { if(mScrollSound) mScrollSound->play(); }
virtual void onCursorChanged(const CursorState& state);
private:
static const int MARQUEE_DELAY = 2000;
static const int MARQUEE_SPEED = 8;
static const int MARQUEE_RATE = 1;
int mMarqueeOffset;
int mMarqueeTime;
Alignment mAlignment;
float mHorizontalMargin;
std::function<void(CursorState state)> mCursorChangedCallback;
std::shared_ptr<Font> mFont;
bool mUppercase;
float mLineSpacing;
unsigned int mSelectorColor;
unsigned int mSelectedColor;
std::shared_ptr<Sound> mScrollSound;
static const unsigned int COLOR_ID_COUNT = 2;
unsigned int mColors[COLOR_ID_COUNT];
};
template <typename T>
TextListComponent<T>::TextListComponent(Window* window) :
IList<TextListData, T>(window)
{
mMarqueeOffset = 0;
mMarqueeTime = -MARQUEE_DELAY;
mHorizontalMargin = 0;
mAlignment = ALIGN_CENTER;
mFont = Font::get(FONT_SIZE_MEDIUM);
mUppercase = false;
mLineSpacing = 1.5f;
mSelectorColor = 0x000000FF;
mSelectedColor = 0;
mColors[0] = 0x0000FFFF;
mColors[1] = 0x00FF00FF;
}
template <typename T>
void TextListComponent<T>::render(const Eigen::Affine3f& parentTrans)
{
Eigen::Affine3f trans = parentTrans * getTransform();
std::shared_ptr<Font>& font = mFont;
if(size() == 0)
return;
const float entrySize = round(font->getHeight(mLineSpacing));
int startEntry = 0;
//number of entries that can fit on the screen simultaniously
int screenCount = (int)(mSize.y() / entrySize + 0.5f);
if(size() >= screenCount)
{
startEntry = mCursor - screenCount/2;
if(startEntry < 0)
startEntry = 0;
if(startEntry >= size() - screenCount)
startEntry = size() - screenCount;
}
float y = 0;
int listCutoff = startEntry + screenCount;
if(listCutoff > size())
listCutoff = size();
// draw selector bar
if(startEntry < listCutoff)
{
Renderer::setMatrix(trans);
Renderer::drawRect(0.f, (mCursor - startEntry)*entrySize + (entrySize - font->getHeight())/2, mSize.x(), font->getHeight(), mSelectorColor);
}
// clip to inside margins
Eigen::Vector3f dim(mSize.x(), mSize.y(), 0);
dim = trans * dim - trans.translation();
Renderer::pushClipRect(Eigen::Vector2i((int)(trans.translation().x() + mHorizontalMargin), (int)trans.translation().y()),
Eigen::Vector2i((int)(dim.x() - mHorizontalMargin*2), (int)dim.y()));
for(int i = startEntry; i < listCutoff; i++)
{
typename IList<TextListData, T>::Entry& entry = mEntries.at((unsigned int)i);
unsigned int color;
if(mCursor == i && mSelectedColor)
color = mSelectedColor;
else
color = mColors[entry.data.colorId];
if(!entry.data.textCache)
entry.data.textCache = std::unique_ptr<TextCache>(font->buildTextCache(mUppercase ? strToUpper(entry.name) : entry.name, 0, 0, 0x000000FF));
entry.data.textCache->setColor(color);
Eigen::Vector3f offset(0, y, 0);
switch(mAlignment)
{
case ALIGN_LEFT:
offset[0] = mHorizontalMargin;
break;
case ALIGN_CENTER:
offset[0] = (mSize.x() - entry.data.textCache->metrics.size.x()) / 2;
if(offset[0] < 0)
offset[0] = 0;
break;
case ALIGN_RIGHT:
offset[0] = (mSize.x() - entry.data.textCache->metrics.size.x());
offset[0] -= mHorizontalMargin;
if(offset[0] < 0)
offset[0] = 0;
break;
}
if(mCursor == i)
offset[0] -= mMarqueeOffset;
Eigen::Affine3f drawTrans = trans;
drawTrans.translate(offset);
Renderer::setMatrix(drawTrans);
font->renderTextCache(entry.data.textCache.get());
y += entrySize;
}
Renderer::popClipRect();
listRenderTitleOverlay(trans);
GuiComponent::renderChildren(trans);
}
template <typename T>
bool TextListComponent<T>::input(InputConfig* config, Input input)
{
if(size() > 0)
{
if(input.value != 0)
{
if(config->isMappedTo("down", input))
{
listInput(1);
return true;
}
if(config->isMappedTo("up", input))
{
listInput(-1);
return true;
}
if(config->isMappedTo("pagedown", input))
{
listInput(10);
return true;
}
if(config->isMappedTo("pageup", input))
{
listInput(-10);
return true;
}
}else{
if(config->isMappedTo("down", input) || config->isMappedTo("up", input) ||
config->isMappedTo("pagedown", input) || config->isMappedTo("pageup", input))
{
stopScrolling();
}
}
}
return GuiComponent::input(config, input);
}
template <typename T>
void TextListComponent<T>::update(int deltaTime)
{
listUpdate(deltaTime);
if(!isScrolling() && size() > 0)
{
//if we're not scrolling and this object's text goes outside our size, marquee it!
const std::string& text = mEntries.at((unsigned int)mCursor).name;
Eigen::Vector2f textSize = mFont->sizeText(text);
//it's long enough to marquee
if(textSize.x() - mMarqueeOffset > mSize.x() - 12 - (mAlignment != ALIGN_CENTER ? mHorizontalMargin : 0))
{
mMarqueeTime += deltaTime;
while(mMarqueeTime > MARQUEE_SPEED)
{
mMarqueeOffset += MARQUEE_RATE;
mMarqueeTime -= MARQUEE_SPEED;
}
}
}
GuiComponent::update(deltaTime);
}
//list management stuff
template <typename T>
void TextListComponent<T>::add(const std::string& name, const T& obj, unsigned int color)
{
assert(color < COLOR_ID_COUNT);
typename IList<TextListData, T>::Entry entry;
entry.name = name;
entry.object = obj;
entry.data.colorId = color;
static_cast<IList< TextListData, T >*>(this)->add(entry);
}
template <typename T>
void TextListComponent<T>::onCursorChanged(const CursorState& state)
{
mMarqueeOffset = 0;
mMarqueeTime = -MARQUEE_DELAY;
if(mCursorChangedCallback)
mCursorChangedCallback(state);
}
template <typename T>
void TextListComponent<T>::applyTheme(const std::shared_ptr<ThemeData>& theme, const std::string& view, const std::string& element, unsigned int properties)
{
GuiComponent::applyTheme(theme, view, element, properties);
const ThemeData::ThemeElement* elem = theme->getElement(view, element, "textlist");
if(!elem)
return;
using namespace ThemeFlags;
if(properties & COLOR)
{
if(elem->has("selectorColor"))
setSelectorColor(elem->get<unsigned int>("selectorColor"));
if(elem->has("selectedColor"))
setSelectedColor(elem->get<unsigned int>("selectedColor"));
if(elem->has("primaryColor"))
setColor(0, elem->get<unsigned int>("primaryColor"));
if(elem->has("secondaryColor"))
setColor(1, elem->get<unsigned int>("secondaryColor"));
}
setFont(Font::getFromTheme(elem, properties, mFont));
if(properties & SOUND && elem->has("scrollSound"))
setSound(Sound::get(elem->get<std::string>("scrollSound")));
if(properties & ALIGNMENT)
{
if(elem->has("alignment"))
{
const std::string& str = elem->get<std::string>("alignment");
if(str == "left")
setAlignment(ALIGN_LEFT);
else if(str == "center")
setAlignment(ALIGN_CENTER);
else if(str == "right")
setAlignment(ALIGN_RIGHT);
else
LOG(LogError) << "Unknown TextListComponent alignment \"" << str << "\"!";
}
if(elem->has("horizontalMargin"))
{
mHorizontalMargin = elem->get<float>("horizontalMargin") * (this->mParent ? this->mParent->getSize().x() : (float)Renderer::getScreenWidth());
}
}
if(properties & FORCE_UPPERCASE && elem->has("forceUppercase"))
setUppercase(elem->get<bool>("forceUppercase"));
if(properties & LINE_SPACING && elem->has("lineSpacing"))
setLineSpacing(elem->get<float>("lineSpacing"));
}

View file

@ -0,0 +1,163 @@
#include "guis/GuiFastSelect.h"
#include "ThemeData.h"
#include "FileSorts.h"
#include "SystemData.h"
static const std::string LETTERS = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
GuiFastSelect::GuiFastSelect(Window* window, IGameListView* gamelist) : GuiComponent(window),
mBackground(window), mSortText(window), mLetterText(window), mGameList(gamelist)
{
setPosition(Renderer::getScreenWidth() * 0.2f, Renderer::getScreenHeight() * 0.2f);
setSize(Renderer::getScreenWidth() * 0.6f, Renderer::getScreenHeight() * 0.6f);
const std::shared_ptr<ThemeData>& theme = mGameList->getTheme();
using namespace ThemeFlags;
mBackground.applyTheme(theme, "fastSelect", "windowBackground", PATH);
mBackground.fitTo(mSize);
addChild(&mBackground);
mLetterText.setSize(mSize.x(), mSize.y() * 0.75f);
mLetterText.setAlignment(ALIGN_CENTER);
mLetterText.applyTheme(theme, "fastSelect", "letter", FONT_PATH | COLOR);
// TODO - set font size
addChild(&mLetterText);
mSortText.setPosition(0, mSize.y() * 0.75f);
mSortText.setSize(mSize.x(), mSize.y() * 0.25f);
mSortText.setAlignment(ALIGN_CENTER);
mSortText.applyTheme(theme, "fastSelect", "subtext", FONT_PATH | COLOR);
// TODO - set font size
addChild(&mSortText);
mSortId = 0; // TODO
updateSortText();
mLetterId = LETTERS.find(mGameList->getCursor()->getName()[0]);
if(mLetterId == std::string::npos)
mLetterId = 0;
mScrollDir = 0;
mScrollAccumulator = 0;
scroll(); // initialize the letter value
}
bool GuiFastSelect::input(InputConfig* config, Input input)
{
if(input.value == 0 && config->isMappedTo("select", input))
{
// the user let go of select; make our changes to the gamelist and close this gui
updateGameListSort();
updateGameListCursor();
delete this;
return true;
}
if(config->isMappedTo("up", input))
{
if(input.value != 0)
setScrollDir(-1);
else
setScrollDir(0);
return true;
}else if(config->isMappedTo("down", input))
{
if(input.value != 0)
setScrollDir(1);
else
setScrollDir(0);
return true;
}else if(config->isMappedTo("left", input) && input.value != 0)
{
mSortId = (mSortId + 1) % FileSorts::SortTypes.size();
updateSortText();
return true;
}else if(config->isMappedTo("right", input) && input.value != 0)
{
mSortId--;
if(mSortId < 0)
mSortId += FileSorts::SortTypes.size();
updateSortText();
return true;
}
return GuiComponent::input(config, input);
}
void GuiFastSelect::setScrollDir(int dir)
{
mScrollDir = dir;
scroll();
mScrollAccumulator = -500;
}
void GuiFastSelect::update(int deltaTime)
{
if(mScrollDir != 0)
{
mScrollAccumulator += deltaTime;
while(mScrollAccumulator >= 150)
{
scroll();
mScrollAccumulator -= 150;
}
}
GuiComponent::update(deltaTime);
}
void GuiFastSelect::scroll()
{
mLetterId += mScrollDir;
if(mLetterId < 0)
mLetterId += LETTERS.length();
else if(mLetterId >= (int)LETTERS.length())
mLetterId -= LETTERS.length();
mLetterText.setText(LETTERS.substr(mLetterId, 1));
}
void GuiFastSelect::updateSortText()
{
std::stringstream ss;
ss << "<- " << FileSorts::SortTypes.at(mSortId).description << " ->";
mSortText.setText(ss.str());
}
void GuiFastSelect::updateGameListSort()
{
const FileData::SortType& sort = FileSorts::SortTypes.at(mSortId);
FileData* root = mGameList->getCursor()->getSystem()->getRootFolder();
root->sort(sort); // will also recursively sort children
// notify that the root folder was sorted
mGameList->onFileChanged(root, FILE_SORTED);
}
void GuiFastSelect::updateGameListCursor()
{
const std::vector<FileData*>& list = mGameList->getCursor()->getParent()->getChildren();
// only skip by letter when the sort mode is alphabetical
const FileData::SortType& sort = FileSorts::SortTypes.at(mSortId);
if(sort.comparisonFunction != &FileSorts::compareFileName)
return;
// find the first entry in the list that either exactly matches our target letter or is beyond our target letter
for(auto it = list.cbegin(); it != list.cend(); it++)
{
char check = (*it)->getName().empty() ? 'A' : (*it)->getName()[0];
// if there's an exact match or we've passed it, set the cursor to this one
if(check == LETTERS[mLetterId] || (sort.ascending && check > LETTERS[mLetterId]) || (!sort.ascending && check < LETTERS[mLetterId]))
{
mGameList->setCursor(*it);
break;
}
}
}

View file

@ -0,0 +1,35 @@
#pragma once
#include "GuiComponent.h"
#include "views/gamelist/IGameListView.h"
#include "components/NinePatchComponent.h"
#include "components/TextComponent.h"
class GuiFastSelect : public GuiComponent
{
public:
GuiFastSelect(Window* window, IGameListView* gamelist);
bool input(InputConfig* config, Input input);
void update(int deltaTime);
private:
void setScrollDir(int dir);
void scroll();
void updateGameListCursor();
void updateGameListSort();
void updateSortText();
int mSortId;
int mLetterId;
int mScrollDir;
int mScrollAccumulator;
NinePatchComponent mBackground;
TextComponent mSortText;
TextComponent mLetterText;
IGameListView* mGameList;
};

View file

@ -0,0 +1,122 @@
#include "guis/GuiGameScraper.h"
#include "guis/GuiTextEditPopup.h"
#include "components/TextComponent.h"
#include "components/ButtonComponent.h"
#include "components/MenuComponent.h"
#include "scrapers/Scraper.h"
#include "Renderer.h"
#include "Log.h"
#include "Settings.h"
GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std::function<void(const ScraperSearchResult&)> doneFunc) : GuiComponent(window),
mGrid(window, Eigen::Vector2i(1, 7)),
mBox(window, ":/frame.png"),
mSearchParams(params),
mClose(false)
{
addChild(&mBox);
addChild(&mGrid);
// row 0 is a spacer
mGameName = std::make_shared<TextComponent>(mWindow, strToUpper(mSearchParams.game->getPath().filename().generic_string()),
Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER);
mGrid.setEntry(mGameName, Eigen::Vector2i(0, 1), false, true);
// row 2 is a spacer
mSystemName = std::make_shared<TextComponent>(mWindow, strToUpper(mSearchParams.system->getFullName()), Font::get(FONT_SIZE_SMALL),
0x888888FF, ALIGN_CENTER);
mGrid.setEntry(mSystemName, Eigen::Vector2i(0, 3), false, true);
// row 4 is a spacer
// ScraperSearchComponent
mSearch = std::make_shared<ScraperSearchComponent>(window, ScraperSearchComponent::NEVER_AUTO_ACCEPT);
mGrid.setEntry(mSearch, Eigen::Vector2i(0, 5), true);
// buttons
std::vector< std::shared_ptr<ButtonComponent> > buttons;
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "INPUT", "search", [&] {
mSearch->openInputScreen(mSearchParams);
mGrid.resetCursor();
}));
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "CANCEL", "cancel", [&] { delete this; }));
mButtonGrid = makeButtonGrid(mWindow, buttons);
mGrid.setEntry(mButtonGrid, Eigen::Vector2i(0, 6), true, false);
// we call this->close() instead of just delete this; in the accept callback:
// this is because of how GuiComponent::update works. if it was just delete this, this would happen when the metadata resolver is done:
// GuiGameScraper::update()
// GuiComponent::update()
// it = mChildren.begin();
// mBox::update()
// it++;
// mSearchComponent::update()
// acceptCallback -> delete this
// it++; // error, mChildren has been deleted because it was part of this
// so instead we do this:
// GuiGameScraper::update()
// GuiComponent::update()
// it = mChildren.begin();
// mBox::update()
// it++;
// mSearchComponent::update()
// acceptCallback -> close() -> mClose = true
// it++; // ok
// if(mClose)
// delete this;
mSearch->setAcceptCallback([this, doneFunc](const ScraperSearchResult& result) { doneFunc(result); close(); });
mSearch->setCancelCallback([&] { delete this; });
setSize(Renderer::getScreenWidth() * 0.95f, Renderer::getScreenHeight() * 0.747f);
setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2);
mGrid.resetCursor();
mSearch->search(params); // start the search
}
void GuiGameScraper::onSizeChanged()
{
mBox.fitTo(mSize, Eigen::Vector3f::Zero(), Eigen::Vector2f(-32, -32));
mGrid.setRowHeightPerc(0, 0.04f, false);
mGrid.setRowHeightPerc(1, mGameName->getFont()->getLetterHeight() / mSize.y(), false); // game name
mGrid.setRowHeightPerc(2, 0.04f, false);
mGrid.setRowHeightPerc(3, mSystemName->getFont()->getLetterHeight() / mSize.y(), false); // system name
mGrid.setRowHeightPerc(4, 0.04f, false);
mGrid.setRowHeightPerc(6, mButtonGrid->getSize().y() / mSize.y(), false); // buttons
mGrid.setSize(mSize);
}
bool GuiGameScraper::input(InputConfig* config, Input input)
{
if(config->isMappedTo("b", input) && input.value)
{
delete this;
return true;
}
return GuiComponent::input(config, input);
}
void GuiGameScraper::update(int deltaTime)
{
GuiComponent::update(deltaTime);
if(mClose)
delete this;
}
std::vector<HelpPrompt> GuiGameScraper::getHelpPrompts()
{
return mGrid.getHelpPrompts();
}
void GuiGameScraper::close()
{
mClose = true;
}

View file

@ -0,0 +1,33 @@
#pragma once
#include "GuiComponent.h"
#include "components/ScraperSearchComponent.h"
#include "components/NinePatchComponent.h"
class GuiGameScraper : public GuiComponent
{
public:
GuiGameScraper(Window* window, ScraperSearchParams params, std::function<void(const ScraperSearchResult&)> doneFunc);
void onSizeChanged() override;
bool input(InputConfig* config, Input input) override;
void update(int deltaTime);
virtual std::vector<HelpPrompt> getHelpPrompts() override;
private:
bool mClose;
void close();
ComponentGrid mGrid;
NinePatchComponent mBox;
std::shared_ptr<TextComponent> mGameName;
std::shared_ptr<TextComponent> mSystemName;
std::shared_ptr<ScraperSearchComponent> mSearch;
std::shared_ptr<ComponentGrid> mButtonGrid;
ScraperSearchParams mSearchParams;
std::function<void()> mCancelFunc;
};

View file

@ -0,0 +1,81 @@
#include "GuiGamelistOptions.h"
#include "GuiMetaDataEd.h"
#include "views/gamelist/IGameListView.h"
#include "views/ViewController.h"
GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : GuiComponent(window),
mSystem(system),
mMenu(window, "OPTIONS")
{
addChild(&mMenu);
// 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);
// edit game metadata
ComponentListRow row;
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);
}
GuiGamelistOptions::~GuiGamelistOptions()
{
// apply sort
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);
}
void GuiGamelistOptions::openMetaDataEd()
{
// open metadata editor
FileData* file = getGamelist()->getCursor();
ScraperSearchParams p;
p.game = file;
p.system = file->getSystem();
mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(), p, file->getPath().filename().string(),
std::bind(&IGameListView::onFileChanged, getGamelist(), file, FILE_METADATA_CHANGED), [this, file] {
boost::filesystem::remove(file->getPath()); //actually delete the file on the filesystem
file->getParent()->removeChild(file); //unlink it so list repopulations triggered from onFileChanged won't see it
getGamelist()->onFileChanged(file, FILE_REMOVED); //tell the view
delete file; //free it
}));
}
bool GuiGamelistOptions::input(InputConfig* config, Input input)
{
if((config->isMappedTo("b", input) || config->isMappedTo("select", input)) && input.value)
{
delete this;
return true;
}
return mMenu.input(config, input);
}
std::vector<HelpPrompt> GuiGamelistOptions::getHelpPrompts()
{
auto prompts = mMenu.getHelpPrompts();
prompts.push_back(HelpPrompt("b", "close"));
return prompts;
}
IGameListView* GuiGamelistOptions::getGamelist()
{
return ViewController::get()->getGameListView(mSystem).get();
}

View file

@ -0,0 +1,27 @@
#include "GuiComponent.h"
#include "components/MenuComponent.h"
#include "components/OptionListComponent.h"
#include "FileSorts.h"
class IGameListView;
class GuiGamelistOptions : public GuiComponent
{
public:
GuiGamelistOptions(Window* window, SystemData* system);
virtual ~GuiGamelistOptions();
virtual bool input(InputConfig* config, Input input) override;
virtual std::vector<HelpPrompt> getHelpPrompts() override;
private:
void openMetaDataEd();
MenuComponent mMenu;
typedef OptionListComponent<const FileData::SortType*> SortList;
std::shared_ptr<SortList> mListSort;
SystemData* mSystem;
IGameListView* getGamelist();
};

277
es-app/src/guis/GuiMenu.cpp Normal file
View file

@ -0,0 +1,277 @@
#include "EmulationStation.h"
#include "guis/GuiMenu.h"
#include "Window.h"
#include "Sound.h"
#include "Log.h"
#include "Settings.h"
#include "guis/GuiMsgBox.h"
#include "guis/GuiSettings.h"
#include "guis/GuiScraperStart.h"
#include "guis/GuiDetectDevice.h"
#include "views/ViewController.h"
#include "components/ButtonComponent.h"
#include "components/SwitchComponent.h"
#include "components/SliderComponent.h"
#include "components/TextComponent.h"
#include "components/OptionListComponent.h"
#include "components/MenuComponent.h"
#include "VolumeControl.h"
#include "scrapers/GamesDBScraper.h"
#include "scrapers/TheArchiveScraper.h"
GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, "MAIN MENU"), mVersion(window)
{
// MAIN MENU
// SCRAPER >
// SOUND SETTINGS >
// UI SETTINGS >
// CONFIGURE INPUT >
// QUIT >
// [version]
auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); };
addEntry("SCRAPER", 0x777777FF, true,
[this, openScrapeNow] {
auto s = new GuiSettings(mWindow, "SCRAPER");
// scrape from
auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, "SCRAPE FROM", false);
std::vector<std::string> scrapers = getScraperList();
for(auto it = scrapers.begin(); it != scrapers.end(); it++)
scraper_list->add(*it, *it, *it == Settings::getInstance()->getString("Scraper"));
s->addWithLabel("SCRAPE FROM", scraper_list);
s->addSaveFunc([scraper_list] { Settings::getInstance()->setString("Scraper", scraper_list->getSelected()); });
// scrape ratings
auto scrape_ratings = std::make_shared<SwitchComponent>(mWindow);
scrape_ratings->setState(Settings::getInstance()->getBool("ScrapeRatings"));
s->addWithLabel("SCRAPE RATINGS", scrape_ratings);
s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); });
// scrape now
ComponentListRow row;
std::function<void()> openAndSave = openScrapeNow;
openAndSave = [s, openAndSave] { s->save(); openAndSave(); };
row.makeAcceptInputHandler(openAndSave);
auto scrape_now = std::make_shared<TextComponent>(mWindow, "SCRAPE NOW", Font::get(FONT_SIZE_MEDIUM), 0x777777FF);
auto bracket = makeArrow(mWindow);
row.addElement(scrape_now, true);
row.addElement(bracket, false);
s->addRow(row);
mWindow->pushGui(s);
});
addEntry("SOUND SETTINGS", 0x777777FF, true,
[this] {
auto s = new GuiSettings(mWindow, "SOUND SETTINGS");
// volume
auto volume = std::make_shared<SliderComponent>(mWindow, 0.f, 100.f, 1.f, "%");
volume->setValue((float)VolumeControl::getInstance()->getVolume());
s->addWithLabel("SYSTEM VOLUME", volume);
s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)volume->getValue()); });
// disable sounds
auto sounds_enabled = std::make_shared<SwitchComponent>(mWindow);
sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds"));
s->addWithLabel("ENABLE SOUNDS", sounds_enabled);
s->addSaveFunc([sounds_enabled] { Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState()); });
mWindow->pushGui(s);
});
addEntry("UI SETTINGS", 0x777777FF, true,
[this] {
auto s = new GuiSettings(mWindow, "UI SETTINGS");
// screensaver time
auto screensaver_time = std::make_shared<SliderComponent>(mWindow, 0.f, 30.f, 1.f, "m");
screensaver_time->setValue((float)(Settings::getInstance()->getInt("ScreenSaverTime") / (1000 * 60)));
s->addWithLabel("SCREENSAVER AFTER", screensaver_time);
s->addSaveFunc([screensaver_time] { Settings::getInstance()->setInt("ScreenSaverTime", (int)round(screensaver_time->getValue()) * (1000 * 60)); });
// screensaver behavior
auto screensaver_behavior = std::make_shared< OptionListComponent<std::string> >(mWindow, "TRANSITION STYLE", false);
std::vector<std::string> screensavers;
screensavers.push_back("dim");
screensavers.push_back("black");
for(auto it = screensavers.begin(); it != screensavers.end(); it++)
screensaver_behavior->add(*it, *it, Settings::getInstance()->getString("ScreenSaverBehavior") == *it);
s->addWithLabel("SCREENSAVER BEHAVIOR", screensaver_behavior);
s->addSaveFunc([screensaver_behavior] { Settings::getInstance()->setString("ScreenSaverBehavior", screensaver_behavior->getSelected()); });
// framerate
auto framerate = std::make_shared<SwitchComponent>(mWindow);
framerate->setState(Settings::getInstance()->getBool("DrawFramerate"));
s->addWithLabel("SHOW FRAMERATE", framerate);
s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); });
// show help
auto show_help = std::make_shared<SwitchComponent>(mWindow);
show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts"));
s->addWithLabel("ON-SCREEN HELP", show_help);
s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); });
// quick system select (left/right in game list view)
auto quick_sys_select = std::make_shared<SwitchComponent>(mWindow);
quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect"));
s->addWithLabel("QUICK SYSTEM SELECT", quick_sys_select);
s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); });
// transition style
auto transition_style = std::make_shared< OptionListComponent<std::string> >(mWindow, "TRANSITION STYLE", false);
std::vector<std::string> transitions;
transitions.push_back("fade");
transitions.push_back("slide");
for(auto it = transitions.begin(); it != transitions.end(); it++)
transition_style->add(*it, *it, Settings::getInstance()->getString("TransitionStyle") == *it);
s->addWithLabel("TRANSITION STYLE", transition_style);
s->addSaveFunc([transition_style] { Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); });
// theme set
auto themeSets = ThemeData::getThemeSets();
if(!themeSets.empty())
{
auto selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet"));
if(selectedSet == themeSets.end())
selectedSet = themeSets.begin();
auto theme_set = std::make_shared< OptionListComponent<std::string> >(mWindow, "THEME SET", false);
for(auto it = themeSets.begin(); it != themeSets.end(); it++)
theme_set->add(it->first, it->first, it == selectedSet);
s->addWithLabel("THEME SET", theme_set);
Window* window = mWindow;
s->addSaveFunc([window, theme_set]
{
bool needReload = false;
if(Settings::getInstance()->getString("ThemeSet") != theme_set->getSelected())
needReload = true;
Settings::getInstance()->setString("ThemeSet", theme_set->getSelected());
if(needReload)
ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation
});
}
mWindow->pushGui(s);
});
addEntry("CONFIGURE INPUT", 0x777777FF, true,
[this] {
mWindow->pushGui(new GuiDetectDevice(mWindow, false, nullptr));
});
addEntry("QUIT", 0x777777FF, true,
[this] {
auto s = new GuiSettings(mWindow, "QUIT");
Window* window = mWindow;
ComponentListRow row;
row.makeAcceptInputHandler([window] {
window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES",
[] {
if(system("sudo shutdown -r now") != 0)
LOG(LogWarning) << "Restart terminated with non-zero result!";
}, "NO", nullptr));
});
row.addElement(std::make_shared<TextComponent>(window, "RESTART SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true);
s->addRow(row);
row.elements.clear();
row.makeAcceptInputHandler([window] {
window->pushGui(new GuiMsgBox(window, "REALLY SHUTDOWN?", "YES",
[] {
if(system("sudo shutdown -h now") != 0)
LOG(LogWarning) << "Shutdown terminated with non-zero result!";
}, "NO", nullptr));
});
row.addElement(std::make_shared<TextComponent>(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true);
s->addRow(row);
if(Settings::getInstance()->getBool("ShowExit"))
{
row.elements.clear();
row.makeAcceptInputHandler([window] {
window->pushGui(new GuiMsgBox(window, "REALLY QUIT?", "YES",
[] {
SDL_Event ev;
ev.type = SDL_QUIT;
SDL_PushEvent(&ev);
}, "NO", nullptr));
});
row.addElement(std::make_shared<TextComponent>(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true);
s->addRow(row);
}
mWindow->pushGui(s);
});
mVersion.setFont(Font::get(FONT_SIZE_SMALL));
mVersion.setColor(0xC6C6C6FF);
mVersion.setText("EMULATIONSTATION V" + strToUpper(PROGRAM_VERSION_STRING));
mVersion.setAlignment(ALIGN_CENTER);
addChild(&mMenu);
addChild(&mVersion);
setSize(mMenu.getSize());
setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, Renderer::getScreenHeight() * 0.15f);
}
void GuiMenu::onSizeChanged()
{
mVersion.setSize(mSize.x(), 0);
mVersion.setPosition(0, mSize.y() - mVersion.getSize().y());
}
void GuiMenu::addEntry(const char* name, unsigned int color, bool add_arrow, const std::function<void()>& func)
{
std::shared_ptr<Font> font = Font::get(FONT_SIZE_MEDIUM);
// populate the list
ComponentListRow row;
row.addElement(std::make_shared<TextComponent>(mWindow, name, font, color), true);
if(add_arrow)
{
std::shared_ptr<ImageComponent> bracket = makeArrow(mWindow);
row.addElement(bracket, false);
}
row.makeAcceptInputHandler(func);
mMenu.addRow(row);
}
bool GuiMenu::input(InputConfig* config, Input input)
{
if(GuiComponent::input(config, input))
return true;
if((config->isMappedTo("b", input) || config->isMappedTo("start", input)) && input.value != 0)
{
delete this;
return true;
}
return false;
}
std::vector<HelpPrompt> GuiMenu::getHelpPrompts()
{
std::vector<HelpPrompt> prompts;
prompts.push_back(HelpPrompt("up/down", "choose"));
prompts.push_back(HelpPrompt("a", "select"));
prompts.push_back(HelpPrompt("start", "close"));
return prompts;
}

21
es-app/src/guis/GuiMenu.h Normal file
View file

@ -0,0 +1,21 @@
#pragma once
#include "GuiComponent.h"
#include "components/MenuComponent.h"
#include <functional>
class GuiMenu : public GuiComponent
{
public:
GuiMenu(Window* window);
bool input(InputConfig* config, Input input) override;
void onSizeChanged() override;
std::vector<HelpPrompt> getHelpPrompts() override;
private:
void addEntry(const char* name, unsigned int color, bool add_arrow, const std::function<void()>& func);
MenuComponent mMenu;
TextComponent mVersion;
};

View file

@ -0,0 +1,264 @@
#include "guis/GuiMetaDataEd.h"
#include "Renderer.h"
#include "Log.h"
#include "components/AsyncReqComponent.h"
#include "Settings.h"
#include "views/ViewController.h"
#include "guis/GuiGameScraper.h"
#include "guis/GuiMsgBox.h"
#include <boost/filesystem.hpp>
#include "components/TextEditComponent.h"
#include "components/DateTimeComponent.h"
#include "components/RatingComponent.h"
#include "guis/GuiTextEditPopup.h"
using namespace Eigen;
GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector<MetaDataDecl>& mdd, ScraperSearchParams scraperParams,
const std::string& header, std::function<void()> saveCallback, std::function<void()> deleteFunc) : GuiComponent(window),
mScraperParams(scraperParams),
mBackground(window, ":/frame.png"),
mGrid(window, Vector2i(1, 3)),
mMetaDataDecl(mdd),
mMetaData(md),
mSavedCallback(saveCallback), mDeleteFunc(deleteFunc)
{
addChild(&mBackground);
addChild(&mGrid);
mHeaderGrid = std::make_shared<ComponentGrid>(mWindow, Vector2i(1, 5));
mTitle = std::make_shared<TextComponent>(mWindow, "EDIT METADATA", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER);
mSubtitle = std::make_shared<TextComponent>(mWindow, strToUpper(scraperParams.game->getPath().filename().generic_string()),
Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER);
mHeaderGrid->setEntry(mTitle, Vector2i(0, 1), false, true);
mHeaderGrid->setEntry(mSubtitle, Vector2i(0, 3), false, true);
mGrid.setEntry(mHeaderGrid, Vector2i(0, 0), false, true);
mList = std::make_shared<ComponentList>(mWindow);
mGrid.setEntry(mList, Vector2i(0, 1), true, true);
// populate list
for(auto iter = mdd.begin(); iter != mdd.end(); iter++)
{
std::shared_ptr<GuiComponent> ed;
// don't add statistics
if(iter->isStatistic)
continue;
// create ed and add it (and any related components) to mMenu
// ed's value will be set below
ComponentListRow row;
auto lbl = std::make_shared<TextComponent>(mWindow, strToUpper(iter->displayName), Font::get(FONT_SIZE_SMALL), 0x777777FF);
row.addElement(lbl, true); // label
switch(iter->type)
{
case MD_RATING:
{
ed = std::make_shared<RatingComponent>(window);
const float height = lbl->getSize().y() * 0.71f;
ed->setSize(0, height);
row.addElement(ed, false, true);
auto spacer = std::make_shared<GuiComponent>(mWindow);
spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0);
row.addElement(spacer, false);
// pass input to the actual RatingComponent instead of the spacer
row.input_handler = std::bind(&GuiComponent::input, ed.get(), std::placeholders::_1, std::placeholders::_2);
break;
}
case MD_DATE:
{
ed = std::make_shared<DateTimeComponent>(window);
row.addElement(ed, false);
auto spacer = std::make_shared<GuiComponent>(mWindow);
spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0);
row.addElement(spacer, false);
// pass input to the actual DateTimeComponent instead of the spacer
row.input_handler = std::bind(&GuiComponent::input, ed.get(), std::placeholders::_1, std::placeholders::_2);
break;
}
case MD_TIME:
{
ed = std::make_shared<DateTimeComponent>(window, DateTimeComponent::DISP_RELATIVE_TO_NOW);
row.addElement(ed, false);
break;
}
case MD_MULTILINE_STRING:
default:
{
// MD_STRING
ed = std::make_shared<TextComponent>(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT);
row.addElement(ed, true);
auto spacer = std::make_shared<GuiComponent>(mWindow);
spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0);
row.addElement(spacer, false);
auto bracket = std::make_shared<ImageComponent>(mWindow);
bracket->setImage(":/arrow.svg");
bracket->setResize(Eigen::Vector2f(0, lbl->getFont()->getLetterHeight()));
row.addElement(bracket, false);
bool multiLine = iter->type == MD_MULTILINE_STRING;
const std::string title = iter->displayPrompt;
auto updateVal = [ed](const std::string& newVal) { ed->setValue(newVal); }; // ok callback (apply new value to ed)
row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] {
mWindow->pushGui(new GuiTextEditPopup(mWindow, title, ed->getValue(), updateVal, multiLine));
});
break;
}
}
assert(ed);
mList->addRow(row);
ed->setValue(mMetaData->get(iter->key));
mEditors.push_back(ed);
}
std::vector< std::shared_ptr<ButtonComponent> > buttons;
if(!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE))
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "SCRAPE", "scrape", std::bind(&GuiMetaDataEd::fetch, this)));
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "SAVE", "save", [&] { save(); delete this; }));
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "CANCEL", "cancel", [&] { delete this; }));
if(mDeleteFunc)
{
auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; };
auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE A FILE!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); };
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "DELETE", "delete", deleteBtnFunc));
}
mButtons = makeButtonGrid(mWindow, buttons);
mGrid.setEntry(mButtons, Vector2i(0, 2), true, false);
// resize + center
setSize(Renderer::getScreenWidth() * 0.5f, Renderer::getScreenHeight() * 0.82f);
setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2);
}
void GuiMetaDataEd::onSizeChanged()
{
mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32));
mGrid.setSize(mSize);
const float titleHeight = mTitle->getFont()->getLetterHeight();
const float subtitleHeight = mSubtitle->getFont()->getLetterHeight();
const float titleSubtitleSpacing = mSize.y() * 0.03f;
mGrid.setRowHeightPerc(0, (titleHeight + titleSubtitleSpacing + subtitleHeight + TITLE_VERT_PADDING) / mSize.y());
mGrid.setRowHeightPerc(2, mButtons->getSize().y() / mSize.y());
mHeaderGrid->setRowHeightPerc(1, titleHeight / mHeaderGrid->getSize().y());
mHeaderGrid->setRowHeightPerc(2, titleSubtitleSpacing / mHeaderGrid->getSize().y());
mHeaderGrid->setRowHeightPerc(3, subtitleHeight / mHeaderGrid->getSize().y());
}
void GuiMetaDataEd::save()
{
for(unsigned int i = 0; i < mEditors.size(); i++)
{
if(mMetaDataDecl.at(i).isStatistic)
continue;
mMetaData->set(mMetaDataDecl.at(i).key, mEditors.at(i)->getValue());
}
if(mSavedCallback)
mSavedCallback();
}
void GuiMetaDataEd::fetch()
{
GuiGameScraper* scr = new GuiGameScraper(mWindow, mScraperParams, std::bind(&GuiMetaDataEd::fetchDone, this, std::placeholders::_1));
mWindow->pushGui(scr);
}
void GuiMetaDataEd::fetchDone(const ScraperSearchResult& result)
{
for(unsigned int i = 0; i < mEditors.size(); i++)
{
if(mMetaDataDecl.at(i).isStatistic)
continue;
const std::string& key = mMetaDataDecl.at(i).key;
mEditors.at(i)->setValue(result.mdl.get(key));
}
}
void GuiMetaDataEd::close(bool closeAllWindows)
{
// find out if the user made any changes
bool dirty = false;
for(unsigned int i = 0; i < mEditors.size(); i++)
{
const std::string& key = mMetaDataDecl.at(i).key;
if(mMetaData->get(key) != mEditors.at(i)->getValue())
{
dirty = true;
break;
}
}
std::function<void()> closeFunc;
if(!closeAllWindows)
{
closeFunc = [this] { delete this; };
}else{
Window* window = mWindow;
closeFunc = [window, this] {
while(window->peekGui() != ViewController::get())
delete window->peekGui();
};
}
if(dirty)
{
// changes were made, ask if the user wants to save them
mWindow->pushGui(new GuiMsgBox(mWindow,
"SAVE CHANGES?",
"YES", [this, closeFunc] { save(); closeFunc(); },
"NO", closeFunc
));
}else{
closeFunc();
}
}
bool GuiMetaDataEd::input(InputConfig* config, Input input)
{
if(GuiComponent::input(config, input))
return true;
const bool isStart = config->isMappedTo("start", input);
if(input.value != 0 && (config->isMappedTo("b", input) || isStart))
{
close(isStart);
return true;
}
return false;
}
std::vector<HelpPrompt> GuiMetaDataEd::getHelpPrompts()
{
std::vector<HelpPrompt> prompts = mGrid.getHelpPrompts();
prompts.push_back(HelpPrompt("b", "back"));
prompts.push_back(HelpPrompt("start", "close"));
return prompts;
}

View file

@ -0,0 +1,43 @@
#pragma once
#include "GuiComponent.h"
#include "components/MenuComponent.h"
#include "MetaData.h"
#include "scrapers/Scraper.h"
#include <functional>
class GuiMetaDataEd : public GuiComponent
{
public:
GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector<MetaDataDecl>& mdd, ScraperSearchParams params,
const std::string& header, std::function<void()> savedCallback, std::function<void()> deleteFunc);
bool input(InputConfig* config, Input input) override;
void onSizeChanged() override;
virtual std::vector<HelpPrompt> getHelpPrompts() override;
private:
void save();
void fetch();
void fetchDone(const ScraperSearchResult& result);
void close(bool closeAllWindows);
NinePatchComponent mBackground;
ComponentGrid mGrid;
std::shared_ptr<TextComponent> mTitle;
std::shared_ptr<TextComponent> mSubtitle;
std::shared_ptr<ComponentGrid> mHeaderGrid;
std::shared_ptr<ComponentList> mList;
std::shared_ptr<ComponentGrid> mButtons;
ScraperSearchParams mScraperParams;
std::vector< std::shared_ptr<GuiComponent> > mEditors;
std::vector<MetaDataDecl> mMetaDataDecl;
MetaDataList* mMetaData;
std::function<void()> mSavedCallback;
std::function<void()> mDeleteFunc;
};

View file

@ -0,0 +1,151 @@
#include "guis/GuiScraperMulti.h"
#include "Renderer.h"
#include "Log.h"
#include "views/ViewController.h"
#include "Gamelist.h"
#include "components/TextComponent.h"
#include "components/ButtonComponent.h"
#include "components/ScraperSearchComponent.h"
#include "components/MenuComponent.h" // for makeButtonGrid
#include "guis/GuiMsgBox.h"
using namespace Eigen;
GuiScraperMulti::GuiScraperMulti(Window* window, const std::queue<ScraperSearchParams>& searches, bool approveResults) :
GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 5)),
mSearchQueue(searches)
{
assert(mSearchQueue.size());
addChild(&mBackground);
addChild(&mGrid);
mTotalGames = mSearchQueue.size();
mCurrentGame = 0;
mTotalSuccessful = 0;
mTotalSkipped = 0;
// set up grid
mTitle = std::make_shared<TextComponent>(mWindow, "SCRAPING IN PROGRESS", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER);
mGrid.setEntry(mTitle, Vector2i(0, 0), false, true);
mSystem = std::make_shared<TextComponent>(mWindow, "SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER);
mGrid.setEntry(mSystem, Vector2i(0, 1), false, true);
mSubtitle = std::make_shared<TextComponent>(mWindow, "subtitle text", Font::get(FONT_SIZE_SMALL), 0x888888FF, ALIGN_CENTER);
mGrid.setEntry(mSubtitle, Vector2i(0, 2), false, true);
mSearchComp = std::make_shared<ScraperSearchComponent>(mWindow,
approveResults ? ScraperSearchComponent::ALWAYS_ACCEPT_MATCHING_CRC : ScraperSearchComponent::ALWAYS_ACCEPT_FIRST_RESULT);
mSearchComp->setAcceptCallback(std::bind(&GuiScraperMulti::acceptResult, this, std::placeholders::_1));
mSearchComp->setSkipCallback(std::bind(&GuiScraperMulti::skip, this));
mSearchComp->setCancelCallback(std::bind(&GuiScraperMulti::finish, this));
mGrid.setEntry(mSearchComp, Vector2i(0, 3), mSearchComp->getSearchType() != ScraperSearchComponent::ALWAYS_ACCEPT_FIRST_RESULT, true);
std::vector< std::shared_ptr<ButtonComponent> > buttons;
if(approveResults)
{
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "INPUT", "search", [&] {
mSearchComp->openInputScreen(mSearchQueue.front());
mGrid.resetCursor();
}));
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "SKIP", "skip", [&] {
skip();
mGrid.resetCursor();
}));
}
buttons.push_back(std::make_shared<ButtonComponent>(mWindow, "STOP", "stop (progress saved)", std::bind(&GuiScraperMulti::finish, this)));
mButtonGrid = makeButtonGrid(mWindow, buttons);
mGrid.setEntry(mButtonGrid, Vector2i(0, 4), true, false);
setSize(Renderer::getScreenWidth() * 0.95f, Renderer::getScreenHeight() * 0.849f);
setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2);
doNextSearch();
}
GuiScraperMulti::~GuiScraperMulti()
{
// view type probably changed (basic -> detailed)
for(auto it = SystemData::sSystemVector.begin(); it != SystemData::sSystemVector.end(); it++)
ViewController::get()->reloadGameListView(*it, false);
}
void GuiScraperMulti::onSizeChanged()
{
mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32));
mGrid.setRowHeightPerc(0, mTitle->getFont()->getLetterHeight() * 1.9725f / mSize.y(), false);
mGrid.setRowHeightPerc(1, (mSystem->getFont()->getLetterHeight() + 2) / mSize.y(), false);
mGrid.setRowHeightPerc(2, mSubtitle->getFont()->getHeight() * 1.75f / mSize.y(), false);
mGrid.setRowHeightPerc(4, mButtonGrid->getSize().y() / mSize.y(), false);
mGrid.setSize(mSize);
}
void GuiScraperMulti::doNextSearch()
{
if(mSearchQueue.empty())
{
finish();
return;
}
// update title
std::stringstream ss;
mSystem->setText(strToUpper(mSearchQueue.front().system->getFullName()));
// update subtitle
ss.str(""); // clear
ss << "GAME " << (mCurrentGame + 1) << " OF " << mTotalGames << " - " << strToUpper(mSearchQueue.front().game->getPath().filename().string());
mSubtitle->setText(ss.str());
mSearchComp->search(mSearchQueue.front());
}
void GuiScraperMulti::acceptResult(const ScraperSearchResult& result)
{
ScraperSearchParams& search = mSearchQueue.front();
search.game->metadata = result.mdl;
updateGamelist(search.system);
mSearchQueue.pop();
mCurrentGame++;
mTotalSuccessful++;
doNextSearch();
}
void GuiScraperMulti::skip()
{
mSearchQueue.pop();
mCurrentGame++;
mTotalSkipped++;
doNextSearch();
}
void GuiScraperMulti::finish()
{
std::stringstream ss;
if(mTotalSuccessful == 0)
{
ss << "NO GAMES WERE SCRAPED.";
}else{
ss << mTotalSuccessful << " GAME" << ((mTotalSuccessful > 1) ? "S" : "") << " SUCCESSFULLY SCRAPED!";
if(mTotalSkipped > 0)
ss << "\n" << mTotalSkipped << " GAME" << ((mTotalSkipped > 1) ? "S" : "") << " SKIPPED.";
}
mWindow->pushGui(new GuiMsgBox(mWindow, ss.str(),
"OK", [&] { delete this; }));
}
std::vector<HelpPrompt> GuiScraperMulti::getHelpPrompts()
{
return mGrid.getHelpPrompts();
}

View file

@ -0,0 +1,43 @@
#pragma once
#include "GuiComponent.h"
#include "components/NinePatchComponent.h"
#include "components/ComponentGrid.h"
#include "scrapers/Scraper.h"
#include <queue>
class ScraperSearchComponent;
class TextComponent;
class GuiScraperMulti : public GuiComponent
{
public:
GuiScraperMulti(Window* window, const std::queue<ScraperSearchParams>& searches, bool approveResults);
virtual ~GuiScraperMulti();
void onSizeChanged() override;
std::vector<HelpPrompt> getHelpPrompts() override;
private:
void acceptResult(const ScraperSearchResult& result);
void skip();
void doNextSearch();
void finish();
unsigned int mTotalGames;
unsigned int mCurrentGame;
unsigned int mTotalSuccessful;
unsigned int mTotalSkipped;
std::queue<ScraperSearchParams> mSearchQueue;
NinePatchComponent mBackground;
ComponentGrid mGrid;
std::shared_ptr<TextComponent> mTitle;
std::shared_ptr<TextComponent> mSystem;
std::shared_ptr<TextComponent> mSubtitle;
std::shared_ptr<ScraperSearchComponent> mSearchComp;
std::shared_ptr<ComponentGrid> mButtonGrid;
};

View file

@ -0,0 +1,127 @@
#include "guis/GuiScraperStart.h"
#include "guis/GuiScraperMulti.h"
#include "guis/GuiMsgBox.h"
#include "views/ViewController.h"
#include "components/TextComponent.h"
#include "components/OptionListComponent.h"
#include "components/SwitchComponent.h"
GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window),
mMenu(window, "SCRAPE NOW")
{
addChild(&mMenu);
// add filters (with first one selected)
mFilters = std::make_shared< OptionListComponent<GameFilterFunc> >(mWindow, "SCRAPE THESE GAMES", false);
mFilters->add("All Games",
[](SystemData*, FileData*) -> bool { return true; }, false);
mFilters->add("Only missing image",
[](SystemData*, FileData* g) -> bool { return g->metadata.get("image").empty(); }, true);
mMenu.addWithLabel("Filter", mFilters);
//add systems (all with a platformid specified selected)
mSystems = std::make_shared< OptionListComponent<SystemData*> >(mWindow, "SCRAPE THESE SYSTEMS", true);
for(auto it = SystemData::sSystemVector.begin(); it != SystemData::sSystemVector.end(); it++)
{
if(!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE))
mSystems->add((*it)->getFullName(), *it, !(*it)->getPlatformIds().empty());
}
mMenu.addWithLabel("Systems", mSystems);
mApproveResults = std::make_shared<SwitchComponent>(mWindow);
mApproveResults->setState(true);
mMenu.addWithLabel("User decides on conflicts", mApproveResults);
mMenu.addButton("START", "start", std::bind(&GuiScraperStart::pressedStart, this));
mMenu.addButton("BACK", "back", [&] { delete this; });
mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f);
}
void GuiScraperStart::pressedStart()
{
std::vector<SystemData*> sys = mSystems->getSelectedObjects();
for(auto it = sys.begin(); it != sys.end(); it++)
{
if((*it)->getPlatformIds().empty())
{
mWindow->pushGui(new GuiMsgBox(mWindow,
strToUpper("Warning: some of your selected systems do not have a platform set. Results may be even more inaccurate than usual!\nContinue anyway?"),
"YES", std::bind(&GuiScraperStart::start, this),
"NO", nullptr));
return;
}
}
start();
}
void GuiScraperStart::start()
{
std::queue<ScraperSearchParams> searches = getSearches(mSystems->getSelectedObjects(), mFilters->getSelected());
if(searches.empty())
{
mWindow->pushGui(new GuiMsgBox(mWindow,
"NO GAMES FIT THAT CRITERIA."));
}else{
GuiScraperMulti* gsm = new GuiScraperMulti(mWindow, searches, mApproveResults->getState());
mWindow->pushGui(gsm);
delete this;
}
}
std::queue<ScraperSearchParams> GuiScraperStart::getSearches(std::vector<SystemData*> systems, GameFilterFunc selector)
{
std::queue<ScraperSearchParams> queue;
for(auto sys = systems.begin(); sys != systems.end(); sys++)
{
std::vector<FileData*> games = (*sys)->getRootFolder()->getFilesRecursive(GAME);
for(auto game = games.begin(); game != games.end(); game++)
{
if(selector((*sys), (*game)))
{
ScraperSearchParams search;
search.game = *game;
search.system = *sys;
queue.push(search);
}
}
}
return queue;
}
bool GuiScraperStart::input(InputConfig* config, Input input)
{
bool consumed = GuiComponent::input(config, input);
if(consumed)
return true;
if(input.value != 0 && config->isMappedTo("b", input))
{
delete this;
return true;
}
if(config->isMappedTo("start", input) && input.value != 0)
{
// close everything
Window* window = mWindow;
while(window->peekGui() && window->peekGui() != ViewController::get())
delete window->peekGui();
}
return false;
}
std::vector<HelpPrompt> GuiScraperStart::getHelpPrompts()
{
std::vector<HelpPrompt> prompts = mMenu.getHelpPrompts();
prompts.push_back(HelpPrompt("b", "back"));
prompts.push_back(HelpPrompt("start", "close"));
return prompts;
}

View file

@ -0,0 +1,38 @@
#pragma once
#include "GuiComponent.h"
#include "SystemData.h"
#include "scrapers/Scraper.h"
#include "components/MenuComponent.h"
#include <queue>
typedef std::function<bool(SystemData*, FileData*)> GameFilterFunc;
template<typename T>
class OptionListComponent;
class SwitchComponent;
//The starting point for a multi-game scrape.
//Allows the user to set various parameters (to set filters, to set which systems to scrape, to enable manual mode).
//Generates a list of "searches" that will be carried out by GuiScraperLog.
class GuiScraperStart : public GuiComponent
{
public:
GuiScraperStart(Window* window);
bool input(InputConfig* config, Input input) override;
virtual std::vector<HelpPrompt> getHelpPrompts() override;
private:
void pressedStart();
void start();
std::queue<ScraperSearchParams> getSearches(std::vector<SystemData*> systems, GameFilterFunc selector);
std::shared_ptr< OptionListComponent<GameFilterFunc> > mFilters;
std::shared_ptr< OptionListComponent<SystemData*> > mSystems;
std::shared_ptr<SwitchComponent> mApproveResults;
MenuComponent mMenu;
};

View file

@ -0,0 +1,60 @@
#include "guis/GuiSettings.h"
#include "Window.h"
#include "Settings.h"
#include "views/ViewController.h"
GuiSettings::GuiSettings(Window* window, const char* title) : GuiComponent(window), mMenu(window, title)
{
addChild(&mMenu);
mMenu.addButton("BACK", "go back", [this] { delete this; });
setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight());
mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f);
}
GuiSettings::~GuiSettings()
{
save();
}
void GuiSettings::save()
{
if(!mSaveFuncs.size())
return;
for(auto it = mSaveFuncs.begin(); it != mSaveFuncs.end(); it++)
(*it)();
Settings::getInstance()->saveFile();
}
bool GuiSettings::input(InputConfig* config, Input input)
{
if(config->isMappedTo("b", input) && input.value != 0)
{
delete this;
return true;
}
if(config->isMappedTo("start", input) && input.value != 0)
{
// close everything
Window* window = mWindow;
while(window->peekGui() && window->peekGui() != ViewController::get())
delete window->peekGui();
return true;
}
return GuiComponent::input(config, input);
}
std::vector<HelpPrompt> GuiSettings::getHelpPrompts()
{
std::vector<HelpPrompt> prompts = mMenu.getHelpPrompts();
prompts.push_back(HelpPrompt("b", "back"));
prompts.push_back(HelpPrompt("start", "close"));
return prompts;
}

View file

@ -0,0 +1,22 @@
#include "GuiComponent.h"
#include "components/MenuComponent.h"
// This is just a really simple template for a GUI that calls some save functions when closed.
class GuiSettings : public GuiComponent
{
public:
GuiSettings(Window* window, const char* title);
virtual ~GuiSettings(); // just calls save();
void save();
inline void addRow(const ComponentListRow& row) { mMenu.addRow(row); };
inline void addWithLabel(const std::string& label, const std::shared_ptr<GuiComponent>& comp) { mMenu.addWithLabel(label, comp); };
inline void addSaveFunc(const std::function<void()>& func) { mSaveFuncs.push_back(func); };
bool input(InputConfig* config, Input input) override;
std::vector<HelpPrompt> getHelpPrompts() override;
private:
MenuComponent mMenu;
std::vector< std::function<void()> > mSaveFuncs;
};

280
es-app/src/main.cpp Normal file
View file

@ -0,0 +1,280 @@
//EmulationStation, a graphical front-end for ROM browsing. Created by Alec "Aloshi" Lofquist.
//http://www.aloshi.com
#include <SDL.h>
#include <iostream>
#include <iomanip>
#include "Renderer.h"
#include "views/ViewController.h"
#include "SystemData.h"
#include <boost/filesystem.hpp>
#include "guis/GuiDetectDevice.h"
#include "guis/GuiMsgBox.h"
#include "AudioManager.h"
#include "platform.h"
#include "Log.h"
#include "Window.h"
#include "EmulationStation.h"
#include "Settings.h"
#include "ScraperCmdLine.h"
#include <sstream>
namespace fs = boost::filesystem;
bool scrape_cmdline = false;
bool parseArgs(int argc, char* argv[], unsigned int* width, unsigned int* height)
{
for(int i = 1; i < argc; i++)
{
if(strcmp(argv[i], "--resolution") == 0)
{
if(i >= argc - 2)
{
std::cerr << "Invalid resolution supplied.";
return false;
}
*width = atoi(argv[i + 1]);
*height = atoi(argv[i + 2]);
i += 2; // skip the argument value
}else if(strcmp(argv[i], "--gamelist-only") == 0)
{
Settings::getInstance()->setBool("ParseGamelistOnly", true);
}else if(strcmp(argv[i], "--ignore-gamelist") == 0)
{
Settings::getInstance()->setBool("IgnoreGamelist", true);
}else if(strcmp(argv[i], "--draw-framerate") == 0)
{
Settings::getInstance()->setBool("DrawFramerate", true);
}else if(strcmp(argv[i], "--no-exit") == 0)
{
Settings::getInstance()->setBool("ShowExit", false);
}else if(strcmp(argv[i], "--debug") == 0)
{
Settings::getInstance()->setBool("Debug", true);
Log::setReportingLevel(LogDebug);
}else if(strcmp(argv[i], "--windowed") == 0)
{
Settings::getInstance()->setBool("Windowed", true);
}else if(strcmp(argv[i], "--scrape") == 0)
{
scrape_cmdline = true;
}else if(strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0)
{
std::cout <<
"EmulationStation, a graphical front-end for ROM browsing.\n"
"Written by Alec \"Aloshi\" Lofquist.\n"
"Version " << PROGRAM_VERSION_STRING << ", built " << PROGRAM_BUILT_STRING << "\n\n"
"Command line arguments:\n"
"--resolution [width] [height] try and force a particular resolution\n"
"--gamelist-only skip automatic game search, only read from gamelist.xml\n"
"--ignore-gamelist ignore the gamelist (useful for troubleshooting)\n"
"--draw-framerate display the framerate\n"
"--no-exit don't show the exit option in the menu\n"
"--debug even more logging\n"
"--scrape scrape using command line interface\n"
"--windowed not fullscreen, should be used with --resolution\n"
"--help, -h summon a sentient, angry tuba\n\n"
"More information available in README.md.\n";
return false; //exit after printing help
}
}
return true;
}
bool verifyHomeFolderExists()
{
//make sure the config directory exists
std::string home = getHomePath();
std::string configDir = home + "/.emulationstation";
if(!fs::exists(configDir))
{
std::cout << "Creating config directory \"" << configDir << "\"\n";
fs::create_directory(configDir);
if(!fs::exists(configDir))
{
std::cerr << "Config directory could not be created!\n";
return false;
}
}
return true;
}
// Returns true if everything is OK,
bool loadSystemConfigFile(const char** errorString)
{
*errorString = NULL;
if(!SystemData::loadConfig())
{
LOG(LogError) << "Error while parsing systems configuration file!";
*errorString = "IT LOOKS LIKE YOUR SYSTEMS CONFIGURATION FILE HAS NOT BEEN SET UP OR IS INVALID. YOU'LL NEED TO DO THIS BY HAND, UNFORTUNATELY.\n\n"
"VISIT EMULATIONSTATION.ORG FOR MORE INFORMATION.";
return false;
}
if(SystemData::sSystemVector.size() == 0)
{
LOG(LogError) << "No systems found! Does at least one system have a game present? (check that extensions match!)\n(Also, make sure you've updated your es_systems.cfg for XML!)";
*errorString = "WE CAN'T FIND ANY SYSTEMS!\n"
"CHECK THAT YOUR PATHS ARE CORRECT IN THE SYSTEMS CONFIGURATION FILE, "
"AND YOUR GAME DIRECTORY HAS AT LEAST ONE GAME WITH THE CORRECT EXTENSION.\n\n"
"VISIT EMULATIONSTATION.ORG FOR MORE INFORMATION.";
return false;
}
return true;
}
//called on exit, assuming we get far enough to have the log initialized
void onExit()
{
Log::close();
}
int main(int argc, char* argv[])
{
unsigned int width = 0;
unsigned int height = 0;
if(!parseArgs(argc, argv, &width, &height))
return 0;
//if ~/.emulationstation doesn't exist and cannot be created, bail
if(!verifyHomeFolderExists())
return 1;
//start the logger
Log::open();
LOG(LogInfo) << "EmulationStation - v" << PROGRAM_VERSION_STRING << ", built " << PROGRAM_BUILT_STRING;
//always close the log on exit
atexit(&onExit);
Window window;
ViewController::init(&window);
window.pushGui(ViewController::get());
if(!scrape_cmdline)
{
if(!window.init(width, height))
{
LOG(LogError) << "Window failed to initialize!";
return 1;
}
window.renderLoadingScreen();
}
const char* errorMsg = NULL;
if(!loadSystemConfigFile(&errorMsg))
{
// something went terribly wrong
if(errorMsg == NULL)
{
LOG(LogError) << "Unknown error occured while parsing system config file.";
if(!scrape_cmdline)
Renderer::deinit();
return 1;
}
// we can't handle es_systems.cfg file problems inside ES itself, so display the error message then quit
window.pushGui(new GuiMsgBox(&window,
errorMsg,
"QUIT", [] {
SDL_Event* quit = new SDL_Event();
quit->type = SDL_QUIT;
SDL_PushEvent(quit);
}));
}
//run the command line scraper then quit
if(scrape_cmdline)
{
return run_scraper_cmdline();
}
//dont generate joystick events while we're loading (hopefully fixes "automatically started emulator" bug)
SDL_JoystickEventState(SDL_DISABLE);
// preload what we can right away instead of waiting for the user to select it
// this makes for no delays when accessing content, but a longer startup time
ViewController::get()->preload();
//choose which GUI to open depending on if an input configuration already exists
if(errorMsg == NULL)
{
if(fs::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0)
{
ViewController::get()->goToStart();
}else{
window.pushGui(new GuiDetectDevice(&window, true, [] { ViewController::get()->goToStart(); }));
}
}
//generate joystick events since we're done loading
SDL_JoystickEventState(SDL_ENABLE);
int lastTime = SDL_GetTicks();
bool running = true;
while(running)
{
SDL_Event event;
while(SDL_PollEvent(&event))
{
switch(event.type)
{
case SDL_JOYHATMOTION:
case SDL_JOYBUTTONDOWN:
case SDL_JOYBUTTONUP:
case SDL_KEYDOWN:
case SDL_KEYUP:
case SDL_JOYAXISMOTION:
case SDL_TEXTINPUT:
case SDL_TEXTEDITING:
case SDL_JOYDEVICEADDED:
case SDL_JOYDEVICEREMOVED:
InputManager::getInstance()->parseEvent(event, &window);
break;
case SDL_QUIT:
running = false;
break;
}
}
if(window.isSleeping())
{
lastTime = SDL_GetTicks();
SDL_Delay(1); // this doesn't need to be accurate, we're just giving up our CPU time until something wakes us up
continue;
}
int curTime = SDL_GetTicks();
int deltaTime = curTime - lastTime;
lastTime = curTime;
// cap deltaTime at 1000
if(deltaTime > 1000 || deltaTime < 0)
deltaTime = 1000;
window.update(deltaTime);
window.render();
Renderer::swapBuffers();
Log::flush();
}
while(window.peekGui() != ViewController::get())
delete window.peekGui();
window.deinit();
SystemData::deleteSystems();
LOG(LogInfo) << "EmulationStation cleanly shutting down.";
return 0;
}

View file

@ -0,0 +1,163 @@
#include "scrapers/GamesDBScraper.h"
#include "Log.h"
#include "pugixml/pugixml.hpp"
#include "MetaData.h"
#include "Settings.h"
#include "Util.h"
#include <boost/assign.hpp>
using namespace PlatformIds;
const std::map<PlatformId, const char*> gamesdb_platformid_map = boost::assign::map_list_of
(THREEDO, "3DO")
(AMIGA, "Amiga")
(AMSTRAD_CPC, "Amstrad CPC")
// missing apple2
(ARCADE, "Arcade")
// missing atari 800
(ATARI_2600, "Atari 2600")
(ATARI_5200, "Atari 5200")
(ATARI_7800, "Atari 7800")
(ATARI_JAGUAR, "Atari Jaguar")
(ATARI_JAGUAR_CD, "Atari Jaguar CD")
(ATARI_LYNX, "Atari Lynx")
// missing atari ST/STE/Falcon
(ATARI_XE, "Atari XE")
(COLECOVISION, "Colecovision")
(COMMODORE_64, "Commodore 64")
(INTELLIVISION, "Intellivision")
(MAC_OS, "Mac OS")
(XBOX, "Microsoft Xbox")
(XBOX_360, "Microsoft Xbox 360")
(NEOGEO, "NeoGeo")
(NEOGEO_POCKET, "Neo Geo Pocket")
(NEOGEO_POCKET_COLOR, "Neo Geo Pocket Color")
(NINTENDO_3DS, "Nintendo 3DS")
(NINTENDO_64, "Nintendo 64")
(NINTENDO_DS, "Nintendo DS")
(NINTENDO_ENTERTAINMENT_SYSTEM, "Nintendo Entertainment System (NES)")
(GAME_BOY, "Nintendo Game Boy")
(GAME_BOY_ADVANCE, "Nintendo Game Boy Advance")
(GAME_BOY_COLOR, "Nintendo Game Boy Color")
(NINTENDO_GAMECUBE, "Nintendo GameCube")
(NINTENDO_WII, "Nintendo Wii")
(NINTENDO_WII_U, "Nintendo Wii U")
(PC, "PC")
(SEGA_32X, "Sega 32X")
(SEGA_CD, "Sega CD")
(SEGA_DREAMCAST, "Sega Dreamcast")
(SEGA_GAME_GEAR, "Sega Game Gear")
(SEGA_GENESIS, "Sega Genesis")
(SEGA_MASTER_SYSTEM, "Sega Master System")
(SEGA_MEGA_DRIVE, "Sega Mega Drive")
(SEGA_SATURN, "Sega Saturn")
(PLAYSTATION, "Sony Playstation")
(PLAYSTATION_2, "Sony Playstation 2")
(PLAYSTATION_3, "Sony Playstation 3")
(PLAYSTATION_4, "Sony Playstation 4")
(PLAYSTATION_VITA, "Sony Playstation Vita")
(PLAYSTATION_PORTABLE, "Sony PSP")
(SUPER_NINTENDO, "Super Nintendo (SNES)")
(TURBOGRAFX_16, "TurboGrafx 16")
(WONDERSWAN, "WonderSwan")
(WONDERSWAN_COLOR, "WonderSwan Color")
(ZX_SPECTRUM, "Sinclair ZX Spectrum");
void thegamesdb_generate_scraper_requests(const ScraperSearchParams& params, std::queue< std::unique_ptr<ScraperRequest> >& requests,
std::vector<ScraperSearchResult>& results)
{
std::string path = "thegamesdb.net/api/GetGame.php?";
std::string cleanName = params.nameOverride;
if(cleanName.empty())
cleanName = params.game->getCleanName();
path += "name=" + HttpReq::urlEncode(cleanName);
if(params.system->getPlatformIds().empty())
{
// no platform specified, we're done
requests.push(std::unique_ptr<ScraperRequest>(new TheGamesDBRequest(results, path)));
}else{
// go through the list, we need to split this into multiple requests
// because TheGamesDB API either sucks or I don't know how to use it properly...
std::string urlBase = path;
auto& platforms = params.system->getPlatformIds();
for(auto platformIt = platforms.begin(); platformIt != platforms.end(); platformIt++)
{
path = urlBase;
auto mapIt = gamesdb_platformid_map.find(*platformIt);
if(mapIt != gamesdb_platformid_map.end())
{
path += "&platform=";
path += HttpReq::urlEncode(mapIt->second);
}else{
LOG(LogWarning) << "TheGamesDB scraper warning - no support for platform " << getPlatformName(*platformIt);
}
requests.push(std::unique_ptr<ScraperRequest>(new TheGamesDBRequest(results, path)));
}
}
}
void TheGamesDBRequest::process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results)
{
assert(req->status() == HttpReq::REQ_SUCCESS);
pugi::xml_document doc;
pugi::xml_parse_result parseResult = doc.load(req->getContent().c_str());
if(!parseResult)
{
std::stringstream ss;
ss << "GamesDBRequest - Error parsing XML. \n\t" << parseResult.description() << "";
std::string err = ss.str();
setError(err);
LOG(LogError) << err;
return;
}
pugi::xml_node data = doc.child("Data");
std::string baseImageUrl = data.child("baseImgUrl").text().get();
pugi::xml_node game = data.child("Game");
while(game && results.size() < MAX_SCRAPER_RESULTS)
{
ScraperSearchResult result;
result.mdl.set("name", game.child("GameTitle").text().get());
result.mdl.set("desc", game.child("Overview").text().get());
boost::posix_time::ptime rd = string_to_ptime(game.child("ReleaseDate").text().get(), "%m/%d/%Y");
result.mdl.setTime("releasedate", rd);
result.mdl.set("developer", game.child("Developer").text().get());
result.mdl.set("publisher", game.child("Publisher").text().get());
result.mdl.set("genre", game.child("Genres").first_child().text().get());
result.mdl.set("players", game.child("Players").text().get());
if(Settings::getInstance()->getBool("ScrapeRatings") && game.child("Rating"))
{
float ratingVal = (game.child("Rating").text().as_int() / 10.0f);
std::stringstream ss;
ss << ratingVal;
result.mdl.set("rating", ss.str());
}
pugi::xml_node images = game.child("Images");
if(images)
{
pugi::xml_node art = images.find_child_by_attribute("boxart", "side", "front");
if(art)
{
result.thumbnailUrl = baseImageUrl + art.attribute("thumb").as_string();
result.imageUrl = baseImageUrl + art.text().get();
}
}
results.push_back(result);
game = game.next_sibling("Game");
}
}

View file

@ -0,0 +1,14 @@
#pragma once
#include "scrapers/Scraper.h"
void thegamesdb_generate_scraper_requests(const ScraperSearchParams& params, std::queue< std::unique_ptr<ScraperRequest> >& requests,
std::vector<ScraperSearchResult>& results);
class TheGamesDBRequest : public ScraperHttpRequest
{
public:
TheGamesDBRequest(std::vector<ScraperSearchResult>& resultsWrite, const std::string& url) : ScraperHttpRequest(resultsWrite, url) {}
protected:
void process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results) override;
};

View file

@ -0,0 +1,293 @@
#include "scrapers/Scraper.h"
#include "Log.h"
#include "Settings.h"
#include <FreeImage.h>
#include <boost/filesystem.hpp>
#include <boost/assign.hpp>
#include "GamesDBScraper.h"
#include "TheArchiveScraper.h"
const std::map<std::string, generate_scraper_requests_func> scraper_request_funcs = boost::assign::map_list_of
("TheGamesDB", &thegamesdb_generate_scraper_requests)
("TheArchive", &thearchive_generate_scraper_requests);
std::unique_ptr<ScraperSearchHandle> startScraperSearch(const ScraperSearchParams& params)
{
const std::string& name = Settings::getInstance()->getString("Scraper");
std::unique_ptr<ScraperSearchHandle> handle(new ScraperSearchHandle());
scraper_request_funcs.at(name)(params, handle->mRequestQueue, handle->mResults);
return handle;
}
std::vector<std::string> getScraperList()
{
std::vector<std::string> list;
for(auto it = scraper_request_funcs.begin(); it != scraper_request_funcs.end(); it++)
{
list.push_back(it->first);
}
return list;
}
// ScraperSearchHandle
ScraperSearchHandle::ScraperSearchHandle()
{
setStatus(ASYNC_IN_PROGRESS);
}
void ScraperSearchHandle::update()
{
if(mStatus == ASYNC_DONE)
return;
while(!mRequestQueue.empty())
{
auto& req = mRequestQueue.front();
AsyncHandleStatus status = req->status();
if(status == ASYNC_ERROR)
{
// propegate error
setError(req->getStatusString());
// empty our queue
while(!mRequestQueue.empty())
mRequestQueue.pop();
return;
}
// finished this one, see if we have any more
if(status == ASYNC_DONE)
{
mRequestQueue.pop();
continue;
}
// status == ASYNC_IN_PROGRESS
}
// we finished without any errors!
if(mRequestQueue.empty())
{
setStatus(ASYNC_DONE);
return;
}
}
// ScraperRequest
ScraperRequest::ScraperRequest(std::vector<ScraperSearchResult>& resultsWrite) : mResults(resultsWrite)
{
}
// ScraperHttpRequest
ScraperHttpRequest::ScraperHttpRequest(std::vector<ScraperSearchResult>& resultsWrite, const std::string& url)
: ScraperRequest(resultsWrite)
{
setStatus(ASYNC_IN_PROGRESS);
mReq = std::unique_ptr<HttpReq>(new HttpReq(url));
}
void ScraperHttpRequest::update()
{
HttpReq::Status status = mReq->status();
if(status == HttpReq::REQ_SUCCESS)
{
setStatus(ASYNC_DONE); // if process() has an error, status will be changed to ASYNC_ERROR
process(mReq, mResults);
return;
}
// not ready yet
if(status == HttpReq::REQ_IN_PROGRESS)
return;
// everything else is some sort of error
LOG(LogError) << "ScraperHttpRequest network error (status: " << status << ") - " << mReq->getErrorMsg();
setError(mReq->getErrorMsg());
}
// metadata resolving stuff
std::unique_ptr<MDResolveHandle> resolveMetaDataAssets(const ScraperSearchResult& result, const ScraperSearchParams& search)
{
return std::unique_ptr<MDResolveHandle>(new MDResolveHandle(result, search));
}
MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search) : mResult(result)
{
if(!result.imageUrl.empty())
{
std::string imgPath = getSaveAsPath(search, "image", result.imageUrl);
mFuncs.push_back(ResolvePair(downloadImageAsync(result.imageUrl, imgPath), [this, imgPath]
{
mResult.mdl.set("image", imgPath);
mResult.imageUrl = "";
}));
}
}
void MDResolveHandle::update()
{
if(mStatus == ASYNC_DONE || mStatus == ASYNC_ERROR)
return;
auto it = mFuncs.begin();
while(it != mFuncs.end())
{
if(it->first->status() == ASYNC_ERROR)
{
setError(it->first->getStatusString());
return;
}else if(it->first->status() == ASYNC_DONE)
{
it->second();
it = mFuncs.erase(it);
continue;
}
it++;
}
if(mFuncs.empty())
setStatus(ASYNC_DONE);
}
std::unique_ptr<ImageDownloadHandle> downloadImageAsync(const std::string& url, const std::string& saveAs)
{
return std::unique_ptr<ImageDownloadHandle>(new ImageDownloadHandle(url, saveAs,
Settings::getInstance()->getInt("ScraperResizeWidth"), Settings::getInstance()->getInt("ScraperResizeHeight")));
}
ImageDownloadHandle::ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight) :
mSavePath(path), mMaxWidth(maxWidth), mMaxHeight(maxHeight), mReq(new HttpReq(url))
{
}
void ImageDownloadHandle::update()
{
if(mReq->status() == HttpReq::REQ_IN_PROGRESS)
return;
if(mReq->status() != HttpReq::REQ_SUCCESS)
{
std::stringstream ss;
ss << "Network error: " << mReq->getErrorMsg();
setError(ss.str());
return;
}
// download is done, save it to disk
std::ofstream stream(mSavePath, std::ios_base::out | std::ios_base::binary);
if(stream.bad())
{
setError("Failed to open image path to write. Permission error? Disk full?");
return;
}
const std::string& content = mReq->getContent();
stream.write(content.data(), content.length());
stream.close();
if(stream.bad())
{
setError("Failed to save image. Disk full?");
return;
}
// resize it
if(!resizeImage(mSavePath, mMaxWidth, mMaxHeight))
{
setError("Error saving resized image. Out of memory? Disk full?");
return;
}
setStatus(ASYNC_DONE);
}
//you can pass 0 for width or height to keep aspect ratio
bool resizeImage(const std::string& path, int maxWidth, int maxHeight)
{
// nothing to do
if(maxWidth == 0 && maxHeight == 0)
return true;
FREE_IMAGE_FORMAT format = FIF_UNKNOWN;
FIBITMAP* image = NULL;
//detect the filetype
format = FreeImage_GetFileType(path.c_str(), 0);
if(format == FIF_UNKNOWN)
format = FreeImage_GetFIFFromFilename(path.c_str());
if(format == FIF_UNKNOWN)
{
LOG(LogError) << "Error - could not detect filetype for image \"" << path << "\"!";
return false;
}
//make sure we can read this filetype first, then load it
if(FreeImage_FIFSupportsReading(format))
{
image = FreeImage_Load(format, path.c_str());
}else{
LOG(LogError) << "Error - file format reading not supported for image \"" << path << "\"!";
return false;
}
float width = (float)FreeImage_GetWidth(image);
float height = (float)FreeImage_GetHeight(image);
if(maxWidth == 0)
{
maxWidth = (int)((maxHeight / height) * width);
}else if(maxHeight == 0)
{
maxHeight = (int)((maxWidth / width) * height);
}
FIBITMAP* imageRescaled = FreeImage_Rescale(image, maxWidth, maxHeight, FILTER_BILINEAR);
FreeImage_Unload(image);
if(imageRescaled == NULL)
{
LOG(LogError) << "Could not resize image! (not enough memory? invalid bitdepth?)";
return false;
}
bool saved = FreeImage_Save(format, imageRescaled, path.c_str());
FreeImage_Unload(imageRescaled);
if(!saved)
LOG(LogError) << "Failed to save resized image!";
return saved;
}
std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix, const std::string& url)
{
const std::string subdirectory = params.system->getName();
const std::string name = params.game->getPath().stem().generic_string() + "-" + suffix;
std::string path = getHomePath() + "/.emulationstation/downloaded_images/";
if(!boost::filesystem::exists(path))
boost::filesystem::create_directory(path);
path += subdirectory + "/";
if(!boost::filesystem::exists(path))
boost::filesystem::create_directory(path);
size_t dot = url.find_last_of('.');
std::string ext;
if(dot != std::string::npos)
ext = url.substr(dot, std::string::npos);
path += name + ext;
return path;
}

View file

@ -0,0 +1,156 @@
#pragma once
#include "MetaData.h"
#include "SystemData.h"
#include "HttpReq.h"
#include "AsyncHandle.h"
#include <vector>
#include <functional>
#include <queue>
#define MAX_SCRAPER_RESULTS 7
struct ScraperSearchParams
{
SystemData* system;
FileData* game;
std::string nameOverride;
};
struct ScraperSearchResult
{
ScraperSearchResult() : mdl(GAME_METADATA) {};
MetaDataList mdl;
std::string imageUrl;
std::string thumbnailUrl;
};
// So let me explain why I've abstracted this so heavily.
// There are two ways I can think of that you'd want to write a scraper.
// 1. Do some HTTP request(s) -> process it -> return the results
// 2. Do some local filesystem queries (an offline scraper) -> return the results
// The first way needs to be asynchronous while it's waiting for the HTTP request to return.
// The second doesn't.
// It would be nice if we could write it like this:
// search = generate_http_request(searchparams);
// wait_until_done(search);
// ... process search ...
// return results;
// We could do this if we used threads. Right now ES doesn't because I'm pretty sure I'll fuck it up,
// and I'm not sure of the performance of threads on the Pi (single-core ARM).
// We could also do this if we used coroutines.
// I can't find a really good cross-platform coroutine library (x86/64/ARM Linux + Windows),
// and I don't want to spend more time chasing libraries than just writing it the long way once.
// So, I did it the "long" way.
// ScraperSearchHandle - one logical search, e.g. "search for mario"
// ScraperRequest - encapsulates some sort of asynchronous request that will ultimately return some results
// ScraperHttpRequest - implementation of ScraperRequest that waits on an HttpReq, then processes it with some processing function.
// a scraper search gathers results from (potentially multiple) ScraperRequests
class ScraperRequest : public AsyncHandle
{
public:
ScraperRequest(std::vector<ScraperSearchResult>& resultsWrite);
// returns "true" once we're done
virtual void update() = 0;
protected:
std::vector<ScraperSearchResult>& mResults;
};
// a single HTTP request that needs to be processed to get the results
class ScraperHttpRequest : public ScraperRequest
{
public:
ScraperHttpRequest(std::vector<ScraperSearchResult>& resultsWrite, const std::string& url);
virtual void update() override;
protected:
virtual void process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results) = 0;
private:
std::unique_ptr<HttpReq> mReq;
};
// a request to get a list of results
class ScraperSearchHandle : public AsyncHandle
{
public:
ScraperSearchHandle();
void update();
inline const std::vector<ScraperSearchResult>& getResults() const { assert(mStatus != ASYNC_IN_PROGRESS); return mResults; }
protected:
friend std::unique_ptr<ScraperSearchHandle> startScraperSearch(const ScraperSearchParams& params);
std::queue< std::unique_ptr<ScraperRequest> > mRequestQueue;
std::vector<ScraperSearchResult> mResults;
};
// will use the current scraper settings to pick the result source
std::unique_ptr<ScraperSearchHandle> startScraperSearch(const ScraperSearchParams& params);
// returns a list of valid scraper names
std::vector<std::string> getScraperList();
typedef void (*generate_scraper_requests_func)(const ScraperSearchParams& params, std::queue< std::unique_ptr<ScraperRequest> >& requests, std::vector<ScraperSearchResult>& results);
// -------------------------------------------------------------------------
// Meta data asset downloading stuff.
class MDResolveHandle : public AsyncHandle
{
public:
MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search);
void update() override;
inline const ScraperSearchResult& getResult() const { assert(mStatus == ASYNC_DONE); return mResult; }
private:
ScraperSearchResult mResult;
typedef std::pair< std::unique_ptr<AsyncHandle>, std::function<void()> > ResolvePair;
std::vector<ResolvePair> mFuncs;
};
class ImageDownloadHandle : public AsyncHandle
{
public:
ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight);
void update() override;
private:
std::unique_ptr<HttpReq> mReq;
std::string mSavePath;
int mMaxWidth;
int mMaxHeight;
};
//About the same as "~/.emulationstation/downloaded_images/[system_name]/[game_name].[url's extension]".
//Will create the "downloaded_images" and "subdirectory" directories if they do not exist.
std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix, const std::string& url);
//Will resize according to Settings::getInt("ScraperResizeWidth") and Settings::getInt("ScraperResizeHeight").
std::unique_ptr<ImageDownloadHandle> downloadImageAsync(const std::string& url, const std::string& saveAs);
// Resolves all metadata assets that need to be downloaded.
std::unique_ptr<MDResolveHandle> resolveMetaDataAssets(const ScraperSearchResult& result, const ScraperSearchParams& search);
//You can pass 0 for maxWidth or maxHeight to automatically keep the aspect ratio.
//Will overwrite the image at [path] with the new resized one.
//Returns true if successful, false otherwise.
bool resizeImage(const std::string& path, int maxWidth, int maxHeight);

View file

@ -0,0 +1,66 @@
#include "TheArchiveScraper.h"
#include "Log.h"
#include "pugixml/pugixml.hpp"
void thearchive_generate_scraper_requests(const ScraperSearchParams& params, std::queue< std::unique_ptr<ScraperRequest> >& requests,
std::vector<ScraperSearchResult>& results)
{
std::string path = "api.archive.vg/2.0/Archive.search/xml/7TTRM4MNTIKR2NNAGASURHJOZJ3QXQC5/";
std::string cleanName = params.nameOverride;
if(cleanName.empty())
cleanName = params.game->getCleanName();
path += HttpReq::urlEncode(cleanName);
//platform TODO, should use some params.system get method
requests.push(std::unique_ptr<ScraperRequest>(new TheArchiveRequest(results, path)));
}
void TheArchiveRequest::process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results)
{
assert(req->status() == HttpReq::REQ_SUCCESS);
pugi::xml_document doc;
pugi::xml_parse_result parseResult = doc.load(req->getContent().c_str());
if(!parseResult)
{
std::stringstream ss;
ss << "TheArchiveRequest - error parsing XML.\n\t" << parseResult.description();
std::string err = ss.str();
setError(err);
LOG(LogError) << err;
return;
}
pugi::xml_node data = doc.child("OpenSearchDescription").child("games");
pugi::xml_node game = data.child("game");
while(game && results.size() < MAX_SCRAPER_RESULTS)
{
ScraperSearchResult result;
result.mdl.set("name", game.child("title").text().get());
result.mdl.set("desc", game.child("description").text().get());
//Archive.search does not return ratings
result.mdl.set("developer", game.child("developer").text().get());
std::string genre = game.child("genre").text().get();
size_t search = genre.find_last_of(" &gt; ");
genre = genre.substr(search == std::string::npos ? 0 : search, std::string::npos);
result.mdl.set("genre", genre);
pugi::xml_node image = game.child("box_front");
pugi::xml_node thumbnail = game.child("box_front_small");
if(image)
result.imageUrl = image.text().get();
if(thumbnail)
result.thumbnailUrl = thumbnail.text().get();
results.push_back(result);
game = game.next_sibling("game");
}
}

View file

@ -0,0 +1,16 @@
#pragma once
#include "scrapers/Scraper.h"
void thearchive_generate_scraper_requests(const ScraperSearchParams& params, std::queue< std::unique_ptr<ScraperRequest> >& requests,
std::vector<ScraperSearchResult>& results);
void thearchive_process_httpreq(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results);
class TheArchiveRequest : public ScraperHttpRequest
{
public:
TheArchiveRequest(std::vector<ScraperSearchResult>& resultsWrite, const std::string& url) : ScraperHttpRequest(resultsWrite, url) {}
protected:
void process(const std::unique_ptr<HttpReq>& req, std::vector<ScraperSearchResult>& results) override;
};

View file

@ -0,0 +1,345 @@
#include "views/SystemView.h"
#include "SystemData.h"
#include "Renderer.h"
#include "Log.h"
#include "Window.h"
#include "views/ViewController.h"
#include "animations/LambdaAnimation.h"
#include "SystemData.h"
#include "Settings.h"
#include "Util.h"
#define SELECTED_SCALE 1.5f
#define LOGO_PADDING ((logoSize().x() * (SELECTED_SCALE - 1)/2) + (mSize.x() * 0.06f))
#define BAND_HEIGHT (logoSize().y() * SELECTED_SCALE)
SystemView::SystemView(Window* window) : IList<SystemViewData, SystemData*>(window, LIST_SCROLL_STYLE_SLOW, LIST_ALWAYS_LOOP),
mSystemInfo(window, "SYSTEM INFO", Font::get(FONT_SIZE_SMALL), 0x33333300, ALIGN_CENTER)
{
mCamOffset = 0;
mExtrasCamOffset = 0;
mExtrasFadeOpacity = 0.0f;
setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight());
mSystemInfo.setSize(mSize.x(), mSystemInfo.getSize().y() * 1.333f);
mSystemInfo.setPosition(0, (mSize.y() + BAND_HEIGHT) / 2);
populate();
}
void SystemView::populate()
{
mEntries.clear();
for(auto it = SystemData::sSystemVector.begin(); it != SystemData::sSystemVector.end(); it++)
{
const std::shared_ptr<ThemeData>& theme = (*it)->getTheme();
Entry e;
e.name = (*it)->getName();
e.object = *it;
// make logo
if(theme->getElement("system", "logo", "image"))
{
ImageComponent* logo = new ImageComponent(mWindow);
logo->setMaxSize(Eigen::Vector2f(logoSize().x(), logoSize().y()));
logo->applyTheme((*it)->getTheme(), "system", "logo", ThemeFlags::PATH);
logo->setPosition((logoSize().x() - logo->getSize().x()) / 2, (logoSize().y() - logo->getSize().y()) / 2); // center
e.data.logo = std::shared_ptr<GuiComponent>(logo);
ImageComponent* logoSelected = new ImageComponent(mWindow);
logoSelected->setMaxSize(Eigen::Vector2f(logoSize().x() * SELECTED_SCALE, logoSize().y() * SELECTED_SCALE * 0.70f));
logoSelected->applyTheme((*it)->getTheme(), "system", "logo", ThemeFlags::PATH);
logoSelected->setPosition((logoSize().x() - logoSelected->getSize().x()) / 2,
(logoSize().y() - logoSelected->getSize().y()) / 2); // center
e.data.logoSelected = std::shared_ptr<GuiComponent>(logoSelected);
}else{
// no logo in theme; use text
TextComponent* text = new TextComponent(mWindow,
(*it)->getName(),
Font::get(FONT_SIZE_LARGE),
0x000000FF,
ALIGN_CENTER);
text->setSize(logoSize());
e.data.logo = std::shared_ptr<GuiComponent>(text);
TextComponent* textSelected = new TextComponent(mWindow,
(*it)->getName(),
Font::get((int)(FONT_SIZE_LARGE * SELECTED_SCALE)),
0x000000FF,
ALIGN_CENTER);
textSelected->setSize(logoSize());
e.data.logoSelected = std::shared_ptr<GuiComponent>(textSelected);
}
// make background extras
e.data.backgroundExtras = std::shared_ptr<ThemeExtras>(new ThemeExtras(mWindow));
e.data.backgroundExtras->setExtras(ThemeData::makeExtras((*it)->getTheme(), "system", mWindow));
this->add(e);
}
}
void SystemView::goToSystem(SystemData* system, bool animate)
{
setCursor(system);
if(!animate)
finishAnimation(0);
}
bool SystemView::input(InputConfig* config, Input input)
{
if(input.value != 0)
{
if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_r && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug"))
{
LOG(LogInfo) << " Reloading SystemList view";
// reload themes
for(auto it = mEntries.begin(); it != mEntries.end(); it++)
it->object->loadTheme();
populate();
updateHelpPrompts();
return true;
}
if(config->isMappedTo("left", input))
{
listInput(-1);
return true;
}
if(config->isMappedTo("right", input))
{
listInput(1);
return true;
}
if(config->isMappedTo("a", input))
{
stopScrolling();
ViewController::get()->goToGameList(getSelected());
return true;
}
}else{
if(config->isMappedTo("left", input) || config->isMappedTo("right", input))
listInput(0);
}
return GuiComponent::input(config, input);
}
void SystemView::update(int deltaTime)
{
listUpdate(deltaTime);
GuiComponent::update(deltaTime);
}
void SystemView::onCursorChanged(const CursorState& state)
{
// update help style
updateHelpPrompts();
float startPos = mCamOffset;
float posMax = (float)mEntries.size();
float target = (float)mCursor;
// what's the shortest way to get to our target?
// it's one of these...
float endPos = target; // directly
float dist = abs(endPos - startPos);
if(abs(target + posMax - startPos) < dist)
endPos = target + posMax; // loop around the end (0 -> max)
if(abs(target - posMax - startPos) < dist)
endPos = target - posMax; // loop around the start (max - 1 -> -1)
// animate mSystemInfo's opacity (fade out, wait, fade back in)
cancelAnimation(1);
cancelAnimation(2);
const float infoStartOpacity = mSystemInfo.getOpacity() / 255.f;
Animation* infoFadeOut = new LambdaAnimation(
[infoStartOpacity, this] (float t)
{
mSystemInfo.setOpacity((unsigned char)(lerp<float>(infoStartOpacity, 0.f, t) * 255));
}, (int)(infoStartOpacity * 150));
unsigned int gameCount = getSelected()->getGameCount();
// also change the text after we've fully faded out
setAnimation(infoFadeOut, 0, [this, gameCount] {
std::stringstream ss;
// only display a game count if there are at least 2 games
if(gameCount > 1)
ss << gameCount << " GAMES AVAILABLE";
mSystemInfo.setText(ss.str());
}, false, 1);
// only display a game count if there are at least 2 games
if(gameCount > 1)
{
Animation* infoFadeIn = new LambdaAnimation(
[this](float t)
{
mSystemInfo.setOpacity((unsigned char)(lerp<float>(0.f, 1.f, t) * 255));
}, 300);
// wait 600ms to fade in
setAnimation(infoFadeIn, 2000, nullptr, false, 2);
}
// no need to animate transition, we're not going anywhere (probably mEntries.size() == 1)
if(endPos == mCamOffset && endPos == mExtrasCamOffset)
return;
Animation* anim;
if(Settings::getInstance()->getString("TransitionStyle") == "fade")
{
float startExtrasFade = mExtrasFadeOpacity;
anim = new LambdaAnimation(
[startExtrasFade, startPos, endPos, posMax, this](float t)
{
t -= 1;
float f = lerp<float>(startPos, endPos, t*t*t + 1);
if(f < 0)
f += posMax;
if(f >= posMax)
f -= posMax;
this->mCamOffset = f;
t += 1;
if(t < 0.3f)
this->mExtrasFadeOpacity = lerp<float>(0.0f, 1.0f, t / 0.3f + startExtrasFade);
else if(t < 0.7f)
this->mExtrasFadeOpacity = 1.0f;
else
this->mExtrasFadeOpacity = lerp<float>(1.0f, 0.0f, (t - 0.7f) / 0.3f);
if(t > 0.5f)
this->mExtrasCamOffset = endPos;
}, 500);
}
else{ // slide
anim = new LambdaAnimation(
[startPos, endPos, posMax, this](float t)
{
t -= 1;
float f = lerp<float>(startPos, endPos, t*t*t + 1);
if(f < 0)
f += posMax;
if(f >= posMax)
f -= posMax;
this->mCamOffset = f;
this->mExtrasCamOffset = f;
}, 500);
}
setAnimation(anim, 0, nullptr, false, 0);
}
void SystemView::render(const Eigen::Affine3f& parentTrans)
{
if(size() == 0)
return;
Eigen::Affine3f trans = getTransform() * parentTrans;
// draw the list elements (titles, backgrounds, logos)
const float logoSizeX = logoSize().x() + LOGO_PADDING;
int logoCount = (int)(mSize.x() / logoSizeX) + 2; // how many logos we need to draw
int center = (int)(mCamOffset);
if(mEntries.size() == 1)
logoCount = 1;
// draw background extras
Eigen::Affine3f extrasTrans = trans;
int extrasCenter = (int)mExtrasCamOffset;
for(int i = extrasCenter - 1; i < extrasCenter + 2; i++)
{
int index = i;
while(index < 0)
index += mEntries.size();
while(index >= (int)mEntries.size())
index -= mEntries.size();
extrasTrans.translation() = trans.translation() + Eigen::Vector3f((i - mExtrasCamOffset) * mSize.x(), 0, 0);
Eigen::Vector2i clipRect = Eigen::Vector2i((int)((i - mExtrasCamOffset) * mSize.x()), 0);
Renderer::pushClipRect(clipRect, mSize.cast<int>());
mEntries.at(index).data.backgroundExtras->render(extrasTrans);
Renderer::popClipRect();
}
// fade extras if necessary
if(mExtrasFadeOpacity)
{
Renderer::setMatrix(trans);
Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000000 | (unsigned char)(mExtrasFadeOpacity * 255));
}
// draw logos
float xOff = (mSize.x() - logoSize().x())/2 - (mCamOffset * logoSizeX);
float yOff = (mSize.y() - logoSize().y())/2;
// background behind the logos
Renderer::setMatrix(trans);
Renderer::drawRect(0.f, (mSize.y() - BAND_HEIGHT) / 2, mSize.x(), BAND_HEIGHT, 0xFFFFFFD8);
Eigen::Affine3f logoTrans = trans;
for(int i = center - logoCount/2; i < center + logoCount/2 + 1; i++)
{
int index = i;
while(index < 0)
index += mEntries.size();
while(index >= (int)mEntries.size())
index -= mEntries.size();
logoTrans.translation() = trans.translation() + Eigen::Vector3f(i * logoSizeX + xOff, yOff, 0);
if(index == mCursor) //scale our selection up
{
// selected
const std::shared_ptr<GuiComponent>& comp = mEntries.at(index).data.logoSelected;
comp->setOpacity(0xFF);
comp->render(logoTrans);
}else{
// not selected
const std::shared_ptr<GuiComponent>& comp = mEntries.at(index).data.logo;
comp->setOpacity(0x80);
comp->render(logoTrans);
}
}
Renderer::setMatrix(trans);
Renderer::drawRect(mSystemInfo.getPosition().x(), mSystemInfo.getPosition().y() - 1, mSize.x(), mSystemInfo.getSize().y(), 0xDDDDDD00 | (unsigned char)(mSystemInfo.getOpacity() / 255.f * 0xD8));
mSystemInfo.render(trans);
}
std::vector<HelpPrompt> SystemView::getHelpPrompts()
{
std::vector<HelpPrompt> prompts;
prompts.push_back(HelpPrompt("left/right", "choose"));
prompts.push_back(HelpPrompt("a", "select"));
return prompts;
}
HelpStyle SystemView::getHelpStyle()
{
HelpStyle style;
style.applyTheme(mEntries.at(mCursor).object->getTheme(), "system");
return style;
}

View file

@ -0,0 +1,48 @@
#pragma once
#include "GuiComponent.h"
#include "components/ImageComponent.h"
#include "components/TextComponent.h"
#include "components/ScrollableContainer.h"
#include "components/IList.h"
#include "resources/TextureResource.h"
class SystemData;
class AnimatedImageComponent;
struct SystemViewData
{
std::shared_ptr<GuiComponent> logo;
std::shared_ptr<GuiComponent> logoSelected;
std::shared_ptr<ThemeExtras> backgroundExtras;
};
class SystemView : public IList<SystemViewData, SystemData*>
{
public:
SystemView(Window* window);
void goToSystem(SystemData* system, bool animate);
bool input(InputConfig* config, Input input) override;
void update(int deltaTime) override;
void render(const Eigen::Affine3f& parentTrans) override;
std::vector<HelpPrompt> getHelpPrompts() override;
virtual HelpStyle getHelpStyle() override;
protected:
void onCursorChanged(const CursorState& state) override;
private:
inline Eigen::Vector2f logoSize() const { return Eigen::Vector2f(mSize.x() * 0.25f, mSize.y() * 0.155f); }
void populate();
TextComponent mSystemInfo;
// unit is list index
float mCamOffset;
float mExtrasCamOffset;
float mExtrasFadeOpacity;
};

View file

@ -0,0 +1,403 @@
#include "views/ViewController.h"
#include "Log.h"
#include "SystemData.h"
#include "Settings.h"
#include "views/gamelist/BasicGameListView.h"
#include "views/gamelist/DetailedGameListView.h"
#include "views/gamelist/GridGameListView.h"
#include "guis/GuiMenu.h"
#include "guis/GuiMsgBox.h"
#include "animations/LaunchAnimation.h"
#include "animations/MoveCameraAnimation.h"
#include "animations/LambdaAnimation.h"
ViewController* ViewController::sInstance = NULL;
ViewController* ViewController::get()
{
assert(sInstance);
return sInstance;
}
void ViewController::init(Window* window)
{
assert(!sInstance);
sInstance = new ViewController(window);
}
ViewController::ViewController(Window* window)
: GuiComponent(window), mCurrentView(nullptr), mCamera(Eigen::Affine3f::Identity()), mFadeOpacity(0), mLockInput(false)
{
mState.viewing = NOTHING;
}
ViewController::~ViewController()
{
assert(sInstance == this);
sInstance = NULL;
}
void ViewController::goToStart()
{
// TODO
/* mState.viewing = START_SCREEN;
mCurrentView.reset();
playViewTransition(); */
goToSystemView(SystemData::sSystemVector.at(0));
}
int ViewController::getSystemId(SystemData* system)
{
std::vector<SystemData*>& sysVec = SystemData::sSystemVector;
return std::find(sysVec.begin(), sysVec.end(), system) - sysVec.begin();
}
void ViewController::goToSystemView(SystemData* system)
{
mState.viewing = SYSTEM_SELECT;
mState.system = system;
auto systemList = getSystemListView();
systemList->setPosition(getSystemId(system) * (float)Renderer::getScreenWidth(), systemList->getPosition().y());
systemList->goToSystem(system, false);
mCurrentView = systemList;
playViewTransition();
}
void ViewController::goToNextGameList()
{
assert(mState.viewing == GAME_LIST);
SystemData* system = getState().getSystem();
assert(system);
goToGameList(system->getNext());
}
void ViewController::goToPrevGameList()
{
assert(mState.viewing == GAME_LIST);
SystemData* system = getState().getSystem();
assert(system);
goToGameList(system->getPrev());
}
void ViewController::goToGameList(SystemData* system)
{
if(mState.viewing == SYSTEM_SELECT)
{
// move system list
auto sysList = getSystemListView();
float offX = sysList->getPosition().x();
int sysId = getSystemId(system);
sysList->setPosition(sysId * (float)Renderer::getScreenWidth(), sysList->getPosition().y());
offX = sysList->getPosition().x() - offX;
mCamera.translation().x() -= offX;
}
mState.viewing = GAME_LIST;
mState.system = system;
mCurrentView = getGameListView(system);
playViewTransition();
}
void ViewController::playViewTransition()
{
Eigen::Vector3f target(Eigen::Vector3f::Identity());
if(mCurrentView)
target = mCurrentView->getPosition();
// no need to animate, we're not going anywhere (probably goToNextGamelist() or goToPrevGamelist() when there's only 1 system)
if(target == -mCamera.translation() && !isAnimationPlaying(0))
return;
if(Settings::getInstance()->getString("TransitionStyle") == "fade")
{
// fade
// stop whatever's currently playing, leaving mFadeOpacity wherever it is
cancelAnimation(0);
auto fadeFunc = [this](float t) {
mFadeOpacity = lerp<float>(0, 1, t);
};
const static int FADE_DURATION = 240; // fade in/out time
const static int FADE_WAIT = 320; // time to wait between in/out
setAnimation(new LambdaAnimation(fadeFunc, FADE_DURATION), 0, [this, fadeFunc, target] {
this->mCamera.translation() = -target;
updateHelpPrompts();
setAnimation(new LambdaAnimation(fadeFunc, FADE_DURATION), FADE_WAIT, nullptr, true);
});
// fast-forward animation if we're partway faded
if(target == -mCamera.translation())
{
// not changing screens, so cancel the first half entirely
advanceAnimation(0, FADE_DURATION);
advanceAnimation(0, FADE_WAIT);
advanceAnimation(0, FADE_DURATION - (int)(mFadeOpacity * FADE_DURATION));
}else{
advanceAnimation(0, (int)(mFadeOpacity * FADE_DURATION));
}
}else{
// slide
setAnimation(new MoveCameraAnimation(mCamera, target));
updateHelpPrompts(); // update help prompts immediately
}
}
void ViewController::onFileChanged(FileData* file, FileChangeType change)
{
auto it = mGameListViews.find(file->getSystem());
if(it != mGameListViews.end())
it->second->onFileChanged(file, change);
}
void ViewController::launch(FileData* game, Eigen::Vector3f center)
{
if(game->getType() != GAME)
{
LOG(LogError) << "tried to launch something that isn't a game";
return;
}
Eigen::Affine3f origCamera = mCamera;
origCamera.translation() = -mCurrentView->getPosition();
center += mCurrentView->getPosition();
stopAnimation(1); // make sure the fade in isn't still playing
mLockInput = true;
if(Settings::getInstance()->getString("TransitionStyle") == "fade")
{
// fade out, launch game, fade back in
auto fadeFunc = [this](float t) {
//t -= 1;
//mFadeOpacity = lerp<float>(0.0f, 1.0f, t*t*t + 1);
mFadeOpacity = lerp<float>(0.0f, 1.0f, t);
};
setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this, game, fadeFunc]
{
game->getSystem()->launchGame(mWindow, game);
mLockInput = false;
setAnimation(new LambdaAnimation(fadeFunc, 800), 0, nullptr, true);
this->onFileChanged(game, FILE_METADATA_CHANGED);
});
}else{
// move camera to zoom in on center + fade out, launch game, come back in
setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 1500), 0, [this, origCamera, center, game]
{
game->getSystem()->launchGame(mWindow, game);
mCamera = origCamera;
mLockInput = false;
setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 600), 0, nullptr, true);
this->onFileChanged(game, FILE_METADATA_CHANGED);
});
}
}
std::shared_ptr<IGameListView> ViewController::getGameListView(SystemData* system)
{
//if we already made one, return that one
auto exists = mGameListViews.find(system);
if(exists != mGameListViews.end())
return exists->second;
//if we didn't, make it, remember it, and return it
std::shared_ptr<IGameListView> view;
//decide type
bool detailed = false;
std::vector<FileData*> files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER);
for(auto it = files.begin(); it != files.end(); it++)
{
if(!(*it)->getThumbnailPath().empty())
{
detailed = true;
break;
}
}
if(detailed)
view = std::shared_ptr<IGameListView>(new DetailedGameListView(mWindow, system->getRootFolder()));
else
view = std::shared_ptr<IGameListView>(new BasicGameListView(mWindow, system->getRootFolder()));
// uncomment for experimental "image grid" view
//view = std::shared_ptr<IGameListView>(new GridGameListView(mWindow, system->getRootFolder()));
view->setTheme(system->getTheme());
std::vector<SystemData*>& sysVec = SystemData::sSystemVector;
int id = std::find(sysVec.begin(), sysVec.end(), system) - sysVec.begin();
view->setPosition(id * (float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight() * 2);
addChild(view.get());
mGameListViews[system] = view;
return view;
}
std::shared_ptr<SystemView> ViewController::getSystemListView()
{
//if we already made one, return that one
if(mSystemListView)
return mSystemListView;
mSystemListView = std::shared_ptr<SystemView>(new SystemView(mWindow));
addChild(mSystemListView.get());
mSystemListView->setPosition(0, (float)Renderer::getScreenHeight());
return mSystemListView;
}
bool ViewController::input(InputConfig* config, Input input)
{
if(mLockInput)
return true;
// open menu
if(config->isMappedTo("start", input) && input.value != 0)
{
// open menu
mWindow->pushGui(new GuiMenu(mWindow));
return true;
}
if(mCurrentView)
return mCurrentView->input(config, input);
return false;
}
void ViewController::update(int deltaTime)
{
if(mCurrentView)
{
mCurrentView->update(deltaTime);
}
updateSelf(deltaTime);
}
void ViewController::render(const Eigen::Affine3f& parentTrans)
{
Eigen::Affine3f trans = mCamera * parentTrans;
// camera position, position + size
Eigen::Vector3f viewStart = trans.inverse().translation();
Eigen::Vector3f viewEnd = trans.inverse() * Eigen::Vector3f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight(), 0);
// draw systemview
getSystemListView()->render(trans);
// draw gamelists
for(auto it = mGameListViews.begin(); it != mGameListViews.end(); it++)
{
// clipping
Eigen::Vector3f guiStart = it->second->getPosition();
Eigen::Vector3f guiEnd = it->second->getPosition() + Eigen::Vector3f(it->second->getSize().x(), it->second->getSize().y(), 0);
if(guiEnd.x() >= viewStart.x() && guiEnd.y() >= viewStart.y() &&
guiStart.x() <= viewEnd.x() && guiStart.y() <= viewEnd.y())
it->second->render(trans);
}
if(mWindow->peekGui() == this)
mWindow->renderHelpPromptsEarly();
// fade out
if(mFadeOpacity)
{
Renderer::setMatrix(parentTrans);
Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(mFadeOpacity * 255));
}
}
void ViewController::preload()
{
for(auto it = SystemData::sSystemVector.begin(); it != SystemData::sSystemVector.end(); it++)
{
getGameListView(*it);
}
}
void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme)
{
for(auto it = mGameListViews.begin(); it != mGameListViews.end(); it++)
{
if(it->second.get() == view)
{
bool isCurrent = (mCurrentView == it->second);
SystemData* system = it->first;
FileData* cursor = view->getCursor();
mGameListViews.erase(it);
if(reloadTheme)
system->loadTheme();
std::shared_ptr<IGameListView> newView = getGameListView(system);
newView->setCursor(cursor);
if(isCurrent)
mCurrentView = newView;
break;
}
}
}
void ViewController::reloadAll()
{
std::map<SystemData*, FileData*> cursorMap;
for(auto it = mGameListViews.begin(); it != mGameListViews.end(); it++)
{
cursorMap[it->first] = it->second->getCursor();
}
mGameListViews.clear();
for(auto it = cursorMap.begin(); it != cursorMap.end(); it++)
{
it->first->loadTheme();
getGameListView(it->first)->setCursor(it->second);
}
mSystemListView.reset();
getSystemListView();
// update mCurrentView since the pointers changed
if(mState.viewing == GAME_LIST)
{
mCurrentView = getGameListView(mState.getSystem());
}else if(mState.viewing == SYSTEM_SELECT)
{
mSystemListView->goToSystem(mState.getSystem(), false);
mCurrentView = mSystemListView;
}else{
goToSystemView(SystemData::sSystemVector.front());
}
updateHelpPrompts();
}
std::vector<HelpPrompt> ViewController::getHelpPrompts()
{
std::vector<HelpPrompt> prompts;
if(!mCurrentView)
return prompts;
prompts = mCurrentView->getHelpPrompts();
prompts.push_back(HelpPrompt("start", "menu"));
return prompts;
}
HelpStyle ViewController::getHelpStyle()
{
if(!mCurrentView)
return GuiComponent::getHelpStyle();
return mCurrentView->getHelpStyle();
}

View file

@ -0,0 +1,87 @@
#pragma once
#include "views/gamelist/IGameListView.h"
#include "views/SystemView.h"
class SystemData;
// Used to smoothly transition the camera between multiple views (e.g. from system to system, from gamelist to gamelist).
class ViewController : public GuiComponent
{
public:
static void init(Window* window);
static ViewController* get();
virtual ~ViewController();
// Try to completely populate the GameListView map.
// Caches things so there's no pauses during transitions.
void preload();
// If a basic view detected a metadata change, it can request to recreate
// the current gamelist view (as it may change to be detailed).
void reloadGameListView(IGameListView* gamelist, bool reloadTheme = false);
inline void reloadGameListView(SystemData* system, bool reloadTheme = false) { reloadGameListView(getGameListView(system).get(), reloadTheme); }
void reloadAll(); // Reload everything with a theme. Used when the "ThemeSet" setting changes.
// Navigation.
void goToNextGameList();
void goToPrevGameList();
void goToGameList(SystemData* system);
void goToSystemView(SystemData* system);
void goToStart();
void onFileChanged(FileData* file, FileChangeType change);
// Plays a nice launch effect and launches the game at the end of it.
// Once the game terminates, plays a return effect.
void launch(FileData* game, Eigen::Vector3f centerCameraOn = Eigen::Vector3f(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0));
bool input(InputConfig* config, Input input) override;
void update(int deltaTime) override;
void render(const Eigen::Affine3f& parentTrans) override;
enum ViewMode
{
NOTHING,
START_SCREEN,
SYSTEM_SELECT,
GAME_LIST
};
struct State
{
ViewMode viewing;
inline SystemData* getSystem() const { assert(viewing == GAME_LIST || viewing == SYSTEM_SELECT); return system; }
private:
friend ViewController;
SystemData* system;
};
inline const State& getState() const { return mState; }
virtual std::vector<HelpPrompt> getHelpPrompts() override;
virtual HelpStyle getHelpStyle() override;
std::shared_ptr<IGameListView> getGameListView(SystemData* system);
std::shared_ptr<SystemView> getSystemListView();
private:
ViewController(Window* window);
static ViewController* sInstance;
void playViewTransition();
int getSystemId(SystemData* system);
std::shared_ptr<GuiComponent> mCurrentView;
std::map< SystemData*, std::shared_ptr<IGameListView> > mGameListViews;
std::shared_ptr<SystemView> mSystemListView;
Eigen::Affine3f mCamera;
float mFadeOpacity;
bool mLockInput;
State mState;
};

View file

@ -0,0 +1,100 @@
#include "views/gamelist/BasicGameListView.h"
#include "views/ViewController.h"
#include "Renderer.h"
#include "Window.h"
#include "ThemeData.h"
#include "SystemData.h"
#include "Settings.h"
BasicGameListView::BasicGameListView(Window* window, FileData* root)
: ISimpleGameListView(window, root), mList(window)
{
mList.setSize(mSize.x(), mSize.y() * 0.8f);
mList.setPosition(0, mSize.y() * 0.2f);
addChild(&mList);
populateList(root->getChildren());
}
void BasicGameListView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
{
ISimpleGameListView::onThemeChanged(theme);
using namespace ThemeFlags;
mList.applyTheme(theme, getName(), "gamelist", ALL);
}
void BasicGameListView::onFileChanged(FileData* file, FileChangeType change)
{
if(change == FILE_METADATA_CHANGED)
{
// might switch to a detailed view
ViewController::get()->reloadGameListView(this);
return;
}
ISimpleGameListView::onFileChanged(file, 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++)
{
mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER));
}
}
FileData* BasicGameListView::getCursor()
{
return mList.getSelected();
}
void BasicGameListView::setCursor(FileData* cursor)
{
if(!mList.setCursor(cursor))
{
populateList(cursor->getParent()->getChildren());
mList.setCursor(cursor);
// update our cursor stack in case our cursor just got set to some folder we weren't in before
if(mCursorStack.empty() || mCursorStack.top() != cursor->getParent())
{
std::stack<FileData*> tmp;
FileData* ptr = cursor->getParent();
while(ptr && ptr != mRoot)
{
tmp.push(ptr);
ptr = ptr->getParent();
}
// flip the stack and put it in mCursorStack
mCursorStack = std::stack<FileData*>();
while(!tmp.empty())
{
mCursorStack.push(tmp.top());
tmp.pop();
}
}
}
}
void BasicGameListView::launch(FileData* game)
{
ViewController::get()->launch(game);
}
std::vector<HelpPrompt> BasicGameListView::getHelpPrompts()
{
std::vector<HelpPrompt> prompts;
if(Settings::getInstance()->getBool("QuickSystemSelect"))
prompts.push_back(HelpPrompt("left/right", "system"));
prompts.push_back(HelpPrompt("up/down", "choose"));
prompts.push_back(HelpPrompt("a", "launch"));
prompts.push_back(HelpPrompt("b", "back"));
prompts.push_back(HelpPrompt("select", "options"));
return prompts;
}

View file

@ -0,0 +1,28 @@
#pragma once
#include "views/gamelist/ISimpleGameListView.h"
#include "components/TextListComponent.h"
class BasicGameListView : public ISimpleGameListView
{
public:
BasicGameListView(Window* window, FileData* root);
// Called when a FileData* is added, has its metadata changed, or is removed
virtual void onFileChanged(FileData* file, FileChangeType change);
virtual void onThemeChanged(const std::shared_ptr<ThemeData>& theme);
virtual FileData* getCursor() override;
virtual void setCursor(FileData* file) override;
virtual const char* getName() const override { return "basic"; }
virtual std::vector<HelpPrompt> getHelpPrompts() override;
protected:
virtual void populateList(const std::vector<FileData*>& files) override;
virtual void launch(FileData* game) override;
TextListComponent<FileData*> mList;
};

View file

@ -0,0 +1,270 @@
#include "views/gamelist/DetailedGameListView.h"
#include "views/ViewController.h"
#include "Window.h"
#include "animations/LambdaAnimation.h"
DetailedGameListView::DetailedGameListView(Window* window, FileData* root) :
BasicGameListView(window, root),
mDescContainer(window), mDescription(window),
mImage(window),
mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window),
mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window),
mRating(window), mReleaseDate(window), mDeveloper(window), mPublisher(window),
mGenre(window), mPlayers(window), mLastPlayed(window), mPlayCount(window)
{
//mHeaderImage.setPosition(mSize.x() * 0.25f, 0);
const float padding = 0.01f;
mList.setPosition(mSize.x() * (0.50f + padding), mList.getPosition().y());
mList.setSize(mSize.x() * (0.50f - padding), mList.getSize().y());
mList.setAlignment(TextListComponent<FileData*>::ALIGN_LEFT);
mList.setCursorChangedCallback([&](const CursorState& state) { updateInfoPanel(); });
// image
mImage.setOrigin(0.5f, 0.5f);
mImage.setPosition(mSize.x() * 0.25f, mList.getPosition().y() + mSize.y() * 0.2125f);
mImage.setMaxSize(mSize.x() * (0.50f - 2*padding), mSize.y() * 0.4f);
addChild(&mImage);
// metadata labels + values
mLblRating.setText("Rating: ");
addChild(&mLblRating);
addChild(&mRating);
mLblReleaseDate.setText("Released: ");
addChild(&mLblReleaseDate);
addChild(&mReleaseDate);
mLblDeveloper.setText("Developer: ");
addChild(&mLblDeveloper);
addChild(&mDeveloper);
mLblPublisher.setText("Publisher: ");
addChild(&mLblPublisher);
addChild(&mPublisher);
mLblGenre.setText("Genre: ");
addChild(&mLblGenre);
addChild(&mGenre);
mLblPlayers.setText("Players: ");
addChild(&mLblPlayers);
addChild(&mPlayers);
mLblLastPlayed.setText("Last played: ");
addChild(&mLblLastPlayed);
mLastPlayed.setDisplayMode(DateTimeComponent::DISP_RELATIVE_TO_NOW);
addChild(&mLastPlayed);
mLblPlayCount.setText("Times played: ");
addChild(&mLblPlayCount);
addChild(&mPlayCount);
mDescContainer.setPosition(mSize.x() * padding, mSize.y() * 0.65f);
mDescContainer.setSize(mSize.x() * (0.50f - 2*padding), mSize.y() - mDescContainer.getPosition().y());
mDescContainer.setAutoScroll(true);
addChild(&mDescContainer);
mDescription.setFont(Font::get(FONT_SIZE_SMALL));
mDescription.setSize(mDescContainer.getSize().x(), 0);
mDescContainer.addChild(&mDescription);
initMDLabels();
initMDValues();
updateInfoPanel();
}
void DetailedGameListView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
{
BasicGameListView::onThemeChanged(theme);
using namespace ThemeFlags;
mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE);
initMDLabels();
std::vector<TextComponent*> labels = getMDLabels();
assert(labels.size() == 8);
const char* lblElements[8] = {
"md_lbl_rating", "md_lbl_releasedate", "md_lbl_developer", "md_lbl_publisher",
"md_lbl_genre", "md_lbl_players", "md_lbl_lastplayed", "md_lbl_playcount"
};
for(unsigned int i = 0; i < labels.size(); i++)
{
labels[i]->applyTheme(theme, getName(), lblElements[i], ALL);
}
initMDValues();
std::vector<GuiComponent*> values = getMDValues();
assert(values.size() == 8);
const char* valElements[8] = {
"md_rating", "md_releasedate", "md_developer", "md_publisher",
"md_genre", "md_players", "md_lastplayed", "md_playcount"
};
for(unsigned int i = 0; i < values.size(); i++)
{
values[i]->applyTheme(theme, getName(), valElements[i], ALL ^ ThemeFlags::TEXT);
}
mDescContainer.applyTheme(theme, getName(), "md_description", POSITION | ThemeFlags::SIZE);
mDescription.setSize(mDescContainer.getSize().x(), 0);
mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | TEXT));
}
void DetailedGameListView::initMDLabels()
{
using namespace Eigen;
std::vector<TextComponent*> components = getMDLabels();
const unsigned int colCount = 2;
const unsigned int rowCount = components.size() / 2;
Vector3f start(mSize.x() * 0.01f, mSize.y() * 0.625f, 0.0f);
const float colSize = (mSize.x() * 0.48f) / colCount;
const float rowPadding = 0.01f * mSize.y();
for(unsigned int i = 0; i < components.size(); i++)
{
const unsigned int row = i % rowCount;
Vector3f pos(0.0f, 0.0f, 0.0f);
if(row == 0)
{
pos = start + Vector3f(colSize * (i / rowCount), 0, 0);
}else{
// work from the last component
GuiComponent* lc = components[i-1];
pos = lc->getPosition() + Vector3f(0, lc->getSize().y() + rowPadding, 0);
}
components[i]->setFont(Font::get(FONT_SIZE_SMALL));
components[i]->setPosition(pos);
}
}
void DetailedGameListView::initMDValues()
{
using namespace Eigen;
std::vector<TextComponent*> labels = getMDLabels();
std::vector<GuiComponent*> values = getMDValues();
std::shared_ptr<Font> defaultFont = Font::get(FONT_SIZE_SMALL);
mRating.setSize(defaultFont->getHeight() * 5.0f, (float)defaultFont->getHeight());
mReleaseDate.setFont(defaultFont);
mDeveloper.setFont(defaultFont);
mPublisher.setFont(defaultFont);
mGenre.setFont(defaultFont);
mPlayers.setFont(defaultFont);
mLastPlayed.setFont(defaultFont);
mPlayCount.setFont(defaultFont);
float bottom = 0.0f;
const float colSize = (mSize.x() * 0.48f) / 2;
for(unsigned int i = 0; i < labels.size(); i++)
{
const float heightDiff = (labels[i]->getSize().y() - values[i]->getSize().y()) / 2;
values[i]->setPosition(labels[i]->getPosition() + Vector3f(labels[i]->getSize().x(), heightDiff, 0));
values[i]->setSize(colSize - labels[i]->getSize().x(), values[i]->getSize().y());
float testBot = values[i]->getPosition().y() + values[i]->getSize().y();
if(testBot > bottom)
bottom = testBot;
}
mDescContainer.setPosition(mDescContainer.getPosition().x(), bottom + mSize.y() * 0.01f);
mDescContainer.setSize(mDescContainer.getSize().x(), mSize.y() - mDescContainer.getPosition().y());
}
void DetailedGameListView::updateInfoPanel()
{
FileData* file = (mList.size() == 0 || mList.isScrolling()) ? NULL : mList.getSelected();
bool fadingOut;
if(file == NULL)
{
//mImage.setImage("");
//mDescription.setText("");
fadingOut = true;
}else{
mImage.setImage(file->metadata.get("image"));
mDescription.setText(file->metadata.get("desc"));
mDescContainer.reset();
if(file->getType() == GAME)
{
mRating.setValue(file->metadata.get("rating"));
mReleaseDate.setValue(file->metadata.get("releasedate"));
mDeveloper.setValue(file->metadata.get("developer"));
mPublisher.setValue(file->metadata.get("publisher"));
mGenre.setValue(file->metadata.get("genre"));
mPlayers.setValue(file->metadata.get("players"));
mLastPlayed.setValue(file->metadata.get("lastplayed"));
mPlayCount.setValue(file->metadata.get("playcount"));
}
fadingOut = false;
}
std::vector<GuiComponent*> comps = getMDValues();
comps.push_back(&mImage);
comps.push_back(&mDescription);
std::vector<TextComponent*> labels = getMDLabels();
comps.insert(comps.end(), labels.begin(), labels.end());
for(auto it = comps.begin(); it != comps.end(); it++)
{
GuiComponent* comp = *it;
// an animation is playing
// then animate if reverse != fadingOut
// an animation is not playing
// then animate if opacity != our target opacity
if((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) ||
(!comp->isAnimationPlaying(0) && comp->getOpacity() != (fadingOut ? 0 : 255)))
{
auto func = [comp](float t)
{
comp->setOpacity((unsigned char)(lerp<float>(0.0f, 1.0f, t)*255));
};
comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut);
}
}
}
void DetailedGameListView::launch(FileData* game)
{
Eigen::Vector3f target(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0);
if(mImage.hasImage())
target << mImage.getCenter().x(), mImage.getCenter().y(), 0;
ViewController::get()->launch(game, target);
}
std::vector<TextComponent*> DetailedGameListView::getMDLabels()
{
std::vector<TextComponent*> ret;
ret.push_back(&mLblRating);
ret.push_back(&mLblReleaseDate);
ret.push_back(&mLblDeveloper);
ret.push_back(&mLblPublisher);
ret.push_back(&mLblGenre);
ret.push_back(&mLblPlayers);
ret.push_back(&mLblLastPlayed);
ret.push_back(&mLblPlayCount);
return ret;
}
std::vector<GuiComponent*> DetailedGameListView::getMDValues()
{
std::vector<GuiComponent*> ret;
ret.push_back(&mRating);
ret.push_back(&mReleaseDate);
ret.push_back(&mDeveloper);
ret.push_back(&mPublisher);
ret.push_back(&mGenre);
ret.push_back(&mPlayers);
ret.push_back(&mLastPlayed);
ret.push_back(&mPlayCount);
return ret;
}

View file

@ -0,0 +1,44 @@
#pragma once
#include "views/gamelist/BasicGameListView.h"
#include "components/ScrollableContainer.h"
#include "components/RatingComponent.h"
#include "components/DateTimeComponent.h"
class DetailedGameListView : public BasicGameListView
{
public:
DetailedGameListView(Window* window, FileData* root);
virtual void onThemeChanged(const std::shared_ptr<ThemeData>& theme) override;
virtual const char* getName() const override { return "detailed"; }
protected:
virtual void launch(FileData* game) override;
private:
void updateInfoPanel();
void initMDLabels();
void initMDValues();
ImageComponent mImage;
TextComponent mLblRating, mLblReleaseDate, mLblDeveloper, mLblPublisher, mLblGenre, mLblPlayers, mLblLastPlayed, mLblPlayCount;
RatingComponent mRating;
DateTimeComponent mReleaseDate;
TextComponent mDeveloper;
TextComponent mPublisher;
TextComponent mGenre;
TextComponent mPlayers;
DateTimeComponent mLastPlayed;
TextComponent mPlayCount;
std::vector<TextComponent*> getMDLabels();
std::vector<GuiComponent*> getMDValues();
ScrollableContainer mDescContainer;
TextComponent mDescription;
};

View file

@ -0,0 +1,59 @@
#include "views/gamelist/GridGameListView.h"
#include "ThemeData.h"
#include "Window.h"
#include "views/ViewController.h"
GridGameListView::GridGameListView(Window* window, FileData* root) : ISimpleGameListView(window, root),
mGrid(window)
{
mGrid.setPosition(0, mSize.y() * 0.2f);
mGrid.setSize(mSize.x(), mSize.y() * 0.8f);
addChild(&mGrid);
populateList(root->getChildren());
}
FileData* GridGameListView::getCursor()
{
return mGrid.getSelected();
}
void GridGameListView::setCursor(FileData* file)
{
if(!mGrid.setCursor(file))
{
populateList(file->getParent()->getChildren());
mGrid.setCursor(file);
}
}
bool GridGameListView::input(InputConfig* config, Input input)
{
if(config->isMappedTo("left", input) || config->isMappedTo("right", input))
return GuiComponent::input(config, input);
return ISimpleGameListView::input(config, input);
}
void GridGameListView::populateList(const std::vector<FileData*>& files)
{
mGrid.clear();
for(auto it = files.begin(); it != files.end(); it++)
{
mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it);
}
}
void GridGameListView::launch(FileData* game)
{
ViewController::get()->launch(game);
}
std::vector<HelpPrompt> GridGameListView::getHelpPrompts()
{
std::vector<HelpPrompt> prompts;
prompts.push_back(HelpPrompt("up/down/left/right", "scroll"));
prompts.push_back(HelpPrompt("a", "launch"));
prompts.push_back(HelpPrompt("b", "back"));
return prompts;
}

View file

@ -0,0 +1,29 @@
#pragma once
#include "views/gamelist/ISimpleGameListView.h"
#include "components/ImageGridComponent.h"
#include "components/ImageComponent.h"
#include <stack>
class GridGameListView : public ISimpleGameListView
{
public:
GridGameListView(Window* window, FileData* root);
//virtual void onThemeChanged(const std::shared_ptr<ThemeData>& theme) override;
virtual FileData* getCursor() override;
virtual void setCursor(FileData*) override;
virtual bool input(InputConfig* config, Input input) override;
virtual const char* getName() const override { return "grid"; }
virtual std::vector<HelpPrompt> getHelpPrompts() override;
protected:
virtual void populateList(const std::vector<FileData*>& files) override;
virtual void launch(FileData* game) override;
ImageGridComponent<FileData*> mGrid;
};

View file

@ -0,0 +1,43 @@
#include "views/gamelist/IGameListView.h"
#include "Window.h"
#include "guis/GuiMetaDataEd.h"
#include "guis/GuiMenu.h"
#include "guis/GuiGamelistOptions.h"
#include "views/ViewController.h"
#include "Settings.h"
#include "Log.h"
#include "Sound.h"
bool IGameListView::input(InputConfig* config, Input input)
{
// select to open GuiGamelistOptions
if(config->isMappedTo("select", input) && input.value)
{
Sound::getFromTheme(mTheme, getName(), "menuOpen")->play();
mWindow->pushGui(new GuiGamelistOptions(mWindow, this->mRoot->getSystem()));
return true;
// Ctrl-R to reload a view when debugging
}else if(Settings::getInstance()->getBool("Debug") && config->getDeviceId() == DEVICE_KEYBOARD &&
(SDL_GetModState() & (KMOD_LCTRL | KMOD_RCTRL)) && input.id == SDLK_r && input.value != 0)
{
LOG(LogDebug) << "reloading view";
ViewController::get()->reloadGameListView(this, true);
return true;
}
return GuiComponent::input(config, input);
}
void IGameListView::setTheme(const std::shared_ptr<ThemeData>& theme)
{
mTheme = theme;
onThemeChanged(theme);
}
HelpStyle IGameListView::getHelpStyle()
{
HelpStyle style;
style.applyTheme(mTheme, getName());
return style;
}

View file

@ -0,0 +1,42 @@
#pragma once
#include "FileData.h"
#include "Renderer.h"
class Window;
class GuiComponent;
class FileData;
class ThemeData;
// This is an interface that defines the minimum for a GameListView.
class IGameListView : public GuiComponent
{
public:
IGameListView(Window* window, FileData* root) : GuiComponent(window), mRoot(root)
{ setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); }
virtual ~IGameListView() {}
// Called when a new file is added, a file is removed, a file's metadata changes, or a file's children are sorted.
// NOTE: FILE_SORTED is only reported for the topmost FileData, where the sort started.
// Since sorts are recursive, that FileData's children probably changed too.
virtual void onFileChanged(FileData* file, FileChangeType change) = 0;
// Called whenever the theme changes.
virtual void onThemeChanged(const std::shared_ptr<ThemeData>& theme) = 0;
void setTheme(const std::shared_ptr<ThemeData>& theme);
inline const std::shared_ptr<ThemeData>& getTheme() const { return mTheme; }
virtual FileData* getCursor() = 0;
virtual void setCursor(FileData*) = 0;
virtual bool input(InputConfig* config, Input input) override;
virtual const char* getName() const = 0;
virtual HelpStyle getHelpStyle() override;
protected:
FileData* mRoot;
std::shared_ptr<ThemeData> mTheme;
};

View file

@ -0,0 +1,109 @@
#include "views/gamelist/ISimpleGameListView.h"
#include "ThemeData.h"
#include "Window.h"
#include "views/ViewController.h"
#include "Sound.h"
#include "Settings.h"
ISimpleGameListView::ISimpleGameListView(Window* window, FileData* root) : IGameListView(window, root),
mHeaderText(window), mHeaderImage(window), mBackground(window), mThemeExtras(window)
{
mHeaderText.setText("Logo Text");
mHeaderText.setSize(mSize.x(), 0);
mHeaderText.setPosition(0, 0);
mHeaderText.setAlignment(ALIGN_CENTER);
mHeaderImage.setResize(0, mSize.y() * 0.185f);
mHeaderImage.setOrigin(0.5f, 0.0f);
mHeaderImage.setPosition(mSize.x() / 2, 0);
mBackground.setResize(mSize.x(), mSize.y());
addChild(&mHeaderText);
addChild(&mBackground);
addChild(&mThemeExtras);
}
void ISimpleGameListView::onThemeChanged(const std::shared_ptr<ThemeData>& theme)
{
using namespace ThemeFlags;
mBackground.applyTheme(theme, getName(), "background", ALL);
mHeaderImage.applyTheme(theme, getName(), "logo", ALL);
mHeaderText.applyTheme(theme, getName(), "logoText", ALL);
mThemeExtras.setExtras(ThemeData::makeExtras(theme, getName(), mWindow));
if(mHeaderImage.hasImage())
{
removeChild(&mHeaderText);
addChild(&mHeaderImage);
}else{
addChild(&mHeaderText);
removeChild(&mHeaderImage);
}
}
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);
}
bool ISimpleGameListView::input(InputConfig* config, Input input)
{
if(input.value != 0)
{
if(config->isMappedTo("a", input))
{
FileData* cursor = getCursor();
if(cursor->getType() == GAME)
{
Sound::getFromTheme(getTheme(), getName(), "launch")->play();
launch(cursor);
}else{
// it's a folder
if(cursor->getChildren().size() > 0)
{
mCursorStack.push(cursor);
populateList(cursor->getChildren());
}
}
return true;
}else if(config->isMappedTo("b", input))
{
if(mCursorStack.size())
{
populateList(mCursorStack.top()->getParent()->getChildren());
setCursor(mCursorStack.top());
mCursorStack.pop();
Sound::getFromTheme(getTheme(), getName(), "back")->play();
}else{
onFocusLost();
ViewController::get()->goToSystemView(getCursor()->getSystem());
}
return true;
}else if(config->isMappedTo("right", input))
{
if(Settings::getInstance()->getBool("QuickSystemSelect"))
{
onFocusLost();
ViewController::get()->goToNextGameList();
return true;
}
}else if(config->isMappedTo("left", input))
{
if(Settings::getInstance()->getBool("QuickSystemSelect"))
{
onFocusLost();
ViewController::get()->goToPrevGameList();
return true;
}
}
}
return IGameListView::input(config, input);
}

View file

@ -0,0 +1,38 @@
#pragma once
#include "views/gamelist/IGameListView.h"
#include "components/TextComponent.h"
#include "components/ImageComponent.h"
class ISimpleGameListView : public IGameListView
{
public:
ISimpleGameListView(Window* window, FileData* root);
virtual ~ISimpleGameListView() {}
// Called when a new file is added, a file is removed, a file's metadata changes, or a file's children are sorted.
// NOTE: FILE_SORTED is only reported for the topmost FileData, where the sort started.
// Since sorts are recursive, that FileData's children probably changed too.
virtual void onFileChanged(FileData* file, FileChangeType change);
// Called whenever the theme changes.
virtual void onThemeChanged(const std::shared_ptr<ThemeData>& theme);
virtual FileData* getCursor() = 0;
virtual void setCursor(FileData*) = 0;
virtual bool input(InputConfig* config, Input input) override;
protected:
virtual void populateList(const std::vector<FileData*>& files) = 0;
virtual void launch(FileData* game) = 0;
TextComponent mHeaderText;
ImageComponent mHeaderImage;
ImageComponent mBackground;
ThemeExtras mThemeExtras;
std::stack<FileData*> mCursorStack;
};