From 7dfa202d685ab733a65b78150812719c1ec91d5c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 27 May 2019 13:39:13 +0200 Subject: [PATCH 001/336] [My Changes] --- .gitignore | 1 + es-app/src/CollectionSystemManager.cpp | 14 +- es-app/src/EmulationStation.h | 6 +- es-app/src/FileData.cpp | 118 +++- es-app/src/FileData.h | 9 + es-app/src/FileSorts.cpp | 4 +- es-app/src/Gamelist.cpp | 72 +- es-app/src/MetaData.cpp | 9 +- es-app/src/MetaData.h | 3 +- es-app/src/SystemData.cpp | 143 +++- es-app/src/SystemData.h | 56 +- .../src/guis/GuiCollectionSystemsOptions.cpp | 30 +- es-app/src/guis/GuiCollectionSystemsOptions.h | 2 +- es-app/src/guis/GuiGameScraper.cpp | 2 +- es-app/src/guis/GuiGamelistFilter.cpp | 8 +- es-app/src/guis/GuiGamelistOptions.cpp | 21 +- .../src/guis/GuiGeneralScreensaverOptions.cpp | 2 +- .../src/guis/GuiGeneralScreensaverOptions.h | 2 +- es-app/src/guis/GuiMenu.cpp | 348 ++++++---- es-app/src/guis/GuiMenu.h | 2 +- es-app/src/guis/GuiMetaDataEd.cpp | 163 ++++- es-app/src/guis/GuiMetaDataEd.h | 8 +- es-app/src/guis/GuiScraperMulti.cpp | 2 +- es-app/src/guis/GuiScraperStart.cpp | 8 +- es-app/src/guis/GuiScreensaverOptions.cpp | 8 +- es-app/src/guis/GuiScreensaverOptions.h | 2 +- es-app/src/guis/GuiSettings.cpp | 8 +- es-app/src/guis/GuiSettings.h | 2 +- es-app/src/main.cpp | 116 ++-- es-app/src/scrapers/ScreenScraper.cpp | 13 +- es-app/src/scrapers/ScreenScraper.h | 2 +- es-app/src/views/SystemView.cpp | 15 +- es-app/src/views/ViewController.cpp | 137 ++-- es-app/src/views/ViewController.h | 9 +- .../src/views/gamelist/BasicGameListView.cpp | 55 +- .../views/gamelist/DetailedGameListView.cpp | 52 +- .../src/views/gamelist/DetailedGameListView.h | 1 + .../src/views/gamelist/GridGameListView.cpp | 74 ++- es-app/src/views/gamelist/GridGameListView.h | 15 +- .../views/gamelist/ISimpleGameListView.cpp | 31 +- es-core/src/GuiComponent.cpp | 107 +++ es-core/src/GuiComponent.h | 53 ++ es-core/src/InputManager.cpp | 2 +- es-core/src/Renderer_init_sdlgl.cpp | 38 +- es-core/src/Scripting.cpp | 2 +- es-core/src/Settings.cpp | 9 +- es-core/src/ThemeData.cpp | 57 +- es-core/src/ThemeData.h | 10 +- es-core/src/Window.cpp | 41 +- es-core/src/Window.h | 1 + es-core/src/components/ComponentGrid.cpp | 6 +- es-core/src/components/ComponentList.cpp | 2 +- es-core/src/components/DateTimeComponent.cpp | 4 +- .../src/components/DateTimeEditComponent.cpp | 4 +- es-core/src/components/GridTileComponent.cpp | 188 +++++- es-core/src/components/GridTileComponent.h | 24 +- es-core/src/components/ImageComponent.cpp | 39 +- es-core/src/components/ImageGridComponent.h | 176 +++-- es-core/src/components/MenuComponent.cpp | 4 +- es-core/src/components/MenuComponent.h | 2 +- es-core/src/components/OptionListComponent.h | 45 +- es-core/src/components/SliderComponent.cpp | 2 +- es-core/src/components/SwitchComponent.cpp | 2 +- es-core/src/components/VideoComponent.cpp | 2 +- es-core/src/guis/GuiInputConfig.cpp | 22 +- es-core/src/guis/GuiInputConfig.h | 3 +- es-core/src/guis/GuiTextEditPopup.cpp | 2 +- es-core/src/platform.cpp | 103 ++- es-core/src/platform.h | 4 +- es-core/src/resources/Font.cpp | 8 +- es-core/src/resources/TextureData.cpp | 7 +- es-core/src/utils/FileSystemUtil.cpp | 30 +- es-core/src/utils/StringUtil.cpp | 629 +++++++++--------- es-core/src/utils/StringUtil.h | 4 + 74 files changed, 2242 insertions(+), 963 deletions(-) diff --git a/.gitignore b/.gitignore index 4055d624e6..19593e251e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ Makefile CPackConfig.cmake CPackSourceConfig.cmake *.cbp +/.vs diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index cb5e2481da..d9082d9a0c 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -258,6 +258,9 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS if (name == "favorites" && file->metadata.get("favorite") == "false") { // need to check if still marked as favorite, if not remove ViewController::get()->getGameListView(curSys).get()->remove(collectionEntry, false); + + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); + ViewController::get()->getGameListView(curSys)->onFileChanged(collectionEntry, FILE_METADATA_CHANGED); } else { @@ -274,6 +277,7 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS CollectionFileData* newGame = new CollectionFileData(file, curSys); rootFolder->addChild(newGame); fileIndex->addToIndex(newGame); + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); ViewController::get()->getGameListView(curSys)->onFileChanged(newGame, FILE_METADATA_CHANGED); } @@ -453,6 +457,13 @@ void CollectionSystemManager::exitEditMode() // adds or removes a game from a specific collection bool CollectionSystemManager::toggleGameInCollection(FileData* file) { + /* + if (file->getType() == FOLDER) + { + if (file->findUniqueGameForFolder() == NULL) + return false; + } + */ if (file->getType() == GAME) { GuiInfoPopup* s; @@ -707,7 +718,8 @@ void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysDa for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) { // we won't iterate all collections - if ((*sysIt)->isGameSystem() && !(*sysIt)->isCollection()) { + if ((*sysIt)->isGameSystem() && !(*sysIt)->isCollection()) + { std::vector files = (*sysIt)->getRootFolder()->getFilesRecursive(GAME); for(auto gameIt = files.cbegin(); gameIt != files.cend(); gameIt++) { diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index 2bd27925bc..57389a1055 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -5,13 +5,13 @@ // 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 9 +#define PROGRAM_VERSION_MINOR 10 #define PROGRAM_VERSION_MAINTENANCE 0 -#define PROGRAM_VERSION_STRING "2.9.0rp-dev" +#define PROGRAM_VERSION_STRING "2.10.0 FCAMOD" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ -#define RESOURCE_VERSION_STRING "2,9,0\0" +#define RESOURCE_VERSION_STRING "2,10,0\0" #define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE #endif // ES_APP_EMULATION_STATION_H diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 16cdd164d3..532ea9a71d 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -22,7 +22,9 @@ FileData::FileData(FileType type, const std::string& path, SystemEnvironmentData // metadata needs at least a name field (since that's what getName() will return) if(metadata.get("name").empty()) metadata.set("name", getDisplayName()); - mSystemName = system->getName(); + + mSystemName = system->getName(); + // mDefaultCore = system->getSystemEnvData()->mDefaultCore; } FileData::~FileData() @@ -58,7 +60,7 @@ const std::string FileData::getThumbnailPath() const if(thumbnail.empty()) { thumbnail = metadata.get("image"); - + /* // no image, try to use local image if(thumbnail.empty() && Settings::getInstance()->getBool("LocalArt")) { @@ -72,12 +74,23 @@ const std::string FileData::getThumbnailPath() const thumbnail = path; } } - } + }*/ } return thumbnail; } +const bool FileData::getFavorite() +{ + return metadata.get("favorite") == "true"; +} + + +const bool FileData::getHidden() +{ + return metadata.get("hidden") == "true"; +} + const std::string& FileData::getName() { return metadata.get("name"); @@ -91,6 +104,26 @@ const std::string& FileData::getSortName() return metadata.get("sortname"); } +FileData* FileData::findUniqueGameForFolder() +{ + std::vector children = getChildren(); + + if (children.size() == 1 && children.at(0)->getType() == GAME) + return children.at(0); + + for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + if ((*it)->getType() == GAME) + return NULL; + + FileData* ret = (*it)->findUniqueGameForFolder(); + if (ret != NULL) + return ret; + } + + return NULL; +} + const std::vector& FileData::getChildrenListToDisplay() { FileFilterIndex* idx = CollectionSystemManager::get()->getSystemToView(mSystem)->getIndex(); @@ -111,10 +144,20 @@ const std::vector& FileData::getChildrenListToDisplay() { } } +const std::string FileData::getCore() const +{ + return metadata.get("core"); +} + +const std::string FileData::getEmulator() const +{ + return metadata.get("emulator"); +} + const std::string FileData::getVideoPath() const { std::string video = metadata.get("video"); - + /* // no video, try to use local video if(video.empty() && Settings::getInstance()->getBool("LocalArt")) { @@ -122,7 +165,7 @@ const std::string FileData::getVideoPath() const if(Utils::FileSystem::exists(path)) video = path; } - + */ return video; } @@ -199,12 +242,13 @@ std::string FileData::getKey() { const bool FileData::isArcadeAsset() { - const std::string stem = Utils::FileSystem::getStem(mPath); - return ( - (mSystem && (mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO))) - && - (MameNames::getInstance()->isBios(stem) || MameNames::getInstance()->isDevice(stem)) - ); + if (mSystem && (mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO))) + { + const std::string stem = Utils::FileSystem::getStem(mPath); + return MameNames::getInstance()->isBios(stem) || MameNames::getInstance()->isDevice(stem); + } + + return false; } FileData* FileData::getSourceFileData() @@ -271,44 +315,68 @@ void FileData::launchGame(Window* window) AudioManager::getInstance()->deinit(); VolumeControl::getInstance()->deinit(); - window->deinit(); + + bool hideWindow = Settings::getInstance()->getBool("HideWindow"); + if (hideWindow) + window->deinit(); std::string command = mEnvData->mLaunchCommand; - const std::string rom = Utils::FileSystem::getEscapedPath(getPath()); + const std::string rom = Utils::FileSystem::getEscapedPath(getPath()); const std::string basename = Utils::FileSystem::getStem(getPath()); - const std::string rom_raw = Utils::FileSystem::getPreferredPath(getPath()); + const std::string rom_raw = Utils::FileSystem::getPreferredPath(getPath()); + + std::string emulator = getEmulator(); + if (emulator.length() == 0) + emulator = mEnvData->getDefaultEmulator(); + + std::string core = getCore(); + if (core.length() == 0) + core = mEnvData->getDefaultCore(emulator); + + std::string customCommandLine = mEnvData->getEmulatorCommandLine(emulator); + if (customCommandLine.length() > 0) + command = customCommandLine; + + command = Utils::String::replace(command, "%EMULATOR%", emulator); + command = Utils::String::replace(command, "%CORE%", core); command = Utils::String::replace(command, "%ROM%", rom); command = Utils::String::replace(command, "%BASENAME%", basename); - command = Utils::String::replace(command, "%ROM_RAW%", rom_raw); + command = Utils::String::replace(command, "%ROM_RAW%", rom_raw); + command = Utils::String::replace(command, "%SYSTEM%", mSystemName); + command = Utils::String::replace(command, "%HOME%", Utils::FileSystem::getHomePath()); Scripting::fireEvent("game-start", rom, basename); LOG(LogInfo) << " " << command; - int exitCode = runSystemCommand(command); - if(exitCode != 0) + int exitCode = runSystemCommand(command, getDisplayName(), hideWindow ? NULL : window); + if (exitCode != 0) { LOG(LogWarning) << "...launch terminated with nonzero exit code " << exitCode << "!"; } Scripting::fireEvent("game-end"); - window->init(); + if (hideWindow) + window->init(); + VolumeControl::getInstance()->init(); window->normalizeNextUpdate(); //update number of times the game has been launched + if (exitCode == 0) + { + FileData* gameToUpdate = getSourceFileData(); - FileData* gameToUpdate = getSourceFileData(); - - int timesPlayed = gameToUpdate->metadata.getInt("playcount") + 1; - gameToUpdate->metadata.set("playcount", std::to_string(static_cast(timesPlayed))); + int timesPlayed = gameToUpdate->metadata.getInt("playcount") + 1; + gameToUpdate->metadata.set("playcount", std::to_string(static_cast(timesPlayed))); - //update last played time - gameToUpdate->metadata.set("lastplayed", Utils::Time::DateTime(Utils::Time::now())); - CollectionSystemManager::get()->refreshCollectionSystems(gameToUpdate); + //update last played time + gameToUpdate->metadata.set("lastplayed", Utils::Time::DateTime(Utils::Time::now())); + CollectionSystemManager::get()->refreshCollectionSystems(gameToUpdate); + } } CollectionFileData::CollectionFileData(FileData* file, SystemData* system) diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 63a9ce16e3..00fd4a923b 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -50,6 +50,14 @@ class FileData virtual const std::string getMarqueePath() const; virtual const std::string getImagePath() const; + virtual const std::string getCore() const; + virtual const std::string getEmulator() const; + + virtual const bool getHidden(); + virtual const bool getFavorite(); + + FileData* findUniqueGameForFolder(); + const std::vector& getChildrenListToDisplay(); std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; @@ -94,6 +102,7 @@ class FileData FileData* mSourceFileData; FileData* mParent; std::string mSystemName; + //std::string mDefaultCore; private: FileType mType; diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index 58b4ba90a1..5043656e11 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -17,8 +17,8 @@ namespace FileSorts FileData::SortType(&compareLastPlayed, true, "last played, ascending"), FileData::SortType(&compareLastPlayed, false, "last played, descending"), - FileData::SortType(&compareNumPlayers, true, "number players, ascending"), - FileData::SortType(&compareNumPlayers, false, "number players, descending"), + FileData::SortType(&compareNumPlayers, true, "number of players, ascending"), + FileData::SortType(&compareNumPlayers, false, "number of players, descending"), FileData::SortType(&compareReleaseDate, true, "release date, ascending"), FileData::SortType(&compareReleaseDate, false, "release date, descending"), diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 14af11757d..ea20baa94a 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -66,11 +66,13 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType LOG(LogWarning) << "gameList: folder doesn't already exist, won't create"; return NULL; } - + // create missing folder + + /* FCA TODO -> 1 seul jeu dans le folder ? FileData* folder = new FileData(FOLDER, Utils::FileSystem::getStem(treeNode->getPath()) + "/" + *path_it, system->getSystemEnvData(), system); treeNode->addChild(folder); - treeNode = folder; + treeNode = folder;*/ } path_it++; @@ -81,12 +83,12 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType void parseGamelist(SystemData* system) { - bool trustGamelist = Settings::getInstance()->getBool("ParseGamelistOnly"); std::string xmlpath = system->getGamelistPath(false); - - if(!Utils::FileSystem::exists(xmlpath)) + if (!Utils::FileSystem::exists(xmlpath)) return; + bool trustGamelist = Settings::getInstance()->getBool("ParseGamelistOnly"); + LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"..."; pugi::xml_document doc; @@ -107,39 +109,41 @@ void parseGamelist(SystemData* system) std::string relativeTo = system->getStartPath(); - const char* tagList[2] = { "game", "folder" }; - FileType typeList[2] = { GAME, FOLDER }; - for(int i = 0; i < 2; i++) + for (pugi::xml_node fileNode : root.children()) { - const char* tag = tagList[i]; - FileType type = typeList[i]; - for(pugi::xml_node fileNode = root.child(tag); fileNode; fileNode = fileNode.next_sibling(tag)) + FileType type = GAME; + + std::string tag = fileNode.name(); + + if (tag == "folder") + type = FOLDER; + else if (tag != "game") + continue; + + const std::string path = Utils::FileSystem::resolveRelativePath(fileNode.child("path").text().get(), relativeTo, false); + + if (!trustGamelist && !Utils::FileSystem::exists(path)) { - const std::string path = Utils::FileSystem::resolveRelativePath(fileNode.child("path").text().get(), relativeTo, false); - - if(!trustGamelist && !Utils::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; - } - else if(!file->isArcadeAsset()) - { - std::string defaultName = file->metadata.get("name"); - file->metadata = MetaDataList::createFromXML(GAME_METADATA, fileNode, relativeTo); + 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; + } + else if(!file->isArcadeAsset()) + { + 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); + //make sure name gets set if one didn't exist + if (file->metadata.get("name").empty()) + file->metadata.set("name", defaultName); - file->metadata.resetChangedFlag(); - } + file->metadata.resetChangedFlag(); } } } diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index 8a977e9509..3c9900d5a4 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -9,6 +9,10 @@ MetaDataDecl gameDecls[] = { {"name", MD_STRING, "", false, "name", "enter game name"}, {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, + + { "emulator", MD_PLIST, "", false, "emulator", "emulator" }, + { "core", MD_PLIST, "", false, "core", "core" }, + {"image", MD_PATH, "", false, "image", "enter path to image"}, {"video", MD_PATH , "", false, "video", "enter path to video"}, {"marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, @@ -40,7 +44,10 @@ MetaDataDecl folderDecls[] = { {"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"} + {"players", MD_INT, "1", false, "players", "enter number of players"}, + + { "favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on" }, + { "hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on" }, }; const std::vector folderMDD(folderDecls, folderDecls + sizeof(folderDecls) / sizeof(folderDecls[0])); diff --git a/es-app/src/MetaData.h b/es-app/src/MetaData.h index 6ed8f6d44c..7205511a2c 100644 --- a/es-app/src/MetaData.h +++ b/es-app/src/MetaData.h @@ -20,7 +20,8 @@ enum MetaDataType MD_PATH, MD_RATING, MD_DATE, - MD_TIME //used for lastplayed + MD_TIME, //used for lastplayed + MD_PLIST }; struct MetaDataDecl diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index d6e72cfd32..4052801b75 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -16,6 +16,8 @@ #include #endif +#include "Window.h" + std::vector SystemData::sSystemVector; SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : @@ -24,15 +26,19 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys mFilterIndex = new FileFilterIndex(); // if it's an actual system, initialize it, if not, just create the data structure - if(!CollectionSystem) + if (!CollectionSystem) { mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); mRootFolder->metadata.set("name", mFullName); - if(!Settings::getInstance()->getBool("ParseGamelistOnly")) + if (!Settings::getInstance()->getBool("ParseGamelistOnly")) + { populateFolder(mRootFolder); + if (mRootFolder->getChildren().size() == 0) + return; + } - if(!Settings::getInstance()->getBool("IgnoreGamelist")) + if (!Settings::getInstance()->getBool("IgnoreGamelist")) parseGamelist(this); mRootFolder->sort(FileSorts::SortTypes.at(0)); @@ -50,12 +56,6 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys SystemData::~SystemData() { - //save changed game data back to xml - if(!Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit") && !mIsCollectionSystem) - { - updateGamelist(this); - } - delete mRootFolder; delete mFilterIndex; } @@ -68,6 +68,12 @@ void SystemData::setIsGameSystemStatus() mIsGameSystem = (mName != "retropie"); } +char _easytolower(char in) { + if (in <= 'Z' && in >= 'A') + return in - ('Z' - 'z'); + return in; +} + void SystemData::populateFolder(FileData* folder) { const std::string& folderPath = folder->getPath(); @@ -77,6 +83,11 @@ void SystemData::populateFolder(FileData* folder) return; } + int di = folderPath.rfind("downloaded_images"); + int md = folderPath.rfind("media"); + if (di > 0 || md > 0) + return; + //make sure that this isn't a symlink to a thing we already have if(Utils::FileSystem::isSymlink(folderPath)) { @@ -87,7 +98,7 @@ void SystemData::populateFolder(FileData* folder) return; } } - + std::string filePath; std::string extension; bool isGame; @@ -104,6 +115,7 @@ void SystemData::populateFolder(FileData* folder) //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 = Utils::FileSystem::getExtension(filePath); + std::transform(extension.begin(), extension.end(), extension.begin(), ::_easytolower); //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 @@ -120,18 +132,23 @@ void SystemData::populateFolder(FileData* folder) isGame = true; } } - + //add directories that also do not match an extension as folders if(!isGame && Utils::FileSystem::isDirectory(filePath)) { - FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); - populateFolder(newFolder); - - //ignore folders that do not contain games - if(newFolder->getChildrenByFilename().size() == 0) - delete newFolder; - else - folder->addChild(newFolder); + if (filePath.rfind("downloaded_images") == std::string::npos && + filePath.rfind("media") == std::string::npos) + { + FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); + populateFolder(newFolder); + + if (newFolder->getChildrenByFilename().size() == 0) + delete newFolder; + else if (newFolder->findUniqueGameForFolder() != NULL) + delete newFolder; + else + folder->addChild(newFolder); + } } } } @@ -168,7 +185,7 @@ std::vector readList(const std::string& str, const char* delims = " } //creates systems from information located in a config file -bool SystemData::loadConfig() +bool SystemData::loadConfig(Window* window) { deleteSystems(); @@ -201,17 +218,65 @@ bool SystemData::loadConfig() LOG(LogError) << "es_systems.cfg is missing the tag!"; return false; } + + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { + std::vector emulatorList; + - for(pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - { - std::string name, fullname, path, cmd, themeFolder; + // std::vector coreList; + std::string name, fullname, path, cmd, themeFolder, defaultCore; + name = system.child("name").text().get(); fullname = system.child("fullname").text().get(); path = system.child("path").text().get(); + defaultCore = system.child("defaultCore").text().get(); + + pugi::xml_node emulators = system.child("emulators"); + if (emulators != NULL) + { + for (pugi::xml_node emulator : emulators.children()) + { + EmulatorData emulatorData; + emulatorData.mName = emulator.attribute("name").value(); + emulatorData.mCommandLine = emulator.attribute("command").value(); + + pugi::xml_node cores = emulator.child("cores"); + if (cores != NULL) + { + for (pugi::xml_node core : cores.children()) + { + const std::string& corename = core.text().get(); + + if (defaultCore.length() == 0) + defaultCore = corename; + + emulatorData.mCores.push_back(corename); + // coreList.push_back(corename); + } + } + + emulatorList.push_back(emulatorData); + } + } + + if (window != NULL) + window->renderLoadingScreen(fullname); // convert extensions list from a string into a vector of strings - std::vector extensions = readList(system.child("extension").text().get()); + + std::vector list = readList(system.child("extension").text().get()); + std::vector extensions; + + for (auto extension = list.cbegin(); extension != list.cend(); extension++) + { + std::string xt = (*extension); + std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); + + if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) + extensions.push_back(xt); + } cmd = system.child("command").text().get(); @@ -253,7 +318,7 @@ bool SystemData::loadConfig() path = Utils::FileSystem::getGenericPath(path); //expand home symbol if the startpath contains ~ - if(path[0] == '~') + if (path[0] == '~') { path.erase(0, 1); path.insert(0, Utils::FileSystem::getHomePath()); @@ -265,18 +330,25 @@ bool SystemData::loadConfig() envData->mSearchExtensions = extensions; envData->mLaunchCommand = cmd; envData->mPlatformIds = platformIds; + // envData->mDefaultCore = defaultCore; + envData->mEmulators = emulatorList; + + // envData->mCores = coreList; SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) { LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; delete newSys; - }else{ - sSystemVector.push_back(newSys); } + else + sSystemVector.push_back(newSys); } - CollectionSystemManager::get()->loadCollectionSystems(); + if (window != NULL) + window->renderLoadingScreen("Favoris"); + + CollectionSystemManager::get()->loadCollectionSystems(); return true; } @@ -328,10 +400,18 @@ void SystemData::writeExampleConfig(const std::string& path) void SystemData::deleteSystems() { + bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); + for(unsigned int i = 0; i < sSystemVector.size(); i++) { - delete sSystemVector.at(i); + SystemData* pData = sSystemVector.at(i); + + if (saveOnExit && !pData->mIsCollectionSystem) + updateGamelist(pData); + + delete pData; } + sSystemVector.clear(); } @@ -495,8 +575,9 @@ void SystemData::loadTheme() sysData.insert(std::pair("system.theme", getThemeFolder())); sysData.insert(std::pair("system.fullName", getFullName())); - mTheme->loadFile(sysData, path); - } catch(ThemeException& e) + mTheme->loadFile(getThemeFolder(), path); + } + catch(ThemeException& e) { LOG(LogError) << e.what(); mTheme = std::make_shared(); // reset to empty diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index ad0456b3c4..3925b74961 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -11,6 +11,14 @@ class FileData; class FileFilterIndex; class ThemeData; +class Window; + +struct EmulatorData +{ + std::string mName; + std::string mCommandLine; + std::vector mCores; +}; struct SystemEnvironmentData { @@ -18,6 +26,52 @@ struct SystemEnvironmentData std::vector mSearchExtensions; std::string mLaunchCommand; std::vector mPlatformIds; + +// std::string mDefaultCore; + + std::vector mEmulators; + + std::vector getCores(std::string emulatorName) + { + std::vector list; + + for (auto& emulator : mEmulators) + if (emulatorName == emulator.mName) + return emulator.mCores; + + return list; + } + + std::string getDefaultEmulator() + { + for (auto& emulator : mEmulators) + return emulator.mName; + + return ""; + } + + std::string getDefaultCore(std::string emulatorName) + { + for (auto& emulator : mEmulators) + { + if (emulatorName == emulator.mName) + { + for (auto core : emulator.mCores) + return core; + } + } + + return ""; + } + + std::string getEmulatorCommandLine(std::string emulatorName) + { + for (auto& emulator : mEmulators) + if (emulatorName == emulator.mName) + return emulator.mCommandLine; + + return ""; + } }; class SystemData @@ -46,7 +100,7 @@ class SystemData unsigned int getDisplayedGameCount() const; 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 bool loadConfig(Window* window); //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 diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index 27df3082eb..be30460aa4 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -9,7 +9,7 @@ #include "CollectionSystemManager.h" #include "Window.h" -GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) : GuiComponent(window), mMenu(window, "GAME COLLECTION SETTINGS") +GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) : GuiComponent(window), mMenu(window, _T("GAME COLLECTION SETTINGS")) { initializeMenu(); } @@ -27,9 +27,9 @@ void GuiCollectionSystemsOptions::initializeMenu() std::vector unusedFolders = CollectionSystemManager::get()->getUnusedSystemsFromTheme(); if (unusedFolders.size() > 0) { - addEntry("CREATE NEW CUSTOM COLLECTION FROM THEME", 0x777777FF, true, + addEntry(_T("CREATE NEW CUSTOM COLLECTION FROM THEME"), 0x777777FF, true, [this, unusedFolders] { - auto s = new GuiSettings(mWindow, "SELECT THEME FOLDER"); + auto s = new GuiSettings(mWindow, _T("SELECT THEME FOLDER")); std::shared_ptr< OptionListComponent > folderThemes = std::make_shared< OptionListComponent >(mWindow, "SELECT THEME FOLDER", true); // add Custom Systems @@ -52,7 +52,7 @@ void GuiCollectionSystemsOptions::initializeMenu() } ComponentListRow row; - row.addElement(std::make_shared(mWindow, "CREATE NEW CUSTOM COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("CREATE NEW CUSTOM COLLECTION"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); auto createCustomCollection = [this](const std::string& newVal) { std::string name = newVal; // we need to store the first Gui and remove it, as it'll be deleted by the actual Gui @@ -62,33 +62,33 @@ void GuiCollectionSystemsOptions::initializeMenu() createCollection(name); }; row.makeAcceptInputHandler([this, createCustomCollection] { - mWindow->pushGui(new GuiTextEditPopup(mWindow, "New Collection Name", "", createCustomCollection, false)); + mWindow->pushGui(new GuiTextEditPopup(mWindow, _T("New Collection Name"), "", createCustomCollection, false)); }); mMenu.addRow(row); bundleCustomCollections = std::make_shared(mWindow); bundleCustomCollections->setState(Settings::getInstance()->getBool("UseCustomCollectionsSystem")); - mMenu.addWithLabel("GROUP UNTHEMED CUSTOM COLLECTIONS", bundleCustomCollections); + mMenu.addWithLabel(_T("GROUP UNTHEMED CUSTOM COLLECTIONS"), bundleCustomCollections); sortAllSystemsSwitch = std::make_shared(mWindow); sortAllSystemsSwitch->setState(Settings::getInstance()->getBool("SortAllSystems")); - mMenu.addWithLabel("SORT CUSTOM COLLECTIONS AND SYSTEMS", sortAllSystemsSwitch); + mMenu.addWithLabel(_T("SORT CUSTOM COLLECTIONS AND SYSTEMS"), sortAllSystemsSwitch); if(CollectionSystemManager::get()->isEditing()) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("FINISH EDITING COLLECTION")+" '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "'", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.makeAcceptInputHandler(std::bind(&GuiCollectionSystemsOptions::exitEditMode, this)); mMenu.addRow(row); } - mMenu.addButton("BACK", "back", std::bind(&GuiCollectionSystemsOptions::applySettings, this)); + mMenu.addButton(_T("BACK"), _T("BACK"), std::bind(&GuiCollectionSystemsOptions::applySettings, this)); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); } -void GuiCollectionSystemsOptions::addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func) +void GuiCollectionSystemsOptions::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) { std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); @@ -139,25 +139,25 @@ void GuiCollectionSystemsOptions::addSystemsToMenu() std::map autoSystems = CollectionSystemManager::get()->getAutoCollectionSystems(); - autoOptionList = std::make_shared< OptionListComponent >(mWindow, "SELECT COLLECTIONS", true); + autoOptionList = std::make_shared< OptionListComponent >(mWindow, _T("SELECT COLLECTIONS"), true); // add Auto Systems for(std::map::const_iterator it = autoSystems.cbegin() ; it != autoSystems.cend() ; it++ ) { autoOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); } - mMenu.addWithLabel("AUTOMATIC GAME COLLECTIONS", autoOptionList); + mMenu.addWithLabel(_T("AUTOMATIC GAME COLLECTIONS"), autoOptionList); std::map customSystems = CollectionSystemManager::get()->getCustomCollectionSystems(); - customOptionList = std::make_shared< OptionListComponent >(mWindow, "SELECT COLLECTIONS", true); + customOptionList = std::make_shared< OptionListComponent >(mWindow, _T("SELECT COLLECTIONS"), true); // add Custom Systems for(std::map::const_iterator it = customSystems.cbegin() ; it != customSystems.cend() ; it++ ) { customOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); } - mMenu.addWithLabel("CUSTOM GAME COLLECTIONS", customOptionList); + mMenu.addWithLabel(_T("CUSTOM GAME COLLECTIONS"), customOptionList); } void GuiCollectionSystemsOptions::applySettings() @@ -209,6 +209,6 @@ bool GuiCollectionSystemsOptions::input(InputConfig* config, Input input) std::vector GuiCollectionSystemsOptions::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); return prompts; } diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.h b/es-app/src/guis/GuiCollectionSystemsOptions.h index fc6113e0a2..0187920630 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.h +++ b/es-app/src/guis/GuiCollectionSystemsOptions.h @@ -22,7 +22,7 @@ class GuiCollectionSystemsOptions : public GuiComponent void initializeMenu(); void applySettings(); void addSystemsToMenu(); - void addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func); + void addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func); void updateSettings(std::string newAutoSettings, std::string newCustomSettings); void createCollection(std::string inName); void exitEditMode(); diff --git a/es-app/src/guis/GuiGameScraper.cpp b/es-app/src/guis/GuiGameScraper.cpp index d8ebcb7160..c94eab6630 100644 --- a/es-app/src/guis/GuiGameScraper.cpp +++ b/es-app/src/guis/GuiGameScraper.cpp @@ -42,7 +42,7 @@ GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std:: mSearch->openInputScreen(mSearchParams); mGrid.resetCursor(); })); - buttons.push_back(std::make_shared(mWindow, "CANCEL", "cancel", [&] { delete this; })); + buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); mButtonGrid = makeButtonGrid(mWindow, buttons); mGrid.setEntry(mButtonGrid, Vector2i(0, 6), true, false); diff --git a/es-app/src/guis/GuiGamelistFilter.cpp b/es-app/src/guis/GuiGamelistFilter.cpp index 8a3625a1e3..1216775439 100644 --- a/es-app/src/guis/GuiGamelistFilter.cpp +++ b/es-app/src/guis/GuiGamelistFilter.cpp @@ -21,14 +21,14 @@ void GuiGamelistFilter::initializeMenu() // show filtered menu row.elements.clear(); - row.addElement(std::make_shared(mWindow, "RESET ALL FILTERS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("RESET ALL FILTERS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistFilter::resetAllFilters, this)); mMenu.addRow(row); row.elements.clear(); addFiltersToMenu(); - mMenu.addButton("BACK", "back", std::bind(&GuiGamelistFilter::applyFilters, this)); + mMenu.addButton(_T("BACK"), _T("BACK"), std::bind(&GuiGamelistFilter::applyFilters, this)); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); } @@ -61,7 +61,7 @@ void GuiGamelistFilter::addFiltersToMenu() FilterIndexType type = (*it).type; // type of filter std::map* allKeys = (*it).allIndexKeys; // all possible filters for this type - std::string menuLabel = (*it).menuLabel; // text to show in menu + std::string menuLabel = _L((*it).menuLabel); // text to show in menu std::shared_ptr< OptionListComponent > optionList; @@ -112,6 +112,6 @@ bool GuiGamelistFilter::input(InputConfig* config, Input input) std::vector GuiGamelistFilter::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); return prompts; } diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 7510f0c5c5..b789667da8 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -34,7 +34,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if(curChar < startChar || curChar > endChar) curChar = startChar; - mJumpToLetterList = std::make_shared(mWindow, "JUMP TO...", false); + mJumpToLetterList = std::make_shared(mWindow, _T("JUMP TO LETTER"), false); for (char c = startChar; c <= endChar; c++) { // check if c is a valid first letter in current list @@ -50,9 +50,10 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui } } - row.addElement(std::make_shared(mWindow, "JUMP TO...", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("JUMP TO LETTER"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.addElement(mJumpToLetterList, false); - row.input_handler = [&](InputConfig* config, Input input) { + row.input_handler = [&](InputConfig* config, Input input) + { if(config->isMappedTo("a", input) && input.value) { jumpToLetter(); @@ -67,20 +68,20 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui mMenu.addRow(row); // sort list by - mListSort = std::make_shared(mWindow, "SORT GAMES BY", false); + mListSort = std::make_shared(mWindow, _T("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 + mListSort->add(_L(Utils::String::toUpper(sort.description)), &sort, i == 0); // TODO - actually make the sort type persistent } - mMenu.addWithLabel("SORT GAMES BY", mListSort); + mMenu.addWithLabel(_T("SORT GAMES BY"), mListSort); } // show filtered menu if(!Settings::getInstance()->getBool("ForceDisableFilters")) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FILTER GAMELIST", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("APPLY FILTER"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); mMenu.addRow(row); @@ -109,7 +110,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "EDIT THIS GAME'S METADATA", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("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); @@ -198,7 +199,7 @@ void GuiGamelistOptions::openMetaDataEd() } mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(), p, Utils::FileSystem::getFileName(file->getPath()), - std::bind(&IGameListView::onFileChanged, ViewController::get()->getGameListView(file->getSystem()).get(), file, FILE_METADATA_CHANGED), deleteBtnFunc)); + std::bind(&IGameListView::onFileChanged, ViewController::get()->getGameListView(file->getSystem()).get(), file, FILE_METADATA_CHANGED), deleteBtnFunc, file)); } void GuiGamelistOptions::jumpToLetter() @@ -257,7 +258,7 @@ HelpStyle GuiGamelistOptions::getHelpStyle() std::vector GuiGamelistOptions::getHelpPrompts() { auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "close")); + prompts.push_back(HelpPrompt("b", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp index 30688c566c..e229aa87c6 100644 --- a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp +++ b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp @@ -8,7 +8,7 @@ #include "guis/GuiVideoScreensaverOptions.h" #include "Settings.h" -GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, const char* title) : GuiScreensaverOptions(window, title) +GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, std::string title) : GuiScreensaverOptions(window, title) { // screensaver time auto screensaver_time = std::make_shared(mWindow, 0.f, 30.f, 1.f, "m"); diff --git a/es-app/src/guis/GuiGeneralScreensaverOptions.h b/es-app/src/guis/GuiGeneralScreensaverOptions.h index 1992d9ad15..1a850e3871 100644 --- a/es-app/src/guis/GuiGeneralScreensaverOptions.h +++ b/es-app/src/guis/GuiGeneralScreensaverOptions.h @@ -7,7 +7,7 @@ class GuiGeneralScreensaverOptions : public GuiScreensaverOptions { public: - GuiGeneralScreensaverOptions(Window* window, const char* title); + GuiGeneralScreensaverOptions(Window* window, std::string title); virtual ~GuiGeneralScreensaverOptions(); private: diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 59d8ae4150..4b1cf2aea6 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -19,32 +19,35 @@ #include #include -GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, "MAIN MENU"), mVersion(window) +GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) + { bool isFullUI = UIModeController::getInstance()->isUIModeFull(); - if (isFullUI) - addEntry("SCRAPER", 0x777777FF, true, [this] { openScraperSettings(); }); - - addEntry("SOUND SETTINGS", 0x777777FF, true, [this] { openSoundSettings(); }); - if (isFullUI) - addEntry("UI SETTINGS", 0x777777FF, true, [this] { openUISettings(); }); + { + addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); + addEntry(_T("CONFIGURE INPUT"), 0x777777FF, true, [this] { openConfigInput(); }); + } - if (isFullUI) - addEntry("GAME COLLECTION SETTINGS", 0x777777FF, true, [this] { openCollectionSystemSettings(); }); + addEntry(_T("SOUND SETTINGS"), 0x777777FF, true, [this] { openSoundSettings(); }); if (isFullUI) - addEntry("OTHER SETTINGS", 0x777777FF, true, [this] { openOtherSettings(); }); + addEntry(_T("SCRAPER"), 0x777777FF, true, [this] { openScraperSettings(); }); if (isFullUI) - addEntry("CONFIGURE INPUT", 0x777777FF, true, [this] { openConfigInput(); }); + { + addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); + addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); + } - addEntry("QUIT", 0x777777FF, true, [this] {openQuitMenu(); }); + + addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); addChild(&mMenu); addVersionInfo(); + setSize(mMenu.getSize()); setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, Renderer::getScreenHeight() * 0.15f); } @@ -88,12 +91,12 @@ void GuiMenu::openScraperSettings() void GuiMenu::openSoundSettings() { - auto s = new GuiSettings(mWindow, "SOUND SETTINGS"); - + auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); + // volume auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); volume->setValue((float)VolumeControl::getInstance()->getVolume()); - s->addWithLabel("SYSTEM VOLUME", volume); + s->addWithLabel(_T("SYSTEM VOLUME"), volume); s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); if (UIModeController::getInstance()->isUIModeFull()) @@ -154,7 +157,7 @@ void GuiMenu::openSoundSettings() // disable sounds auto sounds_enabled = std::make_shared(mWindow); sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); - s->addWithLabel("ENABLE NAVIGATION SOUNDS", sounds_enabled); + s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); s->addSaveFunc([sounds_enabled] { if (sounds_enabled->getState() && !Settings::getInstance()->getBool("EnableSounds") @@ -168,7 +171,7 @@ void GuiMenu::openSoundSettings() auto video_audio = std::make_shared(mWindow); video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); - s->addWithLabel("ENABLE VIDEO AUDIO", video_audio); + s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); #ifdef _RPI_ @@ -202,72 +205,95 @@ void GuiMenu::openSoundSettings() void GuiMenu::openUISettings() { - auto s = new GuiSettings(mWindow, "UI SETTINGS"); + auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); - //UI mode - auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); - std::vector UImodes = UIModeController::getInstance()->getUIModes(); - for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) - UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); - s->addWithLabel("UI MODE", UImodeSelection); - Window* window = mWindow; - s->addSaveFunc([ UImodeSelection, window] + // theme set + auto themeSets = ThemeData::getThemeSets(); + + if (!themeSets.empty()) { - std::string selectedMode = UImodeSelection->getSelected(); - if (selectedMode != "Full") + std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if (selectedSet == themeSets.cend()) + selectedSet = themeSets.cbegin(); + + auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); + for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) + theme_set->add(it->first, it->first, it == selectedSet); + s->addWithLabel(_T("THEME"), theme_set); + + Window* window = mWindow; + s->addSaveFunc([window, theme_set] { - std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; - msg += "This will hide most menu-options to prevent changes to the system.\n"; - msg += "To unlock and return to the full UI, enter this code: \n"; - msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; - msg += "Do you want to proceed?"; - window->pushGui(new GuiMsgBox(window, msg, - "YES", [selectedMode] { - LOG(LogDebug) << "Setting UI mode to " << selectedMode; - Settings::getInstance()->setString("UIMode", selectedMode); - Settings::getInstance()->saveFile(); - }, "NO",nullptr)); - } - }); + bool needReload = false; + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + if (oldTheme != theme_set->getSelected()) + needReload = true; - // screensaver - ComponentListRow screensaver_row; - screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, "SCREENSAVER SETTINGS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - screensaver_row.addElement(makeArrow(mWindow), false); - screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); - s->addRow(screensaver_row); + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - // quick system select (left/right in game list view) - auto quick_sys_select = std::make_shared(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()); }); + if (needReload) + { + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + } + }); + } - // carousel transition option - auto move_carousel = std::make_shared(mWindow); - move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); - s->addWithLabel("CAROUSEL TRANSITIONS", move_carousel); - s->addSaveFunc([move_carousel] { - if (move_carousel->getState() - && !Settings::getInstance()->getBool("MoveCarousel") - && PowerSaver::getMode() == PowerSaver::INSTANT) + // LANGUAGE + + std::vector langues; + langues.push_back("en"); + + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); + if (xmlpath.length() > 0) + { + xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); + if (Utils::FileSystem::isDirectory(*it)) + continue; + + std::string name = *it; + + if (name.rfind("emulationstation2.po") == std::string::npos) + continue; + + name = Utils::FileSystem::getParent(name); + name = Utils::FileSystem::getFileName(name); + + if (name != "en") + langues.push_back(name); } - Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); - }); + + if (langues.size() > 1) + { + auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); + + for (auto it = langues.cbegin(); it != langues.cend(); it++) + language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); + + s->addWithLabel(_T("LANGUAGE"), language); + s->addSaveFunc([language] { + Settings::getInstance()->setString("Language", language->getSelected()); + }); + } + } // transition style - auto transition_style = std::make_shared< OptionListComponent >(mWindow, "TRANSITION STYLE", false); + auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); std::vector transitions; transitions.push_back("fade"); transitions.push_back("slide"); transitions.push_back("instant"); - for(auto it = transitions.cbegin(); it != transitions.cend(); it++) - transition_style->add(*it, *it, Settings::getInstance()->getString("TransitionStyle") == *it); - s->addWithLabel("TRANSITION STYLE", transition_style); + for (auto it = transitions.cbegin(); it != transitions.cend(); it++) + transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); + + s->addWithLabel(_T("TRANSITION STYLE"), transition_style); s->addSaveFunc([transition_style] { if (Settings::getInstance()->getString("TransitionStyle") == "instant" && transition_style->getSelected() != "instant" @@ -279,52 +305,41 @@ void GuiMenu::openUISettings() Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); }); - // theme set - auto themeSets = ThemeData::getThemeSets(); - - if(!themeSets.empty()) - { - std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(selectedSet == themeSets.cend()) - selectedSet = themeSets.cbegin(); - auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME SET", false); - for(auto it = themeSets.cbegin(); it != themeSets.cend(); it++) - theme_set->add(it->first, it->first, it == selectedSet); - s->addWithLabel("THEME SET", theme_set); + auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); + std::vector gameTransitions; + gameTransitions.push_back("fade"); + gameTransitions.push_back("slide"); + gameTransitions.push_back("instant"); + for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) + transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); - Window* window = mWindow; - s->addSaveFunc([window, theme_set] + s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); + s->addSaveFunc([transitionOfGames_style] { + if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" + && transitionOfGames_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) { - bool needReload = false; - std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - if(oldTheme != theme_set->getSelected()) - needReload = true; - - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - - if(needReload) - { - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation - } - }); - } + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); + }); // GameList view style - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, "GAMELIST VIEW STYLE", false); + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); std::vector styles; styles.push_back("automatic"); styles.push_back("basic"); styles.push_back("detailed"); styles.push_back("video"); styles.push_back("grid"); + styles.push_back("gridex"); for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(*it, *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); - s->addWithLabel("GAMELIST VIEW STYLE", gamelist_style); + gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); + + s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); s->addSaveFunc([gamelist_style] { bool needReload = false; if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) @@ -334,9 +349,10 @@ void GuiMenu::openUISettings() ViewController::get()->reloadAll(); }); + // Optionally start in selected system - auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, "START ON SYSTEM", false); - systemfocus_list->add("NONE", "", Settings::getInstance()->getString("StartupSystem") == ""); + auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); + systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { if ("retropie" != (*it)->getName()) @@ -344,21 +360,87 @@ void GuiMenu::openUISettings() systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); } } - s->addWithLabel("START ON SYSTEM", systemfocus_list); + s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); s->addSaveFunc([systemfocus_list] { Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); }); + + /* + //UI mode + auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); + std::vector UImodes = UIModeController::getInstance()->getUIModes(); + for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) + UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); + s->addWithLabel("UI MODE", UImodeSelection); + Window* window = mWindow; + s->addSaveFunc([ UImodeSelection, window] + { + std::string selectedMode = UImodeSelection->getSelected(); + if (selectedMode != "Full") + { + std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; + msg += "This will hide most menu-options to prevent changes to the system.\n"; + msg += "To unlock and return to the full UI, enter this code: \n"; + msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; + msg += "Do you want to proceed?"; + window->pushGui(new GuiMsgBox(window, msg, + "YES", [selectedMode] { + LOG(LogDebug) << "Setting UI mode to " << selectedMode; + Settings::getInstance()->setString("UIMode", selectedMode); + Settings::getInstance()->saveFile(); + }, "NO",nullptr)); + } + }); + */ + + // screensaver + ComponentListRow screensaver_row; + screensaver_row.elements.clear(); + screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + screensaver_row.addElement(makeArrow(mWindow), false); + screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); + s->addRow(screensaver_row); + + + // quick system select (left/right in game list view) + auto hideWindowScreen = std::make_shared(mWindow); + hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); + s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); + s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); + + + // quick system select (left/right in game list view) + auto quick_sys_select = std::make_shared(mWindow); + quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); + s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); + s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); + + // carousel transition option + auto move_carousel = std::make_shared(mWindow); + move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); + s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); + s->addSaveFunc([move_carousel] { + if (move_carousel->getState() + && !Settings::getInstance()->getBool("MoveCarousel") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); + }); + // show help auto show_help = std::make_shared(mWindow); show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); - s->addWithLabel("ON-SCREEN HELP", show_help); + s->addWithLabel(_T("ON-SCREEN HELP"), show_help); s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); // enable filters (ForceDisableFilters) auto enable_filter = std::make_shared(mWindow); enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); - s->addWithLabel("ENABLE FILTERS", enable_filter); + s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); s->addSaveFunc([enable_filter] { bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); @@ -371,16 +453,16 @@ void GuiMenu::openUISettings() void GuiMenu::openOtherSettings() { - auto s = new GuiSettings(mWindow, "OTHER SETTINGS"); + auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); // maximum vram - auto max_vram = std::make_shared(mWindow, 0.f, 1000.f, 10.f, "Mb"); + auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel("VRAM LIMIT", max_vram); + s->addWithLabel(_T("VRAM LIMIT"), max_vram); s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); // power saver - auto power_saver = std::make_shared< OptionListComponent >(mWindow, "POWER SAVER MODES", false); + auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); std::vector modes; modes.push_back("disabled"); modes.push_back("default"); @@ -388,7 +470,8 @@ void GuiMenu::openOtherSettings() modes.push_back("instant"); for (auto it = modes.cbegin(); it != modes.cend(); it++) power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); - s->addWithLabel("POWER SAVER MODES", power_saver); + + s->addWithLabel(_T("POWER SAVER MODES"), power_saver); s->addSaveFunc([this, power_saver] { if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { Settings::getInstance()->setString("TransitionStyle", "instant"); @@ -402,23 +485,23 @@ void GuiMenu::openOtherSettings() // gamelists auto save_gamelists = std::make_shared(mWindow); save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); - s->addWithLabel("SAVE METADATA ON EXIT", save_gamelists); + s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); auto parse_gamelists = std::make_shared(mWindow); parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); - s->addWithLabel("PARSE GAMESLISTS ONLY", parse_gamelists); + s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); - + /* auto local_art = std::make_shared(mWindow); local_art->setState(Settings::getInstance()->getBool("LocalArt")); - s->addWithLabel("SEARCH FOR LOCAL ART", local_art); + s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); - + */ // hidden files auto hidden_files = std::make_shared(mWindow); hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); - s->addWithLabel("SHOW HIDDEN FILES", hidden_files); + s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); #ifdef _RPI_ @@ -442,11 +525,12 @@ void GuiMenu::openOtherSettings() #endif // framerate + /* auto framerate = std::make_shared(mWindow); framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); - s->addWithLabel("SHOW FRAMERATE", framerate); + s->addWithLabel(_T("SHOW FRAMERATE"), framerate); s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); - + */ mWindow->pushGui(s); @@ -455,17 +539,23 @@ void GuiMenu::openOtherSettings() void GuiMenu::openConfigInput() { Window* window = mWindow; - window->pushGui(new GuiMsgBox(window, "ARE YOU SURE YOU WANT TO CONFIGURE INPUT?", "YES", + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + /* + window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), [window] { window->pushGui(new GuiDetectDevice(window, false, nullptr)); - }, "NO", nullptr) - ); + }, _T("NO"), nullptr) + );*/ } void GuiMenu::openQuitMenu() { - auto s = new GuiSettings(mWindow, "QUIT"); + Scripting::fireEvent("quit"); + quitES(""); + return; + + auto s = new GuiSettings(mWindow, _T("QUIT")); Window* window = mWindow; @@ -540,7 +630,7 @@ void GuiMenu::addVersionInfo() } void GuiMenu::openScreensaverOptions() { - mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, "SCREENSAVER SETTINGS")); + mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); } void GuiMenu::openCollectionSystemSettings() { @@ -553,7 +643,7 @@ void GuiMenu::onSizeChanged() mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); } -void GuiMenu::addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func) +void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) { std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); @@ -596,8 +686,8 @@ HelpStyle GuiMenu::getHelpStyle() std::vector GuiMenu::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("up/down", "choose")); - prompts.push_back(HelpPrompt("a", "select")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("SELECT"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiMenu.h b/es-app/src/guis/GuiMenu.h index da8406bb79..25fdcc2634 100644 --- a/es-app/src/guis/GuiMenu.h +++ b/es-app/src/guis/GuiMenu.h @@ -16,7 +16,7 @@ class GuiMenu : public GuiComponent HelpStyle getHelpStyle() override; private: - void addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func); + void addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func); void addVersionInfo(); void openCollectionSystemSettings(); void openConfigInput(); diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index f07a5061e6..5f0df4f525 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -20,7 +20,7 @@ #include "Window.h" GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, - const std::string& /*header*/, std::function saveCallback, std::function deleteFunc) : GuiComponent(window), + const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : GuiComponent(window), mScraperParams(scraperParams), mBackground(window, ":/frame.png"), @@ -35,9 +35,10 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector mHeaderGrid = std::make_shared(mWindow, Vector2i(1, 5)); - mTitle = std::make_shared(mWindow, "EDIT METADATA", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); + mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER); + mHeaderGrid->setEntry(mTitle, Vector2i(0, 1), false, true); mHeaderGrid->setEntry(mSubtitle, Vector2i(0, 3), false, true); @@ -46,6 +47,11 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector mList = std::make_shared(mWindow); mGrid.setEntry(mList, Vector2i(0, 1), true, true); + SystemData* system = file->getSystem(); + + auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); + auto core_choice = std::make_shared>(mWindow, _T("CORE"), false); + // populate list for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) { @@ -55,21 +61,104 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector if(iter->isStatistic) continue; + if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || + iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || + iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") + 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(mWindow, Utils::String::toUpper(iter->displayName), Font::get(FONT_SIZE_SMALL), 0x777777FF); + + if (iter->displayName == "emulator") + { + // if (file->getSystemEnvData()->mEmulators.size() <= 1) + // continue; + + std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); + + std::string currentEmul = file->getEmulator(); + + if (defaultEmul.length() == 0) + emul_choice->add(_T("DEFAULT"), "", false); + else + emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); + + for (auto core : file->getSystemEnvData()->mEmulators) + emul_choice->add(core.mName, core.mName, core.mName == currentEmul); + + row.addElement(std::make_shared(mWindow, _T("EMULATOR"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(emul_choice, false); + + mList->addRow(row); + emul_choice->setTag(iter->key); + mEditors.push_back(emul_choice); + + emul_choice->setSelectedChangedCallback([this, system, core_choice, file](std::string emulatorName) + { + std::string currentCore = file->getCore(); + + std::string defaultCore = system->getSystemEnvData()->getDefaultCore(emulatorName); + if (emulatorName.length() == 0) + defaultCore = system->getSystemEnvData()->getDefaultCore(system->getSystemEnvData()->getDefaultEmulator()); + + core_choice->clear(); + if (defaultCore.length() == 0) + core_choice->add(_T("DEFAULT"), "", false); + else + core_choice->add(_T("DEFAULT")+" ("+ defaultCore+")", "", false); + + std::vector cores = system->getSystemEnvData()->getCores(emulatorName); + + bool found = false; + + for (auto it = cores.begin(); it != cores.end(); it++) + { + std::string core = *it; + core_choice->add(core, core, currentCore == core); + if (currentCore == core) + found = true; + } + + if (!found) + core_choice->selectFirstItem(); + else + core_choice->invalidate(); + }); + + continue; + } + + if (iter->displayName == "core") + { + // core_choice->add(_T("DEFAULT"), "", true); + core_choice->setTag(iter->key); + + row.addElement(std::make_shared(mWindow, "CORE", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(core_choice, false); + + mList->addRow(row); + ed = core_choice; + + mEditors.push_back(core_choice); + + // force change event to load core list + emul_choice->invalidate(); + continue; + } + + auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); row.addElement(lbl, true); // label - switch(iter->type) + switch (iter->type) { - case MD_BOOL: + case MD_BOOL: { ed = std::make_shared(window); row.addElement(ed, false, true); break; } - case MD_RATING: + case MD_RATING: { ed = std::make_shared(window); const float height = lbl->getSize().y() * 0.71f; @@ -85,7 +174,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector break; } - case MD_DATE: + case MD_DATE: { ed = std::make_shared(window); row.addElement(ed, false); @@ -99,14 +188,14 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector break; } - case MD_TIME: + case MD_TIME: { ed = std::make_shared(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); row.addElement(ed, false); break; } - case MD_MULTILINE_STRING: - default: + case MD_MULTILINE_STRING: + default: { // MD_STRING ed = std::make_shared(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); @@ -129,27 +218,30 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector }); break; } - } + } assert(ed); mList->addRow(row); + + ed->setTag(iter->key); ed->setValue(mMetaData->get(iter->key)); + mEditors.push_back(ed); } std::vector< std::shared_ptr > buttons; - if(!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) - buttons.push_back(std::make_shared(mWindow, "SCRAPE", "scrape", std::bind(&GuiMetaDataEd::fetch, this))); + if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) + buttons.push_back(std::make_shared(mWindow, _T("SCRAPE"), _T("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); - buttons.push_back(std::make_shared(mWindow, "SAVE", "save", [&] { save(); delete this; })); - buttons.push_back(std::make_shared(mWindow, "CANCEL", "cancel", [&] { delete this; })); + buttons.push_back(std::make_shared(mWindow, _T("SAVE"), _T("SAVE"), [&] { save(); delete this; })); + buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); if(mDeleteFunc) { auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; - buttons.push_back(std::make_shared(mWindow, "DELETE", "delete", deleteBtnFunc)); + buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE"), deleteBtnFunc)); } mButtons = makeButtonGrid(mWindow, buttons); @@ -184,17 +276,26 @@ void GuiMetaDataEd::save() // remove game from index mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); - for(unsigned int i = 0; i < mEditors.size(); i++) - { - if(mMetaDataDecl.at(i).isStatistic) - continue; - mMetaData->set(mMetaDataDecl.at(i).key, mEditors.at(i)->getValue()); + for (unsigned int i = 0; i < mEditors.size(); i++) + { + std::shared_ptr ed = mEditors.at(i); + + auto val = ed->getValue(); + auto key = ed->getTag(); + + if (key == "core" || key == "emulator") + { + std::shared_ptr> list = std::static_pointer_cast>(ed); + val = list->getSelected(); + } + + mMetaData->set(key, val); } // enter game in index mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); - if(mSavedCallback) + if (mSavedCallback) mSavedCallback(); // update respective Collection Entries @@ -211,10 +312,8 @@ 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; + auto val = mEditors.at(i)->getValue(); + auto key = mEditors.at(i)->getTag(); mEditors.at(i)->setValue(result.mdl.get(key)); } } @@ -225,7 +324,7 @@ void GuiMetaDataEd::close(bool closeAllWindows) bool dirty = false; for(unsigned int i = 0; i < mEditors.size(); i++) { - const std::string& key = mMetaDataDecl.at(i).key; + auto key = mEditors.at(i)->getTag(); if(mMetaData->get(key) != mEditors.at(i)->getValue()) { dirty = true; @@ -250,9 +349,9 @@ void GuiMetaDataEd::close(bool closeAllWindows) { // 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 + "ENREGISTRER LES MODIFICATIONS ?", + "OUI", [this, closeFunc] { save(); closeFunc(); }, + "NON", closeFunc )); }else{ closeFunc(); @@ -277,7 +376,7 @@ bool GuiMetaDataEd::input(InputConfig* config, Input input) std::vector GuiMetaDataEd::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiMetaDataEd.h b/es-app/src/guis/GuiMetaDataEd.h index ee192f982b..8bdf3531d1 100644 --- a/es-app/src/guis/GuiMetaDataEd.h +++ b/es-app/src/guis/GuiMetaDataEd.h @@ -5,17 +5,20 @@ #include "components/ComponentGrid.h" #include "components/NinePatchComponent.h" #include "scrapers/Scraper.h" +#include "components/MenuComponent.h" +#include "components/OptionListComponent.h" #include "GuiComponent.h" #include "MetaData.h" class ComponentList; class TextComponent; +class FileData; class GuiMetaDataEd : public GuiComponent { public: GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams params, - const std::string& header, std::function savedCallback, std::function deleteFunc); + const std::string& header, std::function savedCallback, std::function deleteFunc, FileData* file); bool input(InputConfig* config, Input input) override; void onSizeChanged() override; @@ -38,6 +41,9 @@ class GuiMetaDataEd : public GuiComponent ScraperSearchParams mScraperParams; + // typedef OptionListComponent CoreList; + //std::shared_ptr mCoreList; + std::vector< std::shared_ptr > mEditors; std::vector mMetaDataDecl; diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 28b87db40f..7dcd2d16bd 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -32,7 +32,7 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queue(mWindow, "SCRAPING IN PROGRESS", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); mGrid.setEntry(mTitle, Vector2i(0, 0), false, true); - mSystem = std::make_shared(mWindow, "SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); + mSystem = std::make_shared(mWindow, _T("SYSTEM"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); mGrid.setEntry(mSystem, Vector2i(0, 1), false, true); mSubtitle = std::make_shared(mWindow, "subtitle text", Font::get(FONT_SIZE_SMALL), 0x888888FF, ALIGN_CENTER); diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index 10c2a9b867..2330f095f6 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -34,8 +34,8 @@ GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), 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.addButton("START", "LANCER", std::bind(&GuiScraperStart::pressedStart, this)); + mMenu.addButton(_T("BACK"), _T("BACK"), [&] { delete this; }); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); } @@ -122,7 +122,7 @@ bool GuiScraperStart::input(InputConfig* config, Input input) std::vector GuiScraperStart::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiScreensaverOptions.cpp b/es-app/src/guis/GuiScreensaverOptions.cpp index e9d2f068c3..f6d33f49f2 100644 --- a/es-app/src/guis/GuiScreensaverOptions.cpp +++ b/es-app/src/guis/GuiScreensaverOptions.cpp @@ -5,11 +5,11 @@ #include "SystemData.h" #include "Window.h" -GuiScreensaverOptions::GuiScreensaverOptions(Window* window, const char* title) : GuiComponent(window), mMenu(window, title) +GuiScreensaverOptions::GuiScreensaverOptions(Window* window, std::string title) : GuiComponent(window), mMenu(window, title) { addChild(&mMenu); - mMenu.addButton("BACK", "go back", [this] { delete this; }); + mMenu.addButton(_T("BACK"), _T("BACK"), [this] { delete this; }); setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); @@ -62,8 +62,8 @@ std::vector GuiScreensaverOptions::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiScreensaverOptions.h b/es-app/src/guis/GuiScreensaverOptions.h index 6f61489f8b..e8e4332e1d 100644 --- a/es-app/src/guis/GuiScreensaverOptions.h +++ b/es-app/src/guis/GuiScreensaverOptions.h @@ -8,7 +8,7 @@ class GuiScreensaverOptions : public GuiComponent { public: - GuiScreensaverOptions(Window* window, const char* title); + GuiScreensaverOptions(Window* window, std::string title); virtual ~GuiScreensaverOptions(); // just calls save(); virtual void save(); diff --git a/es-app/src/guis/GuiSettings.cpp b/es-app/src/guis/GuiSettings.cpp index 53f96c439e..f0d3dd697a 100644 --- a/es-app/src/guis/GuiSettings.cpp +++ b/es-app/src/guis/GuiSettings.cpp @@ -5,11 +5,11 @@ #include "SystemData.h" #include "Window.h" -GuiSettings::GuiSettings(Window* window, const char* title) : GuiComponent(window), mMenu(window, title) +GuiSettings::GuiSettings(Window* window, std::string title) : GuiComponent(window), mMenu(window, title) { addChild(&mMenu); - mMenu.addButton("BACK", "go back", [this] { delete this; }); + mMenu.addButton(_T("BACK"), _T("BACK"), [this] { delete this; }); setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); @@ -62,8 +62,8 @@ std::vector GuiSettings::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiSettings.h b/es-app/src/guis/GuiSettings.h index d5b0d97998..05ece518f2 100644 --- a/es-app/src/guis/GuiSettings.h +++ b/es-app/src/guis/GuiSettings.h @@ -8,7 +8,7 @@ class GuiSettings : public GuiComponent { public: - GuiSettings(Window* window, const char* title); + GuiSettings(Window* window, std::string title); virtual ~GuiSettings(); // just calls save(); void save(); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 6c1fe80ef6..136bee9434 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -35,7 +35,7 @@ bool parseArgs(int argc, char* argv[]) for(int i = 1; i < argc; i++) { - if(strcmp(argv[i], "--resolution") == 0) + if (strcmp(argv[i], "--resolution") == 0) { if(i >= argc - 2) { @@ -48,9 +48,10 @@ bool parseArgs(int argc, char* argv[]) i += 2; // skip the argument value Settings::getInstance()->setInt("WindowWidth", width); Settings::getInstance()->setInt("WindowHeight", height); - }else if(strcmp(argv[i], "--screensize") == 0) + } + else if (strcmp(argv[i], "--screensize") == 0) { - if(i >= argc - 2) + if (i >= argc - 2) { std::cerr << "Invalid screensize supplied."; return false; @@ -61,7 +62,8 @@ bool parseArgs(int argc, char* argv[]) i += 2; // skip the argument value Settings::getInstance()->setInt("ScreenWidth", width); Settings::getInstance()->setInt("ScreenHeight", height); - }else if(strcmp(argv[i], "--screenoffset") == 0) + } + else if (strcmp(argv[i], "--screenoffset") == 0) { if(i >= argc - 2) { @@ -74,7 +76,8 @@ bool parseArgs(int argc, char* argv[]) i += 2; // skip the argument value Settings::getInstance()->setInt("ScreenOffsetX", x); Settings::getInstance()->setInt("ScreenOffsetY", y); - }else if (strcmp(argv[i], "--screenrotate") == 0) + } + else if (strcmp(argv[i], "--screenrotate") == 0) { if (i >= argc - 1) { @@ -85,44 +88,60 @@ bool parseArgs(int argc, char* argv[]) int rotate = atoi(argv[i + 1]); ++i; // skip the argument value Settings::getInstance()->setInt("ScreenRotate", rotate); - }else if(strcmp(argv[i], "--gamelist-only") == 0) + } + else if (strcmp(argv[i], "--gamelist-only") == 0) { Settings::getInstance()->setBool("ParseGamelistOnly", true); - }else if(strcmp(argv[i], "--ignore-gamelist") == 0) + } + else if (strcmp(argv[i], "--ignore-gamelist") == 0) { Settings::getInstance()->setBool("IgnoreGamelist", true); - }else if(strcmp(argv[i], "--show-hidden-files") == 0) + } + else if (strcmp(argv[i], "--show-hidden-files") == 0) { Settings::getInstance()->setBool("ShowHiddenFiles", true); - }else if(strcmp(argv[i], "--draw-framerate") == 0) + } + else if (strcmp(argv[i], "--draw-framerate") == 0) { Settings::getInstance()->setBool("DrawFramerate", true); - }else if(strcmp(argv[i], "--no-exit") == 0) + } + else if (strcmp(argv[i], "--no-exit") == 0) { Settings::getInstance()->setBool("ShowExit", false); - }else if(strcmp(argv[i], "--no-splash") == 0) + } + else if (strcmp(argv[i], "--no-splash") == 0) { Settings::getInstance()->setBool("SplashScreen", false); - }else if(strcmp(argv[i], "--debug") == 0) + } + else if (strcmp(argv[i], "--debug") == 0) { Settings::getInstance()->setBool("Debug", true); Settings::getInstance()->setBool("HideConsole", false); Log::setReportingLevel(LogDebug); - }else if(strcmp(argv[i], "--fullscreen-borderless") == 0) + } + else if (strcmp(argv[i], "--fullscreen-borderless") == 0) { Settings::getInstance()->setBool("FullscreenBorderless", true); - }else if(strcmp(argv[i], "--windowed") == 0) + } + else if (strcmp(argv[i], "--fullscreen") == 0) { - Settings::getInstance()->setBool("Windowed", true); - }else if(strcmp(argv[i], "--vsync") == 0) + Settings::getInstance()->setBool("FullscreenBorderless", false); + } + else if (strcmp(argv[i], "--windowed") == 0 || strcmp(argv[i], "-windowed") == 0) { - bool vsync = (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "1") == 0) ? true : false; + Settings::getInstance()->setBool("Windowed", true); + } + else if (strcmp(argv[i], "--vsync") == 0 || strcmp(argv[i], "-vsync") == 0) + { + bool vsync = strlen(argv[i]) <= 7 ? true : (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "1") == 0) ? true : false; Settings::getInstance()->setBool("VSync", vsync); i++; // skip vsync value - }else if(strcmp(argv[i], "--scrape") == 0) + } + else if (strcmp(argv[i], "--scrape") == 0) { scrape_cmdline = true; - }else if(strcmp(argv[i], "--max-vram") == 0) + } + else if (strcmp(argv[i], "--max-vram") == 0) { int maxVRAM = atoi(argv[i + 1]); Settings::getInstance()->setInt("MaxVRAM", maxVRAM); @@ -197,25 +216,29 @@ bool verifyHomeFolderExists() } // Returns true if everything is OK, -bool loadSystemConfigFile(const char** errorString) +bool loadSystemConfigFile(Window* window, const char** errorString) { *errorString = NULL; - - if(!SystemData::loadConfig()) + + if(!SystemData::loadConfig(window)) { 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; } @@ -232,8 +255,9 @@ int main(int argc, char* argv[]) { srand((unsigned int)time(NULL)); - std::locale::global(std::locale("C")); - + //std::locale::global(std::locale("C")); + std::locale::global(std::locale("en-US")); + if(!parseArgs(argc, argv)) return 0; @@ -311,29 +335,33 @@ int main(int argc, char* argv[]) LOG(LogInfo) << " ARB_texture_non_power_of_two: " << (glExts.find("ARB_texture_non_power_of_two") != std::string::npos ? "ok" : "MISSING"); if(splashScreen) { - std::string progressText = "Loading..."; + std::string progressText = "Chargement"; + if (splashScreenProgress) - progressText = "Loading system config..."; + progressText = "Chargement de la configuration"; + window.renderLoadingScreen(progressText); } } const char* errorMsg = NULL; - if(!loadSystemConfigFile(&errorMsg)) + if(!loadSystemConfigFile(&window, &errorMsg)) { // something went terribly wrong - if(errorMsg == NULL) + if (errorMsg == NULL) { LOG(LogError) << "Unknown error occured while parsing system config file."; - if(!scrape_cmdline) + + 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", [] { + _T("QUIT"), [] { SDL_Event* quit = new SDL_Event(); quit->type = SDL_QUIT; SDL_PushEvent(quit); @@ -341,30 +369,26 @@ int main(int argc, char* argv[]) } //run the command line scraper then quit - if(scrape_cmdline) - { + 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(); - - if(splashScreen && splashScreenProgress) - window.renderLoadingScreen("Done."); + // ViewController::get()->preload(); + + if (splashScreen && splashScreenProgress) + window.renderLoadingScreen(_T("Starting UI")); //choose which GUI to open depending on if an input configuration already exists if(errorMsg == NULL) { - if(Utils::FileSystem::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0) - { - ViewController::get()->goToStart(); - }else{ - window.pushGui(new GuiDetectDevice(&window, true, [] { ViewController::get()->goToStart(); })); - } + if (Utils::FileSystem::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0) + ViewController::get()->goToStart(true); + else + window.pushGui(new GuiDetectDevice(&window, true, [] { ViewController::get()->goToStart(true); })); } //generate joystick events since we're done loading @@ -386,9 +410,10 @@ int main(int argc, char* argv[]) { InputManager::getInstance()->parseEvent(event, &window); - if(event.type == SDL_QUIT) + if (event.type == SDL_QUIT) running = false; - } while(SDL_PollEvent(&event)); + } + while(SDL_PollEvent(&event)); // triggered if exiting from SDL_WaitEvent due to event if (ps_standby) @@ -429,6 +454,7 @@ int main(int argc, char* argv[]) while(window.peekGui() != ViewController::get()) delete window.peekGui(); + window.deinit(); MameNames::deinit(); diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index e9eb53378b..31f32ea408 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -125,7 +125,17 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, ScreenScraperRequest::ScreenScraperConfig ssConfig; - path = ssConfig.getGameSearchUrl(params.game->getFileName()); + if (params.nameOverride.length() == 0) + { + path = ssConfig.getGameSearchUrl(params.game->getFileName()); + path += "&romtype=rom"; + } + else + { + path = ssConfig.getGameSearchUrl(params.nameOverride); + path += "&romtype=jeu"; + } + auto& platforms = params.system->getPlatformIds(); for (auto platformIt = platforms.cbegin(); platformIt != platforms.cend(); platformIt++) @@ -142,7 +152,6 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, requests.push(std::unique_ptr(new ScreenScraperRequest(requests, results, path))); } - } void ScreenScraperRequest::process(const std::unique_ptr& req, std::vector& results) diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index 4c749008f1..55c4a89c4f 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -28,7 +28,7 @@ class ScreenScraperRequest : public ScraperHttpRequest const std::string API_DEV_P = { 108, 28, 54, 55, 83, 43, 91, 44, 30, 22, 41, 12, 0, 108, 38, 29 }; const std::string API_DEV_KEY = { 54, 73, 115, 100, 101, 67, 111, 107, 79, 66, 68, 66, 67, 56, 118, 77, 54, 88, 101, 54 }; const std::string API_URL_BASE = "https://www.screenscraper.fr/api2"; - const std::string API_SOFT_NAME = "Emulationstation " + static_cast(PROGRAM_VERSION_STRING); + const std::string API_SOFT_NAME = "Emulationstation 2.9.0"; // +static_cast(PROGRAM_VERSION_STRING); /** Which type of image artwork we need. Possible values (not a comprehensive list): - ss: in-game screenshot diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index cdd5b1ea10..9ed1a22dc6 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -259,7 +259,7 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) if (!getSelected()->isGameSystem()) ss << "CONFIGURATION"; else - ss << gameCount << " GAMES AVAILABLE"; + ss << gameCount << " " << _T("GAMES AVAILABLE"); mSystemInfo.setText(ss.str()); }, false, 1); @@ -376,14 +376,15 @@ std::vector SystemView::getHelpPrompts() { std::vector prompts; if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) - prompts.push_back(HelpPrompt("up/down", "choose")); + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); else - prompts.push_back(HelpPrompt("left/right", "choose")); - prompts.push_back(HelpPrompt("a", "select")); - prompts.push_back(HelpPrompt("x", "random")); + prompts.push_back(HelpPrompt("left/right", _T("CHOOSE"))); - if (!UIModeController::getInstance()->isUIModeKid() && Settings::getInstance()->getBool("ScreenSaverControls")) - prompts.push_back(HelpPrompt("select", "launch screensaver")); + prompts.push_back(HelpPrompt("a", _T("SELECT"))); + //prompts.push_back(HelpPrompt("x", "random")); + +// if (!UIModeController::getInstance()->isUIModeKid() && Settings::getInstance()->getBool("ScreenSaverControls")) + // prompts.push_back(HelpPrompt("select", "launch screensaver")); return prompts; } diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index ada681e260..65fb1b4122 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -43,7 +43,7 @@ ViewController::~ViewController() sInstance = NULL; } -void ViewController::goToStart() +void ViewController::goToStart(bool forceImmediate) { // If specific system is requested, go directly to the game list auto requestedSystem = Settings::getInstance()->getString("StartupSystem"); @@ -52,7 +52,8 @@ void ViewController::goToStart() for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++){ if ((*it)->getName() == requestedSystem) { - goToGameList(*it); + goToSystemView(*it, forceImmediate); +// goToGameList(*it); return; } } @@ -60,14 +61,15 @@ void ViewController::goToStart() // Requested system doesn't exist Settings::getInstance()->setString("StartupSystem", ""); } - goToSystemView(SystemData::sSystemVector.at(0)); + goToSystemView(SystemData::sSystemVector.at(0), forceImmediate); } void ViewController::ReloadAndGoToStart() { - mWindow->renderLoadingScreen("Loading..."); + mWindow->renderLoadingScreen("Chargement"); + ViewController::get()->reloadAll(); - ViewController::get()->goToStart(); + ViewController::get()->goToStart(false); } int ViewController::getSystemId(SystemData* system) @@ -76,7 +78,7 @@ int ViewController::getSystemId(SystemData* system) return (int)(std::find(sysVec.cbegin(), sysVec.cend(), system) - sysVec.cbegin()); } -void ViewController::goToSystemView(SystemData* system) +void ViewController::goToSystemView(SystemData* system, bool forceImmediate) { // Tell any current view it's about to be hidden if (mCurrentView) @@ -95,7 +97,7 @@ void ViewController::goToSystemView(SystemData* system) mCurrentView->onShow(); PowerSaver::setState(true); - playViewTransition(); + playViewTransition(forceImmediate); } void ViewController::goToNextGameList() @@ -139,10 +141,10 @@ void ViewController::goToGameList(SystemData* system) { mCurrentView->onShow(); } - playViewTransition(); + playViewTransition(false); } -void ViewController::playViewTransition() +void ViewController::playViewTransition(bool forceImmediate) { Vector3f target(Vector3f::Zero()); if(mCurrentView) @@ -153,7 +155,7 @@ void ViewController::playViewTransition() return; std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); - if(transition_style == "fade") + if(!forceImmediate && transition_style == "fade") { // fade // stop whatever's currently playing, leaving mFadeOpacity wherever it is @@ -181,7 +183,7 @@ void ViewController::playViewTransition() }else{ advanceAnimation(0, (int)(mFadeOpacity * FADE_DURATION)); } - } else if (transition_style == "slide"){ + } else if (!forceImmediate && transition_style == "slide"){ // slide or simple slide setAnimation(new MoveCameraAnimation(mCamera, target)); updateHelpPrompts(); // update help prompts immediately @@ -212,8 +214,8 @@ void ViewController::launch(FileData* game, Vector3f center) } // Hide the current view - if (mCurrentView) - mCurrentView->onHide(); + //if (mCurrentView) + //mCurrentView->onHide(); Transform4x4f origCamera = mCamera; origCamera.translation() = -mCurrentView->getPosition(); @@ -223,7 +225,7 @@ void ViewController::launch(FileData* game, Vector3f center) mWindow->stopInfoPopup(); // make sure we disable any existing info popup mLockInput = true; - std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); + std::string transition_style = Settings::getInstance()->getString("GameTransitionStyle"); if(transition_style == "fade") { // fade out, launch game, fade back in @@ -240,7 +242,7 @@ void ViewController::launch(FileData* game, Vector3f center) // 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->launchGame(mWindow); + game->launchGame(mWindow); mCamera = origCamera; setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 600), 0, [this] { mLockInput = false; }, true); this->onFileChanged(game, FILE_METADATA_CHANGED); @@ -280,33 +282,72 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste bool themeHasVideoView = system->getTheme()->hasView("video"); + bool themeHasGridView = system->getTheme()->hasView("grid"); + bool themeHasGridExView = system->getTheme()->hasView("gridex"); + //decide type GameListViewType selectedViewType = AUTOMATIC; + bool detailed = false; + std::string viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + if (viewPreference.compare("basic") == 0) selectedViewType = BASIC; - if (viewPreference.compare("detailed") == 0) + else if (viewPreference.compare("detailed") == 0) + { + detailed = true; selectedViewType = DETAILED; - if (viewPreference.compare("grid") == 0) + } + else if (themeHasGridExView && viewPreference.compare("gridex") == 0) + selectedViewType = GRIDEX; + else if (themeHasGridView && viewPreference.compare("grid") == 0) selectedViewType = GRID; - if (viewPreference.compare("video") == 0) + else if (viewPreference.compare("video") == 0) selectedViewType = VIDEO; - if (selectedViewType == AUTOMATIC) + if (selectedViewType == AUTOMATIC || detailed) { - std::vector files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER); - for (auto it = files.cbegin(); it != files.cend(); it++) + if (themeHasGridView && system->getTheme()->getDefaultView() == "grid" && !detailed) + selectedViewType = GRID; + else if (themeHasGridExView && system->getTheme()->getDefaultView() == "gridex" && !detailed) + selectedViewType = GRIDEX; + else { - if (themeHasVideoView && !(*it)->getVideoPath().empty()) - { - selectedViewType = VIDEO; - break; - } - else if (!(*it)->getThumbnailPath().empty()) - { + selectedViewType = BASIC; + + if (system->getTheme()->getDefaultView() == "detailed") selectedViewType = DETAILED; - // Don't break out in case any subsequent files have video + else if (system->getTheme()->getDefaultView() != "basic") + { + std::vector files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER); + for (auto it = files.cbegin(); it != files.cend(); it++) + { + if (themeHasVideoView && !(*it)->getVideoPath().empty() && viewPreference.compare("detailed") != 0) + { + selectedViewType = VIDEO; + break; + } + else if (!(*it)->getThumbnailPath().empty()) + { + if (!detailed && (*it)->metadata.get("thumbnail").length() > 0) + { + if (themeHasGridExView && (*it)->metadata.get("image").length() > 0) + selectedViewType = GRIDEX; + else if (themeHasGridView) + selectedViewType = GRID; + else + selectedViewType = DETAILED; + } + else + selectedViewType = DETAILED; + + if (!themeHasVideoView) + break; + + // Don't break out in case any subsequent files have video + } + } } } } @@ -323,6 +364,13 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste case GRID: view = std::shared_ptr(new GridGameListView(mWindow, system->getRootFolder())); break; + case GRIDEX: + { + GridGameListView* listView = new GridGameListView(mWindow, system->getRootFolder()); + listView->setGridEx(); + view = std::shared_ptr(listView); + } + break; case BASIC: default: view = std::shared_ptr(new BasicGameListView(mWindow, system->getRootFolder())); @@ -356,7 +404,7 @@ std::shared_ptr ViewController::getSystemListView() bool ViewController::input(InputConfig* config, Input input) { - if(mLockInput) + if (mLockInput) return true; // open menu @@ -437,7 +485,7 @@ void ViewController::preload() { i++; char buffer[100]; - sprintf (buffer, "Loading '%s' (%d/%d)", + sprintf (buffer, "Chargement de '%s' (%d/%d)", (*it)->getFullName().c_str(), i, (int)SystemData::sSystemVector.size()); mWindow->renderLoadingScreen(std::string(buffer)); } @@ -454,20 +502,22 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) if(it->second.get() == view) { bool isCurrent = (mCurrentView == it->second); + SystemData* system = it->first; FileData* cursor = view->getCursor(); mGameListViews.erase(it); - if(reloadTheme) + if (reloadTheme) system->loadTheme(); + system->getIndex()->setUIModeFilters(); std::shared_ptr newView = getGameListView(system); // to counter having come from a placeholder - if (!cursor->isPlaceHolder()) { + if (!cursor->isPlaceHolder()) newView->setCursor(cursor); - } - if(isCurrent) + + if (isCurrent) mCurrentView = newView; break; @@ -483,19 +533,26 @@ void ViewController::reloadAll() { // clear all gamelistviews std::map cursorMap; + for(auto it = mGameListViews.cbegin(); it != mGameListViews.cend(); it++) - { cursorMap[it->first] = it->second->getCursor(); - } + mGameListViews.clear(); + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + { + if (cursorMap.find((*it)) == cursorMap.end()) + cursorMap[(*it)] = NULL; + } // load themes, create gamelistviews and reset filters for(auto it = cursorMap.cbegin(); it != cursorMap.cend(); it++) { it->first->loadTheme(); it->first->getIndex()->resetFilters(); - getGameListView(it->first)->setCursor(it->second); + + if (it->second != NULL) + getGameListView(it->first)->setCursor(it->second); } // Rebuild SystemListView @@ -509,11 +566,11 @@ void ViewController::reloadAll() }else if(mState.viewing == SYSTEM_SELECT) { SystemData* system = mState.getSystem(); - goToSystemView(SystemData::sSystemVector.front()); + goToSystemView(SystemData::sSystemVector.front(), false); mSystemListView->goToSystem(system, false); mCurrentView = mSystemListView; }else{ - goToSystemView(SystemData::sSystemVector.front()); + goToSystemView(SystemData::sSystemVector.front(), false); } updateHelpPrompts(); diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h index eec5e9fdb1..7aef661c1c 100644 --- a/es-app/src/views/ViewController.h +++ b/es-app/src/views/ViewController.h @@ -34,8 +34,8 @@ class ViewController : public GuiComponent void goToNextGameList(); void goToPrevGameList(); void goToGameList(SystemData* system); - void goToSystemView(SystemData* system); - void goToStart(); + void goToSystemView(SystemData* system, bool forceImmediate = false); + void goToStart(bool forceImmediate = false); void ReloadAndGoToStart(); void onFileChanged(FileData* file, FileChangeType change); @@ -62,7 +62,8 @@ class ViewController : public GuiComponent BASIC, DETAILED, GRID, - VIDEO + VIDEO, + GRIDEX }; struct State @@ -89,7 +90,7 @@ class ViewController : public GuiComponent ViewController(Window* window); static ViewController* sInstance; - void playViewTransition(); + void playViewTransition(bool forceImmediate); int getSystemId(SystemData* system); std::shared_ptr mCurrentView; diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 899f12578c..c697a09130 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -29,7 +29,7 @@ void BasicGameListView::onThemeChanged(const std::shared_ptr& theme) void BasicGameListView::onFileChanged(FileData* file, FileChangeType change) { - if(change == FILE_METADATA_CHANGED) + if (change == FILE_METADATA_CHANGED) { // might switch to a detailed view ViewController::get()->reloadGameListView(this); @@ -42,12 +42,40 @@ void BasicGameListView::onFileChanged(FileData* file, FileChangeType change) void BasicGameListView::populateList(const std::vector& files) { mList.clear(); - mHeaderText.setText(mRoot->getSystem()->getFullName()); + + std::string systemName = mRoot->getSystem()->getFullName(); + mHeaderText.setText(systemName); + + bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + if (files.size() > 0) { - for(auto it = files.cbegin(); it != files.cend(); it++) + for (auto it = files.cbegin(); it != files.cend(); it++) + { + if ((*it)->getFavorite()) + if (showHiddenFiles || !(*it)->getHidden()) + { + if (systemName == "favorites") + mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + else + + mList.add(_T("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + } + } + /* + for (auto it = files.cbegin(); it != files.cend(); it++) + { + if (!(*it)->getFavorite() && ((*it)->getType() == FOLDER)) + if (showHiddenFiles || !(*it)->getHidden()) + mList.add("[Folder] "+(*it)->getName(), *it, ((*it)->getType() == FOLDER)); + + }*/ + + for (auto it = files.cbegin(); it != files.cend(); it++) { - mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + if (!(*it)->getFavorite()) + if (showHiddenFiles || !(*it)->getHidden()) + mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); } } else @@ -146,17 +174,24 @@ std::vector BasicGameListView::getHelpPrompts() std::vector 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("left/right", _T("SYSTEM"))); + + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("LAUNCH"))); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + if(!UIModeController::getInstance()->isUIModeKid()) - prompts.push_back(HelpPrompt("select", "options")); + prompts.push_back(HelpPrompt("select", _T("options"))); + if(mRoot->getSystem()->isGameSystem()) - prompts.push_back(HelpPrompt("x", "random")); + prompts.push_back(HelpPrompt("x", _T("RANDOM"))); + if(mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) { std::string prompt = CollectionSystemManager::get()->getEditingCollection(); + if (prompt == "Favorites") + prompt = "FAVORIS"; + prompts.push_back(HelpPrompt("y", prompt)); } return prompts; diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 87d2893f56..bfb26740ce 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -32,29 +32,29 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : addChild(&mImage); // metadata labels + values - mLblRating.setText("Rating: "); + mLblRating.setText(_T("Rating") + ": "); addChild(&mLblRating); addChild(&mRating); - mLblReleaseDate.setText("Released: "); + mLblReleaseDate.setText(_T("Released") + ": "); addChild(&mLblReleaseDate); addChild(&mReleaseDate); - mLblDeveloper.setText("Developer: "); + mLblDeveloper.setText(_T("Developer") + ": "); addChild(&mLblDeveloper); addChild(&mDeveloper); - mLblPublisher.setText("Publisher: "); + mLblPublisher.setText(_T("Publisher")+": "); addChild(&mLblPublisher); addChild(&mPublisher); - mLblGenre.setText("Genre: "); + mLblGenre.setText(_T("Genre") + ": "); addChild(&mLblGenre); addChild(&mGenre); - mLblPlayers.setText("Players: "); + mLblPlayers.setText(_T("Players") + ": "); addChild(&mLblPlayers); addChild(&mPlayers); - mLblLastPlayed.setText("Last played: "); + mLblLastPlayed.setText(_T("Last played") + ": "); addChild(&mLblLastPlayed); mLastPlayed.setDisplayRelative(true); addChild(&mLastPlayed); - mLblPlayCount.setText("Times played: "); + mLblPlayCount.setText(_T("Times played")); addChild(&mLblPlayCount); addChild(&mPlayCount); @@ -188,6 +188,15 @@ void DetailedGameListView::initMDValues() mDescContainer.setSize(mDescContainer.getSize().x(), mSize.y() - mDescContainer.getPosition().y()); } +std::string DetailedGameListView::getMetadata(FileData* file, std::string name) +{ + std::string ret = file->metadata.get(name); + if (ret == "unknown") + return _L(ret); + + return ret; +} + void DetailedGameListView::updateInfoPanel() { FileData* file = (mList.size() == 0 || mList.isScrolling()) ? NULL : mList.getSelected(); @@ -199,22 +208,27 @@ void DetailedGameListView::updateInfoPanel() //mDescription.setText(""); fadingOut = true; }else{ - mImage.setImage(file->getImagePath()); - mDescription.setText(file->metadata.get("desc")); + + if (file->getImagePath().empty()) + mImage.setImage(file->getThumbnailPath()); + else + mImage.setImage(file->getImagePath()); + + mDescription.setText(getMetadata(file, "desc")); mDescContainer.reset(); - 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")); - mName.setValue(file->metadata.get("name")); + mRating.setValue(getMetadata(file, "rating")); + mReleaseDate.setValue(getMetadata(file, "releasedate")); + mDeveloper.setValue(getMetadata(file, "developer")); + mPublisher.setValue(getMetadata(file, "publisher")); + mGenre.setValue(getMetadata(file, "genre")); + mPlayers.setValue(getMetadata(file, "players")); + mName.setValue(getMetadata(file, "name")); if(file->getType() == GAME) { - mLastPlayed.setValue(file->metadata.get("lastplayed")); - mPlayCount.setValue(file->metadata.get("playcount")); + mLastPlayed.setValue(getMetadata(file, "lastplayed")); + mPlayCount.setValue(getMetadata(file, "playcount")); } fadingOut = false; diff --git a/es-app/src/views/gamelist/DetailedGameListView.h b/es-app/src/views/gamelist/DetailedGameListView.h index 76c8a93537..3c070c955f 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.h +++ b/es-app/src/views/gamelist/DetailedGameListView.h @@ -23,6 +23,7 @@ class DetailedGameListView : public BasicGameListView void initMDLabels(); void initMDValues(); + std::string getMetadata(FileData* file, std::string name); ImageComponent mImage; diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 91f9c08b0a..a2dbc27428 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -11,6 +11,7 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : ISimpleGameListView(window, root), mGrid(window), mDescContainer(window), mDescription(window), + mImage(window), mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -21,6 +22,8 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : { const float padding = 0.01f; + mGridEx = false; + mGrid.setPosition(mSize.x() * 0.1f, mSize.y() * 0.1f); mGrid.setDefaultZIndex(20); mGrid.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); @@ -28,6 +31,13 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : populateList(root->getChildrenListToDisplay()); + // 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); + mImage.setDefaultZIndex(30); + addChild(&mImage); + // metadata labels + values mLblRating.setText("Rating: "); addChild(&mLblRating); @@ -78,6 +88,12 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : updateInfoPanel(); } +void GridGameListView::setGridEx() +{ + mGridEx = true; + mGrid.setThemeName(getName()); +} + FileData* GridGameListView::getCursor() { return mGrid.getSelected(); @@ -116,10 +132,21 @@ void GridGameListView::populateList(const std::vector& files) mHeaderText.setText(mRoot->getSystem()->getFullName()); if (files.size() > 0) { + std::string systemName = mRoot->getSystem()->getFullName(); + bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + for (auto it = files.cbegin(); it != files.cend(); it++) - { - mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it); - } + if ((*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) + { + if (systemName == "favorites") + mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it); + else + mGrid.add(_T("\uF006 ") + (*it)->getName(), (*it)->getThumbnailPath(), *it); + } + + for (auto it = files.cbegin(); it != files.cend(); it++) + if (!(*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) + mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it); } else { @@ -136,6 +163,14 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) mGrid.applyTheme(theme, getName(), "gamegrid", ALL); mName.applyTheme(theme, getName(), "md_name", ALL); + if (theme->getElement(getName(), "md_image", "image")) + { + mImageVisible = true; + mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + } + else + mImageVisible = false; + initMDLabels(); std::vector labels = getMDLabels(); assert(labels.size() == 8); @@ -168,6 +203,7 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION)); sortChildren(); + updateInfoPanel(); } void GridGameListView::initMDLabels() @@ -240,11 +276,23 @@ void GridGameListView::updateInfoPanel() FileData* file = (mGrid.size() == 0 || mGrid.isScrolling()) ? NULL : mGrid.getSelected(); bool fadingOut; - if(file == NULL) + if (file == NULL) { //mDescription.setText(""); fadingOut = true; - }else{ + } + else + { + if (mImageVisible) + { + if (file->getImagePath().empty()) + mImage.setImage(file->getThumbnailPath()); + else + mImage.setImage(file->getImagePath()); + } + else + mImage.setImage(""); + mDescription.setText(file->metadata.get("desc")); mDescContainer.reset(); @@ -266,11 +314,12 @@ void GridGameListView::updateInfoPanel() } std::vector comps = getMDValues(); + comps.push_back(&mImage); comps.push_back(&mDescription); comps.push_back(&mName); std::vector labels = getMDLabels(); comps.insert(comps.cend(), labels.cbegin(), labels.cend()); - + for(auto it = comps.cbegin(); it != comps.cend(); it++) { GuiComponent* comp = *it; @@ -364,14 +413,15 @@ std::vector GridGameListView::getHelpPrompts() std::vector prompts; if(Settings::getInstance()->getBool("QuickSystemSelect")) - prompts.push_back(HelpPrompt("lr", "system")); - prompts.push_back(HelpPrompt("up/down/left/right", "choose")); - prompts.push_back(HelpPrompt("a", "launch")); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("lr", _T("SYSTEM"))); + prompts.push_back(HelpPrompt("up/down/left/right", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("LAUNCH"))); + prompts.push_back(HelpPrompt("b", _T("BACK"))); if(!UIModeController::getInstance()->isUIModeKid()) - prompts.push_back(HelpPrompt("select", "options")); + prompts.push_back(HelpPrompt("select", _T("OPTIONS"))); if(mRoot->getSystem()->isGameSystem()) - prompts.push_back(HelpPrompt("x", "random")); + prompts.push_back(HelpPrompt("x", _T("RANDOM"))); + if(mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) { std::string prompt = CollectionSystemManager::get()->getEditingCollection(); diff --git a/es-app/src/views/gamelist/GridGameListView.h b/es-app/src/views/gamelist/GridGameListView.h index da09c87903..50cfc24ab7 100644 --- a/es-app/src/views/gamelist/GridGameListView.h +++ b/es-app/src/views/gamelist/GridGameListView.h @@ -7,6 +7,7 @@ #include "components/ScrollableContainer.h" #include "components/ImageGridComponent.h" #include "views/gamelist/ISimpleGameListView.h" +#include "views/gamelist/BasicGameListView.h" class GridGameListView : public ISimpleGameListView { @@ -20,11 +21,19 @@ class GridGameListView : public ISimpleGameListView virtual bool input(InputConfig* config, Input input) override; - virtual const char* getName() const override { return "grid"; } + virtual const char* getName() const override + { + if (mGridEx) + return "gridex"; + + return "grid"; + } virtual std::vector getHelpPrompts() override; virtual void launch(FileData* game) override; + void setGridEx(); + protected: virtual std::string getQuickSystemSelectRightButton() override; virtual std::string getQuickSystemSelectLeftButton() override; @@ -51,6 +60,10 @@ class GridGameListView : public ISimpleGameListView DateTimeComponent mLastPlayed; TextComponent mPlayCount; TextComponent mName; + ImageComponent mImage; + + bool mImageVisible; + bool mGridEx; std::vector getMDLabels(); std::vector getMDValues(); diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 5a1a68ed35..90b1c032ef 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -80,14 +80,24 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) { if(input.value != 0) { - if(config->isMappedTo("a", input)) + if (config->isMappedTo("a", input)) { FileData* cursor = getCursor(); - if(cursor->getType() == GAME) + + if (cursor->getType() == FOLDER) + { + FileData* gameOfFolder = cursor->findUniqueGameForFolder(); + if (gameOfFolder != NULL) + cursor = gameOfFolder; + } + + if (cursor->getType() == GAME) { Sound::getFromTheme(getTheme(), getName(), "launch")->play(); launch(cursor); - }else{ + } + else + { // it's a folder if(cursor->getChildren().size() > 0) { @@ -99,7 +109,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } return true; - }else if(config->isMappedTo("b", input)) + } + else if(config->isMappedTo("b", input)) { if(mCursorStack.size()) { @@ -118,7 +129,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } return true; - }else if(config->isMappedLike(getQuickSystemSelectRightButton(), input)) + } + else if(config->isMappedLike(getQuickSystemSelectRightButton(), input)) { if(Settings::getInstance()->getBool("QuickSystemSelect")) { @@ -126,7 +138,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) ViewController::get()->goToNextGameList(); return true; } - }else if(config->isMappedLike(getQuickSystemSelectLeftButton(), input)) + } + else if(config->isMappedLike(getQuickSystemSelectLeftButton(), input)) { if(Settings::getInstance()->getBool("QuickSystemSelect")) { @@ -134,7 +147,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) ViewController::get()->goToPrevGameList(); return true; } - }else if (config->isMappedTo("x", input)) + } + else if (config->isMappedTo("x", input)) { if (mRoot->getSystem()->isGameSystem()) { @@ -146,7 +160,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } return true; } - }else if (config->isMappedTo("y", input) && UIModeController::getInstance()->isUIModeFull()) + } + else if (config->isMappedTo("y", input) && UIModeController::getInstance()->isUIModeFull()) { if(mRoot->getSystem()->isGameSystem()) { diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index 9b7d22aa22..7cd24b6f15 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -8,6 +8,103 @@ #include "Window.h" #include + +#if defined(_WIN32) +#define _conv(x) GuiTextTool::convertFromWideString(L ## x) +#else +#define _conv(x) x +#endif + +#include +#include +#include "resources\ResourceManager.h" + +std::vector GuiTextTool::mItems; +std::string GuiTextTool::mCurrentLanguage = "en"; +bool GuiTextTool::mCurrentLanguageLoaded = false; + +void GuiTextTool::setLanguage(std::string lang) +{ + mCurrentLanguage = lang; + mCurrentLanguageLoaded = false; +} + +void GuiTextTool::ensureLocalisation() +{ + if (mCurrentLanguageLoaded) + { + if (Settings::getInstance()->getString("Language") == mCurrentLanguage) + return; + + mCurrentLanguage = Settings::getInstance()->getString("Language"); + } + + mCurrentLanguageLoaded = true; + + for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) + delete (*it); + + mItems.clear(); + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/"+ mCurrentLanguage +"/emulationstation2.po"); + if (Utils::FileSystem::exists(xmlpath)) + { + LocalizationItem* currentItem = NULL; + + std::ifstream file(xmlpath); + std::string str; + while (std::getline(file, str)) + { + if (str.length() > 0 && str[0] == '#') + { + if (currentItem != NULL && currentItem->msgid.length() > 0 && currentItem->msgstr.length() > 0) + mItems.push_back(currentItem); + + currentItem = new LocalizationItem(); + } + + if (currentItem != NULL && str.find("msgid") == 0) + { + auto start = str.find("\""); + if (start != std::string::npos) + { + auto end = str.find("\"", start + 1); + if (end != std::string::npos) + currentItem->msgid = str.substr(start + 1, end - start - 1); + } + } + + if (currentItem != NULL && str.find("msgstr") == 0) + { + auto start = str.find("\""); + if (start != std::string::npos) + { + auto end = str.find("\"", start + 1); + if (end != std::string::npos) + currentItem->msgstr = str.substr(start + 1, end - start - 1); + } + } + } + + if (currentItem != NULL) + delete currentItem; + } +} + + +const std::string GuiTextTool::localize(const std::string text) +{ + ensureLocalisation(); + + for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) + { + if (text == (*it)->msgid) + return (*it)->msgstr; + } + + return text; +} + GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255), mPosition(Vector3f::Zero()), mOrigin(Vector2f::Zero()), mRotationOrigin(0.5, 0.5), mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false) @@ -281,6 +378,16 @@ std::string GuiComponent::getValue() const return ""; } +void GuiComponent::setTag(const std::string& value) +{ + mTag = value; +} + +std::string GuiComponent::getTag() const +{ + return mTag; +} + void GuiComponent::textInput(const char* text) { for(auto iter = mChildren.cbegin(); iter != mChildren.cend(); iter++) diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 4c15ba578b..4712ce32f5 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -10,6 +10,10 @@ #include #include +#if defined(_WIN32) +#include +#endif + class Animation; class AnimationController; class Font; @@ -109,6 +113,10 @@ class GuiComponent virtual std::string getValue() const; virtual void setValue(const std::string& value); + + virtual std::string getTag() const; + virtual void setTag(const std::string& value); + virtual void onFocusGained() {}; virtual void onFocusLost() {}; @@ -145,6 +153,8 @@ class GuiComponent GuiComponent* mParent; std::vector mChildren; + std::string mTag; + Vector3f mPosition; Vector2f mOrigin; Vector2f mRotationOrigin; @@ -166,4 +176,47 @@ class GuiComponent AnimationController* mAnimationMap[MAX_ANIMATIONS]; }; + + +class LocalizationItem +{ +public: + std::string msgid; + std::string msgstr; +}; + +class GuiTextTool +{ +public: +#if defined(_WIN32) + static const std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + + std::string string; + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return string; + } +#endif + static void ensureLocalisation(); + static const std::string localize(const std::string text); + + static void setLanguage(std::string lang); + +private: + static std::vector mItems; + static std::string mCurrentLanguage; + static bool mCurrentLanguageLoaded; +}; + +#if defined(_WIN32) +#define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) +#define _L(x) GuiTextTool::localize(x) +#else +#define _T(x) GuiTextTool::localize(x) +#define _L(x) GuiTextTool::localize(x) +#endif // _WIN32 + #endif // ES_CORE_GUI_COMPONENT_H diff --git a/es-core/src/InputManager.cpp b/es-core/src/InputManager.cpp index d074367ebf..59fc54bdac 100644 --- a/es-core/src/InputManager.cpp +++ b/es-core/src/InputManager.cpp @@ -426,7 +426,7 @@ void InputManager::doOnFinish() LOG(LogInfo) << " " << tocall; std::cout << "==============================================\ninput config finish command:\n"; - int exitCode = runSystemCommand(tocall); + int exitCode = runSystemCommand(tocall, "", NULL); std::cout << "==============================================\n"; if(exitCode != 0) diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index f87cddd0cc..fb33b2cea4 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -37,7 +37,7 @@ namespace Renderer { LOG(LogInfo) << "Creating surface..."; - if(SDL_Init(SDL_INIT_VIDEO) != 0) + if (SDL_Init(SDL_INIT_VIDEO) != 0) { LOG(LogError) << "Error initializing SDL!\n " << SDL_GetError(); return false; @@ -62,25 +62,27 @@ namespace Renderer SDL_DisplayMode dispMode; SDL_GetDesktopDisplayMode(0, &dispMode); - windowWidth = Settings::getInstance()->getInt("WindowWidth") ? Settings::getInstance()->getInt("WindowWidth") : dispMode.w; - windowHeight = Settings::getInstance()->getInt("WindowHeight") ? Settings::getInstance()->getInt("WindowHeight") : dispMode.h; - screenWidth = Settings::getInstance()->getInt("ScreenWidth") ? Settings::getInstance()->getInt("ScreenWidth") : windowWidth; - screenHeight = Settings::getInstance()->getInt("ScreenHeight") ? Settings::getInstance()->getInt("ScreenHeight") : windowHeight; + + windowWidth = Settings::getInstance()->getInt("WindowWidth") ? Settings::getInstance()->getInt("WindowWidth") : dispMode.w; + windowHeight = Settings::getInstance()->getInt("WindowHeight") ? Settings::getInstance()->getInt("WindowHeight") : dispMode.h; + screenWidth = Settings::getInstance()->getInt("ScreenWidth") ? Settings::getInstance()->getInt("ScreenWidth") : windowWidth; + screenHeight = Settings::getInstance()->getInt("ScreenHeight") ? Settings::getInstance()->getInt("ScreenHeight") : windowHeight; screenOffsetX = Settings::getInstance()->getInt("ScreenOffsetX") ? Settings::getInstance()->getInt("ScreenOffsetX") : 0; screenOffsetY = Settings::getInstance()->getInt("ScreenOffsetY") ? Settings::getInstance()->getInt("ScreenOffsetY") : 0; - screenRotate = Settings::getInstance()->getInt("ScreenRotate") ? Settings::getInstance()->getInt("ScreenRotate") : 0; - - sdlWindow = SDL_CreateWindow("EmulationStation", - SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, - windowWidth, windowHeight, + screenRotate = Settings::getInstance()->getInt("ScreenRotate") ? Settings::getInstance()->getInt("ScreenRotate") : 0; + + sdlWindow = SDL_CreateWindow("EmulationStation", + SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + windowWidth, windowHeight, SDL_WINDOW_OPENGL | (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN))); - - if(sdlWindow == NULL) + + if (sdlWindow == NULL) { LOG(LogError) << "Error creating SDL window!\n\t" << SDL_GetError(); return false; } - + LOG(LogInfo) << "Created window successfully."; //support screen rotation @@ -125,7 +127,7 @@ namespace Renderer sdlContext = SDL_GL_CreateContext(sdlWindow); // vsync - if(Settings::getInstance()->getBool("VSync")) + // if(Settings::getInstance()->getBool("VSync")) { // SDL_GL_SetSwapInterval(0) for immediate updates (no vsync, default), // 1 for updates synchronized with the vertical retrace, @@ -133,11 +135,11 @@ namespace Renderer // SDL_GL_SetSwapInterval returns 0 on success, -1 on error. // if vsync is requested, try normal vsync; if that doesn't work, try late swap tearing // if that doesn't work, report an error - if(SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) + if (SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; } - else - SDL_GL_SetSwapInterval(0); + // else + // SDL_GL_SetSwapInterval(0); return true; } @@ -215,7 +217,7 @@ namespace Renderer } void swapBuffers() - { + { SDL_GL_SwapWindow(sdlWindow); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } diff --git a/es-core/src/Scripting.cpp b/es-core/src/Scripting.cpp index 3038db0a40..421d4fdb7e 100644 --- a/es-core/src/Scripting.cpp +++ b/es-core/src/Scripting.cpp @@ -28,7 +28,7 @@ namespace Scripting // append folder to path std::string script = *it + " \"" + arg1 + "\" \"" + arg2 + "\""; LOG(LogDebug) << " executing: " << script; - runSystemCommand(script); + runSystemCommand(script, "", NULL); } } } diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 13c09a84a1..a63c75e9d3 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -61,7 +61,7 @@ void Settings::setDefaults() mBoolMap["ShowHiddenFiles"] = false; mBoolMap["DrawFramerate"] = false; mBoolMap["ShowExit"] = true; - mBoolMap["FullscreenBorderless"] = false; + mBoolMap["FullscreenBorderless"] = true; mBoolMap["Windowed"] = false; mBoolMap["SplashScreen"] = true; mBoolMap["SplashScreenProgress"] = true; @@ -91,8 +91,11 @@ void Settings::setDefaults() #else mIntMap["MaxVRAM"] = 100; #endif - - mStringMap["TransitionStyle"] = "fade"; + + mBoolMap["HideWindow"] = true; + mStringMap["GameTransitionStyle"] = "fade"; + mStringMap["TransitionStyle"] = "slide"; + mStringMap["Language"] = "en"; mStringMap["ThemeSet"] = ""; mStringMap["ScreenSaverBehavior"] = "dim"; mStringMap["Scraper"] = "TheGamesDB"; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index cf2548089f..52f75760cc 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -9,7 +9,7 @@ #include #include -std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" }, { "grid" }, { "video" } }; +std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" } }; std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; std::map> ThemeData::sElementMap { @@ -40,7 +40,8 @@ std::map> The { "backgroundCornerSize", NORMALIZED_PAIR }, { "backgroundColor", COLOR }, { "backgroundCenterColor", COLOR }, - { "backgroundEdgeColor", COLOR } } }, + { "backgroundEdgeColor", COLOR }, + { "imageSizeMode", STRING } } }, { "text", { { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, @@ -202,7 +203,7 @@ ThemeData::ThemeData() mVersion = 0; } -void ThemeData::loadFile(std::map sysDataMap, const std::string& path) +void ThemeData::loadFile(std::string system, const std::string& path) { mPaths.push_back(path); @@ -214,9 +215,12 @@ void ThemeData::loadFile(std::map sysDataMap, const st mVersion = 0; mViews.clear(); - mVariables.clear(); - mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); + mSystemThemeFolder = system; + + mVariables.clear(); + //mVariables.insert(system, system); + //mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); pugi::xml_document doc; pugi::xml_parse_result res = doc.load_file(path.c_str()); @@ -232,6 +236,9 @@ void ThemeData::loadFile(std::map sysDataMap, const st if(mVersion == -404) throw error << " tag missing!\n It's either out of date or you need to add " << CURRENT_THEME_FORMAT_VERSION << " inside your tag."; + if (root.attribute("defaultView")) + mDefaultView = root.attribute("defaultView").as_string(); + if(mVersion < MINIMUM_THEME_FORMAT_VERSION) throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; @@ -241,6 +248,22 @@ void ThemeData::loadFile(std::map sysDataMap, const st parseFeatures(root); } +std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) +{ + std::string result = path; + + size_t start_pos = result.find("$system"); + if (start_pos == std::string::npos) + return path; + + result.replace(start_pos, 7, systemThemeFolder); + + //result.replace("$system", systemThemeFolder); + //boost::algorithm::replace_first(result, "$system", systemThemeFolder); + return result; +} + + void ThemeData::parseIncludes(const pugi::xml_node& root) { ThemeException error; @@ -250,8 +273,11 @@ void ThemeData::parseIncludes(const pugi::xml_node& root) { std::string relPath = resolvePlaceholders(node.text().as_string()); std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); - if(!ResourceManager::getInstance()->fileExists(path)) - throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + path = resolveSystemVariable(mSystemThemeFolder, path); + + if (!ResourceManager::getInstance()->fileExists(path)) + continue; + //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; error << " from included file \"" << relPath << "\":\n "; @@ -340,6 +366,12 @@ void ThemeData::parseViews(const pugi::xml_node& root) { ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; parseView(node, view); + + if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) + { + ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; + parseView(node, view); + } } } } @@ -390,10 +422,11 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapsecond) { @@ -492,8 +525,8 @@ const std::shared_ptr& ThemeData::getDefault() { try { - std::map emptyMap; - theme->loadFile(emptyMap, path); + //std::map emptyMap; + theme->loadFile("", path); // emptyMap } catch(ThemeException& e) { LOG(LogError) << e.what(); diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 4fc9faa747..6bbcc01555 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -135,7 +135,7 @@ class ThemeData ThemeData(); // throws ThemeException - void loadFile(std::map sysDataMap, const std::string& path); + void loadFile(std::string system, const std::string& path); enum ElementPropertyType { @@ -159,6 +159,9 @@ class ThemeData static std::map getThemeSets(); static std::string getThemeFromCurrentSet(const std::string& system); + std::string getDefaultView() { return mDefaultView; }; + + private: static std::map< std::string, std::map > sElementMap; static std::vector sSupportedFeatures; @@ -167,6 +170,8 @@ class ThemeData std::deque mPaths; float mVersion; + std::string mDefaultView; + void parseFeatures(const pugi::xml_node& themeRoot); void parseIncludes(const pugi::xml_node& themeRoot); void parseVariables(const pugi::xml_node& root); @@ -174,7 +179,10 @@ class ThemeData void parseView(const pugi::xml_node& viewNode, ThemeView& view); void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element); + std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); + std::map mViews; + std::string mSystemThemeFolder; }; #endif // ES_CORE_THEME_DATA_H diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 0f33e5e240..05337b0e01 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -76,7 +76,6 @@ bool Window::init() } InputManager::getInstance()->init(); - ResourceManager::getInstance()->reloadAll(); //keep a reference to the default fonts, so they don't keep getting destroyed/recreated @@ -91,7 +90,7 @@ bool Window::init() mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); // update our help because font sizes probably changed - if(peekGui()) + if (peekGui()) peekGui()->updateHelpPrompts(); return true; @@ -299,15 +298,15 @@ void Window::setAllowSleep(bool sleep) } void Window::renderLoadingScreen(std::string text) -{ +{ Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); - + ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.6f, 0.0f); + splash.setResize(Renderer::getScreenWidth() * 0.5f, 0.0f); splash.setImage(":/splash.svg"); - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.6f); + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); splash.render(trans); auto& font = mDefaultFonts.at(1); @@ -323,6 +322,36 @@ void Window::renderLoadingScreen(std::string text) Renderer::swapBuffers(); } +void Window::renderBlackScreen(std::string text) +{ + Transform4x4f trans = Transform4x4f::Identity(); + Renderer::setMatrix(trans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); + /* + ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.5f, 0.0f); + splash.setImage(":/splash.svg"); + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.render(trans); + */ + + if (text.length() > 0) + { + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); + + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.835f); + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; + } + + Renderer::swapBuffers(); +} + + void Window::renderHelpPromptsEarly() { mHelp->render(Transform4x4f::Identity()); diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 3f104c648e..5fd238dc62 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -65,6 +65,7 @@ class Window void setAllowSleep(bool sleep); void renderLoadingScreen(std::string text); + void renderBlackScreen(std::string text); void renderHelpPromptsEarly(); // used to render HelpPrompts before a fade void setHelpPrompts(const std::vector& prompts, const HelpStyle& style); diff --git a/es-core/src/components/ComponentGrid.cpp b/es-core/src/components/ComponentGrid.cpp index 066f9a3f0c..e89e7f9c4e 100644 --- a/es-core/src/components/ComponentGrid.cpp +++ b/es-core/src/components/ComponentGrid.cpp @@ -445,11 +445,11 @@ std::vector ComponentGrid::getHelpPrompts() } if(canScrollHoriz && canScrollVert) - prompts.push_back(HelpPrompt("up/down/left/right", "choose")); + prompts.push_back(HelpPrompt("up/down/left/right", _T("CHOOSE"))); else if(canScrollHoriz) - prompts.push_back(HelpPrompt("left/right", "choose")); + prompts.push_back(HelpPrompt("left/right", _T("CHOOSE"))); else if(canScrollVert) - prompts.push_back(HelpPrompt("up/down", "choose")); + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); return prompts; } diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index c2c42d8f39..f8f1bcd518 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -328,7 +328,7 @@ std::vector ComponentList::getHelpPrompts() } if(addMovePrompt) - prompts.push_back(HelpPrompt("up/down", "choose")); + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); } return prompts; diff --git a/es-core/src/components/DateTimeComponent.cpp b/es-core/src/components/DateTimeComponent.cpp index 0052f11ad3..5fcfd2ba73 100644 --- a/es-core/src/components/DateTimeComponent.cpp +++ b/es-core/src/components/DateTimeComponent.cpp @@ -51,7 +51,7 @@ std::string DateTimeComponent::getDisplayString() const if (mDisplayRelative) { //relative time if(mTime.getTime() == 0) - return "never"; + return _T("never"); Utils::Time::DateTime now(Utils::Time::now()); Utils::Time::Duration dur(now.getTime() - mTime.getTime()); @@ -71,7 +71,7 @@ std::string DateTimeComponent::getDisplayString() const } if(mTime.getTime() == 0) - return "unknown"; + return _T("unknown"); return Utils::Time::timeToString(mTime.getTime(), mFormat); } diff --git a/es-core/src/components/DateTimeEditComponent.cpp b/es-core/src/components/DateTimeEditComponent.cpp index d4b6636a39..6676641af5 100644 --- a/es-core/src/components/DateTimeEditComponent.cpp +++ b/es-core/src/components/DateTimeEditComponent.cpp @@ -207,7 +207,7 @@ std::string DateTimeEditComponent::getDisplayString(DisplayMode mode) const { //relative time if(mTime.getTime() == 0) - return "never"; + return _T("never"); Utils::Time::DateTime now(Utils::Time::now()); Utils::Time::Duration dur(now.getTime() - mTime.getTime()); @@ -229,7 +229,7 @@ std::string DateTimeEditComponent::getDisplayString(DisplayMode mode) const } if(mTime.getTime() == 0) - return "unknown"; + return _T("unknown"); return Utils::Time::timeToString(mTime, fmt); } diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 745fede1a3..310830f4fb 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -4,11 +4,18 @@ #include "ThemeData.h" #include "Renderer.h" -GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window) +#include + +#include "animations/LambdaAnimation.h" + +GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window), mLabel(window) { + mSelectedZoomPercent = 1.0f; + mAnimPosition = Vector3f(0, 0); + mDefaultProperties.mSize = getDefaultTileSize(); mDefaultProperties.mPadding = Vector2f(16.0f, 16.0f); - mDefaultProperties.mImageColor = 0xAAAAAABB; + mDefaultProperties.mImageColor = 0xFFFFFFDD; // 0xAAAAAABB; mDefaultProperties.mBackgroundImage = ":/frame.png"; mDefaultProperties.mBackgroundCornerSize = Vector2f(16 ,16); mDefaultProperties.mBackgroundCenterColor = 0xAAAAEEFF; @@ -22,41 +29,126 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mSelectedProperties.mBackgroundCenterColor = 0xFFFFFFFF; mSelectedProperties.mBackgroundEdgeColor = 0xFFFFFFFF; + mLabelSize = Vector2f(1.0, 0.30); + mImage = std::make_shared(mWindow); + mImage->setOrigin(0.5f, 0.5f); + + //mBackground.setOrigin(0.5f, 0.5f); - mBackground.setOrigin(0.5f, 0.5f); + //mLabel.setOrigin(0.5f, 0.5f); + //mLabel.setOrigin(0.5f, 0.0f); + //mLabel.setSize(1.0f, 1.0f); + addChild(&mBackground); addChild(&(*mImage)); + addChild(&mLabel); setSelected(false); setVisible(true); } +void GridTileComponent::resize() +{ + const GridTileProperties& currentProperties = getCurrentProperties(); + + setSize(currentProperties.mSize); + + float height = currentProperties.mSize.y() * mLabelSize.y(); + if (!mLabelVisible) + height = 0; + + mLabel.setPosition(0, mSize.y() - height); + mLabel.setSize(currentProperties.mSize.x(), height); + + if (mLabelSize.x() == 0) + height = 0; + + float topPadding = currentProperties.mPadding.y(); + float bottomPadding = std::max(topPadding, height); + float paddingX = currentProperties.mPadding.x(); + + if (mSelectedZoomPercent != 1.0f) + { + paddingX = mDefaultProperties.mPadding.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.x() * mSelectedZoomPercent; + topPadding = mDefaultProperties.mPadding.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.y() * mSelectedZoomPercent; + + if (mAnimPosition.x() != 0 && mAnimPosition.y() != 0) + { + int x = mAnimPosition.x() - mPosition.x(); + int y = mAnimPosition.y() - mPosition.y(); + + x = x * (1.0 - mSelectedZoomPercent); + y = y * (1.0 - mSelectedZoomPercent); + + mBackground.setPosition(x, y); + } + else + mBackground.setPosition(0, 0); + } + else + mBackground.setPosition(0, 0); + + if (mImage != NULL) + { + mImage->setOrigin(0.5f, 0.5f); + mImage->setPosition(currentProperties.mSize.x() / 2.0f, (currentProperties.mSize.y() - height) / 2.0f); + + if (currentProperties.mImageSizeMode == "minSize") + mImage->setMinSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + else if (currentProperties.mImageSizeMode == "size") + mImage->setSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + else + mImage->setMaxSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + } + + + + + mBackground.setSize(currentProperties.mSize); + mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); +} + void GridTileComponent::render(const Transform4x4f& parentTrans) { + if (!mVisible) + return; + Transform4x4f trans = getTransform() * parentTrans; - if (mVisible) - renderChildren(trans); + //Renderer::setMatrix(trans); + //Renderer::drawRect(0.f, 0.f, mSize.x(), mSize.y(), 0xFF0000FF); + + mBackground.render(trans); + + if (mImage != NULL) + mImage->render(trans); + + if (mLabelVisible) + mLabel.render(trans); } // Update all the tile properties to the new status (selected or default) -void GridTileComponent::update() +void GridTileComponent::update(int deltaTime) { + GuiComponent::update(deltaTime); + const GridTileProperties& currentProperties = getCurrentProperties(); mBackground.setImagePath(currentProperties.mBackgroundImage); - mImage->setColorShift(currentProperties.mImageColor); + if (mImage != NULL) + mImage->setColorShift(currentProperties.mImageColor); + mBackground.setCenterColor(currentProperties.mBackgroundCenterColor); mBackground.setEdgeColor(currentProperties.mBackgroundEdgeColor); resize(); } -void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& /*element*/, unsigned int /*properties*/) +void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); @@ -90,6 +182,9 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem->has("backgroundEdgeColor")) mDefaultProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); + + if (elem && elem->has("imageSizeMode")) + mDefaultProperties.mImageSizeMode = elem->get("imageSizeMode"); } // Apply theme to the selected gridtile @@ -99,7 +194,8 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mSelectedProperties.mSize = elem && elem->has("size") ? elem->get("size") * screen : - getSelectedTileSize(); + mDefaultProperties.mSize; + //getSelectedTileSize(); mSelectedProperties.mPadding = elem && elem->has("padding") ? elem->get("padding") : @@ -127,6 +223,22 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem && elem->has("backgroundEdgeColor")) mSelectedProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); + + mSelectedProperties.mImageSizeMode = elem && elem->has("imageSizeMode") ? + elem->get("imageSizeMode") : + mDefaultProperties.mImageSizeMode; + + elem = theme->getElement(view, element, "text"); + if (elem != NULL) + { + if (elem && elem->has("size")) + mLabelSize = elem->get("size"); + + mLabelVisible = true; + mLabel.applyTheme(theme, view, element, properties); + } + else + mLabelVisible = false; } // Made this a static function because the ImageGridComponent need to know the default tile size @@ -148,39 +260,67 @@ bool GridTileComponent::isSelected() const return mSelected; } -void GridTileComponent::setImage(const std::string& path) +void GridTileComponent::setImage(const std::string& path, std::string name) { - mImage->setImage(path); - - // Resize now to prevent flickering images when scrolling + if (mCurrentPath != path) + { + mCurrentPath = path; + mImage->setImage(path); + } + + mLabel.setText(name); resize(); } -void GridTileComponent::setImage(const std::shared_ptr& texture) +void GridTileComponent::setImage(const std::shared_ptr& texture, std::string name) { mImage->setImage(texture); - + mLabel.setText(name); + // Resize now to prevent flickering images when scrolling resize(); } -void GridTileComponent::setSelected(bool selected) +void GridTileComponent::setSelected(bool selected, Vector3f* pPosition) { mSelected = selected; + + if (selected && pPosition != NULL) + { + auto funeec = [this](float t) + { + this->setSelectedZoom(t); + }; + } + + if (selected && (pPosition != NULL || mSelectedProperties.mPadding.x() != mDefaultProperties.mPadding.y())) + { + if (pPosition == NULL) + mAnimPosition = Vector3f(0, 0, 0); + else + mAnimPosition = Vector3f(pPosition->x(), pPosition->y(), pPosition->z()); + + auto func = [this](float t) + { + this->setSelectedZoom(t); + }; + + setAnimation(new LambdaAnimation(func, 100), 0, [this] { + this->setSelectedZoom(1); + mAnimPosition = Vector3f(0, 0, 0); + }, false, 3); + } } -void GridTileComponent::setVisible(bool visible) +void GridTileComponent::setSelectedZoom(float percent) { - mVisible = visible; + mSelectedZoomPercent = percent; + resize(); } -void GridTileComponent::resize() +void GridTileComponent::setVisible(bool visible) { - const GridTileProperties& currentProperties = getCurrentProperties(); - - mImage->setMaxSize(currentProperties.mSize - currentProperties.mPadding * 2); - mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); - mBackground.fitTo(currentProperties.mSize - mBackground.getCornerSize() * 2); + mVisible = visible; } const GridTileProperties& GridTileComponent::getCurrentProperties() const diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index c191ce94ab..bdaa611a16 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -4,6 +4,7 @@ #include "NinePatchComponent.h" #include "ImageComponent.h" +#include "TextComponent.h" struct GridTileProperties { @@ -14,6 +15,8 @@ struct GridTileProperties Vector2f mBackgroundCornerSize; unsigned int mBackgroundCenterColor; unsigned int mBackgroundEdgeColor; + + std::string mImageSizeMode; }; class GridTileComponent : public GuiComponent @@ -22,7 +25,8 @@ class GridTileComponent : public GuiComponent GridTileComponent(Window* window); void render(const Transform4x4f& parentTrans) override; - void update(); + void update(int deltaTime) override; + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); // Made this a static function because the ImageGridComponent need to know the default tile max size @@ -31,9 +35,9 @@ class GridTileComponent : public GuiComponent Vector2f getSelectedTileSize() const; bool isSelected() const; - void setImage(const std::string& path); - void setImage(const std::shared_ptr& texture); - void setSelected(bool selected); + void setImage(const std::string& path, std::string name); + void setImage(const std::shared_ptr& texture, std::string name); + void setSelected(bool selected, Vector3f* pPosition = NULL); void setVisible(bool visible); private: @@ -41,13 +45,25 @@ class GridTileComponent : public GuiComponent const GridTileProperties& getCurrentProperties() const; std::shared_ptr mImage; + TextComponent mLabel; + Vector2f mLabelSize; + + bool mLabelVisible; + NinePatchComponent mBackground; GridTileProperties mDefaultProperties; GridTileProperties mSelectedProperties; + std::string mCurrentPath; + + void setSelectedZoom(float percent); + + float mSelectedZoomPercent; bool mSelected; bool mVisible; + + Vector3f mAnimPosition; }; #endif // ES_CORE_COMPONENTS_GRID_TILE_COMPONENT_H diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 14f5fac134..8358ee6839 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -133,23 +133,22 @@ void ImageComponent::setDefaultImage(std::string path) void ImageComponent::setImage(std::string path, bool tile) { - if(path.empty() || !ResourceManager::getInstance()->fileExists(path)) + if (path.empty() || !ResourceManager::getInstance()->fileExists(path)) { - if(mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) + if (mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) mTexture.reset(); else mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic); - } else { + } + else mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic); - } - + resize(); } void ImageComponent::setImage(const char* path, size_t length, bool tile) { mTexture.reset(); - mTexture = TextureResource::get("", tile); mTexture->initFromMemory(path, length); @@ -263,7 +262,7 @@ void ImageComponent::setOpacity(unsigned char opacity) void ImageComponent::updateVertices() { - if(!mTexture || !mTexture->isInitialized()) + if (!mTexture || !mTexture->isInitialized()) return; // we go through this mess to make sure everything is properly rounded @@ -274,6 +273,7 @@ void ImageComponent::updateVertices() mVertices[0].pos = Vector2f(topLeft.x(), topLeft.y()); mVertices[1].pos = Vector2f(topLeft.x(), bottomRight.y()); + mVertices[2].pos = Vector2f(bottomRight.x(), topLeft.y()); mVertices[3].pos = Vector2f(bottomRight.x(), topLeft.y()); @@ -281,11 +281,13 @@ void ImageComponent::updateVertices() mVertices[5].pos = Vector2f(bottomRight.x(), bottomRight.y()); float px, py; - if(mTexture->isTiled()) + if (mTexture->isTiled()) { px = mSize.x() / getTextureSize().x(); py = mSize.y() / getTextureSize().y(); - }else{ + } + else + { px = 1; py = 1; } @@ -298,12 +300,13 @@ void ImageComponent::updateVertices() mVertices[4].tex = Vector2f(mTopLeftCrop.x(), 1 - mBottomRightCrop.y()); mVertices[5].tex = Vector2f(px * mBottomRightCrop.x(), 1 - mBottomRightCrop.y()); - if(mFlipX) + if (mFlipX) { for(int i = 0; i < 6; i++) mVertices[i].tex[0] = px - mVertices[i].tex[0]; } - if(mFlipY) + + if (mFlipY) { for(int i = 0; i < 6; i++) mVertices[i].tex[1] = py - mVertices[i].tex[1]; @@ -320,14 +323,16 @@ void ImageComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); Renderer::setMatrix(trans); - if(mTexture && mOpacity > 0) + if (mTexture && mOpacity > 0) { - if(Settings::getInstance()->getBool("DebugImage")) { + if (Settings::getInstance()->getBool("DebugImage")) + { Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; Renderer::drawRect(targetSizePos.x(), targetSizePos.y(), mTargetSize.x(), mTargetSize.y(), 0xFF000033); Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); } - if(mTexture->isInitialized()) + + if (mTexture->isInitialized()) { // actually draw the image // The bind() function returns false if the texture is not currently loaded. A blank @@ -355,7 +360,9 @@ void ImageComponent::render(const Transform4x4f& parentTrans) glDisable(GL_TEXTURE_2D); glDisable(GL_BLEND); - }else{ + } + else + { LOG(LogError) << "Image texture is not initialized!"; mTexture.reset(); } @@ -471,6 +478,6 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s std::vector ImageComponent::getHelpPrompts() { std::vector ret; - ret.push_back(HelpPrompt("a", "select")); + ret.push_back(HelpPrompt("a", "SELECTIONNER")); return ret; } diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 65b329adcb..9dc51138dd 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -50,6 +50,8 @@ class ImageGridComponent : public IList void onSizeChanged() override; inline void setCursorChangedCallback(const std::function& func) { mCursorChangedCallback = func; } + void setThemeName(std::string name) { mName = name; }; + protected: virtual void onCursorChanged(const CursorState& state) override; @@ -57,13 +59,10 @@ class ImageGridComponent : public IList // TILES void buildTiles(); void updateTiles(); - void updateTileAtPos(int tilePos, int imgPos, int bufferTop, int bufferBot); + void updateTileAtPos(int tilePos, int imgPos); int getStartPosition() const; void calcGridDimension(); - // IMAGES & ENTRIES - const int texBuffersBehind[4] = { 1, 1, 1, 1 }; - const int texBuffersForward[4] = { 1, 2, 3, 3 }; bool mEntriesDirty; int mLastCursor; std::string mDefaultGameTexture; @@ -77,6 +76,10 @@ class ImageGridComponent : public IList std::shared_ptr mTheme; std::vector< std::shared_ptr > mTiles; + std::string mName; + + int mStartPosition; + // MISCELLANEOUS ScrollDirection mScrollDirection; std::function mCursorChangedCallback; @@ -87,6 +90,8 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList void ImageGridComponent::update(int deltaTime) { listUpdate(deltaTime); - + for(auto it = mTiles.begin(); it != mTiles.end(); it++) - (*it)->update(); + (*it)->update(deltaTime); } template @@ -179,16 +184,29 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) // If it's the selected image, keep it for later, otherwise render it now if(tile->isSelected()) selectedTile = tile; - else - tile->render(trans); + // else + // tile->render(trans); } - Renderer::popClipRect(); + // Render the selected image on top of the others if (selectedTile != NULL) selectedTile->render(trans); + for (auto it = mTiles.begin(); it != mTiles.end(); it++) + { + std::shared_ptr tile = (*it); + + // If it's the selected image, keep it for later, otherwise render it now + if (tile->isSelected()) + ; // selectedTile = tile; + else + tile->render(trans); + } + + Renderer::popClipRect(); + listRenderTitleOverlay(trans); GuiComponent::renderChildren(trans); @@ -283,23 +301,55 @@ void ImageGridComponent::onSizeChanged() template void ImageGridComponent::onCursorChanged(const CursorState& state) { - updateTiles(); + if (mLastCursor != mCursor) + { + // 1 if scrolling down, -1 if scrolling up + int scrollDirection = mCursor >= mLastCursor ? 1 : -1; + + int partialRow = (int)mLastRowPartial; + int maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); + + if (mCursor == 0) + mStartPosition = 0; + else if (mCursor < mStartPosition) + mStartPosition = std::max(0, mStartPosition - mGridDimension.x()); + else if (mCursor >= maxPos) + { + mStartPosition = mStartPosition + mGridDimension.x(); + + maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); + if (mCursor >= maxPos) + { + int cursorRow = mCursor / mGridDimension.x(); + mStartPosition = cursorRow * mGridDimension.x(); + if (mStartPosition >= mGridDimension.x() && cursorRow > 0) + { + cursorRow--; + mStartPosition = cursorRow * mGridDimension.x(); + } + } + } - if(mCursorChangedCallback) - mCursorChangedCallback(state); + updateTiles(); + + if (mCursorChangedCallback) + mCursorChangedCallback(state); + } + + mLastCursor = mCursor; } // Create and position tiles (mTiles) template void ImageGridComponent::buildTiles() { + mStartPosition = 0; mTiles.clear(); calcGridDimension(); Vector2f tileDistance = mTileSize + mMargin; - Vector2f bufferSize = Vector2f(mScrollDirection == SCROLL_HORIZONTALLY ? tileDistance.x() * texBuffersForward[3] : 0, - mScrollDirection == SCROLL_VERTICALLY ? tileDistance.y() * texBuffersForward[3] : 0); + Vector2f bufferSize = Vector2f(0, 0); // mScrollDirection == SCROLL_HORIZONTALLY ? tileDistance.x() * texBuffersForward[3] : 0, mScrollDirection == SCROLL_VERTICALLY ? tileDistance.y() * texBuffersForward[3] : 0); Vector2f startPosition = mTileSize / 2 - bufferSize; int X, Y; @@ -319,10 +369,10 @@ void ImageGridComponent::buildTiles() tile->setPosition(X * tileDistance.x() + startPosition.x(), Y * tileDistance.y() + startPosition.y()); tile->setOrigin(0.5f, 0.5f); - tile->setImage(""); + tile->setImage("", ""); if (mTheme) - tile->applyTheme(mTheme, "grid", "gridtile", ThemeFlags::ALL); + tile->applyTheme(mTheme, mName, "gridtile", ThemeFlags::ALL); mTiles.push_back(tile); } @@ -334,84 +384,73 @@ void ImageGridComponent::updateTiles() { if (!mTiles.size()) return; - + // Stop updating the tiles at highest scroll speed if (mScrollTier == 3) { for (int ti = 0; ti < (int)mTiles.size(); ti++) { std::shared_ptr tile = mTiles.at(ti); - + tile->setSelected(false); - tile->setImage(mDefaultGameTexture); + tile->setImage(mDefaultGameTexture, ""); tile->setVisible(false); } return; } - // 1 if scrolling down, -1 if scrolling up - int scrollDirection = mCursor >= mLastCursor ? 1 : -1; - - // If going down, update from top to bottom - // If going up, update from bottom to top - int ti = scrollDirection == 1 ? 0 : (int)mTiles.size() - 1; - int end = scrollDirection == 1 ? (int)mTiles.size() : -1; - + int i = 0; + int end = (int)mTiles.size(); int img = getStartPosition(); - if (scrollDirection == -1) - img += (int)mTiles.size() - 1; - - // Calculate buffer size depending on scroll speed and direction - int bufferBehind = (texBuffersForward[3] - texBuffersBehind[mScrollTier]) * mGridDimension.x(); - int bufferForward = (texBuffersForward[3] - texBuffersForward[mScrollTier]) * mGridDimension.x(); - - int bufferTop = scrollDirection == 1 ? bufferBehind : bufferForward; - int bufferBot = scrollDirection == 1 ? bufferForward : bufferBehind; - // Update the tiles - while (ti != end) + while (i != end) { - updateTileAtPos(ti, img, bufferTop, bufferBot); - - ti += scrollDirection; - img += scrollDirection; + updateTileAtPos(i, img); + i++; img++; } mLastCursor = mCursor; } template -void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, int bufferTop, int bufferBot) +void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos) { std::shared_ptr tile = mTiles.at(tilePos); // If we have more tiles than we have to display images on screen, hide them - if(imgPos < 0 || imgPos >= size() - || tilePos < bufferTop || tilePos >= (int)mTiles.size() - bufferBot) // Same for tiles out of the buffer + if(imgPos < 0 || imgPos >= size() || tilePos < 0 || tilePos >= (int) mTiles.size()) // Same for tiles out of the buffer { tile->setSelected(false); - tile->setImage(""); + tile->setImage("", ""); tile->setVisible(false); } else { - tile->setSelected(imgPos == mCursor); + + if (imgPos == mCursor && mCursor != mLastCursor) + { + int dif = mCursor - tilePos; + int idx = mLastCursor - dif; + + if (idx < 0 || idx >= mTiles.size()) + idx = 0; + + tile->setSelected(true, &mTiles.at(idx)->getPosition()); + } + else + tile->setSelected(imgPos == mCursor); + tile->setVisible(true); + std::string name = mEntries.at(imgPos).name; // .object->getName(); std::string imagePath = mEntries.at(imgPos).data.texturePath; + if (ResourceManager::getInstance()->fileExists(imagePath)) - { - tile->setImage(imagePath); - } + tile->setImage(imagePath, name); + else if (mEntries.at(imgPos).object->getType() == 2) + tile->setImage(mDefaultFolderTexture, name); else - { - // FileType::FOLDER = 2, but FileData is our template parameter T, - // so we don't want to bring that dependence to FileData here - if (mEntries.at(imgPos).object->getType() == 2) - tile->setImage(mDefaultFolderTexture); - else - tile->setImage(mDefaultGameTexture); - } + tile->setImage(mDefaultGameTexture, name); } } @@ -419,32 +458,29 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, int bufferT template int ImageGridComponent::getStartPosition() const { + return mStartPosition; + // The "partialRow" variable exist because we want to keep the same positioning behavior in both // case, whenever we have an integer number of rows or not (the last partial row is ignored when // calculating position and the cursor shouldn't end up in this row when close to the end) int partialRow = (int)mLastRowPartial; int cursorRow = mCursor / mGridDimension.x(); + int cursorCol = mCursor % mGridDimension.x(); int start = (cursorRow - ((mGridDimension.y() - partialRow) / 2)) * mGridDimension.x(); - - // Number of tiles which are just used as a buffer for texture loading - int bufferSize = texBuffersForward[3] * mGridDimension.x(); - - if(start + (mGridDimension.x() * (mGridDimension.y() - partialRow)) >= (int)mEntries.size() + bufferSize) + if (start + (mGridDimension.x() * (mGridDimension.y() - partialRow)) >= (int)mEntries.size()) { // If we are at the end put the row as close as we can and no higher, using the following formula // Where E is the nb of entries, X the grid x dim (nb of column), Y the grid y dim (nb of line) // start = first tile of last row - nb column * (nb line - 1) // = (E - 1) / X * X - X * (Y - 1) // = X * ((E - 1) / X - Y + 1) - start = mGridDimension.x() * (((int)mEntries.size() - 1) / mGridDimension.x() - mGridDimension.y() + 1 + partialRow) + bufferSize; + start = mGridDimension.x() * (((int)mEntries.size() - 1) / mGridDimension.x() - mGridDimension.y() + 1 + partialRow); } - if(start < -bufferSize) - { - start = -bufferSize; - } + if (start < 0) + return 0; return start; } @@ -464,7 +500,7 @@ void ImageGridComponent::calcGridDimension() // Invert dimensions for horizontally scrolling grid if (mScrollDirection == SCROLL_HORIZONTALLY) - mGridDimension = Vector2i(mGridDimension.y(), mGridDimension.x()); + mGridDimension = Vector2i((float) (int)gridDimension.y(), mGridDimension.x()); // Grid dimension validation if (mGridDimension.x() < 1) @@ -473,7 +509,7 @@ void ImageGridComponent::calcGridDimension() LOG(LogError) << "Theme defined grid Y dimension below 1"; // Add extra tiles to both side depending on max texture buffer - mGridDimension.y() += texBuffersForward[3] * 2; +// mGridDimension.y() += texBuffersForward[3] * 2; }; diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 2e60b59459..343c86f618 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -7,7 +7,7 @@ #define TITLE_HEIGHT (mTitle->getFont()->getLetterHeight() + TITLE_VERT_PADDING) -MenuComponent::MenuComponent(Window* window, const char* title, const std::shared_ptr& titleFont) : GuiComponent(window), +MenuComponent::MenuComponent(Window* window, std::string title, const std::shared_ptr& titleFont) : GuiComponent(window), mBackground(window), mGrid(window, Vector2i(1, 3)) { addChild(&mBackground); @@ -19,7 +19,7 @@ MenuComponent::MenuComponent(Window* window, const char* title, const std::share mTitle = std::make_shared(mWindow); mTitle->setHorizontalAlignment(ALIGN_CENTER); mTitle->setColor(0x555555FF); - setTitle(title, titleFont); + setTitle(title.c_str(), titleFont); mGrid.setEntry(mTitle, Vector2i(0, 0), false); // set up list which will never change (externally, anyway) diff --git a/es-core/src/components/MenuComponent.h b/es-core/src/components/MenuComponent.h index 7babd2d3a5..e2c9ea31f2 100644 --- a/es-core/src/components/MenuComponent.h +++ b/es-core/src/components/MenuComponent.h @@ -20,7 +20,7 @@ std::shared_ptr makeArrow(Window* window); class MenuComponent : public GuiComponent { public: - MenuComponent(Window* window, const char* title, const std::shared_ptr& titleFont = Font::get(FONT_SIZE_LARGE)); + MenuComponent(Window* window, std::string title, const std::shared_ptr& titleFont = Font::get(FONT_SIZE_LARGE)); void onSizeChanged() override; diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index bf922acdcb..613a22c026 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -87,7 +87,7 @@ class OptionListComponent : public GuiComponent mMenu.addRow(row, (!mParent->mMultiSelect && it->selected)); } - mMenu.addButton("BACK", "accept", [this] { delete this; }); + mMenu.addButton(_T("BACK"), "accept", [this] { delete this; }); if(mParent->mMultiSelect) { @@ -128,7 +128,7 @@ class OptionListComponent : public GuiComponent std::vector getHelpPrompts() override { auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); return prompts; } }; @@ -205,6 +205,9 @@ class OptionListComponent : public GuiComponent }else if(config->isMappedLike("right", input)) { + if (mEntries.size() == 0) + return true; + // move selection to next unsigned int i = getSelectedId(); int next = (i + 1) % mEntries.size(); @@ -268,6 +271,31 @@ class OptionListComponent : public GuiComponent onSelectedChanged(); } + void selectFirstItem() + { + for (unsigned int i = 0; i < mEntries.size(); i++) + mEntries.at(i).selected = false; + + if (mEntries.size() > 0) + mEntries.at(0).selected = true; + + onSelectedChanged(); + } + + void clear() { + mEntries.clear(); + } + + inline void invalidate() { + onSelectedChanged(); + } + + void setSelectedChangedCallback(const std::function& callback) + { + mSelectedChangedCallback = callback; + } + + private: unsigned int getSelectedId() { @@ -287,6 +315,7 @@ class OptionListComponent : public GuiComponent mWindow->pushGui(new OptionListPopup(mWindow, this, mName)); } + void onSelectedChanged() { if(mMultiSelect) @@ -303,26 +332,29 @@ class OptionListComponent : public GuiComponent // display currently selected + l/r cursors for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) { - if(it->selected) + if (it->selected) { mText.setText(Utils::String::toUpper(it->name)); mText.setSize(0, mText.getSize().y()); setSize(mText.getSize().x() + mLeftArrow.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); - if(mParent) // hack since theres no "on child size changed" callback atm... + if (mParent) // hack since theres no "on child size changed" callback atm... mParent->onSizeChanged(); break; } } } + + if (mSelectedChangedCallback) + mSelectedChangedCallback(mEntries.at(getSelectedId()).object); } std::vector getHelpPrompts() override { std::vector prompts; if(!mMultiSelect) - prompts.push_back(HelpPrompt("left/right", "change")); + prompts.push_back(HelpPrompt("left/right", "MODIFIER")); - prompts.push_back(HelpPrompt("a", "select")); + prompts.push_back(HelpPrompt("a", "SELECTIONNER")); return prompts; } @@ -334,6 +366,7 @@ class OptionListComponent : public GuiComponent ImageComponent mRightArrow; std::vector mEntries; + std::function mSelectedChangedCallback; }; #endif // ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H diff --git a/es-core/src/components/SliderComponent.cpp b/es-core/src/components/SliderComponent.cpp index 619c798356..81f97e9b09 100644 --- a/es-core/src/components/SliderComponent.cpp +++ b/es-core/src/components/SliderComponent.cpp @@ -139,6 +139,6 @@ void SliderComponent::onValueChanged() std::vector SliderComponent::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("left/right", "change")); + prompts.push_back(HelpPrompt("left/right", "MODIFIER")); return prompts; } diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index ec549020bc..747e507fc3 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -72,6 +72,6 @@ void SwitchComponent::onStateChanged() std::vector SwitchComponent::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("a", "change")); + prompts.push_back(HelpPrompt("a", "MODIFIER")); return prompts; } diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 6cd5f082a1..fae35d643c 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -228,7 +228,7 @@ void VideoComponent::applyTheme(const std::shared_ptr& theme, const s std::vector VideoComponent::getHelpPrompts() { std::vector ret; - ret.push_back(HelpPrompt("a", "select")); + ret.push_back(HelpPrompt("a", "SELECTIONNER")); return ret; } diff --git a/es-core/src/guis/GuiInputConfig.cpp b/es-core/src/guis/GuiInputConfig.cpp index 29111a4841..3ec21f101f 100755 --- a/es-core/src/guis/GuiInputConfig.cpp +++ b/es-core/src/guis/GuiInputConfig.cpp @@ -133,8 +133,9 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi return false; } - // apply filtering for quirks related to trigger mapping - if(filterTrigger(input, config, i)) + + // filter for input quirks specific to Sony DualShock 3 + if(filterTrigger(input, config)) return false; // we are configuring @@ -336,26 +337,21 @@ void GuiInputConfig::clearAssignment(int inputId) mTargetConfig->unmapInput(GUI_INPUT_CONFIG_LIST[inputId].name); } -bool GuiInputConfig::filterTrigger(Input input, InputConfig* config, int inputId) +bool GuiInputConfig::filterTrigger(Input input, InputConfig* config) { #if defined(__linux__) // match PlayStation joystick with 6 axes only if((strstr(config->getDeviceName().c_str(), "PLAYSTATION") != NULL \ - || strstr(config->getDeviceName().c_str(), "PS3 Ga") != NULL \ - || strstr(config->getDeviceName().c_str(), "PS(R) Ga") != NULL) \ + || strstr(config->getDeviceName().c_str(), "PS3 Game") != NULL \ + || strstr(config->getDeviceName().c_str(), "PS(R) Game") != NULL) \ && InputManager::getInstance()->getAxisCountByDevice(config->getDeviceId()) == 6) { // digital triggers are unwanted if (input.type == TYPE_BUTTON && (input.id == 6 || input.id == 7)) return true; - } - - // ignore negative pole for axes 2/5 only when triggers are being configured - if((mSkipAxis || strstr(GUI_INPUT_CONFIG_LIST[inputId].name, "Trigger") != NULL) \ - && input.type == TYPE_AXIS && (input.id == 2 || input.id == 5) && input.value < 0) - { - mSkipAxis = true; - return true; + // ignore analog values < 0 + if (input.type == TYPE_AXIS && (input.id == 2 || input.id == 5) && input.value < 0) + return true; } #endif diff --git a/es-core/src/guis/GuiInputConfig.h b/es-core/src/guis/GuiInputConfig.h index d33a8fad7b..bef5f03eab 100644 --- a/es-core/src/guis/GuiInputConfig.h +++ b/es-core/src/guis/GuiInputConfig.h @@ -28,7 +28,7 @@ class GuiInputConfig : public GuiComponent bool assign(Input input, int inputId); void clearAssignment(int inputId); - bool filterTrigger(Input input, InputConfig* config, int inputId); + bool filterTrigger(Input input, InputConfig* config); void rowDone(); @@ -50,7 +50,6 @@ class GuiInputConfig : public GuiComponent Input mHeldInput; int mHeldTime; int mHeldInputId; - bool mSkipAxis; BusyComponent mBusyAnim; }; diff --git a/es-core/src/guis/GuiTextEditPopup.cpp b/es-core/src/guis/GuiTextEditPopup.cpp index 267939430e..de7390cbb8 100644 --- a/es-core/src/guis/GuiTextEditPopup.cpp +++ b/es-core/src/guis/GuiTextEditPopup.cpp @@ -68,6 +68,6 @@ bool GuiTextEditPopup::input(InputConfig* config, Input input) std::vector GuiTextEditPopup::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); return prompts; } diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index 81722a549c..d47f6204a0 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -8,6 +8,9 @@ #endif #include +#include +#include "Window.h" + int runShutdownCommand() { #ifdef WIN32 // windows @@ -26,15 +29,111 @@ int runRestartCommand() #endif } -int runSystemCommand(const std::string& cmd_utf8) +std::string trim(const std::string& str) +{ + size_t first = str.find_first_not_of(' '); + if (std::string::npos == first) + { + return str; + } + size_t last = str.find_last_not_of(' '); + return str.substr(first, (last - first + 1)); +} + +void split_cmd(const std::string& cmd, + std::string* executable, + std::string* parameters) +{ + std::string c(cmd); + size_t exec_end; + + c = trim(c); + //boost::trim_all(c); + + if (c[0] == '\"') + { + exec_end = c.find_first_of('\"', 1); + if (std::string::npos != exec_end) + { + *executable = c.substr(1, exec_end - 1); + *parameters = c.substr(exec_end + 1); + } + else + { + *executable = c.substr(1, exec_end); + std::string().swap(*parameters); + } + } + else + { + exec_end = c.find_first_of(' ', 0); + if (std::string::npos != exec_end) + { + *executable = c.substr(0, exec_end); + *parameters = c.substr(exec_end + 1); + } + else + { + *executable = c.substr(0, exec_end); + std::string().swap(*parameters); + } + } +} + +int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window) { #ifdef WIN32 + if (window != NULL) + window->renderBlackScreen("Chargement en cours..."); + // on Windows we use _wsystem to support non-ASCII paths // which requires converting from utf8 to a wstring typedef std::codecvt_utf8 convert_type; std::wstring_convert converter; std::wstring wchar_str = converter.from_bytes(cmd_utf8); - return _wsystem(wchar_str.c_str()); + + std::string exe; + std::string args; + + split_cmd(cmd_utf8, &exe, &args); + + SHELLEXECUTEINFO lpExecInfo; + lpExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); + lpExecInfo.lpFile = exe.c_str(); + lpExecInfo.fMask = SEE_MASK_DOENVSUBST | SEE_MASK_NOCLOSEPROCESS; + lpExecInfo.hwnd = NULL; + lpExecInfo.lpVerb = "open"; // to open program + lpExecInfo.lpParameters = args.c_str(); // file name as an argument + lpExecInfo.lpDirectory = NULL; + lpExecInfo.nShow = SW_SHOW; // show command prompt with normal window size + lpExecInfo.hInstApp = (HINSTANCE)SE_ERR_DDEFAIL; //WINSHELLAPI BOOL WINAPI result; + ShellExecuteEx(&lpExecInfo); + + if (lpExecInfo.hProcess != NULL) + { + if (window == NULL) + WaitForSingleObject(lpExecInfo.hProcess, INFINITE); + else + { + while (WaitForSingleObject(lpExecInfo.hProcess, 50) == 0x00000102L) + { + bool polled = false; + + SDL_Event event; + while (SDL_PollEvent(&event)) + polled = true; + + if (window != NULL && polled) + window->renderBlackScreen("Chargement en cours..."); + } + } + + CloseHandle(lpExecInfo.hProcess); + return 0; + } + + return 1; + //return _wsystem(wchar_str.c_str()); #else return system(cmd_utf8.c_str()); #endif diff --git a/es-core/src/platform.h b/es-core/src/platform.h index 7ffaf5d31e..398f7c12e6 100644 --- a/es-core/src/platform.h +++ b/es-core/src/platform.h @@ -21,9 +21,11 @@ #define GLHEADER #endif +class Window; + int runShutdownCommand(); // shut down the system (returns 0 if successful) int runRestartCommand(); // restart the system (returns 0 if successful) -int runSystemCommand(const std::string& cmd_utf8); // run a utf-8 encoded in the shell (requires wstring conversion on Windows) +int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window); // run a utf-8 encoded in the shell (requires wstring conversion on Windows) int quitES(const std::string& filename); void touch(const std::string& filename); diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index f0e93e61cc..d5086b7e05 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -78,8 +78,8 @@ Font::Font(int size, const std::string& path) : mSize(size), mPath(path) initLibrary(); // always initialize ASCII characters - for(unsigned int i = 32; i < 128; i++) - getGlyph(i); +// for(unsigned int i = 32; i < 128; i++) + // getGlyph(i); clearFaceCache(); } @@ -247,6 +247,10 @@ std::vector getFallbackFontPaths() for(unsigned int i = 0; i < sizeof(fontNames) / sizeof(fontNames[0]); i++) { std::string path = fontDir + fontNames[i]; + + if (i == 0) + path = ":/fontawesome_webfont.ttf"; + if(ResourceManager::getInstance()->fileExists(path)) fontPaths.push_back(path); } diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index f493a91c46..e2a613a859 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -135,6 +135,7 @@ bool TextureData::load() if (!mPath.empty()) { std::shared_ptr& rm = ResourceManager::getInstance(); + const ResourceData& data = rm->getFileData(mPath); // is it an SVG? if (mPath.substr(mPath.size() - 4, std::string::npos) == ".svg") @@ -163,6 +164,9 @@ bool TextureData::uploadAndBind() if (mTextureID != 0) { glBindTexture(GL_TEXTURE_2D, mTextureID); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } else { @@ -182,12 +186,13 @@ bool TextureData::uploadAndBind() glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)mWidth, (GLsizei)mHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, mDataRGBA); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); const GLint wrapMode = mTile ? GL_REPEAT : GL_CLAMP_TO_EDGE; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode); } + return true; } diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 5a0a351c97..e4af303eb3 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -147,7 +147,12 @@ namespace Utils { // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows char* envHome = getenv("HOME"); - if(envHome) + +#ifdef _DEBUG + envHome = "H:/[Emulz]/EmulationStation/"; +#endif + + if (envHome) path = getGenericPath(envHome); #if defined(_WIN32) @@ -174,10 +179,7 @@ namespace Utils std::string getCWDPath() { char temp[512]; - - // return current working directory path return (getcwd(temp, 512) ? getGenericPath(temp) : ""); - } // getCWDPath std::string getExePath() @@ -185,14 +187,22 @@ namespace Utils static std::string path; // only construct the exepath once - if(!path.length()) + if (!path.length()) { - path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); - - if(isRegularFile(path)) +#if defined(_WIN32) + char buffer[MAX_PATH]; + DWORD size = MAX_PATH; + DWORD result = GetModuleFileNameA(NULL, buffer, size); + if (result) { - path = getParent(path); + std::string ret = buffer; + path = getParent(ret); + return ret; } +#endif + path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); + if (isRegularFile(path)) + path = getParent(path); } // return constructed exepath @@ -644,7 +654,7 @@ namespace Utils #endif // _WIN32 // filenames starting with . are hidden in linux, we do this check for windows as well - if(getFileName(path)[0] == '.') + if (getFileName(path)[0] == '.') return true; // not hidden diff --git a/es-core/src/utils/StringUtil.cpp b/es-core/src/utils/StringUtil.cpp index 8d35f9701a..0e1da37ea8 100644 --- a/es-core/src/utils/StringUtil.cpp +++ b/es-core/src/utils/StringUtil.cpp @@ -1,299 +1,336 @@ -#include "utils/StringUtil.h" - -#include -#include - -namespace Utils -{ - namespace String - { - unsigned int chars2Unicode(const std::string& _string, size_t& _cursor) - { - const char& c = _string[_cursor]; - unsigned int result = '?'; - - if((c & 0x80) == 0) // 0xxxxxxx, one byte character - { - // 0xxxxxxx - result = ((_string[_cursor++] ) ); - } - else if((c & 0xE0) == 0xC0) // 110xxxxx, two byte character - { - // 110xxxxx 10xxxxxx - result = ((_string[_cursor++] & 0x1F) << 6) | - ((_string[_cursor++] & 0x3F) ); - } - else if((c & 0xF0) == 0xE0) // 1110xxxx, three byte character - { - // 1110xxxx 10xxxxxx 10xxxxxx - result = ((_string[_cursor++] & 0x0F) << 12) | - ((_string[_cursor++] & 0x3F) << 6) | - ((_string[_cursor++] & 0x3F) ); - } - else if((c & 0xF8) == 0xF0) // 11110xxx, four byte character - { - // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - result = ((_string[_cursor++] & 0x07) << 18) | - ((_string[_cursor++] & 0x3F) << 12) | - ((_string[_cursor++] & 0x3F) << 6) | - ((_string[_cursor++] & 0x3F) ); - } - else - { - // error, invalid unicode - ++_cursor; - } - - return result; - - } // chars2Unicode - - std::string unicode2Chars(const unsigned int _unicode) - { - std::string result; - - if(_unicode < 0x80) // one byte character - { - result += ((_unicode ) & 0xFF); - } - else if(_unicode < 0x800) // two byte character - { - result += ((_unicode >> 6) & 0xFF) | 0xC0; - result += ((_unicode ) & 0x3F) | 0x80; - } - else if(_unicode < 0xFFFF) // three byte character - { - result += ((_unicode >> 12) & 0xFF) | 0xE0; - result += ((_unicode >> 6) & 0x3F) | 0x80; - result += ((_unicode ) & 0x3F) | 0x80; - } - else if(_unicode <= 0x1fffff) // four byte character - { - result += ((_unicode >> 18) & 0xFF) | 0xF0; - result += ((_unicode >> 12) & 0x3F) | 0x80; - result += ((_unicode >> 6) & 0x3F) | 0x80; - result += ((_unicode ) & 0x3F) | 0x80; - } - else - { - // error, invalid unicode - result += '?'; - } - - return result; - - } // unicode2Chars - - size_t nextCursor(const std::string& _string, const size_t _cursor) - { - size_t result = _cursor; - - while(result < _string.length()) - { - ++result; - - if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx - break; - } - - return result; - - } // nextCursor - - size_t prevCursor(const std::string& _string, const size_t _cursor) - { - size_t result = _cursor; - - while(result > 0) - { - --result; - - if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx - break; - } - - return result; - - } // prevCursor - - size_t moveCursor(const std::string& _string, const size_t _cursor, const int _amount) - { - size_t result = _cursor; - - if(_amount > 0) - { - for(int i = 0; i < _amount; ++i) - result = nextCursor(_string, result); - } - else if(_amount < 0) - { - for(int i = _amount; i < 0; ++i) - result = prevCursor(_string, result); - } - - return result; - - } // moveCursor - - std::string toLower(const std::string& _string) - { - std::string string; - - for(size_t i = 0; i < _string.length(); ++i) - string += (char)tolower(_string[i]); - - return string; - - } // toLower - - std::string toUpper(const std::string& _string) - { - std::string string; - - for(size_t i = 0; i < _string.length(); ++i) - string += (char)toupper(_string[i]); - - return string; - - } // toUpper - - std::string trim(const std::string& _string) - { - const size_t strBegin = _string.find_first_not_of(" \t"); - const size_t strEnd = _string.find_last_not_of(" \t"); - - if(strBegin == std::string::npos) - return ""; - - return _string.substr(strBegin, strEnd - strBegin + 1); - - } // trim - - std::string replace(const std::string& _string, const std::string& _replace, const std::string& _with) - { - std::string string = _string; - size_t pos; - - while((pos = string.find(_replace)) != std::string::npos) - string = string.replace(pos, _replace.length(), _with.c_str(), _with.length()); - - return string; - - } // replace - - bool startsWith(const std::string& _string, const std::string& _start) - { - return (_string.find(_start) == 0); - - } // startsWith - - bool endsWith(const std::string& _string, const std::string& _end) - { - return (_string.find(_end) == (_string.size() - _end.size())); - - } // endsWith - - std::string removeParenthesis(const std::string& _string) - { - static const char remove[4] = { '(', ')', '[', ']' }; - std::string string = _string; - size_t start; - size_t end; - bool done = false; - - while(!done) - { - done = true; - - for(int i = 0; i < sizeof(remove); i += 2) - { - end = string.find_first_of(remove[i + 1]); - start = string.find_last_of( remove[i + 0], end); - - if((start != std::string::npos) && (end != std::string::npos)) - { - string.erase(start, end - start + 1); - done = false; - } - } - } - - return trim(string); - - } // removeParenthesis - - stringVector commaStringToVector(const std::string& _string) - { - stringVector vector; - size_t start = 0; - size_t comma = _string.find(","); - - while(comma != std::string::npos) - { - vector.push_back(_string.substr(start, comma - start)); - start = comma + 1; - comma = _string.find(",", start); - } - - vector.push_back(_string.substr(start)); - std::sort(vector.begin(), vector.end()); - - return vector; - - } // commaStringToVector - - std::string vectorToCommaString(stringVector _vector) - { - std::string string; - - std::sort(_vector.begin(), _vector.end()); - - for(stringVector::const_iterator it = _vector.cbegin(); it != _vector.cend(); ++it) - string += (string.length() ? "," : "") + (*it); - - return string; - - } // vectorToCommaString - - std::string format(const char* _format, ...) - { - va_list args; - va_list copy; - - va_start(args, _format); - - va_copy(copy, args); - const int length = vsnprintf(nullptr, 0, _format, copy); - va_end(copy); - - char* buffer = new char[length + 1]; - va_copy(copy, args); - vsnprintf(buffer, length + 1, _format, copy); - va_end(copy); - - va_end(args); - - std::string out(buffer); - delete buffer; - - return out; - - } // format - - // Simple XOR scrambling of a string, with an accompanying key - std::string scramble(const std::string& _input, const std::string& key) - { - std::string buffer = _input; +#include "utils/StringUtil.h" + +#include +#include + +#if defined(_WIN32) +#include +#endif + +namespace Utils +{ + namespace String + { +#if defined(_WIN32) + const std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + + std::string string; + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return string; + } + + const std::wstring convertToWideString(const std::string string) + { + int numBytes = MultiByteToWideChar(CP_UTF8, 0, string.c_str(), (int)string.length(), nullptr, 0); + + std::wstring wstring; + wstring.resize(numBytes); + MultiByteToWideChar(CP_UTF8, 0, string.c_str(), (int)string.length(), (WCHAR*)wstring.c_str(), numBytes); + + return wstring; + } +#endif + + unsigned int chars2Unicode(const std::string& _string, size_t& _cursor) + { + const char& c = _string[_cursor]; + unsigned int result = '?'; + + if((c & 0x80) == 0) // 0xxxxxxx, one byte character + { + // 0xxxxxxx + result = ((_string[_cursor++] ) ); + } + else if((c & 0xE0) == 0xC0) // 110xxxxx, two byte character + { + // 110xxxxx 10xxxxxx + result = ((_string[_cursor++] & 0x1F) << 6) | + ((_string[_cursor++] & 0x3F) ); + } + else if((c & 0xF0) == 0xE0) // 1110xxxx, three byte character + { + // 1110xxxx 10xxxxxx 10xxxxxx + result = ((_string[_cursor++] & 0x0F) << 12) | + ((_string[_cursor++] & 0x3F) << 6) | + ((_string[_cursor++] & 0x3F) ); + } + else if((c & 0xF8) == 0xF0) // 11110xxx, four byte character + { + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + result = ((_string[_cursor++] & 0x07) << 18) | + ((_string[_cursor++] & 0x3F) << 12) | + ((_string[_cursor++] & 0x3F) << 6) | + ((_string[_cursor++] & 0x3F) ); + } + else + { + // error, invalid unicode + ++_cursor; + } + + return result; + + } // chars2Unicode + + std::string unicode2Chars(const unsigned int _unicode) + { + std::string result; + + if(_unicode < 0x80) // one byte character + { + result += ((_unicode ) & 0xFF); + } + else if(_unicode < 0x800) // two byte character + { + result += ((_unicode >> 6) & 0xFF) | 0xC0; + result += ((_unicode ) & 0x3F) | 0x80; + } + else if(_unicode < 0xFFFF) // three byte character + { + result += ((_unicode >> 12) & 0xFF) | 0xE0; + result += ((_unicode >> 6) & 0x3F) | 0x80; + result += ((_unicode ) & 0x3F) | 0x80; + } + else if(_unicode <= 0x1fffff) // four byte character + { + result += ((_unicode >> 18) & 0xFF) | 0xF0; + result += ((_unicode >> 12) & 0x3F) | 0x80; + result += ((_unicode >> 6) & 0x3F) | 0x80; + result += ((_unicode ) & 0x3F) | 0x80; + } + else + { + // error, invalid unicode + result += '?'; + } + + return result; + + } // unicode2Chars + + size_t nextCursor(const std::string& _string, const size_t _cursor) + { + size_t result = _cursor; + + while(result < _string.length()) + { + ++result; + + if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx + break; + } + + return result; + + } // nextCursor + + size_t prevCursor(const std::string& _string, const size_t _cursor) + { + size_t result = _cursor; + + while(result > 0) + { + --result; + + if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx + break; + } + + return result; + + } // prevCursor + + size_t moveCursor(const std::string& _string, const size_t _cursor, const int _amount) + { + size_t result = _cursor; + + if(_amount > 0) + { + for(int i = 0; i < _amount; ++i) + result = nextCursor(_string, result); + } + else if(_amount < 0) + { + for(int i = _amount; i < 0; ++i) + result = prevCursor(_string, result); + } + + return result; + + } // moveCursor + + std::string toLower(const std::string& _string) + { + std::string string; + + for(size_t i = 0; i < _string.length(); ++i) + string += (char)tolower(_string[i]); + + return string; + + } // toLower + + std::string toUpper(const std::string& _string) + { + +#if defined(_WIN32) + std::wstring stringW = convertToWideString(_string); + + auto& f = std::use_facet>(std::locale()); + f.toupper(&stringW[0], &stringW[0] + stringW.size()); + + return convertFromWideString(stringW); +#else + std::string string; + + for(size_t i = 0; i < _string.length(); ++i) + string += (char)toupper(_string[i]); + + return string; +#endif + } // toUpper + + std::string trim(const std::string& _string) + { + const size_t strBegin = _string.find_first_not_of(" \t"); + const size_t strEnd = _string.find_last_not_of(" \t"); + + if(strBegin == std::string::npos) + return ""; + + return _string.substr(strBegin, strEnd - strBegin + 1); + + } // trim + + std::string replace(const std::string& _string, const std::string& _replace, const std::string& _with) + { + std::string string = _string; + size_t pos; + + while((pos = string.find(_replace)) != std::string::npos) + string = string.replace(pos, _replace.length(), _with.c_str(), _with.length()); + + return string; + + } // replace + + bool startsWith(const std::string& _string, const std::string& _start) + { + return (_string.find(_start) == 0); + + } // startsWith + + bool endsWith(const std::string& _string, const std::string& _end) + { + return (_string.find(_end) == (_string.size() - _end.size())); + + } // endsWith + + std::string removeParenthesis(const std::string& _string) + { + static const char remove[4] = { '(', ')', '[', ']' }; + std::string string = _string; + size_t start; + size_t end; + bool done = false; + + while(!done) + { + done = true; + + for(int i = 0; i < sizeof(remove); i += 2) + { + end = string.find_first_of(remove[i + 1]); + start = string.find_last_of( remove[i + 0], end); + + if((start != std::string::npos) && (end != std::string::npos)) + { + string.erase(start, end - start + 1); + done = false; + } + } + } + + return trim(string); + + } // removeParenthesis + + stringVector commaStringToVector(const std::string& _string) + { + stringVector vector; + size_t start = 0; + size_t comma = _string.find(","); + + while(comma != std::string::npos) + { + vector.push_back(_string.substr(start, comma - start)); + start = comma + 1; + comma = _string.find(",", start); + } + + vector.push_back(_string.substr(start)); + std::sort(vector.begin(), vector.end()); + + return vector; + + } // commaStringToVector + + std::string vectorToCommaString(stringVector _vector) + { + std::string string; + + std::sort(_vector.begin(), _vector.end()); + + for(stringVector::const_iterator it = _vector.cbegin(); it != _vector.cend(); ++it) + string += (string.length() ? "," : "") + (*it); + + return string; + + } // vectorToCommaString + + std::string format(const char* _format, ...) + { + va_list args; + va_list copy; + + va_start(args, _format); + + va_copy(copy, args); + const int length = vsnprintf(nullptr, 0, _format, copy); + va_end(copy); + + char* buffer = new char[length + 1]; + va_copy(copy, args); + vsnprintf(buffer, length + 1, _format, copy); + va_end(copy); + + va_end(args); + + std::string out(buffer); + delete buffer; + + return out; + + } // format + + // Simple XOR scrambling of a string, with an accompanying key + std::string scramble(const std::string& _input, const std::string& key) + { + std::string buffer = _input; for (size_t i = 0; i < _input.size(); ++i) { - buffer[i] = _input[i] ^ key[i]; - } - - return buffer; - - } // scramble - - } // String:: - -} // Utils:: + buffer[i] = _input[i] ^ key[i]; + } + + return buffer; + + } // scramble + + } // String:: + +} // Utils:: diff --git a/es-core/src/utils/StringUtil.h b/es-core/src/utils/StringUtil.h index 5fffa1d761..7ebcf0df22 100644 --- a/es-core/src/utils/StringUtil.h +++ b/es-core/src/utils/StringUtil.h @@ -28,6 +28,10 @@ namespace Utils std::string format (const char* _string, ...); std::string scramble (const std::string& _input, const std::string& key); +#if defined(_WIN32) + const std::string convertFromWideString(const std::wstring wstring); + const std::wstring convertToWideString(const std::string string); +#endif } // String:: } // Utils:: From 77162616b7b4dacbedb8e2aaa9eb61210ee1d6da Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 27 May 2019 13:57:26 +0200 Subject: [PATCH 002/336] [Add French localization resources] --- resources/fontawesome_webfont.ttf | Bin 0 -> 120456 bytes resources/locale/fr/emulationstation2.po | 2047 ++++++++++++++++++++++ resources/locale/it/emulationstation2.po | 1884 ++++++++++++++++++++ 3 files changed, 3931 insertions(+) create mode 100644 resources/fontawesome_webfont.ttf create mode 100644 resources/locale/fr/emulationstation2.po create mode 100644 resources/locale/it/emulationstation2.po diff --git a/resources/fontawesome_webfont.ttf b/resources/fontawesome_webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..59dec23e2af3290758890eee55e9f619e6c48202 GIT binary patch literal 120456 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 literal 0 HcmV?d00001 diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po new file mode 100644 index 0000000000..3f34a69461 --- /dev/null +++ b/resources/locale/fr/emulationstation2.po @@ -0,0 +1,2047 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: recalbox-emulationstation\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: +msgid "GAME COLLECTION SETTINGS" +msgstr "LISTES PERSONNALISÉES" + +#: +msgid "CANCEL" +msgstr "ANNULER" + +#: +msgid "automatic" +msgstr "automatique" + +#: +msgid "basic" +msgstr "basique" + +#: +msgid "detailed" +msgstr "détaillée" + +#: +msgid "grid" +msgstr "grille" + +#: +msgid "fade" +msgstr "fondu" + +#: +msgid "instant" +msgstr "instantanée" + +#: +msgid "slide" +msgstr "animée" + +#: +msgid "gridex" +msgstr "grille détaillée" + +#: +msgid "ENABLE NAVIGATION SOUNDS" +msgstr "SONS LORS DE LA NAVIGATION" + +#: +msgid "ENABLE VIDEO AUDIO" +msgstr "SON DES VIDEOS" + +#: +msgid "APPLY FILTER" +msgstr "FILTER LES JEUX" + +#: +msgid "AUTOMATIC GAME COLLECTIONS" +msgstr "COLLECTIONS AUTOMATIQUES" + +#: +msgid "SELECT COLLECTIONS" +msgstr "SÉLECTIONNER LES COLLECTIONS" + +#: +msgid "CUSTOM GAME COLLECTIONS" +msgstr "COLLECTIONS PERSONNALISEES" + +#: +msgid "CREATE NEW CUSTOM COLLECTION FROM THEME" +msgstr "CRÉER UNE COLLECTION A PARTIR D'UN THEME" + +#: +msgid "GAME COLLECTION SETTINGS" +msgstr "PARAMÈTRES DES LISTES DE JEUX" + +#: +msgid "SELECT THEME FOLDER" +msgstr "SÉLECTIONNEZ LE DOSSIER" + +#: +msgid "CREATE NEW CUSTOM COLLECTION" +msgstr "CREER UNE COLLECTION PERSONNALISÉE" + +#: +msgid "New Collection Name" +msgstr "Nom de la collection" + +#: +msgid "GROUP UNTHEMED CUSTOM COLLECTIONS" +msgstr "GROUPER LES COLLECTIONS SANS THEME" + +#: +msgid "SORT CUSTOM COLLECTIONS AND SYSTEMS" +msgstr "TRIER LES COLLECTIONS ET LES SYSTÈMES" + +#: +msgid "FINISH EDITING COLLECTION" +msgstr "TERMINER L'ÉDITION DE LA COLLECTION" + +#: +msgid "PARSE GAMESLISTS ONLY" +msgstr "LISTES DE JEUX XML UNIQUEMENT" + +#: +msgid "POWER SAVER MODES" +msgstr "MODE D'ECONOMIE D'ENERGIE" + +#: +msgid "SAVE METADATA ON EXIT" +msgstr "SAUVER LES DONNÉES EN QUITTANT" + +#: +msgid "SHOW HIDDEN FILES" +msgstr "AFFICHER LES FICHIERS CACHÉS" + +#: +msgid "VRAM LIMIT" +msgstr "MÉMOIRE VIDÉO MAX" + +#: +msgid "RESET ALL FILTERS" +msgstr "RÉINITIALISER TOUS LES FILTRES" + +#: +msgid "GAMELIST VIEW STYLE" +msgstr "STYLE D'AFFICHAGE" + +#: +msgid "GAME LAUNCH TRANSITION" +msgstr "TRANSITION AU LANCEMENT D'UN JEU" + +#: +msgid "RANDOM" +msgstr "ALÉATOIRE" + +#: +msgid "Rating" +msgstr "Note" + +#: +msgid "Released" +msgstr "Sortie" + +#: +msgid "Developer" +msgstr "Développeur" + +#: +msgid "Publisher" +msgstr "Éditeur" + +#: +msgid "Genre" +msgstr "Genre" + +#: +msgid "Players" +msgstr "Joueurs" + +#: +msgid "NO GAMES FOUND - SKIP" +msgstr "AUCUN JEU CORRESPONDANT - PASSER" + +#: +msgid "RETRY" +msgstr "RECOMMENCER" + +#: +msgid "SKIP" +msgstr "PASSER" + +#: +msgid "SEARCH FOR" +msgstr "RECHERCHER" + +#: +msgid "SEARCH" +msgstr "RECHERCHER" + +#: +msgid "SCRAPING IN PROGRESS" +msgstr "SCRAPING EN COURS" + +#: +msgid "SYSTEM" +msgstr "SYSTÈME" + +#: +msgid "subtitle text" +msgstr "sous-titre" + +#: +msgid "INPUT" +msgstr "ÉDITER" + +#: +msgid "search" +msgstr "rechercher" + +#: +msgid "STOP" +msgstr "ARRÊTER" + +#: +msgid "stop (progress saved)" +msgstr "stop (progression sauvegardée)" + +#: +msgid "GAME %i OF %i" +msgstr "JEU %i SUR %i" + +#: +msgid "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 RECALBOX.COM FOR MORE INFORMATION." +msgstr "AUCUN JEU N'A ÉTÉ TROUVÉ SUR LE SYSTÈME !\n" +"VEUILLEZ AJOUTER DES JEUX EN SUIVANT LES INSTRUCTIONS DE LA NOTICE.\n" +"\n" +"RENDEZ-VOUS SUR RECALBOX.COM POUR PLUS D'INFORMATIONS" + +#: +msgid "%i GAME SUCCESSFULLY SCRAPED!" +msgid_plural "%i GAMES SUCCESSFULLY SCRAPED!" +msgstr[0] "%i JEU SCRAPPÉ !" +msgstr[1] "%i JEUX SCRAPPÉS !" + +#: +msgid "%i GAME SKIPPED." +msgid_plural "%i GAMES SKIPPED." +msgstr[0] "%i JEU PASSÉ." +msgstr[1] "%i JEUX PASSÉS." + +#: +msgid "OK" +msgstr "OK" + +#: +msgid "EDIT METADATA" +msgstr "ÉDITER LES DONNÉES" + +#: +msgid "SCRAPE" +msgstr "SCRAPPER" + +#: +msgid "SAVE" +msgstr "SAUVEGARDER" + +#: +msgid "THIS WILL DELETE A FILE!\n" +"ARE YOU SURE?" +msgstr "CELA VA EFFACER LE FICHIER\n" +"ÊTES-VOUS SUR ?" + +#: +msgid "YES" +msgstr "OUI" + +#: +msgid "NO" +msgstr "NON" + +#: +msgid "DELETE" +msgstr "SUPPRIMER" + +#: +msgid "SAVE CHANGES?" +msgstr "SAUVEGARDER ?" + +#: +msgid "BACK" +msgstr "RETOUR" + +#: +msgid "CLOSE" +msgstr "FERMER" + +#: +msgid "MAIN MENU" +msgstr "MENU PRINCIPAL" + +#: +msgid "KODI MEDIA CENTER" +msgstr "KODI MEDIA CENTER" + +#: +msgid "SYSTEM SETTINGS" +msgstr "OPTIONS SYSTÈME" + +#: +msgid "VERSION" +msgstr "VERSION" + +#: +msgid "DISK USAGE" +msgstr "ESPACE DISQUE" + +#: +msgid "STORAGE DEVICE" +msgstr "MÉDIA DE STOCKAGE" + +#: +msgid "LANGUAGE" +msgstr "LANGUE" + +#: +msgid "OVERCLOCK" +msgstr "OVERCLOCK" + +#: +msgid "EXTREM (1100Mhz)" +msgstr "EXTREM (1100Mhz)" + +#: +msgid "TURBO (1000Mhz)" +msgstr "TURBO (1000Mhz)" + +#: +msgid "HIGH (950Mhz)" +msgstr "HIGH (950Mhz)" + +#: +msgid "NONE (700Mhz)" +msgstr "AUCUN (700Mhz)" + +#: +msgid "TURBO (1050Mhz)+" +msgstr "TURBO (1050Mhz)+" + +#: +msgid "HIGH (1050Mhz)" +msgstr "HIGH (1050Mhz)" + +#: +msgid "NONE (900Mhz)" +msgstr "AUCUN (900Mhz)" + +#: +msgid "NONE (1200Mhz)" +msgstr "AUCUN (1200Mhz)" + +#: +msgid "NONE" +msgstr "AUCUN" + +#. NEW SETTINGS ORGANIZATION +#: +msgid "UPDATES" +msgstr "MISES À JOUR" + +#: +msgid "AUTO UPDATES" +msgstr "MISES À JOUR AUTO" + +#: +msgid "START UPDATE" +msgstr "LANCER LA MISE À JOUR" + +#: +msgid "KODI SETTINGS" +msgstr "OPTIONS KODI" + +#: +msgid "ENABLE KODI" +msgstr "ACTIVER KODI" + +#: +msgid "KODI AT START" +msgstr "AUTO DÉMARRAGE DE KODI" + +#: +msgid "START KODI WITH X" +msgstr "X POUR LANCER KODI" + +#: +msgid "SECURITY" +msgstr "SÉCURITÉ" + +#: +msgid "ENFORCE SECURITY" +msgstr "RENFORCER LA SÉCURITÉ" + +#: +msgid "ROOT PASSWORD" +msgstr "MOT DE PASSE ROOT" + +#: +msgid "THE SYSTEM WILL NOW REBOOT" +msgstr "LE SYSTÈME VA REDÉMARRER" + +#: +msgid "GAMES SETTINGS" +msgstr "OPTIONS DES JEUX" + +#: +msgid "GAME RATIO" +msgstr "FORMAT JEUX" + +#: +msgid "SMOOTH GAMES" +msgstr "LISSER LES JEUX" + +#: +msgid "REWIND" +msgstr "REMBOBINAGE" + +#: +msgid "AUTO SAVE/LOAD" +msgstr "SAUVEGARDE/CHARGEMENT AUTOMATIQUE" + +#: +msgid "SHADERS SET" +msgstr "SHADERS SET" + +#: +msgid "SCANLINES" +msgstr "SCANLINES" + +#: +msgid "RETRO" +msgstr "RETRO" + +#: +msgid "RETROACHIEVEMENTS SETTINGS" +msgstr "OPTIONS DE RETROACHIEVEMENTS" + +#: +msgid "RETROACHIEVEMENTS" +msgstr "RETROACHIEVEMENTS" + +#: +msgid "HARDCORE MODE" +msgstr "MODE HARDCORE" + +#: +msgid "USERNAME" +msgstr "NOM UTILISATEUR" + +#: +msgid "PASSWORD" +msgstr "MOT DE PASSE" + +#: +msgid "ADVANCED" +msgstr "AVANCÉES" + +#: +msgid "REALLY UPDATE GAMES LISTS ?" +msgstr "CONFIRMER LA MISE À JOUR DES LISTES DES JEUX ?" + +#: +msgid "UPDATE GAMES LISTS" +msgstr "METTRE À JOUR LES LISTES DES JEUX" + +#: +msgid "CONTROLLERS SETTINGS" +msgstr "OPTIONS MANETTES" + +#: +msgid "UI SETTINGS" +msgstr "OPTIONS DE L'INTERFACE" + +#: +msgid "OVERSCAN" +msgstr "OVERSCAN" + +#: +msgid "SCREENSAVER SETTINGS" +msgstr "ÉCRAN DE VEILLE" + +#: +msgid "SCREENSAVER AFTER" +msgstr "ÉCRAN DE VEILLE APRÈS" + +#: +msgid "TRANSITION STYLE" +msgstr "STYLE DE LA TRANSITION" + +#: +msgid "SCREENSAVER BEHAVIOR" +msgstr "COMPORTEMENT DE L'ÉCRAN DE VEILLE" + +#: +msgid "SHOW FRAMERATE" +msgstr "AFFICHER LE FRAMERATE" + +#: +msgid "ON-SCREEN HELP" +msgstr "AFFICHAGE DE L'AIDE" + +#: +msgid "HIDE WHEN RUNNING GAME" +msgstr "MASQUER AU LANCEMENT D'UN JEU" + +#: +msgid "QUICK SYSTEM SELECT" +msgstr "SÉLECTION RAPIDE DU SYSTÈME" + +#: +msgid "THEME SET" +msgstr "THÈME" + +#: +msgid "SOUND SETTINGS" +msgstr "OPTIONS DU SON" + +#: +msgid "SYSTEM VOLUME" +msgstr "VOLUME DU SYSTÈME" + +#: +msgid "FRONTEND MUSIC" +msgstr "MUSIQUE DE FOND" + +#: +msgid "OUTPUT DEVICE" +msgstr "SORTIE AUDIO" + +#: +msgid "HDMI" +msgstr "HDMI" + +#: +msgid "JACK" +msgstr "JACK" + +#: +msgid "AUTO" +msgstr "AUTO" + +#: +msgid "NETWORK SETTINGS" +msgstr "OPTIONS RÉSEAU" + +#: +msgid "CONNECTED" +msgstr "CONNECTÉ" + +#: +msgid "NOT CONNECTED" +msgstr "DÉCONNECTÉ" + +#: +msgid "STATUS" +msgstr "STATUT" + +#: +msgid "IP ADDRESS" +msgstr "ADRESSE IP" + +#: +msgid "HOSTNAME" +msgstr "NOM SUR LE RÉSEAU" + +#: +msgid "ENABLE WIFI" +msgstr "ACTIVER LE WIFI" + +#: +msgid "WIFI SSID" +msgstr "SSID WIFI" + +#: +msgid "WIFI KEY" +msgstr "CLÉ WIFI" + +#: +msgid "WIFI ENABLED" +msgstr "WIFI ACTIVÉ" + +#: +msgid "WIFI CONFIGURATION ERROR" +msgstr "ERREUR DE CONFIGURATION WIFI" + +#: +msgid "SCRAPER" +msgstr "SCRAPPEUR" + +#: +msgid "SCRAPE FROM" +msgstr "SCRAPPER DEPUIS" + +#: +msgid "SCRAPE RATINGS" +msgstr "SCRAPPER LES NOTES" + +#: +msgid "SCRAPE NOW" +msgstr "SCRAPPER MAINTENANT" + +#: +msgid "QUIT" +msgstr "QUITTER" + +#: +msgid "REALLY RESTART?" +msgstr "REDÉMARRER ?" + +#: +msgid "RESTART SYSTEM" +msgstr "REDÉMARRER" + +#: +msgid "REALLY SHUTDOWN?" +msgstr "ÉTEINDRE ?" + +#: +msgid "SHUTDOWN SYSTEM" +msgstr "ÉTEINDRE" + +#: +msgid "Emulator" +msgstr "Émulateur" + +#: +msgid "Core" +msgstr "Core" + +#: +msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." +msgstr "VOUS ALLEZ CONFIGURER UNE MANETTE. SI VOUS N'AVEZ QU'UN JOYSTICK OU JUSTE UNE CROIX DIRECTIONNELLE, CONFIGUREZ SEULEMENT LES DIRECTIONS ET PASSEZ LES JOYSTICKS EN RESTANT APPUYÉ SUR UN BOUTON. SI VOUS N'AVEZ PAS DE BOUTON SPECIAL POUR HOTKEY, UTILISEZ SELECT. IGNOREZ UN BOUTON QUE VOTRE MANETTE N'A PAS EN RESTANT APPUYÉ SUR UN BOUTON DÉJÀ CONFIGURÉ. LES NOMS DES BOUTONS SONT BASÉS SUR LA SUPER NINTENDO" + +#. GUIMENU +#: +msgid "CONFIGURE A CONTROLLER" +msgstr "CONFIGURER UNE MANETTE" + +#. Bluetooth +#: +msgid "CONTROLLER PAIRED" +msgstr "MANETTE ASSOCIÉE" + +#: +msgid "UNABLE TO PAIR CONTROLLER" +msgstr "IMPOSSIBLE D'ASSOCIER LA MANETTE" + +#: +msgid "AN ERROR OCCURED" +msgstr "UNE ERREUR EST SURVENUE" + +#: +msgid "NO CONTROLLERS FOUND" +msgstr "AUCUNE MANETTE DÉTECTÉE" + +#: +msgid "PAIR A BLUETOOTH CONTROLLER" +msgstr "ASSOCIER UNE MANETTE BLUETOOTH" + +#: +msgid "CONTROLLERS LINKS HAVE BEEN DELETED." +msgstr "LES LIENS VERS LES MANETTES ONT ÉTÉ SUPPRIMÉS." + +#: +msgid "FORGET BLUETOOTH CONTROLLERS" +msgstr "OUBLIER LES MANETTES BLUETOOTH" + +#: +msgid "INPUT P%i" +msgstr "JOUEUR %i" + +#: +msgid "CHOOSE" +msgstr "CHOISIR" + +#: +msgid "SELECT" +msgstr "SÉLECTIONNER" + +#: +msgid "OPTIONS" +msgstr "OPTIONS" + +#: +msgid "JUMP TO LETTER" +msgstr "ALLER À LA LETTRE" + +#: +msgid "SORT GAMES BY" +msgstr "TRIER LES JEUX PAR" + +#. FAVORITES +#: +msgid "FAVORITES ONLY" +msgstr "FAVORIS SEULEMENT" + +#: +msgid "EDIT THIS GAME'S METADATA" +msgstr "ÉDITER LES DONNÉES DU JEU" + +#: +msgid "SCRAPE THESE GAMES" +msgstr "SCRAPPER CES JEUX" + +#: +msgid "All Games" +msgstr "Tous les jeux" + +#. MISSING SCRAPPER TRANSLATIONS +#: +msgid "Only missing image" +msgstr "Seulement images manquantes" + +#: +msgid "FILTER" +msgstr "FILTRE" + +#: +msgid "SCRAPE THESE SYSTEMS" +msgstr "SCRAPPER CES SYSTÈMES" + +#: +msgid "SYSTEMS" +msgstr "SYSTÈMES" + +#: +msgid "USER DECIDES ON CONFLICTS" +msgstr "CHOISIR SI CONFLIT" + +#: +msgid "START" +msgstr "DÉMARRER" + +#: +msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" +"CONTINUE ANYWAY?" +msgstr "ATTENTION: CERTAINS DE VOS SYSTÈMES N'ONT PAS DE PLATEFORME. LES RÉSULTATS POURRAIENT ÊTRE INATTENDUS!\n" +"CONTINUER QUAND MÊME?" + +#: +msgid "NO GAMES FIT THAT CRITERIA." +msgstr "AUCUN JEU NE CORRESPOND." + +#: +msgid "REALLY UPDATE?" +msgstr "METTRE À JOUR LE SYSTÈME ?" + +#: +msgid "NETWORK CONNECTION NEEDED" +msgstr "VEUILLEZ BRANCHER UN CÂBLE RÉSEAU" + +#: +msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" +msgstr "MISE À JOUR TÉLÉCHARGÉE, REDÉMARRAGE EN COURS" + +#: +msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" +msgstr "ÉCHEC DE LA MISE À JOUR" + +#: +msgid "NO UPDATE AVAILABLE" +msgstr "AUCUNE MISE À JOUR DISPONIBLE" + +#: +msgid "enter emulator" +msgstr "entrer l'émulateur" + +#: +msgid "enter core" +msgstr "entrer le core" + +#: +msgid "Ratio" +msgstr "Ratio" + +#: +msgid "enter ratio" +msgstr "entrer le ratio" + +#: +msgid "Name" +msgstr "Nom" + +#: +msgid "enter game name" +msgstr "entrer le nom du jeu" + +#: +msgid "Description" +msgstr "Description" + +#: +msgid "enter description" +msgstr "entrer la description" + +#: +msgid "Image" +msgstr "Image" + +#: +msgid "enter path to image" +msgstr "entrer le chemin vers l'image" + +#: +msgid "Thumbnail" +msgstr "Miniature" + +#: +msgid "enter path to thumbnail" +msgstr "entrer le chemin vers la miniature" + +#: +msgid "enter rating" +msgstr "entrer les votes" + +#: +msgid "Release date" +msgstr "Date de sortie" + +#: +msgid "enter release date" +msgstr "entrer la date de sortie" + +#: +msgid "enter game developer" +msgstr "entrer le développeur du jeu" + +#: +msgid "enter game publisher" +msgstr "entrer l'éditeur du jeu" + +#: +msgid "enter game genre" +msgstr "entrer le genre du jeu" + +#: +msgid "enter number of players" +msgstr "entrer le nombre de joueurs" + +#: +msgid "Favorite" +msgstr "Favori" + +#: +msgid "enter favorite" +msgstr "entrer le favori" + +#: +msgid "Region" +msgstr "Région" + +#: +msgid "enter region" +msgstr "entrer la région" + +#: +msgid "Romtype" +msgstr "Type de rom" + +#: +msgid "enter romtype" +msgstr "entre le type de rom" + +#: +msgid "Hidden" +msgstr "Invisible" + +#: +msgid "HIDDEN" +msgstr "CACHÉ" + +#: +msgid "NAME" +msgstr "NOM" + +#: +msgid "EMULATOR" +msgstr "EMULATEUR" + +#: +msgid "FAVORITE" +msgstr "FAVORI" + +#: +msgid "set hidden" +msgstr "Mettre caché" + +#: +msgid "Play count" +msgstr "Nb parties" + +#: +msgid "enter number of times played" +msgstr "entrer le nombre de parties" + +#: +msgid "Last played" +msgstr "Dernier jeu" + +#: +msgid "enter last played date" +msgstr "entrer la date de la dernière partie" + +#: +msgid "GAMES AVAILABLE" +msgstr "JEUX DISPONIBLES" + +#: +msgid "%i FAVORITE" +msgid_plural "%i FAVORITES" +msgstr[0] "%i FAVORI" +msgstr[1] "%i FAVORIS" + +#: +msgid "SCROLL" +msgstr "SCROLLER" + +#: +msgid "LAUNCH" +msgstr "LANCER" + +#: +msgid "Times played" +msgstr "Nb parties" + +#: +msgid "MENU" +msgstr "MENU" + +#: +msgid "FILENAME, ASCENDING" +msgstr "FICHIER, ASCENDANT" + +#: +msgid "FILENAME, DESCENDING" +msgstr "FICHIER, DESCENDANT" + +#: +msgid "RATING, ASCENDING" +msgstr "VOTES, ASCENDANT" + +#: +msgid "RATING, DESCENDING" +msgstr "VOTES, DESCENDANT" + +#: +msgid "TIMES PLAYED, ASCENDING" +msgstr "PARTIES JOUÉES, ASCENDANT" + +#: +msgid "TIMES PLAYED, DESCENDING" +msgstr "PARTIES JOUÉES, DESCENDANT" + +#: +msgid "LAST PLAYED, ASCENDING" +msgstr "DERNIÈRE PARTIE, ASCENDANT" + +#: +msgid "LAST PLAYED, DESCENDING" +msgstr "DERNIÈRE PARTIE, DESCENDANT" + +#: +msgid "WORKING..." +msgstr "EN COURS..." + +#: +msgid "CHANGE" +msgstr "CHANGER" + +#: +msgid "never" +msgstr "jamais" + +#: +msgid "just now" +msgstr "à l'instant" + +#: +msgid "%i sec ago" +msgid_plural "%i secs ago" +msgstr[0] "il y a %i sec" +msgstr[1] "il y a %i secs" + +#: +msgid "%i min ago" +msgid_plural "%i mins ago" +msgstr[0] "il y a %i min" +msgstr[1] "il y a %i mins" + +#: +msgid "%i hour ago" +msgid_plural "%i hours ago" +msgstr[0] "il y a %i heure" +msgstr[1] "il y a %i heures" + +#: +msgid "%i day ago" +msgid_plural "%i days ago" +msgstr[0] "il y a %i jour" +msgstr[1] "il y a %i jours" + +#: +msgid "unknown" +msgstr "inconnu" + +#: +msgid "SELECT ALL" +msgstr "TOUT SÉLECTIONNER" + +#: +msgid "SELECT NONE" +msgstr "AUCUN" + +#: +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i SÉLECTIONNÉ" +msgstr[1] "%i SÉLECTIONNÉS" + +#: +msgid "UP" +msgstr "HAUT" + +#: +msgid "DOWN" +msgstr "BAS" + +#: +msgid "LEFT" +msgstr "GAUCHE" + +#: +msgid "RIGHT" +msgstr "DROITE" + +#: +msgid "JOYSTICK 1 UP" +msgstr "JOYSTICK 1 HAUT" + +#: +msgid "JOYSTICK 1 LEFT" +msgstr "JOYSTICK 1 GAUCHE" + +#: +msgid "JOYSTICK 2 UP" +msgstr "JOYSTICK 2 HAUT" + +#: +msgid "JOYSTICK 2 LEFT" +msgstr "JOYSTICK 2 GAUCHE" + +#: +msgid "PAGE UP" +msgstr "L1 (PAGE PRECEDENTE)" + +#: +msgid "PAGE DOWN" +msgstr "R1 (PAGE SUIVANTE)" + +#: +msgid "HOTKEY" +msgstr "HOTKEY" + +#: +msgid "CONFIGURING" +msgstr "CONFIGURATION" + +#: +msgid "KEYBOARD" +msgstr "CLAVIER" + +#: +msgid "GAMEPAD %i" +msgstr "JOYSTICK %i" + +#. Config controllers missing translation +#: +msgid "PRESS ANYTHING" +msgstr "APPUYEZ SUR UN BOUTON" + +#: +msgid "ALREADY TAKEN" +msgstr "DÉJÀ PRIS" + +#: +msgid "DISCARD CHANGES" +msgstr "ANNULER LES MODIFICATIONS" + +#: +msgid "WELCOME" +msgstr "BIENVENUE" + +#: +msgid "CONFIGURE INPUT" +msgstr "CONFIGURER LES MANETTES" + +#: +msgid "%i GAMEPAD DETECTED" +msgid_plural "%i GAMEPADS DETECTED" +msgstr[0] "%i MANETTE DÉTECTÉE" +msgstr[1] "%i MANETTES DÉTECTÉES" + +#: +msgid "NO GAMEPADS DETECTED" +msgstr "AUCUNE MANETTE DÉTECTÉE" + +#: +msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." +msgstr "MAINTENEZ UN BOUTON SUR LA MANETTE POUR LA CONFIGURER" + +#: +msgid "PRESS F4 TO QUIT AT ANY TIME." +msgstr "APPUYEZ SUR F4 POUR QUITTER À TOUT MOMENT" + +#: +msgid "PRESS ESC OR THE HOTKEY TO CANCEL." +msgstr "APPUYEZ SUR ECHAP OU LA TOUCHE HOTKEY POUR ANNULER" + +#: +msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" +msgstr "VOULEZ-VOUS LANCER LE MEDIA CENTER KODI ?" + +#: +msgid "LOADING..." +msgstr "CHARGEMENT..." + +#: +msgid "PLEASE WAIT..." +msgstr "VEUILLEZ PATIENTER..." + +#: +msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" +msgstr "VOULEZ-VOUS VRAIMENT ÉTEINDRE SANS SAUVEGARDER LES METADATAS ?" + +#: +msgid "FAST SHUTDOWN SYSTEM" +msgstr "EXTINCTION RAPIDE" + +#: +msgid "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 RECALBOX.FR FOR MORE INFORMATION." +msgstr "Aucun système trouvé !\n" +"\n" +"Visitez le site recalbox.com pour plus d'infos !" + +#: +msgid "ON SCREEN KEYBOARD" +msgstr "CLAVIER VIRTUEL" + +#: +msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" +msgstr "APPUYEZ SUR SHIFT POUR OBTENIR LES MAJUSCULES" + +#: +msgid "SPACE" +msgstr "ESPACE" + +#: +msgid "DELETE A CHAR" +msgstr "EFFACER UN CARACTERE" + +#: +msgid "SHIFT" +msgstr "MAJUSCULE" + +#: +msgid "STOP EDITING" +msgstr "ARRETER L'EDITION" + +#: +msgid "MOVE CURSOR" +msgstr "BOUGER LE CURSEUR" + +#: +msgid "EDIT" +msgstr "EDITER" + +#: +msgid "ACCEPT RESULT" +msgstr "ACCEPTER LE RESULTAT" + +#: +msgid "FILENAME" +msgstr "FICHIER" + +#: +msgid "RATING" +msgstr "VOTES" + +#: +msgid "TIMES PLAYED" +msgstr "PARTIES JOUÉES" + +#: +msgid "LAST PLAYED" +msgstr "DERNIÈRE PARTIE" + +#: +msgid "NUMBER OF PLAYERS" +msgstr "NOMBRE DE JOUEURS" + +#: +msgid "DEVELOPER" +msgstr "DÉVELOPPEUR" + +#: +msgid "GENRE" +msgstr "GENRE" + +#: +msgid "SHOW HIDDEN" +msgstr "AFFICHER LES FICHIERS CACHÉS" + +#: +msgid "EXTREM (1400Mhz)" +msgstr "EXTRÊME (1400MHz)" + +#: +msgid "TURBO (1350Mhz)" +msgstr "TURBO (1350MHz)" + +#: +msgid "HIGH (1300Mhz)" +msgstr "HIGH (1300MHz)" + +#: +msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" +"IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." +msgstr "LES PRÉRÉGLAGES TURBO ET EXTRÊME PEUVENT PROVOQUER DES INSTABILITÉS DU SYSTÈME, UTILISEZ-LES À VOS PROPRES RISQUES.\n" +"SI VOUS CONTINUEZ, LE SYSTÈME REDÉMARRERA MAINTENANT." + +#: +msgid "%i GAME HIDDEN" +msgid_plural "%i GAMES HIDDEN" +msgstr[0] "%i JEU CACHÉ" +msgstr[1] "%i JEUX CACHÉS" + +#: +msgid "Start kodi media player." +msgstr "Démarrer le media player Kodi" + +#: +msgid "Select the language for your recalbox, select an external drive to store your games and configurations, check your current version and the free space on your drive" +msgstr "Choisissez la langue de votre Recalbox, choisissez un lecteur externe pour stocker vos jeux et vos configurations, vérifiez la version actuelle et l'espace libre sur votre disque" + +#: +msgid "Shows your current recalboxOS version." +msgstr "Montre la version actuelle de RecalboxOS." + +#: +msgid "Show how much space is used on your SHARE partition, located either on the SDCARD or on an external drive. The information shows how much GB are used and how much GB your storage has overall (example 13GB/26GB)." +msgstr "Montre l'espace utilisé sur la partition SHARE située sur la carte SD ou le lecteur externe. L'information montre combien de Go sont utilisés sur la capacité totale du support (exemple 13Go/26Go)." + +#: +msgid "Select an external drive to store your roms, saves, configurations etc.\n" +"Use a FAT32 formatted drive. The system does not format the drive. On first boot, with this option enabled, recalbox will create a '/recalbox' folder with all system files inside." +msgstr "Choisissez un lecteur externe pour stocker vos roms, sauvegardes, configurations etc.\n" +"Utilisez un lecteur formaté en FAT32. Le système ne formate pas le lecteur. Au premier démarrage, avec cette option activée, Recalbox créera un dossier '/recalbox' avec tous les fichiers système dedans." + +#: +msgid "Select your language. A reboot is needed to set this configuration active." +msgstr "Choisissez votre langue. Un redémarrage est nécessaire pour activer cette option." + +#: +msgid "Manage your recalbox updates. Select the update type. Activate update check." +msgstr "Gérez les mises à jour de Recalbox. Choisissez le type de mise à jour. Activez la vérification de mise à jour." + +#: +msgid "Check if an update is available, and start the update process." +msgstr "Vérifiez si une mise à jour est disponible et lancer le processus de mise à jour." + +#: +msgid "Stable updates will check for updates on stable recalbox releases. Stable updates are tested and approved by the recalbox team and their testers.\n" +"Unstable updates allows you to get the latest recalbox features by checking our unstable repository. You can test and validate with us the very last version of recalbox.\n" +"If you choose unstable update, be so kind to report issues on the recalbox-os issue board (https://github.com/recalbox/recalbox-os/issues)" +msgstr "Stable vérifiera les mises à jour parmi les sorties stables de Recalbox.\n" +"Les mises à jour stables sont testées et approuvées par l'équipe Recalbox et ses testeurs.\n" +"Les mise à jour unstable vous permettent de profiter des dernières fonctionnalités de Recalbox en vérifiant le dépôt unstable. Vous pouvez tester et valider avec nous la toute dernière version de Recalbox.\n" +"Si vous choisissez les mise à jour unstable, merci de signaler les problèmes sur le forum dédié (https://github.com/recalbox/recalbox-os/issues)." + +#: +msgid "Automatically check if an update is avaialble. If so, it notifies you with a message." +msgstr "Vérifier automatiquement si une mise à jour est disponible. Si tel est le cas, un message vous en informe." + +#: +msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." +msgstr "Configurez l'affichage des jeux, le ratio, les filtres (shaders), sauvgarde/chargement auto et votre compte retroachievement." + +#: +msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." +msgstr "Le ratio est le rapport entre la largeur et la hauteur de l'image. Utilisez AUTO pour laisser l'émulateur choisir le ratio original du jeu, cela vous procurera la meilleur expérience de retrogaming." + +#: +msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." +msgstr "Lisser l'image du jeu. Cette option rend l'image plus lisse en utilisant un filtre bi-linéaire." + +#: +msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." +msgstr "Cette option vous permet de rembobiner le jeu si vous êtes tué par un monstre ou si vous avez fait une erreur. Utilisez la commande HOTKEY + GAUCHE dans le jeu pour rembobiner." + +#: +msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." +msgstr "Sauvegardez automatiquement l'état actuel quand vous quittez un jeu et lancez automatiquement la dernière sauvegarde quand vous lancez un jeu." + +#: +msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." +msgstr "Integer scale est une échelle par facteur de nombre entier comme 2x, 3x, 4x, etc. Cette option met à l'échelle l'image jusqu'au plus grand facteur entier en dessous de la résolution. Par exemple,avec une résolution de 1920x1080 et integer scale activé, une image de 320x240 sera mis à l'échelle jusqu'à 1280x960 en laissant des bords noirs tout autour. Ceci pour maintenir un ratio de pixels de 1:1 avec le format d'image original pour que les pixels ne soit pas inégalement dupliqués." + +#: +msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." +msgstr "Les \"shaders\" sont des filtres pour le rendu du jeu. Vous pouvez choisir un \"shader set\" ici qui est une collection de \"shaders\" sélectionnés pour chaque système. Vous pouvez aussi changer de \"shader\" en cours de jeu avec HOTKEY + L2 ou HOTKEY + R2" + +#: +msgid "Enable or disable RetroAchievements in games." +msgstr "Activez ou désactivez RetroAchievements dans les jeux." + +#: +msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." +msgstr "Le mode Hardcore désactive *toutes* les fonctions rembobinage et sauvegarde dans l'émulateur: vous ne pourrez pas sauvegarder et charger n'importe quand. Vous devrez finir le jeu et débloquer les succès du premier coup, comme sur la console originale. En récompense, vous gagnerez à la fois les succès hardcore et standard, doublant ainsi vos points! Un jeu normal valant 400 points vaudra désormais 800 points si vous le finissez en hardcore!\n" +"Par exemple: si vous finissez un jeu pour 40 points, vous avez ensuite la possibilité d'obtenir 400 points de plus en hardcore." + +#: +msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" +msgstr "Le site web retroachievements.org propose des challenges/succès/trophées sur les système comme NES, SNES, GB, GBC, GBA, Megadrive, TurboGrafx16/PCEngine et plus! Créez un compte sur retroachievements.org et lancer vous dans la quête de succès!" + +#: +msgid "Add and configure up to 5 controllers." +msgstr "Ajoutez et configurez jusqu'à 5 manettes." + +#: +msgid "Pair a bluetooth controller with your recalbox. Your controller must be in pairing mode." +msgstr "Appairez une manette Bluetooth avec votre Recalbox. Votre manette doit être en mode appairage." + +#: +msgid "Forget all paired bluetooth controllers. You will have to pair your controllers again, but this option can help if you have issues to reconnect a controller, which is already paired." +msgstr "Oublier toutes les manettes appairées. Vous devrez appairer votre manette de nouveau mais cette option est utile si vous rencontrez des problèmes pour reconnecter une manette déjà appairée." + +#: +msgid "Configure your EmulationStation experience. Select transition types, help prompts, screensaver behavior. You can also deactivate the onscreen keyboard if you have a real keyboard plugged into your recalbox.\n" +"If you've added games since the last boot, you can also refresh the gamelist from this menu." +msgstr "Configurez votre expérience EmulationStation. Sélectionnez le type de transition, l'aide, le comportement de l'économiseur d'écran. Vous pouvez également désactiver le clavier virtuel si vous avez un clavier branché sur votre Recalbox.\n" +"Si vous avez ajouté des jeux depuis le dernier démarrage, vous pouvez aussi rafraîchir les liste de jeux depuis ce menu." + +#: +msgid "Start the screensaver after N minutes." +msgstr "Démarrer l'économiseur d'écran après N minutes." + +#: +msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." +msgstr "Choisissez le comportement de l'économiseur d'écran. DIM réduira la luminosité de l'écran, BLACK rendra l'écran noir." + +#: +msgid "Shows a help at the bottom of the screen which displays commands you can use." +msgstr "Montre l'aide en bas de l'écran qui vous indique les commandes que vous pouvez utiliser." + +#: +msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." +msgstr "Si activé, vous pouvez changer de système tout en parcourant les listes de jeux en appuyant sur GAUCHE ou DROITE." + +#: +msgid "The onscreen keyboard is necessary to type text if you only have controllers plugged into your recalbox. You can disable it if you have a real keyboard connected." +msgstr "Le clavier virtuel est nécessaire pour taper du texte si vous n'avez que des manettes branchées sur votre Recalbox. Vous pouvez désactiver cette option si vous avez un clavier branché." + +#: +msgid "Select a theme for your recalbox." +msgstr "Sélectionnez un thème pour votre Recalbox." + +#: +msgid "Updates the gamelists, if you added games since the last boot." +msgstr "Mettez à jour les listes de jeux si vous avez ajouté des jeux depuis le dernier démarrage." + +#: +msgid "Configure the sound options of your recalbox." +msgstr "Configurez les options sonores de votre Recalbox." + +#: +msgid "Set the volume of the sound output for the frontend and the games." +msgstr "Réglez le volume de la sortie audio pour EmulationStation et les jeux." + +#: +msgid "Enable or disable the frontend music. You can add your own music as mp3, or ogg format in the 'musics' directory of your recalbox." +msgstr "Activez ou désactivez la musique dans EmulationStation. Vous pouvez ajouter vos propres musiques au format mp3 ou ogg dans le dossier 'musics' de votre Recalbox." + +#: +msgid "Select your output device. Only HDMI and JACK are supported." +msgstr "Sélectionnez votre périphérique de sortie. Seuls HDMI et JACK sont supportés." + +#: +msgid "Configure the network options of your recalbox.\n" +"Check your network status and IP address, set the hostname and configure the WIFI." +msgstr "Configurez les options réseau de votre recalbox. Vérifiez le statut de connexion et l'adresse IP, choisissez le nom réseau et configurez le WIFI." + +#: +msgid "Displays CONNECTED, if you are connected, by checking if your recalbox can access the recalbox.com update server." +msgstr "Affiche CONNECTÉ si vous êtes connecté en vérifiant si votre Recalbox peut accéder au serveur de mise à jour recalbox.com. " + +#: +msgid "The IP address of your recalbox within your local network." +msgstr "L'adresse IP de votre Recalbox dans votre réseau local." + +#: +msgid "Enable or disable WIFI.\n" +"If you disable WIFI, the SSID and the WIFI passwords are saved and can be used when you reactivate it" +msgstr "Activez ou désactivez le WIFI.\n" +"Si vous désactivez le WIFI, le SSID et la clé WIFI sont sauvegardés et réutilisés quand vous réactivez l'option." + +#: +msgid "The name of your recalbox in your local network" +msgstr "Le nom de votre Recalbox sur le réseau local." + +#: +msgid "SSID (WIFI Name) of your network." +msgstr "SSID (Nom WIFI) de votre réseau." + +#: +msgid "Private key of your WIFI network." +msgstr "Clé de sécurité de votre réseau WIFI." + +#: +msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." +msgstr "Récupérez des informations et visuels pour vos jeux. Le scraper télécharge les metadatas et images pour vos jeux depuis différents serveurs et améliore complètement l'expérience d'EmulationStation." + +#: +msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." +msgstr "Sélectionnez un serveur depuis lequel scraper. Le serveur SCREENSCRAPER est recommandé et est basé sur www.screenscraper.fr. Il scrape les données de jeu dans votre langue, si disponible." + +#: +msgid "Begin the scrape process with the configuration shown below." +msgstr "Démarrez le processus de scrape avec la configuration ci-dessous." + +#: +msgid "Scrape and display game ratings." +msgstr "Scrape et affiche la notation des jeux." + +#: +msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." +msgstr "Options avancées. Assurez-vous de savoir ce que vous faîtes avant de changer les valeurs dans ce menu." + +#: +msgid "Overclock your board to increase the performance.\n" +"Overclock settings are tested and validated by the community. Keep in mind that overclocking your board can void your warranty." +msgstr "Overclockez votre carte pour augmenter les performances.\n" +"Les paramètres d'overclocking sont testés et validés par la communauté.\n" +"Gardez à l'esprit qu'overclocker votre carte peut annuler votre garantie." + +#: +msgid "Select which system to show when the recalbox frontend starts. The default value is 'favorites'." +msgstr "Choisissez quel système montrer quand EmulationStation démarre.\n" +"La valeur par défaut est 'favorites'." + +#: +msgid "On boot, recalbox will show the list of games of the selected system rather than the system view." +msgstr "Au démarrage, Recalbox montrera la liste de jeux du système sélectionné plutôt que la vue systèmes." + +#: +msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" +"This option highly speeds up boot time, but new games will not be detected." +msgstr "Montrer seulement les jeux présent dans le fichier gamelist.xml (situé dans le dossier des roms).\n" +"Cette option accélère grandement le temps de démarrage mais les nouveaux jeux ne seront pas détectés." + +#: +msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." +msgstr "Cette option vous permet de mettre le système sélectionné en mode fixe.\n" +"Avec cette option activé, l'utilisateur ne peut pas accéder aux autres systèmes." + +#: +msgid "Always display the basic gamelist view, even if you have scraped your games." +msgstr "Toujours afficher la vue liste de jeux basique, même si vous avez scrappé vos jeux." + +#: +msgid "Override global options like emulator, core, ratio and more for each available system in your recalbox." +msgstr "Passer outre les options globales comme émulateur, core, ratio et plus pour chaque système disponible dans votre Recalbox." + +#: +msgid "Configure boot options that make your recalbox boot straight into a system or into Kodi, lock a user to a single system, or directly show the gamelist." +msgstr "Configurez les options de démarrage qui font démarrer votre Recalbox directement dans un système ou dans Kodi, bloquer un utilisateur dans un seul système ou montrer directement la liste de jeux." + +#: +msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" +msgstr "Activez ou désactivez Kodi, configurez le démarrage de Kodi, activez le bouton X pour démarrer Kodi." + +#: +msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." +msgstr "Activez ou désactivez Kodi. Si Kodi est désactivé, vous ne pourrez pas le lancer avec le bouton X ou le lancer automatiquement au démarrage. L'option du menu sera aussi retirée." + +#: +msgid "Use the X button to start Kodi." +msgstr "Utilisez le bouton X pour démarrer Kodi." + +#: +msgid "Automatically start into Kodi on boot." +msgstr "Lancer directement Kodi au démarrage." + +#: +msgid "Manage your recalbox security." +msgstr "Gérez la sécurité de votre Recalbox." + +#: +msgid "Change the SSH root password." +msgstr "Changez le mot de passe root SSH." + +#. not sure 'bout this one +#: +msgid "Enforce recalbox security." +msgstr "Renforcer la sécurité de Recalbox." + +#: +msgid "Enable or disable overscan.\n" +"Overscan can help you, if you have a black border, or if the image is bigger than your screen. Before setting the overscan, try to configure your TV to have a 1:1 pixel output.\n" +"More overscan settings can be defined in the boot.txt file, available when you plug your SD card into your computer." +msgstr "Activez ou désactivez l'overscan.\n" +"L'overscan peut vous aider si vous avez des bandes noires ou si l'image est plus grande que l'écran. Avant de régler l'overscan, essayez de régler votre TV pour avoir une sortie 1:1.\n" +"D'autres réglages d'overscan peuvent être définis dans le fichier boot.txt disponible lorsque vous branchez votre carte SD sur un ordinateur." + +#: +msgid "Show the framerate in EmulationStation and in game." +msgstr "Affichez le taux de rafraîchissement dans EmulationStation et dans les jeux." + +#: +msgid "Enable or disable the Recalbox Manager.\n" +"The Recalbox Manager is a web application available on http://recalbox , if you are on windows, http://recalbox.local , if you are on Linux or Mac, or directly with your recalbox IP : http://192.168.1.XX.\n" +"You can configure many options from within the manager, and even manage games, saves, and scrapes!" +msgstr "Activez ou désactivez le Gestionnaire Recalbox.\n" +"Le Gestionnaire Recalbox est une application web disponible sur http://recalbox si vous êtes sous Windows, http://recalbox.local si vous êtes sous Linux ou Mac ou directement avec l'adresse IP de la Recalbox: http://192.168.1.XX.\n" +"Vous pouvez configurer de nombreuses options dans le Gestionnaire et même gérer les jeux, les sauvegardes et les scraps!" + +#: +msgid "Enable or disable the recalbox API.\n" +"The Recalbox API is a REST API exposing endpoints to control your recalbox via http requests." +msgstr "Activez ou désactivez l'API Recalbox.\n" +"L'API Recalbox est une API REST permettant de contrôler votre Recalbox avec des requêtes http." + +#: +msgid "Select which emulator to use when you start a game for this system." +msgstr "Selectionnez quel émulateur utiliser quand vous lancer un jeu pour ce système." + +#: +msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." +msgstr "Selectionnez quel core utiliser pour l'émulateur sélectionné. Par exemple, l'émulateur LIBRETRO a plusieurs core pour les jeux Super Nintendo. Le core par défaut que vous choisissez peut aussi être outrepassé par les réglages spécifiques par jeu." + +#: +msgid "USE COMPOSED VISUALS" +msgstr "UTILISER DES IMAGES COMPOSÉES" + +#: +msgid "CHECK UPDATES" +msgstr "VÉRIFIER LES MISES À JOUR" + +#: +msgid "UPDATE TYPE" +msgstr "TYPE DE MISE À JOUR" + +#: +msgid "INTEGER SCALE (PIXEL PERFECT)" +msgstr "INTEGER SCALE (PIXEL PERFECT)" + +#: +msgid "ADVANCED SETTINGS" +msgstr "PARAMÈTRES AVANCÉS" + +#: +msgid "BOOT SETTINGS" +msgstr "PARAMÈTRES DE DÉMARRAGE" + +#: +msgid "GAMELIST ONLY" +msgstr "LISTE DE JEUX UNIQUEMENT" + +#: +msgid "START ON SYSTEM" +msgstr "DÉMARRER SUR UN SYSTÈME" + +#: +msgid "BOOT ON GAMELIST" +msgstr "DÉMARRER SUR LA LISTE DE JEUX" + +#: +msgid "HIDE SYSTEM VIEW" +msgstr "CACHER LA VUE SYSTÈME" + +#: +msgid "EMULATOR ADVANCED CONFIGURATION" +msgstr "CONFIGURATION AVANCÉE DE L'ÉMULATEUR" + +#: +msgid "ADVANCED EMULATOR CONFIGURATION" +msgstr "CONFIGURATION AVANCÉE DE L'ÉMULATEUR" + +#: +msgid "HELP" +msgstr "AIDE" + +#: +msgid "THE SYSTEM IS UP TO DATE" +msgstr "LE SYSTÈME EST À JOUR" + +#: +msgid "FORCE BASIC GAMELIST VIEW" +msgstr "FORCER LA VUE LISTE DE JEUX BASIQUE" + +#: +msgid "DOWNLOADED" +msgstr "TÉLÉCHARGÉ" + +#: +msgid "UPDATE VERSION:" +msgstr "VERSION MISE ­À JOUR:" + +#: +msgid "UPDATE CHANGELOG:" +msgstr "CHANGELOG DE LA MISE À JOUR:" + +#: +msgid "MORE DETAILS" +msgstr "PLUS DE DÉTAILS" + +#: +msgid "CAROUSEL TRANSITIONS" +msgstr "ANIMATION DU BANDEAU" + +#: +msgid "ENABLE FILTERS" +msgstr "FILTRES" + +#: +msgid "THEME CONFIGURATION" +msgstr "CONFIGURATION DU THÈME" + +#: +msgid "THEME COLORSET" +msgstr "COLORSET DU THÈME" + +#: +msgid "THEME ICONSET" +msgstr "ICONSET DU THÈME" + +#: +msgid "THEME MENU" +msgstr "MENU DU THÈME" + +#: +msgid "THEME SYSTEMVIEW" +msgstr "SYSTEMVIEW DU THÈME" + +#: +msgid "THEME GAMELISTVIEW" +msgstr "GAMELISTVIEW DU THÈME" + +#: +msgid "THEME REGION" +msgstr "RÉGION DU THÈME" + +#: +msgid "THIS THEME HAS NO OPTION" +msgstr "CE THÈME N'A PAS D'OPTIONS" + +#: +msgid "MANUAL INPUT" +msgstr "SAISIE MANUELLE" + +#: +msgid "AN ERROR OCCURED - DOWNLOADED" +msgstr "UNE ERREUR EST SURVENUE - TÉLÉCHARGÉ" + +#: +msgid "START KODI" +msgstr "DÉMARRER KODI" + +#: +msgid "Shows the current available update version." +msgstr "Montre les versions disponibles." + +#: +msgid "Shows the current available update changelog." +msgstr "Montre le changelog de la mise à jour disponible." + +#: +msgid "Configure an associated controller. Your controller has to be associated / plugged before." +msgstr "Configurer une manette appairée. Votre manette doit être appairée / branchée auparavant." + +#: +msgid "Choose if carousel will be animated or not during transitions" +msgstr "Choisissez si le bandeau sera animé ou non durant les transitions." + +#: +msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" +msgstr "Sélectionnez le type de transition qui se produit lorsque vous lancez un jeu. FADE fera un fondu au noir et SLIDE zoomera sur l'image du jeu (ou le nom si il n'y a pas de metadatas)." + +#: +msgid "Select exisiting colorset options for this theme." +msgstr "Sélectionnez un colorset disponible pour votre thème" + +#: +msgid "Select exisiting iconset options for this theme." +msgstr "Sélectionnez un iconset disponible pour votre thème" + +#: +msgid "Select exisiting menu style options for this theme." +msgstr "Sélectionnez un style de menu disponible pour votre thème" + +#: +msgid "Select exisiting system view options for this theme." +msgstr "Sélectionnez une vue système disponible pour votre thème" + +#: +msgid "Select exisiting gamelist view options for this theme." +msgstr "Sélectionnez une vue gamelist disponible pour votre thème" + +#: +msgid "Configure theme options if available." +msgstr "Configurez les options du thème si disponibles" + +#: +msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" +msgstr "Sélectionnez une région des logos et images pour les systèmes qui sont différents selon les pays. Ex: Megadrive en Europe / Genesis aux USA" + +#: +msgid "Type the name of your SSID if it is hidden or not listed" +msgstr "Saisissez le nom de votre SSID si il n'est pas dans la liste ou caché" + +#: +msgid "Select a letter and the listing will go directly on the first game starting with this letter." +msgstr "Sélectionnez une lettre et le listing ira directement sur le 1er jeu commençant par cette lettre." + +#: +msgid "Select the way the game list is sortered (alphabetically, by notation...)." +msgstr "Sélectionnez la façon dont la liste de jeux est triée (alphabétique, par note...)." + +#: +msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." +msgstr "Changer entre rendre visible ou non les jeux favoris. Pour ajouter un jeu dans les favoris, sélectionnez le jeu et changer son statut en utilisant 'Y'." + +#: +msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." +msgstr "Changer entre rendre visible ou non les jeux cachés. pour cacher un jeu, éditez ses données et sélectionnez 'Cacher'." + +#: +msgid "This option display a menu which allows to change game data and many others options." +msgstr "Cette option affiche un menu qui permet de changer les données du jeu et de nombreuses autres options." + +#: +msgid "AVAILABLE UPDATE" +msgstr "MISE À JOUR DISPONIBLE" + +#: +msgid "UPDATE CHANGELOG" +msgstr "CHANGELOG DE LA MISE À JOUR" + +#: +msgid "CLOCK IN MENU" +msgstr "HORLOGE DANS LE MENU" + +#: +msgid "Now playing" +msgstr "Lecture en cours" + +#: +msgid "DEFAULT (%1%)" +msgstr "DÉFAUT (%1%)" + +#: +msgid "INPUT REQUIRED" +msgstr "ENTRÉE NÉCESSAIRE" + +#: +msgid "(skipped)" +msgstr "(passé)" + +#: +msgid "UP/DOWN TO SKIP" +msgstr "HAUT/BAS POUR PASSER" + +#: +msgid "A TO UNSET" +msgstr "A POUR ANNULER" + +#: +msgid "DOWN TO SKIP AND KEEP [%1%]" +msgstr "BAS POUR PASSER ET GARDER [%1%]" + +#: +msgid "UP/DOWN TO SKIP AND KEEP [%1%]" +msgstr "HAUT/BAS POUR PASSER ET GARDER [%1%]" + +#: +msgid "Set duration of help popups, 0 means no popup." +msgstr "Régler la durée des popups d'aide, 0 pour désactiver." + +#: +msgid "HELP POPUP DURATION" +msgstr "DURÉE DES POPUPS D'AIDE" + +#: +msgid "Set duration of music popups, 0 means no popup." +msgstr "Régler la durée des popups de musique, 0 pour désactiver." + +#: +msgid "MUSIC POPUP DURATION" +msgstr "DURÉE DES POPUPS DE MUSIQUE" + +#: +msgid "POPUP SETTINGS" +msgstr "RÉGLAGE DES POPUPS" + +#: +msgid "POPUP POSITION" +msgstr "POSITION DES POPUPS" + +#: +msgid "Select the position of popups on screen." +msgstr "Choisissez la position des popups sur l'écran" + +#: +msgid "Set position and duration of popups." +msgstr "Réglez la position et la durée des popups" + +#: +msgid "TOP/RIGHT" +msgstr "HAUT/DROITE" + +#: +msgid "BOTTOM/RIGHT" +msgstr "BAS/DROITE" + +#: +msgid "BOTTOM/LEFT" +msgstr "BAS/GAUCHE" + +#: +msgid "TOP/LEFT" +msgstr "HAUT/GAUCHE" + +#: +msgid "SHOW FOLDERS CONTENT" +msgstr "MONTRER LE CONTENU DES DOSSIERS" + +#: +msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." +msgstr "Basculer entre l'affichage classique de répertoires et l'affichage de tous les jeux en une seule liste." + +#: +msgid "NETPLAY" +msgstr "NETPLAY" + +#: +msgid "NETPLAY SETTINGS" +msgstr "OPTIONS NETPLAY" + +#: +msgid "NETPLAY LOBBY" +msgstr "LOBBY NETPLAY" + +#: +msgid "Enable or disable Netplay in games." +msgstr "Activer ou désactiver Netplay dans les jeux." + +#: +msgid "PORT" +msgstr "PORT" + +#: +msgid "NICKNAME" +msgstr "SURNOM" + +#: +msgid "RELAY SERVER" +msgstr "SERVEUR DE RELAI" + +#: +msgid "Enable or disable connections throught relay servers." +msgstr "Activer ou désactiver les connexions au travers de serveurs de relai." + +#: +msgid "KODI/NETPLAY" +msgstr "KODI/NETPLAY" + +#: +msgid "NO GAMES OR NO CONNECTION" +msgstr "PAS DE JEUX OU PAS DE CONNEXION" + +#: +msgid "HASH NOW" +msgstr "HASHER MAINTENANT" + +#: +msgid "HASH THESE SYSTEMS" +msgstr "HASHER CES SYSTÈMES" + +#: +msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." +msgstr "Ajoutez les hashs des roms dans vos gamelists pour avoir un résultat plus précis en Netplay." + +#: +msgid "HASH ROMS" +msgstr "HASHER LES ROMS" + +#: +msgid "Only missing hashs" +msgstr "Seulement hashs manquants" + +#: +msgid "Username" +msgstr "Nom" + +#: +msgid "Country" +msgstr "Pays" + +#: +msgid "Latency" +msgstr "Latence" + +#: +msgid "Host arch." +msgstr "Arch. de l'hôte" + +#: +msgid "Core ver." +msgstr "Ver. du core" + +#: +msgid "RA ver." +msgstr "Ver. de RA" + +#: +msgid "Can join" +msgstr "Joignable" + +#: +msgid "Rom and core match" +msgstr "Corres. rom et core" + +#: +msgid "Rom found" +msgstr "Rom trouvée" + +#: +msgid "No rom match" +msgstr "Pas de corres. de rom" + +#: +msgid "Match" +msgstr "Correspondance" + +#: +msgid "No Match" +msgstr "Pas de corres." + +#: +msgid "Rom file" +msgstr "Fichier rom" + +#: +msgid "Rom hash" +msgstr "Hash de la rom" + +#: +msgid "THIS COULD TAKE A WHILE, CONFIRM?" +msgstr "CELA PEUX PRENDRE LONGTEMPS, CONFIRMER?" + +#: +msgid "good" +msgstr "bon" + +#: +msgid "bad" +msgstr "mauvais" + +#: +msgid "medium" +msgstr "moyen" + +#: +msgid "NETPLAY POPUP DURATION" +msgstr "DURÉE DES POPUPS NETPLAY" + +#: +msgid "Set duration of netplay popups, 0 means no popup." +msgstr "Régler la durée des popups Netplay, 0 pour désactiver." + +#: +msgid "Player" +msgstr "Joueur" + +#: +msgid "Game" +msgstr "Jeu" + +#: +msgid "A Recalbox friend has started a Netplay game!" +msgstr "Un ami Recalbox a démarré une partie Netplay!" + +#: +msgid "Play online on games running through Retroarch like NES, SNES, FBA, Genesis/Megadrive and more!" +msgstr "Jouez en ligne à des jeux tournant sous Retroarch comme NES, SNES, FBA, Megadrive et bien plus!" + +#: +msgid "Rom, hash and core match" +msgstr "Corres. rom, hash et core" + +#: +msgid "No core match" +msgstr "Pas de corres. du core" + +#: +msgid "Add a clock in the main menu." +msgstr "Ajouter une horloge dans le menu principal" + +#: +msgid "UPGRADING" +msgstr "Mise à jour" + +#: +msgid "PREPARING" +msgstr "Préparation" + +#: +msgid "Starting UI" +msgstr "Initialisation de l'interface" + +#: +msgid "VERIFYING" +msgstr "Verification" + +#: +msgid "EMPTY LIST" +msgstr "Liste vide" + + +#: Retroarch ratio +msgid "Auto" +msgstr "Automatique" + +msgid "Square pixel" +msgstr "Pixel carré" + +msgid "Retroarch Config" +msgstr "Retroarch mode 'Config'" + +msgid "Retroarch Custom" +msgstr "Retroarch mode 'Custom'" + +msgid "Core provided" +msgstr "Fourni par le Core" + +msgid "Do not set" +msgstr "Ne pas toucher" \ No newline at end of file diff --git a/resources/locale/it/emulationstation2.po b/resources/locale/it/emulationstation2.po new file mode 100644 index 0000000000..f74c4c50b7 --- /dev/null +++ b/resources/locale/it/emulationstation2.po @@ -0,0 +1,1884 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: recalbox-emulationstation\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: +msgid "AN UPDATE IS AVAILABLE FOR YOUR RECALBOX" +msgstr "HAY UNA ACTUALIZACIÓN DISPONIBLE PARA RECALBOX" + +#: +msgid "CANCEL" +msgstr "CANCELAR" + +#: +msgid "Rating" +msgstr "Calificación" + +#: +msgid "Released" +msgstr "Lanzado" + +#: +msgid "Developer" +msgstr "Desarrollador" + +#: +msgid "Publisher" +msgstr "Editor" + +#: +msgid "Genre" +msgstr "Género" + +#: +msgid "Players" +msgstr "Jugadores" + +#: +msgid "NO GAMES FOUND - SKIP" +msgstr "JUEGOS NO ENCONTRADOS - OMITIR" + +#: +msgid "RETRY" +msgstr "REINTENTAR" + +#: +msgid "SKIP" +msgstr "OMITIR" + +#: +msgid "SEARCH FOR" +msgstr "BUSCAR POR" + +#: +msgid "SEARCH" +msgstr "BUSCAR" + +#: +msgid "SCRAPING IN PROGRESS" +msgstr "OBTENCIÓN DE DATOS EN CURSO" + +#: +msgid "SYSTEM" +msgstr "SISTEMA" + +#: +msgid "subtitle text" +msgstr "subtítulo" + +#: +msgid "INPUT" +msgstr "ENTRADA" + +#: +msgid "search" +msgstr "buscar" + +#: +msgid "STOP" +msgstr "DETENER" + +#: +msgid "stop (progress saved)" +msgstr "detener (guardar progreso)" + +#: +msgid "GAME %i OF %i" +msgstr "JUEGO %i DE %i" + +#: +msgid "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 RECALBOX.COM FOR MORE INFORMATION." +msgstr "¡NO SE HAN ENCONTRADO SISTEMAS!\n" +"COMPROBAR RUTAS EN ARCHIVO DE CONFIGURACIÓN.\n" +"LA CARPETA DE JUEGOS DEBE CONTENER AL MENOS UNO Y CON LA EXTENSIÓN CORRECTA.\n" +"\n" +"VISITAR RECALBOX.COM PARA OBTENER MÃS INFORMACIÓN" + +#: +msgid "%i GAME SUCCESSFULLY SCRAPED!" +msgid_plural "%i GAMES SUCCESSFULLY SCRAPED!" +msgstr[0] "¡INFORMACIÓN DE %i JUEGO OBTENIDA CORRECTAMENTE!" +msgstr[1] "¡INFORMACIÓN DE %i JUEGOS OBTENIDA CORRECTAMENTE!" + +#: +msgid "%i GAME SKIPPED." +msgid_plural "%i GAMES SKIPPED." +msgstr[0] "%i JUEGO OMITIDO" +msgstr[1] "%i JUEGOS OMITIDOS" + +#: +msgid "OK" +msgstr "OK" + +#: +msgid "EDIT METADATA" +msgstr "EDITAR METADATOS" + +#: +msgid "SCRAPE" +msgstr "OBTENER INFORMACIÓN" + +#: +msgid "SAVE" +msgstr "GUARDAR" + +#: +msgid "THIS WILL DELETE A FILE!\n" +"ARE YOU SURE?" +msgstr "¿SEGURO DESEA BORRAR EL ARCHIVO?" + +#: +msgid "YES" +msgstr "SÃ" + +#: +msgid "NO" +msgstr "NO" + +#: +msgid "DELETE" +msgstr "BORRAR" + +#: +msgid "SAVE CHANGES?" +msgstr "¿GUARDAR CAMBIOS?" + +#: +msgid "BACK" +msgstr "VOLVER" + +#: +msgid "CLOSE" +msgstr "CERRAR" + +#: +msgid "MAIN MENU" +msgstr "MENÚ PRINCIPAL" + +#: +msgid "KODI MEDIA CENTER" +msgstr "KODI MEDIA CENTER" + +#: +msgid "SYSTEM SETTINGS" +msgstr "CONFIGURACIÓN DE SISTEMA" + +#: +msgid "VERSION" +msgstr "VERSIÓN" + +#: +msgid "DISK USAGE" +msgstr "USO DE DISCO" + +#: +msgid "STORAGE DEVICE" +msgstr "ALMACENAMIENTO" + +#: +msgid "LANGUAGE" +msgstr "IDIOMA" + +#: +msgid "OVERCLOCK" +msgstr "OVERCLOCK" + +#: +msgid "EXTREM (1100Mhz)" +msgstr "EXTREMO (1100Mhz)" + +#: +msgid "TURBO (1000Mhz)" +msgstr "TURBO (1000Mhz)" + +#: +msgid "HIGH (950Mhz)" +msgstr "ALTO (950Mhz)" + +#: +msgid "NONE (700Mhz)" +msgstr "NO (700Mhz)" + +#: +msgid "TURBO (1050Mhz)+" +msgstr "TURBO (1050Mhz)+" + +#: +msgid "HIGH (1050Mhz)" +msgstr "ALTO (1050Mhz)" + +#: +msgid "NONE (900Mhz)" +msgstr "NO (900Mhz)" + +#: +msgid "NONE (1200Mhz)" +msgstr "NO (1200Mhz)" + +#: +msgid "NONE" +msgstr "NO" + +#. NEW SETTINGS ORGANIZATION +#: +msgid "UPDATES" +msgstr "ACTUALIZACIONES" + +#: +msgid "AUTO UPDATES" +msgstr "ACTUALIZACIONES AUTOMÃTICAS" + +#: +msgid "START UPDATE" +msgstr "COMENZAR ACTUALIZACIÓN" + +#: +msgid "KODI SETTINGS" +msgstr "CONFIGURACIÓN DE KODI" + +#: +msgid "ENABLE KODI" +msgstr "ACTIVAR KODI" + +#: +msgid "KODI AT START" +msgstr "INICIAR KODI AL ARRANCAR" + +#: +msgid "START KODI WITH X" +msgstr "PULSAR X PARA INICIAR KODI" + +#: +msgid "SECURITY" +msgstr "SEGURIDAD" + +#: +msgid "ENFORCE SECURITY" +msgstr "REFORZAR SEGURIDAD" + +#: +msgid "ROOT PASSWORD" +msgstr "CONTRASEÑA ROOT" + +#: +msgid "THE SYSTEM WILL NOW REBOOT" +msgstr "EL SISTEMA SERà REINICIADO" + +#: +msgid "GAMES SETTINGS" +msgstr "CONFIGURACIÓN DE JUEGOS" + +#: +msgid "GAME RATIO" +msgstr "RELACIÓN DE ASPECTO" + +#: +msgid "SMOOTH GAMES" +msgstr "SUAVIZAR JUEGOS" + +#: +msgid "REWIND" +msgstr "REBOBINAR" + +#: +msgid "AUTO SAVE/LOAD" +msgstr "GUARDAR/CARGAR AUTOMÃTICAMENTE" + +#: +msgid "SHADERS SET" +msgstr "CONJUNTO DE SHADERS" + +#. Yo dejaría en español SCANLINES, ya que en españa también se refiere a este filtro como SCANLINES, y no como LINEAS.. +#: +msgid "SCANLINES" +msgstr "SCANLINES" + +#: +msgid "RETRO" +msgstr "RETRO" + +#: +msgid "RETROACHIEVEMENTS SETTINGS" +msgstr "CONFIGURACIÓN DE RETROLOGROS" + +#: +msgid "RETROACHIEVEMENTS" +msgstr "RETROLOGROS" + +#: +msgid "HARDCORE MODE" +msgstr "MODO HARDCORE" + +#: +msgid "USERNAME" +msgstr "USUARIO" + +#: +msgid "PASSWORD" +msgstr "CONTRASEÑA" + +#: +msgid "ADVANCED" +msgstr "AVANZADO" + +#: +msgid "REALLY UPDATE GAMES LISTS ?" +msgstr "¿ACTUALIZAR LISTA DE JUEGOS?" + +#: +msgid "UPDATE GAMES LISTS" +msgstr "ACTUALIZAR LISTA DE JUEGOS" + +#: +msgid "CONTROLLERS SETTINGS" +msgstr "CONFIGURACIÓN DE MANDOS" + +#: +msgid "UI SETTINGS" +msgstr "CONFIGURACIÓN DE INTERFAZ" + +#: +msgid "OVERSCAN" +msgstr "OVERSCAN" + +#: +msgid "SCREENSAVER AFTER" +msgstr "SALVAPANTALLA A LOS" + +#: +msgid "TRANSITION STYLE" +msgstr "TIPO DE TRANSICIÓN" + +#: +msgid "SCREENSAVER BEHAVIOR" +msgstr "COMPORTAMIENTO DE SALVAPANTALLA" + +#: +msgid "SHOW FRAMERATE" +msgstr "MOSTRAR TASA DE REFRESCO" + +#: +msgid "ON-SCREEN HELP" +msgstr "AYUDA EN PANTALLA" + +#: +msgid "QUICK SYSTEM SELECT" +msgstr "SELECCIÓN RÃPIDA DE SISTEMA" + +#: +msgid "THEME SET" +msgstr "TEMA" + +#: +msgid "SOUND SETTINGS" +msgstr "CONFIGURACIÓN DE AUDIO" + +#: +msgid "SYSTEM VOLUME" +msgstr "VOLUMEN DE SISTEMA" + +#: +msgid "FRONTEND MUSIC" +msgstr "MÚSICA DE FONDO" + +#: +msgid "OUTPUT DEVICE" +msgstr "SALIDA DE AUDIO" + +#: +msgid "HDMI" +msgstr "HDMI" + +#: +msgid "JACK" +msgstr "JACK" + +#: +msgid "AUTO" +msgstr "AUTO" + +#: +msgid "NETWORK SETTINGS" +msgstr "CONFIGURACIÓN DE RED" + +#: +msgid "CONNECTED" +msgstr "CONECTADO" + +#: +msgid "NOT CONNECTED" +msgstr "NO CONECTADO" + +#: +msgid "STATUS" +msgstr "ESTADO" + +#: +msgid "IP ADDRESS" +msgstr "DIRECCIÓN IP" + +#: +msgid "HOSTNAME" +msgstr "NOMBRE DE HOST" + +#: +msgid "ENABLE WIFI" +msgstr "ACTIVAR WIFI" + +#: +msgid "WIFI SSID" +msgstr "SSID WIFI" + +#: +msgid "WIFI KEY" +msgstr "CLAVE WIFI" + +#: +msgid "WIFI ENABLED" +msgstr "WIFI ACTIVADO" + +#: +msgid "WIFI CONFIGURATION ERROR" +msgstr "ERROR DE CONFIGURACIÓN WIFI" + +#: +msgid "SCRAPER" +msgstr "SCRAPER" + +#: +msgid "SCRAPE FROM" +msgstr "OBTENER DATOS DE" + +#: +msgid "SCRAPE RATINGS" +msgstr "OBTENER CALIFICACIONES" + +#: +msgid "SCRAPE NOW" +msgstr "OBTENER DATOS AHORA" + +#: +msgid "QUIT" +msgstr "SALIR" + +#: +msgid "REALLY RESTART?" +msgstr "¿REINICIAR?" + +#: +msgid "RESTART SYSTEM" +msgstr "REINICIAR SISTEMA" + +#: +msgid "REALLY SHUTDOWN?" +msgstr "¿APAGAR?" + +#: +msgid "SHUTDOWN SYSTEM" +msgstr "APAGAR SISTEMA" + +#: +msgid "Emulator" +msgstr "Emulador" + +#: +msgid "Core" +msgstr "Núcleo" + +#: +msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." +msgstr "SI SÓLO EXISTE UN MANDO, CONFIGURAR LAS DIRECCIONES Y OMITIR LA CONFIGURACIÓN, MANTENIENDO PRESIONADO UN BOTÓN.\n" +"SI CARECE DE BOTÓN PARA HOTKEY, UTILIZAR BOTÓN SELECT.\n" +"OMITIR BOTONES FALTANTES, MANTENIENDO PRESIONADO CUALQUIER OTRO BOTÓN.\n" +"LOS NOMBRES DE LOS BOTONES SE BASAN EN EL MANDO DE SNES." + +#. GUIMENU +#: +msgid "CONFIGURE A CONTROLLER" +msgstr "CONFIGURAR UN MANDO" + +#. Bluetooth +#: +msgid "CONTROLLER PAIRED" +msgstr "MANDO VINCULADO" + +#: +msgid "UNABLE TO PAIR CONTROLLER" +msgstr "IMPOSIBLE VINCULAR MANDO" + +#: +msgid "AN ERROR OCCURED" +msgstr "ERROR" + +#: +msgid "NO CONTROLLERS FOUND" +msgstr "NINGÚN MANDO DETECTADO" + +#: +msgid "PAIR A BLUETOOTH CONTROLLER" +msgstr "VINCULAR MANDO BLUETOOTH" + +#: +msgid "CONTROLLERS LINKS HAVE BEEN DELETED." +msgstr "MANDOS VINCULADOS BORRADOS" + +#: +msgid "FORGET BLUETOOTH CONTROLLERS" +msgstr "DESVINCULAR MANDOS BLUETOOTH" + +#: +msgid "INPUT P%i" +msgstr "ENTRADA P%i" + +#: +msgid "CHOOSE" +msgstr "SELECCIONAR" + +#: +msgid "SELECT" +msgstr "SELECCIONAR" + +#: +msgid "OPTIONS" +msgstr "OPCIONES" + +#: +msgid "JUMP TO LETTER" +msgstr "IR A LETRA" + +#: +msgid "SORT GAMES BY" +msgstr "ORDENAR JUEGOS POR" + +#. FAVORITES +#: +msgid "FAVORITES ONLY" +msgstr "SOLO FAVORITOS" + +#: +msgid "EDIT THIS GAME'S METADATA" +msgstr "EDITAR DATOS DEL JUEGO" + +#: +msgid "SCRAPE THESE GAMES" +msgstr "OBTENER DATOS DE JUEGOS" + +#: +msgid "All Games" +msgstr "Todos los juegos" + +#. MISSING SCRAPPER TRANSLATIONS +#: +msgid "Only missing image" +msgstr "Solo imágenes faltantes" + +#: +msgid "FILTER" +msgstr "FILTRO" + +#: +msgid "SCRAPE THESE SYSTEMS" +msgstr "OBTENER SISTEMAS" + +#: +msgid "SYSTEMS" +msgstr "SISTEMAS" + +#: +msgid "USER DECIDES ON CONFLICTS" +msgstr "DECIDIR EN CASO DE CONFLICTOS" + +#: +msgid "START" +msgstr "INICIAR" + +#: +msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" +"CONTINUE ANYWAY?" +msgstr "CUIDADO: ALGÚN SISTEMA SELECCIONADO NO TIENE PLATAFORMA DEFINIDA.\n" +"¡LOS RESULTADOS PODRÃAN SER MÃS INCORRECTOS DE LO HABITUAL!\n" +"¿ACEPTAR?" + +#: +msgid "NO GAMES FIT THAT CRITERIA." +msgstr "NINGÚN JUEGO SE AJUSTA AL CRITERIO" + +#: +msgid "REALLY UPDATE?" +msgstr "¿ACTUALIZAR?" + +#: +msgid "NETWORK CONNECTION NEEDED" +msgstr "SE NECESITA CONEXIÓN DE RED" + +#. Lo correcto sería: +#. +#. ACTUALIZACIÓN DESCARGADA, EL SISTEMA SE REINICIARà +#: +msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" +msgstr "ACTUALIZACIÓN DESCARGADA, EL SISTEMA SE REINICIARÃ" + +#. Lo correcto sería: +#. +#. ACTUALIZACIÓN FALLIDA, EL SISTEMA SE REINICIARà +#: +msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" +msgstr "ACTUALIZACIÓN FALLIDA, EL SISTEMA SE REINICIARÃ" + +#: +msgid "NO UPDATE AVAILABLE" +msgstr "SIN ACTUALIZACIONES DISPONIBLES" + +#: +msgid "enter emulator" +msgstr "ingresar emulador" + +#: +msgid "enter core" +msgstr "ingresar núcleo" + +#: +msgid "Ratio" +msgstr "Proporción" + +#: +msgid "enter ratio" +msgstr "ingresar proporción" + +#: +msgid "Name" +msgstr "Nombre" + +#: +msgid "enter game name" +msgstr "ingresar nombre del juego" + +#: +msgid "Description" +msgstr "Descripción" + +#: +msgid "enter description" +msgstr "ingresar descripción" + +#: +msgid "Image" +msgstr "Imagen" + +#: +msgid "enter path to image" +msgstr "ingresar ruta de imagen" + +#: +msgid "Thumbnail" +msgstr "Miniatura" + +#: +msgid "enter path to thumbnail" +msgstr "ingresar ruta de miniatura " + +#. the english translation has a space befor the word "enter", typo? +#: +msgid "enter rating" +msgstr " ingresar calificación" + +#: +msgid "Release date" +msgstr "Fecha de lanzamiento" + +#: +msgid "enter release date" +msgstr "ingresar fecha de lanzamiento" + +#: +msgid "enter game developer" +msgstr "ingresar desarrollador" + +#. La palabra correcta para "publisher" es "editor". +#: +msgid "enter game publisher" +msgstr "ingresar editor" + +#: +msgid "enter game genre" +msgstr "ingresar género" + +#: +msgid "enter number of players" +msgstr "ingresar número de jugadores" + +#: +msgid "Favorite" +msgstr "Favorito" + +#: +msgid "enter favorite" +msgstr "ingresar favorito" + +#: +msgid "Region" +msgstr "Región" + +#: +msgid "enter region" +msgstr "ingresar región" + +#: +msgid "Romtype" +msgstr "Tipo de ROM" + +#: +msgid "enter romtype" +msgstr "ingresar tipo de ROM" + +#: +msgid "Hidden" +msgstr "Oculto" + +#: +msgid "set hidden" +msgstr "ocultar" + +#: +msgid "Play count" +msgstr "Partidas jugadas" + +#: +msgid "enter number of times played" +msgstr "ingresar partidas jugadas" + +#: +msgid "Last played" +msgstr "Última partida" + +#: +msgid "enter last played date" +msgstr "ingresar fecha de última partida" + +#: +msgid "%i GAME AVAILABLE" +msgid_plural "%i GAMES AVAILABLE" +msgstr[0] "%i JUEGO DISPONIBLE" +msgstr[1] "%i JUEGOS DISPONIBLES" + +#: +msgid "%i FAVORITE" +msgid_plural "%i FAVORITES" +msgstr[0] "%i FAVORITO" +msgstr[1] "%i FAVORITOS" + +#. Imagino que aquí queda mejor utilizar el infinitivo, comparandolo con la siguiente entrada... +#: +msgid "SCROLL" +msgstr "DESPLAZAR" + +#: +msgid "LAUNCH" +msgstr "LANZAR" + +#: +msgid "Times played" +msgstr "Veces jugado" + +#: +msgid "MENU" +msgstr "MENÚ" + +#: +msgid "FILENAME, ASCENDING" +msgstr "NOMBRE, ASCENDENTE" + +#: +msgid "FILENAME, DESCENDING" +msgstr "NOMBRE, DESCENDENTE" + +#: +msgid "RATING, ASCENDING" +msgstr "CALIFICACIÓN, ASCENDENTE" + +#: +msgid "RATING, DESCENDING" +msgstr "CALIFICACIÓN, DESCENDENTE" + +#: +msgid "TIMES PLAYED, ASCENDING" +msgstr "PARTIDAS, ASCENDENTE" + +#: +msgid "TIMES PLAYED, DESCENDING" +msgstr "PARTIDAS, DESCENDENTE" + +#: +msgid "LAST PLAYED, ASCENDING" +msgstr "ÚLTIMA PARTIDA, ASCENDENTE" + +#: +msgid "LAST PLAYED, DESCENDING" +msgstr "ÚLTIMA PARTIDA, DESCENDENTE" + +#: +msgid "WORKING..." +msgstr "TRABAJANDO..." + +#: +msgid "CHANGE" +msgstr "CAMBIAR" + +#: +msgid "never" +msgstr "nunca" + +#: +msgid "just now" +msgstr "ahora mismo" + +#: +msgid "%i sec ago" +msgid_plural "%i secs ago" +msgstr[0] "Hace %i segundo" +msgstr[1] "Hace %i segundos" + +#: +msgid "%i min ago" +msgid_plural "%i mins ago" +msgstr[0] "Hace %i minuto" +msgstr[1] "Hace %i minutos" + +#: +msgid "%i hour ago" +msgid_plural "%i hours ago" +msgstr[0] "Hace %i hora" +msgstr[1] "Hace %i horas" + +#: +msgid "%i day ago" +msgid_plural "%i days ago" +msgstr[0] "Hace %i día" +msgstr[1] "Hace %i días" + +#: +msgid "unknown" +msgstr "desconocido" + +#: +msgid "SELECT ALL" +msgstr "SELECCIONAR TODO" + +#: +msgid "SELECT NONE" +msgstr "SELECCIONAR NINGUNO" + +#: +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i SELECCIONADO" +msgstr[1] "%i SELECCIONADOS" + +#: +msgid "UP" +msgstr "ARRIBA" + +#: +msgid "DOWN" +msgstr "ABAJO" + +#: +msgid "LEFT" +msgstr "IZQUIERDA" + +#: +msgid "RIGHT" +msgstr "DERECHA" + +#. @Claudio, mando se refiere a todo el control, y no queda claro que se trata del joystick/palanca. Voy a volver a poner joystick a todos estos. +#: +msgid "JOYSTICK 1 UP" +msgstr "JOYSTICK 1 ARRIBA" + +#: +msgid "JOYSTICK 1 LEFT" +msgstr "JOYSTICK 1 IZQUIERDA" + +#: +msgid "JOYSTICK 2 UP" +msgstr "JOYSTICK 2 ARRIBA" + +#: +msgid "JOYSTICK 2 LEFT" +msgstr "JOYSTICK 2 IZQUIERDA" + +#. Creo que esta traducción no es correcta, ya que al configurar los botones L1 y R1 debería ser: +#. +#. L1: PÃGINA ANTERIOR +#. R1: PÃGINA SIGUIENTE +#: +msgid "PAGE UP" +msgstr "PÃGINA ARRIBA" + +#: +msgid "PAGE DOWN" +msgstr "PÃGINA ABAJO" + +#: +msgid "HOTKEY" +msgstr "HOTKEY" + +#: +msgid "CONFIGURING" +msgstr "CONFIGURANDO" + +#: +msgid "KEYBOARD" +msgstr "TECLADO" + +#: +msgid "GAMEPAD %i" +msgstr "MANDO %i" + +#. Config controllers missing translation +#: +msgid "PRESS ANYTHING" +msgstr "PULSAR CUALQUIER BOTÓN" + +#: +msgid "ALREADY TAKEN" +msgstr "EN USO" + +#: +msgid "DISCARD CHANGES" +msgstr "DESCARTAR CAMBIOS" + +#: +msgid "WELCOME" +msgstr "BIENVENIDO" + +#: +msgid "CONFIGURE INPUT" +msgstr "CONFIGURAR MANDOS" + +#: +msgid "%i GAMEPAD DETECTED" +msgid_plural "%i GAMEPADS DETECTED" +msgstr[0] "%i MANDO DETECTADO" +msgstr[1] "%i MANDOS DETECTADOS" + +#: +msgid "NO GAMEPADS DETECTED" +msgstr "SIN MANDOS DETECTADOS" + +#: +msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." +msgstr "MANTENER PRESIONADO UN BOTÓN DEL MANDO PARA CONFIGURARLO" + +#: +msgid "PRESS F4 TO QUIT AT ANY TIME." +msgstr "PRESIONAR F4 PARA SALIR EN CUALQUIER MOMENTO" + +#: +msgid "PRESS ESC OR THE HOTKEY TO CANCEL." +msgstr "PRESIONAR ESC O HOTKEY PARA CANCELAR" + +#: +msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" +msgstr "¿INICIAR EL CENTRO MULTIMEDIA KODI?" + +#: +msgid "LOADING..." +msgstr "CARGANDO..." + +#: +msgid "PLEASE WAIT..." +msgstr "ESPERAR..." + +#: +msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" +msgstr "¿APAGAR SIN GUARDAR DATOS?" + +#: +msgid "FAST SHUTDOWN SYSTEM" +msgstr "APAGAR RÃPIDO" + +#. Please change RECALBOX.FR to RECALBOX.COM on the English version! +#: +msgid "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 RECALBOX.FR FOR MORE INFORMATION." +msgstr "¡NO SE HAN ENCONTRADO SISTEMAS!\n" +"COMPROBAR RUTAS EN ARCHIVO DE CONFIGURACIÓN.\n" +"LA CARPETA DE JUEGOS DEBE CONTENER AL MENOS UNO Y CON LA EXTENSIÓN CORRECTA.\n" +"\n" +"VISITAR RECALBOX.COM PARA OBTENER MÃS INFORMACIÓN" + +#: +msgid "ON SCREEN KEYBOARD" +msgstr "TECLADO EN PANTALLA" + +#: +msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" +msgstr "SHIFT PARA MAYÚSCULAS, MINÚSCULAS Y ESPECIALES" + +#: +msgid "SPACE" +msgstr "ESPACIO" + +#: +msgid "DELETE A CHAR" +msgstr "BORRAR UN CARÃCTER" + +#: +msgid "SHIFT" +msgstr "SHIFT" + +#: +msgid "STOP EDITING" +msgstr "TERMINAR EDICIÓN" + +#: +msgid "MOVE CURSOR" +msgstr "MOVER CURSOR" + +#: +msgid "EDIT" +msgstr "EDITAR" + +#: +msgid "ACCEPT RESULT" +msgstr "ACEPTAR RESULTADO" + +#: +msgid "FILENAME" +msgstr "NOMBRE DEL ARCHIVO" + +#: +msgid "RATING" +msgstr "CLASIFICACIÓN" + +#: +msgid "TIMES PLAYED" +msgstr "JUGADO" + +#: +msgid "LAST PLAYED" +msgstr "ÚLTIMA VEZ JUGADO" + +#: +msgid "NUMBER OF PLAYERS" +msgstr "NÚMERO DE JUGADORES" + +#: +msgid "DEVELOPER" +msgstr "DESARROLLADOR" + +#: +msgid "GENRE" +msgstr "GÉNERO" + +#: +msgid "SHOW HIDDEN" +msgstr "MOSTRAR OCULTOS" + +#: +msgid "EXTREM (1400Mhz)" +msgstr "EXTREMO (1400Mhz)" + +#: +msgid "TURBO (1350Mhz)" +msgstr "TURBO (1350Mhz)" + +#: +msgid "HIGH (1300Mhz)" +msgstr "ALTO (1300Mhz)" + +#: +msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" +"IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." +msgstr "ESTABLECER EL OVERCLOCK EN TURBO Y EXTREMO, PUEDE CAUSAR INESTABILIDAD DEL SISTEMA.\n" +"NO USAR SIN ESTAR SEGURO.\n" +"AL CONTINUAR, EL SISTEMA SE REINICIARÃ." + +#: +msgid "%i GAME HIDDEN" +msgid_plural "%i GAMES HIDDEN" +msgstr[0] "%i JUEGO OCULTO" +msgstr[1] "%i JUEGOS OCULTOS" + +#: +msgid "Start kodi media player." +msgstr "Iniciar KODI" + +#: +msgid "Select the language for your recalbox, select an external drive to store your games and configurations, check your current version and the free space on your drive" +msgstr "Seleccionar idioma, seleccionar disco externo para guardar juegos y configuraciones, comprobar versión actual y espacio libre" + +#: +msgid "Shows your current recalboxOS version." +msgstr "Mostrar versión actual de RecalboxOS." + +#: +msgid "Show how much space is used on your SHARE partition, located either on the SDCARD or on an external drive. The information shows how much GB are used and how much GB your storage has overall (example 13GB/26GB)." +msgstr "Mostrar espacio usado en partición SHARE, tanto en SD como en disco externo. La información se muestra en GB usados/total (ejemplo 13GB/26GB)." + +#: +msgid "Select an external drive to store your roms, saves, configurations etc.\n" +"Use a FAT32 formatted drive. The system does not format the drive. On first boot, with this option enabled, recalbox will create a '/recalbox' folder with all system files inside." +msgstr "Seleccionar dispositivo externo para guardar juegos, partidas guardadas, configuraciones y otros.\n" +"Dispositivo requerido en formato FAT32, no será formateado.\n" +"En el siguiente arranque, esta opción crea en el dispositivo externo, una carpeta llamada '/recalbox' con todos los archivos requeridos" + +#: +msgid "Select your language. A reboot is needed to set this configuration active." +msgstr "Seleccionar idioma. Reiniciar para aplicar" + +#: +msgid "Manage your recalbox updates. Select the update type. Activate update check." +msgstr "Gestionar actualizaciones. Seleccionar tipo de actualización. Activar revisión de actualizaciones" + +#: +msgid "Check if an update is available, and start the update process." +msgstr "Revisar si hay actualizaciones disponibles, e iniciar proceso de actualización" + +#. We must change the issues page from github to gitlab on every language! +#: +msgid "Stable updates will check for updates on stable recalbox releases. Stable updates are tested and approved by the recalbox team and their testers.\n" +"Unstable updates allows you to get the latest recalbox features by checking our unstable repository. You can test and validate with us the very last version of recalbox.\n" +"If you choose unstable update, be so kind to report issues on the recalbox-os issue board (https://github.com/recalbox/recalbox-os/issues)" +msgstr "Actualizaciones estables: Las actualizaciones estables se comprueban y aprueban por el equipo de Recalbox y sus testers.\n" +"Actualizaciones inestables: Permiten obtener últimas características de recalbox comprobando nuestro depósito de inestables. Se puede probar y validar con nosotros la última versión de Recalbox.\n" +"Al seleccionar una actualización inestable, por favor reportar los problemas en la plataforma de desarrollo de RecalboxOS (https://gitlab.com/recalbox/recalbox/issues)" + +#. Creo que así queda más parecido al significado original +#: +msgid "Automatically check if an update is avaialble. If so, it notifies you with a message." +msgstr "Comprueba automáticamente si hay alguna actualización disponible. Si es así, se notifica con un mensaje" + +#: +msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." +msgstr "Configurar visualización de juegos, relación de aspecto, filtros (shaders), autoguardado y autocargado, y cuenta de retrologros" + +#: +msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." +msgstr "La relación de aspecto, es la relación entre ancho y alto de imagen.\n" +"Configurar AUTO para que el emulador seleccione la relación de aspecto original del juego, proporcionando la mejor experiencia de retrojuego" + +#: +msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." +msgstr "Suavizar imagen del juego. Esta opción proporciona la imagen más suave, usando filtros bilineales" + +#: +msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." +msgstr "Esta opción permite rebobinar el juego. Usar el botón HOTKEY+IZQUIERDA en el juego para rebobinar" + +#: +msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." +msgstr "Autoguardar la partida al salir del juego, y autocargar la partida al iniciar el juego." + +#: +msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." +msgstr "La escala entera, es el escalado por un factor de un número entero, como 2x, 3x, 4x, etc. Esta opción escala la imagen hasta la escala entera más grande por debajo de la resolución establecida. Por ejemplo, al establecer la resolución de pantalla completa en 1920x1080 y se habilita el escalado entero, sólo escalará una imagen de 320x240 hasta 1280x960 y dejará los bordes negros alrededor. Esto es para mantener una relación de aspecto de píxeles de 1:1 con la imagen original, de modo que los píxeles no se dupliquen de manera desigual" + +#: +msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." +msgstr "Los shaders equivalen a filtros para el renderizado del juego. Puede seleccionar un conjunto de shaders, que es una colección de shaders seleccionados para cada sistema. También se puede cambiar el shader dentro del juego con HOTKEY+L2 o HOTKEY+R2" + +#: +msgid "Enable or disable RetroAchievements in games." +msgstr "Activar o desactivar los retrologros en los juegos" + +#: +msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." +msgstr "El modo hardcore deshabilita toda capacidad de guardar partidas y funciones de rebobinado. Se debe terminar el juego y conseguir logros de una sola vez, igual que en la máquina original. En compensación, se conseguirán tanto los logros estándar y hardcore, y el doble de puntos. Un juego regular de 400 puntos, generará 800 si se completa en hardcore" + +#: +msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" +msgstr "¡La página web retroachievements.org propone desafíos/logros/trofeos en plataformas como NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine y más! ¡Crear cuenta en retroachievements.org y empezar búsqueda de logros!" + +#: +msgid "Add and configure up to 5 controllers." +msgstr "Agregar y configurar hasta 5 mandos" + +#: +msgid "Pair a bluetooth controller with your recalbox. Your controller must be in pairing mode." +msgstr "Vincular mando por bluetooth. El mando debe estar en modo visible" + +#: +msgid "Forget all paired bluetooth controllers. You will have to pair your controllers again, but this option can help if you have issues to reconnect a controller, which is already paired." +msgstr "Olvidar todos los mandos vinculados por bluetooth. Tendrás que vincularlos nuevamente, pero esta opción puede ayudarte si tienes problemas al reconectar un mando que ya está vinculado" + +#: +msgid "Configure your EmulationStation experience. Select transition types, help prompts, screensaver behavior. You can also deactivate the onscreen keyboard if you have a real keyboard plugged into your recalbox.\n" +"If you've added games since the last boot, you can also refresh the gamelist from this menu." +msgstr "Configurar experiencia con EmulationStation. Seleccionar tipos de transición, mensajes de ayuda, comportamiento del salvapantallas. También se puede desactivar el teclado virtual en caso de tener uno de verdad conectado a Recalbox.\n" +"Si se han agregado juegos desde el último arranque, también se puede actualizar la lista de juegos desde este menú" + +#: +msgid "Start the screensaver after N minutes." +msgstr "Iniciar el salvapantallas tras N minutos" + +#: +msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." +msgstr "Configurar el comportamiento del protector de pantalla. DIM reducirá el brillo de pantalla, y BLACK pondrá la pantalla en negro" + +#: +msgid "Shows a help at the bottom of the screen which displays commands you can use." +msgstr "Desplegar una ayuda en la parte inferior de la pantalla que enseña comandos a usar" + +#: +msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." +msgstr "Activado, permite cambiar de sistema pulsando IZQUIERDA o DERECHA en la lista de juegos" + +#: +msgid "The onscreen keyboard is necessary to type text if you only have controllers plugged into your recalbox. You can disable it if you have a real keyboard connected." +msgstr "El teclado virtual es necesario para escribir si existen sólo mandos conectados. Desactivar en caso de tener un teclado real conectado" + +#: +msgid "Select a theme for your recalbox." +msgstr "Seleccionar tema para Recalbox" + +#: +msgid "Updates the gamelists, if you added games since the last boot." +msgstr "Actualizar lista de juegos, en caso de haber agregado juegos desde el último inicio del sistema" + +#: +msgid "Configure the sound options of your recalbox." +msgstr "Configuras las opciones de sonido de Recalbox" + +#: +msgid "Set the volume of the sound output for the frontend and the games." +msgstr "Configurar volumen para salida de audio en interfaz y juegos" + +#: +msgid "Enable or disable the frontend music. You can add your own music as mp3, or ogg format in the 'musics' directory of your recalbox." +msgstr "Activar o desactivar música de interfaz. Permite agregar música propia en formato mp3 u ogg en la carpeta 'musics' de Recalbox" + +#: +msgid "Select your output device. Only HDMI and JACK are supported." +msgstr "Seleccionar dispositivo de salida. Sólo compatible con HDMI y JACK" + +#: +msgid "Configure the network options of your recalbox.\n" +"Check your network status and IP address, set the hostname and configure the WIFI." +msgstr "Configurar opciones de red.\n" +"Comprobar estado de red y dirección IP, establecer nombre de host y configurar WIFI" + +#: +msgid "Displays CONNECTED, if you are connected, by checking if your recalbox can access the recalbox.com update server." +msgstr "Mostrar CONECTADO, comprobando si hay acceso al servidor de actualizaciones" + +#: +msgid "The IP address of your recalbox within your local network." +msgstr "Dirección IP de recalbox" + +#: +msgid "Enable or disable WIFI.\n" +"If you disable WIFI, the SSID and the WIFI passwords are saved and can be used when you reactivate it" +msgstr "Activar o desactivar WIFI.\n" +"Al desactivar WIFI, SSID y contraseñas son guardadas y utilizadas al reactivar" + +#: +msgid "The name of your recalbox in your local network" +msgstr "Nombre de Recalbox en la red de área local" + +#: +msgid "SSID (WIFI Name) of your network." +msgstr "SSID (Nombre del WIFI) de la red" + +#: +msgid "Private key of your WIFI network." +msgstr "Contraseña WIFI" + +#: +msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." +msgstr "Obtener información e imágenes de juegos. El scraper descarga información e imágenes de juegos desde diferentes servidores y mejora la experiencia de usuario completamente en EmulationStation" + +#: +msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." +msgstr "Seleccionar un servidor desde donde descargar información. Se recomienda el servidor SCREENSCRAPER que está basado en www.screenscraper.fr y descarga datos de juegos en tu idioma, si está disponible" + +#: +msgid "Begin the scrape process with the configuration shown below." +msgstr "Comienza el proceso de descarga con la configuración siguiente" + +#: +msgid "Scrape and display game ratings." +msgstr "Descargar y mostrar las puntuaciones del juego" + +#: +msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." +msgstr "Configuraciones avanzadas. Asegúrate de que sabes lo que haces antes de cambiar cualquier valor en este menú" + +#: +msgid "Overclock your board to increase the performance.\n" +"Overclock settings are tested and validated by the community. Keep in mind that overclocking your board can void your warranty." +msgstr "Aumentar la velocidad del reloj para incrementar el rendimiento.\n" +"Estas configuraciones están probadas y validadas por la comunidad.\n" +"Considerar que al aumentar la velocidad del reloj es posible perder la garantía" + +#: +msgid "Select which system to show when the recalbox frontend starts. The default value is 'favorites'." +msgstr "Seleccionar qué sistema se muestra cuando empieza la interfaz del Recalbox. El valor por defecto es 'favoritos'" + +#: +msgid "On boot, recalbox will show the list of games of the selected system rather than the system view." +msgstr "En el arranque, Recalbox mostrará la lista de juegos del sistema seleccionado, no la vista de todos sistemas" + +#: +msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" +"This option highly speeds up boot time, but new games will not be detected." +msgstr "Mostrar sólo los juegos que están en el archivo gamelist.xml (situado en las carpetas de los ROMs).\n" +"Esta opción acelera el tiempo de arranque, pero no detectará nuevos juegos" + +#: +msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." +msgstr "Esta opción te permite establecer el sistema seleccionado en modo fijo. Con esta opción activada, el usuario no puede acceder a otros sistemas" + +#: +msgid "Always display the basic gamelist view, even if you have scraped your games." +msgstr "Mostrar siempre la lista de juegos básica, incluso si has scrapeado tus juegos" + +#: +msgid "Override global options like emulator, core, ratio and more for each available system in your recalbox." +msgstr "Anula opciones globales tales como emulador, núcleo, relación de aspecto y más, para cada sistema disponible" + +#: +msgid "Configure boot options that make your recalbox boot straight into a system or into Kodi, lock a user to a single system, or directly show the gamelist." +msgstr "Configurar las opciones de arranque para que Recalbox inicie directamente en un sistema definido, iniciar en Kodi, bloquear un usuario para un solo sistema, o directamente mostrar la lista de juegos" + +#: +msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" +msgstr "Activar o desactivar KODI, personalizar el arranque de KODI, activar el botón X para iniciar KODI" + +#: +msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." +msgstr "Activa o desactiva Kodi. Si Kodi está desactivado, no podrá ser iniciado con el botón X, o automáticamente al arrancar. El menú de entrada será desactivado" + +#: +msgid "Use the X button to start Kodi." +msgstr "Usar el botón X para iniciar Kodi" + +#: +msgid "Automatically start into Kodi on boot." +msgstr "Iniciar Kodi automáticamente en el arranque" + +#: +msgid "Manage your recalbox security." +msgstr "Administrar la seguridad de Recalbox" + +#: +msgid "Change the SSH root password." +msgstr "Cambiar la contraseña root SSH" + +#: +msgid "Enforce recalbox security." +msgstr "Reforzar la seguridad de Recalbox" + +#. Ordenador? Computador? Computadora? ...en español existen varias acepciones para este tipo de dispositivo (Windows en español lo define como "PC" simplemente, ahora en Windows 10, utiizan el término "Equipo"... Windows 98/Me/2000/XP se refería a "sistema") +#: +msgid "Enable or disable overscan.\n" +"Overscan can help you, if you have a black border, or if the image is bigger than your screen. Before setting the overscan, try to configure your TV to have a 1:1 pixel output.\n" +"More overscan settings can be defined in the boot.txt file, available when you plug your SD card into your computer." +msgstr "Activar o desactivar overscan.\n" +"El overscan puede ayudar al presentar bordes negros, o en caso de imagen de mayor tamaño que la pantalla.\n" +"Antes de configurar overscan, intentar configurar la TV para tener una salida de píxeles 1:1.\n" +"Es posible definir más configuraciones de overscan en el archivo boot.txt, disponible al insertar la tarjeta SD en la computadora" + +#: +msgid "Show the framerate in EmulationStation and in game." +msgstr "Mostrar cuadros por segundo en EmulationStation y en el juego" + +#: +msgid "Enable or disable the Recalbox Manager.\n" +"The Recalbox Manager is a web application available on http://recalbox , if you are on windows, http://recalbox.local , if you are on Linux or Mac, or directly with your recalbox IP : http://192.168.1.XX.\n" +"You can configure many options from within the manager, and even manage games, saves, and scrapes!" +msgstr "Activar o desactivar el Administrador de Recalbox.\n" +"El Administrador de Recalbox es una aplicación web disponible en http://recalbox, en Windows, http://recalbox.local en Linux o Mac, o directamente con la IP de Recalbox: http://192.168.1.XX.\n" +"¡Es posible configurar muchas opciones desde el administrador, incluso juegos, partidas guardadas e información!" + +#: +msgid "Enable or disable the recalbox API.\n" +"The Recalbox API is a REST API exposing endpoints to control your recalbox via http requests." +msgstr "Activar o desactivar la API de Recalbox.\n" +"La API de Recalbox es una API REST que expone puntos finales para controlar Recalbox a través de solicitudes HTTP" + +#: +msgid "Select which emulator to use when you start a game for this system." +msgstr "Seleccionar qué emulador usar cuando se inicia un juego de este sistema" + +#: +msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." +msgstr "Seleccionar qué núcleo usar para el emulador seleccionado. Por ejemplo, el emulador LIBRETRO tiene varios núcleos para ejecutar juegos de Super Nintendo. La configuración del núcleo por defecto que se elija, también puede ser eliminada desde la configuración específica para juegos" + +#: +msgid "USE COMPOSED VISUALS" +msgstr "USAR IMÃGENES COMPUESTAS" + +#: +msgid "CHECK UPDATES" +msgstr "COMPROBAR ACTUALIZACIONES" + +#: +msgid "UPDATE TYPE" +msgstr "TIPO DE ACTUALIZACIÓN" + +#: +msgid "INTEGER SCALE (PIXEL PERFECT)" +msgstr "ESCALA COMPLETA (PIXEL PERFECT)" + +#: +msgid "ADVANCED SETTINGS" +msgstr "OPCIONES AVANZADAS" + +#: +msgid "BOOT SETTINGS" +msgstr "OPCIONES DE ARRANQUE" + +#: +msgid "GAMELIST ONLY" +msgstr "SÓLO LISTA DE JUEGOS" + +#: +msgid "BOOT ON SYSTEM" +msgstr "ARRANCAR EN SISTEMA" + +#: +msgid "BOOT ON GAMELIST" +msgstr "ARRANCAR EN LISTA DE JUEGOS" + +#: +msgid "HIDE SYSTEM VIEW" +msgstr "OCULTAR VISTA DE SISTEMA" + +#: +msgid "EMULATOR ADVANCED CONFIGURATION" +msgstr "CONFIGURACIÓN AVANZADA DEL EMULADOR" + +#: +msgid "ADVANCED EMULATOR CONFIGURATION" +msgstr "CONFIGURACIÓN DEL EMULADOR AVANZADA" + +#: +msgid "HELP" +msgstr "AYUDA" + +#: +msgid "THE SYSTEM IS UP TO DATE" +msgstr "EL SISTEMA ESTà ACTUALIZADO" + +#: +msgid "FORCE BASIC GAMELIST VIEW" +msgstr "FORZAR VISTA DE LISTA DE JUEGOS BÃSICA" + +#: +msgid "DOWNLOADED" +msgstr "DESCARGADO" + +#: +msgid "UPDATE VERSION:" +msgstr "VERSIÓN DE LA ACTUALIZACIÓN:" + +#: +msgid "UPDATE CHANGELOG:" +msgstr "REGISTRO DE CAMBIOS DE LA ACTUALIZACIÓN:" + +#: +msgid "MORE DETAILS" +msgstr "MÃS DETALLES" + +#: +msgid "CAROUSEL ANIMATION" +msgstr "ANIMACIÓN DEL CARRUSEL" + +#: +msgid "THEME CONFIGURATION" +msgstr "CONFIGURACIÓN DEL TEMA" + +#: +msgid "THEME COLORSET" +msgstr "CONJUNTO DE COLORES DEL TEMA" + +#: +msgid "THEME ICONSET" +msgstr "CONJUNTO DE ÃCONOS DEL TEMA" + +#: +msgid "THEME MENU" +msgstr "MENÚ DEL TEMA" + +#: +msgid "THEME SYSTEMVIEW" +msgstr "VISTA DE SISTEMAS DEL TEMA" + +#: +msgid "THEME GAMELISTVIEW" +msgstr "VISTA DE LISTA DE JUEGOS DEL TEMA" + +#: +msgid "THEME REGION" +msgstr "REGIÓN DEL TEMA" + +#: +msgid "THIS THEME HAS NO OPTION" +msgstr "ESTE TEMA NO TIENE OPCIONES" + +#: +msgid "MANUAL INPUT" +msgstr "ENTRADA MANUAL" + +#: +msgid "AN ERROR OCCURED - DOWNLOADED" +msgstr "HA HABIDO UN ERROR - DESCARGADO" + +#: +msgid "START KODI" +msgstr "INICIAR KODI" + +#: +msgid "Shows the current available update version." +msgstr "Mostrar la versión de actualización disponible actualmente" + +#: +msgid "Shows the current available update changelog." +msgstr "Mostrar el registro de cambios de actualización disponible actualmente" + +#: +msgid "Configure an associated controller. Your controller has to be associated / plugged before." +msgstr "Configurar mando conectado. El mando debe estar previamente vinculado/conectado" + +#: +msgid "Choose if carousel will be animated or not during transitions" +msgstr "Elegir si el carrusel estará animado o no durante transiciones" + +#: +msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" +msgstr "Seleccionar el tipo de transición que ocurre cuando comienza un juego. INSTANT no hará nada, FADE se desvanecerá a oscuro, y SLIDE hará un zoom en la cubierta del juego (o nombre, si no hay metadatos)" + +#: +msgid "Select exisiting colorset options for this theme." +msgstr "Seleccionar opciones de conjuntos de colores existentes para este tema" + +#: +msgid "Select exisiting iconset options for this theme." +msgstr "Seleccionar opciones de conjunto de iconos existentes para este tema" + +#: +msgid "Select exisiting menu style options for this theme." +msgstr "Seleccionar opciones de estilo de menú existentes para este tema" + +#: +msgid "Select exisiting system view options for this theme." +msgstr "Seleccionar opciones de vista de sistema existentes para este tema" + +#: +msgid "Select exisiting gamelist view options for this theme." +msgstr "Seleccionar opciones de vista de lista de juegos existentes para este tema" + +#: +msgid "Configure theme options if available." +msgstr "Configurar opciones del tema si está disponible" + +#: +msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" +msgstr "Seleccionar región de logotipos, imágenes para sistemas diferentes entre países Ejemplo, Megadrive en EU, Genesis en USA" + +#: +msgid "Type the name of your SSID if it is hidden or not listed" +msgstr "Ingresar nombre de SSID, si está oculto o no aparece" + +#: +msgid "Select a letter and the listing will go directly on the first game starting with this letter." +msgstr "Seleccionar una letra y la lista irá directamente al primer juego que comience con esta letra" + +#: +msgid "Select the way the game list is sortered (alphabetically, by notation...)." +msgstr "Seleccionar la forma en que se ordena la lista de juegos (alfabéticamente, mediante notación...)" + +#: +msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." +msgstr "Cambiar entre ver o no solo los juegos favoritos. Para agregar un juego a la lista de favoritos, seleccionar el juego y cambiar su estado con 'Y'" + +#: +msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." +msgstr "Cambiar entre ver o no los juegos ocultos. Para ocultar un juego, editar sus datos y seleccionar 'Ocultar'" + +#: +msgid "This option display a menu which allows to change game data and many others options." +msgstr "Esta opción muestra un menú que permite cambiar los datos del juego y muchas otras opciones" + +#: +msgid "AVAILABLE UPDATE" +msgstr "ACTUALIZACIÓN DISPONIBLE" + +#: +msgid "UPDATE CHANGELOG" +msgstr "REGISTRO DE CAMBIOS" + +#: +msgid "CLOCK IN MENU" +msgstr "RELOJ EN MENÚ" + +#: +msgid "Now playing" +msgstr "Reproduciendo ahora" + +#: +msgid "DEFAULT (%1%)" +msgstr "DEFECTO (%1%)" + +#: +msgid "INPUT REQUIRED" +msgstr "INGRESO REQUERIDO" + +#: +msgid "(skipped)" +msgstr "(omitido)" + +#: +msgid "UP/DOWN TO SKIP" +msgstr "ARRIBA/ABAJO PARA OMITIR" + +#. Explanation: you config the controller, you make a mistake (let's say right instead of left), you can press A to erase wrong config and redo +#: +msgid "A TO UNSET" +msgstr "A PARA CAMBIAR" + +#: +msgid "DOWN TO SKIP AND KEEP [%1%]" +msgstr "ABAJO PARA OMITIR Y MANTENER [%1%]" + +#: +msgid "UP/DOWN TO SKIP AND KEEP [%1%]" +msgstr "ARRIBA/ABAJO PARA OMITIR Y MANTENER [%1%]" + +#: +msgid "Set duration of help popups, 0 means no popup." +msgstr "Duración de popups de ayuda, 0 significa sin popup" + +#: +msgid "HELP POPUP DURATION" +msgstr "DURACIÓN DE POPUPS DE AYUDA" + +#: +msgid "Set duration of music popups, 0 means no popup." +msgstr "Duración de popups de música, 0 significa sin popup" + +#: +msgid "MUSIC POPUP DURATION" +msgstr "DURACIÓN DE POPUPS DE MÚSICA" + +#: +msgid "POPUP SETTINGS" +msgstr "CONFIGURACIÓN DE POPUPS" + +#: +msgid "POPUP POSITION" +msgstr "POSICIÓN DE POPUPS" + +#: +msgid "Select the position of popups on screen." +msgstr "Seleccionar posición de popups en pantalla" + +#: +msgid "Set position and duration of popups." +msgstr "Posición y duración de popups" + +#: +msgid "TOP/RIGHT" +msgstr "ARRIBA/DERECHA" + +#: +msgid "BOTTOM/RIGHT" +msgstr "ABAJO/DERECHA" + +#: +msgid "BOTTOM/LEFT" +msgstr "ABAJO/IZQUIERDA" + +#: +msgid "TOP/LEFT" +msgstr "ARRIBA/IZQUIERDA" + +#: +msgid "SHOW FOLDERS CONTENT" +msgstr "MOSTRAR CONTENIDO DE CARPETAS" + +#: +msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." +msgstr "Cambiar entre mostrar estructura de carpetas o ver todos los juegos en un único nivel superior" + +#: +msgid "NETPLAY" +msgstr "NETPLAY" + +#: +msgid "NETPLAY SETTINGS" +msgstr "CONFIGURACIÓN DE NETPLAY" + +#: +msgid "NETPLAY LOBBY" +msgstr "SALA DE ESPERA DE NETPLAY" + +#: +msgid "Enable or disable Netplay in games." +msgstr "Habilitar o deshabilitar Netplay en juegos" + +#: +msgid "PORT" +msgstr "PUERTO" + +#: +msgid "NICKNAME" +msgstr "ALIAS" + +#: +msgid "RELAY SERVER" +msgstr "SERVIDOR DE RETRANSMISIÓN" + +#: +msgid "Enable or disable connections throught relay servers." +msgstr "Habilitar o deshabilitar conexiones mediante servidores de retransmisión" + +#: +msgid "KODI/NETPLAY" +msgstr "KODI/NETPLAY" + +#: +msgid "NO GAMES OR NO CONNECTION" +msgstr "SIN JUEGOS O SIN CONEXIÓN" + +#: +msgid "HASH NOW" +msgstr "CALCULAR HASH AHORA" + +#: +msgid "HASH THESE SYSTEMS" +msgstr "CALCULAR HASH PARA ESTOS SISTEMAS" + +#: +msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." +msgstr "Agregar hash de roms a listas de juegos para obtener resultados más precisos en Netplay." + +#: +msgid "HASH ROMS" +msgstr "CALCULAR HASH DE ROMS" + +#: +msgid "Only missing hashs" +msgstr "Solo hash faltantes" + +#: +msgid "Username" +msgstr "Usuario" + +#: +msgid "Country" +msgstr "País" + +#: +msgid "Latency" +msgstr "Latencia" + +#: +msgid "Host arch." +msgstr "Arq. del anfitrión" + +#: +msgid "Core ver." +msgstr "Ver. núcleo" + +#: +msgid "RA ver." +msgstr "Ver. RA" + +#: +msgid "Can join" +msgstr "Puede unirse" + +#: +msgid "Rom and core match" +msgstr "Rom y núcleo coincidente" + +#: +msgid "Rom found" +msgstr "Rom encontrado" + +#: +msgid "No rom match" +msgstr "Rom sin coincidencia" + +#: +msgid "Match" +msgstr "Coincidencia" + +#: +msgid "No Match" +msgstr "Sin coincidencia" + +#: +msgid "Rom file" +msgstr "Archivo rom" + +#: +msgid "Rom hash" +msgstr "Hash del rom" + +#: +msgid "THIS COULD TAKE A WHILE, CONFIRM?" +msgstr "Esto podría tardar, ¿confirmar?" + +#: +msgid "good" +msgstr "bueno" + +#: +msgid "bad" +msgstr "malo" + +#: +msgid "medium" +msgstr "medio" + +#: +msgid "NETPLAY POPUP DURATION" +msgstr "DURACIÓN POPUPS PARA NETPLAY" + +#: +msgid "Set duration of netplay popups, 0 means no popup." +msgstr "Duración de popups para Netplay, 0 significa sin popups" + +#: +msgid "Player" +msgstr "Jugador" + +#: +msgid "Game" +msgstr "Juego" + +#: +msgid "A Recalbox friend has started a Netplay game!" +msgstr "!Un amigo de Recalbox ha iniciado un juego en Netplay!" + +#: +msgid "Play online on games running through Retroarch like NES, SNES, FBA, Genesis/Megadrive and more!" +msgstr "¡Juega en línea en juegos que funcionan con RetroArch como NES, SNES, FBA, Genesis/Megadrive y más!" + +#: +msgid "Rom, hash and core match" +msgstr "Rom, hash y núcleo coincidente" + +#: +msgid "No core match" +msgstr "Núcleo sin coincidencia" + +#: +msgid "Add a clock in the main menu." +msgstr "" + From c2574ad8be177a7c19ac81aba44110753e99a9e1 Mon Sep 17 00:00:00 2001 From: fabricecaruso <51082152+fabricecaruso@users.noreply.github.com> Date: Mon, 27 May 2019 14:06:41 +0200 Subject: [PATCH 003/336] Update README.md --- README.md | 446 +++++++++++++++++++++++++++++------------------------- 1 file changed, 242 insertions(+), 204 deletions(-) diff --git a/README.md b/README.md index 49ff0c5338..52e38b50b0 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,141 @@ -EmulationStation -================ - -This is a fork of EmulationStation for RetroPie. -EmulationStation is a cross-platform graphical front-end for emulators with controller navigation. - -Building -======== - -EmulationStation uses some C++11 code, which means you'll need to use at least g++-4.7 on Linux, or VS2010 on Windows, to compile. - -EmulationStation has a few dependencies. For building, you'll need CMake, SDL2, FreeImage, FreeType, cURL and RapidJSON. You also should probably install the `fonts-droid` package which contains fallback fonts for Chinese/Japanese/Korean characters, but ES will still work fine without it (this package is only used at run-time). - -**On Debian/Ubuntu:** -All of this be easily installed with `apt-get`: -```bash -sudo apt-get install libsdl2-dev libfreeimage-dev libfreetype6-dev libcurl4-openssl-dev rapidjson-dev \ - libasound2-dev libgl1-mesa-dev build-essential cmake fonts-droid-fallback libvlc-dev \ - libvlccore-dev vlc-bin -``` -**On Fedora:** -All of this be easily installed with `dnf` (with rpmfusion activated) : -```bash -sudo dnf install SDL2-devel freeimage-devel freetype-devel curl-devel \ - alsa-lib-devel mesa-libGL-devel cmake \ - vlc-devel rapidjson-devel -``` - -Note this Repository uses a git submodule - to checkout the source and all submodules, use - -```bash -git clone --recursive https://github.com/RetroPie/EmulationStation.git -``` - -or - -```bash -git clone https://github.com/RetroPie/EmulationStation.git -cd EmulationStation -git submodule update --init -``` - -Then, generate and build the Makefile with CMake: -```bash -cd YourEmulationStationDirectory -cmake . -make -``` - -**On the Raspberry Pi:** - -Complete Raspberry Pi build instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#install_rpi_standalone). - -**On Windows:** - -[FreeImage](http://downloads.sourceforge.net/freeimage/FreeImage3154Win32.zip) - -[FreeType2](http://download.savannah.gnu.org/releases/freetype/freetype-2.4.9.tar.bz2) (you'll need to compile) - -[SDL2](http://www.libsdl.org/release/SDL2-devel-2.0.8-VC.zip) - -[cURL](http://curl.haxx.se/download.html) (you'll need to compile or get the pre-compiled DLL version) - -[RapisJSON](https://github.com/tencent/rapidjson) (you'll need the `include/rapidsjon` added to the include path) - -(Remember to copy necessary .DLLs into the same folder as the executable: probably FreeImage.dll, freetype6.dll, SDL2.dll, libcurl.dll, and zlib1.dll. Exact list depends on if you built your libraries in "static" mode or not.) - -[CMake](http://www.cmake.org/cmake/resources/software.html) (this is used for generating the Visual Studio project) - -(If you don't know how to use CMake, here are some hints: run cmake-gui and point it at your EmulationStation folder. Point the "build" directory somewhere - I use EmulationStation/build. Click configure, choose "Visual Studio [year] Project", fill in red fields as they appear and keep clicking Configure (you may need to check "Advanced"), then click Generate.) - - -Configuring -=========== - -**~/.emulationstation/es_systems.cfg:** -When first run, an example systems configuration file will be created at `~/.emulationstation/es_systems.cfg`. `~` is `$HOME` on Linux, and `%HOMEPATH%` on Windows. This example has some comments explaining how to write the configuration file. See the "Writing an es_systems.cfg" section for more information. - -**Keep in mind you'll have to set up your emulator separately from EmulationStation!** - -**~/.emulationstation/es_input.cfg:** -When you first start EmulationStation, you will be prompted to configure an input device. The process is thus: - -1. Hold a button on the device you want to configure. This includes the keyboard. - -2. Press the buttons as they appear in the list. Some inputs can be skipped by holding any button down for a few seconds (e.g. page up/page down). - -3. You can review your mappings by pressing up and down, making any changes by pressing A. - -4. Choose "SAVE" to save this device and close the input configuration screen. - -The new configuration will be added to the `~/.emulationstation/es_input.cfg` file. - -**Both new and old devices can be (re)configured at any time by pressing the Start button and choosing "CONFIGURE INPUT".** From here, you may unplug the device you used to open the menu and plug in a new one, if necessary. New devices will be appended to the existing input configuration file, so your old devices will remain configured. - -**If your controller stops working, you can delete the `~/.emulationstation/es_input.cfg` file to make the input configuration screen re-appear on next run.** - - -You can use `--help` or `-h` to view a list of command-line options. Briefly outlined here: -``` +EmulationStation +================ + +This is a fork of EmulationStation from jrassa. +EmulationStation is a cross-platform graphical front-end for emulators with controller navigation. + +Changes in my branch +==================== + +- Support for Multiple Emulators/Cores in es_systems.cfg, and setting Emulator/Core per game. + + %HOME%\RetroArch\retroarch.exe -L %HOME%\RetroArch\cores\%CORE%_libretro.dll %ROM% + + + + mame2003_plus + mame2003 + + + + + fbalpha2012 + + + + +- Better Grid view (animations, layout, selection...) +- Created a 'GridEx' mode, which inherits from grid, in order to create a 'preview bar' including a game screenshot. +- Changed 'Automatic' view mode. Switchs to grid if "thumbnails" exist in gamelist. Switchs to gridex if "thumbnails" and "image" exist in gamelist. +- Don't show Directories that contains only one Game : just Show the game. +- Don't show Games what are marked Hidden in gamelist. +- Cleaned menus + changed menu item order (by interest). Simplified "Quit" menu item ( no more popup ) +- Case insensitive file extensions. +- Skip parsing 'downloaded_images' and 'media' folders ( loading time boost ) +- Localisation (French actually supported) +- Corrected favorites ( and custom lists ) management. +- Don't load all fields in Medadata Editor ( too tricky to use on windows, better use an external tool ). +- Windows is now "Windowed No border" by default. On Windows, Exclusive fullscreen can be annoying... +- Theming : Ability to force default view ( attribute defaultView ) +- Windows : Stop using _wsystem for launching games. Run games with ShellExecuteEx instead ( avoids command window ) +- Add an option to leave ES open with a black screen" Chargement en cours..." when launching games ( avoids showing windows desktop ) + +Je crois que c'est à peu près tout.... + +Building +======== + +EmulationStation uses some C++11 code, which means you'll need to use at least g++-4.7 on Linux, or VS2010 on Windows, to compile. + +EmulationStation has a few dependencies. For building, you'll need CMake, SDL2, FreeImage, FreeType, cURL and RapidJSON. You also should probably install the `fonts-droid` package which contains fallback fonts for Chinese/Japanese/Korean characters, but ES will still work fine without it (this package is only used at run-time). + +**On Debian/Ubuntu:** +All of this be easily installed with `apt-get`: +```bash +sudo apt-get install libsdl2-dev libfreeimage-dev libfreetype6-dev libcurl4-openssl-dev rapidjson-dev \ + libasound2-dev libgl1-mesa-dev build-essential cmake fonts-droid-fallback libvlc-dev \ + libvlccore-dev vlc-bin +``` +**On Fedora:** +All of this be easily installed with `dnf` (with rpmfusion activated) : +```bash +sudo dnf install SDL2-devel freeimage-devel freetype-devel curl-devel \ + alsa-lib-devel mesa-libGL-devel cmake \ + vlc-devel rapidjson-devel +``` + +Note this Repository uses a git submodule - to checkout the source and all submodules, use + +```bash +git clone --recursive https://github.com/RetroPie/EmulationStation.git +``` + +or + +```bash +git clone https://github.com/RetroPie/EmulationStation.git +cd EmulationStation +git submodule update --init +``` + +Then, generate and build the Makefile with CMake: +```bash +cd YourEmulationStationDirectory +cmake . +make +``` + +**On the Raspberry Pi:** + +Complete Raspberry Pi build instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#install_rpi_standalone). + +**On Windows:** + +[FreeImage](http://downloads.sourceforge.net/freeimage/FreeImage3154Win32.zip) + +[FreeType2](http://download.savannah.gnu.org/releases/freetype/freetype-2.4.9.tar.bz2) (you'll need to compile) + +[SDL2](http://www.libsdl.org/release/SDL2-devel-2.0.8-VC.zip) + +[cURL](http://curl.haxx.se/download.html) (you'll need to compile or get the pre-compiled DLL version) + +[RapisJSON](https://github.com/tencent/rapidjson) (you'll need the `include/rapidsjon` added to the include path) + +(Remember to copy necessary .DLLs into the same folder as the executable: probably FreeImage.dll, freetype6.dll, SDL2.dll, libcurl.dll, and zlib1.dll. Exact list depends on if you built your libraries in "static" mode or not.) + +[CMake](http://www.cmake.org/cmake/resources/software.html) (this is used for generating the Visual Studio project) + +(If you don't know how to use CMake, here are some hints: run cmake-gui and point it at your EmulationStation folder. Point the "build" directory somewhere - I use EmulationStation/build. Click configure, choose "Visual Studio [year] Project", fill in red fields as they appear and keep clicking Configure (you may need to check "Advanced"), then click Generate.) + + +Configuring +=========== + +**~/.emulationstation/es_systems.cfg:** +When first run, an example systems configuration file will be created at `~/.emulationstation/es_systems.cfg`. `~` is `$HOME` on Linux, and `%HOMEPATH%` on Windows. This example has some comments explaining how to write the configuration file. See the "Writing an es_systems.cfg" section for more information. + +**Keep in mind you'll have to set up your emulator separately from EmulationStation!** + +**~/.emulationstation/es_input.cfg:** +When you first start EmulationStation, you will be prompted to configure an input device. The process is thus: + +1. Hold a button on the device you want to configure. This includes the keyboard. + +2. Press the buttons as they appear in the list. Some inputs can be skipped by holding any button down for a few seconds (e.g. page up/page down). + +3. You can review your mappings by pressing up and down, making any changes by pressing A. + +4. Choose "SAVE" to save this device and close the input configuration screen. + +The new configuration will be added to the `~/.emulationstation/es_input.cfg` file. + +**Both new and old devices can be (re)configured at any time by pressing the Start button and choosing "CONFIGURE INPUT".** From here, you may unplug the device you used to open the menu and plug in a new one, if necessary. New devices will be appended to the existing input configuration file, so your old devices will remain configured. + +**If your controller stops working, you can delete the `~/.emulationstation/es_input.cfg` file to make the input configuration screen re-appear on next run.** + + +You can use `--help` or `-h` to view a list of command-line options. Briefly outlined here: +``` --resolution [width] [height] try and force a particular resolution --gamelist-only skip automatic game search, only read from gamelist.xml --ignore-gamelist ignore the gamelist (useful for troubleshooting) @@ -112,107 +150,107 @@ You can use `--help` or `-h` to view a list of command-line options. Briefly out --force-kid Force the UI mode to be Kid --force-kiosk Force the UI mode to be Kiosk --force-disable-filters Force the UI to ignore applied filters in gamelist ---help, -h summon a sentient, angry tuba -``` - -As long as ES hasn't frozen, you can always press F4 to close the application. - - -Writing an es_systems.cfg -========================= - -Complete configuration instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#config). - -The `es_systems.cfg` file contains the system configuration data for EmulationStation, written in XML. This tells EmulationStation what systems you have, what platform they correspond to (for scraping), and where the games are located. - -ES will check two places for an es_systems.cfg file, in the following order, stopping after it finds one that works: -* `~/.emulationstation/es_systems.cfg` -* `/etc/emulationstation/es_systems.cfg` - -The order EmulationStation displays systems reflects the order you define them in. - -**NOTE:** A system *must* have at least one game present in its "path" directory, or ES will ignore it! If no valid systems are found, ES will report an error and quit! - -Here's an example es_systems.cfg: - -```xml - - - - - - - snes - - - Super Nintendo Entertainment System - - - ~/roms/snes - - - .smc .sfc .SMC .SFC - - - snesemulator %ROM% - - - - snes - - - snes - - -``` - -The following "tags" are replaced by ES in launch commands: - -`%ROM%` - Replaced with absolute path to the selected ROM, with most Bash special characters escaped with a backslash. - -`%BASENAME%` - Replaced with the "base" name of the path to the selected ROM. For example, a path of "/foo/bar.rom", this tag would be "bar". This tag is useful for setting up AdvanceMAME. - -`%ROM_RAW%` - Replaced with the unescaped, absolute path to the selected ROM. If your emulator is picky about paths, you might want to use this instead of %ROM%, but enclosed in quotes. - -See [SYSTEMS.md](SYSTEMS.md) for some live examples in EmulationStation. - -gamelist.xml -============ - -The gamelist.xml file for a system defines metadata for games, such as a name, image (like a screenshot or box art), description, release date, and rating. - -If at least one game in a system has an image specified, ES will use the detailed view for that system (which displays metadata alongside the game list). - -*You can use ES's [scraping](http://en.wikipedia.org/wiki/Web_scraping) tools to avoid creating a gamelist.xml by hand.* There are two ways to run the scraper: - -* **If you want to scrape multiple games:** press start to open the menu and choose the "SCRAPER" option. Adjust your settings and press "SCRAPE NOW". -* **If you just want to scrape one game:** find the game on the game list in ES and press select. Choose "EDIT THIS GAME'S METADATA" and then press the "SCRAPE" button at the bottom of the metadata editor. - -You can also edit metadata within ES by using the metadata editor - just find the game you wish to edit on the gamelist, press Select, and choose "EDIT THIS GAME'S METADATA." - -A command-line version of the scraper is also provided - just run emulationstation with `--scrape` *(currently broken)*. - -The switch `--ignore-gamelist` can be used to ignore the gamelist and force ES to use the non-detailed view. - -If you're writing a tool to generate or parse gamelist.xml files, you should check out [GAMELISTS.md](GAMELISTS.md) for more detailed documentation. - - -Themes -====== - -By default, EmulationStation looks pretty ugly. You can fix that. If you want to know more about making your own themes (or editing existing ones), read [THEMES.md](THEMES.md)! - -I've put some themes up for download on my EmulationStation webpage: http://aloshi.com/emulationstation#themes - -If you're using RetroPie, you should already have a nice set of themes automatically installed! - - --Alec "Aloshi" Lofquist -http://www.aloshi.com -http://www.emulationstation.org +--help, -h summon a sentient, angry tuba +``` + +As long as ES hasn't frozen, you can always press F4 to close the application. + + +Writing an es_systems.cfg +========================= + +Complete configuration instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#config). + +The `es_systems.cfg` file contains the system configuration data for EmulationStation, written in XML. This tells EmulationStation what systems you have, what platform they correspond to (for scraping), and where the games are located. + +ES will check two places for an es_systems.cfg file, in the following order, stopping after it finds one that works: +* `~/.emulationstation/es_systems.cfg` +* `/etc/emulationstation/es_systems.cfg` + +The order EmulationStation displays systems reflects the order you define them in. + +**NOTE:** A system *must* have at least one game present in its "path" directory, or ES will ignore it! If no valid systems are found, ES will report an error and quit! + +Here's an example es_systems.cfg: + +```xml + + + + + + + snes + + + Super Nintendo Entertainment System + + + ~/roms/snes + + + .smc .sfc .SMC .SFC + + + snesemulator %ROM% + + + + snes + + + snes + + +``` + +The following "tags" are replaced by ES in launch commands: + +`%ROM%` - Replaced with absolute path to the selected ROM, with most Bash special characters escaped with a backslash. + +`%BASENAME%` - Replaced with the "base" name of the path to the selected ROM. For example, a path of "/foo/bar.rom", this tag would be "bar". This tag is useful for setting up AdvanceMAME. + +`%ROM_RAW%` - Replaced with the unescaped, absolute path to the selected ROM. If your emulator is picky about paths, you might want to use this instead of %ROM%, but enclosed in quotes. + +See [SYSTEMS.md](SYSTEMS.md) for some live examples in EmulationStation. + +gamelist.xml +============ + +The gamelist.xml file for a system defines metadata for games, such as a name, image (like a screenshot or box art), description, release date, and rating. + +If at least one game in a system has an image specified, ES will use the detailed view for that system (which displays metadata alongside the game list). + +*You can use ES's [scraping](http://en.wikipedia.org/wiki/Web_scraping) tools to avoid creating a gamelist.xml by hand.* There are two ways to run the scraper: + +* **If you want to scrape multiple games:** press start to open the menu and choose the "SCRAPER" option. Adjust your settings and press "SCRAPE NOW". +* **If you just want to scrape one game:** find the game on the game list in ES and press select. Choose "EDIT THIS GAME'S METADATA" and then press the "SCRAPE" button at the bottom of the metadata editor. + +You can also edit metadata within ES by using the metadata editor - just find the game you wish to edit on the gamelist, press Select, and choose "EDIT THIS GAME'S METADATA." + +A command-line version of the scraper is also provided - just run emulationstation with `--scrape` *(currently broken)*. + +The switch `--ignore-gamelist` can be used to ignore the gamelist and force ES to use the non-detailed view. + +If you're writing a tool to generate or parse gamelist.xml files, you should check out [GAMELISTS.md](GAMELISTS.md) for more detailed documentation. + + +Themes +====== + +By default, EmulationStation looks pretty ugly. You can fix that. If you want to know more about making your own themes (or editing existing ones), read [THEMES.md](THEMES.md)! + +I've put some themes up for download on my EmulationStation webpage: http://aloshi.com/emulationstation#themes + +If you're using RetroPie, you should already have a nice set of themes automatically installed! + + +-Alec "Aloshi" Lofquist +http://www.aloshi.com +http://www.emulationstation.org From 96d106b23698a6b712096ce9db1fb903966c145d Mon Sep 17 00:00:00 2001 From: fabricecaruso <51082152+fabricecaruso@users.noreply.github.com> Date: Mon, 27 May 2019 14:10:32 +0200 Subject: [PATCH 004/336] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 52e38b50b0..b4c9a4f011 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Changes in my branch - Support for Multiple Emulators/Cores in es_systems.cfg, and setting Emulator/Core per game. +```xml %HOME%\RetroArch\retroarch.exe -L %HOME%\RetroArch\cores\%CORE%_libretro.dll %ROM% @@ -23,7 +24,7 @@ Changes in my branch - +``` - Better Grid view (animations, layout, selection...) - Created a 'GridEx' mode, which inherits from grid, in order to create a 'preview bar' including a game screenshot. - Changed 'Automatic' view mode. Switchs to grid if "thumbnails" exist in gamelist. Switchs to gridex if "thumbnails" and "image" exist in gamelist. From 72d12b4efd6e977ccfaf1351cc58f9878c81d3f0 Mon Sep 17 00:00:00 2001 From: Fab Date: Mon, 27 May 2019 21:46:37 +0200 Subject: [PATCH 005/336] [Fonts] Change favorite icon with a simple glyph font --- es-core/src/resources/Font.cpp | 2 +- resources/fontawesome_webfont.ttf | Bin 120456 -> 0 bytes resources/glyphs.ttf | Bin 0 -> 1904 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 resources/fontawesome_webfont.ttf create mode 100644 resources/glyphs.ttf diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index d5086b7e05..b32cdec546 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -249,7 +249,7 @@ std::vector getFallbackFontPaths() std::string path = fontDir + fontNames[i]; if (i == 0) - path = ":/fontawesome_webfont.ttf"; + path = ":/glyphs.ttf"; if(ResourceManager::getInstance()->fileExists(path)) fontPaths.push_back(path); diff --git a/resources/fontawesome_webfont.ttf b/resources/fontawesome_webfont.ttf deleted file mode 100644 index 59dec23e2af3290758890eee55e9f619e6c48202..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120456 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 diff --git a/resources/glyphs.ttf b/resources/glyphs.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f6cc4b6a511ac0af60b461af5364f5b398924d43 GIT binary patch literal 1904 zcmdT^TWDNG82)F^=4^L2mtC_-nl^1FsixX=H)qdocH1PC(6os~q)m)b1ZhvRvlrZ( zXHRZYFi>l4@j>t!TM&$Vu^R8TR zDLed?|0HzRz}NG_%7j*K{sH~R;Hf+WzNGdU^e=!vnlCwvdbjI;7d%v~%v#W4Ezb&g zq+~5t$wxzwT>$50t0Y2GZ@mHiI+hJqD>diI?%RvdcR_!YG_8fbO%1@0@A$rkbW02U ziybf685&*F*K|#63ToF7lX{T_G!J?1VB#*sK(x6*GD|y~7b30lTzu@7~`GMUqF+HowtsXfIJKvPCSF?mO6bQ1|J+_72^b zO1F3Pv53ne2YTz8zO-vJn8e(gN|+t(^7CBnfBo3%-pRd_Bddom26*av>Uw}RHJ`lN z_*h44(E&KPR-%Gvk-_!rv$m+i5x@L~C zi%sDZmnVMh(xWf-WSc_WXTM0Sgd5wm<}T*#?0o){0GB0o+bG8SecjC$@;ZI_Lc`gX zk-sSD=9a$ybmL8F{bTr5+(mU_+)td}t(&J?YU>Ri8l-G<)6IGtlR1{jV$|$fNbCWY zOkyukS3M5cZaSne!%aP|um|{*!Wwnb8HK%&7Zh$lovRA#)IuLB?4wEgTwy;2SU}+* zwXul8&9t9AA@}X|V-E(KP#Ak=3kqY;?2^J7nd}{fv43`3;Rf2xzEfC7%-?97ESkqy zp)xr%MS^lPM@2~OtvVj%W45pyk>LvkC(oyss)8qC=D0OKUMV|MA~#pGY{jCAMolAr zbZBtMOe@8Vuxo`%na5*>G5m~W&*phNahSvG$gm9KE>W?0JRTd288I_y#(63kJzsFP zMLts$R!wkQWQEP03U~5?k5#Hmb|IH{cumYYa+xebNlg}^xno-yQL^lpx#c)^;ryIy zQm#0KSy79fu;vAyu*#XGyyH}d1_q>$wIFJhl86;54-^g*;aaYV0pb)zA4AdvQs-dI zfG?o<4&{NSX$i#_KnaSGxiz&9OFQCjElL#L2F3B5hiDMpr1Z9EGg!x_8rGIY<*<#x z!`NoEJ+8X#^2YC1w{BG46g-S^$h`PF%F+F2t?76@R3Bj>A0hmhE+0|7-JcbGh;WO= K|DXJ4p8f$kvL^cg literal 0 HcmV?d00001 From 1f3808d524b1f3acdfbf20ec160f617e0d09a478 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 28 May 2019 16:33:14 +0200 Subject: [PATCH 006/336] [Theming] Bad loading for themes using variables [SplashScreen] Progressbar + simplified splash.svg [Favorites] Better icon + Refresh list when switching favorite [Main Menu] Animations when opening [GameLaunch] Show splash.svg when launching ( when option hide when launch game is disabled ) [Font] Bad sizing when favorite glyph is loaded with small fonts. --- es-app/src/CollectionSystemManager.cpp | 17 ++- es-app/src/EmulationStation.h | 1 + es-app/src/SystemData.cpp | 21 ++- es-app/src/guis/GuiGamelistOptions.cpp | 29 +++- es-app/src/guis/GuiMenu.cpp | 42 ++++-- es-app/src/main.cpp | 16 +-- es-app/src/scrapers/ScreenScraper.h | 2 +- es-app/src/views/SystemView.cpp | 2 +- es-app/src/views/ViewController.cpp | 24 ++-- es-core/src/Settings.cpp | 4 + es-core/src/ThemeData.cpp | 140 ++++++++++++++++--- es-core/src/ThemeData.h | 4 +- es-core/src/Window.cpp | 96 ++++++++++--- es-core/src/Window.h | 9 +- es-core/src/components/GridTileComponent.cpp | 7 +- es-core/src/platform.cpp | 10 +- es-core/src/resources/Font.cpp | 37 ++--- es-core/src/resources/TextureData.cpp | 5 +- es-core/src/utils/FileSystemUtil.cpp | 2 +- resources/glyphs.ttf | Bin 0 -> 1904 bytes resources/locale/fr/emulationstation2.po | 4 + resources/splash.svg | 91 +++++------- 22 files changed, 397 insertions(+), 166 deletions(-) create mode 100644 resources/glyphs.ttf diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index d9082d9a0c..d5d4ac2ef2 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -519,20 +519,29 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) } else { - file->getSourceFileData()->getSystem()->getIndex()->removeFromIndex(file); + SystemData* sysData = file->getSourceFileData()->getSystem(); + sysData->getIndex()->removeFromIndex(file); + MetaDataList* md = &file->getSourceFileData()->metadata; + std::string value = md->get("favorite"); if (value == "false") - { md->set("favorite", "true"); - } else { adding = false; md->set("favorite", "false"); } - file->getSourceFileData()->getSystem()->getIndex()->addToIndex(file); + sysData->getIndex()->addToIndex(file); + refreshCollectionSystems(file->getSourceFileData()); + + SystemData* systemViewToUpdate = getSystemToView(sysData); + if (systemViewToUpdate != NULL) + { + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); + ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(file, FILE_METADATA_CHANGED); + } } if (adding) { diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index 57389a1055..375b11d9cf 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -8,6 +8,7 @@ #define PROGRAM_VERSION_MINOR 10 #define PROGRAM_VERSION_MAINTENANCE 0 #define PROGRAM_VERSION_STRING "2.10.0 FCAMOD" +#define SCRAPER_VERSION_STRING "2.10.0" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 4052801b75..adae7852e0 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -16,6 +16,7 @@ #include #endif +#include "GuiComponent.h" #include "Window.h" std::vector SystemData::sSystemVector; @@ -218,11 +219,15 @@ bool SystemData::loadConfig(Window* window) LOG(LogError) << "es_systems.cfg is missing the tag!"; return false; } - + + float systemCount = 1; + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + systemCount++; + + float currentSystem = 0; for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) { - std::vector emulatorList; - + std::vector emulatorList; // std::vector coreList; @@ -262,7 +267,9 @@ bool SystemData::loadConfig(Window* window) } if (window != NULL) - window->renderLoadingScreen(fullname); + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; // convert extensions list from a string into a vector of strings @@ -346,7 +353,9 @@ bool SystemData::loadConfig(Window* window) } if (window != NULL) - window->renderLoadingScreen("Favoris"); + window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; CollectionSystemManager::get()->loadCollectionSystems(); return true; @@ -575,7 +584,7 @@ void SystemData::loadTheme() sysData.insert(std::pair("system.theme", getThemeFolder())); sysData.insert(std::pair("system.fullName", getFullName())); - mTheme->loadFile(getThemeFolder(), path); + mTheme->loadFile(getThemeFolder(), sysData, path); } catch(ThemeException& e) { diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index b789667da8..01c0741c21 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -11,6 +11,8 @@ #include "GuiMetaDataEd.h" #include "SystemData.h" +#include "animations/LambdaAnimation.h" + GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : GuiComponent(window), mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false) { @@ -118,7 +120,32 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui // center the menu setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); + //mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); + + float x0 = (mSize.x() - mMenu.getSize().x()) / 2; + + float y1 = Renderer::getScreenHeight(); + float y2 = (mSize.y() - mMenu.getSize().y()) / 2; + //float y1 = mMenu.getSize().y(); + //float y2 = (mSize.y() - mMenu.getSize().y()) / 2; + + setPosition(x0, y1); + + auto fadeFunc = [this, x0, y1, y2](float t) { + + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + float y = y1 * (1 - pct) + y2 * pct; + setPosition(x0, y); + }; + + setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, x0, y2] + { + setPosition(x0, y2); + }); + + setPosition(x0, y2); } GuiGamelistOptions::~GuiGamelistOptions() diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 4b1cf2aea6..a383ee2ff4 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -19,12 +19,13 @@ #include #include +#include "animations/LambdaAnimation.h" + GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) { bool isFullUI = UIModeController::getInstance()->isUIModeFull(); - - + if (isFullUI) { addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); @@ -41,15 +42,38 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); } - - + +#if defined(_WIN32) addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); +#else + addEntry(_T("QUIT"), 0x777777FF, true, [this] {openQuitMenu(); }); +#endif addChild(&mMenu); addVersionInfo(); setSize(mMenu.getSize()); - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, Renderer::getScreenHeight() * 0.15f); + + float y1 = Renderer::getScreenHeight(); + float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); + + auto fadeFunc = [this, y1, y2](float t) { + + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + float y = y1 * (1 - pct) + y2 * pct; + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y); + }; + + setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] + { + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + }); + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); } void GuiMenu::openScraperSettings() @@ -402,13 +426,13 @@ void GuiMenu::openUISettings() screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); s->addRow(screensaver_row); - +#if defined(_WIN32) // quick system select (left/right in game list view) auto hideWindowScreen = std::make_shared(mWindow); hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); - +#endif // quick system select (left/right in game list view) auto quick_sys_select = std::make_shared(mWindow); @@ -551,9 +575,11 @@ void GuiMenu::openConfigInput() void GuiMenu::openQuitMenu() { +#if defined(_WIN32) Scripting::fireEvent("quit"); quitES(""); return; +#endif auto s = new GuiSettings(mWindow, _T("QUIT")); @@ -573,8 +599,6 @@ void GuiMenu::openQuitMenu() row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); s->addRow(row); - - if(Settings::getInstance()->getBool("ShowExit")) { row.elements.clear(); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 136bee9434..2a08c2c583 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -322,7 +322,7 @@ int main(int argc, char* argv[]) bool splashScreen = Settings::getInstance()->getBool("SplashScreen"); bool splashScreenProgress = Settings::getInstance()->getBool("SplashScreenProgress"); - if(!scrape_cmdline) + if (!scrape_cmdline) { if(!window.init()) { @@ -333,15 +333,9 @@ int main(int argc, char* argv[]) std::string glExts = (const char*)glGetString(GL_EXTENSIONS); LOG(LogInfo) << "Checking available OpenGL extensions..."; LOG(LogInfo) << " ARB_texture_non_power_of_two: " << (glExts.find("ARB_texture_non_power_of_two") != std::string::npos ? "ok" : "MISSING"); - if(splashScreen) - { - std::string progressText = "Chargement"; - - if (splashScreenProgress) - progressText = "Chargement de la configuration"; - window.renderLoadingScreen(progressText); - } + if (splashScreen) + window.renderLoadingScreen(_T("Loading...")); } const char* errorMsg = NULL; @@ -383,7 +377,7 @@ int main(int argc, char* argv[]) window.renderLoadingScreen(_T("Starting UI")); //choose which GUI to open depending on if an input configuration already exists - if(errorMsg == NULL) + if (errorMsg == NULL) { if (Utils::FileSystem::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0) ViewController::get()->goToStart(true); @@ -394,6 +388,8 @@ int main(int argc, char* argv[]) //generate joystick events since we're done loading SDL_JoystickEventState(SDL_ENABLE); + window.endRenderLoadingScreen(); + int lastTime = SDL_GetTicks(); int ps_time = SDL_GetTicks(); diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index 55c4a89c4f..24063744e2 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -28,7 +28,7 @@ class ScreenScraperRequest : public ScraperHttpRequest const std::string API_DEV_P = { 108, 28, 54, 55, 83, 43, 91, 44, 30, 22, 41, 12, 0, 108, 38, 29 }; const std::string API_DEV_KEY = { 54, 73, 115, 100, 101, 67, 111, 107, 79, 66, 68, 66, 67, 56, 118, 77, 54, 88, 101, 54 }; const std::string API_URL_BASE = "https://www.screenscraper.fr/api2"; - const std::string API_SOFT_NAME = "Emulationstation 2.9.0"; // +static_cast(PROGRAM_VERSION_STRING); + const std::string API_SOFT_NAME = "Emulationstation "+static_cast(SCRAPER_VERSION_STRING); /** Which type of image artwork we need. Possible values (not a comprehensive list): - ss: in-game screenshot diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 9ed1a22dc6..41bc9fddfe 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -271,7 +271,7 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) }, goFast ? 10 : 300); // wait 600ms to fade in - setAnimation(infoFadeIn, goFast ? 0 : 2000, nullptr, false, 2); + setAnimation(infoFadeIn, goFast ? 0 : 500, nullptr, false, 2); // no need to animate transition, we're not going anywhere (probably mEntries.size() == 1) if(endPos == mCamOffset && endPos == mExtrasCamOffset) diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 65fb1b4122..37c61dbbab 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -61,12 +61,13 @@ void ViewController::goToStart(bool forceImmediate) // Requested system doesn't exist Settings::getInstance()->setString("StartupSystem", ""); } + goToSystemView(SystemData::sSystemVector.at(0), forceImmediate); } void ViewController::ReloadAndGoToStart() { - mWindow->renderLoadingScreen("Chargement"); + mWindow->renderLoadingScreen(_T("Loading...")); ViewController::get()->reloadAll(); ViewController::get()->goToStart(false); @@ -235,8 +236,8 @@ void ViewController::launch(FileData* game, Vector3f center) setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this, game, fadeFunc] { game->launchGame(mWindow); - setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this] { mLockInput = false; }, true); - this->onFileChanged(game, FILE_METADATA_CHANGED); + setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this] { mLockInput = false; mWindow->endRenderLoadingScreen(); }, true); + this->onFileChanged(game, FILE_METADATA_CHANGED); }); } else if (transition_style == "slide"){ // move camera to zoom in on center + fade out, launch game, come back in @@ -244,7 +245,7 @@ void ViewController::launch(FileData* game, Vector3f center) { game->launchGame(mWindow); mCamera = origCamera; - setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 600), 0, [this] { mLockInput = false; }, true); + setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 600), 0, [this] { mLockInput = false; mWindow->endRenderLoadingScreen(); }, true); this->onFileChanged(game, FILE_METADATA_CHANGED); }); } else { // instant @@ -252,7 +253,7 @@ void ViewController::launch(FileData* game, Vector3f center) { game->launchGame(mWindow); mCamera = origCamera; - setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 10), 0, [this] { mLockInput = false; }, true); + setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 10), 0, [this] { mLockInput = false; mWindow->endRenderLoadingScreen(); }, true); this->onFileChanged(game, FILE_METADATA_CHANGED); }); } @@ -420,7 +421,7 @@ bool ViewController::input(InputConfig* config, Input input) return true; } - if(mCurrentView) + if (mCurrentView) return mCurrentView->input(config, input); return false; @@ -468,10 +469,15 @@ void ViewController::render(const Transform4x4f& parentTrans) mWindow->renderHelpPromptsEarly(); // fade out - if(mFadeOpacity) + if (mFadeOpacity) { - Renderer::setMatrix(parentTrans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(mFadeOpacity * 255)); + if (Settings::getInstance()->getBool("HideWindow")) + { + Renderer::setMatrix(parentTrans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(mFadeOpacity * 255)); + } + else + mWindow->renderGameLoadingScreen(mFadeOpacity, false); } } diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index a63c75e9d3..41f2e16de4 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -92,7 +92,11 @@ void Settings::setDefaults() mIntMap["MaxVRAM"] = 100; #endif +#if defined(_WIN32) + mBoolMap["HideWindow"] = false; +#else mBoolMap["HideWindow"] = true; +#endif mStringMap["GameTransitionStyle"] = "fade"; mStringMap["TransitionStyle"] = "slide"; mStringMap["Language"] = "en"; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 52f75760cc..9ee8ea2fc9 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -203,7 +203,7 @@ ThemeData::ThemeData() mVersion = 0; } -void ThemeData::loadFile(std::string system, const std::string& path) +void ThemeData::loadFile(std::string system, std::map sysDataMap, const std::string& path) { mPaths.push_back(path); @@ -219,8 +219,7 @@ void ThemeData::loadFile(std::string system, const std::string& path) mSystemThemeFolder = system; mVariables.clear(); - //mVariables.insert(system, system); - //mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); + mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); pugi::xml_document doc; pugi::xml_parse_result res = doc.load_file(path.c_str()); @@ -263,6 +262,67 @@ std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolde return result; } +bool ThemeData::parseSubset(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("subset")) + { + parse = false; + const std::string subsetAttr = node.attribute("subset").as_string(); + const std::string nameAttr = node.attribute("name").as_string(); + + if (subsetAttr == "iconset" || subsetAttr == "gamelistview") + { + if (nameAttr.rfind("1-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (subsetAttr == "systemview") + { + if (nameAttr.rfind("8-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (nameAttr.rfind("2-") != std::string::npos) + { + parse = true; + return parse; + } + /* + if (subsetAttr == "colorset" && nameAttr == mColorset) + { + parse = true; + return parse; + } + if (subsetAttr == "iconset" && nameAttr == mIconset) + { + parse = true; + return parse; + } + if (subsetAttr == "menu" && nameAttr == mMenu) + { + parse = true; + return parse; + } + if (subsetAttr == "systemview" && nameAttr == mSystemview) + { + parse = true; + return parse; + } + if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) + { + parse = true; + return parse; + }*/ + } + + return parse; +} void ThemeData::parseIncludes(const pugi::xml_node& root) { @@ -271,6 +331,9 @@ void ThemeData::parseIncludes(const pugi::xml_node& root) for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) { + if (!parseSubset(node)) + continue; + std::string relPath = resolvePlaceholders(node.text().as_string()); std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); path = resolveSystemVariable(mSystemThemeFolder, path); @@ -391,23 +454,56 @@ void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) if(elemTypeIt == sElementMap.cend()) throw error << "Unknown element of type \"" << node.name() << "\"!"; + if (parseRegion(node)) + { + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + parseElement(node, elemTypeIt->second, + view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); + + if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) + view.orderedKeys.push_back(elemKey); + } + } + } +} + +bool ThemeData::parseRegion(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("region")) + { + std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); + + parse = false; const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); + const std::string nameAttr = node.attribute("region").as_string(); size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while(off != std::string::npos || prevOff != std::string::npos) + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) { std::string elemKey = nameAttr.substr(prevOff, off - prevOff); prevOff = nameAttr.find_first_not_of(delim, off); off = nameAttr.find_first_of(delim, prevOff); - - parseElement(node, elemTypeIt->second, - view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); - - if(std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) - view.orderedKeys.push_back(elemKey); + if (elemKey == regionsetting) + { + parse = true; + return parse; + } } + } + return parse; + } @@ -450,16 +546,28 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapfileExists(path)) + + if (!ResourceManager::getInstance()->fileExists(path)) + { + std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); + if (ResourceManager::getInstance()->fileExists(rootPath)) + path = rootPath; + } + + if (!ResourceManager::getInstance()->fileExists(path)) { std::stringstream ss; ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda ss << "could not find file \"" << node.text().get() << "\" "; if(node.text().get() != path) ss << "(which resolved to \"" << path << "\") "; + LOG(LogWarning) << ss.str(); + OutputDebugString(ss.str().c_str()); } - element.properties[node.name()] = path; + else + element.properties[node.name()] = path; + break; } case COLOR: @@ -525,8 +633,8 @@ const std::shared_ptr& ThemeData::getDefault() { try { - //std::map emptyMap; - theme->loadFile("", path); // emptyMap + std::map emptyMap; + theme->loadFile("", emptyMap, path); } catch(ThemeException& e) { LOG(LogError) << e.what(); diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 6bbcc01555..aec760c4c7 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -135,7 +135,7 @@ class ThemeData ThemeData(); // throws ThemeException - void loadFile(std::string system, const std::string& path); + void loadFile(std::string system, std::map sysDataMap, const std::string& path); enum ElementPropertyType { @@ -178,6 +178,8 @@ class ThemeData void parseViews(const pugi::xml_node& themeRoot); void parseView(const pugi::xml_node& viewNode, ThemeView& view); void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element); + bool parseRegion(const pugi::xml_node& node); + bool parseSubset(const pugi::xml_node& node); std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 05337b0e01..1b361f408e 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -11,11 +11,15 @@ #include #include +#include + Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) { mHelp = new HelpComponent(this); mBackgroundOverlay = new ImageComponent(this); + mSplash = NULL; + } Window::~Window() @@ -297,58 +301,104 @@ void Window::setAllowSleep(bool sleep) mAllowSleep = sleep; } -void Window::renderLoadingScreen(std::string text) +void Window::endRenderLoadingScreen() +{ + mSplash = NULL; +} + +void Window::renderLoadingScreen(std::string text, float percent) { +#if defined(_WIN32) + // Avoid Window Freezing on Windows + SDL_Event event; + while (SDL_PollEvent(&event)) ; +#endif + Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); + if (percent > 0) + { + float baseHeight = 0.04f; + + float w = Renderer::getScreenWidth() / 2; + float h = Renderer::getScreenHeight() * baseHeight; + + float x = Renderer::getScreenWidth() / 2 - w / 2; + float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); + + Renderer::drawRect(x, y, w, h, 0x252525FF); + Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF + } + + ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.5f, 0.0f); - splash.setImage(":/splash.svg"); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); splash.render(trans); + auto& font = mDefaultFonts.at(1); TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.835f); + float y = Math::round(Renderer::getScreenHeight() * 0.8f); // 35 trans = trans.translate(Vector3f(x, y, 0.0f)); Renderer::setMatrix(trans); font->renderTextCache(cache); delete cache; + Renderer::swapBuffers(); } -void Window::renderBlackScreen(std::string text) +void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) { Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); - /* + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); + ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.5f, 0.0f); - splash.setImage(":/splash.svg"); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); splash.render(trans); - */ + + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); - if (text.length() > 0) - { - auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); - - float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.835f); - trans = trans.translate(Vector3f(x, y, 0.0f)); - Renderer::setMatrix(trans); - font->renderTextCache(cache); - delete cache; - } + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.835f); + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; - Renderer::swapBuffers(); + if (swapBuffers) + Renderer::swapBuffers(); } diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 5fd238dc62..0c64a959fa 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -16,6 +16,8 @@ class ImageComponent; class InputConfig; class TextCache; class Transform4x4f; +class TextureResource; + struct HelpStyle; class Window @@ -64,8 +66,9 @@ class Window bool getAllowSleep(); void setAllowSleep(bool sleep); - void renderLoadingScreen(std::string text); - void renderBlackScreen(std::string text); + void endRenderLoadingScreen(); + void renderLoadingScreen(std::string text, float percent = -1); + void renderGameLoadingScreen(float opacity=1, bool swapBuffers=true); void renderHelpPromptsEarly(); // used to render HelpPrompts before a fade void setHelpPrompts(const std::vector& prompts, const HelpStyle& style); @@ -91,6 +94,8 @@ class Window InfoPopup* mInfoPopup; bool mRenderScreenSaver; + std::shared_ptr mSplash; + std::vector mGuiStack; std::vector< std::shared_ptr > mDefaultFonts; diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 310830f4fb..cccf549cfe 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -302,10 +302,13 @@ void GridTileComponent::setSelected(bool selected, Vector3f* pPosition) auto func = [this](float t) { - this->setSelectedZoom(t); + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + this->setSelectedZoom(pct); }; - setAnimation(new LambdaAnimation(func, 100), 0, [this] { + setAnimation(new LambdaAnimation(func, 200), 0, [this] { this->setSelectedZoom(1); mAnimPosition = Vector3f(0, 0, 0); }, false, 3); diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index d47f6204a0..e80e589518 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -1,16 +1,18 @@ #include "platform.h" - #include + #ifdef WIN32 #include +#include #else #include #endif #include -#include #include "Window.h" +#include "GuiComponent.h" + int runShutdownCommand() { #ifdef WIN32 // windows @@ -84,7 +86,7 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo { #ifdef WIN32 if (window != NULL) - window->renderBlackScreen("Chargement en cours..."); + window->renderGameLoadingScreen(); // on Windows we use _wsystem to support non-ASCII paths // which requires converting from utf8 to a wstring @@ -124,7 +126,7 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo polled = true; if (window != NULL && polled) - window->renderBlackScreen("Chargement en cours..."); + window->renderGameLoadingScreen(); } } diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index d5086b7e05..9890954a82 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -78,9 +78,11 @@ Font::Font(int size, const std::string& path) : mSize(size), mPath(path) initLibrary(); // always initialize ASCII characters -// for(unsigned int i = 32; i < 128; i++) - // getGlyph(i); + for (unsigned int i = 32; i < 128; i++) + getGlyph(i); + // getGlyph(61446); + clearFaceCache(); } @@ -140,7 +142,7 @@ Font::FontTexture::~FontTexture() bool Font::FontTexture::findEmpty(const Vector2i& size, Vector2i& cursor_out) { - if(size.x() >= textureSize.x() || size.y() >= textureSize.y()) + if (size.x() >= textureSize.x() || size.y() >= textureSize.y()) return false; if(writePos.x() + size.x() >= textureSize.x() && @@ -235,9 +237,10 @@ std::vector getFallbackFontPaths() fontDir += "\\Fonts\\"; const char* fontNames[] = { + "glyphs.ttf", // latin + "arial.ttf", // latin "meiryo.ttc", // japanese - "simhei.ttf", // chinese - "arial.ttf" // latin + "simhei.ttf" // chinese }; //prepend to font file names @@ -249,9 +252,9 @@ std::vector getFallbackFontPaths() std::string path = fontDir + fontNames[i]; if (i == 0) - path = ":/fontawesome_webfont.ttf"; + path = ":/glyphs.ttf"; - if(ResourceManager::getInstance()->fileExists(path)) + if (ResourceManager::getInstance()->fileExists(path)) fontPaths.push_back(path); } @@ -288,18 +291,17 @@ FT_Face Font::getFaceForChar(unsigned int id) for(unsigned int i = 0; i < fallbackFonts.size() + 1; i++) { auto fit = mFaceCache.find(i); - - if(fit == mFaceCache.cend()) // doesn't exist yet - { + if (fit == mFaceCache.cend()) // doesn't exist yet + { // i == 0 -> mPath // otherwise, take from fallbackFonts const std::string& path = (i == 0 ? mPath : fallbackFonts.at(i - 1)); - ResourceData data = ResourceManager::getInstance()->getFileData(path); - mFaceCache[i] = std::unique_ptr(new FontFace(std::move(data), mSize)); + ResourceData data = ResourceManager::getInstance()->getFileData(path); + mFaceCache[i] = std::unique_ptr(new FontFace(std::move(data), i == 1 && mMaxGlyphHeight > 0 ? mMaxGlyphHeight : mSize)); // Reduce size of gyphs ???? fit = mFaceCache.find(i); } - if(FT_Get_Char_Index(fit->second->face, id) != 0) + if (FT_Get_Char_Index(fit->second->face, id) != 0) return fit->second->face; } @@ -316,7 +318,7 @@ Font::Glyph* Font::getGlyph(unsigned int id) { // is it already loaded? auto it = mGlyphMap.find(id); - if(it != mGlyphMap.cend()) + if (it != mGlyphMap.cend()) return &it->second; // nope, need to make a glyph @@ -329,7 +331,7 @@ Font::Glyph* Font::getGlyph(unsigned int id) FT_GlyphSlot g = face->glyph; - if(FT_Load_Char(face, id, FT_LOAD_RENDER)) + if (FT_Load_Char(face, id, FT_LOAD_RENDER)) { LOG(LogError) << "Could not find glyph for character " << id << " for font " << mPath << ", size " << mSize << "!"; return NULL; @@ -352,9 +354,10 @@ Font::Glyph* Font::getGlyph(unsigned int id) Glyph& glyph = mGlyphMap[id]; glyph.texture = tex; + glyph.texPos = Vector2f(cursor.x() / (float)tex->textureSize.x(), cursor.y() / (float)tex->textureSize.y()); glyph.texSize = Vector2f(glyphSize.x() / (float)tex->textureSize.x(), glyphSize.y() / (float)tex->textureSize.y()); - + glyph.advance = Vector2f((float)g->metrics.horiAdvance / 64.0f, (float)g->metrics.vertAdvance / 64.0f); glyph.bearing = Vector2f((float)g->metrics.horiBearingX / 64.0f, (float)g->metrics.horiBearingY / 64.0f); @@ -364,7 +367,7 @@ Font::Glyph* Font::getGlyph(unsigned int id) glBindTexture(GL_TEXTURE_2D, 0); // update max glyph height - if(glyphSize.y() > mMaxGlyphHeight) + if (id != 61446 && glyphSize.y() > mMaxGlyphHeight) mMaxGlyphHeight = glyphSize.y(); // done diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index e2a613a859..6c472c79ed 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -164,9 +164,6 @@ bool TextureData::uploadAndBind() if (mTextureID != 0) { glBindTexture(GL_TEXTURE_2D, mTextureID); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } else { @@ -186,7 +183,7 @@ bool TextureData::uploadAndBind() glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)mWidth, (GLsizei)mHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, mDataRGBA); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); const GLint wrapMode = mTile ? GL_REPEAT : GL_CLAMP_TO_EDGE; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index e4af303eb3..3c77caed8b 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -149,7 +149,7 @@ namespace Utils char* envHome = getenv("HOME"); #ifdef _DEBUG - envHome = "H:/[Emulz]/EmulationStation/"; + // envHome = "H:/[Emulz]/EmulationStation/"; #endif if (envHome) diff --git a/resources/glyphs.ttf b/resources/glyphs.ttf new file mode 100644 index 0000000000000000000000000000000000000000..238aacf77559719c876b83163fad7c471f284416 GIT binary patch literal 1904 zcmdT^O>7%Q6n^7%*0r4b0+y3NFe6q?#zuN}K}Qe-u)LqUk96oZgZyS2w&3)}0i zH$Rq;h*S!+;(&yNBIv1bKrKi}D5{VUltU$i$_XTt8=&5hAO|=AY0R7TSi&hPXJ#}r z-+SMi_h#RlSs(!5g%yxMdFtt?dOmntN6lwA&gQFH{Ri#NI)JXMIQ^yjIC57|-qe{^@U5=Di~NTuCv{Y~64EEgKZo*zdevH%$L#%gQ1(@fyoMac za;%~psA|h~a6uY%FQH6pT9x^pc;i*%H!-ZYZZxb%$8IenA4L8zNYVg#6M7Iod{6%h zkRaGQ&DR0A9xa;%$ZXIJSz;{)!xH*j5xfH&98nxL0BCLf!wu&;&53{zi8x#fwQ^Qb zk}{^GuB)8Kz-`~$2u?^h?(yNYHBS6q5r-RgHLj17ccf`>gC8KJ#uNU~zQ9mGQpbJ% z(6}#Nx?TJxeu?-mZT(vOk=#6V{sVGx>+apZ2o{N-Uuu0V-H={@LChAAWNKpn#D3W& zyM{+(S28s`GC=~i6xbJPGZQJ>Xexp0U^1?b4D;@|Jo(CzwcYc(=V#UqUhvT5)#OzV z>FYm!x%Z(FZ*T5;=H_6?<)?`&-aTi(Sn>KF@J!|&`<@czz>0fU>b>Oq?p>2F&#div zCYU=yF7)}2y*Bq#P!2yoUhMOYo%uYz>hB$r`h&zdI{NHK9?DBvjLzzhJf6UySz?zuzvuV~n%yhC3wGl2*&k`Viqc=E)+^ zZ(DHeK+GkMorr>MKPc^l0|FDgsYeBNAU+|m1fy_LU?=L!0{38@>jKL#0M`U|!908_ zup2zYBd`~SNI>9z*h?Par*XRR3<{YO7|%?W1jaLyivmlalD7oL^OIWw_rOl_t-vz+ z{28*K!6MEE)WCvMz@P*RP(iKP&CxKOHJN6y0$nOwIz8R2Ga8SmS#2?E)T~piv{2DZ zA%%q)R+ZS{^i*0+2_nbLM%k#*SVU2#pVZ8}PGj+d6wMAzb2oMf!=$lTWGbRW)Px$N z$wc^E+1lp$WQA!BMom^^CbbM|>5R@A^`==a=@xCUyv4&5(G_Qk=$cxlR$x`le35FF zWtPt^*d{f@D(6`va!gxfbWW=kn!07x)6pp3qb;$9QDu>`@vq8g1-I53EQ&viFpgPF z8pG6CI2TY}!s1(?BRUODEIvaNhX| - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + Layer 1 + + + + + + + + + + + + + + + + + + From 1a6e540a920dbac06705eb2a504b2dd18fbb7463 Mon Sep 17 00:00:00 2001 From: Fab Date: Tue, 28 May 2019 18:32:28 +0200 Subject: [PATCH 007/336] [Theme] Fix theme loading --- es-app/src/FileData.h | 268 ++-- es-app/src/SystemData.cpp | 1188 +++++++------- es-app/src/SystemData.h | 40 +- es-app/src/guis/GuiGamelistOptions.cpp | 18 +- es-app/src/guis/GuiMenu.cpp | 1412 ++++++++--------- es-app/src/guis/GuiMetaDataEd.cpp | 764 ++++----- es-core/src/GuiComponent.cpp | 1164 +++++++------- es-core/src/GuiComponent.h | 444 +++--- es-core/src/ThemeData.cpp | 1457 +++++++++--------- es-core/src/Window.cpp | 1062 ++++++------- es-core/src/components/OptionListComponent.h | 16 +- es-core/src/platform.cpp | 328 ++-- es-core/src/utils/FileSystemUtil.cpp | 1334 ++++++++-------- es-core/src/utils/StringUtil.h | 2 +- 14 files changed, 4750 insertions(+), 4747 deletions(-) diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 00fd4a923b..a4a0d7e866 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -1,134 +1,134 @@ -#pragma once -#ifndef ES_APP_FILE_DATA_H -#define ES_APP_FILE_DATA_H - -#include "utils/FileSystemUtil.h" -#include "MetaData.h" -#include - -class SystemData; -class Window; -struct SystemEnvironmentData; - -enum FileType -{ - GAME = 1, // Cannot have children. - FOLDER = 2, - PLACEHOLDER = 3 -}; - -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); - -// A tree node that holds information for a file. -class FileData -{ -public: - FileData(FileType type, const std::string& path, SystemEnvironmentData* envData, SystemData* system); - virtual ~FileData(); - - virtual const std::string& getName(); - virtual const std::string& getSortName(); - inline FileType getType() const { return mType; } - inline const std::string& getPath() const { return mPath; } - inline FileData* getParent() const { return mParent; } - inline const std::unordered_map& getChildrenByFilename() const { return mChildrenByFilename; } - inline const std::vector& getChildren() const { return mChildren; } - inline SystemData* getSystem() const { return mSystem; } - inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } - virtual const std::string getThumbnailPath() const; - virtual const std::string getVideoPath() const; - virtual const std::string getMarqueePath() const; - virtual const std::string getImagePath() const; - - virtual const std::string getCore() const; - virtual const std::string getEmulator() const; - - virtual const bool getHidden(); - virtual const bool getFavorite(); - - FileData* findUniqueGameForFolder(); - - const std::vector& getChildrenListToDisplay(); - std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; - - void addChild(FileData* file); // Error if mType != FOLDER - void removeChild(FileData* file); //Error if mType != FOLDER - - inline bool isPlaceHolder() { return mType == PLACEHOLDER; }; - - virtual inline void refreshMetadata() { return; }; - - virtual std::string getKey(); - const bool isArcadeAsset(); - inline std::string getFullPath() { return getPath(); }; - inline std::string getFileName() { return Utils::FileSystem::getFileName(getPath()); }; - virtual FileData* getSourceFileData(); - inline std::string getSystemName() const { return mSystemName; }; - - // Returns our best guess at the "real" name for this file (will attempt to perform MAME name translation) - std::string getDisplayName() const; - - // As above, but also remove parenthesis - std::string getCleanName() const; - - void launchGame(Window* window); - - 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; - -protected: - FileData* mSourceFileData; - FileData* mParent; - std::string mSystemName; - //std::string mDefaultCore; - -private: - FileType mType; - std::string mPath; - SystemEnvironmentData* mEnvData; - SystemData* mSystem; - std::unordered_map mChildrenByFilename; - std::vector mChildren; - std::vector mFilteredChildren; -}; - -class CollectionFileData : public FileData -{ -public: - CollectionFileData(FileData* file, SystemData* system); - ~CollectionFileData(); - const std::string& getName(); - void refreshMetadata(); - FileData* getSourceFileData(); - std::string getKey(); -private: - // needs to be updated when metadata changes - std::string mCollectionFileName; - bool mDirty; -}; - -FileData::SortType getSortTypeFromString(std::string desc); - -#endif // ES_APP_FILE_DATA_H +#pragma once +#ifndef ES_APP_FILE_DATA_H +#define ES_APP_FILE_DATA_H + +#include "utils/FileSystemUtil.h" +#include "MetaData.h" +#include + +class SystemData; +class Window; +struct SystemEnvironmentData; + +enum FileType +{ + GAME = 1, // Cannot have children. + FOLDER = 2, + PLACEHOLDER = 3 +}; + +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); + +// A tree node that holds information for a file. +class FileData +{ +public: + FileData(FileType type, const std::string& path, SystemEnvironmentData* envData, SystemData* system); + virtual ~FileData(); + + virtual const std::string& getName(); + virtual const std::string& getSortName(); + inline FileType getType() const { return mType; } + inline const std::string& getPath() const { return mPath; } + inline FileData* getParent() const { return mParent; } + inline const std::unordered_map& getChildrenByFilename() const { return mChildrenByFilename; } + inline const std::vector& getChildren() const { return mChildren; } + inline SystemData* getSystem() const { return mSystem; } + inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } + virtual const std::string getThumbnailPath() const; + virtual const std::string getVideoPath() const; + virtual const std::string getMarqueePath() const; + virtual const std::string getImagePath() const; + + virtual const std::string getCore() const; + virtual const std::string getEmulator() const; + + virtual const bool getHidden(); + virtual const bool getFavorite(); + + FileData* findUniqueGameForFolder(); + + const std::vector& getChildrenListToDisplay(); + std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; + + void addChild(FileData* file); // Error if mType != FOLDER + void removeChild(FileData* file); //Error if mType != FOLDER + + inline bool isPlaceHolder() { return mType == PLACEHOLDER; }; + + virtual inline void refreshMetadata() { return; }; + + virtual std::string getKey(); + const bool isArcadeAsset(); + inline std::string getFullPath() { return getPath(); }; + inline std::string getFileName() { return Utils::FileSystem::getFileName(getPath()); }; + virtual FileData* getSourceFileData(); + inline std::string getSystemName() const { return mSystemName; }; + + // Returns our best guess at the "real" name for this file (will attempt to perform MAME name translation) + std::string getDisplayName() const; + + // As above, but also remove parenthesis + std::string getCleanName() const; + + void launchGame(Window* window); + + 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; + +protected: + FileData* mSourceFileData; + FileData* mParent; + std::string mSystemName; + //std::string mDefaultCore; + +private: + FileType mType; + std::string mPath; + SystemEnvironmentData* mEnvData; + SystemData* mSystem; + std::unordered_map mChildrenByFilename; + std::vector mChildren; + std::vector mFilteredChildren; +}; + +class CollectionFileData : public FileData +{ +public: + CollectionFileData(FileData* file, SystemData* system); + ~CollectionFileData(); + const std::string& getName(); + void refreshMetadata(); + FileData* getSourceFileData(); + std::string getKey(); +private: + // needs to be updated when metadata changes + std::string mCollectionFileName; + bool mDirty; +}; + +FileData::SortType getSortTypeFromString(std::string desc); + +#endif // ES_APP_FILE_DATA_H diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index adae7852e0..6389da9957 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -1,594 +1,594 @@ -#include "SystemData.h" - -#include "utils/FileSystemUtil.h" -#include "CollectionSystemManager.h" -#include "FileFilterIndex.h" -#include "FileSorts.h" -#include "Gamelist.h" -#include "Log.h" -#include "platform.h" -#include "Settings.h" -#include "ThemeData.h" -#include "views/UIModeController.h" -#include -#include -#ifdef WIN32 -#include -#endif - -#include "GuiComponent.h" -#include "Window.h" - -std::vector SystemData::sSystemVector; - -SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : - mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) -{ - mFilterIndex = new FileFilterIndex(); - - // if it's an actual system, initialize it, if not, just create the data structure - if (!CollectionSystem) - { - mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); - mRootFolder->metadata.set("name", mFullName); - - if (!Settings::getInstance()->getBool("ParseGamelistOnly")) - { - populateFolder(mRootFolder); - if (mRootFolder->getChildren().size() == 0) - return; - } - - if (!Settings::getInstance()->getBool("IgnoreGamelist")) - parseGamelist(this); - - mRootFolder->sort(FileSorts::SortTypes.at(0)); - - indexAllGameFilters(mRootFolder); - } - else - { - // virtual systems are updated afterwards, we're just creating the data structure - mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this); - } - setIsGameSystemStatus(); - loadTheme(); -} - -SystemData::~SystemData() -{ - delete mRootFolder; - delete mFilterIndex; -} - -void SystemData::setIsGameSystemStatus() -{ - // we exclude non-game systems from specific operations (i.e. the "RetroPie" system, at least) - // if/when there are more in the future, maybe this can be a more complex method, with a proper list - // but for now a simple string comparison is more performant - mIsGameSystem = (mName != "retropie"); -} - -char _easytolower(char in) { - if (in <= 'Z' && in >= 'A') - return in - ('Z' - 'z'); - return in; -} - -void SystemData::populateFolder(FileData* folder) -{ - const std::string& folderPath = folder->getPath(); - if(!Utils::FileSystem::isDirectory(folderPath)) - { - LOG(LogWarning) << "Error - folder with path \"" << folderPath << "\" is not a directory!"; - return; - } - - int di = folderPath.rfind("downloaded_images"); - int md = folderPath.rfind("media"); - if (di > 0 || md > 0) - return; - - //make sure that this isn't a symlink to a thing we already have - if(Utils::FileSystem::isSymlink(folderPath)) - { - //if this symlink resolves to somewhere that's at the beginning of our path, it's gonna recurse - if(folderPath.find(Utils::FileSystem::getCanonicalPath(folderPath)) == 0) - { - LOG(LogWarning) << "Skipping infinitely recursive symlink \"" << folderPath << "\""; - return; - } - } - - std::string filePath; - std::string extension; - bool isGame; - bool showHidden = Settings::getInstance()->getBool("ShowHiddenFiles"); - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(folderPath); - for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - filePath = *it; - - // skip hidden files and folders - if(!showHidden && Utils::FileSystem::isHidden(filePath)) - 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 = Utils::FileSystem::getExtension(filePath); - std::transform(extension.begin(), extension.end(), extension.begin(), ::_easytolower); - - //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(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) - { - FileData* newGame = new FileData(GAME, filePath, mEnvData, this); - - // preventing new arcade assets to be added - if(!newGame->isArcadeAsset()) - { - folder->addChild(newGame); - isGame = true; - } - } - - //add directories that also do not match an extension as folders - if(!isGame && Utils::FileSystem::isDirectory(filePath)) - { - if (filePath.rfind("downloaded_images") == std::string::npos && - filePath.rfind("media") == std::string::npos) - { - FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); - populateFolder(newFolder); - - if (newFolder->getChildrenByFilename().size() == 0) - delete newFolder; - else if (newFolder->findUniqueGameForFolder() != NULL) - delete newFolder; - else - folder->addChild(newFolder); - } - } - } -} - -void SystemData::indexAllGameFilters(const FileData* folder) -{ - const std::vector& children = folder->getChildren(); - - for(std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) - { - switch((*it)->getType()) - { - case GAME: { mFilterIndex->addToIndex(*it); } break; - case FOLDER: { indexAllGameFilters(*it); } break; - } - } -} - -std::vector readList(const std::string& str, const char* delims = " \t\r\n,") -{ - std::vector 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(Window* window) -{ - deleteSystems(); - - std::string path = getConfigPath(false); - - LOG(LogInfo) << "Loading system config file " << path << "..."; - - if(!Utils::FileSystem::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 tag!"; - return false; - } - - float systemCount = 1; - for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - systemCount++; - - float currentSystem = 0; - for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - { - std::vector emulatorList; - - // std::vector coreList; - - std::string name, fullname, path, cmd, themeFolder, defaultCore; - - name = system.child("name").text().get(); - fullname = system.child("fullname").text().get(); - path = system.child("path").text().get(); - defaultCore = system.child("defaultCore").text().get(); - - pugi::xml_node emulators = system.child("emulators"); - if (emulators != NULL) - { - for (pugi::xml_node emulator : emulators.children()) - { - EmulatorData emulatorData; - emulatorData.mName = emulator.attribute("name").value(); - emulatorData.mCommandLine = emulator.attribute("command").value(); - - pugi::xml_node cores = emulator.child("cores"); - if (cores != NULL) - { - for (pugi::xml_node core : cores.children()) - { - const std::string& corename = core.text().get(); - - if (defaultCore.length() == 0) - defaultCore = corename; - - emulatorData.mCores.push_back(corename); - // coreList.push_back(corename); - } - } - - emulatorList.push_back(emulatorData); - } - } - - if (window != NULL) - window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); - - currentSystem++; - - // convert extensions list from a string into a vector of strings - - std::vector list = readList(system.child("extension").text().get()); - std::vector extensions; - - for (auto extension = list.cbegin(); extension != list.cend(); extension++) - { - std::string xt = (*extension); - std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); - - if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) - extensions.push_back(xt); - } - - cmd = system.child("command").text().get(); - - // platform id list - const char* platformList = system.child("platform").text().get(); - std::vector platformStrs = readList(platformList); - std::vector platformIds; - for(auto it = platformStrs.cbegin(); it != platformStrs.cend(); 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 - path = Utils::FileSystem::getGenericPath(path); - - //expand home symbol if the startpath contains ~ - if (path[0] == '~') - { - path.erase(0, 1); - path.insert(0, Utils::FileSystem::getHomePath()); - } - - //create the system runtime environment data - SystemEnvironmentData* envData = new SystemEnvironmentData; - envData->mStartPath = path; - envData->mSearchExtensions = extensions; - envData->mLaunchCommand = cmd; - envData->mPlatformIds = platformIds; - // envData->mDefaultCore = defaultCore; - envData->mEmulators = emulatorList; - - // envData->mCores = coreList; - - SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); - if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) - { - LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; - delete newSys; - } - else - sSystemVector.push_back(newSys); - } - - if (window != NULL) - window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); - - currentSystem++; - - CollectionSystemManager::get()->loadCollectionSystems(); - return true; -} - -void SystemData::writeExampleConfig(const std::string& path) -{ - std::ofstream file(path.c_str()); - - file << "\n" - "\n" - "\n" - " \n" - " \n" - "\n" - " \n" - " nes\n" - "\n" - " \n" - " Nintendo Entertainment System\n" - "\n" - " \n" - " ~/roms/nes\n" - "\n" - " \n" - " .nes .NES\n" - "\n" - " \n" - " retroarch -L ~/cores/libretro-fceumm.so %ROM%\n" - "\n" - " \n" - " nes\n" - "\n" - " \n" - " nes\n" - " \n" - "\n"; - - file.close(); - - LOG(LogError) << "Example config written! Go read it at \"" << path << "\"!"; -} - -void SystemData::deleteSystems() -{ - bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); - - for(unsigned int i = 0; i < sSystemVector.size(); i++) - { - SystemData* pData = sSystemVector.at(i); - - if (saveOnExit && !pData->mIsCollectionSystem) - updateGamelist(pData); - - delete pData; - } - - sSystemVector.clear(); -} - -std::string SystemData::getConfigPath(bool forWrite) -{ - std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_systems.cfg"; - if(forWrite || Utils::FileSystem::exists(path)) - return path; - - return "/etc/emulationstation/es_systems.cfg"; -} - -bool SystemData::isVisible() -{ - return (getDisplayedGameCount() > 0 || - (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || - (mIsCollectionSystem && mName == "favorites")); -} - -SystemData* SystemData::getNext() const -{ - std::vector::const_iterator it = getIterator(); - - do { - it++; - if (it == sSystemVector.cend()) - it = sSystemVector.cbegin(); - } while (!(*it)->isVisible()); - // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. - - return *it; -} - -SystemData* SystemData::getPrev() const -{ - std::vector::const_reverse_iterator it = getRevIterator(); - - do { - it++; - if (it == sSystemVector.crend()) - it = sSystemVector.crbegin(); - } while (!(*it)->isVisible()); - // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. - - return *it; -} - -std::string SystemData::getGamelistPath(bool forWrite) const -{ - std::string filePath; - - filePath = mRootFolder->getPath() + "/gamelist.xml"; - if(Utils::FileSystem::exists(filePath)) - return filePath; - - filePath = Utils::FileSystem::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 - Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath)); - if(forWrite || Utils::FileSystem::exists(filePath)) - return filePath; - - 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. system theme from currently selected theme set [CURRENT_THEME_PATH]/[SYSTEM]/theme.xml - // 3. default system theme from currently selected theme set [CURRENT_THEME_PATH]/theme.xml - - // first, check game folder - std::string localThemePath = mRootFolder->getPath() + "/theme.xml"; - if(Utils::FileSystem::exists(localThemePath)) - return localThemePath; - - // not in game folder, try system theme in theme sets - localThemePath = ThemeData::getThemeFromCurrentSet(mThemeFolder); - - if (Utils::FileSystem::exists(localThemePath)) - return localThemePath; - - // not system theme, try default system theme in theme set - localThemePath = Utils::FileSystem::getParent(Utils::FileSystem::getParent(localThemePath)) + "/theme.xml"; - - return localThemePath; -} - -bool SystemData::hasGamelist() const -{ - return (Utils::FileSystem::exists(getGamelistPath(false))); -} - -unsigned int SystemData::getGameCount() const -{ - return (unsigned int)mRootFolder->getFilesRecursive(GAME).size(); -} - -SystemData* SystemData::getRandomSystem() -{ - // this is a bit brute force. It might be more efficient to just to a while (!gameSystem) do random again... - unsigned int total = 0; - for(auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) - { - if ((*it)->isGameSystem()) - total ++; - } - - // get random number in range - int target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); - for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) - { - if ((*it)->isGameSystem()) - { - if (target > 0) - { - target--; - } - else - { - return (*it); - } - } - } - - // if we end up here, there is no valid system - return NULL; -} - -FileData* SystemData::getRandomGame() -{ - std::vector list = mRootFolder->getFilesRecursive(GAME, true); - unsigned int total = (int)list.size(); - int target = 0; - // get random number in range - if (total == 0) - return NULL; - target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); - return list.at(target); -} - -unsigned int SystemData::getDisplayedGameCount() const -{ - return (unsigned int)mRootFolder->getFilesRecursive(GAME, true).size(); -} - -void SystemData::loadTheme() -{ - mTheme = std::make_shared(); - - std::string path = getThemePath(); - - if(!Utils::FileSystem::exists(path)) // no theme available for this platform - return; - - try - { - // build map with system variables for theme to use, - std::map sysData; - sysData.insert(std::pair("system.name", getName())); - sysData.insert(std::pair("system.theme", getThemeFolder())); - sysData.insert(std::pair("system.fullName", getFullName())); - - mTheme->loadFile(getThemeFolder(), sysData, path); - } - catch(ThemeException& e) - { - LOG(LogError) << e.what(); - mTheme = std::make_shared(); // reset to empty - } -} +#include "SystemData.h" + +#include "utils/FileSystemUtil.h" +#include "CollectionSystemManager.h" +#include "FileFilterIndex.h" +#include "FileSorts.h" +#include "Gamelist.h" +#include "Log.h" +#include "platform.h" +#include "Settings.h" +#include "ThemeData.h" +#include "views/UIModeController.h" +#include +#include +#ifdef WIN32 +#include +#endif + +#include "GuiComponent.h" +#include "Window.h" + +std::vector SystemData::sSystemVector; + +SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : + mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) +{ + mFilterIndex = new FileFilterIndex(); + + // if it's an actual system, initialize it, if not, just create the data structure + if (!CollectionSystem) + { + mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); + mRootFolder->metadata.set("name", mFullName); + + if (!Settings::getInstance()->getBool("ParseGamelistOnly")) + { + populateFolder(mRootFolder); + if (mRootFolder->getChildren().size() == 0) + return; + } + + if (!Settings::getInstance()->getBool("IgnoreGamelist")) + parseGamelist(this); + + mRootFolder->sort(FileSorts::SortTypes.at(0)); + + indexAllGameFilters(mRootFolder); + } + else + { + // virtual systems are updated afterwards, we're just creating the data structure + mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this); + } + setIsGameSystemStatus(); + loadTheme(); +} + +SystemData::~SystemData() +{ + delete mRootFolder; + delete mFilterIndex; +} + +void SystemData::setIsGameSystemStatus() +{ + // we exclude non-game systems from specific operations (i.e. the "RetroPie" system, at least) + // if/when there are more in the future, maybe this can be a more complex method, with a proper list + // but for now a simple string comparison is more performant + mIsGameSystem = (mName != "retropie"); +} + +char _easytolower(char in) { + if (in <= 'Z' && in >= 'A') + return in - ('Z' - 'z'); + return in; +} + +void SystemData::populateFolder(FileData* folder) +{ + const std::string& folderPath = folder->getPath(); + if(!Utils::FileSystem::isDirectory(folderPath)) + { + LOG(LogWarning) << "Error - folder with path \"" << folderPath << "\" is not a directory!"; + return; + } + + int di = folderPath.rfind("downloaded_images"); + int md = folderPath.rfind("media"); + if (di > 0 || md > 0) + return; + + //make sure that this isn't a symlink to a thing we already have + if(Utils::FileSystem::isSymlink(folderPath)) + { + //if this symlink resolves to somewhere that's at the beginning of our path, it's gonna recurse + if(folderPath.find(Utils::FileSystem::getCanonicalPath(folderPath)) == 0) + { + LOG(LogWarning) << "Skipping infinitely recursive symlink \"" << folderPath << "\""; + return; + } + } + + std::string filePath; + std::string extension; + bool isGame; + bool showHidden = Settings::getInstance()->getBool("ShowHiddenFiles"); + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(folderPath); + for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + filePath = *it; + + // skip hidden files and folders + if(!showHidden && Utils::FileSystem::isHidden(filePath)) + 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 = Utils::FileSystem::getExtension(filePath); + std::transform(extension.begin(), extension.end(), extension.begin(), ::_easytolower); + + //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(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) + { + FileData* newGame = new FileData(GAME, filePath, mEnvData, this); + + // preventing new arcade assets to be added + if(!newGame->isArcadeAsset()) + { + folder->addChild(newGame); + isGame = true; + } + } + + //add directories that also do not match an extension as folders + if(!isGame && Utils::FileSystem::isDirectory(filePath)) + { + if (filePath.rfind("downloaded_images") == std::string::npos && + filePath.rfind("media") == std::string::npos) + { + FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); + populateFolder(newFolder); + + if (newFolder->getChildrenByFilename().size() == 0) + delete newFolder; + else if (newFolder->findUniqueGameForFolder() != NULL) + delete newFolder; + else + folder->addChild(newFolder); + } + } + } +} + +void SystemData::indexAllGameFilters(const FileData* folder) +{ + const std::vector& children = folder->getChildren(); + + for(std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + switch((*it)->getType()) + { + case GAME: { mFilterIndex->addToIndex(*it); } break; + case FOLDER: { indexAllGameFilters(*it); } break; + } + } +} + +std::vector readList(const std::string& str, const char* delims = " \t\r\n,") +{ + std::vector 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(Window* window) +{ + deleteSystems(); + + std::string path = getConfigPath(false); + + LOG(LogInfo) << "Loading system config file " << path << "..."; + + if(!Utils::FileSystem::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 tag!"; + return false; + } + + float systemCount = 1; + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + systemCount++; + + float currentSystem = 0; + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { + std::vector emulatorList; + + // std::vector coreList; + + std::string name, fullname, path, cmd, themeFolder, defaultCore; + + name = system.child("name").text().get(); + fullname = system.child("fullname").text().get(); + path = system.child("path").text().get(); + defaultCore = system.child("defaultCore").text().get(); + + pugi::xml_node emulators = system.child("emulators"); + if (emulators != NULL) + { + for (pugi::xml_node emulator : emulators.children()) + { + EmulatorData emulatorData; + emulatorData.mName = emulator.attribute("name").value(); + emulatorData.mCommandLine = emulator.attribute("command").value(); + + pugi::xml_node cores = emulator.child("cores"); + if (cores != NULL) + { + for (pugi::xml_node core : cores.children()) + { + const std::string& corename = core.text().get(); + + if (defaultCore.length() == 0) + defaultCore = corename; + + emulatorData.mCores.push_back(corename); + // coreList.push_back(corename); + } + } + + emulatorList.push_back(emulatorData); + } + } + + if (window != NULL) + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; + + // convert extensions list from a string into a vector of strings + + std::vector list = readList(system.child("extension").text().get()); + std::vector extensions; + + for (auto extension = list.cbegin(); extension != list.cend(); extension++) + { + std::string xt = (*extension); + std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); + + if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) + extensions.push_back(xt); + } + + cmd = system.child("command").text().get(); + + // platform id list + const char* platformList = system.child("platform").text().get(); + std::vector platformStrs = readList(platformList); + std::vector platformIds; + for(auto it = platformStrs.cbegin(); it != platformStrs.cend(); 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 + path = Utils::FileSystem::getGenericPath(path); + + //expand home symbol if the startpath contains ~ + if (path[0] == '~') + { + path.erase(0, 1); + path.insert(0, Utils::FileSystem::getHomePath()); + } + + //create the system runtime environment data + SystemEnvironmentData* envData = new SystemEnvironmentData; + envData->mStartPath = path; + envData->mSearchExtensions = extensions; + envData->mLaunchCommand = cmd; + envData->mPlatformIds = platformIds; + // envData->mDefaultCore = defaultCore; + envData->mEmulators = emulatorList; + + // envData->mCores = coreList; + + SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); + if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) + { + LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; + delete newSys; + } + else + sSystemVector.push_back(newSys); + } + + if (window != NULL) + window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; + + CollectionSystemManager::get()->loadCollectionSystems(); + return true; +} + +void SystemData::writeExampleConfig(const std::string& path) +{ + std::ofstream file(path.c_str()); + + file << "\n" + "\n" + "\n" + " \n" + " \n" + "\n" + " \n" + " nes\n" + "\n" + " \n" + " Nintendo Entertainment System\n" + "\n" + " \n" + " ~/roms/nes\n" + "\n" + " \n" + " .nes .NES\n" + "\n" + " \n" + " retroarch -L ~/cores/libretro-fceumm.so %ROM%\n" + "\n" + " \n" + " nes\n" + "\n" + " \n" + " nes\n" + " \n" + "\n"; + + file.close(); + + LOG(LogError) << "Example config written! Go read it at \"" << path << "\"!"; +} + +void SystemData::deleteSystems() +{ + bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); + + for(unsigned int i = 0; i < sSystemVector.size(); i++) + { + SystemData* pData = sSystemVector.at(i); + + if (saveOnExit && !pData->mIsCollectionSystem) + updateGamelist(pData); + + delete pData; + } + + sSystemVector.clear(); +} + +std::string SystemData::getConfigPath(bool forWrite) +{ + std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_systems.cfg"; + if(forWrite || Utils::FileSystem::exists(path)) + return path; + + return "/etc/emulationstation/es_systems.cfg"; +} + +bool SystemData::isVisible() +{ + return (getDisplayedGameCount() > 0 || + (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || + (mIsCollectionSystem && mName == "favorites")); +} + +SystemData* SystemData::getNext() const +{ + std::vector::const_iterator it = getIterator(); + + do { + it++; + if (it == sSystemVector.cend()) + it = sSystemVector.cbegin(); + } while (!(*it)->isVisible()); + // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. + + return *it; +} + +SystemData* SystemData::getPrev() const +{ + std::vector::const_reverse_iterator it = getRevIterator(); + + do { + it++; + if (it == sSystemVector.crend()) + it = sSystemVector.crbegin(); + } while (!(*it)->isVisible()); + // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. + + return *it; +} + +std::string SystemData::getGamelistPath(bool forWrite) const +{ + std::string filePath; + + filePath = mRootFolder->getPath() + "/gamelist.xml"; + if(Utils::FileSystem::exists(filePath)) + return filePath; + + filePath = Utils::FileSystem::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 + Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath)); + if(forWrite || Utils::FileSystem::exists(filePath)) + return filePath; + + 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. system theme from currently selected theme set [CURRENT_THEME_PATH]/[SYSTEM]/theme.xml + // 3. default system theme from currently selected theme set [CURRENT_THEME_PATH]/theme.xml + + // first, check game folder + std::string localThemePath = mRootFolder->getPath() + "/theme.xml"; + if(Utils::FileSystem::exists(localThemePath)) + return localThemePath; + + // not in game folder, try system theme in theme sets + localThemePath = ThemeData::getThemeFromCurrentSet(mThemeFolder); + + if (Utils::FileSystem::exists(localThemePath)) + return localThemePath; + + // not system theme, try default system theme in theme set + localThemePath = Utils::FileSystem::getParent(Utils::FileSystem::getParent(localThemePath)) + "/theme.xml"; + + return localThemePath; +} + +bool SystemData::hasGamelist() const +{ + return (Utils::FileSystem::exists(getGamelistPath(false))); +} + +unsigned int SystemData::getGameCount() const +{ + return (unsigned int)mRootFolder->getFilesRecursive(GAME).size(); +} + +SystemData* SystemData::getRandomSystem() +{ + // this is a bit brute force. It might be more efficient to just to a while (!gameSystem) do random again... + unsigned int total = 0; + for(auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) + { + if ((*it)->isGameSystem()) + total ++; + } + + // get random number in range + int target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); + for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) + { + if ((*it)->isGameSystem()) + { + if (target > 0) + { + target--; + } + else + { + return (*it); + } + } + } + + // if we end up here, there is no valid system + return NULL; +} + +FileData* SystemData::getRandomGame() +{ + std::vector list = mRootFolder->getFilesRecursive(GAME, true); + unsigned int total = (int)list.size(); + int target = 0; + // get random number in range + if (total == 0) + return NULL; + target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); + return list.at(target); +} + +unsigned int SystemData::getDisplayedGameCount() const +{ + return (unsigned int)mRootFolder->getFilesRecursive(GAME, true).size(); +} + +void SystemData::loadTheme() +{ + mTheme = std::make_shared(); + + std::string path = getThemePath(); + + if(!Utils::FileSystem::exists(path)) // no theme available for this platform + return; + + try + { + // build map with system variables for theme to use, + std::map sysData; + sysData.insert(std::pair("system.name", getName())); + sysData.insert(std::pair("system.theme", getThemeFolder())); + sysData.insert(std::pair("system.fullName", getFullName())); + + mTheme->loadFile(getThemeFolder(), sysData, path); + } + catch(ThemeException& e) + { + LOG(LogError) << e.what(); + mTheme = std::make_shared(); // reset to empty + } +} diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 3925b74961..fd0db7a977 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -33,42 +33,42 @@ struct SystemEnvironmentData std::vector getCores(std::string emulatorName) { - std::vector list; - - for (auto& emulator : mEmulators) - if (emulatorName == emulator.mName) - return emulator.mCores; + std::vector list; + + for (auto& emulator : mEmulators) + if (emulatorName == emulator.mName) + return emulator.mCores; return list; } std::string getDefaultEmulator() { - for (auto& emulator : mEmulators) - return emulator.mName; + for (auto& emulator : mEmulators) + return emulator.mName; return ""; } std::string getDefaultCore(std::string emulatorName) - { - for (auto& emulator : mEmulators) - { - if (emulatorName == emulator.mName) - { - for (auto core : emulator.mCores) - return core; - } - } + { + for (auto& emulator : mEmulators) + { + if (emulatorName == emulator.mName) + { + for (auto core : emulator.mCores) + return core; + } + } return ""; } std::string getEmulatorCommandLine(std::string emulatorName) - { - for (auto& emulator : mEmulators) - if (emulatorName == emulator.mName) - return emulator.mCommandLine; + { + for (auto& emulator : mEmulators) + if (emulatorName == emulator.mName) + return emulator.mCommandLine; return ""; } diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 01c0741c21..fd42411e4a 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -124,13 +124,13 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui float x0 = (mSize.x() - mMenu.getSize().x()) / 2; - float y1 = Renderer::getScreenHeight(); + float y1 = Renderer::getScreenHeight(); float y2 = (mSize.y() - mMenu.getSize().y()) / 2; - //float y1 = mMenu.getSize().y(); - //float y2 = (mSize.y() - mMenu.getSize().y()) / 2; - - setPosition(x0, y1); - + //float y1 = mMenu.getSize().y(); + //float y2 = (mSize.y() - mMenu.getSize().y()) / 2; + + setPosition(x0, y1); + auto fadeFunc = [this, x0, y1, y2](float t) { t -= 1; // cubic ease out @@ -142,9 +142,9 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, x0, y2] { - setPosition(x0, y2); - }); - + setPosition(x0, y2); + }); + setPosition(x0, y2); } diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index a383ee2ff4..beaf8e8749 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -1,64 +1,64 @@ -#include "guis/GuiMenu.h" - -#include "components/OptionListComponent.h" -#include "components/SliderComponent.h" -#include "components/SwitchComponent.h" -#include "guis/GuiCollectionSystemsOptions.h" -#include "guis/GuiDetectDevice.h" -#include "guis/GuiGeneralScreensaverOptions.h" -#include "guis/GuiMsgBox.h" -#include "guis/GuiScraperStart.h" -#include "guis/GuiSettings.h" -#include "views/UIModeController.h" -#include "views/ViewController.h" -#include "CollectionSystemManager.h" -#include "EmulationStation.h" -#include "Scripting.h" -#include "SystemData.h" -#include "VolumeControl.h" -#include -#include - -#include "animations/LambdaAnimation.h" - -GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) - -{ - bool isFullUI = UIModeController::getInstance()->isUIModeFull(); - - if (isFullUI) - { - addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); - addEntry(_T("CONFIGURE INPUT"), 0x777777FF, true, [this] { openConfigInput(); }); - } - - addEntry(_T("SOUND SETTINGS"), 0x777777FF, true, [this] { openSoundSettings(); }); - - if (isFullUI) - addEntry(_T("SCRAPER"), 0x777777FF, true, [this] { openScraperSettings(); }); - - if (isFullUI) - { - addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); - addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); - } - -#if defined(_WIN32) - addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); -#else - addEntry(_T("QUIT"), 0x777777FF, true, [this] {openQuitMenu(); }); -#endif - - addChild(&mMenu); - addVersionInfo(); - - setSize(mMenu.getSize()); - - float y1 = Renderer::getScreenHeight(); - float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; - - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); - +#include "guis/GuiMenu.h" + +#include "components/OptionListComponent.h" +#include "components/SliderComponent.h" +#include "components/SwitchComponent.h" +#include "guis/GuiCollectionSystemsOptions.h" +#include "guis/GuiDetectDevice.h" +#include "guis/GuiGeneralScreensaverOptions.h" +#include "guis/GuiMsgBox.h" +#include "guis/GuiScraperStart.h" +#include "guis/GuiSettings.h" +#include "views/UIModeController.h" +#include "views/ViewController.h" +#include "CollectionSystemManager.h" +#include "EmulationStation.h" +#include "Scripting.h" +#include "SystemData.h" +#include "VolumeControl.h" +#include +#include + +#include "animations/LambdaAnimation.h" + +GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) + +{ + bool isFullUI = UIModeController::getInstance()->isUIModeFull(); + + if (isFullUI) + { + addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); + addEntry(_T("CONFIGURE INPUT"), 0x777777FF, true, [this] { openConfigInput(); }); + } + + addEntry(_T("SOUND SETTINGS"), 0x777777FF, true, [this] { openSoundSettings(); }); + + if (isFullUI) + addEntry(_T("SCRAPER"), 0x777777FF, true, [this] { openScraperSettings(); }); + + if (isFullUI) + { + addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); + addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); + } + +#if defined(_WIN32) + addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); +#else + addEntry(_T("QUIT"), 0x777777FF, true, [this] {openQuitMenu(); }); +#endif + + addChild(&mMenu); + addVersionInfo(); + + setSize(mMenu.getSize()); + + float y1 = Renderer::getScreenHeight(); + float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); + auto fadeFunc = [this, y1, y2](float t) { t -= 1; // cubic ease out @@ -70,648 +70,648 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] { - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); - }); - - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); -} - -void GuiMenu::openScraperSettings() -{ - auto s = new GuiSettings(mWindow, "SCRAPER"); - - // scrape from - auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, "SCRAPE FROM", false); - std::vector scrapers = getScraperList(); - - // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. - for(auto it = scrapers.cbegin(); it != scrapers.cend(); 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(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; - auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; - std::function openAndSave = openScrapeNow; - openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; - row.makeAcceptInputHandler(openAndSave); - - auto scrape_now = std::make_shared(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); -} - -void GuiMenu::openSoundSettings() -{ - auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); - - // volume - auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); - volume->setValue((float)VolumeControl::getInstance()->getVolume()); - s->addWithLabel(_T("SYSTEM VOLUME"), volume); - s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); - - if (UIModeController::getInstance()->isUIModeFull()) - { -#if defined(__linux__) - // audio card - auto audio_card = std::make_shared< OptionListComponent >(mWindow, "AUDIO CARD", false); - std::vector audio_cards; - #ifdef _RPI_ - // RPi Specific Audio Cards - audio_cards.push_back("local"); - audio_cards.push_back("hdmi"); - audio_cards.push_back("both"); - #endif - audio_cards.push_back("default"); - audio_cards.push_back("sysdefault"); - audio_cards.push_back("dmix"); - audio_cards.push_back("hw"); - audio_cards.push_back("plughw"); - audio_cards.push_back("null"); - if (Settings::getInstance()->getString("AudioCard") != "") { - if(std::find(audio_cards.begin(), audio_cards.end(), Settings::getInstance()->getString("AudioCard")) == audio_cards.end()) { - audio_cards.push_back(Settings::getInstance()->getString("AudioCard")); - } - } - for(auto ac = audio_cards.cbegin(); ac != audio_cards.cend(); ac++) - audio_card->add(*ac, *ac, Settings::getInstance()->getString("AudioCard") == *ac); - s->addWithLabel("AUDIO CARD", audio_card); - s->addSaveFunc([audio_card] { - Settings::getInstance()->setString("AudioCard", audio_card->getSelected()); - VolumeControl::getInstance()->deinit(); - VolumeControl::getInstance()->init(); - }); - - // volume control device - auto vol_dev = std::make_shared< OptionListComponent >(mWindow, "AUDIO DEVICE", false); - std::vector transitions; - transitions.push_back("PCM"); - transitions.push_back("Speaker"); - transitions.push_back("Master"); - transitions.push_back("Digital"); - transitions.push_back("Analogue"); - if (Settings::getInstance()->getString("AudioDevice") != "") { - if(std::find(transitions.begin(), transitions.end(), Settings::getInstance()->getString("AudioDevice")) == transitions.end()) { - transitions.push_back(Settings::getInstance()->getString("AudioDevice")); - } - } - for(auto it = transitions.cbegin(); it != transitions.cend(); it++) - vol_dev->add(*it, *it, Settings::getInstance()->getString("AudioDevice") == *it); - s->addWithLabel("AUDIO DEVICE", vol_dev); - s->addSaveFunc([vol_dev] { - Settings::getInstance()->setString("AudioDevice", vol_dev->getSelected()); - VolumeControl::getInstance()->deinit(); - VolumeControl::getInstance()->init(); - }); -#endif - - // disable sounds - auto sounds_enabled = std::make_shared(mWindow); - sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); - s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); - s->addSaveFunc([sounds_enabled] { - if (sounds_enabled->getState() - && !Settings::getInstance()->getBool("EnableSounds") - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState()); - }); - - auto video_audio = std::make_shared(mWindow); - video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); - s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); - s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); - -#ifdef _RPI_ - // OMX player Audio Device - auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, "OMX PLAYER AUDIO DEVICE", false); - std::vector omx_cards; - // RPi Specific Audio Cards - omx_cards.push_back("local"); - omx_cards.push_back("hdmi"); - omx_cards.push_back("both"); - omx_cards.push_back("alsa:hw:0,0"); - omx_cards.push_back("alsa:hw:1,0"); - if (Settings::getInstance()->getString("OMXAudioDev") != "") { - if (std::find(omx_cards.begin(), omx_cards.end(), Settings::getInstance()->getString("OMXAudioDev")) == omx_cards.end()) { - omx_cards.push_back(Settings::getInstance()->getString("OMXAudioDev")); - } - } - for (auto it = omx_cards.cbegin(); it != omx_cards.cend(); it++) - omx_audio_dev->add(*it, *it, Settings::getInstance()->getString("OMXAudioDev") == *it); - s->addWithLabel("OMX PLAYER AUDIO DEVICE", omx_audio_dev); - s->addSaveFunc([omx_audio_dev] { - if (Settings::getInstance()->getString("OMXAudioDev") != omx_audio_dev->getSelected()) - Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); - }); -#endif - } - - mWindow->pushGui(s); - -} - -void GuiMenu::openUISettings() -{ - auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); - - // theme set - auto themeSets = ThemeData::getThemeSets(); - - if (!themeSets.empty()) - { - std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if (selectedSet == themeSets.cend()) - selectedSet = themeSets.cbegin(); - - auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); - for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) - theme_set->add(it->first, it->first, it == selectedSet); - s->addWithLabel(_T("THEME"), theme_set); - - Window* window = mWindow; - s->addSaveFunc([window, theme_set] - { - bool needReload = false; - std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - if (oldTheme != theme_set->getSelected()) - needReload = true; - - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - - if (needReload) - { - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation - } - }); - } - - // LANGUAGE - - std::vector langues; - langues.push_back("en"); - - - std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); - if (xmlpath.length() > 0) - { - xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; - - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); - for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if (Utils::FileSystem::isDirectory(*it)) - continue; - - std::string name = *it; - - if (name.rfind("emulationstation2.po") == std::string::npos) - continue; - - name = Utils::FileSystem::getParent(name); - name = Utils::FileSystem::getFileName(name); - - if (name != "en") - langues.push_back(name); - } - - if (langues.size() > 1) - { - auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); - - for (auto it = langues.cbegin(); it != langues.cend(); it++) - language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); - - s->addWithLabel(_T("LANGUAGE"), language); - s->addSaveFunc([language] { - Settings::getInstance()->setString("Language", language->getSelected()); - }); - } - } - - // transition style - auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); - std::vector transitions; - transitions.push_back("fade"); - transitions.push_back("slide"); - transitions.push_back("instant"); - for (auto it = transitions.cbegin(); it != transitions.cend(); it++) - transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); - - s->addWithLabel(_T("TRANSITION STYLE"), transition_style); - s->addSaveFunc([transition_style] { - if (Settings::getInstance()->getString("TransitionStyle") == "instant" - && transition_style->getSelected() != "instant" - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); - }); - - - auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); - std::vector gameTransitions; - gameTransitions.push_back("fade"); - gameTransitions.push_back("slide"); - gameTransitions.push_back("instant"); - for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) - transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); - - s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); - s->addSaveFunc([transitionOfGames_style] { - if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" - && transitionOfGames_style->getSelected() != "instant" - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); - }); - - // GameList view style - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); - std::vector styles; - styles.push_back("automatic"); - styles.push_back("basic"); - styles.push_back("detailed"); - styles.push_back("video"); - styles.push_back("grid"); - styles.push_back("gridex"); - - for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); - - s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); - s->addSaveFunc([gamelist_style] { - bool needReload = false; - if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) - needReload = true; - Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); - if (needReload) - ViewController::get()->reloadAll(); - }); - - - // Optionally start in selected system - auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); - systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); - for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) - { - if ("retropie" != (*it)->getName()) - { - systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); - } - } - s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); - s->addSaveFunc([systemfocus_list] { - Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); - }); - - - /* - //UI mode - auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); - std::vector UImodes = UIModeController::getInstance()->getUIModes(); - for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) - UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); - s->addWithLabel("UI MODE", UImodeSelection); - Window* window = mWindow; - s->addSaveFunc([ UImodeSelection, window] - { - std::string selectedMode = UImodeSelection->getSelected(); - if (selectedMode != "Full") - { - std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; - msg += "This will hide most menu-options to prevent changes to the system.\n"; - msg += "To unlock and return to the full UI, enter this code: \n"; - msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; - msg += "Do you want to proceed?"; - window->pushGui(new GuiMsgBox(window, msg, - "YES", [selectedMode] { - LOG(LogDebug) << "Setting UI mode to " << selectedMode; - Settings::getInstance()->setString("UIMode", selectedMode); - Settings::getInstance()->saveFile(); - }, "NO",nullptr)); - } - }); - */ - - // screensaver - ComponentListRow screensaver_row; - screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - screensaver_row.addElement(makeArrow(mWindow), false); - screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); - s->addRow(screensaver_row); - -#if defined(_WIN32) - // quick system select (left/right in game list view) - auto hideWindowScreen = std::make_shared(mWindow); - hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); - s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); - s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); -#endif - - // quick system select (left/right in game list view) - auto quick_sys_select = std::make_shared(mWindow); - quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); - s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); - s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); - - // carousel transition option - auto move_carousel = std::make_shared(mWindow); - move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); - s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); - s->addSaveFunc([move_carousel] { - if (move_carousel->getState() - && !Settings::getInstance()->getBool("MoveCarousel") - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); - }); - - // show help - auto show_help = std::make_shared(mWindow); - show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); - s->addWithLabel(_T("ON-SCREEN HELP"), show_help); - s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); - - // enable filters (ForceDisableFilters) - auto enable_filter = std::make_shared(mWindow); - enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); - s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); - s->addSaveFunc([enable_filter] { - bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); - Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); - if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); - }); - - mWindow->pushGui(s); - -} - -void GuiMenu::openOtherSettings() -{ - auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); - - // maximum vram - auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); - max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel(_T("VRAM LIMIT"), max_vram); - s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); - - // power saver - auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); - std::vector modes; - modes.push_back("disabled"); - modes.push_back("default"); - modes.push_back("enhanced"); - modes.push_back("instant"); - for (auto it = modes.cbegin(); it != modes.cend(); it++) - power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); - - s->addWithLabel(_T("POWER SAVER MODES"), power_saver); - s->addSaveFunc([this, power_saver] { - if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { - Settings::getInstance()->setString("TransitionStyle", "instant"); - Settings::getInstance()->setBool("MoveCarousel", false); - Settings::getInstance()->setBool("EnableSounds", false); - } - Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); - PowerSaver::init(); - }); - - // gamelists - auto save_gamelists = std::make_shared(mWindow); - save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); - s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); - s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); - - auto parse_gamelists = std::make_shared(mWindow); - parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); - s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); - s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); - /* - auto local_art = std::make_shared(mWindow); - local_art->setState(Settings::getInstance()->getBool("LocalArt")); - s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); - s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); - */ - // hidden files - auto hidden_files = std::make_shared(mWindow); - hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); - s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); - s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); - -#ifdef _RPI_ - // Video Player - VideoOmxPlayer - auto omx_player = std::make_shared(mWindow); - omx_player->setState(Settings::getInstance()->getBool("VideoOmxPlayer")); - s->addWithLabel("USE OMX PLAYER (HW ACCELERATED)", omx_player); - s->addSaveFunc([omx_player] - { - // need to reload all views to re-create the right video components - bool needReload = false; - if(Settings::getInstance()->getBool("VideoOmxPlayer") != omx_player->getState()) - needReload = true; - - Settings::getInstance()->setBool("VideoOmxPlayer", omx_player->getState()); - - if(needReload) - ViewController::get()->reloadAll(); - }); - -#endif - - // framerate - /* - auto framerate = std::make_shared(mWindow); - framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); - s->addWithLabel(_T("SHOW FRAMERATE"), framerate); - s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); - */ - - mWindow->pushGui(s); - -} - -void GuiMenu::openConfigInput() -{ - Window* window = mWindow; - window->pushGui(new GuiDetectDevice(window, false, nullptr)); - /* - window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), - [window] { - window->pushGui(new GuiDetectDevice(window, false, nullptr)); - }, _T("NO"), nullptr) - );*/ - -} - -void GuiMenu::openQuitMenu() -{ -#if defined(_WIN32) - Scripting::fireEvent("quit"); - quitES(""); - return; -#endif - - auto s = new GuiSettings(mWindow, _T("QUIT")); - - Window* window = mWindow; - - ComponentListRow row; - if (UIModeController::getInstance()->isUIModeFull()) - { - row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", - [] { - Scripting::fireEvent("quit"); - if(quitES("/tmp/es-restart") != 0) - LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", 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", - [] { - Scripting::fireEvent("quit"); - quitES(""); - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - s->addRow(row); - } - } - row.elements.clear(); - row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", - [] { - Scripting::fireEvent("quit", "reboot"); - Scripting::fireEvent("reboot"); - if (quitES("/tmp/es-sysrestart") != 0) - LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(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", - [] { - Scripting::fireEvent("quit", "shutdown"); - Scripting::fireEvent("shutdown"); - if (quitES("/tmp/es-shutdown") != 0) - LOG(LogWarning) << "Shutdown terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - s->addRow(row); - - mWindow->pushGui(s); -} - -void GuiMenu::addVersionInfo() -{ - std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); - - mVersion.setFont(Font::get(FONT_SIZE_SMALL)); - mVersion.setColor(0x5E5E5EFF); - mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); - mVersion.setHorizontalAlignment(ALIGN_CENTER); - addChild(&mVersion); -} - -void GuiMenu::openScreensaverOptions() { - mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); -} - -void GuiMenu::openCollectionSystemSettings() { - mWindow->pushGui(new GuiCollectionSystemsOptions(mWindow)); -} - -void GuiMenu::onSizeChanged() -{ - mVersion.setSize(mSize.x(), 0); - mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); -} - -void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) -{ - std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); - - // populate the list - ComponentListRow row; - row.addElement(std::make_shared(mWindow, name, font, color), true); - - if(add_arrow) - { - std::shared_ptr 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; -} - -HelpStyle GuiMenu::getHelpStyle() -{ - HelpStyle style = HelpStyle(); - style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); - return style; -} - -std::vector GuiMenu::getHelpPrompts() -{ - std::vector prompts; - prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); - prompts.push_back(HelpPrompt("a", _T("SELECT"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); - return prompts; -} + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + }); + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); +} + +void GuiMenu::openScraperSettings() +{ + auto s = new GuiSettings(mWindow, "SCRAPER"); + + // scrape from + auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, "SCRAPE FROM", false); + std::vector scrapers = getScraperList(); + + // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. + for(auto it = scrapers.cbegin(); it != scrapers.cend(); 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(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; + auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; + std::function openAndSave = openScrapeNow; + openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; + row.makeAcceptInputHandler(openAndSave); + + auto scrape_now = std::make_shared(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); +} + +void GuiMenu::openSoundSettings() +{ + auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); + + // volume + auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); + volume->setValue((float)VolumeControl::getInstance()->getVolume()); + s->addWithLabel(_T("SYSTEM VOLUME"), volume); + s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); + + if (UIModeController::getInstance()->isUIModeFull()) + { +#if defined(__linux__) + // audio card + auto audio_card = std::make_shared< OptionListComponent >(mWindow, "AUDIO CARD", false); + std::vector audio_cards; + #ifdef _RPI_ + // RPi Specific Audio Cards + audio_cards.push_back("local"); + audio_cards.push_back("hdmi"); + audio_cards.push_back("both"); + #endif + audio_cards.push_back("default"); + audio_cards.push_back("sysdefault"); + audio_cards.push_back("dmix"); + audio_cards.push_back("hw"); + audio_cards.push_back("plughw"); + audio_cards.push_back("null"); + if (Settings::getInstance()->getString("AudioCard") != "") { + if(std::find(audio_cards.begin(), audio_cards.end(), Settings::getInstance()->getString("AudioCard")) == audio_cards.end()) { + audio_cards.push_back(Settings::getInstance()->getString("AudioCard")); + } + } + for(auto ac = audio_cards.cbegin(); ac != audio_cards.cend(); ac++) + audio_card->add(*ac, *ac, Settings::getInstance()->getString("AudioCard") == *ac); + s->addWithLabel("AUDIO CARD", audio_card); + s->addSaveFunc([audio_card] { + Settings::getInstance()->setString("AudioCard", audio_card->getSelected()); + VolumeControl::getInstance()->deinit(); + VolumeControl::getInstance()->init(); + }); + + // volume control device + auto vol_dev = std::make_shared< OptionListComponent >(mWindow, "AUDIO DEVICE", false); + std::vector transitions; + transitions.push_back("PCM"); + transitions.push_back("Speaker"); + transitions.push_back("Master"); + transitions.push_back("Digital"); + transitions.push_back("Analogue"); + if (Settings::getInstance()->getString("AudioDevice") != "") { + if(std::find(transitions.begin(), transitions.end(), Settings::getInstance()->getString("AudioDevice")) == transitions.end()) { + transitions.push_back(Settings::getInstance()->getString("AudioDevice")); + } + } + for(auto it = transitions.cbegin(); it != transitions.cend(); it++) + vol_dev->add(*it, *it, Settings::getInstance()->getString("AudioDevice") == *it); + s->addWithLabel("AUDIO DEVICE", vol_dev); + s->addSaveFunc([vol_dev] { + Settings::getInstance()->setString("AudioDevice", vol_dev->getSelected()); + VolumeControl::getInstance()->deinit(); + VolumeControl::getInstance()->init(); + }); +#endif + + // disable sounds + auto sounds_enabled = std::make_shared(mWindow); + sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); + s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); + s->addSaveFunc([sounds_enabled] { + if (sounds_enabled->getState() + && !Settings::getInstance()->getBool("EnableSounds") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState()); + }); + + auto video_audio = std::make_shared(mWindow); + video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); + s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); + s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); + +#ifdef _RPI_ + // OMX player Audio Device + auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, "OMX PLAYER AUDIO DEVICE", false); + std::vector omx_cards; + // RPi Specific Audio Cards + omx_cards.push_back("local"); + omx_cards.push_back("hdmi"); + omx_cards.push_back("both"); + omx_cards.push_back("alsa:hw:0,0"); + omx_cards.push_back("alsa:hw:1,0"); + if (Settings::getInstance()->getString("OMXAudioDev") != "") { + if (std::find(omx_cards.begin(), omx_cards.end(), Settings::getInstance()->getString("OMXAudioDev")) == omx_cards.end()) { + omx_cards.push_back(Settings::getInstance()->getString("OMXAudioDev")); + } + } + for (auto it = omx_cards.cbegin(); it != omx_cards.cend(); it++) + omx_audio_dev->add(*it, *it, Settings::getInstance()->getString("OMXAudioDev") == *it); + s->addWithLabel("OMX PLAYER AUDIO DEVICE", omx_audio_dev); + s->addSaveFunc([omx_audio_dev] { + if (Settings::getInstance()->getString("OMXAudioDev") != omx_audio_dev->getSelected()) + Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); + }); +#endif + } + + mWindow->pushGui(s); + +} + +void GuiMenu::openUISettings() +{ + auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); + + // theme set + auto themeSets = ThemeData::getThemeSets(); + + if (!themeSets.empty()) + { + std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if (selectedSet == themeSets.cend()) + selectedSet = themeSets.cbegin(); + + auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); + for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) + theme_set->add(it->first, it->first, it == selectedSet); + s->addWithLabel(_T("THEME"), theme_set); + + Window* window = mWindow; + s->addSaveFunc([window, theme_set] + { + bool needReload = false; + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + if (oldTheme != theme_set->getSelected()) + needReload = true; + + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); + + if (needReload) + { + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + } + }); + } + + // LANGUAGE + + std::vector langues; + langues.push_back("en"); + + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); + if (xmlpath.length() > 0) + { + xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if (Utils::FileSystem::isDirectory(*it)) + continue; + + std::string name = *it; + + if (name.rfind("emulationstation2.po") == std::string::npos) + continue; + + name = Utils::FileSystem::getParent(name); + name = Utils::FileSystem::getFileName(name); + + if (name != "en") + langues.push_back(name); + } + + if (langues.size() > 1) + { + auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); + + for (auto it = langues.cbegin(); it != langues.cend(); it++) + language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); + + s->addWithLabel(_T("LANGUAGE"), language); + s->addSaveFunc([language] { + Settings::getInstance()->setString("Language", language->getSelected()); + }); + } + } + + // transition style + auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); + std::vector transitions; + transitions.push_back("fade"); + transitions.push_back("slide"); + transitions.push_back("instant"); + for (auto it = transitions.cbegin(); it != transitions.cend(); it++) + transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); + + s->addWithLabel(_T("TRANSITION STYLE"), transition_style); + s->addSaveFunc([transition_style] { + if (Settings::getInstance()->getString("TransitionStyle") == "instant" + && transition_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); + }); + + + auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); + std::vector gameTransitions; + gameTransitions.push_back("fade"); + gameTransitions.push_back("slide"); + gameTransitions.push_back("instant"); + for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) + transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); + + s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); + s->addSaveFunc([transitionOfGames_style] { + if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" + && transitionOfGames_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); + }); + + // GameList view style + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); + std::vector styles; + styles.push_back("automatic"); + styles.push_back("basic"); + styles.push_back("detailed"); + styles.push_back("video"); + styles.push_back("grid"); + styles.push_back("gridex"); + + for (auto it = styles.cbegin(); it != styles.cend(); it++) + gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); + + s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); + s->addSaveFunc([gamelist_style] { + bool needReload = false; + if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) + needReload = true; + Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); + if (needReload) + ViewController::get()->reloadAll(); + }); + + + // Optionally start in selected system + auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); + systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + { + if ("retropie" != (*it)->getName()) + { + systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); + } + } + s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); + s->addSaveFunc([systemfocus_list] { + Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); + }); + + + /* + //UI mode + auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); + std::vector UImodes = UIModeController::getInstance()->getUIModes(); + for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) + UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); + s->addWithLabel("UI MODE", UImodeSelection); + Window* window = mWindow; + s->addSaveFunc([ UImodeSelection, window] + { + std::string selectedMode = UImodeSelection->getSelected(); + if (selectedMode != "Full") + { + std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; + msg += "This will hide most menu-options to prevent changes to the system.\n"; + msg += "To unlock and return to the full UI, enter this code: \n"; + msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; + msg += "Do you want to proceed?"; + window->pushGui(new GuiMsgBox(window, msg, + "YES", [selectedMode] { + LOG(LogDebug) << "Setting UI mode to " << selectedMode; + Settings::getInstance()->setString("UIMode", selectedMode); + Settings::getInstance()->saveFile(); + }, "NO",nullptr)); + } + }); + */ + + // screensaver + ComponentListRow screensaver_row; + screensaver_row.elements.clear(); + screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + screensaver_row.addElement(makeArrow(mWindow), false); + screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); + s->addRow(screensaver_row); + +#if defined(_WIN32) + // quick system select (left/right in game list view) + auto hideWindowScreen = std::make_shared(mWindow); + hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); + s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); + s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); +#endif + + // quick system select (left/right in game list view) + auto quick_sys_select = std::make_shared(mWindow); + quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); + s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); + s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); + + // carousel transition option + auto move_carousel = std::make_shared(mWindow); + move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); + s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); + s->addSaveFunc([move_carousel] { + if (move_carousel->getState() + && !Settings::getInstance()->getBool("MoveCarousel") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); + }); + + // show help + auto show_help = std::make_shared(mWindow); + show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); + s->addWithLabel(_T("ON-SCREEN HELP"), show_help); + s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); + + // enable filters (ForceDisableFilters) + auto enable_filter = std::make_shared(mWindow); + enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); + s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); + s->addSaveFunc([enable_filter] { + bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); + Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); + if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); + }); + + mWindow->pushGui(s); + +} + +void GuiMenu::openOtherSettings() +{ + auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); + + // maximum vram + auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); + max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); + s->addWithLabel(_T("VRAM LIMIT"), max_vram); + s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); + + // power saver + auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); + std::vector modes; + modes.push_back("disabled"); + modes.push_back("default"); + modes.push_back("enhanced"); + modes.push_back("instant"); + for (auto it = modes.cbegin(); it != modes.cend(); it++) + power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); + + s->addWithLabel(_T("POWER SAVER MODES"), power_saver); + s->addSaveFunc([this, power_saver] { + if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { + Settings::getInstance()->setString("TransitionStyle", "instant"); + Settings::getInstance()->setBool("MoveCarousel", false); + Settings::getInstance()->setBool("EnableSounds", false); + } + Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); + PowerSaver::init(); + }); + + // gamelists + auto save_gamelists = std::make_shared(mWindow); + save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); + s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); + s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); + + auto parse_gamelists = std::make_shared(mWindow); + parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); + s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); + s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); + /* + auto local_art = std::make_shared(mWindow); + local_art->setState(Settings::getInstance()->getBool("LocalArt")); + s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); + s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); + */ + // hidden files + auto hidden_files = std::make_shared(mWindow); + hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); + s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); + s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); + +#ifdef _RPI_ + // Video Player - VideoOmxPlayer + auto omx_player = std::make_shared(mWindow); + omx_player->setState(Settings::getInstance()->getBool("VideoOmxPlayer")); + s->addWithLabel("USE OMX PLAYER (HW ACCELERATED)", omx_player); + s->addSaveFunc([omx_player] + { + // need to reload all views to re-create the right video components + bool needReload = false; + if(Settings::getInstance()->getBool("VideoOmxPlayer") != omx_player->getState()) + needReload = true; + + Settings::getInstance()->setBool("VideoOmxPlayer", omx_player->getState()); + + if(needReload) + ViewController::get()->reloadAll(); + }); + +#endif + + // framerate + /* + auto framerate = std::make_shared(mWindow); + framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); + s->addWithLabel(_T("SHOW FRAMERATE"), framerate); + s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); + */ + + mWindow->pushGui(s); + +} + +void GuiMenu::openConfigInput() +{ + Window* window = mWindow; + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + /* + window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), + [window] { + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + }, _T("NO"), nullptr) + );*/ + +} + +void GuiMenu::openQuitMenu() +{ +#if defined(_WIN32) + Scripting::fireEvent("quit"); + quitES(""); + return; +#endif + + auto s = new GuiSettings(mWindow, _T("QUIT")); + + Window* window = mWindow; + + ComponentListRow row; + if (UIModeController::getInstance()->isUIModeFull()) + { + row.makeAcceptInputHandler([window] { + window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + [] { + Scripting::fireEvent("quit"); + if(quitES("/tmp/es-restart") != 0) + LOG(LogWarning) << "Restart terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", 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", + [] { + Scripting::fireEvent("quit"); + quitES(""); + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + s->addRow(row); + } + } + row.elements.clear(); + row.makeAcceptInputHandler([window] { + window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + [] { + Scripting::fireEvent("quit", "reboot"); + Scripting::fireEvent("reboot"); + if (quitES("/tmp/es-sysrestart") != 0) + LOG(LogWarning) << "Restart terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(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", + [] { + Scripting::fireEvent("quit", "shutdown"); + Scripting::fireEvent("shutdown"); + if (quitES("/tmp/es-shutdown") != 0) + LOG(LogWarning) << "Shutdown terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + s->addRow(row); + + mWindow->pushGui(s); +} + +void GuiMenu::addVersionInfo() +{ + std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); + + mVersion.setFont(Font::get(FONT_SIZE_SMALL)); + mVersion.setColor(0x5E5E5EFF); + mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); + mVersion.setHorizontalAlignment(ALIGN_CENTER); + addChild(&mVersion); +} + +void GuiMenu::openScreensaverOptions() { + mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); +} + +void GuiMenu::openCollectionSystemSettings() { + mWindow->pushGui(new GuiCollectionSystemsOptions(mWindow)); +} + +void GuiMenu::onSizeChanged() +{ + mVersion.setSize(mSize.x(), 0); + mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); +} + +void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) +{ + std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); + + // populate the list + ComponentListRow row; + row.addElement(std::make_shared(mWindow, name, font, color), true); + + if(add_arrow) + { + std::shared_ptr 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; +} + +HelpStyle GuiMenu::getHelpStyle() +{ + HelpStyle style = HelpStyle(); + style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); + return style; +} + +std::vector GuiMenu::getHelpPrompts() +{ + std::vector prompts; + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("SELECT"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + return prompts; +} diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 5f0df4f525..31835bfa1b 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -1,382 +1,382 @@ -#include "guis/GuiMetaDataEd.h" - -#include "components/ButtonComponent.h" -#include "components/ComponentList.h" -#include "components/DateTimeEditComponent.h" -#include "components/MenuComponent.h" -#include "components/RatingComponent.h" -#include "components/SwitchComponent.h" -#include "components/TextComponent.h" -#include "guis/GuiGameScraper.h" -#include "guis/GuiMsgBox.h" -#include "guis/GuiTextEditPopup.h" -#include "resources/Font.h" -#include "utils/StringUtil.h" -#include "views/ViewController.h" -#include "CollectionSystemManager.h" -#include "FileData.h" -#include "FileFilterIndex.h" -#include "SystemData.h" -#include "Window.h" - -GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, - const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : 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(mWindow, Vector2i(1, 5)); - - mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); - mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), - 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(mWindow); - mGrid.setEntry(mList, Vector2i(0, 1), true, true); - - SystemData* system = file->getSystem(); - - auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); - auto core_choice = std::make_shared>(mWindow, _T("CORE"), false); - - // populate list - for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) - { - std::shared_ptr ed; - - // don't add statistics - if(iter->isStatistic) - continue; - - if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || - iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || - iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") - continue; - - // create ed and add it (and any related components) to mMenu - // ed's value will be set below - ComponentListRow row; - - if (iter->displayName == "emulator") - { - // if (file->getSystemEnvData()->mEmulators.size() <= 1) - // continue; - - std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); - - std::string currentEmul = file->getEmulator(); - - if (defaultEmul.length() == 0) - emul_choice->add(_T("DEFAULT"), "", false); - else - emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); - - for (auto core : file->getSystemEnvData()->mEmulators) - emul_choice->add(core.mName, core.mName, core.mName == currentEmul); - - row.addElement(std::make_shared(mWindow, _T("EMULATOR"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(emul_choice, false); - - mList->addRow(row); - emul_choice->setTag(iter->key); - mEditors.push_back(emul_choice); - - emul_choice->setSelectedChangedCallback([this, system, core_choice, file](std::string emulatorName) - { - std::string currentCore = file->getCore(); - - std::string defaultCore = system->getSystemEnvData()->getDefaultCore(emulatorName); - if (emulatorName.length() == 0) - defaultCore = system->getSystemEnvData()->getDefaultCore(system->getSystemEnvData()->getDefaultEmulator()); - - core_choice->clear(); - if (defaultCore.length() == 0) - core_choice->add(_T("DEFAULT"), "", false); - else - core_choice->add(_T("DEFAULT")+" ("+ defaultCore+")", "", false); - - std::vector cores = system->getSystemEnvData()->getCores(emulatorName); - - bool found = false; - - for (auto it = cores.begin(); it != cores.end(); it++) - { - std::string core = *it; - core_choice->add(core, core, currentCore == core); - if (currentCore == core) - found = true; - } - - if (!found) - core_choice->selectFirstItem(); - else - core_choice->invalidate(); - }); - - continue; - } - - if (iter->displayName == "core") - { - // core_choice->add(_T("DEFAULT"), "", true); - core_choice->setTag(iter->key); - - row.addElement(std::make_shared(mWindow, "CORE", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(core_choice, false); - - mList->addRow(row); - ed = core_choice; - - mEditors.push_back(core_choice); - - // force change event to load core list - emul_choice->invalidate(); - continue; - } - - auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); - row.addElement(lbl, true); // label - - switch (iter->type) - { - case MD_BOOL: - { - ed = std::make_shared(window); - row.addElement(ed, false, true); - break; - } - case MD_RATING: - { - ed = std::make_shared(window); - const float height = lbl->getSize().y() * 0.71f; - ed->setSize(0, height); - row.addElement(ed, false, true); - - auto spacer = std::make_shared(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(window); - row.addElement(ed, false); - - auto spacer = std::make_shared(mWindow); - spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0); - row.addElement(spacer, false); - - // pass input to the actual DateTimeEditComponent 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(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); - row.addElement(ed, false); - break; - } - case MD_MULTILINE_STRING: - default: - { - // MD_STRING - ed = std::make_shared(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); - row.addElement(ed, true); - - auto spacer = std::make_shared(mWindow); - spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); - row.addElement(spacer, false); - - auto bracket = std::make_shared(mWindow); - bracket->setImage(":/arrow.svg"); - bracket->setResize(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->setTag(iter->key); - ed->setValue(mMetaData->get(iter->key)); - - mEditors.push_back(ed); - } - - std::vector< std::shared_ptr > buttons; - - if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) - buttons.push_back(std::make_shared(mWindow, _T("SCRAPE"), _T("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); - - buttons.push_back(std::make_shared(mWindow, _T("SAVE"), _T("SAVE"), [&] { save(); delete this; })); - buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); - - if(mDeleteFunc) - { - auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; - auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; - buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE"), deleteBtnFunc)); - } - - mButtons = makeButtonGrid(mWindow, buttons); - mGrid.setEntry(mButtons, Vector2i(0, 2), true, false); - - // resize + center - float width = (float)Math::min(Renderer::getScreenHeight(), (int)(Renderer::getScreenWidth() * 0.90f)); - setSize(width, 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() -{ - // remove game from index - mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); - - for (unsigned int i = 0; i < mEditors.size(); i++) - { - std::shared_ptr ed = mEditors.at(i); - - auto val = ed->getValue(); - auto key = ed->getTag(); - - if (key == "core" || key == "emulator") - { - std::shared_ptr> list = std::static_pointer_cast>(ed); - val = list->getSelected(); - } - - mMetaData->set(key, val); - } - - // enter game in index - mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); - - if (mSavedCallback) - mSavedCallback(); - - // update respective Collection Entries - CollectionSystemManager::get()->refreshCollectionSystems(mScraperParams.game); -} - -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++) - { - auto val = mEditors.at(i)->getValue(); - auto key = mEditors.at(i)->getTag(); - 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++) - { - auto key = mEditors.at(i)->getTag(); - if(mMetaData->get(key) != mEditors.at(i)->getValue()) - { - dirty = true; - break; - } - } - - std::function 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, - "ENREGISTRER LES MODIFICATIONS ?", - "OUI", [this, closeFunc] { save(); closeFunc(); }, - "NON", 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 GuiMetaDataEd::getHelpPrompts() -{ - std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); - return prompts; -} +#include "guis/GuiMetaDataEd.h" + +#include "components/ButtonComponent.h" +#include "components/ComponentList.h" +#include "components/DateTimeEditComponent.h" +#include "components/MenuComponent.h" +#include "components/RatingComponent.h" +#include "components/SwitchComponent.h" +#include "components/TextComponent.h" +#include "guis/GuiGameScraper.h" +#include "guis/GuiMsgBox.h" +#include "guis/GuiTextEditPopup.h" +#include "resources/Font.h" +#include "utils/StringUtil.h" +#include "views/ViewController.h" +#include "CollectionSystemManager.h" +#include "FileData.h" +#include "FileFilterIndex.h" +#include "SystemData.h" +#include "Window.h" + +GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, + const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : 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(mWindow, Vector2i(1, 5)); + + mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); + mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), + 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(mWindow); + mGrid.setEntry(mList, Vector2i(0, 1), true, true); + + SystemData* system = file->getSystem(); + + auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); + auto core_choice = std::make_shared>(mWindow, _T("CORE"), false); + + // populate list + for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + { + std::shared_ptr ed; + + // don't add statistics + if(iter->isStatistic) + continue; + + if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || + iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || + iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") + continue; + + // create ed and add it (and any related components) to mMenu + // ed's value will be set below + ComponentListRow row; + + if (iter->displayName == "emulator") + { + // if (file->getSystemEnvData()->mEmulators.size() <= 1) + // continue; + + std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); + + std::string currentEmul = file->getEmulator(); + + if (defaultEmul.length() == 0) + emul_choice->add(_T("DEFAULT"), "", false); + else + emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); + + for (auto core : file->getSystemEnvData()->mEmulators) + emul_choice->add(core.mName, core.mName, core.mName == currentEmul); + + row.addElement(std::make_shared(mWindow, _T("EMULATOR"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(emul_choice, false); + + mList->addRow(row); + emul_choice->setTag(iter->key); + mEditors.push_back(emul_choice); + + emul_choice->setSelectedChangedCallback([this, system, core_choice, file](std::string emulatorName) + { + std::string currentCore = file->getCore(); + + std::string defaultCore = system->getSystemEnvData()->getDefaultCore(emulatorName); + if (emulatorName.length() == 0) + defaultCore = system->getSystemEnvData()->getDefaultCore(system->getSystemEnvData()->getDefaultEmulator()); + + core_choice->clear(); + if (defaultCore.length() == 0) + core_choice->add(_T("DEFAULT"), "", false); + else + core_choice->add(_T("DEFAULT")+" ("+ defaultCore+")", "", false); + + std::vector cores = system->getSystemEnvData()->getCores(emulatorName); + + bool found = false; + + for (auto it = cores.begin(); it != cores.end(); it++) + { + std::string core = *it; + core_choice->add(core, core, currentCore == core); + if (currentCore == core) + found = true; + } + + if (!found) + core_choice->selectFirstItem(); + else + core_choice->invalidate(); + }); + + continue; + } + + if (iter->displayName == "core") + { + // core_choice->add(_T("DEFAULT"), "", true); + core_choice->setTag(iter->key); + + row.addElement(std::make_shared(mWindow, "CORE", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(core_choice, false); + + mList->addRow(row); + ed = core_choice; + + mEditors.push_back(core_choice); + + // force change event to load core list + emul_choice->invalidate(); + continue; + } + + auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + row.addElement(lbl, true); // label + + switch (iter->type) + { + case MD_BOOL: + { + ed = std::make_shared(window); + row.addElement(ed, false, true); + break; + } + case MD_RATING: + { + ed = std::make_shared(window); + const float height = lbl->getSize().y() * 0.71f; + ed->setSize(0, height); + row.addElement(ed, false, true); + + auto spacer = std::make_shared(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(window); + row.addElement(ed, false); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0); + row.addElement(spacer, false); + + // pass input to the actual DateTimeEditComponent 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(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); + row.addElement(ed, false); + break; + } + case MD_MULTILINE_STRING: + default: + { + // MD_STRING + ed = std::make_shared(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); + row.addElement(ed, true); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); + row.addElement(spacer, false); + + auto bracket = std::make_shared(mWindow); + bracket->setImage(":/arrow.svg"); + bracket->setResize(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->setTag(iter->key); + ed->setValue(mMetaData->get(iter->key)); + + mEditors.push_back(ed); + } + + std::vector< std::shared_ptr > buttons; + + if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) + buttons.push_back(std::make_shared(mWindow, _T("SCRAPE"), _T("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); + + buttons.push_back(std::make_shared(mWindow, _T("SAVE"), _T("SAVE"), [&] { save(); delete this; })); + buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); + + if(mDeleteFunc) + { + auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; + auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; + buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE"), deleteBtnFunc)); + } + + mButtons = makeButtonGrid(mWindow, buttons); + mGrid.setEntry(mButtons, Vector2i(0, 2), true, false); + + // resize + center + float width = (float)Math::min(Renderer::getScreenHeight(), (int)(Renderer::getScreenWidth() * 0.90f)); + setSize(width, 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() +{ + // remove game from index + mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); + + for (unsigned int i = 0; i < mEditors.size(); i++) + { + std::shared_ptr ed = mEditors.at(i); + + auto val = ed->getValue(); + auto key = ed->getTag(); + + if (key == "core" || key == "emulator") + { + std::shared_ptr> list = std::static_pointer_cast>(ed); + val = list->getSelected(); + } + + mMetaData->set(key, val); + } + + // enter game in index + mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); + + if (mSavedCallback) + mSavedCallback(); + + // update respective Collection Entries + CollectionSystemManager::get()->refreshCollectionSystems(mScraperParams.game); +} + +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++) + { + auto val = mEditors.at(i)->getValue(); + auto key = mEditors.at(i)->getTag(); + 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++) + { + auto key = mEditors.at(i)->getTag(); + if(mMetaData->get(key) != mEditors.at(i)->getValue()) + { + dirty = true; + break; + } + } + + std::function 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, + "ENREGISTRER LES MODIFICATIONS ?", + "OUI", [this, closeFunc] { save(); closeFunc(); }, + "NON", 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 GuiMetaDataEd::getHelpPrompts() +{ + std::vector prompts = mGrid.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + return prompts; +} diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index 7cd24b6f15..d05f77c3d4 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -1,588 +1,588 @@ -#include "GuiComponent.h" - -#include "animations/Animation.h" -#include "animations/AnimationController.h" -#include "Log.h" -#include "Renderer.h" -#include "ThemeData.h" -#include "Window.h" -#include - - -#if defined(_WIN32) -#define _conv(x) GuiTextTool::convertFromWideString(L ## x) -#else -#define _conv(x) x -#endif - -#include -#include -#include "resources\ResourceManager.h" - -std::vector GuiTextTool::mItems; -std::string GuiTextTool::mCurrentLanguage = "en"; -bool GuiTextTool::mCurrentLanguageLoaded = false; - -void GuiTextTool::setLanguage(std::string lang) -{ - mCurrentLanguage = lang; - mCurrentLanguageLoaded = false; -} - -void GuiTextTool::ensureLocalisation() -{ - if (mCurrentLanguageLoaded) - { - if (Settings::getInstance()->getString("Language") == mCurrentLanguage) - return; - - mCurrentLanguage = Settings::getInstance()->getString("Language"); - } - - mCurrentLanguageLoaded = true; - - for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) - delete (*it); - - mItems.clear(); - - std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/"+ mCurrentLanguage +"/emulationstation2.po"); +#include "GuiComponent.h" + +#include "animations/Animation.h" +#include "animations/AnimationController.h" +#include "Log.h" +#include "Renderer.h" +#include "ThemeData.h" +#include "Window.h" +#include + + +#if defined(_WIN32) +#define _conv(x) GuiTextTool::convertFromWideString(L ## x) +#else +#define _conv(x) x +#endif + +#include +#include +#include "resources\ResourceManager.h" + +std::vector GuiTextTool::mItems; +std::string GuiTextTool::mCurrentLanguage = "en"; +bool GuiTextTool::mCurrentLanguageLoaded = false; + +void GuiTextTool::setLanguage(std::string lang) +{ + mCurrentLanguage = lang; + mCurrentLanguageLoaded = false; +} + +void GuiTextTool::ensureLocalisation() +{ + if (mCurrentLanguageLoaded) + { + if (Settings::getInstance()->getString("Language") == mCurrentLanguage) + return; + + mCurrentLanguage = Settings::getInstance()->getString("Language"); + } + + mCurrentLanguageLoaded = true; + + for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) + delete (*it); + + mItems.clear(); + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/"+ mCurrentLanguage +"/emulationstation2.po"); if (Utils::FileSystem::exists(xmlpath)) - { - LocalizationItem* currentItem = NULL; - - std::ifstream file(xmlpath); - std::string str; - while (std::getline(file, str)) - { - if (str.length() > 0 && str[0] == '#') - { - if (currentItem != NULL && currentItem->msgid.length() > 0 && currentItem->msgstr.length() > 0) - mItems.push_back(currentItem); - - currentItem = new LocalizationItem(); - } - - if (currentItem != NULL && str.find("msgid") == 0) - { - auto start = str.find("\""); - if (start != std::string::npos) - { - auto end = str.find("\"", start + 1); - if (end != std::string::npos) - currentItem->msgid = str.substr(start + 1, end - start - 1); - } - } - - if (currentItem != NULL && str.find("msgstr") == 0) - { - auto start = str.find("\""); - if (start != std::string::npos) - { - auto end = str.find("\"", start + 1); - if (end != std::string::npos) - currentItem->msgstr = str.substr(start + 1, end - start - 1); - } - } - } - - if (currentItem != NULL) - delete currentItem; - } -} - - -const std::string GuiTextTool::localize(const std::string text) -{ - ensureLocalisation(); - + { + LocalizationItem* currentItem = NULL; + + std::ifstream file(xmlpath); + std::string str; + while (std::getline(file, str)) + { + if (str.length() > 0 && str[0] == '#') + { + if (currentItem != NULL && currentItem->msgid.length() > 0 && currentItem->msgstr.length() > 0) + mItems.push_back(currentItem); + + currentItem = new LocalizationItem(); + } + + if (currentItem != NULL && str.find("msgid") == 0) + { + auto start = str.find("\""); + if (start != std::string::npos) + { + auto end = str.find("\"", start + 1); + if (end != std::string::npos) + currentItem->msgid = str.substr(start + 1, end - start - 1); + } + } + + if (currentItem != NULL && str.find("msgstr") == 0) + { + auto start = str.find("\""); + if (start != std::string::npos) + { + auto end = str.find("\"", start + 1); + if (end != std::string::npos) + currentItem->msgstr = str.substr(start + 1, end - start - 1); + } + } + } + + if (currentItem != NULL) + delete currentItem; + } +} + + +const std::string GuiTextTool::localize(const std::string text) +{ + ensureLocalisation(); + for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) { if (text == (*it)->msgid) return (*it)->msgstr; - } - - return text; -} - -GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255), - mPosition(Vector3f::Zero()), mOrigin(Vector2f::Zero()), mRotationOrigin(0.5, 0.5), - mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false) -{ - for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) - mAnimationMap[i] = NULL; -} - -GuiComponent::~GuiComponent() -{ - mWindow->removeGui(this); - - cancelAllAnimations(); - - if(mParent) - mParent->removeChild(this); - - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->setParent(NULL); -} - -bool GuiComponent::input(InputConfig* config, Input input) -{ - for(unsigned int i = 0; i < getChildCount(); i++) - { - if(getChild(i)->input(config, input)) - return true; - } - - return false; -} - -void GuiComponent::updateSelf(int deltaTime) -{ - for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) - advanceAnimation(i, deltaTime); -} - -void GuiComponent::updateChildren(int deltaTime) -{ - for(unsigned int i = 0; i < getChildCount(); i++) - { - getChild(i)->update(deltaTime); - } -} - -void GuiComponent::update(int deltaTime) -{ - updateSelf(deltaTime); - updateChildren(deltaTime); -} - -void GuiComponent::render(const Transform4x4f& parentTrans) -{ - Transform4x4f trans = parentTrans * getTransform(); - renderChildren(trans); -} - -void GuiComponent::renderChildren(const Transform4x4f& transform) const -{ - for(unsigned int i = 0; i < getChildCount(); i++) - { - getChild(i)->render(transform); - } -} - -Vector3f GuiComponent::getPosition() const -{ - return mPosition; -} - -void GuiComponent::setPosition(float x, float y, float z) -{ - mPosition = Vector3f(x, y, z); - onPositionChanged(); -} - -Vector2f GuiComponent::getOrigin() const -{ - return mOrigin; -} - -void GuiComponent::setOrigin(float x, float y) -{ - mOrigin = Vector2f(x, y); - onOriginChanged(); -} - -Vector2f GuiComponent::getRotationOrigin() const -{ - return mRotationOrigin; -} - -void GuiComponent::setRotationOrigin(float x, float y) -{ - mRotationOrigin = Vector2f(x, y); -} - -Vector2f GuiComponent::getSize() const -{ - return mSize; -} - -void GuiComponent::setSize(float w, float h) -{ - mSize = Vector2f(w, h); - onSizeChanged(); -} - -float GuiComponent::getRotation() const -{ - return mRotation; -} - -void GuiComponent::setRotation(float rotation) -{ - mRotation = rotation; -} - -float GuiComponent::getScale() const -{ - return mScale; -} - -void GuiComponent::setScale(float scale) -{ - mScale = scale; -} - -float GuiComponent::getZIndex() const -{ - return mZIndex; -} - -void GuiComponent::setZIndex(float z) -{ - mZIndex = z; -} - -float GuiComponent::getDefaultZIndex() const -{ - return mDefaultZIndex; -} - -void GuiComponent::setDefaultZIndex(float z) -{ - mDefaultZIndex = z; -} - -Vector2f GuiComponent::getCenter() const -{ - return Vector2f(mPosition.x() - (getSize().x() * mOrigin.x()) + getSize().x() / 2, - mPosition.y() - (getSize().y() * mOrigin.y()) + getSize().y() / 2); -} - -//Children stuff. -void GuiComponent::addChild(GuiComponent* cmp) -{ - mChildren.push_back(cmp); - - if(cmp->getParent()) - cmp->getParent()->removeChild(cmp); - - cmp->setParent(this); -} - -void GuiComponent::removeChild(GuiComponent* cmp) -{ - if(!cmp->getParent()) - return; - - if(cmp->getParent() != this) - { - LOG(LogError) << "Tried to remove child from incorrect parent!"; - } - - cmp->setParent(NULL); - - for(auto i = mChildren.cbegin(); i != mChildren.cend(); i++) - { - if(*i == cmp) - { - mChildren.erase(i); - return; - } - } -} - -void GuiComponent::clearChildren() -{ - mChildren.clear(); -} - -void GuiComponent::sortChildren() -{ - std::stable_sort(mChildren.begin(), mChildren.end(), [](GuiComponent* a, GuiComponent* b) { - return b->getZIndex() > a->getZIndex(); - }); -} - -unsigned int GuiComponent::getChildCount() const -{ - return (int)mChildren.size(); -} - -GuiComponent* GuiComponent::getChild(unsigned int i) const -{ - return mChildren.at(i); -} - -void GuiComponent::setParent(GuiComponent* parent) -{ - mParent = parent; -} - -GuiComponent* GuiComponent::getParent() const -{ - return mParent; -} - -unsigned char GuiComponent::getOpacity() const -{ - return mOpacity; -} - -void GuiComponent::setOpacity(unsigned char opacity) -{ - mOpacity = opacity; - for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - (*it)->setOpacity(opacity); - } -} - -const Transform4x4f& GuiComponent::getTransform() -{ - mTransform = Transform4x4f::Identity(); - mTransform.translate(mPosition); - if (mScale != 1.0) - { - mTransform.scale(mScale); - } - if (mRotation != 0.0) - { - // Calculate offset as difference between origin and rotation origin - Vector2f rotationSize = getRotationSize(); - float xOff = (mOrigin.x() - mRotationOrigin.x()) * rotationSize.x(); - float yOff = (mOrigin.y() - mRotationOrigin.y()) * rotationSize.y(); - - // transform to offset point - if (xOff != 0.0 || yOff != 0.0) - mTransform.translate(Vector3f(xOff * -1, yOff * -1, 0.0f)); - - // apply rotation transform - mTransform.rotateZ(mRotation); - - // Tranform back to original point - if (xOff != 0.0 || yOff != 0.0) - mTransform.translate(Vector3f(xOff, yOff, 0.0f)); - } - mTransform.translate(Vector3f(mOrigin.x() * mSize.x() * -1, mOrigin.y() * mSize.y() * -1, 0.0f)); - return mTransform; -} - -void GuiComponent::setValue(const std::string& /*value*/) -{ -} - -std::string GuiComponent::getValue() const -{ - return ""; -} - -void GuiComponent::setTag(const std::string& value) -{ - mTag = value; -} - -std::string GuiComponent::getTag() const -{ - return mTag; -} - -void GuiComponent::textInput(const char* text) -{ - for(auto iter = mChildren.cbegin(); iter != mChildren.cend(); iter++) - { - (*iter)->textInput(text); - } -} - -void GuiComponent::setAnimation(Animation* anim, int delay, std::function finishedCallback, bool reverse, unsigned char slot) -{ - assert(slot < MAX_ANIMATIONS); - - AnimationController* oldAnim = mAnimationMap[slot]; - mAnimationMap[slot] = new AnimationController(anim, delay, finishedCallback, reverse); - - if(oldAnim) - delete oldAnim; -} - -bool GuiComponent::stopAnimation(unsigned char slot) -{ - assert(slot < MAX_ANIMATIONS); - if(mAnimationMap[slot]) - { - delete mAnimationMap[slot]; - mAnimationMap[slot] = NULL; - return true; - }else{ - return false; - } -} - -bool GuiComponent::cancelAnimation(unsigned char slot) -{ - assert(slot < MAX_ANIMATIONS); - if(mAnimationMap[slot]) - { - mAnimationMap[slot]->removeFinishedCallback(); - delete mAnimationMap[slot]; - mAnimationMap[slot] = NULL; - return true; - }else{ - return false; - } -} - -bool GuiComponent::finishAnimation(unsigned char slot) -{ - assert(slot < MAX_ANIMATIONS); - if(mAnimationMap[slot]) - { - // skip to animation's end - const bool done = mAnimationMap[slot]->update(mAnimationMap[slot]->getAnimation()->getDuration() - mAnimationMap[slot]->getTime()); - assert(done); - - delete mAnimationMap[slot]; // will also call finishedCallback - mAnimationMap[slot] = NULL; - return true; - }else{ - return false; - } -} - -bool GuiComponent::advanceAnimation(unsigned char slot, unsigned int time) -{ - assert(slot < MAX_ANIMATIONS); - AnimationController* anim = mAnimationMap[slot]; - if(anim) - { - bool done = anim->update(time); - if(done) - { - mAnimationMap[slot] = NULL; - delete anim; - } - return true; - }else{ - return false; - } -} - -void GuiComponent::stopAllAnimations() -{ - for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) - stopAnimation(i); -} - -void GuiComponent::cancelAllAnimations() -{ - for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) - cancelAnimation(i); -} - -bool GuiComponent::isAnimationPlaying(unsigned char slot) const -{ - return mAnimationMap[slot] != NULL; -} - -bool GuiComponent::isAnimationReversed(unsigned char slot) const -{ - assert(mAnimationMap[slot] != NULL); - return mAnimationMap[slot]->isReversed(); -} - -int GuiComponent::getAnimationTime(unsigned char slot) const -{ - assert(mAnimationMap[slot] != NULL); - return mAnimationMap[slot]->getTime(); -} - -void GuiComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) -{ - Vector2f scale = getParent() ? getParent()->getSize() : Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - - const ThemeData::ThemeElement* elem = theme->getElement(view, element, ""); - if(!elem) - return; - - using namespace ThemeFlags; - if(properties & POSITION && elem->has("pos")) - { - Vector2f denormalized = elem->get("pos") * scale; - setPosition(Vector3f(denormalized.x(), denormalized.y(), 0)); - } - - if(properties & ThemeFlags::SIZE && elem->has("size")) - setSize(elem->get("size") * scale); - - // position + size also implies origin - if((properties & ORIGIN || (properties & POSITION && properties & ThemeFlags::SIZE)) && elem->has("origin")) - setOrigin(elem->get("origin")); - - if(properties & ThemeFlags::ROTATION) { - if(elem->has("rotation")) - setRotationDegrees(elem->get("rotation")); - if(elem->has("rotationOrigin")) - setRotationOrigin(elem->get("rotationOrigin")); - } - - if(properties & ThemeFlags::Z_INDEX && elem->has("zIndex")) - setZIndex(elem->get("zIndex")); - else - setZIndex(getDefaultZIndex()); -} - -void GuiComponent::updateHelpPrompts() -{ - if(getParent()) - { - getParent()->updateHelpPrompts(); - return; - } - - std::vector prompts = getHelpPrompts(); - - if(mWindow->peekGui() == this) - mWindow->setHelpPrompts(prompts, getHelpStyle()); -} - -HelpStyle GuiComponent::getHelpStyle() -{ - return HelpStyle(); -} - -bool GuiComponent::isProcessing() const -{ - return mIsProcessing; -} - -void GuiComponent::onShow() -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->onShow(); -} - -void GuiComponent::onHide() -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->onHide(); -} - -void GuiComponent::onScreenSaverActivate() -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->onScreenSaverActivate(); -} - -void GuiComponent::onScreenSaverDeactivate() -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->onScreenSaverDeactivate(); -} - -void GuiComponent::topWindow(bool isTop) -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->topWindow(isTop); + } + + return text; +} + +GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255), + mPosition(Vector3f::Zero()), mOrigin(Vector2f::Zero()), mRotationOrigin(0.5, 0.5), + mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false) +{ + for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) + mAnimationMap[i] = NULL; +} + +GuiComponent::~GuiComponent() +{ + mWindow->removeGui(this); + + cancelAllAnimations(); + + if(mParent) + mParent->removeChild(this); + + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->setParent(NULL); +} + +bool GuiComponent::input(InputConfig* config, Input input) +{ + for(unsigned int i = 0; i < getChildCount(); i++) + { + if(getChild(i)->input(config, input)) + return true; + } + + return false; +} + +void GuiComponent::updateSelf(int deltaTime) +{ + for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) + advanceAnimation(i, deltaTime); +} + +void GuiComponent::updateChildren(int deltaTime) +{ + for(unsigned int i = 0; i < getChildCount(); i++) + { + getChild(i)->update(deltaTime); + } +} + +void GuiComponent::update(int deltaTime) +{ + updateSelf(deltaTime); + updateChildren(deltaTime); +} + +void GuiComponent::render(const Transform4x4f& parentTrans) +{ + Transform4x4f trans = parentTrans * getTransform(); + renderChildren(trans); +} + +void GuiComponent::renderChildren(const Transform4x4f& transform) const +{ + for(unsigned int i = 0; i < getChildCount(); i++) + { + getChild(i)->render(transform); + } +} + +Vector3f GuiComponent::getPosition() const +{ + return mPosition; +} + +void GuiComponent::setPosition(float x, float y, float z) +{ + mPosition = Vector3f(x, y, z); + onPositionChanged(); +} + +Vector2f GuiComponent::getOrigin() const +{ + return mOrigin; +} + +void GuiComponent::setOrigin(float x, float y) +{ + mOrigin = Vector2f(x, y); + onOriginChanged(); +} + +Vector2f GuiComponent::getRotationOrigin() const +{ + return mRotationOrigin; +} + +void GuiComponent::setRotationOrigin(float x, float y) +{ + mRotationOrigin = Vector2f(x, y); +} + +Vector2f GuiComponent::getSize() const +{ + return mSize; +} + +void GuiComponent::setSize(float w, float h) +{ + mSize = Vector2f(w, h); + onSizeChanged(); +} + +float GuiComponent::getRotation() const +{ + return mRotation; +} + +void GuiComponent::setRotation(float rotation) +{ + mRotation = rotation; +} + +float GuiComponent::getScale() const +{ + return mScale; +} + +void GuiComponent::setScale(float scale) +{ + mScale = scale; +} + +float GuiComponent::getZIndex() const +{ + return mZIndex; +} + +void GuiComponent::setZIndex(float z) +{ + mZIndex = z; +} + +float GuiComponent::getDefaultZIndex() const +{ + return mDefaultZIndex; +} + +void GuiComponent::setDefaultZIndex(float z) +{ + mDefaultZIndex = z; +} + +Vector2f GuiComponent::getCenter() const +{ + return Vector2f(mPosition.x() - (getSize().x() * mOrigin.x()) + getSize().x() / 2, + mPosition.y() - (getSize().y() * mOrigin.y()) + getSize().y() / 2); +} + +//Children stuff. +void GuiComponent::addChild(GuiComponent* cmp) +{ + mChildren.push_back(cmp); + + if(cmp->getParent()) + cmp->getParent()->removeChild(cmp); + + cmp->setParent(this); +} + +void GuiComponent::removeChild(GuiComponent* cmp) +{ + if(!cmp->getParent()) + return; + + if(cmp->getParent() != this) + { + LOG(LogError) << "Tried to remove child from incorrect parent!"; + } + + cmp->setParent(NULL); + + for(auto i = mChildren.cbegin(); i != mChildren.cend(); i++) + { + if(*i == cmp) + { + mChildren.erase(i); + return; + } + } +} + +void GuiComponent::clearChildren() +{ + mChildren.clear(); +} + +void GuiComponent::sortChildren() +{ + std::stable_sort(mChildren.begin(), mChildren.end(), [](GuiComponent* a, GuiComponent* b) { + return b->getZIndex() > a->getZIndex(); + }); +} + +unsigned int GuiComponent::getChildCount() const +{ + return (int)mChildren.size(); +} + +GuiComponent* GuiComponent::getChild(unsigned int i) const +{ + return mChildren.at(i); +} + +void GuiComponent::setParent(GuiComponent* parent) +{ + mParent = parent; +} + +GuiComponent* GuiComponent::getParent() const +{ + return mParent; +} + +unsigned char GuiComponent::getOpacity() const +{ + return mOpacity; +} + +void GuiComponent::setOpacity(unsigned char opacity) +{ + mOpacity = opacity; + for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + (*it)->setOpacity(opacity); + } +} + +const Transform4x4f& GuiComponent::getTransform() +{ + mTransform = Transform4x4f::Identity(); + mTransform.translate(mPosition); + if (mScale != 1.0) + { + mTransform.scale(mScale); + } + if (mRotation != 0.0) + { + // Calculate offset as difference between origin and rotation origin + Vector2f rotationSize = getRotationSize(); + float xOff = (mOrigin.x() - mRotationOrigin.x()) * rotationSize.x(); + float yOff = (mOrigin.y() - mRotationOrigin.y()) * rotationSize.y(); + + // transform to offset point + if (xOff != 0.0 || yOff != 0.0) + mTransform.translate(Vector3f(xOff * -1, yOff * -1, 0.0f)); + + // apply rotation transform + mTransform.rotateZ(mRotation); + + // Tranform back to original point + if (xOff != 0.0 || yOff != 0.0) + mTransform.translate(Vector3f(xOff, yOff, 0.0f)); + } + mTransform.translate(Vector3f(mOrigin.x() * mSize.x() * -1, mOrigin.y() * mSize.y() * -1, 0.0f)); + return mTransform; +} + +void GuiComponent::setValue(const std::string& /*value*/) +{ +} + +std::string GuiComponent::getValue() const +{ + return ""; +} + +void GuiComponent::setTag(const std::string& value) +{ + mTag = value; +} + +std::string GuiComponent::getTag() const +{ + return mTag; +} + +void GuiComponent::textInput(const char* text) +{ + for(auto iter = mChildren.cbegin(); iter != mChildren.cend(); iter++) + { + (*iter)->textInput(text); + } +} + +void GuiComponent::setAnimation(Animation* anim, int delay, std::function finishedCallback, bool reverse, unsigned char slot) +{ + assert(slot < MAX_ANIMATIONS); + + AnimationController* oldAnim = mAnimationMap[slot]; + mAnimationMap[slot] = new AnimationController(anim, delay, finishedCallback, reverse); + + if(oldAnim) + delete oldAnim; +} + +bool GuiComponent::stopAnimation(unsigned char slot) +{ + assert(slot < MAX_ANIMATIONS); + if(mAnimationMap[slot]) + { + delete mAnimationMap[slot]; + mAnimationMap[slot] = NULL; + return true; + }else{ + return false; + } +} + +bool GuiComponent::cancelAnimation(unsigned char slot) +{ + assert(slot < MAX_ANIMATIONS); + if(mAnimationMap[slot]) + { + mAnimationMap[slot]->removeFinishedCallback(); + delete mAnimationMap[slot]; + mAnimationMap[slot] = NULL; + return true; + }else{ + return false; + } +} + +bool GuiComponent::finishAnimation(unsigned char slot) +{ + assert(slot < MAX_ANIMATIONS); + if(mAnimationMap[slot]) + { + // skip to animation's end + const bool done = mAnimationMap[slot]->update(mAnimationMap[slot]->getAnimation()->getDuration() - mAnimationMap[slot]->getTime()); + assert(done); + + delete mAnimationMap[slot]; // will also call finishedCallback + mAnimationMap[slot] = NULL; + return true; + }else{ + return false; + } +} + +bool GuiComponent::advanceAnimation(unsigned char slot, unsigned int time) +{ + assert(slot < MAX_ANIMATIONS); + AnimationController* anim = mAnimationMap[slot]; + if(anim) + { + bool done = anim->update(time); + if(done) + { + mAnimationMap[slot] = NULL; + delete anim; + } + return true; + }else{ + return false; + } +} + +void GuiComponent::stopAllAnimations() +{ + for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) + stopAnimation(i); +} + +void GuiComponent::cancelAllAnimations() +{ + for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) + cancelAnimation(i); +} + +bool GuiComponent::isAnimationPlaying(unsigned char slot) const +{ + return mAnimationMap[slot] != NULL; +} + +bool GuiComponent::isAnimationReversed(unsigned char slot) const +{ + assert(mAnimationMap[slot] != NULL); + return mAnimationMap[slot]->isReversed(); +} + +int GuiComponent::getAnimationTime(unsigned char slot) const +{ + assert(mAnimationMap[slot] != NULL); + return mAnimationMap[slot]->getTime(); +} + +void GuiComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) +{ + Vector2f scale = getParent() ? getParent()->getSize() : Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + + const ThemeData::ThemeElement* elem = theme->getElement(view, element, ""); + if(!elem) + return; + + using namespace ThemeFlags; + if(properties & POSITION && elem->has("pos")) + { + Vector2f denormalized = elem->get("pos") * scale; + setPosition(Vector3f(denormalized.x(), denormalized.y(), 0)); + } + + if(properties & ThemeFlags::SIZE && elem->has("size")) + setSize(elem->get("size") * scale); + + // position + size also implies origin + if((properties & ORIGIN || (properties & POSITION && properties & ThemeFlags::SIZE)) && elem->has("origin")) + setOrigin(elem->get("origin")); + + if(properties & ThemeFlags::ROTATION) { + if(elem->has("rotation")) + setRotationDegrees(elem->get("rotation")); + if(elem->has("rotationOrigin")) + setRotationOrigin(elem->get("rotationOrigin")); + } + + if(properties & ThemeFlags::Z_INDEX && elem->has("zIndex")) + setZIndex(elem->get("zIndex")); + else + setZIndex(getDefaultZIndex()); +} + +void GuiComponent::updateHelpPrompts() +{ + if(getParent()) + { + getParent()->updateHelpPrompts(); + return; + } + + std::vector prompts = getHelpPrompts(); + + if(mWindow->peekGui() == this) + mWindow->setHelpPrompts(prompts, getHelpStyle()); +} + +HelpStyle GuiComponent::getHelpStyle() +{ + return HelpStyle(); +} + +bool GuiComponent::isProcessing() const +{ + return mIsProcessing; +} + +void GuiComponent::onShow() +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->onShow(); +} + +void GuiComponent::onHide() +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->onHide(); +} + +void GuiComponent::onScreenSaverActivate() +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->onScreenSaverActivate(); +} + +void GuiComponent::onScreenSaverDeactivate() +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->onScreenSaverDeactivate(); +} + +void GuiComponent::topWindow(bool isTop) +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->topWindow(isTop); } \ No newline at end of file diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 4712ce32f5..d850727610 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -1,222 +1,222 @@ -#pragma once -#ifndef ES_CORE_GUI_COMPONENT_H -#define ES_CORE_GUI_COMPONENT_H - -#include "math/Misc.h" -#include "math/Transform4x4f.h" -#include "HelpPrompt.h" -#include "HelpStyle.h" -#include "InputConfig.h" -#include -#include - -#if defined(_WIN32) -#include -#endif - -class Animation; -class AnimationController; -class Font; -class InputConfig; -class ThemeData; -class Window; - -class GuiComponent -{ -public: - GuiComponent(Window* window); - virtual ~GuiComponent(); - - virtual void textInput(const char* text); - - //Called when input is received. - //Return true if the input is consumed, false if it should continue to be passed to other children. - virtual bool input(InputConfig* config, Input input); - - //Called when time passes. Default implementation calls updateSelf(deltaTime) and updateChildren(deltaTime) - so you should probably call GuiComponent::update(deltaTime) at some point (or at least updateSelf so animations work). - virtual void update(int deltaTime); - - //Called when it's time to render. By default, just calls renderChildren(parentTrans * getTransform()). - //You probably want to override this like so: - //1. Calculate the new transform that your control will draw at with Transform4x4f t = parentTrans * getTransform(). - //2. Set the renderer to use that new transform as the model matrix - Renderer::setMatrix(t); - //3. Draw your component. - //4. Tell your children to render, based on your component's transform - renderChildren(t). - virtual void render(const Transform4x4f& parentTrans); - - Vector3f getPosition() const; - inline void setPosition(const Vector3f& offset) { setPosition(offset.x(), offset.y(), offset.z()); } - void setPosition(float x, float y, float z = 0.0f); - virtual void onPositionChanged() {}; - - //Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) - Vector2f getOrigin() const; - void setOrigin(float originX, float originY); - inline void setOrigin(Vector2f origin) { setOrigin(origin.x(), origin.y()); } - virtual void onOriginChanged() {}; - - //Sets the rotation origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) - Vector2f getRotationOrigin() const; - void setRotationOrigin(float originX, float originY); - inline void setRotationOrigin(Vector2f origin) { setRotationOrigin(origin.x(), origin.y()); } - - virtual Vector2f getSize() const; - inline void setSize(const Vector2f& size) { setSize(size.x(), size.y()); } - void setSize(float w, float h); - virtual void onSizeChanged() {}; - - virtual Vector2f getRotationSize() const { return getSize(); }; - - float getRotation() const; - void setRotation(float rotation); - inline void setRotationDegrees(float rotation) { setRotation((float)ES_DEG_TO_RAD(rotation)); } - - float getScale() const; - void setScale(float scale); - - float getZIndex() const; - void setZIndex(float zIndex); - - float getDefaultZIndex() const; - void setDefaultZIndex(float zIndex); - - // Returns the center point of the image (takes origin into account). - Vector2f getCenter() const; - - void setParent(GuiComponent* parent); - GuiComponent* getParent() const; - - void addChild(GuiComponent* cmp); - void removeChild(GuiComponent* cmp); - void clearChildren(); - void sortChildren(); - unsigned int getChildCount() const; - GuiComponent* getChild(unsigned int i) const; - - // animation will be automatically deleted when it completes or is stopped. - bool isAnimationPlaying(unsigned char slot) const; - bool isAnimationReversed(unsigned char slot) const; - int getAnimationTime(unsigned char slot) const; - void setAnimation(Animation* animation, int delay = 0, std::function finishedCallback = nullptr, bool reverse = false, unsigned char slot = 0); - bool stopAnimation(unsigned char slot); - bool cancelAnimation(unsigned char slot); // Like stopAnimation, but doesn't call finishedCallback - only removes the animation, leaving things in their current state. Returns true if successful (an animation was in this slot). - bool finishAnimation(unsigned char slot); // Calls update(1.f) and finishedCallback, then deletes the animation - basically skips to the end. Returns true if successful (an animation was in this slot). - bool advanceAnimation(unsigned char slot, unsigned int time); // Returns true if successful (an animation was in this slot). - void stopAllAnimations(); - void cancelAllAnimations(); - - virtual unsigned char getOpacity() const; - virtual void setOpacity(unsigned char opacity); - - const Transform4x4f& getTransform(); - - virtual std::string getValue() const; - virtual void setValue(const std::string& value); - - - virtual std::string getTag() const; - virtual void setTag(const std::string& value); - - virtual void onFocusGained() {}; - virtual void onFocusLost() {}; - - virtual void onShow(); - virtual void onHide(); - - virtual void onScreenSaverActivate(); - virtual void onScreenSaverDeactivate(); - virtual void topWindow(bool isTop); - - // Default implementation just handles and tags as normalized float pairs. - // You probably want to keep this behavior for any derived classes as well as add your own. - virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); - - // Returns a list of help prompts. - virtual std::vector getHelpPrompts() { return std::vector(); }; - - // Called whenever help prompts change. - void updateHelpPrompts(); - - virtual HelpStyle getHelpStyle(); - - // Returns true if the component is busy doing background processing (e.g. HTTP downloads) - bool isProcessing() const; - -protected: - void renderChildren(const Transform4x4f& transform) const; - void updateSelf(int deltaTime); // updates animations - void updateChildren(int deltaTime); // updates animations - - unsigned char mOpacity; - Window* mWindow; - - GuiComponent* mParent; - std::vector mChildren; - - std::string mTag; - - Vector3f mPosition; - Vector2f mOrigin; - Vector2f mRotationOrigin; - Vector2f mSize; - - float mRotation = 0.0; - float mScale = 1.0; - - float mDefaultZIndex = 0; - float mZIndex = 0; - - bool mIsProcessing; - -public: - const static unsigned char MAX_ANIMATIONS = 4; - -private: - Transform4x4f mTransform; //Don't access this directly! Use getTransform()! - AnimationController* mAnimationMap[MAX_ANIMATIONS]; -}; - - - -class LocalizationItem -{ -public: - std::string msgid; - std::string msgstr; -}; - -class GuiTextTool -{ -public: -#if defined(_WIN32) - static const std::string convertFromWideString(const std::wstring wstring) - { - int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); - - std::string string; - string.resize(numBytes); - WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); - - return string; - } -#endif - static void ensureLocalisation(); - static const std::string localize(const std::string text); - - static void setLanguage(std::string lang); - -private: - static std::vector mItems; - static std::string mCurrentLanguage; - static bool mCurrentLanguageLoaded; -}; - -#if defined(_WIN32) -#define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) -#define _L(x) GuiTextTool::localize(x) -#else -#define _T(x) GuiTextTool::localize(x) -#define _L(x) GuiTextTool::localize(x) -#endif // _WIN32 - -#endif // ES_CORE_GUI_COMPONENT_H +#pragma once +#ifndef ES_CORE_GUI_COMPONENT_H +#define ES_CORE_GUI_COMPONENT_H + +#include "math/Misc.h" +#include "math/Transform4x4f.h" +#include "HelpPrompt.h" +#include "HelpStyle.h" +#include "InputConfig.h" +#include +#include + +#if defined(_WIN32) +#include +#endif + +class Animation; +class AnimationController; +class Font; +class InputConfig; +class ThemeData; +class Window; + +class GuiComponent +{ +public: + GuiComponent(Window* window); + virtual ~GuiComponent(); + + virtual void textInput(const char* text); + + //Called when input is received. + //Return true if the input is consumed, false if it should continue to be passed to other children. + virtual bool input(InputConfig* config, Input input); + + //Called when time passes. Default implementation calls updateSelf(deltaTime) and updateChildren(deltaTime) - so you should probably call GuiComponent::update(deltaTime) at some point (or at least updateSelf so animations work). + virtual void update(int deltaTime); + + //Called when it's time to render. By default, just calls renderChildren(parentTrans * getTransform()). + //You probably want to override this like so: + //1. Calculate the new transform that your control will draw at with Transform4x4f t = parentTrans * getTransform(). + //2. Set the renderer to use that new transform as the model matrix - Renderer::setMatrix(t); + //3. Draw your component. + //4. Tell your children to render, based on your component's transform - renderChildren(t). + virtual void render(const Transform4x4f& parentTrans); + + Vector3f getPosition() const; + inline void setPosition(const Vector3f& offset) { setPosition(offset.x(), offset.y(), offset.z()); } + void setPosition(float x, float y, float z = 0.0f); + virtual void onPositionChanged() {}; + + //Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) + Vector2f getOrigin() const; + void setOrigin(float originX, float originY); + inline void setOrigin(Vector2f origin) { setOrigin(origin.x(), origin.y()); } + virtual void onOriginChanged() {}; + + //Sets the rotation origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) + Vector2f getRotationOrigin() const; + void setRotationOrigin(float originX, float originY); + inline void setRotationOrigin(Vector2f origin) { setRotationOrigin(origin.x(), origin.y()); } + + virtual Vector2f getSize() const; + inline void setSize(const Vector2f& size) { setSize(size.x(), size.y()); } + void setSize(float w, float h); + virtual void onSizeChanged() {}; + + virtual Vector2f getRotationSize() const { return getSize(); }; + + float getRotation() const; + void setRotation(float rotation); + inline void setRotationDegrees(float rotation) { setRotation((float)ES_DEG_TO_RAD(rotation)); } + + float getScale() const; + void setScale(float scale); + + float getZIndex() const; + void setZIndex(float zIndex); + + float getDefaultZIndex() const; + void setDefaultZIndex(float zIndex); + + // Returns the center point of the image (takes origin into account). + Vector2f getCenter() const; + + void setParent(GuiComponent* parent); + GuiComponent* getParent() const; + + void addChild(GuiComponent* cmp); + void removeChild(GuiComponent* cmp); + void clearChildren(); + void sortChildren(); + unsigned int getChildCount() const; + GuiComponent* getChild(unsigned int i) const; + + // animation will be automatically deleted when it completes or is stopped. + bool isAnimationPlaying(unsigned char slot) const; + bool isAnimationReversed(unsigned char slot) const; + int getAnimationTime(unsigned char slot) const; + void setAnimation(Animation* animation, int delay = 0, std::function finishedCallback = nullptr, bool reverse = false, unsigned char slot = 0); + bool stopAnimation(unsigned char slot); + bool cancelAnimation(unsigned char slot); // Like stopAnimation, but doesn't call finishedCallback - only removes the animation, leaving things in their current state. Returns true if successful (an animation was in this slot). + bool finishAnimation(unsigned char slot); // Calls update(1.f) and finishedCallback, then deletes the animation - basically skips to the end. Returns true if successful (an animation was in this slot). + bool advanceAnimation(unsigned char slot, unsigned int time); // Returns true if successful (an animation was in this slot). + void stopAllAnimations(); + void cancelAllAnimations(); + + virtual unsigned char getOpacity() const; + virtual void setOpacity(unsigned char opacity); + + const Transform4x4f& getTransform(); + + virtual std::string getValue() const; + virtual void setValue(const std::string& value); + + + virtual std::string getTag() const; + virtual void setTag(const std::string& value); + + virtual void onFocusGained() {}; + virtual void onFocusLost() {}; + + virtual void onShow(); + virtual void onHide(); + + virtual void onScreenSaverActivate(); + virtual void onScreenSaverDeactivate(); + virtual void topWindow(bool isTop); + + // Default implementation just handles and tags as normalized float pairs. + // You probably want to keep this behavior for any derived classes as well as add your own. + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); + + // Returns a list of help prompts. + virtual std::vector getHelpPrompts() { return std::vector(); }; + + // Called whenever help prompts change. + void updateHelpPrompts(); + + virtual HelpStyle getHelpStyle(); + + // Returns true if the component is busy doing background processing (e.g. HTTP downloads) + bool isProcessing() const; + +protected: + void renderChildren(const Transform4x4f& transform) const; + void updateSelf(int deltaTime); // updates animations + void updateChildren(int deltaTime); // updates animations + + unsigned char mOpacity; + Window* mWindow; + + GuiComponent* mParent; + std::vector mChildren; + + std::string mTag; + + Vector3f mPosition; + Vector2f mOrigin; + Vector2f mRotationOrigin; + Vector2f mSize; + + float mRotation = 0.0; + float mScale = 1.0; + + float mDefaultZIndex = 0; + float mZIndex = 0; + + bool mIsProcessing; + +public: + const static unsigned char MAX_ANIMATIONS = 4; + +private: + Transform4x4f mTransform; //Don't access this directly! Use getTransform()! + AnimationController* mAnimationMap[MAX_ANIMATIONS]; +}; + + + +class LocalizationItem +{ +public: + std::string msgid; + std::string msgstr; +}; + +class GuiTextTool +{ +public: +#if defined(_WIN32) + static const std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + + std::string string; + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return string; + } +#endif + static void ensureLocalisation(); + static const std::string localize(const std::string text); + + static void setLanguage(std::string lang); + +private: + static std::vector mItems; + static std::string mCurrentLanguage; + static bool mCurrentLanguageLoaded; +}; + +#if defined(_WIN32) +#define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) +#define _L(x) GuiTextTool::localize(x) +#else +#define _T(x) GuiTextTool::localize(x) +#define _L(x) GuiTextTool::localize(x) +#endif // _WIN32 + +#endif // ES_CORE_GUI_COMPONENT_H diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 9ee8ea2fc9..ba4121ce08 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -1,727 +1,730 @@ -#include "ThemeData.h" - -#include "components/ImageComponent.h" -#include "components/TextComponent.h" -#include "utils/FileSystemUtil.h" -#include "Log.h" -#include "platform.h" -#include "Settings.h" -#include -#include - -std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" } }; -std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; - -std::map> ThemeData::sElementMap { - { "image", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "maxSize", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "path", PATH }, - { "default", PATH }, - { "tile", BOOLEAN }, - { "color", COLOR }, - { "zIndex", FLOAT } } }, - { "imagegrid", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "margin", NORMALIZED_PAIR }, - { "gameImage", PATH }, - { "folderImage", PATH }, - { "scrollDirection", STRING } } }, - { "gridtile", { - { "size", NORMALIZED_PAIR }, - { "padding", NORMALIZED_PAIR }, - { "imageColor", COLOR }, - { "backgroundImage", PATH }, - { "backgroundCornerSize", NORMALIZED_PAIR }, - { "backgroundColor", COLOR }, - { "backgroundCenterColor", COLOR }, - { "backgroundEdgeColor", COLOR }, - { "imageSizeMode", STRING } } }, - { "text", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "text", STRING }, - { "backgroundColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "color", COLOR }, - { "alignment", STRING }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "value", STRING }, - { "zIndex", FLOAT } } }, - { "textlist", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "selectorHeight", FLOAT }, - { "selectorOffsetY", FLOAT }, - { "selectorColor", COLOR }, - { "selectorImagePath", PATH }, - { "selectorImageTile", BOOLEAN }, - { "selectedColor", COLOR }, - { "primaryColor", COLOR }, - { "secondaryColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "scrollSound", PATH }, - { "alignment", STRING }, - { "horizontalMargin", FLOAT }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "zIndex", FLOAT } } }, - { "container", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "zIndex", FLOAT } } }, - { "ninepatch", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "path", PATH }, - { "zIndex", FLOAT } } }, - { "datetime", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "backgroundColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "color", COLOR }, - { "alignment", STRING }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "value", STRING }, - { "format", STRING }, - { "displayRelative", BOOLEAN }, - { "zIndex", FLOAT } } }, - { "rating", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "color", COLOR }, - { "filledPath", PATH }, - { "unfilledPath", PATH }, - { "zIndex", FLOAT } } }, - { "sound", { - { "path", PATH } } }, - { "helpsystem", { - { "pos", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "textColor", COLOR }, - { "iconColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT } } }, - { "video", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "maxSize", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "default", PATH }, - { "delay", FLOAT }, - { "zIndex", FLOAT }, - { "showSnapshotNoVideo", BOOLEAN }, - { "showSnapshotDelay", BOOLEAN } } }, - { "carousel", { - { "type", STRING }, - { "size", NORMALIZED_PAIR }, - { "pos", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "color", COLOR }, - { "logoScale", FLOAT }, - { "logoRotation", FLOAT }, - { "logoRotationOrigin", NORMALIZED_PAIR }, - { "logoSize", NORMALIZED_PAIR }, - { "logoAlignment", STRING }, - { "maxLogoCount", FLOAT }, - { "zIndex", FLOAT } } } -}; - -#define MINIMUM_THEME_FORMAT_VERSION 3 -#define CURRENT_THEME_FORMAT_VERSION 6 - -// helper -unsigned int getHexColor(const char* str) -{ - ThemeException error; - if(!str) - throw error << "Empty color"; - - size_t len = strlen(str); - if(len != 6 && len != 8) - throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; - - unsigned int val; - std::stringstream ss; - ss << str; - ss >> std::hex >> val; - - if(len == 6) - val = (val << 8) | 0xFF; - - return val; -} - -std::map mVariables; - -std::string resolvePlaceholders(const char* in) -{ - std::string inStr(in); - - if(inStr.empty()) - return inStr; - - const size_t variableBegin = inStr.find("${"); - const size_t variableEnd = inStr.find("}", variableBegin); - - if((variableBegin == std::string::npos) || (variableEnd == std::string::npos)) - return inStr; - - std::string prefix = inStr.substr(0, variableBegin); - std::string replace = inStr.substr(variableBegin + 2, variableEnd - (variableBegin + 2)); - std::string suffix = resolvePlaceholders(inStr.substr(variableEnd + 1).c_str()); - - return prefix + mVariables[replace] + suffix; -} - -ThemeData::ThemeData() -{ - mVersion = 0; -} - -void ThemeData::loadFile(std::string system, std::map sysDataMap, const std::string& path) -{ - mPaths.push_back(path); - - ThemeException error; - error.setFiles(mPaths); - - if(!Utils::FileSystem::exists(path)) - throw error << "File does not exist!"; - - mVersion = 0; - mViews.clear(); - - mSystemThemeFolder = system; - - mVariables.clear(); - mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); - - pugi::xml_document doc; - pugi::xml_parse_result res = doc.load_file(path.c_str()); - if(!res) - throw error << "XML parsing error: \n " << res.description(); - - pugi::xml_node root = doc.child("theme"); - if(!root) - throw error << "Missing tag!"; - - // parse version - mVersion = root.child("formatVersion").text().as_float(-404); - if(mVersion == -404) - throw error << " tag missing!\n It's either out of date or you need to add " << CURRENT_THEME_FORMAT_VERSION << " inside your tag."; - - if (root.attribute("defaultView")) - mDefaultView = root.attribute("defaultView").as_string(); - - if(mVersion < MINIMUM_THEME_FORMAT_VERSION) - throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; - - parseVariables(root); - parseIncludes(root); - parseViews(root); - parseFeatures(root); -} - -std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) -{ - std::string result = path; - - size_t start_pos = result.find("$system"); - if (start_pos == std::string::npos) - return path; - - result.replace(start_pos, 7, systemThemeFolder); - - //result.replace("$system", systemThemeFolder); - //boost::algorithm::replace_first(result, "$system", systemThemeFolder); - return result; -} - -bool ThemeData::parseSubset(const pugi::xml_node& node) -{ - bool parse = true; - - if (node.attribute("subset")) - { - parse = false; - const std::string subsetAttr = node.attribute("subset").as_string(); - const std::string nameAttr = node.attribute("name").as_string(); - - if (subsetAttr == "iconset" || subsetAttr == "gamelistview") - { - if (nameAttr.rfind("1-") != std::string::npos) - { - parse = true; - return parse; - } - } - else if (subsetAttr == "systemview") - { - if (nameAttr.rfind("8-") != std::string::npos) - { - parse = true; - return parse; - } - } - else if (nameAttr.rfind("2-") != std::string::npos) - { - parse = true; - return parse; - } - /* - if (subsetAttr == "colorset" && nameAttr == mColorset) - { - parse = true; - return parse; - } - if (subsetAttr == "iconset" && nameAttr == mIconset) - { - parse = true; - return parse; - } - if (subsetAttr == "menu" && nameAttr == mMenu) - { - parse = true; - return parse; - } - if (subsetAttr == "systemview" && nameAttr == mSystemview) - { - parse = true; - return parse; - } - if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) - { - parse = true; - return parse; - }*/ - } - - return parse; -} - -void ThemeData::parseIncludes(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) - { - if (!parseSubset(node)) - continue; - - std::string relPath = resolvePlaceholders(node.text().as_string()); - std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); - path = resolveSystemVariable(mSystemThemeFolder, path); - - if (!ResourceManager::getInstance()->fileExists(path)) - continue; - //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; - - error << " from included file \"" << relPath << "\":\n "; - - mPaths.push_back(path); - - pugi::xml_document includeDoc; - pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); - if(!result) - throw error << "Error parsing file: \n " << result.description(); - - pugi::xml_node theme = includeDoc.child("theme"); - if(!theme) - throw error << "Missing tag!"; - - parseVariables(theme); - parseIncludes(theme); - parseViews(theme); - parseFeatures(theme); - - mPaths.pop_back(); - } -} - -void ThemeData::parseFeatures(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) - { - if(!node.attribute("supported")) - throw error << "Feature missing \"supported\" attribute!"; - - const std::string supportedAttr = node.attribute("supported").as_string(); - - if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) - { - parseViews(node); - } - } -} - -void ThemeData::parseVariables(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - pugi::xml_node variables = root.child("variables"); - - if(!variables) - return; - - for(pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) - { - std::string key = it->name(); - std::string val = it->text().as_string(); - - if (!val.empty()) - mVariables.insert(std::pair(key, val)); - } -} - -void ThemeData::parseViews(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - // parse views - for(pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) - { - if(!node.attribute("name")) - throw error << "View missing \"name\" attribute!"; - - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - std::string viewKey; - while(off != std::string::npos || prevOff != std::string::npos) - { - viewKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - - if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) - { - ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; - parseView(node, view); - - if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) - { - ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; - parseView(node, view); - } - } - } - } -} - -void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) - { - if(!node.attribute("name")) - throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; - - auto elemTypeIt = sElementMap.find(node.name()); - if(elemTypeIt == sElementMap.cend()) - throw error << "Unknown element of type \"" << node.name() << "\"!"; - - if (parseRegion(node)) - { - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while (off != std::string::npos || prevOff != std::string::npos) - { - std::string elemKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - - parseElement(node, elemTypeIt->second, - view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); - - if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) - view.orderedKeys.push_back(elemKey); - } - } - } -} - -bool ThemeData::parseRegion(const pugi::xml_node& node) -{ - bool parse = true; - - if (node.attribute("region")) - { - std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); - - parse = false; - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("region").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while (off != std::string::npos || prevOff != std::string::npos) - { - std::string elemKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - if (elemKey == regionsetting) - { - parse = true; - return parse; - } - } - - } - return parse; - -} - - -void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element) -{ - ThemeException error; - error.setFiles(mPaths); - - element.type = root.name(); - element.extra = root.attribute("extra").as_bool(false); - - for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) - { - auto typeIt = typeMap.find(node.name()); - if (typeIt == typeMap.cend()) - continue; - //throw error << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ")."; - - std::string str = resolveSystemVariable(mSystemThemeFolder, resolvePlaceholders(node.text().as_string())); - - switch(typeIt->second) - { - case NORMALIZED_PAIR: - { - size_t divider = str.find(' '); - if(divider == std::string::npos) - throw error << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")"; - - std::string first = str.substr(0, divider); - std::string second = str.substr(divider, std::string::npos); - - Vector2f val((float)atof(first.c_str()), (float)atof(second.c_str())); - - element.properties[node.name()] = val; - break; - } - case STRING: - element.properties[node.name()] = str; - break; - case PATH: - { - std::string path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true); - - if (!ResourceManager::getInstance()->fileExists(path)) - { - std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); - if (ResourceManager::getInstance()->fileExists(rootPath)) - path = rootPath; - } - - if (!ResourceManager::getInstance()->fileExists(path)) - { - std::stringstream ss; - ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda - ss << "could not find file \"" << node.text().get() << "\" "; - if(node.text().get() != path) - ss << "(which resolved to \"" << path << "\") "; - - LOG(LogWarning) << ss.str(); - OutputDebugString(ss.str().c_str()); - } - else - element.properties[node.name()] = path; - - break; - } - case COLOR: - element.properties[node.name()] = getHexColor(str.c_str()); - break; - case FLOAT: - { - float floatVal = static_cast(strtod(str.c_str(), 0)); - element.properties[node.name()] = floatVal; - break; - } - - case BOOLEAN: - { - // only look at first char - char first = str[0]; - // 1*, t* (true), T* (True), y* (yes), Y* (YES) - bool boolVal = (first == '1' || first == 't' || first == 'T' || first == 'y' || first == 'Y'); - - element.properties[node.name()] = boolVal; - break; - } - default: - throw error << "Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name(); - } - } -} - -bool ThemeData::hasView(const std::string& view) -{ - auto viewIt = mViews.find(view); - return (viewIt != mViews.cend()); -} - -const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const -{ - auto viewIt = mViews.find(view); - if(viewIt == mViews.cend()) - return NULL; // not found - - auto elemIt = viewIt->second.elements.find(element); - if(elemIt == viewIt->second.elements.cend()) return NULL; - - if(elemIt->second.type != expectedType && !expectedType.empty()) - { - LOG(LogWarning) << " requested mismatched theme type for [" << view << "." << element << "] - expected \"" - << expectedType << "\", got \"" << elemIt->second.type << "\""; - return NULL; - } - - return &elemIt->second; -} - -const std::shared_ptr& ThemeData::getDefault() -{ - static std::shared_ptr theme = nullptr; - if(theme == nullptr) - { - theme = std::shared_ptr(new ThemeData()); - - const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_theme_default.xml"; - if(Utils::FileSystem::exists(path)) - { - try - { - std::map emptyMap; - theme->loadFile("", emptyMap, path); - } catch(ThemeException& e) - { - LOG(LogError) << e.what(); - theme = std::shared_ptr(new ThemeData()); //reset to empty - } - } - } - - return theme; -} - -std::vector ThemeData::makeExtras(const std::shared_ptr& theme, const std::string& view, Window* window) -{ - std::vector comps; - - auto viewIt = theme->mViews.find(view); - if(viewIt == theme->mViews.cend()) - return comps; - - for(auto it = viewIt->second.orderedKeys.cbegin(); it != viewIt->second.orderedKeys.cend(); it++) - { - ThemeElement& elem = viewIt->second.elements.at(*it); - if(elem.extra) - { - GuiComponent* comp = NULL; - const std::string& t = elem.type; - if(t == "image") - comp = new ImageComponent(window); - else if(t == "text") - comp = new TextComponent(window); - - comp->setDefaultZIndex(10); - comp->applyTheme(theme, view, *it, ThemeFlags::ALL); - comps.push_back(comp); - } - } - - return comps; -} - -std::map ThemeData::getThemeSets() -{ - std::map sets; - - static const size_t pathCount = 2; - std::string paths[pathCount] = - { - "/etc/emulationstation/themes", - Utils::FileSystem::getHomePath() + "/.emulationstation/themes" - }; - - for(size_t i = 0; i < pathCount; i++) - { - if(!Utils::FileSystem::isDirectory(paths[i])) - continue; - - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(paths[i]); - - for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if(Utils::FileSystem::isDirectory(*it)) - { - ThemeSet set = {*it}; - sets[set.getName()] = set; - } - } - } - - return sets; -} - -std::string ThemeData::getThemeFromCurrentSet(const std::string& system) -{ - std::map themeSets = ThemeData::getThemeSets(); - if(themeSets.empty()) - { - // no theme sets available - return ""; - } - - std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(set == themeSets.cend()) - { - // currently selected theme set is missing, so just pick the first available set - set = themeSets.cbegin(); - Settings::getInstance()->setString("ThemeSet", set->first); - } - - return set->second.getThemePath(system); -} +#include "ThemeData.h" + +#include "components/ImageComponent.h" +#include "components/TextComponent.h" +#include "utils/FileSystemUtil.h" +#include "Log.h" +#include "platform.h" +#include "Settings.h" +#include +#include + +std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" } }; +std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; + +std::map> ThemeData::sElementMap { + { "image", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "maxSize", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "path", PATH }, + { "default", PATH }, + { "tile", BOOLEAN }, + { "color", COLOR }, + { "zIndex", FLOAT } } }, + { "imagegrid", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "margin", NORMALIZED_PAIR }, + { "gameImage", PATH }, + { "folderImage", PATH }, + { "scrollDirection", STRING } } }, + { "gridtile", { + { "size", NORMALIZED_PAIR }, + { "padding", NORMALIZED_PAIR }, + { "imageColor", COLOR }, + { "backgroundImage", PATH }, + { "backgroundCornerSize", NORMALIZED_PAIR }, + { "backgroundColor", COLOR }, + { "backgroundCenterColor", COLOR }, + { "backgroundEdgeColor", COLOR }, + { "imageSizeMode", STRING } } }, + { "text", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "text", STRING }, + { "backgroundColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "color", COLOR }, + { "alignment", STRING }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "value", STRING }, + { "zIndex", FLOAT } } }, + { "textlist", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "selectorHeight", FLOAT }, + { "selectorOffsetY", FLOAT }, + { "selectorColor", COLOR }, + { "selectorImagePath", PATH }, + { "selectorImageTile", BOOLEAN }, + { "selectedColor", COLOR }, + { "primaryColor", COLOR }, + { "secondaryColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "scrollSound", PATH }, + { "alignment", STRING }, + { "horizontalMargin", FLOAT }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "zIndex", FLOAT } } }, + { "container", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "zIndex", FLOAT } } }, + { "ninepatch", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "path", PATH }, + { "zIndex", FLOAT } } }, + { "datetime", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "backgroundColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "color", COLOR }, + { "alignment", STRING }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "value", STRING }, + { "format", STRING }, + { "displayRelative", BOOLEAN }, + { "zIndex", FLOAT } } }, + { "rating", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "color", COLOR }, + { "filledPath", PATH }, + { "unfilledPath", PATH }, + { "zIndex", FLOAT } } }, + { "sound", { + { "path", PATH } } }, + { "helpsystem", { + { "pos", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "textColor", COLOR }, + { "iconColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT } } }, + { "video", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "maxSize", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "default", PATH }, + { "delay", FLOAT }, + { "zIndex", FLOAT }, + { "showSnapshotNoVideo", BOOLEAN }, + { "showSnapshotDelay", BOOLEAN } } }, + { "carousel", { + { "type", STRING }, + { "size", NORMALIZED_PAIR }, + { "pos", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "color", COLOR }, + { "logoScale", FLOAT }, + { "logoRotation", FLOAT }, + { "logoRotationOrigin", NORMALIZED_PAIR }, + { "logoSize", NORMALIZED_PAIR }, + { "logoAlignment", STRING }, + { "maxLogoCount", FLOAT }, + { "zIndex", FLOAT } } } +}; + +#define MINIMUM_THEME_FORMAT_VERSION 3 +#define CURRENT_THEME_FORMAT_VERSION 6 + +// helper +unsigned int getHexColor(const char* str) +{ + ThemeException error; + if(!str) + throw error << "Empty color"; + + size_t len = strlen(str); + if(len != 6 && len != 8) + throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; + + unsigned int val; + std::stringstream ss; + ss << str; + ss >> std::hex >> val; + + if(len == 6) + val = (val << 8) | 0xFF; + + return val; +} + +std::map mVariables; + +std::string resolvePlaceholders(const char* in) +{ + std::string inStr(in); + + if(inStr.empty()) + return inStr; + + const size_t variableBegin = inStr.find("${"); + const size_t variableEnd = inStr.find("}", variableBegin); + + if((variableBegin == std::string::npos) || (variableEnd == std::string::npos)) + return inStr; + + std::string prefix = inStr.substr(0, variableBegin); + std::string replace = inStr.substr(variableBegin + 2, variableEnd - (variableBegin + 2)); + std::string suffix = resolvePlaceholders(inStr.substr(variableEnd + 1).c_str()); + + return prefix + mVariables[replace] + suffix; +} + +ThemeData::ThemeData() +{ + mVersion = 0; +} + +void ThemeData::loadFile(std::string system, std::map sysDataMap, const std::string& path) +{ + mPaths.push_back(path); + + ThemeException error; + error.setFiles(mPaths); + + if(!Utils::FileSystem::exists(path)) + throw error << "File does not exist!"; + + mVersion = 0; + mViews.clear(); + + mSystemThemeFolder = system; + + mVariables.clear(); + mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); + + pugi::xml_document doc; + pugi::xml_parse_result res = doc.load_file(path.c_str()); + if(!res) + throw error << "XML parsing error: \n " << res.description(); + + pugi::xml_node root = doc.child("theme"); + if(!root) + throw error << "Missing tag!"; + + // parse version + mVersion = root.child("formatVersion").text().as_float(-404); + if(mVersion == -404) + throw error << " tag missing!\n It's either out of date or you need to add " << CURRENT_THEME_FORMAT_VERSION << " inside your tag."; + + if (root.attribute("defaultView")) + mDefaultView = root.attribute("defaultView").as_string(); + + if(mVersion < MINIMUM_THEME_FORMAT_VERSION) + throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; + + parseVariables(root); + parseIncludes(root); + parseViews(root); + parseFeatures(root); +} + +std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) +{ + std::string result = path; + + size_t start_pos = result.find("$system"); + if (start_pos == std::string::npos) + return path; + + result.replace(start_pos, 7, systemThemeFolder); + + //result.replace("$system", systemThemeFolder); + //boost::algorithm::replace_first(result, "$system", systemThemeFolder); + return result; +} + +bool ThemeData::parseSubset(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("subset")) + { + parse = false; + const std::string subsetAttr = node.attribute("subset").as_string(); + const std::string nameAttr = node.attribute("name").as_string(); + + if (subsetAttr == "iconset" || subsetAttr == "gamelistview") + { + if (nameAttr.rfind("1-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (subsetAttr == "systemview") + { + if (nameAttr.rfind("8-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (nameAttr.rfind("2-") != std::string::npos) + { + parse = true; + return parse; + } + /* + if (subsetAttr == "colorset" && nameAttr == mColorset) + { + parse = true; + return parse; + } + if (subsetAttr == "iconset" && nameAttr == mIconset) + { + parse = true; + return parse; + } + if (subsetAttr == "menu" && nameAttr == mMenu) + { + parse = true; + return parse; + } + if (subsetAttr == "systemview" && nameAttr == mSystemview) + { + parse = true; + return parse; + } + if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) + { + parse = true; + return parse; + }*/ + } + + return parse; +} + +void ThemeData::parseIncludes(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) + { + if (!parseSubset(node)) + continue; + + std::string relPath = resolvePlaceholders(node.text().as_string()); + std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); + path = resolveSystemVariable(mSystemThemeFolder, path); + + if (!ResourceManager::getInstance()->fileExists(path)) + continue; + //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + + error << " from included file \"" << relPath << "\":\n "; + + mPaths.push_back(path); + + pugi::xml_document includeDoc; + pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); + if(!result) + throw error << "Error parsing file: \n " << result.description(); + + pugi::xml_node theme = includeDoc.child("theme"); + if(!theme) + throw error << "Missing tag!"; + + parseVariables(theme); + parseIncludes(theme); + parseViews(theme); + parseFeatures(theme); + + mPaths.pop_back(); + } +} + +void ThemeData::parseFeatures(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + for(pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) + { + if(!node.attribute("supported")) + throw error << "Feature missing \"supported\" attribute!"; + + const std::string supportedAttr = node.attribute("supported").as_string(); + + if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) + { + parseViews(node); + } + } +} + +void ThemeData::parseVariables(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + pugi::xml_node variables = root.child("variables"); + + if(!variables) + return; + + for(pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) + { + std::string key = it->name(); + std::string val = it->text().as_string(); + + if (!val.empty()) + mVariables.insert(std::pair(key, val)); + } +} + +void ThemeData::parseViews(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + // parse views + for(pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) + { + if(!node.attribute("name")) + throw error << "View missing \"name\" attribute!"; + + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + std::string viewKey; + while(off != std::string::npos || prevOff != std::string::npos) + { + viewKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) + { + ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; + parseView(node, view); + + if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) + { + ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; + parseView(node, view); + } + } + } + } +} + +void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) +{ + ThemeException error; + error.setFiles(mPaths); + + for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + { + if(!node.attribute("name")) + throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; + + auto elemTypeIt = sElementMap.find(node.name()); + if(elemTypeIt == sElementMap.cend()) + throw error << "Unknown element of type \"" << node.name() << "\"!"; + + if (parseRegion(node)) + { + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + parseElement(node, elemTypeIt->second, + view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); + + if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) + view.orderedKeys.push_back(elemKey); + } + } + } +} + +bool ThemeData::parseRegion(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("region")) + { + std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); + + parse = false; + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("region").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + if (elemKey == regionsetting) + { + parse = true; + return parse; + } + } + + } + return parse; + +} + + +void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element) +{ + ThemeException error; + error.setFiles(mPaths); + + element.type = root.name(); + element.extra = root.attribute("extra").as_bool(false); + + for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + { + auto typeIt = typeMap.find(node.name()); + if (typeIt == typeMap.cend()) + continue; + //throw error << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ")."; + + std::string str = resolveSystemVariable(mSystemThemeFolder, resolvePlaceholders(node.text().as_string())); + + switch(typeIt->second) + { + case NORMALIZED_PAIR: + { + size_t divider = str.find(' '); + if(divider == std::string::npos) + throw error << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")"; + + std::string first = str.substr(0, divider); + std::string second = str.substr(divider, std::string::npos); + + Vector2f val((float)atof(first.c_str()), (float)atof(second.c_str())); + + element.properties[node.name()] = val; + break; + } + case STRING: + element.properties[node.name()] = str; + break; + case PATH: + { + std::string path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true); + + if (!ResourceManager::getInstance()->fileExists(path)) + { + std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); + if (ResourceManager::getInstance()->fileExists(rootPath)) + path = rootPath; + } + + if (!ResourceManager::getInstance()->fileExists(path)) + { + std::stringstream ss; + ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda + ss << "could not find file \"" << node.text().get() << "\" "; + if(node.text().get() != path) + ss << "(which resolved to \"" << path << "\") "; + + LOG(LogWarning) << ss.str(); + + auto test = element.properties[node.name()]; + if (test.s.empty()) + element.properties[node.name()] = path; + } + else + element.properties[node.name()] = path; + + break; + } + case COLOR: + element.properties[node.name()] = getHexColor(str.c_str()); + break; + case FLOAT: + { + float floatVal = static_cast(strtod(str.c_str(), 0)); + element.properties[node.name()] = floatVal; + break; + } + + case BOOLEAN: + { + // only look at first char + char first = str[0]; + // 1*, t* (true), T* (True), y* (yes), Y* (YES) + bool boolVal = (first == '1' || first == 't' || first == 'T' || first == 'y' || first == 'Y'); + + element.properties[node.name()] = boolVal; + break; + } + default: + throw error << "Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name(); + } + } +} + +bool ThemeData::hasView(const std::string& view) +{ + auto viewIt = mViews.find(view); + return (viewIt != mViews.cend()); +} + +const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const +{ + auto viewIt = mViews.find(view); + if(viewIt == mViews.cend()) + return NULL; // not found + + auto elemIt = viewIt->second.elements.find(element); + if(elemIt == viewIt->second.elements.cend()) return NULL; + + if(elemIt->second.type != expectedType && !expectedType.empty()) + { + LOG(LogWarning) << " requested mismatched theme type for [" << view << "." << element << "] - expected \"" + << expectedType << "\", got \"" << elemIt->second.type << "\""; + return NULL; + } + + return &elemIt->second; +} + +const std::shared_ptr& ThemeData::getDefault() +{ + static std::shared_ptr theme = nullptr; + if(theme == nullptr) + { + theme = std::shared_ptr(new ThemeData()); + + const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_theme_default.xml"; + if(Utils::FileSystem::exists(path)) + { + try + { + std::map emptyMap; + theme->loadFile("", emptyMap, path); + } catch(ThemeException& e) + { + LOG(LogError) << e.what(); + theme = std::shared_ptr(new ThemeData()); //reset to empty + } + } + } + + return theme; +} + +std::vector ThemeData::makeExtras(const std::shared_ptr& theme, const std::string& view, Window* window) +{ + std::vector comps; + + auto viewIt = theme->mViews.find(view); + if(viewIt == theme->mViews.cend()) + return comps; + + for(auto it = viewIt->second.orderedKeys.cbegin(); it != viewIt->second.orderedKeys.cend(); it++) + { + ThemeElement& elem = viewIt->second.elements.at(*it); + if(elem.extra) + { + GuiComponent* comp = NULL; + const std::string& t = elem.type; + if(t == "image") + comp = new ImageComponent(window); + else if(t == "text") + comp = new TextComponent(window); + + comp->setDefaultZIndex(10); + comp->applyTheme(theme, view, *it, ThemeFlags::ALL); + comps.push_back(comp); + } + } + + return comps; +} + +std::map ThemeData::getThemeSets() +{ + std::map sets; + + static const size_t pathCount = 2; + std::string paths[pathCount] = + { + "/etc/emulationstation/themes", + Utils::FileSystem::getHomePath() + "/.emulationstation/themes" + }; + + for(size_t i = 0; i < pathCount; i++) + { + if(!Utils::FileSystem::isDirectory(paths[i])) + continue; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(paths[i]); + + for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if(Utils::FileSystem::isDirectory(*it)) + { + ThemeSet set = {*it}; + sets[set.getName()] = set; + } + } + } + + return sets; +} + +std::string ThemeData::getThemeFromCurrentSet(const std::string& system) +{ + std::map themeSets = ThemeData::getThemeSets(); + if(themeSets.empty()) + { + // no theme sets available + return ""; + } + + std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if(set == themeSets.cend()) + { + // currently selected theme set is missing, so just pick the first available set + set = themeSets.cbegin(); + Settings::getInstance()->setString("ThemeSet", set->first); + } + + return set->second.getThemePath(system); +} diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 1b361f408e..7a3164f882 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -1,531 +1,531 @@ -#include "Window.h" - -#include "components/HelpComponent.h" -#include "components/ImageComponent.h" -#include "resources/Font.h" -#include "resources/TextureResource.h" -#include "InputManager.h" -#include "Log.h" -#include "Renderer.h" -#include "Scripting.h" -#include -#include - -#include - -Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), - mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) -{ - mHelp = new HelpComponent(this); - mBackgroundOverlay = new ImageComponent(this); - mSplash = NULL; - -} - -Window::~Window() -{ - delete mBackgroundOverlay; - - // delete all our GUIs - while(peekGui()) - delete peekGui(); - - delete mHelp; -} - -void Window::pushGui(GuiComponent* gui) -{ - if (mGuiStack.size() > 0) - { - auto& top = mGuiStack.back(); - top->topWindow(false); - } - mGuiStack.push_back(gui); - gui->updateHelpPrompts(); -} - -void Window::removeGui(GuiComponent* gui) -{ - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - { - if(*i == gui) - { - i = mGuiStack.erase(i); - - if(i == mGuiStack.cend() && mGuiStack.size()) // we just popped the stack and the stack is not empty - { - mGuiStack.back()->updateHelpPrompts(); - mGuiStack.back()->topWindow(true); - } - - return; - } - } -} - -GuiComponent* Window::peekGui() -{ - if(mGuiStack.size() == 0) - return NULL; - - return mGuiStack.back(); -} - -bool Window::init() -{ - if(!Renderer::init()) - { - LOG(LogError) << "Renderer failed to initialize!"; - return false; - } - - InputManager::getInstance()->init(); - ResourceManager::getInstance()->reloadAll(); - - //keep a reference to the default fonts, so they don't keep getting destroyed/recreated - if(mDefaultFonts.empty()) - { - mDefaultFonts.push_back(Font::get(FONT_SIZE_SMALL)); - mDefaultFonts.push_back(Font::get(FONT_SIZE_MEDIUM)); - mDefaultFonts.push_back(Font::get(FONT_SIZE_LARGE)); - } - - mBackgroundOverlay->setImage(":/scroll_gradient.png"); - mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - - // update our help because font sizes probably changed - if (peekGui()) - peekGui()->updateHelpPrompts(); - - return true; -} - -void Window::deinit() -{ - // Hide all GUI elements on uninitialisation - this disable - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - { - (*i)->onHide(); - } - InputManager::getInstance()->deinit(); - ResourceManager::getInstance()->unloadAll(); - Renderer::deinit(); -} - -void Window::textInput(const char* text) -{ - if(peekGui()) - peekGui()->textInput(text); -} - -void Window::input(InputConfig* config, Input input) -{ - if (mScreenSaver) { - if(mScreenSaver->isScreenSaverActive() && Settings::getInstance()->getBool("ScreenSaverControls") && - (Settings::getInstance()->getString("ScreenSaverBehavior") == "random video")) - { - if(mScreenSaver->getCurrentGame() != NULL && (config->isMappedLike("right", input) || config->isMappedTo("start", input) || config->isMappedTo("select", input))) - { - if(config->isMappedLike("right", input) || config->isMappedTo("select", input)) - { - if (input.value != 0) { - // handle screensaver control - mScreenSaver->nextVideo(); - } - return; - } - else if(config->isMappedTo("start", input) && input.value != 0) - { - // launch game! - cancelScreenSaver(); - mScreenSaver->launchGame(); - // to force handling the wake up process - mSleeping = true; - } - } - } - } - - if(mSleeping) - { - // wake up - mTimeSinceLastInput = 0; - cancelScreenSaver(); - mSleeping = false; - onWake(); - return; - } - - mTimeSinceLastInput = 0; - if (cancelScreenSaver()) - return; - - if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle debug grid with Ctrl-G - Settings::getInstance()->setBool("DebugGrid", !Settings::getInstance()->getBool("DebugGrid")); - } - else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle TextComponent debug view with Ctrl-T - Settings::getInstance()->setBool("DebugText", !Settings::getInstance()->getBool("DebugText")); - } - else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle TextComponent debug view with Ctrl-I - Settings::getInstance()->setBool("DebugImage", !Settings::getInstance()->getBool("DebugImage")); - } - else - { - if (peekGui()) - { - this->peekGui()->input(config, input); // this is where the majority of inputs will be consumed: the GuiComponent Stack - } - } -} - -void Window::update(int deltaTime) -{ - if(mNormalizeNextUpdate) - { - mNormalizeNextUpdate = false; - if(deltaTime > mAverageDeltaTime) - deltaTime = mAverageDeltaTime; - } - - mFrameTimeElapsed += deltaTime; - mFrameCountElapsed++; - if(mFrameTimeElapsed > 500) - { - mAverageDeltaTime = mFrameTimeElapsed / mFrameCountElapsed; - - if(Settings::getInstance()->getBool("DrawFramerate")) - { - std::stringstream ss; - - // fps - ss << std::fixed << std::setprecision(1) << (1000.0f * (float)mFrameCountElapsed / (float)mFrameTimeElapsed) << "fps, "; - ss << std::fixed << std::setprecision(2) << ((float)mFrameTimeElapsed / (float)mFrameCountElapsed) << "ms"; - - // vram - float textureVramUsageMb = TextureResource::getTotalMemUsage() / 1000.0f / 1000.0f; - float textureTotalUsageMb = TextureResource::getTotalTextureSize() / 1000.0f / 1000.0f; - float fontVramUsageMb = Font::getTotalMemUsage() / 1000.0f / 1000.0f; - - ss << "\nFont VRAM: " << fontVramUsageMb << " Tex VRAM: " << textureVramUsageMb << - " Tex Max: " << textureTotalUsageMb; - mFrameDataText = std::unique_ptr(mDefaultFonts.at(1)->buildTextCache(ss.str(), 50.f, 50.f, 0xFF00FFFF)); - } - - mFrameTimeElapsed = 0; - mFrameCountElapsed = 0; - } - - mTimeSinceLastInput += deltaTime; - - if(peekGui()) - peekGui()->update(deltaTime); - - // Update the screensaver - if (mScreenSaver) - mScreenSaver->update(deltaTime); -} - -void Window::render() -{ - Transform4x4f transform = Transform4x4f::Identity(); - - mRenderedHelpPrompts = false; - - // draw only bottom and top of GuiStack (if they are different) - if(mGuiStack.size()) - { - auto& bottom = mGuiStack.front(); - auto& top = mGuiStack.back(); - - bottom->render(transform); - if(bottom != top) - { - mBackgroundOverlay->render(transform); - top->render(transform); - } - } - - if(!mRenderedHelpPrompts) - mHelp->render(transform); - - if(Settings::getInstance()->getBool("DrawFramerate") && mFrameDataText) - { - Renderer::setMatrix(Transform4x4f::Identity()); - mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); - } - - unsigned int screensaverTime = (unsigned int)Settings::getInstance()->getInt("ScreenSaverTime"); - if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) - startScreenSaver(); - - // Always call the screensaver render function regardless of whether the screensaver is active - // or not because it may perform a fade on transition - renderScreenSaver(); - - if(!mRenderScreenSaver && mInfoPopup) - { - mInfoPopup->render(transform); - } - - if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) - { - if (!isProcessing() && mAllowSleep && (!mScreenSaver || mScreenSaver->allowSleep())) - { - // go to sleep - if (mSleeping == false) { - mSleeping = true; - onSleep(); - } - } - } -} - -void Window::normalizeNextUpdate() -{ - mNormalizeNextUpdate = true; -} - -bool Window::getAllowSleep() -{ - return mAllowSleep; -} - -void Window::setAllowSleep(bool sleep) -{ - mAllowSleep = sleep; -} - -void Window::endRenderLoadingScreen() -{ - mSplash = NULL; -} - -void Window::renderLoadingScreen(std::string text, float percent) -{ -#if defined(_WIN32) - // Avoid Window Freezing on Windows - SDL_Event event; - while (SDL_PollEvent(&event)) ; -#endif - - Transform4x4f trans = Transform4x4f::Identity(); - Renderer::setMatrix(trans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); - - if (percent > 0) - { - float baseHeight = 0.04f; - - float w = Renderer::getScreenWidth() / 2; - float h = Renderer::getScreenHeight() * baseHeight; - - float x = Renderer::getScreenWidth() / 2 - w / 2; - float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); - - Renderer::drawRect(x, y, w, h, 0x252525FF); - Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF - } - - - ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); - -#if defined(_WIN32) - if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); -#endif - - if (mSplash != NULL) - splash.setImage(mSplash); - else - splash.setImage(":/splash.svg"); - - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); - splash.render(trans); - - - auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); - - float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.8f); // 35 - trans = trans.translate(Vector3f(x, y, 0.0f)); - Renderer::setMatrix(trans); - font->renderTextCache(cache); - delete cache; - - - Renderer::swapBuffers(); -} - -void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) -{ - Transform4x4f trans = Transform4x4f::Identity(); - Renderer::setMatrix(trans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); - - ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); - -#if defined(_WIN32) - if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); -#endif - - if (mSplash != NULL) - splash.setImage(mSplash); - else - splash.setImage(":/splash.svg"); - - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); - splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); - splash.render(trans); - - auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); - - float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.835f); - trans = trans.translate(Vector3f(x, y, 0.0f)); - Renderer::setMatrix(trans); - font->renderTextCache(cache); - delete cache; - - if (swapBuffers) - Renderer::swapBuffers(); -} - - -void Window::renderHelpPromptsEarly() -{ - mHelp->render(Transform4x4f::Identity()); - mRenderedHelpPrompts = true; -} - -void Window::setHelpPrompts(const std::vector& prompts, const HelpStyle& style) -{ - mHelp->clearPrompts(); - mHelp->setStyle(style); - - std::vector addPrompts; - - std::map inputSeenMap; - std::map mappedToSeenMap; - for(auto it = prompts.cbegin(); it != prompts.cend(); it++) - { - // only add it if the same icon hasn't already been added - if(inputSeenMap.emplace(it->first, true).second) - { - // this symbol hasn't been seen yet, what about the action name? - auto mappedTo = mappedToSeenMap.find(it->second); - if(mappedTo != mappedToSeenMap.cend()) - { - // yes, it has! - - // can we combine? (dpad only) - if((it->first == "up/down" && addPrompts.at(mappedTo->second).first != "left/right") || - (it->first == "left/right" && addPrompts.at(mappedTo->second).first != "up/down")) - { - // yes! - addPrompts.at(mappedTo->second).first = "up/down/left/right"; - // don't need to add this to addPrompts since we just merged - }else{ - // no, we can't combine! - addPrompts.push_back(*it); - } - }else{ - // no, it hasn't! - mappedToSeenMap.emplace(it->second, (int)addPrompts.size()); - addPrompts.push_back(*it); - } - } - } - - // sort prompts so it goes [dpad_all] [dpad_u/d] [dpad_l/r] [a/b/x/y/l/r] [start/select] - std::sort(addPrompts.begin(), addPrompts.end(), [](const HelpPrompt& a, const HelpPrompt& b) -> bool { - - static const char* map[] = { - "up/down/left/right", - "up/down", - "left/right", - "a", "b", "x", "y", "l", "r", - "start", "select", - NULL - }; - - int i = 0; - int aVal = 0; - int bVal = 0; - while(map[i] != NULL) - { - if(a.first == map[i]) - aVal = i; - if(b.first == map[i]) - bVal = i; - i++; - } - - return aVal > bVal; - }); - - mHelp->setPrompts(addPrompts); -} - - -void Window::onSleep() -{ - Scripting::fireEvent("sleep"); -} - -void Window::onWake() -{ - Scripting::fireEvent("wake"); -} - -bool Window::isProcessing() -{ - return count_if(mGuiStack.cbegin(), mGuiStack.cend(), [](GuiComponent* c) { return c->isProcessing(); }) > 0; -} - -void Window::startScreenSaver() -{ - if (mScreenSaver && !mRenderScreenSaver) - { - // Tell the GUI components the screensaver is starting - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - (*i)->onScreenSaverActivate(); - - mScreenSaver->startScreenSaver(); - mRenderScreenSaver = true; - } -} - -bool Window::cancelScreenSaver() -{ - if (mScreenSaver && mRenderScreenSaver) - { - mScreenSaver->stopScreenSaver(); - mRenderScreenSaver = false; - mScreenSaver->resetCounts(); - - // Tell the GUI components the screensaver has stopped - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - (*i)->onScreenSaverDeactivate(); - - return true; - } - - return false; -} - -void Window::renderScreenSaver() -{ - if (mScreenSaver) - mScreenSaver->renderScreenSaver(); -} +#include "Window.h" + +#include "components/HelpComponent.h" +#include "components/ImageComponent.h" +#include "resources/Font.h" +#include "resources/TextureResource.h" +#include "InputManager.h" +#include "Log.h" +#include "Renderer.h" +#include "Scripting.h" +#include +#include + +#include + +Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), + mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) +{ + mHelp = new HelpComponent(this); + mBackgroundOverlay = new ImageComponent(this); + mSplash = NULL; + +} + +Window::~Window() +{ + delete mBackgroundOverlay; + + // delete all our GUIs + while(peekGui()) + delete peekGui(); + + delete mHelp; +} + +void Window::pushGui(GuiComponent* gui) +{ + if (mGuiStack.size() > 0) + { + auto& top = mGuiStack.back(); + top->topWindow(false); + } + mGuiStack.push_back(gui); + gui->updateHelpPrompts(); +} + +void Window::removeGui(GuiComponent* gui) +{ + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + { + if(*i == gui) + { + i = mGuiStack.erase(i); + + if(i == mGuiStack.cend() && mGuiStack.size()) // we just popped the stack and the stack is not empty + { + mGuiStack.back()->updateHelpPrompts(); + mGuiStack.back()->topWindow(true); + } + + return; + } + } +} + +GuiComponent* Window::peekGui() +{ + if(mGuiStack.size() == 0) + return NULL; + + return mGuiStack.back(); +} + +bool Window::init() +{ + if(!Renderer::init()) + { + LOG(LogError) << "Renderer failed to initialize!"; + return false; + } + + InputManager::getInstance()->init(); + ResourceManager::getInstance()->reloadAll(); + + //keep a reference to the default fonts, so they don't keep getting destroyed/recreated + if(mDefaultFonts.empty()) + { + mDefaultFonts.push_back(Font::get(FONT_SIZE_SMALL)); + mDefaultFonts.push_back(Font::get(FONT_SIZE_MEDIUM)); + mDefaultFonts.push_back(Font::get(FONT_SIZE_LARGE)); + } + + mBackgroundOverlay->setImage(":/scroll_gradient.png"); + mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + + // update our help because font sizes probably changed + if (peekGui()) + peekGui()->updateHelpPrompts(); + + return true; +} + +void Window::deinit() +{ + // Hide all GUI elements on uninitialisation - this disable + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + { + (*i)->onHide(); + } + InputManager::getInstance()->deinit(); + ResourceManager::getInstance()->unloadAll(); + Renderer::deinit(); +} + +void Window::textInput(const char* text) +{ + if(peekGui()) + peekGui()->textInput(text); +} + +void Window::input(InputConfig* config, Input input) +{ + if (mScreenSaver) { + if(mScreenSaver->isScreenSaverActive() && Settings::getInstance()->getBool("ScreenSaverControls") && + (Settings::getInstance()->getString("ScreenSaverBehavior") == "random video")) + { + if(mScreenSaver->getCurrentGame() != NULL && (config->isMappedLike("right", input) || config->isMappedTo("start", input) || config->isMappedTo("select", input))) + { + if(config->isMappedLike("right", input) || config->isMappedTo("select", input)) + { + if (input.value != 0) { + // handle screensaver control + mScreenSaver->nextVideo(); + } + return; + } + else if(config->isMappedTo("start", input) && input.value != 0) + { + // launch game! + cancelScreenSaver(); + mScreenSaver->launchGame(); + // to force handling the wake up process + mSleeping = true; + } + } + } + } + + if(mSleeping) + { + // wake up + mTimeSinceLastInput = 0; + cancelScreenSaver(); + mSleeping = false; + onWake(); + return; + } + + mTimeSinceLastInput = 0; + if (cancelScreenSaver()) + return; + + if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle debug grid with Ctrl-G + Settings::getInstance()->setBool("DebugGrid", !Settings::getInstance()->getBool("DebugGrid")); + } + else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle TextComponent debug view with Ctrl-T + Settings::getInstance()->setBool("DebugText", !Settings::getInstance()->getBool("DebugText")); + } + else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle TextComponent debug view with Ctrl-I + Settings::getInstance()->setBool("DebugImage", !Settings::getInstance()->getBool("DebugImage")); + } + else + { + if (peekGui()) + { + this->peekGui()->input(config, input); // this is where the majority of inputs will be consumed: the GuiComponent Stack + } + } +} + +void Window::update(int deltaTime) +{ + if(mNormalizeNextUpdate) + { + mNormalizeNextUpdate = false; + if(deltaTime > mAverageDeltaTime) + deltaTime = mAverageDeltaTime; + } + + mFrameTimeElapsed += deltaTime; + mFrameCountElapsed++; + if(mFrameTimeElapsed > 500) + { + mAverageDeltaTime = mFrameTimeElapsed / mFrameCountElapsed; + + if(Settings::getInstance()->getBool("DrawFramerate")) + { + std::stringstream ss; + + // fps + ss << std::fixed << std::setprecision(1) << (1000.0f * (float)mFrameCountElapsed / (float)mFrameTimeElapsed) << "fps, "; + ss << std::fixed << std::setprecision(2) << ((float)mFrameTimeElapsed / (float)mFrameCountElapsed) << "ms"; + + // vram + float textureVramUsageMb = TextureResource::getTotalMemUsage() / 1000.0f / 1000.0f; + float textureTotalUsageMb = TextureResource::getTotalTextureSize() / 1000.0f / 1000.0f; + float fontVramUsageMb = Font::getTotalMemUsage() / 1000.0f / 1000.0f; + + ss << "\nFont VRAM: " << fontVramUsageMb << " Tex VRAM: " << textureVramUsageMb << + " Tex Max: " << textureTotalUsageMb; + mFrameDataText = std::unique_ptr(mDefaultFonts.at(1)->buildTextCache(ss.str(), 50.f, 50.f, 0xFF00FFFF)); + } + + mFrameTimeElapsed = 0; + mFrameCountElapsed = 0; + } + + mTimeSinceLastInput += deltaTime; + + if(peekGui()) + peekGui()->update(deltaTime); + + // Update the screensaver + if (mScreenSaver) + mScreenSaver->update(deltaTime); +} + +void Window::render() +{ + Transform4x4f transform = Transform4x4f::Identity(); + + mRenderedHelpPrompts = false; + + // draw only bottom and top of GuiStack (if they are different) + if(mGuiStack.size()) + { + auto& bottom = mGuiStack.front(); + auto& top = mGuiStack.back(); + + bottom->render(transform); + if(bottom != top) + { + mBackgroundOverlay->render(transform); + top->render(transform); + } + } + + if(!mRenderedHelpPrompts) + mHelp->render(transform); + + if(Settings::getInstance()->getBool("DrawFramerate") && mFrameDataText) + { + Renderer::setMatrix(Transform4x4f::Identity()); + mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); + } + + unsigned int screensaverTime = (unsigned int)Settings::getInstance()->getInt("ScreenSaverTime"); + if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) + startScreenSaver(); + + // Always call the screensaver render function regardless of whether the screensaver is active + // or not because it may perform a fade on transition + renderScreenSaver(); + + if(!mRenderScreenSaver && mInfoPopup) + { + mInfoPopup->render(transform); + } + + if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) + { + if (!isProcessing() && mAllowSleep && (!mScreenSaver || mScreenSaver->allowSleep())) + { + // go to sleep + if (mSleeping == false) { + mSleeping = true; + onSleep(); + } + } + } +} + +void Window::normalizeNextUpdate() +{ + mNormalizeNextUpdate = true; +} + +bool Window::getAllowSleep() +{ + return mAllowSleep; +} + +void Window::setAllowSleep(bool sleep) +{ + mAllowSleep = sleep; +} + +void Window::endRenderLoadingScreen() +{ + mSplash = NULL; +} + +void Window::renderLoadingScreen(std::string text, float percent) +{ +#if defined(_WIN32) + // Avoid Window Freezing on Windows + SDL_Event event; + while (SDL_PollEvent(&event)) ; +#endif + + Transform4x4f trans = Transform4x4f::Identity(); + Renderer::setMatrix(trans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); + + if (percent >= 0) + { + float baseHeight = 0.04f; + + float w = Renderer::getScreenWidth() / 2; + float h = Renderer::getScreenHeight() * baseHeight; + + float x = Renderer::getScreenWidth() / 2 - w / 2; + float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); + + Renderer::drawRect(x, y, w, h, 0x252525FF); + Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF + } + + + ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.render(trans); + + + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); + + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.78f); // 35 + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; + + + Renderer::swapBuffers(); +} + +void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) +{ + Transform4x4f trans = Transform4x4f::Identity(); + Renderer::setMatrix(trans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); + + ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); + splash.render(trans); + + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); + + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.835f); + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; + + if (swapBuffers) + Renderer::swapBuffers(); +} + + +void Window::renderHelpPromptsEarly() +{ + mHelp->render(Transform4x4f::Identity()); + mRenderedHelpPrompts = true; +} + +void Window::setHelpPrompts(const std::vector& prompts, const HelpStyle& style) +{ + mHelp->clearPrompts(); + mHelp->setStyle(style); + + std::vector addPrompts; + + std::map inputSeenMap; + std::map mappedToSeenMap; + for(auto it = prompts.cbegin(); it != prompts.cend(); it++) + { + // only add it if the same icon hasn't already been added + if(inputSeenMap.emplace(it->first, true).second) + { + // this symbol hasn't been seen yet, what about the action name? + auto mappedTo = mappedToSeenMap.find(it->second); + if(mappedTo != mappedToSeenMap.cend()) + { + // yes, it has! + + // can we combine? (dpad only) + if((it->first == "up/down" && addPrompts.at(mappedTo->second).first != "left/right") || + (it->first == "left/right" && addPrompts.at(mappedTo->second).first != "up/down")) + { + // yes! + addPrompts.at(mappedTo->second).first = "up/down/left/right"; + // don't need to add this to addPrompts since we just merged + }else{ + // no, we can't combine! + addPrompts.push_back(*it); + } + }else{ + // no, it hasn't! + mappedToSeenMap.emplace(it->second, (int)addPrompts.size()); + addPrompts.push_back(*it); + } + } + } + + // sort prompts so it goes [dpad_all] [dpad_u/d] [dpad_l/r] [a/b/x/y/l/r] [start/select] + std::sort(addPrompts.begin(), addPrompts.end(), [](const HelpPrompt& a, const HelpPrompt& b) -> bool { + + static const char* map[] = { + "up/down/left/right", + "up/down", + "left/right", + "a", "b", "x", "y", "l", "r", + "start", "select", + NULL + }; + + int i = 0; + int aVal = 0; + int bVal = 0; + while(map[i] != NULL) + { + if(a.first == map[i]) + aVal = i; + if(b.first == map[i]) + bVal = i; + i++; + } + + return aVal > bVal; + }); + + mHelp->setPrompts(addPrompts); +} + + +void Window::onSleep() +{ + Scripting::fireEvent("sleep"); +} + +void Window::onWake() +{ + Scripting::fireEvent("wake"); +} + +bool Window::isProcessing() +{ + return count_if(mGuiStack.cbegin(), mGuiStack.cend(), [](GuiComponent* c) { return c->isProcessing(); }) > 0; +} + +void Window::startScreenSaver() +{ + if (mScreenSaver && !mRenderScreenSaver) + { + // Tell the GUI components the screensaver is starting + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + (*i)->onScreenSaverActivate(); + + mScreenSaver->startScreenSaver(); + mRenderScreenSaver = true; + } +} + +bool Window::cancelScreenSaver() +{ + if (mScreenSaver && mRenderScreenSaver) + { + mScreenSaver->stopScreenSaver(); + mRenderScreenSaver = false; + mScreenSaver->resetCounts(); + + // Tell the GUI components the screensaver has stopped + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + (*i)->onScreenSaverDeactivate(); + + return true; + } + + return false; +} + +void Window::renderScreenSaver() +{ + if (mScreenSaver) + mScreenSaver->renderScreenSaver(); +} diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index 613a22c026..f2d36950d0 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -282,17 +282,17 @@ class OptionListComponent : public GuiComponent onSelectedChanged(); } - void clear() { - mEntries.clear(); + void clear() { + mEntries.clear(); } - inline void invalidate() { - onSelectedChanged(); + inline void invalidate() { + onSelectedChanged(); } - void setSelectedChangedCallback(const std::function& callback) - { - mSelectedChangedCallback = callback; + void setSelectedChangedCallback(const std::function& callback) + { + mSelectedChangedCallback = callback; } @@ -344,7 +344,7 @@ class OptionListComponent : public GuiComponent } } - if (mSelectedChangedCallback) + if (mSelectedChangedCallback) mSelectedChangedCallback(mEntries.at(getSelectedId()).object); } diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index e80e589518..bf07b39759 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -1,165 +1,165 @@ -#include "platform.h" -#include - -#ifdef WIN32 -#include -#include -#else -#include -#endif -#include - -#include "Window.h" - -#include "GuiComponent.h" - -int runShutdownCommand() -{ -#ifdef WIN32 // windows - return system("shutdown -s -t 0"); -#else // osx / linux - return system("sudo shutdown -h now"); -#endif -} - -int runRestartCommand() -{ -#ifdef WIN32 // windows - return system("shutdown -r -t 0"); -#else // osx / linux - return system("sudo shutdown -r now"); -#endif -} - -std::string trim(const std::string& str) -{ - size_t first = str.find_first_not_of(' '); - if (std::string::npos == first) - { - return str; - } - size_t last = str.find_last_not_of(' '); - return str.substr(first, (last - first + 1)); -} - -void split_cmd(const std::string& cmd, - std::string* executable, - std::string* parameters) -{ - std::string c(cmd); - size_t exec_end; - - c = trim(c); - //boost::trim_all(c); - - if (c[0] == '\"') - { - exec_end = c.find_first_of('\"', 1); - if (std::string::npos != exec_end) - { - *executable = c.substr(1, exec_end - 1); - *parameters = c.substr(exec_end + 1); - } - else - { - *executable = c.substr(1, exec_end); - std::string().swap(*parameters); - } - } - else - { - exec_end = c.find_first_of(' ', 0); - if (std::string::npos != exec_end) - { - *executable = c.substr(0, exec_end); - *parameters = c.substr(exec_end + 1); - } - else - { - *executable = c.substr(0, exec_end); - std::string().swap(*parameters); - } - } -} - -int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window) -{ -#ifdef WIN32 - if (window != NULL) - window->renderGameLoadingScreen(); - - // on Windows we use _wsystem to support non-ASCII paths - // which requires converting from utf8 to a wstring - typedef std::codecvt_utf8 convert_type; - std::wstring_convert converter; - std::wstring wchar_str = converter.from_bytes(cmd_utf8); - - std::string exe; - std::string args; - - split_cmd(cmd_utf8, &exe, &args); - - SHELLEXECUTEINFO lpExecInfo; - lpExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); - lpExecInfo.lpFile = exe.c_str(); - lpExecInfo.fMask = SEE_MASK_DOENVSUBST | SEE_MASK_NOCLOSEPROCESS; - lpExecInfo.hwnd = NULL; - lpExecInfo.lpVerb = "open"; // to open program - lpExecInfo.lpParameters = args.c_str(); // file name as an argument - lpExecInfo.lpDirectory = NULL; - lpExecInfo.nShow = SW_SHOW; // show command prompt with normal window size - lpExecInfo.hInstApp = (HINSTANCE)SE_ERR_DDEFAIL; //WINSHELLAPI BOOL WINAPI result; - ShellExecuteEx(&lpExecInfo); - - if (lpExecInfo.hProcess != NULL) - { - if (window == NULL) - WaitForSingleObject(lpExecInfo.hProcess, INFINITE); - else - { - while (WaitForSingleObject(lpExecInfo.hProcess, 50) == 0x00000102L) - { - bool polled = false; - - SDL_Event event; - while (SDL_PollEvent(&event)) - polled = true; - - if (window != NULL && polled) - window->renderGameLoadingScreen(); - } - } - - CloseHandle(lpExecInfo.hProcess); - return 0; - } - - return 1; - //return _wsystem(wchar_str.c_str()); -#else - return system(cmd_utf8.c_str()); -#endif -} - -int quitES(const std::string& filename) -{ - if (!filename.empty()) - touch(filename); - SDL_Event* quit = new SDL_Event(); - quit->type = SDL_QUIT; - SDL_PushEvent(quit); - return 0; -} - -void touch(const std::string& filename) -{ -#ifdef WIN32 - FILE* fp = fopen(filename.c_str(), "ab+"); - if (fp != NULL) - fclose(fp); -#else - int fd = open(filename.c_str(), O_CREAT|O_WRONLY, 0644); - if (fd >= 0) - close(fd); -#endif +#include "platform.h" +#include + +#ifdef WIN32 +#include +#include +#else +#include +#endif +#include + +#include "Window.h" + +#include "GuiComponent.h" + +int runShutdownCommand() +{ +#ifdef WIN32 // windows + return system("shutdown -s -t 0"); +#else // osx / linux + return system("sudo shutdown -h now"); +#endif +} + +int runRestartCommand() +{ +#ifdef WIN32 // windows + return system("shutdown -r -t 0"); +#else // osx / linux + return system("sudo shutdown -r now"); +#endif +} + +std::string trim(const std::string& str) +{ + size_t first = str.find_first_not_of(' '); + if (std::string::npos == first) + { + return str; + } + size_t last = str.find_last_not_of(' '); + return str.substr(first, (last - first + 1)); +} + +void split_cmd(const std::string& cmd, + std::string* executable, + std::string* parameters) +{ + std::string c(cmd); + size_t exec_end; + + c = trim(c); + //boost::trim_all(c); + + if (c[0] == '\"') + { + exec_end = c.find_first_of('\"', 1); + if (std::string::npos != exec_end) + { + *executable = c.substr(1, exec_end - 1); + *parameters = c.substr(exec_end + 1); + } + else + { + *executable = c.substr(1, exec_end); + std::string().swap(*parameters); + } + } + else + { + exec_end = c.find_first_of(' ', 0); + if (std::string::npos != exec_end) + { + *executable = c.substr(0, exec_end); + *parameters = c.substr(exec_end + 1); + } + else + { + *executable = c.substr(0, exec_end); + std::string().swap(*parameters); + } + } +} + +int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window) +{ +#ifdef WIN32 + if (window != NULL) + window->renderGameLoadingScreen(); + + // on Windows we use _wsystem to support non-ASCII paths + // which requires converting from utf8 to a wstring + typedef std::codecvt_utf8 convert_type; + std::wstring_convert converter; + std::wstring wchar_str = converter.from_bytes(cmd_utf8); + + std::string exe; + std::string args; + + split_cmd(cmd_utf8, &exe, &args); + + SHELLEXECUTEINFO lpExecInfo; + lpExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); + lpExecInfo.lpFile = exe.c_str(); + lpExecInfo.fMask = SEE_MASK_DOENVSUBST | SEE_MASK_NOCLOSEPROCESS; + lpExecInfo.hwnd = NULL; + lpExecInfo.lpVerb = "open"; // to open program + lpExecInfo.lpParameters = args.c_str(); // file name as an argument + lpExecInfo.lpDirectory = NULL; + lpExecInfo.nShow = SW_SHOW; // show command prompt with normal window size + lpExecInfo.hInstApp = (HINSTANCE)SE_ERR_DDEFAIL; //WINSHELLAPI BOOL WINAPI result; + ShellExecuteEx(&lpExecInfo); + + if (lpExecInfo.hProcess != NULL) + { + if (window == NULL) + WaitForSingleObject(lpExecInfo.hProcess, INFINITE); + else + { + while (WaitForSingleObject(lpExecInfo.hProcess, 50) == 0x00000102L) + { + bool polled = false; + + SDL_Event event; + while (SDL_PollEvent(&event)) + polled = true; + + if (window != NULL && polled) + window->renderGameLoadingScreen(); + } + } + + CloseHandle(lpExecInfo.hProcess); + return 0; + } + + return 1; + //return _wsystem(wchar_str.c_str()); +#else + return system(cmd_utf8.c_str()); +#endif +} + +int quitES(const std::string& filename) +{ + if (!filename.empty()) + touch(filename); + SDL_Event* quit = new SDL_Event(); + quit->type = SDL_QUIT; + SDL_PushEvent(quit); + return 0; +} + +void touch(const std::string& filename) +{ +#ifdef WIN32 + FILE* fp = fopen(filename.c_str(), "ab+"); + if (fp != NULL) + fclose(fp); +#else + int fd = open(filename.c_str(), O_CREAT|O_WRONLY, 0644); + if (fd >= 0) + close(fd); +#endif } \ No newline at end of file diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 3c77caed8b..83b5270cb1 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -1,667 +1,667 @@ -#define _FILE_OFFSET_BITS 64 - -#include "utils/FileSystemUtil.h" - -#include "Settings.h" -#include -#include - -#if defined(_WIN32) -// because windows... -#include -#include -#define getcwd _getcwd -#define mkdir(x,y) _mkdir(x) -#define snprintf _snprintf -#define stat64 _stat64 -#define unlink _unlink -#define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) -#define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) -#else // _WIN32 -#include -#include -#endif // _WIN32 - -namespace Utils -{ - namespace FileSystem - { - -#if defined(_WIN32) - static std::string convertFromWideString(const std::wstring wstring) - { - int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); - std::string string; - - string.resize(numBytes); - WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); - - return std::string(string); - - } // convertFromWideString -#endif // _WIN32 - - stringList getDirContent(const std::string& _path, const bool _recursive) - { - std::string path = getGenericPath(_path); - stringList contentList; - - // only parse the directory, if it's a directory - if(isDirectory(path)) - { - -#if defined(_WIN32) - WIN32_FIND_DATAW findData; - std::string wildcard = path + "/*"; - HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); - - if(hFind != INVALID_HANDLE_VALUE) - { - // loop over all files in the directory - do - { - std::string name = convertFromWideString(findData.cFileName); - - // ignore "." and ".." - if((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - contentList.push_back(fullName); - - if(_recursive && isDirectory(fullName)) - contentList.merge(getDirContent(fullName, true)); - } - } - while(FindNextFileW(hFind, &findData)); - - FindClose(hFind); - } -#else // _WIN32 - DIR* dir = opendir(path.c_str()); - - if(dir != NULL) - { - struct dirent* entry; - - // loop over all files in the directory - while((entry = readdir(dir)) != NULL) - { - std::string name(entry->d_name); - - // ignore "." and ".." - if((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - contentList.push_back(fullName); - - if(_recursive && isDirectory(fullName)) - contentList.merge(getDirContent(fullName, true)); - } - } - - closedir(dir); - } -#endif // _WIN32 - - } - - // sort the content list - contentList.sort(); - - // return the content list - return contentList; - - } // getDirContent - - stringList getPathList(const std::string& _path) - { - stringList pathList; - std::string path = getGenericPath(_path); - size_t start = 0; - size_t end = 0; - - // split at '/' - while((end = path.find("/", start)) != std::string::npos) - { - if(end != start) - pathList.push_back(std::string(path, start, end - start)); - - start = end + 1; - } - - // add last folder / file to pathList - if(start != path.size()) - pathList.push_back(std::string(path, start, path.size() - start)); - - // return the path list - return pathList; - - } // getPathList - - std::string getHomePath() - { - static std::string path; - - // only construct the homepath once - if(!path.length()) - { - // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows - char* envHome = getenv("HOME"); - -#ifdef _DEBUG - // envHome = "H:/[Emulz]/EmulationStation/"; -#endif - - if (envHome) - path = getGenericPath(envHome); - -#if defined(_WIN32) - // but does not seem to work for Windows XP or Vista, so try something else - if(!path.length()) - { - char* envHomeDrive = getenv("HOMEDRIVE"); - char* envHomePath = getenv("HOMEPATH"); - if(envHomeDrive && envHomePath) - path = getGenericPath(std::string(envHomeDrive) + "/" + envHomePath); - } -#endif // _WIN32 - - // no homepath found, fall back to current working directory - if(!path.length()) - path = getCWDPath(); - } - - // return constructed homepath - return path; - - } // getHomePath - - std::string getCWDPath() - { - char temp[512]; - return (getcwd(temp, 512) ? getGenericPath(temp) : ""); - } // getCWDPath - - std::string getExePath() - { - static std::string path; - - // only construct the exepath once - if (!path.length()) - { -#if defined(_WIN32) - char buffer[MAX_PATH]; - DWORD size = MAX_PATH; - DWORD result = GetModuleFileNameA(NULL, buffer, size); - if (result) - { - std::string ret = buffer; - path = getParent(ret); - return ret; - } -#endif - path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); - if (isRegularFile(path)) - path = getParent(path); - } - - // return constructed exepath - return path; - - } // getExePath - - std::string getPreferredPath(const std::string& _path) - { - std::string path = _path; - size_t offset = std::string::npos; -#if defined(_WIN32) - // convert '/' to '\\' - while((offset = path.find('/')) != std::string::npos) - path.replace(offset, 1, "\\"); -#endif // _WIN32 - return path; - } - - std::string getGenericPath(const std::string& _path) - { - std::string path = _path; - size_t offset = std::string::npos; - - // remove "\\\\?\\" - if((path.find("\\\\?\\")) == 0) - path.erase(0, 4); - - // convert '\\' to '/' - while((offset = path.find('\\')) != std::string::npos) - path.replace(offset, 1 ,"/"); - - // remove double '/' - while((offset = path.find("//")) != std::string::npos) - path.erase(offset, 1); - - // remove trailing '/' - while(path.length() && ((offset = path.find_last_of('/')) == (path.length() - 1))) - path.erase(offset, 1); - - // return generic path - return path; - - } // getGenericPath - - std::string getEscapedPath(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // windows escapes stuff by just putting everything in quotes - return '"' + getPreferredPath(path) + '"'; -#else // _WIN32 - // insert a backslash before most characters that would mess up a bash path - const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; - const char* invalidChar = invalidChars; - - while(*invalidChar) - { - size_t start = 0; - size_t offset = 0; - - while((offset = path.find(*invalidChar, start)) != std::string::npos) - { - start = offset + 1; - - if((offset == 0) || (path[offset - 1] != '\\')) - { - path.insert(offset, 1, '\\'); - ++start; - } - } - - ++invalidChar; - } - - // return escaped path - return path; -#endif // _WIN32 - - } // getEscapedPath - - std::string getCanonicalPath(const std::string& _path) - { - // temporary hack for builtin resources - if((_path[0] == ':') && (_path[1] == '/')) - return _path; - - std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); - - // cleanup path - bool scan = true; - while(scan) - { - stringList pathList = getPathList(path); - - path.clear(); - scan = false; - - for(stringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); ++it) - { - // ignore empty - if((*it).empty()) - continue; - - // remove "/./" - if((*it) == ".") - continue; - - // resolve "/../" - if((*it) == "..") - { - path = getParent(path); - continue; - } - -#if defined(_WIN32) - // append folder to path - path += (path.size() == 0) ? (*it) : ("/" + (*it)); -#else // _WIN32 - // append folder to path - path += ("/" + (*it)); -#endif // _WIN32 - - // resolve symlink - if(isSymlink(path)) - { - std::string resolved = resolveSymlink(path); - - if(resolved.empty()) - return ""; - - if(isAbsolute(resolved)) - path = resolved; - else - path = getParent(path) + "/" + resolved; - - for(++it; it != pathList.cend(); ++it) - path += (path.size() == 0) ? (*it) : ("/" + (*it)); - - scan = true; - break; - } - } - } - - // return canonical path - return path; - - } // getCanonicalPath - - std::string getAbsolutePath(const std::string& _path, const std::string& _base) - { - std::string path = getGenericPath(_path); - std::string base = isAbsolute(_base) ? getGenericPath(_base) : getAbsolutePath(_base); - - // return absolute path - return isAbsolute(path) ? path : getGenericPath(base + "/" + path); - - } // getAbsolutePath - - std::string getParent(const std::string& _path) - { - std::string path = getGenericPath(_path); - size_t offset = std::string::npos; - - // find last '/' and erase it - if((offset = path.find_last_of('/')) != std::string::npos) - return path.erase(offset); - - // no parent found - return path; - - } // getParent - - std::string getFileName(const std::string& _path) - { - std::string path = getGenericPath(_path); - size_t offset = std::string::npos; - - // find last '/' and return the filename - if((offset = path.find_last_of('/')) != std::string::npos) - return ((path[offset + 1] == 0) ? "." : std::string(path, offset + 1)); - - // no '/' found, entire path is a filename - return path; - - } // getFileName - - std::string getStem(const std::string& _path) - { - std::string fileName = getFileName(_path); - size_t offset = std::string::npos; - - // empty fileName - if(fileName == ".") - return fileName; - - // find last '.' and erase the extension - if((offset = fileName.find_last_of('.')) != std::string::npos) - return fileName.erase(offset); - - // no '.' found, filename has no extension - return fileName; - - } // getStem - - std::string getExtension(const std::string& _path) - { - std::string fileName = getFileName(_path); - size_t offset = std::string::npos; - - // empty fileName - if(fileName == ".") - return fileName; - - // find last '.' and return the extension - if((offset = fileName.find_last_of('.')) != std::string::npos) - return std::string(fileName, offset); - - // no '.' found, filename has no extension - return "."; - - } // getExtension - - std::string resolveRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) - { - std::string path = getGenericPath(_path); - std::string relativeTo = isDirectory(_relativeTo) ? getGenericPath(_relativeTo) : getParent(_relativeTo); - - // nothing to resolve - if(!path.length()) - return path; - - // replace '.' with relativeTo - if((path[0] == '.') && (path[1] == '/')) - return (relativeTo + &(path[1])); - - // replace '~' with homePath - if(_allowHome && (path[0] == '~') && (path[1] == '/')) - return (getHomePath() + &(path[1])); - - // nothing to resolve - return path; - - } // resolveRelativePath - - std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) - { - bool contains = false; - std::string path = removeCommonPath(_path, _relativeTo, contains); - - if(contains) - { - // success - return ("./" + path); - } - - if(_allowHome) - { - path = removeCommonPath(_path, getHomePath(), contains); - - if(contains) - { - // success - return ("~/" + path); - } - } - - // nothing to resolve - return path; - - } // createRelativePath - - std::string removeCommonPath(const std::string& _path, const std::string& _common, bool& _contains) - { - std::string path = getGenericPath(_path); - std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); - - // check if path contains common - if(path.find(common) == 0) - { - _contains = true; - return path.substr(common.length() + 1); - } - - // it didn't - _contains = false; - return path; - - } // removeCommonPath - - std::string resolveSymlink(const std::string& _path) - { - std::string path = getGenericPath(_path); - std::string resolved; - -#if defined(_WIN32) - HANDLE hFile = CreateFile(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); - - if(hFile != INVALID_HANDLE_VALUE) - { - resolved.resize(GetFinalPathNameByHandle(hFile, nullptr, 0, FILE_NAME_NORMALIZED) + 1); - if(GetFinalPathNameByHandle(hFile, (LPSTR)resolved.data(), (DWORD)resolved.size(), FILE_NAME_NORMALIZED) > 0) - { - resolved.resize(resolved.size() - 1); - resolved = getGenericPath(resolved); - } - CloseHandle(hFile); - } -#else // _WIN32 - struct stat info; - - // check if lstat succeeded - if(lstat(path.c_str(), &info) == 0) - { - resolved.resize(info.st_size); - if(readlink(path.c_str(), (char*)resolved.data(), resolved.size()) > 0) - resolved = getGenericPath(resolved); - } -#endif // _WIN32 - - // return resolved path - return resolved; - - } // resolveSymlink - - bool removeFile(const std::string& _path) - { - std::string path = getGenericPath(_path); - - // don't remove if it doesn't exists - if(!exists(path)) - return true; - - // try to remove file - return (unlink(path.c_str()) == 0); - - } // removeFile - - bool createDirectory(const std::string& _path) - { - std::string path = getGenericPath(_path); - - // don't create if it already exists - if(exists(path)) - return true; - - // try to create directory - if(mkdir(path.c_str(), 0755) == 0) - return true; - - // failed to create directory, try to create the parent - std::string parent = getParent(path); - - // only try to create parent if it's not identical to path - if(parent != path) - createDirectory(parent); - - // try to create directory again now that the parent should exist - return (mkdir(path.c_str(), 0755) == 0); - - } // createDirectory - - bool exists(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - return (stat64(path.c_str(), &info) == 0); - - } // exists - - bool isAbsolute(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - return ((path.size() > 1) && (path[1] == ':')); -#else // _WIN32 - return ((path.size() > 0) && (path[0] == '/')); -#endif // _WIN32 - - } // isAbsolute - - bool isRegularFile(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - if(stat64(path.c_str(), &info) != 0) - return false; - - // check for S_IFREG attribute - return (S_ISREG(info.st_mode)); - - } // isRegularFile - - bool isDirectory(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat info; - - // check if stat succeeded - if(stat(path.c_str(), &info) != 0) - return false; - - // check for S_IFDIR attribute - return (S_ISDIR(info.st_mode)); - - } // isDirectory - - bool isSymlink(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // check for symlink attribute - const DWORD Attributes = GetFileAttributes(path.c_str()); - if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT)) - return true; -#else // _WIN32 - struct stat info; - - // check if lstat succeeded - if(lstat(path.c_str(), &info) != 0) - return false; - - // check for S_IFLNK attribute - return (S_ISLNK(info.st_mode)); -#endif // _WIN32 - - // not a symlink - return false; - - } // isSymlink - - bool isHidden(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // check for hidden attribute - const DWORD Attributes = GetFileAttributes(path.c_str()); - if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_HIDDEN)) - return true; -#endif // _WIN32 - - // filenames starting with . are hidden in linux, we do this check for windows as well - if (getFileName(path)[0] == '.') - return true; - - // not hidden - return false; - - } // isHidden - - } // FileSystem:: - -} // Utils:: +#define _FILE_OFFSET_BITS 64 + +#include "utils/FileSystemUtil.h" + +#include "Settings.h" +#include +#include + +#if defined(_WIN32) +// because windows... +#include +#include +#define getcwd _getcwd +#define mkdir(x,y) _mkdir(x) +#define snprintf _snprintf +#define stat64 _stat64 +#define unlink _unlink +#define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) +#define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) +#else // _WIN32 +#include +#include +#endif // _WIN32 + +namespace Utils +{ + namespace FileSystem + { + +#if defined(_WIN32) + static std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + std::string string; + + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return std::string(string); + + } // convertFromWideString +#endif // _WIN32 + + stringList getDirContent(const std::string& _path, const bool _recursive) + { + std::string path = getGenericPath(_path); + stringList contentList; + + // only parse the directory, if it's a directory + if(isDirectory(path)) + { + +#if defined(_WIN32) + WIN32_FIND_DATAW findData; + std::string wildcard = path + "/*"; + HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); + + if(hFind != INVALID_HANDLE_VALUE) + { + // loop over all files in the directory + do + { + std::string name = convertFromWideString(findData.cFileName); + + // ignore "." and ".." + if((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + contentList.push_back(fullName); + + if(_recursive && isDirectory(fullName)) + contentList.merge(getDirContent(fullName, true)); + } + } + while(FindNextFileW(hFind, &findData)); + + FindClose(hFind); + } +#else // _WIN32 + DIR* dir = opendir(path.c_str()); + + if(dir != NULL) + { + struct dirent* entry; + + // loop over all files in the directory + while((entry = readdir(dir)) != NULL) + { + std::string name(entry->d_name); + + // ignore "." and ".." + if((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + contentList.push_back(fullName); + + if(_recursive && isDirectory(fullName)) + contentList.merge(getDirContent(fullName, true)); + } + } + + closedir(dir); + } +#endif // _WIN32 + + } + + // sort the content list + contentList.sort(); + + // return the content list + return contentList; + + } // getDirContent + + stringList getPathList(const std::string& _path) + { + stringList pathList; + std::string path = getGenericPath(_path); + size_t start = 0; + size_t end = 0; + + // split at '/' + while((end = path.find("/", start)) != std::string::npos) + { + if(end != start) + pathList.push_back(std::string(path, start, end - start)); + + start = end + 1; + } + + // add last folder / file to pathList + if(start != path.size()) + pathList.push_back(std::string(path, start, path.size() - start)); + + // return the path list + return pathList; + + } // getPathList + + std::string getHomePath() + { + static std::string path; + + // only construct the homepath once + if(!path.length()) + { + // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows + char* envHome = getenv("HOME"); + +#ifdef _DEBUG + // envHome = "H:/[Emulz]/EmulationStation/"; +#endif + + if (envHome) + path = getGenericPath(envHome); + +#if defined(_WIN32) + // but does not seem to work for Windows XP or Vista, so try something else + if(!path.length()) + { + char* envHomeDrive = getenv("HOMEDRIVE"); + char* envHomePath = getenv("HOMEPATH"); + if(envHomeDrive && envHomePath) + path = getGenericPath(std::string(envHomeDrive) + "/" + envHomePath); + } +#endif // _WIN32 + + // no homepath found, fall back to current working directory + if(!path.length()) + path = getCWDPath(); + } + + // return constructed homepath + return path; + + } // getHomePath + + std::string getCWDPath() + { + char temp[512]; + return (getcwd(temp, 512) ? getGenericPath(temp) : ""); + } // getCWDPath + + std::string getExePath() + { + static std::string path; + + // only construct the exepath once + if (!path.length()) + { +#if defined(_WIN32) + char buffer[MAX_PATH]; + DWORD size = MAX_PATH; + DWORD result = GetModuleFileNameA(NULL, buffer, size); + if (result) + { + std::string ret = buffer; + path = getParent(ret); + return ret; + } +#endif + path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); + if (isRegularFile(path)) + path = getParent(path); + } + + // return constructed exepath + return path; + + } // getExePath + + std::string getPreferredPath(const std::string& _path) + { + std::string path = _path; + size_t offset = std::string::npos; +#if defined(_WIN32) + // convert '/' to '\\' + while((offset = path.find('/')) != std::string::npos) + path.replace(offset, 1, "\\"); +#endif // _WIN32 + return path; + } + + std::string getGenericPath(const std::string& _path) + { + std::string path = _path; + size_t offset = std::string::npos; + + // remove "\\\\?\\" + if((path.find("\\\\?\\")) == 0) + path.erase(0, 4); + + // convert '\\' to '/' + while((offset = path.find('\\')) != std::string::npos) + path.replace(offset, 1 ,"/"); + + // remove double '/' + while((offset = path.find("//")) != std::string::npos) + path.erase(offset, 1); + + // remove trailing '/' + while(path.length() && ((offset = path.find_last_of('/')) == (path.length() - 1))) + path.erase(offset, 1); + + // return generic path + return path; + + } // getGenericPath + + std::string getEscapedPath(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // windows escapes stuff by just putting everything in quotes + return '"' + getPreferredPath(path) + '"'; +#else // _WIN32 + // insert a backslash before most characters that would mess up a bash path + const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; + const char* invalidChar = invalidChars; + + while(*invalidChar) + { + size_t start = 0; + size_t offset = 0; + + while((offset = path.find(*invalidChar, start)) != std::string::npos) + { + start = offset + 1; + + if((offset == 0) || (path[offset - 1] != '\\')) + { + path.insert(offset, 1, '\\'); + ++start; + } + } + + ++invalidChar; + } + + // return escaped path + return path; +#endif // _WIN32 + + } // getEscapedPath + + std::string getCanonicalPath(const std::string& _path) + { + // temporary hack for builtin resources + if((_path[0] == ':') && (_path[1] == '/')) + return _path; + + std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); + + // cleanup path + bool scan = true; + while(scan) + { + stringList pathList = getPathList(path); + + path.clear(); + scan = false; + + for(stringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); ++it) + { + // ignore empty + if((*it).empty()) + continue; + + // remove "/./" + if((*it) == ".") + continue; + + // resolve "/../" + if((*it) == "..") + { + path = getParent(path); + continue; + } + +#if defined(_WIN32) + // append folder to path + path += (path.size() == 0) ? (*it) : ("/" + (*it)); +#else // _WIN32 + // append folder to path + path += ("/" + (*it)); +#endif // _WIN32 + + // resolve symlink + if(isSymlink(path)) + { + std::string resolved = resolveSymlink(path); + + if(resolved.empty()) + return ""; + + if(isAbsolute(resolved)) + path = resolved; + else + path = getParent(path) + "/" + resolved; + + for(++it; it != pathList.cend(); ++it) + path += (path.size() == 0) ? (*it) : ("/" + (*it)); + + scan = true; + break; + } + } + } + + // return canonical path + return path; + + } // getCanonicalPath + + std::string getAbsolutePath(const std::string& _path, const std::string& _base) + { + std::string path = getGenericPath(_path); + std::string base = isAbsolute(_base) ? getGenericPath(_base) : getAbsolutePath(_base); + + // return absolute path + return isAbsolute(path) ? path : getGenericPath(base + "/" + path); + + } // getAbsolutePath + + std::string getParent(const std::string& _path) + { + std::string path = getGenericPath(_path); + size_t offset = std::string::npos; + + // find last '/' and erase it + if((offset = path.find_last_of('/')) != std::string::npos) + return path.erase(offset); + + // no parent found + return path; + + } // getParent + + std::string getFileName(const std::string& _path) + { + std::string path = getGenericPath(_path); + size_t offset = std::string::npos; + + // find last '/' and return the filename + if((offset = path.find_last_of('/')) != std::string::npos) + return ((path[offset + 1] == 0) ? "." : std::string(path, offset + 1)); + + // no '/' found, entire path is a filename + return path; + + } // getFileName + + std::string getStem(const std::string& _path) + { + std::string fileName = getFileName(_path); + size_t offset = std::string::npos; + + // empty fileName + if(fileName == ".") + return fileName; + + // find last '.' and erase the extension + if((offset = fileName.find_last_of('.')) != std::string::npos) + return fileName.erase(offset); + + // no '.' found, filename has no extension + return fileName; + + } // getStem + + std::string getExtension(const std::string& _path) + { + std::string fileName = getFileName(_path); + size_t offset = std::string::npos; + + // empty fileName + if(fileName == ".") + return fileName; + + // find last '.' and return the extension + if((offset = fileName.find_last_of('.')) != std::string::npos) + return std::string(fileName, offset); + + // no '.' found, filename has no extension + return "."; + + } // getExtension + + std::string resolveRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) + { + std::string path = getGenericPath(_path); + std::string relativeTo = isDirectory(_relativeTo) ? getGenericPath(_relativeTo) : getParent(_relativeTo); + + // nothing to resolve + if(!path.length()) + return path; + + // replace '.' with relativeTo + if((path[0] == '.') && (path[1] == '/')) + return (relativeTo + &(path[1])); + + // replace '~' with homePath + if(_allowHome && (path[0] == '~') && (path[1] == '/')) + return (getHomePath() + &(path[1])); + + // nothing to resolve + return path; + + } // resolveRelativePath + + std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) + { + bool contains = false; + std::string path = removeCommonPath(_path, _relativeTo, contains); + + if(contains) + { + // success + return ("./" + path); + } + + if(_allowHome) + { + path = removeCommonPath(_path, getHomePath(), contains); + + if(contains) + { + // success + return ("~/" + path); + } + } + + // nothing to resolve + return path; + + } // createRelativePath + + std::string removeCommonPath(const std::string& _path, const std::string& _common, bool& _contains) + { + std::string path = getGenericPath(_path); + std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); + + // check if path contains common + if(path.find(common) == 0) + { + _contains = true; + return path.substr(common.length() + 1); + } + + // it didn't + _contains = false; + return path; + + } // removeCommonPath + + std::string resolveSymlink(const std::string& _path) + { + std::string path = getGenericPath(_path); + std::string resolved; + +#if defined(_WIN32) + HANDLE hFile = CreateFile(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); + + if(hFile != INVALID_HANDLE_VALUE) + { + resolved.resize(GetFinalPathNameByHandle(hFile, nullptr, 0, FILE_NAME_NORMALIZED) + 1); + if(GetFinalPathNameByHandle(hFile, (LPSTR)resolved.data(), (DWORD)resolved.size(), FILE_NAME_NORMALIZED) > 0) + { + resolved.resize(resolved.size() - 1); + resolved = getGenericPath(resolved); + } + CloseHandle(hFile); + } +#else // _WIN32 + struct stat info; + + // check if lstat succeeded + if(lstat(path.c_str(), &info) == 0) + { + resolved.resize(info.st_size); + if(readlink(path.c_str(), (char*)resolved.data(), resolved.size()) > 0) + resolved = getGenericPath(resolved); + } +#endif // _WIN32 + + // return resolved path + return resolved; + + } // resolveSymlink + + bool removeFile(const std::string& _path) + { + std::string path = getGenericPath(_path); + + // don't remove if it doesn't exists + if(!exists(path)) + return true; + + // try to remove file + return (unlink(path.c_str()) == 0); + + } // removeFile + + bool createDirectory(const std::string& _path) + { + std::string path = getGenericPath(_path); + + // don't create if it already exists + if(exists(path)) + return true; + + // try to create directory + if(mkdir(path.c_str(), 0755) == 0) + return true; + + // failed to create directory, try to create the parent + std::string parent = getParent(path); + + // only try to create parent if it's not identical to path + if(parent != path) + createDirectory(parent); + + // try to create directory again now that the parent should exist + return (mkdir(path.c_str(), 0755) == 0); + + } // createDirectory + + bool exists(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + return (stat64(path.c_str(), &info) == 0); + + } // exists + + bool isAbsolute(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + return ((path.size() > 1) && (path[1] == ':')); +#else // _WIN32 + return ((path.size() > 0) && (path[0] == '/')); +#endif // _WIN32 + + } // isAbsolute + + bool isRegularFile(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + if(stat64(path.c_str(), &info) != 0) + return false; + + // check for S_IFREG attribute + return (S_ISREG(info.st_mode)); + + } // isRegularFile + + bool isDirectory(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat info; + + // check if stat succeeded + if(stat(path.c_str(), &info) != 0) + return false; + + // check for S_IFDIR attribute + return (S_ISDIR(info.st_mode)); + + } // isDirectory + + bool isSymlink(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // check for symlink attribute + const DWORD Attributes = GetFileAttributes(path.c_str()); + if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT)) + return true; +#else // _WIN32 + struct stat info; + + // check if lstat succeeded + if(lstat(path.c_str(), &info) != 0) + return false; + + // check for S_IFLNK attribute + return (S_ISLNK(info.st_mode)); +#endif // _WIN32 + + // not a symlink + return false; + + } // isSymlink + + bool isHidden(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // check for hidden attribute + const DWORD Attributes = GetFileAttributes(path.c_str()); + if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_HIDDEN)) + return true; +#endif // _WIN32 + + // filenames starting with . are hidden in linux, we do this check for windows as well + if (getFileName(path)[0] == '.') + return true; + + // not hidden + return false; + + } // isHidden + + } // FileSystem:: + +} // Utils:: diff --git a/es-core/src/utils/StringUtil.h b/es-core/src/utils/StringUtil.h index 7ebcf0df22..3ffb5c5eac 100644 --- a/es-core/src/utils/StringUtil.h +++ b/es-core/src/utils/StringUtil.h @@ -28,7 +28,7 @@ namespace Utils std::string format (const char* _string, ...); std::string scramble (const std::string& _input, const std::string& key); -#if defined(_WIN32) +#if defined(_WIN32) const std::string convertFromWideString(const std::wstring wstring); const std::wstring convertToWideString(const std::string string); #endif From ab3002a2745e20e78c21bc682dae167e1dc4f464 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 3 Jun 2019 17:02:57 +0200 Subject: [PATCH 008/336] [Misc Optimisations] ( loading, fonts, metadata... ) [Theming] Menu themings [Clipping] Make Clip before OpenGL .... --- es-app/src/FileData.cpp | 6 +- es-app/src/FileData.h | 2 +- es-app/src/FileSorts.cpp | 12 +- es-app/src/MetaData.cpp | 62 ++++-- es-app/src/MetaData.h | 11 ++ es-app/src/SystemData.cpp | 62 +++--- es-app/src/components/RatingComponent.cpp | 5 + .../src/guis/GuiCollectionSystemsOptions.cpp | 14 +- es-app/src/guis/GuiGamelistFilter.cpp | 3 +- es-app/src/guis/GuiGamelistOptions.cpp | 10 +- .../src/guis/GuiGeneralScreensaverOptions.cpp | 4 +- es-app/src/guis/GuiMenu.cpp | 85 +++++--- es-app/src/guis/GuiMetaDataEd.cpp | 27 +-- es-app/src/guis/GuiScraperMulti.cpp | 5 + es-app/src/guis/GuiScraperStart.cpp | 29 +-- es-app/src/guis/GuiSettings.cpp | 11 +- es-app/src/guis/GuiSettings.h | 2 + es-app/src/main.cpp | 69 ++++++- es-app/src/views/SystemView.cpp | 22 ++- es-app/src/views/SystemView.h | 1 + es-app/src/views/ViewController.cpp | 46 +++-- es-app/src/views/gamelist/IGameListView.cpp | 4 + es-core/src/GuiComponent.h | 2 + es-core/src/ImageIO.cpp | 87 ++++++++- es-core/src/ImageIO.h | 2 + es-core/src/PowerSaver.cpp | 1 + es-core/src/Renderer.h | 2 + es-core/src/Renderer_draw_gl.cpp | 51 ++++- es-core/src/Renderer_init_sdlgl.cpp | 27 ++- es-core/src/Settings.cpp | 6 + es-core/src/ThemeData.cpp | 184 +++++++++++++++--- es-core/src/ThemeData.h | 32 ++- es-core/src/Window.cpp | 4 +- es-core/src/components/ButtonComponent.cpp | 14 +- es-core/src/components/ButtonComponent.h | 3 +- es-core/src/components/ComponentGrid.cpp | 5 +- es-core/src/components/ComponentGrid.h | 5 +- es-core/src/components/ComponentList.cpp | 51 +++-- es-core/src/components/GridTileComponent.cpp | 52 +++-- es-core/src/components/GridTileComponent.h | 1 + es-core/src/components/ImageComponent.cpp | 32 ++- es-core/src/components/ImageComponent.h | 4 + es-core/src/components/MenuComponent.cpp | 32 ++- es-core/src/components/MenuComponent.h | 8 +- es-core/src/components/NinePatchComponent.cpp | 13 ++ es-core/src/components/NinePatchComponent.h | 2 + es-core/src/components/OptionListComponent.h | 43 ++-- es-core/src/components/SliderComponent.cpp | 22 ++- es-core/src/components/SliderComponent.h | 3 + es-core/src/components/SwitchComponent.cpp | 8 + es-core/src/components/SwitchComponent.h | 2 + es-core/src/components/TextComponent.cpp | 14 +- es-core/src/components/TextComponent.h | 2 +- es-core/src/components/VideoVlcComponent.cpp | 1 + es-core/src/guis/GuiDetectDevice.cpp | 17 +- es-core/src/guis/GuiInputConfig.cpp | 16 +- es-core/src/guis/GuiMsgBox.cpp | 8 +- es-core/src/guis/GuiTextEditPopup.cpp | 5 + es-core/src/resources/Font.cpp | 77 +++++--- es-core/src/resources/Font.h | 6 +- es-core/src/resources/TextureData.cpp | 22 ++- es-core/src/resources/TextureData.h | 1 + es-core/src/utils/FileSystemUtil.cpp | 123 +++++++++++- es-core/src/utils/FileSystemUtil.h | 14 +- 64 files changed, 1154 insertions(+), 342 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 532ea9a71d..71cdc3352d 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -20,11 +20,10 @@ FileData::FileData(FileType type, const std::string& path, SystemEnvironmentData : mType(type), mPath(path), mSystem(system), mEnvData(envData), mSourceFileData(NULL), 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()) + if (metadata.get("name").empty()) metadata.set("name", getDisplayName()); mSystemName = system->getName(); - // mDefaultCore = system->getSystemEnvData()->mDefaultCore; } FileData::~FileData() @@ -85,7 +84,6 @@ const bool FileData::getFavorite() return metadata.get("favorite") == "true"; } - const bool FileData::getHidden() { return metadata.get("hidden") == "true"; @@ -291,7 +289,7 @@ void FileData::removeChild(FileData* file) } void FileData::sort(ComparisonFunction& comparator, bool ascending) -{ +{ std::stable_sort(mChildren.begin(), mChildren.end(), comparator); for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 00fd4a923b..d8bafed6d2 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -111,7 +111,7 @@ class FileData SystemData* mSystem; std::unordered_map mChildrenByFilename; std::vector mChildren; - std::vector mFilteredChildren; + std::vector mFilteredChildren; }; class CollectionFileData : public FileData diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index 5043656e11..d1237c0a2d 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -41,16 +41,8 @@ namespace FileSorts //returns if file1 should come before file2 bool compareName(const FileData* file1, const FileData* file2) { - // we compare the actual metadata name, as collection files have the system appended which messes up the order - std::string name1 = Utils::String::toUpper(file1->metadata.get("sortname")); - std::string name2 = Utils::String::toUpper(file2->metadata.get("sortname")); - if(name1.empty()){ - name1 = Utils::String::toUpper(file1->metadata.get("name")); - } - if(name2.empty()){ - name2 = Utils::String::toUpper(file2->metadata.get("name")); - } - return name1.compare(name2) < 0; + std::string name1 = file1->metadata.getSortName(); + return name1.compare(file2->metadata.getSortName()) < 0; } bool compareRating(const FileData* file1, const FileData* file2) diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index 3c9900d5a4..c9c1716188 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -1,6 +1,7 @@ #include "MetaData.h" #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" #include "Log.h" #include @@ -51,6 +52,20 @@ MetaDataDecl folderDecls[] = { }; const std::vector folderMDD(folderDecls, folderDecls + sizeof(folderDecls) / sizeof(folderDecls[0])); +std::map MetaDataList::mDefaultGameMap = MetaDataList::BuildDefaultMap(GAME_METADATA); +std::map MetaDataList::mDefaultFolderMap = MetaDataList::BuildDefaultMap(FOLDER_METADATA); + +std::map MetaDataList::BuildDefaultMap(MetaDataListType type) +{ + std::map ret; + + const std::vector& mdd = getMDDByType(type); + for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + ret[iter->key] = iter->defaultValue; + + return ret; +} + const std::vector& getMDDByType(MetaDataListType type) { switch(type) @@ -65,17 +80,11 @@ const std::vector& getMDDByType(MetaDataListType type) return gameMDD; } +MetaDataList::MetaDataList(MetaDataListType type) : mType(type), mWasChanged(false) +{ - -MetaDataList::MetaDataList(MetaDataListType type) - : mType(type), mWasChanged(false) -{ - const std::vector& mdd = getMDD(); - for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) - set(iter->key, iter->defaultValue); } - MetaDataList MetaDataList::createFromXML(MetaDataListType type, pugi::xml_node& node, const std::string& relativeTo) { MetaDataList mdl(type); @@ -85,17 +94,14 @@ MetaDataList MetaDataList::createFromXML(MetaDataListType type, pugi::xml_node& for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) { pugi::xml_node md = node.child(iter->key.c_str()); - if(md) - { - // if it's a path, resolve relative paths + if (md) + { std::string value = md.text().get(); - if (iter->type == MD_PATH) - { + + if (iter->type == MD_PATH) // if it's a path, resolve relative paths value = Utils::FileSystem::resolveRelativePath(value, relativeTo, true); - } + mdl.set(iter->key, value); - }else{ - mdl.set(iter->key, iter->defaultValue); } } @@ -113,7 +119,7 @@ void MetaDataList::appendToXML(pugi::xml_node& parent, bool ignoreDefaults, cons { // we have this value! // if it's just the default (and we ignore defaults), don't write it - if(ignoreDefaults && mapIter->second == mddIter->defaultValue) + if (ignoreDefaults && mapIter->second == mddIter->defaultValue) continue; // try and make paths relative if we can @@ -128,13 +134,23 @@ void MetaDataList::appendToXML(pugi::xml_node& parent, bool ignoreDefaults, cons void MetaDataList::set(const std::string& key, const std::string& value) { + if ((key == "sortname" || key == "name") && !value.empty()) + mSortName = Utils::String::toUpper(value); + mMap[key] = value; mWasChanged = true; } const std::string& MetaDataList::get(const std::string& key) const { - return mMap.at(key); + auto it = mMap.find(key); + if (it != mMap.end()) + return it->second; + + if (mType == GAME_METADATA) + return mDefaultGameMap.at(key); + + return mDefaultFolderMap.at(key); } int MetaDataList::getInt(const std::string& key) const @@ -150,9 +166,15 @@ float MetaDataList::getFloat(const std::string& key) const bool MetaDataList::isDefault() { const std::vector& mdd = getMDD(); + + for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + { + auto it = mMap.find(iter->key); + if (it == mMap.end()) + continue; - for (unsigned int i = 1; i < mMap.size(); i++) { - if (mMap.at(mdd[i].key) != mdd[i].defaultValue) return false; + if (it->second != iter->defaultValue) + return false; } return true; diff --git a/es-app/src/MetaData.h b/es-app/src/MetaData.h index 7205511a2c..c0c01f264d 100644 --- a/es-app/src/MetaData.h +++ b/es-app/src/MetaData.h @@ -64,10 +64,21 @@ class MetaDataList inline MetaDataListType getType() const { return mType; } inline const std::vector& getMDD() const { return getMDDByType(getType()); } + const std::string getSortName() const { + return mSortName; + } + private: MetaDataListType mType; std::map mMap; bool mWasChanged; + + static std::map mDefaultGameMap; + static std::map mDefaultFolderMap; + + static std::map BuildDefaultMap(MetaDataListType type); + + std::string mSortName; }; #endif // ES_APP_META_DATA_H diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index adae7852e0..b2cd549059 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -51,6 +51,7 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys // virtual systems are updated afterwards, we're just creating the data structure mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this); } + setIsGameSystemStatus(); loadTheme(); } @@ -84,11 +85,6 @@ void SystemData::populateFolder(FileData* folder) return; } - int di = folderPath.rfind("downloaded_images"); - int md = folderPath.rfind("media"); - if (di > 0 || md > 0) - return; - //make sure that this isn't a symlink to a thing we already have if(Utils::FileSystem::isSymlink(folderPath)) { @@ -100,34 +96,36 @@ void SystemData::populateFolder(FileData* folder) } } - std::string filePath; +// std::string filePath; std::string extension; bool isGame; bool showHidden = Settings::getInstance()->getBool("ShowHiddenFiles"); - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(folderPath); - for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + + Utils::FileSystem::fileList dirContent = Utils::FileSystem::getDirInfo(folderPath, false); + for(Utils::FileSystem::fileList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) { - filePath = *it; + auto fileInfo = *it; + //filePath = *it; // skip hidden files and folders - if(!showHidden && Utils::FileSystem::isHidden(filePath)) + if(!showHidden && fileInfo.readOnly) 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 = Utils::FileSystem::getExtension(filePath); + extension = Utils::FileSystem::getExtension(fileInfo.path); std::transform(extension.begin(), extension.end(), extension.begin(), ::_easytolower); //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(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) { - FileData* newGame = new FileData(GAME, filePath, mEnvData, this); + FileData* newGame = new FileData(GAME, fileInfo.path, mEnvData, this); // preventing new arcade assets to be added - if(!newGame->isArcadeAsset()) + if (extension != ".zip" || !newGame->isArcadeAsset()) { folder->addChild(newGame); isGame = true; @@ -135,21 +133,20 @@ void SystemData::populateFolder(FileData* folder) } //add directories that also do not match an extension as folders - if(!isGame && Utils::FileSystem::isDirectory(filePath)) + if (!isGame && fileInfo.directory) { - if (filePath.rfind("downloaded_images") == std::string::npos && - filePath.rfind("media") == std::string::npos) - { - FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); - populateFolder(newFolder); - - if (newFolder->getChildrenByFilename().size() == 0) - delete newFolder; - else if (newFolder->findUniqueGameForFolder() != NULL) - delete newFolder; - else - folder->addChild(newFolder); - } + if (fileInfo.path.rfind("downloaded_images") != std::string::npos || fileInfo.path.rfind("media") != std::string::npos) + continue; + + FileData* newFolder = new FileData(FOLDER, fileInfo.path, mEnvData, this); + populateFolder(newFolder); + + if (newFolder->getChildrenByFilename().size() == 0) + delete newFolder; + else if (newFolder->findUniqueGameForFolder() != NULL) + delete newFolder; + else + folder->addChild(newFolder); } } } @@ -229,8 +226,6 @@ bool SystemData::loadConfig(Window* window) { std::vector emulatorList; - // std::vector coreList; - std::string name, fullname, path, cmd, themeFolder, defaultCore; name = system.child("name").text().get(); @@ -258,7 +253,6 @@ bool SystemData::loadConfig(Window* window) defaultCore = corename; emulatorData.mCores.push_back(corename); - // coreList.push_back(corename); } } @@ -337,11 +331,8 @@ bool SystemData::loadConfig(Window* window) envData->mSearchExtensions = extensions; envData->mLaunchCommand = cmd; envData->mPlatformIds = platformIds; - // envData->mDefaultCore = defaultCore; envData->mEmulators = emulatorList; - // envData->mCores = coreList; - SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) { @@ -572,8 +563,7 @@ void SystemData::loadTheme() mTheme = std::make_shared(); std::string path = getThemePath(); - - if(!Utils::FileSystem::exists(path)) // no theme available for this platform + if (!Utils::FileSystem::exists(path)) // no theme available for this platform return; try diff --git a/es-app/src/components/RatingComponent.cpp b/es-app/src/components/RatingComponent.cpp index 924a0cbccf..05063694ab 100644 --- a/es-app/src/components/RatingComponent.cpp +++ b/es-app/src/components/RatingComponent.cpp @@ -114,6 +114,11 @@ void RatingComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = parentTrans * getTransform(); trans.round(); + + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + Renderer::setMatrix(trans); glEnable(GL_TEXTURE_2D); diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index be30460aa4..c47c7e7d5e 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -27,7 +27,7 @@ void GuiCollectionSystemsOptions::initializeMenu() std::vector unusedFolders = CollectionSystemManager::get()->getUnusedSystemsFromTheme(); if (unusedFolders.size() > 0) { - addEntry(_T("CREATE NEW CUSTOM COLLECTION FROM THEME"), 0x777777FF, true, + addEntry(_T("CREATE NEW CUSTOM COLLECTION FROM THEME"), ThemeData::getMenuTheme()->Text.color, true, [this, unusedFolders] { auto s = new GuiSettings(mWindow, _T("SELECT THEME FOLDER")); std::shared_ptr< OptionListComponent > folderThemes = std::make_shared< OptionListComponent >(mWindow, "SELECT THEME FOLDER", true); @@ -47,12 +47,14 @@ void GuiCollectionSystemsOptions::initializeMenu() row.addElement(themeFolder, true); s->addRow(row); } + + s->updatePosition(); mWindow->pushGui(s); }); } ComponentListRow row; - row.addElement(std::make_shared(mWindow, _T("CREATE NEW CUSTOM COLLECTION"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("CREATE NEW CUSTOM COLLECTION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); auto createCustomCollection = [this](const std::string& newVal) { std::string name = newVal; // we need to store the first Gui and remove it, as it'll be deleted by the actual Gui @@ -85,16 +87,18 @@ void GuiCollectionSystemsOptions::initializeMenu() mMenu.addButton(_T("BACK"), _T("BACK"), std::bind(&GuiCollectionSystemsOptions::applySettings, this)); - mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); + mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); } void GuiCollectionSystemsOptions::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) { - std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); + auto theme = ThemeData::getMenuTheme(); + + std::shared_ptr font = theme->Text.font; // Font::get(FONT_SIZE_MEDIUM); // populate the list ComponentListRow row; - row.addElement(std::make_shared(mWindow, name, font, color), true); + row.addElement(std::make_shared(mWindow, name, font, theme->Text.color), true); if(add_arrow) { diff --git a/es-app/src/guis/GuiGamelistFilter.cpp b/es-app/src/guis/GuiGamelistFilter.cpp index 1216775439..ffdc11bc43 100644 --- a/es-app/src/guis/GuiGamelistFilter.cpp +++ b/es-app/src/guis/GuiGamelistFilter.cpp @@ -19,9 +19,10 @@ void GuiGamelistFilter::initializeMenu() ComponentListRow row; + // show filtered menu row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("RESET ALL FILTERS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("RESET ALL FILTERS"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistFilter::resetAllFilters, this)); mMenu.addRow(row); row.elements.clear(); diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 01c0741c21..dcb16a4741 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -52,7 +52,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui } } - row.addElement(std::make_shared(mWindow, _T("JUMP TO LETTER"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("JUMP TO LETTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(mJumpToLetterList, false); row.input_handler = [&](InputConfig* config, Input input) { @@ -83,7 +83,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if(!Settings::getInstance()->getBool("ForceDisableFilters")) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("APPLY FILTER"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("APPLY FILTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); mMenu.addRow(row); @@ -96,7 +96,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui CollectionSystemManager::get()->getCustomCollectionsBundle()->getName() == system->getName())) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "ADD/REMOVE GAMES TO THIS GAME COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, "ADD/REMOVE GAMES TO THIS GAME COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::startEditMode, this)); mMenu.addRow(row); } @@ -104,7 +104,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if(UIModeController::getInstance()->isUIModeFull() && CollectionSystemManager::get()->isEditing()) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::exitEditMode, this)); mMenu.addRow(row); } @@ -112,7 +112,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("EDIT THIS GAME'S METADATA"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("EDIT THIS GAME'S METADATA"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openMetaDataEd, this)); mMenu.addRow(row); diff --git a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp index e229aa87c6..e5c0c84409 100644 --- a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp +++ b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp @@ -50,13 +50,13 @@ GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, std:: // show filtered menu row.elements.clear(); - row.addElement(std::make_shared(mWindow, "VIDEO SCREENSAVER SETTINGS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, "VIDEO SCREENSAVER SETTINGS", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGeneralScreensaverOptions::openVideoScreensaverOptions, this)); addRow(row); row.elements.clear(); - row.addElement(std::make_shared(mWindow, "SLIDESHOW SCREENSAVER SETTINGS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, "SLIDESHOW SCREENSAVER SETTINGS", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGeneralScreensaverOptions::openSlideshowScreensaverOptions, this)); addRow(row); diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index a383ee2ff4..8173bc36c1 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -22,31 +22,32 @@ #include "animations/LambdaAnimation.h" GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) - { - bool isFullUI = UIModeController::getInstance()->isUIModeFull(); + auto theme = ThemeData::getMenuTheme(); + + bool isFullUI = UIModeController::getInstance()->isUIModeFull(); if (isFullUI) { - addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); - addEntry(_T("CONFIGURE INPUT"), 0x777777FF, true, [this] { openConfigInput(); }); + addEntry(_T("UI SETTINGS"), theme->Text.color, true, [this] { openUISettings(); }); + addEntry(_T("CONFIGURE INPUT"), theme->Text.color, true, [this] { openConfigInput(); }); } - addEntry(_T("SOUND SETTINGS"), 0x777777FF, true, [this] { openSoundSettings(); }); + addEntry(_T("SOUND SETTINGS"), theme->Text.color, true, [this] { openSoundSettings(); }); if (isFullUI) - addEntry(_T("SCRAPER"), 0x777777FF, true, [this] { openScraperSettings(); }); + addEntry(_T("SCRAPER"), theme->Text.color, true, [this] { openScraperSettings(); }); if (isFullUI) { - addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); - addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); + addEntry(_T("GAME COLLECTION SETTINGS"), theme->Text.color, true, [this] { openCollectionSystemSettings(); }); + addEntry(_T("ADVANCED SETTINGS"), theme->Text.color, true, [this] { openOtherSettings(); }); } #if defined(_WIN32) - addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); + addEntry(_T("QUIT"), theme->Text.color, false, [this] {openQuitMenu(); }); #else - addEntry(_T("QUIT"), 0x777777FF, true, [this] {openQuitMenu(); }); + addEntry(_T("QUIT"), theme->Text.color, true, [this] {openQuitMenu(); }); #endif addChild(&mMenu); @@ -78,23 +79,23 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN void GuiMenu::openScraperSettings() { - auto s = new GuiSettings(mWindow, "SCRAPER"); + auto s = new GuiSettings(mWindow, _T("SCRAPER")); // scrape from - auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, "SCRAPE FROM", false); + auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, _T("SCRAPE FROM"), false); std::vector scrapers = getScraperList(); // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. for(auto it = scrapers.cbegin(); it != scrapers.cend(); it++) scraper_list->add(*it, *it, *it == Settings::getInstance()->getString("Scraper")); - s->addWithLabel("SCRAPE FROM", scraper_list); + s->addWithLabel(_T("SCRAPE FROM"), scraper_list); s->addSaveFunc([scraper_list] { Settings::getInstance()->setString("Scraper", scraper_list->getSelected()); }); // scrape ratings auto scrape_ratings = std::make_shared(mWindow); scrape_ratings->setState(Settings::getInstance()->getBool("ScrapeRatings")); - s->addWithLabel("SCRAPE RATINGS", scrape_ratings); + s->addWithLabel(_T("SCRAPE RATINGS"), scrape_ratings); s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); }); // scrape now @@ -104,12 +105,13 @@ void GuiMenu::openScraperSettings() openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; row.makeAcceptInputHandler(openAndSave); - auto scrape_now = std::make_shared(mWindow, "SCRAPE NOW", Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + auto scrape_now = std::make_shared(mWindow, _T("SCRAPE NOW"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); auto bracket = makeArrow(mWindow); row.addElement(scrape_now, true); row.addElement(bracket, false); s->addRow(row); + s->updatePosition(); mWindow->pushGui(s); } @@ -223,6 +225,7 @@ void GuiMenu::openSoundSettings() #endif } + s->updatePosition(); mWindow->pushGui(s); } @@ -232,6 +235,7 @@ void GuiMenu::openUISettings() auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); // theme set + auto theme = ThemeData::getMenuTheme(); auto themeSets = ThemeData::getThemeSets(); if (!themeSets.empty()) @@ -421,7 +425,7 @@ void GuiMenu::openUISettings() // screensaver ComponentListRow screensaver_row; screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); screensaver_row.addElement(makeArrow(mWindow), false); screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); s->addRow(screensaver_row); @@ -471,6 +475,7 @@ void GuiMenu::openUISettings() if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); }); + s->updatePosition(); mWindow->pushGui(s); } @@ -548,14 +553,41 @@ void GuiMenu::openOtherSettings() #endif +#if defined(_WIN32) + // vsync + auto vsync = std::make_shared(mWindow); + vsync->setState(Settings::getInstance()->getBool("VSync")); + s->addWithLabel(_T("VSYNC"), vsync); + s->addSaveFunc([vsync] + { + Settings::getInstance()->setBool("VSync", vsync->getState()); + + // vsync + if (Settings::getInstance()->getBool("VSync")) + { + // SDL_GL_SetSwapInterval(0) for immediate updates (no vsync, default), + // 1 for updates synchronized with the vertical retrace, + // or -1 for late swap tearing. + // SDL_GL_SetSwapInterval returns 0 on success, -1 on error. + // if vsync is requested, try normal vsync; if that doesn't work, try late swap tearing + // if that doesn't work, report an error + if (SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) + LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; + } + else + SDL_GL_SetSwapInterval(0); + }); +#endif + // framerate - /* + auto framerate = std::make_shared(mWindow); framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); s->addWithLabel(_T("SHOW FRAMERATE"), framerate); s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); - */ + + s->updatePosition(); mWindow->pushGui(s); } @@ -639,6 +671,7 @@ void GuiMenu::openQuitMenu() row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); s->addRow(row); + s->updatePosition(); mWindow->pushGui(s); } @@ -646,8 +679,13 @@ void GuiMenu::addVersionInfo() { std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); - mVersion.setFont(Font::get(FONT_SIZE_SMALL)); - mVersion.setColor(0x5E5E5EFF); + auto theme = ThemeData::getMenuTheme(); +// mVersion.setFont(Font::get(FONT_SIZE_SMALL)); +// mVersion.setColor(0x5E5E5EFF); + + mVersion.setFont(theme->Footer.font); + mVersion.setColor(theme->Footer.color); + mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); mVersion.setHorizontalAlignment(ALIGN_CENTER); addChild(&mVersion); @@ -669,20 +707,21 @@ void GuiMenu::onSizeChanged() void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) { - std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); + auto theme = ThemeData::getMenuTheme(); + std::shared_ptr font = theme->Text.font; + color = theme->Text.color; // populate the list ComponentListRow row; row.addElement(std::make_shared(mWindow, name, font, color), true); - if(add_arrow) + if (add_arrow) { std::shared_ptr bracket = makeArrow(mWindow); row.addElement(bracket, false); } row.makeAcceptInputHandler(func); - mMenu.addRow(row); } diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 5f0df4f525..ee9abc7a1f 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -30,14 +30,19 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector mMetaData(md), mSavedCallback(saveCallback), mDeleteFunc(deleteFunc) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + addChild(&mBackground); addChild(&mGrid); mHeaderGrid = std::make_shared(mWindow, Vector2i(1, 5)); - mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); + mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), theme->Title.font, theme->Title.color, ALIGN_CENTER); mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), - Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER); + theme->TextSmall.font, theme->TextSmall.color, ALIGN_CENTER); mHeaderGrid->setEntry(mTitle, Vector2i(0, 1), false, true); mHeaderGrid->setEntry(mSubtitle, Vector2i(0, 3), false, true); @@ -60,11 +65,13 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector // don't add statistics if(iter->isStatistic) continue; - + +#if defined(_WIN32) if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") continue; +#endif // create ed and add it (and any related components) to mMenu // ed's value will be set below @@ -72,22 +79,18 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector if (iter->displayName == "emulator") { - // if (file->getSystemEnvData()->mEmulators.size() <= 1) - // continue; - std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); - std::string currentEmul = file->getEmulator(); if (defaultEmul.length() == 0) - emul_choice->add(_T("DEFAULT"), "", false); + emul_choice->add(_T("DEFAULT"), "", true); else emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); for (auto core : file->getSystemEnvData()->mEmulators) emul_choice->add(core.mName, core.mName, core.mName == currentEmul); - row.addElement(std::make_shared(mWindow, _T("EMULATOR"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("EMULATOR"), theme->Text.font, theme->Text.color), true); row.addElement(emul_choice, false); mList->addRow(row); @@ -134,7 +137,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector // core_choice->add(_T("DEFAULT"), "", true); core_choice->setTag(iter->key); - row.addElement(std::make_shared(mWindow, "CORE", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, "CORE", theme->Text.font, theme->Text.color), true); row.addElement(core_choice, false); mList->addRow(row); @@ -147,7 +150,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector continue; } - auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), theme->Text.font, theme->Text.color); row.addElement(lbl, true); // label switch (iter->type) @@ -198,7 +201,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector default: { // MD_STRING - ed = std::make_shared(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); + ed = std::make_shared(window, "", theme->Text.font, theme->Text.color, ALIGN_RIGHT); row.addElement(ed, true); auto spacer = std::make_shared(mWindow); diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 7dcd2d16bd..210cac7ae6 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -15,6 +15,11 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queueBackground.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + assert(mSearchQueue.size()); addChild(&mBackground); diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index 2330f095f6..6762fea19a 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -9,32 +9,32 @@ #include "SystemData.h" GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), - mMenu(window, "SCRAPE NOW") + mMenu(window, _T("SCRAPE NOW")) { addChild(&mMenu); // add filters (with first one selected) - mFilters = std::make_shared< OptionListComponent >(mWindow, "SCRAPE THESE GAMES", false); - mFilters->add("All Games", + mFilters = std::make_shared< OptionListComponent >(mWindow, _T("SCRAPE THESE GAMES"), false); + mFilters->add(_T("All Games"), [](SystemData*, FileData*) -> bool { return true; }, false); - mFilters->add("Only missing image", + mFilters->add(_T("Only missing image"), [](SystemData*, FileData* g) -> bool { return g->metadata.get("image").empty(); }, true); - mMenu.addWithLabel("Filter", mFilters); + mMenu.addWithLabel(_T("FILTER"), mFilters); //add systems (all with a platformid specified selected) - mSystems = std::make_shared< OptionListComponent >(mWindow, "SCRAPE THESE SYSTEMS", true); + mSystems = std::make_shared< OptionListComponent >(mWindow, _T("SCRAPE THESE SYSTEMS"), true); for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { if(!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) mSystems->add((*it)->getFullName(), *it, !(*it)->getPlatformIds().empty()); } - mMenu.addWithLabel("Systems", mSystems); + mMenu.addWithLabel(_T("SYSTEMS"), mSystems); mApproveResults = std::make_shared(mWindow); mApproveResults->setState(true); - mMenu.addWithLabel("User decides on conflicts", mApproveResults); + mMenu.addWithLabel(_T("USER DECIDES ON CONFLICTS"), mApproveResults); - mMenu.addButton("START", "LANCER", std::bind(&GuiScraperStart::pressedStart, this)); + mMenu.addButton(_T("START"), _T("START"), std::bind(&GuiScraperStart::pressedStart, this)); mMenu.addButton(_T("BACK"), _T("BACK"), [&] { delete this; }); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); @@ -49,8 +49,8 @@ void GuiScraperStart::pressedStart() { mWindow->pushGui(new GuiMsgBox(mWindow, Utils::String::toUpper("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)); + _T("YES"), std::bind(&GuiScraperStart::start, this), + _T("NO"), nullptr)); return; } } @@ -64,9 +64,10 @@ void GuiScraperStart::start() if(searches.empty()) { - mWindow->pushGui(new GuiMsgBox(mWindow, - "NO GAMES FIT THAT CRITERIA.")); - }else{ + mWindow->pushGui(new GuiMsgBox(mWindow, _T("NO GAMES FIT THAT CRITERIA."))); + } + else + { GuiScraperMulti* gsm = new GuiScraperMulti(mWindow, searches, mApproveResults->getState()); mWindow->pushGui(gsm); delete this; diff --git a/es-app/src/guis/GuiSettings.cpp b/es-app/src/guis/GuiSettings.cpp index f0d3dd697a..0ffc6f65af 100644 --- a/es-app/src/guis/GuiSettings.cpp +++ b/es-app/src/guis/GuiSettings.cpp @@ -8,11 +8,8 @@ GuiSettings::GuiSettings(Window* window, std::string title) : GuiComponent(window), mMenu(window, title) { addChild(&mMenu); - mMenu.addButton(_T("BACK"), _T("BACK"), [this] { delete this; }); - - setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); + updatePosition(); } GuiSettings::~GuiSettings() @@ -20,6 +17,12 @@ GuiSettings::~GuiSettings() save(); } +void GuiSettings::updatePosition() +{ + setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); +} + void GuiSettings::save() { if(!mSaveFuncs.size()) diff --git a/es-app/src/guis/GuiSettings.h b/es-app/src/guis/GuiSettings.h index 05ece518f2..ac9a22d084 100644 --- a/es-app/src/guis/GuiSettings.h +++ b/es-app/src/guis/GuiSettings.h @@ -11,6 +11,8 @@ class GuiSettings : public GuiComponent GuiSettings(Window* window, std::string title); virtual ~GuiSettings(); // just calls save(); + + void updatePosition(); void save(); inline void addRow(const ComponentListRow& row) { mMenu.addRow(row); }; inline void addWithLabel(const std::string& label, const std::shared_ptr& comp) { mMenu.addWithLabel(label, comp); }; diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 2a08c2c583..2f42e183f5 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -133,9 +133,23 @@ bool parseArgs(int argc, char* argv[]) } else if (strcmp(argv[i], "--vsync") == 0 || strcmp(argv[i], "-vsync") == 0) { - bool vsync = strlen(argv[i]) <= 7 ? true : (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "1") == 0) ? true : false; + bool vsync = false; + if (i == argc - 1) + vsync = true; + else + { + std::string arg = argv[i + 1]; + if (arg.find("-") == 0) + vsync = true; + else + { + vsync = (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "1") == 0) ? true : false; + i++; // skip vsync value + } + } + Settings::getInstance()->setBool("VSync", vsync); - i++; // skip vsync value + } else if (strcmp(argv[i], "--scrape") == 0) { @@ -390,6 +404,22 @@ int main(int argc, char* argv[]) window.endRenderLoadingScreen(); +#ifdef WIN32 + DWORD displayFrequency = 60; + + DEVMODE lpDevMode; + memset(&lpDevMode, 0, sizeof(DEVMODE)); + lpDevMode.dmSize = sizeof(DEVMODE); + lpDevMode.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFLAGS | DM_DISPLAYFREQUENCY; + lpDevMode.dmDriverExtra = 0; + + if (EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &lpDevMode) != 0) { + displayFrequency = lpDevMode.dmDisplayFrequency; // default value if cannot retrieve from user settings. + } + + int timeLimit = (1000 / displayFrequency) - 5; // Margin for vsync +#endif + int lastTime = SDL_GetTicks(); int ps_time = SDL_GetTicks(); @@ -397,10 +427,12 @@ int main(int argc, char* argv[]) while(running) { + int processStart = SDL_GetTicks(); + SDL_Event event; bool ps_standby = PowerSaver::getState() && (int) SDL_GetTicks() - ps_time > PowerSaver::getMode(); - if(ps_standby ? SDL_WaitEventTimeout(&event, PowerSaver::getTimeout()) : SDL_PollEvent(&event)) + if (ps_standby ? SDL_WaitEventTimeout(&event, PowerSaver::getTimeout()) : SDL_PollEvent(&event)) { do { @@ -423,13 +455,13 @@ int main(int argc, char* argv[]) { // If exitting SDL_WaitEventTimeout due to timeout. Trail considering // timeout as an event - ps_time = SDL_GetTicks(); + ps_time = SDL_GetTicks(); } - if(window.isSleeping()) + 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 + SDL_Delay(10); // this doesn't need to be accurate, we're just giving up our CPU time until something wakes us up continue; } @@ -438,14 +470,33 @@ int main(int argc, char* argv[]) lastTime = curTime; // cap deltaTime if it ever goes negative - if(deltaTime < 0) + if (deltaTime < 0) deltaTime = 1000; window.update(deltaTime); window.render(); - Renderer::swapBuffers(); - + Log::flush(); + + int processDuration = SDL_GetTicks() - processStart; + +#ifdef WIN32 + if (processDuration < timeLimit) + Sleep(timeLimit - processDuration); + + int swapStart = SDL_GetTicks(); +#endif + + Renderer::swapBuffers(); +/* +#ifdef WIN32 + int swapDuration = SDL_GetTicks() - swapStart; + + char buffer[100]; + sprintf_s(buffer, "px=%d swap=%d, sleep=%d\n", processDuration, swapDuration, timeLimit - processDuration); + OutputDebugStringA(buffer); +#endif +*/ } while(window.peekGui() != ViewController::get()) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 41bc9fddfe..805b4f8cd2 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -234,7 +234,6 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) 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); @@ -270,8 +269,12 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) mSystemInfo.setOpacity((unsigned char)(Math::lerp(0.f, 1.f, t) * 255)); }, goFast ? 10 : 300); +// ViewController::get()->getGameListView(mEntries.at(mCursor).object); // fake preload + // wait 600ms to fade in - setAnimation(infoFadeIn, goFast ? 0 : 500, nullptr, false, 2); + setAnimation(infoFadeIn, goFast ? 0 : 500, [this] { + ViewController::get()->getGameListView(mEntries.at(mCursor).object); // fake preload + }, false, 2); // no need to animate transition, we're not going anywhere (probably mEntries.size() == 1) if(endPos == mCamOffset && endPos == mExtrasCamOffset) @@ -349,6 +352,10 @@ void SystemView::render(const Transform4x4f& parentTrans) Transform4x4f trans = getTransform() * parentTrans; + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + auto systemInfoZIndex = mSystemInfo.getZIndex(); auto minMax = std::minmax(mCarousel.zIndex, systemInfoZIndex); @@ -488,6 +495,12 @@ void SystemView::renderCarousel(const Transform4x4f& trans) break; } + if (mCarousel.logoPos.x() >= 0) + xOff = mCarousel.logoPos.x() - (mCarousel.type == HORIZONTAL ? (mCamOffset * logoSpacing[0]) : 0); + + if (mCarousel.logoPos.y() >= 0) + yOff = mCarousel.logoPos.y() - (mCarousel.type == VERTICAL ? (mCamOffset * logoSpacing[1]) : 0); + int center = (int)(mCamOffset); int logoCount = Math::min(mCarousel.maxLogoCount, (int)mEntries.size()); @@ -495,7 +508,7 @@ void SystemView::renderCarousel(const Transform4x4f& trans) int bufferIndex = getScrollingVelocity() + 1; int bufferLeft = logoBuffersLeft[bufferIndex]; int bufferRight = logoBuffersRight[bufferIndex]; - if (logoCount == 1) + if (logoCount == 1 && mCamOffset == 0) { bufferLeft = 0; bufferRight = 0; @@ -610,6 +623,7 @@ void SystemView::getDefaultElements(void) mCarousel.logoRotationOrigin.y() = 0.5; mCarousel.logoSize.x() = 0.25f * mSize.x(); mCarousel.logoSize.y() = 0.155f * mSize.y(); + mCarousel.logoPos = Vector2f(-1, -1); mCarousel.maxLogoCount = 3; mCarousel.zIndex = 40; @@ -649,6 +663,8 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) mCarousel.logoScale = elem->get("logoScale"); if (elem->has("logoSize")) mCarousel.logoSize = elem->get("logoSize") * mSize; + if (elem->has("logoPos")) + mCarousel.logoPos = elem->get("logoPos") * mSize; if (elem->has("maxLogoCount")) mCarousel.maxLogoCount = (int)Math::round(elem->get("maxLogoCount")); if (elem->has("zIndex")) diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index db1205424a..4de89cc918 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -38,6 +38,7 @@ struct SystemViewCarousel unsigned int color; int maxLogoCount; // number of logos shown on the carousel Vector2f logoSize; + Vector2f logoPos; float zIndex; }; diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 37c61dbbab..15fd7eb10a 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -134,14 +134,12 @@ void ViewController::goToGameList(SystemData* system) mState.system = system; if (mCurrentView) - { mCurrentView->onHide(); - } + mCurrentView = getGameListView(system); if (mCurrentView) - { - mCurrentView->onShow(); - } + mCurrentView->onShow(); + playViewTransition(false); } @@ -156,7 +154,7 @@ void ViewController::playViewTransition(bool forceImmediate) return; std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); - if(!forceImmediate && transition_style == "fade") + if (!forceImmediate && transition_style == "fade") { // fade // stop whatever's currently playing, leaving mFadeOpacity wherever it is @@ -175,20 +173,24 @@ void ViewController::playViewTransition(bool forceImmediate) }); // fast-forward animation if we're partway faded - if(target == -mCamera.translation()) + 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 if (!forceImmediate && transition_style == "slide"){ + else + advanceAnimation(0, (int)(mFadeOpacity * FADE_DURATION)); + } + else if (!forceImmediate && transition_style == "slide") + { // slide or simple slide setAnimation(new MoveCameraAnimation(mCamera, target)); updateHelpPrompts(); // update help prompts immediately - } else { + } + else + { // instant setAnimation(new LambdaAnimation( [this, target](float /*t*/) @@ -450,9 +452,15 @@ void ViewController::render(const Transform4x4f& parentTrans) // Keep track of UI mode changes. UIModeController::getInstance()->monitorUIMode(); - // draw systemview - getSystemListView()->render(trans); + // clipping + Vector3f sysStart = getSystemListView()->getPosition(); + Vector3f sysEnd = getSystemListView()->getPosition() + Vector3f(getSystemListView()->getSize().x(), getSystemListView()->getSize().y(), 0); + // draw systemview +// if ((sysStart.x() == viewStart.x() && sysStart.y() < viewStart.y() && sysEnd.x() > viewEnd.x() && sysEnd.y() > viewEnd.y()) || +// (sysStart.x() < viewEnd.x() && sysStart.y() < viewEnd.y() && sysEnd.x() > viewStart.x() && sysEnd.y() > viewStart.y())) + getSystemListView()->render(trans); + // draw gamelists for(auto it = mGameListViews.cbegin(); it != mGameListViews.cend(); it++) { @@ -460,8 +468,8 @@ void ViewController::render(const Transform4x4f& parentTrans) Vector3f guiStart = it->second->getPosition(); Vector3f guiEnd = it->second->getPosition() + 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()) +// if ((guiStart.x() == viewStart.x() && guiStart.y() < viewStart.y() && guiEnd.x() > viewEnd.x() && guiEnd.y() > viewEnd.y()) || +// (guiStart.x() < viewEnd.x() && guiStart.y() < viewEnd.y() && guiEnd.x() > viewStart.x() && guiEnd.y() > viewStart.y())) it->second->render(trans); } @@ -486,14 +494,10 @@ void ViewController::preload() uint32_t i = 0; for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { - if(Settings::getInstance()->getBool("SplashScreen") && - Settings::getInstance()->getBool("SplashScreenProgress")) + if (Settings::getInstance()->getBool("SplashScreen") && Settings::getInstance()->getBool("SplashScreenProgress")) { i++; - char buffer[100]; - sprintf (buffer, "Chargement de '%s' (%d/%d)", - (*it)->getFullName().c_str(), i, (int)SystemData::sSystemVector.size()); - mWindow->renderLoadingScreen(std::string(buffer)); + mWindow->renderLoadingScreen(_T("Preloading UI"), (float) i / (float) SystemData::sSystemVector.size()); } (*it)->getIndex()->resetFilters(); diff --git a/es-app/src/views/gamelist/IGameListView.cpp b/es-app/src/views/gamelist/IGameListView.cpp index 5fd5dbf076..722eeabcdd 100644 --- a/es-app/src/views/gamelist/IGameListView.cpp +++ b/es-app/src/views/gamelist/IGameListView.cpp @@ -44,6 +44,10 @@ void IGameListView::render(const Transform4x4f& parentTrans) { Transform4x4f trans = parentTrans * getTransform(); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + float scaleX = trans.r0().x(); float scaleY = trans.r1().y(); diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 4712ce32f5..31774a179e 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -65,6 +65,8 @@ class GuiComponent void setSize(float w, float h); virtual void onSizeChanged() {}; + virtual void setColor(unsigned int color) {}; + virtual Vector2f getRotationSize() const { return getSize(); }; float getRotation() const; diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index 3285308cd2..f21e6f8f11 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -10,7 +10,8 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d width = 0; height = 0; FIMEMORY * fiMemory = FreeImage_OpenMemory((BYTE *)data, (DWORD)size); - if (fiMemory != nullptr) { + if (fiMemory != nullptr) + { //detect the filetype from data FREE_IMAGE_FORMAT format = FreeImage_GetFileTypeFromMemory(fiMemory); if (format != FIF_UNKNOWN && FreeImage_FIFSupportsReading(format)) @@ -31,21 +32,26 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d } } if (fiBitmap != nullptr) - { + { width = FreeImage_GetWidth(fiBitmap); height = FreeImage_GetHeight(fiBitmap); //loop through scanlines and add all pixel data to the return vector //this is necessary, because width*height*bpp might not be == pitch + unsigned char * tempData = new unsigned char[width * height * 4]; + unsigned char * bytes = FreeImage_GetBits(fiBitmap); + + // memcpy(tempData, bytes, width * height * 4); +/* for (size_t i = 0; i < height; i++) { const BYTE * scanLine = FreeImage_GetScanLine(fiBitmap, (int)i); memcpy(tempData + (i * width * 4), scanLine, width * 4); - } + }*/ //convert from BGRA to RGBA for(size_t i = 0; i < width*height; i++) { - RGBQUAD bgra = ((RGBQUAD *)tempData)[i]; + RGBQUAD bgra = ((RGBQUAD *)bytes)[i]; RGBQUAD rgba; rgba.rgbBlue = bgra.rgbRed; rgba.rgbGreen = bgra.rgbGreen; @@ -53,6 +59,7 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d rgba.rgbReserved = bgra.rgbReserved; ((RGBQUAD *)tempData)[i] = rgba; } + rawData = std::vector(tempData, tempData + width * height * 4); //free bitmap data FreeImage_Unload(fiBitmap); @@ -74,6 +81,78 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d return rawData; } +unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height) +{ + width = 0; + height = 0; + + FIMEMORY * fiMemory = FreeImage_OpenMemory((BYTE *)data, (DWORD)size); + if (fiMemory != nullptr) + { + //detect the filetype from data + FREE_IMAGE_FORMAT format = FreeImage_GetFileTypeFromMemory(fiMemory); + if (format != FIF_UNKNOWN && FreeImage_FIFSupportsReading(format)) + { + //file type is supported. load image + FIBITMAP * fiBitmap = FreeImage_LoadFromMemory(format, fiMemory); + if (fiBitmap != nullptr) + { + //loaded. convert to 32bit if necessary + if (FreeImage_GetBPP(fiBitmap) != 32) + { + FIBITMAP * fiConverted = FreeImage_ConvertTo32Bits(fiBitmap); + if (fiConverted != nullptr) + { + //free original bitmap data + FreeImage_Unload(fiBitmap); + fiBitmap = fiConverted; + } + } + if (fiBitmap != nullptr) + { + width = FreeImage_GetWidth(fiBitmap); + height = FreeImage_GetHeight(fiBitmap); + + //loop through scanlines and add all pixel data to the return vector + //this is necessary, because width*height*bpp might not be == pitch + + unsigned char* tempData = new unsigned char[width * height * 4]; + + int w = (int)width; + + for (int y = (int)height; --y >= 0; ) + { + unsigned int* argb = (unsigned int*)FreeImage_GetScanLine(fiBitmap, y); + unsigned int* abgr = (unsigned int*)(tempData + (y * width * 4)); + for (int x = w; --x >= 0;) + { + unsigned int c = argb[x]; + abgr[x] = (c & 0xFF00FF00) | ((c & 0xFF) << 16) | ((c >> 16) & 0xFF); + } + } + + FreeImage_Unload(fiBitmap); + FreeImage_CloseMemory(fiMemory); + + return tempData; + } + } + else + { + LOG(LogError) << "Error - Failed to load image from memory!"; + } + } + else + { + LOG(LogError) << "Error - File type " << (format == FIF_UNKNOWN ? "unknown" : "unsupported") << "!"; + } + //free FIMEMORY again + FreeImage_CloseMemory(fiMemory); + } + + return NULL; +} + void ImageIO::flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height) { unsigned int temp; diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index ad8ab48069..acbe6cb526 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -9,6 +9,8 @@ class ImageIO { public: static std::vector loadFromMemoryRGBA32(const unsigned char * data, const size_t size, size_t & width, size_t & height); + static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height); + static void flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height); }; diff --git a/es-core/src/PowerSaver.cpp b/es-core/src/PowerSaver.cpp index 8c68955f90..bc1d651a5c 100644 --- a/es-core/src/PowerSaver.cpp +++ b/es-core/src/PowerSaver.cpp @@ -62,6 +62,7 @@ void PowerSaver::updateMode() } else { mMode = DEFAULT; } + updateTimeouts(); } diff --git a/es-core/src/Renderer.h b/es-core/src/Renderer.h index 358cf4031f..5d8b0bd3ba 100644 --- a/es-core/src/Renderer.h +++ b/es-core/src/Renderer.h @@ -38,6 +38,8 @@ namespace Renderer void drawRect(int x, int y, int w, int h, unsigned int color, GLenum blend_sfactor = GL_SRC_ALPHA, GLenum blend_dfactor = GL_ONE_MINUS_SRC_ALPHA); void drawRect(float x, float y, float w, float h, unsigned int color, GLenum blend_sfactor = GL_SRC_ALPHA, GLenum blend_dfactor = GL_ONE_MINUS_SRC_ALPHA); + + bool isVisibleOnScreen(float x, float y, float w, float h); } #endif // ES_CORE_RENDERER_H diff --git a/es-core/src/Renderer_draw_gl.cpp b/es-core/src/Renderer_draw_gl.cpp index cac28c5d4b..b584c618b1 100644 --- a/es-core/src/Renderer_draw_gl.cpp +++ b/es-core/src/Renderer_draw_gl.cpp @@ -15,6 +15,7 @@ namespace Renderer { }; std::stack clipStack; + std::stack nativeClipStack; void setColor4bArray(GLubyte* array, unsigned int color) { @@ -62,7 +63,7 @@ namespace Renderer { } //make sure the box fits within clipStack.top(), and clip further accordingly - if(clipStack.size()) + if (clipStack.size()) { const ClipRect& top = clipStack.top(); if(top.x > box.x) @@ -81,6 +82,7 @@ namespace Renderer { box.h = 0; clipStack.push(box); + nativeClipStack.push(ClipRect(pos.x(), pos.y(), dim.x(), dim.y())); glScissor(box.x, box.y, box.w, box.h); glEnable(GL_SCISSOR_TEST); @@ -95,15 +97,60 @@ namespace Renderer { } clipStack.pop(); + nativeClipStack.pop(); + if(clipStack.empty()) { glDisable(GL_SCISSOR_TEST); - }else{ + } + else + { const ClipRect& top = clipStack.top(); glScissor(top.x, top.y, top.w, top.h); } } + bool valueInRange(int value, int min, int max) + { + return (value >= min) && (value <= max); + } + + bool rectOverlap(ClipRect A, ClipRect B) + { + bool xOverlap = valueInRange(A.x, B.x, B.x + B.w) || + valueInRange(B.x, A.x, A.x + A.w); + + bool yOverlap = valueInRange(A.y, B.y, B.y + B.h) || + valueInRange(B.y, A.y, A.y + A.h); + + return xOverlap && yOverlap; + } + + bool isVisibleOnScreen(float x, float y, float w, float h) + { + ClipRect screen = ClipRect(0, 0, Renderer::getWindowWidth(), Renderer::getWindowHeight()); + ClipRect box = ClipRect(x, y, w, h); + + if (w > 0 && x + w <= 0) + return false; + + if (h > 0 && y + h <= 0) + return false; + + if (x == screen.w || y == screen.h) + return false; + + if (!rectOverlap(screen, box)) + return false; + + if (clipStack.empty()) + return true; + + screen = nativeClipStack.top(); + return rectOverlap(screen, box); + } + + void drawRect(float x, float y, float w, float h, unsigned int color, GLenum blend_sfactor, GLenum blend_dfactor) { drawRect((int)Math::round(x), (int)Math::round(y), (int)Math::round(w), (int)Math::round(h), color, blend_sfactor, blend_dfactor); diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index fb33b2cea4..c51fddf94a 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -75,14 +75,15 @@ namespace Renderer SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, windowWidth, windowHeight, - SDL_WINDOW_OPENGL | (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN))); + SDL_WINDOW_OPENGL | (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN)) + ); if (sdlWindow == NULL) { LOG(LogError) << "Error creating SDL window!\n\t" << SDL_GetError(); return false; } - + LOG(LogInfo) << "Created window successfully."; //support screen rotation @@ -127,7 +128,7 @@ namespace Renderer sdlContext = SDL_GL_CreateContext(sdlWindow); // vsync - // if(Settings::getInstance()->getBool("VSync")) + if (Settings::getInstance()->getBool("VSync")) { // SDL_GL_SetSwapInterval(0) for immediate updates (no vsync, default), // 1 for updates synchronized with the vertical retrace, @@ -135,12 +136,13 @@ namespace Renderer // SDL_GL_SetSwapInterval returns 0 on success, -1 on error. // if vsync is requested, try normal vsync; if that doesn't work, try late swap tearing // if that doesn't work, report an error + if (SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) - LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; + LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; } - // else - // SDL_GL_SetSwapInterval(0); - + else + SDL_GL_SetSwapInterval(0); + return true; } @@ -218,7 +220,18 @@ namespace Renderer void swapBuffers() { +#ifdef WIN32 + glFlush(); + glFinish(); + Sleep(0); +#endif + SDL_GL_SwapWindow(sdlWindow); + +#ifdef WIN32 + Sleep(0); +#endif + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } }; diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 41f2e16de4..a21d117a44 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -24,7 +24,9 @@ std::vector settings_dont_save { { "ShowExit" }, { "SplashScreen" }, { "SplashScreenProgress" }, +#if !defined(_WIN32) { "VSync" }, +#endif { "FullscreenBorderless" }, { "Windowed" }, { "WindowWidth" }, @@ -67,7 +69,11 @@ void Settings::setDefaults() mBoolMap["SplashScreenProgress"] = true; mStringMap["StartupSystem"] = ""; +#if defined(_WIN32) mBoolMap["VSync"] = true; +#else + mBoolMap["VSync"] = false; +#endif mBoolMap["EnableSounds"] = true; mBoolMap["ShowHelpPrompts"] = true; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 9ee8ea2fc9..bcc1a72180 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -9,7 +9,7 @@ #include #include -std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" } }; +std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" }, { "menu" } }; std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; std::map> ThemeData::sElementMap { @@ -37,11 +37,11 @@ std::map> The { "padding", NORMALIZED_PAIR }, { "imageColor", COLOR }, { "backgroundImage", PATH }, - { "backgroundCornerSize", NORMALIZED_PAIR }, + { "backgroundCornerSize", NORMALIZED_PAIR }, { "backgroundColor", COLOR }, { "backgroundCenterColor", COLOR }, { "backgroundEdgeColor", COLOR }, - { "imageSizeMode", STRING } } }, + { "imageSizeMode", STRING } } }, { "text", { { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, @@ -146,11 +146,32 @@ std::map> The { "logoRotation", FLOAT }, { "logoRotationOrigin", NORMALIZED_PAIR }, { "logoSize", NORMALIZED_PAIR }, + { "logoPos", NORMALIZED_PAIR }, { "logoAlignment", STRING }, { "maxLogoCount", FLOAT }, - { "zIndex", FLOAT } } } + { "zIndex", FLOAT } } }, + + { "menuText", { + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "separatorColor", COLOR }, + { "selectorColor", COLOR }, + { "selectedColor", COLOR }, + { "color", COLOR } } }, + + { "menuTextSmall", { + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "color", COLOR } } }, + + { "menuBackground", { + { "path", PATH }, + { "fadePath", PATH }, + { "color", COLOR } } } }; +std::shared_ptr ThemeData::MenuTheme; + #define MINIMUM_THEME_FORMAT_VERSION 3 #define CURRENT_THEME_FORMAT_VERSION 6 @@ -158,12 +179,20 @@ std::map> The unsigned int getHexColor(const char* str) { ThemeException error; - if(!str) - throw error << "Empty color"; + if (!str) + { + //throw error << "Empty color"; + LOG(LogWarning) << "Empty color"; + return 0; + } size_t len = strlen(str); - if(len != 6 && len != 8) - throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; + if (len != 6 && len != 8) + { + //throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; + LOG(LogWarning) << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; + return 0; + } unsigned int val; std::stringstream ss; @@ -199,7 +228,7 @@ std::string resolvePlaceholders(const char* in) } ThemeData::ThemeData() -{ +{ mVersion = 0; } @@ -235,16 +264,18 @@ void ThemeData::loadFile(std::string system, std::map if(mVersion == -404) throw error << " tag missing!\n It's either out of date or you need to add " << CURRENT_THEME_FORMAT_VERSION << " inside your tag."; - if (root.attribute("defaultView")) - mDefaultView = root.attribute("defaultView").as_string(); - if(mVersion < MINIMUM_THEME_FORMAT_VERSION) throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; + if (root.attribute("defaultView")) + mDefaultView = root.attribute("defaultView").as_string(); + parseVariables(root); parseIncludes(root); - parseViews(root); + parseViews(root); parseFeatures(root); + + MenuTheme = std::shared_ptr(new ThemeMenu(*this)); } std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) @@ -256,9 +287,6 @@ std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolde return path; result.replace(start_pos, 7, systemThemeFolder); - - //result.replace("$system", systemThemeFolder); - //boost::algorithm::replace_first(result, "$system", systemThemeFolder); return result; } @@ -339,10 +367,11 @@ void ThemeData::parseIncludes(const pugi::xml_node& root) path = resolveSystemVariable(mSystemThemeFolder, path); if (!ResourceManager::getInstance()->fileExists(path)) - continue; + { //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; - - error << " from included file \"" << relPath << "\":\n "; + LOG(LogWarning) << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + continue; + } mPaths.push_back(path); @@ -403,13 +432,14 @@ void ThemeData::parseVariables(const pugi::xml_node& root) } } + void ThemeData::parseViews(const pugi::xml_node& root) { ThemeException error; error.setFiles(mPaths); // parse views - for(pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) + for (pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) { if(!node.attribute("name")) throw error << "View missing \"name\" attribute!"; @@ -440,19 +470,28 @@ void ThemeData::parseViews(const pugi::xml_node& root) } } + void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) { ThemeException error; error.setFiles(mPaths); - for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + for (pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) { - if(!node.attribute("name")) - throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; + if (!node.attribute("name")) + { + //throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; + LOG(LogWarning) << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; + continue; + } auto elemTypeIt = sElementMap.find(node.name()); - if(elemTypeIt == sElementMap.cend()) - throw error << "Unknown element of type \"" << node.name() << "\"!"; + if (elemTypeIt == sElementMap.cend()) + { + //throw error << "Unknown element of type \"" << node.name() << "\"!"; + LOG(LogWarning) << "Unknown element of type \"" << node.name() << "\"!"; + continue; + } if (parseRegion(node)) { @@ -519,8 +558,11 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapsecond.getThemePath(system); } + +ThemeData::ThemeMenu::ThemeMenu(ThemeData& theme) +{ + Title.font = Font::get(FONT_SIZE_LARGE); + Footer.font = Font::get(FONT_SIZE_SMALL); + Text.font = Font::get(FONT_SIZE_MEDIUM); + TextSmall.font = Font::get(FONT_SIZE_SMALL); + + auto elem = theme.getElement("menu", "menubg", "menuBackground"); + if (elem) + { + if (elem->has("path") && ResourceManager::getInstance()->fileExists(elem->get("path"))) + Background.path = elem->get("path"); + + if (elem->has("fadePath") && ResourceManager::getInstance()->fileExists(elem->get("fadePath"))) + Background.fadePath = elem->get("fadePath"); + + if (elem->has("color")) + Background.color = elem->get("color"); + } + + elem = theme.getElement("menu", "menutitle", "menuText"); + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + Title.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_LARGE)); + if (elem->has("color")) + Title.color = elem->get("color"); + } + + elem = theme.getElement("menu", "menufooter", "menuText"); + + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + Footer.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_SMALL)); + if (elem->has("color")) + Footer.color = elem->get("color"); + } + + elem = theme.getElement("menu", "menutextsmall", "menuTextSmall"); + + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + TextSmall.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_SMALL)); + + if (elem->has("color")) + TextSmall.color = elem->get("color"); + if (elem->has("selectedColor")) + Text.selectedColor = elem->get("selectedColor"); + if (elem->has("selectorColor")) + Text.selectedColor = elem->get("selectorColor"); + } + + elem = theme.getElement("menu", "menutext", "menuText"); + + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + Text.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_MEDIUM)); + + if (elem->has("color")) + Text.color = elem->get("color"); + if (elem->has("separatorColor")) + Text.separatorColor = elem->get("separatorColor"); + if (elem->has("selectedColor")) + Text.selectedColor = elem->get("selectedColor"); + if (elem->has("selectorColor")) + Text.selectorColor = elem->get("selectorColor"); + } +} \ No newline at end of file diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index aec760c4c7..27fa912433 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -4,6 +4,7 @@ #include "math/Vector2f.h" #include "utils/FileSystemUtil.h" + #include #include #include @@ -21,6 +22,7 @@ class NinePatchComponent; class Sound; class TextComponent; class Window; +class Font; namespace ThemeFlags { @@ -81,9 +83,31 @@ struct ThemeSet inline std::string getThemePath(const std::string& system) const { return path + "/" + system + "/theme.xml"; } }; +struct MenuElement { + unsigned int color; + unsigned int selectedColor; + unsigned int selectorColor; + unsigned int separatorColor; + std::string path; + std::string fadePath; + std::shared_ptr font; +}; + class ThemeData { public: + class ThemeMenu + { + public: + ThemeMenu(ThemeData& theme); + + MenuElement Background{ 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, ":/frame.png", ":/scroll_gradient.png", nullptr }; + MenuElement Title{ 0x555555FF, 0x555555FF, 0x555555FF, 0xFFFFFFFF, "", "", nullptr }; + MenuElement Text{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, "", "", nullptr }; + MenuElement TextSmall{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, "", "", nullptr }; + MenuElement Footer{ 0xC6C6C6FF, 0xC6C6C6FF, 0xC6C6C6FF, 0xFFFFFFFF, "", "", nullptr }; + }; + class ThemeElement { @@ -130,6 +154,7 @@ class ThemeData std::vector orderedKeys; }; + public: ThemeData(); @@ -161,6 +186,7 @@ class ThemeData std::string getDefaultView() { return mDefaultView; }; + static const std::shared_ptr& getMenuTheme() { return MenuTheme; } private: static std::map< std::string, std::map > sElementMap; @@ -171,7 +197,7 @@ class ThemeData float mVersion; std::string mDefaultView; - + void parseFeatures(const pugi::xml_node& themeRoot); void parseIncludes(const pugi::xml_node& themeRoot); void parseVariables(const pugi::xml_node& root); @@ -180,11 +206,13 @@ class ThemeData void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element); bool parseRegion(const pugi::xml_node& node); bool parseSubset(const pugi::xml_node& node); - + std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); std::map mViews; std::string mSystemThemeFolder; + + static std::shared_ptr MenuTheme; }; #endif // ES_CORE_THEME_DATA_H diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 1b361f408e..87f5118e5b 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -318,7 +318,7 @@ void Window::renderLoadingScreen(std::string text, float percent) Renderer::setMatrix(trans); Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); - if (percent > 0) + if (percent >= 0) { float baseHeight = 0.04f; @@ -354,7 +354,7 @@ void Window::renderLoadingScreen(std::string text, float percent) TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.8f); // 35 + float y = Math::round(Renderer::getScreenHeight() * 0.78f); // 35 trans = trans.translate(Vector3f(x, y, 0.0f)); Renderer::setMatrix(trans); font->renderTextCache(cache); diff --git a/es-core/src/components/ButtonComponent.cpp b/es-core/src/components/ButtonComponent.cpp index 9496046419..08eeeb34bc 100644 --- a/es-core/src/components/ButtonComponent.cpp +++ b/es-core/src/components/ButtonComponent.cpp @@ -11,6 +11,13 @@ ButtonComponent::ButtonComponent(Window* window, const std::string& text, const mEnabled(true), mTextColorFocused(0xFFFFFFFF), mTextColorUnfocused(0x777777FF) { + auto menuTheme = ThemeData::getMenuTheme(); + + mFont = menuTheme->Text.font; + mTextColorUnfocused = menuTheme->Text.color; + mTextColorFocused = menuTheme->Text.selectedColor; + mColor = menuTheme->Text.color; + setPressedFunc(func); setText(text, helpText); updateImage(); @@ -71,6 +78,7 @@ void ButtonComponent::setEnabled(bool enabled) void ButtonComponent::updateImage() { + /* if(!mEnabled || !mPressedFunc) { mBox.setImagePath(":/button_filled.png"); @@ -78,9 +86,9 @@ void ButtonComponent::updateImage() mBox.setEdgeColor(0x770000FF); return; } - - mBox.setCenterColor(0xFFFFFFFF); - mBox.setEdgeColor(0xFFFFFFFF); + */ + mBox.setCenterColor(mColor); + mBox.setEdgeColor(mColor); mBox.setImagePath(mFocused ? ":/button_filled.png" : ":/button.png"); } diff --git a/es-core/src/components/ButtonComponent.h b/es-core/src/components/ButtonComponent.h index 65288209cd..790bbc6d43 100644 --- a/es-core/src/components/ButtonComponent.h +++ b/es-core/src/components/ButtonComponent.h @@ -38,7 +38,7 @@ class ButtonComponent : public GuiComponent bool mEnabled; unsigned int mTextColorFocused; unsigned int mTextColorUnfocused; - + unsigned int getCurTextColor() const; void updateImage(); @@ -46,6 +46,7 @@ class ButtonComponent : public GuiComponent std::string mHelpText; std::unique_ptr mTextCache; NinePatchComponent mBox; + unsigned int mColor; }; #endif // ES_CORE_COMPONENTS_BUTTON_COMPONENT_H diff --git a/es-core/src/components/ComponentGrid.cpp b/es-core/src/components/ComponentGrid.cpp index e89e7f9c4e..7ba5b0c1c3 100644 --- a/es-core/src/components/ComponentGrid.cpp +++ b/es-core/src/components/ComponentGrid.cpp @@ -5,11 +5,12 @@ using namespace GridFlags; -ComponentGrid::ComponentGrid(Window* window, const Vector2i& gridDimensions) : GuiComponent(window), +ComponentGrid::ComponentGrid(Window* window, const Vector2i& gridDimensions, unsigned int separatorColor) : GuiComponent(window), mGridSize(gridDimensions), mCursor(0, 0) { assert(gridDimensions.x() > 0 && gridDimensions.y() > 0); + mSeparatorColor = separatorColor; mCells.reserve(gridDimensions.x() * gridDimensions.y()); mColWidths = new float[gridDimensions.x()]; @@ -195,7 +196,7 @@ void ComponentGrid::updateSeparators() } mLineColors.reserve(mLines.size()); - Renderer::buildGLColorArray((GLubyte*)mLineColors.data(), 0xC6C7C6FF, (unsigned int)mLines.size()); + Renderer::buildGLColorArray((GLubyte*)mLineColors.data(), mSeparatorColor, (unsigned int)mLines.size()); } void ComponentGrid::onSizeChanged() diff --git a/es-core/src/components/ComponentGrid.h b/es-core/src/components/ComponentGrid.h index aa6519175f..15fe8e76df 100644 --- a/es-core/src/components/ComponentGrid.h +++ b/es-core/src/components/ComponentGrid.h @@ -29,9 +29,11 @@ namespace GridFlags class ComponentGrid : public GuiComponent { public: - ComponentGrid(Window* window, const Vector2i& gridDimensions); + ComponentGrid(Window* window, const Vector2i& gridDimensions, unsigned int separatorColor = 0xC6C7C6FF); virtual ~ComponentGrid(); + void setSeparatorColor(unsigned int separatorColor) { mSeparatorColor = separatorColor; updateSeparators(); } + bool removeEntry(const std::shared_ptr& comp); void setEntry(const std::shared_ptr& comp, const Vector2i& pos, bool canFocus, bool resize = true, @@ -105,6 +107,7 @@ class ComponentGrid : public GuiComponent std::vector mLines; std::vector mLineColors; + unsigned int mSeparatorColor; // Update position & size void updateCellComponent(const GridEntry& cell); diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index f8f1bcd518..4befbde724 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -135,7 +135,7 @@ void ComponentList::updateCameraOffset() { // move the camera to scroll const float totalHeight = getTotalRowHeight(); - if(totalHeight > mSize.y()) + if (totalHeight > mSize.y() && mCursor < mEntries.size()) { float target = mSelectorBarOffset + getRowHeight(mEntries.at(mCursor).data)/2 - (mSize.y() / 2); @@ -162,10 +162,17 @@ void ComponentList::render(const Transform4x4f& parentTrans) if(!size()) return; + auto menuTheme = ThemeData::getMenuTheme(); + unsigned int selectorColor = menuTheme->Text.selectorColor; + unsigned int selectedColor = menuTheme->Text.selectedColor; + unsigned int bgColor = menuTheme->Background.color; + unsigned int separatorColor = menuTheme->Text.separatorColor; + unsigned int textColor = menuTheme->Text.color; + Transform4x4f trans = parentTrans * getTransform(); trans.round(); - // clip everything to be inside our bounds + // clip everything to be inside our boundsz Vector3f dim(mSize.x(), mSize.y(), 0); dim = trans * dim - trans.translation(); Renderer::pushClipRect(Vector2i((int)trans.translation().x(), (int)trans.translation().y()), @@ -183,10 +190,11 @@ void ComponentList::render(const Transform4x4f& parentTrans) drawAll = !mFocused || i != (unsigned int)mCursor; for(auto it = entry.data.elements.cbegin(); it != entry.data.elements.cend(); it++) { - if(drawAll || it->invert_when_selected) + if (drawAll || it->invert_when_selected) { + it->component->setColor(textColor); it->component->render(trans); - }else{ + } else { drawAfterCursor.push_back(it->component.get()); } } @@ -196,7 +204,7 @@ void ComponentList::render(const Transform4x4f& parentTrans) Renderer::setMatrix(trans); // draw selector bar - if(mFocused) + if (mFocused) { // inversion: src * (1 - dst) + dst * 0 = where src = 1 // need a function that goes roughly 0x777777 -> 0xFFFFFF @@ -204,20 +212,33 @@ void ComponentList::render(const Transform4x4f& parentTrans) // (1 - dst) + 0x77 const float selectedRowHeight = getRowHeight(mEntries.at(mCursor).data); - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0xFFFFFFFF, - GL_ONE_MINUS_DST_COLOR, GL_ZERO); - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0x777777FF, - GL_ONE, GL_ONE); - + + if ((selectorColor != bgColor) && ((selectorColor & 0xFF) != 0x00)) { + //Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, GL_ONE_MINUS_DST_COLOR, GL_ZERO); + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, GL_ZERO, GL_ONE_MINUS_SRC_COLOR); + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, GL_ONE, GL_ONE); + } + + // Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0xFFFFFFFF, GL_ONE_MINUS_DST_COLOR, GL_ZERO); + // Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0x777777FF, GL_ONE, GL_ONE); + // hack to draw 2px dark on left/right of the bar - Renderer::drawRect(0.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF); - Renderer::drawRect(mSize.x() - 2.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF); + //Renderer::drawRect(0.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF); + //Renderer::drawRect(mSize.x() - 2.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF); + + auto& entry = mEntries.at(mCursor); + for (auto& element : entry.data.elements) + { + element.component->setColor(selectedColor); + drawAfterCursor.push_back(element.component.get()); + } + for(auto it = drawAfterCursor.cbegin(); it != drawAfterCursor.cend(); it++) (*it)->render(trans); // reset matrix if one of these components changed it - if(drawAfterCursor.size()) + if (drawAfterCursor.size()) Renderer::setMatrix(trans); } @@ -225,10 +246,10 @@ void ComponentList::render(const Transform4x4f& parentTrans) float y = 0; for(unsigned int i = 0; i < mEntries.size(); i++) { - Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, 0xC6C7C6FF); + Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, separatorColor); y += getRowHeight(mEntries.at(i).data); } - Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, 0xC6C7C6FF); + Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, separatorColor); Renderer::popClipRect(); } diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index cccf549cfe..ae85435063 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -13,6 +13,8 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mSelectedZoomPercent = 1.0f; mAnimPosition = Vector3f(0, 0); + mLabelMerged = false; + mDefaultProperties.mSize = getDefaultTileSize(); mDefaultProperties.mPadding = Vector2f(16.0f, 16.0f); mDefaultProperties.mImageColor = 0xFFFFFFDD; // 0xAAAAAABB; @@ -57,11 +59,20 @@ void GridTileComponent::resize() setSize(currentProperties.mSize); float height = currentProperties.mSize.y() * mLabelSize.y(); - if (!mLabelVisible) - height = 0; - mLabel.setPosition(0, mSize.y() - height); - mLabel.setSize(currentProperties.mSize.x(), height); + if (mLabelMerged) + { + mLabel.setPosition(currentProperties.mPadding.x(), mSize.y() - height - currentProperties.mPadding.y()); + mLabel.setSize(currentProperties.mSize.x() - 2 * currentProperties.mPadding.x(), height); + } + else + { + mLabel.setPosition(0, mSize.y() - height); + mLabel.setSize(currentProperties.mSize.x(), height); + } + + if (!mLabelVisible || mLabelMerged) + height = 0; if (mLabelSize.x() == 0) height = 0; @@ -90,7 +101,8 @@ void GridTileComponent::resize() } else mBackground.setPosition(0, 0); - + + if (mImage != NULL) { mImage->setOrigin(0.5f, 0.5f); @@ -106,8 +118,11 @@ void GridTileComponent::resize() + if (!mLabelMerged && currentProperties.mImageSizeMode == "minSize") + mBackground.setSize(currentProperties.mSize.x(), currentProperties.mSize.y() - bottomPadding); + else + mBackground.setSize(currentProperties.mSize); - mBackground.setSize(currentProperties.mSize); mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); } @@ -118,14 +133,26 @@ void GridTileComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = getTransform() * parentTrans; + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + //Renderer::setMatrix(trans); //Renderer::drawRect(0.f, 0.f, mSize.x(), mSize.y(), 0xFF0000FF); - - mBackground.render(trans); + + if (mBackground.getCornerSize().x() == 0) + { + + Renderer::setMatrix(trans); + Renderer::drawRect(mBackground.getPosition().x(), mBackground.getPosition().y(), mBackground.getSize().x(), mBackground.getSize().y(), mBackground.getCenterColor()); + Renderer::setMatrix(parentTrans); + } + else + mBackground.render(trans); if (mImage != NULL) mImage->render(trans); - + if (mLabelVisible) mLabel.render(trans); } @@ -155,7 +182,7 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons // Apply theme to the default gridtile const ThemeData::ThemeElement* elem = theme->getElement(view, "default", "gridtile"); if (elem) - { + { if (elem->has("size")) mDefaultProperties.mSize = elem->get("size") * screen; @@ -232,7 +259,10 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem != NULL) { if (elem && elem->has("size")) + { mLabelSize = elem->get("size"); + mLabelMerged = mLabelSize.x() == 0; + } mLabelVisible = true; mLabel.applyTheme(theme, view, element, properties); @@ -264,7 +294,7 @@ void GridTileComponent::setImage(const std::string& path, std::string name) { if (mCurrentPath != path) { - mCurrentPath = path; + mCurrentPath = path; mImage->setImage(path); } diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index bdaa611a16..46fc6c4364 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -49,6 +49,7 @@ class GridTileComponent : public GuiComponent Vector2f mLabelSize; bool mLabelVisible; + bool mLabelMerged; NinePatchComponent mBackground; diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 8358ee6839..3ec03372de 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -25,6 +25,7 @@ ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : G mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f) { updateColors(); + mSizeChanged = false; } ImageComponent::~ImageComponent() @@ -33,17 +34,22 @@ ImageComponent::~ImageComponent() void ImageComponent::resize() { - if(!mTexture) + if (!mTexture) return; const Vector2f textureSize = mTexture->getSourceImageSize(); - if(textureSize == Vector2f::Zero()) + if (textureSize == Vector2f::Zero()) return; - if(mTexture->isTiled()) + int ox = mSize.x(); + int oy = mSize.y(); + + if (mTexture->isTiled()) { mSize = mTargetSize; - }else{ + } + else + { // SVG rasterization is determined by height (see SVGResource.cpp), and rasterization is done in terms of pixels // if rounding is off enough in the rasterization step (for images with extreme aspect ratios), it can cause cutoff when the aspect ratio breaks // so, we always make sure the resultant height is an integer to make sure cutoff doesn't happen, and scale width from that @@ -115,8 +121,12 @@ void ImageComponent::resize() mSize[0] = Math::round(mSize.x()); mSize[1] = Math::round(mSize.y()); + + if (ox != mSize.x() || oy != mSize.y()) + mSizeChanged = true; + // mSize.y() should already be rounded - mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); + //mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); onSizeChanged(); } @@ -323,13 +333,23 @@ void ImageComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); Renderer::setMatrix(trans); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + if (mTexture && mOpacity > 0) { + if (mSizeChanged) + { + mSizeChanged = false; + mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); + } + if (Settings::getInstance()->getBool("DebugImage")) { Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; Renderer::drawRect(targetSizePos.x(), targetSizePos.y(), mTargetSize.x(), mTargetSize.y(), 0xFF000033); - Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); } if (mTexture->isInitialized()) diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 7a93a0d851..3f5ce3138a 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -57,6 +57,8 @@ class ImageComponent : public GuiComponent // Multiply all pixels in the image by this color when rendering. void setColorShift(unsigned int color); + virtual void setColor(unsigned int color) { setColorShift(color); } + void setFlipX(bool flip); // Mirror on the X axis. void setFlipY(bool flip); // Mirror on the Y axis. @@ -108,6 +110,8 @@ class ImageComponent : public GuiComponent Vector2f mTopLeftCrop; Vector2f mBottomRightCrop; + + bool mSizeChanged; }; #endif // ES_CORE_COMPONENTS_IMAGE_COMPONENT_H diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 343c86f618..f891c04490 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -10,16 +10,20 @@ MenuComponent::MenuComponent(Window* window, std::string title, const std::shared_ptr& titleFont) : GuiComponent(window), mBackground(window), mGrid(window, Vector2i(1, 3)) { + auto theme = ThemeData::getMenuTheme(); + addChild(&mBackground); addChild(&mGrid); - - mBackground.setImagePath(":/frame.png"); + + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); // set up title mTitle = std::make_shared(mWindow); mTitle->setHorizontalAlignment(ALIGN_CENTER); - mTitle->setColor(0x555555FF); - setTitle(title.c_str(), titleFont); + mTitle->setColor(theme->Title.color); // 0x555555FF + setTitle(title.c_str(), theme->Title.font); // titleFont mGrid.setEntry(mTitle, Vector2i(0, 0), false); // set up list which will never change (externally, anyway) @@ -32,6 +36,16 @@ MenuComponent::MenuComponent(Window* window, std::string title, const std::share mGrid.resetCursor(); } +void MenuComponent::addWithLabel(const std::string& label, const std::shared_ptr& comp, bool setCursorHere, bool invert_when_selected) +{ + auto theme = ThemeData::getMenuTheme(); + + ComponentListRow row; + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(label), theme->Text.font, theme->Text.color), true); + row.addElement(comp, false, invert_when_selected); + addRow(row, setCursorHere); +} + void MenuComponent::setTitle(const char* title, const std::shared_ptr& font) { mTitle->setText(Utils::String::toUpper(title)); @@ -40,7 +54,9 @@ void MenuComponent::setTitle(const char* title, const std::shared_ptr& fon float MenuComponent::getButtonGridHeight() const { - return (mButtonGrid ? mButtonGrid->getSize().y() : Font::get(FONT_SIZE_MEDIUM)->getHeight() + BUTTON_GRID_VERT_PADDING); + auto menuTheme = ThemeData::getMenuTheme(); + return (mButtonGrid ? mButtonGrid->getSize().y() : menuTheme->Text.font->getHeight() + BUTTON_GRID_VERT_PADDING); + //return (mButtonGrid ? mButtonGrid->getSize().y() : Font::get(FONT_SIZE_MEDIUM)->getHeight() + BUTTON_GRID_VERT_PADDING); } void MenuComponent::updateSize() @@ -126,8 +142,12 @@ std::shared_ptr makeButtonGrid(Window* window, const std::vector< std::shared_ptr makeArrow(Window* window) { + auto menuTheme = ThemeData::getMenuTheme(); + auto bracket = std::make_shared(window); bracket->setImage(":/arrow.svg"); - bracket->setResize(0, Math::round(Font::get(FONT_SIZE_MEDIUM)->getLetterHeight())); + bracket->setColorShift(menuTheme->Text.color); + bracket->setResize(0, round(menuTheme->Text.font->getLetterHeight())); + //bracket->setResize(0, Math::round(Font::get(FONT_SIZE_MEDIUM)->getLetterHeight())); return bracket; } diff --git a/es-core/src/components/MenuComponent.h b/es-core/src/components/MenuComponent.h index e2c9ea31f2..d5a8db473c 100644 --- a/es-core/src/components/MenuComponent.h +++ b/es-core/src/components/MenuComponent.h @@ -26,13 +26,7 @@ class MenuComponent : public GuiComponent inline void addRow(const ComponentListRow& row, bool setCursorHere = false) { mList->addRow(row, setCursorHere); updateSize(); } - inline void addWithLabel(const std::string& label, const std::shared_ptr& comp, bool setCursorHere = false, bool invert_when_selected = true) - { - ComponentListRow row; - row.addElement(std::make_shared(mWindow, Utils::String::toUpper(label), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(comp, false, invert_when_selected); - addRow(row, setCursorHere); - } + void addWithLabel(const std::string& label, const std::shared_ptr& comp, bool setCursorHere = false, bool invert_when_selected = true); void addButton(const std::string& label, const std::string& helpText, const std::function& callback); diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index fedf95115b..f3ff0bba01 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -106,6 +106,10 @@ void NinePatchComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); trans.round(); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + if(mTexture && mVertices != NULL) { Renderer::setMatrix(trans); @@ -166,18 +170,27 @@ void NinePatchComponent::fitTo(Vector2f size, Vector3f position, Vector2f paddin void NinePatchComponent::setImagePath(const std::string& path) { + if (mPath == path) + return; + mPath = path; buildVertices(); } void NinePatchComponent::setEdgeColor(unsigned int edgeColor) { + if (mEdgeColor == edgeColor) + return; + mEdgeColor = edgeColor; updateColors(); } void NinePatchComponent::setCenterColor(unsigned int centerColor) { + if (mCenterColor == centerColor) + return; + mCenterColor = centerColor; updateColors(); } diff --git a/es-core/src/components/NinePatchComponent.h b/es-core/src/components/NinePatchComponent.h index af9e3b9343..8d0dbf697e 100644 --- a/es-core/src/components/NinePatchComponent.h +++ b/es-core/src/components/NinePatchComponent.h @@ -35,6 +35,8 @@ class NinePatchComponent : public GuiComponent void setEdgeColor(unsigned int edgeColor); // Apply a color shift to the "edge" parts of the ninepatch. void setCenterColor(unsigned int centerColor); // Apply a color shift to the "center" part of the ninepatch. + unsigned int getCenterColor() { return mCenterColor; }; + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; const Vector2f& getCornerSize() const; diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index 613a22c026..97c78c0e6c 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -39,7 +39,10 @@ class OptionListComponent : public GuiComponent OptionListPopup(Window* window, OptionListComponent* parent, const std::string& title) : GuiComponent(window), mMenu(window, title.c_str()), mParent(parent) { - auto font = Font::get(FONT_SIZE_MEDIUM); + auto menuTheme = ThemeData::getMenuTheme(); + auto font = menuTheme->Text.font; + auto color = menuTheme->Text.color; + ComponentListRow row; // for select all/none @@ -48,7 +51,7 @@ class OptionListComponent : public GuiComponent for(auto it = mParent->mEntries.begin(); it != mParent->mEntries.end(); it++) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, 0x777777FF), true); + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, color), true); OptionListData& e = *it; @@ -87,11 +90,11 @@ class OptionListComponent : public GuiComponent mMenu.addRow(row, (!mParent->mMultiSelect && it->selected)); } - mMenu.addButton(_T("BACK"), "accept", [this] { delete this; }); + mMenu.addButton(_T("BACK"), _T("accept"), [this] { delete this; }); if(mParent->mMultiSelect) { - mMenu.addButton("SELECT ALL", "select all", [this, checkboxes] { + mMenu.addButton(_T("SELECT ALL"), _T("SELECT ALL"), [this, checkboxes] { for(unsigned int i = 0; i < mParent->mEntries.size(); i++) { mParent->mEntries.at(i).selected = true; @@ -100,7 +103,7 @@ class OptionListComponent : public GuiComponent mParent->onSelectedChanged(); }); - mMenu.addButton("SELECT NONE", "select none", [this, checkboxes] { + mMenu.addButton(_T("SELECT NONE"), _T("SELECT NONE"), [this, checkboxes] { for(unsigned int i = 0; i < mParent->mEntries.size(); i++) { mParent->mEntries.at(i).selected = false; @@ -110,7 +113,10 @@ class OptionListComponent : public GuiComponent }); } - mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); + mMenu.setPosition( + (Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, + (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); + //Renderer::getScreenHeight() * 0.15f); addChild(&mMenu); } @@ -137,29 +143,42 @@ class OptionListComponent : public GuiComponent OptionListComponent(Window* window, const std::string& name, bool multiSelect = false) : GuiComponent(window), mMultiSelect(multiSelect), mName(name), mText(window), mLeftArrow(window), mRightArrow(window) { - auto font = Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT); - mText.setFont(font); - mText.setColor(0x777777FF); + auto theme = ThemeData::getMenuTheme(); + + // auto font = Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT); + mText.setFont(theme->Text.font); // font + mText.setColor(theme->Text.color); // 0x777777FF mText.setHorizontalAlignment(ALIGN_CENTER); addChild(&mText); mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); - if(mMultiSelect) + if (mMultiSelect) { mRightArrow.setImage(":/arrow.svg"); + mRightArrow.setColorShift(theme->Text.color); addChild(&mRightArrow); - }else{ + } else { mLeftArrow.setImage(":/option_arrow.svg"); + mLeftArrow.setColorShift(theme->Text.color); mLeftArrow.setFlipX(true); addChild(&mLeftArrow); mRightArrow.setImage(":/option_arrow.svg"); + mLeftArrow.setColorShift(theme->Text.color); addChild(&mRightArrow); } - setSize(mLeftArrow.getSize().x() + mRightArrow.getSize().x(), font->getHeight()); + setSize(mLeftArrow.getSize().x() + mRightArrow.getSize().x(), theme->Text.font->getHeight()); + } + + + virtual void setColor(unsigned int color) + { + mText.setColor(color); + mLeftArrow.setColorShift(color); + mRightArrow.setColorShift(color); } // handles positioning/resizing of text and arrows diff --git a/es-core/src/components/SliderComponent.cpp b/es-core/src/components/SliderComponent.cpp index 81f97e9b09..a8e9657100 100644 --- a/es-core/src/components/SliderComponent.cpp +++ b/es-core/src/components/SliderComponent.cpp @@ -14,10 +14,20 @@ SliderComponent::SliderComponent(Window* window, float min, float max, float inc // some sane default value mValue = (max + min) / 2; + auto menuTheme = ThemeData::getMenuTheme(); + mColor = menuTheme->Text.color; + mKnob.setOrigin(0.5f, 0.5f); mKnob.setImage(":/slider_knob.svg"); - - setSize(Renderer::getScreenWidth() * 0.15f, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight()); + mKnob.setColorShift(mColor); + + setSize(Renderer::getScreenWidth() * 0.15f, menuTheme->Text.font->getLetterHeight()); +} + +void SliderComponent::setColor(unsigned int color) { + mColor = color; + mKnob.setColorShift(mColor); + onValueChanged(); } bool SliderComponent::input(InputConfig* config, Input input) @@ -73,7 +83,7 @@ void SliderComponent::render(const Transform4x4f& parentTrans) //render line const float lineWidth = 2; - Renderer::drawRect(mKnob.getSize().x() / 2, mSize.y() / 2 - lineWidth / 2, width, lineWidth, 0x777777FF); + Renderer::drawRect(mKnob.getSize().x() / 2, mSize.y() / 2 - lineWidth / 2, width, lineWidth, mColor); //render knob mKnob.render(trans); @@ -108,7 +118,7 @@ void SliderComponent::onSizeChanged() void SliderComponent::onValueChanged() { // update suffix textcache - if(mFont) + if (mFont) { std::stringstream ss; ss << std::fixed; @@ -126,7 +136,7 @@ void SliderComponent::onValueChanged() const std::string max = ss.str(); Vector2f textSize = mFont->sizeText(max); - mValueCache = std::shared_ptr(mFont->buildTextCache(val, mSize.x() - textSize.x(), (mSize.y() - textSize.y()) / 2, 0x777777FF)); + mValueCache = std::shared_ptr(mFont->buildTextCache(val, mSize.x() - textSize.x(), (mSize.y() - textSize.y()) / 2, mColor)); mValueCache->metrics.size[0] = textSize.x(); // fudge the width } @@ -139,6 +149,6 @@ void SliderComponent::onValueChanged() std::vector SliderComponent::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("left/right", "MODIFIER")); + prompts.push_back(HelpPrompt("left/right", _T("CHANGE"))); return prompts; } diff --git a/es-core/src/components/SliderComponent.h b/es-core/src/components/SliderComponent.h index e4297d5b7b..c74d7e7d2e 100644 --- a/es-core/src/components/SliderComponent.h +++ b/es-core/src/components/SliderComponent.h @@ -25,6 +25,7 @@ class SliderComponent : public GuiComponent void onSizeChanged() override; virtual std::vector getHelpPrompts() override; + virtual void setColor(unsigned int color); private: void onValueChanged(); @@ -40,6 +41,8 @@ class SliderComponent : public GuiComponent std::string mSuffix; std::shared_ptr mFont; std::shared_ptr mValueCache; + + unsigned int mColor; }; #endif // ES_CORE_COMPONENTS_SLIDER_COMPONENT_H diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index 747e507fc3..27ba63294e 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -5,11 +5,19 @@ SwitchComponent::SwitchComponent(Window* window, bool state) : GuiComponent(window), mImage(window), mState(state) { + auto menuTheme = ThemeData::getMenuTheme(); + mImage.setImage(":/off.svg"); mImage.setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight()); + mImage.setColorShift(menuTheme->Text.color); + mSize = mImage.getSize(); } +void SwitchComponent::setColor(unsigned int color) { + mImage.setColorShift(color); +} + void SwitchComponent::onSizeChanged() { mImage.setSize(mSize); diff --git a/es-core/src/components/SwitchComponent.h b/es-core/src/components/SwitchComponent.h index 2e2d83f6b4..6cc981fb99 100644 --- a/es-core/src/components/SwitchComponent.h +++ b/es-core/src/components/SwitchComponent.h @@ -23,6 +23,8 @@ class SwitchComponent : public GuiComponent virtual std::vector getHelpPrompts() override; + void setColor(unsigned int color); + private: void onStateChanged(); diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index d1fc995f40..2800b41eaa 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -95,15 +95,22 @@ void TextComponent::setUppercase(bool uppercase) void TextComponent::render(const Transform4x4f& parentTrans) { + if (mColorOpacity == 0) + return; + Transform4x4f trans = parentTrans * getTransform(); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + if (mRenderBackground) { Renderer::setMatrix(trans); Renderer::drawRect(0.f, 0.f, mSize.x(), mSize.y(), mBgColor); } - - if(mTextCache) + + if (mTextCache) { const Vector2f& textSize = mTextCache->metrics.size; float yOff = 0; @@ -116,7 +123,7 @@ void TextComponent::render(const Transform4x4f& parentTrans) yOff = (getSize().y() - textSize.y()); break; case ALIGN_CENTER: - yOff = (getSize().y() - textSize.y()) / 2.0f; + yOff = (int) (getSize().y() - textSize.y()) / 2.0f; break; } Vector3f off(0, yOff, 0); @@ -148,6 +155,7 @@ void TextComponent::render(const Transform4x4f& parentTrans) break; } } + mFont->renderTextCache(mTextCache.get()); } } diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index 1721105772..e472cec335 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -23,7 +23,7 @@ class TextComponent : public GuiComponent void setUppercase(bool uppercase); void onSizeChanged() override; void setText(const std::string& text); - void setColor(unsigned int color); + virtual void setColor(unsigned int color); void setHorizontalAlignment(Alignment align); void setVerticalAlignment(Alignment align); void setLineSpacing(float spacing); diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 91f7532e40..ea712c0a60 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -135,6 +135,7 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) float x, y; Transform4x4f trans = parentTrans * getTransform(); + GuiComponent::renderChildren(trans); Renderer::setMatrix(trans); diff --git a/es-core/src/guis/GuiDetectDevice.cpp b/es-core/src/guis/GuiDetectDevice.cpp index faadbf4ac3..cd92f1664e 100644 --- a/es-core/src/guis/GuiDetectDevice.cpp +++ b/es-core/src/guis/GuiDetectDevice.cpp @@ -14,6 +14,12 @@ GuiDetectDevice::GuiDetectDevice(Window* window, bool firstRun, const std::function& doneCallback) : GuiComponent(window), mFirstRun(firstRun), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 5)) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + mGrid.setSeparatorColor(theme->Text.separatorColor); + mHoldingConfig = NULL; mHoldTime = 0; mDoneCallback = doneCallback; @@ -23,9 +29,10 @@ GuiDetectDevice::GuiDetectDevice(Window* window, bool firstRun, const std::funct // title mTitle = std::make_shared(mWindow, firstRun ? "WELCOME" : "CONFIGURE INPUT", - Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); - mGrid.setEntry(mTitle, Vector2i(0, 0), false, true, Vector2i(1, 1), GridFlags::BORDER_BOTTOM); + ThemeData::getMenuTheme()->Title.font, ThemeData::getMenuTheme()->Title.color, ALIGN_CENTER); + mGrid.setEntry(mTitle, Vector2i(0, 0), false, true, Vector2i(1, 1), GridFlags::BORDER_BOTTOM); + // device info std::stringstream deviceInfo; int numDevices = InputManager::getInstance()->getNumJoysticks(); @@ -34,15 +41,15 @@ GuiDetectDevice::GuiDetectDevice(Window* window, bool firstRun, const std::funct deviceInfo << numDevices << " GAMEPAD" << (numDevices > 1 ? "S" : "") << " DETECTED"; else deviceInfo << "NO GAMEPADS DETECTED"; - mDeviceInfo = std::make_shared(mWindow, deviceInfo.str(), Font::get(FONT_SIZE_SMALL), 0x999999FF, ALIGN_CENTER); + mDeviceInfo = std::make_shared(mWindow, deviceInfo.str(), ThemeData::getMenuTheme()->TextSmall.font, 0x999999FF, ALIGN_CENTER); mGrid.setEntry(mDeviceInfo, Vector2i(0, 1), false, true); // message - mMsg1 = std::make_shared(mWindow, "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT.", Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER); + mMsg1 = std::make_shared(mWindow, "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT.", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color, ALIGN_CENTER); mGrid.setEntry(mMsg1, Vector2i(0, 2), false, true); const char* msg2str = firstRun ? "PRESS F4 TO QUIT AT ANY TIME." : "PRESS ESC TO CANCEL."; - mMsg2 = std::make_shared(mWindow, msg2str, Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER); + mMsg2 = std::make_shared(mWindow, msg2str, ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color, ALIGN_CENTER); mGrid.setEntry(mMsg2, Vector2i(0, 3), false, true); // currently held device diff --git a/es-core/src/guis/GuiInputConfig.cpp b/es-core/src/guis/GuiInputConfig.cpp index 3ec21f101f..52cf142de2 100755 --- a/es-core/src/guis/GuiInputConfig.cpp +++ b/es-core/src/guis/GuiInputConfig.cpp @@ -51,9 +51,15 @@ static const InputConfigStructure GUI_INPUT_CONFIG_LIST[inputCount] = #define HOLD_TO_SKIP_MS 1000 GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfigureAll, const std::function& okCallback) : GuiComponent(window), - mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 7)), + mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 7)), mTargetConfig(target), mHoldingInput(false), mBusyAnim(window) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + mGrid.setSeparatorColor(theme->Text.separatorColor); + LOG(LogInfo) << "Configuring device " << target->getDeviceId() << " (" << target->getDeviceName() << ")."; if(reconfigureAll) @@ -95,7 +101,7 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi // icon auto icon = std::make_shared(mWindow); icon->setImage(GUI_INPUT_CONFIG_LIST[i].icon); - icon->setColorShift(0x777777FF); + icon->setColorShift(ThemeData::getMenuTheme()->Text.color); icon->setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight() * 1.25f); row.addElement(icon, false); @@ -104,7 +110,7 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi spacer->setSize(16, 0); row.addElement(spacer, false); - auto text = std::make_shared(mWindow, GUI_INPUT_CONFIG_LIST[i].dispName, Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + auto text = std::make_shared(mWindow, GUI_INPUT_CONFIG_LIST[i].dispName, ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); row.addElement(text, true); auto mapping = std::make_shared(mWindow, "-NOT DEFINED-", Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT), 0x999999FF, ALIGN_RIGHT); @@ -258,7 +264,7 @@ void GuiInputConfig::update(int deltaTime) std::stringstream ss; ss << "HOLD FOR " << HOLD_TO_SKIP_MS/1000 - curSec << "S TO SKIP"; text->setText(ss.str()); - text->setColor(0x777777FF); + text->setColor(ThemeData::getMenuTheme()->Text.color); } } } @@ -301,7 +307,7 @@ void GuiInputConfig::setNotDefined(const std::shared_ptr& text) void GuiInputConfig::setAssignedTo(const std::shared_ptr& text, Input input) { text->setText(Utils::String::toUpper(input.string())); - text->setColor(0x777777FF); + text->setColor(ThemeData::getMenuTheme()->Text.color); } void GuiInputConfig::error(const std::shared_ptr& text, const std::string& /*msg*/) diff --git a/es-core/src/guis/GuiMsgBox.cpp b/es-core/src/guis/GuiMsgBox.cpp index 7f05603200..843a844a48 100644 --- a/es-core/src/guis/GuiMsgBox.cpp +++ b/es-core/src/guis/GuiMsgBox.cpp @@ -11,10 +11,16 @@ GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, const std::string& name3, const std::function& func3) : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 2)) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + float width = Renderer::getScreenWidth() * 0.6f; // max width float minWidth = Renderer::getScreenWidth() * 0.3f; // minimum width - mMsg = std::make_shared(mWindow, text, Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); + + mMsg = std::make_shared(mWindow, text, ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color, ALIGN_CENTER); mGrid.setEntry(mMsg, Vector2i(0, 0), false, false); // create the buttons diff --git a/es-core/src/guis/GuiTextEditPopup.cpp b/es-core/src/guis/GuiTextEditPopup.cpp index de7390cbb8..1244ff0871 100644 --- a/es-core/src/guis/GuiTextEditPopup.cpp +++ b/es-core/src/guis/GuiTextEditPopup.cpp @@ -8,6 +8,11 @@ GuiTextEditPopup::GuiTextEditPopup(Window* window, const std::string& title, con const std::function& okCallback, bool multiLine, const char* acceptBtnText) : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 3)), mMultiLine(multiLine) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + addChild(&mBackground); addChild(&mGrid); diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index 9890954a82..fe2a1b6a08 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -22,6 +22,7 @@ Font::FontFace::FontFace(ResourceData&& d, int size) : data(d) Font::FontFace::~FontFace() { + if(face) FT_Done_Face(face); } @@ -74,9 +75,12 @@ Font::Font(int size, const std::string& path) : mSize(size), mPath(path) mMaxGlyphHeight = 0; - if(!sLibrary) + if (!sLibrary) initLibrary(); + for (unsigned int i = 0; i < 255; i++) + mGlyphCacheArray[i] = NULL; + // always initialize ASCII characters for (unsigned int i = 32; i < 128; i++) getGlyph(i); @@ -88,6 +92,9 @@ Font::Font(int size, const std::string& path) : mSize(size), mPath(path) Font::~Font() { + for (auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); it++) + delete it->second; + unload(ResourceManager::getInstance()); } @@ -316,10 +323,20 @@ void Font::clearFaceCache() Font::Glyph* Font::getGlyph(unsigned int id) { - // is it already loaded? - auto it = mGlyphMap.find(id); - if (it != mGlyphMap.cend()) - return &it->second; + if (id < 255) + { + // FCA : optimisation : array is always really fastest than a map + Glyph* fastCache = mGlyphCacheArray[id]; + if (fastCache != NULL) + return fastCache; + } + else + { + // is it already loaded? + auto it = mGlyphMap.find(id); + if (it != mGlyphMap.cend()) + return it->second; + } // nope, need to make a glyph FT_Face face = getFaceForChar(id); @@ -351,15 +368,13 @@ Font::Glyph* Font::getGlyph(unsigned int id) } // create glyph - Glyph& glyph = mGlyphMap[id]; + Glyph* pGlyph = new Glyph(); - glyph.texture = tex; - - glyph.texPos = Vector2f(cursor.x() / (float)tex->textureSize.x(), cursor.y() / (float)tex->textureSize.y()); - glyph.texSize = Vector2f(glyphSize.x() / (float)tex->textureSize.x(), glyphSize.y() / (float)tex->textureSize.y()); - - glyph.advance = Vector2f((float)g->metrics.horiAdvance / 64.0f, (float)g->metrics.vertAdvance / 64.0f); - glyph.bearing = Vector2f((float)g->metrics.horiBearingX / 64.0f, (float)g->metrics.horiBearingY / 64.0f); + pGlyph->texture = tex; + pGlyph->texPos = Vector2f(cursor.x() / (float)tex->textureSize.x(), cursor.y() / (float)tex->textureSize.y()); + pGlyph->texSize = Vector2f(glyphSize.x() / (float)tex->textureSize.x(), glyphSize.y() / (float)tex->textureSize.y()); + pGlyph->advance = Vector2f((float)g->metrics.horiAdvance / 64.0f, (float)g->metrics.vertAdvance / 64.0f); + pGlyph->bearing = Vector2f((float)g->metrics.horiBearingX / 64.0f, (float)g->metrics.horiBearingY / 64.0f); // upload glyph bitmap to texture glBindTexture(GL_TEXTURE_2D, tex->textureId); @@ -370,8 +385,12 @@ Font::Glyph* Font::getGlyph(unsigned int id) if (id != 61446 && glyphSize.y() > mMaxGlyphHeight) mMaxGlyphHeight = glyphSize.y(); - // done - return &glyph; + mGlyphMap[id] = pGlyph; + + if (id < 255) + mGlyphCacheArray[id] = pGlyph; + + return pGlyph; } // completely recreate the texture data for all textures based on mGlyphs information @@ -379,9 +398,7 @@ void Font::rebuildTextures() { // recreate OpenGL textures for(auto it = mTextures.begin(); it != mTextures.end(); it++) - { it->initTexture(); - } // reupload the texture data for(auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); it++) @@ -392,11 +409,11 @@ void Font::rebuildTextures() // load the glyph bitmap through FT FT_Load_Char(face, it->first, FT_LOAD_RENDER); - FontTexture* tex = it->second.texture; + FontTexture* tex = it->second->texture; // find the position/size - Vector2i cursor((int)(it->second.texPos.x() * tex->textureSize.x()), (int)(it->second.texPos.y() * tex->textureSize.y())); - Vector2i glyphSize((int)(it->second.texSize.x() * tex->textureSize.x()), (int)(it->second.texSize.y() * tex->textureSize.y())); + Vector2i cursor((int)(it->second->texPos.x() * tex->textureSize.x()), (int)(it->second->texPos.y() * tex->textureSize.y())); + Vector2i glyphSize((int)(it->second->texSize.x() * tex->textureSize.x()), (int)(it->second->texSize.y() * tex->textureSize.y())); // upload to texture glBindTexture(GL_TEXTURE_2D, tex->textureId); @@ -708,18 +725,26 @@ void TextCache::setColor(unsigned int color) std::shared_ptr Font::getFromTheme(const ThemeData::ThemeElement* elem, unsigned int properties, const std::shared_ptr& orig) { using namespace ThemeFlags; - if(!(properties & FONT_PATH) && !(properties & FONT_SIZE)) + if (!(properties & FONT_PATH) && !(properties & FONT_SIZE)) return orig; - + std::shared_ptr font; int size = (orig ? orig->mSize : FONT_SIZE_MEDIUM); std::string path = (orig ? orig->mPath : getDefaultPath()); float sh = (float)Renderer::getScreenHeight(); - if(properties & FONT_SIZE && elem->has("fontSize")) - size = (int)(sh * elem->get("fontSize")); - if(properties & FONT_PATH && elem->has("fontPath")) - path = elem->get("fontPath"); + if (properties & FONT_SIZE && elem->has("fontSize")) + { + if ((int)(sh * elem->get("fontSize")) > 0) + size = (int)(sh * elem->get("fontSize")); + } + + if (properties & FONT_PATH && elem->has("fontPath")) + { + std::string tmppath = elem->get("fontPath"); + if (ResourceManager::getInstance()->fileExists(tmppath)) + path = tmppath; + } return get(size, path); } diff --git a/es-core/src/resources/Font.h b/es-core/src/resources/Font.h index 585c892461..c9c81af643 100644 --- a/es-core/src/resources/Font.h +++ b/es-core/src/resources/Font.h @@ -120,7 +120,11 @@ class Font : public IReloadable Vector2f bearing; }; - std::map mGlyphMap; + // used to cache 255 first chars + Glyph* mGlyphCacheArray[255]; + + // used to cache every char + std::map mGlyphMap; Glyph* getGlyph(unsigned int id); diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 6c472c79ed..48aae8932a 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -98,8 +98,10 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng return true; } - std::vector imageRGBA = ImageIO::loadFromMemoryRGBA32((const unsigned char*)(fileData), length, width, height); - if (imageRGBA.size() == 0) + unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height); + +// std::vector imageRGBA = ImageIO::loadFromMemoryRGBA32((const unsigned char*)(fileData), length, width, height); + if (imageRGBA == NULL) { LOG(LogError) << "Could not initialize texture from memory, invalid data! (file path: " << mPath << ", data ptr: " << (size_t)fileData << ", reported size: " << length << ")"; return false; @@ -109,7 +111,7 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng mSourceHeight = (float) height; mScalable = false; - return initFromRGBA(imageRGBA.data(), width, height); + return initFromRGBAEx(imageRGBA, width, height); } bool TextureData::initFromRGBA(const unsigned char* dataRGBA, size_t width, size_t height) @@ -127,6 +129,20 @@ bool TextureData::initFromRGBA(const unsigned char* dataRGBA, size_t width, size return true; } +bool TextureData::initFromRGBAEx(unsigned char* dataRGBA, size_t width, size_t height) +{ + // If already initialised then don't read again + std::unique_lock lock(mMutex); + if (mDataRGBA) + return true; + + // Take a copy + mDataRGBA = dataRGBA; + mWidth = width; + mHeight = height; + return true; +} + bool TextureData::load() { bool retval = false; diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 3ac6f239ef..2355d439a9 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -22,6 +22,7 @@ class TextureData bool initSVGFromMemory(const unsigned char* fileData, size_t length); bool initImageFromMemory(const unsigned char* fileData, size_t length); bool initFromRGBA(const unsigned char* dataRGBA, size_t width, size_t height); + bool initFromRGBAEx(unsigned char* dataRGBA, size_t width, size_t height); // Read the data into memory if necessary bool load(); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 3c77caed8b..150a6191a2 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -41,15 +41,114 @@ namespace Utils } // convertFromWideString #endif // _WIN32 - stringList getDirContent(const std::string& _path, const bool _recursive) + bool compareFileInfo(const FileInfo& first, const FileInfo& second) + { + unsigned int i = 0; + while ((i < first.path.length()) && (i < second.path.length())) + { + if (tolower(first.path[i]) < tolower(second.path[i])) return true; + else if (tolower(first.path[i]) > tolower(second.path[i])) return false; + ++i; + } + return (first.path.length() < second.path.length()); + } + + fileList getDirInfo(const std::string& _path, const bool _recursive) { std::string path = getGenericPath(_path); - stringList contentList; + fileList contentList; // only parse the directory, if it's a directory - if(isDirectory(path)) + if (isDirectory(path)) { +#if defined(_WIN32) + WIN32_FIND_DATAW findData; + std::string wildcard = path + "/*"; + HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); + + if (hFind != INVALID_HANDLE_VALUE) + { + // loop over all files in the directory + do + { + std::string name = convertFromWideString(findData.cFileName); + // ignore "." and ".." + if ((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + + FileInfo fi; + fi.path = fullName; + fi.readOnly = (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN; + fi.directory = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY; + contentList.push_back(fi); + + if (_recursive && fi.directory) + { + fileList fl = getDirInfo(fullName, true); + for (auto it = fl.cbegin(); it != fl.cend(); ++it) + contentList.push_back(*it); + } + } + } while (FindNextFileW(hFind, &findData)); + + FindClose(hFind); + } +#else // _WIN32 + DIR* dir = opendir(path.c_str()); + + if (dir != NULL) + { + struct dirent* entry; + + // loop over all files in the directory + while ((entry = readdir(dir)) != NULL) + { + std::string name(entry->d_name); + + // ignore "." and ".." + if ((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + + FileInfo fi; + fi.path = fullName; + fi.readOnly = Utils::FileSystem::isHidden(fullName); + fi.directory = isDirectory(fullName); + contentList.push_back(fi); + + if (_recursive && fi.directory) + { + fileList fl = getDirInfo(fullName, true); + for (auto it = fl.cbegin(); it != fl.cend(); ++it) + contentList.push_back(*it); + } + } + } + + closedir(dir); + } +#endif // _WIN32 + + } + + // sort the content list + // Why loose time -> It will be sorted later ???? contentList.sort(compareFileInfo); + + // return the content list + return contentList; + + } // getDirContent + + stringList getDirContent(const std::string& _path, const bool _recursive, const bool includeHidden) + { + std::string path = getGenericPath(_path); + stringList contentList; + + // only parse the directory, if it's a directory + if(isDirectory(path)) + { #if defined(_WIN32) WIN32_FIND_DATAW findData; std::string wildcard = path + "/*"; @@ -66,10 +165,14 @@ namespace Utils if((name != ".") && (name != "..")) { std::string fullName(getGenericPath(path + "/" + name)); + + if (!includeHidden && (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN) + continue; + contentList.push_back(fullName); - if(_recursive && isDirectory(fullName)) - contentList.merge(getDirContent(fullName, true)); + if(_recursive && (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) + contentList.merge(getDirContent(fullName, true, includeHidden)); } } while(FindNextFileW(hFind, &findData)); @@ -92,6 +195,10 @@ namespace Utils if((name != ".") && (name != "..")) { std::string fullName(getGenericPath(path + "/" + name)); + + if (!includeHidden && Utils::FileSystem::isHidden(fullName)) + continue; + contentList.push_back(fullName); if(_recursive && isDirectory(fullName)) @@ -106,7 +213,7 @@ namespace Utils } // sort the content list - contentList.sort(); +// Why loose time -> It will be sorted later ???? contentList.sort(); // return the content list return contentList; @@ -147,9 +254,9 @@ namespace Utils { // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows char* envHome = getenv("HOME"); - + envHome = "H:/[Emulz]/EmulationStation/"; #ifdef _DEBUG - // envHome = "H:/[Emulz]/EmulationStation/"; + envHome = "H:/[Emulz]/EmulationStation/"; #endif if (envHome) diff --git a/es-core/src/utils/FileSystemUtil.h b/es-core/src/utils/FileSystemUtil.h index 48c7f4f5ac..e8b01741c0 100644 --- a/es-core/src/utils/FileSystemUtil.h +++ b/es-core/src/utils/FileSystemUtil.h @@ -9,9 +9,19 @@ namespace Utils { namespace FileSystem { + struct FileInfo + { + public: + std::string path; + bool readOnly; + bool directory; + }; + typedef std::list stringList; + typedef std::list fileList; - stringList getDirContent (const std::string& _path, const bool _recursive = false); + fileList getDirInfo(const std::string& _path, const bool _recursive = false); + stringList getDirContent (const std::string& _path, const bool _recursive = false, const bool includeHidden = true); stringList getPathList (const std::string& _path); std::string getHomePath (); std::string getCWDPath (); @@ -40,6 +50,8 @@ namespace Utils } // FileSystem:: + + } // Utils:: #endif // ES_CORE_UTILS_FILE_SYSTEM_UTIL_H From 2c2994d076af7af6c3e9226aa95800c54d088b24 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 27 May 2019 13:39:13 +0200 Subject: [PATCH 009/336] [My Changes] --- .gitignore | 1 + es-app/src/CollectionSystemManager.cpp | 14 +- es-app/src/EmulationStation.h | 6 +- es-app/src/FileData.cpp | 118 +++- es-app/src/FileData.h | 9 + es-app/src/FileSorts.cpp | 4 +- es-app/src/Gamelist.cpp | 72 +- es-app/src/MetaData.cpp | 9 +- es-app/src/MetaData.h | 3 +- es-app/src/SystemData.cpp | 143 +++- es-app/src/SystemData.h | 56 +- .../src/guis/GuiCollectionSystemsOptions.cpp | 30 +- es-app/src/guis/GuiCollectionSystemsOptions.h | 2 +- es-app/src/guis/GuiGameScraper.cpp | 2 +- es-app/src/guis/GuiGamelistFilter.cpp | 8 +- es-app/src/guis/GuiGamelistOptions.cpp | 21 +- .../src/guis/GuiGeneralScreensaverOptions.cpp | 2 +- .../src/guis/GuiGeneralScreensaverOptions.h | 2 +- es-app/src/guis/GuiMenu.cpp | 348 ++++++---- es-app/src/guis/GuiMenu.h | 2 +- es-app/src/guis/GuiMetaDataEd.cpp | 163 ++++- es-app/src/guis/GuiMetaDataEd.h | 8 +- es-app/src/guis/GuiScraperMulti.cpp | 2 +- es-app/src/guis/GuiScraperStart.cpp | 8 +- es-app/src/guis/GuiScreensaverOptions.cpp | 8 +- es-app/src/guis/GuiScreensaverOptions.h | 2 +- es-app/src/guis/GuiSettings.cpp | 8 +- es-app/src/guis/GuiSettings.h | 2 +- es-app/src/main.cpp | 116 ++-- es-app/src/scrapers/ScreenScraper.cpp | 13 +- es-app/src/scrapers/ScreenScraper.h | 2 +- es-app/src/views/SystemView.cpp | 15 +- es-app/src/views/ViewController.cpp | 137 ++-- es-app/src/views/ViewController.h | 9 +- .../src/views/gamelist/BasicGameListView.cpp | 55 +- .../views/gamelist/DetailedGameListView.cpp | 52 +- .../src/views/gamelist/DetailedGameListView.h | 1 + .../src/views/gamelist/GridGameListView.cpp | 74 ++- es-app/src/views/gamelist/GridGameListView.h | 15 +- .../views/gamelist/ISimpleGameListView.cpp | 31 +- es-core/src/GuiComponent.cpp | 107 +++ es-core/src/GuiComponent.h | 53 ++ es-core/src/InputManager.cpp | 2 +- es-core/src/Renderer_init_sdlgl.cpp | 38 +- es-core/src/Scripting.cpp | 2 +- es-core/src/Settings.cpp | 9 +- es-core/src/ThemeData.cpp | 57 +- es-core/src/ThemeData.h | 10 +- es-core/src/Window.cpp | 41 +- es-core/src/Window.h | 1 + es-core/src/components/ComponentGrid.cpp | 6 +- es-core/src/components/ComponentList.cpp | 2 +- es-core/src/components/DateTimeComponent.cpp | 4 +- .../src/components/DateTimeEditComponent.cpp | 4 +- es-core/src/components/GridTileComponent.cpp | 188 +++++- es-core/src/components/GridTileComponent.h | 24 +- es-core/src/components/ImageComponent.cpp | 39 +- es-core/src/components/ImageGridComponent.h | 176 +++-- es-core/src/components/MenuComponent.cpp | 4 +- es-core/src/components/MenuComponent.h | 2 +- es-core/src/components/OptionListComponent.h | 45 +- es-core/src/components/SliderComponent.cpp | 2 +- es-core/src/components/SwitchComponent.cpp | 2 +- es-core/src/components/VideoComponent.cpp | 2 +- es-core/src/guis/GuiInputConfig.cpp | 22 +- es-core/src/guis/GuiInputConfig.h | 3 +- es-core/src/guis/GuiTextEditPopup.cpp | 2 +- es-core/src/platform.cpp | 103 ++- es-core/src/platform.h | 4 +- es-core/src/resources/Font.cpp | 8 +- es-core/src/resources/TextureData.cpp | 7 +- es-core/src/utils/FileSystemUtil.cpp | 30 +- es-core/src/utils/StringUtil.cpp | 629 +++++++++--------- es-core/src/utils/StringUtil.h | 4 + 74 files changed, 2242 insertions(+), 963 deletions(-) diff --git a/.gitignore b/.gitignore index 4055d624e6..19593e251e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ Makefile CPackConfig.cmake CPackSourceConfig.cmake *.cbp +/.vs diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 0ccdd9ee1d..bd6eb63e91 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -258,6 +258,9 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS if (name == "favorites" && file->metadata.get("favorite") == "false") { // need to check if still marked as favorite, if not remove ViewController::get()->getGameListView(curSys).get()->remove(collectionEntry, false); + + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); + ViewController::get()->getGameListView(curSys)->onFileChanged(collectionEntry, FILE_METADATA_CHANGED); } else { @@ -274,6 +277,7 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS CollectionFileData* newGame = new CollectionFileData(file, curSys); rootFolder->addChild(newGame); fileIndex->addToIndex(newGame); + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); ViewController::get()->getGameListView(curSys)->onFileChanged(newGame, FILE_METADATA_CHANGED); } @@ -453,6 +457,13 @@ void CollectionSystemManager::exitEditMode() // adds or removes a game from a specific collection bool CollectionSystemManager::toggleGameInCollection(FileData* file) { + /* + if (file->getType() == FOLDER) + { + if (file->findUniqueGameForFolder() == NULL) + return false; + } + */ if (file->getType() == GAME) { GuiInfoPopup* s; @@ -707,7 +718,8 @@ void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysDa for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) { // we won't iterate all collections - if ((*sysIt)->isGameSystem() && !(*sysIt)->isCollection()) { + if ((*sysIt)->isGameSystem() && !(*sysIt)->isCollection()) + { std::vector files = (*sysIt)->getRootFolder()->getFilesRecursive(GAME); for(auto gameIt = files.cbegin(); gameIt != files.cend(); gameIt++) { diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index 2bd27925bc..57389a1055 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -5,13 +5,13 @@ // 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 9 +#define PROGRAM_VERSION_MINOR 10 #define PROGRAM_VERSION_MAINTENANCE 0 -#define PROGRAM_VERSION_STRING "2.9.0rp-dev" +#define PROGRAM_VERSION_STRING "2.10.0 FCAMOD" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ -#define RESOURCE_VERSION_STRING "2,9,0\0" +#define RESOURCE_VERSION_STRING "2,10,0\0" #define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE #endif // ES_APP_EMULATION_STATION_H diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 16cdd164d3..532ea9a71d 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -22,7 +22,9 @@ FileData::FileData(FileType type, const std::string& path, SystemEnvironmentData // metadata needs at least a name field (since that's what getName() will return) if(metadata.get("name").empty()) metadata.set("name", getDisplayName()); - mSystemName = system->getName(); + + mSystemName = system->getName(); + // mDefaultCore = system->getSystemEnvData()->mDefaultCore; } FileData::~FileData() @@ -58,7 +60,7 @@ const std::string FileData::getThumbnailPath() const if(thumbnail.empty()) { thumbnail = metadata.get("image"); - + /* // no image, try to use local image if(thumbnail.empty() && Settings::getInstance()->getBool("LocalArt")) { @@ -72,12 +74,23 @@ const std::string FileData::getThumbnailPath() const thumbnail = path; } } - } + }*/ } return thumbnail; } +const bool FileData::getFavorite() +{ + return metadata.get("favorite") == "true"; +} + + +const bool FileData::getHidden() +{ + return metadata.get("hidden") == "true"; +} + const std::string& FileData::getName() { return metadata.get("name"); @@ -91,6 +104,26 @@ const std::string& FileData::getSortName() return metadata.get("sortname"); } +FileData* FileData::findUniqueGameForFolder() +{ + std::vector children = getChildren(); + + if (children.size() == 1 && children.at(0)->getType() == GAME) + return children.at(0); + + for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + if ((*it)->getType() == GAME) + return NULL; + + FileData* ret = (*it)->findUniqueGameForFolder(); + if (ret != NULL) + return ret; + } + + return NULL; +} + const std::vector& FileData::getChildrenListToDisplay() { FileFilterIndex* idx = CollectionSystemManager::get()->getSystemToView(mSystem)->getIndex(); @@ -111,10 +144,20 @@ const std::vector& FileData::getChildrenListToDisplay() { } } +const std::string FileData::getCore() const +{ + return metadata.get("core"); +} + +const std::string FileData::getEmulator() const +{ + return metadata.get("emulator"); +} + const std::string FileData::getVideoPath() const { std::string video = metadata.get("video"); - + /* // no video, try to use local video if(video.empty() && Settings::getInstance()->getBool("LocalArt")) { @@ -122,7 +165,7 @@ const std::string FileData::getVideoPath() const if(Utils::FileSystem::exists(path)) video = path; } - + */ return video; } @@ -199,12 +242,13 @@ std::string FileData::getKey() { const bool FileData::isArcadeAsset() { - const std::string stem = Utils::FileSystem::getStem(mPath); - return ( - (mSystem && (mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO))) - && - (MameNames::getInstance()->isBios(stem) || MameNames::getInstance()->isDevice(stem)) - ); + if (mSystem && (mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO))) + { + const std::string stem = Utils::FileSystem::getStem(mPath); + return MameNames::getInstance()->isBios(stem) || MameNames::getInstance()->isDevice(stem); + } + + return false; } FileData* FileData::getSourceFileData() @@ -271,44 +315,68 @@ void FileData::launchGame(Window* window) AudioManager::getInstance()->deinit(); VolumeControl::getInstance()->deinit(); - window->deinit(); + + bool hideWindow = Settings::getInstance()->getBool("HideWindow"); + if (hideWindow) + window->deinit(); std::string command = mEnvData->mLaunchCommand; - const std::string rom = Utils::FileSystem::getEscapedPath(getPath()); + const std::string rom = Utils::FileSystem::getEscapedPath(getPath()); const std::string basename = Utils::FileSystem::getStem(getPath()); - const std::string rom_raw = Utils::FileSystem::getPreferredPath(getPath()); + const std::string rom_raw = Utils::FileSystem::getPreferredPath(getPath()); + + std::string emulator = getEmulator(); + if (emulator.length() == 0) + emulator = mEnvData->getDefaultEmulator(); + + std::string core = getCore(); + if (core.length() == 0) + core = mEnvData->getDefaultCore(emulator); + + std::string customCommandLine = mEnvData->getEmulatorCommandLine(emulator); + if (customCommandLine.length() > 0) + command = customCommandLine; + + command = Utils::String::replace(command, "%EMULATOR%", emulator); + command = Utils::String::replace(command, "%CORE%", core); command = Utils::String::replace(command, "%ROM%", rom); command = Utils::String::replace(command, "%BASENAME%", basename); - command = Utils::String::replace(command, "%ROM_RAW%", rom_raw); + command = Utils::String::replace(command, "%ROM_RAW%", rom_raw); + command = Utils::String::replace(command, "%SYSTEM%", mSystemName); + command = Utils::String::replace(command, "%HOME%", Utils::FileSystem::getHomePath()); Scripting::fireEvent("game-start", rom, basename); LOG(LogInfo) << " " << command; - int exitCode = runSystemCommand(command); - if(exitCode != 0) + int exitCode = runSystemCommand(command, getDisplayName(), hideWindow ? NULL : window); + if (exitCode != 0) { LOG(LogWarning) << "...launch terminated with nonzero exit code " << exitCode << "!"; } Scripting::fireEvent("game-end"); - window->init(); + if (hideWindow) + window->init(); + VolumeControl::getInstance()->init(); window->normalizeNextUpdate(); //update number of times the game has been launched + if (exitCode == 0) + { + FileData* gameToUpdate = getSourceFileData(); - FileData* gameToUpdate = getSourceFileData(); - - int timesPlayed = gameToUpdate->metadata.getInt("playcount") + 1; - gameToUpdate->metadata.set("playcount", std::to_string(static_cast(timesPlayed))); + int timesPlayed = gameToUpdate->metadata.getInt("playcount") + 1; + gameToUpdate->metadata.set("playcount", std::to_string(static_cast(timesPlayed))); - //update last played time - gameToUpdate->metadata.set("lastplayed", Utils::Time::DateTime(Utils::Time::now())); - CollectionSystemManager::get()->refreshCollectionSystems(gameToUpdate); + //update last played time + gameToUpdate->metadata.set("lastplayed", Utils::Time::DateTime(Utils::Time::now())); + CollectionSystemManager::get()->refreshCollectionSystems(gameToUpdate); + } } CollectionFileData::CollectionFileData(FileData* file, SystemData* system) diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 63a9ce16e3..00fd4a923b 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -50,6 +50,14 @@ class FileData virtual const std::string getMarqueePath() const; virtual const std::string getImagePath() const; + virtual const std::string getCore() const; + virtual const std::string getEmulator() const; + + virtual const bool getHidden(); + virtual const bool getFavorite(); + + FileData* findUniqueGameForFolder(); + const std::vector& getChildrenListToDisplay(); std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; @@ -94,6 +102,7 @@ class FileData FileData* mSourceFileData; FileData* mParent; std::string mSystemName; + //std::string mDefaultCore; private: FileType mType; diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index 58b4ba90a1..5043656e11 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -17,8 +17,8 @@ namespace FileSorts FileData::SortType(&compareLastPlayed, true, "last played, ascending"), FileData::SortType(&compareLastPlayed, false, "last played, descending"), - FileData::SortType(&compareNumPlayers, true, "number players, ascending"), - FileData::SortType(&compareNumPlayers, false, "number players, descending"), + FileData::SortType(&compareNumPlayers, true, "number of players, ascending"), + FileData::SortType(&compareNumPlayers, false, "number of players, descending"), FileData::SortType(&compareReleaseDate, true, "release date, ascending"), FileData::SortType(&compareReleaseDate, false, "release date, descending"), diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 14af11757d..ea20baa94a 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -66,11 +66,13 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType LOG(LogWarning) << "gameList: folder doesn't already exist, won't create"; return NULL; } - + // create missing folder + + /* FCA TODO -> 1 seul jeu dans le folder ? FileData* folder = new FileData(FOLDER, Utils::FileSystem::getStem(treeNode->getPath()) + "/" + *path_it, system->getSystemEnvData(), system); treeNode->addChild(folder); - treeNode = folder; + treeNode = folder;*/ } path_it++; @@ -81,12 +83,12 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType void parseGamelist(SystemData* system) { - bool trustGamelist = Settings::getInstance()->getBool("ParseGamelistOnly"); std::string xmlpath = system->getGamelistPath(false); - - if(!Utils::FileSystem::exists(xmlpath)) + if (!Utils::FileSystem::exists(xmlpath)) return; + bool trustGamelist = Settings::getInstance()->getBool("ParseGamelistOnly"); + LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"..."; pugi::xml_document doc; @@ -107,39 +109,41 @@ void parseGamelist(SystemData* system) std::string relativeTo = system->getStartPath(); - const char* tagList[2] = { "game", "folder" }; - FileType typeList[2] = { GAME, FOLDER }; - for(int i = 0; i < 2; i++) + for (pugi::xml_node fileNode : root.children()) { - const char* tag = tagList[i]; - FileType type = typeList[i]; - for(pugi::xml_node fileNode = root.child(tag); fileNode; fileNode = fileNode.next_sibling(tag)) + FileType type = GAME; + + std::string tag = fileNode.name(); + + if (tag == "folder") + type = FOLDER; + else if (tag != "game") + continue; + + const std::string path = Utils::FileSystem::resolveRelativePath(fileNode.child("path").text().get(), relativeTo, false); + + if (!trustGamelist && !Utils::FileSystem::exists(path)) { - const std::string path = Utils::FileSystem::resolveRelativePath(fileNode.child("path").text().get(), relativeTo, false); - - if(!trustGamelist && !Utils::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; - } - else if(!file->isArcadeAsset()) - { - std::string defaultName = file->metadata.get("name"); - file->metadata = MetaDataList::createFromXML(GAME_METADATA, fileNode, relativeTo); + 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; + } + else if(!file->isArcadeAsset()) + { + 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); + //make sure name gets set if one didn't exist + if (file->metadata.get("name").empty()) + file->metadata.set("name", defaultName); - file->metadata.resetChangedFlag(); - } + file->metadata.resetChangedFlag(); } } } diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index 8a977e9509..3c9900d5a4 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -9,6 +9,10 @@ MetaDataDecl gameDecls[] = { {"name", MD_STRING, "", false, "name", "enter game name"}, {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, + + { "emulator", MD_PLIST, "", false, "emulator", "emulator" }, + { "core", MD_PLIST, "", false, "core", "core" }, + {"image", MD_PATH, "", false, "image", "enter path to image"}, {"video", MD_PATH , "", false, "video", "enter path to video"}, {"marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, @@ -40,7 +44,10 @@ MetaDataDecl folderDecls[] = { {"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"} + {"players", MD_INT, "1", false, "players", "enter number of players"}, + + { "favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on" }, + { "hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on" }, }; const std::vector folderMDD(folderDecls, folderDecls + sizeof(folderDecls) / sizeof(folderDecls[0])); diff --git a/es-app/src/MetaData.h b/es-app/src/MetaData.h index 6ed8f6d44c..7205511a2c 100644 --- a/es-app/src/MetaData.h +++ b/es-app/src/MetaData.h @@ -20,7 +20,8 @@ enum MetaDataType MD_PATH, MD_RATING, MD_DATE, - MD_TIME //used for lastplayed + MD_TIME, //used for lastplayed + MD_PLIST }; struct MetaDataDecl diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index d6e72cfd32..4052801b75 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -16,6 +16,8 @@ #include #endif +#include "Window.h" + std::vector SystemData::sSystemVector; SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : @@ -24,15 +26,19 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys mFilterIndex = new FileFilterIndex(); // if it's an actual system, initialize it, if not, just create the data structure - if(!CollectionSystem) + if (!CollectionSystem) { mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); mRootFolder->metadata.set("name", mFullName); - if(!Settings::getInstance()->getBool("ParseGamelistOnly")) + if (!Settings::getInstance()->getBool("ParseGamelistOnly")) + { populateFolder(mRootFolder); + if (mRootFolder->getChildren().size() == 0) + return; + } - if(!Settings::getInstance()->getBool("IgnoreGamelist")) + if (!Settings::getInstance()->getBool("IgnoreGamelist")) parseGamelist(this); mRootFolder->sort(FileSorts::SortTypes.at(0)); @@ -50,12 +56,6 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys SystemData::~SystemData() { - //save changed game data back to xml - if(!Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit") && !mIsCollectionSystem) - { - updateGamelist(this); - } - delete mRootFolder; delete mFilterIndex; } @@ -68,6 +68,12 @@ void SystemData::setIsGameSystemStatus() mIsGameSystem = (mName != "retropie"); } +char _easytolower(char in) { + if (in <= 'Z' && in >= 'A') + return in - ('Z' - 'z'); + return in; +} + void SystemData::populateFolder(FileData* folder) { const std::string& folderPath = folder->getPath(); @@ -77,6 +83,11 @@ void SystemData::populateFolder(FileData* folder) return; } + int di = folderPath.rfind("downloaded_images"); + int md = folderPath.rfind("media"); + if (di > 0 || md > 0) + return; + //make sure that this isn't a symlink to a thing we already have if(Utils::FileSystem::isSymlink(folderPath)) { @@ -87,7 +98,7 @@ void SystemData::populateFolder(FileData* folder) return; } } - + std::string filePath; std::string extension; bool isGame; @@ -104,6 +115,7 @@ void SystemData::populateFolder(FileData* folder) //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 = Utils::FileSystem::getExtension(filePath); + std::transform(extension.begin(), extension.end(), extension.begin(), ::_easytolower); //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 @@ -120,18 +132,23 @@ void SystemData::populateFolder(FileData* folder) isGame = true; } } - + //add directories that also do not match an extension as folders if(!isGame && Utils::FileSystem::isDirectory(filePath)) { - FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); - populateFolder(newFolder); - - //ignore folders that do not contain games - if(newFolder->getChildrenByFilename().size() == 0) - delete newFolder; - else - folder->addChild(newFolder); + if (filePath.rfind("downloaded_images") == std::string::npos && + filePath.rfind("media") == std::string::npos) + { + FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); + populateFolder(newFolder); + + if (newFolder->getChildrenByFilename().size() == 0) + delete newFolder; + else if (newFolder->findUniqueGameForFolder() != NULL) + delete newFolder; + else + folder->addChild(newFolder); + } } } } @@ -168,7 +185,7 @@ std::vector readList(const std::string& str, const char* delims = " } //creates systems from information located in a config file -bool SystemData::loadConfig() +bool SystemData::loadConfig(Window* window) { deleteSystems(); @@ -201,17 +218,65 @@ bool SystemData::loadConfig() LOG(LogError) << "es_systems.cfg is missing the tag!"; return false; } + + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { + std::vector emulatorList; + - for(pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - { - std::string name, fullname, path, cmd, themeFolder; + // std::vector coreList; + std::string name, fullname, path, cmd, themeFolder, defaultCore; + name = system.child("name").text().get(); fullname = system.child("fullname").text().get(); path = system.child("path").text().get(); + defaultCore = system.child("defaultCore").text().get(); + + pugi::xml_node emulators = system.child("emulators"); + if (emulators != NULL) + { + for (pugi::xml_node emulator : emulators.children()) + { + EmulatorData emulatorData; + emulatorData.mName = emulator.attribute("name").value(); + emulatorData.mCommandLine = emulator.attribute("command").value(); + + pugi::xml_node cores = emulator.child("cores"); + if (cores != NULL) + { + for (pugi::xml_node core : cores.children()) + { + const std::string& corename = core.text().get(); + + if (defaultCore.length() == 0) + defaultCore = corename; + + emulatorData.mCores.push_back(corename); + // coreList.push_back(corename); + } + } + + emulatorList.push_back(emulatorData); + } + } + + if (window != NULL) + window->renderLoadingScreen(fullname); // convert extensions list from a string into a vector of strings - std::vector extensions = readList(system.child("extension").text().get()); + + std::vector list = readList(system.child("extension").text().get()); + std::vector extensions; + + for (auto extension = list.cbegin(); extension != list.cend(); extension++) + { + std::string xt = (*extension); + std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); + + if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) + extensions.push_back(xt); + } cmd = system.child("command").text().get(); @@ -253,7 +318,7 @@ bool SystemData::loadConfig() path = Utils::FileSystem::getGenericPath(path); //expand home symbol if the startpath contains ~ - if(path[0] == '~') + if (path[0] == '~') { path.erase(0, 1); path.insert(0, Utils::FileSystem::getHomePath()); @@ -265,18 +330,25 @@ bool SystemData::loadConfig() envData->mSearchExtensions = extensions; envData->mLaunchCommand = cmd; envData->mPlatformIds = platformIds; + // envData->mDefaultCore = defaultCore; + envData->mEmulators = emulatorList; + + // envData->mCores = coreList; SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) { LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; delete newSys; - }else{ - sSystemVector.push_back(newSys); } + else + sSystemVector.push_back(newSys); } - CollectionSystemManager::get()->loadCollectionSystems(); + if (window != NULL) + window->renderLoadingScreen("Favoris"); + + CollectionSystemManager::get()->loadCollectionSystems(); return true; } @@ -328,10 +400,18 @@ void SystemData::writeExampleConfig(const std::string& path) void SystemData::deleteSystems() { + bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); + for(unsigned int i = 0; i < sSystemVector.size(); i++) { - delete sSystemVector.at(i); + SystemData* pData = sSystemVector.at(i); + + if (saveOnExit && !pData->mIsCollectionSystem) + updateGamelist(pData); + + delete pData; } + sSystemVector.clear(); } @@ -495,8 +575,9 @@ void SystemData::loadTheme() sysData.insert(std::pair("system.theme", getThemeFolder())); sysData.insert(std::pair("system.fullName", getFullName())); - mTheme->loadFile(sysData, path); - } catch(ThemeException& e) + mTheme->loadFile(getThemeFolder(), path); + } + catch(ThemeException& e) { LOG(LogError) << e.what(); mTheme = std::make_shared(); // reset to empty diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index ad0456b3c4..3925b74961 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -11,6 +11,14 @@ class FileData; class FileFilterIndex; class ThemeData; +class Window; + +struct EmulatorData +{ + std::string mName; + std::string mCommandLine; + std::vector mCores; +}; struct SystemEnvironmentData { @@ -18,6 +26,52 @@ struct SystemEnvironmentData std::vector mSearchExtensions; std::string mLaunchCommand; std::vector mPlatformIds; + +// std::string mDefaultCore; + + std::vector mEmulators; + + std::vector getCores(std::string emulatorName) + { + std::vector list; + + for (auto& emulator : mEmulators) + if (emulatorName == emulator.mName) + return emulator.mCores; + + return list; + } + + std::string getDefaultEmulator() + { + for (auto& emulator : mEmulators) + return emulator.mName; + + return ""; + } + + std::string getDefaultCore(std::string emulatorName) + { + for (auto& emulator : mEmulators) + { + if (emulatorName == emulator.mName) + { + for (auto core : emulator.mCores) + return core; + } + } + + return ""; + } + + std::string getEmulatorCommandLine(std::string emulatorName) + { + for (auto& emulator : mEmulators) + if (emulatorName == emulator.mName) + return emulator.mCommandLine; + + return ""; + } }; class SystemData @@ -46,7 +100,7 @@ class SystemData unsigned int getDisplayedGameCount() const; 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 bool loadConfig(Window* window); //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 diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index 27df3082eb..be30460aa4 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -9,7 +9,7 @@ #include "CollectionSystemManager.h" #include "Window.h" -GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) : GuiComponent(window), mMenu(window, "GAME COLLECTION SETTINGS") +GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) : GuiComponent(window), mMenu(window, _T("GAME COLLECTION SETTINGS")) { initializeMenu(); } @@ -27,9 +27,9 @@ void GuiCollectionSystemsOptions::initializeMenu() std::vector unusedFolders = CollectionSystemManager::get()->getUnusedSystemsFromTheme(); if (unusedFolders.size() > 0) { - addEntry("CREATE NEW CUSTOM COLLECTION FROM THEME", 0x777777FF, true, + addEntry(_T("CREATE NEW CUSTOM COLLECTION FROM THEME"), 0x777777FF, true, [this, unusedFolders] { - auto s = new GuiSettings(mWindow, "SELECT THEME FOLDER"); + auto s = new GuiSettings(mWindow, _T("SELECT THEME FOLDER")); std::shared_ptr< OptionListComponent > folderThemes = std::make_shared< OptionListComponent >(mWindow, "SELECT THEME FOLDER", true); // add Custom Systems @@ -52,7 +52,7 @@ void GuiCollectionSystemsOptions::initializeMenu() } ComponentListRow row; - row.addElement(std::make_shared(mWindow, "CREATE NEW CUSTOM COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("CREATE NEW CUSTOM COLLECTION"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); auto createCustomCollection = [this](const std::string& newVal) { std::string name = newVal; // we need to store the first Gui and remove it, as it'll be deleted by the actual Gui @@ -62,33 +62,33 @@ void GuiCollectionSystemsOptions::initializeMenu() createCollection(name); }; row.makeAcceptInputHandler([this, createCustomCollection] { - mWindow->pushGui(new GuiTextEditPopup(mWindow, "New Collection Name", "", createCustomCollection, false)); + mWindow->pushGui(new GuiTextEditPopup(mWindow, _T("New Collection Name"), "", createCustomCollection, false)); }); mMenu.addRow(row); bundleCustomCollections = std::make_shared(mWindow); bundleCustomCollections->setState(Settings::getInstance()->getBool("UseCustomCollectionsSystem")); - mMenu.addWithLabel("GROUP UNTHEMED CUSTOM COLLECTIONS", bundleCustomCollections); + mMenu.addWithLabel(_T("GROUP UNTHEMED CUSTOM COLLECTIONS"), bundleCustomCollections); sortAllSystemsSwitch = std::make_shared(mWindow); sortAllSystemsSwitch->setState(Settings::getInstance()->getBool("SortAllSystems")); - mMenu.addWithLabel("SORT CUSTOM COLLECTIONS AND SYSTEMS", sortAllSystemsSwitch); + mMenu.addWithLabel(_T("SORT CUSTOM COLLECTIONS AND SYSTEMS"), sortAllSystemsSwitch); if(CollectionSystemManager::get()->isEditing()) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("FINISH EDITING COLLECTION")+" '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "'", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.makeAcceptInputHandler(std::bind(&GuiCollectionSystemsOptions::exitEditMode, this)); mMenu.addRow(row); } - mMenu.addButton("BACK", "back", std::bind(&GuiCollectionSystemsOptions::applySettings, this)); + mMenu.addButton(_T("BACK"), _T("BACK"), std::bind(&GuiCollectionSystemsOptions::applySettings, this)); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); } -void GuiCollectionSystemsOptions::addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func) +void GuiCollectionSystemsOptions::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) { std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); @@ -139,25 +139,25 @@ void GuiCollectionSystemsOptions::addSystemsToMenu() std::map autoSystems = CollectionSystemManager::get()->getAutoCollectionSystems(); - autoOptionList = std::make_shared< OptionListComponent >(mWindow, "SELECT COLLECTIONS", true); + autoOptionList = std::make_shared< OptionListComponent >(mWindow, _T("SELECT COLLECTIONS"), true); // add Auto Systems for(std::map::const_iterator it = autoSystems.cbegin() ; it != autoSystems.cend() ; it++ ) { autoOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); } - mMenu.addWithLabel("AUTOMATIC GAME COLLECTIONS", autoOptionList); + mMenu.addWithLabel(_T("AUTOMATIC GAME COLLECTIONS"), autoOptionList); std::map customSystems = CollectionSystemManager::get()->getCustomCollectionSystems(); - customOptionList = std::make_shared< OptionListComponent >(mWindow, "SELECT COLLECTIONS", true); + customOptionList = std::make_shared< OptionListComponent >(mWindow, _T("SELECT COLLECTIONS"), true); // add Custom Systems for(std::map::const_iterator it = customSystems.cbegin() ; it != customSystems.cend() ; it++ ) { customOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); } - mMenu.addWithLabel("CUSTOM GAME COLLECTIONS", customOptionList); + mMenu.addWithLabel(_T("CUSTOM GAME COLLECTIONS"), customOptionList); } void GuiCollectionSystemsOptions::applySettings() @@ -209,6 +209,6 @@ bool GuiCollectionSystemsOptions::input(InputConfig* config, Input input) std::vector GuiCollectionSystemsOptions::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); return prompts; } diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.h b/es-app/src/guis/GuiCollectionSystemsOptions.h index fc6113e0a2..0187920630 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.h +++ b/es-app/src/guis/GuiCollectionSystemsOptions.h @@ -22,7 +22,7 @@ class GuiCollectionSystemsOptions : public GuiComponent void initializeMenu(); void applySettings(); void addSystemsToMenu(); - void addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func); + void addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func); void updateSettings(std::string newAutoSettings, std::string newCustomSettings); void createCollection(std::string inName); void exitEditMode(); diff --git a/es-app/src/guis/GuiGameScraper.cpp b/es-app/src/guis/GuiGameScraper.cpp index d8ebcb7160..c94eab6630 100644 --- a/es-app/src/guis/GuiGameScraper.cpp +++ b/es-app/src/guis/GuiGameScraper.cpp @@ -42,7 +42,7 @@ GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std:: mSearch->openInputScreen(mSearchParams); mGrid.resetCursor(); })); - buttons.push_back(std::make_shared(mWindow, "CANCEL", "cancel", [&] { delete this; })); + buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); mButtonGrid = makeButtonGrid(mWindow, buttons); mGrid.setEntry(mButtonGrid, Vector2i(0, 6), true, false); diff --git a/es-app/src/guis/GuiGamelistFilter.cpp b/es-app/src/guis/GuiGamelistFilter.cpp index 8a3625a1e3..1216775439 100644 --- a/es-app/src/guis/GuiGamelistFilter.cpp +++ b/es-app/src/guis/GuiGamelistFilter.cpp @@ -21,14 +21,14 @@ void GuiGamelistFilter::initializeMenu() // show filtered menu row.elements.clear(); - row.addElement(std::make_shared(mWindow, "RESET ALL FILTERS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("RESET ALL FILTERS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistFilter::resetAllFilters, this)); mMenu.addRow(row); row.elements.clear(); addFiltersToMenu(); - mMenu.addButton("BACK", "back", std::bind(&GuiGamelistFilter::applyFilters, this)); + mMenu.addButton(_T("BACK"), _T("BACK"), std::bind(&GuiGamelistFilter::applyFilters, this)); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); } @@ -61,7 +61,7 @@ void GuiGamelistFilter::addFiltersToMenu() FilterIndexType type = (*it).type; // type of filter std::map* allKeys = (*it).allIndexKeys; // all possible filters for this type - std::string menuLabel = (*it).menuLabel; // text to show in menu + std::string menuLabel = _L((*it).menuLabel); // text to show in menu std::shared_ptr< OptionListComponent > optionList; @@ -112,6 +112,6 @@ bool GuiGamelistFilter::input(InputConfig* config, Input input) std::vector GuiGamelistFilter::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); return prompts; } diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 7510f0c5c5..b789667da8 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -34,7 +34,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if(curChar < startChar || curChar > endChar) curChar = startChar; - mJumpToLetterList = std::make_shared(mWindow, "JUMP TO...", false); + mJumpToLetterList = std::make_shared(mWindow, _T("JUMP TO LETTER"), false); for (char c = startChar; c <= endChar; c++) { // check if c is a valid first letter in current list @@ -50,9 +50,10 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui } } - row.addElement(std::make_shared(mWindow, "JUMP TO...", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("JUMP TO LETTER"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.addElement(mJumpToLetterList, false); - row.input_handler = [&](InputConfig* config, Input input) { + row.input_handler = [&](InputConfig* config, Input input) + { if(config->isMappedTo("a", input) && input.value) { jumpToLetter(); @@ -67,20 +68,20 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui mMenu.addRow(row); // sort list by - mListSort = std::make_shared(mWindow, "SORT GAMES BY", false); + mListSort = std::make_shared(mWindow, _T("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 + mListSort->add(_L(Utils::String::toUpper(sort.description)), &sort, i == 0); // TODO - actually make the sort type persistent } - mMenu.addWithLabel("SORT GAMES BY", mListSort); + mMenu.addWithLabel(_T("SORT GAMES BY"), mListSort); } // show filtered menu if(!Settings::getInstance()->getBool("ForceDisableFilters")) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FILTER GAMELIST", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("APPLY FILTER"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); mMenu.addRow(row); @@ -109,7 +110,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "EDIT THIS GAME'S METADATA", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("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); @@ -198,7 +199,7 @@ void GuiGamelistOptions::openMetaDataEd() } mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(), p, Utils::FileSystem::getFileName(file->getPath()), - std::bind(&IGameListView::onFileChanged, ViewController::get()->getGameListView(file->getSystem()).get(), file, FILE_METADATA_CHANGED), deleteBtnFunc)); + std::bind(&IGameListView::onFileChanged, ViewController::get()->getGameListView(file->getSystem()).get(), file, FILE_METADATA_CHANGED), deleteBtnFunc, file)); } void GuiGamelistOptions::jumpToLetter() @@ -257,7 +258,7 @@ HelpStyle GuiGamelistOptions::getHelpStyle() std::vector GuiGamelistOptions::getHelpPrompts() { auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "close")); + prompts.push_back(HelpPrompt("b", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp index 30688c566c..e229aa87c6 100644 --- a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp +++ b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp @@ -8,7 +8,7 @@ #include "guis/GuiVideoScreensaverOptions.h" #include "Settings.h" -GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, const char* title) : GuiScreensaverOptions(window, title) +GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, std::string title) : GuiScreensaverOptions(window, title) { // screensaver time auto screensaver_time = std::make_shared(mWindow, 0.f, 30.f, 1.f, "m"); diff --git a/es-app/src/guis/GuiGeneralScreensaverOptions.h b/es-app/src/guis/GuiGeneralScreensaverOptions.h index 1992d9ad15..1a850e3871 100644 --- a/es-app/src/guis/GuiGeneralScreensaverOptions.h +++ b/es-app/src/guis/GuiGeneralScreensaverOptions.h @@ -7,7 +7,7 @@ class GuiGeneralScreensaverOptions : public GuiScreensaverOptions { public: - GuiGeneralScreensaverOptions(Window* window, const char* title); + GuiGeneralScreensaverOptions(Window* window, std::string title); virtual ~GuiGeneralScreensaverOptions(); private: diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 59d8ae4150..4b1cf2aea6 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -19,32 +19,35 @@ #include #include -GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, "MAIN MENU"), mVersion(window) +GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) + { bool isFullUI = UIModeController::getInstance()->isUIModeFull(); - if (isFullUI) - addEntry("SCRAPER", 0x777777FF, true, [this] { openScraperSettings(); }); - - addEntry("SOUND SETTINGS", 0x777777FF, true, [this] { openSoundSettings(); }); - if (isFullUI) - addEntry("UI SETTINGS", 0x777777FF, true, [this] { openUISettings(); }); + { + addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); + addEntry(_T("CONFIGURE INPUT"), 0x777777FF, true, [this] { openConfigInput(); }); + } - if (isFullUI) - addEntry("GAME COLLECTION SETTINGS", 0x777777FF, true, [this] { openCollectionSystemSettings(); }); + addEntry(_T("SOUND SETTINGS"), 0x777777FF, true, [this] { openSoundSettings(); }); if (isFullUI) - addEntry("OTHER SETTINGS", 0x777777FF, true, [this] { openOtherSettings(); }); + addEntry(_T("SCRAPER"), 0x777777FF, true, [this] { openScraperSettings(); }); if (isFullUI) - addEntry("CONFIGURE INPUT", 0x777777FF, true, [this] { openConfigInput(); }); + { + addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); + addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); + } - addEntry("QUIT", 0x777777FF, true, [this] {openQuitMenu(); }); + + addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); addChild(&mMenu); addVersionInfo(); + setSize(mMenu.getSize()); setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, Renderer::getScreenHeight() * 0.15f); } @@ -88,12 +91,12 @@ void GuiMenu::openScraperSettings() void GuiMenu::openSoundSettings() { - auto s = new GuiSettings(mWindow, "SOUND SETTINGS"); - + auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); + // volume auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); volume->setValue((float)VolumeControl::getInstance()->getVolume()); - s->addWithLabel("SYSTEM VOLUME", volume); + s->addWithLabel(_T("SYSTEM VOLUME"), volume); s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); if (UIModeController::getInstance()->isUIModeFull()) @@ -154,7 +157,7 @@ void GuiMenu::openSoundSettings() // disable sounds auto sounds_enabled = std::make_shared(mWindow); sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); - s->addWithLabel("ENABLE NAVIGATION SOUNDS", sounds_enabled); + s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); s->addSaveFunc([sounds_enabled] { if (sounds_enabled->getState() && !Settings::getInstance()->getBool("EnableSounds") @@ -168,7 +171,7 @@ void GuiMenu::openSoundSettings() auto video_audio = std::make_shared(mWindow); video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); - s->addWithLabel("ENABLE VIDEO AUDIO", video_audio); + s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); #ifdef _RPI_ @@ -202,72 +205,95 @@ void GuiMenu::openSoundSettings() void GuiMenu::openUISettings() { - auto s = new GuiSettings(mWindow, "UI SETTINGS"); + auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); - //UI mode - auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); - std::vector UImodes = UIModeController::getInstance()->getUIModes(); - for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) - UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); - s->addWithLabel("UI MODE", UImodeSelection); - Window* window = mWindow; - s->addSaveFunc([ UImodeSelection, window] + // theme set + auto themeSets = ThemeData::getThemeSets(); + + if (!themeSets.empty()) { - std::string selectedMode = UImodeSelection->getSelected(); - if (selectedMode != "Full") + std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if (selectedSet == themeSets.cend()) + selectedSet = themeSets.cbegin(); + + auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); + for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) + theme_set->add(it->first, it->first, it == selectedSet); + s->addWithLabel(_T("THEME"), theme_set); + + Window* window = mWindow; + s->addSaveFunc([window, theme_set] { - std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; - msg += "This will hide most menu-options to prevent changes to the system.\n"; - msg += "To unlock and return to the full UI, enter this code: \n"; - msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; - msg += "Do you want to proceed?"; - window->pushGui(new GuiMsgBox(window, msg, - "YES", [selectedMode] { - LOG(LogDebug) << "Setting UI mode to " << selectedMode; - Settings::getInstance()->setString("UIMode", selectedMode); - Settings::getInstance()->saveFile(); - }, "NO",nullptr)); - } - }); + bool needReload = false; + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + if (oldTheme != theme_set->getSelected()) + needReload = true; - // screensaver - ComponentListRow screensaver_row; - screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, "SCREENSAVER SETTINGS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - screensaver_row.addElement(makeArrow(mWindow), false); - screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); - s->addRow(screensaver_row); + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - // quick system select (left/right in game list view) - auto quick_sys_select = std::make_shared(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()); }); + if (needReload) + { + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + } + }); + } - // carousel transition option - auto move_carousel = std::make_shared(mWindow); - move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); - s->addWithLabel("CAROUSEL TRANSITIONS", move_carousel); - s->addSaveFunc([move_carousel] { - if (move_carousel->getState() - && !Settings::getInstance()->getBool("MoveCarousel") - && PowerSaver::getMode() == PowerSaver::INSTANT) + // LANGUAGE + + std::vector langues; + langues.push_back("en"); + + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); + if (xmlpath.length() > 0) + { + xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); + if (Utils::FileSystem::isDirectory(*it)) + continue; + + std::string name = *it; + + if (name.rfind("emulationstation2.po") == std::string::npos) + continue; + + name = Utils::FileSystem::getParent(name); + name = Utils::FileSystem::getFileName(name); + + if (name != "en") + langues.push_back(name); } - Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); - }); + + if (langues.size() > 1) + { + auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); + + for (auto it = langues.cbegin(); it != langues.cend(); it++) + language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); + + s->addWithLabel(_T("LANGUAGE"), language); + s->addSaveFunc([language] { + Settings::getInstance()->setString("Language", language->getSelected()); + }); + } + } // transition style - auto transition_style = std::make_shared< OptionListComponent >(mWindow, "TRANSITION STYLE", false); + auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); std::vector transitions; transitions.push_back("fade"); transitions.push_back("slide"); transitions.push_back("instant"); - for(auto it = transitions.cbegin(); it != transitions.cend(); it++) - transition_style->add(*it, *it, Settings::getInstance()->getString("TransitionStyle") == *it); - s->addWithLabel("TRANSITION STYLE", transition_style); + for (auto it = transitions.cbegin(); it != transitions.cend(); it++) + transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); + + s->addWithLabel(_T("TRANSITION STYLE"), transition_style); s->addSaveFunc([transition_style] { if (Settings::getInstance()->getString("TransitionStyle") == "instant" && transition_style->getSelected() != "instant" @@ -279,52 +305,41 @@ void GuiMenu::openUISettings() Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); }); - // theme set - auto themeSets = ThemeData::getThemeSets(); - - if(!themeSets.empty()) - { - std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(selectedSet == themeSets.cend()) - selectedSet = themeSets.cbegin(); - auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME SET", false); - for(auto it = themeSets.cbegin(); it != themeSets.cend(); it++) - theme_set->add(it->first, it->first, it == selectedSet); - s->addWithLabel("THEME SET", theme_set); + auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); + std::vector gameTransitions; + gameTransitions.push_back("fade"); + gameTransitions.push_back("slide"); + gameTransitions.push_back("instant"); + for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) + transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); - Window* window = mWindow; - s->addSaveFunc([window, theme_set] + s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); + s->addSaveFunc([transitionOfGames_style] { + if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" + && transitionOfGames_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) { - bool needReload = false; - std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - if(oldTheme != theme_set->getSelected()) - needReload = true; - - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - - if(needReload) - { - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation - } - }); - } + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); + }); // GameList view style - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, "GAMELIST VIEW STYLE", false); + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); std::vector styles; styles.push_back("automatic"); styles.push_back("basic"); styles.push_back("detailed"); styles.push_back("video"); styles.push_back("grid"); + styles.push_back("gridex"); for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(*it, *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); - s->addWithLabel("GAMELIST VIEW STYLE", gamelist_style); + gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); + + s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); s->addSaveFunc([gamelist_style] { bool needReload = false; if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) @@ -334,9 +349,10 @@ void GuiMenu::openUISettings() ViewController::get()->reloadAll(); }); + // Optionally start in selected system - auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, "START ON SYSTEM", false); - systemfocus_list->add("NONE", "", Settings::getInstance()->getString("StartupSystem") == ""); + auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); + systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { if ("retropie" != (*it)->getName()) @@ -344,21 +360,87 @@ void GuiMenu::openUISettings() systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); } } - s->addWithLabel("START ON SYSTEM", systemfocus_list); + s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); s->addSaveFunc([systemfocus_list] { Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); }); + + /* + //UI mode + auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); + std::vector UImodes = UIModeController::getInstance()->getUIModes(); + for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) + UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); + s->addWithLabel("UI MODE", UImodeSelection); + Window* window = mWindow; + s->addSaveFunc([ UImodeSelection, window] + { + std::string selectedMode = UImodeSelection->getSelected(); + if (selectedMode != "Full") + { + std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; + msg += "This will hide most menu-options to prevent changes to the system.\n"; + msg += "To unlock and return to the full UI, enter this code: \n"; + msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; + msg += "Do you want to proceed?"; + window->pushGui(new GuiMsgBox(window, msg, + "YES", [selectedMode] { + LOG(LogDebug) << "Setting UI mode to " << selectedMode; + Settings::getInstance()->setString("UIMode", selectedMode); + Settings::getInstance()->saveFile(); + }, "NO",nullptr)); + } + }); + */ + + // screensaver + ComponentListRow screensaver_row; + screensaver_row.elements.clear(); + screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + screensaver_row.addElement(makeArrow(mWindow), false); + screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); + s->addRow(screensaver_row); + + + // quick system select (left/right in game list view) + auto hideWindowScreen = std::make_shared(mWindow); + hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); + s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); + s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); + + + // quick system select (left/right in game list view) + auto quick_sys_select = std::make_shared(mWindow); + quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); + s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); + s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); + + // carousel transition option + auto move_carousel = std::make_shared(mWindow); + move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); + s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); + s->addSaveFunc([move_carousel] { + if (move_carousel->getState() + && !Settings::getInstance()->getBool("MoveCarousel") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); + }); + // show help auto show_help = std::make_shared(mWindow); show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); - s->addWithLabel("ON-SCREEN HELP", show_help); + s->addWithLabel(_T("ON-SCREEN HELP"), show_help); s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); // enable filters (ForceDisableFilters) auto enable_filter = std::make_shared(mWindow); enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); - s->addWithLabel("ENABLE FILTERS", enable_filter); + s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); s->addSaveFunc([enable_filter] { bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); @@ -371,16 +453,16 @@ void GuiMenu::openUISettings() void GuiMenu::openOtherSettings() { - auto s = new GuiSettings(mWindow, "OTHER SETTINGS"); + auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); // maximum vram - auto max_vram = std::make_shared(mWindow, 0.f, 1000.f, 10.f, "Mb"); + auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel("VRAM LIMIT", max_vram); + s->addWithLabel(_T("VRAM LIMIT"), max_vram); s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); // power saver - auto power_saver = std::make_shared< OptionListComponent >(mWindow, "POWER SAVER MODES", false); + auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); std::vector modes; modes.push_back("disabled"); modes.push_back("default"); @@ -388,7 +470,8 @@ void GuiMenu::openOtherSettings() modes.push_back("instant"); for (auto it = modes.cbegin(); it != modes.cend(); it++) power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); - s->addWithLabel("POWER SAVER MODES", power_saver); + + s->addWithLabel(_T("POWER SAVER MODES"), power_saver); s->addSaveFunc([this, power_saver] { if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { Settings::getInstance()->setString("TransitionStyle", "instant"); @@ -402,23 +485,23 @@ void GuiMenu::openOtherSettings() // gamelists auto save_gamelists = std::make_shared(mWindow); save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); - s->addWithLabel("SAVE METADATA ON EXIT", save_gamelists); + s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); auto parse_gamelists = std::make_shared(mWindow); parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); - s->addWithLabel("PARSE GAMESLISTS ONLY", parse_gamelists); + s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); - + /* auto local_art = std::make_shared(mWindow); local_art->setState(Settings::getInstance()->getBool("LocalArt")); - s->addWithLabel("SEARCH FOR LOCAL ART", local_art); + s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); - + */ // hidden files auto hidden_files = std::make_shared(mWindow); hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); - s->addWithLabel("SHOW HIDDEN FILES", hidden_files); + s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); #ifdef _RPI_ @@ -442,11 +525,12 @@ void GuiMenu::openOtherSettings() #endif // framerate + /* auto framerate = std::make_shared(mWindow); framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); - s->addWithLabel("SHOW FRAMERATE", framerate); + s->addWithLabel(_T("SHOW FRAMERATE"), framerate); s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); - + */ mWindow->pushGui(s); @@ -455,17 +539,23 @@ void GuiMenu::openOtherSettings() void GuiMenu::openConfigInput() { Window* window = mWindow; - window->pushGui(new GuiMsgBox(window, "ARE YOU SURE YOU WANT TO CONFIGURE INPUT?", "YES", + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + /* + window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), [window] { window->pushGui(new GuiDetectDevice(window, false, nullptr)); - }, "NO", nullptr) - ); + }, _T("NO"), nullptr) + );*/ } void GuiMenu::openQuitMenu() { - auto s = new GuiSettings(mWindow, "QUIT"); + Scripting::fireEvent("quit"); + quitES(""); + return; + + auto s = new GuiSettings(mWindow, _T("QUIT")); Window* window = mWindow; @@ -540,7 +630,7 @@ void GuiMenu::addVersionInfo() } void GuiMenu::openScreensaverOptions() { - mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, "SCREENSAVER SETTINGS")); + mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); } void GuiMenu::openCollectionSystemSettings() { @@ -553,7 +643,7 @@ void GuiMenu::onSizeChanged() mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); } -void GuiMenu::addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func) +void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) { std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); @@ -596,8 +686,8 @@ HelpStyle GuiMenu::getHelpStyle() std::vector GuiMenu::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("up/down", "choose")); - prompts.push_back(HelpPrompt("a", "select")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("SELECT"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiMenu.h b/es-app/src/guis/GuiMenu.h index da8406bb79..25fdcc2634 100644 --- a/es-app/src/guis/GuiMenu.h +++ b/es-app/src/guis/GuiMenu.h @@ -16,7 +16,7 @@ class GuiMenu : public GuiComponent HelpStyle getHelpStyle() override; private: - void addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func); + void addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func); void addVersionInfo(); void openCollectionSystemSettings(); void openConfigInput(); diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index f07a5061e6..5f0df4f525 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -20,7 +20,7 @@ #include "Window.h" GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, - const std::string& /*header*/, std::function saveCallback, std::function deleteFunc) : GuiComponent(window), + const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : GuiComponent(window), mScraperParams(scraperParams), mBackground(window, ":/frame.png"), @@ -35,9 +35,10 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector mHeaderGrid = std::make_shared(mWindow, Vector2i(1, 5)); - mTitle = std::make_shared(mWindow, "EDIT METADATA", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); + mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER); + mHeaderGrid->setEntry(mTitle, Vector2i(0, 1), false, true); mHeaderGrid->setEntry(mSubtitle, Vector2i(0, 3), false, true); @@ -46,6 +47,11 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector mList = std::make_shared(mWindow); mGrid.setEntry(mList, Vector2i(0, 1), true, true); + SystemData* system = file->getSystem(); + + auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); + auto core_choice = std::make_shared>(mWindow, _T("CORE"), false); + // populate list for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) { @@ -55,21 +61,104 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector if(iter->isStatistic) continue; + if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || + iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || + iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") + 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(mWindow, Utils::String::toUpper(iter->displayName), Font::get(FONT_SIZE_SMALL), 0x777777FF); + + if (iter->displayName == "emulator") + { + // if (file->getSystemEnvData()->mEmulators.size() <= 1) + // continue; + + std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); + + std::string currentEmul = file->getEmulator(); + + if (defaultEmul.length() == 0) + emul_choice->add(_T("DEFAULT"), "", false); + else + emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); + + for (auto core : file->getSystemEnvData()->mEmulators) + emul_choice->add(core.mName, core.mName, core.mName == currentEmul); + + row.addElement(std::make_shared(mWindow, _T("EMULATOR"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(emul_choice, false); + + mList->addRow(row); + emul_choice->setTag(iter->key); + mEditors.push_back(emul_choice); + + emul_choice->setSelectedChangedCallback([this, system, core_choice, file](std::string emulatorName) + { + std::string currentCore = file->getCore(); + + std::string defaultCore = system->getSystemEnvData()->getDefaultCore(emulatorName); + if (emulatorName.length() == 0) + defaultCore = system->getSystemEnvData()->getDefaultCore(system->getSystemEnvData()->getDefaultEmulator()); + + core_choice->clear(); + if (defaultCore.length() == 0) + core_choice->add(_T("DEFAULT"), "", false); + else + core_choice->add(_T("DEFAULT")+" ("+ defaultCore+")", "", false); + + std::vector cores = system->getSystemEnvData()->getCores(emulatorName); + + bool found = false; + + for (auto it = cores.begin(); it != cores.end(); it++) + { + std::string core = *it; + core_choice->add(core, core, currentCore == core); + if (currentCore == core) + found = true; + } + + if (!found) + core_choice->selectFirstItem(); + else + core_choice->invalidate(); + }); + + continue; + } + + if (iter->displayName == "core") + { + // core_choice->add(_T("DEFAULT"), "", true); + core_choice->setTag(iter->key); + + row.addElement(std::make_shared(mWindow, "CORE", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(core_choice, false); + + mList->addRow(row); + ed = core_choice; + + mEditors.push_back(core_choice); + + // force change event to load core list + emul_choice->invalidate(); + continue; + } + + auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); row.addElement(lbl, true); // label - switch(iter->type) + switch (iter->type) { - case MD_BOOL: + case MD_BOOL: { ed = std::make_shared(window); row.addElement(ed, false, true); break; } - case MD_RATING: + case MD_RATING: { ed = std::make_shared(window); const float height = lbl->getSize().y() * 0.71f; @@ -85,7 +174,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector break; } - case MD_DATE: + case MD_DATE: { ed = std::make_shared(window); row.addElement(ed, false); @@ -99,14 +188,14 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector break; } - case MD_TIME: + case MD_TIME: { ed = std::make_shared(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); row.addElement(ed, false); break; } - case MD_MULTILINE_STRING: - default: + case MD_MULTILINE_STRING: + default: { // MD_STRING ed = std::make_shared(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); @@ -129,27 +218,30 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector }); break; } - } + } assert(ed); mList->addRow(row); + + ed->setTag(iter->key); ed->setValue(mMetaData->get(iter->key)); + mEditors.push_back(ed); } std::vector< std::shared_ptr > buttons; - if(!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) - buttons.push_back(std::make_shared(mWindow, "SCRAPE", "scrape", std::bind(&GuiMetaDataEd::fetch, this))); + if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) + buttons.push_back(std::make_shared(mWindow, _T("SCRAPE"), _T("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); - buttons.push_back(std::make_shared(mWindow, "SAVE", "save", [&] { save(); delete this; })); - buttons.push_back(std::make_shared(mWindow, "CANCEL", "cancel", [&] { delete this; })); + buttons.push_back(std::make_shared(mWindow, _T("SAVE"), _T("SAVE"), [&] { save(); delete this; })); + buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); if(mDeleteFunc) { auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; - buttons.push_back(std::make_shared(mWindow, "DELETE", "delete", deleteBtnFunc)); + buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE"), deleteBtnFunc)); } mButtons = makeButtonGrid(mWindow, buttons); @@ -184,17 +276,26 @@ void GuiMetaDataEd::save() // remove game from index mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); - for(unsigned int i = 0; i < mEditors.size(); i++) - { - if(mMetaDataDecl.at(i).isStatistic) - continue; - mMetaData->set(mMetaDataDecl.at(i).key, mEditors.at(i)->getValue()); + for (unsigned int i = 0; i < mEditors.size(); i++) + { + std::shared_ptr ed = mEditors.at(i); + + auto val = ed->getValue(); + auto key = ed->getTag(); + + if (key == "core" || key == "emulator") + { + std::shared_ptr> list = std::static_pointer_cast>(ed); + val = list->getSelected(); + } + + mMetaData->set(key, val); } // enter game in index mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); - if(mSavedCallback) + if (mSavedCallback) mSavedCallback(); // update respective Collection Entries @@ -211,10 +312,8 @@ 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; + auto val = mEditors.at(i)->getValue(); + auto key = mEditors.at(i)->getTag(); mEditors.at(i)->setValue(result.mdl.get(key)); } } @@ -225,7 +324,7 @@ void GuiMetaDataEd::close(bool closeAllWindows) bool dirty = false; for(unsigned int i = 0; i < mEditors.size(); i++) { - const std::string& key = mMetaDataDecl.at(i).key; + auto key = mEditors.at(i)->getTag(); if(mMetaData->get(key) != mEditors.at(i)->getValue()) { dirty = true; @@ -250,9 +349,9 @@ void GuiMetaDataEd::close(bool closeAllWindows) { // 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 + "ENREGISTRER LES MODIFICATIONS ?", + "OUI", [this, closeFunc] { save(); closeFunc(); }, + "NON", closeFunc )); }else{ closeFunc(); @@ -277,7 +376,7 @@ bool GuiMetaDataEd::input(InputConfig* config, Input input) std::vector GuiMetaDataEd::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiMetaDataEd.h b/es-app/src/guis/GuiMetaDataEd.h index ee192f982b..8bdf3531d1 100644 --- a/es-app/src/guis/GuiMetaDataEd.h +++ b/es-app/src/guis/GuiMetaDataEd.h @@ -5,17 +5,20 @@ #include "components/ComponentGrid.h" #include "components/NinePatchComponent.h" #include "scrapers/Scraper.h" +#include "components/MenuComponent.h" +#include "components/OptionListComponent.h" #include "GuiComponent.h" #include "MetaData.h" class ComponentList; class TextComponent; +class FileData; class GuiMetaDataEd : public GuiComponent { public: GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams params, - const std::string& header, std::function savedCallback, std::function deleteFunc); + const std::string& header, std::function savedCallback, std::function deleteFunc, FileData* file); bool input(InputConfig* config, Input input) override; void onSizeChanged() override; @@ -38,6 +41,9 @@ class GuiMetaDataEd : public GuiComponent ScraperSearchParams mScraperParams; + // typedef OptionListComponent CoreList; + //std::shared_ptr mCoreList; + std::vector< std::shared_ptr > mEditors; std::vector mMetaDataDecl; diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 28b87db40f..7dcd2d16bd 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -32,7 +32,7 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queue(mWindow, "SCRAPING IN PROGRESS", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); mGrid.setEntry(mTitle, Vector2i(0, 0), false, true); - mSystem = std::make_shared(mWindow, "SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); + mSystem = std::make_shared(mWindow, _T("SYSTEM"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); mGrid.setEntry(mSystem, Vector2i(0, 1), false, true); mSubtitle = std::make_shared(mWindow, "subtitle text", Font::get(FONT_SIZE_SMALL), 0x888888FF, ALIGN_CENTER); diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index 10c2a9b867..2330f095f6 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -34,8 +34,8 @@ GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), 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.addButton("START", "LANCER", std::bind(&GuiScraperStart::pressedStart, this)); + mMenu.addButton(_T("BACK"), _T("BACK"), [&] { delete this; }); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); } @@ -122,7 +122,7 @@ bool GuiScraperStart::input(InputConfig* config, Input input) std::vector GuiScraperStart::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiScreensaverOptions.cpp b/es-app/src/guis/GuiScreensaverOptions.cpp index e9d2f068c3..f6d33f49f2 100644 --- a/es-app/src/guis/GuiScreensaverOptions.cpp +++ b/es-app/src/guis/GuiScreensaverOptions.cpp @@ -5,11 +5,11 @@ #include "SystemData.h" #include "Window.h" -GuiScreensaverOptions::GuiScreensaverOptions(Window* window, const char* title) : GuiComponent(window), mMenu(window, title) +GuiScreensaverOptions::GuiScreensaverOptions(Window* window, std::string title) : GuiComponent(window), mMenu(window, title) { addChild(&mMenu); - mMenu.addButton("BACK", "go back", [this] { delete this; }); + mMenu.addButton(_T("BACK"), _T("BACK"), [this] { delete this; }); setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); @@ -62,8 +62,8 @@ std::vector GuiScreensaverOptions::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiScreensaverOptions.h b/es-app/src/guis/GuiScreensaverOptions.h index 6f61489f8b..e8e4332e1d 100644 --- a/es-app/src/guis/GuiScreensaverOptions.h +++ b/es-app/src/guis/GuiScreensaverOptions.h @@ -8,7 +8,7 @@ class GuiScreensaverOptions : public GuiComponent { public: - GuiScreensaverOptions(Window* window, const char* title); + GuiScreensaverOptions(Window* window, std::string title); virtual ~GuiScreensaverOptions(); // just calls save(); virtual void save(); diff --git a/es-app/src/guis/GuiSettings.cpp b/es-app/src/guis/GuiSettings.cpp index 53f96c439e..f0d3dd697a 100644 --- a/es-app/src/guis/GuiSettings.cpp +++ b/es-app/src/guis/GuiSettings.cpp @@ -5,11 +5,11 @@ #include "SystemData.h" #include "Window.h" -GuiSettings::GuiSettings(Window* window, const char* title) : GuiComponent(window), mMenu(window, title) +GuiSettings::GuiSettings(Window* window, std::string title) : GuiComponent(window), mMenu(window, title) { addChild(&mMenu); - mMenu.addButton("BACK", "go back", [this] { delete this; }); + mMenu.addButton(_T("BACK"), _T("BACK"), [this] { delete this; }); setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); @@ -62,8 +62,8 @@ std::vector GuiSettings::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiSettings.h b/es-app/src/guis/GuiSettings.h index d5b0d97998..05ece518f2 100644 --- a/es-app/src/guis/GuiSettings.h +++ b/es-app/src/guis/GuiSettings.h @@ -8,7 +8,7 @@ class GuiSettings : public GuiComponent { public: - GuiSettings(Window* window, const char* title); + GuiSettings(Window* window, std::string title); virtual ~GuiSettings(); // just calls save(); void save(); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 6c1fe80ef6..136bee9434 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -35,7 +35,7 @@ bool parseArgs(int argc, char* argv[]) for(int i = 1; i < argc; i++) { - if(strcmp(argv[i], "--resolution") == 0) + if (strcmp(argv[i], "--resolution") == 0) { if(i >= argc - 2) { @@ -48,9 +48,10 @@ bool parseArgs(int argc, char* argv[]) i += 2; // skip the argument value Settings::getInstance()->setInt("WindowWidth", width); Settings::getInstance()->setInt("WindowHeight", height); - }else if(strcmp(argv[i], "--screensize") == 0) + } + else if (strcmp(argv[i], "--screensize") == 0) { - if(i >= argc - 2) + if (i >= argc - 2) { std::cerr << "Invalid screensize supplied."; return false; @@ -61,7 +62,8 @@ bool parseArgs(int argc, char* argv[]) i += 2; // skip the argument value Settings::getInstance()->setInt("ScreenWidth", width); Settings::getInstance()->setInt("ScreenHeight", height); - }else if(strcmp(argv[i], "--screenoffset") == 0) + } + else if (strcmp(argv[i], "--screenoffset") == 0) { if(i >= argc - 2) { @@ -74,7 +76,8 @@ bool parseArgs(int argc, char* argv[]) i += 2; // skip the argument value Settings::getInstance()->setInt("ScreenOffsetX", x); Settings::getInstance()->setInt("ScreenOffsetY", y); - }else if (strcmp(argv[i], "--screenrotate") == 0) + } + else if (strcmp(argv[i], "--screenrotate") == 0) { if (i >= argc - 1) { @@ -85,44 +88,60 @@ bool parseArgs(int argc, char* argv[]) int rotate = atoi(argv[i + 1]); ++i; // skip the argument value Settings::getInstance()->setInt("ScreenRotate", rotate); - }else if(strcmp(argv[i], "--gamelist-only") == 0) + } + else if (strcmp(argv[i], "--gamelist-only") == 0) { Settings::getInstance()->setBool("ParseGamelistOnly", true); - }else if(strcmp(argv[i], "--ignore-gamelist") == 0) + } + else if (strcmp(argv[i], "--ignore-gamelist") == 0) { Settings::getInstance()->setBool("IgnoreGamelist", true); - }else if(strcmp(argv[i], "--show-hidden-files") == 0) + } + else if (strcmp(argv[i], "--show-hidden-files") == 0) { Settings::getInstance()->setBool("ShowHiddenFiles", true); - }else if(strcmp(argv[i], "--draw-framerate") == 0) + } + else if (strcmp(argv[i], "--draw-framerate") == 0) { Settings::getInstance()->setBool("DrawFramerate", true); - }else if(strcmp(argv[i], "--no-exit") == 0) + } + else if (strcmp(argv[i], "--no-exit") == 0) { Settings::getInstance()->setBool("ShowExit", false); - }else if(strcmp(argv[i], "--no-splash") == 0) + } + else if (strcmp(argv[i], "--no-splash") == 0) { Settings::getInstance()->setBool("SplashScreen", false); - }else if(strcmp(argv[i], "--debug") == 0) + } + else if (strcmp(argv[i], "--debug") == 0) { Settings::getInstance()->setBool("Debug", true); Settings::getInstance()->setBool("HideConsole", false); Log::setReportingLevel(LogDebug); - }else if(strcmp(argv[i], "--fullscreen-borderless") == 0) + } + else if (strcmp(argv[i], "--fullscreen-borderless") == 0) { Settings::getInstance()->setBool("FullscreenBorderless", true); - }else if(strcmp(argv[i], "--windowed") == 0) + } + else if (strcmp(argv[i], "--fullscreen") == 0) { - Settings::getInstance()->setBool("Windowed", true); - }else if(strcmp(argv[i], "--vsync") == 0) + Settings::getInstance()->setBool("FullscreenBorderless", false); + } + else if (strcmp(argv[i], "--windowed") == 0 || strcmp(argv[i], "-windowed") == 0) { - bool vsync = (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "1") == 0) ? true : false; + Settings::getInstance()->setBool("Windowed", true); + } + else if (strcmp(argv[i], "--vsync") == 0 || strcmp(argv[i], "-vsync") == 0) + { + bool vsync = strlen(argv[i]) <= 7 ? true : (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "1") == 0) ? true : false; Settings::getInstance()->setBool("VSync", vsync); i++; // skip vsync value - }else if(strcmp(argv[i], "--scrape") == 0) + } + else if (strcmp(argv[i], "--scrape") == 0) { scrape_cmdline = true; - }else if(strcmp(argv[i], "--max-vram") == 0) + } + else if (strcmp(argv[i], "--max-vram") == 0) { int maxVRAM = atoi(argv[i + 1]); Settings::getInstance()->setInt("MaxVRAM", maxVRAM); @@ -197,25 +216,29 @@ bool verifyHomeFolderExists() } // Returns true if everything is OK, -bool loadSystemConfigFile(const char** errorString) +bool loadSystemConfigFile(Window* window, const char** errorString) { *errorString = NULL; - - if(!SystemData::loadConfig()) + + if(!SystemData::loadConfig(window)) { 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; } @@ -232,8 +255,9 @@ int main(int argc, char* argv[]) { srand((unsigned int)time(NULL)); - std::locale::global(std::locale("C")); - + //std::locale::global(std::locale("C")); + std::locale::global(std::locale("en-US")); + if(!parseArgs(argc, argv)) return 0; @@ -311,29 +335,33 @@ int main(int argc, char* argv[]) LOG(LogInfo) << " ARB_texture_non_power_of_two: " << (glExts.find("ARB_texture_non_power_of_two") != std::string::npos ? "ok" : "MISSING"); if(splashScreen) { - std::string progressText = "Loading..."; + std::string progressText = "Chargement"; + if (splashScreenProgress) - progressText = "Loading system config..."; + progressText = "Chargement de la configuration"; + window.renderLoadingScreen(progressText); } } const char* errorMsg = NULL; - if(!loadSystemConfigFile(&errorMsg)) + if(!loadSystemConfigFile(&window, &errorMsg)) { // something went terribly wrong - if(errorMsg == NULL) + if (errorMsg == NULL) { LOG(LogError) << "Unknown error occured while parsing system config file."; - if(!scrape_cmdline) + + 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", [] { + _T("QUIT"), [] { SDL_Event* quit = new SDL_Event(); quit->type = SDL_QUIT; SDL_PushEvent(quit); @@ -341,30 +369,26 @@ int main(int argc, char* argv[]) } //run the command line scraper then quit - if(scrape_cmdline) - { + 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(); - - if(splashScreen && splashScreenProgress) - window.renderLoadingScreen("Done."); + // ViewController::get()->preload(); + + if (splashScreen && splashScreenProgress) + window.renderLoadingScreen(_T("Starting UI")); //choose which GUI to open depending on if an input configuration already exists if(errorMsg == NULL) { - if(Utils::FileSystem::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0) - { - ViewController::get()->goToStart(); - }else{ - window.pushGui(new GuiDetectDevice(&window, true, [] { ViewController::get()->goToStart(); })); - } + if (Utils::FileSystem::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0) + ViewController::get()->goToStart(true); + else + window.pushGui(new GuiDetectDevice(&window, true, [] { ViewController::get()->goToStart(true); })); } //generate joystick events since we're done loading @@ -386,9 +410,10 @@ int main(int argc, char* argv[]) { InputManager::getInstance()->parseEvent(event, &window); - if(event.type == SDL_QUIT) + if (event.type == SDL_QUIT) running = false; - } while(SDL_PollEvent(&event)); + } + while(SDL_PollEvent(&event)); // triggered if exiting from SDL_WaitEvent due to event if (ps_standby) @@ -429,6 +454,7 @@ int main(int argc, char* argv[]) while(window.peekGui() != ViewController::get()) delete window.peekGui(); + window.deinit(); MameNames::deinit(); diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index e9eb53378b..31f32ea408 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -125,7 +125,17 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, ScreenScraperRequest::ScreenScraperConfig ssConfig; - path = ssConfig.getGameSearchUrl(params.game->getFileName()); + if (params.nameOverride.length() == 0) + { + path = ssConfig.getGameSearchUrl(params.game->getFileName()); + path += "&romtype=rom"; + } + else + { + path = ssConfig.getGameSearchUrl(params.nameOverride); + path += "&romtype=jeu"; + } + auto& platforms = params.system->getPlatformIds(); for (auto platformIt = platforms.cbegin(); platformIt != platforms.cend(); platformIt++) @@ -142,7 +152,6 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, requests.push(std::unique_ptr(new ScreenScraperRequest(requests, results, path))); } - } void ScreenScraperRequest::process(const std::unique_ptr& req, std::vector& results) diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index 4c749008f1..55c4a89c4f 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -28,7 +28,7 @@ class ScreenScraperRequest : public ScraperHttpRequest const std::string API_DEV_P = { 108, 28, 54, 55, 83, 43, 91, 44, 30, 22, 41, 12, 0, 108, 38, 29 }; const std::string API_DEV_KEY = { 54, 73, 115, 100, 101, 67, 111, 107, 79, 66, 68, 66, 67, 56, 118, 77, 54, 88, 101, 54 }; const std::string API_URL_BASE = "https://www.screenscraper.fr/api2"; - const std::string API_SOFT_NAME = "Emulationstation " + static_cast(PROGRAM_VERSION_STRING); + const std::string API_SOFT_NAME = "Emulationstation 2.9.0"; // +static_cast(PROGRAM_VERSION_STRING); /** Which type of image artwork we need. Possible values (not a comprehensive list): - ss: in-game screenshot diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index cdd5b1ea10..9ed1a22dc6 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -259,7 +259,7 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) if (!getSelected()->isGameSystem()) ss << "CONFIGURATION"; else - ss << gameCount << " GAMES AVAILABLE"; + ss << gameCount << " " << _T("GAMES AVAILABLE"); mSystemInfo.setText(ss.str()); }, false, 1); @@ -376,14 +376,15 @@ std::vector SystemView::getHelpPrompts() { std::vector prompts; if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) - prompts.push_back(HelpPrompt("up/down", "choose")); + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); else - prompts.push_back(HelpPrompt("left/right", "choose")); - prompts.push_back(HelpPrompt("a", "select")); - prompts.push_back(HelpPrompt("x", "random")); + prompts.push_back(HelpPrompt("left/right", _T("CHOOSE"))); - if (!UIModeController::getInstance()->isUIModeKid() && Settings::getInstance()->getBool("ScreenSaverControls")) - prompts.push_back(HelpPrompt("select", "launch screensaver")); + prompts.push_back(HelpPrompt("a", _T("SELECT"))); + //prompts.push_back(HelpPrompt("x", "random")); + +// if (!UIModeController::getInstance()->isUIModeKid() && Settings::getInstance()->getBool("ScreenSaverControls")) + // prompts.push_back(HelpPrompt("select", "launch screensaver")); return prompts; } diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index ada681e260..65fb1b4122 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -43,7 +43,7 @@ ViewController::~ViewController() sInstance = NULL; } -void ViewController::goToStart() +void ViewController::goToStart(bool forceImmediate) { // If specific system is requested, go directly to the game list auto requestedSystem = Settings::getInstance()->getString("StartupSystem"); @@ -52,7 +52,8 @@ void ViewController::goToStart() for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++){ if ((*it)->getName() == requestedSystem) { - goToGameList(*it); + goToSystemView(*it, forceImmediate); +// goToGameList(*it); return; } } @@ -60,14 +61,15 @@ void ViewController::goToStart() // Requested system doesn't exist Settings::getInstance()->setString("StartupSystem", ""); } - goToSystemView(SystemData::sSystemVector.at(0)); + goToSystemView(SystemData::sSystemVector.at(0), forceImmediate); } void ViewController::ReloadAndGoToStart() { - mWindow->renderLoadingScreen("Loading..."); + mWindow->renderLoadingScreen("Chargement"); + ViewController::get()->reloadAll(); - ViewController::get()->goToStart(); + ViewController::get()->goToStart(false); } int ViewController::getSystemId(SystemData* system) @@ -76,7 +78,7 @@ int ViewController::getSystemId(SystemData* system) return (int)(std::find(sysVec.cbegin(), sysVec.cend(), system) - sysVec.cbegin()); } -void ViewController::goToSystemView(SystemData* system) +void ViewController::goToSystemView(SystemData* system, bool forceImmediate) { // Tell any current view it's about to be hidden if (mCurrentView) @@ -95,7 +97,7 @@ void ViewController::goToSystemView(SystemData* system) mCurrentView->onShow(); PowerSaver::setState(true); - playViewTransition(); + playViewTransition(forceImmediate); } void ViewController::goToNextGameList() @@ -139,10 +141,10 @@ void ViewController::goToGameList(SystemData* system) { mCurrentView->onShow(); } - playViewTransition(); + playViewTransition(false); } -void ViewController::playViewTransition() +void ViewController::playViewTransition(bool forceImmediate) { Vector3f target(Vector3f::Zero()); if(mCurrentView) @@ -153,7 +155,7 @@ void ViewController::playViewTransition() return; std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); - if(transition_style == "fade") + if(!forceImmediate && transition_style == "fade") { // fade // stop whatever's currently playing, leaving mFadeOpacity wherever it is @@ -181,7 +183,7 @@ void ViewController::playViewTransition() }else{ advanceAnimation(0, (int)(mFadeOpacity * FADE_DURATION)); } - } else if (transition_style == "slide"){ + } else if (!forceImmediate && transition_style == "slide"){ // slide or simple slide setAnimation(new MoveCameraAnimation(mCamera, target)); updateHelpPrompts(); // update help prompts immediately @@ -212,8 +214,8 @@ void ViewController::launch(FileData* game, Vector3f center) } // Hide the current view - if (mCurrentView) - mCurrentView->onHide(); + //if (mCurrentView) + //mCurrentView->onHide(); Transform4x4f origCamera = mCamera; origCamera.translation() = -mCurrentView->getPosition(); @@ -223,7 +225,7 @@ void ViewController::launch(FileData* game, Vector3f center) mWindow->stopInfoPopup(); // make sure we disable any existing info popup mLockInput = true; - std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); + std::string transition_style = Settings::getInstance()->getString("GameTransitionStyle"); if(transition_style == "fade") { // fade out, launch game, fade back in @@ -240,7 +242,7 @@ void ViewController::launch(FileData* game, Vector3f center) // 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->launchGame(mWindow); + game->launchGame(mWindow); mCamera = origCamera; setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 600), 0, [this] { mLockInput = false; }, true); this->onFileChanged(game, FILE_METADATA_CHANGED); @@ -280,33 +282,72 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste bool themeHasVideoView = system->getTheme()->hasView("video"); + bool themeHasGridView = system->getTheme()->hasView("grid"); + bool themeHasGridExView = system->getTheme()->hasView("gridex"); + //decide type GameListViewType selectedViewType = AUTOMATIC; + bool detailed = false; + std::string viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + if (viewPreference.compare("basic") == 0) selectedViewType = BASIC; - if (viewPreference.compare("detailed") == 0) + else if (viewPreference.compare("detailed") == 0) + { + detailed = true; selectedViewType = DETAILED; - if (viewPreference.compare("grid") == 0) + } + else if (themeHasGridExView && viewPreference.compare("gridex") == 0) + selectedViewType = GRIDEX; + else if (themeHasGridView && viewPreference.compare("grid") == 0) selectedViewType = GRID; - if (viewPreference.compare("video") == 0) + else if (viewPreference.compare("video") == 0) selectedViewType = VIDEO; - if (selectedViewType == AUTOMATIC) + if (selectedViewType == AUTOMATIC || detailed) { - std::vector files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER); - for (auto it = files.cbegin(); it != files.cend(); it++) + if (themeHasGridView && system->getTheme()->getDefaultView() == "grid" && !detailed) + selectedViewType = GRID; + else if (themeHasGridExView && system->getTheme()->getDefaultView() == "gridex" && !detailed) + selectedViewType = GRIDEX; + else { - if (themeHasVideoView && !(*it)->getVideoPath().empty()) - { - selectedViewType = VIDEO; - break; - } - else if (!(*it)->getThumbnailPath().empty()) - { + selectedViewType = BASIC; + + if (system->getTheme()->getDefaultView() == "detailed") selectedViewType = DETAILED; - // Don't break out in case any subsequent files have video + else if (system->getTheme()->getDefaultView() != "basic") + { + std::vector files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER); + for (auto it = files.cbegin(); it != files.cend(); it++) + { + if (themeHasVideoView && !(*it)->getVideoPath().empty() && viewPreference.compare("detailed") != 0) + { + selectedViewType = VIDEO; + break; + } + else if (!(*it)->getThumbnailPath().empty()) + { + if (!detailed && (*it)->metadata.get("thumbnail").length() > 0) + { + if (themeHasGridExView && (*it)->metadata.get("image").length() > 0) + selectedViewType = GRIDEX; + else if (themeHasGridView) + selectedViewType = GRID; + else + selectedViewType = DETAILED; + } + else + selectedViewType = DETAILED; + + if (!themeHasVideoView) + break; + + // Don't break out in case any subsequent files have video + } + } } } } @@ -323,6 +364,13 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste case GRID: view = std::shared_ptr(new GridGameListView(mWindow, system->getRootFolder())); break; + case GRIDEX: + { + GridGameListView* listView = new GridGameListView(mWindow, system->getRootFolder()); + listView->setGridEx(); + view = std::shared_ptr(listView); + } + break; case BASIC: default: view = std::shared_ptr(new BasicGameListView(mWindow, system->getRootFolder())); @@ -356,7 +404,7 @@ std::shared_ptr ViewController::getSystemListView() bool ViewController::input(InputConfig* config, Input input) { - if(mLockInput) + if (mLockInput) return true; // open menu @@ -437,7 +485,7 @@ void ViewController::preload() { i++; char buffer[100]; - sprintf (buffer, "Loading '%s' (%d/%d)", + sprintf (buffer, "Chargement de '%s' (%d/%d)", (*it)->getFullName().c_str(), i, (int)SystemData::sSystemVector.size()); mWindow->renderLoadingScreen(std::string(buffer)); } @@ -454,20 +502,22 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) if(it->second.get() == view) { bool isCurrent = (mCurrentView == it->second); + SystemData* system = it->first; FileData* cursor = view->getCursor(); mGameListViews.erase(it); - if(reloadTheme) + if (reloadTheme) system->loadTheme(); + system->getIndex()->setUIModeFilters(); std::shared_ptr newView = getGameListView(system); // to counter having come from a placeholder - if (!cursor->isPlaceHolder()) { + if (!cursor->isPlaceHolder()) newView->setCursor(cursor); - } - if(isCurrent) + + if (isCurrent) mCurrentView = newView; break; @@ -483,19 +533,26 @@ void ViewController::reloadAll() { // clear all gamelistviews std::map cursorMap; + for(auto it = mGameListViews.cbegin(); it != mGameListViews.cend(); it++) - { cursorMap[it->first] = it->second->getCursor(); - } + mGameListViews.clear(); + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + { + if (cursorMap.find((*it)) == cursorMap.end()) + cursorMap[(*it)] = NULL; + } // load themes, create gamelistviews and reset filters for(auto it = cursorMap.cbegin(); it != cursorMap.cend(); it++) { it->first->loadTheme(); it->first->getIndex()->resetFilters(); - getGameListView(it->first)->setCursor(it->second); + + if (it->second != NULL) + getGameListView(it->first)->setCursor(it->second); } // Rebuild SystemListView @@ -509,11 +566,11 @@ void ViewController::reloadAll() }else if(mState.viewing == SYSTEM_SELECT) { SystemData* system = mState.getSystem(); - goToSystemView(SystemData::sSystemVector.front()); + goToSystemView(SystemData::sSystemVector.front(), false); mSystemListView->goToSystem(system, false); mCurrentView = mSystemListView; }else{ - goToSystemView(SystemData::sSystemVector.front()); + goToSystemView(SystemData::sSystemVector.front(), false); } updateHelpPrompts(); diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h index eec5e9fdb1..7aef661c1c 100644 --- a/es-app/src/views/ViewController.h +++ b/es-app/src/views/ViewController.h @@ -34,8 +34,8 @@ class ViewController : public GuiComponent void goToNextGameList(); void goToPrevGameList(); void goToGameList(SystemData* system); - void goToSystemView(SystemData* system); - void goToStart(); + void goToSystemView(SystemData* system, bool forceImmediate = false); + void goToStart(bool forceImmediate = false); void ReloadAndGoToStart(); void onFileChanged(FileData* file, FileChangeType change); @@ -62,7 +62,8 @@ class ViewController : public GuiComponent BASIC, DETAILED, GRID, - VIDEO + VIDEO, + GRIDEX }; struct State @@ -89,7 +90,7 @@ class ViewController : public GuiComponent ViewController(Window* window); static ViewController* sInstance; - void playViewTransition(); + void playViewTransition(bool forceImmediate); int getSystemId(SystemData* system); std::shared_ptr mCurrentView; diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 899f12578c..c697a09130 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -29,7 +29,7 @@ void BasicGameListView::onThemeChanged(const std::shared_ptr& theme) void BasicGameListView::onFileChanged(FileData* file, FileChangeType change) { - if(change == FILE_METADATA_CHANGED) + if (change == FILE_METADATA_CHANGED) { // might switch to a detailed view ViewController::get()->reloadGameListView(this); @@ -42,12 +42,40 @@ void BasicGameListView::onFileChanged(FileData* file, FileChangeType change) void BasicGameListView::populateList(const std::vector& files) { mList.clear(); - mHeaderText.setText(mRoot->getSystem()->getFullName()); + + std::string systemName = mRoot->getSystem()->getFullName(); + mHeaderText.setText(systemName); + + bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + if (files.size() > 0) { - for(auto it = files.cbegin(); it != files.cend(); it++) + for (auto it = files.cbegin(); it != files.cend(); it++) + { + if ((*it)->getFavorite()) + if (showHiddenFiles || !(*it)->getHidden()) + { + if (systemName == "favorites") + mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + else + + mList.add(_T("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + } + } + /* + for (auto it = files.cbegin(); it != files.cend(); it++) + { + if (!(*it)->getFavorite() && ((*it)->getType() == FOLDER)) + if (showHiddenFiles || !(*it)->getHidden()) + mList.add("[Folder] "+(*it)->getName(), *it, ((*it)->getType() == FOLDER)); + + }*/ + + for (auto it = files.cbegin(); it != files.cend(); it++) { - mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + if (!(*it)->getFavorite()) + if (showHiddenFiles || !(*it)->getHidden()) + mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); } } else @@ -146,17 +174,24 @@ std::vector BasicGameListView::getHelpPrompts() std::vector 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("left/right", _T("SYSTEM"))); + + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("LAUNCH"))); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + if(!UIModeController::getInstance()->isUIModeKid()) - prompts.push_back(HelpPrompt("select", "options")); + prompts.push_back(HelpPrompt("select", _T("options"))); + if(mRoot->getSystem()->isGameSystem()) - prompts.push_back(HelpPrompt("x", "random")); + prompts.push_back(HelpPrompt("x", _T("RANDOM"))); + if(mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) { std::string prompt = CollectionSystemManager::get()->getEditingCollection(); + if (prompt == "Favorites") + prompt = "FAVORIS"; + prompts.push_back(HelpPrompt("y", prompt)); } return prompts; diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 87d2893f56..bfb26740ce 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -32,29 +32,29 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : addChild(&mImage); // metadata labels + values - mLblRating.setText("Rating: "); + mLblRating.setText(_T("Rating") + ": "); addChild(&mLblRating); addChild(&mRating); - mLblReleaseDate.setText("Released: "); + mLblReleaseDate.setText(_T("Released") + ": "); addChild(&mLblReleaseDate); addChild(&mReleaseDate); - mLblDeveloper.setText("Developer: "); + mLblDeveloper.setText(_T("Developer") + ": "); addChild(&mLblDeveloper); addChild(&mDeveloper); - mLblPublisher.setText("Publisher: "); + mLblPublisher.setText(_T("Publisher")+": "); addChild(&mLblPublisher); addChild(&mPublisher); - mLblGenre.setText("Genre: "); + mLblGenre.setText(_T("Genre") + ": "); addChild(&mLblGenre); addChild(&mGenre); - mLblPlayers.setText("Players: "); + mLblPlayers.setText(_T("Players") + ": "); addChild(&mLblPlayers); addChild(&mPlayers); - mLblLastPlayed.setText("Last played: "); + mLblLastPlayed.setText(_T("Last played") + ": "); addChild(&mLblLastPlayed); mLastPlayed.setDisplayRelative(true); addChild(&mLastPlayed); - mLblPlayCount.setText("Times played: "); + mLblPlayCount.setText(_T("Times played")); addChild(&mLblPlayCount); addChild(&mPlayCount); @@ -188,6 +188,15 @@ void DetailedGameListView::initMDValues() mDescContainer.setSize(mDescContainer.getSize().x(), mSize.y() - mDescContainer.getPosition().y()); } +std::string DetailedGameListView::getMetadata(FileData* file, std::string name) +{ + std::string ret = file->metadata.get(name); + if (ret == "unknown") + return _L(ret); + + return ret; +} + void DetailedGameListView::updateInfoPanel() { FileData* file = (mList.size() == 0 || mList.isScrolling()) ? NULL : mList.getSelected(); @@ -199,22 +208,27 @@ void DetailedGameListView::updateInfoPanel() //mDescription.setText(""); fadingOut = true; }else{ - mImage.setImage(file->getImagePath()); - mDescription.setText(file->metadata.get("desc")); + + if (file->getImagePath().empty()) + mImage.setImage(file->getThumbnailPath()); + else + mImage.setImage(file->getImagePath()); + + mDescription.setText(getMetadata(file, "desc")); mDescContainer.reset(); - 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")); - mName.setValue(file->metadata.get("name")); + mRating.setValue(getMetadata(file, "rating")); + mReleaseDate.setValue(getMetadata(file, "releasedate")); + mDeveloper.setValue(getMetadata(file, "developer")); + mPublisher.setValue(getMetadata(file, "publisher")); + mGenre.setValue(getMetadata(file, "genre")); + mPlayers.setValue(getMetadata(file, "players")); + mName.setValue(getMetadata(file, "name")); if(file->getType() == GAME) { - mLastPlayed.setValue(file->metadata.get("lastplayed")); - mPlayCount.setValue(file->metadata.get("playcount")); + mLastPlayed.setValue(getMetadata(file, "lastplayed")); + mPlayCount.setValue(getMetadata(file, "playcount")); } fadingOut = false; diff --git a/es-app/src/views/gamelist/DetailedGameListView.h b/es-app/src/views/gamelist/DetailedGameListView.h index 76c8a93537..3c070c955f 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.h +++ b/es-app/src/views/gamelist/DetailedGameListView.h @@ -23,6 +23,7 @@ class DetailedGameListView : public BasicGameListView void initMDLabels(); void initMDValues(); + std::string getMetadata(FileData* file, std::string name); ImageComponent mImage; diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 91f9c08b0a..a2dbc27428 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -11,6 +11,7 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : ISimpleGameListView(window, root), mGrid(window), mDescContainer(window), mDescription(window), + mImage(window), mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -21,6 +22,8 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : { const float padding = 0.01f; + mGridEx = false; + mGrid.setPosition(mSize.x() * 0.1f, mSize.y() * 0.1f); mGrid.setDefaultZIndex(20); mGrid.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); @@ -28,6 +31,13 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : populateList(root->getChildrenListToDisplay()); + // 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); + mImage.setDefaultZIndex(30); + addChild(&mImage); + // metadata labels + values mLblRating.setText("Rating: "); addChild(&mLblRating); @@ -78,6 +88,12 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : updateInfoPanel(); } +void GridGameListView::setGridEx() +{ + mGridEx = true; + mGrid.setThemeName(getName()); +} + FileData* GridGameListView::getCursor() { return mGrid.getSelected(); @@ -116,10 +132,21 @@ void GridGameListView::populateList(const std::vector& files) mHeaderText.setText(mRoot->getSystem()->getFullName()); if (files.size() > 0) { + std::string systemName = mRoot->getSystem()->getFullName(); + bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + for (auto it = files.cbegin(); it != files.cend(); it++) - { - mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it); - } + if ((*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) + { + if (systemName == "favorites") + mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it); + else + mGrid.add(_T("\uF006 ") + (*it)->getName(), (*it)->getThumbnailPath(), *it); + } + + for (auto it = files.cbegin(); it != files.cend(); it++) + if (!(*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) + mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it); } else { @@ -136,6 +163,14 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) mGrid.applyTheme(theme, getName(), "gamegrid", ALL); mName.applyTheme(theme, getName(), "md_name", ALL); + if (theme->getElement(getName(), "md_image", "image")) + { + mImageVisible = true; + mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + } + else + mImageVisible = false; + initMDLabels(); std::vector labels = getMDLabels(); assert(labels.size() == 8); @@ -168,6 +203,7 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION)); sortChildren(); + updateInfoPanel(); } void GridGameListView::initMDLabels() @@ -240,11 +276,23 @@ void GridGameListView::updateInfoPanel() FileData* file = (mGrid.size() == 0 || mGrid.isScrolling()) ? NULL : mGrid.getSelected(); bool fadingOut; - if(file == NULL) + if (file == NULL) { //mDescription.setText(""); fadingOut = true; - }else{ + } + else + { + if (mImageVisible) + { + if (file->getImagePath().empty()) + mImage.setImage(file->getThumbnailPath()); + else + mImage.setImage(file->getImagePath()); + } + else + mImage.setImage(""); + mDescription.setText(file->metadata.get("desc")); mDescContainer.reset(); @@ -266,11 +314,12 @@ void GridGameListView::updateInfoPanel() } std::vector comps = getMDValues(); + comps.push_back(&mImage); comps.push_back(&mDescription); comps.push_back(&mName); std::vector labels = getMDLabels(); comps.insert(comps.cend(), labels.cbegin(), labels.cend()); - + for(auto it = comps.cbegin(); it != comps.cend(); it++) { GuiComponent* comp = *it; @@ -364,14 +413,15 @@ std::vector GridGameListView::getHelpPrompts() std::vector prompts; if(Settings::getInstance()->getBool("QuickSystemSelect")) - prompts.push_back(HelpPrompt("lr", "system")); - prompts.push_back(HelpPrompt("up/down/left/right", "choose")); - prompts.push_back(HelpPrompt("a", "launch")); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("lr", _T("SYSTEM"))); + prompts.push_back(HelpPrompt("up/down/left/right", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("LAUNCH"))); + prompts.push_back(HelpPrompt("b", _T("BACK"))); if(!UIModeController::getInstance()->isUIModeKid()) - prompts.push_back(HelpPrompt("select", "options")); + prompts.push_back(HelpPrompt("select", _T("OPTIONS"))); if(mRoot->getSystem()->isGameSystem()) - prompts.push_back(HelpPrompt("x", "random")); + prompts.push_back(HelpPrompt("x", _T("RANDOM"))); + if(mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) { std::string prompt = CollectionSystemManager::get()->getEditingCollection(); diff --git a/es-app/src/views/gamelist/GridGameListView.h b/es-app/src/views/gamelist/GridGameListView.h index da09c87903..50cfc24ab7 100644 --- a/es-app/src/views/gamelist/GridGameListView.h +++ b/es-app/src/views/gamelist/GridGameListView.h @@ -7,6 +7,7 @@ #include "components/ScrollableContainer.h" #include "components/ImageGridComponent.h" #include "views/gamelist/ISimpleGameListView.h" +#include "views/gamelist/BasicGameListView.h" class GridGameListView : public ISimpleGameListView { @@ -20,11 +21,19 @@ class GridGameListView : public ISimpleGameListView virtual bool input(InputConfig* config, Input input) override; - virtual const char* getName() const override { return "grid"; } + virtual const char* getName() const override + { + if (mGridEx) + return "gridex"; + + return "grid"; + } virtual std::vector getHelpPrompts() override; virtual void launch(FileData* game) override; + void setGridEx(); + protected: virtual std::string getQuickSystemSelectRightButton() override; virtual std::string getQuickSystemSelectLeftButton() override; @@ -51,6 +60,10 @@ class GridGameListView : public ISimpleGameListView DateTimeComponent mLastPlayed; TextComponent mPlayCount; TextComponent mName; + ImageComponent mImage; + + bool mImageVisible; + bool mGridEx; std::vector getMDLabels(); std::vector getMDValues(); diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 5a1a68ed35..90b1c032ef 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -80,14 +80,24 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) { if(input.value != 0) { - if(config->isMappedTo("a", input)) + if (config->isMappedTo("a", input)) { FileData* cursor = getCursor(); - if(cursor->getType() == GAME) + + if (cursor->getType() == FOLDER) + { + FileData* gameOfFolder = cursor->findUniqueGameForFolder(); + if (gameOfFolder != NULL) + cursor = gameOfFolder; + } + + if (cursor->getType() == GAME) { Sound::getFromTheme(getTheme(), getName(), "launch")->play(); launch(cursor); - }else{ + } + else + { // it's a folder if(cursor->getChildren().size() > 0) { @@ -99,7 +109,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } return true; - }else if(config->isMappedTo("b", input)) + } + else if(config->isMappedTo("b", input)) { if(mCursorStack.size()) { @@ -118,7 +129,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } return true; - }else if(config->isMappedLike(getQuickSystemSelectRightButton(), input)) + } + else if(config->isMappedLike(getQuickSystemSelectRightButton(), input)) { if(Settings::getInstance()->getBool("QuickSystemSelect")) { @@ -126,7 +138,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) ViewController::get()->goToNextGameList(); return true; } - }else if(config->isMappedLike(getQuickSystemSelectLeftButton(), input)) + } + else if(config->isMappedLike(getQuickSystemSelectLeftButton(), input)) { if(Settings::getInstance()->getBool("QuickSystemSelect")) { @@ -134,7 +147,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) ViewController::get()->goToPrevGameList(); return true; } - }else if (config->isMappedTo("x", input)) + } + else if (config->isMappedTo("x", input)) { if (mRoot->getSystem()->isGameSystem()) { @@ -146,7 +160,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } return true; } - }else if (config->isMappedTo("y", input) && UIModeController::getInstance()->isUIModeFull()) + } + else if (config->isMappedTo("y", input) && UIModeController::getInstance()->isUIModeFull()) { if(mRoot->getSystem()->isGameSystem()) { diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index 9b7d22aa22..7cd24b6f15 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -8,6 +8,103 @@ #include "Window.h" #include + +#if defined(_WIN32) +#define _conv(x) GuiTextTool::convertFromWideString(L ## x) +#else +#define _conv(x) x +#endif + +#include +#include +#include "resources\ResourceManager.h" + +std::vector GuiTextTool::mItems; +std::string GuiTextTool::mCurrentLanguage = "en"; +bool GuiTextTool::mCurrentLanguageLoaded = false; + +void GuiTextTool::setLanguage(std::string lang) +{ + mCurrentLanguage = lang; + mCurrentLanguageLoaded = false; +} + +void GuiTextTool::ensureLocalisation() +{ + if (mCurrentLanguageLoaded) + { + if (Settings::getInstance()->getString("Language") == mCurrentLanguage) + return; + + mCurrentLanguage = Settings::getInstance()->getString("Language"); + } + + mCurrentLanguageLoaded = true; + + for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) + delete (*it); + + mItems.clear(); + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/"+ mCurrentLanguage +"/emulationstation2.po"); + if (Utils::FileSystem::exists(xmlpath)) + { + LocalizationItem* currentItem = NULL; + + std::ifstream file(xmlpath); + std::string str; + while (std::getline(file, str)) + { + if (str.length() > 0 && str[0] == '#') + { + if (currentItem != NULL && currentItem->msgid.length() > 0 && currentItem->msgstr.length() > 0) + mItems.push_back(currentItem); + + currentItem = new LocalizationItem(); + } + + if (currentItem != NULL && str.find("msgid") == 0) + { + auto start = str.find("\""); + if (start != std::string::npos) + { + auto end = str.find("\"", start + 1); + if (end != std::string::npos) + currentItem->msgid = str.substr(start + 1, end - start - 1); + } + } + + if (currentItem != NULL && str.find("msgstr") == 0) + { + auto start = str.find("\""); + if (start != std::string::npos) + { + auto end = str.find("\"", start + 1); + if (end != std::string::npos) + currentItem->msgstr = str.substr(start + 1, end - start - 1); + } + } + } + + if (currentItem != NULL) + delete currentItem; + } +} + + +const std::string GuiTextTool::localize(const std::string text) +{ + ensureLocalisation(); + + for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) + { + if (text == (*it)->msgid) + return (*it)->msgstr; + } + + return text; +} + GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255), mPosition(Vector3f::Zero()), mOrigin(Vector2f::Zero()), mRotationOrigin(0.5, 0.5), mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false) @@ -281,6 +378,16 @@ std::string GuiComponent::getValue() const return ""; } +void GuiComponent::setTag(const std::string& value) +{ + mTag = value; +} + +std::string GuiComponent::getTag() const +{ + return mTag; +} + void GuiComponent::textInput(const char* text) { for(auto iter = mChildren.cbegin(); iter != mChildren.cend(); iter++) diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 4c15ba578b..4712ce32f5 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -10,6 +10,10 @@ #include #include +#if defined(_WIN32) +#include +#endif + class Animation; class AnimationController; class Font; @@ -109,6 +113,10 @@ class GuiComponent virtual std::string getValue() const; virtual void setValue(const std::string& value); + + virtual std::string getTag() const; + virtual void setTag(const std::string& value); + virtual void onFocusGained() {}; virtual void onFocusLost() {}; @@ -145,6 +153,8 @@ class GuiComponent GuiComponent* mParent; std::vector mChildren; + std::string mTag; + Vector3f mPosition; Vector2f mOrigin; Vector2f mRotationOrigin; @@ -166,4 +176,47 @@ class GuiComponent AnimationController* mAnimationMap[MAX_ANIMATIONS]; }; + + +class LocalizationItem +{ +public: + std::string msgid; + std::string msgstr; +}; + +class GuiTextTool +{ +public: +#if defined(_WIN32) + static const std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + + std::string string; + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return string; + } +#endif + static void ensureLocalisation(); + static const std::string localize(const std::string text); + + static void setLanguage(std::string lang); + +private: + static std::vector mItems; + static std::string mCurrentLanguage; + static bool mCurrentLanguageLoaded; +}; + +#if defined(_WIN32) +#define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) +#define _L(x) GuiTextTool::localize(x) +#else +#define _T(x) GuiTextTool::localize(x) +#define _L(x) GuiTextTool::localize(x) +#endif // _WIN32 + #endif // ES_CORE_GUI_COMPONENT_H diff --git a/es-core/src/InputManager.cpp b/es-core/src/InputManager.cpp index d074367ebf..59fc54bdac 100644 --- a/es-core/src/InputManager.cpp +++ b/es-core/src/InputManager.cpp @@ -426,7 +426,7 @@ void InputManager::doOnFinish() LOG(LogInfo) << " " << tocall; std::cout << "==============================================\ninput config finish command:\n"; - int exitCode = runSystemCommand(tocall); + int exitCode = runSystemCommand(tocall, "", NULL); std::cout << "==============================================\n"; if(exitCode != 0) diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index f87cddd0cc..fb33b2cea4 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -37,7 +37,7 @@ namespace Renderer { LOG(LogInfo) << "Creating surface..."; - if(SDL_Init(SDL_INIT_VIDEO) != 0) + if (SDL_Init(SDL_INIT_VIDEO) != 0) { LOG(LogError) << "Error initializing SDL!\n " << SDL_GetError(); return false; @@ -62,25 +62,27 @@ namespace Renderer SDL_DisplayMode dispMode; SDL_GetDesktopDisplayMode(0, &dispMode); - windowWidth = Settings::getInstance()->getInt("WindowWidth") ? Settings::getInstance()->getInt("WindowWidth") : dispMode.w; - windowHeight = Settings::getInstance()->getInt("WindowHeight") ? Settings::getInstance()->getInt("WindowHeight") : dispMode.h; - screenWidth = Settings::getInstance()->getInt("ScreenWidth") ? Settings::getInstance()->getInt("ScreenWidth") : windowWidth; - screenHeight = Settings::getInstance()->getInt("ScreenHeight") ? Settings::getInstance()->getInt("ScreenHeight") : windowHeight; + + windowWidth = Settings::getInstance()->getInt("WindowWidth") ? Settings::getInstance()->getInt("WindowWidth") : dispMode.w; + windowHeight = Settings::getInstance()->getInt("WindowHeight") ? Settings::getInstance()->getInt("WindowHeight") : dispMode.h; + screenWidth = Settings::getInstance()->getInt("ScreenWidth") ? Settings::getInstance()->getInt("ScreenWidth") : windowWidth; + screenHeight = Settings::getInstance()->getInt("ScreenHeight") ? Settings::getInstance()->getInt("ScreenHeight") : windowHeight; screenOffsetX = Settings::getInstance()->getInt("ScreenOffsetX") ? Settings::getInstance()->getInt("ScreenOffsetX") : 0; screenOffsetY = Settings::getInstance()->getInt("ScreenOffsetY") ? Settings::getInstance()->getInt("ScreenOffsetY") : 0; - screenRotate = Settings::getInstance()->getInt("ScreenRotate") ? Settings::getInstance()->getInt("ScreenRotate") : 0; - - sdlWindow = SDL_CreateWindow("EmulationStation", - SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, - windowWidth, windowHeight, + screenRotate = Settings::getInstance()->getInt("ScreenRotate") ? Settings::getInstance()->getInt("ScreenRotate") : 0; + + sdlWindow = SDL_CreateWindow("EmulationStation", + SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + windowWidth, windowHeight, SDL_WINDOW_OPENGL | (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN))); - - if(sdlWindow == NULL) + + if (sdlWindow == NULL) { LOG(LogError) << "Error creating SDL window!\n\t" << SDL_GetError(); return false; } - + LOG(LogInfo) << "Created window successfully."; //support screen rotation @@ -125,7 +127,7 @@ namespace Renderer sdlContext = SDL_GL_CreateContext(sdlWindow); // vsync - if(Settings::getInstance()->getBool("VSync")) + // if(Settings::getInstance()->getBool("VSync")) { // SDL_GL_SetSwapInterval(0) for immediate updates (no vsync, default), // 1 for updates synchronized with the vertical retrace, @@ -133,11 +135,11 @@ namespace Renderer // SDL_GL_SetSwapInterval returns 0 on success, -1 on error. // if vsync is requested, try normal vsync; if that doesn't work, try late swap tearing // if that doesn't work, report an error - if(SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) + if (SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; } - else - SDL_GL_SetSwapInterval(0); + // else + // SDL_GL_SetSwapInterval(0); return true; } @@ -215,7 +217,7 @@ namespace Renderer } void swapBuffers() - { + { SDL_GL_SwapWindow(sdlWindow); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } diff --git a/es-core/src/Scripting.cpp b/es-core/src/Scripting.cpp index 3038db0a40..421d4fdb7e 100644 --- a/es-core/src/Scripting.cpp +++ b/es-core/src/Scripting.cpp @@ -28,7 +28,7 @@ namespace Scripting // append folder to path std::string script = *it + " \"" + arg1 + "\" \"" + arg2 + "\""; LOG(LogDebug) << " executing: " << script; - runSystemCommand(script); + runSystemCommand(script, "", NULL); } } } diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 13c09a84a1..a63c75e9d3 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -61,7 +61,7 @@ void Settings::setDefaults() mBoolMap["ShowHiddenFiles"] = false; mBoolMap["DrawFramerate"] = false; mBoolMap["ShowExit"] = true; - mBoolMap["FullscreenBorderless"] = false; + mBoolMap["FullscreenBorderless"] = true; mBoolMap["Windowed"] = false; mBoolMap["SplashScreen"] = true; mBoolMap["SplashScreenProgress"] = true; @@ -91,8 +91,11 @@ void Settings::setDefaults() #else mIntMap["MaxVRAM"] = 100; #endif - - mStringMap["TransitionStyle"] = "fade"; + + mBoolMap["HideWindow"] = true; + mStringMap["GameTransitionStyle"] = "fade"; + mStringMap["TransitionStyle"] = "slide"; + mStringMap["Language"] = "en"; mStringMap["ThemeSet"] = ""; mStringMap["ScreenSaverBehavior"] = "dim"; mStringMap["Scraper"] = "TheGamesDB"; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index cf2548089f..52f75760cc 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -9,7 +9,7 @@ #include #include -std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" }, { "grid" }, { "video" } }; +std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" } }; std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; std::map> ThemeData::sElementMap { @@ -40,7 +40,8 @@ std::map> The { "backgroundCornerSize", NORMALIZED_PAIR }, { "backgroundColor", COLOR }, { "backgroundCenterColor", COLOR }, - { "backgroundEdgeColor", COLOR } } }, + { "backgroundEdgeColor", COLOR }, + { "imageSizeMode", STRING } } }, { "text", { { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, @@ -202,7 +203,7 @@ ThemeData::ThemeData() mVersion = 0; } -void ThemeData::loadFile(std::map sysDataMap, const std::string& path) +void ThemeData::loadFile(std::string system, const std::string& path) { mPaths.push_back(path); @@ -214,9 +215,12 @@ void ThemeData::loadFile(std::map sysDataMap, const st mVersion = 0; mViews.clear(); - mVariables.clear(); - mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); + mSystemThemeFolder = system; + + mVariables.clear(); + //mVariables.insert(system, system); + //mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); pugi::xml_document doc; pugi::xml_parse_result res = doc.load_file(path.c_str()); @@ -232,6 +236,9 @@ void ThemeData::loadFile(std::map sysDataMap, const st if(mVersion == -404) throw error << " tag missing!\n It's either out of date or you need to add " << CURRENT_THEME_FORMAT_VERSION << " inside your tag."; + if (root.attribute("defaultView")) + mDefaultView = root.attribute("defaultView").as_string(); + if(mVersion < MINIMUM_THEME_FORMAT_VERSION) throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; @@ -241,6 +248,22 @@ void ThemeData::loadFile(std::map sysDataMap, const st parseFeatures(root); } +std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) +{ + std::string result = path; + + size_t start_pos = result.find("$system"); + if (start_pos == std::string::npos) + return path; + + result.replace(start_pos, 7, systemThemeFolder); + + //result.replace("$system", systemThemeFolder); + //boost::algorithm::replace_first(result, "$system", systemThemeFolder); + return result; +} + + void ThemeData::parseIncludes(const pugi::xml_node& root) { ThemeException error; @@ -250,8 +273,11 @@ void ThemeData::parseIncludes(const pugi::xml_node& root) { std::string relPath = resolvePlaceholders(node.text().as_string()); std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); - if(!ResourceManager::getInstance()->fileExists(path)) - throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + path = resolveSystemVariable(mSystemThemeFolder, path); + + if (!ResourceManager::getInstance()->fileExists(path)) + continue; + //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; error << " from included file \"" << relPath << "\":\n "; @@ -340,6 +366,12 @@ void ThemeData::parseViews(const pugi::xml_node& root) { ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; parseView(node, view); + + if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) + { + ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; + parseView(node, view); + } } } } @@ -390,10 +422,11 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapsecond) { @@ -492,8 +525,8 @@ const std::shared_ptr& ThemeData::getDefault() { try { - std::map emptyMap; - theme->loadFile(emptyMap, path); + //std::map emptyMap; + theme->loadFile("", path); // emptyMap } catch(ThemeException& e) { LOG(LogError) << e.what(); diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 4fc9faa747..6bbcc01555 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -135,7 +135,7 @@ class ThemeData ThemeData(); // throws ThemeException - void loadFile(std::map sysDataMap, const std::string& path); + void loadFile(std::string system, const std::string& path); enum ElementPropertyType { @@ -159,6 +159,9 @@ class ThemeData static std::map getThemeSets(); static std::string getThemeFromCurrentSet(const std::string& system); + std::string getDefaultView() { return mDefaultView; }; + + private: static std::map< std::string, std::map > sElementMap; static std::vector sSupportedFeatures; @@ -167,6 +170,8 @@ class ThemeData std::deque mPaths; float mVersion; + std::string mDefaultView; + void parseFeatures(const pugi::xml_node& themeRoot); void parseIncludes(const pugi::xml_node& themeRoot); void parseVariables(const pugi::xml_node& root); @@ -174,7 +179,10 @@ class ThemeData void parseView(const pugi::xml_node& viewNode, ThemeView& view); void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element); + std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); + std::map mViews; + std::string mSystemThemeFolder; }; #endif // ES_CORE_THEME_DATA_H diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 0f33e5e240..05337b0e01 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -76,7 +76,6 @@ bool Window::init() } InputManager::getInstance()->init(); - ResourceManager::getInstance()->reloadAll(); //keep a reference to the default fonts, so they don't keep getting destroyed/recreated @@ -91,7 +90,7 @@ bool Window::init() mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); // update our help because font sizes probably changed - if(peekGui()) + if (peekGui()) peekGui()->updateHelpPrompts(); return true; @@ -299,15 +298,15 @@ void Window::setAllowSleep(bool sleep) } void Window::renderLoadingScreen(std::string text) -{ +{ Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); - + ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.6f, 0.0f); + splash.setResize(Renderer::getScreenWidth() * 0.5f, 0.0f); splash.setImage(":/splash.svg"); - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.6f); + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); splash.render(trans); auto& font = mDefaultFonts.at(1); @@ -323,6 +322,36 @@ void Window::renderLoadingScreen(std::string text) Renderer::swapBuffers(); } +void Window::renderBlackScreen(std::string text) +{ + Transform4x4f trans = Transform4x4f::Identity(); + Renderer::setMatrix(trans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); + /* + ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.5f, 0.0f); + splash.setImage(":/splash.svg"); + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.render(trans); + */ + + if (text.length() > 0) + { + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); + + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.835f); + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; + } + + Renderer::swapBuffers(); +} + + void Window::renderHelpPromptsEarly() { mHelp->render(Transform4x4f::Identity()); diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 3f104c648e..5fd238dc62 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -65,6 +65,7 @@ class Window void setAllowSleep(bool sleep); void renderLoadingScreen(std::string text); + void renderBlackScreen(std::string text); void renderHelpPromptsEarly(); // used to render HelpPrompts before a fade void setHelpPrompts(const std::vector& prompts, const HelpStyle& style); diff --git a/es-core/src/components/ComponentGrid.cpp b/es-core/src/components/ComponentGrid.cpp index 066f9a3f0c..e89e7f9c4e 100644 --- a/es-core/src/components/ComponentGrid.cpp +++ b/es-core/src/components/ComponentGrid.cpp @@ -445,11 +445,11 @@ std::vector ComponentGrid::getHelpPrompts() } if(canScrollHoriz && canScrollVert) - prompts.push_back(HelpPrompt("up/down/left/right", "choose")); + prompts.push_back(HelpPrompt("up/down/left/right", _T("CHOOSE"))); else if(canScrollHoriz) - prompts.push_back(HelpPrompt("left/right", "choose")); + prompts.push_back(HelpPrompt("left/right", _T("CHOOSE"))); else if(canScrollVert) - prompts.push_back(HelpPrompt("up/down", "choose")); + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); return prompts; } diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index c2c42d8f39..f8f1bcd518 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -328,7 +328,7 @@ std::vector ComponentList::getHelpPrompts() } if(addMovePrompt) - prompts.push_back(HelpPrompt("up/down", "choose")); + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); } return prompts; diff --git a/es-core/src/components/DateTimeComponent.cpp b/es-core/src/components/DateTimeComponent.cpp index 0052f11ad3..5fcfd2ba73 100644 --- a/es-core/src/components/DateTimeComponent.cpp +++ b/es-core/src/components/DateTimeComponent.cpp @@ -51,7 +51,7 @@ std::string DateTimeComponent::getDisplayString() const if (mDisplayRelative) { //relative time if(mTime.getTime() == 0) - return "never"; + return _T("never"); Utils::Time::DateTime now(Utils::Time::now()); Utils::Time::Duration dur(now.getTime() - mTime.getTime()); @@ -71,7 +71,7 @@ std::string DateTimeComponent::getDisplayString() const } if(mTime.getTime() == 0) - return "unknown"; + return _T("unknown"); return Utils::Time::timeToString(mTime.getTime(), mFormat); } diff --git a/es-core/src/components/DateTimeEditComponent.cpp b/es-core/src/components/DateTimeEditComponent.cpp index d4b6636a39..6676641af5 100644 --- a/es-core/src/components/DateTimeEditComponent.cpp +++ b/es-core/src/components/DateTimeEditComponent.cpp @@ -207,7 +207,7 @@ std::string DateTimeEditComponent::getDisplayString(DisplayMode mode) const { //relative time if(mTime.getTime() == 0) - return "never"; + return _T("never"); Utils::Time::DateTime now(Utils::Time::now()); Utils::Time::Duration dur(now.getTime() - mTime.getTime()); @@ -229,7 +229,7 @@ std::string DateTimeEditComponent::getDisplayString(DisplayMode mode) const } if(mTime.getTime() == 0) - return "unknown"; + return _T("unknown"); return Utils::Time::timeToString(mTime, fmt); } diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 745fede1a3..310830f4fb 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -4,11 +4,18 @@ #include "ThemeData.h" #include "Renderer.h" -GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window) +#include + +#include "animations/LambdaAnimation.h" + +GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window), mLabel(window) { + mSelectedZoomPercent = 1.0f; + mAnimPosition = Vector3f(0, 0); + mDefaultProperties.mSize = getDefaultTileSize(); mDefaultProperties.mPadding = Vector2f(16.0f, 16.0f); - mDefaultProperties.mImageColor = 0xAAAAAABB; + mDefaultProperties.mImageColor = 0xFFFFFFDD; // 0xAAAAAABB; mDefaultProperties.mBackgroundImage = ":/frame.png"; mDefaultProperties.mBackgroundCornerSize = Vector2f(16 ,16); mDefaultProperties.mBackgroundCenterColor = 0xAAAAEEFF; @@ -22,41 +29,126 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mSelectedProperties.mBackgroundCenterColor = 0xFFFFFFFF; mSelectedProperties.mBackgroundEdgeColor = 0xFFFFFFFF; + mLabelSize = Vector2f(1.0, 0.30); + mImage = std::make_shared(mWindow); + mImage->setOrigin(0.5f, 0.5f); + + //mBackground.setOrigin(0.5f, 0.5f); - mBackground.setOrigin(0.5f, 0.5f); + //mLabel.setOrigin(0.5f, 0.5f); + //mLabel.setOrigin(0.5f, 0.0f); + //mLabel.setSize(1.0f, 1.0f); + addChild(&mBackground); addChild(&(*mImage)); + addChild(&mLabel); setSelected(false); setVisible(true); } +void GridTileComponent::resize() +{ + const GridTileProperties& currentProperties = getCurrentProperties(); + + setSize(currentProperties.mSize); + + float height = currentProperties.mSize.y() * mLabelSize.y(); + if (!mLabelVisible) + height = 0; + + mLabel.setPosition(0, mSize.y() - height); + mLabel.setSize(currentProperties.mSize.x(), height); + + if (mLabelSize.x() == 0) + height = 0; + + float topPadding = currentProperties.mPadding.y(); + float bottomPadding = std::max(topPadding, height); + float paddingX = currentProperties.mPadding.x(); + + if (mSelectedZoomPercent != 1.0f) + { + paddingX = mDefaultProperties.mPadding.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.x() * mSelectedZoomPercent; + topPadding = mDefaultProperties.mPadding.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.y() * mSelectedZoomPercent; + + if (mAnimPosition.x() != 0 && mAnimPosition.y() != 0) + { + int x = mAnimPosition.x() - mPosition.x(); + int y = mAnimPosition.y() - mPosition.y(); + + x = x * (1.0 - mSelectedZoomPercent); + y = y * (1.0 - mSelectedZoomPercent); + + mBackground.setPosition(x, y); + } + else + mBackground.setPosition(0, 0); + } + else + mBackground.setPosition(0, 0); + + if (mImage != NULL) + { + mImage->setOrigin(0.5f, 0.5f); + mImage->setPosition(currentProperties.mSize.x() / 2.0f, (currentProperties.mSize.y() - height) / 2.0f); + + if (currentProperties.mImageSizeMode == "minSize") + mImage->setMinSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + else if (currentProperties.mImageSizeMode == "size") + mImage->setSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + else + mImage->setMaxSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + } + + + + + mBackground.setSize(currentProperties.mSize); + mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); +} + void GridTileComponent::render(const Transform4x4f& parentTrans) { + if (!mVisible) + return; + Transform4x4f trans = getTransform() * parentTrans; - if (mVisible) - renderChildren(trans); + //Renderer::setMatrix(trans); + //Renderer::drawRect(0.f, 0.f, mSize.x(), mSize.y(), 0xFF0000FF); + + mBackground.render(trans); + + if (mImage != NULL) + mImage->render(trans); + + if (mLabelVisible) + mLabel.render(trans); } // Update all the tile properties to the new status (selected or default) -void GridTileComponent::update() +void GridTileComponent::update(int deltaTime) { + GuiComponent::update(deltaTime); + const GridTileProperties& currentProperties = getCurrentProperties(); mBackground.setImagePath(currentProperties.mBackgroundImage); - mImage->setColorShift(currentProperties.mImageColor); + if (mImage != NULL) + mImage->setColorShift(currentProperties.mImageColor); + mBackground.setCenterColor(currentProperties.mBackgroundCenterColor); mBackground.setEdgeColor(currentProperties.mBackgroundEdgeColor); resize(); } -void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& /*element*/, unsigned int /*properties*/) +void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); @@ -90,6 +182,9 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem->has("backgroundEdgeColor")) mDefaultProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); + + if (elem && elem->has("imageSizeMode")) + mDefaultProperties.mImageSizeMode = elem->get("imageSizeMode"); } // Apply theme to the selected gridtile @@ -99,7 +194,8 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mSelectedProperties.mSize = elem && elem->has("size") ? elem->get("size") * screen : - getSelectedTileSize(); + mDefaultProperties.mSize; + //getSelectedTileSize(); mSelectedProperties.mPadding = elem && elem->has("padding") ? elem->get("padding") : @@ -127,6 +223,22 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem && elem->has("backgroundEdgeColor")) mSelectedProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); + + mSelectedProperties.mImageSizeMode = elem && elem->has("imageSizeMode") ? + elem->get("imageSizeMode") : + mDefaultProperties.mImageSizeMode; + + elem = theme->getElement(view, element, "text"); + if (elem != NULL) + { + if (elem && elem->has("size")) + mLabelSize = elem->get("size"); + + mLabelVisible = true; + mLabel.applyTheme(theme, view, element, properties); + } + else + mLabelVisible = false; } // Made this a static function because the ImageGridComponent need to know the default tile size @@ -148,39 +260,67 @@ bool GridTileComponent::isSelected() const return mSelected; } -void GridTileComponent::setImage(const std::string& path) +void GridTileComponent::setImage(const std::string& path, std::string name) { - mImage->setImage(path); - - // Resize now to prevent flickering images when scrolling + if (mCurrentPath != path) + { + mCurrentPath = path; + mImage->setImage(path); + } + + mLabel.setText(name); resize(); } -void GridTileComponent::setImage(const std::shared_ptr& texture) +void GridTileComponent::setImage(const std::shared_ptr& texture, std::string name) { mImage->setImage(texture); - + mLabel.setText(name); + // Resize now to prevent flickering images when scrolling resize(); } -void GridTileComponent::setSelected(bool selected) +void GridTileComponent::setSelected(bool selected, Vector3f* pPosition) { mSelected = selected; + + if (selected && pPosition != NULL) + { + auto funeec = [this](float t) + { + this->setSelectedZoom(t); + }; + } + + if (selected && (pPosition != NULL || mSelectedProperties.mPadding.x() != mDefaultProperties.mPadding.y())) + { + if (pPosition == NULL) + mAnimPosition = Vector3f(0, 0, 0); + else + mAnimPosition = Vector3f(pPosition->x(), pPosition->y(), pPosition->z()); + + auto func = [this](float t) + { + this->setSelectedZoom(t); + }; + + setAnimation(new LambdaAnimation(func, 100), 0, [this] { + this->setSelectedZoom(1); + mAnimPosition = Vector3f(0, 0, 0); + }, false, 3); + } } -void GridTileComponent::setVisible(bool visible) +void GridTileComponent::setSelectedZoom(float percent) { - mVisible = visible; + mSelectedZoomPercent = percent; + resize(); } -void GridTileComponent::resize() +void GridTileComponent::setVisible(bool visible) { - const GridTileProperties& currentProperties = getCurrentProperties(); - - mImage->setMaxSize(currentProperties.mSize - currentProperties.mPadding * 2); - mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); - mBackground.fitTo(currentProperties.mSize - mBackground.getCornerSize() * 2); + mVisible = visible; } const GridTileProperties& GridTileComponent::getCurrentProperties() const diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index c191ce94ab..bdaa611a16 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -4,6 +4,7 @@ #include "NinePatchComponent.h" #include "ImageComponent.h" +#include "TextComponent.h" struct GridTileProperties { @@ -14,6 +15,8 @@ struct GridTileProperties Vector2f mBackgroundCornerSize; unsigned int mBackgroundCenterColor; unsigned int mBackgroundEdgeColor; + + std::string mImageSizeMode; }; class GridTileComponent : public GuiComponent @@ -22,7 +25,8 @@ class GridTileComponent : public GuiComponent GridTileComponent(Window* window); void render(const Transform4x4f& parentTrans) override; - void update(); + void update(int deltaTime) override; + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); // Made this a static function because the ImageGridComponent need to know the default tile max size @@ -31,9 +35,9 @@ class GridTileComponent : public GuiComponent Vector2f getSelectedTileSize() const; bool isSelected() const; - void setImage(const std::string& path); - void setImage(const std::shared_ptr& texture); - void setSelected(bool selected); + void setImage(const std::string& path, std::string name); + void setImage(const std::shared_ptr& texture, std::string name); + void setSelected(bool selected, Vector3f* pPosition = NULL); void setVisible(bool visible); private: @@ -41,13 +45,25 @@ class GridTileComponent : public GuiComponent const GridTileProperties& getCurrentProperties() const; std::shared_ptr mImage; + TextComponent mLabel; + Vector2f mLabelSize; + + bool mLabelVisible; + NinePatchComponent mBackground; GridTileProperties mDefaultProperties; GridTileProperties mSelectedProperties; + std::string mCurrentPath; + + void setSelectedZoom(float percent); + + float mSelectedZoomPercent; bool mSelected; bool mVisible; + + Vector3f mAnimPosition; }; #endif // ES_CORE_COMPONENTS_GRID_TILE_COMPONENT_H diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 14f5fac134..8358ee6839 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -133,23 +133,22 @@ void ImageComponent::setDefaultImage(std::string path) void ImageComponent::setImage(std::string path, bool tile) { - if(path.empty() || !ResourceManager::getInstance()->fileExists(path)) + if (path.empty() || !ResourceManager::getInstance()->fileExists(path)) { - if(mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) + if (mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) mTexture.reset(); else mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic); - } else { + } + else mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic); - } - + resize(); } void ImageComponent::setImage(const char* path, size_t length, bool tile) { mTexture.reset(); - mTexture = TextureResource::get("", tile); mTexture->initFromMemory(path, length); @@ -263,7 +262,7 @@ void ImageComponent::setOpacity(unsigned char opacity) void ImageComponent::updateVertices() { - if(!mTexture || !mTexture->isInitialized()) + if (!mTexture || !mTexture->isInitialized()) return; // we go through this mess to make sure everything is properly rounded @@ -274,6 +273,7 @@ void ImageComponent::updateVertices() mVertices[0].pos = Vector2f(topLeft.x(), topLeft.y()); mVertices[1].pos = Vector2f(topLeft.x(), bottomRight.y()); + mVertices[2].pos = Vector2f(bottomRight.x(), topLeft.y()); mVertices[3].pos = Vector2f(bottomRight.x(), topLeft.y()); @@ -281,11 +281,13 @@ void ImageComponent::updateVertices() mVertices[5].pos = Vector2f(bottomRight.x(), bottomRight.y()); float px, py; - if(mTexture->isTiled()) + if (mTexture->isTiled()) { px = mSize.x() / getTextureSize().x(); py = mSize.y() / getTextureSize().y(); - }else{ + } + else + { px = 1; py = 1; } @@ -298,12 +300,13 @@ void ImageComponent::updateVertices() mVertices[4].tex = Vector2f(mTopLeftCrop.x(), 1 - mBottomRightCrop.y()); mVertices[5].tex = Vector2f(px * mBottomRightCrop.x(), 1 - mBottomRightCrop.y()); - if(mFlipX) + if (mFlipX) { for(int i = 0; i < 6; i++) mVertices[i].tex[0] = px - mVertices[i].tex[0]; } - if(mFlipY) + + if (mFlipY) { for(int i = 0; i < 6; i++) mVertices[i].tex[1] = py - mVertices[i].tex[1]; @@ -320,14 +323,16 @@ void ImageComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); Renderer::setMatrix(trans); - if(mTexture && mOpacity > 0) + if (mTexture && mOpacity > 0) { - if(Settings::getInstance()->getBool("DebugImage")) { + if (Settings::getInstance()->getBool("DebugImage")) + { Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; Renderer::drawRect(targetSizePos.x(), targetSizePos.y(), mTargetSize.x(), mTargetSize.y(), 0xFF000033); Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); } - if(mTexture->isInitialized()) + + if (mTexture->isInitialized()) { // actually draw the image // The bind() function returns false if the texture is not currently loaded. A blank @@ -355,7 +360,9 @@ void ImageComponent::render(const Transform4x4f& parentTrans) glDisable(GL_TEXTURE_2D); glDisable(GL_BLEND); - }else{ + } + else + { LOG(LogError) << "Image texture is not initialized!"; mTexture.reset(); } @@ -471,6 +478,6 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s std::vector ImageComponent::getHelpPrompts() { std::vector ret; - ret.push_back(HelpPrompt("a", "select")); + ret.push_back(HelpPrompt("a", "SELECTIONNER")); return ret; } diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 65b329adcb..9dc51138dd 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -50,6 +50,8 @@ class ImageGridComponent : public IList void onSizeChanged() override; inline void setCursorChangedCallback(const std::function& func) { mCursorChangedCallback = func; } + void setThemeName(std::string name) { mName = name; }; + protected: virtual void onCursorChanged(const CursorState& state) override; @@ -57,13 +59,10 @@ class ImageGridComponent : public IList // TILES void buildTiles(); void updateTiles(); - void updateTileAtPos(int tilePos, int imgPos, int bufferTop, int bufferBot); + void updateTileAtPos(int tilePos, int imgPos); int getStartPosition() const; void calcGridDimension(); - // IMAGES & ENTRIES - const int texBuffersBehind[4] = { 1, 1, 1, 1 }; - const int texBuffersForward[4] = { 1, 2, 3, 3 }; bool mEntriesDirty; int mLastCursor; std::string mDefaultGameTexture; @@ -77,6 +76,10 @@ class ImageGridComponent : public IList std::shared_ptr mTheme; std::vector< std::shared_ptr > mTiles; + std::string mName; + + int mStartPosition; + // MISCELLANEOUS ScrollDirection mScrollDirection; std::function mCursorChangedCallback; @@ -87,6 +90,8 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList void ImageGridComponent::update(int deltaTime) { listUpdate(deltaTime); - + for(auto it = mTiles.begin(); it != mTiles.end(); it++) - (*it)->update(); + (*it)->update(deltaTime); } template @@ -179,16 +184,29 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) // If it's the selected image, keep it for later, otherwise render it now if(tile->isSelected()) selectedTile = tile; - else - tile->render(trans); + // else + // tile->render(trans); } - Renderer::popClipRect(); + // Render the selected image on top of the others if (selectedTile != NULL) selectedTile->render(trans); + for (auto it = mTiles.begin(); it != mTiles.end(); it++) + { + std::shared_ptr tile = (*it); + + // If it's the selected image, keep it for later, otherwise render it now + if (tile->isSelected()) + ; // selectedTile = tile; + else + tile->render(trans); + } + + Renderer::popClipRect(); + listRenderTitleOverlay(trans); GuiComponent::renderChildren(trans); @@ -283,23 +301,55 @@ void ImageGridComponent::onSizeChanged() template void ImageGridComponent::onCursorChanged(const CursorState& state) { - updateTiles(); + if (mLastCursor != mCursor) + { + // 1 if scrolling down, -1 if scrolling up + int scrollDirection = mCursor >= mLastCursor ? 1 : -1; + + int partialRow = (int)mLastRowPartial; + int maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); + + if (mCursor == 0) + mStartPosition = 0; + else if (mCursor < mStartPosition) + mStartPosition = std::max(0, mStartPosition - mGridDimension.x()); + else if (mCursor >= maxPos) + { + mStartPosition = mStartPosition + mGridDimension.x(); + + maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); + if (mCursor >= maxPos) + { + int cursorRow = mCursor / mGridDimension.x(); + mStartPosition = cursorRow * mGridDimension.x(); + if (mStartPosition >= mGridDimension.x() && cursorRow > 0) + { + cursorRow--; + mStartPosition = cursorRow * mGridDimension.x(); + } + } + } - if(mCursorChangedCallback) - mCursorChangedCallback(state); + updateTiles(); + + if (mCursorChangedCallback) + mCursorChangedCallback(state); + } + + mLastCursor = mCursor; } // Create and position tiles (mTiles) template void ImageGridComponent::buildTiles() { + mStartPosition = 0; mTiles.clear(); calcGridDimension(); Vector2f tileDistance = mTileSize + mMargin; - Vector2f bufferSize = Vector2f(mScrollDirection == SCROLL_HORIZONTALLY ? tileDistance.x() * texBuffersForward[3] : 0, - mScrollDirection == SCROLL_VERTICALLY ? tileDistance.y() * texBuffersForward[3] : 0); + Vector2f bufferSize = Vector2f(0, 0); // mScrollDirection == SCROLL_HORIZONTALLY ? tileDistance.x() * texBuffersForward[3] : 0, mScrollDirection == SCROLL_VERTICALLY ? tileDistance.y() * texBuffersForward[3] : 0); Vector2f startPosition = mTileSize / 2 - bufferSize; int X, Y; @@ -319,10 +369,10 @@ void ImageGridComponent::buildTiles() tile->setPosition(X * tileDistance.x() + startPosition.x(), Y * tileDistance.y() + startPosition.y()); tile->setOrigin(0.5f, 0.5f); - tile->setImage(""); + tile->setImage("", ""); if (mTheme) - tile->applyTheme(mTheme, "grid", "gridtile", ThemeFlags::ALL); + tile->applyTheme(mTheme, mName, "gridtile", ThemeFlags::ALL); mTiles.push_back(tile); } @@ -334,84 +384,73 @@ void ImageGridComponent::updateTiles() { if (!mTiles.size()) return; - + // Stop updating the tiles at highest scroll speed if (mScrollTier == 3) { for (int ti = 0; ti < (int)mTiles.size(); ti++) { std::shared_ptr tile = mTiles.at(ti); - + tile->setSelected(false); - tile->setImage(mDefaultGameTexture); + tile->setImage(mDefaultGameTexture, ""); tile->setVisible(false); } return; } - // 1 if scrolling down, -1 if scrolling up - int scrollDirection = mCursor >= mLastCursor ? 1 : -1; - - // If going down, update from top to bottom - // If going up, update from bottom to top - int ti = scrollDirection == 1 ? 0 : (int)mTiles.size() - 1; - int end = scrollDirection == 1 ? (int)mTiles.size() : -1; - + int i = 0; + int end = (int)mTiles.size(); int img = getStartPosition(); - if (scrollDirection == -1) - img += (int)mTiles.size() - 1; - - // Calculate buffer size depending on scroll speed and direction - int bufferBehind = (texBuffersForward[3] - texBuffersBehind[mScrollTier]) * mGridDimension.x(); - int bufferForward = (texBuffersForward[3] - texBuffersForward[mScrollTier]) * mGridDimension.x(); - - int bufferTop = scrollDirection == 1 ? bufferBehind : bufferForward; - int bufferBot = scrollDirection == 1 ? bufferForward : bufferBehind; - // Update the tiles - while (ti != end) + while (i != end) { - updateTileAtPos(ti, img, bufferTop, bufferBot); - - ti += scrollDirection; - img += scrollDirection; + updateTileAtPos(i, img); + i++; img++; } mLastCursor = mCursor; } template -void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, int bufferTop, int bufferBot) +void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos) { std::shared_ptr tile = mTiles.at(tilePos); // If we have more tiles than we have to display images on screen, hide them - if(imgPos < 0 || imgPos >= size() - || tilePos < bufferTop || tilePos >= (int)mTiles.size() - bufferBot) // Same for tiles out of the buffer + if(imgPos < 0 || imgPos >= size() || tilePos < 0 || tilePos >= (int) mTiles.size()) // Same for tiles out of the buffer { tile->setSelected(false); - tile->setImage(""); + tile->setImage("", ""); tile->setVisible(false); } else { - tile->setSelected(imgPos == mCursor); + + if (imgPos == mCursor && mCursor != mLastCursor) + { + int dif = mCursor - tilePos; + int idx = mLastCursor - dif; + + if (idx < 0 || idx >= mTiles.size()) + idx = 0; + + tile->setSelected(true, &mTiles.at(idx)->getPosition()); + } + else + tile->setSelected(imgPos == mCursor); + tile->setVisible(true); + std::string name = mEntries.at(imgPos).name; // .object->getName(); std::string imagePath = mEntries.at(imgPos).data.texturePath; + if (ResourceManager::getInstance()->fileExists(imagePath)) - { - tile->setImage(imagePath); - } + tile->setImage(imagePath, name); + else if (mEntries.at(imgPos).object->getType() == 2) + tile->setImage(mDefaultFolderTexture, name); else - { - // FileType::FOLDER = 2, but FileData is our template parameter T, - // so we don't want to bring that dependence to FileData here - if (mEntries.at(imgPos).object->getType() == 2) - tile->setImage(mDefaultFolderTexture); - else - tile->setImage(mDefaultGameTexture); - } + tile->setImage(mDefaultGameTexture, name); } } @@ -419,32 +458,29 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, int bufferT template int ImageGridComponent::getStartPosition() const { + return mStartPosition; + // The "partialRow" variable exist because we want to keep the same positioning behavior in both // case, whenever we have an integer number of rows or not (the last partial row is ignored when // calculating position and the cursor shouldn't end up in this row when close to the end) int partialRow = (int)mLastRowPartial; int cursorRow = mCursor / mGridDimension.x(); + int cursorCol = mCursor % mGridDimension.x(); int start = (cursorRow - ((mGridDimension.y() - partialRow) / 2)) * mGridDimension.x(); - - // Number of tiles which are just used as a buffer for texture loading - int bufferSize = texBuffersForward[3] * mGridDimension.x(); - - if(start + (mGridDimension.x() * (mGridDimension.y() - partialRow)) >= (int)mEntries.size() + bufferSize) + if (start + (mGridDimension.x() * (mGridDimension.y() - partialRow)) >= (int)mEntries.size()) { // If we are at the end put the row as close as we can and no higher, using the following formula // Where E is the nb of entries, X the grid x dim (nb of column), Y the grid y dim (nb of line) // start = first tile of last row - nb column * (nb line - 1) // = (E - 1) / X * X - X * (Y - 1) // = X * ((E - 1) / X - Y + 1) - start = mGridDimension.x() * (((int)mEntries.size() - 1) / mGridDimension.x() - mGridDimension.y() + 1 + partialRow) + bufferSize; + start = mGridDimension.x() * (((int)mEntries.size() - 1) / mGridDimension.x() - mGridDimension.y() + 1 + partialRow); } - if(start < -bufferSize) - { - start = -bufferSize; - } + if (start < 0) + return 0; return start; } @@ -464,7 +500,7 @@ void ImageGridComponent::calcGridDimension() // Invert dimensions for horizontally scrolling grid if (mScrollDirection == SCROLL_HORIZONTALLY) - mGridDimension = Vector2i(mGridDimension.y(), mGridDimension.x()); + mGridDimension = Vector2i((float) (int)gridDimension.y(), mGridDimension.x()); // Grid dimension validation if (mGridDimension.x() < 1) @@ -473,7 +509,7 @@ void ImageGridComponent::calcGridDimension() LOG(LogError) << "Theme defined grid Y dimension below 1"; // Add extra tiles to both side depending on max texture buffer - mGridDimension.y() += texBuffersForward[3] * 2; +// mGridDimension.y() += texBuffersForward[3] * 2; }; diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 2e60b59459..343c86f618 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -7,7 +7,7 @@ #define TITLE_HEIGHT (mTitle->getFont()->getLetterHeight() + TITLE_VERT_PADDING) -MenuComponent::MenuComponent(Window* window, const char* title, const std::shared_ptr& titleFont) : GuiComponent(window), +MenuComponent::MenuComponent(Window* window, std::string title, const std::shared_ptr& titleFont) : GuiComponent(window), mBackground(window), mGrid(window, Vector2i(1, 3)) { addChild(&mBackground); @@ -19,7 +19,7 @@ MenuComponent::MenuComponent(Window* window, const char* title, const std::share mTitle = std::make_shared(mWindow); mTitle->setHorizontalAlignment(ALIGN_CENTER); mTitle->setColor(0x555555FF); - setTitle(title, titleFont); + setTitle(title.c_str(), titleFont); mGrid.setEntry(mTitle, Vector2i(0, 0), false); // set up list which will never change (externally, anyway) diff --git a/es-core/src/components/MenuComponent.h b/es-core/src/components/MenuComponent.h index 7babd2d3a5..e2c9ea31f2 100644 --- a/es-core/src/components/MenuComponent.h +++ b/es-core/src/components/MenuComponent.h @@ -20,7 +20,7 @@ std::shared_ptr makeArrow(Window* window); class MenuComponent : public GuiComponent { public: - MenuComponent(Window* window, const char* title, const std::shared_ptr& titleFont = Font::get(FONT_SIZE_LARGE)); + MenuComponent(Window* window, std::string title, const std::shared_ptr& titleFont = Font::get(FONT_SIZE_LARGE)); void onSizeChanged() override; diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index bf922acdcb..613a22c026 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -87,7 +87,7 @@ class OptionListComponent : public GuiComponent mMenu.addRow(row, (!mParent->mMultiSelect && it->selected)); } - mMenu.addButton("BACK", "accept", [this] { delete this; }); + mMenu.addButton(_T("BACK"), "accept", [this] { delete this; }); if(mParent->mMultiSelect) { @@ -128,7 +128,7 @@ class OptionListComponent : public GuiComponent std::vector getHelpPrompts() override { auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); return prompts; } }; @@ -205,6 +205,9 @@ class OptionListComponent : public GuiComponent }else if(config->isMappedLike("right", input)) { + if (mEntries.size() == 0) + return true; + // move selection to next unsigned int i = getSelectedId(); int next = (i + 1) % mEntries.size(); @@ -268,6 +271,31 @@ class OptionListComponent : public GuiComponent onSelectedChanged(); } + void selectFirstItem() + { + for (unsigned int i = 0; i < mEntries.size(); i++) + mEntries.at(i).selected = false; + + if (mEntries.size() > 0) + mEntries.at(0).selected = true; + + onSelectedChanged(); + } + + void clear() { + mEntries.clear(); + } + + inline void invalidate() { + onSelectedChanged(); + } + + void setSelectedChangedCallback(const std::function& callback) + { + mSelectedChangedCallback = callback; + } + + private: unsigned int getSelectedId() { @@ -287,6 +315,7 @@ class OptionListComponent : public GuiComponent mWindow->pushGui(new OptionListPopup(mWindow, this, mName)); } + void onSelectedChanged() { if(mMultiSelect) @@ -303,26 +332,29 @@ class OptionListComponent : public GuiComponent // display currently selected + l/r cursors for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) { - if(it->selected) + if (it->selected) { mText.setText(Utils::String::toUpper(it->name)); mText.setSize(0, mText.getSize().y()); setSize(mText.getSize().x() + mLeftArrow.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); - if(mParent) // hack since theres no "on child size changed" callback atm... + if (mParent) // hack since theres no "on child size changed" callback atm... mParent->onSizeChanged(); break; } } } + + if (mSelectedChangedCallback) + mSelectedChangedCallback(mEntries.at(getSelectedId()).object); } std::vector getHelpPrompts() override { std::vector prompts; if(!mMultiSelect) - prompts.push_back(HelpPrompt("left/right", "change")); + prompts.push_back(HelpPrompt("left/right", "MODIFIER")); - prompts.push_back(HelpPrompt("a", "select")); + prompts.push_back(HelpPrompt("a", "SELECTIONNER")); return prompts; } @@ -334,6 +366,7 @@ class OptionListComponent : public GuiComponent ImageComponent mRightArrow; std::vector mEntries; + std::function mSelectedChangedCallback; }; #endif // ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H diff --git a/es-core/src/components/SliderComponent.cpp b/es-core/src/components/SliderComponent.cpp index 619c798356..81f97e9b09 100644 --- a/es-core/src/components/SliderComponent.cpp +++ b/es-core/src/components/SliderComponent.cpp @@ -139,6 +139,6 @@ void SliderComponent::onValueChanged() std::vector SliderComponent::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("left/right", "change")); + prompts.push_back(HelpPrompt("left/right", "MODIFIER")); return prompts; } diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index ec549020bc..747e507fc3 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -72,6 +72,6 @@ void SwitchComponent::onStateChanged() std::vector SwitchComponent::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("a", "change")); + prompts.push_back(HelpPrompt("a", "MODIFIER")); return prompts; } diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 6cd5f082a1..fae35d643c 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -228,7 +228,7 @@ void VideoComponent::applyTheme(const std::shared_ptr& theme, const s std::vector VideoComponent::getHelpPrompts() { std::vector ret; - ret.push_back(HelpPrompt("a", "select")); + ret.push_back(HelpPrompt("a", "SELECTIONNER")); return ret; } diff --git a/es-core/src/guis/GuiInputConfig.cpp b/es-core/src/guis/GuiInputConfig.cpp index 29111a4841..3ec21f101f 100755 --- a/es-core/src/guis/GuiInputConfig.cpp +++ b/es-core/src/guis/GuiInputConfig.cpp @@ -133,8 +133,9 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi return false; } - // apply filtering for quirks related to trigger mapping - if(filterTrigger(input, config, i)) + + // filter for input quirks specific to Sony DualShock 3 + if(filterTrigger(input, config)) return false; // we are configuring @@ -336,26 +337,21 @@ void GuiInputConfig::clearAssignment(int inputId) mTargetConfig->unmapInput(GUI_INPUT_CONFIG_LIST[inputId].name); } -bool GuiInputConfig::filterTrigger(Input input, InputConfig* config, int inputId) +bool GuiInputConfig::filterTrigger(Input input, InputConfig* config) { #if defined(__linux__) // match PlayStation joystick with 6 axes only if((strstr(config->getDeviceName().c_str(), "PLAYSTATION") != NULL \ - || strstr(config->getDeviceName().c_str(), "PS3 Ga") != NULL \ - || strstr(config->getDeviceName().c_str(), "PS(R) Ga") != NULL) \ + || strstr(config->getDeviceName().c_str(), "PS3 Game") != NULL \ + || strstr(config->getDeviceName().c_str(), "PS(R) Game") != NULL) \ && InputManager::getInstance()->getAxisCountByDevice(config->getDeviceId()) == 6) { // digital triggers are unwanted if (input.type == TYPE_BUTTON && (input.id == 6 || input.id == 7)) return true; - } - - // ignore negative pole for axes 2/5 only when triggers are being configured - if((mSkipAxis || strstr(GUI_INPUT_CONFIG_LIST[inputId].name, "Trigger") != NULL) \ - && input.type == TYPE_AXIS && (input.id == 2 || input.id == 5) && input.value < 0) - { - mSkipAxis = true; - return true; + // ignore analog values < 0 + if (input.type == TYPE_AXIS && (input.id == 2 || input.id == 5) && input.value < 0) + return true; } #endif diff --git a/es-core/src/guis/GuiInputConfig.h b/es-core/src/guis/GuiInputConfig.h index d33a8fad7b..bef5f03eab 100644 --- a/es-core/src/guis/GuiInputConfig.h +++ b/es-core/src/guis/GuiInputConfig.h @@ -28,7 +28,7 @@ class GuiInputConfig : public GuiComponent bool assign(Input input, int inputId); void clearAssignment(int inputId); - bool filterTrigger(Input input, InputConfig* config, int inputId); + bool filterTrigger(Input input, InputConfig* config); void rowDone(); @@ -50,7 +50,6 @@ class GuiInputConfig : public GuiComponent Input mHeldInput; int mHeldTime; int mHeldInputId; - bool mSkipAxis; BusyComponent mBusyAnim; }; diff --git a/es-core/src/guis/GuiTextEditPopup.cpp b/es-core/src/guis/GuiTextEditPopup.cpp index 267939430e..de7390cbb8 100644 --- a/es-core/src/guis/GuiTextEditPopup.cpp +++ b/es-core/src/guis/GuiTextEditPopup.cpp @@ -68,6 +68,6 @@ bool GuiTextEditPopup::input(InputConfig* config, Input input) std::vector GuiTextEditPopup::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); return prompts; } diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index 81722a549c..d47f6204a0 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -8,6 +8,9 @@ #endif #include +#include +#include "Window.h" + int runShutdownCommand() { #ifdef WIN32 // windows @@ -26,15 +29,111 @@ int runRestartCommand() #endif } -int runSystemCommand(const std::string& cmd_utf8) +std::string trim(const std::string& str) +{ + size_t first = str.find_first_not_of(' '); + if (std::string::npos == first) + { + return str; + } + size_t last = str.find_last_not_of(' '); + return str.substr(first, (last - first + 1)); +} + +void split_cmd(const std::string& cmd, + std::string* executable, + std::string* parameters) +{ + std::string c(cmd); + size_t exec_end; + + c = trim(c); + //boost::trim_all(c); + + if (c[0] == '\"') + { + exec_end = c.find_first_of('\"', 1); + if (std::string::npos != exec_end) + { + *executable = c.substr(1, exec_end - 1); + *parameters = c.substr(exec_end + 1); + } + else + { + *executable = c.substr(1, exec_end); + std::string().swap(*parameters); + } + } + else + { + exec_end = c.find_first_of(' ', 0); + if (std::string::npos != exec_end) + { + *executable = c.substr(0, exec_end); + *parameters = c.substr(exec_end + 1); + } + else + { + *executable = c.substr(0, exec_end); + std::string().swap(*parameters); + } + } +} + +int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window) { #ifdef WIN32 + if (window != NULL) + window->renderBlackScreen("Chargement en cours..."); + // on Windows we use _wsystem to support non-ASCII paths // which requires converting from utf8 to a wstring typedef std::codecvt_utf8 convert_type; std::wstring_convert converter; std::wstring wchar_str = converter.from_bytes(cmd_utf8); - return _wsystem(wchar_str.c_str()); + + std::string exe; + std::string args; + + split_cmd(cmd_utf8, &exe, &args); + + SHELLEXECUTEINFO lpExecInfo; + lpExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); + lpExecInfo.lpFile = exe.c_str(); + lpExecInfo.fMask = SEE_MASK_DOENVSUBST | SEE_MASK_NOCLOSEPROCESS; + lpExecInfo.hwnd = NULL; + lpExecInfo.lpVerb = "open"; // to open program + lpExecInfo.lpParameters = args.c_str(); // file name as an argument + lpExecInfo.lpDirectory = NULL; + lpExecInfo.nShow = SW_SHOW; // show command prompt with normal window size + lpExecInfo.hInstApp = (HINSTANCE)SE_ERR_DDEFAIL; //WINSHELLAPI BOOL WINAPI result; + ShellExecuteEx(&lpExecInfo); + + if (lpExecInfo.hProcess != NULL) + { + if (window == NULL) + WaitForSingleObject(lpExecInfo.hProcess, INFINITE); + else + { + while (WaitForSingleObject(lpExecInfo.hProcess, 50) == 0x00000102L) + { + bool polled = false; + + SDL_Event event; + while (SDL_PollEvent(&event)) + polled = true; + + if (window != NULL && polled) + window->renderBlackScreen("Chargement en cours..."); + } + } + + CloseHandle(lpExecInfo.hProcess); + return 0; + } + + return 1; + //return _wsystem(wchar_str.c_str()); #else return system(cmd_utf8.c_str()); #endif diff --git a/es-core/src/platform.h b/es-core/src/platform.h index 7ffaf5d31e..398f7c12e6 100644 --- a/es-core/src/platform.h +++ b/es-core/src/platform.h @@ -21,9 +21,11 @@ #define GLHEADER #endif +class Window; + int runShutdownCommand(); // shut down the system (returns 0 if successful) int runRestartCommand(); // restart the system (returns 0 if successful) -int runSystemCommand(const std::string& cmd_utf8); // run a utf-8 encoded in the shell (requires wstring conversion on Windows) +int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window); // run a utf-8 encoded in the shell (requires wstring conversion on Windows) int quitES(const std::string& filename); void touch(const std::string& filename); diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index f0e93e61cc..d5086b7e05 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -78,8 +78,8 @@ Font::Font(int size, const std::string& path) : mSize(size), mPath(path) initLibrary(); // always initialize ASCII characters - for(unsigned int i = 32; i < 128; i++) - getGlyph(i); +// for(unsigned int i = 32; i < 128; i++) + // getGlyph(i); clearFaceCache(); } @@ -247,6 +247,10 @@ std::vector getFallbackFontPaths() for(unsigned int i = 0; i < sizeof(fontNames) / sizeof(fontNames[0]); i++) { std::string path = fontDir + fontNames[i]; + + if (i == 0) + path = ":/fontawesome_webfont.ttf"; + if(ResourceManager::getInstance()->fileExists(path)) fontPaths.push_back(path); } diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index f493a91c46..e2a613a859 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -135,6 +135,7 @@ bool TextureData::load() if (!mPath.empty()) { std::shared_ptr& rm = ResourceManager::getInstance(); + const ResourceData& data = rm->getFileData(mPath); // is it an SVG? if (mPath.substr(mPath.size() - 4, std::string::npos) == ".svg") @@ -163,6 +164,9 @@ bool TextureData::uploadAndBind() if (mTextureID != 0) { glBindTexture(GL_TEXTURE_2D, mTextureID); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } else { @@ -182,12 +186,13 @@ bool TextureData::uploadAndBind() glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)mWidth, (GLsizei)mHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, mDataRGBA); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); const GLint wrapMode = mTile ? GL_REPEAT : GL_CLAMP_TO_EDGE; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode); } + return true; } diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 5a0a351c97..e4af303eb3 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -147,7 +147,12 @@ namespace Utils { // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows char* envHome = getenv("HOME"); - if(envHome) + +#ifdef _DEBUG + envHome = "H:/[Emulz]/EmulationStation/"; +#endif + + if (envHome) path = getGenericPath(envHome); #if defined(_WIN32) @@ -174,10 +179,7 @@ namespace Utils std::string getCWDPath() { char temp[512]; - - // return current working directory path return (getcwd(temp, 512) ? getGenericPath(temp) : ""); - } // getCWDPath std::string getExePath() @@ -185,14 +187,22 @@ namespace Utils static std::string path; // only construct the exepath once - if(!path.length()) + if (!path.length()) { - path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); - - if(isRegularFile(path)) +#if defined(_WIN32) + char buffer[MAX_PATH]; + DWORD size = MAX_PATH; + DWORD result = GetModuleFileNameA(NULL, buffer, size); + if (result) { - path = getParent(path); + std::string ret = buffer; + path = getParent(ret); + return ret; } +#endif + path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); + if (isRegularFile(path)) + path = getParent(path); } // return constructed exepath @@ -644,7 +654,7 @@ namespace Utils #endif // _WIN32 // filenames starting with . are hidden in linux, we do this check for windows as well - if(getFileName(path)[0] == '.') + if (getFileName(path)[0] == '.') return true; // not hidden diff --git a/es-core/src/utils/StringUtil.cpp b/es-core/src/utils/StringUtil.cpp index 8d35f9701a..0e1da37ea8 100644 --- a/es-core/src/utils/StringUtil.cpp +++ b/es-core/src/utils/StringUtil.cpp @@ -1,299 +1,336 @@ -#include "utils/StringUtil.h" - -#include -#include - -namespace Utils -{ - namespace String - { - unsigned int chars2Unicode(const std::string& _string, size_t& _cursor) - { - const char& c = _string[_cursor]; - unsigned int result = '?'; - - if((c & 0x80) == 0) // 0xxxxxxx, one byte character - { - // 0xxxxxxx - result = ((_string[_cursor++] ) ); - } - else if((c & 0xE0) == 0xC0) // 110xxxxx, two byte character - { - // 110xxxxx 10xxxxxx - result = ((_string[_cursor++] & 0x1F) << 6) | - ((_string[_cursor++] & 0x3F) ); - } - else if((c & 0xF0) == 0xE0) // 1110xxxx, three byte character - { - // 1110xxxx 10xxxxxx 10xxxxxx - result = ((_string[_cursor++] & 0x0F) << 12) | - ((_string[_cursor++] & 0x3F) << 6) | - ((_string[_cursor++] & 0x3F) ); - } - else if((c & 0xF8) == 0xF0) // 11110xxx, four byte character - { - // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - result = ((_string[_cursor++] & 0x07) << 18) | - ((_string[_cursor++] & 0x3F) << 12) | - ((_string[_cursor++] & 0x3F) << 6) | - ((_string[_cursor++] & 0x3F) ); - } - else - { - // error, invalid unicode - ++_cursor; - } - - return result; - - } // chars2Unicode - - std::string unicode2Chars(const unsigned int _unicode) - { - std::string result; - - if(_unicode < 0x80) // one byte character - { - result += ((_unicode ) & 0xFF); - } - else if(_unicode < 0x800) // two byte character - { - result += ((_unicode >> 6) & 0xFF) | 0xC0; - result += ((_unicode ) & 0x3F) | 0x80; - } - else if(_unicode < 0xFFFF) // three byte character - { - result += ((_unicode >> 12) & 0xFF) | 0xE0; - result += ((_unicode >> 6) & 0x3F) | 0x80; - result += ((_unicode ) & 0x3F) | 0x80; - } - else if(_unicode <= 0x1fffff) // four byte character - { - result += ((_unicode >> 18) & 0xFF) | 0xF0; - result += ((_unicode >> 12) & 0x3F) | 0x80; - result += ((_unicode >> 6) & 0x3F) | 0x80; - result += ((_unicode ) & 0x3F) | 0x80; - } - else - { - // error, invalid unicode - result += '?'; - } - - return result; - - } // unicode2Chars - - size_t nextCursor(const std::string& _string, const size_t _cursor) - { - size_t result = _cursor; - - while(result < _string.length()) - { - ++result; - - if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx - break; - } - - return result; - - } // nextCursor - - size_t prevCursor(const std::string& _string, const size_t _cursor) - { - size_t result = _cursor; - - while(result > 0) - { - --result; - - if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx - break; - } - - return result; - - } // prevCursor - - size_t moveCursor(const std::string& _string, const size_t _cursor, const int _amount) - { - size_t result = _cursor; - - if(_amount > 0) - { - for(int i = 0; i < _amount; ++i) - result = nextCursor(_string, result); - } - else if(_amount < 0) - { - for(int i = _amount; i < 0; ++i) - result = prevCursor(_string, result); - } - - return result; - - } // moveCursor - - std::string toLower(const std::string& _string) - { - std::string string; - - for(size_t i = 0; i < _string.length(); ++i) - string += (char)tolower(_string[i]); - - return string; - - } // toLower - - std::string toUpper(const std::string& _string) - { - std::string string; - - for(size_t i = 0; i < _string.length(); ++i) - string += (char)toupper(_string[i]); - - return string; - - } // toUpper - - std::string trim(const std::string& _string) - { - const size_t strBegin = _string.find_first_not_of(" \t"); - const size_t strEnd = _string.find_last_not_of(" \t"); - - if(strBegin == std::string::npos) - return ""; - - return _string.substr(strBegin, strEnd - strBegin + 1); - - } // trim - - std::string replace(const std::string& _string, const std::string& _replace, const std::string& _with) - { - std::string string = _string; - size_t pos; - - while((pos = string.find(_replace)) != std::string::npos) - string = string.replace(pos, _replace.length(), _with.c_str(), _with.length()); - - return string; - - } // replace - - bool startsWith(const std::string& _string, const std::string& _start) - { - return (_string.find(_start) == 0); - - } // startsWith - - bool endsWith(const std::string& _string, const std::string& _end) - { - return (_string.find(_end) == (_string.size() - _end.size())); - - } // endsWith - - std::string removeParenthesis(const std::string& _string) - { - static const char remove[4] = { '(', ')', '[', ']' }; - std::string string = _string; - size_t start; - size_t end; - bool done = false; - - while(!done) - { - done = true; - - for(int i = 0; i < sizeof(remove); i += 2) - { - end = string.find_first_of(remove[i + 1]); - start = string.find_last_of( remove[i + 0], end); - - if((start != std::string::npos) && (end != std::string::npos)) - { - string.erase(start, end - start + 1); - done = false; - } - } - } - - return trim(string); - - } // removeParenthesis - - stringVector commaStringToVector(const std::string& _string) - { - stringVector vector; - size_t start = 0; - size_t comma = _string.find(","); - - while(comma != std::string::npos) - { - vector.push_back(_string.substr(start, comma - start)); - start = comma + 1; - comma = _string.find(",", start); - } - - vector.push_back(_string.substr(start)); - std::sort(vector.begin(), vector.end()); - - return vector; - - } // commaStringToVector - - std::string vectorToCommaString(stringVector _vector) - { - std::string string; - - std::sort(_vector.begin(), _vector.end()); - - for(stringVector::const_iterator it = _vector.cbegin(); it != _vector.cend(); ++it) - string += (string.length() ? "," : "") + (*it); - - return string; - - } // vectorToCommaString - - std::string format(const char* _format, ...) - { - va_list args; - va_list copy; - - va_start(args, _format); - - va_copy(copy, args); - const int length = vsnprintf(nullptr, 0, _format, copy); - va_end(copy); - - char* buffer = new char[length + 1]; - va_copy(copy, args); - vsnprintf(buffer, length + 1, _format, copy); - va_end(copy); - - va_end(args); - - std::string out(buffer); - delete buffer; - - return out; - - } // format - - // Simple XOR scrambling of a string, with an accompanying key - std::string scramble(const std::string& _input, const std::string& key) - { - std::string buffer = _input; +#include "utils/StringUtil.h" + +#include +#include + +#if defined(_WIN32) +#include +#endif + +namespace Utils +{ + namespace String + { +#if defined(_WIN32) + const std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + + std::string string; + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return string; + } + + const std::wstring convertToWideString(const std::string string) + { + int numBytes = MultiByteToWideChar(CP_UTF8, 0, string.c_str(), (int)string.length(), nullptr, 0); + + std::wstring wstring; + wstring.resize(numBytes); + MultiByteToWideChar(CP_UTF8, 0, string.c_str(), (int)string.length(), (WCHAR*)wstring.c_str(), numBytes); + + return wstring; + } +#endif + + unsigned int chars2Unicode(const std::string& _string, size_t& _cursor) + { + const char& c = _string[_cursor]; + unsigned int result = '?'; + + if((c & 0x80) == 0) // 0xxxxxxx, one byte character + { + // 0xxxxxxx + result = ((_string[_cursor++] ) ); + } + else if((c & 0xE0) == 0xC0) // 110xxxxx, two byte character + { + // 110xxxxx 10xxxxxx + result = ((_string[_cursor++] & 0x1F) << 6) | + ((_string[_cursor++] & 0x3F) ); + } + else if((c & 0xF0) == 0xE0) // 1110xxxx, three byte character + { + // 1110xxxx 10xxxxxx 10xxxxxx + result = ((_string[_cursor++] & 0x0F) << 12) | + ((_string[_cursor++] & 0x3F) << 6) | + ((_string[_cursor++] & 0x3F) ); + } + else if((c & 0xF8) == 0xF0) // 11110xxx, four byte character + { + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + result = ((_string[_cursor++] & 0x07) << 18) | + ((_string[_cursor++] & 0x3F) << 12) | + ((_string[_cursor++] & 0x3F) << 6) | + ((_string[_cursor++] & 0x3F) ); + } + else + { + // error, invalid unicode + ++_cursor; + } + + return result; + + } // chars2Unicode + + std::string unicode2Chars(const unsigned int _unicode) + { + std::string result; + + if(_unicode < 0x80) // one byte character + { + result += ((_unicode ) & 0xFF); + } + else if(_unicode < 0x800) // two byte character + { + result += ((_unicode >> 6) & 0xFF) | 0xC0; + result += ((_unicode ) & 0x3F) | 0x80; + } + else if(_unicode < 0xFFFF) // three byte character + { + result += ((_unicode >> 12) & 0xFF) | 0xE0; + result += ((_unicode >> 6) & 0x3F) | 0x80; + result += ((_unicode ) & 0x3F) | 0x80; + } + else if(_unicode <= 0x1fffff) // four byte character + { + result += ((_unicode >> 18) & 0xFF) | 0xF0; + result += ((_unicode >> 12) & 0x3F) | 0x80; + result += ((_unicode >> 6) & 0x3F) | 0x80; + result += ((_unicode ) & 0x3F) | 0x80; + } + else + { + // error, invalid unicode + result += '?'; + } + + return result; + + } // unicode2Chars + + size_t nextCursor(const std::string& _string, const size_t _cursor) + { + size_t result = _cursor; + + while(result < _string.length()) + { + ++result; + + if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx + break; + } + + return result; + + } // nextCursor + + size_t prevCursor(const std::string& _string, const size_t _cursor) + { + size_t result = _cursor; + + while(result > 0) + { + --result; + + if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx + break; + } + + return result; + + } // prevCursor + + size_t moveCursor(const std::string& _string, const size_t _cursor, const int _amount) + { + size_t result = _cursor; + + if(_amount > 0) + { + for(int i = 0; i < _amount; ++i) + result = nextCursor(_string, result); + } + else if(_amount < 0) + { + for(int i = _amount; i < 0; ++i) + result = prevCursor(_string, result); + } + + return result; + + } // moveCursor + + std::string toLower(const std::string& _string) + { + std::string string; + + for(size_t i = 0; i < _string.length(); ++i) + string += (char)tolower(_string[i]); + + return string; + + } // toLower + + std::string toUpper(const std::string& _string) + { + +#if defined(_WIN32) + std::wstring stringW = convertToWideString(_string); + + auto& f = std::use_facet>(std::locale()); + f.toupper(&stringW[0], &stringW[0] + stringW.size()); + + return convertFromWideString(stringW); +#else + std::string string; + + for(size_t i = 0; i < _string.length(); ++i) + string += (char)toupper(_string[i]); + + return string; +#endif + } // toUpper + + std::string trim(const std::string& _string) + { + const size_t strBegin = _string.find_first_not_of(" \t"); + const size_t strEnd = _string.find_last_not_of(" \t"); + + if(strBegin == std::string::npos) + return ""; + + return _string.substr(strBegin, strEnd - strBegin + 1); + + } // trim + + std::string replace(const std::string& _string, const std::string& _replace, const std::string& _with) + { + std::string string = _string; + size_t pos; + + while((pos = string.find(_replace)) != std::string::npos) + string = string.replace(pos, _replace.length(), _with.c_str(), _with.length()); + + return string; + + } // replace + + bool startsWith(const std::string& _string, const std::string& _start) + { + return (_string.find(_start) == 0); + + } // startsWith + + bool endsWith(const std::string& _string, const std::string& _end) + { + return (_string.find(_end) == (_string.size() - _end.size())); + + } // endsWith + + std::string removeParenthesis(const std::string& _string) + { + static const char remove[4] = { '(', ')', '[', ']' }; + std::string string = _string; + size_t start; + size_t end; + bool done = false; + + while(!done) + { + done = true; + + for(int i = 0; i < sizeof(remove); i += 2) + { + end = string.find_first_of(remove[i + 1]); + start = string.find_last_of( remove[i + 0], end); + + if((start != std::string::npos) && (end != std::string::npos)) + { + string.erase(start, end - start + 1); + done = false; + } + } + } + + return trim(string); + + } // removeParenthesis + + stringVector commaStringToVector(const std::string& _string) + { + stringVector vector; + size_t start = 0; + size_t comma = _string.find(","); + + while(comma != std::string::npos) + { + vector.push_back(_string.substr(start, comma - start)); + start = comma + 1; + comma = _string.find(",", start); + } + + vector.push_back(_string.substr(start)); + std::sort(vector.begin(), vector.end()); + + return vector; + + } // commaStringToVector + + std::string vectorToCommaString(stringVector _vector) + { + std::string string; + + std::sort(_vector.begin(), _vector.end()); + + for(stringVector::const_iterator it = _vector.cbegin(); it != _vector.cend(); ++it) + string += (string.length() ? "," : "") + (*it); + + return string; + + } // vectorToCommaString + + std::string format(const char* _format, ...) + { + va_list args; + va_list copy; + + va_start(args, _format); + + va_copy(copy, args); + const int length = vsnprintf(nullptr, 0, _format, copy); + va_end(copy); + + char* buffer = new char[length + 1]; + va_copy(copy, args); + vsnprintf(buffer, length + 1, _format, copy); + va_end(copy); + + va_end(args); + + std::string out(buffer); + delete buffer; + + return out; + + } // format + + // Simple XOR scrambling of a string, with an accompanying key + std::string scramble(const std::string& _input, const std::string& key) + { + std::string buffer = _input; for (size_t i = 0; i < _input.size(); ++i) { - buffer[i] = _input[i] ^ key[i]; - } - - return buffer; - - } // scramble - - } // String:: - -} // Utils:: + buffer[i] = _input[i] ^ key[i]; + } + + return buffer; + + } // scramble + + } // String:: + +} // Utils:: diff --git a/es-core/src/utils/StringUtil.h b/es-core/src/utils/StringUtil.h index 5fffa1d761..7ebcf0df22 100644 --- a/es-core/src/utils/StringUtil.h +++ b/es-core/src/utils/StringUtil.h @@ -28,6 +28,10 @@ namespace Utils std::string format (const char* _string, ...); std::string scramble (const std::string& _input, const std::string& key); +#if defined(_WIN32) + const std::string convertFromWideString(const std::wstring wstring); + const std::wstring convertToWideString(const std::string string); +#endif } // String:: } // Utils:: From 3bd1bdd4d1a1ef220d214349ead7c833dd45ab72 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 27 May 2019 13:57:26 +0200 Subject: [PATCH 010/336] [Add French localization resources] --- resources/fontawesome_webfont.ttf | Bin 0 -> 120456 bytes resources/locale/fr/emulationstation2.po | 2047 ++++++++++++++++++++++ resources/locale/it/emulationstation2.po | 1884 ++++++++++++++++++++ 3 files changed, 3931 insertions(+) create mode 100644 resources/fontawesome_webfont.ttf create mode 100644 resources/locale/fr/emulationstation2.po create mode 100644 resources/locale/it/emulationstation2.po diff --git a/resources/fontawesome_webfont.ttf b/resources/fontawesome_webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..59dec23e2af3290758890eee55e9f619e6c48202 GIT binary patch literal 120456 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 literal 0 HcmV?d00001 diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po new file mode 100644 index 0000000000..3f34a69461 --- /dev/null +++ b/resources/locale/fr/emulationstation2.po @@ -0,0 +1,2047 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: recalbox-emulationstation\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: +msgid "GAME COLLECTION SETTINGS" +msgstr "LISTES PERSONNALISÉES" + +#: +msgid "CANCEL" +msgstr "ANNULER" + +#: +msgid "automatic" +msgstr "automatique" + +#: +msgid "basic" +msgstr "basique" + +#: +msgid "detailed" +msgstr "détaillée" + +#: +msgid "grid" +msgstr "grille" + +#: +msgid "fade" +msgstr "fondu" + +#: +msgid "instant" +msgstr "instantanée" + +#: +msgid "slide" +msgstr "animée" + +#: +msgid "gridex" +msgstr "grille détaillée" + +#: +msgid "ENABLE NAVIGATION SOUNDS" +msgstr "SONS LORS DE LA NAVIGATION" + +#: +msgid "ENABLE VIDEO AUDIO" +msgstr "SON DES VIDEOS" + +#: +msgid "APPLY FILTER" +msgstr "FILTER LES JEUX" + +#: +msgid "AUTOMATIC GAME COLLECTIONS" +msgstr "COLLECTIONS AUTOMATIQUES" + +#: +msgid "SELECT COLLECTIONS" +msgstr "SÉLECTIONNER LES COLLECTIONS" + +#: +msgid "CUSTOM GAME COLLECTIONS" +msgstr "COLLECTIONS PERSONNALISEES" + +#: +msgid "CREATE NEW CUSTOM COLLECTION FROM THEME" +msgstr "CRÉER UNE COLLECTION A PARTIR D'UN THEME" + +#: +msgid "GAME COLLECTION SETTINGS" +msgstr "PARAMÈTRES DES LISTES DE JEUX" + +#: +msgid "SELECT THEME FOLDER" +msgstr "SÉLECTIONNEZ LE DOSSIER" + +#: +msgid "CREATE NEW CUSTOM COLLECTION" +msgstr "CREER UNE COLLECTION PERSONNALISÉE" + +#: +msgid "New Collection Name" +msgstr "Nom de la collection" + +#: +msgid "GROUP UNTHEMED CUSTOM COLLECTIONS" +msgstr "GROUPER LES COLLECTIONS SANS THEME" + +#: +msgid "SORT CUSTOM COLLECTIONS AND SYSTEMS" +msgstr "TRIER LES COLLECTIONS ET LES SYSTÈMES" + +#: +msgid "FINISH EDITING COLLECTION" +msgstr "TERMINER L'ÉDITION DE LA COLLECTION" + +#: +msgid "PARSE GAMESLISTS ONLY" +msgstr "LISTES DE JEUX XML UNIQUEMENT" + +#: +msgid "POWER SAVER MODES" +msgstr "MODE D'ECONOMIE D'ENERGIE" + +#: +msgid "SAVE METADATA ON EXIT" +msgstr "SAUVER LES DONNÉES EN QUITTANT" + +#: +msgid "SHOW HIDDEN FILES" +msgstr "AFFICHER LES FICHIERS CACHÉS" + +#: +msgid "VRAM LIMIT" +msgstr "MÉMOIRE VIDÉO MAX" + +#: +msgid "RESET ALL FILTERS" +msgstr "RÉINITIALISER TOUS LES FILTRES" + +#: +msgid "GAMELIST VIEW STYLE" +msgstr "STYLE D'AFFICHAGE" + +#: +msgid "GAME LAUNCH TRANSITION" +msgstr "TRANSITION AU LANCEMENT D'UN JEU" + +#: +msgid "RANDOM" +msgstr "ALÉATOIRE" + +#: +msgid "Rating" +msgstr "Note" + +#: +msgid "Released" +msgstr "Sortie" + +#: +msgid "Developer" +msgstr "Développeur" + +#: +msgid "Publisher" +msgstr "Éditeur" + +#: +msgid "Genre" +msgstr "Genre" + +#: +msgid "Players" +msgstr "Joueurs" + +#: +msgid "NO GAMES FOUND - SKIP" +msgstr "AUCUN JEU CORRESPONDANT - PASSER" + +#: +msgid "RETRY" +msgstr "RECOMMENCER" + +#: +msgid "SKIP" +msgstr "PASSER" + +#: +msgid "SEARCH FOR" +msgstr "RECHERCHER" + +#: +msgid "SEARCH" +msgstr "RECHERCHER" + +#: +msgid "SCRAPING IN PROGRESS" +msgstr "SCRAPING EN COURS" + +#: +msgid "SYSTEM" +msgstr "SYSTÈME" + +#: +msgid "subtitle text" +msgstr "sous-titre" + +#: +msgid "INPUT" +msgstr "ÉDITER" + +#: +msgid "search" +msgstr "rechercher" + +#: +msgid "STOP" +msgstr "ARRÊTER" + +#: +msgid "stop (progress saved)" +msgstr "stop (progression sauvegardée)" + +#: +msgid "GAME %i OF %i" +msgstr "JEU %i SUR %i" + +#: +msgid "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 RECALBOX.COM FOR MORE INFORMATION." +msgstr "AUCUN JEU N'A ÉTÉ TROUVÉ SUR LE SYSTÈME !\n" +"VEUILLEZ AJOUTER DES JEUX EN SUIVANT LES INSTRUCTIONS DE LA NOTICE.\n" +"\n" +"RENDEZ-VOUS SUR RECALBOX.COM POUR PLUS D'INFORMATIONS" + +#: +msgid "%i GAME SUCCESSFULLY SCRAPED!" +msgid_plural "%i GAMES SUCCESSFULLY SCRAPED!" +msgstr[0] "%i JEU SCRAPPÉ !" +msgstr[1] "%i JEUX SCRAPPÉS !" + +#: +msgid "%i GAME SKIPPED." +msgid_plural "%i GAMES SKIPPED." +msgstr[0] "%i JEU PASSÉ." +msgstr[1] "%i JEUX PASSÉS." + +#: +msgid "OK" +msgstr "OK" + +#: +msgid "EDIT METADATA" +msgstr "ÉDITER LES DONNÉES" + +#: +msgid "SCRAPE" +msgstr "SCRAPPER" + +#: +msgid "SAVE" +msgstr "SAUVEGARDER" + +#: +msgid "THIS WILL DELETE A FILE!\n" +"ARE YOU SURE?" +msgstr "CELA VA EFFACER LE FICHIER\n" +"ÊTES-VOUS SUR ?" + +#: +msgid "YES" +msgstr "OUI" + +#: +msgid "NO" +msgstr "NON" + +#: +msgid "DELETE" +msgstr "SUPPRIMER" + +#: +msgid "SAVE CHANGES?" +msgstr "SAUVEGARDER ?" + +#: +msgid "BACK" +msgstr "RETOUR" + +#: +msgid "CLOSE" +msgstr "FERMER" + +#: +msgid "MAIN MENU" +msgstr "MENU PRINCIPAL" + +#: +msgid "KODI MEDIA CENTER" +msgstr "KODI MEDIA CENTER" + +#: +msgid "SYSTEM SETTINGS" +msgstr "OPTIONS SYSTÈME" + +#: +msgid "VERSION" +msgstr "VERSION" + +#: +msgid "DISK USAGE" +msgstr "ESPACE DISQUE" + +#: +msgid "STORAGE DEVICE" +msgstr "MÉDIA DE STOCKAGE" + +#: +msgid "LANGUAGE" +msgstr "LANGUE" + +#: +msgid "OVERCLOCK" +msgstr "OVERCLOCK" + +#: +msgid "EXTREM (1100Mhz)" +msgstr "EXTREM (1100Mhz)" + +#: +msgid "TURBO (1000Mhz)" +msgstr "TURBO (1000Mhz)" + +#: +msgid "HIGH (950Mhz)" +msgstr "HIGH (950Mhz)" + +#: +msgid "NONE (700Mhz)" +msgstr "AUCUN (700Mhz)" + +#: +msgid "TURBO (1050Mhz)+" +msgstr "TURBO (1050Mhz)+" + +#: +msgid "HIGH (1050Mhz)" +msgstr "HIGH (1050Mhz)" + +#: +msgid "NONE (900Mhz)" +msgstr "AUCUN (900Mhz)" + +#: +msgid "NONE (1200Mhz)" +msgstr "AUCUN (1200Mhz)" + +#: +msgid "NONE" +msgstr "AUCUN" + +#. NEW SETTINGS ORGANIZATION +#: +msgid "UPDATES" +msgstr "MISES À JOUR" + +#: +msgid "AUTO UPDATES" +msgstr "MISES À JOUR AUTO" + +#: +msgid "START UPDATE" +msgstr "LANCER LA MISE À JOUR" + +#: +msgid "KODI SETTINGS" +msgstr "OPTIONS KODI" + +#: +msgid "ENABLE KODI" +msgstr "ACTIVER KODI" + +#: +msgid "KODI AT START" +msgstr "AUTO DÉMARRAGE DE KODI" + +#: +msgid "START KODI WITH X" +msgstr "X POUR LANCER KODI" + +#: +msgid "SECURITY" +msgstr "SÉCURITÉ" + +#: +msgid "ENFORCE SECURITY" +msgstr "RENFORCER LA SÉCURITÉ" + +#: +msgid "ROOT PASSWORD" +msgstr "MOT DE PASSE ROOT" + +#: +msgid "THE SYSTEM WILL NOW REBOOT" +msgstr "LE SYSTÈME VA REDÉMARRER" + +#: +msgid "GAMES SETTINGS" +msgstr "OPTIONS DES JEUX" + +#: +msgid "GAME RATIO" +msgstr "FORMAT JEUX" + +#: +msgid "SMOOTH GAMES" +msgstr "LISSER LES JEUX" + +#: +msgid "REWIND" +msgstr "REMBOBINAGE" + +#: +msgid "AUTO SAVE/LOAD" +msgstr "SAUVEGARDE/CHARGEMENT AUTOMATIQUE" + +#: +msgid "SHADERS SET" +msgstr "SHADERS SET" + +#: +msgid "SCANLINES" +msgstr "SCANLINES" + +#: +msgid "RETRO" +msgstr "RETRO" + +#: +msgid "RETROACHIEVEMENTS SETTINGS" +msgstr "OPTIONS DE RETROACHIEVEMENTS" + +#: +msgid "RETROACHIEVEMENTS" +msgstr "RETROACHIEVEMENTS" + +#: +msgid "HARDCORE MODE" +msgstr "MODE HARDCORE" + +#: +msgid "USERNAME" +msgstr "NOM UTILISATEUR" + +#: +msgid "PASSWORD" +msgstr "MOT DE PASSE" + +#: +msgid "ADVANCED" +msgstr "AVANCÉES" + +#: +msgid "REALLY UPDATE GAMES LISTS ?" +msgstr "CONFIRMER LA MISE À JOUR DES LISTES DES JEUX ?" + +#: +msgid "UPDATE GAMES LISTS" +msgstr "METTRE À JOUR LES LISTES DES JEUX" + +#: +msgid "CONTROLLERS SETTINGS" +msgstr "OPTIONS MANETTES" + +#: +msgid "UI SETTINGS" +msgstr "OPTIONS DE L'INTERFACE" + +#: +msgid "OVERSCAN" +msgstr "OVERSCAN" + +#: +msgid "SCREENSAVER SETTINGS" +msgstr "ÉCRAN DE VEILLE" + +#: +msgid "SCREENSAVER AFTER" +msgstr "ÉCRAN DE VEILLE APRÈS" + +#: +msgid "TRANSITION STYLE" +msgstr "STYLE DE LA TRANSITION" + +#: +msgid "SCREENSAVER BEHAVIOR" +msgstr "COMPORTEMENT DE L'ÉCRAN DE VEILLE" + +#: +msgid "SHOW FRAMERATE" +msgstr "AFFICHER LE FRAMERATE" + +#: +msgid "ON-SCREEN HELP" +msgstr "AFFICHAGE DE L'AIDE" + +#: +msgid "HIDE WHEN RUNNING GAME" +msgstr "MASQUER AU LANCEMENT D'UN JEU" + +#: +msgid "QUICK SYSTEM SELECT" +msgstr "SÉLECTION RAPIDE DU SYSTÈME" + +#: +msgid "THEME SET" +msgstr "THÈME" + +#: +msgid "SOUND SETTINGS" +msgstr "OPTIONS DU SON" + +#: +msgid "SYSTEM VOLUME" +msgstr "VOLUME DU SYSTÈME" + +#: +msgid "FRONTEND MUSIC" +msgstr "MUSIQUE DE FOND" + +#: +msgid "OUTPUT DEVICE" +msgstr "SORTIE AUDIO" + +#: +msgid "HDMI" +msgstr "HDMI" + +#: +msgid "JACK" +msgstr "JACK" + +#: +msgid "AUTO" +msgstr "AUTO" + +#: +msgid "NETWORK SETTINGS" +msgstr "OPTIONS RÉSEAU" + +#: +msgid "CONNECTED" +msgstr "CONNECTÉ" + +#: +msgid "NOT CONNECTED" +msgstr "DÉCONNECTÉ" + +#: +msgid "STATUS" +msgstr "STATUT" + +#: +msgid "IP ADDRESS" +msgstr "ADRESSE IP" + +#: +msgid "HOSTNAME" +msgstr "NOM SUR LE RÉSEAU" + +#: +msgid "ENABLE WIFI" +msgstr "ACTIVER LE WIFI" + +#: +msgid "WIFI SSID" +msgstr "SSID WIFI" + +#: +msgid "WIFI KEY" +msgstr "CLÉ WIFI" + +#: +msgid "WIFI ENABLED" +msgstr "WIFI ACTIVÉ" + +#: +msgid "WIFI CONFIGURATION ERROR" +msgstr "ERREUR DE CONFIGURATION WIFI" + +#: +msgid "SCRAPER" +msgstr "SCRAPPEUR" + +#: +msgid "SCRAPE FROM" +msgstr "SCRAPPER DEPUIS" + +#: +msgid "SCRAPE RATINGS" +msgstr "SCRAPPER LES NOTES" + +#: +msgid "SCRAPE NOW" +msgstr "SCRAPPER MAINTENANT" + +#: +msgid "QUIT" +msgstr "QUITTER" + +#: +msgid "REALLY RESTART?" +msgstr "REDÉMARRER ?" + +#: +msgid "RESTART SYSTEM" +msgstr "REDÉMARRER" + +#: +msgid "REALLY SHUTDOWN?" +msgstr "ÉTEINDRE ?" + +#: +msgid "SHUTDOWN SYSTEM" +msgstr "ÉTEINDRE" + +#: +msgid "Emulator" +msgstr "Émulateur" + +#: +msgid "Core" +msgstr "Core" + +#: +msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." +msgstr "VOUS ALLEZ CONFIGURER UNE MANETTE. SI VOUS N'AVEZ QU'UN JOYSTICK OU JUSTE UNE CROIX DIRECTIONNELLE, CONFIGUREZ SEULEMENT LES DIRECTIONS ET PASSEZ LES JOYSTICKS EN RESTANT APPUYÉ SUR UN BOUTON. SI VOUS N'AVEZ PAS DE BOUTON SPECIAL POUR HOTKEY, UTILISEZ SELECT. IGNOREZ UN BOUTON QUE VOTRE MANETTE N'A PAS EN RESTANT APPUYÉ SUR UN BOUTON DÉJÀ CONFIGURÉ. LES NOMS DES BOUTONS SONT BASÉS SUR LA SUPER NINTENDO" + +#. GUIMENU +#: +msgid "CONFIGURE A CONTROLLER" +msgstr "CONFIGURER UNE MANETTE" + +#. Bluetooth +#: +msgid "CONTROLLER PAIRED" +msgstr "MANETTE ASSOCIÉE" + +#: +msgid "UNABLE TO PAIR CONTROLLER" +msgstr "IMPOSSIBLE D'ASSOCIER LA MANETTE" + +#: +msgid "AN ERROR OCCURED" +msgstr "UNE ERREUR EST SURVENUE" + +#: +msgid "NO CONTROLLERS FOUND" +msgstr "AUCUNE MANETTE DÉTECTÉE" + +#: +msgid "PAIR A BLUETOOTH CONTROLLER" +msgstr "ASSOCIER UNE MANETTE BLUETOOTH" + +#: +msgid "CONTROLLERS LINKS HAVE BEEN DELETED." +msgstr "LES LIENS VERS LES MANETTES ONT ÉTÉ SUPPRIMÉS." + +#: +msgid "FORGET BLUETOOTH CONTROLLERS" +msgstr "OUBLIER LES MANETTES BLUETOOTH" + +#: +msgid "INPUT P%i" +msgstr "JOUEUR %i" + +#: +msgid "CHOOSE" +msgstr "CHOISIR" + +#: +msgid "SELECT" +msgstr "SÉLECTIONNER" + +#: +msgid "OPTIONS" +msgstr "OPTIONS" + +#: +msgid "JUMP TO LETTER" +msgstr "ALLER À LA LETTRE" + +#: +msgid "SORT GAMES BY" +msgstr "TRIER LES JEUX PAR" + +#. FAVORITES +#: +msgid "FAVORITES ONLY" +msgstr "FAVORIS SEULEMENT" + +#: +msgid "EDIT THIS GAME'S METADATA" +msgstr "ÉDITER LES DONNÉES DU JEU" + +#: +msgid "SCRAPE THESE GAMES" +msgstr "SCRAPPER CES JEUX" + +#: +msgid "All Games" +msgstr "Tous les jeux" + +#. MISSING SCRAPPER TRANSLATIONS +#: +msgid "Only missing image" +msgstr "Seulement images manquantes" + +#: +msgid "FILTER" +msgstr "FILTRE" + +#: +msgid "SCRAPE THESE SYSTEMS" +msgstr "SCRAPPER CES SYSTÈMES" + +#: +msgid "SYSTEMS" +msgstr "SYSTÈMES" + +#: +msgid "USER DECIDES ON CONFLICTS" +msgstr "CHOISIR SI CONFLIT" + +#: +msgid "START" +msgstr "DÉMARRER" + +#: +msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" +"CONTINUE ANYWAY?" +msgstr "ATTENTION: CERTAINS DE VOS SYSTÈMES N'ONT PAS DE PLATEFORME. LES RÉSULTATS POURRAIENT ÊTRE INATTENDUS!\n" +"CONTINUER QUAND MÊME?" + +#: +msgid "NO GAMES FIT THAT CRITERIA." +msgstr "AUCUN JEU NE CORRESPOND." + +#: +msgid "REALLY UPDATE?" +msgstr "METTRE À JOUR LE SYSTÈME ?" + +#: +msgid "NETWORK CONNECTION NEEDED" +msgstr "VEUILLEZ BRANCHER UN CÂBLE RÉSEAU" + +#: +msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" +msgstr "MISE À JOUR TÉLÉCHARGÉE, REDÉMARRAGE EN COURS" + +#: +msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" +msgstr "ÉCHEC DE LA MISE À JOUR" + +#: +msgid "NO UPDATE AVAILABLE" +msgstr "AUCUNE MISE À JOUR DISPONIBLE" + +#: +msgid "enter emulator" +msgstr "entrer l'émulateur" + +#: +msgid "enter core" +msgstr "entrer le core" + +#: +msgid "Ratio" +msgstr "Ratio" + +#: +msgid "enter ratio" +msgstr "entrer le ratio" + +#: +msgid "Name" +msgstr "Nom" + +#: +msgid "enter game name" +msgstr "entrer le nom du jeu" + +#: +msgid "Description" +msgstr "Description" + +#: +msgid "enter description" +msgstr "entrer la description" + +#: +msgid "Image" +msgstr "Image" + +#: +msgid "enter path to image" +msgstr "entrer le chemin vers l'image" + +#: +msgid "Thumbnail" +msgstr "Miniature" + +#: +msgid "enter path to thumbnail" +msgstr "entrer le chemin vers la miniature" + +#: +msgid "enter rating" +msgstr "entrer les votes" + +#: +msgid "Release date" +msgstr "Date de sortie" + +#: +msgid "enter release date" +msgstr "entrer la date de sortie" + +#: +msgid "enter game developer" +msgstr "entrer le développeur du jeu" + +#: +msgid "enter game publisher" +msgstr "entrer l'éditeur du jeu" + +#: +msgid "enter game genre" +msgstr "entrer le genre du jeu" + +#: +msgid "enter number of players" +msgstr "entrer le nombre de joueurs" + +#: +msgid "Favorite" +msgstr "Favori" + +#: +msgid "enter favorite" +msgstr "entrer le favori" + +#: +msgid "Region" +msgstr "Région" + +#: +msgid "enter region" +msgstr "entrer la région" + +#: +msgid "Romtype" +msgstr "Type de rom" + +#: +msgid "enter romtype" +msgstr "entre le type de rom" + +#: +msgid "Hidden" +msgstr "Invisible" + +#: +msgid "HIDDEN" +msgstr "CACHÉ" + +#: +msgid "NAME" +msgstr "NOM" + +#: +msgid "EMULATOR" +msgstr "EMULATEUR" + +#: +msgid "FAVORITE" +msgstr "FAVORI" + +#: +msgid "set hidden" +msgstr "Mettre caché" + +#: +msgid "Play count" +msgstr "Nb parties" + +#: +msgid "enter number of times played" +msgstr "entrer le nombre de parties" + +#: +msgid "Last played" +msgstr "Dernier jeu" + +#: +msgid "enter last played date" +msgstr "entrer la date de la dernière partie" + +#: +msgid "GAMES AVAILABLE" +msgstr "JEUX DISPONIBLES" + +#: +msgid "%i FAVORITE" +msgid_plural "%i FAVORITES" +msgstr[0] "%i FAVORI" +msgstr[1] "%i FAVORIS" + +#: +msgid "SCROLL" +msgstr "SCROLLER" + +#: +msgid "LAUNCH" +msgstr "LANCER" + +#: +msgid "Times played" +msgstr "Nb parties" + +#: +msgid "MENU" +msgstr "MENU" + +#: +msgid "FILENAME, ASCENDING" +msgstr "FICHIER, ASCENDANT" + +#: +msgid "FILENAME, DESCENDING" +msgstr "FICHIER, DESCENDANT" + +#: +msgid "RATING, ASCENDING" +msgstr "VOTES, ASCENDANT" + +#: +msgid "RATING, DESCENDING" +msgstr "VOTES, DESCENDANT" + +#: +msgid "TIMES PLAYED, ASCENDING" +msgstr "PARTIES JOUÉES, ASCENDANT" + +#: +msgid "TIMES PLAYED, DESCENDING" +msgstr "PARTIES JOUÉES, DESCENDANT" + +#: +msgid "LAST PLAYED, ASCENDING" +msgstr "DERNIÈRE PARTIE, ASCENDANT" + +#: +msgid "LAST PLAYED, DESCENDING" +msgstr "DERNIÈRE PARTIE, DESCENDANT" + +#: +msgid "WORKING..." +msgstr "EN COURS..." + +#: +msgid "CHANGE" +msgstr "CHANGER" + +#: +msgid "never" +msgstr "jamais" + +#: +msgid "just now" +msgstr "à l'instant" + +#: +msgid "%i sec ago" +msgid_plural "%i secs ago" +msgstr[0] "il y a %i sec" +msgstr[1] "il y a %i secs" + +#: +msgid "%i min ago" +msgid_plural "%i mins ago" +msgstr[0] "il y a %i min" +msgstr[1] "il y a %i mins" + +#: +msgid "%i hour ago" +msgid_plural "%i hours ago" +msgstr[0] "il y a %i heure" +msgstr[1] "il y a %i heures" + +#: +msgid "%i day ago" +msgid_plural "%i days ago" +msgstr[0] "il y a %i jour" +msgstr[1] "il y a %i jours" + +#: +msgid "unknown" +msgstr "inconnu" + +#: +msgid "SELECT ALL" +msgstr "TOUT SÉLECTIONNER" + +#: +msgid "SELECT NONE" +msgstr "AUCUN" + +#: +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i SÉLECTIONNÉ" +msgstr[1] "%i SÉLECTIONNÉS" + +#: +msgid "UP" +msgstr "HAUT" + +#: +msgid "DOWN" +msgstr "BAS" + +#: +msgid "LEFT" +msgstr "GAUCHE" + +#: +msgid "RIGHT" +msgstr "DROITE" + +#: +msgid "JOYSTICK 1 UP" +msgstr "JOYSTICK 1 HAUT" + +#: +msgid "JOYSTICK 1 LEFT" +msgstr "JOYSTICK 1 GAUCHE" + +#: +msgid "JOYSTICK 2 UP" +msgstr "JOYSTICK 2 HAUT" + +#: +msgid "JOYSTICK 2 LEFT" +msgstr "JOYSTICK 2 GAUCHE" + +#: +msgid "PAGE UP" +msgstr "L1 (PAGE PRECEDENTE)" + +#: +msgid "PAGE DOWN" +msgstr "R1 (PAGE SUIVANTE)" + +#: +msgid "HOTKEY" +msgstr "HOTKEY" + +#: +msgid "CONFIGURING" +msgstr "CONFIGURATION" + +#: +msgid "KEYBOARD" +msgstr "CLAVIER" + +#: +msgid "GAMEPAD %i" +msgstr "JOYSTICK %i" + +#. Config controllers missing translation +#: +msgid "PRESS ANYTHING" +msgstr "APPUYEZ SUR UN BOUTON" + +#: +msgid "ALREADY TAKEN" +msgstr "DÉJÀ PRIS" + +#: +msgid "DISCARD CHANGES" +msgstr "ANNULER LES MODIFICATIONS" + +#: +msgid "WELCOME" +msgstr "BIENVENUE" + +#: +msgid "CONFIGURE INPUT" +msgstr "CONFIGURER LES MANETTES" + +#: +msgid "%i GAMEPAD DETECTED" +msgid_plural "%i GAMEPADS DETECTED" +msgstr[0] "%i MANETTE DÉTECTÉE" +msgstr[1] "%i MANETTES DÉTECTÉES" + +#: +msgid "NO GAMEPADS DETECTED" +msgstr "AUCUNE MANETTE DÉTECTÉE" + +#: +msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." +msgstr "MAINTENEZ UN BOUTON SUR LA MANETTE POUR LA CONFIGURER" + +#: +msgid "PRESS F4 TO QUIT AT ANY TIME." +msgstr "APPUYEZ SUR F4 POUR QUITTER À TOUT MOMENT" + +#: +msgid "PRESS ESC OR THE HOTKEY TO CANCEL." +msgstr "APPUYEZ SUR ECHAP OU LA TOUCHE HOTKEY POUR ANNULER" + +#: +msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" +msgstr "VOULEZ-VOUS LANCER LE MEDIA CENTER KODI ?" + +#: +msgid "LOADING..." +msgstr "CHARGEMENT..." + +#: +msgid "PLEASE WAIT..." +msgstr "VEUILLEZ PATIENTER..." + +#: +msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" +msgstr "VOULEZ-VOUS VRAIMENT ÉTEINDRE SANS SAUVEGARDER LES METADATAS ?" + +#: +msgid "FAST SHUTDOWN SYSTEM" +msgstr "EXTINCTION RAPIDE" + +#: +msgid "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 RECALBOX.FR FOR MORE INFORMATION." +msgstr "Aucun système trouvé !\n" +"\n" +"Visitez le site recalbox.com pour plus d'infos !" + +#: +msgid "ON SCREEN KEYBOARD" +msgstr "CLAVIER VIRTUEL" + +#: +msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" +msgstr "APPUYEZ SUR SHIFT POUR OBTENIR LES MAJUSCULES" + +#: +msgid "SPACE" +msgstr "ESPACE" + +#: +msgid "DELETE A CHAR" +msgstr "EFFACER UN CARACTERE" + +#: +msgid "SHIFT" +msgstr "MAJUSCULE" + +#: +msgid "STOP EDITING" +msgstr "ARRETER L'EDITION" + +#: +msgid "MOVE CURSOR" +msgstr "BOUGER LE CURSEUR" + +#: +msgid "EDIT" +msgstr "EDITER" + +#: +msgid "ACCEPT RESULT" +msgstr "ACCEPTER LE RESULTAT" + +#: +msgid "FILENAME" +msgstr "FICHIER" + +#: +msgid "RATING" +msgstr "VOTES" + +#: +msgid "TIMES PLAYED" +msgstr "PARTIES JOUÉES" + +#: +msgid "LAST PLAYED" +msgstr "DERNIÈRE PARTIE" + +#: +msgid "NUMBER OF PLAYERS" +msgstr "NOMBRE DE JOUEURS" + +#: +msgid "DEVELOPER" +msgstr "DÉVELOPPEUR" + +#: +msgid "GENRE" +msgstr "GENRE" + +#: +msgid "SHOW HIDDEN" +msgstr "AFFICHER LES FICHIERS CACHÉS" + +#: +msgid "EXTREM (1400Mhz)" +msgstr "EXTRÊME (1400MHz)" + +#: +msgid "TURBO (1350Mhz)" +msgstr "TURBO (1350MHz)" + +#: +msgid "HIGH (1300Mhz)" +msgstr "HIGH (1300MHz)" + +#: +msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" +"IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." +msgstr "LES PRÉRÉGLAGES TURBO ET EXTRÊME PEUVENT PROVOQUER DES INSTABILITÉS DU SYSTÈME, UTILISEZ-LES À VOS PROPRES RISQUES.\n" +"SI VOUS CONTINUEZ, LE SYSTÈME REDÉMARRERA MAINTENANT." + +#: +msgid "%i GAME HIDDEN" +msgid_plural "%i GAMES HIDDEN" +msgstr[0] "%i JEU CACHÉ" +msgstr[1] "%i JEUX CACHÉS" + +#: +msgid "Start kodi media player." +msgstr "Démarrer le media player Kodi" + +#: +msgid "Select the language for your recalbox, select an external drive to store your games and configurations, check your current version and the free space on your drive" +msgstr "Choisissez la langue de votre Recalbox, choisissez un lecteur externe pour stocker vos jeux et vos configurations, vérifiez la version actuelle et l'espace libre sur votre disque" + +#: +msgid "Shows your current recalboxOS version." +msgstr "Montre la version actuelle de RecalboxOS." + +#: +msgid "Show how much space is used on your SHARE partition, located either on the SDCARD or on an external drive. The information shows how much GB are used and how much GB your storage has overall (example 13GB/26GB)." +msgstr "Montre l'espace utilisé sur la partition SHARE située sur la carte SD ou le lecteur externe. L'information montre combien de Go sont utilisés sur la capacité totale du support (exemple 13Go/26Go)." + +#: +msgid "Select an external drive to store your roms, saves, configurations etc.\n" +"Use a FAT32 formatted drive. The system does not format the drive. On first boot, with this option enabled, recalbox will create a '/recalbox' folder with all system files inside." +msgstr "Choisissez un lecteur externe pour stocker vos roms, sauvegardes, configurations etc.\n" +"Utilisez un lecteur formaté en FAT32. Le système ne formate pas le lecteur. Au premier démarrage, avec cette option activée, Recalbox créera un dossier '/recalbox' avec tous les fichiers système dedans." + +#: +msgid "Select your language. A reboot is needed to set this configuration active." +msgstr "Choisissez votre langue. Un redémarrage est nécessaire pour activer cette option." + +#: +msgid "Manage your recalbox updates. Select the update type. Activate update check." +msgstr "Gérez les mises à jour de Recalbox. Choisissez le type de mise à jour. Activez la vérification de mise à jour." + +#: +msgid "Check if an update is available, and start the update process." +msgstr "Vérifiez si une mise à jour est disponible et lancer le processus de mise à jour." + +#: +msgid "Stable updates will check for updates on stable recalbox releases. Stable updates are tested and approved by the recalbox team and their testers.\n" +"Unstable updates allows you to get the latest recalbox features by checking our unstable repository. You can test and validate with us the very last version of recalbox.\n" +"If you choose unstable update, be so kind to report issues on the recalbox-os issue board (https://github.com/recalbox/recalbox-os/issues)" +msgstr "Stable vérifiera les mises à jour parmi les sorties stables de Recalbox.\n" +"Les mises à jour stables sont testées et approuvées par l'équipe Recalbox et ses testeurs.\n" +"Les mise à jour unstable vous permettent de profiter des dernières fonctionnalités de Recalbox en vérifiant le dépôt unstable. Vous pouvez tester et valider avec nous la toute dernière version de Recalbox.\n" +"Si vous choisissez les mise à jour unstable, merci de signaler les problèmes sur le forum dédié (https://github.com/recalbox/recalbox-os/issues)." + +#: +msgid "Automatically check if an update is avaialble. If so, it notifies you with a message." +msgstr "Vérifier automatiquement si une mise à jour est disponible. Si tel est le cas, un message vous en informe." + +#: +msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." +msgstr "Configurez l'affichage des jeux, le ratio, les filtres (shaders), sauvgarde/chargement auto et votre compte retroachievement." + +#: +msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." +msgstr "Le ratio est le rapport entre la largeur et la hauteur de l'image. Utilisez AUTO pour laisser l'émulateur choisir le ratio original du jeu, cela vous procurera la meilleur expérience de retrogaming." + +#: +msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." +msgstr "Lisser l'image du jeu. Cette option rend l'image plus lisse en utilisant un filtre bi-linéaire." + +#: +msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." +msgstr "Cette option vous permet de rembobiner le jeu si vous êtes tué par un monstre ou si vous avez fait une erreur. Utilisez la commande HOTKEY + GAUCHE dans le jeu pour rembobiner." + +#: +msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." +msgstr "Sauvegardez automatiquement l'état actuel quand vous quittez un jeu et lancez automatiquement la dernière sauvegarde quand vous lancez un jeu." + +#: +msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." +msgstr "Integer scale est une échelle par facteur de nombre entier comme 2x, 3x, 4x, etc. Cette option met à l'échelle l'image jusqu'au plus grand facteur entier en dessous de la résolution. Par exemple,avec une résolution de 1920x1080 et integer scale activé, une image de 320x240 sera mis à l'échelle jusqu'à 1280x960 en laissant des bords noirs tout autour. Ceci pour maintenir un ratio de pixels de 1:1 avec le format d'image original pour que les pixels ne soit pas inégalement dupliqués." + +#: +msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." +msgstr "Les \"shaders\" sont des filtres pour le rendu du jeu. Vous pouvez choisir un \"shader set\" ici qui est une collection de \"shaders\" sélectionnés pour chaque système. Vous pouvez aussi changer de \"shader\" en cours de jeu avec HOTKEY + L2 ou HOTKEY + R2" + +#: +msgid "Enable or disable RetroAchievements in games." +msgstr "Activez ou désactivez RetroAchievements dans les jeux." + +#: +msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." +msgstr "Le mode Hardcore désactive *toutes* les fonctions rembobinage et sauvegarde dans l'émulateur: vous ne pourrez pas sauvegarder et charger n'importe quand. Vous devrez finir le jeu et débloquer les succès du premier coup, comme sur la console originale. En récompense, vous gagnerez à la fois les succès hardcore et standard, doublant ainsi vos points! Un jeu normal valant 400 points vaudra désormais 800 points si vous le finissez en hardcore!\n" +"Par exemple: si vous finissez un jeu pour 40 points, vous avez ensuite la possibilité d'obtenir 400 points de plus en hardcore." + +#: +msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" +msgstr "Le site web retroachievements.org propose des challenges/succès/trophées sur les système comme NES, SNES, GB, GBC, GBA, Megadrive, TurboGrafx16/PCEngine et plus! Créez un compte sur retroachievements.org et lancer vous dans la quête de succès!" + +#: +msgid "Add and configure up to 5 controllers." +msgstr "Ajoutez et configurez jusqu'à 5 manettes." + +#: +msgid "Pair a bluetooth controller with your recalbox. Your controller must be in pairing mode." +msgstr "Appairez une manette Bluetooth avec votre Recalbox. Votre manette doit être en mode appairage." + +#: +msgid "Forget all paired bluetooth controllers. You will have to pair your controllers again, but this option can help if you have issues to reconnect a controller, which is already paired." +msgstr "Oublier toutes les manettes appairées. Vous devrez appairer votre manette de nouveau mais cette option est utile si vous rencontrez des problèmes pour reconnecter une manette déjà appairée." + +#: +msgid "Configure your EmulationStation experience. Select transition types, help prompts, screensaver behavior. You can also deactivate the onscreen keyboard if you have a real keyboard plugged into your recalbox.\n" +"If you've added games since the last boot, you can also refresh the gamelist from this menu." +msgstr "Configurez votre expérience EmulationStation. Sélectionnez le type de transition, l'aide, le comportement de l'économiseur d'écran. Vous pouvez également désactiver le clavier virtuel si vous avez un clavier branché sur votre Recalbox.\n" +"Si vous avez ajouté des jeux depuis le dernier démarrage, vous pouvez aussi rafraîchir les liste de jeux depuis ce menu." + +#: +msgid "Start the screensaver after N minutes." +msgstr "Démarrer l'économiseur d'écran après N minutes." + +#: +msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." +msgstr "Choisissez le comportement de l'économiseur d'écran. DIM réduira la luminosité de l'écran, BLACK rendra l'écran noir." + +#: +msgid "Shows a help at the bottom of the screen which displays commands you can use." +msgstr "Montre l'aide en bas de l'écran qui vous indique les commandes que vous pouvez utiliser." + +#: +msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." +msgstr "Si activé, vous pouvez changer de système tout en parcourant les listes de jeux en appuyant sur GAUCHE ou DROITE." + +#: +msgid "The onscreen keyboard is necessary to type text if you only have controllers plugged into your recalbox. You can disable it if you have a real keyboard connected." +msgstr "Le clavier virtuel est nécessaire pour taper du texte si vous n'avez que des manettes branchées sur votre Recalbox. Vous pouvez désactiver cette option si vous avez un clavier branché." + +#: +msgid "Select a theme for your recalbox." +msgstr "Sélectionnez un thème pour votre Recalbox." + +#: +msgid "Updates the gamelists, if you added games since the last boot." +msgstr "Mettez à jour les listes de jeux si vous avez ajouté des jeux depuis le dernier démarrage." + +#: +msgid "Configure the sound options of your recalbox." +msgstr "Configurez les options sonores de votre Recalbox." + +#: +msgid "Set the volume of the sound output for the frontend and the games." +msgstr "Réglez le volume de la sortie audio pour EmulationStation et les jeux." + +#: +msgid "Enable or disable the frontend music. You can add your own music as mp3, or ogg format in the 'musics' directory of your recalbox." +msgstr "Activez ou désactivez la musique dans EmulationStation. Vous pouvez ajouter vos propres musiques au format mp3 ou ogg dans le dossier 'musics' de votre Recalbox." + +#: +msgid "Select your output device. Only HDMI and JACK are supported." +msgstr "Sélectionnez votre périphérique de sortie. Seuls HDMI et JACK sont supportés." + +#: +msgid "Configure the network options of your recalbox.\n" +"Check your network status and IP address, set the hostname and configure the WIFI." +msgstr "Configurez les options réseau de votre recalbox. Vérifiez le statut de connexion et l'adresse IP, choisissez le nom réseau et configurez le WIFI." + +#: +msgid "Displays CONNECTED, if you are connected, by checking if your recalbox can access the recalbox.com update server." +msgstr "Affiche CONNECTÉ si vous êtes connecté en vérifiant si votre Recalbox peut accéder au serveur de mise à jour recalbox.com. " + +#: +msgid "The IP address of your recalbox within your local network." +msgstr "L'adresse IP de votre Recalbox dans votre réseau local." + +#: +msgid "Enable or disable WIFI.\n" +"If you disable WIFI, the SSID and the WIFI passwords are saved and can be used when you reactivate it" +msgstr "Activez ou désactivez le WIFI.\n" +"Si vous désactivez le WIFI, le SSID et la clé WIFI sont sauvegardés et réutilisés quand vous réactivez l'option." + +#: +msgid "The name of your recalbox in your local network" +msgstr "Le nom de votre Recalbox sur le réseau local." + +#: +msgid "SSID (WIFI Name) of your network." +msgstr "SSID (Nom WIFI) de votre réseau." + +#: +msgid "Private key of your WIFI network." +msgstr "Clé de sécurité de votre réseau WIFI." + +#: +msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." +msgstr "Récupérez des informations et visuels pour vos jeux. Le scraper télécharge les metadatas et images pour vos jeux depuis différents serveurs et améliore complètement l'expérience d'EmulationStation." + +#: +msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." +msgstr "Sélectionnez un serveur depuis lequel scraper. Le serveur SCREENSCRAPER est recommandé et est basé sur www.screenscraper.fr. Il scrape les données de jeu dans votre langue, si disponible." + +#: +msgid "Begin the scrape process with the configuration shown below." +msgstr "Démarrez le processus de scrape avec la configuration ci-dessous." + +#: +msgid "Scrape and display game ratings." +msgstr "Scrape et affiche la notation des jeux." + +#: +msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." +msgstr "Options avancées. Assurez-vous de savoir ce que vous faîtes avant de changer les valeurs dans ce menu." + +#: +msgid "Overclock your board to increase the performance.\n" +"Overclock settings are tested and validated by the community. Keep in mind that overclocking your board can void your warranty." +msgstr "Overclockez votre carte pour augmenter les performances.\n" +"Les paramètres d'overclocking sont testés et validés par la communauté.\n" +"Gardez à l'esprit qu'overclocker votre carte peut annuler votre garantie." + +#: +msgid "Select which system to show when the recalbox frontend starts. The default value is 'favorites'." +msgstr "Choisissez quel système montrer quand EmulationStation démarre.\n" +"La valeur par défaut est 'favorites'." + +#: +msgid "On boot, recalbox will show the list of games of the selected system rather than the system view." +msgstr "Au démarrage, Recalbox montrera la liste de jeux du système sélectionné plutôt que la vue systèmes." + +#: +msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" +"This option highly speeds up boot time, but new games will not be detected." +msgstr "Montrer seulement les jeux présent dans le fichier gamelist.xml (situé dans le dossier des roms).\n" +"Cette option accélère grandement le temps de démarrage mais les nouveaux jeux ne seront pas détectés." + +#: +msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." +msgstr "Cette option vous permet de mettre le système sélectionné en mode fixe.\n" +"Avec cette option activé, l'utilisateur ne peut pas accéder aux autres systèmes." + +#: +msgid "Always display the basic gamelist view, even if you have scraped your games." +msgstr "Toujours afficher la vue liste de jeux basique, même si vous avez scrappé vos jeux." + +#: +msgid "Override global options like emulator, core, ratio and more for each available system in your recalbox." +msgstr "Passer outre les options globales comme émulateur, core, ratio et plus pour chaque système disponible dans votre Recalbox." + +#: +msgid "Configure boot options that make your recalbox boot straight into a system or into Kodi, lock a user to a single system, or directly show the gamelist." +msgstr "Configurez les options de démarrage qui font démarrer votre Recalbox directement dans un système ou dans Kodi, bloquer un utilisateur dans un seul système ou montrer directement la liste de jeux." + +#: +msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" +msgstr "Activez ou désactivez Kodi, configurez le démarrage de Kodi, activez le bouton X pour démarrer Kodi." + +#: +msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." +msgstr "Activez ou désactivez Kodi. Si Kodi est désactivé, vous ne pourrez pas le lancer avec le bouton X ou le lancer automatiquement au démarrage. L'option du menu sera aussi retirée." + +#: +msgid "Use the X button to start Kodi." +msgstr "Utilisez le bouton X pour démarrer Kodi." + +#: +msgid "Automatically start into Kodi on boot." +msgstr "Lancer directement Kodi au démarrage." + +#: +msgid "Manage your recalbox security." +msgstr "Gérez la sécurité de votre Recalbox." + +#: +msgid "Change the SSH root password." +msgstr "Changez le mot de passe root SSH." + +#. not sure 'bout this one +#: +msgid "Enforce recalbox security." +msgstr "Renforcer la sécurité de Recalbox." + +#: +msgid "Enable or disable overscan.\n" +"Overscan can help you, if you have a black border, or if the image is bigger than your screen. Before setting the overscan, try to configure your TV to have a 1:1 pixel output.\n" +"More overscan settings can be defined in the boot.txt file, available when you plug your SD card into your computer." +msgstr "Activez ou désactivez l'overscan.\n" +"L'overscan peut vous aider si vous avez des bandes noires ou si l'image est plus grande que l'écran. Avant de régler l'overscan, essayez de régler votre TV pour avoir une sortie 1:1.\n" +"D'autres réglages d'overscan peuvent être définis dans le fichier boot.txt disponible lorsque vous branchez votre carte SD sur un ordinateur." + +#: +msgid "Show the framerate in EmulationStation and in game." +msgstr "Affichez le taux de rafraîchissement dans EmulationStation et dans les jeux." + +#: +msgid "Enable or disable the Recalbox Manager.\n" +"The Recalbox Manager is a web application available on http://recalbox , if you are on windows, http://recalbox.local , if you are on Linux or Mac, or directly with your recalbox IP : http://192.168.1.XX.\n" +"You can configure many options from within the manager, and even manage games, saves, and scrapes!" +msgstr "Activez ou désactivez le Gestionnaire Recalbox.\n" +"Le Gestionnaire Recalbox est une application web disponible sur http://recalbox si vous êtes sous Windows, http://recalbox.local si vous êtes sous Linux ou Mac ou directement avec l'adresse IP de la Recalbox: http://192.168.1.XX.\n" +"Vous pouvez configurer de nombreuses options dans le Gestionnaire et même gérer les jeux, les sauvegardes et les scraps!" + +#: +msgid "Enable or disable the recalbox API.\n" +"The Recalbox API is a REST API exposing endpoints to control your recalbox via http requests." +msgstr "Activez ou désactivez l'API Recalbox.\n" +"L'API Recalbox est une API REST permettant de contrôler votre Recalbox avec des requêtes http." + +#: +msgid "Select which emulator to use when you start a game for this system." +msgstr "Selectionnez quel émulateur utiliser quand vous lancer un jeu pour ce système." + +#: +msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." +msgstr "Selectionnez quel core utiliser pour l'émulateur sélectionné. Par exemple, l'émulateur LIBRETRO a plusieurs core pour les jeux Super Nintendo. Le core par défaut que vous choisissez peut aussi être outrepassé par les réglages spécifiques par jeu." + +#: +msgid "USE COMPOSED VISUALS" +msgstr "UTILISER DES IMAGES COMPOSÉES" + +#: +msgid "CHECK UPDATES" +msgstr "VÉRIFIER LES MISES À JOUR" + +#: +msgid "UPDATE TYPE" +msgstr "TYPE DE MISE À JOUR" + +#: +msgid "INTEGER SCALE (PIXEL PERFECT)" +msgstr "INTEGER SCALE (PIXEL PERFECT)" + +#: +msgid "ADVANCED SETTINGS" +msgstr "PARAMÈTRES AVANCÉS" + +#: +msgid "BOOT SETTINGS" +msgstr "PARAMÈTRES DE DÉMARRAGE" + +#: +msgid "GAMELIST ONLY" +msgstr "LISTE DE JEUX UNIQUEMENT" + +#: +msgid "START ON SYSTEM" +msgstr "DÉMARRER SUR UN SYSTÈME" + +#: +msgid "BOOT ON GAMELIST" +msgstr "DÉMARRER SUR LA LISTE DE JEUX" + +#: +msgid "HIDE SYSTEM VIEW" +msgstr "CACHER LA VUE SYSTÈME" + +#: +msgid "EMULATOR ADVANCED CONFIGURATION" +msgstr "CONFIGURATION AVANCÉE DE L'ÉMULATEUR" + +#: +msgid "ADVANCED EMULATOR CONFIGURATION" +msgstr "CONFIGURATION AVANCÉE DE L'ÉMULATEUR" + +#: +msgid "HELP" +msgstr "AIDE" + +#: +msgid "THE SYSTEM IS UP TO DATE" +msgstr "LE SYSTÈME EST À JOUR" + +#: +msgid "FORCE BASIC GAMELIST VIEW" +msgstr "FORCER LA VUE LISTE DE JEUX BASIQUE" + +#: +msgid "DOWNLOADED" +msgstr "TÉLÉCHARGÉ" + +#: +msgid "UPDATE VERSION:" +msgstr "VERSION MISE ­À JOUR:" + +#: +msgid "UPDATE CHANGELOG:" +msgstr "CHANGELOG DE LA MISE À JOUR:" + +#: +msgid "MORE DETAILS" +msgstr "PLUS DE DÉTAILS" + +#: +msgid "CAROUSEL TRANSITIONS" +msgstr "ANIMATION DU BANDEAU" + +#: +msgid "ENABLE FILTERS" +msgstr "FILTRES" + +#: +msgid "THEME CONFIGURATION" +msgstr "CONFIGURATION DU THÈME" + +#: +msgid "THEME COLORSET" +msgstr "COLORSET DU THÈME" + +#: +msgid "THEME ICONSET" +msgstr "ICONSET DU THÈME" + +#: +msgid "THEME MENU" +msgstr "MENU DU THÈME" + +#: +msgid "THEME SYSTEMVIEW" +msgstr "SYSTEMVIEW DU THÈME" + +#: +msgid "THEME GAMELISTVIEW" +msgstr "GAMELISTVIEW DU THÈME" + +#: +msgid "THEME REGION" +msgstr "RÉGION DU THÈME" + +#: +msgid "THIS THEME HAS NO OPTION" +msgstr "CE THÈME N'A PAS D'OPTIONS" + +#: +msgid "MANUAL INPUT" +msgstr "SAISIE MANUELLE" + +#: +msgid "AN ERROR OCCURED - DOWNLOADED" +msgstr "UNE ERREUR EST SURVENUE - TÉLÉCHARGÉ" + +#: +msgid "START KODI" +msgstr "DÉMARRER KODI" + +#: +msgid "Shows the current available update version." +msgstr "Montre les versions disponibles." + +#: +msgid "Shows the current available update changelog." +msgstr "Montre le changelog de la mise à jour disponible." + +#: +msgid "Configure an associated controller. Your controller has to be associated / plugged before." +msgstr "Configurer une manette appairée. Votre manette doit être appairée / branchée auparavant." + +#: +msgid "Choose if carousel will be animated or not during transitions" +msgstr "Choisissez si le bandeau sera animé ou non durant les transitions." + +#: +msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" +msgstr "Sélectionnez le type de transition qui se produit lorsque vous lancez un jeu. FADE fera un fondu au noir et SLIDE zoomera sur l'image du jeu (ou le nom si il n'y a pas de metadatas)." + +#: +msgid "Select exisiting colorset options for this theme." +msgstr "Sélectionnez un colorset disponible pour votre thème" + +#: +msgid "Select exisiting iconset options for this theme." +msgstr "Sélectionnez un iconset disponible pour votre thème" + +#: +msgid "Select exisiting menu style options for this theme." +msgstr "Sélectionnez un style de menu disponible pour votre thème" + +#: +msgid "Select exisiting system view options for this theme." +msgstr "Sélectionnez une vue système disponible pour votre thème" + +#: +msgid "Select exisiting gamelist view options for this theme." +msgstr "Sélectionnez une vue gamelist disponible pour votre thème" + +#: +msgid "Configure theme options if available." +msgstr "Configurez les options du thème si disponibles" + +#: +msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" +msgstr "Sélectionnez une région des logos et images pour les systèmes qui sont différents selon les pays. Ex: Megadrive en Europe / Genesis aux USA" + +#: +msgid "Type the name of your SSID if it is hidden or not listed" +msgstr "Saisissez le nom de votre SSID si il n'est pas dans la liste ou caché" + +#: +msgid "Select a letter and the listing will go directly on the first game starting with this letter." +msgstr "Sélectionnez une lettre et le listing ira directement sur le 1er jeu commençant par cette lettre." + +#: +msgid "Select the way the game list is sortered (alphabetically, by notation...)." +msgstr "Sélectionnez la façon dont la liste de jeux est triée (alphabétique, par note...)." + +#: +msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." +msgstr "Changer entre rendre visible ou non les jeux favoris. Pour ajouter un jeu dans les favoris, sélectionnez le jeu et changer son statut en utilisant 'Y'." + +#: +msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." +msgstr "Changer entre rendre visible ou non les jeux cachés. pour cacher un jeu, éditez ses données et sélectionnez 'Cacher'." + +#: +msgid "This option display a menu which allows to change game data and many others options." +msgstr "Cette option affiche un menu qui permet de changer les données du jeu et de nombreuses autres options." + +#: +msgid "AVAILABLE UPDATE" +msgstr "MISE À JOUR DISPONIBLE" + +#: +msgid "UPDATE CHANGELOG" +msgstr "CHANGELOG DE LA MISE À JOUR" + +#: +msgid "CLOCK IN MENU" +msgstr "HORLOGE DANS LE MENU" + +#: +msgid "Now playing" +msgstr "Lecture en cours" + +#: +msgid "DEFAULT (%1%)" +msgstr "DÉFAUT (%1%)" + +#: +msgid "INPUT REQUIRED" +msgstr "ENTRÉE NÉCESSAIRE" + +#: +msgid "(skipped)" +msgstr "(passé)" + +#: +msgid "UP/DOWN TO SKIP" +msgstr "HAUT/BAS POUR PASSER" + +#: +msgid "A TO UNSET" +msgstr "A POUR ANNULER" + +#: +msgid "DOWN TO SKIP AND KEEP [%1%]" +msgstr "BAS POUR PASSER ET GARDER [%1%]" + +#: +msgid "UP/DOWN TO SKIP AND KEEP [%1%]" +msgstr "HAUT/BAS POUR PASSER ET GARDER [%1%]" + +#: +msgid "Set duration of help popups, 0 means no popup." +msgstr "Régler la durée des popups d'aide, 0 pour désactiver." + +#: +msgid "HELP POPUP DURATION" +msgstr "DURÉE DES POPUPS D'AIDE" + +#: +msgid "Set duration of music popups, 0 means no popup." +msgstr "Régler la durée des popups de musique, 0 pour désactiver." + +#: +msgid "MUSIC POPUP DURATION" +msgstr "DURÉE DES POPUPS DE MUSIQUE" + +#: +msgid "POPUP SETTINGS" +msgstr "RÉGLAGE DES POPUPS" + +#: +msgid "POPUP POSITION" +msgstr "POSITION DES POPUPS" + +#: +msgid "Select the position of popups on screen." +msgstr "Choisissez la position des popups sur l'écran" + +#: +msgid "Set position and duration of popups." +msgstr "Réglez la position et la durée des popups" + +#: +msgid "TOP/RIGHT" +msgstr "HAUT/DROITE" + +#: +msgid "BOTTOM/RIGHT" +msgstr "BAS/DROITE" + +#: +msgid "BOTTOM/LEFT" +msgstr "BAS/GAUCHE" + +#: +msgid "TOP/LEFT" +msgstr "HAUT/GAUCHE" + +#: +msgid "SHOW FOLDERS CONTENT" +msgstr "MONTRER LE CONTENU DES DOSSIERS" + +#: +msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." +msgstr "Basculer entre l'affichage classique de répertoires et l'affichage de tous les jeux en une seule liste." + +#: +msgid "NETPLAY" +msgstr "NETPLAY" + +#: +msgid "NETPLAY SETTINGS" +msgstr "OPTIONS NETPLAY" + +#: +msgid "NETPLAY LOBBY" +msgstr "LOBBY NETPLAY" + +#: +msgid "Enable or disable Netplay in games." +msgstr "Activer ou désactiver Netplay dans les jeux." + +#: +msgid "PORT" +msgstr "PORT" + +#: +msgid "NICKNAME" +msgstr "SURNOM" + +#: +msgid "RELAY SERVER" +msgstr "SERVEUR DE RELAI" + +#: +msgid "Enable or disable connections throught relay servers." +msgstr "Activer ou désactiver les connexions au travers de serveurs de relai." + +#: +msgid "KODI/NETPLAY" +msgstr "KODI/NETPLAY" + +#: +msgid "NO GAMES OR NO CONNECTION" +msgstr "PAS DE JEUX OU PAS DE CONNEXION" + +#: +msgid "HASH NOW" +msgstr "HASHER MAINTENANT" + +#: +msgid "HASH THESE SYSTEMS" +msgstr "HASHER CES SYSTÈMES" + +#: +msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." +msgstr "Ajoutez les hashs des roms dans vos gamelists pour avoir un résultat plus précis en Netplay." + +#: +msgid "HASH ROMS" +msgstr "HASHER LES ROMS" + +#: +msgid "Only missing hashs" +msgstr "Seulement hashs manquants" + +#: +msgid "Username" +msgstr "Nom" + +#: +msgid "Country" +msgstr "Pays" + +#: +msgid "Latency" +msgstr "Latence" + +#: +msgid "Host arch." +msgstr "Arch. de l'hôte" + +#: +msgid "Core ver." +msgstr "Ver. du core" + +#: +msgid "RA ver." +msgstr "Ver. de RA" + +#: +msgid "Can join" +msgstr "Joignable" + +#: +msgid "Rom and core match" +msgstr "Corres. rom et core" + +#: +msgid "Rom found" +msgstr "Rom trouvée" + +#: +msgid "No rom match" +msgstr "Pas de corres. de rom" + +#: +msgid "Match" +msgstr "Correspondance" + +#: +msgid "No Match" +msgstr "Pas de corres." + +#: +msgid "Rom file" +msgstr "Fichier rom" + +#: +msgid "Rom hash" +msgstr "Hash de la rom" + +#: +msgid "THIS COULD TAKE A WHILE, CONFIRM?" +msgstr "CELA PEUX PRENDRE LONGTEMPS, CONFIRMER?" + +#: +msgid "good" +msgstr "bon" + +#: +msgid "bad" +msgstr "mauvais" + +#: +msgid "medium" +msgstr "moyen" + +#: +msgid "NETPLAY POPUP DURATION" +msgstr "DURÉE DES POPUPS NETPLAY" + +#: +msgid "Set duration of netplay popups, 0 means no popup." +msgstr "Régler la durée des popups Netplay, 0 pour désactiver." + +#: +msgid "Player" +msgstr "Joueur" + +#: +msgid "Game" +msgstr "Jeu" + +#: +msgid "A Recalbox friend has started a Netplay game!" +msgstr "Un ami Recalbox a démarré une partie Netplay!" + +#: +msgid "Play online on games running through Retroarch like NES, SNES, FBA, Genesis/Megadrive and more!" +msgstr "Jouez en ligne à des jeux tournant sous Retroarch comme NES, SNES, FBA, Megadrive et bien plus!" + +#: +msgid "Rom, hash and core match" +msgstr "Corres. rom, hash et core" + +#: +msgid "No core match" +msgstr "Pas de corres. du core" + +#: +msgid "Add a clock in the main menu." +msgstr "Ajouter une horloge dans le menu principal" + +#: +msgid "UPGRADING" +msgstr "Mise à jour" + +#: +msgid "PREPARING" +msgstr "Préparation" + +#: +msgid "Starting UI" +msgstr "Initialisation de l'interface" + +#: +msgid "VERIFYING" +msgstr "Verification" + +#: +msgid "EMPTY LIST" +msgstr "Liste vide" + + +#: Retroarch ratio +msgid "Auto" +msgstr "Automatique" + +msgid "Square pixel" +msgstr "Pixel carré" + +msgid "Retroarch Config" +msgstr "Retroarch mode 'Config'" + +msgid "Retroarch Custom" +msgstr "Retroarch mode 'Custom'" + +msgid "Core provided" +msgstr "Fourni par le Core" + +msgid "Do not set" +msgstr "Ne pas toucher" \ No newline at end of file diff --git a/resources/locale/it/emulationstation2.po b/resources/locale/it/emulationstation2.po new file mode 100644 index 0000000000..f74c4c50b7 --- /dev/null +++ b/resources/locale/it/emulationstation2.po @@ -0,0 +1,1884 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: recalbox-emulationstation\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: +msgid "AN UPDATE IS AVAILABLE FOR YOUR RECALBOX" +msgstr "HAY UNA ACTUALIZACIÓN DISPONIBLE PARA RECALBOX" + +#: +msgid "CANCEL" +msgstr "CANCELAR" + +#: +msgid "Rating" +msgstr "Calificación" + +#: +msgid "Released" +msgstr "Lanzado" + +#: +msgid "Developer" +msgstr "Desarrollador" + +#: +msgid "Publisher" +msgstr "Editor" + +#: +msgid "Genre" +msgstr "Género" + +#: +msgid "Players" +msgstr "Jugadores" + +#: +msgid "NO GAMES FOUND - SKIP" +msgstr "JUEGOS NO ENCONTRADOS - OMITIR" + +#: +msgid "RETRY" +msgstr "REINTENTAR" + +#: +msgid "SKIP" +msgstr "OMITIR" + +#: +msgid "SEARCH FOR" +msgstr "BUSCAR POR" + +#: +msgid "SEARCH" +msgstr "BUSCAR" + +#: +msgid "SCRAPING IN PROGRESS" +msgstr "OBTENCIÓN DE DATOS EN CURSO" + +#: +msgid "SYSTEM" +msgstr "SISTEMA" + +#: +msgid "subtitle text" +msgstr "subtítulo" + +#: +msgid "INPUT" +msgstr "ENTRADA" + +#: +msgid "search" +msgstr "buscar" + +#: +msgid "STOP" +msgstr "DETENER" + +#: +msgid "stop (progress saved)" +msgstr "detener (guardar progreso)" + +#: +msgid "GAME %i OF %i" +msgstr "JUEGO %i DE %i" + +#: +msgid "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 RECALBOX.COM FOR MORE INFORMATION." +msgstr "¡NO SE HAN ENCONTRADO SISTEMAS!\n" +"COMPROBAR RUTAS EN ARCHIVO DE CONFIGURACIÓN.\n" +"LA CARPETA DE JUEGOS DEBE CONTENER AL MENOS UNO Y CON LA EXTENSIÓN CORRECTA.\n" +"\n" +"VISITAR RECALBOX.COM PARA OBTENER MÃS INFORMACIÓN" + +#: +msgid "%i GAME SUCCESSFULLY SCRAPED!" +msgid_plural "%i GAMES SUCCESSFULLY SCRAPED!" +msgstr[0] "¡INFORMACIÓN DE %i JUEGO OBTENIDA CORRECTAMENTE!" +msgstr[1] "¡INFORMACIÓN DE %i JUEGOS OBTENIDA CORRECTAMENTE!" + +#: +msgid "%i GAME SKIPPED." +msgid_plural "%i GAMES SKIPPED." +msgstr[0] "%i JUEGO OMITIDO" +msgstr[1] "%i JUEGOS OMITIDOS" + +#: +msgid "OK" +msgstr "OK" + +#: +msgid "EDIT METADATA" +msgstr "EDITAR METADATOS" + +#: +msgid "SCRAPE" +msgstr "OBTENER INFORMACIÓN" + +#: +msgid "SAVE" +msgstr "GUARDAR" + +#: +msgid "THIS WILL DELETE A FILE!\n" +"ARE YOU SURE?" +msgstr "¿SEGURO DESEA BORRAR EL ARCHIVO?" + +#: +msgid "YES" +msgstr "SÃ" + +#: +msgid "NO" +msgstr "NO" + +#: +msgid "DELETE" +msgstr "BORRAR" + +#: +msgid "SAVE CHANGES?" +msgstr "¿GUARDAR CAMBIOS?" + +#: +msgid "BACK" +msgstr "VOLVER" + +#: +msgid "CLOSE" +msgstr "CERRAR" + +#: +msgid "MAIN MENU" +msgstr "MENÚ PRINCIPAL" + +#: +msgid "KODI MEDIA CENTER" +msgstr "KODI MEDIA CENTER" + +#: +msgid "SYSTEM SETTINGS" +msgstr "CONFIGURACIÓN DE SISTEMA" + +#: +msgid "VERSION" +msgstr "VERSIÓN" + +#: +msgid "DISK USAGE" +msgstr "USO DE DISCO" + +#: +msgid "STORAGE DEVICE" +msgstr "ALMACENAMIENTO" + +#: +msgid "LANGUAGE" +msgstr "IDIOMA" + +#: +msgid "OVERCLOCK" +msgstr "OVERCLOCK" + +#: +msgid "EXTREM (1100Mhz)" +msgstr "EXTREMO (1100Mhz)" + +#: +msgid "TURBO (1000Mhz)" +msgstr "TURBO (1000Mhz)" + +#: +msgid "HIGH (950Mhz)" +msgstr "ALTO (950Mhz)" + +#: +msgid "NONE (700Mhz)" +msgstr "NO (700Mhz)" + +#: +msgid "TURBO (1050Mhz)+" +msgstr "TURBO (1050Mhz)+" + +#: +msgid "HIGH (1050Mhz)" +msgstr "ALTO (1050Mhz)" + +#: +msgid "NONE (900Mhz)" +msgstr "NO (900Mhz)" + +#: +msgid "NONE (1200Mhz)" +msgstr "NO (1200Mhz)" + +#: +msgid "NONE" +msgstr "NO" + +#. NEW SETTINGS ORGANIZATION +#: +msgid "UPDATES" +msgstr "ACTUALIZACIONES" + +#: +msgid "AUTO UPDATES" +msgstr "ACTUALIZACIONES AUTOMÃTICAS" + +#: +msgid "START UPDATE" +msgstr "COMENZAR ACTUALIZACIÓN" + +#: +msgid "KODI SETTINGS" +msgstr "CONFIGURACIÓN DE KODI" + +#: +msgid "ENABLE KODI" +msgstr "ACTIVAR KODI" + +#: +msgid "KODI AT START" +msgstr "INICIAR KODI AL ARRANCAR" + +#: +msgid "START KODI WITH X" +msgstr "PULSAR X PARA INICIAR KODI" + +#: +msgid "SECURITY" +msgstr "SEGURIDAD" + +#: +msgid "ENFORCE SECURITY" +msgstr "REFORZAR SEGURIDAD" + +#: +msgid "ROOT PASSWORD" +msgstr "CONTRASEÑA ROOT" + +#: +msgid "THE SYSTEM WILL NOW REBOOT" +msgstr "EL SISTEMA SERà REINICIADO" + +#: +msgid "GAMES SETTINGS" +msgstr "CONFIGURACIÓN DE JUEGOS" + +#: +msgid "GAME RATIO" +msgstr "RELACIÓN DE ASPECTO" + +#: +msgid "SMOOTH GAMES" +msgstr "SUAVIZAR JUEGOS" + +#: +msgid "REWIND" +msgstr "REBOBINAR" + +#: +msgid "AUTO SAVE/LOAD" +msgstr "GUARDAR/CARGAR AUTOMÃTICAMENTE" + +#: +msgid "SHADERS SET" +msgstr "CONJUNTO DE SHADERS" + +#. Yo dejaría en español SCANLINES, ya que en españa también se refiere a este filtro como SCANLINES, y no como LINEAS.. +#: +msgid "SCANLINES" +msgstr "SCANLINES" + +#: +msgid "RETRO" +msgstr "RETRO" + +#: +msgid "RETROACHIEVEMENTS SETTINGS" +msgstr "CONFIGURACIÓN DE RETROLOGROS" + +#: +msgid "RETROACHIEVEMENTS" +msgstr "RETROLOGROS" + +#: +msgid "HARDCORE MODE" +msgstr "MODO HARDCORE" + +#: +msgid "USERNAME" +msgstr "USUARIO" + +#: +msgid "PASSWORD" +msgstr "CONTRASEÑA" + +#: +msgid "ADVANCED" +msgstr "AVANZADO" + +#: +msgid "REALLY UPDATE GAMES LISTS ?" +msgstr "¿ACTUALIZAR LISTA DE JUEGOS?" + +#: +msgid "UPDATE GAMES LISTS" +msgstr "ACTUALIZAR LISTA DE JUEGOS" + +#: +msgid "CONTROLLERS SETTINGS" +msgstr "CONFIGURACIÓN DE MANDOS" + +#: +msgid "UI SETTINGS" +msgstr "CONFIGURACIÓN DE INTERFAZ" + +#: +msgid "OVERSCAN" +msgstr "OVERSCAN" + +#: +msgid "SCREENSAVER AFTER" +msgstr "SALVAPANTALLA A LOS" + +#: +msgid "TRANSITION STYLE" +msgstr "TIPO DE TRANSICIÓN" + +#: +msgid "SCREENSAVER BEHAVIOR" +msgstr "COMPORTAMIENTO DE SALVAPANTALLA" + +#: +msgid "SHOW FRAMERATE" +msgstr "MOSTRAR TASA DE REFRESCO" + +#: +msgid "ON-SCREEN HELP" +msgstr "AYUDA EN PANTALLA" + +#: +msgid "QUICK SYSTEM SELECT" +msgstr "SELECCIÓN RÃPIDA DE SISTEMA" + +#: +msgid "THEME SET" +msgstr "TEMA" + +#: +msgid "SOUND SETTINGS" +msgstr "CONFIGURACIÓN DE AUDIO" + +#: +msgid "SYSTEM VOLUME" +msgstr "VOLUMEN DE SISTEMA" + +#: +msgid "FRONTEND MUSIC" +msgstr "MÚSICA DE FONDO" + +#: +msgid "OUTPUT DEVICE" +msgstr "SALIDA DE AUDIO" + +#: +msgid "HDMI" +msgstr "HDMI" + +#: +msgid "JACK" +msgstr "JACK" + +#: +msgid "AUTO" +msgstr "AUTO" + +#: +msgid "NETWORK SETTINGS" +msgstr "CONFIGURACIÓN DE RED" + +#: +msgid "CONNECTED" +msgstr "CONECTADO" + +#: +msgid "NOT CONNECTED" +msgstr "NO CONECTADO" + +#: +msgid "STATUS" +msgstr "ESTADO" + +#: +msgid "IP ADDRESS" +msgstr "DIRECCIÓN IP" + +#: +msgid "HOSTNAME" +msgstr "NOMBRE DE HOST" + +#: +msgid "ENABLE WIFI" +msgstr "ACTIVAR WIFI" + +#: +msgid "WIFI SSID" +msgstr "SSID WIFI" + +#: +msgid "WIFI KEY" +msgstr "CLAVE WIFI" + +#: +msgid "WIFI ENABLED" +msgstr "WIFI ACTIVADO" + +#: +msgid "WIFI CONFIGURATION ERROR" +msgstr "ERROR DE CONFIGURACIÓN WIFI" + +#: +msgid "SCRAPER" +msgstr "SCRAPER" + +#: +msgid "SCRAPE FROM" +msgstr "OBTENER DATOS DE" + +#: +msgid "SCRAPE RATINGS" +msgstr "OBTENER CALIFICACIONES" + +#: +msgid "SCRAPE NOW" +msgstr "OBTENER DATOS AHORA" + +#: +msgid "QUIT" +msgstr "SALIR" + +#: +msgid "REALLY RESTART?" +msgstr "¿REINICIAR?" + +#: +msgid "RESTART SYSTEM" +msgstr "REINICIAR SISTEMA" + +#: +msgid "REALLY SHUTDOWN?" +msgstr "¿APAGAR?" + +#: +msgid "SHUTDOWN SYSTEM" +msgstr "APAGAR SISTEMA" + +#: +msgid "Emulator" +msgstr "Emulador" + +#: +msgid "Core" +msgstr "Núcleo" + +#: +msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." +msgstr "SI SÓLO EXISTE UN MANDO, CONFIGURAR LAS DIRECCIONES Y OMITIR LA CONFIGURACIÓN, MANTENIENDO PRESIONADO UN BOTÓN.\n" +"SI CARECE DE BOTÓN PARA HOTKEY, UTILIZAR BOTÓN SELECT.\n" +"OMITIR BOTONES FALTANTES, MANTENIENDO PRESIONADO CUALQUIER OTRO BOTÓN.\n" +"LOS NOMBRES DE LOS BOTONES SE BASAN EN EL MANDO DE SNES." + +#. GUIMENU +#: +msgid "CONFIGURE A CONTROLLER" +msgstr "CONFIGURAR UN MANDO" + +#. Bluetooth +#: +msgid "CONTROLLER PAIRED" +msgstr "MANDO VINCULADO" + +#: +msgid "UNABLE TO PAIR CONTROLLER" +msgstr "IMPOSIBLE VINCULAR MANDO" + +#: +msgid "AN ERROR OCCURED" +msgstr "ERROR" + +#: +msgid "NO CONTROLLERS FOUND" +msgstr "NINGÚN MANDO DETECTADO" + +#: +msgid "PAIR A BLUETOOTH CONTROLLER" +msgstr "VINCULAR MANDO BLUETOOTH" + +#: +msgid "CONTROLLERS LINKS HAVE BEEN DELETED." +msgstr "MANDOS VINCULADOS BORRADOS" + +#: +msgid "FORGET BLUETOOTH CONTROLLERS" +msgstr "DESVINCULAR MANDOS BLUETOOTH" + +#: +msgid "INPUT P%i" +msgstr "ENTRADA P%i" + +#: +msgid "CHOOSE" +msgstr "SELECCIONAR" + +#: +msgid "SELECT" +msgstr "SELECCIONAR" + +#: +msgid "OPTIONS" +msgstr "OPCIONES" + +#: +msgid "JUMP TO LETTER" +msgstr "IR A LETRA" + +#: +msgid "SORT GAMES BY" +msgstr "ORDENAR JUEGOS POR" + +#. FAVORITES +#: +msgid "FAVORITES ONLY" +msgstr "SOLO FAVORITOS" + +#: +msgid "EDIT THIS GAME'S METADATA" +msgstr "EDITAR DATOS DEL JUEGO" + +#: +msgid "SCRAPE THESE GAMES" +msgstr "OBTENER DATOS DE JUEGOS" + +#: +msgid "All Games" +msgstr "Todos los juegos" + +#. MISSING SCRAPPER TRANSLATIONS +#: +msgid "Only missing image" +msgstr "Solo imágenes faltantes" + +#: +msgid "FILTER" +msgstr "FILTRO" + +#: +msgid "SCRAPE THESE SYSTEMS" +msgstr "OBTENER SISTEMAS" + +#: +msgid "SYSTEMS" +msgstr "SISTEMAS" + +#: +msgid "USER DECIDES ON CONFLICTS" +msgstr "DECIDIR EN CASO DE CONFLICTOS" + +#: +msgid "START" +msgstr "INICIAR" + +#: +msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" +"CONTINUE ANYWAY?" +msgstr "CUIDADO: ALGÚN SISTEMA SELECCIONADO NO TIENE PLATAFORMA DEFINIDA.\n" +"¡LOS RESULTADOS PODRÃAN SER MÃS INCORRECTOS DE LO HABITUAL!\n" +"¿ACEPTAR?" + +#: +msgid "NO GAMES FIT THAT CRITERIA." +msgstr "NINGÚN JUEGO SE AJUSTA AL CRITERIO" + +#: +msgid "REALLY UPDATE?" +msgstr "¿ACTUALIZAR?" + +#: +msgid "NETWORK CONNECTION NEEDED" +msgstr "SE NECESITA CONEXIÓN DE RED" + +#. Lo correcto sería: +#. +#. ACTUALIZACIÓN DESCARGADA, EL SISTEMA SE REINICIARà +#: +msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" +msgstr "ACTUALIZACIÓN DESCARGADA, EL SISTEMA SE REINICIARÃ" + +#. Lo correcto sería: +#. +#. ACTUALIZACIÓN FALLIDA, EL SISTEMA SE REINICIARà +#: +msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" +msgstr "ACTUALIZACIÓN FALLIDA, EL SISTEMA SE REINICIARÃ" + +#: +msgid "NO UPDATE AVAILABLE" +msgstr "SIN ACTUALIZACIONES DISPONIBLES" + +#: +msgid "enter emulator" +msgstr "ingresar emulador" + +#: +msgid "enter core" +msgstr "ingresar núcleo" + +#: +msgid "Ratio" +msgstr "Proporción" + +#: +msgid "enter ratio" +msgstr "ingresar proporción" + +#: +msgid "Name" +msgstr "Nombre" + +#: +msgid "enter game name" +msgstr "ingresar nombre del juego" + +#: +msgid "Description" +msgstr "Descripción" + +#: +msgid "enter description" +msgstr "ingresar descripción" + +#: +msgid "Image" +msgstr "Imagen" + +#: +msgid "enter path to image" +msgstr "ingresar ruta de imagen" + +#: +msgid "Thumbnail" +msgstr "Miniatura" + +#: +msgid "enter path to thumbnail" +msgstr "ingresar ruta de miniatura " + +#. the english translation has a space befor the word "enter", typo? +#: +msgid "enter rating" +msgstr " ingresar calificación" + +#: +msgid "Release date" +msgstr "Fecha de lanzamiento" + +#: +msgid "enter release date" +msgstr "ingresar fecha de lanzamiento" + +#: +msgid "enter game developer" +msgstr "ingresar desarrollador" + +#. La palabra correcta para "publisher" es "editor". +#: +msgid "enter game publisher" +msgstr "ingresar editor" + +#: +msgid "enter game genre" +msgstr "ingresar género" + +#: +msgid "enter number of players" +msgstr "ingresar número de jugadores" + +#: +msgid "Favorite" +msgstr "Favorito" + +#: +msgid "enter favorite" +msgstr "ingresar favorito" + +#: +msgid "Region" +msgstr "Región" + +#: +msgid "enter region" +msgstr "ingresar región" + +#: +msgid "Romtype" +msgstr "Tipo de ROM" + +#: +msgid "enter romtype" +msgstr "ingresar tipo de ROM" + +#: +msgid "Hidden" +msgstr "Oculto" + +#: +msgid "set hidden" +msgstr "ocultar" + +#: +msgid "Play count" +msgstr "Partidas jugadas" + +#: +msgid "enter number of times played" +msgstr "ingresar partidas jugadas" + +#: +msgid "Last played" +msgstr "Última partida" + +#: +msgid "enter last played date" +msgstr "ingresar fecha de última partida" + +#: +msgid "%i GAME AVAILABLE" +msgid_plural "%i GAMES AVAILABLE" +msgstr[0] "%i JUEGO DISPONIBLE" +msgstr[1] "%i JUEGOS DISPONIBLES" + +#: +msgid "%i FAVORITE" +msgid_plural "%i FAVORITES" +msgstr[0] "%i FAVORITO" +msgstr[1] "%i FAVORITOS" + +#. Imagino que aquí queda mejor utilizar el infinitivo, comparandolo con la siguiente entrada... +#: +msgid "SCROLL" +msgstr "DESPLAZAR" + +#: +msgid "LAUNCH" +msgstr "LANZAR" + +#: +msgid "Times played" +msgstr "Veces jugado" + +#: +msgid "MENU" +msgstr "MENÚ" + +#: +msgid "FILENAME, ASCENDING" +msgstr "NOMBRE, ASCENDENTE" + +#: +msgid "FILENAME, DESCENDING" +msgstr "NOMBRE, DESCENDENTE" + +#: +msgid "RATING, ASCENDING" +msgstr "CALIFICACIÓN, ASCENDENTE" + +#: +msgid "RATING, DESCENDING" +msgstr "CALIFICACIÓN, DESCENDENTE" + +#: +msgid "TIMES PLAYED, ASCENDING" +msgstr "PARTIDAS, ASCENDENTE" + +#: +msgid "TIMES PLAYED, DESCENDING" +msgstr "PARTIDAS, DESCENDENTE" + +#: +msgid "LAST PLAYED, ASCENDING" +msgstr "ÚLTIMA PARTIDA, ASCENDENTE" + +#: +msgid "LAST PLAYED, DESCENDING" +msgstr "ÚLTIMA PARTIDA, DESCENDENTE" + +#: +msgid "WORKING..." +msgstr "TRABAJANDO..." + +#: +msgid "CHANGE" +msgstr "CAMBIAR" + +#: +msgid "never" +msgstr "nunca" + +#: +msgid "just now" +msgstr "ahora mismo" + +#: +msgid "%i sec ago" +msgid_plural "%i secs ago" +msgstr[0] "Hace %i segundo" +msgstr[1] "Hace %i segundos" + +#: +msgid "%i min ago" +msgid_plural "%i mins ago" +msgstr[0] "Hace %i minuto" +msgstr[1] "Hace %i minutos" + +#: +msgid "%i hour ago" +msgid_plural "%i hours ago" +msgstr[0] "Hace %i hora" +msgstr[1] "Hace %i horas" + +#: +msgid "%i day ago" +msgid_plural "%i days ago" +msgstr[0] "Hace %i día" +msgstr[1] "Hace %i días" + +#: +msgid "unknown" +msgstr "desconocido" + +#: +msgid "SELECT ALL" +msgstr "SELECCIONAR TODO" + +#: +msgid "SELECT NONE" +msgstr "SELECCIONAR NINGUNO" + +#: +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i SELECCIONADO" +msgstr[1] "%i SELECCIONADOS" + +#: +msgid "UP" +msgstr "ARRIBA" + +#: +msgid "DOWN" +msgstr "ABAJO" + +#: +msgid "LEFT" +msgstr "IZQUIERDA" + +#: +msgid "RIGHT" +msgstr "DERECHA" + +#. @Claudio, mando se refiere a todo el control, y no queda claro que se trata del joystick/palanca. Voy a volver a poner joystick a todos estos. +#: +msgid "JOYSTICK 1 UP" +msgstr "JOYSTICK 1 ARRIBA" + +#: +msgid "JOYSTICK 1 LEFT" +msgstr "JOYSTICK 1 IZQUIERDA" + +#: +msgid "JOYSTICK 2 UP" +msgstr "JOYSTICK 2 ARRIBA" + +#: +msgid "JOYSTICK 2 LEFT" +msgstr "JOYSTICK 2 IZQUIERDA" + +#. Creo que esta traducción no es correcta, ya que al configurar los botones L1 y R1 debería ser: +#. +#. L1: PÃGINA ANTERIOR +#. R1: PÃGINA SIGUIENTE +#: +msgid "PAGE UP" +msgstr "PÃGINA ARRIBA" + +#: +msgid "PAGE DOWN" +msgstr "PÃGINA ABAJO" + +#: +msgid "HOTKEY" +msgstr "HOTKEY" + +#: +msgid "CONFIGURING" +msgstr "CONFIGURANDO" + +#: +msgid "KEYBOARD" +msgstr "TECLADO" + +#: +msgid "GAMEPAD %i" +msgstr "MANDO %i" + +#. Config controllers missing translation +#: +msgid "PRESS ANYTHING" +msgstr "PULSAR CUALQUIER BOTÓN" + +#: +msgid "ALREADY TAKEN" +msgstr "EN USO" + +#: +msgid "DISCARD CHANGES" +msgstr "DESCARTAR CAMBIOS" + +#: +msgid "WELCOME" +msgstr "BIENVENIDO" + +#: +msgid "CONFIGURE INPUT" +msgstr "CONFIGURAR MANDOS" + +#: +msgid "%i GAMEPAD DETECTED" +msgid_plural "%i GAMEPADS DETECTED" +msgstr[0] "%i MANDO DETECTADO" +msgstr[1] "%i MANDOS DETECTADOS" + +#: +msgid "NO GAMEPADS DETECTED" +msgstr "SIN MANDOS DETECTADOS" + +#: +msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." +msgstr "MANTENER PRESIONADO UN BOTÓN DEL MANDO PARA CONFIGURARLO" + +#: +msgid "PRESS F4 TO QUIT AT ANY TIME." +msgstr "PRESIONAR F4 PARA SALIR EN CUALQUIER MOMENTO" + +#: +msgid "PRESS ESC OR THE HOTKEY TO CANCEL." +msgstr "PRESIONAR ESC O HOTKEY PARA CANCELAR" + +#: +msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" +msgstr "¿INICIAR EL CENTRO MULTIMEDIA KODI?" + +#: +msgid "LOADING..." +msgstr "CARGANDO..." + +#: +msgid "PLEASE WAIT..." +msgstr "ESPERAR..." + +#: +msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" +msgstr "¿APAGAR SIN GUARDAR DATOS?" + +#: +msgid "FAST SHUTDOWN SYSTEM" +msgstr "APAGAR RÃPIDO" + +#. Please change RECALBOX.FR to RECALBOX.COM on the English version! +#: +msgid "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 RECALBOX.FR FOR MORE INFORMATION." +msgstr "¡NO SE HAN ENCONTRADO SISTEMAS!\n" +"COMPROBAR RUTAS EN ARCHIVO DE CONFIGURACIÓN.\n" +"LA CARPETA DE JUEGOS DEBE CONTENER AL MENOS UNO Y CON LA EXTENSIÓN CORRECTA.\n" +"\n" +"VISITAR RECALBOX.COM PARA OBTENER MÃS INFORMACIÓN" + +#: +msgid "ON SCREEN KEYBOARD" +msgstr "TECLADO EN PANTALLA" + +#: +msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" +msgstr "SHIFT PARA MAYÚSCULAS, MINÚSCULAS Y ESPECIALES" + +#: +msgid "SPACE" +msgstr "ESPACIO" + +#: +msgid "DELETE A CHAR" +msgstr "BORRAR UN CARÃCTER" + +#: +msgid "SHIFT" +msgstr "SHIFT" + +#: +msgid "STOP EDITING" +msgstr "TERMINAR EDICIÓN" + +#: +msgid "MOVE CURSOR" +msgstr "MOVER CURSOR" + +#: +msgid "EDIT" +msgstr "EDITAR" + +#: +msgid "ACCEPT RESULT" +msgstr "ACEPTAR RESULTADO" + +#: +msgid "FILENAME" +msgstr "NOMBRE DEL ARCHIVO" + +#: +msgid "RATING" +msgstr "CLASIFICACIÓN" + +#: +msgid "TIMES PLAYED" +msgstr "JUGADO" + +#: +msgid "LAST PLAYED" +msgstr "ÚLTIMA VEZ JUGADO" + +#: +msgid "NUMBER OF PLAYERS" +msgstr "NÚMERO DE JUGADORES" + +#: +msgid "DEVELOPER" +msgstr "DESARROLLADOR" + +#: +msgid "GENRE" +msgstr "GÉNERO" + +#: +msgid "SHOW HIDDEN" +msgstr "MOSTRAR OCULTOS" + +#: +msgid "EXTREM (1400Mhz)" +msgstr "EXTREMO (1400Mhz)" + +#: +msgid "TURBO (1350Mhz)" +msgstr "TURBO (1350Mhz)" + +#: +msgid "HIGH (1300Mhz)" +msgstr "ALTO (1300Mhz)" + +#: +msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" +"IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." +msgstr "ESTABLECER EL OVERCLOCK EN TURBO Y EXTREMO, PUEDE CAUSAR INESTABILIDAD DEL SISTEMA.\n" +"NO USAR SIN ESTAR SEGURO.\n" +"AL CONTINUAR, EL SISTEMA SE REINICIARÃ." + +#: +msgid "%i GAME HIDDEN" +msgid_plural "%i GAMES HIDDEN" +msgstr[0] "%i JUEGO OCULTO" +msgstr[1] "%i JUEGOS OCULTOS" + +#: +msgid "Start kodi media player." +msgstr "Iniciar KODI" + +#: +msgid "Select the language for your recalbox, select an external drive to store your games and configurations, check your current version and the free space on your drive" +msgstr "Seleccionar idioma, seleccionar disco externo para guardar juegos y configuraciones, comprobar versión actual y espacio libre" + +#: +msgid "Shows your current recalboxOS version." +msgstr "Mostrar versión actual de RecalboxOS." + +#: +msgid "Show how much space is used on your SHARE partition, located either on the SDCARD or on an external drive. The information shows how much GB are used and how much GB your storage has overall (example 13GB/26GB)." +msgstr "Mostrar espacio usado en partición SHARE, tanto en SD como en disco externo. La información se muestra en GB usados/total (ejemplo 13GB/26GB)." + +#: +msgid "Select an external drive to store your roms, saves, configurations etc.\n" +"Use a FAT32 formatted drive. The system does not format the drive. On first boot, with this option enabled, recalbox will create a '/recalbox' folder with all system files inside." +msgstr "Seleccionar dispositivo externo para guardar juegos, partidas guardadas, configuraciones y otros.\n" +"Dispositivo requerido en formato FAT32, no será formateado.\n" +"En el siguiente arranque, esta opción crea en el dispositivo externo, una carpeta llamada '/recalbox' con todos los archivos requeridos" + +#: +msgid "Select your language. A reboot is needed to set this configuration active." +msgstr "Seleccionar idioma. Reiniciar para aplicar" + +#: +msgid "Manage your recalbox updates. Select the update type. Activate update check." +msgstr "Gestionar actualizaciones. Seleccionar tipo de actualización. Activar revisión de actualizaciones" + +#: +msgid "Check if an update is available, and start the update process." +msgstr "Revisar si hay actualizaciones disponibles, e iniciar proceso de actualización" + +#. We must change the issues page from github to gitlab on every language! +#: +msgid "Stable updates will check for updates on stable recalbox releases. Stable updates are tested and approved by the recalbox team and their testers.\n" +"Unstable updates allows you to get the latest recalbox features by checking our unstable repository. You can test and validate with us the very last version of recalbox.\n" +"If you choose unstable update, be so kind to report issues on the recalbox-os issue board (https://github.com/recalbox/recalbox-os/issues)" +msgstr "Actualizaciones estables: Las actualizaciones estables se comprueban y aprueban por el equipo de Recalbox y sus testers.\n" +"Actualizaciones inestables: Permiten obtener últimas características de recalbox comprobando nuestro depósito de inestables. Se puede probar y validar con nosotros la última versión de Recalbox.\n" +"Al seleccionar una actualización inestable, por favor reportar los problemas en la plataforma de desarrollo de RecalboxOS (https://gitlab.com/recalbox/recalbox/issues)" + +#. Creo que así queda más parecido al significado original +#: +msgid "Automatically check if an update is avaialble. If so, it notifies you with a message." +msgstr "Comprueba automáticamente si hay alguna actualización disponible. Si es así, se notifica con un mensaje" + +#: +msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." +msgstr "Configurar visualización de juegos, relación de aspecto, filtros (shaders), autoguardado y autocargado, y cuenta de retrologros" + +#: +msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." +msgstr "La relación de aspecto, es la relación entre ancho y alto de imagen.\n" +"Configurar AUTO para que el emulador seleccione la relación de aspecto original del juego, proporcionando la mejor experiencia de retrojuego" + +#: +msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." +msgstr "Suavizar imagen del juego. Esta opción proporciona la imagen más suave, usando filtros bilineales" + +#: +msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." +msgstr "Esta opción permite rebobinar el juego. Usar el botón HOTKEY+IZQUIERDA en el juego para rebobinar" + +#: +msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." +msgstr "Autoguardar la partida al salir del juego, y autocargar la partida al iniciar el juego." + +#: +msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." +msgstr "La escala entera, es el escalado por un factor de un número entero, como 2x, 3x, 4x, etc. Esta opción escala la imagen hasta la escala entera más grande por debajo de la resolución establecida. Por ejemplo, al establecer la resolución de pantalla completa en 1920x1080 y se habilita el escalado entero, sólo escalará una imagen de 320x240 hasta 1280x960 y dejará los bordes negros alrededor. Esto es para mantener una relación de aspecto de píxeles de 1:1 con la imagen original, de modo que los píxeles no se dupliquen de manera desigual" + +#: +msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." +msgstr "Los shaders equivalen a filtros para el renderizado del juego. Puede seleccionar un conjunto de shaders, que es una colección de shaders seleccionados para cada sistema. También se puede cambiar el shader dentro del juego con HOTKEY+L2 o HOTKEY+R2" + +#: +msgid "Enable or disable RetroAchievements in games." +msgstr "Activar o desactivar los retrologros en los juegos" + +#: +msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." +msgstr "El modo hardcore deshabilita toda capacidad de guardar partidas y funciones de rebobinado. Se debe terminar el juego y conseguir logros de una sola vez, igual que en la máquina original. En compensación, se conseguirán tanto los logros estándar y hardcore, y el doble de puntos. Un juego regular de 400 puntos, generará 800 si se completa en hardcore" + +#: +msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" +msgstr "¡La página web retroachievements.org propone desafíos/logros/trofeos en plataformas como NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine y más! ¡Crear cuenta en retroachievements.org y empezar búsqueda de logros!" + +#: +msgid "Add and configure up to 5 controllers." +msgstr "Agregar y configurar hasta 5 mandos" + +#: +msgid "Pair a bluetooth controller with your recalbox. Your controller must be in pairing mode." +msgstr "Vincular mando por bluetooth. El mando debe estar en modo visible" + +#: +msgid "Forget all paired bluetooth controllers. You will have to pair your controllers again, but this option can help if you have issues to reconnect a controller, which is already paired." +msgstr "Olvidar todos los mandos vinculados por bluetooth. Tendrás que vincularlos nuevamente, pero esta opción puede ayudarte si tienes problemas al reconectar un mando que ya está vinculado" + +#: +msgid "Configure your EmulationStation experience. Select transition types, help prompts, screensaver behavior. You can also deactivate the onscreen keyboard if you have a real keyboard plugged into your recalbox.\n" +"If you've added games since the last boot, you can also refresh the gamelist from this menu." +msgstr "Configurar experiencia con EmulationStation. Seleccionar tipos de transición, mensajes de ayuda, comportamiento del salvapantallas. También se puede desactivar el teclado virtual en caso de tener uno de verdad conectado a Recalbox.\n" +"Si se han agregado juegos desde el último arranque, también se puede actualizar la lista de juegos desde este menú" + +#: +msgid "Start the screensaver after N minutes." +msgstr "Iniciar el salvapantallas tras N minutos" + +#: +msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." +msgstr "Configurar el comportamiento del protector de pantalla. DIM reducirá el brillo de pantalla, y BLACK pondrá la pantalla en negro" + +#: +msgid "Shows a help at the bottom of the screen which displays commands you can use." +msgstr "Desplegar una ayuda en la parte inferior de la pantalla que enseña comandos a usar" + +#: +msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." +msgstr "Activado, permite cambiar de sistema pulsando IZQUIERDA o DERECHA en la lista de juegos" + +#: +msgid "The onscreen keyboard is necessary to type text if you only have controllers plugged into your recalbox. You can disable it if you have a real keyboard connected." +msgstr "El teclado virtual es necesario para escribir si existen sólo mandos conectados. Desactivar en caso de tener un teclado real conectado" + +#: +msgid "Select a theme for your recalbox." +msgstr "Seleccionar tema para Recalbox" + +#: +msgid "Updates the gamelists, if you added games since the last boot." +msgstr "Actualizar lista de juegos, en caso de haber agregado juegos desde el último inicio del sistema" + +#: +msgid "Configure the sound options of your recalbox." +msgstr "Configuras las opciones de sonido de Recalbox" + +#: +msgid "Set the volume of the sound output for the frontend and the games." +msgstr "Configurar volumen para salida de audio en interfaz y juegos" + +#: +msgid "Enable or disable the frontend music. You can add your own music as mp3, or ogg format in the 'musics' directory of your recalbox." +msgstr "Activar o desactivar música de interfaz. Permite agregar música propia en formato mp3 u ogg en la carpeta 'musics' de Recalbox" + +#: +msgid "Select your output device. Only HDMI and JACK are supported." +msgstr "Seleccionar dispositivo de salida. Sólo compatible con HDMI y JACK" + +#: +msgid "Configure the network options of your recalbox.\n" +"Check your network status and IP address, set the hostname and configure the WIFI." +msgstr "Configurar opciones de red.\n" +"Comprobar estado de red y dirección IP, establecer nombre de host y configurar WIFI" + +#: +msgid "Displays CONNECTED, if you are connected, by checking if your recalbox can access the recalbox.com update server." +msgstr "Mostrar CONECTADO, comprobando si hay acceso al servidor de actualizaciones" + +#: +msgid "The IP address of your recalbox within your local network." +msgstr "Dirección IP de recalbox" + +#: +msgid "Enable or disable WIFI.\n" +"If you disable WIFI, the SSID and the WIFI passwords are saved and can be used when you reactivate it" +msgstr "Activar o desactivar WIFI.\n" +"Al desactivar WIFI, SSID y contraseñas son guardadas y utilizadas al reactivar" + +#: +msgid "The name of your recalbox in your local network" +msgstr "Nombre de Recalbox en la red de área local" + +#: +msgid "SSID (WIFI Name) of your network." +msgstr "SSID (Nombre del WIFI) de la red" + +#: +msgid "Private key of your WIFI network." +msgstr "Contraseña WIFI" + +#: +msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." +msgstr "Obtener información e imágenes de juegos. El scraper descarga información e imágenes de juegos desde diferentes servidores y mejora la experiencia de usuario completamente en EmulationStation" + +#: +msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." +msgstr "Seleccionar un servidor desde donde descargar información. Se recomienda el servidor SCREENSCRAPER que está basado en www.screenscraper.fr y descarga datos de juegos en tu idioma, si está disponible" + +#: +msgid "Begin the scrape process with the configuration shown below." +msgstr "Comienza el proceso de descarga con la configuración siguiente" + +#: +msgid "Scrape and display game ratings." +msgstr "Descargar y mostrar las puntuaciones del juego" + +#: +msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." +msgstr "Configuraciones avanzadas. Asegúrate de que sabes lo que haces antes de cambiar cualquier valor en este menú" + +#: +msgid "Overclock your board to increase the performance.\n" +"Overclock settings are tested and validated by the community. Keep in mind that overclocking your board can void your warranty." +msgstr "Aumentar la velocidad del reloj para incrementar el rendimiento.\n" +"Estas configuraciones están probadas y validadas por la comunidad.\n" +"Considerar que al aumentar la velocidad del reloj es posible perder la garantía" + +#: +msgid "Select which system to show when the recalbox frontend starts. The default value is 'favorites'." +msgstr "Seleccionar qué sistema se muestra cuando empieza la interfaz del Recalbox. El valor por defecto es 'favoritos'" + +#: +msgid "On boot, recalbox will show the list of games of the selected system rather than the system view." +msgstr "En el arranque, Recalbox mostrará la lista de juegos del sistema seleccionado, no la vista de todos sistemas" + +#: +msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" +"This option highly speeds up boot time, but new games will not be detected." +msgstr "Mostrar sólo los juegos que están en el archivo gamelist.xml (situado en las carpetas de los ROMs).\n" +"Esta opción acelera el tiempo de arranque, pero no detectará nuevos juegos" + +#: +msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." +msgstr "Esta opción te permite establecer el sistema seleccionado en modo fijo. Con esta opción activada, el usuario no puede acceder a otros sistemas" + +#: +msgid "Always display the basic gamelist view, even if you have scraped your games." +msgstr "Mostrar siempre la lista de juegos básica, incluso si has scrapeado tus juegos" + +#: +msgid "Override global options like emulator, core, ratio and more for each available system in your recalbox." +msgstr "Anula opciones globales tales como emulador, núcleo, relación de aspecto y más, para cada sistema disponible" + +#: +msgid "Configure boot options that make your recalbox boot straight into a system or into Kodi, lock a user to a single system, or directly show the gamelist." +msgstr "Configurar las opciones de arranque para que Recalbox inicie directamente en un sistema definido, iniciar en Kodi, bloquear un usuario para un solo sistema, o directamente mostrar la lista de juegos" + +#: +msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" +msgstr "Activar o desactivar KODI, personalizar el arranque de KODI, activar el botón X para iniciar KODI" + +#: +msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." +msgstr "Activa o desactiva Kodi. Si Kodi está desactivado, no podrá ser iniciado con el botón X, o automáticamente al arrancar. El menú de entrada será desactivado" + +#: +msgid "Use the X button to start Kodi." +msgstr "Usar el botón X para iniciar Kodi" + +#: +msgid "Automatically start into Kodi on boot." +msgstr "Iniciar Kodi automáticamente en el arranque" + +#: +msgid "Manage your recalbox security." +msgstr "Administrar la seguridad de Recalbox" + +#: +msgid "Change the SSH root password." +msgstr "Cambiar la contraseña root SSH" + +#: +msgid "Enforce recalbox security." +msgstr "Reforzar la seguridad de Recalbox" + +#. Ordenador? Computador? Computadora? ...en español existen varias acepciones para este tipo de dispositivo (Windows en español lo define como "PC" simplemente, ahora en Windows 10, utiizan el término "Equipo"... Windows 98/Me/2000/XP se refería a "sistema") +#: +msgid "Enable or disable overscan.\n" +"Overscan can help you, if you have a black border, or if the image is bigger than your screen. Before setting the overscan, try to configure your TV to have a 1:1 pixel output.\n" +"More overscan settings can be defined in the boot.txt file, available when you plug your SD card into your computer." +msgstr "Activar o desactivar overscan.\n" +"El overscan puede ayudar al presentar bordes negros, o en caso de imagen de mayor tamaño que la pantalla.\n" +"Antes de configurar overscan, intentar configurar la TV para tener una salida de píxeles 1:1.\n" +"Es posible definir más configuraciones de overscan en el archivo boot.txt, disponible al insertar la tarjeta SD en la computadora" + +#: +msgid "Show the framerate in EmulationStation and in game." +msgstr "Mostrar cuadros por segundo en EmulationStation y en el juego" + +#: +msgid "Enable or disable the Recalbox Manager.\n" +"The Recalbox Manager is a web application available on http://recalbox , if you are on windows, http://recalbox.local , if you are on Linux or Mac, or directly with your recalbox IP : http://192.168.1.XX.\n" +"You can configure many options from within the manager, and even manage games, saves, and scrapes!" +msgstr "Activar o desactivar el Administrador de Recalbox.\n" +"El Administrador de Recalbox es una aplicación web disponible en http://recalbox, en Windows, http://recalbox.local en Linux o Mac, o directamente con la IP de Recalbox: http://192.168.1.XX.\n" +"¡Es posible configurar muchas opciones desde el administrador, incluso juegos, partidas guardadas e información!" + +#: +msgid "Enable or disable the recalbox API.\n" +"The Recalbox API is a REST API exposing endpoints to control your recalbox via http requests." +msgstr "Activar o desactivar la API de Recalbox.\n" +"La API de Recalbox es una API REST que expone puntos finales para controlar Recalbox a través de solicitudes HTTP" + +#: +msgid "Select which emulator to use when you start a game for this system." +msgstr "Seleccionar qué emulador usar cuando se inicia un juego de este sistema" + +#: +msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." +msgstr "Seleccionar qué núcleo usar para el emulador seleccionado. Por ejemplo, el emulador LIBRETRO tiene varios núcleos para ejecutar juegos de Super Nintendo. La configuración del núcleo por defecto que se elija, también puede ser eliminada desde la configuración específica para juegos" + +#: +msgid "USE COMPOSED VISUALS" +msgstr "USAR IMÃGENES COMPUESTAS" + +#: +msgid "CHECK UPDATES" +msgstr "COMPROBAR ACTUALIZACIONES" + +#: +msgid "UPDATE TYPE" +msgstr "TIPO DE ACTUALIZACIÓN" + +#: +msgid "INTEGER SCALE (PIXEL PERFECT)" +msgstr "ESCALA COMPLETA (PIXEL PERFECT)" + +#: +msgid "ADVANCED SETTINGS" +msgstr "OPCIONES AVANZADAS" + +#: +msgid "BOOT SETTINGS" +msgstr "OPCIONES DE ARRANQUE" + +#: +msgid "GAMELIST ONLY" +msgstr "SÓLO LISTA DE JUEGOS" + +#: +msgid "BOOT ON SYSTEM" +msgstr "ARRANCAR EN SISTEMA" + +#: +msgid "BOOT ON GAMELIST" +msgstr "ARRANCAR EN LISTA DE JUEGOS" + +#: +msgid "HIDE SYSTEM VIEW" +msgstr "OCULTAR VISTA DE SISTEMA" + +#: +msgid "EMULATOR ADVANCED CONFIGURATION" +msgstr "CONFIGURACIÓN AVANZADA DEL EMULADOR" + +#: +msgid "ADVANCED EMULATOR CONFIGURATION" +msgstr "CONFIGURACIÓN DEL EMULADOR AVANZADA" + +#: +msgid "HELP" +msgstr "AYUDA" + +#: +msgid "THE SYSTEM IS UP TO DATE" +msgstr "EL SISTEMA ESTà ACTUALIZADO" + +#: +msgid "FORCE BASIC GAMELIST VIEW" +msgstr "FORZAR VISTA DE LISTA DE JUEGOS BÃSICA" + +#: +msgid "DOWNLOADED" +msgstr "DESCARGADO" + +#: +msgid "UPDATE VERSION:" +msgstr "VERSIÓN DE LA ACTUALIZACIÓN:" + +#: +msgid "UPDATE CHANGELOG:" +msgstr "REGISTRO DE CAMBIOS DE LA ACTUALIZACIÓN:" + +#: +msgid "MORE DETAILS" +msgstr "MÃS DETALLES" + +#: +msgid "CAROUSEL ANIMATION" +msgstr "ANIMACIÓN DEL CARRUSEL" + +#: +msgid "THEME CONFIGURATION" +msgstr "CONFIGURACIÓN DEL TEMA" + +#: +msgid "THEME COLORSET" +msgstr "CONJUNTO DE COLORES DEL TEMA" + +#: +msgid "THEME ICONSET" +msgstr "CONJUNTO DE ÃCONOS DEL TEMA" + +#: +msgid "THEME MENU" +msgstr "MENÚ DEL TEMA" + +#: +msgid "THEME SYSTEMVIEW" +msgstr "VISTA DE SISTEMAS DEL TEMA" + +#: +msgid "THEME GAMELISTVIEW" +msgstr "VISTA DE LISTA DE JUEGOS DEL TEMA" + +#: +msgid "THEME REGION" +msgstr "REGIÓN DEL TEMA" + +#: +msgid "THIS THEME HAS NO OPTION" +msgstr "ESTE TEMA NO TIENE OPCIONES" + +#: +msgid "MANUAL INPUT" +msgstr "ENTRADA MANUAL" + +#: +msgid "AN ERROR OCCURED - DOWNLOADED" +msgstr "HA HABIDO UN ERROR - DESCARGADO" + +#: +msgid "START KODI" +msgstr "INICIAR KODI" + +#: +msgid "Shows the current available update version." +msgstr "Mostrar la versión de actualización disponible actualmente" + +#: +msgid "Shows the current available update changelog." +msgstr "Mostrar el registro de cambios de actualización disponible actualmente" + +#: +msgid "Configure an associated controller. Your controller has to be associated / plugged before." +msgstr "Configurar mando conectado. El mando debe estar previamente vinculado/conectado" + +#: +msgid "Choose if carousel will be animated or not during transitions" +msgstr "Elegir si el carrusel estará animado o no durante transiciones" + +#: +msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" +msgstr "Seleccionar el tipo de transición que ocurre cuando comienza un juego. INSTANT no hará nada, FADE se desvanecerá a oscuro, y SLIDE hará un zoom en la cubierta del juego (o nombre, si no hay metadatos)" + +#: +msgid "Select exisiting colorset options for this theme." +msgstr "Seleccionar opciones de conjuntos de colores existentes para este tema" + +#: +msgid "Select exisiting iconset options for this theme." +msgstr "Seleccionar opciones de conjunto de iconos existentes para este tema" + +#: +msgid "Select exisiting menu style options for this theme." +msgstr "Seleccionar opciones de estilo de menú existentes para este tema" + +#: +msgid "Select exisiting system view options for this theme." +msgstr "Seleccionar opciones de vista de sistema existentes para este tema" + +#: +msgid "Select exisiting gamelist view options for this theme." +msgstr "Seleccionar opciones de vista de lista de juegos existentes para este tema" + +#: +msgid "Configure theme options if available." +msgstr "Configurar opciones del tema si está disponible" + +#: +msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" +msgstr "Seleccionar región de logotipos, imágenes para sistemas diferentes entre países Ejemplo, Megadrive en EU, Genesis en USA" + +#: +msgid "Type the name of your SSID if it is hidden or not listed" +msgstr "Ingresar nombre de SSID, si está oculto o no aparece" + +#: +msgid "Select a letter and the listing will go directly on the first game starting with this letter." +msgstr "Seleccionar una letra y la lista irá directamente al primer juego que comience con esta letra" + +#: +msgid "Select the way the game list is sortered (alphabetically, by notation...)." +msgstr "Seleccionar la forma en que se ordena la lista de juegos (alfabéticamente, mediante notación...)" + +#: +msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." +msgstr "Cambiar entre ver o no solo los juegos favoritos. Para agregar un juego a la lista de favoritos, seleccionar el juego y cambiar su estado con 'Y'" + +#: +msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." +msgstr "Cambiar entre ver o no los juegos ocultos. Para ocultar un juego, editar sus datos y seleccionar 'Ocultar'" + +#: +msgid "This option display a menu which allows to change game data and many others options." +msgstr "Esta opción muestra un menú que permite cambiar los datos del juego y muchas otras opciones" + +#: +msgid "AVAILABLE UPDATE" +msgstr "ACTUALIZACIÓN DISPONIBLE" + +#: +msgid "UPDATE CHANGELOG" +msgstr "REGISTRO DE CAMBIOS" + +#: +msgid "CLOCK IN MENU" +msgstr "RELOJ EN MENÚ" + +#: +msgid "Now playing" +msgstr "Reproduciendo ahora" + +#: +msgid "DEFAULT (%1%)" +msgstr "DEFECTO (%1%)" + +#: +msgid "INPUT REQUIRED" +msgstr "INGRESO REQUERIDO" + +#: +msgid "(skipped)" +msgstr "(omitido)" + +#: +msgid "UP/DOWN TO SKIP" +msgstr "ARRIBA/ABAJO PARA OMITIR" + +#. Explanation: you config the controller, you make a mistake (let's say right instead of left), you can press A to erase wrong config and redo +#: +msgid "A TO UNSET" +msgstr "A PARA CAMBIAR" + +#: +msgid "DOWN TO SKIP AND KEEP [%1%]" +msgstr "ABAJO PARA OMITIR Y MANTENER [%1%]" + +#: +msgid "UP/DOWN TO SKIP AND KEEP [%1%]" +msgstr "ARRIBA/ABAJO PARA OMITIR Y MANTENER [%1%]" + +#: +msgid "Set duration of help popups, 0 means no popup." +msgstr "Duración de popups de ayuda, 0 significa sin popup" + +#: +msgid "HELP POPUP DURATION" +msgstr "DURACIÓN DE POPUPS DE AYUDA" + +#: +msgid "Set duration of music popups, 0 means no popup." +msgstr "Duración de popups de música, 0 significa sin popup" + +#: +msgid "MUSIC POPUP DURATION" +msgstr "DURACIÓN DE POPUPS DE MÚSICA" + +#: +msgid "POPUP SETTINGS" +msgstr "CONFIGURACIÓN DE POPUPS" + +#: +msgid "POPUP POSITION" +msgstr "POSICIÓN DE POPUPS" + +#: +msgid "Select the position of popups on screen." +msgstr "Seleccionar posición de popups en pantalla" + +#: +msgid "Set position and duration of popups." +msgstr "Posición y duración de popups" + +#: +msgid "TOP/RIGHT" +msgstr "ARRIBA/DERECHA" + +#: +msgid "BOTTOM/RIGHT" +msgstr "ABAJO/DERECHA" + +#: +msgid "BOTTOM/LEFT" +msgstr "ABAJO/IZQUIERDA" + +#: +msgid "TOP/LEFT" +msgstr "ARRIBA/IZQUIERDA" + +#: +msgid "SHOW FOLDERS CONTENT" +msgstr "MOSTRAR CONTENIDO DE CARPETAS" + +#: +msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." +msgstr "Cambiar entre mostrar estructura de carpetas o ver todos los juegos en un único nivel superior" + +#: +msgid "NETPLAY" +msgstr "NETPLAY" + +#: +msgid "NETPLAY SETTINGS" +msgstr "CONFIGURACIÓN DE NETPLAY" + +#: +msgid "NETPLAY LOBBY" +msgstr "SALA DE ESPERA DE NETPLAY" + +#: +msgid "Enable or disable Netplay in games." +msgstr "Habilitar o deshabilitar Netplay en juegos" + +#: +msgid "PORT" +msgstr "PUERTO" + +#: +msgid "NICKNAME" +msgstr "ALIAS" + +#: +msgid "RELAY SERVER" +msgstr "SERVIDOR DE RETRANSMISIÓN" + +#: +msgid "Enable or disable connections throught relay servers." +msgstr "Habilitar o deshabilitar conexiones mediante servidores de retransmisión" + +#: +msgid "KODI/NETPLAY" +msgstr "KODI/NETPLAY" + +#: +msgid "NO GAMES OR NO CONNECTION" +msgstr "SIN JUEGOS O SIN CONEXIÓN" + +#: +msgid "HASH NOW" +msgstr "CALCULAR HASH AHORA" + +#: +msgid "HASH THESE SYSTEMS" +msgstr "CALCULAR HASH PARA ESTOS SISTEMAS" + +#: +msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." +msgstr "Agregar hash de roms a listas de juegos para obtener resultados más precisos en Netplay." + +#: +msgid "HASH ROMS" +msgstr "CALCULAR HASH DE ROMS" + +#: +msgid "Only missing hashs" +msgstr "Solo hash faltantes" + +#: +msgid "Username" +msgstr "Usuario" + +#: +msgid "Country" +msgstr "País" + +#: +msgid "Latency" +msgstr "Latencia" + +#: +msgid "Host arch." +msgstr "Arq. del anfitrión" + +#: +msgid "Core ver." +msgstr "Ver. núcleo" + +#: +msgid "RA ver." +msgstr "Ver. RA" + +#: +msgid "Can join" +msgstr "Puede unirse" + +#: +msgid "Rom and core match" +msgstr "Rom y núcleo coincidente" + +#: +msgid "Rom found" +msgstr "Rom encontrado" + +#: +msgid "No rom match" +msgstr "Rom sin coincidencia" + +#: +msgid "Match" +msgstr "Coincidencia" + +#: +msgid "No Match" +msgstr "Sin coincidencia" + +#: +msgid "Rom file" +msgstr "Archivo rom" + +#: +msgid "Rom hash" +msgstr "Hash del rom" + +#: +msgid "THIS COULD TAKE A WHILE, CONFIRM?" +msgstr "Esto podría tardar, ¿confirmar?" + +#: +msgid "good" +msgstr "bueno" + +#: +msgid "bad" +msgstr "malo" + +#: +msgid "medium" +msgstr "medio" + +#: +msgid "NETPLAY POPUP DURATION" +msgstr "DURACIÓN POPUPS PARA NETPLAY" + +#: +msgid "Set duration of netplay popups, 0 means no popup." +msgstr "Duración de popups para Netplay, 0 significa sin popups" + +#: +msgid "Player" +msgstr "Jugador" + +#: +msgid "Game" +msgstr "Juego" + +#: +msgid "A Recalbox friend has started a Netplay game!" +msgstr "!Un amigo de Recalbox ha iniciado un juego en Netplay!" + +#: +msgid "Play online on games running through Retroarch like NES, SNES, FBA, Genesis/Megadrive and more!" +msgstr "¡Juega en línea en juegos que funcionan con RetroArch como NES, SNES, FBA, Genesis/Megadrive y más!" + +#: +msgid "Rom, hash and core match" +msgstr "Rom, hash y núcleo coincidente" + +#: +msgid "No core match" +msgstr "Núcleo sin coincidencia" + +#: +msgid "Add a clock in the main menu." +msgstr "" + From a91ae44a4df2eb04809f65f0ad01f1adf422f724 Mon Sep 17 00:00:00 2001 From: fabricecaruso <51082152+fabricecaruso@users.noreply.github.com> Date: Mon, 27 May 2019 14:06:41 +0200 Subject: [PATCH 011/336] Update README.md --- README.md | 446 +++++++++++++++++++++++++++++------------------------- 1 file changed, 242 insertions(+), 204 deletions(-) diff --git a/README.md b/README.md index 49ff0c5338..52e38b50b0 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,141 @@ -EmulationStation -================ - -This is a fork of EmulationStation for RetroPie. -EmulationStation is a cross-platform graphical front-end for emulators with controller navigation. - -Building -======== - -EmulationStation uses some C++11 code, which means you'll need to use at least g++-4.7 on Linux, or VS2010 on Windows, to compile. - -EmulationStation has a few dependencies. For building, you'll need CMake, SDL2, FreeImage, FreeType, cURL and RapidJSON. You also should probably install the `fonts-droid` package which contains fallback fonts for Chinese/Japanese/Korean characters, but ES will still work fine without it (this package is only used at run-time). - -**On Debian/Ubuntu:** -All of this be easily installed with `apt-get`: -```bash -sudo apt-get install libsdl2-dev libfreeimage-dev libfreetype6-dev libcurl4-openssl-dev rapidjson-dev \ - libasound2-dev libgl1-mesa-dev build-essential cmake fonts-droid-fallback libvlc-dev \ - libvlccore-dev vlc-bin -``` -**On Fedora:** -All of this be easily installed with `dnf` (with rpmfusion activated) : -```bash -sudo dnf install SDL2-devel freeimage-devel freetype-devel curl-devel \ - alsa-lib-devel mesa-libGL-devel cmake \ - vlc-devel rapidjson-devel -``` - -Note this Repository uses a git submodule - to checkout the source and all submodules, use - -```bash -git clone --recursive https://github.com/RetroPie/EmulationStation.git -``` - -or - -```bash -git clone https://github.com/RetroPie/EmulationStation.git -cd EmulationStation -git submodule update --init -``` - -Then, generate and build the Makefile with CMake: -```bash -cd YourEmulationStationDirectory -cmake . -make -``` - -**On the Raspberry Pi:** - -Complete Raspberry Pi build instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#install_rpi_standalone). - -**On Windows:** - -[FreeImage](http://downloads.sourceforge.net/freeimage/FreeImage3154Win32.zip) - -[FreeType2](http://download.savannah.gnu.org/releases/freetype/freetype-2.4.9.tar.bz2) (you'll need to compile) - -[SDL2](http://www.libsdl.org/release/SDL2-devel-2.0.8-VC.zip) - -[cURL](http://curl.haxx.se/download.html) (you'll need to compile or get the pre-compiled DLL version) - -[RapisJSON](https://github.com/tencent/rapidjson) (you'll need the `include/rapidsjon` added to the include path) - -(Remember to copy necessary .DLLs into the same folder as the executable: probably FreeImage.dll, freetype6.dll, SDL2.dll, libcurl.dll, and zlib1.dll. Exact list depends on if you built your libraries in "static" mode or not.) - -[CMake](http://www.cmake.org/cmake/resources/software.html) (this is used for generating the Visual Studio project) - -(If you don't know how to use CMake, here are some hints: run cmake-gui and point it at your EmulationStation folder. Point the "build" directory somewhere - I use EmulationStation/build. Click configure, choose "Visual Studio [year] Project", fill in red fields as they appear and keep clicking Configure (you may need to check "Advanced"), then click Generate.) - - -Configuring -=========== - -**~/.emulationstation/es_systems.cfg:** -When first run, an example systems configuration file will be created at `~/.emulationstation/es_systems.cfg`. `~` is `$HOME` on Linux, and `%HOMEPATH%` on Windows. This example has some comments explaining how to write the configuration file. See the "Writing an es_systems.cfg" section for more information. - -**Keep in mind you'll have to set up your emulator separately from EmulationStation!** - -**~/.emulationstation/es_input.cfg:** -When you first start EmulationStation, you will be prompted to configure an input device. The process is thus: - -1. Hold a button on the device you want to configure. This includes the keyboard. - -2. Press the buttons as they appear in the list. Some inputs can be skipped by holding any button down for a few seconds (e.g. page up/page down). - -3. You can review your mappings by pressing up and down, making any changes by pressing A. - -4. Choose "SAVE" to save this device and close the input configuration screen. - -The new configuration will be added to the `~/.emulationstation/es_input.cfg` file. - -**Both new and old devices can be (re)configured at any time by pressing the Start button and choosing "CONFIGURE INPUT".** From here, you may unplug the device you used to open the menu and plug in a new one, if necessary. New devices will be appended to the existing input configuration file, so your old devices will remain configured. - -**If your controller stops working, you can delete the `~/.emulationstation/es_input.cfg` file to make the input configuration screen re-appear on next run.** - - -You can use `--help` or `-h` to view a list of command-line options. Briefly outlined here: -``` +EmulationStation +================ + +This is a fork of EmulationStation from jrassa. +EmulationStation is a cross-platform graphical front-end for emulators with controller navigation. + +Changes in my branch +==================== + +- Support for Multiple Emulators/Cores in es_systems.cfg, and setting Emulator/Core per game. + + %HOME%\RetroArch\retroarch.exe -L %HOME%\RetroArch\cores\%CORE%_libretro.dll %ROM% + + + + mame2003_plus + mame2003 + + + + + fbalpha2012 + + + + +- Better Grid view (animations, layout, selection...) +- Created a 'GridEx' mode, which inherits from grid, in order to create a 'preview bar' including a game screenshot. +- Changed 'Automatic' view mode. Switchs to grid if "thumbnails" exist in gamelist. Switchs to gridex if "thumbnails" and "image" exist in gamelist. +- Don't show Directories that contains only one Game : just Show the game. +- Don't show Games what are marked Hidden in gamelist. +- Cleaned menus + changed menu item order (by interest). Simplified "Quit" menu item ( no more popup ) +- Case insensitive file extensions. +- Skip parsing 'downloaded_images' and 'media' folders ( loading time boost ) +- Localisation (French actually supported) +- Corrected favorites ( and custom lists ) management. +- Don't load all fields in Medadata Editor ( too tricky to use on windows, better use an external tool ). +- Windows is now "Windowed No border" by default. On Windows, Exclusive fullscreen can be annoying... +- Theming : Ability to force default view ( attribute defaultView ) +- Windows : Stop using _wsystem for launching games. Run games with ShellExecuteEx instead ( avoids command window ) +- Add an option to leave ES open with a black screen" Chargement en cours..." when launching games ( avoids showing windows desktop ) + +Je crois que c'est à peu près tout.... + +Building +======== + +EmulationStation uses some C++11 code, which means you'll need to use at least g++-4.7 on Linux, or VS2010 on Windows, to compile. + +EmulationStation has a few dependencies. For building, you'll need CMake, SDL2, FreeImage, FreeType, cURL and RapidJSON. You also should probably install the `fonts-droid` package which contains fallback fonts for Chinese/Japanese/Korean characters, but ES will still work fine without it (this package is only used at run-time). + +**On Debian/Ubuntu:** +All of this be easily installed with `apt-get`: +```bash +sudo apt-get install libsdl2-dev libfreeimage-dev libfreetype6-dev libcurl4-openssl-dev rapidjson-dev \ + libasound2-dev libgl1-mesa-dev build-essential cmake fonts-droid-fallback libvlc-dev \ + libvlccore-dev vlc-bin +``` +**On Fedora:** +All of this be easily installed with `dnf` (with rpmfusion activated) : +```bash +sudo dnf install SDL2-devel freeimage-devel freetype-devel curl-devel \ + alsa-lib-devel mesa-libGL-devel cmake \ + vlc-devel rapidjson-devel +``` + +Note this Repository uses a git submodule - to checkout the source and all submodules, use + +```bash +git clone --recursive https://github.com/RetroPie/EmulationStation.git +``` + +or + +```bash +git clone https://github.com/RetroPie/EmulationStation.git +cd EmulationStation +git submodule update --init +``` + +Then, generate and build the Makefile with CMake: +```bash +cd YourEmulationStationDirectory +cmake . +make +``` + +**On the Raspberry Pi:** + +Complete Raspberry Pi build instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#install_rpi_standalone). + +**On Windows:** + +[FreeImage](http://downloads.sourceforge.net/freeimage/FreeImage3154Win32.zip) + +[FreeType2](http://download.savannah.gnu.org/releases/freetype/freetype-2.4.9.tar.bz2) (you'll need to compile) + +[SDL2](http://www.libsdl.org/release/SDL2-devel-2.0.8-VC.zip) + +[cURL](http://curl.haxx.se/download.html) (you'll need to compile or get the pre-compiled DLL version) + +[RapisJSON](https://github.com/tencent/rapidjson) (you'll need the `include/rapidsjon` added to the include path) + +(Remember to copy necessary .DLLs into the same folder as the executable: probably FreeImage.dll, freetype6.dll, SDL2.dll, libcurl.dll, and zlib1.dll. Exact list depends on if you built your libraries in "static" mode or not.) + +[CMake](http://www.cmake.org/cmake/resources/software.html) (this is used for generating the Visual Studio project) + +(If you don't know how to use CMake, here are some hints: run cmake-gui and point it at your EmulationStation folder. Point the "build" directory somewhere - I use EmulationStation/build. Click configure, choose "Visual Studio [year] Project", fill in red fields as they appear and keep clicking Configure (you may need to check "Advanced"), then click Generate.) + + +Configuring +=========== + +**~/.emulationstation/es_systems.cfg:** +When first run, an example systems configuration file will be created at `~/.emulationstation/es_systems.cfg`. `~` is `$HOME` on Linux, and `%HOMEPATH%` on Windows. This example has some comments explaining how to write the configuration file. See the "Writing an es_systems.cfg" section for more information. + +**Keep in mind you'll have to set up your emulator separately from EmulationStation!** + +**~/.emulationstation/es_input.cfg:** +When you first start EmulationStation, you will be prompted to configure an input device. The process is thus: + +1. Hold a button on the device you want to configure. This includes the keyboard. + +2. Press the buttons as they appear in the list. Some inputs can be skipped by holding any button down for a few seconds (e.g. page up/page down). + +3. You can review your mappings by pressing up and down, making any changes by pressing A. + +4. Choose "SAVE" to save this device and close the input configuration screen. + +The new configuration will be added to the `~/.emulationstation/es_input.cfg` file. + +**Both new and old devices can be (re)configured at any time by pressing the Start button and choosing "CONFIGURE INPUT".** From here, you may unplug the device you used to open the menu and plug in a new one, if necessary. New devices will be appended to the existing input configuration file, so your old devices will remain configured. + +**If your controller stops working, you can delete the `~/.emulationstation/es_input.cfg` file to make the input configuration screen re-appear on next run.** + + +You can use `--help` or `-h` to view a list of command-line options. Briefly outlined here: +``` --resolution [width] [height] try and force a particular resolution --gamelist-only skip automatic game search, only read from gamelist.xml --ignore-gamelist ignore the gamelist (useful for troubleshooting) @@ -112,107 +150,107 @@ You can use `--help` or `-h` to view a list of command-line options. Briefly out --force-kid Force the UI mode to be Kid --force-kiosk Force the UI mode to be Kiosk --force-disable-filters Force the UI to ignore applied filters in gamelist ---help, -h summon a sentient, angry tuba -``` - -As long as ES hasn't frozen, you can always press F4 to close the application. - - -Writing an es_systems.cfg -========================= - -Complete configuration instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#config). - -The `es_systems.cfg` file contains the system configuration data for EmulationStation, written in XML. This tells EmulationStation what systems you have, what platform they correspond to (for scraping), and where the games are located. - -ES will check two places for an es_systems.cfg file, in the following order, stopping after it finds one that works: -* `~/.emulationstation/es_systems.cfg` -* `/etc/emulationstation/es_systems.cfg` - -The order EmulationStation displays systems reflects the order you define them in. - -**NOTE:** A system *must* have at least one game present in its "path" directory, or ES will ignore it! If no valid systems are found, ES will report an error and quit! - -Here's an example es_systems.cfg: - -```xml - - - - - - - snes - - - Super Nintendo Entertainment System - - - ~/roms/snes - - - .smc .sfc .SMC .SFC - - - snesemulator %ROM% - - - - snes - - - snes - - -``` - -The following "tags" are replaced by ES in launch commands: - -`%ROM%` - Replaced with absolute path to the selected ROM, with most Bash special characters escaped with a backslash. - -`%BASENAME%` - Replaced with the "base" name of the path to the selected ROM. For example, a path of "/foo/bar.rom", this tag would be "bar". This tag is useful for setting up AdvanceMAME. - -`%ROM_RAW%` - Replaced with the unescaped, absolute path to the selected ROM. If your emulator is picky about paths, you might want to use this instead of %ROM%, but enclosed in quotes. - -See [SYSTEMS.md](SYSTEMS.md) for some live examples in EmulationStation. - -gamelist.xml -============ - -The gamelist.xml file for a system defines metadata for games, such as a name, image (like a screenshot or box art), description, release date, and rating. - -If at least one game in a system has an image specified, ES will use the detailed view for that system (which displays metadata alongside the game list). - -*You can use ES's [scraping](http://en.wikipedia.org/wiki/Web_scraping) tools to avoid creating a gamelist.xml by hand.* There are two ways to run the scraper: - -* **If you want to scrape multiple games:** press start to open the menu and choose the "SCRAPER" option. Adjust your settings and press "SCRAPE NOW". -* **If you just want to scrape one game:** find the game on the game list in ES and press select. Choose "EDIT THIS GAME'S METADATA" and then press the "SCRAPE" button at the bottom of the metadata editor. - -You can also edit metadata within ES by using the metadata editor - just find the game you wish to edit on the gamelist, press Select, and choose "EDIT THIS GAME'S METADATA." - -A command-line version of the scraper is also provided - just run emulationstation with `--scrape` *(currently broken)*. - -The switch `--ignore-gamelist` can be used to ignore the gamelist and force ES to use the non-detailed view. - -If you're writing a tool to generate or parse gamelist.xml files, you should check out [GAMELISTS.md](GAMELISTS.md) for more detailed documentation. - - -Themes -====== - -By default, EmulationStation looks pretty ugly. You can fix that. If you want to know more about making your own themes (or editing existing ones), read [THEMES.md](THEMES.md)! - -I've put some themes up for download on my EmulationStation webpage: http://aloshi.com/emulationstation#themes - -If you're using RetroPie, you should already have a nice set of themes automatically installed! - - --Alec "Aloshi" Lofquist -http://www.aloshi.com -http://www.emulationstation.org +--help, -h summon a sentient, angry tuba +``` + +As long as ES hasn't frozen, you can always press F4 to close the application. + + +Writing an es_systems.cfg +========================= + +Complete configuration instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#config). + +The `es_systems.cfg` file contains the system configuration data for EmulationStation, written in XML. This tells EmulationStation what systems you have, what platform they correspond to (for scraping), and where the games are located. + +ES will check two places for an es_systems.cfg file, in the following order, stopping after it finds one that works: +* `~/.emulationstation/es_systems.cfg` +* `/etc/emulationstation/es_systems.cfg` + +The order EmulationStation displays systems reflects the order you define them in. + +**NOTE:** A system *must* have at least one game present in its "path" directory, or ES will ignore it! If no valid systems are found, ES will report an error and quit! + +Here's an example es_systems.cfg: + +```xml + + + + + + + snes + + + Super Nintendo Entertainment System + + + ~/roms/snes + + + .smc .sfc .SMC .SFC + + + snesemulator %ROM% + + + + snes + + + snes + + +``` + +The following "tags" are replaced by ES in launch commands: + +`%ROM%` - Replaced with absolute path to the selected ROM, with most Bash special characters escaped with a backslash. + +`%BASENAME%` - Replaced with the "base" name of the path to the selected ROM. For example, a path of "/foo/bar.rom", this tag would be "bar". This tag is useful for setting up AdvanceMAME. + +`%ROM_RAW%` - Replaced with the unescaped, absolute path to the selected ROM. If your emulator is picky about paths, you might want to use this instead of %ROM%, but enclosed in quotes. + +See [SYSTEMS.md](SYSTEMS.md) for some live examples in EmulationStation. + +gamelist.xml +============ + +The gamelist.xml file for a system defines metadata for games, such as a name, image (like a screenshot or box art), description, release date, and rating. + +If at least one game in a system has an image specified, ES will use the detailed view for that system (which displays metadata alongside the game list). + +*You can use ES's [scraping](http://en.wikipedia.org/wiki/Web_scraping) tools to avoid creating a gamelist.xml by hand.* There are two ways to run the scraper: + +* **If you want to scrape multiple games:** press start to open the menu and choose the "SCRAPER" option. Adjust your settings and press "SCRAPE NOW". +* **If you just want to scrape one game:** find the game on the game list in ES and press select. Choose "EDIT THIS GAME'S METADATA" and then press the "SCRAPE" button at the bottom of the metadata editor. + +You can also edit metadata within ES by using the metadata editor - just find the game you wish to edit on the gamelist, press Select, and choose "EDIT THIS GAME'S METADATA." + +A command-line version of the scraper is also provided - just run emulationstation with `--scrape` *(currently broken)*. + +The switch `--ignore-gamelist` can be used to ignore the gamelist and force ES to use the non-detailed view. + +If you're writing a tool to generate or parse gamelist.xml files, you should check out [GAMELISTS.md](GAMELISTS.md) for more detailed documentation. + + +Themes +====== + +By default, EmulationStation looks pretty ugly. You can fix that. If you want to know more about making your own themes (or editing existing ones), read [THEMES.md](THEMES.md)! + +I've put some themes up for download on my EmulationStation webpage: http://aloshi.com/emulationstation#themes + +If you're using RetroPie, you should already have a nice set of themes automatically installed! + + +-Alec "Aloshi" Lofquist +http://www.aloshi.com +http://www.emulationstation.org From 35ea1553b08931a8e8806f41401f18252b20dce3 Mon Sep 17 00:00:00 2001 From: fabricecaruso <51082152+fabricecaruso@users.noreply.github.com> Date: Mon, 27 May 2019 14:10:32 +0200 Subject: [PATCH 012/336] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 52e38b50b0..b4c9a4f011 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Changes in my branch - Support for Multiple Emulators/Cores in es_systems.cfg, and setting Emulator/Core per game. +```xml %HOME%\RetroArch\retroarch.exe -L %HOME%\RetroArch\cores\%CORE%_libretro.dll %ROM% @@ -23,7 +24,7 @@ Changes in my branch - +``` - Better Grid view (animations, layout, selection...) - Created a 'GridEx' mode, which inherits from grid, in order to create a 'preview bar' including a game screenshot. - Changed 'Automatic' view mode. Switchs to grid if "thumbnails" exist in gamelist. Switchs to gridex if "thumbnails" and "image" exist in gamelist. From 983c1eb7b9abbb0c5d8ddda8b88443e76b432d21 Mon Sep 17 00:00:00 2001 From: Fab Date: Mon, 27 May 2019 21:46:37 +0200 Subject: [PATCH 013/336] [Fonts] Change favorite icon with a simple glyph font --- es-core/src/resources/Font.cpp | 2 +- resources/fontawesome_webfont.ttf | Bin 120456 -> 0 bytes resources/glyphs.ttf | Bin 0 -> 1904 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 resources/fontawesome_webfont.ttf create mode 100644 resources/glyphs.ttf diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index d5086b7e05..b32cdec546 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -249,7 +249,7 @@ std::vector getFallbackFontPaths() std::string path = fontDir + fontNames[i]; if (i == 0) - path = ":/fontawesome_webfont.ttf"; + path = ":/glyphs.ttf"; if(ResourceManager::getInstance()->fileExists(path)) fontPaths.push_back(path); diff --git a/resources/fontawesome_webfont.ttf b/resources/fontawesome_webfont.ttf deleted file mode 100644 index 59dec23e2af3290758890eee55e9f619e6c48202..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120456 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 diff --git a/resources/glyphs.ttf b/resources/glyphs.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f6cc4b6a511ac0af60b461af5364f5b398924d43 GIT binary patch literal 1904 zcmdT^TWDNG82)F^=4^L2mtC_-nl^1FsixX=H)qdocH1PC(6os~q)m)b1ZhvRvlrZ( zXHRZYFi>l4@j>t!TM&$Vu^R8TR zDLed?|0HzRz}NG_%7j*K{sH~R;Hf+WzNGdU^e=!vnlCwvdbjI;7d%v~%v#W4Ezb&g zq+~5t$wxzwT>$50t0Y2GZ@mHiI+hJqD>diI?%RvdcR_!YG_8fbO%1@0@A$rkbW02U ziybf685&*F*K|#63ToF7lX{T_G!J?1VB#*sK(x6*GD|y~7b30lTzu@7~`GMUqF+HowtsXfIJKvPCSF?mO6bQ1|J+_72^b zO1F3Pv53ne2YTz8zO-vJn8e(gN|+t(^7CBnfBo3%-pRd_Bddom26*av>Uw}RHJ`lN z_*h44(E&KPR-%Gvk-_!rv$m+i5x@L~C zi%sDZmnVMh(xWf-WSc_WXTM0Sgd5wm<}T*#?0o){0GB0o+bG8SecjC$@;ZI_Lc`gX zk-sSD=9a$ybmL8F{bTr5+(mU_+)td}t(&J?YU>Ri8l-G<)6IGtlR1{jV$|$fNbCWY zOkyukS3M5cZaSne!%aP|um|{*!Wwnb8HK%&7Zh$lovRA#)IuLB?4wEgTwy;2SU}+* zwXul8&9t9AA@}X|V-E(KP#Ak=3kqY;?2^J7nd}{fv43`3;Rf2xzEfC7%-?97ESkqy zp)xr%MS^lPM@2~OtvVj%W45pyk>LvkC(oyss)8qC=D0OKUMV|MA~#pGY{jCAMolAr zbZBtMOe@8Vuxo`%na5*>G5m~W&*phNahSvG$gm9KE>W?0JRTd288I_y#(63kJzsFP zMLts$R!wkQWQEP03U~5?k5#Hmb|IH{cumYYa+xebNlg}^xno-yQL^lpx#c)^;ryIy zQm#0KSy79fu;vAyu*#XGyyH}d1_q>$wIFJhl86;54-^g*;aaYV0pb)zA4AdvQs-dI zfG?o<4&{NSX$i#_KnaSGxiz&9OFQCjElL#L2F3B5hiDMpr1Z9EGg!x_8rGIY<*<#x z!`NoEJ+8X#^2YC1w{BG46g-S^$h`PF%F+F2t?76@R3Bj>A0hmhE+0|7-JcbGh;WO= K|DXJ4p8f$kvL^cg literal 0 HcmV?d00001 From 67dfce3bde593e908b5cb20a41ca3deea57c8994 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 28 May 2019 16:33:14 +0200 Subject: [PATCH 014/336] [Theming] Bad loading for themes using variables [SplashScreen] Progressbar + simplified splash.svg [Favorites] Better icon + Refresh list when switching favorite [Main Menu] Animations when opening [GameLaunch] Show splash.svg when launching ( when option hide when launch game is disabled ) [Font] Bad sizing when favorite glyph is loaded with small fonts. --- es-app/src/CollectionSystemManager.cpp | 17 ++- es-app/src/EmulationStation.h | 1 + es-app/src/SystemData.cpp | 21 ++- es-app/src/guis/GuiGamelistOptions.cpp | 29 +++- es-app/src/guis/GuiMenu.cpp | 42 ++++-- es-app/src/main.cpp | 16 +-- es-app/src/scrapers/ScreenScraper.h | 2 +- es-app/src/views/SystemView.cpp | 2 +- es-app/src/views/ViewController.cpp | 24 ++-- es-core/src/Settings.cpp | 4 + es-core/src/ThemeData.cpp | 140 ++++++++++++++++--- es-core/src/ThemeData.h | 4 +- es-core/src/Window.cpp | 96 ++++++++++--- es-core/src/Window.h | 9 +- es-core/src/components/GridTileComponent.cpp | 7 +- es-core/src/platform.cpp | 10 +- es-core/src/resources/Font.cpp | 35 ++--- es-core/src/resources/TextureData.cpp | 5 +- es-core/src/utils/FileSystemUtil.cpp | 2 +- resources/locale/fr/emulationstation2.po | 4 + resources/splash.svg | 91 +++++------- 21 files changed, 396 insertions(+), 165 deletions(-) diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index bd6eb63e91..16af995b20 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -519,20 +519,29 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) } else { - file->getSourceFileData()->getSystem()->getIndex()->removeFromIndex(file); + SystemData* sysData = file->getSourceFileData()->getSystem(); + sysData->getIndex()->removeFromIndex(file); + MetaDataList* md = &file->getSourceFileData()->metadata; + std::string value = md->get("favorite"); if (value == "false") - { md->set("favorite", "true"); - } else { adding = false; md->set("favorite", "false"); } - file->getSourceFileData()->getSystem()->getIndex()->addToIndex(file); + sysData->getIndex()->addToIndex(file); + refreshCollectionSystems(file->getSourceFileData()); + + SystemData* systemViewToUpdate = getSystemToView(sysData); + if (systemViewToUpdate != NULL) + { + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); + ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(file, FILE_METADATA_CHANGED); + } } if (adding) { diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index 57389a1055..375b11d9cf 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -8,6 +8,7 @@ #define PROGRAM_VERSION_MINOR 10 #define PROGRAM_VERSION_MAINTENANCE 0 #define PROGRAM_VERSION_STRING "2.10.0 FCAMOD" +#define SCRAPER_VERSION_STRING "2.10.0" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 4052801b75..adae7852e0 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -16,6 +16,7 @@ #include #endif +#include "GuiComponent.h" #include "Window.h" std::vector SystemData::sSystemVector; @@ -218,11 +219,15 @@ bool SystemData::loadConfig(Window* window) LOG(LogError) << "es_systems.cfg is missing the tag!"; return false; } - + + float systemCount = 1; + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + systemCount++; + + float currentSystem = 0; for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) { - std::vector emulatorList; - + std::vector emulatorList; // std::vector coreList; @@ -262,7 +267,9 @@ bool SystemData::loadConfig(Window* window) } if (window != NULL) - window->renderLoadingScreen(fullname); + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; // convert extensions list from a string into a vector of strings @@ -346,7 +353,9 @@ bool SystemData::loadConfig(Window* window) } if (window != NULL) - window->renderLoadingScreen("Favoris"); + window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; CollectionSystemManager::get()->loadCollectionSystems(); return true; @@ -575,7 +584,7 @@ void SystemData::loadTheme() sysData.insert(std::pair("system.theme", getThemeFolder())); sysData.insert(std::pair("system.fullName", getFullName())); - mTheme->loadFile(getThemeFolder(), path); + mTheme->loadFile(getThemeFolder(), sysData, path); } catch(ThemeException& e) { diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index b789667da8..01c0741c21 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -11,6 +11,8 @@ #include "GuiMetaDataEd.h" #include "SystemData.h" +#include "animations/LambdaAnimation.h" + GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : GuiComponent(window), mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false) { @@ -118,7 +120,32 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui // center the menu setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); + //mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); + + float x0 = (mSize.x() - mMenu.getSize().x()) / 2; + + float y1 = Renderer::getScreenHeight(); + float y2 = (mSize.y() - mMenu.getSize().y()) / 2; + //float y1 = mMenu.getSize().y(); + //float y2 = (mSize.y() - mMenu.getSize().y()) / 2; + + setPosition(x0, y1); + + auto fadeFunc = [this, x0, y1, y2](float t) { + + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + float y = y1 * (1 - pct) + y2 * pct; + setPosition(x0, y); + }; + + setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, x0, y2] + { + setPosition(x0, y2); + }); + + setPosition(x0, y2); } GuiGamelistOptions::~GuiGamelistOptions() diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 4b1cf2aea6..a383ee2ff4 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -19,12 +19,13 @@ #include #include +#include "animations/LambdaAnimation.h" + GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) { bool isFullUI = UIModeController::getInstance()->isUIModeFull(); - - + if (isFullUI) { addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); @@ -41,15 +42,38 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); } - - + +#if defined(_WIN32) addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); +#else + addEntry(_T("QUIT"), 0x777777FF, true, [this] {openQuitMenu(); }); +#endif addChild(&mMenu); addVersionInfo(); setSize(mMenu.getSize()); - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, Renderer::getScreenHeight() * 0.15f); + + float y1 = Renderer::getScreenHeight(); + float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); + + auto fadeFunc = [this, y1, y2](float t) { + + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + float y = y1 * (1 - pct) + y2 * pct; + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y); + }; + + setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] + { + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + }); + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); } void GuiMenu::openScraperSettings() @@ -402,13 +426,13 @@ void GuiMenu::openUISettings() screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); s->addRow(screensaver_row); - +#if defined(_WIN32) // quick system select (left/right in game list view) auto hideWindowScreen = std::make_shared(mWindow); hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); - +#endif // quick system select (left/right in game list view) auto quick_sys_select = std::make_shared(mWindow); @@ -551,9 +575,11 @@ void GuiMenu::openConfigInput() void GuiMenu::openQuitMenu() { +#if defined(_WIN32) Scripting::fireEvent("quit"); quitES(""); return; +#endif auto s = new GuiSettings(mWindow, _T("QUIT")); @@ -573,8 +599,6 @@ void GuiMenu::openQuitMenu() row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); s->addRow(row); - - if(Settings::getInstance()->getBool("ShowExit")) { row.elements.clear(); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 136bee9434..2a08c2c583 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -322,7 +322,7 @@ int main(int argc, char* argv[]) bool splashScreen = Settings::getInstance()->getBool("SplashScreen"); bool splashScreenProgress = Settings::getInstance()->getBool("SplashScreenProgress"); - if(!scrape_cmdline) + if (!scrape_cmdline) { if(!window.init()) { @@ -333,15 +333,9 @@ int main(int argc, char* argv[]) std::string glExts = (const char*)glGetString(GL_EXTENSIONS); LOG(LogInfo) << "Checking available OpenGL extensions..."; LOG(LogInfo) << " ARB_texture_non_power_of_two: " << (glExts.find("ARB_texture_non_power_of_two") != std::string::npos ? "ok" : "MISSING"); - if(splashScreen) - { - std::string progressText = "Chargement"; - - if (splashScreenProgress) - progressText = "Chargement de la configuration"; - window.renderLoadingScreen(progressText); - } + if (splashScreen) + window.renderLoadingScreen(_T("Loading...")); } const char* errorMsg = NULL; @@ -383,7 +377,7 @@ int main(int argc, char* argv[]) window.renderLoadingScreen(_T("Starting UI")); //choose which GUI to open depending on if an input configuration already exists - if(errorMsg == NULL) + if (errorMsg == NULL) { if (Utils::FileSystem::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0) ViewController::get()->goToStart(true); @@ -394,6 +388,8 @@ int main(int argc, char* argv[]) //generate joystick events since we're done loading SDL_JoystickEventState(SDL_ENABLE); + window.endRenderLoadingScreen(); + int lastTime = SDL_GetTicks(); int ps_time = SDL_GetTicks(); diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index 55c4a89c4f..24063744e2 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -28,7 +28,7 @@ class ScreenScraperRequest : public ScraperHttpRequest const std::string API_DEV_P = { 108, 28, 54, 55, 83, 43, 91, 44, 30, 22, 41, 12, 0, 108, 38, 29 }; const std::string API_DEV_KEY = { 54, 73, 115, 100, 101, 67, 111, 107, 79, 66, 68, 66, 67, 56, 118, 77, 54, 88, 101, 54 }; const std::string API_URL_BASE = "https://www.screenscraper.fr/api2"; - const std::string API_SOFT_NAME = "Emulationstation 2.9.0"; // +static_cast(PROGRAM_VERSION_STRING); + const std::string API_SOFT_NAME = "Emulationstation "+static_cast(SCRAPER_VERSION_STRING); /** Which type of image artwork we need. Possible values (not a comprehensive list): - ss: in-game screenshot diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 9ed1a22dc6..41bc9fddfe 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -271,7 +271,7 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) }, goFast ? 10 : 300); // wait 600ms to fade in - setAnimation(infoFadeIn, goFast ? 0 : 2000, nullptr, false, 2); + setAnimation(infoFadeIn, goFast ? 0 : 500, nullptr, false, 2); // no need to animate transition, we're not going anywhere (probably mEntries.size() == 1) if(endPos == mCamOffset && endPos == mExtrasCamOffset) diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 65fb1b4122..37c61dbbab 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -61,12 +61,13 @@ void ViewController::goToStart(bool forceImmediate) // Requested system doesn't exist Settings::getInstance()->setString("StartupSystem", ""); } + goToSystemView(SystemData::sSystemVector.at(0), forceImmediate); } void ViewController::ReloadAndGoToStart() { - mWindow->renderLoadingScreen("Chargement"); + mWindow->renderLoadingScreen(_T("Loading...")); ViewController::get()->reloadAll(); ViewController::get()->goToStart(false); @@ -235,8 +236,8 @@ void ViewController::launch(FileData* game, Vector3f center) setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this, game, fadeFunc] { game->launchGame(mWindow); - setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this] { mLockInput = false; }, true); - this->onFileChanged(game, FILE_METADATA_CHANGED); + setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this] { mLockInput = false; mWindow->endRenderLoadingScreen(); }, true); + this->onFileChanged(game, FILE_METADATA_CHANGED); }); } else if (transition_style == "slide"){ // move camera to zoom in on center + fade out, launch game, come back in @@ -244,7 +245,7 @@ void ViewController::launch(FileData* game, Vector3f center) { game->launchGame(mWindow); mCamera = origCamera; - setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 600), 0, [this] { mLockInput = false; }, true); + setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 600), 0, [this] { mLockInput = false; mWindow->endRenderLoadingScreen(); }, true); this->onFileChanged(game, FILE_METADATA_CHANGED); }); } else { // instant @@ -252,7 +253,7 @@ void ViewController::launch(FileData* game, Vector3f center) { game->launchGame(mWindow); mCamera = origCamera; - setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 10), 0, [this] { mLockInput = false; }, true); + setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 10), 0, [this] { mLockInput = false; mWindow->endRenderLoadingScreen(); }, true); this->onFileChanged(game, FILE_METADATA_CHANGED); }); } @@ -420,7 +421,7 @@ bool ViewController::input(InputConfig* config, Input input) return true; } - if(mCurrentView) + if (mCurrentView) return mCurrentView->input(config, input); return false; @@ -468,10 +469,15 @@ void ViewController::render(const Transform4x4f& parentTrans) mWindow->renderHelpPromptsEarly(); // fade out - if(mFadeOpacity) + if (mFadeOpacity) { - Renderer::setMatrix(parentTrans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(mFadeOpacity * 255)); + if (Settings::getInstance()->getBool("HideWindow")) + { + Renderer::setMatrix(parentTrans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(mFadeOpacity * 255)); + } + else + mWindow->renderGameLoadingScreen(mFadeOpacity, false); } } diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index a63c75e9d3..41f2e16de4 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -92,7 +92,11 @@ void Settings::setDefaults() mIntMap["MaxVRAM"] = 100; #endif +#if defined(_WIN32) + mBoolMap["HideWindow"] = false; +#else mBoolMap["HideWindow"] = true; +#endif mStringMap["GameTransitionStyle"] = "fade"; mStringMap["TransitionStyle"] = "slide"; mStringMap["Language"] = "en"; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 52f75760cc..9ee8ea2fc9 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -203,7 +203,7 @@ ThemeData::ThemeData() mVersion = 0; } -void ThemeData::loadFile(std::string system, const std::string& path) +void ThemeData::loadFile(std::string system, std::map sysDataMap, const std::string& path) { mPaths.push_back(path); @@ -219,8 +219,7 @@ void ThemeData::loadFile(std::string system, const std::string& path) mSystemThemeFolder = system; mVariables.clear(); - //mVariables.insert(system, system); - //mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); + mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); pugi::xml_document doc; pugi::xml_parse_result res = doc.load_file(path.c_str()); @@ -263,6 +262,67 @@ std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolde return result; } +bool ThemeData::parseSubset(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("subset")) + { + parse = false; + const std::string subsetAttr = node.attribute("subset").as_string(); + const std::string nameAttr = node.attribute("name").as_string(); + + if (subsetAttr == "iconset" || subsetAttr == "gamelistview") + { + if (nameAttr.rfind("1-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (subsetAttr == "systemview") + { + if (nameAttr.rfind("8-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (nameAttr.rfind("2-") != std::string::npos) + { + parse = true; + return parse; + } + /* + if (subsetAttr == "colorset" && nameAttr == mColorset) + { + parse = true; + return parse; + } + if (subsetAttr == "iconset" && nameAttr == mIconset) + { + parse = true; + return parse; + } + if (subsetAttr == "menu" && nameAttr == mMenu) + { + parse = true; + return parse; + } + if (subsetAttr == "systemview" && nameAttr == mSystemview) + { + parse = true; + return parse; + } + if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) + { + parse = true; + return parse; + }*/ + } + + return parse; +} void ThemeData::parseIncludes(const pugi::xml_node& root) { @@ -271,6 +331,9 @@ void ThemeData::parseIncludes(const pugi::xml_node& root) for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) { + if (!parseSubset(node)) + continue; + std::string relPath = resolvePlaceholders(node.text().as_string()); std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); path = resolveSystemVariable(mSystemThemeFolder, path); @@ -391,23 +454,56 @@ void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) if(elemTypeIt == sElementMap.cend()) throw error << "Unknown element of type \"" << node.name() << "\"!"; + if (parseRegion(node)) + { + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + parseElement(node, elemTypeIt->second, + view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); + + if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) + view.orderedKeys.push_back(elemKey); + } + } + } +} + +bool ThemeData::parseRegion(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("region")) + { + std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); + + parse = false; const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); + const std::string nameAttr = node.attribute("region").as_string(); size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while(off != std::string::npos || prevOff != std::string::npos) + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) { std::string elemKey = nameAttr.substr(prevOff, off - prevOff); prevOff = nameAttr.find_first_not_of(delim, off); off = nameAttr.find_first_of(delim, prevOff); - - parseElement(node, elemTypeIt->second, - view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); - - if(std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) - view.orderedKeys.push_back(elemKey); + if (elemKey == regionsetting) + { + parse = true; + return parse; + } } + } + return parse; + } @@ -450,16 +546,28 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapfileExists(path)) + + if (!ResourceManager::getInstance()->fileExists(path)) + { + std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); + if (ResourceManager::getInstance()->fileExists(rootPath)) + path = rootPath; + } + + if (!ResourceManager::getInstance()->fileExists(path)) { std::stringstream ss; ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda ss << "could not find file \"" << node.text().get() << "\" "; if(node.text().get() != path) ss << "(which resolved to \"" << path << "\") "; + LOG(LogWarning) << ss.str(); + OutputDebugString(ss.str().c_str()); } - element.properties[node.name()] = path; + else + element.properties[node.name()] = path; + break; } case COLOR: @@ -525,8 +633,8 @@ const std::shared_ptr& ThemeData::getDefault() { try { - //std::map emptyMap; - theme->loadFile("", path); // emptyMap + std::map emptyMap; + theme->loadFile("", emptyMap, path); } catch(ThemeException& e) { LOG(LogError) << e.what(); diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 6bbcc01555..aec760c4c7 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -135,7 +135,7 @@ class ThemeData ThemeData(); // throws ThemeException - void loadFile(std::string system, const std::string& path); + void loadFile(std::string system, std::map sysDataMap, const std::string& path); enum ElementPropertyType { @@ -178,6 +178,8 @@ class ThemeData void parseViews(const pugi::xml_node& themeRoot); void parseView(const pugi::xml_node& viewNode, ThemeView& view); void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element); + bool parseRegion(const pugi::xml_node& node); + bool parseSubset(const pugi::xml_node& node); std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 05337b0e01..1b361f408e 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -11,11 +11,15 @@ #include #include +#include + Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) { mHelp = new HelpComponent(this); mBackgroundOverlay = new ImageComponent(this); + mSplash = NULL; + } Window::~Window() @@ -297,58 +301,104 @@ void Window::setAllowSleep(bool sleep) mAllowSleep = sleep; } -void Window::renderLoadingScreen(std::string text) +void Window::endRenderLoadingScreen() +{ + mSplash = NULL; +} + +void Window::renderLoadingScreen(std::string text, float percent) { +#if defined(_WIN32) + // Avoid Window Freezing on Windows + SDL_Event event; + while (SDL_PollEvent(&event)) ; +#endif + Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); + if (percent > 0) + { + float baseHeight = 0.04f; + + float w = Renderer::getScreenWidth() / 2; + float h = Renderer::getScreenHeight() * baseHeight; + + float x = Renderer::getScreenWidth() / 2 - w / 2; + float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); + + Renderer::drawRect(x, y, w, h, 0x252525FF); + Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF + } + + ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.5f, 0.0f); - splash.setImage(":/splash.svg"); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); splash.render(trans); + auto& font = mDefaultFonts.at(1); TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.835f); + float y = Math::round(Renderer::getScreenHeight() * 0.8f); // 35 trans = trans.translate(Vector3f(x, y, 0.0f)); Renderer::setMatrix(trans); font->renderTextCache(cache); delete cache; + Renderer::swapBuffers(); } -void Window::renderBlackScreen(std::string text) +void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) { Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); - /* + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); + ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.5f, 0.0f); - splash.setImage(":/splash.svg"); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); splash.render(trans); - */ + + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); - if (text.length() > 0) - { - auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); - - float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.835f); - trans = trans.translate(Vector3f(x, y, 0.0f)); - Renderer::setMatrix(trans); - font->renderTextCache(cache); - delete cache; - } + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.835f); + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; - Renderer::swapBuffers(); + if (swapBuffers) + Renderer::swapBuffers(); } diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 5fd238dc62..0c64a959fa 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -16,6 +16,8 @@ class ImageComponent; class InputConfig; class TextCache; class Transform4x4f; +class TextureResource; + struct HelpStyle; class Window @@ -64,8 +66,9 @@ class Window bool getAllowSleep(); void setAllowSleep(bool sleep); - void renderLoadingScreen(std::string text); - void renderBlackScreen(std::string text); + void endRenderLoadingScreen(); + void renderLoadingScreen(std::string text, float percent = -1); + void renderGameLoadingScreen(float opacity=1, bool swapBuffers=true); void renderHelpPromptsEarly(); // used to render HelpPrompts before a fade void setHelpPrompts(const std::vector& prompts, const HelpStyle& style); @@ -91,6 +94,8 @@ class Window InfoPopup* mInfoPopup; bool mRenderScreenSaver; + std::shared_ptr mSplash; + std::vector mGuiStack; std::vector< std::shared_ptr > mDefaultFonts; diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 310830f4fb..cccf549cfe 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -302,10 +302,13 @@ void GridTileComponent::setSelected(bool selected, Vector3f* pPosition) auto func = [this](float t) { - this->setSelectedZoom(t); + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + this->setSelectedZoom(pct); }; - setAnimation(new LambdaAnimation(func, 100), 0, [this] { + setAnimation(new LambdaAnimation(func, 200), 0, [this] { this->setSelectedZoom(1); mAnimPosition = Vector3f(0, 0, 0); }, false, 3); diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index d47f6204a0..e80e589518 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -1,16 +1,18 @@ #include "platform.h" - #include + #ifdef WIN32 #include +#include #else #include #endif #include -#include #include "Window.h" +#include "GuiComponent.h" + int runShutdownCommand() { #ifdef WIN32 // windows @@ -84,7 +86,7 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo { #ifdef WIN32 if (window != NULL) - window->renderBlackScreen("Chargement en cours..."); + window->renderGameLoadingScreen(); // on Windows we use _wsystem to support non-ASCII paths // which requires converting from utf8 to a wstring @@ -124,7 +126,7 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo polled = true; if (window != NULL && polled) - window->renderBlackScreen("Chargement en cours..."); + window->renderGameLoadingScreen(); } } diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index b32cdec546..9890954a82 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -78,9 +78,11 @@ Font::Font(int size, const std::string& path) : mSize(size), mPath(path) initLibrary(); // always initialize ASCII characters -// for(unsigned int i = 32; i < 128; i++) - // getGlyph(i); + for (unsigned int i = 32; i < 128; i++) + getGlyph(i); + // getGlyph(61446); + clearFaceCache(); } @@ -140,7 +142,7 @@ Font::FontTexture::~FontTexture() bool Font::FontTexture::findEmpty(const Vector2i& size, Vector2i& cursor_out) { - if(size.x() >= textureSize.x() || size.y() >= textureSize.y()) + if (size.x() >= textureSize.x() || size.y() >= textureSize.y()) return false; if(writePos.x() + size.x() >= textureSize.x() && @@ -235,9 +237,10 @@ std::vector getFallbackFontPaths() fontDir += "\\Fonts\\"; const char* fontNames[] = { + "glyphs.ttf", // latin + "arial.ttf", // latin "meiryo.ttc", // japanese - "simhei.ttf", // chinese - "arial.ttf" // latin + "simhei.ttf" // chinese }; //prepend to font file names @@ -251,7 +254,7 @@ std::vector getFallbackFontPaths() if (i == 0) path = ":/glyphs.ttf"; - if(ResourceManager::getInstance()->fileExists(path)) + if (ResourceManager::getInstance()->fileExists(path)) fontPaths.push_back(path); } @@ -288,18 +291,17 @@ FT_Face Font::getFaceForChar(unsigned int id) for(unsigned int i = 0; i < fallbackFonts.size() + 1; i++) { auto fit = mFaceCache.find(i); - - if(fit == mFaceCache.cend()) // doesn't exist yet - { + if (fit == mFaceCache.cend()) // doesn't exist yet + { // i == 0 -> mPath // otherwise, take from fallbackFonts const std::string& path = (i == 0 ? mPath : fallbackFonts.at(i - 1)); - ResourceData data = ResourceManager::getInstance()->getFileData(path); - mFaceCache[i] = std::unique_ptr(new FontFace(std::move(data), mSize)); + ResourceData data = ResourceManager::getInstance()->getFileData(path); + mFaceCache[i] = std::unique_ptr(new FontFace(std::move(data), i == 1 && mMaxGlyphHeight > 0 ? mMaxGlyphHeight : mSize)); // Reduce size of gyphs ???? fit = mFaceCache.find(i); } - if(FT_Get_Char_Index(fit->second->face, id) != 0) + if (FT_Get_Char_Index(fit->second->face, id) != 0) return fit->second->face; } @@ -316,7 +318,7 @@ Font::Glyph* Font::getGlyph(unsigned int id) { // is it already loaded? auto it = mGlyphMap.find(id); - if(it != mGlyphMap.cend()) + if (it != mGlyphMap.cend()) return &it->second; // nope, need to make a glyph @@ -329,7 +331,7 @@ Font::Glyph* Font::getGlyph(unsigned int id) FT_GlyphSlot g = face->glyph; - if(FT_Load_Char(face, id, FT_LOAD_RENDER)) + if (FT_Load_Char(face, id, FT_LOAD_RENDER)) { LOG(LogError) << "Could not find glyph for character " << id << " for font " << mPath << ", size " << mSize << "!"; return NULL; @@ -352,9 +354,10 @@ Font::Glyph* Font::getGlyph(unsigned int id) Glyph& glyph = mGlyphMap[id]; glyph.texture = tex; + glyph.texPos = Vector2f(cursor.x() / (float)tex->textureSize.x(), cursor.y() / (float)tex->textureSize.y()); glyph.texSize = Vector2f(glyphSize.x() / (float)tex->textureSize.x(), glyphSize.y() / (float)tex->textureSize.y()); - + glyph.advance = Vector2f((float)g->metrics.horiAdvance / 64.0f, (float)g->metrics.vertAdvance / 64.0f); glyph.bearing = Vector2f((float)g->metrics.horiBearingX / 64.0f, (float)g->metrics.horiBearingY / 64.0f); @@ -364,7 +367,7 @@ Font::Glyph* Font::getGlyph(unsigned int id) glBindTexture(GL_TEXTURE_2D, 0); // update max glyph height - if(glyphSize.y() > mMaxGlyphHeight) + if (id != 61446 && glyphSize.y() > mMaxGlyphHeight) mMaxGlyphHeight = glyphSize.y(); // done diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index e2a613a859..6c472c79ed 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -164,9 +164,6 @@ bool TextureData::uploadAndBind() if (mTextureID != 0) { glBindTexture(GL_TEXTURE_2D, mTextureID); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } else { @@ -186,7 +183,7 @@ bool TextureData::uploadAndBind() glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)mWidth, (GLsizei)mHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, mDataRGBA); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); const GLint wrapMode = mTile ? GL_REPEAT : GL_CLAMP_TO_EDGE; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index e4af303eb3..3c77caed8b 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -149,7 +149,7 @@ namespace Utils char* envHome = getenv("HOME"); #ifdef _DEBUG - envHome = "H:/[Emulz]/EmulationStation/"; + // envHome = "H:/[Emulz]/EmulationStation/"; #endif if (envHome) diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index 3f34a69461..ccd347f69a 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -92,6 +92,10 @@ msgstr "CREER UNE COLLECTION PERSONNALISÉE" msgid "New Collection Name" msgstr "Nom de la collection" +#: +msgid "Loading..." +msgstr "Chargement en cours..." + #: msgid "GROUP UNTHEMED CUSTOM COLLECTIONS" msgstr "GROUPER LES COLLECTIONS SANS THEME" diff --git a/resources/splash.svg b/resources/splash.svg index b15cdec4e2..7ae4f6bcc9 100644 --- a/resources/splash.svg +++ b/resources/splash.svg @@ -1,55 +1,36 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + Layer 1 + + + + + + + + + + + + + + + + + + From ac8c9093a9c4ce6a3cd7577b5bcb2d355658db63 Mon Sep 17 00:00:00 2001 From: Fab Date: Tue, 28 May 2019 18:32:28 +0200 Subject: [PATCH 015/336] [Theme] Fix theme loading --- es-app/src/FileData.h | 268 ++-- es-app/src/SystemData.cpp | 1188 +++++++------- es-app/src/SystemData.h | 40 +- es-app/src/guis/GuiGamelistOptions.cpp | 18 +- es-app/src/guis/GuiMenu.cpp | 1412 ++++++++--------- es-app/src/guis/GuiMetaDataEd.cpp | 764 ++++----- es-core/src/GuiComponent.cpp | 1164 +++++++------- es-core/src/GuiComponent.h | 444 +++--- es-core/src/ThemeData.cpp | 1457 +++++++++--------- es-core/src/Window.cpp | 1062 ++++++------- es-core/src/components/OptionListComponent.h | 16 +- es-core/src/platform.cpp | 328 ++-- es-core/src/utils/FileSystemUtil.cpp | 1334 ++++++++-------- es-core/src/utils/StringUtil.h | 2 +- 14 files changed, 4750 insertions(+), 4747 deletions(-) diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 00fd4a923b..a4a0d7e866 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -1,134 +1,134 @@ -#pragma once -#ifndef ES_APP_FILE_DATA_H -#define ES_APP_FILE_DATA_H - -#include "utils/FileSystemUtil.h" -#include "MetaData.h" -#include - -class SystemData; -class Window; -struct SystemEnvironmentData; - -enum FileType -{ - GAME = 1, // Cannot have children. - FOLDER = 2, - PLACEHOLDER = 3 -}; - -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); - -// A tree node that holds information for a file. -class FileData -{ -public: - FileData(FileType type, const std::string& path, SystemEnvironmentData* envData, SystemData* system); - virtual ~FileData(); - - virtual const std::string& getName(); - virtual const std::string& getSortName(); - inline FileType getType() const { return mType; } - inline const std::string& getPath() const { return mPath; } - inline FileData* getParent() const { return mParent; } - inline const std::unordered_map& getChildrenByFilename() const { return mChildrenByFilename; } - inline const std::vector& getChildren() const { return mChildren; } - inline SystemData* getSystem() const { return mSystem; } - inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } - virtual const std::string getThumbnailPath() const; - virtual const std::string getVideoPath() const; - virtual const std::string getMarqueePath() const; - virtual const std::string getImagePath() const; - - virtual const std::string getCore() const; - virtual const std::string getEmulator() const; - - virtual const bool getHidden(); - virtual const bool getFavorite(); - - FileData* findUniqueGameForFolder(); - - const std::vector& getChildrenListToDisplay(); - std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; - - void addChild(FileData* file); // Error if mType != FOLDER - void removeChild(FileData* file); //Error if mType != FOLDER - - inline bool isPlaceHolder() { return mType == PLACEHOLDER; }; - - virtual inline void refreshMetadata() { return; }; - - virtual std::string getKey(); - const bool isArcadeAsset(); - inline std::string getFullPath() { return getPath(); }; - inline std::string getFileName() { return Utils::FileSystem::getFileName(getPath()); }; - virtual FileData* getSourceFileData(); - inline std::string getSystemName() const { return mSystemName; }; - - // Returns our best guess at the "real" name for this file (will attempt to perform MAME name translation) - std::string getDisplayName() const; - - // As above, but also remove parenthesis - std::string getCleanName() const; - - void launchGame(Window* window); - - 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; - -protected: - FileData* mSourceFileData; - FileData* mParent; - std::string mSystemName; - //std::string mDefaultCore; - -private: - FileType mType; - std::string mPath; - SystemEnvironmentData* mEnvData; - SystemData* mSystem; - std::unordered_map mChildrenByFilename; - std::vector mChildren; - std::vector mFilteredChildren; -}; - -class CollectionFileData : public FileData -{ -public: - CollectionFileData(FileData* file, SystemData* system); - ~CollectionFileData(); - const std::string& getName(); - void refreshMetadata(); - FileData* getSourceFileData(); - std::string getKey(); -private: - // needs to be updated when metadata changes - std::string mCollectionFileName; - bool mDirty; -}; - -FileData::SortType getSortTypeFromString(std::string desc); - -#endif // ES_APP_FILE_DATA_H +#pragma once +#ifndef ES_APP_FILE_DATA_H +#define ES_APP_FILE_DATA_H + +#include "utils/FileSystemUtil.h" +#include "MetaData.h" +#include + +class SystemData; +class Window; +struct SystemEnvironmentData; + +enum FileType +{ + GAME = 1, // Cannot have children. + FOLDER = 2, + PLACEHOLDER = 3 +}; + +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); + +// A tree node that holds information for a file. +class FileData +{ +public: + FileData(FileType type, const std::string& path, SystemEnvironmentData* envData, SystemData* system); + virtual ~FileData(); + + virtual const std::string& getName(); + virtual const std::string& getSortName(); + inline FileType getType() const { return mType; } + inline const std::string& getPath() const { return mPath; } + inline FileData* getParent() const { return mParent; } + inline const std::unordered_map& getChildrenByFilename() const { return mChildrenByFilename; } + inline const std::vector& getChildren() const { return mChildren; } + inline SystemData* getSystem() const { return mSystem; } + inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } + virtual const std::string getThumbnailPath() const; + virtual const std::string getVideoPath() const; + virtual const std::string getMarqueePath() const; + virtual const std::string getImagePath() const; + + virtual const std::string getCore() const; + virtual const std::string getEmulator() const; + + virtual const bool getHidden(); + virtual const bool getFavorite(); + + FileData* findUniqueGameForFolder(); + + const std::vector& getChildrenListToDisplay(); + std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; + + void addChild(FileData* file); // Error if mType != FOLDER + void removeChild(FileData* file); //Error if mType != FOLDER + + inline bool isPlaceHolder() { return mType == PLACEHOLDER; }; + + virtual inline void refreshMetadata() { return; }; + + virtual std::string getKey(); + const bool isArcadeAsset(); + inline std::string getFullPath() { return getPath(); }; + inline std::string getFileName() { return Utils::FileSystem::getFileName(getPath()); }; + virtual FileData* getSourceFileData(); + inline std::string getSystemName() const { return mSystemName; }; + + // Returns our best guess at the "real" name for this file (will attempt to perform MAME name translation) + std::string getDisplayName() const; + + // As above, but also remove parenthesis + std::string getCleanName() const; + + void launchGame(Window* window); + + 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; + +protected: + FileData* mSourceFileData; + FileData* mParent; + std::string mSystemName; + //std::string mDefaultCore; + +private: + FileType mType; + std::string mPath; + SystemEnvironmentData* mEnvData; + SystemData* mSystem; + std::unordered_map mChildrenByFilename; + std::vector mChildren; + std::vector mFilteredChildren; +}; + +class CollectionFileData : public FileData +{ +public: + CollectionFileData(FileData* file, SystemData* system); + ~CollectionFileData(); + const std::string& getName(); + void refreshMetadata(); + FileData* getSourceFileData(); + std::string getKey(); +private: + // needs to be updated when metadata changes + std::string mCollectionFileName; + bool mDirty; +}; + +FileData::SortType getSortTypeFromString(std::string desc); + +#endif // ES_APP_FILE_DATA_H diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index adae7852e0..6389da9957 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -1,594 +1,594 @@ -#include "SystemData.h" - -#include "utils/FileSystemUtil.h" -#include "CollectionSystemManager.h" -#include "FileFilterIndex.h" -#include "FileSorts.h" -#include "Gamelist.h" -#include "Log.h" -#include "platform.h" -#include "Settings.h" -#include "ThemeData.h" -#include "views/UIModeController.h" -#include -#include -#ifdef WIN32 -#include -#endif - -#include "GuiComponent.h" -#include "Window.h" - -std::vector SystemData::sSystemVector; - -SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : - mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) -{ - mFilterIndex = new FileFilterIndex(); - - // if it's an actual system, initialize it, if not, just create the data structure - if (!CollectionSystem) - { - mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); - mRootFolder->metadata.set("name", mFullName); - - if (!Settings::getInstance()->getBool("ParseGamelistOnly")) - { - populateFolder(mRootFolder); - if (mRootFolder->getChildren().size() == 0) - return; - } - - if (!Settings::getInstance()->getBool("IgnoreGamelist")) - parseGamelist(this); - - mRootFolder->sort(FileSorts::SortTypes.at(0)); - - indexAllGameFilters(mRootFolder); - } - else - { - // virtual systems are updated afterwards, we're just creating the data structure - mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this); - } - setIsGameSystemStatus(); - loadTheme(); -} - -SystemData::~SystemData() -{ - delete mRootFolder; - delete mFilterIndex; -} - -void SystemData::setIsGameSystemStatus() -{ - // we exclude non-game systems from specific operations (i.e. the "RetroPie" system, at least) - // if/when there are more in the future, maybe this can be a more complex method, with a proper list - // but for now a simple string comparison is more performant - mIsGameSystem = (mName != "retropie"); -} - -char _easytolower(char in) { - if (in <= 'Z' && in >= 'A') - return in - ('Z' - 'z'); - return in; -} - -void SystemData::populateFolder(FileData* folder) -{ - const std::string& folderPath = folder->getPath(); - if(!Utils::FileSystem::isDirectory(folderPath)) - { - LOG(LogWarning) << "Error - folder with path \"" << folderPath << "\" is not a directory!"; - return; - } - - int di = folderPath.rfind("downloaded_images"); - int md = folderPath.rfind("media"); - if (di > 0 || md > 0) - return; - - //make sure that this isn't a symlink to a thing we already have - if(Utils::FileSystem::isSymlink(folderPath)) - { - //if this symlink resolves to somewhere that's at the beginning of our path, it's gonna recurse - if(folderPath.find(Utils::FileSystem::getCanonicalPath(folderPath)) == 0) - { - LOG(LogWarning) << "Skipping infinitely recursive symlink \"" << folderPath << "\""; - return; - } - } - - std::string filePath; - std::string extension; - bool isGame; - bool showHidden = Settings::getInstance()->getBool("ShowHiddenFiles"); - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(folderPath); - for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - filePath = *it; - - // skip hidden files and folders - if(!showHidden && Utils::FileSystem::isHidden(filePath)) - 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 = Utils::FileSystem::getExtension(filePath); - std::transform(extension.begin(), extension.end(), extension.begin(), ::_easytolower); - - //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(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) - { - FileData* newGame = new FileData(GAME, filePath, mEnvData, this); - - // preventing new arcade assets to be added - if(!newGame->isArcadeAsset()) - { - folder->addChild(newGame); - isGame = true; - } - } - - //add directories that also do not match an extension as folders - if(!isGame && Utils::FileSystem::isDirectory(filePath)) - { - if (filePath.rfind("downloaded_images") == std::string::npos && - filePath.rfind("media") == std::string::npos) - { - FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); - populateFolder(newFolder); - - if (newFolder->getChildrenByFilename().size() == 0) - delete newFolder; - else if (newFolder->findUniqueGameForFolder() != NULL) - delete newFolder; - else - folder->addChild(newFolder); - } - } - } -} - -void SystemData::indexAllGameFilters(const FileData* folder) -{ - const std::vector& children = folder->getChildren(); - - for(std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) - { - switch((*it)->getType()) - { - case GAME: { mFilterIndex->addToIndex(*it); } break; - case FOLDER: { indexAllGameFilters(*it); } break; - } - } -} - -std::vector readList(const std::string& str, const char* delims = " \t\r\n,") -{ - std::vector 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(Window* window) -{ - deleteSystems(); - - std::string path = getConfigPath(false); - - LOG(LogInfo) << "Loading system config file " << path << "..."; - - if(!Utils::FileSystem::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 tag!"; - return false; - } - - float systemCount = 1; - for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - systemCount++; - - float currentSystem = 0; - for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - { - std::vector emulatorList; - - // std::vector coreList; - - std::string name, fullname, path, cmd, themeFolder, defaultCore; - - name = system.child("name").text().get(); - fullname = system.child("fullname").text().get(); - path = system.child("path").text().get(); - defaultCore = system.child("defaultCore").text().get(); - - pugi::xml_node emulators = system.child("emulators"); - if (emulators != NULL) - { - for (pugi::xml_node emulator : emulators.children()) - { - EmulatorData emulatorData; - emulatorData.mName = emulator.attribute("name").value(); - emulatorData.mCommandLine = emulator.attribute("command").value(); - - pugi::xml_node cores = emulator.child("cores"); - if (cores != NULL) - { - for (pugi::xml_node core : cores.children()) - { - const std::string& corename = core.text().get(); - - if (defaultCore.length() == 0) - defaultCore = corename; - - emulatorData.mCores.push_back(corename); - // coreList.push_back(corename); - } - } - - emulatorList.push_back(emulatorData); - } - } - - if (window != NULL) - window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); - - currentSystem++; - - // convert extensions list from a string into a vector of strings - - std::vector list = readList(system.child("extension").text().get()); - std::vector extensions; - - for (auto extension = list.cbegin(); extension != list.cend(); extension++) - { - std::string xt = (*extension); - std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); - - if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) - extensions.push_back(xt); - } - - cmd = system.child("command").text().get(); - - // platform id list - const char* platformList = system.child("platform").text().get(); - std::vector platformStrs = readList(platformList); - std::vector platformIds; - for(auto it = platformStrs.cbegin(); it != platformStrs.cend(); 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 - path = Utils::FileSystem::getGenericPath(path); - - //expand home symbol if the startpath contains ~ - if (path[0] == '~') - { - path.erase(0, 1); - path.insert(0, Utils::FileSystem::getHomePath()); - } - - //create the system runtime environment data - SystemEnvironmentData* envData = new SystemEnvironmentData; - envData->mStartPath = path; - envData->mSearchExtensions = extensions; - envData->mLaunchCommand = cmd; - envData->mPlatformIds = platformIds; - // envData->mDefaultCore = defaultCore; - envData->mEmulators = emulatorList; - - // envData->mCores = coreList; - - SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); - if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) - { - LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; - delete newSys; - } - else - sSystemVector.push_back(newSys); - } - - if (window != NULL) - window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); - - currentSystem++; - - CollectionSystemManager::get()->loadCollectionSystems(); - return true; -} - -void SystemData::writeExampleConfig(const std::string& path) -{ - std::ofstream file(path.c_str()); - - file << "\n" - "\n" - "\n" - " \n" - " \n" - "\n" - " \n" - " nes\n" - "\n" - " \n" - " Nintendo Entertainment System\n" - "\n" - " \n" - " ~/roms/nes\n" - "\n" - " \n" - " .nes .NES\n" - "\n" - " \n" - " retroarch -L ~/cores/libretro-fceumm.so %ROM%\n" - "\n" - " \n" - " nes\n" - "\n" - " \n" - " nes\n" - " \n" - "\n"; - - file.close(); - - LOG(LogError) << "Example config written! Go read it at \"" << path << "\"!"; -} - -void SystemData::deleteSystems() -{ - bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); - - for(unsigned int i = 0; i < sSystemVector.size(); i++) - { - SystemData* pData = sSystemVector.at(i); - - if (saveOnExit && !pData->mIsCollectionSystem) - updateGamelist(pData); - - delete pData; - } - - sSystemVector.clear(); -} - -std::string SystemData::getConfigPath(bool forWrite) -{ - std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_systems.cfg"; - if(forWrite || Utils::FileSystem::exists(path)) - return path; - - return "/etc/emulationstation/es_systems.cfg"; -} - -bool SystemData::isVisible() -{ - return (getDisplayedGameCount() > 0 || - (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || - (mIsCollectionSystem && mName == "favorites")); -} - -SystemData* SystemData::getNext() const -{ - std::vector::const_iterator it = getIterator(); - - do { - it++; - if (it == sSystemVector.cend()) - it = sSystemVector.cbegin(); - } while (!(*it)->isVisible()); - // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. - - return *it; -} - -SystemData* SystemData::getPrev() const -{ - std::vector::const_reverse_iterator it = getRevIterator(); - - do { - it++; - if (it == sSystemVector.crend()) - it = sSystemVector.crbegin(); - } while (!(*it)->isVisible()); - // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. - - return *it; -} - -std::string SystemData::getGamelistPath(bool forWrite) const -{ - std::string filePath; - - filePath = mRootFolder->getPath() + "/gamelist.xml"; - if(Utils::FileSystem::exists(filePath)) - return filePath; - - filePath = Utils::FileSystem::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 - Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath)); - if(forWrite || Utils::FileSystem::exists(filePath)) - return filePath; - - 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. system theme from currently selected theme set [CURRENT_THEME_PATH]/[SYSTEM]/theme.xml - // 3. default system theme from currently selected theme set [CURRENT_THEME_PATH]/theme.xml - - // first, check game folder - std::string localThemePath = mRootFolder->getPath() + "/theme.xml"; - if(Utils::FileSystem::exists(localThemePath)) - return localThemePath; - - // not in game folder, try system theme in theme sets - localThemePath = ThemeData::getThemeFromCurrentSet(mThemeFolder); - - if (Utils::FileSystem::exists(localThemePath)) - return localThemePath; - - // not system theme, try default system theme in theme set - localThemePath = Utils::FileSystem::getParent(Utils::FileSystem::getParent(localThemePath)) + "/theme.xml"; - - return localThemePath; -} - -bool SystemData::hasGamelist() const -{ - return (Utils::FileSystem::exists(getGamelistPath(false))); -} - -unsigned int SystemData::getGameCount() const -{ - return (unsigned int)mRootFolder->getFilesRecursive(GAME).size(); -} - -SystemData* SystemData::getRandomSystem() -{ - // this is a bit brute force. It might be more efficient to just to a while (!gameSystem) do random again... - unsigned int total = 0; - for(auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) - { - if ((*it)->isGameSystem()) - total ++; - } - - // get random number in range - int target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); - for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) - { - if ((*it)->isGameSystem()) - { - if (target > 0) - { - target--; - } - else - { - return (*it); - } - } - } - - // if we end up here, there is no valid system - return NULL; -} - -FileData* SystemData::getRandomGame() -{ - std::vector list = mRootFolder->getFilesRecursive(GAME, true); - unsigned int total = (int)list.size(); - int target = 0; - // get random number in range - if (total == 0) - return NULL; - target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); - return list.at(target); -} - -unsigned int SystemData::getDisplayedGameCount() const -{ - return (unsigned int)mRootFolder->getFilesRecursive(GAME, true).size(); -} - -void SystemData::loadTheme() -{ - mTheme = std::make_shared(); - - std::string path = getThemePath(); - - if(!Utils::FileSystem::exists(path)) // no theme available for this platform - return; - - try - { - // build map with system variables for theme to use, - std::map sysData; - sysData.insert(std::pair("system.name", getName())); - sysData.insert(std::pair("system.theme", getThemeFolder())); - sysData.insert(std::pair("system.fullName", getFullName())); - - mTheme->loadFile(getThemeFolder(), sysData, path); - } - catch(ThemeException& e) - { - LOG(LogError) << e.what(); - mTheme = std::make_shared(); // reset to empty - } -} +#include "SystemData.h" + +#include "utils/FileSystemUtil.h" +#include "CollectionSystemManager.h" +#include "FileFilterIndex.h" +#include "FileSorts.h" +#include "Gamelist.h" +#include "Log.h" +#include "platform.h" +#include "Settings.h" +#include "ThemeData.h" +#include "views/UIModeController.h" +#include +#include +#ifdef WIN32 +#include +#endif + +#include "GuiComponent.h" +#include "Window.h" + +std::vector SystemData::sSystemVector; + +SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : + mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) +{ + mFilterIndex = new FileFilterIndex(); + + // if it's an actual system, initialize it, if not, just create the data structure + if (!CollectionSystem) + { + mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); + mRootFolder->metadata.set("name", mFullName); + + if (!Settings::getInstance()->getBool("ParseGamelistOnly")) + { + populateFolder(mRootFolder); + if (mRootFolder->getChildren().size() == 0) + return; + } + + if (!Settings::getInstance()->getBool("IgnoreGamelist")) + parseGamelist(this); + + mRootFolder->sort(FileSorts::SortTypes.at(0)); + + indexAllGameFilters(mRootFolder); + } + else + { + // virtual systems are updated afterwards, we're just creating the data structure + mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this); + } + setIsGameSystemStatus(); + loadTheme(); +} + +SystemData::~SystemData() +{ + delete mRootFolder; + delete mFilterIndex; +} + +void SystemData::setIsGameSystemStatus() +{ + // we exclude non-game systems from specific operations (i.e. the "RetroPie" system, at least) + // if/when there are more in the future, maybe this can be a more complex method, with a proper list + // but for now a simple string comparison is more performant + mIsGameSystem = (mName != "retropie"); +} + +char _easytolower(char in) { + if (in <= 'Z' && in >= 'A') + return in - ('Z' - 'z'); + return in; +} + +void SystemData::populateFolder(FileData* folder) +{ + const std::string& folderPath = folder->getPath(); + if(!Utils::FileSystem::isDirectory(folderPath)) + { + LOG(LogWarning) << "Error - folder with path \"" << folderPath << "\" is not a directory!"; + return; + } + + int di = folderPath.rfind("downloaded_images"); + int md = folderPath.rfind("media"); + if (di > 0 || md > 0) + return; + + //make sure that this isn't a symlink to a thing we already have + if(Utils::FileSystem::isSymlink(folderPath)) + { + //if this symlink resolves to somewhere that's at the beginning of our path, it's gonna recurse + if(folderPath.find(Utils::FileSystem::getCanonicalPath(folderPath)) == 0) + { + LOG(LogWarning) << "Skipping infinitely recursive symlink \"" << folderPath << "\""; + return; + } + } + + std::string filePath; + std::string extension; + bool isGame; + bool showHidden = Settings::getInstance()->getBool("ShowHiddenFiles"); + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(folderPath); + for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + filePath = *it; + + // skip hidden files and folders + if(!showHidden && Utils::FileSystem::isHidden(filePath)) + 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 = Utils::FileSystem::getExtension(filePath); + std::transform(extension.begin(), extension.end(), extension.begin(), ::_easytolower); + + //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(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) + { + FileData* newGame = new FileData(GAME, filePath, mEnvData, this); + + // preventing new arcade assets to be added + if(!newGame->isArcadeAsset()) + { + folder->addChild(newGame); + isGame = true; + } + } + + //add directories that also do not match an extension as folders + if(!isGame && Utils::FileSystem::isDirectory(filePath)) + { + if (filePath.rfind("downloaded_images") == std::string::npos && + filePath.rfind("media") == std::string::npos) + { + FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); + populateFolder(newFolder); + + if (newFolder->getChildrenByFilename().size() == 0) + delete newFolder; + else if (newFolder->findUniqueGameForFolder() != NULL) + delete newFolder; + else + folder->addChild(newFolder); + } + } + } +} + +void SystemData::indexAllGameFilters(const FileData* folder) +{ + const std::vector& children = folder->getChildren(); + + for(std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + switch((*it)->getType()) + { + case GAME: { mFilterIndex->addToIndex(*it); } break; + case FOLDER: { indexAllGameFilters(*it); } break; + } + } +} + +std::vector readList(const std::string& str, const char* delims = " \t\r\n,") +{ + std::vector 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(Window* window) +{ + deleteSystems(); + + std::string path = getConfigPath(false); + + LOG(LogInfo) << "Loading system config file " << path << "..."; + + if(!Utils::FileSystem::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 tag!"; + return false; + } + + float systemCount = 1; + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + systemCount++; + + float currentSystem = 0; + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { + std::vector emulatorList; + + // std::vector coreList; + + std::string name, fullname, path, cmd, themeFolder, defaultCore; + + name = system.child("name").text().get(); + fullname = system.child("fullname").text().get(); + path = system.child("path").text().get(); + defaultCore = system.child("defaultCore").text().get(); + + pugi::xml_node emulators = system.child("emulators"); + if (emulators != NULL) + { + for (pugi::xml_node emulator : emulators.children()) + { + EmulatorData emulatorData; + emulatorData.mName = emulator.attribute("name").value(); + emulatorData.mCommandLine = emulator.attribute("command").value(); + + pugi::xml_node cores = emulator.child("cores"); + if (cores != NULL) + { + for (pugi::xml_node core : cores.children()) + { + const std::string& corename = core.text().get(); + + if (defaultCore.length() == 0) + defaultCore = corename; + + emulatorData.mCores.push_back(corename); + // coreList.push_back(corename); + } + } + + emulatorList.push_back(emulatorData); + } + } + + if (window != NULL) + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; + + // convert extensions list from a string into a vector of strings + + std::vector list = readList(system.child("extension").text().get()); + std::vector extensions; + + for (auto extension = list.cbegin(); extension != list.cend(); extension++) + { + std::string xt = (*extension); + std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); + + if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) + extensions.push_back(xt); + } + + cmd = system.child("command").text().get(); + + // platform id list + const char* platformList = system.child("platform").text().get(); + std::vector platformStrs = readList(platformList); + std::vector platformIds; + for(auto it = platformStrs.cbegin(); it != platformStrs.cend(); 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 + path = Utils::FileSystem::getGenericPath(path); + + //expand home symbol if the startpath contains ~ + if (path[0] == '~') + { + path.erase(0, 1); + path.insert(0, Utils::FileSystem::getHomePath()); + } + + //create the system runtime environment data + SystemEnvironmentData* envData = new SystemEnvironmentData; + envData->mStartPath = path; + envData->mSearchExtensions = extensions; + envData->mLaunchCommand = cmd; + envData->mPlatformIds = platformIds; + // envData->mDefaultCore = defaultCore; + envData->mEmulators = emulatorList; + + // envData->mCores = coreList; + + SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); + if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) + { + LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; + delete newSys; + } + else + sSystemVector.push_back(newSys); + } + + if (window != NULL) + window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; + + CollectionSystemManager::get()->loadCollectionSystems(); + return true; +} + +void SystemData::writeExampleConfig(const std::string& path) +{ + std::ofstream file(path.c_str()); + + file << "\n" + "\n" + "\n" + " \n" + " \n" + "\n" + " \n" + " nes\n" + "\n" + " \n" + " Nintendo Entertainment System\n" + "\n" + " \n" + " ~/roms/nes\n" + "\n" + " \n" + " .nes .NES\n" + "\n" + " \n" + " retroarch -L ~/cores/libretro-fceumm.so %ROM%\n" + "\n" + " \n" + " nes\n" + "\n" + " \n" + " nes\n" + " \n" + "\n"; + + file.close(); + + LOG(LogError) << "Example config written! Go read it at \"" << path << "\"!"; +} + +void SystemData::deleteSystems() +{ + bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); + + for(unsigned int i = 0; i < sSystemVector.size(); i++) + { + SystemData* pData = sSystemVector.at(i); + + if (saveOnExit && !pData->mIsCollectionSystem) + updateGamelist(pData); + + delete pData; + } + + sSystemVector.clear(); +} + +std::string SystemData::getConfigPath(bool forWrite) +{ + std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_systems.cfg"; + if(forWrite || Utils::FileSystem::exists(path)) + return path; + + return "/etc/emulationstation/es_systems.cfg"; +} + +bool SystemData::isVisible() +{ + return (getDisplayedGameCount() > 0 || + (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || + (mIsCollectionSystem && mName == "favorites")); +} + +SystemData* SystemData::getNext() const +{ + std::vector::const_iterator it = getIterator(); + + do { + it++; + if (it == sSystemVector.cend()) + it = sSystemVector.cbegin(); + } while (!(*it)->isVisible()); + // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. + + return *it; +} + +SystemData* SystemData::getPrev() const +{ + std::vector::const_reverse_iterator it = getRevIterator(); + + do { + it++; + if (it == sSystemVector.crend()) + it = sSystemVector.crbegin(); + } while (!(*it)->isVisible()); + // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. + + return *it; +} + +std::string SystemData::getGamelistPath(bool forWrite) const +{ + std::string filePath; + + filePath = mRootFolder->getPath() + "/gamelist.xml"; + if(Utils::FileSystem::exists(filePath)) + return filePath; + + filePath = Utils::FileSystem::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 + Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath)); + if(forWrite || Utils::FileSystem::exists(filePath)) + return filePath; + + 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. system theme from currently selected theme set [CURRENT_THEME_PATH]/[SYSTEM]/theme.xml + // 3. default system theme from currently selected theme set [CURRENT_THEME_PATH]/theme.xml + + // first, check game folder + std::string localThemePath = mRootFolder->getPath() + "/theme.xml"; + if(Utils::FileSystem::exists(localThemePath)) + return localThemePath; + + // not in game folder, try system theme in theme sets + localThemePath = ThemeData::getThemeFromCurrentSet(mThemeFolder); + + if (Utils::FileSystem::exists(localThemePath)) + return localThemePath; + + // not system theme, try default system theme in theme set + localThemePath = Utils::FileSystem::getParent(Utils::FileSystem::getParent(localThemePath)) + "/theme.xml"; + + return localThemePath; +} + +bool SystemData::hasGamelist() const +{ + return (Utils::FileSystem::exists(getGamelistPath(false))); +} + +unsigned int SystemData::getGameCount() const +{ + return (unsigned int)mRootFolder->getFilesRecursive(GAME).size(); +} + +SystemData* SystemData::getRandomSystem() +{ + // this is a bit brute force. It might be more efficient to just to a while (!gameSystem) do random again... + unsigned int total = 0; + for(auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) + { + if ((*it)->isGameSystem()) + total ++; + } + + // get random number in range + int target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); + for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) + { + if ((*it)->isGameSystem()) + { + if (target > 0) + { + target--; + } + else + { + return (*it); + } + } + } + + // if we end up here, there is no valid system + return NULL; +} + +FileData* SystemData::getRandomGame() +{ + std::vector list = mRootFolder->getFilesRecursive(GAME, true); + unsigned int total = (int)list.size(); + int target = 0; + // get random number in range + if (total == 0) + return NULL; + target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); + return list.at(target); +} + +unsigned int SystemData::getDisplayedGameCount() const +{ + return (unsigned int)mRootFolder->getFilesRecursive(GAME, true).size(); +} + +void SystemData::loadTheme() +{ + mTheme = std::make_shared(); + + std::string path = getThemePath(); + + if(!Utils::FileSystem::exists(path)) // no theme available for this platform + return; + + try + { + // build map with system variables for theme to use, + std::map sysData; + sysData.insert(std::pair("system.name", getName())); + sysData.insert(std::pair("system.theme", getThemeFolder())); + sysData.insert(std::pair("system.fullName", getFullName())); + + mTheme->loadFile(getThemeFolder(), sysData, path); + } + catch(ThemeException& e) + { + LOG(LogError) << e.what(); + mTheme = std::make_shared(); // reset to empty + } +} diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 3925b74961..fd0db7a977 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -33,42 +33,42 @@ struct SystemEnvironmentData std::vector getCores(std::string emulatorName) { - std::vector list; - - for (auto& emulator : mEmulators) - if (emulatorName == emulator.mName) - return emulator.mCores; + std::vector list; + + for (auto& emulator : mEmulators) + if (emulatorName == emulator.mName) + return emulator.mCores; return list; } std::string getDefaultEmulator() { - for (auto& emulator : mEmulators) - return emulator.mName; + for (auto& emulator : mEmulators) + return emulator.mName; return ""; } std::string getDefaultCore(std::string emulatorName) - { - for (auto& emulator : mEmulators) - { - if (emulatorName == emulator.mName) - { - for (auto core : emulator.mCores) - return core; - } - } + { + for (auto& emulator : mEmulators) + { + if (emulatorName == emulator.mName) + { + for (auto core : emulator.mCores) + return core; + } + } return ""; } std::string getEmulatorCommandLine(std::string emulatorName) - { - for (auto& emulator : mEmulators) - if (emulatorName == emulator.mName) - return emulator.mCommandLine; + { + for (auto& emulator : mEmulators) + if (emulatorName == emulator.mName) + return emulator.mCommandLine; return ""; } diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 01c0741c21..fd42411e4a 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -124,13 +124,13 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui float x0 = (mSize.x() - mMenu.getSize().x()) / 2; - float y1 = Renderer::getScreenHeight(); + float y1 = Renderer::getScreenHeight(); float y2 = (mSize.y() - mMenu.getSize().y()) / 2; - //float y1 = mMenu.getSize().y(); - //float y2 = (mSize.y() - mMenu.getSize().y()) / 2; - - setPosition(x0, y1); - + //float y1 = mMenu.getSize().y(); + //float y2 = (mSize.y() - mMenu.getSize().y()) / 2; + + setPosition(x0, y1); + auto fadeFunc = [this, x0, y1, y2](float t) { t -= 1; // cubic ease out @@ -142,9 +142,9 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, x0, y2] { - setPosition(x0, y2); - }); - + setPosition(x0, y2); + }); + setPosition(x0, y2); } diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index a383ee2ff4..beaf8e8749 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -1,64 +1,64 @@ -#include "guis/GuiMenu.h" - -#include "components/OptionListComponent.h" -#include "components/SliderComponent.h" -#include "components/SwitchComponent.h" -#include "guis/GuiCollectionSystemsOptions.h" -#include "guis/GuiDetectDevice.h" -#include "guis/GuiGeneralScreensaverOptions.h" -#include "guis/GuiMsgBox.h" -#include "guis/GuiScraperStart.h" -#include "guis/GuiSettings.h" -#include "views/UIModeController.h" -#include "views/ViewController.h" -#include "CollectionSystemManager.h" -#include "EmulationStation.h" -#include "Scripting.h" -#include "SystemData.h" -#include "VolumeControl.h" -#include -#include - -#include "animations/LambdaAnimation.h" - -GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) - -{ - bool isFullUI = UIModeController::getInstance()->isUIModeFull(); - - if (isFullUI) - { - addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); - addEntry(_T("CONFIGURE INPUT"), 0x777777FF, true, [this] { openConfigInput(); }); - } - - addEntry(_T("SOUND SETTINGS"), 0x777777FF, true, [this] { openSoundSettings(); }); - - if (isFullUI) - addEntry(_T("SCRAPER"), 0x777777FF, true, [this] { openScraperSettings(); }); - - if (isFullUI) - { - addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); - addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); - } - -#if defined(_WIN32) - addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); -#else - addEntry(_T("QUIT"), 0x777777FF, true, [this] {openQuitMenu(); }); -#endif - - addChild(&mMenu); - addVersionInfo(); - - setSize(mMenu.getSize()); - - float y1 = Renderer::getScreenHeight(); - float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; - - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); - +#include "guis/GuiMenu.h" + +#include "components/OptionListComponent.h" +#include "components/SliderComponent.h" +#include "components/SwitchComponent.h" +#include "guis/GuiCollectionSystemsOptions.h" +#include "guis/GuiDetectDevice.h" +#include "guis/GuiGeneralScreensaverOptions.h" +#include "guis/GuiMsgBox.h" +#include "guis/GuiScraperStart.h" +#include "guis/GuiSettings.h" +#include "views/UIModeController.h" +#include "views/ViewController.h" +#include "CollectionSystemManager.h" +#include "EmulationStation.h" +#include "Scripting.h" +#include "SystemData.h" +#include "VolumeControl.h" +#include +#include + +#include "animations/LambdaAnimation.h" + +GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) + +{ + bool isFullUI = UIModeController::getInstance()->isUIModeFull(); + + if (isFullUI) + { + addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); + addEntry(_T("CONFIGURE INPUT"), 0x777777FF, true, [this] { openConfigInput(); }); + } + + addEntry(_T("SOUND SETTINGS"), 0x777777FF, true, [this] { openSoundSettings(); }); + + if (isFullUI) + addEntry(_T("SCRAPER"), 0x777777FF, true, [this] { openScraperSettings(); }); + + if (isFullUI) + { + addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); + addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); + } + +#if defined(_WIN32) + addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); +#else + addEntry(_T("QUIT"), 0x777777FF, true, [this] {openQuitMenu(); }); +#endif + + addChild(&mMenu); + addVersionInfo(); + + setSize(mMenu.getSize()); + + float y1 = Renderer::getScreenHeight(); + float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); + auto fadeFunc = [this, y1, y2](float t) { t -= 1; // cubic ease out @@ -70,648 +70,648 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] { - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); - }); - - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); -} - -void GuiMenu::openScraperSettings() -{ - auto s = new GuiSettings(mWindow, "SCRAPER"); - - // scrape from - auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, "SCRAPE FROM", false); - std::vector scrapers = getScraperList(); - - // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. - for(auto it = scrapers.cbegin(); it != scrapers.cend(); 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(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; - auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; - std::function openAndSave = openScrapeNow; - openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; - row.makeAcceptInputHandler(openAndSave); - - auto scrape_now = std::make_shared(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); -} - -void GuiMenu::openSoundSettings() -{ - auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); - - // volume - auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); - volume->setValue((float)VolumeControl::getInstance()->getVolume()); - s->addWithLabel(_T("SYSTEM VOLUME"), volume); - s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); - - if (UIModeController::getInstance()->isUIModeFull()) - { -#if defined(__linux__) - // audio card - auto audio_card = std::make_shared< OptionListComponent >(mWindow, "AUDIO CARD", false); - std::vector audio_cards; - #ifdef _RPI_ - // RPi Specific Audio Cards - audio_cards.push_back("local"); - audio_cards.push_back("hdmi"); - audio_cards.push_back("both"); - #endif - audio_cards.push_back("default"); - audio_cards.push_back("sysdefault"); - audio_cards.push_back("dmix"); - audio_cards.push_back("hw"); - audio_cards.push_back("plughw"); - audio_cards.push_back("null"); - if (Settings::getInstance()->getString("AudioCard") != "") { - if(std::find(audio_cards.begin(), audio_cards.end(), Settings::getInstance()->getString("AudioCard")) == audio_cards.end()) { - audio_cards.push_back(Settings::getInstance()->getString("AudioCard")); - } - } - for(auto ac = audio_cards.cbegin(); ac != audio_cards.cend(); ac++) - audio_card->add(*ac, *ac, Settings::getInstance()->getString("AudioCard") == *ac); - s->addWithLabel("AUDIO CARD", audio_card); - s->addSaveFunc([audio_card] { - Settings::getInstance()->setString("AudioCard", audio_card->getSelected()); - VolumeControl::getInstance()->deinit(); - VolumeControl::getInstance()->init(); - }); - - // volume control device - auto vol_dev = std::make_shared< OptionListComponent >(mWindow, "AUDIO DEVICE", false); - std::vector transitions; - transitions.push_back("PCM"); - transitions.push_back("Speaker"); - transitions.push_back("Master"); - transitions.push_back("Digital"); - transitions.push_back("Analogue"); - if (Settings::getInstance()->getString("AudioDevice") != "") { - if(std::find(transitions.begin(), transitions.end(), Settings::getInstance()->getString("AudioDevice")) == transitions.end()) { - transitions.push_back(Settings::getInstance()->getString("AudioDevice")); - } - } - for(auto it = transitions.cbegin(); it != transitions.cend(); it++) - vol_dev->add(*it, *it, Settings::getInstance()->getString("AudioDevice") == *it); - s->addWithLabel("AUDIO DEVICE", vol_dev); - s->addSaveFunc([vol_dev] { - Settings::getInstance()->setString("AudioDevice", vol_dev->getSelected()); - VolumeControl::getInstance()->deinit(); - VolumeControl::getInstance()->init(); - }); -#endif - - // disable sounds - auto sounds_enabled = std::make_shared(mWindow); - sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); - s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); - s->addSaveFunc([sounds_enabled] { - if (sounds_enabled->getState() - && !Settings::getInstance()->getBool("EnableSounds") - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState()); - }); - - auto video_audio = std::make_shared(mWindow); - video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); - s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); - s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); - -#ifdef _RPI_ - // OMX player Audio Device - auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, "OMX PLAYER AUDIO DEVICE", false); - std::vector omx_cards; - // RPi Specific Audio Cards - omx_cards.push_back("local"); - omx_cards.push_back("hdmi"); - omx_cards.push_back("both"); - omx_cards.push_back("alsa:hw:0,0"); - omx_cards.push_back("alsa:hw:1,0"); - if (Settings::getInstance()->getString("OMXAudioDev") != "") { - if (std::find(omx_cards.begin(), omx_cards.end(), Settings::getInstance()->getString("OMXAudioDev")) == omx_cards.end()) { - omx_cards.push_back(Settings::getInstance()->getString("OMXAudioDev")); - } - } - for (auto it = omx_cards.cbegin(); it != omx_cards.cend(); it++) - omx_audio_dev->add(*it, *it, Settings::getInstance()->getString("OMXAudioDev") == *it); - s->addWithLabel("OMX PLAYER AUDIO DEVICE", omx_audio_dev); - s->addSaveFunc([omx_audio_dev] { - if (Settings::getInstance()->getString("OMXAudioDev") != omx_audio_dev->getSelected()) - Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); - }); -#endif - } - - mWindow->pushGui(s); - -} - -void GuiMenu::openUISettings() -{ - auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); - - // theme set - auto themeSets = ThemeData::getThemeSets(); - - if (!themeSets.empty()) - { - std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if (selectedSet == themeSets.cend()) - selectedSet = themeSets.cbegin(); - - auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); - for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) - theme_set->add(it->first, it->first, it == selectedSet); - s->addWithLabel(_T("THEME"), theme_set); - - Window* window = mWindow; - s->addSaveFunc([window, theme_set] - { - bool needReload = false; - std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - if (oldTheme != theme_set->getSelected()) - needReload = true; - - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - - if (needReload) - { - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation - } - }); - } - - // LANGUAGE - - std::vector langues; - langues.push_back("en"); - - - std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); - if (xmlpath.length() > 0) - { - xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; - - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); - for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if (Utils::FileSystem::isDirectory(*it)) - continue; - - std::string name = *it; - - if (name.rfind("emulationstation2.po") == std::string::npos) - continue; - - name = Utils::FileSystem::getParent(name); - name = Utils::FileSystem::getFileName(name); - - if (name != "en") - langues.push_back(name); - } - - if (langues.size() > 1) - { - auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); - - for (auto it = langues.cbegin(); it != langues.cend(); it++) - language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); - - s->addWithLabel(_T("LANGUAGE"), language); - s->addSaveFunc([language] { - Settings::getInstance()->setString("Language", language->getSelected()); - }); - } - } - - // transition style - auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); - std::vector transitions; - transitions.push_back("fade"); - transitions.push_back("slide"); - transitions.push_back("instant"); - for (auto it = transitions.cbegin(); it != transitions.cend(); it++) - transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); - - s->addWithLabel(_T("TRANSITION STYLE"), transition_style); - s->addSaveFunc([transition_style] { - if (Settings::getInstance()->getString("TransitionStyle") == "instant" - && transition_style->getSelected() != "instant" - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); - }); - - - auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); - std::vector gameTransitions; - gameTransitions.push_back("fade"); - gameTransitions.push_back("slide"); - gameTransitions.push_back("instant"); - for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) - transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); - - s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); - s->addSaveFunc([transitionOfGames_style] { - if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" - && transitionOfGames_style->getSelected() != "instant" - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); - }); - - // GameList view style - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); - std::vector styles; - styles.push_back("automatic"); - styles.push_back("basic"); - styles.push_back("detailed"); - styles.push_back("video"); - styles.push_back("grid"); - styles.push_back("gridex"); - - for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); - - s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); - s->addSaveFunc([gamelist_style] { - bool needReload = false; - if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) - needReload = true; - Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); - if (needReload) - ViewController::get()->reloadAll(); - }); - - - // Optionally start in selected system - auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); - systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); - for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) - { - if ("retropie" != (*it)->getName()) - { - systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); - } - } - s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); - s->addSaveFunc([systemfocus_list] { - Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); - }); - - - /* - //UI mode - auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); - std::vector UImodes = UIModeController::getInstance()->getUIModes(); - for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) - UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); - s->addWithLabel("UI MODE", UImodeSelection); - Window* window = mWindow; - s->addSaveFunc([ UImodeSelection, window] - { - std::string selectedMode = UImodeSelection->getSelected(); - if (selectedMode != "Full") - { - std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; - msg += "This will hide most menu-options to prevent changes to the system.\n"; - msg += "To unlock and return to the full UI, enter this code: \n"; - msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; - msg += "Do you want to proceed?"; - window->pushGui(new GuiMsgBox(window, msg, - "YES", [selectedMode] { - LOG(LogDebug) << "Setting UI mode to " << selectedMode; - Settings::getInstance()->setString("UIMode", selectedMode); - Settings::getInstance()->saveFile(); - }, "NO",nullptr)); - } - }); - */ - - // screensaver - ComponentListRow screensaver_row; - screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - screensaver_row.addElement(makeArrow(mWindow), false); - screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); - s->addRow(screensaver_row); - -#if defined(_WIN32) - // quick system select (left/right in game list view) - auto hideWindowScreen = std::make_shared(mWindow); - hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); - s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); - s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); -#endif - - // quick system select (left/right in game list view) - auto quick_sys_select = std::make_shared(mWindow); - quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); - s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); - s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); - - // carousel transition option - auto move_carousel = std::make_shared(mWindow); - move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); - s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); - s->addSaveFunc([move_carousel] { - if (move_carousel->getState() - && !Settings::getInstance()->getBool("MoveCarousel") - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); - }); - - // show help - auto show_help = std::make_shared(mWindow); - show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); - s->addWithLabel(_T("ON-SCREEN HELP"), show_help); - s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); - - // enable filters (ForceDisableFilters) - auto enable_filter = std::make_shared(mWindow); - enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); - s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); - s->addSaveFunc([enable_filter] { - bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); - Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); - if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); - }); - - mWindow->pushGui(s); - -} - -void GuiMenu::openOtherSettings() -{ - auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); - - // maximum vram - auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); - max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel(_T("VRAM LIMIT"), max_vram); - s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); - - // power saver - auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); - std::vector modes; - modes.push_back("disabled"); - modes.push_back("default"); - modes.push_back("enhanced"); - modes.push_back("instant"); - for (auto it = modes.cbegin(); it != modes.cend(); it++) - power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); - - s->addWithLabel(_T("POWER SAVER MODES"), power_saver); - s->addSaveFunc([this, power_saver] { - if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { - Settings::getInstance()->setString("TransitionStyle", "instant"); - Settings::getInstance()->setBool("MoveCarousel", false); - Settings::getInstance()->setBool("EnableSounds", false); - } - Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); - PowerSaver::init(); - }); - - // gamelists - auto save_gamelists = std::make_shared(mWindow); - save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); - s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); - s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); - - auto parse_gamelists = std::make_shared(mWindow); - parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); - s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); - s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); - /* - auto local_art = std::make_shared(mWindow); - local_art->setState(Settings::getInstance()->getBool("LocalArt")); - s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); - s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); - */ - // hidden files - auto hidden_files = std::make_shared(mWindow); - hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); - s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); - s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); - -#ifdef _RPI_ - // Video Player - VideoOmxPlayer - auto omx_player = std::make_shared(mWindow); - omx_player->setState(Settings::getInstance()->getBool("VideoOmxPlayer")); - s->addWithLabel("USE OMX PLAYER (HW ACCELERATED)", omx_player); - s->addSaveFunc([omx_player] - { - // need to reload all views to re-create the right video components - bool needReload = false; - if(Settings::getInstance()->getBool("VideoOmxPlayer") != omx_player->getState()) - needReload = true; - - Settings::getInstance()->setBool("VideoOmxPlayer", omx_player->getState()); - - if(needReload) - ViewController::get()->reloadAll(); - }); - -#endif - - // framerate - /* - auto framerate = std::make_shared(mWindow); - framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); - s->addWithLabel(_T("SHOW FRAMERATE"), framerate); - s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); - */ - - mWindow->pushGui(s); - -} - -void GuiMenu::openConfigInput() -{ - Window* window = mWindow; - window->pushGui(new GuiDetectDevice(window, false, nullptr)); - /* - window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), - [window] { - window->pushGui(new GuiDetectDevice(window, false, nullptr)); - }, _T("NO"), nullptr) - );*/ - -} - -void GuiMenu::openQuitMenu() -{ -#if defined(_WIN32) - Scripting::fireEvent("quit"); - quitES(""); - return; -#endif - - auto s = new GuiSettings(mWindow, _T("QUIT")); - - Window* window = mWindow; - - ComponentListRow row; - if (UIModeController::getInstance()->isUIModeFull()) - { - row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", - [] { - Scripting::fireEvent("quit"); - if(quitES("/tmp/es-restart") != 0) - LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", 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", - [] { - Scripting::fireEvent("quit"); - quitES(""); - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - s->addRow(row); - } - } - row.elements.clear(); - row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", - [] { - Scripting::fireEvent("quit", "reboot"); - Scripting::fireEvent("reboot"); - if (quitES("/tmp/es-sysrestart") != 0) - LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(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", - [] { - Scripting::fireEvent("quit", "shutdown"); - Scripting::fireEvent("shutdown"); - if (quitES("/tmp/es-shutdown") != 0) - LOG(LogWarning) << "Shutdown terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - s->addRow(row); - - mWindow->pushGui(s); -} - -void GuiMenu::addVersionInfo() -{ - std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); - - mVersion.setFont(Font::get(FONT_SIZE_SMALL)); - mVersion.setColor(0x5E5E5EFF); - mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); - mVersion.setHorizontalAlignment(ALIGN_CENTER); - addChild(&mVersion); -} - -void GuiMenu::openScreensaverOptions() { - mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); -} - -void GuiMenu::openCollectionSystemSettings() { - mWindow->pushGui(new GuiCollectionSystemsOptions(mWindow)); -} - -void GuiMenu::onSizeChanged() -{ - mVersion.setSize(mSize.x(), 0); - mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); -} - -void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) -{ - std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); - - // populate the list - ComponentListRow row; - row.addElement(std::make_shared(mWindow, name, font, color), true); - - if(add_arrow) - { - std::shared_ptr 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; -} - -HelpStyle GuiMenu::getHelpStyle() -{ - HelpStyle style = HelpStyle(); - style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); - return style; -} - -std::vector GuiMenu::getHelpPrompts() -{ - std::vector prompts; - prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); - prompts.push_back(HelpPrompt("a", _T("SELECT"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); - return prompts; -} + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + }); + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); +} + +void GuiMenu::openScraperSettings() +{ + auto s = new GuiSettings(mWindow, "SCRAPER"); + + // scrape from + auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, "SCRAPE FROM", false); + std::vector scrapers = getScraperList(); + + // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. + for(auto it = scrapers.cbegin(); it != scrapers.cend(); 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(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; + auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; + std::function openAndSave = openScrapeNow; + openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; + row.makeAcceptInputHandler(openAndSave); + + auto scrape_now = std::make_shared(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); +} + +void GuiMenu::openSoundSettings() +{ + auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); + + // volume + auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); + volume->setValue((float)VolumeControl::getInstance()->getVolume()); + s->addWithLabel(_T("SYSTEM VOLUME"), volume); + s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); + + if (UIModeController::getInstance()->isUIModeFull()) + { +#if defined(__linux__) + // audio card + auto audio_card = std::make_shared< OptionListComponent >(mWindow, "AUDIO CARD", false); + std::vector audio_cards; + #ifdef _RPI_ + // RPi Specific Audio Cards + audio_cards.push_back("local"); + audio_cards.push_back("hdmi"); + audio_cards.push_back("both"); + #endif + audio_cards.push_back("default"); + audio_cards.push_back("sysdefault"); + audio_cards.push_back("dmix"); + audio_cards.push_back("hw"); + audio_cards.push_back("plughw"); + audio_cards.push_back("null"); + if (Settings::getInstance()->getString("AudioCard") != "") { + if(std::find(audio_cards.begin(), audio_cards.end(), Settings::getInstance()->getString("AudioCard")) == audio_cards.end()) { + audio_cards.push_back(Settings::getInstance()->getString("AudioCard")); + } + } + for(auto ac = audio_cards.cbegin(); ac != audio_cards.cend(); ac++) + audio_card->add(*ac, *ac, Settings::getInstance()->getString("AudioCard") == *ac); + s->addWithLabel("AUDIO CARD", audio_card); + s->addSaveFunc([audio_card] { + Settings::getInstance()->setString("AudioCard", audio_card->getSelected()); + VolumeControl::getInstance()->deinit(); + VolumeControl::getInstance()->init(); + }); + + // volume control device + auto vol_dev = std::make_shared< OptionListComponent >(mWindow, "AUDIO DEVICE", false); + std::vector transitions; + transitions.push_back("PCM"); + transitions.push_back("Speaker"); + transitions.push_back("Master"); + transitions.push_back("Digital"); + transitions.push_back("Analogue"); + if (Settings::getInstance()->getString("AudioDevice") != "") { + if(std::find(transitions.begin(), transitions.end(), Settings::getInstance()->getString("AudioDevice")) == transitions.end()) { + transitions.push_back(Settings::getInstance()->getString("AudioDevice")); + } + } + for(auto it = transitions.cbegin(); it != transitions.cend(); it++) + vol_dev->add(*it, *it, Settings::getInstance()->getString("AudioDevice") == *it); + s->addWithLabel("AUDIO DEVICE", vol_dev); + s->addSaveFunc([vol_dev] { + Settings::getInstance()->setString("AudioDevice", vol_dev->getSelected()); + VolumeControl::getInstance()->deinit(); + VolumeControl::getInstance()->init(); + }); +#endif + + // disable sounds + auto sounds_enabled = std::make_shared(mWindow); + sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); + s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); + s->addSaveFunc([sounds_enabled] { + if (sounds_enabled->getState() + && !Settings::getInstance()->getBool("EnableSounds") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState()); + }); + + auto video_audio = std::make_shared(mWindow); + video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); + s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); + s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); + +#ifdef _RPI_ + // OMX player Audio Device + auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, "OMX PLAYER AUDIO DEVICE", false); + std::vector omx_cards; + // RPi Specific Audio Cards + omx_cards.push_back("local"); + omx_cards.push_back("hdmi"); + omx_cards.push_back("both"); + omx_cards.push_back("alsa:hw:0,0"); + omx_cards.push_back("alsa:hw:1,0"); + if (Settings::getInstance()->getString("OMXAudioDev") != "") { + if (std::find(omx_cards.begin(), omx_cards.end(), Settings::getInstance()->getString("OMXAudioDev")) == omx_cards.end()) { + omx_cards.push_back(Settings::getInstance()->getString("OMXAudioDev")); + } + } + for (auto it = omx_cards.cbegin(); it != omx_cards.cend(); it++) + omx_audio_dev->add(*it, *it, Settings::getInstance()->getString("OMXAudioDev") == *it); + s->addWithLabel("OMX PLAYER AUDIO DEVICE", omx_audio_dev); + s->addSaveFunc([omx_audio_dev] { + if (Settings::getInstance()->getString("OMXAudioDev") != omx_audio_dev->getSelected()) + Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); + }); +#endif + } + + mWindow->pushGui(s); + +} + +void GuiMenu::openUISettings() +{ + auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); + + // theme set + auto themeSets = ThemeData::getThemeSets(); + + if (!themeSets.empty()) + { + std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if (selectedSet == themeSets.cend()) + selectedSet = themeSets.cbegin(); + + auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); + for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) + theme_set->add(it->first, it->first, it == selectedSet); + s->addWithLabel(_T("THEME"), theme_set); + + Window* window = mWindow; + s->addSaveFunc([window, theme_set] + { + bool needReload = false; + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + if (oldTheme != theme_set->getSelected()) + needReload = true; + + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); + + if (needReload) + { + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + } + }); + } + + // LANGUAGE + + std::vector langues; + langues.push_back("en"); + + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); + if (xmlpath.length() > 0) + { + xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if (Utils::FileSystem::isDirectory(*it)) + continue; + + std::string name = *it; + + if (name.rfind("emulationstation2.po") == std::string::npos) + continue; + + name = Utils::FileSystem::getParent(name); + name = Utils::FileSystem::getFileName(name); + + if (name != "en") + langues.push_back(name); + } + + if (langues.size() > 1) + { + auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); + + for (auto it = langues.cbegin(); it != langues.cend(); it++) + language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); + + s->addWithLabel(_T("LANGUAGE"), language); + s->addSaveFunc([language] { + Settings::getInstance()->setString("Language", language->getSelected()); + }); + } + } + + // transition style + auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); + std::vector transitions; + transitions.push_back("fade"); + transitions.push_back("slide"); + transitions.push_back("instant"); + for (auto it = transitions.cbegin(); it != transitions.cend(); it++) + transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); + + s->addWithLabel(_T("TRANSITION STYLE"), transition_style); + s->addSaveFunc([transition_style] { + if (Settings::getInstance()->getString("TransitionStyle") == "instant" + && transition_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); + }); + + + auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); + std::vector gameTransitions; + gameTransitions.push_back("fade"); + gameTransitions.push_back("slide"); + gameTransitions.push_back("instant"); + for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) + transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); + + s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); + s->addSaveFunc([transitionOfGames_style] { + if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" + && transitionOfGames_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); + }); + + // GameList view style + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); + std::vector styles; + styles.push_back("automatic"); + styles.push_back("basic"); + styles.push_back("detailed"); + styles.push_back("video"); + styles.push_back("grid"); + styles.push_back("gridex"); + + for (auto it = styles.cbegin(); it != styles.cend(); it++) + gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); + + s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); + s->addSaveFunc([gamelist_style] { + bool needReload = false; + if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) + needReload = true; + Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); + if (needReload) + ViewController::get()->reloadAll(); + }); + + + // Optionally start in selected system + auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); + systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + { + if ("retropie" != (*it)->getName()) + { + systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); + } + } + s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); + s->addSaveFunc([systemfocus_list] { + Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); + }); + + + /* + //UI mode + auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); + std::vector UImodes = UIModeController::getInstance()->getUIModes(); + for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) + UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); + s->addWithLabel("UI MODE", UImodeSelection); + Window* window = mWindow; + s->addSaveFunc([ UImodeSelection, window] + { + std::string selectedMode = UImodeSelection->getSelected(); + if (selectedMode != "Full") + { + std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; + msg += "This will hide most menu-options to prevent changes to the system.\n"; + msg += "To unlock and return to the full UI, enter this code: \n"; + msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; + msg += "Do you want to proceed?"; + window->pushGui(new GuiMsgBox(window, msg, + "YES", [selectedMode] { + LOG(LogDebug) << "Setting UI mode to " << selectedMode; + Settings::getInstance()->setString("UIMode", selectedMode); + Settings::getInstance()->saveFile(); + }, "NO",nullptr)); + } + }); + */ + + // screensaver + ComponentListRow screensaver_row; + screensaver_row.elements.clear(); + screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + screensaver_row.addElement(makeArrow(mWindow), false); + screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); + s->addRow(screensaver_row); + +#if defined(_WIN32) + // quick system select (left/right in game list view) + auto hideWindowScreen = std::make_shared(mWindow); + hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); + s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); + s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); +#endif + + // quick system select (left/right in game list view) + auto quick_sys_select = std::make_shared(mWindow); + quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); + s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); + s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); + + // carousel transition option + auto move_carousel = std::make_shared(mWindow); + move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); + s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); + s->addSaveFunc([move_carousel] { + if (move_carousel->getState() + && !Settings::getInstance()->getBool("MoveCarousel") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); + }); + + // show help + auto show_help = std::make_shared(mWindow); + show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); + s->addWithLabel(_T("ON-SCREEN HELP"), show_help); + s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); + + // enable filters (ForceDisableFilters) + auto enable_filter = std::make_shared(mWindow); + enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); + s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); + s->addSaveFunc([enable_filter] { + bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); + Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); + if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); + }); + + mWindow->pushGui(s); + +} + +void GuiMenu::openOtherSettings() +{ + auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); + + // maximum vram + auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); + max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); + s->addWithLabel(_T("VRAM LIMIT"), max_vram); + s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); + + // power saver + auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); + std::vector modes; + modes.push_back("disabled"); + modes.push_back("default"); + modes.push_back("enhanced"); + modes.push_back("instant"); + for (auto it = modes.cbegin(); it != modes.cend(); it++) + power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); + + s->addWithLabel(_T("POWER SAVER MODES"), power_saver); + s->addSaveFunc([this, power_saver] { + if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { + Settings::getInstance()->setString("TransitionStyle", "instant"); + Settings::getInstance()->setBool("MoveCarousel", false); + Settings::getInstance()->setBool("EnableSounds", false); + } + Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); + PowerSaver::init(); + }); + + // gamelists + auto save_gamelists = std::make_shared(mWindow); + save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); + s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); + s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); + + auto parse_gamelists = std::make_shared(mWindow); + parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); + s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); + s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); + /* + auto local_art = std::make_shared(mWindow); + local_art->setState(Settings::getInstance()->getBool("LocalArt")); + s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); + s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); + */ + // hidden files + auto hidden_files = std::make_shared(mWindow); + hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); + s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); + s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); + +#ifdef _RPI_ + // Video Player - VideoOmxPlayer + auto omx_player = std::make_shared(mWindow); + omx_player->setState(Settings::getInstance()->getBool("VideoOmxPlayer")); + s->addWithLabel("USE OMX PLAYER (HW ACCELERATED)", omx_player); + s->addSaveFunc([omx_player] + { + // need to reload all views to re-create the right video components + bool needReload = false; + if(Settings::getInstance()->getBool("VideoOmxPlayer") != omx_player->getState()) + needReload = true; + + Settings::getInstance()->setBool("VideoOmxPlayer", omx_player->getState()); + + if(needReload) + ViewController::get()->reloadAll(); + }); + +#endif + + // framerate + /* + auto framerate = std::make_shared(mWindow); + framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); + s->addWithLabel(_T("SHOW FRAMERATE"), framerate); + s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); + */ + + mWindow->pushGui(s); + +} + +void GuiMenu::openConfigInput() +{ + Window* window = mWindow; + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + /* + window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), + [window] { + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + }, _T("NO"), nullptr) + );*/ + +} + +void GuiMenu::openQuitMenu() +{ +#if defined(_WIN32) + Scripting::fireEvent("quit"); + quitES(""); + return; +#endif + + auto s = new GuiSettings(mWindow, _T("QUIT")); + + Window* window = mWindow; + + ComponentListRow row; + if (UIModeController::getInstance()->isUIModeFull()) + { + row.makeAcceptInputHandler([window] { + window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + [] { + Scripting::fireEvent("quit"); + if(quitES("/tmp/es-restart") != 0) + LOG(LogWarning) << "Restart terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", 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", + [] { + Scripting::fireEvent("quit"); + quitES(""); + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + s->addRow(row); + } + } + row.elements.clear(); + row.makeAcceptInputHandler([window] { + window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + [] { + Scripting::fireEvent("quit", "reboot"); + Scripting::fireEvent("reboot"); + if (quitES("/tmp/es-sysrestart") != 0) + LOG(LogWarning) << "Restart terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(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", + [] { + Scripting::fireEvent("quit", "shutdown"); + Scripting::fireEvent("shutdown"); + if (quitES("/tmp/es-shutdown") != 0) + LOG(LogWarning) << "Shutdown terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + s->addRow(row); + + mWindow->pushGui(s); +} + +void GuiMenu::addVersionInfo() +{ + std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); + + mVersion.setFont(Font::get(FONT_SIZE_SMALL)); + mVersion.setColor(0x5E5E5EFF); + mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); + mVersion.setHorizontalAlignment(ALIGN_CENTER); + addChild(&mVersion); +} + +void GuiMenu::openScreensaverOptions() { + mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); +} + +void GuiMenu::openCollectionSystemSettings() { + mWindow->pushGui(new GuiCollectionSystemsOptions(mWindow)); +} + +void GuiMenu::onSizeChanged() +{ + mVersion.setSize(mSize.x(), 0); + mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); +} + +void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) +{ + std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); + + // populate the list + ComponentListRow row; + row.addElement(std::make_shared(mWindow, name, font, color), true); + + if(add_arrow) + { + std::shared_ptr 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; +} + +HelpStyle GuiMenu::getHelpStyle() +{ + HelpStyle style = HelpStyle(); + style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); + return style; +} + +std::vector GuiMenu::getHelpPrompts() +{ + std::vector prompts; + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("SELECT"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + return prompts; +} diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 5f0df4f525..31835bfa1b 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -1,382 +1,382 @@ -#include "guis/GuiMetaDataEd.h" - -#include "components/ButtonComponent.h" -#include "components/ComponentList.h" -#include "components/DateTimeEditComponent.h" -#include "components/MenuComponent.h" -#include "components/RatingComponent.h" -#include "components/SwitchComponent.h" -#include "components/TextComponent.h" -#include "guis/GuiGameScraper.h" -#include "guis/GuiMsgBox.h" -#include "guis/GuiTextEditPopup.h" -#include "resources/Font.h" -#include "utils/StringUtil.h" -#include "views/ViewController.h" -#include "CollectionSystemManager.h" -#include "FileData.h" -#include "FileFilterIndex.h" -#include "SystemData.h" -#include "Window.h" - -GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, - const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : 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(mWindow, Vector2i(1, 5)); - - mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); - mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), - 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(mWindow); - mGrid.setEntry(mList, Vector2i(0, 1), true, true); - - SystemData* system = file->getSystem(); - - auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); - auto core_choice = std::make_shared>(mWindow, _T("CORE"), false); - - // populate list - for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) - { - std::shared_ptr ed; - - // don't add statistics - if(iter->isStatistic) - continue; - - if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || - iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || - iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") - continue; - - // create ed and add it (and any related components) to mMenu - // ed's value will be set below - ComponentListRow row; - - if (iter->displayName == "emulator") - { - // if (file->getSystemEnvData()->mEmulators.size() <= 1) - // continue; - - std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); - - std::string currentEmul = file->getEmulator(); - - if (defaultEmul.length() == 0) - emul_choice->add(_T("DEFAULT"), "", false); - else - emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); - - for (auto core : file->getSystemEnvData()->mEmulators) - emul_choice->add(core.mName, core.mName, core.mName == currentEmul); - - row.addElement(std::make_shared(mWindow, _T("EMULATOR"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(emul_choice, false); - - mList->addRow(row); - emul_choice->setTag(iter->key); - mEditors.push_back(emul_choice); - - emul_choice->setSelectedChangedCallback([this, system, core_choice, file](std::string emulatorName) - { - std::string currentCore = file->getCore(); - - std::string defaultCore = system->getSystemEnvData()->getDefaultCore(emulatorName); - if (emulatorName.length() == 0) - defaultCore = system->getSystemEnvData()->getDefaultCore(system->getSystemEnvData()->getDefaultEmulator()); - - core_choice->clear(); - if (defaultCore.length() == 0) - core_choice->add(_T("DEFAULT"), "", false); - else - core_choice->add(_T("DEFAULT")+" ("+ defaultCore+")", "", false); - - std::vector cores = system->getSystemEnvData()->getCores(emulatorName); - - bool found = false; - - for (auto it = cores.begin(); it != cores.end(); it++) - { - std::string core = *it; - core_choice->add(core, core, currentCore == core); - if (currentCore == core) - found = true; - } - - if (!found) - core_choice->selectFirstItem(); - else - core_choice->invalidate(); - }); - - continue; - } - - if (iter->displayName == "core") - { - // core_choice->add(_T("DEFAULT"), "", true); - core_choice->setTag(iter->key); - - row.addElement(std::make_shared(mWindow, "CORE", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(core_choice, false); - - mList->addRow(row); - ed = core_choice; - - mEditors.push_back(core_choice); - - // force change event to load core list - emul_choice->invalidate(); - continue; - } - - auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); - row.addElement(lbl, true); // label - - switch (iter->type) - { - case MD_BOOL: - { - ed = std::make_shared(window); - row.addElement(ed, false, true); - break; - } - case MD_RATING: - { - ed = std::make_shared(window); - const float height = lbl->getSize().y() * 0.71f; - ed->setSize(0, height); - row.addElement(ed, false, true); - - auto spacer = std::make_shared(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(window); - row.addElement(ed, false); - - auto spacer = std::make_shared(mWindow); - spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0); - row.addElement(spacer, false); - - // pass input to the actual DateTimeEditComponent 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(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); - row.addElement(ed, false); - break; - } - case MD_MULTILINE_STRING: - default: - { - // MD_STRING - ed = std::make_shared(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); - row.addElement(ed, true); - - auto spacer = std::make_shared(mWindow); - spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); - row.addElement(spacer, false); - - auto bracket = std::make_shared(mWindow); - bracket->setImage(":/arrow.svg"); - bracket->setResize(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->setTag(iter->key); - ed->setValue(mMetaData->get(iter->key)); - - mEditors.push_back(ed); - } - - std::vector< std::shared_ptr > buttons; - - if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) - buttons.push_back(std::make_shared(mWindow, _T("SCRAPE"), _T("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); - - buttons.push_back(std::make_shared(mWindow, _T("SAVE"), _T("SAVE"), [&] { save(); delete this; })); - buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); - - if(mDeleteFunc) - { - auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; - auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; - buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE"), deleteBtnFunc)); - } - - mButtons = makeButtonGrid(mWindow, buttons); - mGrid.setEntry(mButtons, Vector2i(0, 2), true, false); - - // resize + center - float width = (float)Math::min(Renderer::getScreenHeight(), (int)(Renderer::getScreenWidth() * 0.90f)); - setSize(width, 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() -{ - // remove game from index - mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); - - for (unsigned int i = 0; i < mEditors.size(); i++) - { - std::shared_ptr ed = mEditors.at(i); - - auto val = ed->getValue(); - auto key = ed->getTag(); - - if (key == "core" || key == "emulator") - { - std::shared_ptr> list = std::static_pointer_cast>(ed); - val = list->getSelected(); - } - - mMetaData->set(key, val); - } - - // enter game in index - mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); - - if (mSavedCallback) - mSavedCallback(); - - // update respective Collection Entries - CollectionSystemManager::get()->refreshCollectionSystems(mScraperParams.game); -} - -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++) - { - auto val = mEditors.at(i)->getValue(); - auto key = mEditors.at(i)->getTag(); - 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++) - { - auto key = mEditors.at(i)->getTag(); - if(mMetaData->get(key) != mEditors.at(i)->getValue()) - { - dirty = true; - break; - } - } - - std::function 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, - "ENREGISTRER LES MODIFICATIONS ?", - "OUI", [this, closeFunc] { save(); closeFunc(); }, - "NON", 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 GuiMetaDataEd::getHelpPrompts() -{ - std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); - return prompts; -} +#include "guis/GuiMetaDataEd.h" + +#include "components/ButtonComponent.h" +#include "components/ComponentList.h" +#include "components/DateTimeEditComponent.h" +#include "components/MenuComponent.h" +#include "components/RatingComponent.h" +#include "components/SwitchComponent.h" +#include "components/TextComponent.h" +#include "guis/GuiGameScraper.h" +#include "guis/GuiMsgBox.h" +#include "guis/GuiTextEditPopup.h" +#include "resources/Font.h" +#include "utils/StringUtil.h" +#include "views/ViewController.h" +#include "CollectionSystemManager.h" +#include "FileData.h" +#include "FileFilterIndex.h" +#include "SystemData.h" +#include "Window.h" + +GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, + const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : 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(mWindow, Vector2i(1, 5)); + + mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); + mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), + 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(mWindow); + mGrid.setEntry(mList, Vector2i(0, 1), true, true); + + SystemData* system = file->getSystem(); + + auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); + auto core_choice = std::make_shared>(mWindow, _T("CORE"), false); + + // populate list + for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + { + std::shared_ptr ed; + + // don't add statistics + if(iter->isStatistic) + continue; + + if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || + iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || + iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") + continue; + + // create ed and add it (and any related components) to mMenu + // ed's value will be set below + ComponentListRow row; + + if (iter->displayName == "emulator") + { + // if (file->getSystemEnvData()->mEmulators.size() <= 1) + // continue; + + std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); + + std::string currentEmul = file->getEmulator(); + + if (defaultEmul.length() == 0) + emul_choice->add(_T("DEFAULT"), "", false); + else + emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); + + for (auto core : file->getSystemEnvData()->mEmulators) + emul_choice->add(core.mName, core.mName, core.mName == currentEmul); + + row.addElement(std::make_shared(mWindow, _T("EMULATOR"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(emul_choice, false); + + mList->addRow(row); + emul_choice->setTag(iter->key); + mEditors.push_back(emul_choice); + + emul_choice->setSelectedChangedCallback([this, system, core_choice, file](std::string emulatorName) + { + std::string currentCore = file->getCore(); + + std::string defaultCore = system->getSystemEnvData()->getDefaultCore(emulatorName); + if (emulatorName.length() == 0) + defaultCore = system->getSystemEnvData()->getDefaultCore(system->getSystemEnvData()->getDefaultEmulator()); + + core_choice->clear(); + if (defaultCore.length() == 0) + core_choice->add(_T("DEFAULT"), "", false); + else + core_choice->add(_T("DEFAULT")+" ("+ defaultCore+")", "", false); + + std::vector cores = system->getSystemEnvData()->getCores(emulatorName); + + bool found = false; + + for (auto it = cores.begin(); it != cores.end(); it++) + { + std::string core = *it; + core_choice->add(core, core, currentCore == core); + if (currentCore == core) + found = true; + } + + if (!found) + core_choice->selectFirstItem(); + else + core_choice->invalidate(); + }); + + continue; + } + + if (iter->displayName == "core") + { + // core_choice->add(_T("DEFAULT"), "", true); + core_choice->setTag(iter->key); + + row.addElement(std::make_shared(mWindow, "CORE", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(core_choice, false); + + mList->addRow(row); + ed = core_choice; + + mEditors.push_back(core_choice); + + // force change event to load core list + emul_choice->invalidate(); + continue; + } + + auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + row.addElement(lbl, true); // label + + switch (iter->type) + { + case MD_BOOL: + { + ed = std::make_shared(window); + row.addElement(ed, false, true); + break; + } + case MD_RATING: + { + ed = std::make_shared(window); + const float height = lbl->getSize().y() * 0.71f; + ed->setSize(0, height); + row.addElement(ed, false, true); + + auto spacer = std::make_shared(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(window); + row.addElement(ed, false); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0); + row.addElement(spacer, false); + + // pass input to the actual DateTimeEditComponent 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(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); + row.addElement(ed, false); + break; + } + case MD_MULTILINE_STRING: + default: + { + // MD_STRING + ed = std::make_shared(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); + row.addElement(ed, true); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); + row.addElement(spacer, false); + + auto bracket = std::make_shared(mWindow); + bracket->setImage(":/arrow.svg"); + bracket->setResize(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->setTag(iter->key); + ed->setValue(mMetaData->get(iter->key)); + + mEditors.push_back(ed); + } + + std::vector< std::shared_ptr > buttons; + + if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) + buttons.push_back(std::make_shared(mWindow, _T("SCRAPE"), _T("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); + + buttons.push_back(std::make_shared(mWindow, _T("SAVE"), _T("SAVE"), [&] { save(); delete this; })); + buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); + + if(mDeleteFunc) + { + auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; + auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; + buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE"), deleteBtnFunc)); + } + + mButtons = makeButtonGrid(mWindow, buttons); + mGrid.setEntry(mButtons, Vector2i(0, 2), true, false); + + // resize + center + float width = (float)Math::min(Renderer::getScreenHeight(), (int)(Renderer::getScreenWidth() * 0.90f)); + setSize(width, 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() +{ + // remove game from index + mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); + + for (unsigned int i = 0; i < mEditors.size(); i++) + { + std::shared_ptr ed = mEditors.at(i); + + auto val = ed->getValue(); + auto key = ed->getTag(); + + if (key == "core" || key == "emulator") + { + std::shared_ptr> list = std::static_pointer_cast>(ed); + val = list->getSelected(); + } + + mMetaData->set(key, val); + } + + // enter game in index + mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); + + if (mSavedCallback) + mSavedCallback(); + + // update respective Collection Entries + CollectionSystemManager::get()->refreshCollectionSystems(mScraperParams.game); +} + +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++) + { + auto val = mEditors.at(i)->getValue(); + auto key = mEditors.at(i)->getTag(); + 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++) + { + auto key = mEditors.at(i)->getTag(); + if(mMetaData->get(key) != mEditors.at(i)->getValue()) + { + dirty = true; + break; + } + } + + std::function 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, + "ENREGISTRER LES MODIFICATIONS ?", + "OUI", [this, closeFunc] { save(); closeFunc(); }, + "NON", 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 GuiMetaDataEd::getHelpPrompts() +{ + std::vector prompts = mGrid.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + return prompts; +} diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index 7cd24b6f15..d05f77c3d4 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -1,588 +1,588 @@ -#include "GuiComponent.h" - -#include "animations/Animation.h" -#include "animations/AnimationController.h" -#include "Log.h" -#include "Renderer.h" -#include "ThemeData.h" -#include "Window.h" -#include - - -#if defined(_WIN32) -#define _conv(x) GuiTextTool::convertFromWideString(L ## x) -#else -#define _conv(x) x -#endif - -#include -#include -#include "resources\ResourceManager.h" - -std::vector GuiTextTool::mItems; -std::string GuiTextTool::mCurrentLanguage = "en"; -bool GuiTextTool::mCurrentLanguageLoaded = false; - -void GuiTextTool::setLanguage(std::string lang) -{ - mCurrentLanguage = lang; - mCurrentLanguageLoaded = false; -} - -void GuiTextTool::ensureLocalisation() -{ - if (mCurrentLanguageLoaded) - { - if (Settings::getInstance()->getString("Language") == mCurrentLanguage) - return; - - mCurrentLanguage = Settings::getInstance()->getString("Language"); - } - - mCurrentLanguageLoaded = true; - - for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) - delete (*it); - - mItems.clear(); - - std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/"+ mCurrentLanguage +"/emulationstation2.po"); +#include "GuiComponent.h" + +#include "animations/Animation.h" +#include "animations/AnimationController.h" +#include "Log.h" +#include "Renderer.h" +#include "ThemeData.h" +#include "Window.h" +#include + + +#if defined(_WIN32) +#define _conv(x) GuiTextTool::convertFromWideString(L ## x) +#else +#define _conv(x) x +#endif + +#include +#include +#include "resources\ResourceManager.h" + +std::vector GuiTextTool::mItems; +std::string GuiTextTool::mCurrentLanguage = "en"; +bool GuiTextTool::mCurrentLanguageLoaded = false; + +void GuiTextTool::setLanguage(std::string lang) +{ + mCurrentLanguage = lang; + mCurrentLanguageLoaded = false; +} + +void GuiTextTool::ensureLocalisation() +{ + if (mCurrentLanguageLoaded) + { + if (Settings::getInstance()->getString("Language") == mCurrentLanguage) + return; + + mCurrentLanguage = Settings::getInstance()->getString("Language"); + } + + mCurrentLanguageLoaded = true; + + for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) + delete (*it); + + mItems.clear(); + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/"+ mCurrentLanguage +"/emulationstation2.po"); if (Utils::FileSystem::exists(xmlpath)) - { - LocalizationItem* currentItem = NULL; - - std::ifstream file(xmlpath); - std::string str; - while (std::getline(file, str)) - { - if (str.length() > 0 && str[0] == '#') - { - if (currentItem != NULL && currentItem->msgid.length() > 0 && currentItem->msgstr.length() > 0) - mItems.push_back(currentItem); - - currentItem = new LocalizationItem(); - } - - if (currentItem != NULL && str.find("msgid") == 0) - { - auto start = str.find("\""); - if (start != std::string::npos) - { - auto end = str.find("\"", start + 1); - if (end != std::string::npos) - currentItem->msgid = str.substr(start + 1, end - start - 1); - } - } - - if (currentItem != NULL && str.find("msgstr") == 0) - { - auto start = str.find("\""); - if (start != std::string::npos) - { - auto end = str.find("\"", start + 1); - if (end != std::string::npos) - currentItem->msgstr = str.substr(start + 1, end - start - 1); - } - } - } - - if (currentItem != NULL) - delete currentItem; - } -} - - -const std::string GuiTextTool::localize(const std::string text) -{ - ensureLocalisation(); - + { + LocalizationItem* currentItem = NULL; + + std::ifstream file(xmlpath); + std::string str; + while (std::getline(file, str)) + { + if (str.length() > 0 && str[0] == '#') + { + if (currentItem != NULL && currentItem->msgid.length() > 0 && currentItem->msgstr.length() > 0) + mItems.push_back(currentItem); + + currentItem = new LocalizationItem(); + } + + if (currentItem != NULL && str.find("msgid") == 0) + { + auto start = str.find("\""); + if (start != std::string::npos) + { + auto end = str.find("\"", start + 1); + if (end != std::string::npos) + currentItem->msgid = str.substr(start + 1, end - start - 1); + } + } + + if (currentItem != NULL && str.find("msgstr") == 0) + { + auto start = str.find("\""); + if (start != std::string::npos) + { + auto end = str.find("\"", start + 1); + if (end != std::string::npos) + currentItem->msgstr = str.substr(start + 1, end - start - 1); + } + } + } + + if (currentItem != NULL) + delete currentItem; + } +} + + +const std::string GuiTextTool::localize(const std::string text) +{ + ensureLocalisation(); + for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) { if (text == (*it)->msgid) return (*it)->msgstr; - } - - return text; -} - -GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255), - mPosition(Vector3f::Zero()), mOrigin(Vector2f::Zero()), mRotationOrigin(0.5, 0.5), - mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false) -{ - for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) - mAnimationMap[i] = NULL; -} - -GuiComponent::~GuiComponent() -{ - mWindow->removeGui(this); - - cancelAllAnimations(); - - if(mParent) - mParent->removeChild(this); - - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->setParent(NULL); -} - -bool GuiComponent::input(InputConfig* config, Input input) -{ - for(unsigned int i = 0; i < getChildCount(); i++) - { - if(getChild(i)->input(config, input)) - return true; - } - - return false; -} - -void GuiComponent::updateSelf(int deltaTime) -{ - for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) - advanceAnimation(i, deltaTime); -} - -void GuiComponent::updateChildren(int deltaTime) -{ - for(unsigned int i = 0; i < getChildCount(); i++) - { - getChild(i)->update(deltaTime); - } -} - -void GuiComponent::update(int deltaTime) -{ - updateSelf(deltaTime); - updateChildren(deltaTime); -} - -void GuiComponent::render(const Transform4x4f& parentTrans) -{ - Transform4x4f trans = parentTrans * getTransform(); - renderChildren(trans); -} - -void GuiComponent::renderChildren(const Transform4x4f& transform) const -{ - for(unsigned int i = 0; i < getChildCount(); i++) - { - getChild(i)->render(transform); - } -} - -Vector3f GuiComponent::getPosition() const -{ - return mPosition; -} - -void GuiComponent::setPosition(float x, float y, float z) -{ - mPosition = Vector3f(x, y, z); - onPositionChanged(); -} - -Vector2f GuiComponent::getOrigin() const -{ - return mOrigin; -} - -void GuiComponent::setOrigin(float x, float y) -{ - mOrigin = Vector2f(x, y); - onOriginChanged(); -} - -Vector2f GuiComponent::getRotationOrigin() const -{ - return mRotationOrigin; -} - -void GuiComponent::setRotationOrigin(float x, float y) -{ - mRotationOrigin = Vector2f(x, y); -} - -Vector2f GuiComponent::getSize() const -{ - return mSize; -} - -void GuiComponent::setSize(float w, float h) -{ - mSize = Vector2f(w, h); - onSizeChanged(); -} - -float GuiComponent::getRotation() const -{ - return mRotation; -} - -void GuiComponent::setRotation(float rotation) -{ - mRotation = rotation; -} - -float GuiComponent::getScale() const -{ - return mScale; -} - -void GuiComponent::setScale(float scale) -{ - mScale = scale; -} - -float GuiComponent::getZIndex() const -{ - return mZIndex; -} - -void GuiComponent::setZIndex(float z) -{ - mZIndex = z; -} - -float GuiComponent::getDefaultZIndex() const -{ - return mDefaultZIndex; -} - -void GuiComponent::setDefaultZIndex(float z) -{ - mDefaultZIndex = z; -} - -Vector2f GuiComponent::getCenter() const -{ - return Vector2f(mPosition.x() - (getSize().x() * mOrigin.x()) + getSize().x() / 2, - mPosition.y() - (getSize().y() * mOrigin.y()) + getSize().y() / 2); -} - -//Children stuff. -void GuiComponent::addChild(GuiComponent* cmp) -{ - mChildren.push_back(cmp); - - if(cmp->getParent()) - cmp->getParent()->removeChild(cmp); - - cmp->setParent(this); -} - -void GuiComponent::removeChild(GuiComponent* cmp) -{ - if(!cmp->getParent()) - return; - - if(cmp->getParent() != this) - { - LOG(LogError) << "Tried to remove child from incorrect parent!"; - } - - cmp->setParent(NULL); - - for(auto i = mChildren.cbegin(); i != mChildren.cend(); i++) - { - if(*i == cmp) - { - mChildren.erase(i); - return; - } - } -} - -void GuiComponent::clearChildren() -{ - mChildren.clear(); -} - -void GuiComponent::sortChildren() -{ - std::stable_sort(mChildren.begin(), mChildren.end(), [](GuiComponent* a, GuiComponent* b) { - return b->getZIndex() > a->getZIndex(); - }); -} - -unsigned int GuiComponent::getChildCount() const -{ - return (int)mChildren.size(); -} - -GuiComponent* GuiComponent::getChild(unsigned int i) const -{ - return mChildren.at(i); -} - -void GuiComponent::setParent(GuiComponent* parent) -{ - mParent = parent; -} - -GuiComponent* GuiComponent::getParent() const -{ - return mParent; -} - -unsigned char GuiComponent::getOpacity() const -{ - return mOpacity; -} - -void GuiComponent::setOpacity(unsigned char opacity) -{ - mOpacity = opacity; - for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - (*it)->setOpacity(opacity); - } -} - -const Transform4x4f& GuiComponent::getTransform() -{ - mTransform = Transform4x4f::Identity(); - mTransform.translate(mPosition); - if (mScale != 1.0) - { - mTransform.scale(mScale); - } - if (mRotation != 0.0) - { - // Calculate offset as difference between origin and rotation origin - Vector2f rotationSize = getRotationSize(); - float xOff = (mOrigin.x() - mRotationOrigin.x()) * rotationSize.x(); - float yOff = (mOrigin.y() - mRotationOrigin.y()) * rotationSize.y(); - - // transform to offset point - if (xOff != 0.0 || yOff != 0.0) - mTransform.translate(Vector3f(xOff * -1, yOff * -1, 0.0f)); - - // apply rotation transform - mTransform.rotateZ(mRotation); - - // Tranform back to original point - if (xOff != 0.0 || yOff != 0.0) - mTransform.translate(Vector3f(xOff, yOff, 0.0f)); - } - mTransform.translate(Vector3f(mOrigin.x() * mSize.x() * -1, mOrigin.y() * mSize.y() * -1, 0.0f)); - return mTransform; -} - -void GuiComponent::setValue(const std::string& /*value*/) -{ -} - -std::string GuiComponent::getValue() const -{ - return ""; -} - -void GuiComponent::setTag(const std::string& value) -{ - mTag = value; -} - -std::string GuiComponent::getTag() const -{ - return mTag; -} - -void GuiComponent::textInput(const char* text) -{ - for(auto iter = mChildren.cbegin(); iter != mChildren.cend(); iter++) - { - (*iter)->textInput(text); - } -} - -void GuiComponent::setAnimation(Animation* anim, int delay, std::function finishedCallback, bool reverse, unsigned char slot) -{ - assert(slot < MAX_ANIMATIONS); - - AnimationController* oldAnim = mAnimationMap[slot]; - mAnimationMap[slot] = new AnimationController(anim, delay, finishedCallback, reverse); - - if(oldAnim) - delete oldAnim; -} - -bool GuiComponent::stopAnimation(unsigned char slot) -{ - assert(slot < MAX_ANIMATIONS); - if(mAnimationMap[slot]) - { - delete mAnimationMap[slot]; - mAnimationMap[slot] = NULL; - return true; - }else{ - return false; - } -} - -bool GuiComponent::cancelAnimation(unsigned char slot) -{ - assert(slot < MAX_ANIMATIONS); - if(mAnimationMap[slot]) - { - mAnimationMap[slot]->removeFinishedCallback(); - delete mAnimationMap[slot]; - mAnimationMap[slot] = NULL; - return true; - }else{ - return false; - } -} - -bool GuiComponent::finishAnimation(unsigned char slot) -{ - assert(slot < MAX_ANIMATIONS); - if(mAnimationMap[slot]) - { - // skip to animation's end - const bool done = mAnimationMap[slot]->update(mAnimationMap[slot]->getAnimation()->getDuration() - mAnimationMap[slot]->getTime()); - assert(done); - - delete mAnimationMap[slot]; // will also call finishedCallback - mAnimationMap[slot] = NULL; - return true; - }else{ - return false; - } -} - -bool GuiComponent::advanceAnimation(unsigned char slot, unsigned int time) -{ - assert(slot < MAX_ANIMATIONS); - AnimationController* anim = mAnimationMap[slot]; - if(anim) - { - bool done = anim->update(time); - if(done) - { - mAnimationMap[slot] = NULL; - delete anim; - } - return true; - }else{ - return false; - } -} - -void GuiComponent::stopAllAnimations() -{ - for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) - stopAnimation(i); -} - -void GuiComponent::cancelAllAnimations() -{ - for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) - cancelAnimation(i); -} - -bool GuiComponent::isAnimationPlaying(unsigned char slot) const -{ - return mAnimationMap[slot] != NULL; -} - -bool GuiComponent::isAnimationReversed(unsigned char slot) const -{ - assert(mAnimationMap[slot] != NULL); - return mAnimationMap[slot]->isReversed(); -} - -int GuiComponent::getAnimationTime(unsigned char slot) const -{ - assert(mAnimationMap[slot] != NULL); - return mAnimationMap[slot]->getTime(); -} - -void GuiComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) -{ - Vector2f scale = getParent() ? getParent()->getSize() : Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - - const ThemeData::ThemeElement* elem = theme->getElement(view, element, ""); - if(!elem) - return; - - using namespace ThemeFlags; - if(properties & POSITION && elem->has("pos")) - { - Vector2f denormalized = elem->get("pos") * scale; - setPosition(Vector3f(denormalized.x(), denormalized.y(), 0)); - } - - if(properties & ThemeFlags::SIZE && elem->has("size")) - setSize(elem->get("size") * scale); - - // position + size also implies origin - if((properties & ORIGIN || (properties & POSITION && properties & ThemeFlags::SIZE)) && elem->has("origin")) - setOrigin(elem->get("origin")); - - if(properties & ThemeFlags::ROTATION) { - if(elem->has("rotation")) - setRotationDegrees(elem->get("rotation")); - if(elem->has("rotationOrigin")) - setRotationOrigin(elem->get("rotationOrigin")); - } - - if(properties & ThemeFlags::Z_INDEX && elem->has("zIndex")) - setZIndex(elem->get("zIndex")); - else - setZIndex(getDefaultZIndex()); -} - -void GuiComponent::updateHelpPrompts() -{ - if(getParent()) - { - getParent()->updateHelpPrompts(); - return; - } - - std::vector prompts = getHelpPrompts(); - - if(mWindow->peekGui() == this) - mWindow->setHelpPrompts(prompts, getHelpStyle()); -} - -HelpStyle GuiComponent::getHelpStyle() -{ - return HelpStyle(); -} - -bool GuiComponent::isProcessing() const -{ - return mIsProcessing; -} - -void GuiComponent::onShow() -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->onShow(); -} - -void GuiComponent::onHide() -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->onHide(); -} - -void GuiComponent::onScreenSaverActivate() -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->onScreenSaverActivate(); -} - -void GuiComponent::onScreenSaverDeactivate() -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->onScreenSaverDeactivate(); -} - -void GuiComponent::topWindow(bool isTop) -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->topWindow(isTop); + } + + return text; +} + +GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255), + mPosition(Vector3f::Zero()), mOrigin(Vector2f::Zero()), mRotationOrigin(0.5, 0.5), + mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false) +{ + for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) + mAnimationMap[i] = NULL; +} + +GuiComponent::~GuiComponent() +{ + mWindow->removeGui(this); + + cancelAllAnimations(); + + if(mParent) + mParent->removeChild(this); + + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->setParent(NULL); +} + +bool GuiComponent::input(InputConfig* config, Input input) +{ + for(unsigned int i = 0; i < getChildCount(); i++) + { + if(getChild(i)->input(config, input)) + return true; + } + + return false; +} + +void GuiComponent::updateSelf(int deltaTime) +{ + for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) + advanceAnimation(i, deltaTime); +} + +void GuiComponent::updateChildren(int deltaTime) +{ + for(unsigned int i = 0; i < getChildCount(); i++) + { + getChild(i)->update(deltaTime); + } +} + +void GuiComponent::update(int deltaTime) +{ + updateSelf(deltaTime); + updateChildren(deltaTime); +} + +void GuiComponent::render(const Transform4x4f& parentTrans) +{ + Transform4x4f trans = parentTrans * getTransform(); + renderChildren(trans); +} + +void GuiComponent::renderChildren(const Transform4x4f& transform) const +{ + for(unsigned int i = 0; i < getChildCount(); i++) + { + getChild(i)->render(transform); + } +} + +Vector3f GuiComponent::getPosition() const +{ + return mPosition; +} + +void GuiComponent::setPosition(float x, float y, float z) +{ + mPosition = Vector3f(x, y, z); + onPositionChanged(); +} + +Vector2f GuiComponent::getOrigin() const +{ + return mOrigin; +} + +void GuiComponent::setOrigin(float x, float y) +{ + mOrigin = Vector2f(x, y); + onOriginChanged(); +} + +Vector2f GuiComponent::getRotationOrigin() const +{ + return mRotationOrigin; +} + +void GuiComponent::setRotationOrigin(float x, float y) +{ + mRotationOrigin = Vector2f(x, y); +} + +Vector2f GuiComponent::getSize() const +{ + return mSize; +} + +void GuiComponent::setSize(float w, float h) +{ + mSize = Vector2f(w, h); + onSizeChanged(); +} + +float GuiComponent::getRotation() const +{ + return mRotation; +} + +void GuiComponent::setRotation(float rotation) +{ + mRotation = rotation; +} + +float GuiComponent::getScale() const +{ + return mScale; +} + +void GuiComponent::setScale(float scale) +{ + mScale = scale; +} + +float GuiComponent::getZIndex() const +{ + return mZIndex; +} + +void GuiComponent::setZIndex(float z) +{ + mZIndex = z; +} + +float GuiComponent::getDefaultZIndex() const +{ + return mDefaultZIndex; +} + +void GuiComponent::setDefaultZIndex(float z) +{ + mDefaultZIndex = z; +} + +Vector2f GuiComponent::getCenter() const +{ + return Vector2f(mPosition.x() - (getSize().x() * mOrigin.x()) + getSize().x() / 2, + mPosition.y() - (getSize().y() * mOrigin.y()) + getSize().y() / 2); +} + +//Children stuff. +void GuiComponent::addChild(GuiComponent* cmp) +{ + mChildren.push_back(cmp); + + if(cmp->getParent()) + cmp->getParent()->removeChild(cmp); + + cmp->setParent(this); +} + +void GuiComponent::removeChild(GuiComponent* cmp) +{ + if(!cmp->getParent()) + return; + + if(cmp->getParent() != this) + { + LOG(LogError) << "Tried to remove child from incorrect parent!"; + } + + cmp->setParent(NULL); + + for(auto i = mChildren.cbegin(); i != mChildren.cend(); i++) + { + if(*i == cmp) + { + mChildren.erase(i); + return; + } + } +} + +void GuiComponent::clearChildren() +{ + mChildren.clear(); +} + +void GuiComponent::sortChildren() +{ + std::stable_sort(mChildren.begin(), mChildren.end(), [](GuiComponent* a, GuiComponent* b) { + return b->getZIndex() > a->getZIndex(); + }); +} + +unsigned int GuiComponent::getChildCount() const +{ + return (int)mChildren.size(); +} + +GuiComponent* GuiComponent::getChild(unsigned int i) const +{ + return mChildren.at(i); +} + +void GuiComponent::setParent(GuiComponent* parent) +{ + mParent = parent; +} + +GuiComponent* GuiComponent::getParent() const +{ + return mParent; +} + +unsigned char GuiComponent::getOpacity() const +{ + return mOpacity; +} + +void GuiComponent::setOpacity(unsigned char opacity) +{ + mOpacity = opacity; + for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + (*it)->setOpacity(opacity); + } +} + +const Transform4x4f& GuiComponent::getTransform() +{ + mTransform = Transform4x4f::Identity(); + mTransform.translate(mPosition); + if (mScale != 1.0) + { + mTransform.scale(mScale); + } + if (mRotation != 0.0) + { + // Calculate offset as difference between origin and rotation origin + Vector2f rotationSize = getRotationSize(); + float xOff = (mOrigin.x() - mRotationOrigin.x()) * rotationSize.x(); + float yOff = (mOrigin.y() - mRotationOrigin.y()) * rotationSize.y(); + + // transform to offset point + if (xOff != 0.0 || yOff != 0.0) + mTransform.translate(Vector3f(xOff * -1, yOff * -1, 0.0f)); + + // apply rotation transform + mTransform.rotateZ(mRotation); + + // Tranform back to original point + if (xOff != 0.0 || yOff != 0.0) + mTransform.translate(Vector3f(xOff, yOff, 0.0f)); + } + mTransform.translate(Vector3f(mOrigin.x() * mSize.x() * -1, mOrigin.y() * mSize.y() * -1, 0.0f)); + return mTransform; +} + +void GuiComponent::setValue(const std::string& /*value*/) +{ +} + +std::string GuiComponent::getValue() const +{ + return ""; +} + +void GuiComponent::setTag(const std::string& value) +{ + mTag = value; +} + +std::string GuiComponent::getTag() const +{ + return mTag; +} + +void GuiComponent::textInput(const char* text) +{ + for(auto iter = mChildren.cbegin(); iter != mChildren.cend(); iter++) + { + (*iter)->textInput(text); + } +} + +void GuiComponent::setAnimation(Animation* anim, int delay, std::function finishedCallback, bool reverse, unsigned char slot) +{ + assert(slot < MAX_ANIMATIONS); + + AnimationController* oldAnim = mAnimationMap[slot]; + mAnimationMap[slot] = new AnimationController(anim, delay, finishedCallback, reverse); + + if(oldAnim) + delete oldAnim; +} + +bool GuiComponent::stopAnimation(unsigned char slot) +{ + assert(slot < MAX_ANIMATIONS); + if(mAnimationMap[slot]) + { + delete mAnimationMap[slot]; + mAnimationMap[slot] = NULL; + return true; + }else{ + return false; + } +} + +bool GuiComponent::cancelAnimation(unsigned char slot) +{ + assert(slot < MAX_ANIMATIONS); + if(mAnimationMap[slot]) + { + mAnimationMap[slot]->removeFinishedCallback(); + delete mAnimationMap[slot]; + mAnimationMap[slot] = NULL; + return true; + }else{ + return false; + } +} + +bool GuiComponent::finishAnimation(unsigned char slot) +{ + assert(slot < MAX_ANIMATIONS); + if(mAnimationMap[slot]) + { + // skip to animation's end + const bool done = mAnimationMap[slot]->update(mAnimationMap[slot]->getAnimation()->getDuration() - mAnimationMap[slot]->getTime()); + assert(done); + + delete mAnimationMap[slot]; // will also call finishedCallback + mAnimationMap[slot] = NULL; + return true; + }else{ + return false; + } +} + +bool GuiComponent::advanceAnimation(unsigned char slot, unsigned int time) +{ + assert(slot < MAX_ANIMATIONS); + AnimationController* anim = mAnimationMap[slot]; + if(anim) + { + bool done = anim->update(time); + if(done) + { + mAnimationMap[slot] = NULL; + delete anim; + } + return true; + }else{ + return false; + } +} + +void GuiComponent::stopAllAnimations() +{ + for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) + stopAnimation(i); +} + +void GuiComponent::cancelAllAnimations() +{ + for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) + cancelAnimation(i); +} + +bool GuiComponent::isAnimationPlaying(unsigned char slot) const +{ + return mAnimationMap[slot] != NULL; +} + +bool GuiComponent::isAnimationReversed(unsigned char slot) const +{ + assert(mAnimationMap[slot] != NULL); + return mAnimationMap[slot]->isReversed(); +} + +int GuiComponent::getAnimationTime(unsigned char slot) const +{ + assert(mAnimationMap[slot] != NULL); + return mAnimationMap[slot]->getTime(); +} + +void GuiComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) +{ + Vector2f scale = getParent() ? getParent()->getSize() : Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + + const ThemeData::ThemeElement* elem = theme->getElement(view, element, ""); + if(!elem) + return; + + using namespace ThemeFlags; + if(properties & POSITION && elem->has("pos")) + { + Vector2f denormalized = elem->get("pos") * scale; + setPosition(Vector3f(denormalized.x(), denormalized.y(), 0)); + } + + if(properties & ThemeFlags::SIZE && elem->has("size")) + setSize(elem->get("size") * scale); + + // position + size also implies origin + if((properties & ORIGIN || (properties & POSITION && properties & ThemeFlags::SIZE)) && elem->has("origin")) + setOrigin(elem->get("origin")); + + if(properties & ThemeFlags::ROTATION) { + if(elem->has("rotation")) + setRotationDegrees(elem->get("rotation")); + if(elem->has("rotationOrigin")) + setRotationOrigin(elem->get("rotationOrigin")); + } + + if(properties & ThemeFlags::Z_INDEX && elem->has("zIndex")) + setZIndex(elem->get("zIndex")); + else + setZIndex(getDefaultZIndex()); +} + +void GuiComponent::updateHelpPrompts() +{ + if(getParent()) + { + getParent()->updateHelpPrompts(); + return; + } + + std::vector prompts = getHelpPrompts(); + + if(mWindow->peekGui() == this) + mWindow->setHelpPrompts(prompts, getHelpStyle()); +} + +HelpStyle GuiComponent::getHelpStyle() +{ + return HelpStyle(); +} + +bool GuiComponent::isProcessing() const +{ + return mIsProcessing; +} + +void GuiComponent::onShow() +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->onShow(); +} + +void GuiComponent::onHide() +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->onHide(); +} + +void GuiComponent::onScreenSaverActivate() +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->onScreenSaverActivate(); +} + +void GuiComponent::onScreenSaverDeactivate() +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->onScreenSaverDeactivate(); +} + +void GuiComponent::topWindow(bool isTop) +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->topWindow(isTop); } \ No newline at end of file diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 4712ce32f5..d850727610 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -1,222 +1,222 @@ -#pragma once -#ifndef ES_CORE_GUI_COMPONENT_H -#define ES_CORE_GUI_COMPONENT_H - -#include "math/Misc.h" -#include "math/Transform4x4f.h" -#include "HelpPrompt.h" -#include "HelpStyle.h" -#include "InputConfig.h" -#include -#include - -#if defined(_WIN32) -#include -#endif - -class Animation; -class AnimationController; -class Font; -class InputConfig; -class ThemeData; -class Window; - -class GuiComponent -{ -public: - GuiComponent(Window* window); - virtual ~GuiComponent(); - - virtual void textInput(const char* text); - - //Called when input is received. - //Return true if the input is consumed, false if it should continue to be passed to other children. - virtual bool input(InputConfig* config, Input input); - - //Called when time passes. Default implementation calls updateSelf(deltaTime) and updateChildren(deltaTime) - so you should probably call GuiComponent::update(deltaTime) at some point (or at least updateSelf so animations work). - virtual void update(int deltaTime); - - //Called when it's time to render. By default, just calls renderChildren(parentTrans * getTransform()). - //You probably want to override this like so: - //1. Calculate the new transform that your control will draw at with Transform4x4f t = parentTrans * getTransform(). - //2. Set the renderer to use that new transform as the model matrix - Renderer::setMatrix(t); - //3. Draw your component. - //4. Tell your children to render, based on your component's transform - renderChildren(t). - virtual void render(const Transform4x4f& parentTrans); - - Vector3f getPosition() const; - inline void setPosition(const Vector3f& offset) { setPosition(offset.x(), offset.y(), offset.z()); } - void setPosition(float x, float y, float z = 0.0f); - virtual void onPositionChanged() {}; - - //Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) - Vector2f getOrigin() const; - void setOrigin(float originX, float originY); - inline void setOrigin(Vector2f origin) { setOrigin(origin.x(), origin.y()); } - virtual void onOriginChanged() {}; - - //Sets the rotation origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) - Vector2f getRotationOrigin() const; - void setRotationOrigin(float originX, float originY); - inline void setRotationOrigin(Vector2f origin) { setRotationOrigin(origin.x(), origin.y()); } - - virtual Vector2f getSize() const; - inline void setSize(const Vector2f& size) { setSize(size.x(), size.y()); } - void setSize(float w, float h); - virtual void onSizeChanged() {}; - - virtual Vector2f getRotationSize() const { return getSize(); }; - - float getRotation() const; - void setRotation(float rotation); - inline void setRotationDegrees(float rotation) { setRotation((float)ES_DEG_TO_RAD(rotation)); } - - float getScale() const; - void setScale(float scale); - - float getZIndex() const; - void setZIndex(float zIndex); - - float getDefaultZIndex() const; - void setDefaultZIndex(float zIndex); - - // Returns the center point of the image (takes origin into account). - Vector2f getCenter() const; - - void setParent(GuiComponent* parent); - GuiComponent* getParent() const; - - void addChild(GuiComponent* cmp); - void removeChild(GuiComponent* cmp); - void clearChildren(); - void sortChildren(); - unsigned int getChildCount() const; - GuiComponent* getChild(unsigned int i) const; - - // animation will be automatically deleted when it completes or is stopped. - bool isAnimationPlaying(unsigned char slot) const; - bool isAnimationReversed(unsigned char slot) const; - int getAnimationTime(unsigned char slot) const; - void setAnimation(Animation* animation, int delay = 0, std::function finishedCallback = nullptr, bool reverse = false, unsigned char slot = 0); - bool stopAnimation(unsigned char slot); - bool cancelAnimation(unsigned char slot); // Like stopAnimation, but doesn't call finishedCallback - only removes the animation, leaving things in their current state. Returns true if successful (an animation was in this slot). - bool finishAnimation(unsigned char slot); // Calls update(1.f) and finishedCallback, then deletes the animation - basically skips to the end. Returns true if successful (an animation was in this slot). - bool advanceAnimation(unsigned char slot, unsigned int time); // Returns true if successful (an animation was in this slot). - void stopAllAnimations(); - void cancelAllAnimations(); - - virtual unsigned char getOpacity() const; - virtual void setOpacity(unsigned char opacity); - - const Transform4x4f& getTransform(); - - virtual std::string getValue() const; - virtual void setValue(const std::string& value); - - - virtual std::string getTag() const; - virtual void setTag(const std::string& value); - - virtual void onFocusGained() {}; - virtual void onFocusLost() {}; - - virtual void onShow(); - virtual void onHide(); - - virtual void onScreenSaverActivate(); - virtual void onScreenSaverDeactivate(); - virtual void topWindow(bool isTop); - - // Default implementation just handles and tags as normalized float pairs. - // You probably want to keep this behavior for any derived classes as well as add your own. - virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); - - // Returns a list of help prompts. - virtual std::vector getHelpPrompts() { return std::vector(); }; - - // Called whenever help prompts change. - void updateHelpPrompts(); - - virtual HelpStyle getHelpStyle(); - - // Returns true if the component is busy doing background processing (e.g. HTTP downloads) - bool isProcessing() const; - -protected: - void renderChildren(const Transform4x4f& transform) const; - void updateSelf(int deltaTime); // updates animations - void updateChildren(int deltaTime); // updates animations - - unsigned char mOpacity; - Window* mWindow; - - GuiComponent* mParent; - std::vector mChildren; - - std::string mTag; - - Vector3f mPosition; - Vector2f mOrigin; - Vector2f mRotationOrigin; - Vector2f mSize; - - float mRotation = 0.0; - float mScale = 1.0; - - float mDefaultZIndex = 0; - float mZIndex = 0; - - bool mIsProcessing; - -public: - const static unsigned char MAX_ANIMATIONS = 4; - -private: - Transform4x4f mTransform; //Don't access this directly! Use getTransform()! - AnimationController* mAnimationMap[MAX_ANIMATIONS]; -}; - - - -class LocalizationItem -{ -public: - std::string msgid; - std::string msgstr; -}; - -class GuiTextTool -{ -public: -#if defined(_WIN32) - static const std::string convertFromWideString(const std::wstring wstring) - { - int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); - - std::string string; - string.resize(numBytes); - WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); - - return string; - } -#endif - static void ensureLocalisation(); - static const std::string localize(const std::string text); - - static void setLanguage(std::string lang); - -private: - static std::vector mItems; - static std::string mCurrentLanguage; - static bool mCurrentLanguageLoaded; -}; - -#if defined(_WIN32) -#define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) -#define _L(x) GuiTextTool::localize(x) -#else -#define _T(x) GuiTextTool::localize(x) -#define _L(x) GuiTextTool::localize(x) -#endif // _WIN32 - -#endif // ES_CORE_GUI_COMPONENT_H +#pragma once +#ifndef ES_CORE_GUI_COMPONENT_H +#define ES_CORE_GUI_COMPONENT_H + +#include "math/Misc.h" +#include "math/Transform4x4f.h" +#include "HelpPrompt.h" +#include "HelpStyle.h" +#include "InputConfig.h" +#include +#include + +#if defined(_WIN32) +#include +#endif + +class Animation; +class AnimationController; +class Font; +class InputConfig; +class ThemeData; +class Window; + +class GuiComponent +{ +public: + GuiComponent(Window* window); + virtual ~GuiComponent(); + + virtual void textInput(const char* text); + + //Called when input is received. + //Return true if the input is consumed, false if it should continue to be passed to other children. + virtual bool input(InputConfig* config, Input input); + + //Called when time passes. Default implementation calls updateSelf(deltaTime) and updateChildren(deltaTime) - so you should probably call GuiComponent::update(deltaTime) at some point (or at least updateSelf so animations work). + virtual void update(int deltaTime); + + //Called when it's time to render. By default, just calls renderChildren(parentTrans * getTransform()). + //You probably want to override this like so: + //1. Calculate the new transform that your control will draw at with Transform4x4f t = parentTrans * getTransform(). + //2. Set the renderer to use that new transform as the model matrix - Renderer::setMatrix(t); + //3. Draw your component. + //4. Tell your children to render, based on your component's transform - renderChildren(t). + virtual void render(const Transform4x4f& parentTrans); + + Vector3f getPosition() const; + inline void setPosition(const Vector3f& offset) { setPosition(offset.x(), offset.y(), offset.z()); } + void setPosition(float x, float y, float z = 0.0f); + virtual void onPositionChanged() {}; + + //Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) + Vector2f getOrigin() const; + void setOrigin(float originX, float originY); + inline void setOrigin(Vector2f origin) { setOrigin(origin.x(), origin.y()); } + virtual void onOriginChanged() {}; + + //Sets the rotation origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) + Vector2f getRotationOrigin() const; + void setRotationOrigin(float originX, float originY); + inline void setRotationOrigin(Vector2f origin) { setRotationOrigin(origin.x(), origin.y()); } + + virtual Vector2f getSize() const; + inline void setSize(const Vector2f& size) { setSize(size.x(), size.y()); } + void setSize(float w, float h); + virtual void onSizeChanged() {}; + + virtual Vector2f getRotationSize() const { return getSize(); }; + + float getRotation() const; + void setRotation(float rotation); + inline void setRotationDegrees(float rotation) { setRotation((float)ES_DEG_TO_RAD(rotation)); } + + float getScale() const; + void setScale(float scale); + + float getZIndex() const; + void setZIndex(float zIndex); + + float getDefaultZIndex() const; + void setDefaultZIndex(float zIndex); + + // Returns the center point of the image (takes origin into account). + Vector2f getCenter() const; + + void setParent(GuiComponent* parent); + GuiComponent* getParent() const; + + void addChild(GuiComponent* cmp); + void removeChild(GuiComponent* cmp); + void clearChildren(); + void sortChildren(); + unsigned int getChildCount() const; + GuiComponent* getChild(unsigned int i) const; + + // animation will be automatically deleted when it completes or is stopped. + bool isAnimationPlaying(unsigned char slot) const; + bool isAnimationReversed(unsigned char slot) const; + int getAnimationTime(unsigned char slot) const; + void setAnimation(Animation* animation, int delay = 0, std::function finishedCallback = nullptr, bool reverse = false, unsigned char slot = 0); + bool stopAnimation(unsigned char slot); + bool cancelAnimation(unsigned char slot); // Like stopAnimation, but doesn't call finishedCallback - only removes the animation, leaving things in their current state. Returns true if successful (an animation was in this slot). + bool finishAnimation(unsigned char slot); // Calls update(1.f) and finishedCallback, then deletes the animation - basically skips to the end. Returns true if successful (an animation was in this slot). + bool advanceAnimation(unsigned char slot, unsigned int time); // Returns true if successful (an animation was in this slot). + void stopAllAnimations(); + void cancelAllAnimations(); + + virtual unsigned char getOpacity() const; + virtual void setOpacity(unsigned char opacity); + + const Transform4x4f& getTransform(); + + virtual std::string getValue() const; + virtual void setValue(const std::string& value); + + + virtual std::string getTag() const; + virtual void setTag(const std::string& value); + + virtual void onFocusGained() {}; + virtual void onFocusLost() {}; + + virtual void onShow(); + virtual void onHide(); + + virtual void onScreenSaverActivate(); + virtual void onScreenSaverDeactivate(); + virtual void topWindow(bool isTop); + + // Default implementation just handles and tags as normalized float pairs. + // You probably want to keep this behavior for any derived classes as well as add your own. + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); + + // Returns a list of help prompts. + virtual std::vector getHelpPrompts() { return std::vector(); }; + + // Called whenever help prompts change. + void updateHelpPrompts(); + + virtual HelpStyle getHelpStyle(); + + // Returns true if the component is busy doing background processing (e.g. HTTP downloads) + bool isProcessing() const; + +protected: + void renderChildren(const Transform4x4f& transform) const; + void updateSelf(int deltaTime); // updates animations + void updateChildren(int deltaTime); // updates animations + + unsigned char mOpacity; + Window* mWindow; + + GuiComponent* mParent; + std::vector mChildren; + + std::string mTag; + + Vector3f mPosition; + Vector2f mOrigin; + Vector2f mRotationOrigin; + Vector2f mSize; + + float mRotation = 0.0; + float mScale = 1.0; + + float mDefaultZIndex = 0; + float mZIndex = 0; + + bool mIsProcessing; + +public: + const static unsigned char MAX_ANIMATIONS = 4; + +private: + Transform4x4f mTransform; //Don't access this directly! Use getTransform()! + AnimationController* mAnimationMap[MAX_ANIMATIONS]; +}; + + + +class LocalizationItem +{ +public: + std::string msgid; + std::string msgstr; +}; + +class GuiTextTool +{ +public: +#if defined(_WIN32) + static const std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + + std::string string; + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return string; + } +#endif + static void ensureLocalisation(); + static const std::string localize(const std::string text); + + static void setLanguage(std::string lang); + +private: + static std::vector mItems; + static std::string mCurrentLanguage; + static bool mCurrentLanguageLoaded; +}; + +#if defined(_WIN32) +#define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) +#define _L(x) GuiTextTool::localize(x) +#else +#define _T(x) GuiTextTool::localize(x) +#define _L(x) GuiTextTool::localize(x) +#endif // _WIN32 + +#endif // ES_CORE_GUI_COMPONENT_H diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 9ee8ea2fc9..ba4121ce08 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -1,727 +1,730 @@ -#include "ThemeData.h" - -#include "components/ImageComponent.h" -#include "components/TextComponent.h" -#include "utils/FileSystemUtil.h" -#include "Log.h" -#include "platform.h" -#include "Settings.h" -#include -#include - -std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" } }; -std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; - -std::map> ThemeData::sElementMap { - { "image", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "maxSize", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "path", PATH }, - { "default", PATH }, - { "tile", BOOLEAN }, - { "color", COLOR }, - { "zIndex", FLOAT } } }, - { "imagegrid", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "margin", NORMALIZED_PAIR }, - { "gameImage", PATH }, - { "folderImage", PATH }, - { "scrollDirection", STRING } } }, - { "gridtile", { - { "size", NORMALIZED_PAIR }, - { "padding", NORMALIZED_PAIR }, - { "imageColor", COLOR }, - { "backgroundImage", PATH }, - { "backgroundCornerSize", NORMALIZED_PAIR }, - { "backgroundColor", COLOR }, - { "backgroundCenterColor", COLOR }, - { "backgroundEdgeColor", COLOR }, - { "imageSizeMode", STRING } } }, - { "text", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "text", STRING }, - { "backgroundColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "color", COLOR }, - { "alignment", STRING }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "value", STRING }, - { "zIndex", FLOAT } } }, - { "textlist", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "selectorHeight", FLOAT }, - { "selectorOffsetY", FLOAT }, - { "selectorColor", COLOR }, - { "selectorImagePath", PATH }, - { "selectorImageTile", BOOLEAN }, - { "selectedColor", COLOR }, - { "primaryColor", COLOR }, - { "secondaryColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "scrollSound", PATH }, - { "alignment", STRING }, - { "horizontalMargin", FLOAT }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "zIndex", FLOAT } } }, - { "container", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "zIndex", FLOAT } } }, - { "ninepatch", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "path", PATH }, - { "zIndex", FLOAT } } }, - { "datetime", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "backgroundColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "color", COLOR }, - { "alignment", STRING }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "value", STRING }, - { "format", STRING }, - { "displayRelative", BOOLEAN }, - { "zIndex", FLOAT } } }, - { "rating", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "color", COLOR }, - { "filledPath", PATH }, - { "unfilledPath", PATH }, - { "zIndex", FLOAT } } }, - { "sound", { - { "path", PATH } } }, - { "helpsystem", { - { "pos", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "textColor", COLOR }, - { "iconColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT } } }, - { "video", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "maxSize", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "default", PATH }, - { "delay", FLOAT }, - { "zIndex", FLOAT }, - { "showSnapshotNoVideo", BOOLEAN }, - { "showSnapshotDelay", BOOLEAN } } }, - { "carousel", { - { "type", STRING }, - { "size", NORMALIZED_PAIR }, - { "pos", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "color", COLOR }, - { "logoScale", FLOAT }, - { "logoRotation", FLOAT }, - { "logoRotationOrigin", NORMALIZED_PAIR }, - { "logoSize", NORMALIZED_PAIR }, - { "logoAlignment", STRING }, - { "maxLogoCount", FLOAT }, - { "zIndex", FLOAT } } } -}; - -#define MINIMUM_THEME_FORMAT_VERSION 3 -#define CURRENT_THEME_FORMAT_VERSION 6 - -// helper -unsigned int getHexColor(const char* str) -{ - ThemeException error; - if(!str) - throw error << "Empty color"; - - size_t len = strlen(str); - if(len != 6 && len != 8) - throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; - - unsigned int val; - std::stringstream ss; - ss << str; - ss >> std::hex >> val; - - if(len == 6) - val = (val << 8) | 0xFF; - - return val; -} - -std::map mVariables; - -std::string resolvePlaceholders(const char* in) -{ - std::string inStr(in); - - if(inStr.empty()) - return inStr; - - const size_t variableBegin = inStr.find("${"); - const size_t variableEnd = inStr.find("}", variableBegin); - - if((variableBegin == std::string::npos) || (variableEnd == std::string::npos)) - return inStr; - - std::string prefix = inStr.substr(0, variableBegin); - std::string replace = inStr.substr(variableBegin + 2, variableEnd - (variableBegin + 2)); - std::string suffix = resolvePlaceholders(inStr.substr(variableEnd + 1).c_str()); - - return prefix + mVariables[replace] + suffix; -} - -ThemeData::ThemeData() -{ - mVersion = 0; -} - -void ThemeData::loadFile(std::string system, std::map sysDataMap, const std::string& path) -{ - mPaths.push_back(path); - - ThemeException error; - error.setFiles(mPaths); - - if(!Utils::FileSystem::exists(path)) - throw error << "File does not exist!"; - - mVersion = 0; - mViews.clear(); - - mSystemThemeFolder = system; - - mVariables.clear(); - mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); - - pugi::xml_document doc; - pugi::xml_parse_result res = doc.load_file(path.c_str()); - if(!res) - throw error << "XML parsing error: \n " << res.description(); - - pugi::xml_node root = doc.child("theme"); - if(!root) - throw error << "Missing tag!"; - - // parse version - mVersion = root.child("formatVersion").text().as_float(-404); - if(mVersion == -404) - throw error << " tag missing!\n It's either out of date or you need to add " << CURRENT_THEME_FORMAT_VERSION << " inside your tag."; - - if (root.attribute("defaultView")) - mDefaultView = root.attribute("defaultView").as_string(); - - if(mVersion < MINIMUM_THEME_FORMAT_VERSION) - throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; - - parseVariables(root); - parseIncludes(root); - parseViews(root); - parseFeatures(root); -} - -std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) -{ - std::string result = path; - - size_t start_pos = result.find("$system"); - if (start_pos == std::string::npos) - return path; - - result.replace(start_pos, 7, systemThemeFolder); - - //result.replace("$system", systemThemeFolder); - //boost::algorithm::replace_first(result, "$system", systemThemeFolder); - return result; -} - -bool ThemeData::parseSubset(const pugi::xml_node& node) -{ - bool parse = true; - - if (node.attribute("subset")) - { - parse = false; - const std::string subsetAttr = node.attribute("subset").as_string(); - const std::string nameAttr = node.attribute("name").as_string(); - - if (subsetAttr == "iconset" || subsetAttr == "gamelistview") - { - if (nameAttr.rfind("1-") != std::string::npos) - { - parse = true; - return parse; - } - } - else if (subsetAttr == "systemview") - { - if (nameAttr.rfind("8-") != std::string::npos) - { - parse = true; - return parse; - } - } - else if (nameAttr.rfind("2-") != std::string::npos) - { - parse = true; - return parse; - } - /* - if (subsetAttr == "colorset" && nameAttr == mColorset) - { - parse = true; - return parse; - } - if (subsetAttr == "iconset" && nameAttr == mIconset) - { - parse = true; - return parse; - } - if (subsetAttr == "menu" && nameAttr == mMenu) - { - parse = true; - return parse; - } - if (subsetAttr == "systemview" && nameAttr == mSystemview) - { - parse = true; - return parse; - } - if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) - { - parse = true; - return parse; - }*/ - } - - return parse; -} - -void ThemeData::parseIncludes(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) - { - if (!parseSubset(node)) - continue; - - std::string relPath = resolvePlaceholders(node.text().as_string()); - std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); - path = resolveSystemVariable(mSystemThemeFolder, path); - - if (!ResourceManager::getInstance()->fileExists(path)) - continue; - //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; - - error << " from included file \"" << relPath << "\":\n "; - - mPaths.push_back(path); - - pugi::xml_document includeDoc; - pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); - if(!result) - throw error << "Error parsing file: \n " << result.description(); - - pugi::xml_node theme = includeDoc.child("theme"); - if(!theme) - throw error << "Missing tag!"; - - parseVariables(theme); - parseIncludes(theme); - parseViews(theme); - parseFeatures(theme); - - mPaths.pop_back(); - } -} - -void ThemeData::parseFeatures(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) - { - if(!node.attribute("supported")) - throw error << "Feature missing \"supported\" attribute!"; - - const std::string supportedAttr = node.attribute("supported").as_string(); - - if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) - { - parseViews(node); - } - } -} - -void ThemeData::parseVariables(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - pugi::xml_node variables = root.child("variables"); - - if(!variables) - return; - - for(pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) - { - std::string key = it->name(); - std::string val = it->text().as_string(); - - if (!val.empty()) - mVariables.insert(std::pair(key, val)); - } -} - -void ThemeData::parseViews(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - // parse views - for(pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) - { - if(!node.attribute("name")) - throw error << "View missing \"name\" attribute!"; - - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - std::string viewKey; - while(off != std::string::npos || prevOff != std::string::npos) - { - viewKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - - if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) - { - ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; - parseView(node, view); - - if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) - { - ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; - parseView(node, view); - } - } - } - } -} - -void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) - { - if(!node.attribute("name")) - throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; - - auto elemTypeIt = sElementMap.find(node.name()); - if(elemTypeIt == sElementMap.cend()) - throw error << "Unknown element of type \"" << node.name() << "\"!"; - - if (parseRegion(node)) - { - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while (off != std::string::npos || prevOff != std::string::npos) - { - std::string elemKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - - parseElement(node, elemTypeIt->second, - view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); - - if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) - view.orderedKeys.push_back(elemKey); - } - } - } -} - -bool ThemeData::parseRegion(const pugi::xml_node& node) -{ - bool parse = true; - - if (node.attribute("region")) - { - std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); - - parse = false; - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("region").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while (off != std::string::npos || prevOff != std::string::npos) - { - std::string elemKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - if (elemKey == regionsetting) - { - parse = true; - return parse; - } - } - - } - return parse; - -} - - -void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element) -{ - ThemeException error; - error.setFiles(mPaths); - - element.type = root.name(); - element.extra = root.attribute("extra").as_bool(false); - - for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) - { - auto typeIt = typeMap.find(node.name()); - if (typeIt == typeMap.cend()) - continue; - //throw error << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ")."; - - std::string str = resolveSystemVariable(mSystemThemeFolder, resolvePlaceholders(node.text().as_string())); - - switch(typeIt->second) - { - case NORMALIZED_PAIR: - { - size_t divider = str.find(' '); - if(divider == std::string::npos) - throw error << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")"; - - std::string first = str.substr(0, divider); - std::string second = str.substr(divider, std::string::npos); - - Vector2f val((float)atof(first.c_str()), (float)atof(second.c_str())); - - element.properties[node.name()] = val; - break; - } - case STRING: - element.properties[node.name()] = str; - break; - case PATH: - { - std::string path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true); - - if (!ResourceManager::getInstance()->fileExists(path)) - { - std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); - if (ResourceManager::getInstance()->fileExists(rootPath)) - path = rootPath; - } - - if (!ResourceManager::getInstance()->fileExists(path)) - { - std::stringstream ss; - ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda - ss << "could not find file \"" << node.text().get() << "\" "; - if(node.text().get() != path) - ss << "(which resolved to \"" << path << "\") "; - - LOG(LogWarning) << ss.str(); - OutputDebugString(ss.str().c_str()); - } - else - element.properties[node.name()] = path; - - break; - } - case COLOR: - element.properties[node.name()] = getHexColor(str.c_str()); - break; - case FLOAT: - { - float floatVal = static_cast(strtod(str.c_str(), 0)); - element.properties[node.name()] = floatVal; - break; - } - - case BOOLEAN: - { - // only look at first char - char first = str[0]; - // 1*, t* (true), T* (True), y* (yes), Y* (YES) - bool boolVal = (first == '1' || first == 't' || first == 'T' || first == 'y' || first == 'Y'); - - element.properties[node.name()] = boolVal; - break; - } - default: - throw error << "Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name(); - } - } -} - -bool ThemeData::hasView(const std::string& view) -{ - auto viewIt = mViews.find(view); - return (viewIt != mViews.cend()); -} - -const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const -{ - auto viewIt = mViews.find(view); - if(viewIt == mViews.cend()) - return NULL; // not found - - auto elemIt = viewIt->second.elements.find(element); - if(elemIt == viewIt->second.elements.cend()) return NULL; - - if(elemIt->second.type != expectedType && !expectedType.empty()) - { - LOG(LogWarning) << " requested mismatched theme type for [" << view << "." << element << "] - expected \"" - << expectedType << "\", got \"" << elemIt->second.type << "\""; - return NULL; - } - - return &elemIt->second; -} - -const std::shared_ptr& ThemeData::getDefault() -{ - static std::shared_ptr theme = nullptr; - if(theme == nullptr) - { - theme = std::shared_ptr(new ThemeData()); - - const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_theme_default.xml"; - if(Utils::FileSystem::exists(path)) - { - try - { - std::map emptyMap; - theme->loadFile("", emptyMap, path); - } catch(ThemeException& e) - { - LOG(LogError) << e.what(); - theme = std::shared_ptr(new ThemeData()); //reset to empty - } - } - } - - return theme; -} - -std::vector ThemeData::makeExtras(const std::shared_ptr& theme, const std::string& view, Window* window) -{ - std::vector comps; - - auto viewIt = theme->mViews.find(view); - if(viewIt == theme->mViews.cend()) - return comps; - - for(auto it = viewIt->second.orderedKeys.cbegin(); it != viewIt->second.orderedKeys.cend(); it++) - { - ThemeElement& elem = viewIt->second.elements.at(*it); - if(elem.extra) - { - GuiComponent* comp = NULL; - const std::string& t = elem.type; - if(t == "image") - comp = new ImageComponent(window); - else if(t == "text") - comp = new TextComponent(window); - - comp->setDefaultZIndex(10); - comp->applyTheme(theme, view, *it, ThemeFlags::ALL); - comps.push_back(comp); - } - } - - return comps; -} - -std::map ThemeData::getThemeSets() -{ - std::map sets; - - static const size_t pathCount = 2; - std::string paths[pathCount] = - { - "/etc/emulationstation/themes", - Utils::FileSystem::getHomePath() + "/.emulationstation/themes" - }; - - for(size_t i = 0; i < pathCount; i++) - { - if(!Utils::FileSystem::isDirectory(paths[i])) - continue; - - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(paths[i]); - - for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if(Utils::FileSystem::isDirectory(*it)) - { - ThemeSet set = {*it}; - sets[set.getName()] = set; - } - } - } - - return sets; -} - -std::string ThemeData::getThemeFromCurrentSet(const std::string& system) -{ - std::map themeSets = ThemeData::getThemeSets(); - if(themeSets.empty()) - { - // no theme sets available - return ""; - } - - std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(set == themeSets.cend()) - { - // currently selected theme set is missing, so just pick the first available set - set = themeSets.cbegin(); - Settings::getInstance()->setString("ThemeSet", set->first); - } - - return set->second.getThemePath(system); -} +#include "ThemeData.h" + +#include "components/ImageComponent.h" +#include "components/TextComponent.h" +#include "utils/FileSystemUtil.h" +#include "Log.h" +#include "platform.h" +#include "Settings.h" +#include +#include + +std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" } }; +std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; + +std::map> ThemeData::sElementMap { + { "image", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "maxSize", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "path", PATH }, + { "default", PATH }, + { "tile", BOOLEAN }, + { "color", COLOR }, + { "zIndex", FLOAT } } }, + { "imagegrid", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "margin", NORMALIZED_PAIR }, + { "gameImage", PATH }, + { "folderImage", PATH }, + { "scrollDirection", STRING } } }, + { "gridtile", { + { "size", NORMALIZED_PAIR }, + { "padding", NORMALIZED_PAIR }, + { "imageColor", COLOR }, + { "backgroundImage", PATH }, + { "backgroundCornerSize", NORMALIZED_PAIR }, + { "backgroundColor", COLOR }, + { "backgroundCenterColor", COLOR }, + { "backgroundEdgeColor", COLOR }, + { "imageSizeMode", STRING } } }, + { "text", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "text", STRING }, + { "backgroundColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "color", COLOR }, + { "alignment", STRING }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "value", STRING }, + { "zIndex", FLOAT } } }, + { "textlist", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "selectorHeight", FLOAT }, + { "selectorOffsetY", FLOAT }, + { "selectorColor", COLOR }, + { "selectorImagePath", PATH }, + { "selectorImageTile", BOOLEAN }, + { "selectedColor", COLOR }, + { "primaryColor", COLOR }, + { "secondaryColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "scrollSound", PATH }, + { "alignment", STRING }, + { "horizontalMargin", FLOAT }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "zIndex", FLOAT } } }, + { "container", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "zIndex", FLOAT } } }, + { "ninepatch", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "path", PATH }, + { "zIndex", FLOAT } } }, + { "datetime", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "backgroundColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "color", COLOR }, + { "alignment", STRING }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "value", STRING }, + { "format", STRING }, + { "displayRelative", BOOLEAN }, + { "zIndex", FLOAT } } }, + { "rating", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "color", COLOR }, + { "filledPath", PATH }, + { "unfilledPath", PATH }, + { "zIndex", FLOAT } } }, + { "sound", { + { "path", PATH } } }, + { "helpsystem", { + { "pos", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "textColor", COLOR }, + { "iconColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT } } }, + { "video", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "maxSize", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "default", PATH }, + { "delay", FLOAT }, + { "zIndex", FLOAT }, + { "showSnapshotNoVideo", BOOLEAN }, + { "showSnapshotDelay", BOOLEAN } } }, + { "carousel", { + { "type", STRING }, + { "size", NORMALIZED_PAIR }, + { "pos", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "color", COLOR }, + { "logoScale", FLOAT }, + { "logoRotation", FLOAT }, + { "logoRotationOrigin", NORMALIZED_PAIR }, + { "logoSize", NORMALIZED_PAIR }, + { "logoAlignment", STRING }, + { "maxLogoCount", FLOAT }, + { "zIndex", FLOAT } } } +}; + +#define MINIMUM_THEME_FORMAT_VERSION 3 +#define CURRENT_THEME_FORMAT_VERSION 6 + +// helper +unsigned int getHexColor(const char* str) +{ + ThemeException error; + if(!str) + throw error << "Empty color"; + + size_t len = strlen(str); + if(len != 6 && len != 8) + throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; + + unsigned int val; + std::stringstream ss; + ss << str; + ss >> std::hex >> val; + + if(len == 6) + val = (val << 8) | 0xFF; + + return val; +} + +std::map mVariables; + +std::string resolvePlaceholders(const char* in) +{ + std::string inStr(in); + + if(inStr.empty()) + return inStr; + + const size_t variableBegin = inStr.find("${"); + const size_t variableEnd = inStr.find("}", variableBegin); + + if((variableBegin == std::string::npos) || (variableEnd == std::string::npos)) + return inStr; + + std::string prefix = inStr.substr(0, variableBegin); + std::string replace = inStr.substr(variableBegin + 2, variableEnd - (variableBegin + 2)); + std::string suffix = resolvePlaceholders(inStr.substr(variableEnd + 1).c_str()); + + return prefix + mVariables[replace] + suffix; +} + +ThemeData::ThemeData() +{ + mVersion = 0; +} + +void ThemeData::loadFile(std::string system, std::map sysDataMap, const std::string& path) +{ + mPaths.push_back(path); + + ThemeException error; + error.setFiles(mPaths); + + if(!Utils::FileSystem::exists(path)) + throw error << "File does not exist!"; + + mVersion = 0; + mViews.clear(); + + mSystemThemeFolder = system; + + mVariables.clear(); + mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); + + pugi::xml_document doc; + pugi::xml_parse_result res = doc.load_file(path.c_str()); + if(!res) + throw error << "XML parsing error: \n " << res.description(); + + pugi::xml_node root = doc.child("theme"); + if(!root) + throw error << "Missing tag!"; + + // parse version + mVersion = root.child("formatVersion").text().as_float(-404); + if(mVersion == -404) + throw error << " tag missing!\n It's either out of date or you need to add " << CURRENT_THEME_FORMAT_VERSION << " inside your tag."; + + if (root.attribute("defaultView")) + mDefaultView = root.attribute("defaultView").as_string(); + + if(mVersion < MINIMUM_THEME_FORMAT_VERSION) + throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; + + parseVariables(root); + parseIncludes(root); + parseViews(root); + parseFeatures(root); +} + +std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) +{ + std::string result = path; + + size_t start_pos = result.find("$system"); + if (start_pos == std::string::npos) + return path; + + result.replace(start_pos, 7, systemThemeFolder); + + //result.replace("$system", systemThemeFolder); + //boost::algorithm::replace_first(result, "$system", systemThemeFolder); + return result; +} + +bool ThemeData::parseSubset(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("subset")) + { + parse = false; + const std::string subsetAttr = node.attribute("subset").as_string(); + const std::string nameAttr = node.attribute("name").as_string(); + + if (subsetAttr == "iconset" || subsetAttr == "gamelistview") + { + if (nameAttr.rfind("1-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (subsetAttr == "systemview") + { + if (nameAttr.rfind("8-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (nameAttr.rfind("2-") != std::string::npos) + { + parse = true; + return parse; + } + /* + if (subsetAttr == "colorset" && nameAttr == mColorset) + { + parse = true; + return parse; + } + if (subsetAttr == "iconset" && nameAttr == mIconset) + { + parse = true; + return parse; + } + if (subsetAttr == "menu" && nameAttr == mMenu) + { + parse = true; + return parse; + } + if (subsetAttr == "systemview" && nameAttr == mSystemview) + { + parse = true; + return parse; + } + if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) + { + parse = true; + return parse; + }*/ + } + + return parse; +} + +void ThemeData::parseIncludes(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) + { + if (!parseSubset(node)) + continue; + + std::string relPath = resolvePlaceholders(node.text().as_string()); + std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); + path = resolveSystemVariable(mSystemThemeFolder, path); + + if (!ResourceManager::getInstance()->fileExists(path)) + continue; + //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + + error << " from included file \"" << relPath << "\":\n "; + + mPaths.push_back(path); + + pugi::xml_document includeDoc; + pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); + if(!result) + throw error << "Error parsing file: \n " << result.description(); + + pugi::xml_node theme = includeDoc.child("theme"); + if(!theme) + throw error << "Missing tag!"; + + parseVariables(theme); + parseIncludes(theme); + parseViews(theme); + parseFeatures(theme); + + mPaths.pop_back(); + } +} + +void ThemeData::parseFeatures(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + for(pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) + { + if(!node.attribute("supported")) + throw error << "Feature missing \"supported\" attribute!"; + + const std::string supportedAttr = node.attribute("supported").as_string(); + + if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) + { + parseViews(node); + } + } +} + +void ThemeData::parseVariables(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + pugi::xml_node variables = root.child("variables"); + + if(!variables) + return; + + for(pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) + { + std::string key = it->name(); + std::string val = it->text().as_string(); + + if (!val.empty()) + mVariables.insert(std::pair(key, val)); + } +} + +void ThemeData::parseViews(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + // parse views + for(pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) + { + if(!node.attribute("name")) + throw error << "View missing \"name\" attribute!"; + + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + std::string viewKey; + while(off != std::string::npos || prevOff != std::string::npos) + { + viewKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) + { + ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; + parseView(node, view); + + if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) + { + ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; + parseView(node, view); + } + } + } + } +} + +void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) +{ + ThemeException error; + error.setFiles(mPaths); + + for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + { + if(!node.attribute("name")) + throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; + + auto elemTypeIt = sElementMap.find(node.name()); + if(elemTypeIt == sElementMap.cend()) + throw error << "Unknown element of type \"" << node.name() << "\"!"; + + if (parseRegion(node)) + { + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + parseElement(node, elemTypeIt->second, + view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); + + if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) + view.orderedKeys.push_back(elemKey); + } + } + } +} + +bool ThemeData::parseRegion(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("region")) + { + std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); + + parse = false; + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("region").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + if (elemKey == regionsetting) + { + parse = true; + return parse; + } + } + + } + return parse; + +} + + +void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element) +{ + ThemeException error; + error.setFiles(mPaths); + + element.type = root.name(); + element.extra = root.attribute("extra").as_bool(false); + + for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + { + auto typeIt = typeMap.find(node.name()); + if (typeIt == typeMap.cend()) + continue; + //throw error << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ")."; + + std::string str = resolveSystemVariable(mSystemThemeFolder, resolvePlaceholders(node.text().as_string())); + + switch(typeIt->second) + { + case NORMALIZED_PAIR: + { + size_t divider = str.find(' '); + if(divider == std::string::npos) + throw error << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")"; + + std::string first = str.substr(0, divider); + std::string second = str.substr(divider, std::string::npos); + + Vector2f val((float)atof(first.c_str()), (float)atof(second.c_str())); + + element.properties[node.name()] = val; + break; + } + case STRING: + element.properties[node.name()] = str; + break; + case PATH: + { + std::string path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true); + + if (!ResourceManager::getInstance()->fileExists(path)) + { + std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); + if (ResourceManager::getInstance()->fileExists(rootPath)) + path = rootPath; + } + + if (!ResourceManager::getInstance()->fileExists(path)) + { + std::stringstream ss; + ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda + ss << "could not find file \"" << node.text().get() << "\" "; + if(node.text().get() != path) + ss << "(which resolved to \"" << path << "\") "; + + LOG(LogWarning) << ss.str(); + + auto test = element.properties[node.name()]; + if (test.s.empty()) + element.properties[node.name()] = path; + } + else + element.properties[node.name()] = path; + + break; + } + case COLOR: + element.properties[node.name()] = getHexColor(str.c_str()); + break; + case FLOAT: + { + float floatVal = static_cast(strtod(str.c_str(), 0)); + element.properties[node.name()] = floatVal; + break; + } + + case BOOLEAN: + { + // only look at first char + char first = str[0]; + // 1*, t* (true), T* (True), y* (yes), Y* (YES) + bool boolVal = (first == '1' || first == 't' || first == 'T' || first == 'y' || first == 'Y'); + + element.properties[node.name()] = boolVal; + break; + } + default: + throw error << "Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name(); + } + } +} + +bool ThemeData::hasView(const std::string& view) +{ + auto viewIt = mViews.find(view); + return (viewIt != mViews.cend()); +} + +const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const +{ + auto viewIt = mViews.find(view); + if(viewIt == mViews.cend()) + return NULL; // not found + + auto elemIt = viewIt->second.elements.find(element); + if(elemIt == viewIt->second.elements.cend()) return NULL; + + if(elemIt->second.type != expectedType && !expectedType.empty()) + { + LOG(LogWarning) << " requested mismatched theme type for [" << view << "." << element << "] - expected \"" + << expectedType << "\", got \"" << elemIt->second.type << "\""; + return NULL; + } + + return &elemIt->second; +} + +const std::shared_ptr& ThemeData::getDefault() +{ + static std::shared_ptr theme = nullptr; + if(theme == nullptr) + { + theme = std::shared_ptr(new ThemeData()); + + const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_theme_default.xml"; + if(Utils::FileSystem::exists(path)) + { + try + { + std::map emptyMap; + theme->loadFile("", emptyMap, path); + } catch(ThemeException& e) + { + LOG(LogError) << e.what(); + theme = std::shared_ptr(new ThemeData()); //reset to empty + } + } + } + + return theme; +} + +std::vector ThemeData::makeExtras(const std::shared_ptr& theme, const std::string& view, Window* window) +{ + std::vector comps; + + auto viewIt = theme->mViews.find(view); + if(viewIt == theme->mViews.cend()) + return comps; + + for(auto it = viewIt->second.orderedKeys.cbegin(); it != viewIt->second.orderedKeys.cend(); it++) + { + ThemeElement& elem = viewIt->second.elements.at(*it); + if(elem.extra) + { + GuiComponent* comp = NULL; + const std::string& t = elem.type; + if(t == "image") + comp = new ImageComponent(window); + else if(t == "text") + comp = new TextComponent(window); + + comp->setDefaultZIndex(10); + comp->applyTheme(theme, view, *it, ThemeFlags::ALL); + comps.push_back(comp); + } + } + + return comps; +} + +std::map ThemeData::getThemeSets() +{ + std::map sets; + + static const size_t pathCount = 2; + std::string paths[pathCount] = + { + "/etc/emulationstation/themes", + Utils::FileSystem::getHomePath() + "/.emulationstation/themes" + }; + + for(size_t i = 0; i < pathCount; i++) + { + if(!Utils::FileSystem::isDirectory(paths[i])) + continue; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(paths[i]); + + for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if(Utils::FileSystem::isDirectory(*it)) + { + ThemeSet set = {*it}; + sets[set.getName()] = set; + } + } + } + + return sets; +} + +std::string ThemeData::getThemeFromCurrentSet(const std::string& system) +{ + std::map themeSets = ThemeData::getThemeSets(); + if(themeSets.empty()) + { + // no theme sets available + return ""; + } + + std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if(set == themeSets.cend()) + { + // currently selected theme set is missing, so just pick the first available set + set = themeSets.cbegin(); + Settings::getInstance()->setString("ThemeSet", set->first); + } + + return set->second.getThemePath(system); +} diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 1b361f408e..7a3164f882 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -1,531 +1,531 @@ -#include "Window.h" - -#include "components/HelpComponent.h" -#include "components/ImageComponent.h" -#include "resources/Font.h" -#include "resources/TextureResource.h" -#include "InputManager.h" -#include "Log.h" -#include "Renderer.h" -#include "Scripting.h" -#include -#include - -#include - -Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), - mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) -{ - mHelp = new HelpComponent(this); - mBackgroundOverlay = new ImageComponent(this); - mSplash = NULL; - -} - -Window::~Window() -{ - delete mBackgroundOverlay; - - // delete all our GUIs - while(peekGui()) - delete peekGui(); - - delete mHelp; -} - -void Window::pushGui(GuiComponent* gui) -{ - if (mGuiStack.size() > 0) - { - auto& top = mGuiStack.back(); - top->topWindow(false); - } - mGuiStack.push_back(gui); - gui->updateHelpPrompts(); -} - -void Window::removeGui(GuiComponent* gui) -{ - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - { - if(*i == gui) - { - i = mGuiStack.erase(i); - - if(i == mGuiStack.cend() && mGuiStack.size()) // we just popped the stack and the stack is not empty - { - mGuiStack.back()->updateHelpPrompts(); - mGuiStack.back()->topWindow(true); - } - - return; - } - } -} - -GuiComponent* Window::peekGui() -{ - if(mGuiStack.size() == 0) - return NULL; - - return mGuiStack.back(); -} - -bool Window::init() -{ - if(!Renderer::init()) - { - LOG(LogError) << "Renderer failed to initialize!"; - return false; - } - - InputManager::getInstance()->init(); - ResourceManager::getInstance()->reloadAll(); - - //keep a reference to the default fonts, so they don't keep getting destroyed/recreated - if(mDefaultFonts.empty()) - { - mDefaultFonts.push_back(Font::get(FONT_SIZE_SMALL)); - mDefaultFonts.push_back(Font::get(FONT_SIZE_MEDIUM)); - mDefaultFonts.push_back(Font::get(FONT_SIZE_LARGE)); - } - - mBackgroundOverlay->setImage(":/scroll_gradient.png"); - mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - - // update our help because font sizes probably changed - if (peekGui()) - peekGui()->updateHelpPrompts(); - - return true; -} - -void Window::deinit() -{ - // Hide all GUI elements on uninitialisation - this disable - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - { - (*i)->onHide(); - } - InputManager::getInstance()->deinit(); - ResourceManager::getInstance()->unloadAll(); - Renderer::deinit(); -} - -void Window::textInput(const char* text) -{ - if(peekGui()) - peekGui()->textInput(text); -} - -void Window::input(InputConfig* config, Input input) -{ - if (mScreenSaver) { - if(mScreenSaver->isScreenSaverActive() && Settings::getInstance()->getBool("ScreenSaverControls") && - (Settings::getInstance()->getString("ScreenSaverBehavior") == "random video")) - { - if(mScreenSaver->getCurrentGame() != NULL && (config->isMappedLike("right", input) || config->isMappedTo("start", input) || config->isMappedTo("select", input))) - { - if(config->isMappedLike("right", input) || config->isMappedTo("select", input)) - { - if (input.value != 0) { - // handle screensaver control - mScreenSaver->nextVideo(); - } - return; - } - else if(config->isMappedTo("start", input) && input.value != 0) - { - // launch game! - cancelScreenSaver(); - mScreenSaver->launchGame(); - // to force handling the wake up process - mSleeping = true; - } - } - } - } - - if(mSleeping) - { - // wake up - mTimeSinceLastInput = 0; - cancelScreenSaver(); - mSleeping = false; - onWake(); - return; - } - - mTimeSinceLastInput = 0; - if (cancelScreenSaver()) - return; - - if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle debug grid with Ctrl-G - Settings::getInstance()->setBool("DebugGrid", !Settings::getInstance()->getBool("DebugGrid")); - } - else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle TextComponent debug view with Ctrl-T - Settings::getInstance()->setBool("DebugText", !Settings::getInstance()->getBool("DebugText")); - } - else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle TextComponent debug view with Ctrl-I - Settings::getInstance()->setBool("DebugImage", !Settings::getInstance()->getBool("DebugImage")); - } - else - { - if (peekGui()) - { - this->peekGui()->input(config, input); // this is where the majority of inputs will be consumed: the GuiComponent Stack - } - } -} - -void Window::update(int deltaTime) -{ - if(mNormalizeNextUpdate) - { - mNormalizeNextUpdate = false; - if(deltaTime > mAverageDeltaTime) - deltaTime = mAverageDeltaTime; - } - - mFrameTimeElapsed += deltaTime; - mFrameCountElapsed++; - if(mFrameTimeElapsed > 500) - { - mAverageDeltaTime = mFrameTimeElapsed / mFrameCountElapsed; - - if(Settings::getInstance()->getBool("DrawFramerate")) - { - std::stringstream ss; - - // fps - ss << std::fixed << std::setprecision(1) << (1000.0f * (float)mFrameCountElapsed / (float)mFrameTimeElapsed) << "fps, "; - ss << std::fixed << std::setprecision(2) << ((float)mFrameTimeElapsed / (float)mFrameCountElapsed) << "ms"; - - // vram - float textureVramUsageMb = TextureResource::getTotalMemUsage() / 1000.0f / 1000.0f; - float textureTotalUsageMb = TextureResource::getTotalTextureSize() / 1000.0f / 1000.0f; - float fontVramUsageMb = Font::getTotalMemUsage() / 1000.0f / 1000.0f; - - ss << "\nFont VRAM: " << fontVramUsageMb << " Tex VRAM: " << textureVramUsageMb << - " Tex Max: " << textureTotalUsageMb; - mFrameDataText = std::unique_ptr(mDefaultFonts.at(1)->buildTextCache(ss.str(), 50.f, 50.f, 0xFF00FFFF)); - } - - mFrameTimeElapsed = 0; - mFrameCountElapsed = 0; - } - - mTimeSinceLastInput += deltaTime; - - if(peekGui()) - peekGui()->update(deltaTime); - - // Update the screensaver - if (mScreenSaver) - mScreenSaver->update(deltaTime); -} - -void Window::render() -{ - Transform4x4f transform = Transform4x4f::Identity(); - - mRenderedHelpPrompts = false; - - // draw only bottom and top of GuiStack (if they are different) - if(mGuiStack.size()) - { - auto& bottom = mGuiStack.front(); - auto& top = mGuiStack.back(); - - bottom->render(transform); - if(bottom != top) - { - mBackgroundOverlay->render(transform); - top->render(transform); - } - } - - if(!mRenderedHelpPrompts) - mHelp->render(transform); - - if(Settings::getInstance()->getBool("DrawFramerate") && mFrameDataText) - { - Renderer::setMatrix(Transform4x4f::Identity()); - mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); - } - - unsigned int screensaverTime = (unsigned int)Settings::getInstance()->getInt("ScreenSaverTime"); - if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) - startScreenSaver(); - - // Always call the screensaver render function regardless of whether the screensaver is active - // or not because it may perform a fade on transition - renderScreenSaver(); - - if(!mRenderScreenSaver && mInfoPopup) - { - mInfoPopup->render(transform); - } - - if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) - { - if (!isProcessing() && mAllowSleep && (!mScreenSaver || mScreenSaver->allowSleep())) - { - // go to sleep - if (mSleeping == false) { - mSleeping = true; - onSleep(); - } - } - } -} - -void Window::normalizeNextUpdate() -{ - mNormalizeNextUpdate = true; -} - -bool Window::getAllowSleep() -{ - return mAllowSleep; -} - -void Window::setAllowSleep(bool sleep) -{ - mAllowSleep = sleep; -} - -void Window::endRenderLoadingScreen() -{ - mSplash = NULL; -} - -void Window::renderLoadingScreen(std::string text, float percent) -{ -#if defined(_WIN32) - // Avoid Window Freezing on Windows - SDL_Event event; - while (SDL_PollEvent(&event)) ; -#endif - - Transform4x4f trans = Transform4x4f::Identity(); - Renderer::setMatrix(trans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); - - if (percent > 0) - { - float baseHeight = 0.04f; - - float w = Renderer::getScreenWidth() / 2; - float h = Renderer::getScreenHeight() * baseHeight; - - float x = Renderer::getScreenWidth() / 2 - w / 2; - float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); - - Renderer::drawRect(x, y, w, h, 0x252525FF); - Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF - } - - - ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); - -#if defined(_WIN32) - if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); -#endif - - if (mSplash != NULL) - splash.setImage(mSplash); - else - splash.setImage(":/splash.svg"); - - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); - splash.render(trans); - - - auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); - - float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.8f); // 35 - trans = trans.translate(Vector3f(x, y, 0.0f)); - Renderer::setMatrix(trans); - font->renderTextCache(cache); - delete cache; - - - Renderer::swapBuffers(); -} - -void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) -{ - Transform4x4f trans = Transform4x4f::Identity(); - Renderer::setMatrix(trans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); - - ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); - -#if defined(_WIN32) - if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); -#endif - - if (mSplash != NULL) - splash.setImage(mSplash); - else - splash.setImage(":/splash.svg"); - - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); - splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); - splash.render(trans); - - auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); - - float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.835f); - trans = trans.translate(Vector3f(x, y, 0.0f)); - Renderer::setMatrix(trans); - font->renderTextCache(cache); - delete cache; - - if (swapBuffers) - Renderer::swapBuffers(); -} - - -void Window::renderHelpPromptsEarly() -{ - mHelp->render(Transform4x4f::Identity()); - mRenderedHelpPrompts = true; -} - -void Window::setHelpPrompts(const std::vector& prompts, const HelpStyle& style) -{ - mHelp->clearPrompts(); - mHelp->setStyle(style); - - std::vector addPrompts; - - std::map inputSeenMap; - std::map mappedToSeenMap; - for(auto it = prompts.cbegin(); it != prompts.cend(); it++) - { - // only add it if the same icon hasn't already been added - if(inputSeenMap.emplace(it->first, true).second) - { - // this symbol hasn't been seen yet, what about the action name? - auto mappedTo = mappedToSeenMap.find(it->second); - if(mappedTo != mappedToSeenMap.cend()) - { - // yes, it has! - - // can we combine? (dpad only) - if((it->first == "up/down" && addPrompts.at(mappedTo->second).first != "left/right") || - (it->first == "left/right" && addPrompts.at(mappedTo->second).first != "up/down")) - { - // yes! - addPrompts.at(mappedTo->second).first = "up/down/left/right"; - // don't need to add this to addPrompts since we just merged - }else{ - // no, we can't combine! - addPrompts.push_back(*it); - } - }else{ - // no, it hasn't! - mappedToSeenMap.emplace(it->second, (int)addPrompts.size()); - addPrompts.push_back(*it); - } - } - } - - // sort prompts so it goes [dpad_all] [dpad_u/d] [dpad_l/r] [a/b/x/y/l/r] [start/select] - std::sort(addPrompts.begin(), addPrompts.end(), [](const HelpPrompt& a, const HelpPrompt& b) -> bool { - - static const char* map[] = { - "up/down/left/right", - "up/down", - "left/right", - "a", "b", "x", "y", "l", "r", - "start", "select", - NULL - }; - - int i = 0; - int aVal = 0; - int bVal = 0; - while(map[i] != NULL) - { - if(a.first == map[i]) - aVal = i; - if(b.first == map[i]) - bVal = i; - i++; - } - - return aVal > bVal; - }); - - mHelp->setPrompts(addPrompts); -} - - -void Window::onSleep() -{ - Scripting::fireEvent("sleep"); -} - -void Window::onWake() -{ - Scripting::fireEvent("wake"); -} - -bool Window::isProcessing() -{ - return count_if(mGuiStack.cbegin(), mGuiStack.cend(), [](GuiComponent* c) { return c->isProcessing(); }) > 0; -} - -void Window::startScreenSaver() -{ - if (mScreenSaver && !mRenderScreenSaver) - { - // Tell the GUI components the screensaver is starting - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - (*i)->onScreenSaverActivate(); - - mScreenSaver->startScreenSaver(); - mRenderScreenSaver = true; - } -} - -bool Window::cancelScreenSaver() -{ - if (mScreenSaver && mRenderScreenSaver) - { - mScreenSaver->stopScreenSaver(); - mRenderScreenSaver = false; - mScreenSaver->resetCounts(); - - // Tell the GUI components the screensaver has stopped - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - (*i)->onScreenSaverDeactivate(); - - return true; - } - - return false; -} - -void Window::renderScreenSaver() -{ - if (mScreenSaver) - mScreenSaver->renderScreenSaver(); -} +#include "Window.h" + +#include "components/HelpComponent.h" +#include "components/ImageComponent.h" +#include "resources/Font.h" +#include "resources/TextureResource.h" +#include "InputManager.h" +#include "Log.h" +#include "Renderer.h" +#include "Scripting.h" +#include +#include + +#include + +Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), + mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) +{ + mHelp = new HelpComponent(this); + mBackgroundOverlay = new ImageComponent(this); + mSplash = NULL; + +} + +Window::~Window() +{ + delete mBackgroundOverlay; + + // delete all our GUIs + while(peekGui()) + delete peekGui(); + + delete mHelp; +} + +void Window::pushGui(GuiComponent* gui) +{ + if (mGuiStack.size() > 0) + { + auto& top = mGuiStack.back(); + top->topWindow(false); + } + mGuiStack.push_back(gui); + gui->updateHelpPrompts(); +} + +void Window::removeGui(GuiComponent* gui) +{ + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + { + if(*i == gui) + { + i = mGuiStack.erase(i); + + if(i == mGuiStack.cend() && mGuiStack.size()) // we just popped the stack and the stack is not empty + { + mGuiStack.back()->updateHelpPrompts(); + mGuiStack.back()->topWindow(true); + } + + return; + } + } +} + +GuiComponent* Window::peekGui() +{ + if(mGuiStack.size() == 0) + return NULL; + + return mGuiStack.back(); +} + +bool Window::init() +{ + if(!Renderer::init()) + { + LOG(LogError) << "Renderer failed to initialize!"; + return false; + } + + InputManager::getInstance()->init(); + ResourceManager::getInstance()->reloadAll(); + + //keep a reference to the default fonts, so they don't keep getting destroyed/recreated + if(mDefaultFonts.empty()) + { + mDefaultFonts.push_back(Font::get(FONT_SIZE_SMALL)); + mDefaultFonts.push_back(Font::get(FONT_SIZE_MEDIUM)); + mDefaultFonts.push_back(Font::get(FONT_SIZE_LARGE)); + } + + mBackgroundOverlay->setImage(":/scroll_gradient.png"); + mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + + // update our help because font sizes probably changed + if (peekGui()) + peekGui()->updateHelpPrompts(); + + return true; +} + +void Window::deinit() +{ + // Hide all GUI elements on uninitialisation - this disable + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + { + (*i)->onHide(); + } + InputManager::getInstance()->deinit(); + ResourceManager::getInstance()->unloadAll(); + Renderer::deinit(); +} + +void Window::textInput(const char* text) +{ + if(peekGui()) + peekGui()->textInput(text); +} + +void Window::input(InputConfig* config, Input input) +{ + if (mScreenSaver) { + if(mScreenSaver->isScreenSaverActive() && Settings::getInstance()->getBool("ScreenSaverControls") && + (Settings::getInstance()->getString("ScreenSaverBehavior") == "random video")) + { + if(mScreenSaver->getCurrentGame() != NULL && (config->isMappedLike("right", input) || config->isMappedTo("start", input) || config->isMappedTo("select", input))) + { + if(config->isMappedLike("right", input) || config->isMappedTo("select", input)) + { + if (input.value != 0) { + // handle screensaver control + mScreenSaver->nextVideo(); + } + return; + } + else if(config->isMappedTo("start", input) && input.value != 0) + { + // launch game! + cancelScreenSaver(); + mScreenSaver->launchGame(); + // to force handling the wake up process + mSleeping = true; + } + } + } + } + + if(mSleeping) + { + // wake up + mTimeSinceLastInput = 0; + cancelScreenSaver(); + mSleeping = false; + onWake(); + return; + } + + mTimeSinceLastInput = 0; + if (cancelScreenSaver()) + return; + + if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle debug grid with Ctrl-G + Settings::getInstance()->setBool("DebugGrid", !Settings::getInstance()->getBool("DebugGrid")); + } + else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle TextComponent debug view with Ctrl-T + Settings::getInstance()->setBool("DebugText", !Settings::getInstance()->getBool("DebugText")); + } + else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle TextComponent debug view with Ctrl-I + Settings::getInstance()->setBool("DebugImage", !Settings::getInstance()->getBool("DebugImage")); + } + else + { + if (peekGui()) + { + this->peekGui()->input(config, input); // this is where the majority of inputs will be consumed: the GuiComponent Stack + } + } +} + +void Window::update(int deltaTime) +{ + if(mNormalizeNextUpdate) + { + mNormalizeNextUpdate = false; + if(deltaTime > mAverageDeltaTime) + deltaTime = mAverageDeltaTime; + } + + mFrameTimeElapsed += deltaTime; + mFrameCountElapsed++; + if(mFrameTimeElapsed > 500) + { + mAverageDeltaTime = mFrameTimeElapsed / mFrameCountElapsed; + + if(Settings::getInstance()->getBool("DrawFramerate")) + { + std::stringstream ss; + + // fps + ss << std::fixed << std::setprecision(1) << (1000.0f * (float)mFrameCountElapsed / (float)mFrameTimeElapsed) << "fps, "; + ss << std::fixed << std::setprecision(2) << ((float)mFrameTimeElapsed / (float)mFrameCountElapsed) << "ms"; + + // vram + float textureVramUsageMb = TextureResource::getTotalMemUsage() / 1000.0f / 1000.0f; + float textureTotalUsageMb = TextureResource::getTotalTextureSize() / 1000.0f / 1000.0f; + float fontVramUsageMb = Font::getTotalMemUsage() / 1000.0f / 1000.0f; + + ss << "\nFont VRAM: " << fontVramUsageMb << " Tex VRAM: " << textureVramUsageMb << + " Tex Max: " << textureTotalUsageMb; + mFrameDataText = std::unique_ptr(mDefaultFonts.at(1)->buildTextCache(ss.str(), 50.f, 50.f, 0xFF00FFFF)); + } + + mFrameTimeElapsed = 0; + mFrameCountElapsed = 0; + } + + mTimeSinceLastInput += deltaTime; + + if(peekGui()) + peekGui()->update(deltaTime); + + // Update the screensaver + if (mScreenSaver) + mScreenSaver->update(deltaTime); +} + +void Window::render() +{ + Transform4x4f transform = Transform4x4f::Identity(); + + mRenderedHelpPrompts = false; + + // draw only bottom and top of GuiStack (if they are different) + if(mGuiStack.size()) + { + auto& bottom = mGuiStack.front(); + auto& top = mGuiStack.back(); + + bottom->render(transform); + if(bottom != top) + { + mBackgroundOverlay->render(transform); + top->render(transform); + } + } + + if(!mRenderedHelpPrompts) + mHelp->render(transform); + + if(Settings::getInstance()->getBool("DrawFramerate") && mFrameDataText) + { + Renderer::setMatrix(Transform4x4f::Identity()); + mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); + } + + unsigned int screensaverTime = (unsigned int)Settings::getInstance()->getInt("ScreenSaverTime"); + if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) + startScreenSaver(); + + // Always call the screensaver render function regardless of whether the screensaver is active + // or not because it may perform a fade on transition + renderScreenSaver(); + + if(!mRenderScreenSaver && mInfoPopup) + { + mInfoPopup->render(transform); + } + + if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) + { + if (!isProcessing() && mAllowSleep && (!mScreenSaver || mScreenSaver->allowSleep())) + { + // go to sleep + if (mSleeping == false) { + mSleeping = true; + onSleep(); + } + } + } +} + +void Window::normalizeNextUpdate() +{ + mNormalizeNextUpdate = true; +} + +bool Window::getAllowSleep() +{ + return mAllowSleep; +} + +void Window::setAllowSleep(bool sleep) +{ + mAllowSleep = sleep; +} + +void Window::endRenderLoadingScreen() +{ + mSplash = NULL; +} + +void Window::renderLoadingScreen(std::string text, float percent) +{ +#if defined(_WIN32) + // Avoid Window Freezing on Windows + SDL_Event event; + while (SDL_PollEvent(&event)) ; +#endif + + Transform4x4f trans = Transform4x4f::Identity(); + Renderer::setMatrix(trans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); + + if (percent >= 0) + { + float baseHeight = 0.04f; + + float w = Renderer::getScreenWidth() / 2; + float h = Renderer::getScreenHeight() * baseHeight; + + float x = Renderer::getScreenWidth() / 2 - w / 2; + float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); + + Renderer::drawRect(x, y, w, h, 0x252525FF); + Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF + } + + + ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.render(trans); + + + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); + + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.78f); // 35 + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; + + + Renderer::swapBuffers(); +} + +void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) +{ + Transform4x4f trans = Transform4x4f::Identity(); + Renderer::setMatrix(trans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); + + ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); + splash.render(trans); + + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); + + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.835f); + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; + + if (swapBuffers) + Renderer::swapBuffers(); +} + + +void Window::renderHelpPromptsEarly() +{ + mHelp->render(Transform4x4f::Identity()); + mRenderedHelpPrompts = true; +} + +void Window::setHelpPrompts(const std::vector& prompts, const HelpStyle& style) +{ + mHelp->clearPrompts(); + mHelp->setStyle(style); + + std::vector addPrompts; + + std::map inputSeenMap; + std::map mappedToSeenMap; + for(auto it = prompts.cbegin(); it != prompts.cend(); it++) + { + // only add it if the same icon hasn't already been added + if(inputSeenMap.emplace(it->first, true).second) + { + // this symbol hasn't been seen yet, what about the action name? + auto mappedTo = mappedToSeenMap.find(it->second); + if(mappedTo != mappedToSeenMap.cend()) + { + // yes, it has! + + // can we combine? (dpad only) + if((it->first == "up/down" && addPrompts.at(mappedTo->second).first != "left/right") || + (it->first == "left/right" && addPrompts.at(mappedTo->second).first != "up/down")) + { + // yes! + addPrompts.at(mappedTo->second).first = "up/down/left/right"; + // don't need to add this to addPrompts since we just merged + }else{ + // no, we can't combine! + addPrompts.push_back(*it); + } + }else{ + // no, it hasn't! + mappedToSeenMap.emplace(it->second, (int)addPrompts.size()); + addPrompts.push_back(*it); + } + } + } + + // sort prompts so it goes [dpad_all] [dpad_u/d] [dpad_l/r] [a/b/x/y/l/r] [start/select] + std::sort(addPrompts.begin(), addPrompts.end(), [](const HelpPrompt& a, const HelpPrompt& b) -> bool { + + static const char* map[] = { + "up/down/left/right", + "up/down", + "left/right", + "a", "b", "x", "y", "l", "r", + "start", "select", + NULL + }; + + int i = 0; + int aVal = 0; + int bVal = 0; + while(map[i] != NULL) + { + if(a.first == map[i]) + aVal = i; + if(b.first == map[i]) + bVal = i; + i++; + } + + return aVal > bVal; + }); + + mHelp->setPrompts(addPrompts); +} + + +void Window::onSleep() +{ + Scripting::fireEvent("sleep"); +} + +void Window::onWake() +{ + Scripting::fireEvent("wake"); +} + +bool Window::isProcessing() +{ + return count_if(mGuiStack.cbegin(), mGuiStack.cend(), [](GuiComponent* c) { return c->isProcessing(); }) > 0; +} + +void Window::startScreenSaver() +{ + if (mScreenSaver && !mRenderScreenSaver) + { + // Tell the GUI components the screensaver is starting + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + (*i)->onScreenSaverActivate(); + + mScreenSaver->startScreenSaver(); + mRenderScreenSaver = true; + } +} + +bool Window::cancelScreenSaver() +{ + if (mScreenSaver && mRenderScreenSaver) + { + mScreenSaver->stopScreenSaver(); + mRenderScreenSaver = false; + mScreenSaver->resetCounts(); + + // Tell the GUI components the screensaver has stopped + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + (*i)->onScreenSaverDeactivate(); + + return true; + } + + return false; +} + +void Window::renderScreenSaver() +{ + if (mScreenSaver) + mScreenSaver->renderScreenSaver(); +} diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index 613a22c026..f2d36950d0 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -282,17 +282,17 @@ class OptionListComponent : public GuiComponent onSelectedChanged(); } - void clear() { - mEntries.clear(); + void clear() { + mEntries.clear(); } - inline void invalidate() { - onSelectedChanged(); + inline void invalidate() { + onSelectedChanged(); } - void setSelectedChangedCallback(const std::function& callback) - { - mSelectedChangedCallback = callback; + void setSelectedChangedCallback(const std::function& callback) + { + mSelectedChangedCallback = callback; } @@ -344,7 +344,7 @@ class OptionListComponent : public GuiComponent } } - if (mSelectedChangedCallback) + if (mSelectedChangedCallback) mSelectedChangedCallback(mEntries.at(getSelectedId()).object); } diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index e80e589518..bf07b39759 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -1,165 +1,165 @@ -#include "platform.h" -#include - -#ifdef WIN32 -#include -#include -#else -#include -#endif -#include - -#include "Window.h" - -#include "GuiComponent.h" - -int runShutdownCommand() -{ -#ifdef WIN32 // windows - return system("shutdown -s -t 0"); -#else // osx / linux - return system("sudo shutdown -h now"); -#endif -} - -int runRestartCommand() -{ -#ifdef WIN32 // windows - return system("shutdown -r -t 0"); -#else // osx / linux - return system("sudo shutdown -r now"); -#endif -} - -std::string trim(const std::string& str) -{ - size_t first = str.find_first_not_of(' '); - if (std::string::npos == first) - { - return str; - } - size_t last = str.find_last_not_of(' '); - return str.substr(first, (last - first + 1)); -} - -void split_cmd(const std::string& cmd, - std::string* executable, - std::string* parameters) -{ - std::string c(cmd); - size_t exec_end; - - c = trim(c); - //boost::trim_all(c); - - if (c[0] == '\"') - { - exec_end = c.find_first_of('\"', 1); - if (std::string::npos != exec_end) - { - *executable = c.substr(1, exec_end - 1); - *parameters = c.substr(exec_end + 1); - } - else - { - *executable = c.substr(1, exec_end); - std::string().swap(*parameters); - } - } - else - { - exec_end = c.find_first_of(' ', 0); - if (std::string::npos != exec_end) - { - *executable = c.substr(0, exec_end); - *parameters = c.substr(exec_end + 1); - } - else - { - *executable = c.substr(0, exec_end); - std::string().swap(*parameters); - } - } -} - -int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window) -{ -#ifdef WIN32 - if (window != NULL) - window->renderGameLoadingScreen(); - - // on Windows we use _wsystem to support non-ASCII paths - // which requires converting from utf8 to a wstring - typedef std::codecvt_utf8 convert_type; - std::wstring_convert converter; - std::wstring wchar_str = converter.from_bytes(cmd_utf8); - - std::string exe; - std::string args; - - split_cmd(cmd_utf8, &exe, &args); - - SHELLEXECUTEINFO lpExecInfo; - lpExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); - lpExecInfo.lpFile = exe.c_str(); - lpExecInfo.fMask = SEE_MASK_DOENVSUBST | SEE_MASK_NOCLOSEPROCESS; - lpExecInfo.hwnd = NULL; - lpExecInfo.lpVerb = "open"; // to open program - lpExecInfo.lpParameters = args.c_str(); // file name as an argument - lpExecInfo.lpDirectory = NULL; - lpExecInfo.nShow = SW_SHOW; // show command prompt with normal window size - lpExecInfo.hInstApp = (HINSTANCE)SE_ERR_DDEFAIL; //WINSHELLAPI BOOL WINAPI result; - ShellExecuteEx(&lpExecInfo); - - if (lpExecInfo.hProcess != NULL) - { - if (window == NULL) - WaitForSingleObject(lpExecInfo.hProcess, INFINITE); - else - { - while (WaitForSingleObject(lpExecInfo.hProcess, 50) == 0x00000102L) - { - bool polled = false; - - SDL_Event event; - while (SDL_PollEvent(&event)) - polled = true; - - if (window != NULL && polled) - window->renderGameLoadingScreen(); - } - } - - CloseHandle(lpExecInfo.hProcess); - return 0; - } - - return 1; - //return _wsystem(wchar_str.c_str()); -#else - return system(cmd_utf8.c_str()); -#endif -} - -int quitES(const std::string& filename) -{ - if (!filename.empty()) - touch(filename); - SDL_Event* quit = new SDL_Event(); - quit->type = SDL_QUIT; - SDL_PushEvent(quit); - return 0; -} - -void touch(const std::string& filename) -{ -#ifdef WIN32 - FILE* fp = fopen(filename.c_str(), "ab+"); - if (fp != NULL) - fclose(fp); -#else - int fd = open(filename.c_str(), O_CREAT|O_WRONLY, 0644); - if (fd >= 0) - close(fd); -#endif +#include "platform.h" +#include + +#ifdef WIN32 +#include +#include +#else +#include +#endif +#include + +#include "Window.h" + +#include "GuiComponent.h" + +int runShutdownCommand() +{ +#ifdef WIN32 // windows + return system("shutdown -s -t 0"); +#else // osx / linux + return system("sudo shutdown -h now"); +#endif +} + +int runRestartCommand() +{ +#ifdef WIN32 // windows + return system("shutdown -r -t 0"); +#else // osx / linux + return system("sudo shutdown -r now"); +#endif +} + +std::string trim(const std::string& str) +{ + size_t first = str.find_first_not_of(' '); + if (std::string::npos == first) + { + return str; + } + size_t last = str.find_last_not_of(' '); + return str.substr(first, (last - first + 1)); +} + +void split_cmd(const std::string& cmd, + std::string* executable, + std::string* parameters) +{ + std::string c(cmd); + size_t exec_end; + + c = trim(c); + //boost::trim_all(c); + + if (c[0] == '\"') + { + exec_end = c.find_first_of('\"', 1); + if (std::string::npos != exec_end) + { + *executable = c.substr(1, exec_end - 1); + *parameters = c.substr(exec_end + 1); + } + else + { + *executable = c.substr(1, exec_end); + std::string().swap(*parameters); + } + } + else + { + exec_end = c.find_first_of(' ', 0); + if (std::string::npos != exec_end) + { + *executable = c.substr(0, exec_end); + *parameters = c.substr(exec_end + 1); + } + else + { + *executable = c.substr(0, exec_end); + std::string().swap(*parameters); + } + } +} + +int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window) +{ +#ifdef WIN32 + if (window != NULL) + window->renderGameLoadingScreen(); + + // on Windows we use _wsystem to support non-ASCII paths + // which requires converting from utf8 to a wstring + typedef std::codecvt_utf8 convert_type; + std::wstring_convert converter; + std::wstring wchar_str = converter.from_bytes(cmd_utf8); + + std::string exe; + std::string args; + + split_cmd(cmd_utf8, &exe, &args); + + SHELLEXECUTEINFO lpExecInfo; + lpExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); + lpExecInfo.lpFile = exe.c_str(); + lpExecInfo.fMask = SEE_MASK_DOENVSUBST | SEE_MASK_NOCLOSEPROCESS; + lpExecInfo.hwnd = NULL; + lpExecInfo.lpVerb = "open"; // to open program + lpExecInfo.lpParameters = args.c_str(); // file name as an argument + lpExecInfo.lpDirectory = NULL; + lpExecInfo.nShow = SW_SHOW; // show command prompt with normal window size + lpExecInfo.hInstApp = (HINSTANCE)SE_ERR_DDEFAIL; //WINSHELLAPI BOOL WINAPI result; + ShellExecuteEx(&lpExecInfo); + + if (lpExecInfo.hProcess != NULL) + { + if (window == NULL) + WaitForSingleObject(lpExecInfo.hProcess, INFINITE); + else + { + while (WaitForSingleObject(lpExecInfo.hProcess, 50) == 0x00000102L) + { + bool polled = false; + + SDL_Event event; + while (SDL_PollEvent(&event)) + polled = true; + + if (window != NULL && polled) + window->renderGameLoadingScreen(); + } + } + + CloseHandle(lpExecInfo.hProcess); + return 0; + } + + return 1; + //return _wsystem(wchar_str.c_str()); +#else + return system(cmd_utf8.c_str()); +#endif +} + +int quitES(const std::string& filename) +{ + if (!filename.empty()) + touch(filename); + SDL_Event* quit = new SDL_Event(); + quit->type = SDL_QUIT; + SDL_PushEvent(quit); + return 0; +} + +void touch(const std::string& filename) +{ +#ifdef WIN32 + FILE* fp = fopen(filename.c_str(), "ab+"); + if (fp != NULL) + fclose(fp); +#else + int fd = open(filename.c_str(), O_CREAT|O_WRONLY, 0644); + if (fd >= 0) + close(fd); +#endif } \ No newline at end of file diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 3c77caed8b..83b5270cb1 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -1,667 +1,667 @@ -#define _FILE_OFFSET_BITS 64 - -#include "utils/FileSystemUtil.h" - -#include "Settings.h" -#include -#include - -#if defined(_WIN32) -// because windows... -#include -#include -#define getcwd _getcwd -#define mkdir(x,y) _mkdir(x) -#define snprintf _snprintf -#define stat64 _stat64 -#define unlink _unlink -#define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) -#define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) -#else // _WIN32 -#include -#include -#endif // _WIN32 - -namespace Utils -{ - namespace FileSystem - { - -#if defined(_WIN32) - static std::string convertFromWideString(const std::wstring wstring) - { - int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); - std::string string; - - string.resize(numBytes); - WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); - - return std::string(string); - - } // convertFromWideString -#endif // _WIN32 - - stringList getDirContent(const std::string& _path, const bool _recursive) - { - std::string path = getGenericPath(_path); - stringList contentList; - - // only parse the directory, if it's a directory - if(isDirectory(path)) - { - -#if defined(_WIN32) - WIN32_FIND_DATAW findData; - std::string wildcard = path + "/*"; - HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); - - if(hFind != INVALID_HANDLE_VALUE) - { - // loop over all files in the directory - do - { - std::string name = convertFromWideString(findData.cFileName); - - // ignore "." and ".." - if((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - contentList.push_back(fullName); - - if(_recursive && isDirectory(fullName)) - contentList.merge(getDirContent(fullName, true)); - } - } - while(FindNextFileW(hFind, &findData)); - - FindClose(hFind); - } -#else // _WIN32 - DIR* dir = opendir(path.c_str()); - - if(dir != NULL) - { - struct dirent* entry; - - // loop over all files in the directory - while((entry = readdir(dir)) != NULL) - { - std::string name(entry->d_name); - - // ignore "." and ".." - if((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - contentList.push_back(fullName); - - if(_recursive && isDirectory(fullName)) - contentList.merge(getDirContent(fullName, true)); - } - } - - closedir(dir); - } -#endif // _WIN32 - - } - - // sort the content list - contentList.sort(); - - // return the content list - return contentList; - - } // getDirContent - - stringList getPathList(const std::string& _path) - { - stringList pathList; - std::string path = getGenericPath(_path); - size_t start = 0; - size_t end = 0; - - // split at '/' - while((end = path.find("/", start)) != std::string::npos) - { - if(end != start) - pathList.push_back(std::string(path, start, end - start)); - - start = end + 1; - } - - // add last folder / file to pathList - if(start != path.size()) - pathList.push_back(std::string(path, start, path.size() - start)); - - // return the path list - return pathList; - - } // getPathList - - std::string getHomePath() - { - static std::string path; - - // only construct the homepath once - if(!path.length()) - { - // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows - char* envHome = getenv("HOME"); - -#ifdef _DEBUG - // envHome = "H:/[Emulz]/EmulationStation/"; -#endif - - if (envHome) - path = getGenericPath(envHome); - -#if defined(_WIN32) - // but does not seem to work for Windows XP or Vista, so try something else - if(!path.length()) - { - char* envHomeDrive = getenv("HOMEDRIVE"); - char* envHomePath = getenv("HOMEPATH"); - if(envHomeDrive && envHomePath) - path = getGenericPath(std::string(envHomeDrive) + "/" + envHomePath); - } -#endif // _WIN32 - - // no homepath found, fall back to current working directory - if(!path.length()) - path = getCWDPath(); - } - - // return constructed homepath - return path; - - } // getHomePath - - std::string getCWDPath() - { - char temp[512]; - return (getcwd(temp, 512) ? getGenericPath(temp) : ""); - } // getCWDPath - - std::string getExePath() - { - static std::string path; - - // only construct the exepath once - if (!path.length()) - { -#if defined(_WIN32) - char buffer[MAX_PATH]; - DWORD size = MAX_PATH; - DWORD result = GetModuleFileNameA(NULL, buffer, size); - if (result) - { - std::string ret = buffer; - path = getParent(ret); - return ret; - } -#endif - path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); - if (isRegularFile(path)) - path = getParent(path); - } - - // return constructed exepath - return path; - - } // getExePath - - std::string getPreferredPath(const std::string& _path) - { - std::string path = _path; - size_t offset = std::string::npos; -#if defined(_WIN32) - // convert '/' to '\\' - while((offset = path.find('/')) != std::string::npos) - path.replace(offset, 1, "\\"); -#endif // _WIN32 - return path; - } - - std::string getGenericPath(const std::string& _path) - { - std::string path = _path; - size_t offset = std::string::npos; - - // remove "\\\\?\\" - if((path.find("\\\\?\\")) == 0) - path.erase(0, 4); - - // convert '\\' to '/' - while((offset = path.find('\\')) != std::string::npos) - path.replace(offset, 1 ,"/"); - - // remove double '/' - while((offset = path.find("//")) != std::string::npos) - path.erase(offset, 1); - - // remove trailing '/' - while(path.length() && ((offset = path.find_last_of('/')) == (path.length() - 1))) - path.erase(offset, 1); - - // return generic path - return path; - - } // getGenericPath - - std::string getEscapedPath(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // windows escapes stuff by just putting everything in quotes - return '"' + getPreferredPath(path) + '"'; -#else // _WIN32 - // insert a backslash before most characters that would mess up a bash path - const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; - const char* invalidChar = invalidChars; - - while(*invalidChar) - { - size_t start = 0; - size_t offset = 0; - - while((offset = path.find(*invalidChar, start)) != std::string::npos) - { - start = offset + 1; - - if((offset == 0) || (path[offset - 1] != '\\')) - { - path.insert(offset, 1, '\\'); - ++start; - } - } - - ++invalidChar; - } - - // return escaped path - return path; -#endif // _WIN32 - - } // getEscapedPath - - std::string getCanonicalPath(const std::string& _path) - { - // temporary hack for builtin resources - if((_path[0] == ':') && (_path[1] == '/')) - return _path; - - std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); - - // cleanup path - bool scan = true; - while(scan) - { - stringList pathList = getPathList(path); - - path.clear(); - scan = false; - - for(stringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); ++it) - { - // ignore empty - if((*it).empty()) - continue; - - // remove "/./" - if((*it) == ".") - continue; - - // resolve "/../" - if((*it) == "..") - { - path = getParent(path); - continue; - } - -#if defined(_WIN32) - // append folder to path - path += (path.size() == 0) ? (*it) : ("/" + (*it)); -#else // _WIN32 - // append folder to path - path += ("/" + (*it)); -#endif // _WIN32 - - // resolve symlink - if(isSymlink(path)) - { - std::string resolved = resolveSymlink(path); - - if(resolved.empty()) - return ""; - - if(isAbsolute(resolved)) - path = resolved; - else - path = getParent(path) + "/" + resolved; - - for(++it; it != pathList.cend(); ++it) - path += (path.size() == 0) ? (*it) : ("/" + (*it)); - - scan = true; - break; - } - } - } - - // return canonical path - return path; - - } // getCanonicalPath - - std::string getAbsolutePath(const std::string& _path, const std::string& _base) - { - std::string path = getGenericPath(_path); - std::string base = isAbsolute(_base) ? getGenericPath(_base) : getAbsolutePath(_base); - - // return absolute path - return isAbsolute(path) ? path : getGenericPath(base + "/" + path); - - } // getAbsolutePath - - std::string getParent(const std::string& _path) - { - std::string path = getGenericPath(_path); - size_t offset = std::string::npos; - - // find last '/' and erase it - if((offset = path.find_last_of('/')) != std::string::npos) - return path.erase(offset); - - // no parent found - return path; - - } // getParent - - std::string getFileName(const std::string& _path) - { - std::string path = getGenericPath(_path); - size_t offset = std::string::npos; - - // find last '/' and return the filename - if((offset = path.find_last_of('/')) != std::string::npos) - return ((path[offset + 1] == 0) ? "." : std::string(path, offset + 1)); - - // no '/' found, entire path is a filename - return path; - - } // getFileName - - std::string getStem(const std::string& _path) - { - std::string fileName = getFileName(_path); - size_t offset = std::string::npos; - - // empty fileName - if(fileName == ".") - return fileName; - - // find last '.' and erase the extension - if((offset = fileName.find_last_of('.')) != std::string::npos) - return fileName.erase(offset); - - // no '.' found, filename has no extension - return fileName; - - } // getStem - - std::string getExtension(const std::string& _path) - { - std::string fileName = getFileName(_path); - size_t offset = std::string::npos; - - // empty fileName - if(fileName == ".") - return fileName; - - // find last '.' and return the extension - if((offset = fileName.find_last_of('.')) != std::string::npos) - return std::string(fileName, offset); - - // no '.' found, filename has no extension - return "."; - - } // getExtension - - std::string resolveRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) - { - std::string path = getGenericPath(_path); - std::string relativeTo = isDirectory(_relativeTo) ? getGenericPath(_relativeTo) : getParent(_relativeTo); - - // nothing to resolve - if(!path.length()) - return path; - - // replace '.' with relativeTo - if((path[0] == '.') && (path[1] == '/')) - return (relativeTo + &(path[1])); - - // replace '~' with homePath - if(_allowHome && (path[0] == '~') && (path[1] == '/')) - return (getHomePath() + &(path[1])); - - // nothing to resolve - return path; - - } // resolveRelativePath - - std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) - { - bool contains = false; - std::string path = removeCommonPath(_path, _relativeTo, contains); - - if(contains) - { - // success - return ("./" + path); - } - - if(_allowHome) - { - path = removeCommonPath(_path, getHomePath(), contains); - - if(contains) - { - // success - return ("~/" + path); - } - } - - // nothing to resolve - return path; - - } // createRelativePath - - std::string removeCommonPath(const std::string& _path, const std::string& _common, bool& _contains) - { - std::string path = getGenericPath(_path); - std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); - - // check if path contains common - if(path.find(common) == 0) - { - _contains = true; - return path.substr(common.length() + 1); - } - - // it didn't - _contains = false; - return path; - - } // removeCommonPath - - std::string resolveSymlink(const std::string& _path) - { - std::string path = getGenericPath(_path); - std::string resolved; - -#if defined(_WIN32) - HANDLE hFile = CreateFile(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); - - if(hFile != INVALID_HANDLE_VALUE) - { - resolved.resize(GetFinalPathNameByHandle(hFile, nullptr, 0, FILE_NAME_NORMALIZED) + 1); - if(GetFinalPathNameByHandle(hFile, (LPSTR)resolved.data(), (DWORD)resolved.size(), FILE_NAME_NORMALIZED) > 0) - { - resolved.resize(resolved.size() - 1); - resolved = getGenericPath(resolved); - } - CloseHandle(hFile); - } -#else // _WIN32 - struct stat info; - - // check if lstat succeeded - if(lstat(path.c_str(), &info) == 0) - { - resolved.resize(info.st_size); - if(readlink(path.c_str(), (char*)resolved.data(), resolved.size()) > 0) - resolved = getGenericPath(resolved); - } -#endif // _WIN32 - - // return resolved path - return resolved; - - } // resolveSymlink - - bool removeFile(const std::string& _path) - { - std::string path = getGenericPath(_path); - - // don't remove if it doesn't exists - if(!exists(path)) - return true; - - // try to remove file - return (unlink(path.c_str()) == 0); - - } // removeFile - - bool createDirectory(const std::string& _path) - { - std::string path = getGenericPath(_path); - - // don't create if it already exists - if(exists(path)) - return true; - - // try to create directory - if(mkdir(path.c_str(), 0755) == 0) - return true; - - // failed to create directory, try to create the parent - std::string parent = getParent(path); - - // only try to create parent if it's not identical to path - if(parent != path) - createDirectory(parent); - - // try to create directory again now that the parent should exist - return (mkdir(path.c_str(), 0755) == 0); - - } // createDirectory - - bool exists(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - return (stat64(path.c_str(), &info) == 0); - - } // exists - - bool isAbsolute(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - return ((path.size() > 1) && (path[1] == ':')); -#else // _WIN32 - return ((path.size() > 0) && (path[0] == '/')); -#endif // _WIN32 - - } // isAbsolute - - bool isRegularFile(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - if(stat64(path.c_str(), &info) != 0) - return false; - - // check for S_IFREG attribute - return (S_ISREG(info.st_mode)); - - } // isRegularFile - - bool isDirectory(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat info; - - // check if stat succeeded - if(stat(path.c_str(), &info) != 0) - return false; - - // check for S_IFDIR attribute - return (S_ISDIR(info.st_mode)); - - } // isDirectory - - bool isSymlink(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // check for symlink attribute - const DWORD Attributes = GetFileAttributes(path.c_str()); - if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT)) - return true; -#else // _WIN32 - struct stat info; - - // check if lstat succeeded - if(lstat(path.c_str(), &info) != 0) - return false; - - // check for S_IFLNK attribute - return (S_ISLNK(info.st_mode)); -#endif // _WIN32 - - // not a symlink - return false; - - } // isSymlink - - bool isHidden(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // check for hidden attribute - const DWORD Attributes = GetFileAttributes(path.c_str()); - if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_HIDDEN)) - return true; -#endif // _WIN32 - - // filenames starting with . are hidden in linux, we do this check for windows as well - if (getFileName(path)[0] == '.') - return true; - - // not hidden - return false; - - } // isHidden - - } // FileSystem:: - -} // Utils:: +#define _FILE_OFFSET_BITS 64 + +#include "utils/FileSystemUtil.h" + +#include "Settings.h" +#include +#include + +#if defined(_WIN32) +// because windows... +#include +#include +#define getcwd _getcwd +#define mkdir(x,y) _mkdir(x) +#define snprintf _snprintf +#define stat64 _stat64 +#define unlink _unlink +#define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) +#define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) +#else // _WIN32 +#include +#include +#endif // _WIN32 + +namespace Utils +{ + namespace FileSystem + { + +#if defined(_WIN32) + static std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + std::string string; + + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return std::string(string); + + } // convertFromWideString +#endif // _WIN32 + + stringList getDirContent(const std::string& _path, const bool _recursive) + { + std::string path = getGenericPath(_path); + stringList contentList; + + // only parse the directory, if it's a directory + if(isDirectory(path)) + { + +#if defined(_WIN32) + WIN32_FIND_DATAW findData; + std::string wildcard = path + "/*"; + HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); + + if(hFind != INVALID_HANDLE_VALUE) + { + // loop over all files in the directory + do + { + std::string name = convertFromWideString(findData.cFileName); + + // ignore "." and ".." + if((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + contentList.push_back(fullName); + + if(_recursive && isDirectory(fullName)) + contentList.merge(getDirContent(fullName, true)); + } + } + while(FindNextFileW(hFind, &findData)); + + FindClose(hFind); + } +#else // _WIN32 + DIR* dir = opendir(path.c_str()); + + if(dir != NULL) + { + struct dirent* entry; + + // loop over all files in the directory + while((entry = readdir(dir)) != NULL) + { + std::string name(entry->d_name); + + // ignore "." and ".." + if((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + contentList.push_back(fullName); + + if(_recursive && isDirectory(fullName)) + contentList.merge(getDirContent(fullName, true)); + } + } + + closedir(dir); + } +#endif // _WIN32 + + } + + // sort the content list + contentList.sort(); + + // return the content list + return contentList; + + } // getDirContent + + stringList getPathList(const std::string& _path) + { + stringList pathList; + std::string path = getGenericPath(_path); + size_t start = 0; + size_t end = 0; + + // split at '/' + while((end = path.find("/", start)) != std::string::npos) + { + if(end != start) + pathList.push_back(std::string(path, start, end - start)); + + start = end + 1; + } + + // add last folder / file to pathList + if(start != path.size()) + pathList.push_back(std::string(path, start, path.size() - start)); + + // return the path list + return pathList; + + } // getPathList + + std::string getHomePath() + { + static std::string path; + + // only construct the homepath once + if(!path.length()) + { + // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows + char* envHome = getenv("HOME"); + +#ifdef _DEBUG + // envHome = "H:/[Emulz]/EmulationStation/"; +#endif + + if (envHome) + path = getGenericPath(envHome); + +#if defined(_WIN32) + // but does not seem to work for Windows XP or Vista, so try something else + if(!path.length()) + { + char* envHomeDrive = getenv("HOMEDRIVE"); + char* envHomePath = getenv("HOMEPATH"); + if(envHomeDrive && envHomePath) + path = getGenericPath(std::string(envHomeDrive) + "/" + envHomePath); + } +#endif // _WIN32 + + // no homepath found, fall back to current working directory + if(!path.length()) + path = getCWDPath(); + } + + // return constructed homepath + return path; + + } // getHomePath + + std::string getCWDPath() + { + char temp[512]; + return (getcwd(temp, 512) ? getGenericPath(temp) : ""); + } // getCWDPath + + std::string getExePath() + { + static std::string path; + + // only construct the exepath once + if (!path.length()) + { +#if defined(_WIN32) + char buffer[MAX_PATH]; + DWORD size = MAX_PATH; + DWORD result = GetModuleFileNameA(NULL, buffer, size); + if (result) + { + std::string ret = buffer; + path = getParent(ret); + return ret; + } +#endif + path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); + if (isRegularFile(path)) + path = getParent(path); + } + + // return constructed exepath + return path; + + } // getExePath + + std::string getPreferredPath(const std::string& _path) + { + std::string path = _path; + size_t offset = std::string::npos; +#if defined(_WIN32) + // convert '/' to '\\' + while((offset = path.find('/')) != std::string::npos) + path.replace(offset, 1, "\\"); +#endif // _WIN32 + return path; + } + + std::string getGenericPath(const std::string& _path) + { + std::string path = _path; + size_t offset = std::string::npos; + + // remove "\\\\?\\" + if((path.find("\\\\?\\")) == 0) + path.erase(0, 4); + + // convert '\\' to '/' + while((offset = path.find('\\')) != std::string::npos) + path.replace(offset, 1 ,"/"); + + // remove double '/' + while((offset = path.find("//")) != std::string::npos) + path.erase(offset, 1); + + // remove trailing '/' + while(path.length() && ((offset = path.find_last_of('/')) == (path.length() - 1))) + path.erase(offset, 1); + + // return generic path + return path; + + } // getGenericPath + + std::string getEscapedPath(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // windows escapes stuff by just putting everything in quotes + return '"' + getPreferredPath(path) + '"'; +#else // _WIN32 + // insert a backslash before most characters that would mess up a bash path + const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; + const char* invalidChar = invalidChars; + + while(*invalidChar) + { + size_t start = 0; + size_t offset = 0; + + while((offset = path.find(*invalidChar, start)) != std::string::npos) + { + start = offset + 1; + + if((offset == 0) || (path[offset - 1] != '\\')) + { + path.insert(offset, 1, '\\'); + ++start; + } + } + + ++invalidChar; + } + + // return escaped path + return path; +#endif // _WIN32 + + } // getEscapedPath + + std::string getCanonicalPath(const std::string& _path) + { + // temporary hack for builtin resources + if((_path[0] == ':') && (_path[1] == '/')) + return _path; + + std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); + + // cleanup path + bool scan = true; + while(scan) + { + stringList pathList = getPathList(path); + + path.clear(); + scan = false; + + for(stringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); ++it) + { + // ignore empty + if((*it).empty()) + continue; + + // remove "/./" + if((*it) == ".") + continue; + + // resolve "/../" + if((*it) == "..") + { + path = getParent(path); + continue; + } + +#if defined(_WIN32) + // append folder to path + path += (path.size() == 0) ? (*it) : ("/" + (*it)); +#else // _WIN32 + // append folder to path + path += ("/" + (*it)); +#endif // _WIN32 + + // resolve symlink + if(isSymlink(path)) + { + std::string resolved = resolveSymlink(path); + + if(resolved.empty()) + return ""; + + if(isAbsolute(resolved)) + path = resolved; + else + path = getParent(path) + "/" + resolved; + + for(++it; it != pathList.cend(); ++it) + path += (path.size() == 0) ? (*it) : ("/" + (*it)); + + scan = true; + break; + } + } + } + + // return canonical path + return path; + + } // getCanonicalPath + + std::string getAbsolutePath(const std::string& _path, const std::string& _base) + { + std::string path = getGenericPath(_path); + std::string base = isAbsolute(_base) ? getGenericPath(_base) : getAbsolutePath(_base); + + // return absolute path + return isAbsolute(path) ? path : getGenericPath(base + "/" + path); + + } // getAbsolutePath + + std::string getParent(const std::string& _path) + { + std::string path = getGenericPath(_path); + size_t offset = std::string::npos; + + // find last '/' and erase it + if((offset = path.find_last_of('/')) != std::string::npos) + return path.erase(offset); + + // no parent found + return path; + + } // getParent + + std::string getFileName(const std::string& _path) + { + std::string path = getGenericPath(_path); + size_t offset = std::string::npos; + + // find last '/' and return the filename + if((offset = path.find_last_of('/')) != std::string::npos) + return ((path[offset + 1] == 0) ? "." : std::string(path, offset + 1)); + + // no '/' found, entire path is a filename + return path; + + } // getFileName + + std::string getStem(const std::string& _path) + { + std::string fileName = getFileName(_path); + size_t offset = std::string::npos; + + // empty fileName + if(fileName == ".") + return fileName; + + // find last '.' and erase the extension + if((offset = fileName.find_last_of('.')) != std::string::npos) + return fileName.erase(offset); + + // no '.' found, filename has no extension + return fileName; + + } // getStem + + std::string getExtension(const std::string& _path) + { + std::string fileName = getFileName(_path); + size_t offset = std::string::npos; + + // empty fileName + if(fileName == ".") + return fileName; + + // find last '.' and return the extension + if((offset = fileName.find_last_of('.')) != std::string::npos) + return std::string(fileName, offset); + + // no '.' found, filename has no extension + return "."; + + } // getExtension + + std::string resolveRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) + { + std::string path = getGenericPath(_path); + std::string relativeTo = isDirectory(_relativeTo) ? getGenericPath(_relativeTo) : getParent(_relativeTo); + + // nothing to resolve + if(!path.length()) + return path; + + // replace '.' with relativeTo + if((path[0] == '.') && (path[1] == '/')) + return (relativeTo + &(path[1])); + + // replace '~' with homePath + if(_allowHome && (path[0] == '~') && (path[1] == '/')) + return (getHomePath() + &(path[1])); + + // nothing to resolve + return path; + + } // resolveRelativePath + + std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) + { + bool contains = false; + std::string path = removeCommonPath(_path, _relativeTo, contains); + + if(contains) + { + // success + return ("./" + path); + } + + if(_allowHome) + { + path = removeCommonPath(_path, getHomePath(), contains); + + if(contains) + { + // success + return ("~/" + path); + } + } + + // nothing to resolve + return path; + + } // createRelativePath + + std::string removeCommonPath(const std::string& _path, const std::string& _common, bool& _contains) + { + std::string path = getGenericPath(_path); + std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); + + // check if path contains common + if(path.find(common) == 0) + { + _contains = true; + return path.substr(common.length() + 1); + } + + // it didn't + _contains = false; + return path; + + } // removeCommonPath + + std::string resolveSymlink(const std::string& _path) + { + std::string path = getGenericPath(_path); + std::string resolved; + +#if defined(_WIN32) + HANDLE hFile = CreateFile(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); + + if(hFile != INVALID_HANDLE_VALUE) + { + resolved.resize(GetFinalPathNameByHandle(hFile, nullptr, 0, FILE_NAME_NORMALIZED) + 1); + if(GetFinalPathNameByHandle(hFile, (LPSTR)resolved.data(), (DWORD)resolved.size(), FILE_NAME_NORMALIZED) > 0) + { + resolved.resize(resolved.size() - 1); + resolved = getGenericPath(resolved); + } + CloseHandle(hFile); + } +#else // _WIN32 + struct stat info; + + // check if lstat succeeded + if(lstat(path.c_str(), &info) == 0) + { + resolved.resize(info.st_size); + if(readlink(path.c_str(), (char*)resolved.data(), resolved.size()) > 0) + resolved = getGenericPath(resolved); + } +#endif // _WIN32 + + // return resolved path + return resolved; + + } // resolveSymlink + + bool removeFile(const std::string& _path) + { + std::string path = getGenericPath(_path); + + // don't remove if it doesn't exists + if(!exists(path)) + return true; + + // try to remove file + return (unlink(path.c_str()) == 0); + + } // removeFile + + bool createDirectory(const std::string& _path) + { + std::string path = getGenericPath(_path); + + // don't create if it already exists + if(exists(path)) + return true; + + // try to create directory + if(mkdir(path.c_str(), 0755) == 0) + return true; + + // failed to create directory, try to create the parent + std::string parent = getParent(path); + + // only try to create parent if it's not identical to path + if(parent != path) + createDirectory(parent); + + // try to create directory again now that the parent should exist + return (mkdir(path.c_str(), 0755) == 0); + + } // createDirectory + + bool exists(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + return (stat64(path.c_str(), &info) == 0); + + } // exists + + bool isAbsolute(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + return ((path.size() > 1) && (path[1] == ':')); +#else // _WIN32 + return ((path.size() > 0) && (path[0] == '/')); +#endif // _WIN32 + + } // isAbsolute + + bool isRegularFile(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + if(stat64(path.c_str(), &info) != 0) + return false; + + // check for S_IFREG attribute + return (S_ISREG(info.st_mode)); + + } // isRegularFile + + bool isDirectory(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat info; + + // check if stat succeeded + if(stat(path.c_str(), &info) != 0) + return false; + + // check for S_IFDIR attribute + return (S_ISDIR(info.st_mode)); + + } // isDirectory + + bool isSymlink(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // check for symlink attribute + const DWORD Attributes = GetFileAttributes(path.c_str()); + if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT)) + return true; +#else // _WIN32 + struct stat info; + + // check if lstat succeeded + if(lstat(path.c_str(), &info) != 0) + return false; + + // check for S_IFLNK attribute + return (S_ISLNK(info.st_mode)); +#endif // _WIN32 + + // not a symlink + return false; + + } // isSymlink + + bool isHidden(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // check for hidden attribute + const DWORD Attributes = GetFileAttributes(path.c_str()); + if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_HIDDEN)) + return true; +#endif // _WIN32 + + // filenames starting with . are hidden in linux, we do this check for windows as well + if (getFileName(path)[0] == '.') + return true; + + // not hidden + return false; + + } // isHidden + + } // FileSystem:: + +} // Utils:: diff --git a/es-core/src/utils/StringUtil.h b/es-core/src/utils/StringUtil.h index 7ebcf0df22..3ffb5c5eac 100644 --- a/es-core/src/utils/StringUtil.h +++ b/es-core/src/utils/StringUtil.h @@ -28,7 +28,7 @@ namespace Utils std::string format (const char* _string, ...); std::string scramble (const std::string& _input, const std::string& key); -#if defined(_WIN32) +#if defined(_WIN32) const std::string convertFromWideString(const std::wstring wstring); const std::wstring convertToWideString(const std::string string); #endif From f0adfc3c5d4c131b22949c62d37fbbb53c520f4b Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 3 Jun 2019 17:02:57 +0200 Subject: [PATCH 016/336] [Misc Optimisations] ( loading, fonts, metadata... ) [Theming] Menu themings [Clipping] Make Clip before OpenGL .... --- es-app/src/FileData.cpp | 6 +- es-app/src/FileData.h | 268 +-- es-app/src/FileSorts.cpp | 12 +- es-app/src/MetaData.cpp | 62 +- es-app/src/MetaData.h | 11 + es-app/src/SystemData.cpp | 1178 ++++++------ es-app/src/components/RatingComponent.cpp | 5 + .../src/guis/GuiCollectionSystemsOptions.cpp | 14 +- es-app/src/guis/GuiGamelistFilter.cpp | 3 +- es-app/src/guis/GuiGamelistOptions.cpp | 10 +- .../src/guis/GuiGeneralScreensaverOptions.cpp | 4 +- es-app/src/guis/GuiMenu.cpp | 1451 +++++++-------- es-app/src/guis/GuiMetaDataEd.cpp | 767 ++++---- es-app/src/guis/GuiScraperMulti.cpp | 5 + es-app/src/guis/GuiScraperStart.cpp | 29 +- es-app/src/guis/GuiSettings.cpp | 11 +- es-app/src/guis/GuiSettings.h | 2 + es-app/src/main.cpp | 69 +- es-app/src/views/SystemView.cpp | 22 +- es-app/src/views/SystemView.h | 1 + es-app/src/views/ViewController.cpp | 46 +- es-app/src/views/gamelist/IGameListView.cpp | 4 + es-core/src/GuiComponent.h | 446 ++--- es-core/src/ImageIO.cpp | 87 +- es-core/src/ImageIO.h | 2 + es-core/src/PowerSaver.cpp | 1 + es-core/src/Renderer.h | 2 + es-core/src/Renderer_draw_gl.cpp | 51 +- es-core/src/Renderer_init_sdlgl.cpp | 27 +- es-core/src/Settings.cpp | 6 + es-core/src/ThemeData.cpp | 1579 +++++++++-------- es-core/src/ThemeData.h | 32 +- es-core/src/Window.cpp | 1062 +++++------ es-core/src/components/ButtonComponent.cpp | 14 +- es-core/src/components/ButtonComponent.h | 3 +- es-core/src/components/ComponentGrid.cpp | 5 +- es-core/src/components/ComponentGrid.h | 5 +- es-core/src/components/ComponentList.cpp | 51 +- es-core/src/components/GridTileComponent.cpp | 52 +- es-core/src/components/GridTileComponent.h | 1 + es-core/src/components/ImageComponent.cpp | 32 +- es-core/src/components/ImageComponent.h | 4 + es-core/src/components/MenuComponent.cpp | 32 +- es-core/src/components/MenuComponent.h | 8 +- es-core/src/components/NinePatchComponent.cpp | 13 + es-core/src/components/NinePatchComponent.h | 2 + es-core/src/components/OptionListComponent.h | 43 +- es-core/src/components/SliderComponent.cpp | 22 +- es-core/src/components/SliderComponent.h | 3 + es-core/src/components/SwitchComponent.cpp | 8 + es-core/src/components/SwitchComponent.h | 2 + es-core/src/components/TextComponent.cpp | 14 +- es-core/src/components/TextComponent.h | 2 +- es-core/src/components/VideoVlcComponent.cpp | 1 + es-core/src/guis/GuiDetectDevice.cpp | 17 +- es-core/src/guis/GuiInputConfig.cpp | 16 +- es-core/src/guis/GuiMsgBox.cpp | 8 +- es-core/src/guis/GuiTextEditPopup.cpp | 5 + es-core/src/resources/Font.cpp | 77 +- es-core/src/resources/Font.h | 6 +- es-core/src/resources/TextureData.cpp | 22 +- es-core/src/resources/TextureData.h | 1 + es-core/src/utils/FileSystemUtil.cpp | 1441 ++++++++------- es-core/src/utils/FileSystemUtil.h | 14 +- 64 files changed, 5004 insertions(+), 4195 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 532ea9a71d..71cdc3352d 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -20,11 +20,10 @@ FileData::FileData(FileType type, const std::string& path, SystemEnvironmentData : mType(type), mPath(path), mSystem(system), mEnvData(envData), mSourceFileData(NULL), 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()) + if (metadata.get("name").empty()) metadata.set("name", getDisplayName()); mSystemName = system->getName(); - // mDefaultCore = system->getSystemEnvData()->mDefaultCore; } FileData::~FileData() @@ -85,7 +84,6 @@ const bool FileData::getFavorite() return metadata.get("favorite") == "true"; } - const bool FileData::getHidden() { return metadata.get("hidden") == "true"; @@ -291,7 +289,7 @@ void FileData::removeChild(FileData* file) } void FileData::sort(ComparisonFunction& comparator, bool ascending) -{ +{ std::stable_sort(mChildren.begin(), mChildren.end(), comparator); for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index a4a0d7e866..d8bafed6d2 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -1,134 +1,134 @@ -#pragma once -#ifndef ES_APP_FILE_DATA_H -#define ES_APP_FILE_DATA_H - -#include "utils/FileSystemUtil.h" -#include "MetaData.h" -#include - -class SystemData; -class Window; -struct SystemEnvironmentData; - -enum FileType -{ - GAME = 1, // Cannot have children. - FOLDER = 2, - PLACEHOLDER = 3 -}; - -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); - -// A tree node that holds information for a file. -class FileData -{ -public: - FileData(FileType type, const std::string& path, SystemEnvironmentData* envData, SystemData* system); - virtual ~FileData(); - - virtual const std::string& getName(); - virtual const std::string& getSortName(); - inline FileType getType() const { return mType; } - inline const std::string& getPath() const { return mPath; } - inline FileData* getParent() const { return mParent; } - inline const std::unordered_map& getChildrenByFilename() const { return mChildrenByFilename; } - inline const std::vector& getChildren() const { return mChildren; } - inline SystemData* getSystem() const { return mSystem; } - inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } - virtual const std::string getThumbnailPath() const; - virtual const std::string getVideoPath() const; - virtual const std::string getMarqueePath() const; - virtual const std::string getImagePath() const; - - virtual const std::string getCore() const; - virtual const std::string getEmulator() const; - - virtual const bool getHidden(); - virtual const bool getFavorite(); - - FileData* findUniqueGameForFolder(); - - const std::vector& getChildrenListToDisplay(); - std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; - - void addChild(FileData* file); // Error if mType != FOLDER - void removeChild(FileData* file); //Error if mType != FOLDER - - inline bool isPlaceHolder() { return mType == PLACEHOLDER; }; - - virtual inline void refreshMetadata() { return; }; - - virtual std::string getKey(); - const bool isArcadeAsset(); - inline std::string getFullPath() { return getPath(); }; - inline std::string getFileName() { return Utils::FileSystem::getFileName(getPath()); }; - virtual FileData* getSourceFileData(); - inline std::string getSystemName() const { return mSystemName; }; - - // Returns our best guess at the "real" name for this file (will attempt to perform MAME name translation) - std::string getDisplayName() const; - - // As above, but also remove parenthesis - std::string getCleanName() const; - - void launchGame(Window* window); - - 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; - -protected: - FileData* mSourceFileData; - FileData* mParent; - std::string mSystemName; - //std::string mDefaultCore; - -private: - FileType mType; - std::string mPath; - SystemEnvironmentData* mEnvData; - SystemData* mSystem; - std::unordered_map mChildrenByFilename; - std::vector mChildren; - std::vector mFilteredChildren; -}; - -class CollectionFileData : public FileData -{ -public: - CollectionFileData(FileData* file, SystemData* system); - ~CollectionFileData(); - const std::string& getName(); - void refreshMetadata(); - FileData* getSourceFileData(); - std::string getKey(); -private: - // needs to be updated when metadata changes - std::string mCollectionFileName; - bool mDirty; -}; - -FileData::SortType getSortTypeFromString(std::string desc); - -#endif // ES_APP_FILE_DATA_H +#pragma once +#ifndef ES_APP_FILE_DATA_H +#define ES_APP_FILE_DATA_H + +#include "utils/FileSystemUtil.h" +#include "MetaData.h" +#include + +class SystemData; +class Window; +struct SystemEnvironmentData; + +enum FileType +{ + GAME = 1, // Cannot have children. + FOLDER = 2, + PLACEHOLDER = 3 +}; + +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); + +// A tree node that holds information for a file. +class FileData +{ +public: + FileData(FileType type, const std::string& path, SystemEnvironmentData* envData, SystemData* system); + virtual ~FileData(); + + virtual const std::string& getName(); + virtual const std::string& getSortName(); + inline FileType getType() const { return mType; } + inline const std::string& getPath() const { return mPath; } + inline FileData* getParent() const { return mParent; } + inline const std::unordered_map& getChildrenByFilename() const { return mChildrenByFilename; } + inline const std::vector& getChildren() const { return mChildren; } + inline SystemData* getSystem() const { return mSystem; } + inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } + virtual const std::string getThumbnailPath() const; + virtual const std::string getVideoPath() const; + virtual const std::string getMarqueePath() const; + virtual const std::string getImagePath() const; + + virtual const std::string getCore() const; + virtual const std::string getEmulator() const; + + virtual const bool getHidden(); + virtual const bool getFavorite(); + + FileData* findUniqueGameForFolder(); + + const std::vector& getChildrenListToDisplay(); + std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; + + void addChild(FileData* file); // Error if mType != FOLDER + void removeChild(FileData* file); //Error if mType != FOLDER + + inline bool isPlaceHolder() { return mType == PLACEHOLDER; }; + + virtual inline void refreshMetadata() { return; }; + + virtual std::string getKey(); + const bool isArcadeAsset(); + inline std::string getFullPath() { return getPath(); }; + inline std::string getFileName() { return Utils::FileSystem::getFileName(getPath()); }; + virtual FileData* getSourceFileData(); + inline std::string getSystemName() const { return mSystemName; }; + + // Returns our best guess at the "real" name for this file (will attempt to perform MAME name translation) + std::string getDisplayName() const; + + // As above, but also remove parenthesis + std::string getCleanName() const; + + void launchGame(Window* window); + + 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; + +protected: + FileData* mSourceFileData; + FileData* mParent; + std::string mSystemName; + //std::string mDefaultCore; + +private: + FileType mType; + std::string mPath; + SystemEnvironmentData* mEnvData; + SystemData* mSystem; + std::unordered_map mChildrenByFilename; + std::vector mChildren; + std::vector mFilteredChildren; +}; + +class CollectionFileData : public FileData +{ +public: + CollectionFileData(FileData* file, SystemData* system); + ~CollectionFileData(); + const std::string& getName(); + void refreshMetadata(); + FileData* getSourceFileData(); + std::string getKey(); +private: + // needs to be updated when metadata changes + std::string mCollectionFileName; + bool mDirty; +}; + +FileData::SortType getSortTypeFromString(std::string desc); + +#endif // ES_APP_FILE_DATA_H diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index 5043656e11..d1237c0a2d 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -41,16 +41,8 @@ namespace FileSorts //returns if file1 should come before file2 bool compareName(const FileData* file1, const FileData* file2) { - // we compare the actual metadata name, as collection files have the system appended which messes up the order - std::string name1 = Utils::String::toUpper(file1->metadata.get("sortname")); - std::string name2 = Utils::String::toUpper(file2->metadata.get("sortname")); - if(name1.empty()){ - name1 = Utils::String::toUpper(file1->metadata.get("name")); - } - if(name2.empty()){ - name2 = Utils::String::toUpper(file2->metadata.get("name")); - } - return name1.compare(name2) < 0; + std::string name1 = file1->metadata.getSortName(); + return name1.compare(file2->metadata.getSortName()) < 0; } bool compareRating(const FileData* file1, const FileData* file2) diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index 3c9900d5a4..c9c1716188 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -1,6 +1,7 @@ #include "MetaData.h" #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" #include "Log.h" #include @@ -51,6 +52,20 @@ MetaDataDecl folderDecls[] = { }; const std::vector folderMDD(folderDecls, folderDecls + sizeof(folderDecls) / sizeof(folderDecls[0])); +std::map MetaDataList::mDefaultGameMap = MetaDataList::BuildDefaultMap(GAME_METADATA); +std::map MetaDataList::mDefaultFolderMap = MetaDataList::BuildDefaultMap(FOLDER_METADATA); + +std::map MetaDataList::BuildDefaultMap(MetaDataListType type) +{ + std::map ret; + + const std::vector& mdd = getMDDByType(type); + for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + ret[iter->key] = iter->defaultValue; + + return ret; +} + const std::vector& getMDDByType(MetaDataListType type) { switch(type) @@ -65,17 +80,11 @@ const std::vector& getMDDByType(MetaDataListType type) return gameMDD; } +MetaDataList::MetaDataList(MetaDataListType type) : mType(type), mWasChanged(false) +{ - -MetaDataList::MetaDataList(MetaDataListType type) - : mType(type), mWasChanged(false) -{ - const std::vector& mdd = getMDD(); - for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) - set(iter->key, iter->defaultValue); } - MetaDataList MetaDataList::createFromXML(MetaDataListType type, pugi::xml_node& node, const std::string& relativeTo) { MetaDataList mdl(type); @@ -85,17 +94,14 @@ MetaDataList MetaDataList::createFromXML(MetaDataListType type, pugi::xml_node& for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) { pugi::xml_node md = node.child(iter->key.c_str()); - if(md) - { - // if it's a path, resolve relative paths + if (md) + { std::string value = md.text().get(); - if (iter->type == MD_PATH) - { + + if (iter->type == MD_PATH) // if it's a path, resolve relative paths value = Utils::FileSystem::resolveRelativePath(value, relativeTo, true); - } + mdl.set(iter->key, value); - }else{ - mdl.set(iter->key, iter->defaultValue); } } @@ -113,7 +119,7 @@ void MetaDataList::appendToXML(pugi::xml_node& parent, bool ignoreDefaults, cons { // we have this value! // if it's just the default (and we ignore defaults), don't write it - if(ignoreDefaults && mapIter->second == mddIter->defaultValue) + if (ignoreDefaults && mapIter->second == mddIter->defaultValue) continue; // try and make paths relative if we can @@ -128,13 +134,23 @@ void MetaDataList::appendToXML(pugi::xml_node& parent, bool ignoreDefaults, cons void MetaDataList::set(const std::string& key, const std::string& value) { + if ((key == "sortname" || key == "name") && !value.empty()) + mSortName = Utils::String::toUpper(value); + mMap[key] = value; mWasChanged = true; } const std::string& MetaDataList::get(const std::string& key) const { - return mMap.at(key); + auto it = mMap.find(key); + if (it != mMap.end()) + return it->second; + + if (mType == GAME_METADATA) + return mDefaultGameMap.at(key); + + return mDefaultFolderMap.at(key); } int MetaDataList::getInt(const std::string& key) const @@ -150,9 +166,15 @@ float MetaDataList::getFloat(const std::string& key) const bool MetaDataList::isDefault() { const std::vector& mdd = getMDD(); + + for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + { + auto it = mMap.find(iter->key); + if (it == mMap.end()) + continue; - for (unsigned int i = 1; i < mMap.size(); i++) { - if (mMap.at(mdd[i].key) != mdd[i].defaultValue) return false; + if (it->second != iter->defaultValue) + return false; } return true; diff --git a/es-app/src/MetaData.h b/es-app/src/MetaData.h index 7205511a2c..c0c01f264d 100644 --- a/es-app/src/MetaData.h +++ b/es-app/src/MetaData.h @@ -64,10 +64,21 @@ class MetaDataList inline MetaDataListType getType() const { return mType; } inline const std::vector& getMDD() const { return getMDDByType(getType()); } + const std::string getSortName() const { + return mSortName; + } + private: MetaDataListType mType; std::map mMap; bool mWasChanged; + + static std::map mDefaultGameMap; + static std::map mDefaultFolderMap; + + static std::map BuildDefaultMap(MetaDataListType type); + + std::string mSortName; }; #endif // ES_APP_META_DATA_H diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 6389da9957..b2cd549059 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -1,594 +1,584 @@ -#include "SystemData.h" - -#include "utils/FileSystemUtil.h" -#include "CollectionSystemManager.h" -#include "FileFilterIndex.h" -#include "FileSorts.h" -#include "Gamelist.h" -#include "Log.h" -#include "platform.h" -#include "Settings.h" -#include "ThemeData.h" -#include "views/UIModeController.h" -#include -#include -#ifdef WIN32 -#include -#endif - -#include "GuiComponent.h" -#include "Window.h" - -std::vector SystemData::sSystemVector; - -SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : - mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) -{ - mFilterIndex = new FileFilterIndex(); - - // if it's an actual system, initialize it, if not, just create the data structure - if (!CollectionSystem) - { - mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); - mRootFolder->metadata.set("name", mFullName); - - if (!Settings::getInstance()->getBool("ParseGamelistOnly")) - { - populateFolder(mRootFolder); - if (mRootFolder->getChildren().size() == 0) - return; - } - - if (!Settings::getInstance()->getBool("IgnoreGamelist")) - parseGamelist(this); - - mRootFolder->sort(FileSorts::SortTypes.at(0)); - - indexAllGameFilters(mRootFolder); - } - else - { - // virtual systems are updated afterwards, we're just creating the data structure - mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this); - } - setIsGameSystemStatus(); - loadTheme(); -} - -SystemData::~SystemData() -{ - delete mRootFolder; - delete mFilterIndex; -} - -void SystemData::setIsGameSystemStatus() -{ - // we exclude non-game systems from specific operations (i.e. the "RetroPie" system, at least) - // if/when there are more in the future, maybe this can be a more complex method, with a proper list - // but for now a simple string comparison is more performant - mIsGameSystem = (mName != "retropie"); -} - -char _easytolower(char in) { - if (in <= 'Z' && in >= 'A') - return in - ('Z' - 'z'); - return in; -} - -void SystemData::populateFolder(FileData* folder) -{ - const std::string& folderPath = folder->getPath(); - if(!Utils::FileSystem::isDirectory(folderPath)) - { - LOG(LogWarning) << "Error - folder with path \"" << folderPath << "\" is not a directory!"; - return; - } - - int di = folderPath.rfind("downloaded_images"); - int md = folderPath.rfind("media"); - if (di > 0 || md > 0) - return; - - //make sure that this isn't a symlink to a thing we already have - if(Utils::FileSystem::isSymlink(folderPath)) - { - //if this symlink resolves to somewhere that's at the beginning of our path, it's gonna recurse - if(folderPath.find(Utils::FileSystem::getCanonicalPath(folderPath)) == 0) - { - LOG(LogWarning) << "Skipping infinitely recursive symlink \"" << folderPath << "\""; - return; - } - } - - std::string filePath; - std::string extension; - bool isGame; - bool showHidden = Settings::getInstance()->getBool("ShowHiddenFiles"); - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(folderPath); - for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - filePath = *it; - - // skip hidden files and folders - if(!showHidden && Utils::FileSystem::isHidden(filePath)) - 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 = Utils::FileSystem::getExtension(filePath); - std::transform(extension.begin(), extension.end(), extension.begin(), ::_easytolower); - - //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(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) - { - FileData* newGame = new FileData(GAME, filePath, mEnvData, this); - - // preventing new arcade assets to be added - if(!newGame->isArcadeAsset()) - { - folder->addChild(newGame); - isGame = true; - } - } - - //add directories that also do not match an extension as folders - if(!isGame && Utils::FileSystem::isDirectory(filePath)) - { - if (filePath.rfind("downloaded_images") == std::string::npos && - filePath.rfind("media") == std::string::npos) - { - FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); - populateFolder(newFolder); - - if (newFolder->getChildrenByFilename().size() == 0) - delete newFolder; - else if (newFolder->findUniqueGameForFolder() != NULL) - delete newFolder; - else - folder->addChild(newFolder); - } - } - } -} - -void SystemData::indexAllGameFilters(const FileData* folder) -{ - const std::vector& children = folder->getChildren(); - - for(std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) - { - switch((*it)->getType()) - { - case GAME: { mFilterIndex->addToIndex(*it); } break; - case FOLDER: { indexAllGameFilters(*it); } break; - } - } -} - -std::vector readList(const std::string& str, const char* delims = " \t\r\n,") -{ - std::vector 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(Window* window) -{ - deleteSystems(); - - std::string path = getConfigPath(false); - - LOG(LogInfo) << "Loading system config file " << path << "..."; - - if(!Utils::FileSystem::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 tag!"; - return false; - } - - float systemCount = 1; - for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - systemCount++; - - float currentSystem = 0; - for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - { - std::vector emulatorList; - - // std::vector coreList; - - std::string name, fullname, path, cmd, themeFolder, defaultCore; - - name = system.child("name").text().get(); - fullname = system.child("fullname").text().get(); - path = system.child("path").text().get(); - defaultCore = system.child("defaultCore").text().get(); - - pugi::xml_node emulators = system.child("emulators"); - if (emulators != NULL) - { - for (pugi::xml_node emulator : emulators.children()) - { - EmulatorData emulatorData; - emulatorData.mName = emulator.attribute("name").value(); - emulatorData.mCommandLine = emulator.attribute("command").value(); - - pugi::xml_node cores = emulator.child("cores"); - if (cores != NULL) - { - for (pugi::xml_node core : cores.children()) - { - const std::string& corename = core.text().get(); - - if (defaultCore.length() == 0) - defaultCore = corename; - - emulatorData.mCores.push_back(corename); - // coreList.push_back(corename); - } - } - - emulatorList.push_back(emulatorData); - } - } - - if (window != NULL) - window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); - - currentSystem++; - - // convert extensions list from a string into a vector of strings - - std::vector list = readList(system.child("extension").text().get()); - std::vector extensions; - - for (auto extension = list.cbegin(); extension != list.cend(); extension++) - { - std::string xt = (*extension); - std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); - - if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) - extensions.push_back(xt); - } - - cmd = system.child("command").text().get(); - - // platform id list - const char* platformList = system.child("platform").text().get(); - std::vector platformStrs = readList(platformList); - std::vector platformIds; - for(auto it = platformStrs.cbegin(); it != platformStrs.cend(); 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 - path = Utils::FileSystem::getGenericPath(path); - - //expand home symbol if the startpath contains ~ - if (path[0] == '~') - { - path.erase(0, 1); - path.insert(0, Utils::FileSystem::getHomePath()); - } - - //create the system runtime environment data - SystemEnvironmentData* envData = new SystemEnvironmentData; - envData->mStartPath = path; - envData->mSearchExtensions = extensions; - envData->mLaunchCommand = cmd; - envData->mPlatformIds = platformIds; - // envData->mDefaultCore = defaultCore; - envData->mEmulators = emulatorList; - - // envData->mCores = coreList; - - SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); - if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) - { - LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; - delete newSys; - } - else - sSystemVector.push_back(newSys); - } - - if (window != NULL) - window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); - - currentSystem++; - - CollectionSystemManager::get()->loadCollectionSystems(); - return true; -} - -void SystemData::writeExampleConfig(const std::string& path) -{ - std::ofstream file(path.c_str()); - - file << "\n" - "\n" - "\n" - " \n" - " \n" - "\n" - " \n" - " nes\n" - "\n" - " \n" - " Nintendo Entertainment System\n" - "\n" - " \n" - " ~/roms/nes\n" - "\n" - " \n" - " .nes .NES\n" - "\n" - " \n" - " retroarch -L ~/cores/libretro-fceumm.so %ROM%\n" - "\n" - " \n" - " nes\n" - "\n" - " \n" - " nes\n" - " \n" - "\n"; - - file.close(); - - LOG(LogError) << "Example config written! Go read it at \"" << path << "\"!"; -} - -void SystemData::deleteSystems() -{ - bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); - - for(unsigned int i = 0; i < sSystemVector.size(); i++) - { - SystemData* pData = sSystemVector.at(i); - - if (saveOnExit && !pData->mIsCollectionSystem) - updateGamelist(pData); - - delete pData; - } - - sSystemVector.clear(); -} - -std::string SystemData::getConfigPath(bool forWrite) -{ - std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_systems.cfg"; - if(forWrite || Utils::FileSystem::exists(path)) - return path; - - return "/etc/emulationstation/es_systems.cfg"; -} - -bool SystemData::isVisible() -{ - return (getDisplayedGameCount() > 0 || - (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || - (mIsCollectionSystem && mName == "favorites")); -} - -SystemData* SystemData::getNext() const -{ - std::vector::const_iterator it = getIterator(); - - do { - it++; - if (it == sSystemVector.cend()) - it = sSystemVector.cbegin(); - } while (!(*it)->isVisible()); - // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. - - return *it; -} - -SystemData* SystemData::getPrev() const -{ - std::vector::const_reverse_iterator it = getRevIterator(); - - do { - it++; - if (it == sSystemVector.crend()) - it = sSystemVector.crbegin(); - } while (!(*it)->isVisible()); - // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. - - return *it; -} - -std::string SystemData::getGamelistPath(bool forWrite) const -{ - std::string filePath; - - filePath = mRootFolder->getPath() + "/gamelist.xml"; - if(Utils::FileSystem::exists(filePath)) - return filePath; - - filePath = Utils::FileSystem::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 - Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath)); - if(forWrite || Utils::FileSystem::exists(filePath)) - return filePath; - - 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. system theme from currently selected theme set [CURRENT_THEME_PATH]/[SYSTEM]/theme.xml - // 3. default system theme from currently selected theme set [CURRENT_THEME_PATH]/theme.xml - - // first, check game folder - std::string localThemePath = mRootFolder->getPath() + "/theme.xml"; - if(Utils::FileSystem::exists(localThemePath)) - return localThemePath; - - // not in game folder, try system theme in theme sets - localThemePath = ThemeData::getThemeFromCurrentSet(mThemeFolder); - - if (Utils::FileSystem::exists(localThemePath)) - return localThemePath; - - // not system theme, try default system theme in theme set - localThemePath = Utils::FileSystem::getParent(Utils::FileSystem::getParent(localThemePath)) + "/theme.xml"; - - return localThemePath; -} - -bool SystemData::hasGamelist() const -{ - return (Utils::FileSystem::exists(getGamelistPath(false))); -} - -unsigned int SystemData::getGameCount() const -{ - return (unsigned int)mRootFolder->getFilesRecursive(GAME).size(); -} - -SystemData* SystemData::getRandomSystem() -{ - // this is a bit brute force. It might be more efficient to just to a while (!gameSystem) do random again... - unsigned int total = 0; - for(auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) - { - if ((*it)->isGameSystem()) - total ++; - } - - // get random number in range - int target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); - for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) - { - if ((*it)->isGameSystem()) - { - if (target > 0) - { - target--; - } - else - { - return (*it); - } - } - } - - // if we end up here, there is no valid system - return NULL; -} - -FileData* SystemData::getRandomGame() -{ - std::vector list = mRootFolder->getFilesRecursive(GAME, true); - unsigned int total = (int)list.size(); - int target = 0; - // get random number in range - if (total == 0) - return NULL; - target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); - return list.at(target); -} - -unsigned int SystemData::getDisplayedGameCount() const -{ - return (unsigned int)mRootFolder->getFilesRecursive(GAME, true).size(); -} - -void SystemData::loadTheme() -{ - mTheme = std::make_shared(); - - std::string path = getThemePath(); - - if(!Utils::FileSystem::exists(path)) // no theme available for this platform - return; - - try - { - // build map with system variables for theme to use, - std::map sysData; - sysData.insert(std::pair("system.name", getName())); - sysData.insert(std::pair("system.theme", getThemeFolder())); - sysData.insert(std::pair("system.fullName", getFullName())); - - mTheme->loadFile(getThemeFolder(), sysData, path); - } - catch(ThemeException& e) - { - LOG(LogError) << e.what(); - mTheme = std::make_shared(); // reset to empty - } -} +#include "SystemData.h" + +#include "utils/FileSystemUtil.h" +#include "CollectionSystemManager.h" +#include "FileFilterIndex.h" +#include "FileSorts.h" +#include "Gamelist.h" +#include "Log.h" +#include "platform.h" +#include "Settings.h" +#include "ThemeData.h" +#include "views/UIModeController.h" +#include +#include +#ifdef WIN32 +#include +#endif + +#include "GuiComponent.h" +#include "Window.h" + +std::vector SystemData::sSystemVector; + +SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : + mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) +{ + mFilterIndex = new FileFilterIndex(); + + // if it's an actual system, initialize it, if not, just create the data structure + if (!CollectionSystem) + { + mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); + mRootFolder->metadata.set("name", mFullName); + + if (!Settings::getInstance()->getBool("ParseGamelistOnly")) + { + populateFolder(mRootFolder); + if (mRootFolder->getChildren().size() == 0) + return; + } + + if (!Settings::getInstance()->getBool("IgnoreGamelist")) + parseGamelist(this); + + mRootFolder->sort(FileSorts::SortTypes.at(0)); + + indexAllGameFilters(mRootFolder); + } + else + { + // virtual systems are updated afterwards, we're just creating the data structure + mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this); + } + + setIsGameSystemStatus(); + loadTheme(); +} + +SystemData::~SystemData() +{ + delete mRootFolder; + delete mFilterIndex; +} + +void SystemData::setIsGameSystemStatus() +{ + // we exclude non-game systems from specific operations (i.e. the "RetroPie" system, at least) + // if/when there are more in the future, maybe this can be a more complex method, with a proper list + // but for now a simple string comparison is more performant + mIsGameSystem = (mName != "retropie"); +} + +char _easytolower(char in) { + if (in <= 'Z' && in >= 'A') + return in - ('Z' - 'z'); + return in; +} + +void SystemData::populateFolder(FileData* folder) +{ + const std::string& folderPath = folder->getPath(); + if(!Utils::FileSystem::isDirectory(folderPath)) + { + LOG(LogWarning) << "Error - folder with path \"" << folderPath << "\" is not a directory!"; + return; + } + + //make sure that this isn't a symlink to a thing we already have + if(Utils::FileSystem::isSymlink(folderPath)) + { + //if this symlink resolves to somewhere that's at the beginning of our path, it's gonna recurse + if(folderPath.find(Utils::FileSystem::getCanonicalPath(folderPath)) == 0) + { + LOG(LogWarning) << "Skipping infinitely recursive symlink \"" << folderPath << "\""; + return; + } + } + +// std::string filePath; + std::string extension; + bool isGame; + bool showHidden = Settings::getInstance()->getBool("ShowHiddenFiles"); + + Utils::FileSystem::fileList dirContent = Utils::FileSystem::getDirInfo(folderPath, false); + for(Utils::FileSystem::fileList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + auto fileInfo = *it; + //filePath = *it; + + // skip hidden files and folders + if(!showHidden && fileInfo.readOnly) + 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 = Utils::FileSystem::getExtension(fileInfo.path); + std::transform(extension.begin(), extension.end(), extension.begin(), ::_easytolower); + + //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(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) + { + FileData* newGame = new FileData(GAME, fileInfo.path, mEnvData, this); + + // preventing new arcade assets to be added + if (extension != ".zip" || !newGame->isArcadeAsset()) + { + folder->addChild(newGame); + isGame = true; + } + } + + //add directories that also do not match an extension as folders + if (!isGame && fileInfo.directory) + { + if (fileInfo.path.rfind("downloaded_images") != std::string::npos || fileInfo.path.rfind("media") != std::string::npos) + continue; + + FileData* newFolder = new FileData(FOLDER, fileInfo.path, mEnvData, this); + populateFolder(newFolder); + + if (newFolder->getChildrenByFilename().size() == 0) + delete newFolder; + else if (newFolder->findUniqueGameForFolder() != NULL) + delete newFolder; + else + folder->addChild(newFolder); + } + } +} + +void SystemData::indexAllGameFilters(const FileData* folder) +{ + const std::vector& children = folder->getChildren(); + + for(std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + switch((*it)->getType()) + { + case GAME: { mFilterIndex->addToIndex(*it); } break; + case FOLDER: { indexAllGameFilters(*it); } break; + } + } +} + +std::vector readList(const std::string& str, const char* delims = " \t\r\n,") +{ + std::vector 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(Window* window) +{ + deleteSystems(); + + std::string path = getConfigPath(false); + + LOG(LogInfo) << "Loading system config file " << path << "..."; + + if(!Utils::FileSystem::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 tag!"; + return false; + } + + float systemCount = 1; + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + systemCount++; + + float currentSystem = 0; + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { + std::vector emulatorList; + + std::string name, fullname, path, cmd, themeFolder, defaultCore; + + name = system.child("name").text().get(); + fullname = system.child("fullname").text().get(); + path = system.child("path").text().get(); + defaultCore = system.child("defaultCore").text().get(); + + pugi::xml_node emulators = system.child("emulators"); + if (emulators != NULL) + { + for (pugi::xml_node emulator : emulators.children()) + { + EmulatorData emulatorData; + emulatorData.mName = emulator.attribute("name").value(); + emulatorData.mCommandLine = emulator.attribute("command").value(); + + pugi::xml_node cores = emulator.child("cores"); + if (cores != NULL) + { + for (pugi::xml_node core : cores.children()) + { + const std::string& corename = core.text().get(); + + if (defaultCore.length() == 0) + defaultCore = corename; + + emulatorData.mCores.push_back(corename); + } + } + + emulatorList.push_back(emulatorData); + } + } + + if (window != NULL) + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; + + // convert extensions list from a string into a vector of strings + + std::vector list = readList(system.child("extension").text().get()); + std::vector extensions; + + for (auto extension = list.cbegin(); extension != list.cend(); extension++) + { + std::string xt = (*extension); + std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); + + if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) + extensions.push_back(xt); + } + + cmd = system.child("command").text().get(); + + // platform id list + const char* platformList = system.child("platform").text().get(); + std::vector platformStrs = readList(platformList); + std::vector platformIds; + for(auto it = platformStrs.cbegin(); it != platformStrs.cend(); 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 + path = Utils::FileSystem::getGenericPath(path); + + //expand home symbol if the startpath contains ~ + if (path[0] == '~') + { + path.erase(0, 1); + path.insert(0, Utils::FileSystem::getHomePath()); + } + + //create the system runtime environment data + SystemEnvironmentData* envData = new SystemEnvironmentData; + envData->mStartPath = path; + envData->mSearchExtensions = extensions; + envData->mLaunchCommand = cmd; + envData->mPlatformIds = platformIds; + envData->mEmulators = emulatorList; + + SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); + if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) + { + LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; + delete newSys; + } + else + sSystemVector.push_back(newSys); + } + + if (window != NULL) + window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; + + CollectionSystemManager::get()->loadCollectionSystems(); + return true; +} + +void SystemData::writeExampleConfig(const std::string& path) +{ + std::ofstream file(path.c_str()); + + file << "\n" + "\n" + "\n" + " \n" + " \n" + "\n" + " \n" + " nes\n" + "\n" + " \n" + " Nintendo Entertainment System\n" + "\n" + " \n" + " ~/roms/nes\n" + "\n" + " \n" + " .nes .NES\n" + "\n" + " \n" + " retroarch -L ~/cores/libretro-fceumm.so %ROM%\n" + "\n" + " \n" + " nes\n" + "\n" + " \n" + " nes\n" + " \n" + "\n"; + + file.close(); + + LOG(LogError) << "Example config written! Go read it at \"" << path << "\"!"; +} + +void SystemData::deleteSystems() +{ + bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); + + for(unsigned int i = 0; i < sSystemVector.size(); i++) + { + SystemData* pData = sSystemVector.at(i); + + if (saveOnExit && !pData->mIsCollectionSystem) + updateGamelist(pData); + + delete pData; + } + + sSystemVector.clear(); +} + +std::string SystemData::getConfigPath(bool forWrite) +{ + std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_systems.cfg"; + if(forWrite || Utils::FileSystem::exists(path)) + return path; + + return "/etc/emulationstation/es_systems.cfg"; +} + +bool SystemData::isVisible() +{ + return (getDisplayedGameCount() > 0 || + (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || + (mIsCollectionSystem && mName == "favorites")); +} + +SystemData* SystemData::getNext() const +{ + std::vector::const_iterator it = getIterator(); + + do { + it++; + if (it == sSystemVector.cend()) + it = sSystemVector.cbegin(); + } while (!(*it)->isVisible()); + // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. + + return *it; +} + +SystemData* SystemData::getPrev() const +{ + std::vector::const_reverse_iterator it = getRevIterator(); + + do { + it++; + if (it == sSystemVector.crend()) + it = sSystemVector.crbegin(); + } while (!(*it)->isVisible()); + // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. + + return *it; +} + +std::string SystemData::getGamelistPath(bool forWrite) const +{ + std::string filePath; + + filePath = mRootFolder->getPath() + "/gamelist.xml"; + if(Utils::FileSystem::exists(filePath)) + return filePath; + + filePath = Utils::FileSystem::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 + Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath)); + if(forWrite || Utils::FileSystem::exists(filePath)) + return filePath; + + 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. system theme from currently selected theme set [CURRENT_THEME_PATH]/[SYSTEM]/theme.xml + // 3. default system theme from currently selected theme set [CURRENT_THEME_PATH]/theme.xml + + // first, check game folder + std::string localThemePath = mRootFolder->getPath() + "/theme.xml"; + if(Utils::FileSystem::exists(localThemePath)) + return localThemePath; + + // not in game folder, try system theme in theme sets + localThemePath = ThemeData::getThemeFromCurrentSet(mThemeFolder); + + if (Utils::FileSystem::exists(localThemePath)) + return localThemePath; + + // not system theme, try default system theme in theme set + localThemePath = Utils::FileSystem::getParent(Utils::FileSystem::getParent(localThemePath)) + "/theme.xml"; + + return localThemePath; +} + +bool SystemData::hasGamelist() const +{ + return (Utils::FileSystem::exists(getGamelistPath(false))); +} + +unsigned int SystemData::getGameCount() const +{ + return (unsigned int)mRootFolder->getFilesRecursive(GAME).size(); +} + +SystemData* SystemData::getRandomSystem() +{ + // this is a bit brute force. It might be more efficient to just to a while (!gameSystem) do random again... + unsigned int total = 0; + for(auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) + { + if ((*it)->isGameSystem()) + total ++; + } + + // get random number in range + int target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); + for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) + { + if ((*it)->isGameSystem()) + { + if (target > 0) + { + target--; + } + else + { + return (*it); + } + } + } + + // if we end up here, there is no valid system + return NULL; +} + +FileData* SystemData::getRandomGame() +{ + std::vector list = mRootFolder->getFilesRecursive(GAME, true); + unsigned int total = (int)list.size(); + int target = 0; + // get random number in range + if (total == 0) + return NULL; + target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); + return list.at(target); +} + +unsigned int SystemData::getDisplayedGameCount() const +{ + return (unsigned int)mRootFolder->getFilesRecursive(GAME, true).size(); +} + +void SystemData::loadTheme() +{ + mTheme = std::make_shared(); + + std::string path = getThemePath(); + if (!Utils::FileSystem::exists(path)) // no theme available for this platform + return; + + try + { + // build map with system variables for theme to use, + std::map sysData; + sysData.insert(std::pair("system.name", getName())); + sysData.insert(std::pair("system.theme", getThemeFolder())); + sysData.insert(std::pair("system.fullName", getFullName())); + + mTheme->loadFile(getThemeFolder(), sysData, path); + } + catch(ThemeException& e) + { + LOG(LogError) << e.what(); + mTheme = std::make_shared(); // reset to empty + } +} diff --git a/es-app/src/components/RatingComponent.cpp b/es-app/src/components/RatingComponent.cpp index 924a0cbccf..05063694ab 100644 --- a/es-app/src/components/RatingComponent.cpp +++ b/es-app/src/components/RatingComponent.cpp @@ -114,6 +114,11 @@ void RatingComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = parentTrans * getTransform(); trans.round(); + + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + Renderer::setMatrix(trans); glEnable(GL_TEXTURE_2D); diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index be30460aa4..c47c7e7d5e 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -27,7 +27,7 @@ void GuiCollectionSystemsOptions::initializeMenu() std::vector unusedFolders = CollectionSystemManager::get()->getUnusedSystemsFromTheme(); if (unusedFolders.size() > 0) { - addEntry(_T("CREATE NEW CUSTOM COLLECTION FROM THEME"), 0x777777FF, true, + addEntry(_T("CREATE NEW CUSTOM COLLECTION FROM THEME"), ThemeData::getMenuTheme()->Text.color, true, [this, unusedFolders] { auto s = new GuiSettings(mWindow, _T("SELECT THEME FOLDER")); std::shared_ptr< OptionListComponent > folderThemes = std::make_shared< OptionListComponent >(mWindow, "SELECT THEME FOLDER", true); @@ -47,12 +47,14 @@ void GuiCollectionSystemsOptions::initializeMenu() row.addElement(themeFolder, true); s->addRow(row); } + + s->updatePosition(); mWindow->pushGui(s); }); } ComponentListRow row; - row.addElement(std::make_shared(mWindow, _T("CREATE NEW CUSTOM COLLECTION"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("CREATE NEW CUSTOM COLLECTION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); auto createCustomCollection = [this](const std::string& newVal) { std::string name = newVal; // we need to store the first Gui and remove it, as it'll be deleted by the actual Gui @@ -85,16 +87,18 @@ void GuiCollectionSystemsOptions::initializeMenu() mMenu.addButton(_T("BACK"), _T("BACK"), std::bind(&GuiCollectionSystemsOptions::applySettings, this)); - mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); + mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); } void GuiCollectionSystemsOptions::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) { - std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); + auto theme = ThemeData::getMenuTheme(); + + std::shared_ptr font = theme->Text.font; // Font::get(FONT_SIZE_MEDIUM); // populate the list ComponentListRow row; - row.addElement(std::make_shared(mWindow, name, font, color), true); + row.addElement(std::make_shared(mWindow, name, font, theme->Text.color), true); if(add_arrow) { diff --git a/es-app/src/guis/GuiGamelistFilter.cpp b/es-app/src/guis/GuiGamelistFilter.cpp index 1216775439..ffdc11bc43 100644 --- a/es-app/src/guis/GuiGamelistFilter.cpp +++ b/es-app/src/guis/GuiGamelistFilter.cpp @@ -19,9 +19,10 @@ void GuiGamelistFilter::initializeMenu() ComponentListRow row; + // show filtered menu row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("RESET ALL FILTERS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("RESET ALL FILTERS"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistFilter::resetAllFilters, this)); mMenu.addRow(row); row.elements.clear(); diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index fd42411e4a..7a2d13c20f 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -52,7 +52,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui } } - row.addElement(std::make_shared(mWindow, _T("JUMP TO LETTER"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("JUMP TO LETTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(mJumpToLetterList, false); row.input_handler = [&](InputConfig* config, Input input) { @@ -83,7 +83,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if(!Settings::getInstance()->getBool("ForceDisableFilters")) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("APPLY FILTER"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("APPLY FILTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); mMenu.addRow(row); @@ -96,7 +96,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui CollectionSystemManager::get()->getCustomCollectionsBundle()->getName() == system->getName())) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "ADD/REMOVE GAMES TO THIS GAME COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, "ADD/REMOVE GAMES TO THIS GAME COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::startEditMode, this)); mMenu.addRow(row); } @@ -104,7 +104,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if(UIModeController::getInstance()->isUIModeFull() && CollectionSystemManager::get()->isEditing()) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::exitEditMode, this)); mMenu.addRow(row); } @@ -112,7 +112,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("EDIT THIS GAME'S METADATA"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("EDIT THIS GAME'S METADATA"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openMetaDataEd, this)); mMenu.addRow(row); diff --git a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp index e229aa87c6..e5c0c84409 100644 --- a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp +++ b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp @@ -50,13 +50,13 @@ GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, std:: // show filtered menu row.elements.clear(); - row.addElement(std::make_shared(mWindow, "VIDEO SCREENSAVER SETTINGS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, "VIDEO SCREENSAVER SETTINGS", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGeneralScreensaverOptions::openVideoScreensaverOptions, this)); addRow(row); row.elements.clear(); - row.addElement(std::make_shared(mWindow, "SLIDESHOW SCREENSAVER SETTINGS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, "SLIDESHOW SCREENSAVER SETTINGS", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGeneralScreensaverOptions::openSlideshowScreensaverOptions, this)); addRow(row); diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index beaf8e8749..8173bc36c1 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -1,64 +1,65 @@ -#include "guis/GuiMenu.h" - -#include "components/OptionListComponent.h" -#include "components/SliderComponent.h" -#include "components/SwitchComponent.h" -#include "guis/GuiCollectionSystemsOptions.h" -#include "guis/GuiDetectDevice.h" -#include "guis/GuiGeneralScreensaverOptions.h" -#include "guis/GuiMsgBox.h" -#include "guis/GuiScraperStart.h" -#include "guis/GuiSettings.h" -#include "views/UIModeController.h" -#include "views/ViewController.h" -#include "CollectionSystemManager.h" -#include "EmulationStation.h" -#include "Scripting.h" -#include "SystemData.h" -#include "VolumeControl.h" -#include -#include - -#include "animations/LambdaAnimation.h" - -GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) - -{ - bool isFullUI = UIModeController::getInstance()->isUIModeFull(); - - if (isFullUI) - { - addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); - addEntry(_T("CONFIGURE INPUT"), 0x777777FF, true, [this] { openConfigInput(); }); - } - - addEntry(_T("SOUND SETTINGS"), 0x777777FF, true, [this] { openSoundSettings(); }); - - if (isFullUI) - addEntry(_T("SCRAPER"), 0x777777FF, true, [this] { openScraperSettings(); }); - - if (isFullUI) - { - addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); - addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); - } - -#if defined(_WIN32) - addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); -#else - addEntry(_T("QUIT"), 0x777777FF, true, [this] {openQuitMenu(); }); -#endif - - addChild(&mMenu); - addVersionInfo(); - - setSize(mMenu.getSize()); - - float y1 = Renderer::getScreenHeight(); - float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; - - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); - +#include "guis/GuiMenu.h" + +#include "components/OptionListComponent.h" +#include "components/SliderComponent.h" +#include "components/SwitchComponent.h" +#include "guis/GuiCollectionSystemsOptions.h" +#include "guis/GuiDetectDevice.h" +#include "guis/GuiGeneralScreensaverOptions.h" +#include "guis/GuiMsgBox.h" +#include "guis/GuiScraperStart.h" +#include "guis/GuiSettings.h" +#include "views/UIModeController.h" +#include "views/ViewController.h" +#include "CollectionSystemManager.h" +#include "EmulationStation.h" +#include "Scripting.h" +#include "SystemData.h" +#include "VolumeControl.h" +#include +#include + +#include "animations/LambdaAnimation.h" + +GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) +{ + auto theme = ThemeData::getMenuTheme(); + + bool isFullUI = UIModeController::getInstance()->isUIModeFull(); + + if (isFullUI) + { + addEntry(_T("UI SETTINGS"), theme->Text.color, true, [this] { openUISettings(); }); + addEntry(_T("CONFIGURE INPUT"), theme->Text.color, true, [this] { openConfigInput(); }); + } + + addEntry(_T("SOUND SETTINGS"), theme->Text.color, true, [this] { openSoundSettings(); }); + + if (isFullUI) + addEntry(_T("SCRAPER"), theme->Text.color, true, [this] { openScraperSettings(); }); + + if (isFullUI) + { + addEntry(_T("GAME COLLECTION SETTINGS"), theme->Text.color, true, [this] { openCollectionSystemSettings(); }); + addEntry(_T("ADVANCED SETTINGS"), theme->Text.color, true, [this] { openOtherSettings(); }); + } + +#if defined(_WIN32) + addEntry(_T("QUIT"), theme->Text.color, false, [this] {openQuitMenu(); }); +#else + addEntry(_T("QUIT"), theme->Text.color, true, [this] {openQuitMenu(); }); +#endif + + addChild(&mMenu); + addVersionInfo(); + + setSize(mMenu.getSize()); + + float y1 = Renderer::getScreenHeight(); + float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); + auto fadeFunc = [this, y1, y2](float t) { t -= 1; // cubic ease out @@ -70,648 +71,686 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] { - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); - }); - - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); -} - -void GuiMenu::openScraperSettings() -{ - auto s = new GuiSettings(mWindow, "SCRAPER"); - - // scrape from - auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, "SCRAPE FROM", false); - std::vector scrapers = getScraperList(); - - // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. - for(auto it = scrapers.cbegin(); it != scrapers.cend(); 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(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; - auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; - std::function openAndSave = openScrapeNow; - openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; - row.makeAcceptInputHandler(openAndSave); - - auto scrape_now = std::make_shared(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); -} - -void GuiMenu::openSoundSettings() -{ - auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); - - // volume - auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); - volume->setValue((float)VolumeControl::getInstance()->getVolume()); - s->addWithLabel(_T("SYSTEM VOLUME"), volume); - s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); - - if (UIModeController::getInstance()->isUIModeFull()) - { -#if defined(__linux__) - // audio card - auto audio_card = std::make_shared< OptionListComponent >(mWindow, "AUDIO CARD", false); - std::vector audio_cards; - #ifdef _RPI_ - // RPi Specific Audio Cards - audio_cards.push_back("local"); - audio_cards.push_back("hdmi"); - audio_cards.push_back("both"); - #endif - audio_cards.push_back("default"); - audio_cards.push_back("sysdefault"); - audio_cards.push_back("dmix"); - audio_cards.push_back("hw"); - audio_cards.push_back("plughw"); - audio_cards.push_back("null"); - if (Settings::getInstance()->getString("AudioCard") != "") { - if(std::find(audio_cards.begin(), audio_cards.end(), Settings::getInstance()->getString("AudioCard")) == audio_cards.end()) { - audio_cards.push_back(Settings::getInstance()->getString("AudioCard")); - } - } - for(auto ac = audio_cards.cbegin(); ac != audio_cards.cend(); ac++) - audio_card->add(*ac, *ac, Settings::getInstance()->getString("AudioCard") == *ac); - s->addWithLabel("AUDIO CARD", audio_card); - s->addSaveFunc([audio_card] { - Settings::getInstance()->setString("AudioCard", audio_card->getSelected()); - VolumeControl::getInstance()->deinit(); - VolumeControl::getInstance()->init(); - }); - - // volume control device - auto vol_dev = std::make_shared< OptionListComponent >(mWindow, "AUDIO DEVICE", false); - std::vector transitions; - transitions.push_back("PCM"); - transitions.push_back("Speaker"); - transitions.push_back("Master"); - transitions.push_back("Digital"); - transitions.push_back("Analogue"); - if (Settings::getInstance()->getString("AudioDevice") != "") { - if(std::find(transitions.begin(), transitions.end(), Settings::getInstance()->getString("AudioDevice")) == transitions.end()) { - transitions.push_back(Settings::getInstance()->getString("AudioDevice")); - } - } - for(auto it = transitions.cbegin(); it != transitions.cend(); it++) - vol_dev->add(*it, *it, Settings::getInstance()->getString("AudioDevice") == *it); - s->addWithLabel("AUDIO DEVICE", vol_dev); - s->addSaveFunc([vol_dev] { - Settings::getInstance()->setString("AudioDevice", vol_dev->getSelected()); - VolumeControl::getInstance()->deinit(); - VolumeControl::getInstance()->init(); - }); -#endif - - // disable sounds - auto sounds_enabled = std::make_shared(mWindow); - sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); - s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); - s->addSaveFunc([sounds_enabled] { - if (sounds_enabled->getState() - && !Settings::getInstance()->getBool("EnableSounds") - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState()); - }); - - auto video_audio = std::make_shared(mWindow); - video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); - s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); - s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); - -#ifdef _RPI_ - // OMX player Audio Device - auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, "OMX PLAYER AUDIO DEVICE", false); - std::vector omx_cards; - // RPi Specific Audio Cards - omx_cards.push_back("local"); - omx_cards.push_back("hdmi"); - omx_cards.push_back("both"); - omx_cards.push_back("alsa:hw:0,0"); - omx_cards.push_back("alsa:hw:1,0"); - if (Settings::getInstance()->getString("OMXAudioDev") != "") { - if (std::find(omx_cards.begin(), omx_cards.end(), Settings::getInstance()->getString("OMXAudioDev")) == omx_cards.end()) { - omx_cards.push_back(Settings::getInstance()->getString("OMXAudioDev")); - } - } - for (auto it = omx_cards.cbegin(); it != omx_cards.cend(); it++) - omx_audio_dev->add(*it, *it, Settings::getInstance()->getString("OMXAudioDev") == *it); - s->addWithLabel("OMX PLAYER AUDIO DEVICE", omx_audio_dev); - s->addSaveFunc([omx_audio_dev] { - if (Settings::getInstance()->getString("OMXAudioDev") != omx_audio_dev->getSelected()) - Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); - }); -#endif - } - - mWindow->pushGui(s); - -} - -void GuiMenu::openUISettings() -{ - auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); - - // theme set - auto themeSets = ThemeData::getThemeSets(); - - if (!themeSets.empty()) - { - std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if (selectedSet == themeSets.cend()) - selectedSet = themeSets.cbegin(); - - auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); - for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) - theme_set->add(it->first, it->first, it == selectedSet); - s->addWithLabel(_T("THEME"), theme_set); - - Window* window = mWindow; - s->addSaveFunc([window, theme_set] - { - bool needReload = false; - std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - if (oldTheme != theme_set->getSelected()) - needReload = true; - - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - - if (needReload) - { - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation - } - }); - } - - // LANGUAGE - - std::vector langues; - langues.push_back("en"); - - - std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); - if (xmlpath.length() > 0) - { - xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; - - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); - for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if (Utils::FileSystem::isDirectory(*it)) - continue; - - std::string name = *it; - - if (name.rfind("emulationstation2.po") == std::string::npos) - continue; - - name = Utils::FileSystem::getParent(name); - name = Utils::FileSystem::getFileName(name); - - if (name != "en") - langues.push_back(name); - } - - if (langues.size() > 1) - { - auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); - - for (auto it = langues.cbegin(); it != langues.cend(); it++) - language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); - - s->addWithLabel(_T("LANGUAGE"), language); - s->addSaveFunc([language] { - Settings::getInstance()->setString("Language", language->getSelected()); - }); - } - } - - // transition style - auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); - std::vector transitions; - transitions.push_back("fade"); - transitions.push_back("slide"); - transitions.push_back("instant"); - for (auto it = transitions.cbegin(); it != transitions.cend(); it++) - transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); - - s->addWithLabel(_T("TRANSITION STYLE"), transition_style); - s->addSaveFunc([transition_style] { - if (Settings::getInstance()->getString("TransitionStyle") == "instant" - && transition_style->getSelected() != "instant" - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); - }); - - - auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); - std::vector gameTransitions; - gameTransitions.push_back("fade"); - gameTransitions.push_back("slide"); - gameTransitions.push_back("instant"); - for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) - transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); - - s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); - s->addSaveFunc([transitionOfGames_style] { - if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" - && transitionOfGames_style->getSelected() != "instant" - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); - }); - - // GameList view style - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); - std::vector styles; - styles.push_back("automatic"); - styles.push_back("basic"); - styles.push_back("detailed"); - styles.push_back("video"); - styles.push_back("grid"); - styles.push_back("gridex"); - - for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); - - s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); - s->addSaveFunc([gamelist_style] { - bool needReload = false; - if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) - needReload = true; - Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); - if (needReload) - ViewController::get()->reloadAll(); - }); - - - // Optionally start in selected system - auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); - systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); - for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) - { - if ("retropie" != (*it)->getName()) - { - systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); - } - } - s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); - s->addSaveFunc([systemfocus_list] { - Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); - }); - - - /* - //UI mode - auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); - std::vector UImodes = UIModeController::getInstance()->getUIModes(); - for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) - UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); - s->addWithLabel("UI MODE", UImodeSelection); - Window* window = mWindow; - s->addSaveFunc([ UImodeSelection, window] - { - std::string selectedMode = UImodeSelection->getSelected(); - if (selectedMode != "Full") - { - std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; - msg += "This will hide most menu-options to prevent changes to the system.\n"; - msg += "To unlock and return to the full UI, enter this code: \n"; - msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; - msg += "Do you want to proceed?"; - window->pushGui(new GuiMsgBox(window, msg, - "YES", [selectedMode] { - LOG(LogDebug) << "Setting UI mode to " << selectedMode; - Settings::getInstance()->setString("UIMode", selectedMode); - Settings::getInstance()->saveFile(); - }, "NO",nullptr)); - } - }); - */ - - // screensaver - ComponentListRow screensaver_row; - screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - screensaver_row.addElement(makeArrow(mWindow), false); - screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); - s->addRow(screensaver_row); - -#if defined(_WIN32) - // quick system select (left/right in game list view) - auto hideWindowScreen = std::make_shared(mWindow); - hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); - s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); - s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); -#endif - - // quick system select (left/right in game list view) - auto quick_sys_select = std::make_shared(mWindow); - quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); - s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); - s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); - - // carousel transition option - auto move_carousel = std::make_shared(mWindow); - move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); - s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); - s->addSaveFunc([move_carousel] { - if (move_carousel->getState() - && !Settings::getInstance()->getBool("MoveCarousel") - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); - }); - - // show help - auto show_help = std::make_shared(mWindow); - show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); - s->addWithLabel(_T("ON-SCREEN HELP"), show_help); - s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); - - // enable filters (ForceDisableFilters) - auto enable_filter = std::make_shared(mWindow); - enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); - s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); - s->addSaveFunc([enable_filter] { - bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); - Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); - if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); - }); - - mWindow->pushGui(s); - -} - -void GuiMenu::openOtherSettings() -{ - auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); - - // maximum vram - auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); - max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel(_T("VRAM LIMIT"), max_vram); - s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); - - // power saver - auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); - std::vector modes; - modes.push_back("disabled"); - modes.push_back("default"); - modes.push_back("enhanced"); - modes.push_back("instant"); - for (auto it = modes.cbegin(); it != modes.cend(); it++) - power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); - - s->addWithLabel(_T("POWER SAVER MODES"), power_saver); - s->addSaveFunc([this, power_saver] { - if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { - Settings::getInstance()->setString("TransitionStyle", "instant"); - Settings::getInstance()->setBool("MoveCarousel", false); - Settings::getInstance()->setBool("EnableSounds", false); - } - Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); - PowerSaver::init(); - }); - - // gamelists - auto save_gamelists = std::make_shared(mWindow); - save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); - s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); - s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); - - auto parse_gamelists = std::make_shared(mWindow); - parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); - s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); - s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); - /* - auto local_art = std::make_shared(mWindow); - local_art->setState(Settings::getInstance()->getBool("LocalArt")); - s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); - s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); - */ - // hidden files - auto hidden_files = std::make_shared(mWindow); - hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); - s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); - s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); - -#ifdef _RPI_ - // Video Player - VideoOmxPlayer - auto omx_player = std::make_shared(mWindow); - omx_player->setState(Settings::getInstance()->getBool("VideoOmxPlayer")); - s->addWithLabel("USE OMX PLAYER (HW ACCELERATED)", omx_player); - s->addSaveFunc([omx_player] - { - // need to reload all views to re-create the right video components - bool needReload = false; - if(Settings::getInstance()->getBool("VideoOmxPlayer") != omx_player->getState()) - needReload = true; - - Settings::getInstance()->setBool("VideoOmxPlayer", omx_player->getState()); - - if(needReload) - ViewController::get()->reloadAll(); - }); - -#endif - - // framerate - /* - auto framerate = std::make_shared(mWindow); - framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); - s->addWithLabel(_T("SHOW FRAMERATE"), framerate); - s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); - */ - - mWindow->pushGui(s); - -} - -void GuiMenu::openConfigInput() -{ - Window* window = mWindow; - window->pushGui(new GuiDetectDevice(window, false, nullptr)); - /* - window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), - [window] { - window->pushGui(new GuiDetectDevice(window, false, nullptr)); - }, _T("NO"), nullptr) - );*/ - -} - -void GuiMenu::openQuitMenu() -{ -#if defined(_WIN32) - Scripting::fireEvent("quit"); - quitES(""); - return; -#endif - - auto s = new GuiSettings(mWindow, _T("QUIT")); - - Window* window = mWindow; - - ComponentListRow row; - if (UIModeController::getInstance()->isUIModeFull()) - { - row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", - [] { - Scripting::fireEvent("quit"); - if(quitES("/tmp/es-restart") != 0) - LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", 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", - [] { - Scripting::fireEvent("quit"); - quitES(""); - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - s->addRow(row); - } - } - row.elements.clear(); - row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", - [] { - Scripting::fireEvent("quit", "reboot"); - Scripting::fireEvent("reboot"); - if (quitES("/tmp/es-sysrestart") != 0) - LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(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", - [] { - Scripting::fireEvent("quit", "shutdown"); - Scripting::fireEvent("shutdown"); - if (quitES("/tmp/es-shutdown") != 0) - LOG(LogWarning) << "Shutdown terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - s->addRow(row); - - mWindow->pushGui(s); -} - -void GuiMenu::addVersionInfo() -{ - std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); - - mVersion.setFont(Font::get(FONT_SIZE_SMALL)); - mVersion.setColor(0x5E5E5EFF); - mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); - mVersion.setHorizontalAlignment(ALIGN_CENTER); - addChild(&mVersion); -} - -void GuiMenu::openScreensaverOptions() { - mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); -} - -void GuiMenu::openCollectionSystemSettings() { - mWindow->pushGui(new GuiCollectionSystemsOptions(mWindow)); -} - -void GuiMenu::onSizeChanged() -{ - mVersion.setSize(mSize.x(), 0); - mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); -} - -void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) -{ - std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); - - // populate the list - ComponentListRow row; - row.addElement(std::make_shared(mWindow, name, font, color), true); - - if(add_arrow) - { - std::shared_ptr 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; -} - -HelpStyle GuiMenu::getHelpStyle() -{ - HelpStyle style = HelpStyle(); - style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); - return style; -} - -std::vector GuiMenu::getHelpPrompts() -{ - std::vector prompts; - prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); - prompts.push_back(HelpPrompt("a", _T("SELECT"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); - return prompts; -} + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + }); + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); +} + +void GuiMenu::openScraperSettings() +{ + auto s = new GuiSettings(mWindow, _T("SCRAPER")); + + // scrape from + auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, _T("SCRAPE FROM"), false); + std::vector scrapers = getScraperList(); + + // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. + for(auto it = scrapers.cbegin(); it != scrapers.cend(); it++) + scraper_list->add(*it, *it, *it == Settings::getInstance()->getString("Scraper")); + + s->addWithLabel(_T("SCRAPE FROM"), scraper_list); + s->addSaveFunc([scraper_list] { Settings::getInstance()->setString("Scraper", scraper_list->getSelected()); }); + + // scrape ratings + auto scrape_ratings = std::make_shared(mWindow); + scrape_ratings->setState(Settings::getInstance()->getBool("ScrapeRatings")); + s->addWithLabel(_T("SCRAPE RATINGS"), scrape_ratings); + s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); }); + + // scrape now + ComponentListRow row; + auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; + std::function openAndSave = openScrapeNow; + openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; + row.makeAcceptInputHandler(openAndSave); + + auto scrape_now = std::make_shared(mWindow, _T("SCRAPE NOW"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); + auto bracket = makeArrow(mWindow); + row.addElement(scrape_now, true); + row.addElement(bracket, false); + s->addRow(row); + + s->updatePosition(); + mWindow->pushGui(s); +} + +void GuiMenu::openSoundSettings() +{ + auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); + + // volume + auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); + volume->setValue((float)VolumeControl::getInstance()->getVolume()); + s->addWithLabel(_T("SYSTEM VOLUME"), volume); + s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); + + if (UIModeController::getInstance()->isUIModeFull()) + { +#if defined(__linux__) + // audio card + auto audio_card = std::make_shared< OptionListComponent >(mWindow, "AUDIO CARD", false); + std::vector audio_cards; + #ifdef _RPI_ + // RPi Specific Audio Cards + audio_cards.push_back("local"); + audio_cards.push_back("hdmi"); + audio_cards.push_back("both"); + #endif + audio_cards.push_back("default"); + audio_cards.push_back("sysdefault"); + audio_cards.push_back("dmix"); + audio_cards.push_back("hw"); + audio_cards.push_back("plughw"); + audio_cards.push_back("null"); + if (Settings::getInstance()->getString("AudioCard") != "") { + if(std::find(audio_cards.begin(), audio_cards.end(), Settings::getInstance()->getString("AudioCard")) == audio_cards.end()) { + audio_cards.push_back(Settings::getInstance()->getString("AudioCard")); + } + } + for(auto ac = audio_cards.cbegin(); ac != audio_cards.cend(); ac++) + audio_card->add(*ac, *ac, Settings::getInstance()->getString("AudioCard") == *ac); + s->addWithLabel("AUDIO CARD", audio_card); + s->addSaveFunc([audio_card] { + Settings::getInstance()->setString("AudioCard", audio_card->getSelected()); + VolumeControl::getInstance()->deinit(); + VolumeControl::getInstance()->init(); + }); + + // volume control device + auto vol_dev = std::make_shared< OptionListComponent >(mWindow, "AUDIO DEVICE", false); + std::vector transitions; + transitions.push_back("PCM"); + transitions.push_back("Speaker"); + transitions.push_back("Master"); + transitions.push_back("Digital"); + transitions.push_back("Analogue"); + if (Settings::getInstance()->getString("AudioDevice") != "") { + if(std::find(transitions.begin(), transitions.end(), Settings::getInstance()->getString("AudioDevice")) == transitions.end()) { + transitions.push_back(Settings::getInstance()->getString("AudioDevice")); + } + } + for(auto it = transitions.cbegin(); it != transitions.cend(); it++) + vol_dev->add(*it, *it, Settings::getInstance()->getString("AudioDevice") == *it); + s->addWithLabel("AUDIO DEVICE", vol_dev); + s->addSaveFunc([vol_dev] { + Settings::getInstance()->setString("AudioDevice", vol_dev->getSelected()); + VolumeControl::getInstance()->deinit(); + VolumeControl::getInstance()->init(); + }); +#endif + + // disable sounds + auto sounds_enabled = std::make_shared(mWindow); + sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); + s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); + s->addSaveFunc([sounds_enabled] { + if (sounds_enabled->getState() + && !Settings::getInstance()->getBool("EnableSounds") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState()); + }); + + auto video_audio = std::make_shared(mWindow); + video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); + s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); + s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); + +#ifdef _RPI_ + // OMX player Audio Device + auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, "OMX PLAYER AUDIO DEVICE", false); + std::vector omx_cards; + // RPi Specific Audio Cards + omx_cards.push_back("local"); + omx_cards.push_back("hdmi"); + omx_cards.push_back("both"); + omx_cards.push_back("alsa:hw:0,0"); + omx_cards.push_back("alsa:hw:1,0"); + if (Settings::getInstance()->getString("OMXAudioDev") != "") { + if (std::find(omx_cards.begin(), omx_cards.end(), Settings::getInstance()->getString("OMXAudioDev")) == omx_cards.end()) { + omx_cards.push_back(Settings::getInstance()->getString("OMXAudioDev")); + } + } + for (auto it = omx_cards.cbegin(); it != omx_cards.cend(); it++) + omx_audio_dev->add(*it, *it, Settings::getInstance()->getString("OMXAudioDev") == *it); + s->addWithLabel("OMX PLAYER AUDIO DEVICE", omx_audio_dev); + s->addSaveFunc([omx_audio_dev] { + if (Settings::getInstance()->getString("OMXAudioDev") != omx_audio_dev->getSelected()) + Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); + }); +#endif + } + + s->updatePosition(); + mWindow->pushGui(s); + +} + +void GuiMenu::openUISettings() +{ + auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); + + // theme set + auto theme = ThemeData::getMenuTheme(); + auto themeSets = ThemeData::getThemeSets(); + + if (!themeSets.empty()) + { + std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if (selectedSet == themeSets.cend()) + selectedSet = themeSets.cbegin(); + + auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); + for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) + theme_set->add(it->first, it->first, it == selectedSet); + s->addWithLabel(_T("THEME"), theme_set); + + Window* window = mWindow; + s->addSaveFunc([window, theme_set] + { + bool needReload = false; + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + if (oldTheme != theme_set->getSelected()) + needReload = true; + + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); + + if (needReload) + { + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + } + }); + } + + // LANGUAGE + + std::vector langues; + langues.push_back("en"); + + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); + if (xmlpath.length() > 0) + { + xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if (Utils::FileSystem::isDirectory(*it)) + continue; + + std::string name = *it; + + if (name.rfind("emulationstation2.po") == std::string::npos) + continue; + + name = Utils::FileSystem::getParent(name); + name = Utils::FileSystem::getFileName(name); + + if (name != "en") + langues.push_back(name); + } + + if (langues.size() > 1) + { + auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); + + for (auto it = langues.cbegin(); it != langues.cend(); it++) + language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); + + s->addWithLabel(_T("LANGUAGE"), language); + s->addSaveFunc([language] { + Settings::getInstance()->setString("Language", language->getSelected()); + }); + } + } + + // transition style + auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); + std::vector transitions; + transitions.push_back("fade"); + transitions.push_back("slide"); + transitions.push_back("instant"); + for (auto it = transitions.cbegin(); it != transitions.cend(); it++) + transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); + + s->addWithLabel(_T("TRANSITION STYLE"), transition_style); + s->addSaveFunc([transition_style] { + if (Settings::getInstance()->getString("TransitionStyle") == "instant" + && transition_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); + }); + + + auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); + std::vector gameTransitions; + gameTransitions.push_back("fade"); + gameTransitions.push_back("slide"); + gameTransitions.push_back("instant"); + for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) + transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); + + s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); + s->addSaveFunc([transitionOfGames_style] { + if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" + && transitionOfGames_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); + }); + + // GameList view style + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); + std::vector styles; + styles.push_back("automatic"); + styles.push_back("basic"); + styles.push_back("detailed"); + styles.push_back("video"); + styles.push_back("grid"); + styles.push_back("gridex"); + + for (auto it = styles.cbegin(); it != styles.cend(); it++) + gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); + + s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); + s->addSaveFunc([gamelist_style] { + bool needReload = false; + if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) + needReload = true; + Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); + if (needReload) + ViewController::get()->reloadAll(); + }); + + + // Optionally start in selected system + auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); + systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + { + if ("retropie" != (*it)->getName()) + { + systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); + } + } + s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); + s->addSaveFunc([systemfocus_list] { + Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); + }); + + + /* + //UI mode + auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); + std::vector UImodes = UIModeController::getInstance()->getUIModes(); + for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) + UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); + s->addWithLabel("UI MODE", UImodeSelection); + Window* window = mWindow; + s->addSaveFunc([ UImodeSelection, window] + { + std::string selectedMode = UImodeSelection->getSelected(); + if (selectedMode != "Full") + { + std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; + msg += "This will hide most menu-options to prevent changes to the system.\n"; + msg += "To unlock and return to the full UI, enter this code: \n"; + msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; + msg += "Do you want to proceed?"; + window->pushGui(new GuiMsgBox(window, msg, + "YES", [selectedMode] { + LOG(LogDebug) << "Setting UI mode to " << selectedMode; + Settings::getInstance()->setString("UIMode", selectedMode); + Settings::getInstance()->saveFile(); + }, "NO",nullptr)); + } + }); + */ + + // screensaver + ComponentListRow screensaver_row; + screensaver_row.elements.clear(); + screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); + screensaver_row.addElement(makeArrow(mWindow), false); + screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); + s->addRow(screensaver_row); + +#if defined(_WIN32) + // quick system select (left/right in game list view) + auto hideWindowScreen = std::make_shared(mWindow); + hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); + s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); + s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); +#endif + + // quick system select (left/right in game list view) + auto quick_sys_select = std::make_shared(mWindow); + quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); + s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); + s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); + + // carousel transition option + auto move_carousel = std::make_shared(mWindow); + move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); + s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); + s->addSaveFunc([move_carousel] { + if (move_carousel->getState() + && !Settings::getInstance()->getBool("MoveCarousel") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); + }); + + // show help + auto show_help = std::make_shared(mWindow); + show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); + s->addWithLabel(_T("ON-SCREEN HELP"), show_help); + s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); + + // enable filters (ForceDisableFilters) + auto enable_filter = std::make_shared(mWindow); + enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); + s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); + s->addSaveFunc([enable_filter] { + bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); + Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); + if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); + }); + + s->updatePosition(); + mWindow->pushGui(s); + +} + +void GuiMenu::openOtherSettings() +{ + auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); + + // maximum vram + auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); + max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); + s->addWithLabel(_T("VRAM LIMIT"), max_vram); + s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); + + // power saver + auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); + std::vector modes; + modes.push_back("disabled"); + modes.push_back("default"); + modes.push_back("enhanced"); + modes.push_back("instant"); + for (auto it = modes.cbegin(); it != modes.cend(); it++) + power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); + + s->addWithLabel(_T("POWER SAVER MODES"), power_saver); + s->addSaveFunc([this, power_saver] { + if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { + Settings::getInstance()->setString("TransitionStyle", "instant"); + Settings::getInstance()->setBool("MoveCarousel", false); + Settings::getInstance()->setBool("EnableSounds", false); + } + Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); + PowerSaver::init(); + }); + + // gamelists + auto save_gamelists = std::make_shared(mWindow); + save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); + s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); + s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); + + auto parse_gamelists = std::make_shared(mWindow); + parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); + s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); + s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); + /* + auto local_art = std::make_shared(mWindow); + local_art->setState(Settings::getInstance()->getBool("LocalArt")); + s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); + s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); + */ + // hidden files + auto hidden_files = std::make_shared(mWindow); + hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); + s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); + s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); + +#ifdef _RPI_ + // Video Player - VideoOmxPlayer + auto omx_player = std::make_shared(mWindow); + omx_player->setState(Settings::getInstance()->getBool("VideoOmxPlayer")); + s->addWithLabel("USE OMX PLAYER (HW ACCELERATED)", omx_player); + s->addSaveFunc([omx_player] + { + // need to reload all views to re-create the right video components + bool needReload = false; + if(Settings::getInstance()->getBool("VideoOmxPlayer") != omx_player->getState()) + needReload = true; + + Settings::getInstance()->setBool("VideoOmxPlayer", omx_player->getState()); + + if(needReload) + ViewController::get()->reloadAll(); + }); + +#endif + +#if defined(_WIN32) + // vsync + auto vsync = std::make_shared(mWindow); + vsync->setState(Settings::getInstance()->getBool("VSync")); + s->addWithLabel(_T("VSYNC"), vsync); + s->addSaveFunc([vsync] + { + Settings::getInstance()->setBool("VSync", vsync->getState()); + + // vsync + if (Settings::getInstance()->getBool("VSync")) + { + // SDL_GL_SetSwapInterval(0) for immediate updates (no vsync, default), + // 1 for updates synchronized with the vertical retrace, + // or -1 for late swap tearing. + // SDL_GL_SetSwapInterval returns 0 on success, -1 on error. + // if vsync is requested, try normal vsync; if that doesn't work, try late swap tearing + // if that doesn't work, report an error + if (SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) + LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; + } + else + SDL_GL_SetSwapInterval(0); + }); +#endif + + // framerate + + auto framerate = std::make_shared(mWindow); + framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); + s->addWithLabel(_T("SHOW FRAMERATE"), framerate); + s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); + + + s->updatePosition(); + mWindow->pushGui(s); + +} + +void GuiMenu::openConfigInput() +{ + Window* window = mWindow; + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + /* + window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), + [window] { + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + }, _T("NO"), nullptr) + );*/ + +} + +void GuiMenu::openQuitMenu() +{ +#if defined(_WIN32) + Scripting::fireEvent("quit"); + quitES(""); + return; +#endif + + auto s = new GuiSettings(mWindow, _T("QUIT")); + + Window* window = mWindow; + + ComponentListRow row; + if (UIModeController::getInstance()->isUIModeFull()) + { + row.makeAcceptInputHandler([window] { + window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + [] { + Scripting::fireEvent("quit"); + if(quitES("/tmp/es-restart") != 0) + LOG(LogWarning) << "Restart terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", 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", + [] { + Scripting::fireEvent("quit"); + quitES(""); + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + s->addRow(row); + } + } + row.elements.clear(); + row.makeAcceptInputHandler([window] { + window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + [] { + Scripting::fireEvent("quit", "reboot"); + Scripting::fireEvent("reboot"); + if (quitES("/tmp/es-sysrestart") != 0) + LOG(LogWarning) << "Restart terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(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", + [] { + Scripting::fireEvent("quit", "shutdown"); + Scripting::fireEvent("shutdown"); + if (quitES("/tmp/es-shutdown") != 0) + LOG(LogWarning) << "Shutdown terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + s->addRow(row); + + s->updatePosition(); + mWindow->pushGui(s); +} + +void GuiMenu::addVersionInfo() +{ + std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); + + auto theme = ThemeData::getMenuTheme(); +// mVersion.setFont(Font::get(FONT_SIZE_SMALL)); +// mVersion.setColor(0x5E5E5EFF); + + mVersion.setFont(theme->Footer.font); + mVersion.setColor(theme->Footer.color); + + mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); + mVersion.setHorizontalAlignment(ALIGN_CENTER); + addChild(&mVersion); +} + +void GuiMenu::openScreensaverOptions() { + mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); +} + +void GuiMenu::openCollectionSystemSettings() { + mWindow->pushGui(new GuiCollectionSystemsOptions(mWindow)); +} + +void GuiMenu::onSizeChanged() +{ + mVersion.setSize(mSize.x(), 0); + mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); +} + +void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) +{ + auto theme = ThemeData::getMenuTheme(); + std::shared_ptr font = theme->Text.font; + color = theme->Text.color; + + // populate the list + ComponentListRow row; + row.addElement(std::make_shared(mWindow, name, font, color), true); + + if (add_arrow) + { + std::shared_ptr 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; +} + +HelpStyle GuiMenu::getHelpStyle() +{ + HelpStyle style = HelpStyle(); + style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); + return style; +} + +std::vector GuiMenu::getHelpPrompts() +{ + std::vector prompts; + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("SELECT"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + return prompts; +} diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 31835bfa1b..ee9abc7a1f 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -1,382 +1,385 @@ -#include "guis/GuiMetaDataEd.h" - -#include "components/ButtonComponent.h" -#include "components/ComponentList.h" -#include "components/DateTimeEditComponent.h" -#include "components/MenuComponent.h" -#include "components/RatingComponent.h" -#include "components/SwitchComponent.h" -#include "components/TextComponent.h" -#include "guis/GuiGameScraper.h" -#include "guis/GuiMsgBox.h" -#include "guis/GuiTextEditPopup.h" -#include "resources/Font.h" -#include "utils/StringUtil.h" -#include "views/ViewController.h" -#include "CollectionSystemManager.h" -#include "FileData.h" -#include "FileFilterIndex.h" -#include "SystemData.h" -#include "Window.h" - -GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, - const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : 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(mWindow, Vector2i(1, 5)); - - mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); - mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), - 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(mWindow); - mGrid.setEntry(mList, Vector2i(0, 1), true, true); - - SystemData* system = file->getSystem(); - - auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); - auto core_choice = std::make_shared>(mWindow, _T("CORE"), false); - - // populate list - for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) - { - std::shared_ptr ed; - - // don't add statistics - if(iter->isStatistic) - continue; - - if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || - iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || - iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") - continue; - - // create ed and add it (and any related components) to mMenu - // ed's value will be set below - ComponentListRow row; - - if (iter->displayName == "emulator") - { - // if (file->getSystemEnvData()->mEmulators.size() <= 1) - // continue; - - std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); - - std::string currentEmul = file->getEmulator(); - - if (defaultEmul.length() == 0) - emul_choice->add(_T("DEFAULT"), "", false); - else - emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); - - for (auto core : file->getSystemEnvData()->mEmulators) - emul_choice->add(core.mName, core.mName, core.mName == currentEmul); - - row.addElement(std::make_shared(mWindow, _T("EMULATOR"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(emul_choice, false); - - mList->addRow(row); - emul_choice->setTag(iter->key); - mEditors.push_back(emul_choice); - - emul_choice->setSelectedChangedCallback([this, system, core_choice, file](std::string emulatorName) - { - std::string currentCore = file->getCore(); - - std::string defaultCore = system->getSystemEnvData()->getDefaultCore(emulatorName); - if (emulatorName.length() == 0) - defaultCore = system->getSystemEnvData()->getDefaultCore(system->getSystemEnvData()->getDefaultEmulator()); - - core_choice->clear(); - if (defaultCore.length() == 0) - core_choice->add(_T("DEFAULT"), "", false); - else - core_choice->add(_T("DEFAULT")+" ("+ defaultCore+")", "", false); - - std::vector cores = system->getSystemEnvData()->getCores(emulatorName); - - bool found = false; - - for (auto it = cores.begin(); it != cores.end(); it++) - { - std::string core = *it; - core_choice->add(core, core, currentCore == core); - if (currentCore == core) - found = true; - } - - if (!found) - core_choice->selectFirstItem(); - else - core_choice->invalidate(); - }); - - continue; - } - - if (iter->displayName == "core") - { - // core_choice->add(_T("DEFAULT"), "", true); - core_choice->setTag(iter->key); - - row.addElement(std::make_shared(mWindow, "CORE", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(core_choice, false); - - mList->addRow(row); - ed = core_choice; - - mEditors.push_back(core_choice); - - // force change event to load core list - emul_choice->invalidate(); - continue; - } - - auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); - row.addElement(lbl, true); // label - - switch (iter->type) - { - case MD_BOOL: - { - ed = std::make_shared(window); - row.addElement(ed, false, true); - break; - } - case MD_RATING: - { - ed = std::make_shared(window); - const float height = lbl->getSize().y() * 0.71f; - ed->setSize(0, height); - row.addElement(ed, false, true); - - auto spacer = std::make_shared(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(window); - row.addElement(ed, false); - - auto spacer = std::make_shared(mWindow); - spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0); - row.addElement(spacer, false); - - // pass input to the actual DateTimeEditComponent 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(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); - row.addElement(ed, false); - break; - } - case MD_MULTILINE_STRING: - default: - { - // MD_STRING - ed = std::make_shared(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); - row.addElement(ed, true); - - auto spacer = std::make_shared(mWindow); - spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); - row.addElement(spacer, false); - - auto bracket = std::make_shared(mWindow); - bracket->setImage(":/arrow.svg"); - bracket->setResize(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->setTag(iter->key); - ed->setValue(mMetaData->get(iter->key)); - - mEditors.push_back(ed); - } - - std::vector< std::shared_ptr > buttons; - - if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) - buttons.push_back(std::make_shared(mWindow, _T("SCRAPE"), _T("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); - - buttons.push_back(std::make_shared(mWindow, _T("SAVE"), _T("SAVE"), [&] { save(); delete this; })); - buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); - - if(mDeleteFunc) - { - auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; - auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; - buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE"), deleteBtnFunc)); - } - - mButtons = makeButtonGrid(mWindow, buttons); - mGrid.setEntry(mButtons, Vector2i(0, 2), true, false); - - // resize + center - float width = (float)Math::min(Renderer::getScreenHeight(), (int)(Renderer::getScreenWidth() * 0.90f)); - setSize(width, 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() -{ - // remove game from index - mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); - - for (unsigned int i = 0; i < mEditors.size(); i++) - { - std::shared_ptr ed = mEditors.at(i); - - auto val = ed->getValue(); - auto key = ed->getTag(); - - if (key == "core" || key == "emulator") - { - std::shared_ptr> list = std::static_pointer_cast>(ed); - val = list->getSelected(); - } - - mMetaData->set(key, val); - } - - // enter game in index - mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); - - if (mSavedCallback) - mSavedCallback(); - - // update respective Collection Entries - CollectionSystemManager::get()->refreshCollectionSystems(mScraperParams.game); -} - -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++) - { - auto val = mEditors.at(i)->getValue(); - auto key = mEditors.at(i)->getTag(); - 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++) - { - auto key = mEditors.at(i)->getTag(); - if(mMetaData->get(key) != mEditors.at(i)->getValue()) - { - dirty = true; - break; - } - } - - std::function 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, - "ENREGISTRER LES MODIFICATIONS ?", - "OUI", [this, closeFunc] { save(); closeFunc(); }, - "NON", 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 GuiMetaDataEd::getHelpPrompts() -{ - std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); - return prompts; -} +#include "guis/GuiMetaDataEd.h" + +#include "components/ButtonComponent.h" +#include "components/ComponentList.h" +#include "components/DateTimeEditComponent.h" +#include "components/MenuComponent.h" +#include "components/RatingComponent.h" +#include "components/SwitchComponent.h" +#include "components/TextComponent.h" +#include "guis/GuiGameScraper.h" +#include "guis/GuiMsgBox.h" +#include "guis/GuiTextEditPopup.h" +#include "resources/Font.h" +#include "utils/StringUtil.h" +#include "views/ViewController.h" +#include "CollectionSystemManager.h" +#include "FileData.h" +#include "FileFilterIndex.h" +#include "SystemData.h" +#include "Window.h" + +GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, + const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : GuiComponent(window), + mScraperParams(scraperParams), + + mBackground(window, ":/frame.png"), + mGrid(window, Vector2i(1, 3)), + + mMetaDataDecl(mdd), + mMetaData(md), + mSavedCallback(saveCallback), mDeleteFunc(deleteFunc) +{ + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + + addChild(&mBackground); + addChild(&mGrid); + + mHeaderGrid = std::make_shared(mWindow, Vector2i(1, 5)); + + mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), theme->Title.font, theme->Title.color, ALIGN_CENTER); + mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), + theme->TextSmall.font, theme->TextSmall.color, 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(mWindow); + mGrid.setEntry(mList, Vector2i(0, 1), true, true); + + SystemData* system = file->getSystem(); + + auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); + auto core_choice = std::make_shared>(mWindow, _T("CORE"), false); + + // populate list + for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + { + std::shared_ptr ed; + + // don't add statistics + if(iter->isStatistic) + continue; + +#if defined(_WIN32) + if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || + iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || + iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") + continue; +#endif + + // create ed and add it (and any related components) to mMenu + // ed's value will be set below + ComponentListRow row; + + if (iter->displayName == "emulator") + { + std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); + std::string currentEmul = file->getEmulator(); + + if (defaultEmul.length() == 0) + emul_choice->add(_T("DEFAULT"), "", true); + else + emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); + + for (auto core : file->getSystemEnvData()->mEmulators) + emul_choice->add(core.mName, core.mName, core.mName == currentEmul); + + row.addElement(std::make_shared(mWindow, _T("EMULATOR"), theme->Text.font, theme->Text.color), true); + row.addElement(emul_choice, false); + + mList->addRow(row); + emul_choice->setTag(iter->key); + mEditors.push_back(emul_choice); + + emul_choice->setSelectedChangedCallback([this, system, core_choice, file](std::string emulatorName) + { + std::string currentCore = file->getCore(); + + std::string defaultCore = system->getSystemEnvData()->getDefaultCore(emulatorName); + if (emulatorName.length() == 0) + defaultCore = system->getSystemEnvData()->getDefaultCore(system->getSystemEnvData()->getDefaultEmulator()); + + core_choice->clear(); + if (defaultCore.length() == 0) + core_choice->add(_T("DEFAULT"), "", false); + else + core_choice->add(_T("DEFAULT")+" ("+ defaultCore+")", "", false); + + std::vector cores = system->getSystemEnvData()->getCores(emulatorName); + + bool found = false; + + for (auto it = cores.begin(); it != cores.end(); it++) + { + std::string core = *it; + core_choice->add(core, core, currentCore == core); + if (currentCore == core) + found = true; + } + + if (!found) + core_choice->selectFirstItem(); + else + core_choice->invalidate(); + }); + + continue; + } + + if (iter->displayName == "core") + { + // core_choice->add(_T("DEFAULT"), "", true); + core_choice->setTag(iter->key); + + row.addElement(std::make_shared(mWindow, "CORE", theme->Text.font, theme->Text.color), true); + row.addElement(core_choice, false); + + mList->addRow(row); + ed = core_choice; + + mEditors.push_back(core_choice); + + // force change event to load core list + emul_choice->invalidate(); + continue; + } + + auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), theme->Text.font, theme->Text.color); + row.addElement(lbl, true); // label + + switch (iter->type) + { + case MD_BOOL: + { + ed = std::make_shared(window); + row.addElement(ed, false, true); + break; + } + case MD_RATING: + { + ed = std::make_shared(window); + const float height = lbl->getSize().y() * 0.71f; + ed->setSize(0, height); + row.addElement(ed, false, true); + + auto spacer = std::make_shared(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(window); + row.addElement(ed, false); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0); + row.addElement(spacer, false); + + // pass input to the actual DateTimeEditComponent 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(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); + row.addElement(ed, false); + break; + } + case MD_MULTILINE_STRING: + default: + { + // MD_STRING + ed = std::make_shared(window, "", theme->Text.font, theme->Text.color, ALIGN_RIGHT); + row.addElement(ed, true); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); + row.addElement(spacer, false); + + auto bracket = std::make_shared(mWindow); + bracket->setImage(":/arrow.svg"); + bracket->setResize(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->setTag(iter->key); + ed->setValue(mMetaData->get(iter->key)); + + mEditors.push_back(ed); + } + + std::vector< std::shared_ptr > buttons; + + if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) + buttons.push_back(std::make_shared(mWindow, _T("SCRAPE"), _T("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); + + buttons.push_back(std::make_shared(mWindow, _T("SAVE"), _T("SAVE"), [&] { save(); delete this; })); + buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); + + if(mDeleteFunc) + { + auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; + auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; + buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE"), deleteBtnFunc)); + } + + mButtons = makeButtonGrid(mWindow, buttons); + mGrid.setEntry(mButtons, Vector2i(0, 2), true, false); + + // resize + center + float width = (float)Math::min(Renderer::getScreenHeight(), (int)(Renderer::getScreenWidth() * 0.90f)); + setSize(width, 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() +{ + // remove game from index + mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); + + for (unsigned int i = 0; i < mEditors.size(); i++) + { + std::shared_ptr ed = mEditors.at(i); + + auto val = ed->getValue(); + auto key = ed->getTag(); + + if (key == "core" || key == "emulator") + { + std::shared_ptr> list = std::static_pointer_cast>(ed); + val = list->getSelected(); + } + + mMetaData->set(key, val); + } + + // enter game in index + mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); + + if (mSavedCallback) + mSavedCallback(); + + // update respective Collection Entries + CollectionSystemManager::get()->refreshCollectionSystems(mScraperParams.game); +} + +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++) + { + auto val = mEditors.at(i)->getValue(); + auto key = mEditors.at(i)->getTag(); + 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++) + { + auto key = mEditors.at(i)->getTag(); + if(mMetaData->get(key) != mEditors.at(i)->getValue()) + { + dirty = true; + break; + } + } + + std::function 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, + "ENREGISTRER LES MODIFICATIONS ?", + "OUI", [this, closeFunc] { save(); closeFunc(); }, + "NON", 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 GuiMetaDataEd::getHelpPrompts() +{ + std::vector prompts = mGrid.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + return prompts; +} diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 7dcd2d16bd..210cac7ae6 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -15,6 +15,11 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queueBackground.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + assert(mSearchQueue.size()); addChild(&mBackground); diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index 2330f095f6..6762fea19a 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -9,32 +9,32 @@ #include "SystemData.h" GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), - mMenu(window, "SCRAPE NOW") + mMenu(window, _T("SCRAPE NOW")) { addChild(&mMenu); // add filters (with first one selected) - mFilters = std::make_shared< OptionListComponent >(mWindow, "SCRAPE THESE GAMES", false); - mFilters->add("All Games", + mFilters = std::make_shared< OptionListComponent >(mWindow, _T("SCRAPE THESE GAMES"), false); + mFilters->add(_T("All Games"), [](SystemData*, FileData*) -> bool { return true; }, false); - mFilters->add("Only missing image", + mFilters->add(_T("Only missing image"), [](SystemData*, FileData* g) -> bool { return g->metadata.get("image").empty(); }, true); - mMenu.addWithLabel("Filter", mFilters); + mMenu.addWithLabel(_T("FILTER"), mFilters); //add systems (all with a platformid specified selected) - mSystems = std::make_shared< OptionListComponent >(mWindow, "SCRAPE THESE SYSTEMS", true); + mSystems = std::make_shared< OptionListComponent >(mWindow, _T("SCRAPE THESE SYSTEMS"), true); for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { if(!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) mSystems->add((*it)->getFullName(), *it, !(*it)->getPlatformIds().empty()); } - mMenu.addWithLabel("Systems", mSystems); + mMenu.addWithLabel(_T("SYSTEMS"), mSystems); mApproveResults = std::make_shared(mWindow); mApproveResults->setState(true); - mMenu.addWithLabel("User decides on conflicts", mApproveResults); + mMenu.addWithLabel(_T("USER DECIDES ON CONFLICTS"), mApproveResults); - mMenu.addButton("START", "LANCER", std::bind(&GuiScraperStart::pressedStart, this)); + mMenu.addButton(_T("START"), _T("START"), std::bind(&GuiScraperStart::pressedStart, this)); mMenu.addButton(_T("BACK"), _T("BACK"), [&] { delete this; }); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); @@ -49,8 +49,8 @@ void GuiScraperStart::pressedStart() { mWindow->pushGui(new GuiMsgBox(mWindow, Utils::String::toUpper("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)); + _T("YES"), std::bind(&GuiScraperStart::start, this), + _T("NO"), nullptr)); return; } } @@ -64,9 +64,10 @@ void GuiScraperStart::start() if(searches.empty()) { - mWindow->pushGui(new GuiMsgBox(mWindow, - "NO GAMES FIT THAT CRITERIA.")); - }else{ + mWindow->pushGui(new GuiMsgBox(mWindow, _T("NO GAMES FIT THAT CRITERIA."))); + } + else + { GuiScraperMulti* gsm = new GuiScraperMulti(mWindow, searches, mApproveResults->getState()); mWindow->pushGui(gsm); delete this; diff --git a/es-app/src/guis/GuiSettings.cpp b/es-app/src/guis/GuiSettings.cpp index f0d3dd697a..0ffc6f65af 100644 --- a/es-app/src/guis/GuiSettings.cpp +++ b/es-app/src/guis/GuiSettings.cpp @@ -8,11 +8,8 @@ GuiSettings::GuiSettings(Window* window, std::string title) : GuiComponent(window), mMenu(window, title) { addChild(&mMenu); - mMenu.addButton(_T("BACK"), _T("BACK"), [this] { delete this; }); - - setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); + updatePosition(); } GuiSettings::~GuiSettings() @@ -20,6 +17,12 @@ GuiSettings::~GuiSettings() save(); } +void GuiSettings::updatePosition() +{ + setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); +} + void GuiSettings::save() { if(!mSaveFuncs.size()) diff --git a/es-app/src/guis/GuiSettings.h b/es-app/src/guis/GuiSettings.h index 05ece518f2..ac9a22d084 100644 --- a/es-app/src/guis/GuiSettings.h +++ b/es-app/src/guis/GuiSettings.h @@ -11,6 +11,8 @@ class GuiSettings : public GuiComponent GuiSettings(Window* window, std::string title); virtual ~GuiSettings(); // just calls save(); + + void updatePosition(); void save(); inline void addRow(const ComponentListRow& row) { mMenu.addRow(row); }; inline void addWithLabel(const std::string& label, const std::shared_ptr& comp) { mMenu.addWithLabel(label, comp); }; diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 2a08c2c583..2f42e183f5 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -133,9 +133,23 @@ bool parseArgs(int argc, char* argv[]) } else if (strcmp(argv[i], "--vsync") == 0 || strcmp(argv[i], "-vsync") == 0) { - bool vsync = strlen(argv[i]) <= 7 ? true : (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "1") == 0) ? true : false; + bool vsync = false; + if (i == argc - 1) + vsync = true; + else + { + std::string arg = argv[i + 1]; + if (arg.find("-") == 0) + vsync = true; + else + { + vsync = (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "1") == 0) ? true : false; + i++; // skip vsync value + } + } + Settings::getInstance()->setBool("VSync", vsync); - i++; // skip vsync value + } else if (strcmp(argv[i], "--scrape") == 0) { @@ -390,6 +404,22 @@ int main(int argc, char* argv[]) window.endRenderLoadingScreen(); +#ifdef WIN32 + DWORD displayFrequency = 60; + + DEVMODE lpDevMode; + memset(&lpDevMode, 0, sizeof(DEVMODE)); + lpDevMode.dmSize = sizeof(DEVMODE); + lpDevMode.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFLAGS | DM_DISPLAYFREQUENCY; + lpDevMode.dmDriverExtra = 0; + + if (EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &lpDevMode) != 0) { + displayFrequency = lpDevMode.dmDisplayFrequency; // default value if cannot retrieve from user settings. + } + + int timeLimit = (1000 / displayFrequency) - 5; // Margin for vsync +#endif + int lastTime = SDL_GetTicks(); int ps_time = SDL_GetTicks(); @@ -397,10 +427,12 @@ int main(int argc, char* argv[]) while(running) { + int processStart = SDL_GetTicks(); + SDL_Event event; bool ps_standby = PowerSaver::getState() && (int) SDL_GetTicks() - ps_time > PowerSaver::getMode(); - if(ps_standby ? SDL_WaitEventTimeout(&event, PowerSaver::getTimeout()) : SDL_PollEvent(&event)) + if (ps_standby ? SDL_WaitEventTimeout(&event, PowerSaver::getTimeout()) : SDL_PollEvent(&event)) { do { @@ -423,13 +455,13 @@ int main(int argc, char* argv[]) { // If exitting SDL_WaitEventTimeout due to timeout. Trail considering // timeout as an event - ps_time = SDL_GetTicks(); + ps_time = SDL_GetTicks(); } - if(window.isSleeping()) + 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 + SDL_Delay(10); // this doesn't need to be accurate, we're just giving up our CPU time until something wakes us up continue; } @@ -438,14 +470,33 @@ int main(int argc, char* argv[]) lastTime = curTime; // cap deltaTime if it ever goes negative - if(deltaTime < 0) + if (deltaTime < 0) deltaTime = 1000; window.update(deltaTime); window.render(); - Renderer::swapBuffers(); - + Log::flush(); + + int processDuration = SDL_GetTicks() - processStart; + +#ifdef WIN32 + if (processDuration < timeLimit) + Sleep(timeLimit - processDuration); + + int swapStart = SDL_GetTicks(); +#endif + + Renderer::swapBuffers(); +/* +#ifdef WIN32 + int swapDuration = SDL_GetTicks() - swapStart; + + char buffer[100]; + sprintf_s(buffer, "px=%d swap=%d, sleep=%d\n", processDuration, swapDuration, timeLimit - processDuration); + OutputDebugStringA(buffer); +#endif +*/ } while(window.peekGui() != ViewController::get()) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 41bc9fddfe..805b4f8cd2 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -234,7 +234,6 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) 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); @@ -270,8 +269,12 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) mSystemInfo.setOpacity((unsigned char)(Math::lerp(0.f, 1.f, t) * 255)); }, goFast ? 10 : 300); +// ViewController::get()->getGameListView(mEntries.at(mCursor).object); // fake preload + // wait 600ms to fade in - setAnimation(infoFadeIn, goFast ? 0 : 500, nullptr, false, 2); + setAnimation(infoFadeIn, goFast ? 0 : 500, [this] { + ViewController::get()->getGameListView(mEntries.at(mCursor).object); // fake preload + }, false, 2); // no need to animate transition, we're not going anywhere (probably mEntries.size() == 1) if(endPos == mCamOffset && endPos == mExtrasCamOffset) @@ -349,6 +352,10 @@ void SystemView::render(const Transform4x4f& parentTrans) Transform4x4f trans = getTransform() * parentTrans; + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + auto systemInfoZIndex = mSystemInfo.getZIndex(); auto minMax = std::minmax(mCarousel.zIndex, systemInfoZIndex); @@ -488,6 +495,12 @@ void SystemView::renderCarousel(const Transform4x4f& trans) break; } + if (mCarousel.logoPos.x() >= 0) + xOff = mCarousel.logoPos.x() - (mCarousel.type == HORIZONTAL ? (mCamOffset * logoSpacing[0]) : 0); + + if (mCarousel.logoPos.y() >= 0) + yOff = mCarousel.logoPos.y() - (mCarousel.type == VERTICAL ? (mCamOffset * logoSpacing[1]) : 0); + int center = (int)(mCamOffset); int logoCount = Math::min(mCarousel.maxLogoCount, (int)mEntries.size()); @@ -495,7 +508,7 @@ void SystemView::renderCarousel(const Transform4x4f& trans) int bufferIndex = getScrollingVelocity() + 1; int bufferLeft = logoBuffersLeft[bufferIndex]; int bufferRight = logoBuffersRight[bufferIndex]; - if (logoCount == 1) + if (logoCount == 1 && mCamOffset == 0) { bufferLeft = 0; bufferRight = 0; @@ -610,6 +623,7 @@ void SystemView::getDefaultElements(void) mCarousel.logoRotationOrigin.y() = 0.5; mCarousel.logoSize.x() = 0.25f * mSize.x(); mCarousel.logoSize.y() = 0.155f * mSize.y(); + mCarousel.logoPos = Vector2f(-1, -1); mCarousel.maxLogoCount = 3; mCarousel.zIndex = 40; @@ -649,6 +663,8 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) mCarousel.logoScale = elem->get("logoScale"); if (elem->has("logoSize")) mCarousel.logoSize = elem->get("logoSize") * mSize; + if (elem->has("logoPos")) + mCarousel.logoPos = elem->get("logoPos") * mSize; if (elem->has("maxLogoCount")) mCarousel.maxLogoCount = (int)Math::round(elem->get("maxLogoCount")); if (elem->has("zIndex")) diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index db1205424a..4de89cc918 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -38,6 +38,7 @@ struct SystemViewCarousel unsigned int color; int maxLogoCount; // number of logos shown on the carousel Vector2f logoSize; + Vector2f logoPos; float zIndex; }; diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 37c61dbbab..15fd7eb10a 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -134,14 +134,12 @@ void ViewController::goToGameList(SystemData* system) mState.system = system; if (mCurrentView) - { mCurrentView->onHide(); - } + mCurrentView = getGameListView(system); if (mCurrentView) - { - mCurrentView->onShow(); - } + mCurrentView->onShow(); + playViewTransition(false); } @@ -156,7 +154,7 @@ void ViewController::playViewTransition(bool forceImmediate) return; std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); - if(!forceImmediate && transition_style == "fade") + if (!forceImmediate && transition_style == "fade") { // fade // stop whatever's currently playing, leaving mFadeOpacity wherever it is @@ -175,20 +173,24 @@ void ViewController::playViewTransition(bool forceImmediate) }); // fast-forward animation if we're partway faded - if(target == -mCamera.translation()) + 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 if (!forceImmediate && transition_style == "slide"){ + else + advanceAnimation(0, (int)(mFadeOpacity * FADE_DURATION)); + } + else if (!forceImmediate && transition_style == "slide") + { // slide or simple slide setAnimation(new MoveCameraAnimation(mCamera, target)); updateHelpPrompts(); // update help prompts immediately - } else { + } + else + { // instant setAnimation(new LambdaAnimation( [this, target](float /*t*/) @@ -450,9 +452,15 @@ void ViewController::render(const Transform4x4f& parentTrans) // Keep track of UI mode changes. UIModeController::getInstance()->monitorUIMode(); - // draw systemview - getSystemListView()->render(trans); + // clipping + Vector3f sysStart = getSystemListView()->getPosition(); + Vector3f sysEnd = getSystemListView()->getPosition() + Vector3f(getSystemListView()->getSize().x(), getSystemListView()->getSize().y(), 0); + // draw systemview +// if ((sysStart.x() == viewStart.x() && sysStart.y() < viewStart.y() && sysEnd.x() > viewEnd.x() && sysEnd.y() > viewEnd.y()) || +// (sysStart.x() < viewEnd.x() && sysStart.y() < viewEnd.y() && sysEnd.x() > viewStart.x() && sysEnd.y() > viewStart.y())) + getSystemListView()->render(trans); + // draw gamelists for(auto it = mGameListViews.cbegin(); it != mGameListViews.cend(); it++) { @@ -460,8 +468,8 @@ void ViewController::render(const Transform4x4f& parentTrans) Vector3f guiStart = it->second->getPosition(); Vector3f guiEnd = it->second->getPosition() + 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()) +// if ((guiStart.x() == viewStart.x() && guiStart.y() < viewStart.y() && guiEnd.x() > viewEnd.x() && guiEnd.y() > viewEnd.y()) || +// (guiStart.x() < viewEnd.x() && guiStart.y() < viewEnd.y() && guiEnd.x() > viewStart.x() && guiEnd.y() > viewStart.y())) it->second->render(trans); } @@ -486,14 +494,10 @@ void ViewController::preload() uint32_t i = 0; for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { - if(Settings::getInstance()->getBool("SplashScreen") && - Settings::getInstance()->getBool("SplashScreenProgress")) + if (Settings::getInstance()->getBool("SplashScreen") && Settings::getInstance()->getBool("SplashScreenProgress")) { i++; - char buffer[100]; - sprintf (buffer, "Chargement de '%s' (%d/%d)", - (*it)->getFullName().c_str(), i, (int)SystemData::sSystemVector.size()); - mWindow->renderLoadingScreen(std::string(buffer)); + mWindow->renderLoadingScreen(_T("Preloading UI"), (float) i / (float) SystemData::sSystemVector.size()); } (*it)->getIndex()->resetFilters(); diff --git a/es-app/src/views/gamelist/IGameListView.cpp b/es-app/src/views/gamelist/IGameListView.cpp index 5fd5dbf076..722eeabcdd 100644 --- a/es-app/src/views/gamelist/IGameListView.cpp +++ b/es-app/src/views/gamelist/IGameListView.cpp @@ -44,6 +44,10 @@ void IGameListView::render(const Transform4x4f& parentTrans) { Transform4x4f trans = parentTrans * getTransform(); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + float scaleX = trans.r0().x(); float scaleY = trans.r1().y(); diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index d850727610..31774a179e 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -1,222 +1,224 @@ -#pragma once -#ifndef ES_CORE_GUI_COMPONENT_H -#define ES_CORE_GUI_COMPONENT_H - -#include "math/Misc.h" -#include "math/Transform4x4f.h" -#include "HelpPrompt.h" -#include "HelpStyle.h" -#include "InputConfig.h" -#include -#include - -#if defined(_WIN32) -#include -#endif - -class Animation; -class AnimationController; -class Font; -class InputConfig; -class ThemeData; -class Window; - -class GuiComponent -{ -public: - GuiComponent(Window* window); - virtual ~GuiComponent(); - - virtual void textInput(const char* text); - - //Called when input is received. - //Return true if the input is consumed, false if it should continue to be passed to other children. - virtual bool input(InputConfig* config, Input input); - - //Called when time passes. Default implementation calls updateSelf(deltaTime) and updateChildren(deltaTime) - so you should probably call GuiComponent::update(deltaTime) at some point (or at least updateSelf so animations work). - virtual void update(int deltaTime); - - //Called when it's time to render. By default, just calls renderChildren(parentTrans * getTransform()). - //You probably want to override this like so: - //1. Calculate the new transform that your control will draw at with Transform4x4f t = parentTrans * getTransform(). - //2. Set the renderer to use that new transform as the model matrix - Renderer::setMatrix(t); - //3. Draw your component. - //4. Tell your children to render, based on your component's transform - renderChildren(t). - virtual void render(const Transform4x4f& parentTrans); - - Vector3f getPosition() const; - inline void setPosition(const Vector3f& offset) { setPosition(offset.x(), offset.y(), offset.z()); } - void setPosition(float x, float y, float z = 0.0f); - virtual void onPositionChanged() {}; - - //Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) - Vector2f getOrigin() const; - void setOrigin(float originX, float originY); - inline void setOrigin(Vector2f origin) { setOrigin(origin.x(), origin.y()); } - virtual void onOriginChanged() {}; - - //Sets the rotation origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) - Vector2f getRotationOrigin() const; - void setRotationOrigin(float originX, float originY); - inline void setRotationOrigin(Vector2f origin) { setRotationOrigin(origin.x(), origin.y()); } - - virtual Vector2f getSize() const; - inline void setSize(const Vector2f& size) { setSize(size.x(), size.y()); } - void setSize(float w, float h); - virtual void onSizeChanged() {}; - - virtual Vector2f getRotationSize() const { return getSize(); }; - - float getRotation() const; - void setRotation(float rotation); - inline void setRotationDegrees(float rotation) { setRotation((float)ES_DEG_TO_RAD(rotation)); } - - float getScale() const; - void setScale(float scale); - - float getZIndex() const; - void setZIndex(float zIndex); - - float getDefaultZIndex() const; - void setDefaultZIndex(float zIndex); - - // Returns the center point of the image (takes origin into account). - Vector2f getCenter() const; - - void setParent(GuiComponent* parent); - GuiComponent* getParent() const; - - void addChild(GuiComponent* cmp); - void removeChild(GuiComponent* cmp); - void clearChildren(); - void sortChildren(); - unsigned int getChildCount() const; - GuiComponent* getChild(unsigned int i) const; - - // animation will be automatically deleted when it completes or is stopped. - bool isAnimationPlaying(unsigned char slot) const; - bool isAnimationReversed(unsigned char slot) const; - int getAnimationTime(unsigned char slot) const; - void setAnimation(Animation* animation, int delay = 0, std::function finishedCallback = nullptr, bool reverse = false, unsigned char slot = 0); - bool stopAnimation(unsigned char slot); - bool cancelAnimation(unsigned char slot); // Like stopAnimation, but doesn't call finishedCallback - only removes the animation, leaving things in their current state. Returns true if successful (an animation was in this slot). - bool finishAnimation(unsigned char slot); // Calls update(1.f) and finishedCallback, then deletes the animation - basically skips to the end. Returns true if successful (an animation was in this slot). - bool advanceAnimation(unsigned char slot, unsigned int time); // Returns true if successful (an animation was in this slot). - void stopAllAnimations(); - void cancelAllAnimations(); - - virtual unsigned char getOpacity() const; - virtual void setOpacity(unsigned char opacity); - - const Transform4x4f& getTransform(); - - virtual std::string getValue() const; - virtual void setValue(const std::string& value); - - - virtual std::string getTag() const; - virtual void setTag(const std::string& value); - - virtual void onFocusGained() {}; - virtual void onFocusLost() {}; - - virtual void onShow(); - virtual void onHide(); - - virtual void onScreenSaverActivate(); - virtual void onScreenSaverDeactivate(); - virtual void topWindow(bool isTop); - - // Default implementation just handles and tags as normalized float pairs. - // You probably want to keep this behavior for any derived classes as well as add your own. - virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); - - // Returns a list of help prompts. - virtual std::vector getHelpPrompts() { return std::vector(); }; - - // Called whenever help prompts change. - void updateHelpPrompts(); - - virtual HelpStyle getHelpStyle(); - - // Returns true if the component is busy doing background processing (e.g. HTTP downloads) - bool isProcessing() const; - -protected: - void renderChildren(const Transform4x4f& transform) const; - void updateSelf(int deltaTime); // updates animations - void updateChildren(int deltaTime); // updates animations - - unsigned char mOpacity; - Window* mWindow; - - GuiComponent* mParent; - std::vector mChildren; - - std::string mTag; - - Vector3f mPosition; - Vector2f mOrigin; - Vector2f mRotationOrigin; - Vector2f mSize; - - float mRotation = 0.0; - float mScale = 1.0; - - float mDefaultZIndex = 0; - float mZIndex = 0; - - bool mIsProcessing; - -public: - const static unsigned char MAX_ANIMATIONS = 4; - -private: - Transform4x4f mTransform; //Don't access this directly! Use getTransform()! - AnimationController* mAnimationMap[MAX_ANIMATIONS]; -}; - - - -class LocalizationItem -{ -public: - std::string msgid; - std::string msgstr; -}; - -class GuiTextTool -{ -public: -#if defined(_WIN32) - static const std::string convertFromWideString(const std::wstring wstring) - { - int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); - - std::string string; - string.resize(numBytes); - WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); - - return string; - } -#endif - static void ensureLocalisation(); - static const std::string localize(const std::string text); - - static void setLanguage(std::string lang); - -private: - static std::vector mItems; - static std::string mCurrentLanguage; - static bool mCurrentLanguageLoaded; -}; - -#if defined(_WIN32) -#define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) -#define _L(x) GuiTextTool::localize(x) -#else -#define _T(x) GuiTextTool::localize(x) -#define _L(x) GuiTextTool::localize(x) -#endif // _WIN32 - -#endif // ES_CORE_GUI_COMPONENT_H +#pragma once +#ifndef ES_CORE_GUI_COMPONENT_H +#define ES_CORE_GUI_COMPONENT_H + +#include "math/Misc.h" +#include "math/Transform4x4f.h" +#include "HelpPrompt.h" +#include "HelpStyle.h" +#include "InputConfig.h" +#include +#include + +#if defined(_WIN32) +#include +#endif + +class Animation; +class AnimationController; +class Font; +class InputConfig; +class ThemeData; +class Window; + +class GuiComponent +{ +public: + GuiComponent(Window* window); + virtual ~GuiComponent(); + + virtual void textInput(const char* text); + + //Called when input is received. + //Return true if the input is consumed, false if it should continue to be passed to other children. + virtual bool input(InputConfig* config, Input input); + + //Called when time passes. Default implementation calls updateSelf(deltaTime) and updateChildren(deltaTime) - so you should probably call GuiComponent::update(deltaTime) at some point (or at least updateSelf so animations work). + virtual void update(int deltaTime); + + //Called when it's time to render. By default, just calls renderChildren(parentTrans * getTransform()). + //You probably want to override this like so: + //1. Calculate the new transform that your control will draw at with Transform4x4f t = parentTrans * getTransform(). + //2. Set the renderer to use that new transform as the model matrix - Renderer::setMatrix(t); + //3. Draw your component. + //4. Tell your children to render, based on your component's transform - renderChildren(t). + virtual void render(const Transform4x4f& parentTrans); + + Vector3f getPosition() const; + inline void setPosition(const Vector3f& offset) { setPosition(offset.x(), offset.y(), offset.z()); } + void setPosition(float x, float y, float z = 0.0f); + virtual void onPositionChanged() {}; + + //Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) + Vector2f getOrigin() const; + void setOrigin(float originX, float originY); + inline void setOrigin(Vector2f origin) { setOrigin(origin.x(), origin.y()); } + virtual void onOriginChanged() {}; + + //Sets the rotation origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) + Vector2f getRotationOrigin() const; + void setRotationOrigin(float originX, float originY); + inline void setRotationOrigin(Vector2f origin) { setRotationOrigin(origin.x(), origin.y()); } + + virtual Vector2f getSize() const; + inline void setSize(const Vector2f& size) { setSize(size.x(), size.y()); } + void setSize(float w, float h); + virtual void onSizeChanged() {}; + + virtual void setColor(unsigned int color) {}; + + virtual Vector2f getRotationSize() const { return getSize(); }; + + float getRotation() const; + void setRotation(float rotation); + inline void setRotationDegrees(float rotation) { setRotation((float)ES_DEG_TO_RAD(rotation)); } + + float getScale() const; + void setScale(float scale); + + float getZIndex() const; + void setZIndex(float zIndex); + + float getDefaultZIndex() const; + void setDefaultZIndex(float zIndex); + + // Returns the center point of the image (takes origin into account). + Vector2f getCenter() const; + + void setParent(GuiComponent* parent); + GuiComponent* getParent() const; + + void addChild(GuiComponent* cmp); + void removeChild(GuiComponent* cmp); + void clearChildren(); + void sortChildren(); + unsigned int getChildCount() const; + GuiComponent* getChild(unsigned int i) const; + + // animation will be automatically deleted when it completes or is stopped. + bool isAnimationPlaying(unsigned char slot) const; + bool isAnimationReversed(unsigned char slot) const; + int getAnimationTime(unsigned char slot) const; + void setAnimation(Animation* animation, int delay = 0, std::function finishedCallback = nullptr, bool reverse = false, unsigned char slot = 0); + bool stopAnimation(unsigned char slot); + bool cancelAnimation(unsigned char slot); // Like stopAnimation, but doesn't call finishedCallback - only removes the animation, leaving things in their current state. Returns true if successful (an animation was in this slot). + bool finishAnimation(unsigned char slot); // Calls update(1.f) and finishedCallback, then deletes the animation - basically skips to the end. Returns true if successful (an animation was in this slot). + bool advanceAnimation(unsigned char slot, unsigned int time); // Returns true if successful (an animation was in this slot). + void stopAllAnimations(); + void cancelAllAnimations(); + + virtual unsigned char getOpacity() const; + virtual void setOpacity(unsigned char opacity); + + const Transform4x4f& getTransform(); + + virtual std::string getValue() const; + virtual void setValue(const std::string& value); + + + virtual std::string getTag() const; + virtual void setTag(const std::string& value); + + virtual void onFocusGained() {}; + virtual void onFocusLost() {}; + + virtual void onShow(); + virtual void onHide(); + + virtual void onScreenSaverActivate(); + virtual void onScreenSaverDeactivate(); + virtual void topWindow(bool isTop); + + // Default implementation just handles and tags as normalized float pairs. + // You probably want to keep this behavior for any derived classes as well as add your own. + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); + + // Returns a list of help prompts. + virtual std::vector getHelpPrompts() { return std::vector(); }; + + // Called whenever help prompts change. + void updateHelpPrompts(); + + virtual HelpStyle getHelpStyle(); + + // Returns true if the component is busy doing background processing (e.g. HTTP downloads) + bool isProcessing() const; + +protected: + void renderChildren(const Transform4x4f& transform) const; + void updateSelf(int deltaTime); // updates animations + void updateChildren(int deltaTime); // updates animations + + unsigned char mOpacity; + Window* mWindow; + + GuiComponent* mParent; + std::vector mChildren; + + std::string mTag; + + Vector3f mPosition; + Vector2f mOrigin; + Vector2f mRotationOrigin; + Vector2f mSize; + + float mRotation = 0.0; + float mScale = 1.0; + + float mDefaultZIndex = 0; + float mZIndex = 0; + + bool mIsProcessing; + +public: + const static unsigned char MAX_ANIMATIONS = 4; + +private: + Transform4x4f mTransform; //Don't access this directly! Use getTransform()! + AnimationController* mAnimationMap[MAX_ANIMATIONS]; +}; + + + +class LocalizationItem +{ +public: + std::string msgid; + std::string msgstr; +}; + +class GuiTextTool +{ +public: +#if defined(_WIN32) + static const std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + + std::string string; + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return string; + } +#endif + static void ensureLocalisation(); + static const std::string localize(const std::string text); + + static void setLanguage(std::string lang); + +private: + static std::vector mItems; + static std::string mCurrentLanguage; + static bool mCurrentLanguageLoaded; +}; + +#if defined(_WIN32) +#define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) +#define _L(x) GuiTextTool::localize(x) +#else +#define _T(x) GuiTextTool::localize(x) +#define _L(x) GuiTextTool::localize(x) +#endif // _WIN32 + +#endif // ES_CORE_GUI_COMPONENT_H diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index 3285308cd2..f21e6f8f11 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -10,7 +10,8 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d width = 0; height = 0; FIMEMORY * fiMemory = FreeImage_OpenMemory((BYTE *)data, (DWORD)size); - if (fiMemory != nullptr) { + if (fiMemory != nullptr) + { //detect the filetype from data FREE_IMAGE_FORMAT format = FreeImage_GetFileTypeFromMemory(fiMemory); if (format != FIF_UNKNOWN && FreeImage_FIFSupportsReading(format)) @@ -31,21 +32,26 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d } } if (fiBitmap != nullptr) - { + { width = FreeImage_GetWidth(fiBitmap); height = FreeImage_GetHeight(fiBitmap); //loop through scanlines and add all pixel data to the return vector //this is necessary, because width*height*bpp might not be == pitch + unsigned char * tempData = new unsigned char[width * height * 4]; + unsigned char * bytes = FreeImage_GetBits(fiBitmap); + + // memcpy(tempData, bytes, width * height * 4); +/* for (size_t i = 0; i < height; i++) { const BYTE * scanLine = FreeImage_GetScanLine(fiBitmap, (int)i); memcpy(tempData + (i * width * 4), scanLine, width * 4); - } + }*/ //convert from BGRA to RGBA for(size_t i = 0; i < width*height; i++) { - RGBQUAD bgra = ((RGBQUAD *)tempData)[i]; + RGBQUAD bgra = ((RGBQUAD *)bytes)[i]; RGBQUAD rgba; rgba.rgbBlue = bgra.rgbRed; rgba.rgbGreen = bgra.rgbGreen; @@ -53,6 +59,7 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d rgba.rgbReserved = bgra.rgbReserved; ((RGBQUAD *)tempData)[i] = rgba; } + rawData = std::vector(tempData, tempData + width * height * 4); //free bitmap data FreeImage_Unload(fiBitmap); @@ -74,6 +81,78 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d return rawData; } +unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height) +{ + width = 0; + height = 0; + + FIMEMORY * fiMemory = FreeImage_OpenMemory((BYTE *)data, (DWORD)size); + if (fiMemory != nullptr) + { + //detect the filetype from data + FREE_IMAGE_FORMAT format = FreeImage_GetFileTypeFromMemory(fiMemory); + if (format != FIF_UNKNOWN && FreeImage_FIFSupportsReading(format)) + { + //file type is supported. load image + FIBITMAP * fiBitmap = FreeImage_LoadFromMemory(format, fiMemory); + if (fiBitmap != nullptr) + { + //loaded. convert to 32bit if necessary + if (FreeImage_GetBPP(fiBitmap) != 32) + { + FIBITMAP * fiConverted = FreeImage_ConvertTo32Bits(fiBitmap); + if (fiConverted != nullptr) + { + //free original bitmap data + FreeImage_Unload(fiBitmap); + fiBitmap = fiConverted; + } + } + if (fiBitmap != nullptr) + { + width = FreeImage_GetWidth(fiBitmap); + height = FreeImage_GetHeight(fiBitmap); + + //loop through scanlines and add all pixel data to the return vector + //this is necessary, because width*height*bpp might not be == pitch + + unsigned char* tempData = new unsigned char[width * height * 4]; + + int w = (int)width; + + for (int y = (int)height; --y >= 0; ) + { + unsigned int* argb = (unsigned int*)FreeImage_GetScanLine(fiBitmap, y); + unsigned int* abgr = (unsigned int*)(tempData + (y * width * 4)); + for (int x = w; --x >= 0;) + { + unsigned int c = argb[x]; + abgr[x] = (c & 0xFF00FF00) | ((c & 0xFF) << 16) | ((c >> 16) & 0xFF); + } + } + + FreeImage_Unload(fiBitmap); + FreeImage_CloseMemory(fiMemory); + + return tempData; + } + } + else + { + LOG(LogError) << "Error - Failed to load image from memory!"; + } + } + else + { + LOG(LogError) << "Error - File type " << (format == FIF_UNKNOWN ? "unknown" : "unsupported") << "!"; + } + //free FIMEMORY again + FreeImage_CloseMemory(fiMemory); + } + + return NULL; +} + void ImageIO::flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height) { unsigned int temp; diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index ad8ab48069..acbe6cb526 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -9,6 +9,8 @@ class ImageIO { public: static std::vector loadFromMemoryRGBA32(const unsigned char * data, const size_t size, size_t & width, size_t & height); + static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height); + static void flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height); }; diff --git a/es-core/src/PowerSaver.cpp b/es-core/src/PowerSaver.cpp index 8c68955f90..bc1d651a5c 100644 --- a/es-core/src/PowerSaver.cpp +++ b/es-core/src/PowerSaver.cpp @@ -62,6 +62,7 @@ void PowerSaver::updateMode() } else { mMode = DEFAULT; } + updateTimeouts(); } diff --git a/es-core/src/Renderer.h b/es-core/src/Renderer.h index 358cf4031f..5d8b0bd3ba 100644 --- a/es-core/src/Renderer.h +++ b/es-core/src/Renderer.h @@ -38,6 +38,8 @@ namespace Renderer void drawRect(int x, int y, int w, int h, unsigned int color, GLenum blend_sfactor = GL_SRC_ALPHA, GLenum blend_dfactor = GL_ONE_MINUS_SRC_ALPHA); void drawRect(float x, float y, float w, float h, unsigned int color, GLenum blend_sfactor = GL_SRC_ALPHA, GLenum blend_dfactor = GL_ONE_MINUS_SRC_ALPHA); + + bool isVisibleOnScreen(float x, float y, float w, float h); } #endif // ES_CORE_RENDERER_H diff --git a/es-core/src/Renderer_draw_gl.cpp b/es-core/src/Renderer_draw_gl.cpp index cac28c5d4b..b584c618b1 100644 --- a/es-core/src/Renderer_draw_gl.cpp +++ b/es-core/src/Renderer_draw_gl.cpp @@ -15,6 +15,7 @@ namespace Renderer { }; std::stack clipStack; + std::stack nativeClipStack; void setColor4bArray(GLubyte* array, unsigned int color) { @@ -62,7 +63,7 @@ namespace Renderer { } //make sure the box fits within clipStack.top(), and clip further accordingly - if(clipStack.size()) + if (clipStack.size()) { const ClipRect& top = clipStack.top(); if(top.x > box.x) @@ -81,6 +82,7 @@ namespace Renderer { box.h = 0; clipStack.push(box); + nativeClipStack.push(ClipRect(pos.x(), pos.y(), dim.x(), dim.y())); glScissor(box.x, box.y, box.w, box.h); glEnable(GL_SCISSOR_TEST); @@ -95,15 +97,60 @@ namespace Renderer { } clipStack.pop(); + nativeClipStack.pop(); + if(clipStack.empty()) { glDisable(GL_SCISSOR_TEST); - }else{ + } + else + { const ClipRect& top = clipStack.top(); glScissor(top.x, top.y, top.w, top.h); } } + bool valueInRange(int value, int min, int max) + { + return (value >= min) && (value <= max); + } + + bool rectOverlap(ClipRect A, ClipRect B) + { + bool xOverlap = valueInRange(A.x, B.x, B.x + B.w) || + valueInRange(B.x, A.x, A.x + A.w); + + bool yOverlap = valueInRange(A.y, B.y, B.y + B.h) || + valueInRange(B.y, A.y, A.y + A.h); + + return xOverlap && yOverlap; + } + + bool isVisibleOnScreen(float x, float y, float w, float h) + { + ClipRect screen = ClipRect(0, 0, Renderer::getWindowWidth(), Renderer::getWindowHeight()); + ClipRect box = ClipRect(x, y, w, h); + + if (w > 0 && x + w <= 0) + return false; + + if (h > 0 && y + h <= 0) + return false; + + if (x == screen.w || y == screen.h) + return false; + + if (!rectOverlap(screen, box)) + return false; + + if (clipStack.empty()) + return true; + + screen = nativeClipStack.top(); + return rectOverlap(screen, box); + } + + void drawRect(float x, float y, float w, float h, unsigned int color, GLenum blend_sfactor, GLenum blend_dfactor) { drawRect((int)Math::round(x), (int)Math::round(y), (int)Math::round(w), (int)Math::round(h), color, blend_sfactor, blend_dfactor); diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index fb33b2cea4..c51fddf94a 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -75,14 +75,15 @@ namespace Renderer SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, windowWidth, windowHeight, - SDL_WINDOW_OPENGL | (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN))); + SDL_WINDOW_OPENGL | (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN)) + ); if (sdlWindow == NULL) { LOG(LogError) << "Error creating SDL window!\n\t" << SDL_GetError(); return false; } - + LOG(LogInfo) << "Created window successfully."; //support screen rotation @@ -127,7 +128,7 @@ namespace Renderer sdlContext = SDL_GL_CreateContext(sdlWindow); // vsync - // if(Settings::getInstance()->getBool("VSync")) + if (Settings::getInstance()->getBool("VSync")) { // SDL_GL_SetSwapInterval(0) for immediate updates (no vsync, default), // 1 for updates synchronized with the vertical retrace, @@ -135,12 +136,13 @@ namespace Renderer // SDL_GL_SetSwapInterval returns 0 on success, -1 on error. // if vsync is requested, try normal vsync; if that doesn't work, try late swap tearing // if that doesn't work, report an error + if (SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) - LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; + LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; } - // else - // SDL_GL_SetSwapInterval(0); - + else + SDL_GL_SetSwapInterval(0); + return true; } @@ -218,7 +220,18 @@ namespace Renderer void swapBuffers() { +#ifdef WIN32 + glFlush(); + glFinish(); + Sleep(0); +#endif + SDL_GL_SwapWindow(sdlWindow); + +#ifdef WIN32 + Sleep(0); +#endif + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } }; diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 41f2e16de4..a21d117a44 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -24,7 +24,9 @@ std::vector settings_dont_save { { "ShowExit" }, { "SplashScreen" }, { "SplashScreenProgress" }, +#if !defined(_WIN32) { "VSync" }, +#endif { "FullscreenBorderless" }, { "Windowed" }, { "WindowWidth" }, @@ -67,7 +69,11 @@ void Settings::setDefaults() mBoolMap["SplashScreenProgress"] = true; mStringMap["StartupSystem"] = ""; +#if defined(_WIN32) mBoolMap["VSync"] = true; +#else + mBoolMap["VSync"] = false; +#endif mBoolMap["EnableSounds"] = true; mBoolMap["ShowHelpPrompts"] = true; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index ba4121ce08..bcc1a72180 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -1,730 +1,849 @@ -#include "ThemeData.h" - -#include "components/ImageComponent.h" -#include "components/TextComponent.h" -#include "utils/FileSystemUtil.h" -#include "Log.h" -#include "platform.h" -#include "Settings.h" -#include -#include - -std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" } }; -std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; - -std::map> ThemeData::sElementMap { - { "image", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "maxSize", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "path", PATH }, - { "default", PATH }, - { "tile", BOOLEAN }, - { "color", COLOR }, - { "zIndex", FLOAT } } }, - { "imagegrid", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "margin", NORMALIZED_PAIR }, - { "gameImage", PATH }, - { "folderImage", PATH }, - { "scrollDirection", STRING } } }, - { "gridtile", { - { "size", NORMALIZED_PAIR }, - { "padding", NORMALIZED_PAIR }, - { "imageColor", COLOR }, - { "backgroundImage", PATH }, - { "backgroundCornerSize", NORMALIZED_PAIR }, - { "backgroundColor", COLOR }, - { "backgroundCenterColor", COLOR }, - { "backgroundEdgeColor", COLOR }, - { "imageSizeMode", STRING } } }, - { "text", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "text", STRING }, - { "backgroundColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "color", COLOR }, - { "alignment", STRING }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "value", STRING }, - { "zIndex", FLOAT } } }, - { "textlist", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "selectorHeight", FLOAT }, - { "selectorOffsetY", FLOAT }, - { "selectorColor", COLOR }, - { "selectorImagePath", PATH }, - { "selectorImageTile", BOOLEAN }, - { "selectedColor", COLOR }, - { "primaryColor", COLOR }, - { "secondaryColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "scrollSound", PATH }, - { "alignment", STRING }, - { "horizontalMargin", FLOAT }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "zIndex", FLOAT } } }, - { "container", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "zIndex", FLOAT } } }, - { "ninepatch", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "path", PATH }, - { "zIndex", FLOAT } } }, - { "datetime", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "backgroundColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "color", COLOR }, - { "alignment", STRING }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "value", STRING }, - { "format", STRING }, - { "displayRelative", BOOLEAN }, - { "zIndex", FLOAT } } }, - { "rating", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "color", COLOR }, - { "filledPath", PATH }, - { "unfilledPath", PATH }, - { "zIndex", FLOAT } } }, - { "sound", { - { "path", PATH } } }, - { "helpsystem", { - { "pos", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "textColor", COLOR }, - { "iconColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT } } }, - { "video", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "maxSize", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "default", PATH }, - { "delay", FLOAT }, - { "zIndex", FLOAT }, - { "showSnapshotNoVideo", BOOLEAN }, - { "showSnapshotDelay", BOOLEAN } } }, - { "carousel", { - { "type", STRING }, - { "size", NORMALIZED_PAIR }, - { "pos", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "color", COLOR }, - { "logoScale", FLOAT }, - { "logoRotation", FLOAT }, - { "logoRotationOrigin", NORMALIZED_PAIR }, - { "logoSize", NORMALIZED_PAIR }, - { "logoAlignment", STRING }, - { "maxLogoCount", FLOAT }, - { "zIndex", FLOAT } } } -}; - -#define MINIMUM_THEME_FORMAT_VERSION 3 -#define CURRENT_THEME_FORMAT_VERSION 6 - -// helper -unsigned int getHexColor(const char* str) -{ - ThemeException error; - if(!str) - throw error << "Empty color"; - - size_t len = strlen(str); - if(len != 6 && len != 8) - throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; - - unsigned int val; - std::stringstream ss; - ss << str; - ss >> std::hex >> val; - - if(len == 6) - val = (val << 8) | 0xFF; - - return val; -} - -std::map mVariables; - -std::string resolvePlaceholders(const char* in) -{ - std::string inStr(in); - - if(inStr.empty()) - return inStr; - - const size_t variableBegin = inStr.find("${"); - const size_t variableEnd = inStr.find("}", variableBegin); - - if((variableBegin == std::string::npos) || (variableEnd == std::string::npos)) - return inStr; - - std::string prefix = inStr.substr(0, variableBegin); - std::string replace = inStr.substr(variableBegin + 2, variableEnd - (variableBegin + 2)); - std::string suffix = resolvePlaceholders(inStr.substr(variableEnd + 1).c_str()); - - return prefix + mVariables[replace] + suffix; -} - -ThemeData::ThemeData() -{ - mVersion = 0; -} - -void ThemeData::loadFile(std::string system, std::map sysDataMap, const std::string& path) -{ - mPaths.push_back(path); - - ThemeException error; - error.setFiles(mPaths); - - if(!Utils::FileSystem::exists(path)) - throw error << "File does not exist!"; - - mVersion = 0; - mViews.clear(); - - mSystemThemeFolder = system; - - mVariables.clear(); - mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); - - pugi::xml_document doc; - pugi::xml_parse_result res = doc.load_file(path.c_str()); - if(!res) - throw error << "XML parsing error: \n " << res.description(); - - pugi::xml_node root = doc.child("theme"); - if(!root) - throw error << "Missing tag!"; - - // parse version - mVersion = root.child("formatVersion").text().as_float(-404); - if(mVersion == -404) - throw error << " tag missing!\n It's either out of date or you need to add " << CURRENT_THEME_FORMAT_VERSION << " inside your tag."; - - if (root.attribute("defaultView")) - mDefaultView = root.attribute("defaultView").as_string(); - - if(mVersion < MINIMUM_THEME_FORMAT_VERSION) - throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; - - parseVariables(root); - parseIncludes(root); - parseViews(root); - parseFeatures(root); -} - -std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) -{ - std::string result = path; - - size_t start_pos = result.find("$system"); - if (start_pos == std::string::npos) - return path; - - result.replace(start_pos, 7, systemThemeFolder); - - //result.replace("$system", systemThemeFolder); - //boost::algorithm::replace_first(result, "$system", systemThemeFolder); - return result; -} - -bool ThemeData::parseSubset(const pugi::xml_node& node) -{ - bool parse = true; - - if (node.attribute("subset")) - { - parse = false; - const std::string subsetAttr = node.attribute("subset").as_string(); - const std::string nameAttr = node.attribute("name").as_string(); - - if (subsetAttr == "iconset" || subsetAttr == "gamelistview") - { - if (nameAttr.rfind("1-") != std::string::npos) - { - parse = true; - return parse; - } - } - else if (subsetAttr == "systemview") - { - if (nameAttr.rfind("8-") != std::string::npos) - { - parse = true; - return parse; - } - } - else if (nameAttr.rfind("2-") != std::string::npos) - { - parse = true; - return parse; - } - /* - if (subsetAttr == "colorset" && nameAttr == mColorset) - { - parse = true; - return parse; - } - if (subsetAttr == "iconset" && nameAttr == mIconset) - { - parse = true; - return parse; - } - if (subsetAttr == "menu" && nameAttr == mMenu) - { - parse = true; - return parse; - } - if (subsetAttr == "systemview" && nameAttr == mSystemview) - { - parse = true; - return parse; - } - if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) - { - parse = true; - return parse; - }*/ - } - - return parse; -} - -void ThemeData::parseIncludes(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) - { - if (!parseSubset(node)) - continue; - - std::string relPath = resolvePlaceholders(node.text().as_string()); - std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); - path = resolveSystemVariable(mSystemThemeFolder, path); - - if (!ResourceManager::getInstance()->fileExists(path)) - continue; - //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; - - error << " from included file \"" << relPath << "\":\n "; - - mPaths.push_back(path); - - pugi::xml_document includeDoc; - pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); - if(!result) - throw error << "Error parsing file: \n " << result.description(); - - pugi::xml_node theme = includeDoc.child("theme"); - if(!theme) - throw error << "Missing tag!"; - - parseVariables(theme); - parseIncludes(theme); - parseViews(theme); - parseFeatures(theme); - - mPaths.pop_back(); - } -} - -void ThemeData::parseFeatures(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) - { - if(!node.attribute("supported")) - throw error << "Feature missing \"supported\" attribute!"; - - const std::string supportedAttr = node.attribute("supported").as_string(); - - if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) - { - parseViews(node); - } - } -} - -void ThemeData::parseVariables(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - pugi::xml_node variables = root.child("variables"); - - if(!variables) - return; - - for(pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) - { - std::string key = it->name(); - std::string val = it->text().as_string(); - - if (!val.empty()) - mVariables.insert(std::pair(key, val)); - } -} - -void ThemeData::parseViews(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - // parse views - for(pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) - { - if(!node.attribute("name")) - throw error << "View missing \"name\" attribute!"; - - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - std::string viewKey; - while(off != std::string::npos || prevOff != std::string::npos) - { - viewKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - - if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) - { - ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; - parseView(node, view); - - if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) - { - ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; - parseView(node, view); - } - } - } - } -} - -void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) - { - if(!node.attribute("name")) - throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; - - auto elemTypeIt = sElementMap.find(node.name()); - if(elemTypeIt == sElementMap.cend()) - throw error << "Unknown element of type \"" << node.name() << "\"!"; - - if (parseRegion(node)) - { - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while (off != std::string::npos || prevOff != std::string::npos) - { - std::string elemKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - - parseElement(node, elemTypeIt->second, - view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); - - if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) - view.orderedKeys.push_back(elemKey); - } - } - } -} - -bool ThemeData::parseRegion(const pugi::xml_node& node) -{ - bool parse = true; - - if (node.attribute("region")) - { - std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); - - parse = false; - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("region").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while (off != std::string::npos || prevOff != std::string::npos) - { - std::string elemKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - if (elemKey == regionsetting) - { - parse = true; - return parse; - } - } - - } - return parse; - -} - - -void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element) -{ - ThemeException error; - error.setFiles(mPaths); - - element.type = root.name(); - element.extra = root.attribute("extra").as_bool(false); - - for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) - { - auto typeIt = typeMap.find(node.name()); - if (typeIt == typeMap.cend()) - continue; - //throw error << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ")."; - - std::string str = resolveSystemVariable(mSystemThemeFolder, resolvePlaceholders(node.text().as_string())); - - switch(typeIt->second) - { - case NORMALIZED_PAIR: - { - size_t divider = str.find(' '); - if(divider == std::string::npos) - throw error << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")"; - - std::string first = str.substr(0, divider); - std::string second = str.substr(divider, std::string::npos); - - Vector2f val((float)atof(first.c_str()), (float)atof(second.c_str())); - - element.properties[node.name()] = val; - break; - } - case STRING: - element.properties[node.name()] = str; - break; - case PATH: - { - std::string path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true); - - if (!ResourceManager::getInstance()->fileExists(path)) - { - std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); - if (ResourceManager::getInstance()->fileExists(rootPath)) - path = rootPath; - } - - if (!ResourceManager::getInstance()->fileExists(path)) - { - std::stringstream ss; - ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda - ss << "could not find file \"" << node.text().get() << "\" "; - if(node.text().get() != path) - ss << "(which resolved to \"" << path << "\") "; - - LOG(LogWarning) << ss.str(); - - auto test = element.properties[node.name()]; - if (test.s.empty()) - element.properties[node.name()] = path; - } - else - element.properties[node.name()] = path; - - break; - } - case COLOR: - element.properties[node.name()] = getHexColor(str.c_str()); - break; - case FLOAT: - { - float floatVal = static_cast(strtod(str.c_str(), 0)); - element.properties[node.name()] = floatVal; - break; - } - - case BOOLEAN: - { - // only look at first char - char first = str[0]; - // 1*, t* (true), T* (True), y* (yes), Y* (YES) - bool boolVal = (first == '1' || first == 't' || first == 'T' || first == 'y' || first == 'Y'); - - element.properties[node.name()] = boolVal; - break; - } - default: - throw error << "Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name(); - } - } -} - -bool ThemeData::hasView(const std::string& view) -{ - auto viewIt = mViews.find(view); - return (viewIt != mViews.cend()); -} - -const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const -{ - auto viewIt = mViews.find(view); - if(viewIt == mViews.cend()) - return NULL; // not found - - auto elemIt = viewIt->second.elements.find(element); - if(elemIt == viewIt->second.elements.cend()) return NULL; - - if(elemIt->second.type != expectedType && !expectedType.empty()) - { - LOG(LogWarning) << " requested mismatched theme type for [" << view << "." << element << "] - expected \"" - << expectedType << "\", got \"" << elemIt->second.type << "\""; - return NULL; - } - - return &elemIt->second; -} - -const std::shared_ptr& ThemeData::getDefault() -{ - static std::shared_ptr theme = nullptr; - if(theme == nullptr) - { - theme = std::shared_ptr(new ThemeData()); - - const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_theme_default.xml"; - if(Utils::FileSystem::exists(path)) - { - try - { - std::map emptyMap; - theme->loadFile("", emptyMap, path); - } catch(ThemeException& e) - { - LOG(LogError) << e.what(); - theme = std::shared_ptr(new ThemeData()); //reset to empty - } - } - } - - return theme; -} - -std::vector ThemeData::makeExtras(const std::shared_ptr& theme, const std::string& view, Window* window) -{ - std::vector comps; - - auto viewIt = theme->mViews.find(view); - if(viewIt == theme->mViews.cend()) - return comps; - - for(auto it = viewIt->second.orderedKeys.cbegin(); it != viewIt->second.orderedKeys.cend(); it++) - { - ThemeElement& elem = viewIt->second.elements.at(*it); - if(elem.extra) - { - GuiComponent* comp = NULL; - const std::string& t = elem.type; - if(t == "image") - comp = new ImageComponent(window); - else if(t == "text") - comp = new TextComponent(window); - - comp->setDefaultZIndex(10); - comp->applyTheme(theme, view, *it, ThemeFlags::ALL); - comps.push_back(comp); - } - } - - return comps; -} - -std::map ThemeData::getThemeSets() -{ - std::map sets; - - static const size_t pathCount = 2; - std::string paths[pathCount] = - { - "/etc/emulationstation/themes", - Utils::FileSystem::getHomePath() + "/.emulationstation/themes" - }; - - for(size_t i = 0; i < pathCount; i++) - { - if(!Utils::FileSystem::isDirectory(paths[i])) - continue; - - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(paths[i]); - - for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if(Utils::FileSystem::isDirectory(*it)) - { - ThemeSet set = {*it}; - sets[set.getName()] = set; - } - } - } - - return sets; -} - -std::string ThemeData::getThemeFromCurrentSet(const std::string& system) -{ - std::map themeSets = ThemeData::getThemeSets(); - if(themeSets.empty()) - { - // no theme sets available - return ""; - } - - std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(set == themeSets.cend()) - { - // currently selected theme set is missing, so just pick the first available set - set = themeSets.cbegin(); - Settings::getInstance()->setString("ThemeSet", set->first); - } - - return set->second.getThemePath(system); -} +#include "ThemeData.h" + +#include "components/ImageComponent.h" +#include "components/TextComponent.h" +#include "utils/FileSystemUtil.h" +#include "Log.h" +#include "platform.h" +#include "Settings.h" +#include +#include + +std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" }, { "menu" } }; +std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; + +std::map> ThemeData::sElementMap { + { "image", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "maxSize", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "path", PATH }, + { "default", PATH }, + { "tile", BOOLEAN }, + { "color", COLOR }, + { "zIndex", FLOAT } } }, + { "imagegrid", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "margin", NORMALIZED_PAIR }, + { "gameImage", PATH }, + { "folderImage", PATH }, + { "scrollDirection", STRING } } }, + { "gridtile", { + { "size", NORMALIZED_PAIR }, + { "padding", NORMALIZED_PAIR }, + { "imageColor", COLOR }, + { "backgroundImage", PATH }, + { "backgroundCornerSize", NORMALIZED_PAIR }, + { "backgroundColor", COLOR }, + { "backgroundCenterColor", COLOR }, + { "backgroundEdgeColor", COLOR }, + { "imageSizeMode", STRING } } }, + { "text", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "text", STRING }, + { "backgroundColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "color", COLOR }, + { "alignment", STRING }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "value", STRING }, + { "zIndex", FLOAT } } }, + { "textlist", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "selectorHeight", FLOAT }, + { "selectorOffsetY", FLOAT }, + { "selectorColor", COLOR }, + { "selectorImagePath", PATH }, + { "selectorImageTile", BOOLEAN }, + { "selectedColor", COLOR }, + { "primaryColor", COLOR }, + { "secondaryColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "scrollSound", PATH }, + { "alignment", STRING }, + { "horizontalMargin", FLOAT }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "zIndex", FLOAT } } }, + { "container", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "zIndex", FLOAT } } }, + { "ninepatch", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "path", PATH }, + { "zIndex", FLOAT } } }, + { "datetime", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "backgroundColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "color", COLOR }, + { "alignment", STRING }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "value", STRING }, + { "format", STRING }, + { "displayRelative", BOOLEAN }, + { "zIndex", FLOAT } } }, + { "rating", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "color", COLOR }, + { "filledPath", PATH }, + { "unfilledPath", PATH }, + { "zIndex", FLOAT } } }, + { "sound", { + { "path", PATH } } }, + { "helpsystem", { + { "pos", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "textColor", COLOR }, + { "iconColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT } } }, + { "video", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "maxSize", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "default", PATH }, + { "delay", FLOAT }, + { "zIndex", FLOAT }, + { "showSnapshotNoVideo", BOOLEAN }, + { "showSnapshotDelay", BOOLEAN } } }, + { "carousel", { + { "type", STRING }, + { "size", NORMALIZED_PAIR }, + { "pos", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "color", COLOR }, + { "logoScale", FLOAT }, + { "logoRotation", FLOAT }, + { "logoRotationOrigin", NORMALIZED_PAIR }, + { "logoSize", NORMALIZED_PAIR }, + { "logoPos", NORMALIZED_PAIR }, + { "logoAlignment", STRING }, + { "maxLogoCount", FLOAT }, + { "zIndex", FLOAT } } }, + + { "menuText", { + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "separatorColor", COLOR }, + { "selectorColor", COLOR }, + { "selectedColor", COLOR }, + { "color", COLOR } } }, + + { "menuTextSmall", { + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "color", COLOR } } }, + + { "menuBackground", { + { "path", PATH }, + { "fadePath", PATH }, + { "color", COLOR } } } +}; + +std::shared_ptr ThemeData::MenuTheme; + +#define MINIMUM_THEME_FORMAT_VERSION 3 +#define CURRENT_THEME_FORMAT_VERSION 6 + +// helper +unsigned int getHexColor(const char* str) +{ + ThemeException error; + if (!str) + { + //throw error << "Empty color"; + LOG(LogWarning) << "Empty color"; + return 0; + } + + size_t len = strlen(str); + if (len != 6 && len != 8) + { + //throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; + LOG(LogWarning) << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; + return 0; + } + + unsigned int val; + std::stringstream ss; + ss << str; + ss >> std::hex >> val; + + if(len == 6) + val = (val << 8) | 0xFF; + + return val; +} + +std::map mVariables; + +std::string resolvePlaceholders(const char* in) +{ + std::string inStr(in); + + if(inStr.empty()) + return inStr; + + const size_t variableBegin = inStr.find("${"); + const size_t variableEnd = inStr.find("}", variableBegin); + + if((variableBegin == std::string::npos) || (variableEnd == std::string::npos)) + return inStr; + + std::string prefix = inStr.substr(0, variableBegin); + std::string replace = inStr.substr(variableBegin + 2, variableEnd - (variableBegin + 2)); + std::string suffix = resolvePlaceholders(inStr.substr(variableEnd + 1).c_str()); + + return prefix + mVariables[replace] + suffix; +} + +ThemeData::ThemeData() +{ + mVersion = 0; +} + +void ThemeData::loadFile(std::string system, std::map sysDataMap, const std::string& path) +{ + mPaths.push_back(path); + + ThemeException error; + error.setFiles(mPaths); + + if(!Utils::FileSystem::exists(path)) + throw error << "File does not exist!"; + + mVersion = 0; + mViews.clear(); + + mSystemThemeFolder = system; + + mVariables.clear(); + mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); + + pugi::xml_document doc; + pugi::xml_parse_result res = doc.load_file(path.c_str()); + if(!res) + throw error << "XML parsing error: \n " << res.description(); + + pugi::xml_node root = doc.child("theme"); + if(!root) + throw error << "Missing tag!"; + + // parse version + mVersion = root.child("formatVersion").text().as_float(-404); + if(mVersion == -404) + throw error << " tag missing!\n It's either out of date or you need to add " << CURRENT_THEME_FORMAT_VERSION << " inside your tag."; + + if(mVersion < MINIMUM_THEME_FORMAT_VERSION) + throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; + + if (root.attribute("defaultView")) + mDefaultView = root.attribute("defaultView").as_string(); + + parseVariables(root); + parseIncludes(root); + parseViews(root); + parseFeatures(root); + + MenuTheme = std::shared_ptr(new ThemeMenu(*this)); +} + +std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) +{ + std::string result = path; + + size_t start_pos = result.find("$system"); + if (start_pos == std::string::npos) + return path; + + result.replace(start_pos, 7, systemThemeFolder); + return result; +} + +bool ThemeData::parseSubset(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("subset")) + { + parse = false; + const std::string subsetAttr = node.attribute("subset").as_string(); + const std::string nameAttr = node.attribute("name").as_string(); + + if (subsetAttr == "iconset" || subsetAttr == "gamelistview") + { + if (nameAttr.rfind("1-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (subsetAttr == "systemview") + { + if (nameAttr.rfind("8-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (nameAttr.rfind("2-") != std::string::npos) + { + parse = true; + return parse; + } + /* + if (subsetAttr == "colorset" && nameAttr == mColorset) + { + parse = true; + return parse; + } + if (subsetAttr == "iconset" && nameAttr == mIconset) + { + parse = true; + return parse; + } + if (subsetAttr == "menu" && nameAttr == mMenu) + { + parse = true; + return parse; + } + if (subsetAttr == "systemview" && nameAttr == mSystemview) + { + parse = true; + return parse; + } + if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) + { + parse = true; + return parse; + }*/ + } + + return parse; +} + +void ThemeData::parseIncludes(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) + { + if (!parseSubset(node)) + continue; + + std::string relPath = resolvePlaceholders(node.text().as_string()); + std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); + path = resolveSystemVariable(mSystemThemeFolder, path); + + if (!ResourceManager::getInstance()->fileExists(path)) + { + //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + LOG(LogWarning) << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + continue; + } + + mPaths.push_back(path); + + pugi::xml_document includeDoc; + pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); + if(!result) + throw error << "Error parsing file: \n " << result.description(); + + pugi::xml_node theme = includeDoc.child("theme"); + if(!theme) + throw error << "Missing tag!"; + + parseVariables(theme); + parseIncludes(theme); + parseViews(theme); + parseFeatures(theme); + + mPaths.pop_back(); + } +} + +void ThemeData::parseFeatures(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + for(pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) + { + if(!node.attribute("supported")) + throw error << "Feature missing \"supported\" attribute!"; + + const std::string supportedAttr = node.attribute("supported").as_string(); + + if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) + { + parseViews(node); + } + } +} + +void ThemeData::parseVariables(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + pugi::xml_node variables = root.child("variables"); + + if(!variables) + return; + + for(pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) + { + std::string key = it->name(); + std::string val = it->text().as_string(); + + if (!val.empty()) + mVariables.insert(std::pair(key, val)); + } +} + + +void ThemeData::parseViews(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + // parse views + for (pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) + { + if(!node.attribute("name")) + throw error << "View missing \"name\" attribute!"; + + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + std::string viewKey; + while(off != std::string::npos || prevOff != std::string::npos) + { + viewKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) + { + ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; + parseView(node, view); + + if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) + { + ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; + parseView(node, view); + } + } + } + } +} + + +void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) +{ + ThemeException error; + error.setFiles(mPaths); + + for (pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + { + if (!node.attribute("name")) + { + //throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; + LOG(LogWarning) << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; + continue; + } + + auto elemTypeIt = sElementMap.find(node.name()); + if (elemTypeIt == sElementMap.cend()) + { + //throw error << "Unknown element of type \"" << node.name() << "\"!"; + LOG(LogWarning) << "Unknown element of type \"" << node.name() << "\"!"; + continue; + } + + if (parseRegion(node)) + { + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + parseElement(node, elemTypeIt->second, + view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); + + if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) + view.orderedKeys.push_back(elemKey); + } + } + } +} + +bool ThemeData::parseRegion(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("region")) + { + std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); + + parse = false; + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("region").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + if (elemKey == regionsetting) + { + parse = true; + return parse; + } + } + + } + return parse; + +} + + +void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element) +{ + ThemeException error; + error.setFiles(mPaths); + + element.type = root.name(); + element.extra = root.attribute("extra").as_bool(false); + + for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + { + auto typeIt = typeMap.find(node.name()); + if (typeIt == typeMap.cend()) + { + //throw error << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ")."; + LOG(LogWarning) << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ")."; + continue; + } + + std::string str = resolveSystemVariable(mSystemThemeFolder, resolvePlaceholders(node.text().as_string())); + + switch(typeIt->second) + { + case NORMALIZED_PAIR: + { + size_t divider = str.find(' '); + if (divider == std::string::npos) + { + // throw error << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")"; + LOG(LogWarning) << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")"; + break; + } + + std::string first = str.substr(0, divider); + std::string second = str.substr(divider, std::string::npos); + + Vector2f val((float)atof(first.c_str()), (float)atof(second.c_str())); + + element.properties[node.name()] = val; + break; + } + case STRING: + element.properties[node.name()] = str; + break; + case PATH: + { + std::string path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true); + + if (!ResourceManager::getInstance()->fileExists(path)) + { + std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); + if (ResourceManager::getInstance()->fileExists(rootPath)) + path = rootPath; + } + + if (!ResourceManager::getInstance()->fileExists(path)) + { + std::stringstream ss; + ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda + ss << "could not find file \"" << node.text().get() << "\" "; + if(node.text().get() != path) + ss << "(which resolved to \"" << path << "\") "; + + LOG(LogWarning) << ss.str(); + + auto test = element.properties[node.name()]; + if (test.s.empty()) + element.properties[node.name()] = path; + } + else + element.properties[node.name()] = path; + + break; + } + case COLOR: + element.properties[node.name()] = getHexColor(str.c_str()); + break; + case FLOAT: + { + float floatVal = static_cast(strtod(str.c_str(), 0)); + element.properties[node.name()] = floatVal; + break; + } + + case BOOLEAN: + { + // only look at first char + char first = str[0]; + // 1*, t* (true), T* (True), y* (yes), Y* (YES) + bool boolVal = (first == '1' || first == 't' || first == 'T' || first == 'y' || first == 'Y'); + + element.properties[node.name()] = boolVal; + break; + } + default: + LOG(LogWarning) << "Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name(); + break; // throw error << "Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name(); + } + } +} + +bool ThemeData::hasView(const std::string& view) +{ + auto viewIt = mViews.find(view); + return (viewIt != mViews.cend()); +} + +const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const +{ + auto viewIt = mViews.find(view); + if(viewIt == mViews.cend()) + return NULL; // not found + + auto elemIt = viewIt->second.elements.find(element); + if(elemIt == viewIt->second.elements.cend()) return NULL; + + if(elemIt->second.type != expectedType && !expectedType.empty()) + { + LOG(LogWarning) << " requested mismatched theme type for [" << view << "." << element << "] - expected \"" + << expectedType << "\", got \"" << elemIt->second.type << "\""; + return NULL; + } + + return &elemIt->second; +} + +const std::shared_ptr& ThemeData::getDefault() +{ + static std::shared_ptr theme = nullptr; + if(theme == nullptr) + { + theme = std::shared_ptr(new ThemeData()); + + const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_theme_default.xml"; + if(Utils::FileSystem::exists(path)) + { + try + { + std::map emptyMap; + theme->loadFile("", emptyMap, path); + } catch(ThemeException& e) + { + LOG(LogError) << e.what(); + theme = std::shared_ptr(new ThemeData()); //reset to empty + } + } + } + + return theme; +} + +std::vector ThemeData::makeExtras(const std::shared_ptr& theme, const std::string& view, Window* window) +{ + std::vector comps; + + auto viewIt = theme->mViews.find(view); + if(viewIt == theme->mViews.cend()) + return comps; + + for(auto it = viewIt->second.orderedKeys.cbegin(); it != viewIt->second.orderedKeys.cend(); it++) + { + ThemeElement& elem = viewIt->second.elements.at(*it); + if(elem.extra) + { + GuiComponent* comp = NULL; + const std::string& t = elem.type; + if(t == "image") + comp = new ImageComponent(window); + else if(t == "text") + comp = new TextComponent(window); + + comp->setDefaultZIndex(10); + comp->applyTheme(theme, view, *it, ThemeFlags::ALL); + comps.push_back(comp); + } + } + + return comps; +} + +std::map ThemeData::getThemeSets() +{ + std::map sets; + + static const size_t pathCount = 2; + std::string paths[pathCount] = + { + "/etc/emulationstation/themes", + Utils::FileSystem::getHomePath() + "/.emulationstation/themes" + }; + + for(size_t i = 0; i < pathCount; i++) + { + if(!Utils::FileSystem::isDirectory(paths[i])) + continue; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(paths[i]); + + for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if(Utils::FileSystem::isDirectory(*it)) + { + ThemeSet set = {*it}; + sets[set.getName()] = set; + } + } + } + + return sets; +} + +std::string ThemeData::getThemeFromCurrentSet(const std::string& system) +{ + std::map themeSets = ThemeData::getThemeSets(); + if(themeSets.empty()) + { + // no theme sets available + return ""; + } + + std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if(set == themeSets.cend()) + { + // currently selected theme set is missing, so just pick the first available set + set = themeSets.cbegin(); + Settings::getInstance()->setString("ThemeSet", set->first); + } + + return set->second.getThemePath(system); +} + +ThemeData::ThemeMenu::ThemeMenu(ThemeData& theme) +{ + Title.font = Font::get(FONT_SIZE_LARGE); + Footer.font = Font::get(FONT_SIZE_SMALL); + Text.font = Font::get(FONT_SIZE_MEDIUM); + TextSmall.font = Font::get(FONT_SIZE_SMALL); + + auto elem = theme.getElement("menu", "menubg", "menuBackground"); + if (elem) + { + if (elem->has("path") && ResourceManager::getInstance()->fileExists(elem->get("path"))) + Background.path = elem->get("path"); + + if (elem->has("fadePath") && ResourceManager::getInstance()->fileExists(elem->get("fadePath"))) + Background.fadePath = elem->get("fadePath"); + + if (elem->has("color")) + Background.color = elem->get("color"); + } + + elem = theme.getElement("menu", "menutitle", "menuText"); + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + Title.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_LARGE)); + if (elem->has("color")) + Title.color = elem->get("color"); + } + + elem = theme.getElement("menu", "menufooter", "menuText"); + + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + Footer.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_SMALL)); + if (elem->has("color")) + Footer.color = elem->get("color"); + } + + elem = theme.getElement("menu", "menutextsmall", "menuTextSmall"); + + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + TextSmall.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_SMALL)); + + if (elem->has("color")) + TextSmall.color = elem->get("color"); + if (elem->has("selectedColor")) + Text.selectedColor = elem->get("selectedColor"); + if (elem->has("selectorColor")) + Text.selectedColor = elem->get("selectorColor"); + } + + elem = theme.getElement("menu", "menutext", "menuText"); + + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + Text.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_MEDIUM)); + + if (elem->has("color")) + Text.color = elem->get("color"); + if (elem->has("separatorColor")) + Text.separatorColor = elem->get("separatorColor"); + if (elem->has("selectedColor")) + Text.selectedColor = elem->get("selectedColor"); + if (elem->has("selectorColor")) + Text.selectorColor = elem->get("selectorColor"); + } +} \ No newline at end of file diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index aec760c4c7..27fa912433 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -4,6 +4,7 @@ #include "math/Vector2f.h" #include "utils/FileSystemUtil.h" + #include #include #include @@ -21,6 +22,7 @@ class NinePatchComponent; class Sound; class TextComponent; class Window; +class Font; namespace ThemeFlags { @@ -81,9 +83,31 @@ struct ThemeSet inline std::string getThemePath(const std::string& system) const { return path + "/" + system + "/theme.xml"; } }; +struct MenuElement { + unsigned int color; + unsigned int selectedColor; + unsigned int selectorColor; + unsigned int separatorColor; + std::string path; + std::string fadePath; + std::shared_ptr font; +}; + class ThemeData { public: + class ThemeMenu + { + public: + ThemeMenu(ThemeData& theme); + + MenuElement Background{ 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, ":/frame.png", ":/scroll_gradient.png", nullptr }; + MenuElement Title{ 0x555555FF, 0x555555FF, 0x555555FF, 0xFFFFFFFF, "", "", nullptr }; + MenuElement Text{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, "", "", nullptr }; + MenuElement TextSmall{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, "", "", nullptr }; + MenuElement Footer{ 0xC6C6C6FF, 0xC6C6C6FF, 0xC6C6C6FF, 0xFFFFFFFF, "", "", nullptr }; + }; + class ThemeElement { @@ -130,6 +154,7 @@ class ThemeData std::vector orderedKeys; }; + public: ThemeData(); @@ -161,6 +186,7 @@ class ThemeData std::string getDefaultView() { return mDefaultView; }; + static const std::shared_ptr& getMenuTheme() { return MenuTheme; } private: static std::map< std::string, std::map > sElementMap; @@ -171,7 +197,7 @@ class ThemeData float mVersion; std::string mDefaultView; - + void parseFeatures(const pugi::xml_node& themeRoot); void parseIncludes(const pugi::xml_node& themeRoot); void parseVariables(const pugi::xml_node& root); @@ -180,11 +206,13 @@ class ThemeData void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element); bool parseRegion(const pugi::xml_node& node); bool parseSubset(const pugi::xml_node& node); - + std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); std::map mViews; std::string mSystemThemeFolder; + + static std::shared_ptr MenuTheme; }; #endif // ES_CORE_THEME_DATA_H diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 7a3164f882..87f5118e5b 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -1,531 +1,531 @@ -#include "Window.h" - -#include "components/HelpComponent.h" -#include "components/ImageComponent.h" -#include "resources/Font.h" -#include "resources/TextureResource.h" -#include "InputManager.h" -#include "Log.h" -#include "Renderer.h" -#include "Scripting.h" -#include -#include - -#include - -Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), - mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) -{ - mHelp = new HelpComponent(this); - mBackgroundOverlay = new ImageComponent(this); - mSplash = NULL; - -} - -Window::~Window() -{ - delete mBackgroundOverlay; - - // delete all our GUIs - while(peekGui()) - delete peekGui(); - - delete mHelp; -} - -void Window::pushGui(GuiComponent* gui) -{ - if (mGuiStack.size() > 0) - { - auto& top = mGuiStack.back(); - top->topWindow(false); - } - mGuiStack.push_back(gui); - gui->updateHelpPrompts(); -} - -void Window::removeGui(GuiComponent* gui) -{ - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - { - if(*i == gui) - { - i = mGuiStack.erase(i); - - if(i == mGuiStack.cend() && mGuiStack.size()) // we just popped the stack and the stack is not empty - { - mGuiStack.back()->updateHelpPrompts(); - mGuiStack.back()->topWindow(true); - } - - return; - } - } -} - -GuiComponent* Window::peekGui() -{ - if(mGuiStack.size() == 0) - return NULL; - - return mGuiStack.back(); -} - -bool Window::init() -{ - if(!Renderer::init()) - { - LOG(LogError) << "Renderer failed to initialize!"; - return false; - } - - InputManager::getInstance()->init(); - ResourceManager::getInstance()->reloadAll(); - - //keep a reference to the default fonts, so they don't keep getting destroyed/recreated - if(mDefaultFonts.empty()) - { - mDefaultFonts.push_back(Font::get(FONT_SIZE_SMALL)); - mDefaultFonts.push_back(Font::get(FONT_SIZE_MEDIUM)); - mDefaultFonts.push_back(Font::get(FONT_SIZE_LARGE)); - } - - mBackgroundOverlay->setImage(":/scroll_gradient.png"); - mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - - // update our help because font sizes probably changed - if (peekGui()) - peekGui()->updateHelpPrompts(); - - return true; -} - -void Window::deinit() -{ - // Hide all GUI elements on uninitialisation - this disable - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - { - (*i)->onHide(); - } - InputManager::getInstance()->deinit(); - ResourceManager::getInstance()->unloadAll(); - Renderer::deinit(); -} - -void Window::textInput(const char* text) -{ - if(peekGui()) - peekGui()->textInput(text); -} - -void Window::input(InputConfig* config, Input input) -{ - if (mScreenSaver) { - if(mScreenSaver->isScreenSaverActive() && Settings::getInstance()->getBool("ScreenSaverControls") && - (Settings::getInstance()->getString("ScreenSaverBehavior") == "random video")) - { - if(mScreenSaver->getCurrentGame() != NULL && (config->isMappedLike("right", input) || config->isMappedTo("start", input) || config->isMappedTo("select", input))) - { - if(config->isMappedLike("right", input) || config->isMappedTo("select", input)) - { - if (input.value != 0) { - // handle screensaver control - mScreenSaver->nextVideo(); - } - return; - } - else if(config->isMappedTo("start", input) && input.value != 0) - { - // launch game! - cancelScreenSaver(); - mScreenSaver->launchGame(); - // to force handling the wake up process - mSleeping = true; - } - } - } - } - - if(mSleeping) - { - // wake up - mTimeSinceLastInput = 0; - cancelScreenSaver(); - mSleeping = false; - onWake(); - return; - } - - mTimeSinceLastInput = 0; - if (cancelScreenSaver()) - return; - - if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle debug grid with Ctrl-G - Settings::getInstance()->setBool("DebugGrid", !Settings::getInstance()->getBool("DebugGrid")); - } - else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle TextComponent debug view with Ctrl-T - Settings::getInstance()->setBool("DebugText", !Settings::getInstance()->getBool("DebugText")); - } - else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle TextComponent debug view with Ctrl-I - Settings::getInstance()->setBool("DebugImage", !Settings::getInstance()->getBool("DebugImage")); - } - else - { - if (peekGui()) - { - this->peekGui()->input(config, input); // this is where the majority of inputs will be consumed: the GuiComponent Stack - } - } -} - -void Window::update(int deltaTime) -{ - if(mNormalizeNextUpdate) - { - mNormalizeNextUpdate = false; - if(deltaTime > mAverageDeltaTime) - deltaTime = mAverageDeltaTime; - } - - mFrameTimeElapsed += deltaTime; - mFrameCountElapsed++; - if(mFrameTimeElapsed > 500) - { - mAverageDeltaTime = mFrameTimeElapsed / mFrameCountElapsed; - - if(Settings::getInstance()->getBool("DrawFramerate")) - { - std::stringstream ss; - - // fps - ss << std::fixed << std::setprecision(1) << (1000.0f * (float)mFrameCountElapsed / (float)mFrameTimeElapsed) << "fps, "; - ss << std::fixed << std::setprecision(2) << ((float)mFrameTimeElapsed / (float)mFrameCountElapsed) << "ms"; - - // vram - float textureVramUsageMb = TextureResource::getTotalMemUsage() / 1000.0f / 1000.0f; - float textureTotalUsageMb = TextureResource::getTotalTextureSize() / 1000.0f / 1000.0f; - float fontVramUsageMb = Font::getTotalMemUsage() / 1000.0f / 1000.0f; - - ss << "\nFont VRAM: " << fontVramUsageMb << " Tex VRAM: " << textureVramUsageMb << - " Tex Max: " << textureTotalUsageMb; - mFrameDataText = std::unique_ptr(mDefaultFonts.at(1)->buildTextCache(ss.str(), 50.f, 50.f, 0xFF00FFFF)); - } - - mFrameTimeElapsed = 0; - mFrameCountElapsed = 0; - } - - mTimeSinceLastInput += deltaTime; - - if(peekGui()) - peekGui()->update(deltaTime); - - // Update the screensaver - if (mScreenSaver) - mScreenSaver->update(deltaTime); -} - -void Window::render() -{ - Transform4x4f transform = Transform4x4f::Identity(); - - mRenderedHelpPrompts = false; - - // draw only bottom and top of GuiStack (if they are different) - if(mGuiStack.size()) - { - auto& bottom = mGuiStack.front(); - auto& top = mGuiStack.back(); - - bottom->render(transform); - if(bottom != top) - { - mBackgroundOverlay->render(transform); - top->render(transform); - } - } - - if(!mRenderedHelpPrompts) - mHelp->render(transform); - - if(Settings::getInstance()->getBool("DrawFramerate") && mFrameDataText) - { - Renderer::setMatrix(Transform4x4f::Identity()); - mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); - } - - unsigned int screensaverTime = (unsigned int)Settings::getInstance()->getInt("ScreenSaverTime"); - if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) - startScreenSaver(); - - // Always call the screensaver render function regardless of whether the screensaver is active - // or not because it may perform a fade on transition - renderScreenSaver(); - - if(!mRenderScreenSaver && mInfoPopup) - { - mInfoPopup->render(transform); - } - - if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) - { - if (!isProcessing() && mAllowSleep && (!mScreenSaver || mScreenSaver->allowSleep())) - { - // go to sleep - if (mSleeping == false) { - mSleeping = true; - onSleep(); - } - } - } -} - -void Window::normalizeNextUpdate() -{ - mNormalizeNextUpdate = true; -} - -bool Window::getAllowSleep() -{ - return mAllowSleep; -} - -void Window::setAllowSleep(bool sleep) -{ - mAllowSleep = sleep; -} - -void Window::endRenderLoadingScreen() -{ - mSplash = NULL; -} - -void Window::renderLoadingScreen(std::string text, float percent) -{ -#if defined(_WIN32) - // Avoid Window Freezing on Windows - SDL_Event event; - while (SDL_PollEvent(&event)) ; -#endif - - Transform4x4f trans = Transform4x4f::Identity(); - Renderer::setMatrix(trans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); - - if (percent >= 0) - { - float baseHeight = 0.04f; - - float w = Renderer::getScreenWidth() / 2; - float h = Renderer::getScreenHeight() * baseHeight; - - float x = Renderer::getScreenWidth() / 2 - w / 2; - float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); - - Renderer::drawRect(x, y, w, h, 0x252525FF); - Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF - } - - - ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); - -#if defined(_WIN32) - if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); -#endif - - if (mSplash != NULL) - splash.setImage(mSplash); - else - splash.setImage(":/splash.svg"); - - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); - splash.render(trans); - - - auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); - - float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.78f); // 35 - trans = trans.translate(Vector3f(x, y, 0.0f)); - Renderer::setMatrix(trans); - font->renderTextCache(cache); - delete cache; - - - Renderer::swapBuffers(); -} - -void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) -{ - Transform4x4f trans = Transform4x4f::Identity(); - Renderer::setMatrix(trans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); - - ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); - -#if defined(_WIN32) - if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); -#endif - - if (mSplash != NULL) - splash.setImage(mSplash); - else - splash.setImage(":/splash.svg"); - - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); - splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); - splash.render(trans); - - auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); - - float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.835f); - trans = trans.translate(Vector3f(x, y, 0.0f)); - Renderer::setMatrix(trans); - font->renderTextCache(cache); - delete cache; - - if (swapBuffers) - Renderer::swapBuffers(); -} - - -void Window::renderHelpPromptsEarly() -{ - mHelp->render(Transform4x4f::Identity()); - mRenderedHelpPrompts = true; -} - -void Window::setHelpPrompts(const std::vector& prompts, const HelpStyle& style) -{ - mHelp->clearPrompts(); - mHelp->setStyle(style); - - std::vector addPrompts; - - std::map inputSeenMap; - std::map mappedToSeenMap; - for(auto it = prompts.cbegin(); it != prompts.cend(); it++) - { - // only add it if the same icon hasn't already been added - if(inputSeenMap.emplace(it->first, true).second) - { - // this symbol hasn't been seen yet, what about the action name? - auto mappedTo = mappedToSeenMap.find(it->second); - if(mappedTo != mappedToSeenMap.cend()) - { - // yes, it has! - - // can we combine? (dpad only) - if((it->first == "up/down" && addPrompts.at(mappedTo->second).first != "left/right") || - (it->first == "left/right" && addPrompts.at(mappedTo->second).first != "up/down")) - { - // yes! - addPrompts.at(mappedTo->second).first = "up/down/left/right"; - // don't need to add this to addPrompts since we just merged - }else{ - // no, we can't combine! - addPrompts.push_back(*it); - } - }else{ - // no, it hasn't! - mappedToSeenMap.emplace(it->second, (int)addPrompts.size()); - addPrompts.push_back(*it); - } - } - } - - // sort prompts so it goes [dpad_all] [dpad_u/d] [dpad_l/r] [a/b/x/y/l/r] [start/select] - std::sort(addPrompts.begin(), addPrompts.end(), [](const HelpPrompt& a, const HelpPrompt& b) -> bool { - - static const char* map[] = { - "up/down/left/right", - "up/down", - "left/right", - "a", "b", "x", "y", "l", "r", - "start", "select", - NULL - }; - - int i = 0; - int aVal = 0; - int bVal = 0; - while(map[i] != NULL) - { - if(a.first == map[i]) - aVal = i; - if(b.first == map[i]) - bVal = i; - i++; - } - - return aVal > bVal; - }); - - mHelp->setPrompts(addPrompts); -} - - -void Window::onSleep() -{ - Scripting::fireEvent("sleep"); -} - -void Window::onWake() -{ - Scripting::fireEvent("wake"); -} - -bool Window::isProcessing() -{ - return count_if(mGuiStack.cbegin(), mGuiStack.cend(), [](GuiComponent* c) { return c->isProcessing(); }) > 0; -} - -void Window::startScreenSaver() -{ - if (mScreenSaver && !mRenderScreenSaver) - { - // Tell the GUI components the screensaver is starting - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - (*i)->onScreenSaverActivate(); - - mScreenSaver->startScreenSaver(); - mRenderScreenSaver = true; - } -} - -bool Window::cancelScreenSaver() -{ - if (mScreenSaver && mRenderScreenSaver) - { - mScreenSaver->stopScreenSaver(); - mRenderScreenSaver = false; - mScreenSaver->resetCounts(); - - // Tell the GUI components the screensaver has stopped - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - (*i)->onScreenSaverDeactivate(); - - return true; - } - - return false; -} - -void Window::renderScreenSaver() -{ - if (mScreenSaver) - mScreenSaver->renderScreenSaver(); -} +#include "Window.h" + +#include "components/HelpComponent.h" +#include "components/ImageComponent.h" +#include "resources/Font.h" +#include "resources/TextureResource.h" +#include "InputManager.h" +#include "Log.h" +#include "Renderer.h" +#include "Scripting.h" +#include +#include + +#include + +Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), + mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) +{ + mHelp = new HelpComponent(this); + mBackgroundOverlay = new ImageComponent(this); + mSplash = NULL; + +} + +Window::~Window() +{ + delete mBackgroundOverlay; + + // delete all our GUIs + while(peekGui()) + delete peekGui(); + + delete mHelp; +} + +void Window::pushGui(GuiComponent* gui) +{ + if (mGuiStack.size() > 0) + { + auto& top = mGuiStack.back(); + top->topWindow(false); + } + mGuiStack.push_back(gui); + gui->updateHelpPrompts(); +} + +void Window::removeGui(GuiComponent* gui) +{ + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + { + if(*i == gui) + { + i = mGuiStack.erase(i); + + if(i == mGuiStack.cend() && mGuiStack.size()) // we just popped the stack and the stack is not empty + { + mGuiStack.back()->updateHelpPrompts(); + mGuiStack.back()->topWindow(true); + } + + return; + } + } +} + +GuiComponent* Window::peekGui() +{ + if(mGuiStack.size() == 0) + return NULL; + + return mGuiStack.back(); +} + +bool Window::init() +{ + if(!Renderer::init()) + { + LOG(LogError) << "Renderer failed to initialize!"; + return false; + } + + InputManager::getInstance()->init(); + ResourceManager::getInstance()->reloadAll(); + + //keep a reference to the default fonts, so they don't keep getting destroyed/recreated + if(mDefaultFonts.empty()) + { + mDefaultFonts.push_back(Font::get(FONT_SIZE_SMALL)); + mDefaultFonts.push_back(Font::get(FONT_SIZE_MEDIUM)); + mDefaultFonts.push_back(Font::get(FONT_SIZE_LARGE)); + } + + mBackgroundOverlay->setImage(":/scroll_gradient.png"); + mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + + // update our help because font sizes probably changed + if (peekGui()) + peekGui()->updateHelpPrompts(); + + return true; +} + +void Window::deinit() +{ + // Hide all GUI elements on uninitialisation - this disable + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + { + (*i)->onHide(); + } + InputManager::getInstance()->deinit(); + ResourceManager::getInstance()->unloadAll(); + Renderer::deinit(); +} + +void Window::textInput(const char* text) +{ + if(peekGui()) + peekGui()->textInput(text); +} + +void Window::input(InputConfig* config, Input input) +{ + if (mScreenSaver) { + if(mScreenSaver->isScreenSaverActive() && Settings::getInstance()->getBool("ScreenSaverControls") && + (Settings::getInstance()->getString("ScreenSaverBehavior") == "random video")) + { + if(mScreenSaver->getCurrentGame() != NULL && (config->isMappedLike("right", input) || config->isMappedTo("start", input) || config->isMappedTo("select", input))) + { + if(config->isMappedLike("right", input) || config->isMappedTo("select", input)) + { + if (input.value != 0) { + // handle screensaver control + mScreenSaver->nextVideo(); + } + return; + } + else if(config->isMappedTo("start", input) && input.value != 0) + { + // launch game! + cancelScreenSaver(); + mScreenSaver->launchGame(); + // to force handling the wake up process + mSleeping = true; + } + } + } + } + + if(mSleeping) + { + // wake up + mTimeSinceLastInput = 0; + cancelScreenSaver(); + mSleeping = false; + onWake(); + return; + } + + mTimeSinceLastInput = 0; + if (cancelScreenSaver()) + return; + + if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle debug grid with Ctrl-G + Settings::getInstance()->setBool("DebugGrid", !Settings::getInstance()->getBool("DebugGrid")); + } + else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle TextComponent debug view with Ctrl-T + Settings::getInstance()->setBool("DebugText", !Settings::getInstance()->getBool("DebugText")); + } + else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle TextComponent debug view with Ctrl-I + Settings::getInstance()->setBool("DebugImage", !Settings::getInstance()->getBool("DebugImage")); + } + else + { + if (peekGui()) + { + this->peekGui()->input(config, input); // this is where the majority of inputs will be consumed: the GuiComponent Stack + } + } +} + +void Window::update(int deltaTime) +{ + if(mNormalizeNextUpdate) + { + mNormalizeNextUpdate = false; + if(deltaTime > mAverageDeltaTime) + deltaTime = mAverageDeltaTime; + } + + mFrameTimeElapsed += deltaTime; + mFrameCountElapsed++; + if(mFrameTimeElapsed > 500) + { + mAverageDeltaTime = mFrameTimeElapsed / mFrameCountElapsed; + + if(Settings::getInstance()->getBool("DrawFramerate")) + { + std::stringstream ss; + + // fps + ss << std::fixed << std::setprecision(1) << (1000.0f * (float)mFrameCountElapsed / (float)mFrameTimeElapsed) << "fps, "; + ss << std::fixed << std::setprecision(2) << ((float)mFrameTimeElapsed / (float)mFrameCountElapsed) << "ms"; + + // vram + float textureVramUsageMb = TextureResource::getTotalMemUsage() / 1000.0f / 1000.0f; + float textureTotalUsageMb = TextureResource::getTotalTextureSize() / 1000.0f / 1000.0f; + float fontVramUsageMb = Font::getTotalMemUsage() / 1000.0f / 1000.0f; + + ss << "\nFont VRAM: " << fontVramUsageMb << " Tex VRAM: " << textureVramUsageMb << + " Tex Max: " << textureTotalUsageMb; + mFrameDataText = std::unique_ptr(mDefaultFonts.at(1)->buildTextCache(ss.str(), 50.f, 50.f, 0xFF00FFFF)); + } + + mFrameTimeElapsed = 0; + mFrameCountElapsed = 0; + } + + mTimeSinceLastInput += deltaTime; + + if(peekGui()) + peekGui()->update(deltaTime); + + // Update the screensaver + if (mScreenSaver) + mScreenSaver->update(deltaTime); +} + +void Window::render() +{ + Transform4x4f transform = Transform4x4f::Identity(); + + mRenderedHelpPrompts = false; + + // draw only bottom and top of GuiStack (if they are different) + if(mGuiStack.size()) + { + auto& bottom = mGuiStack.front(); + auto& top = mGuiStack.back(); + + bottom->render(transform); + if(bottom != top) + { + mBackgroundOverlay->render(transform); + top->render(transform); + } + } + + if(!mRenderedHelpPrompts) + mHelp->render(transform); + + if(Settings::getInstance()->getBool("DrawFramerate") && mFrameDataText) + { + Renderer::setMatrix(Transform4x4f::Identity()); + mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); + } + + unsigned int screensaverTime = (unsigned int)Settings::getInstance()->getInt("ScreenSaverTime"); + if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) + startScreenSaver(); + + // Always call the screensaver render function regardless of whether the screensaver is active + // or not because it may perform a fade on transition + renderScreenSaver(); + + if(!mRenderScreenSaver && mInfoPopup) + { + mInfoPopup->render(transform); + } + + if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) + { + if (!isProcessing() && mAllowSleep && (!mScreenSaver || mScreenSaver->allowSleep())) + { + // go to sleep + if (mSleeping == false) { + mSleeping = true; + onSleep(); + } + } + } +} + +void Window::normalizeNextUpdate() +{ + mNormalizeNextUpdate = true; +} + +bool Window::getAllowSleep() +{ + return mAllowSleep; +} + +void Window::setAllowSleep(bool sleep) +{ + mAllowSleep = sleep; +} + +void Window::endRenderLoadingScreen() +{ + mSplash = NULL; +} + +void Window::renderLoadingScreen(std::string text, float percent) +{ +#if defined(_WIN32) + // Avoid Window Freezing on Windows + SDL_Event event; + while (SDL_PollEvent(&event)) ; +#endif + + Transform4x4f trans = Transform4x4f::Identity(); + Renderer::setMatrix(trans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); + + if (percent >= 0) + { + float baseHeight = 0.04f; + + float w = Renderer::getScreenWidth() / 2; + float h = Renderer::getScreenHeight() * baseHeight; + + float x = Renderer::getScreenWidth() / 2 - w / 2; + float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); + + Renderer::drawRect(x, y, w, h, 0x252525FF); + Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF + } + + + ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.render(trans); + + + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); + + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.78f); // 35 + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; + + + Renderer::swapBuffers(); +} + +void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) +{ + Transform4x4f trans = Transform4x4f::Identity(); + Renderer::setMatrix(trans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); + + ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); + splash.render(trans); + + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); + + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.835f); + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; + + if (swapBuffers) + Renderer::swapBuffers(); +} + + +void Window::renderHelpPromptsEarly() +{ + mHelp->render(Transform4x4f::Identity()); + mRenderedHelpPrompts = true; +} + +void Window::setHelpPrompts(const std::vector& prompts, const HelpStyle& style) +{ + mHelp->clearPrompts(); + mHelp->setStyle(style); + + std::vector addPrompts; + + std::map inputSeenMap; + std::map mappedToSeenMap; + for(auto it = prompts.cbegin(); it != prompts.cend(); it++) + { + // only add it if the same icon hasn't already been added + if(inputSeenMap.emplace(it->first, true).second) + { + // this symbol hasn't been seen yet, what about the action name? + auto mappedTo = mappedToSeenMap.find(it->second); + if(mappedTo != mappedToSeenMap.cend()) + { + // yes, it has! + + // can we combine? (dpad only) + if((it->first == "up/down" && addPrompts.at(mappedTo->second).first != "left/right") || + (it->first == "left/right" && addPrompts.at(mappedTo->second).first != "up/down")) + { + // yes! + addPrompts.at(mappedTo->second).first = "up/down/left/right"; + // don't need to add this to addPrompts since we just merged + }else{ + // no, we can't combine! + addPrompts.push_back(*it); + } + }else{ + // no, it hasn't! + mappedToSeenMap.emplace(it->second, (int)addPrompts.size()); + addPrompts.push_back(*it); + } + } + } + + // sort prompts so it goes [dpad_all] [dpad_u/d] [dpad_l/r] [a/b/x/y/l/r] [start/select] + std::sort(addPrompts.begin(), addPrompts.end(), [](const HelpPrompt& a, const HelpPrompt& b) -> bool { + + static const char* map[] = { + "up/down/left/right", + "up/down", + "left/right", + "a", "b", "x", "y", "l", "r", + "start", "select", + NULL + }; + + int i = 0; + int aVal = 0; + int bVal = 0; + while(map[i] != NULL) + { + if(a.first == map[i]) + aVal = i; + if(b.first == map[i]) + bVal = i; + i++; + } + + return aVal > bVal; + }); + + mHelp->setPrompts(addPrompts); +} + + +void Window::onSleep() +{ + Scripting::fireEvent("sleep"); +} + +void Window::onWake() +{ + Scripting::fireEvent("wake"); +} + +bool Window::isProcessing() +{ + return count_if(mGuiStack.cbegin(), mGuiStack.cend(), [](GuiComponent* c) { return c->isProcessing(); }) > 0; +} + +void Window::startScreenSaver() +{ + if (mScreenSaver && !mRenderScreenSaver) + { + // Tell the GUI components the screensaver is starting + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + (*i)->onScreenSaverActivate(); + + mScreenSaver->startScreenSaver(); + mRenderScreenSaver = true; + } +} + +bool Window::cancelScreenSaver() +{ + if (mScreenSaver && mRenderScreenSaver) + { + mScreenSaver->stopScreenSaver(); + mRenderScreenSaver = false; + mScreenSaver->resetCounts(); + + // Tell the GUI components the screensaver has stopped + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + (*i)->onScreenSaverDeactivate(); + + return true; + } + + return false; +} + +void Window::renderScreenSaver() +{ + if (mScreenSaver) + mScreenSaver->renderScreenSaver(); +} diff --git a/es-core/src/components/ButtonComponent.cpp b/es-core/src/components/ButtonComponent.cpp index 9496046419..08eeeb34bc 100644 --- a/es-core/src/components/ButtonComponent.cpp +++ b/es-core/src/components/ButtonComponent.cpp @@ -11,6 +11,13 @@ ButtonComponent::ButtonComponent(Window* window, const std::string& text, const mEnabled(true), mTextColorFocused(0xFFFFFFFF), mTextColorUnfocused(0x777777FF) { + auto menuTheme = ThemeData::getMenuTheme(); + + mFont = menuTheme->Text.font; + mTextColorUnfocused = menuTheme->Text.color; + mTextColorFocused = menuTheme->Text.selectedColor; + mColor = menuTheme->Text.color; + setPressedFunc(func); setText(text, helpText); updateImage(); @@ -71,6 +78,7 @@ void ButtonComponent::setEnabled(bool enabled) void ButtonComponent::updateImage() { + /* if(!mEnabled || !mPressedFunc) { mBox.setImagePath(":/button_filled.png"); @@ -78,9 +86,9 @@ void ButtonComponent::updateImage() mBox.setEdgeColor(0x770000FF); return; } - - mBox.setCenterColor(0xFFFFFFFF); - mBox.setEdgeColor(0xFFFFFFFF); + */ + mBox.setCenterColor(mColor); + mBox.setEdgeColor(mColor); mBox.setImagePath(mFocused ? ":/button_filled.png" : ":/button.png"); } diff --git a/es-core/src/components/ButtonComponent.h b/es-core/src/components/ButtonComponent.h index 65288209cd..790bbc6d43 100644 --- a/es-core/src/components/ButtonComponent.h +++ b/es-core/src/components/ButtonComponent.h @@ -38,7 +38,7 @@ class ButtonComponent : public GuiComponent bool mEnabled; unsigned int mTextColorFocused; unsigned int mTextColorUnfocused; - + unsigned int getCurTextColor() const; void updateImage(); @@ -46,6 +46,7 @@ class ButtonComponent : public GuiComponent std::string mHelpText; std::unique_ptr mTextCache; NinePatchComponent mBox; + unsigned int mColor; }; #endif // ES_CORE_COMPONENTS_BUTTON_COMPONENT_H diff --git a/es-core/src/components/ComponentGrid.cpp b/es-core/src/components/ComponentGrid.cpp index e89e7f9c4e..7ba5b0c1c3 100644 --- a/es-core/src/components/ComponentGrid.cpp +++ b/es-core/src/components/ComponentGrid.cpp @@ -5,11 +5,12 @@ using namespace GridFlags; -ComponentGrid::ComponentGrid(Window* window, const Vector2i& gridDimensions) : GuiComponent(window), +ComponentGrid::ComponentGrid(Window* window, const Vector2i& gridDimensions, unsigned int separatorColor) : GuiComponent(window), mGridSize(gridDimensions), mCursor(0, 0) { assert(gridDimensions.x() > 0 && gridDimensions.y() > 0); + mSeparatorColor = separatorColor; mCells.reserve(gridDimensions.x() * gridDimensions.y()); mColWidths = new float[gridDimensions.x()]; @@ -195,7 +196,7 @@ void ComponentGrid::updateSeparators() } mLineColors.reserve(mLines.size()); - Renderer::buildGLColorArray((GLubyte*)mLineColors.data(), 0xC6C7C6FF, (unsigned int)mLines.size()); + Renderer::buildGLColorArray((GLubyte*)mLineColors.data(), mSeparatorColor, (unsigned int)mLines.size()); } void ComponentGrid::onSizeChanged() diff --git a/es-core/src/components/ComponentGrid.h b/es-core/src/components/ComponentGrid.h index aa6519175f..15fe8e76df 100644 --- a/es-core/src/components/ComponentGrid.h +++ b/es-core/src/components/ComponentGrid.h @@ -29,9 +29,11 @@ namespace GridFlags class ComponentGrid : public GuiComponent { public: - ComponentGrid(Window* window, const Vector2i& gridDimensions); + ComponentGrid(Window* window, const Vector2i& gridDimensions, unsigned int separatorColor = 0xC6C7C6FF); virtual ~ComponentGrid(); + void setSeparatorColor(unsigned int separatorColor) { mSeparatorColor = separatorColor; updateSeparators(); } + bool removeEntry(const std::shared_ptr& comp); void setEntry(const std::shared_ptr& comp, const Vector2i& pos, bool canFocus, bool resize = true, @@ -105,6 +107,7 @@ class ComponentGrid : public GuiComponent std::vector mLines; std::vector mLineColors; + unsigned int mSeparatorColor; // Update position & size void updateCellComponent(const GridEntry& cell); diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index f8f1bcd518..4befbde724 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -135,7 +135,7 @@ void ComponentList::updateCameraOffset() { // move the camera to scroll const float totalHeight = getTotalRowHeight(); - if(totalHeight > mSize.y()) + if (totalHeight > mSize.y() && mCursor < mEntries.size()) { float target = mSelectorBarOffset + getRowHeight(mEntries.at(mCursor).data)/2 - (mSize.y() / 2); @@ -162,10 +162,17 @@ void ComponentList::render(const Transform4x4f& parentTrans) if(!size()) return; + auto menuTheme = ThemeData::getMenuTheme(); + unsigned int selectorColor = menuTheme->Text.selectorColor; + unsigned int selectedColor = menuTheme->Text.selectedColor; + unsigned int bgColor = menuTheme->Background.color; + unsigned int separatorColor = menuTheme->Text.separatorColor; + unsigned int textColor = menuTheme->Text.color; + Transform4x4f trans = parentTrans * getTransform(); trans.round(); - // clip everything to be inside our bounds + // clip everything to be inside our boundsz Vector3f dim(mSize.x(), mSize.y(), 0); dim = trans * dim - trans.translation(); Renderer::pushClipRect(Vector2i((int)trans.translation().x(), (int)trans.translation().y()), @@ -183,10 +190,11 @@ void ComponentList::render(const Transform4x4f& parentTrans) drawAll = !mFocused || i != (unsigned int)mCursor; for(auto it = entry.data.elements.cbegin(); it != entry.data.elements.cend(); it++) { - if(drawAll || it->invert_when_selected) + if (drawAll || it->invert_when_selected) { + it->component->setColor(textColor); it->component->render(trans); - }else{ + } else { drawAfterCursor.push_back(it->component.get()); } } @@ -196,7 +204,7 @@ void ComponentList::render(const Transform4x4f& parentTrans) Renderer::setMatrix(trans); // draw selector bar - if(mFocused) + if (mFocused) { // inversion: src * (1 - dst) + dst * 0 = where src = 1 // need a function that goes roughly 0x777777 -> 0xFFFFFF @@ -204,20 +212,33 @@ void ComponentList::render(const Transform4x4f& parentTrans) // (1 - dst) + 0x77 const float selectedRowHeight = getRowHeight(mEntries.at(mCursor).data); - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0xFFFFFFFF, - GL_ONE_MINUS_DST_COLOR, GL_ZERO); - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0x777777FF, - GL_ONE, GL_ONE); - + + if ((selectorColor != bgColor) && ((selectorColor & 0xFF) != 0x00)) { + //Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, GL_ONE_MINUS_DST_COLOR, GL_ZERO); + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, GL_ZERO, GL_ONE_MINUS_SRC_COLOR); + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, GL_ONE, GL_ONE); + } + + // Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0xFFFFFFFF, GL_ONE_MINUS_DST_COLOR, GL_ZERO); + // Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0x777777FF, GL_ONE, GL_ONE); + // hack to draw 2px dark on left/right of the bar - Renderer::drawRect(0.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF); - Renderer::drawRect(mSize.x() - 2.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF); + //Renderer::drawRect(0.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF); + //Renderer::drawRect(mSize.x() - 2.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF); + + auto& entry = mEntries.at(mCursor); + for (auto& element : entry.data.elements) + { + element.component->setColor(selectedColor); + drawAfterCursor.push_back(element.component.get()); + } + for(auto it = drawAfterCursor.cbegin(); it != drawAfterCursor.cend(); it++) (*it)->render(trans); // reset matrix if one of these components changed it - if(drawAfterCursor.size()) + if (drawAfterCursor.size()) Renderer::setMatrix(trans); } @@ -225,10 +246,10 @@ void ComponentList::render(const Transform4x4f& parentTrans) float y = 0; for(unsigned int i = 0; i < mEntries.size(); i++) { - Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, 0xC6C7C6FF); + Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, separatorColor); y += getRowHeight(mEntries.at(i).data); } - Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, 0xC6C7C6FF); + Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, separatorColor); Renderer::popClipRect(); } diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index cccf549cfe..ae85435063 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -13,6 +13,8 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mSelectedZoomPercent = 1.0f; mAnimPosition = Vector3f(0, 0); + mLabelMerged = false; + mDefaultProperties.mSize = getDefaultTileSize(); mDefaultProperties.mPadding = Vector2f(16.0f, 16.0f); mDefaultProperties.mImageColor = 0xFFFFFFDD; // 0xAAAAAABB; @@ -57,11 +59,20 @@ void GridTileComponent::resize() setSize(currentProperties.mSize); float height = currentProperties.mSize.y() * mLabelSize.y(); - if (!mLabelVisible) - height = 0; - mLabel.setPosition(0, mSize.y() - height); - mLabel.setSize(currentProperties.mSize.x(), height); + if (mLabelMerged) + { + mLabel.setPosition(currentProperties.mPadding.x(), mSize.y() - height - currentProperties.mPadding.y()); + mLabel.setSize(currentProperties.mSize.x() - 2 * currentProperties.mPadding.x(), height); + } + else + { + mLabel.setPosition(0, mSize.y() - height); + mLabel.setSize(currentProperties.mSize.x(), height); + } + + if (!mLabelVisible || mLabelMerged) + height = 0; if (mLabelSize.x() == 0) height = 0; @@ -90,7 +101,8 @@ void GridTileComponent::resize() } else mBackground.setPosition(0, 0); - + + if (mImage != NULL) { mImage->setOrigin(0.5f, 0.5f); @@ -106,8 +118,11 @@ void GridTileComponent::resize() + if (!mLabelMerged && currentProperties.mImageSizeMode == "minSize") + mBackground.setSize(currentProperties.mSize.x(), currentProperties.mSize.y() - bottomPadding); + else + mBackground.setSize(currentProperties.mSize); - mBackground.setSize(currentProperties.mSize); mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); } @@ -118,14 +133,26 @@ void GridTileComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = getTransform() * parentTrans; + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + //Renderer::setMatrix(trans); //Renderer::drawRect(0.f, 0.f, mSize.x(), mSize.y(), 0xFF0000FF); - - mBackground.render(trans); + + if (mBackground.getCornerSize().x() == 0) + { + + Renderer::setMatrix(trans); + Renderer::drawRect(mBackground.getPosition().x(), mBackground.getPosition().y(), mBackground.getSize().x(), mBackground.getSize().y(), mBackground.getCenterColor()); + Renderer::setMatrix(parentTrans); + } + else + mBackground.render(trans); if (mImage != NULL) mImage->render(trans); - + if (mLabelVisible) mLabel.render(trans); } @@ -155,7 +182,7 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons // Apply theme to the default gridtile const ThemeData::ThemeElement* elem = theme->getElement(view, "default", "gridtile"); if (elem) - { + { if (elem->has("size")) mDefaultProperties.mSize = elem->get("size") * screen; @@ -232,7 +259,10 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem != NULL) { if (elem && elem->has("size")) + { mLabelSize = elem->get("size"); + mLabelMerged = mLabelSize.x() == 0; + } mLabelVisible = true; mLabel.applyTheme(theme, view, element, properties); @@ -264,7 +294,7 @@ void GridTileComponent::setImage(const std::string& path, std::string name) { if (mCurrentPath != path) { - mCurrentPath = path; + mCurrentPath = path; mImage->setImage(path); } diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index bdaa611a16..46fc6c4364 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -49,6 +49,7 @@ class GridTileComponent : public GuiComponent Vector2f mLabelSize; bool mLabelVisible; + bool mLabelMerged; NinePatchComponent mBackground; diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 8358ee6839..3ec03372de 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -25,6 +25,7 @@ ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : G mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f) { updateColors(); + mSizeChanged = false; } ImageComponent::~ImageComponent() @@ -33,17 +34,22 @@ ImageComponent::~ImageComponent() void ImageComponent::resize() { - if(!mTexture) + if (!mTexture) return; const Vector2f textureSize = mTexture->getSourceImageSize(); - if(textureSize == Vector2f::Zero()) + if (textureSize == Vector2f::Zero()) return; - if(mTexture->isTiled()) + int ox = mSize.x(); + int oy = mSize.y(); + + if (mTexture->isTiled()) { mSize = mTargetSize; - }else{ + } + else + { // SVG rasterization is determined by height (see SVGResource.cpp), and rasterization is done in terms of pixels // if rounding is off enough in the rasterization step (for images with extreme aspect ratios), it can cause cutoff when the aspect ratio breaks // so, we always make sure the resultant height is an integer to make sure cutoff doesn't happen, and scale width from that @@ -115,8 +121,12 @@ void ImageComponent::resize() mSize[0] = Math::round(mSize.x()); mSize[1] = Math::round(mSize.y()); + + if (ox != mSize.x() || oy != mSize.y()) + mSizeChanged = true; + // mSize.y() should already be rounded - mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); + //mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); onSizeChanged(); } @@ -323,13 +333,23 @@ void ImageComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); Renderer::setMatrix(trans); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + if (mTexture && mOpacity > 0) { + if (mSizeChanged) + { + mSizeChanged = false; + mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); + } + if (Settings::getInstance()->getBool("DebugImage")) { Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; Renderer::drawRect(targetSizePos.x(), targetSizePos.y(), mTargetSize.x(), mTargetSize.y(), 0xFF000033); - Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); } if (mTexture->isInitialized()) diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 7a93a0d851..3f5ce3138a 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -57,6 +57,8 @@ class ImageComponent : public GuiComponent // Multiply all pixels in the image by this color when rendering. void setColorShift(unsigned int color); + virtual void setColor(unsigned int color) { setColorShift(color); } + void setFlipX(bool flip); // Mirror on the X axis. void setFlipY(bool flip); // Mirror on the Y axis. @@ -108,6 +110,8 @@ class ImageComponent : public GuiComponent Vector2f mTopLeftCrop; Vector2f mBottomRightCrop; + + bool mSizeChanged; }; #endif // ES_CORE_COMPONENTS_IMAGE_COMPONENT_H diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 343c86f618..f891c04490 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -10,16 +10,20 @@ MenuComponent::MenuComponent(Window* window, std::string title, const std::shared_ptr& titleFont) : GuiComponent(window), mBackground(window), mGrid(window, Vector2i(1, 3)) { + auto theme = ThemeData::getMenuTheme(); + addChild(&mBackground); addChild(&mGrid); - - mBackground.setImagePath(":/frame.png"); + + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); // set up title mTitle = std::make_shared(mWindow); mTitle->setHorizontalAlignment(ALIGN_CENTER); - mTitle->setColor(0x555555FF); - setTitle(title.c_str(), titleFont); + mTitle->setColor(theme->Title.color); // 0x555555FF + setTitle(title.c_str(), theme->Title.font); // titleFont mGrid.setEntry(mTitle, Vector2i(0, 0), false); // set up list which will never change (externally, anyway) @@ -32,6 +36,16 @@ MenuComponent::MenuComponent(Window* window, std::string title, const std::share mGrid.resetCursor(); } +void MenuComponent::addWithLabel(const std::string& label, const std::shared_ptr& comp, bool setCursorHere, bool invert_when_selected) +{ + auto theme = ThemeData::getMenuTheme(); + + ComponentListRow row; + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(label), theme->Text.font, theme->Text.color), true); + row.addElement(comp, false, invert_when_selected); + addRow(row, setCursorHere); +} + void MenuComponent::setTitle(const char* title, const std::shared_ptr& font) { mTitle->setText(Utils::String::toUpper(title)); @@ -40,7 +54,9 @@ void MenuComponent::setTitle(const char* title, const std::shared_ptr& fon float MenuComponent::getButtonGridHeight() const { - return (mButtonGrid ? mButtonGrid->getSize().y() : Font::get(FONT_SIZE_MEDIUM)->getHeight() + BUTTON_GRID_VERT_PADDING); + auto menuTheme = ThemeData::getMenuTheme(); + return (mButtonGrid ? mButtonGrid->getSize().y() : menuTheme->Text.font->getHeight() + BUTTON_GRID_VERT_PADDING); + //return (mButtonGrid ? mButtonGrid->getSize().y() : Font::get(FONT_SIZE_MEDIUM)->getHeight() + BUTTON_GRID_VERT_PADDING); } void MenuComponent::updateSize() @@ -126,8 +142,12 @@ std::shared_ptr makeButtonGrid(Window* window, const std::vector< std::shared_ptr makeArrow(Window* window) { + auto menuTheme = ThemeData::getMenuTheme(); + auto bracket = std::make_shared(window); bracket->setImage(":/arrow.svg"); - bracket->setResize(0, Math::round(Font::get(FONT_SIZE_MEDIUM)->getLetterHeight())); + bracket->setColorShift(menuTheme->Text.color); + bracket->setResize(0, round(menuTheme->Text.font->getLetterHeight())); + //bracket->setResize(0, Math::round(Font::get(FONT_SIZE_MEDIUM)->getLetterHeight())); return bracket; } diff --git a/es-core/src/components/MenuComponent.h b/es-core/src/components/MenuComponent.h index e2c9ea31f2..d5a8db473c 100644 --- a/es-core/src/components/MenuComponent.h +++ b/es-core/src/components/MenuComponent.h @@ -26,13 +26,7 @@ class MenuComponent : public GuiComponent inline void addRow(const ComponentListRow& row, bool setCursorHere = false) { mList->addRow(row, setCursorHere); updateSize(); } - inline void addWithLabel(const std::string& label, const std::shared_ptr& comp, bool setCursorHere = false, bool invert_when_selected = true) - { - ComponentListRow row; - row.addElement(std::make_shared(mWindow, Utils::String::toUpper(label), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(comp, false, invert_when_selected); - addRow(row, setCursorHere); - } + void addWithLabel(const std::string& label, const std::shared_ptr& comp, bool setCursorHere = false, bool invert_when_selected = true); void addButton(const std::string& label, const std::string& helpText, const std::function& callback); diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index fedf95115b..f3ff0bba01 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -106,6 +106,10 @@ void NinePatchComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); trans.round(); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + if(mTexture && mVertices != NULL) { Renderer::setMatrix(trans); @@ -166,18 +170,27 @@ void NinePatchComponent::fitTo(Vector2f size, Vector3f position, Vector2f paddin void NinePatchComponent::setImagePath(const std::string& path) { + if (mPath == path) + return; + mPath = path; buildVertices(); } void NinePatchComponent::setEdgeColor(unsigned int edgeColor) { + if (mEdgeColor == edgeColor) + return; + mEdgeColor = edgeColor; updateColors(); } void NinePatchComponent::setCenterColor(unsigned int centerColor) { + if (mCenterColor == centerColor) + return; + mCenterColor = centerColor; updateColors(); } diff --git a/es-core/src/components/NinePatchComponent.h b/es-core/src/components/NinePatchComponent.h index af9e3b9343..8d0dbf697e 100644 --- a/es-core/src/components/NinePatchComponent.h +++ b/es-core/src/components/NinePatchComponent.h @@ -35,6 +35,8 @@ class NinePatchComponent : public GuiComponent void setEdgeColor(unsigned int edgeColor); // Apply a color shift to the "edge" parts of the ninepatch. void setCenterColor(unsigned int centerColor); // Apply a color shift to the "center" part of the ninepatch. + unsigned int getCenterColor() { return mCenterColor; }; + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; const Vector2f& getCornerSize() const; diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index f2d36950d0..e1dc87eb7a 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -39,7 +39,10 @@ class OptionListComponent : public GuiComponent OptionListPopup(Window* window, OptionListComponent* parent, const std::string& title) : GuiComponent(window), mMenu(window, title.c_str()), mParent(parent) { - auto font = Font::get(FONT_SIZE_MEDIUM); + auto menuTheme = ThemeData::getMenuTheme(); + auto font = menuTheme->Text.font; + auto color = menuTheme->Text.color; + ComponentListRow row; // for select all/none @@ -48,7 +51,7 @@ class OptionListComponent : public GuiComponent for(auto it = mParent->mEntries.begin(); it != mParent->mEntries.end(); it++) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, 0x777777FF), true); + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, color), true); OptionListData& e = *it; @@ -87,11 +90,11 @@ class OptionListComponent : public GuiComponent mMenu.addRow(row, (!mParent->mMultiSelect && it->selected)); } - mMenu.addButton(_T("BACK"), "accept", [this] { delete this; }); + mMenu.addButton(_T("BACK"), _T("accept"), [this] { delete this; }); if(mParent->mMultiSelect) { - mMenu.addButton("SELECT ALL", "select all", [this, checkboxes] { + mMenu.addButton(_T("SELECT ALL"), _T("SELECT ALL"), [this, checkboxes] { for(unsigned int i = 0; i < mParent->mEntries.size(); i++) { mParent->mEntries.at(i).selected = true; @@ -100,7 +103,7 @@ class OptionListComponent : public GuiComponent mParent->onSelectedChanged(); }); - mMenu.addButton("SELECT NONE", "select none", [this, checkboxes] { + mMenu.addButton(_T("SELECT NONE"), _T("SELECT NONE"), [this, checkboxes] { for(unsigned int i = 0; i < mParent->mEntries.size(); i++) { mParent->mEntries.at(i).selected = false; @@ -110,7 +113,10 @@ class OptionListComponent : public GuiComponent }); } - mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); + mMenu.setPosition( + (Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, + (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); + //Renderer::getScreenHeight() * 0.15f); addChild(&mMenu); } @@ -137,29 +143,42 @@ class OptionListComponent : public GuiComponent OptionListComponent(Window* window, const std::string& name, bool multiSelect = false) : GuiComponent(window), mMultiSelect(multiSelect), mName(name), mText(window), mLeftArrow(window), mRightArrow(window) { - auto font = Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT); - mText.setFont(font); - mText.setColor(0x777777FF); + auto theme = ThemeData::getMenuTheme(); + + // auto font = Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT); + mText.setFont(theme->Text.font); // font + mText.setColor(theme->Text.color); // 0x777777FF mText.setHorizontalAlignment(ALIGN_CENTER); addChild(&mText); mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); - if(mMultiSelect) + if (mMultiSelect) { mRightArrow.setImage(":/arrow.svg"); + mRightArrow.setColorShift(theme->Text.color); addChild(&mRightArrow); - }else{ + } else { mLeftArrow.setImage(":/option_arrow.svg"); + mLeftArrow.setColorShift(theme->Text.color); mLeftArrow.setFlipX(true); addChild(&mLeftArrow); mRightArrow.setImage(":/option_arrow.svg"); + mLeftArrow.setColorShift(theme->Text.color); addChild(&mRightArrow); } - setSize(mLeftArrow.getSize().x() + mRightArrow.getSize().x(), font->getHeight()); + setSize(mLeftArrow.getSize().x() + mRightArrow.getSize().x(), theme->Text.font->getHeight()); + } + + + virtual void setColor(unsigned int color) + { + mText.setColor(color); + mLeftArrow.setColorShift(color); + mRightArrow.setColorShift(color); } // handles positioning/resizing of text and arrows diff --git a/es-core/src/components/SliderComponent.cpp b/es-core/src/components/SliderComponent.cpp index 81f97e9b09..a8e9657100 100644 --- a/es-core/src/components/SliderComponent.cpp +++ b/es-core/src/components/SliderComponent.cpp @@ -14,10 +14,20 @@ SliderComponent::SliderComponent(Window* window, float min, float max, float inc // some sane default value mValue = (max + min) / 2; + auto menuTheme = ThemeData::getMenuTheme(); + mColor = menuTheme->Text.color; + mKnob.setOrigin(0.5f, 0.5f); mKnob.setImage(":/slider_knob.svg"); - - setSize(Renderer::getScreenWidth() * 0.15f, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight()); + mKnob.setColorShift(mColor); + + setSize(Renderer::getScreenWidth() * 0.15f, menuTheme->Text.font->getLetterHeight()); +} + +void SliderComponent::setColor(unsigned int color) { + mColor = color; + mKnob.setColorShift(mColor); + onValueChanged(); } bool SliderComponent::input(InputConfig* config, Input input) @@ -73,7 +83,7 @@ void SliderComponent::render(const Transform4x4f& parentTrans) //render line const float lineWidth = 2; - Renderer::drawRect(mKnob.getSize().x() / 2, mSize.y() / 2 - lineWidth / 2, width, lineWidth, 0x777777FF); + Renderer::drawRect(mKnob.getSize().x() / 2, mSize.y() / 2 - lineWidth / 2, width, lineWidth, mColor); //render knob mKnob.render(trans); @@ -108,7 +118,7 @@ void SliderComponent::onSizeChanged() void SliderComponent::onValueChanged() { // update suffix textcache - if(mFont) + if (mFont) { std::stringstream ss; ss << std::fixed; @@ -126,7 +136,7 @@ void SliderComponent::onValueChanged() const std::string max = ss.str(); Vector2f textSize = mFont->sizeText(max); - mValueCache = std::shared_ptr(mFont->buildTextCache(val, mSize.x() - textSize.x(), (mSize.y() - textSize.y()) / 2, 0x777777FF)); + mValueCache = std::shared_ptr(mFont->buildTextCache(val, mSize.x() - textSize.x(), (mSize.y() - textSize.y()) / 2, mColor)); mValueCache->metrics.size[0] = textSize.x(); // fudge the width } @@ -139,6 +149,6 @@ void SliderComponent::onValueChanged() std::vector SliderComponent::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("left/right", "MODIFIER")); + prompts.push_back(HelpPrompt("left/right", _T("CHANGE"))); return prompts; } diff --git a/es-core/src/components/SliderComponent.h b/es-core/src/components/SliderComponent.h index e4297d5b7b..c74d7e7d2e 100644 --- a/es-core/src/components/SliderComponent.h +++ b/es-core/src/components/SliderComponent.h @@ -25,6 +25,7 @@ class SliderComponent : public GuiComponent void onSizeChanged() override; virtual std::vector getHelpPrompts() override; + virtual void setColor(unsigned int color); private: void onValueChanged(); @@ -40,6 +41,8 @@ class SliderComponent : public GuiComponent std::string mSuffix; std::shared_ptr mFont; std::shared_ptr mValueCache; + + unsigned int mColor; }; #endif // ES_CORE_COMPONENTS_SLIDER_COMPONENT_H diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index 747e507fc3..27ba63294e 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -5,11 +5,19 @@ SwitchComponent::SwitchComponent(Window* window, bool state) : GuiComponent(window), mImage(window), mState(state) { + auto menuTheme = ThemeData::getMenuTheme(); + mImage.setImage(":/off.svg"); mImage.setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight()); + mImage.setColorShift(menuTheme->Text.color); + mSize = mImage.getSize(); } +void SwitchComponent::setColor(unsigned int color) { + mImage.setColorShift(color); +} + void SwitchComponent::onSizeChanged() { mImage.setSize(mSize); diff --git a/es-core/src/components/SwitchComponent.h b/es-core/src/components/SwitchComponent.h index 2e2d83f6b4..6cc981fb99 100644 --- a/es-core/src/components/SwitchComponent.h +++ b/es-core/src/components/SwitchComponent.h @@ -23,6 +23,8 @@ class SwitchComponent : public GuiComponent virtual std::vector getHelpPrompts() override; + void setColor(unsigned int color); + private: void onStateChanged(); diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index d1fc995f40..2800b41eaa 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -95,15 +95,22 @@ void TextComponent::setUppercase(bool uppercase) void TextComponent::render(const Transform4x4f& parentTrans) { + if (mColorOpacity == 0) + return; + Transform4x4f trans = parentTrans * getTransform(); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + if (mRenderBackground) { Renderer::setMatrix(trans); Renderer::drawRect(0.f, 0.f, mSize.x(), mSize.y(), mBgColor); } - - if(mTextCache) + + if (mTextCache) { const Vector2f& textSize = mTextCache->metrics.size; float yOff = 0; @@ -116,7 +123,7 @@ void TextComponent::render(const Transform4x4f& parentTrans) yOff = (getSize().y() - textSize.y()); break; case ALIGN_CENTER: - yOff = (getSize().y() - textSize.y()) / 2.0f; + yOff = (int) (getSize().y() - textSize.y()) / 2.0f; break; } Vector3f off(0, yOff, 0); @@ -148,6 +155,7 @@ void TextComponent::render(const Transform4x4f& parentTrans) break; } } + mFont->renderTextCache(mTextCache.get()); } } diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index 1721105772..e472cec335 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -23,7 +23,7 @@ class TextComponent : public GuiComponent void setUppercase(bool uppercase); void onSizeChanged() override; void setText(const std::string& text); - void setColor(unsigned int color); + virtual void setColor(unsigned int color); void setHorizontalAlignment(Alignment align); void setVerticalAlignment(Alignment align); void setLineSpacing(float spacing); diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 91f7532e40..ea712c0a60 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -135,6 +135,7 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) float x, y; Transform4x4f trans = parentTrans * getTransform(); + GuiComponent::renderChildren(trans); Renderer::setMatrix(trans); diff --git a/es-core/src/guis/GuiDetectDevice.cpp b/es-core/src/guis/GuiDetectDevice.cpp index faadbf4ac3..cd92f1664e 100644 --- a/es-core/src/guis/GuiDetectDevice.cpp +++ b/es-core/src/guis/GuiDetectDevice.cpp @@ -14,6 +14,12 @@ GuiDetectDevice::GuiDetectDevice(Window* window, bool firstRun, const std::function& doneCallback) : GuiComponent(window), mFirstRun(firstRun), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 5)) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + mGrid.setSeparatorColor(theme->Text.separatorColor); + mHoldingConfig = NULL; mHoldTime = 0; mDoneCallback = doneCallback; @@ -23,9 +29,10 @@ GuiDetectDevice::GuiDetectDevice(Window* window, bool firstRun, const std::funct // title mTitle = std::make_shared(mWindow, firstRun ? "WELCOME" : "CONFIGURE INPUT", - Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); - mGrid.setEntry(mTitle, Vector2i(0, 0), false, true, Vector2i(1, 1), GridFlags::BORDER_BOTTOM); + ThemeData::getMenuTheme()->Title.font, ThemeData::getMenuTheme()->Title.color, ALIGN_CENTER); + mGrid.setEntry(mTitle, Vector2i(0, 0), false, true, Vector2i(1, 1), GridFlags::BORDER_BOTTOM); + // device info std::stringstream deviceInfo; int numDevices = InputManager::getInstance()->getNumJoysticks(); @@ -34,15 +41,15 @@ GuiDetectDevice::GuiDetectDevice(Window* window, bool firstRun, const std::funct deviceInfo << numDevices << " GAMEPAD" << (numDevices > 1 ? "S" : "") << " DETECTED"; else deviceInfo << "NO GAMEPADS DETECTED"; - mDeviceInfo = std::make_shared(mWindow, deviceInfo.str(), Font::get(FONT_SIZE_SMALL), 0x999999FF, ALIGN_CENTER); + mDeviceInfo = std::make_shared(mWindow, deviceInfo.str(), ThemeData::getMenuTheme()->TextSmall.font, 0x999999FF, ALIGN_CENTER); mGrid.setEntry(mDeviceInfo, Vector2i(0, 1), false, true); // message - mMsg1 = std::make_shared(mWindow, "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT.", Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER); + mMsg1 = std::make_shared(mWindow, "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT.", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color, ALIGN_CENTER); mGrid.setEntry(mMsg1, Vector2i(0, 2), false, true); const char* msg2str = firstRun ? "PRESS F4 TO QUIT AT ANY TIME." : "PRESS ESC TO CANCEL."; - mMsg2 = std::make_shared(mWindow, msg2str, Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER); + mMsg2 = std::make_shared(mWindow, msg2str, ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color, ALIGN_CENTER); mGrid.setEntry(mMsg2, Vector2i(0, 3), false, true); // currently held device diff --git a/es-core/src/guis/GuiInputConfig.cpp b/es-core/src/guis/GuiInputConfig.cpp index 3ec21f101f..52cf142de2 100755 --- a/es-core/src/guis/GuiInputConfig.cpp +++ b/es-core/src/guis/GuiInputConfig.cpp @@ -51,9 +51,15 @@ static const InputConfigStructure GUI_INPUT_CONFIG_LIST[inputCount] = #define HOLD_TO_SKIP_MS 1000 GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfigureAll, const std::function& okCallback) : GuiComponent(window), - mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 7)), + mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 7)), mTargetConfig(target), mHoldingInput(false), mBusyAnim(window) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + mGrid.setSeparatorColor(theme->Text.separatorColor); + LOG(LogInfo) << "Configuring device " << target->getDeviceId() << " (" << target->getDeviceName() << ")."; if(reconfigureAll) @@ -95,7 +101,7 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi // icon auto icon = std::make_shared(mWindow); icon->setImage(GUI_INPUT_CONFIG_LIST[i].icon); - icon->setColorShift(0x777777FF); + icon->setColorShift(ThemeData::getMenuTheme()->Text.color); icon->setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight() * 1.25f); row.addElement(icon, false); @@ -104,7 +110,7 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi spacer->setSize(16, 0); row.addElement(spacer, false); - auto text = std::make_shared(mWindow, GUI_INPUT_CONFIG_LIST[i].dispName, Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + auto text = std::make_shared(mWindow, GUI_INPUT_CONFIG_LIST[i].dispName, ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); row.addElement(text, true); auto mapping = std::make_shared(mWindow, "-NOT DEFINED-", Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT), 0x999999FF, ALIGN_RIGHT); @@ -258,7 +264,7 @@ void GuiInputConfig::update(int deltaTime) std::stringstream ss; ss << "HOLD FOR " << HOLD_TO_SKIP_MS/1000 - curSec << "S TO SKIP"; text->setText(ss.str()); - text->setColor(0x777777FF); + text->setColor(ThemeData::getMenuTheme()->Text.color); } } } @@ -301,7 +307,7 @@ void GuiInputConfig::setNotDefined(const std::shared_ptr& text) void GuiInputConfig::setAssignedTo(const std::shared_ptr& text, Input input) { text->setText(Utils::String::toUpper(input.string())); - text->setColor(0x777777FF); + text->setColor(ThemeData::getMenuTheme()->Text.color); } void GuiInputConfig::error(const std::shared_ptr& text, const std::string& /*msg*/) diff --git a/es-core/src/guis/GuiMsgBox.cpp b/es-core/src/guis/GuiMsgBox.cpp index 7f05603200..843a844a48 100644 --- a/es-core/src/guis/GuiMsgBox.cpp +++ b/es-core/src/guis/GuiMsgBox.cpp @@ -11,10 +11,16 @@ GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, const std::string& name3, const std::function& func3) : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 2)) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + float width = Renderer::getScreenWidth() * 0.6f; // max width float minWidth = Renderer::getScreenWidth() * 0.3f; // minimum width - mMsg = std::make_shared(mWindow, text, Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); + + mMsg = std::make_shared(mWindow, text, ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color, ALIGN_CENTER); mGrid.setEntry(mMsg, Vector2i(0, 0), false, false); // create the buttons diff --git a/es-core/src/guis/GuiTextEditPopup.cpp b/es-core/src/guis/GuiTextEditPopup.cpp index de7390cbb8..1244ff0871 100644 --- a/es-core/src/guis/GuiTextEditPopup.cpp +++ b/es-core/src/guis/GuiTextEditPopup.cpp @@ -8,6 +8,11 @@ GuiTextEditPopup::GuiTextEditPopup(Window* window, const std::string& title, con const std::function& okCallback, bool multiLine, const char* acceptBtnText) : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 3)), mMultiLine(multiLine) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + addChild(&mBackground); addChild(&mGrid); diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index 9890954a82..fe2a1b6a08 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -22,6 +22,7 @@ Font::FontFace::FontFace(ResourceData&& d, int size) : data(d) Font::FontFace::~FontFace() { + if(face) FT_Done_Face(face); } @@ -74,9 +75,12 @@ Font::Font(int size, const std::string& path) : mSize(size), mPath(path) mMaxGlyphHeight = 0; - if(!sLibrary) + if (!sLibrary) initLibrary(); + for (unsigned int i = 0; i < 255; i++) + mGlyphCacheArray[i] = NULL; + // always initialize ASCII characters for (unsigned int i = 32; i < 128; i++) getGlyph(i); @@ -88,6 +92,9 @@ Font::Font(int size, const std::string& path) : mSize(size), mPath(path) Font::~Font() { + for (auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); it++) + delete it->second; + unload(ResourceManager::getInstance()); } @@ -316,10 +323,20 @@ void Font::clearFaceCache() Font::Glyph* Font::getGlyph(unsigned int id) { - // is it already loaded? - auto it = mGlyphMap.find(id); - if (it != mGlyphMap.cend()) - return &it->second; + if (id < 255) + { + // FCA : optimisation : array is always really fastest than a map + Glyph* fastCache = mGlyphCacheArray[id]; + if (fastCache != NULL) + return fastCache; + } + else + { + // is it already loaded? + auto it = mGlyphMap.find(id); + if (it != mGlyphMap.cend()) + return it->second; + } // nope, need to make a glyph FT_Face face = getFaceForChar(id); @@ -351,15 +368,13 @@ Font::Glyph* Font::getGlyph(unsigned int id) } // create glyph - Glyph& glyph = mGlyphMap[id]; + Glyph* pGlyph = new Glyph(); - glyph.texture = tex; - - glyph.texPos = Vector2f(cursor.x() / (float)tex->textureSize.x(), cursor.y() / (float)tex->textureSize.y()); - glyph.texSize = Vector2f(glyphSize.x() / (float)tex->textureSize.x(), glyphSize.y() / (float)tex->textureSize.y()); - - glyph.advance = Vector2f((float)g->metrics.horiAdvance / 64.0f, (float)g->metrics.vertAdvance / 64.0f); - glyph.bearing = Vector2f((float)g->metrics.horiBearingX / 64.0f, (float)g->metrics.horiBearingY / 64.0f); + pGlyph->texture = tex; + pGlyph->texPos = Vector2f(cursor.x() / (float)tex->textureSize.x(), cursor.y() / (float)tex->textureSize.y()); + pGlyph->texSize = Vector2f(glyphSize.x() / (float)tex->textureSize.x(), glyphSize.y() / (float)tex->textureSize.y()); + pGlyph->advance = Vector2f((float)g->metrics.horiAdvance / 64.0f, (float)g->metrics.vertAdvance / 64.0f); + pGlyph->bearing = Vector2f((float)g->metrics.horiBearingX / 64.0f, (float)g->metrics.horiBearingY / 64.0f); // upload glyph bitmap to texture glBindTexture(GL_TEXTURE_2D, tex->textureId); @@ -370,8 +385,12 @@ Font::Glyph* Font::getGlyph(unsigned int id) if (id != 61446 && glyphSize.y() > mMaxGlyphHeight) mMaxGlyphHeight = glyphSize.y(); - // done - return &glyph; + mGlyphMap[id] = pGlyph; + + if (id < 255) + mGlyphCacheArray[id] = pGlyph; + + return pGlyph; } // completely recreate the texture data for all textures based on mGlyphs information @@ -379,9 +398,7 @@ void Font::rebuildTextures() { // recreate OpenGL textures for(auto it = mTextures.begin(); it != mTextures.end(); it++) - { it->initTexture(); - } // reupload the texture data for(auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); it++) @@ -392,11 +409,11 @@ void Font::rebuildTextures() // load the glyph bitmap through FT FT_Load_Char(face, it->first, FT_LOAD_RENDER); - FontTexture* tex = it->second.texture; + FontTexture* tex = it->second->texture; // find the position/size - Vector2i cursor((int)(it->second.texPos.x() * tex->textureSize.x()), (int)(it->second.texPos.y() * tex->textureSize.y())); - Vector2i glyphSize((int)(it->second.texSize.x() * tex->textureSize.x()), (int)(it->second.texSize.y() * tex->textureSize.y())); + Vector2i cursor((int)(it->second->texPos.x() * tex->textureSize.x()), (int)(it->second->texPos.y() * tex->textureSize.y())); + Vector2i glyphSize((int)(it->second->texSize.x() * tex->textureSize.x()), (int)(it->second->texSize.y() * tex->textureSize.y())); // upload to texture glBindTexture(GL_TEXTURE_2D, tex->textureId); @@ -708,18 +725,26 @@ void TextCache::setColor(unsigned int color) std::shared_ptr Font::getFromTheme(const ThemeData::ThemeElement* elem, unsigned int properties, const std::shared_ptr& orig) { using namespace ThemeFlags; - if(!(properties & FONT_PATH) && !(properties & FONT_SIZE)) + if (!(properties & FONT_PATH) && !(properties & FONT_SIZE)) return orig; - + std::shared_ptr font; int size = (orig ? orig->mSize : FONT_SIZE_MEDIUM); std::string path = (orig ? orig->mPath : getDefaultPath()); float sh = (float)Renderer::getScreenHeight(); - if(properties & FONT_SIZE && elem->has("fontSize")) - size = (int)(sh * elem->get("fontSize")); - if(properties & FONT_PATH && elem->has("fontPath")) - path = elem->get("fontPath"); + if (properties & FONT_SIZE && elem->has("fontSize")) + { + if ((int)(sh * elem->get("fontSize")) > 0) + size = (int)(sh * elem->get("fontSize")); + } + + if (properties & FONT_PATH && elem->has("fontPath")) + { + std::string tmppath = elem->get("fontPath"); + if (ResourceManager::getInstance()->fileExists(tmppath)) + path = tmppath; + } return get(size, path); } diff --git a/es-core/src/resources/Font.h b/es-core/src/resources/Font.h index 585c892461..c9c81af643 100644 --- a/es-core/src/resources/Font.h +++ b/es-core/src/resources/Font.h @@ -120,7 +120,11 @@ class Font : public IReloadable Vector2f bearing; }; - std::map mGlyphMap; + // used to cache 255 first chars + Glyph* mGlyphCacheArray[255]; + + // used to cache every char + std::map mGlyphMap; Glyph* getGlyph(unsigned int id); diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 6c472c79ed..48aae8932a 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -98,8 +98,10 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng return true; } - std::vector imageRGBA = ImageIO::loadFromMemoryRGBA32((const unsigned char*)(fileData), length, width, height); - if (imageRGBA.size() == 0) + unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height); + +// std::vector imageRGBA = ImageIO::loadFromMemoryRGBA32((const unsigned char*)(fileData), length, width, height); + if (imageRGBA == NULL) { LOG(LogError) << "Could not initialize texture from memory, invalid data! (file path: " << mPath << ", data ptr: " << (size_t)fileData << ", reported size: " << length << ")"; return false; @@ -109,7 +111,7 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng mSourceHeight = (float) height; mScalable = false; - return initFromRGBA(imageRGBA.data(), width, height); + return initFromRGBAEx(imageRGBA, width, height); } bool TextureData::initFromRGBA(const unsigned char* dataRGBA, size_t width, size_t height) @@ -127,6 +129,20 @@ bool TextureData::initFromRGBA(const unsigned char* dataRGBA, size_t width, size return true; } +bool TextureData::initFromRGBAEx(unsigned char* dataRGBA, size_t width, size_t height) +{ + // If already initialised then don't read again + std::unique_lock lock(mMutex); + if (mDataRGBA) + return true; + + // Take a copy + mDataRGBA = dataRGBA; + mWidth = width; + mHeight = height; + return true; +} + bool TextureData::load() { bool retval = false; diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 3ac6f239ef..2355d439a9 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -22,6 +22,7 @@ class TextureData bool initSVGFromMemory(const unsigned char* fileData, size_t length); bool initImageFromMemory(const unsigned char* fileData, size_t length); bool initFromRGBA(const unsigned char* dataRGBA, size_t width, size_t height); + bool initFromRGBAEx(unsigned char* dataRGBA, size_t width, size_t height); // Read the data into memory if necessary bool load(); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 83b5270cb1..150a6191a2 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -1,667 +1,774 @@ -#define _FILE_OFFSET_BITS 64 - -#include "utils/FileSystemUtil.h" - -#include "Settings.h" -#include -#include - -#if defined(_WIN32) -// because windows... -#include -#include -#define getcwd _getcwd -#define mkdir(x,y) _mkdir(x) -#define snprintf _snprintf -#define stat64 _stat64 -#define unlink _unlink -#define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) -#define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) -#else // _WIN32 -#include -#include -#endif // _WIN32 - -namespace Utils -{ - namespace FileSystem - { - -#if defined(_WIN32) - static std::string convertFromWideString(const std::wstring wstring) - { - int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); - std::string string; - - string.resize(numBytes); - WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); - - return std::string(string); - - } // convertFromWideString -#endif // _WIN32 - - stringList getDirContent(const std::string& _path, const bool _recursive) - { - std::string path = getGenericPath(_path); - stringList contentList; - - // only parse the directory, if it's a directory - if(isDirectory(path)) - { - -#if defined(_WIN32) - WIN32_FIND_DATAW findData; - std::string wildcard = path + "/*"; - HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); - - if(hFind != INVALID_HANDLE_VALUE) - { - // loop over all files in the directory - do - { - std::string name = convertFromWideString(findData.cFileName); - - // ignore "." and ".." - if((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - contentList.push_back(fullName); - - if(_recursive && isDirectory(fullName)) - contentList.merge(getDirContent(fullName, true)); - } - } - while(FindNextFileW(hFind, &findData)); - - FindClose(hFind); - } -#else // _WIN32 - DIR* dir = opendir(path.c_str()); - - if(dir != NULL) - { - struct dirent* entry; - - // loop over all files in the directory - while((entry = readdir(dir)) != NULL) - { - std::string name(entry->d_name); - - // ignore "." and ".." - if((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - contentList.push_back(fullName); - - if(_recursive && isDirectory(fullName)) - contentList.merge(getDirContent(fullName, true)); - } - } - - closedir(dir); - } -#endif // _WIN32 - - } - - // sort the content list - contentList.sort(); - - // return the content list - return contentList; - - } // getDirContent - - stringList getPathList(const std::string& _path) - { - stringList pathList; - std::string path = getGenericPath(_path); - size_t start = 0; - size_t end = 0; - - // split at '/' - while((end = path.find("/", start)) != std::string::npos) - { - if(end != start) - pathList.push_back(std::string(path, start, end - start)); - - start = end + 1; - } - - // add last folder / file to pathList - if(start != path.size()) - pathList.push_back(std::string(path, start, path.size() - start)); - - // return the path list - return pathList; - - } // getPathList - - std::string getHomePath() - { - static std::string path; - - // only construct the homepath once - if(!path.length()) - { - // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows - char* envHome = getenv("HOME"); - -#ifdef _DEBUG - // envHome = "H:/[Emulz]/EmulationStation/"; -#endif - - if (envHome) - path = getGenericPath(envHome); - -#if defined(_WIN32) - // but does not seem to work for Windows XP or Vista, so try something else - if(!path.length()) - { - char* envHomeDrive = getenv("HOMEDRIVE"); - char* envHomePath = getenv("HOMEPATH"); - if(envHomeDrive && envHomePath) - path = getGenericPath(std::string(envHomeDrive) + "/" + envHomePath); - } -#endif // _WIN32 - - // no homepath found, fall back to current working directory - if(!path.length()) - path = getCWDPath(); - } - - // return constructed homepath - return path; - - } // getHomePath - - std::string getCWDPath() - { - char temp[512]; - return (getcwd(temp, 512) ? getGenericPath(temp) : ""); - } // getCWDPath - - std::string getExePath() - { - static std::string path; - - // only construct the exepath once - if (!path.length()) - { -#if defined(_WIN32) - char buffer[MAX_PATH]; - DWORD size = MAX_PATH; - DWORD result = GetModuleFileNameA(NULL, buffer, size); - if (result) - { - std::string ret = buffer; - path = getParent(ret); - return ret; - } -#endif - path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); - if (isRegularFile(path)) - path = getParent(path); - } - - // return constructed exepath - return path; - - } // getExePath - - std::string getPreferredPath(const std::string& _path) - { - std::string path = _path; - size_t offset = std::string::npos; -#if defined(_WIN32) - // convert '/' to '\\' - while((offset = path.find('/')) != std::string::npos) - path.replace(offset, 1, "\\"); -#endif // _WIN32 - return path; - } - - std::string getGenericPath(const std::string& _path) - { - std::string path = _path; - size_t offset = std::string::npos; - - // remove "\\\\?\\" - if((path.find("\\\\?\\")) == 0) - path.erase(0, 4); - - // convert '\\' to '/' - while((offset = path.find('\\')) != std::string::npos) - path.replace(offset, 1 ,"/"); - - // remove double '/' - while((offset = path.find("//")) != std::string::npos) - path.erase(offset, 1); - - // remove trailing '/' - while(path.length() && ((offset = path.find_last_of('/')) == (path.length() - 1))) - path.erase(offset, 1); - - // return generic path - return path; - - } // getGenericPath - - std::string getEscapedPath(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // windows escapes stuff by just putting everything in quotes - return '"' + getPreferredPath(path) + '"'; -#else // _WIN32 - // insert a backslash before most characters that would mess up a bash path - const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; - const char* invalidChar = invalidChars; - - while(*invalidChar) - { - size_t start = 0; - size_t offset = 0; - - while((offset = path.find(*invalidChar, start)) != std::string::npos) - { - start = offset + 1; - - if((offset == 0) || (path[offset - 1] != '\\')) - { - path.insert(offset, 1, '\\'); - ++start; - } - } - - ++invalidChar; - } - - // return escaped path - return path; -#endif // _WIN32 - - } // getEscapedPath - - std::string getCanonicalPath(const std::string& _path) - { - // temporary hack for builtin resources - if((_path[0] == ':') && (_path[1] == '/')) - return _path; - - std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); - - // cleanup path - bool scan = true; - while(scan) - { - stringList pathList = getPathList(path); - - path.clear(); - scan = false; - - for(stringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); ++it) - { - // ignore empty - if((*it).empty()) - continue; - - // remove "/./" - if((*it) == ".") - continue; - - // resolve "/../" - if((*it) == "..") - { - path = getParent(path); - continue; - } - -#if defined(_WIN32) - // append folder to path - path += (path.size() == 0) ? (*it) : ("/" + (*it)); -#else // _WIN32 - // append folder to path - path += ("/" + (*it)); -#endif // _WIN32 - - // resolve symlink - if(isSymlink(path)) - { - std::string resolved = resolveSymlink(path); - - if(resolved.empty()) - return ""; - - if(isAbsolute(resolved)) - path = resolved; - else - path = getParent(path) + "/" + resolved; - - for(++it; it != pathList.cend(); ++it) - path += (path.size() == 0) ? (*it) : ("/" + (*it)); - - scan = true; - break; - } - } - } - - // return canonical path - return path; - - } // getCanonicalPath - - std::string getAbsolutePath(const std::string& _path, const std::string& _base) - { - std::string path = getGenericPath(_path); - std::string base = isAbsolute(_base) ? getGenericPath(_base) : getAbsolutePath(_base); - - // return absolute path - return isAbsolute(path) ? path : getGenericPath(base + "/" + path); - - } // getAbsolutePath - - std::string getParent(const std::string& _path) - { - std::string path = getGenericPath(_path); - size_t offset = std::string::npos; - - // find last '/' and erase it - if((offset = path.find_last_of('/')) != std::string::npos) - return path.erase(offset); - - // no parent found - return path; - - } // getParent - - std::string getFileName(const std::string& _path) - { - std::string path = getGenericPath(_path); - size_t offset = std::string::npos; - - // find last '/' and return the filename - if((offset = path.find_last_of('/')) != std::string::npos) - return ((path[offset + 1] == 0) ? "." : std::string(path, offset + 1)); - - // no '/' found, entire path is a filename - return path; - - } // getFileName - - std::string getStem(const std::string& _path) - { - std::string fileName = getFileName(_path); - size_t offset = std::string::npos; - - // empty fileName - if(fileName == ".") - return fileName; - - // find last '.' and erase the extension - if((offset = fileName.find_last_of('.')) != std::string::npos) - return fileName.erase(offset); - - // no '.' found, filename has no extension - return fileName; - - } // getStem - - std::string getExtension(const std::string& _path) - { - std::string fileName = getFileName(_path); - size_t offset = std::string::npos; - - // empty fileName - if(fileName == ".") - return fileName; - - // find last '.' and return the extension - if((offset = fileName.find_last_of('.')) != std::string::npos) - return std::string(fileName, offset); - - // no '.' found, filename has no extension - return "."; - - } // getExtension - - std::string resolveRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) - { - std::string path = getGenericPath(_path); - std::string relativeTo = isDirectory(_relativeTo) ? getGenericPath(_relativeTo) : getParent(_relativeTo); - - // nothing to resolve - if(!path.length()) - return path; - - // replace '.' with relativeTo - if((path[0] == '.') && (path[1] == '/')) - return (relativeTo + &(path[1])); - - // replace '~' with homePath - if(_allowHome && (path[0] == '~') && (path[1] == '/')) - return (getHomePath() + &(path[1])); - - // nothing to resolve - return path; - - } // resolveRelativePath - - std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) - { - bool contains = false; - std::string path = removeCommonPath(_path, _relativeTo, contains); - - if(contains) - { - // success - return ("./" + path); - } - - if(_allowHome) - { - path = removeCommonPath(_path, getHomePath(), contains); - - if(contains) - { - // success - return ("~/" + path); - } - } - - // nothing to resolve - return path; - - } // createRelativePath - - std::string removeCommonPath(const std::string& _path, const std::string& _common, bool& _contains) - { - std::string path = getGenericPath(_path); - std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); - - // check if path contains common - if(path.find(common) == 0) - { - _contains = true; - return path.substr(common.length() + 1); - } - - // it didn't - _contains = false; - return path; - - } // removeCommonPath - - std::string resolveSymlink(const std::string& _path) - { - std::string path = getGenericPath(_path); - std::string resolved; - -#if defined(_WIN32) - HANDLE hFile = CreateFile(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); - - if(hFile != INVALID_HANDLE_VALUE) - { - resolved.resize(GetFinalPathNameByHandle(hFile, nullptr, 0, FILE_NAME_NORMALIZED) + 1); - if(GetFinalPathNameByHandle(hFile, (LPSTR)resolved.data(), (DWORD)resolved.size(), FILE_NAME_NORMALIZED) > 0) - { - resolved.resize(resolved.size() - 1); - resolved = getGenericPath(resolved); - } - CloseHandle(hFile); - } -#else // _WIN32 - struct stat info; - - // check if lstat succeeded - if(lstat(path.c_str(), &info) == 0) - { - resolved.resize(info.st_size); - if(readlink(path.c_str(), (char*)resolved.data(), resolved.size()) > 0) - resolved = getGenericPath(resolved); - } -#endif // _WIN32 - - // return resolved path - return resolved; - - } // resolveSymlink - - bool removeFile(const std::string& _path) - { - std::string path = getGenericPath(_path); - - // don't remove if it doesn't exists - if(!exists(path)) - return true; - - // try to remove file - return (unlink(path.c_str()) == 0); - - } // removeFile - - bool createDirectory(const std::string& _path) - { - std::string path = getGenericPath(_path); - - // don't create if it already exists - if(exists(path)) - return true; - - // try to create directory - if(mkdir(path.c_str(), 0755) == 0) - return true; - - // failed to create directory, try to create the parent - std::string parent = getParent(path); - - // only try to create parent if it's not identical to path - if(parent != path) - createDirectory(parent); - - // try to create directory again now that the parent should exist - return (mkdir(path.c_str(), 0755) == 0); - - } // createDirectory - - bool exists(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - return (stat64(path.c_str(), &info) == 0); - - } // exists - - bool isAbsolute(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - return ((path.size() > 1) && (path[1] == ':')); -#else // _WIN32 - return ((path.size() > 0) && (path[0] == '/')); -#endif // _WIN32 - - } // isAbsolute - - bool isRegularFile(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - if(stat64(path.c_str(), &info) != 0) - return false; - - // check for S_IFREG attribute - return (S_ISREG(info.st_mode)); - - } // isRegularFile - - bool isDirectory(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat info; - - // check if stat succeeded - if(stat(path.c_str(), &info) != 0) - return false; - - // check for S_IFDIR attribute - return (S_ISDIR(info.st_mode)); - - } // isDirectory - - bool isSymlink(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // check for symlink attribute - const DWORD Attributes = GetFileAttributes(path.c_str()); - if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT)) - return true; -#else // _WIN32 - struct stat info; - - // check if lstat succeeded - if(lstat(path.c_str(), &info) != 0) - return false; - - // check for S_IFLNK attribute - return (S_ISLNK(info.st_mode)); -#endif // _WIN32 - - // not a symlink - return false; - - } // isSymlink - - bool isHidden(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // check for hidden attribute - const DWORD Attributes = GetFileAttributes(path.c_str()); - if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_HIDDEN)) - return true; -#endif // _WIN32 - - // filenames starting with . are hidden in linux, we do this check for windows as well - if (getFileName(path)[0] == '.') - return true; - - // not hidden - return false; - - } // isHidden - - } // FileSystem:: - -} // Utils:: +#define _FILE_OFFSET_BITS 64 + +#include "utils/FileSystemUtil.h" + +#include "Settings.h" +#include +#include + +#if defined(_WIN32) +// because windows... +#include +#include +#define getcwd _getcwd +#define mkdir(x,y) _mkdir(x) +#define snprintf _snprintf +#define stat64 _stat64 +#define unlink _unlink +#define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) +#define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) +#else // _WIN32 +#include +#include +#endif // _WIN32 + +namespace Utils +{ + namespace FileSystem + { + +#if defined(_WIN32) + static std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + std::string string; + + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return std::string(string); + + } // convertFromWideString +#endif // _WIN32 + + bool compareFileInfo(const FileInfo& first, const FileInfo& second) + { + unsigned int i = 0; + while ((i < first.path.length()) && (i < second.path.length())) + { + if (tolower(first.path[i]) < tolower(second.path[i])) return true; + else if (tolower(first.path[i]) > tolower(second.path[i])) return false; + ++i; + } + return (first.path.length() < second.path.length()); + } + + fileList getDirInfo(const std::string& _path, const bool _recursive) + { + std::string path = getGenericPath(_path); + fileList contentList; + + // only parse the directory, if it's a directory + if (isDirectory(path)) + { +#if defined(_WIN32) + WIN32_FIND_DATAW findData; + std::string wildcard = path + "/*"; + HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); + + if (hFind != INVALID_HANDLE_VALUE) + { + // loop over all files in the directory + do + { + std::string name = convertFromWideString(findData.cFileName); + + // ignore "." and ".." + if ((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + + FileInfo fi; + fi.path = fullName; + fi.readOnly = (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN; + fi.directory = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY; + contentList.push_back(fi); + + if (_recursive && fi.directory) + { + fileList fl = getDirInfo(fullName, true); + for (auto it = fl.cbegin(); it != fl.cend(); ++it) + contentList.push_back(*it); + } + } + } while (FindNextFileW(hFind, &findData)); + + FindClose(hFind); + } +#else // _WIN32 + DIR* dir = opendir(path.c_str()); + + if (dir != NULL) + { + struct dirent* entry; + + // loop over all files in the directory + while ((entry = readdir(dir)) != NULL) + { + std::string name(entry->d_name); + + // ignore "." and ".." + if ((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + + FileInfo fi; + fi.path = fullName; + fi.readOnly = Utils::FileSystem::isHidden(fullName); + fi.directory = isDirectory(fullName); + contentList.push_back(fi); + + if (_recursive && fi.directory) + { + fileList fl = getDirInfo(fullName, true); + for (auto it = fl.cbegin(); it != fl.cend(); ++it) + contentList.push_back(*it); + } + } + } + + closedir(dir); + } +#endif // _WIN32 + + } + + // sort the content list + // Why loose time -> It will be sorted later ???? contentList.sort(compareFileInfo); + + // return the content list + return contentList; + + } // getDirContent + + stringList getDirContent(const std::string& _path, const bool _recursive, const bool includeHidden) + { + std::string path = getGenericPath(_path); + stringList contentList; + + // only parse the directory, if it's a directory + if(isDirectory(path)) + { +#if defined(_WIN32) + WIN32_FIND_DATAW findData; + std::string wildcard = path + "/*"; + HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); + + if(hFind != INVALID_HANDLE_VALUE) + { + // loop over all files in the directory + do + { + std::string name = convertFromWideString(findData.cFileName); + + // ignore "." and ".." + if((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + + if (!includeHidden && (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN) + continue; + + contentList.push_back(fullName); + + if(_recursive && (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) + contentList.merge(getDirContent(fullName, true, includeHidden)); + } + } + while(FindNextFileW(hFind, &findData)); + + FindClose(hFind); + } +#else // _WIN32 + DIR* dir = opendir(path.c_str()); + + if(dir != NULL) + { + struct dirent* entry; + + // loop over all files in the directory + while((entry = readdir(dir)) != NULL) + { + std::string name(entry->d_name); + + // ignore "." and ".." + if((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + + if (!includeHidden && Utils::FileSystem::isHidden(fullName)) + continue; + + contentList.push_back(fullName); + + if(_recursive && isDirectory(fullName)) + contentList.merge(getDirContent(fullName, true)); + } + } + + closedir(dir); + } +#endif // _WIN32 + + } + + // sort the content list +// Why loose time -> It will be sorted later ???? contentList.sort(); + + // return the content list + return contentList; + + } // getDirContent + + stringList getPathList(const std::string& _path) + { + stringList pathList; + std::string path = getGenericPath(_path); + size_t start = 0; + size_t end = 0; + + // split at '/' + while((end = path.find("/", start)) != std::string::npos) + { + if(end != start) + pathList.push_back(std::string(path, start, end - start)); + + start = end + 1; + } + + // add last folder / file to pathList + if(start != path.size()) + pathList.push_back(std::string(path, start, path.size() - start)); + + // return the path list + return pathList; + + } // getPathList + + std::string getHomePath() + { + static std::string path; + + // only construct the homepath once + if(!path.length()) + { + // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows + char* envHome = getenv("HOME"); + envHome = "H:/[Emulz]/EmulationStation/"; +#ifdef _DEBUG + envHome = "H:/[Emulz]/EmulationStation/"; +#endif + + if (envHome) + path = getGenericPath(envHome); + +#if defined(_WIN32) + // but does not seem to work for Windows XP or Vista, so try something else + if(!path.length()) + { + char* envHomeDrive = getenv("HOMEDRIVE"); + char* envHomePath = getenv("HOMEPATH"); + if(envHomeDrive && envHomePath) + path = getGenericPath(std::string(envHomeDrive) + "/" + envHomePath); + } +#endif // _WIN32 + + // no homepath found, fall back to current working directory + if(!path.length()) + path = getCWDPath(); + } + + // return constructed homepath + return path; + + } // getHomePath + + std::string getCWDPath() + { + char temp[512]; + return (getcwd(temp, 512) ? getGenericPath(temp) : ""); + } // getCWDPath + + std::string getExePath() + { + static std::string path; + + // only construct the exepath once + if (!path.length()) + { +#if defined(_WIN32) + char buffer[MAX_PATH]; + DWORD size = MAX_PATH; + DWORD result = GetModuleFileNameA(NULL, buffer, size); + if (result) + { + std::string ret = buffer; + path = getParent(ret); + return ret; + } +#endif + path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); + if (isRegularFile(path)) + path = getParent(path); + } + + // return constructed exepath + return path; + + } // getExePath + + std::string getPreferredPath(const std::string& _path) + { + std::string path = _path; + size_t offset = std::string::npos; +#if defined(_WIN32) + // convert '/' to '\\' + while((offset = path.find('/')) != std::string::npos) + path.replace(offset, 1, "\\"); +#endif // _WIN32 + return path; + } + + std::string getGenericPath(const std::string& _path) + { + std::string path = _path; + size_t offset = std::string::npos; + + // remove "\\\\?\\" + if((path.find("\\\\?\\")) == 0) + path.erase(0, 4); + + // convert '\\' to '/' + while((offset = path.find('\\')) != std::string::npos) + path.replace(offset, 1 ,"/"); + + // remove double '/' + while((offset = path.find("//")) != std::string::npos) + path.erase(offset, 1); + + // remove trailing '/' + while(path.length() && ((offset = path.find_last_of('/')) == (path.length() - 1))) + path.erase(offset, 1); + + // return generic path + return path; + + } // getGenericPath + + std::string getEscapedPath(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // windows escapes stuff by just putting everything in quotes + return '"' + getPreferredPath(path) + '"'; +#else // _WIN32 + // insert a backslash before most characters that would mess up a bash path + const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; + const char* invalidChar = invalidChars; + + while(*invalidChar) + { + size_t start = 0; + size_t offset = 0; + + while((offset = path.find(*invalidChar, start)) != std::string::npos) + { + start = offset + 1; + + if((offset == 0) || (path[offset - 1] != '\\')) + { + path.insert(offset, 1, '\\'); + ++start; + } + } + + ++invalidChar; + } + + // return escaped path + return path; +#endif // _WIN32 + + } // getEscapedPath + + std::string getCanonicalPath(const std::string& _path) + { + // temporary hack for builtin resources + if((_path[0] == ':') && (_path[1] == '/')) + return _path; + + std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); + + // cleanup path + bool scan = true; + while(scan) + { + stringList pathList = getPathList(path); + + path.clear(); + scan = false; + + for(stringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); ++it) + { + // ignore empty + if((*it).empty()) + continue; + + // remove "/./" + if((*it) == ".") + continue; + + // resolve "/../" + if((*it) == "..") + { + path = getParent(path); + continue; + } + +#if defined(_WIN32) + // append folder to path + path += (path.size() == 0) ? (*it) : ("/" + (*it)); +#else // _WIN32 + // append folder to path + path += ("/" + (*it)); +#endif // _WIN32 + + // resolve symlink + if(isSymlink(path)) + { + std::string resolved = resolveSymlink(path); + + if(resolved.empty()) + return ""; + + if(isAbsolute(resolved)) + path = resolved; + else + path = getParent(path) + "/" + resolved; + + for(++it; it != pathList.cend(); ++it) + path += (path.size() == 0) ? (*it) : ("/" + (*it)); + + scan = true; + break; + } + } + } + + // return canonical path + return path; + + } // getCanonicalPath + + std::string getAbsolutePath(const std::string& _path, const std::string& _base) + { + std::string path = getGenericPath(_path); + std::string base = isAbsolute(_base) ? getGenericPath(_base) : getAbsolutePath(_base); + + // return absolute path + return isAbsolute(path) ? path : getGenericPath(base + "/" + path); + + } // getAbsolutePath + + std::string getParent(const std::string& _path) + { + std::string path = getGenericPath(_path); + size_t offset = std::string::npos; + + // find last '/' and erase it + if((offset = path.find_last_of('/')) != std::string::npos) + return path.erase(offset); + + // no parent found + return path; + + } // getParent + + std::string getFileName(const std::string& _path) + { + std::string path = getGenericPath(_path); + size_t offset = std::string::npos; + + // find last '/' and return the filename + if((offset = path.find_last_of('/')) != std::string::npos) + return ((path[offset + 1] == 0) ? "." : std::string(path, offset + 1)); + + // no '/' found, entire path is a filename + return path; + + } // getFileName + + std::string getStem(const std::string& _path) + { + std::string fileName = getFileName(_path); + size_t offset = std::string::npos; + + // empty fileName + if(fileName == ".") + return fileName; + + // find last '.' and erase the extension + if((offset = fileName.find_last_of('.')) != std::string::npos) + return fileName.erase(offset); + + // no '.' found, filename has no extension + return fileName; + + } // getStem + + std::string getExtension(const std::string& _path) + { + std::string fileName = getFileName(_path); + size_t offset = std::string::npos; + + // empty fileName + if(fileName == ".") + return fileName; + + // find last '.' and return the extension + if((offset = fileName.find_last_of('.')) != std::string::npos) + return std::string(fileName, offset); + + // no '.' found, filename has no extension + return "."; + + } // getExtension + + std::string resolveRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) + { + std::string path = getGenericPath(_path); + std::string relativeTo = isDirectory(_relativeTo) ? getGenericPath(_relativeTo) : getParent(_relativeTo); + + // nothing to resolve + if(!path.length()) + return path; + + // replace '.' with relativeTo + if((path[0] == '.') && (path[1] == '/')) + return (relativeTo + &(path[1])); + + // replace '~' with homePath + if(_allowHome && (path[0] == '~') && (path[1] == '/')) + return (getHomePath() + &(path[1])); + + // nothing to resolve + return path; + + } // resolveRelativePath + + std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) + { + bool contains = false; + std::string path = removeCommonPath(_path, _relativeTo, contains); + + if(contains) + { + // success + return ("./" + path); + } + + if(_allowHome) + { + path = removeCommonPath(_path, getHomePath(), contains); + + if(contains) + { + // success + return ("~/" + path); + } + } + + // nothing to resolve + return path; + + } // createRelativePath + + std::string removeCommonPath(const std::string& _path, const std::string& _common, bool& _contains) + { + std::string path = getGenericPath(_path); + std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); + + // check if path contains common + if(path.find(common) == 0) + { + _contains = true; + return path.substr(common.length() + 1); + } + + // it didn't + _contains = false; + return path; + + } // removeCommonPath + + std::string resolveSymlink(const std::string& _path) + { + std::string path = getGenericPath(_path); + std::string resolved; + +#if defined(_WIN32) + HANDLE hFile = CreateFile(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); + + if(hFile != INVALID_HANDLE_VALUE) + { + resolved.resize(GetFinalPathNameByHandle(hFile, nullptr, 0, FILE_NAME_NORMALIZED) + 1); + if(GetFinalPathNameByHandle(hFile, (LPSTR)resolved.data(), (DWORD)resolved.size(), FILE_NAME_NORMALIZED) > 0) + { + resolved.resize(resolved.size() - 1); + resolved = getGenericPath(resolved); + } + CloseHandle(hFile); + } +#else // _WIN32 + struct stat info; + + // check if lstat succeeded + if(lstat(path.c_str(), &info) == 0) + { + resolved.resize(info.st_size); + if(readlink(path.c_str(), (char*)resolved.data(), resolved.size()) > 0) + resolved = getGenericPath(resolved); + } +#endif // _WIN32 + + // return resolved path + return resolved; + + } // resolveSymlink + + bool removeFile(const std::string& _path) + { + std::string path = getGenericPath(_path); + + // don't remove if it doesn't exists + if(!exists(path)) + return true; + + // try to remove file + return (unlink(path.c_str()) == 0); + + } // removeFile + + bool createDirectory(const std::string& _path) + { + std::string path = getGenericPath(_path); + + // don't create if it already exists + if(exists(path)) + return true; + + // try to create directory + if(mkdir(path.c_str(), 0755) == 0) + return true; + + // failed to create directory, try to create the parent + std::string parent = getParent(path); + + // only try to create parent if it's not identical to path + if(parent != path) + createDirectory(parent); + + // try to create directory again now that the parent should exist + return (mkdir(path.c_str(), 0755) == 0); + + } // createDirectory + + bool exists(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + return (stat64(path.c_str(), &info) == 0); + + } // exists + + bool isAbsolute(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + return ((path.size() > 1) && (path[1] == ':')); +#else // _WIN32 + return ((path.size() > 0) && (path[0] == '/')); +#endif // _WIN32 + + } // isAbsolute + + bool isRegularFile(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + if(stat64(path.c_str(), &info) != 0) + return false; + + // check for S_IFREG attribute + return (S_ISREG(info.st_mode)); + + } // isRegularFile + + bool isDirectory(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat info; + + // check if stat succeeded + if(stat(path.c_str(), &info) != 0) + return false; + + // check for S_IFDIR attribute + return (S_ISDIR(info.st_mode)); + + } // isDirectory + + bool isSymlink(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // check for symlink attribute + const DWORD Attributes = GetFileAttributes(path.c_str()); + if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT)) + return true; +#else // _WIN32 + struct stat info; + + // check if lstat succeeded + if(lstat(path.c_str(), &info) != 0) + return false; + + // check for S_IFLNK attribute + return (S_ISLNK(info.st_mode)); +#endif // _WIN32 + + // not a symlink + return false; + + } // isSymlink + + bool isHidden(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // check for hidden attribute + const DWORD Attributes = GetFileAttributes(path.c_str()); + if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_HIDDEN)) + return true; +#endif // _WIN32 + + // filenames starting with . are hidden in linux, we do this check for windows as well + if (getFileName(path)[0] == '.') + return true; + + // not hidden + return false; + + } // isHidden + + } // FileSystem:: + +} // Utils:: diff --git a/es-core/src/utils/FileSystemUtil.h b/es-core/src/utils/FileSystemUtil.h index 48c7f4f5ac..e8b01741c0 100644 --- a/es-core/src/utils/FileSystemUtil.h +++ b/es-core/src/utils/FileSystemUtil.h @@ -9,9 +9,19 @@ namespace Utils { namespace FileSystem { + struct FileInfo + { + public: + std::string path; + bool readOnly; + bool directory; + }; + typedef std::list stringList; + typedef std::list fileList; - stringList getDirContent (const std::string& _path, const bool _recursive = false); + fileList getDirInfo(const std::string& _path, const bool _recursive = false); + stringList getDirContent (const std::string& _path, const bool _recursive = false, const bool includeHidden = true); stringList getPathList (const std::string& _path); std::string getHomePath (); std::string getCWDPath (); @@ -40,6 +50,8 @@ namespace Utils } // FileSystem:: + + } // Utils:: #endif // ES_CORE_UTILS_FILE_SYSTEM_UTIL_H From af0b3e2a0e08bf3c3f4a32d81efcc921fae12fb8 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 13 Jun 2019 17:44:12 +0200 Subject: [PATCH 017/336] - Better GridView (with animations) - Added Custom Views in Themes -> Enables creation of multiple view - Full Themable Menus - Support for Recalbox themes - Merge view "detailed" and "video" -> video is not longer needed to display a video, a video element can be created in detailed view. - Video / Theming -> Support video elements as extras - Images : Don't load oversized image in VRAM -> max size is set by the container. --- es-app/src/FileData.cpp | 26 +- es-app/src/FileData.h | 2 +- es-app/src/FileSorts.cpp | 4 +- es-app/src/Gamelist.cpp | 27 +- es-app/src/MetaData.cpp | 8 +- es-app/src/MetaData.h | 7 +- es-app/src/SystemData.cpp | 16 +- es-app/src/SystemData.h | 254 +++--- es-app/src/guis/GuiGamelistOptions.cpp | 616 +++++++------- es-app/src/guis/GuiGamelistOptions.h | 3 + es-app/src/guis/GuiMenu.cpp | 435 +++++++--- es-app/src/guis/GuiMenu.h | 2 +- es-app/src/guis/GuiMetaDataEd.cpp | 2 +- es-app/src/guis/GuiSettings.h | 12 + .../guis/GuiSlideshowScreensaverOptions.cpp | 2 +- es-app/src/main.cpp | 10 +- es-app/src/views/ViewController.cpp | 119 +-- es-app/src/views/ViewController.h | 4 +- .../src/views/gamelist/BasicGameListView.cpp | 6 + es-app/src/views/gamelist/BasicGameListView.h | 10 +- .../views/gamelist/DetailedGameListView.cpp | 88 +- .../src/views/gamelist/DetailedGameListView.h | 18 +- .../src/views/gamelist/GridGameListView.cpp | 49 +- es-app/src/views/gamelist/GridGameListView.h | 8 +- es-app/src/views/gamelist/IGameListView.cpp | 5 + es-app/src/views/gamelist/IGameListView.h | 5 + .../views/gamelist/ISimpleGameListView.cpp | 17 +- .../src/views/gamelist/ISimpleGameListView.h | 1 + es-app/src/views/gamelist/VideoGameListView.h | 9 +- es-core/src/GuiComponent.cpp | 10 +- es-core/src/ImageIO.cpp | 50 +- es-core/src/ImageIO.h | 7 +- es-core/src/Settings.cpp | 18 +- es-core/src/ThemeData.cpp | 402 ++++++++- es-core/src/ThemeData.h | 58 +- es-core/src/Window.cpp | 18 +- es-core/src/Window.h | 4 +- es-core/src/components/ButtonComponent.cpp | 10 +- es-core/src/components/GridTileComponent.cpp | 424 ++++++++-- es-core/src/components/GridTileComponent.h | 35 +- es-core/src/components/ImageComponent.cpp | 37 +- es-core/src/components/ImageComponent.h | 14 +- es-core/src/components/ImageGridComponent.h | 557 +++++++++---- es-core/src/components/MenuComponent.cpp | 2 +- es-core/src/components/NinePatchComponent.h | 1 + es-core/src/components/OptionListComponent.h | 766 +++++++++--------- es-core/src/components/SliderComponent.cpp | 2 +- es-core/src/components/SwitchComponent.cpp | 6 +- es-core/src/components/TextComponent.cpp | 6 + es-core/src/components/TextComponent.h | 2 + es-core/src/components/VideoComponent.cpp | 122 +-- es-core/src/components/VideoComponent.h | 13 +- es-core/src/components/VideoVlcComponent.cpp | 18 +- es-core/src/resources/ResourceManager.cpp | 16 +- es-core/src/resources/ResourceManager.h | 2 +- es-core/src/resources/TextureData.cpp | 16 +- es-core/src/resources/TextureData.h | 6 + es-core/src/resources/TextureDataManager.cpp | 1 + es-core/src/resources/TextureResource.cpp | 14 +- es-core/src/resources/TextureResource.h | 4 +- es-core/src/utils/FileSystemUtil.cpp | 32 +- es-core/src/utils/FileSystemUtil.h | 3 +- 62 files changed, 3054 insertions(+), 1387 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 71cdc3352d..a5485bb65a 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -59,7 +59,7 @@ const std::string FileData::getThumbnailPath() const if(thumbnail.empty()) { thumbnail = metadata.get("image"); - /* + // no image, try to use local image if(thumbnail.empty() && Settings::getInstance()->getBool("LocalArt")) { @@ -73,7 +73,7 @@ const std::string FileData::getThumbnailPath() const thumbnail = path; } } - }*/ + } } return thumbnail; @@ -93,15 +93,15 @@ const std::string& FileData::getName() { return metadata.get("name"); } - +/* const std::string& FileData::getSortName() { if (metadata.get("sortname").empty()) return metadata.get("name"); - else - return metadata.get("sortname"); + + return metadata.get("sortname"); } - +*/ FileData* FileData::findUniqueGameForFolder() { std::vector children = getChildren(); @@ -155,15 +155,15 @@ const std::string FileData::getEmulator() const const std::string FileData::getVideoPath() const { std::string video = metadata.get("video"); - /* + // no video, try to use local video if(video.empty() && Settings::getInstance()->getBool("LocalArt")) { std::string path = mEnvData->mStartPath + "/images/" + getDisplayName() + "-video.mp4"; - if(Utils::FileSystem::exists(path)) + if (Utils::FileSystem::exists(path)) video = path; } - */ + return video; } @@ -172,7 +172,7 @@ const std::string FileData::getMarqueePath() const std::string marquee = metadata.get("marquee"); // no marquee, try to use local marquee - if(marquee.empty() && Settings::getInstance()->getBool("LocalArt")) + if (marquee.empty() && Settings::getInstance()->getBool("LocalArt")) { const char* extList[2] = { ".png", ".jpg" }; for(int i = 0; i < 2; i++) @@ -315,8 +315,7 @@ void FileData::launchGame(Window* window) VolumeControl::getInstance()->deinit(); bool hideWindow = Settings::getInstance()->getBool("HideWindow"); - if (hideWindow) - window->deinit(); + window->deinit(hideWindow); std::string command = mEnvData->mLaunchCommand; @@ -357,8 +356,7 @@ void FileData::launchGame(Window* window) Scripting::fireEvent("game-end"); - if (hideWindow) - window->init(); + window->init(hideWindow); VolumeControl::getInstance()->init(); window->normalizeNextUpdate(); diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index d8bafed6d2..93b0cc2601 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -37,7 +37,7 @@ class FileData virtual ~FileData(); virtual const std::string& getName(); - virtual const std::string& getSortName(); + //virtual const std::string& getSortName(); inline FileType getType() const { return mType; } inline const std::string& getPath() const { return mPath; } inline FileData* getParent() const { return mParent; } diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index d1237c0a2d..046100cab4 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -41,8 +41,8 @@ namespace FileSorts //returns if file1 should come before file2 bool compareName(const FileData* file1, const FileData* file2) { - std::string name1 = file1->metadata.getSortName(); - return name1.compare(file2->metadata.getSortName()) < 0; + std::string name1 = file1->metadata.getName(); + return name1.compare(file2->metadata.getName()) < 0; } bool compareRating(const FileData* file1, const FileData* file2) diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index ea20baa94a..f0328caf76 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -106,6 +106,8 @@ void parseGamelist(SystemData* system) LOG(LogError) << "Could not find node in gamelist \"" << xmlpath << "\"!"; return; } + + system->setSystemViewMode(root.attribute("defaultView").value()); std::string relativeTo = system->getStartPath(); @@ -181,12 +183,15 @@ void updateGamelist(SystemData* system) if(Settings::getInstance()->getBool("IgnoreGamelist")) return; + int numUpdated = 0; + pugi::xml_document doc; pugi::xml_node root; std::string xmlReadPath = system->getGamelistPath(false); if(Utils::FileSystem::exists(xmlReadPath)) { + //parse an existing file first pugi::xml_parse_result result = doc.load_file(xmlReadPath.c_str()); @@ -197,6 +202,20 @@ void updateGamelist(SystemData* system) } root = doc.child("gameList"); + + std::string viewMode = root.attribute("defaultView").value(); + if (viewMode != system->getSystemViewMode()) + { + numUpdated++; + + if (system->getSystemViewMode().empty()) + root.remove_attribute("defaultView"); + else if (root.attribute("defaultView").empty()) + root.append_attribute("defaultView") = system->getSystemViewMode().c_str(); + else + root.attribute("defaultView") = system->getSystemViewMode().c_str(); + } + if(!root) { LOG(LogError) << "Could not find node in gamelist \"" << xmlReadPath << "\"!"; @@ -205,6 +224,12 @@ void updateGamelist(SystemData* system) }else{ //set up an empty gamelist to append to root = doc.append_child("gameList"); + + if (!system->getSystemViewMode().empty()) + { + numUpdated++; + root.append_attribute("defaultView") = system->getSystemViewMode().c_str(); + } } @@ -212,8 +237,6 @@ void updateGamelist(SystemData* system) FileData* rootFolder = system->getRootFolder(); if (rootFolder != nullptr) { - int numUpdated = 0; - //get only files, no folders std::vector files = rootFolder->getFilesRecursive(GAME | FOLDER); //iterate through all files, checking if they're already in the XML diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index c9c1716188..cdaa4dd151 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -8,7 +8,7 @@ MetaDataDecl gameDecls[] = { // key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd {"name", MD_STRING, "", false, "name", "enter game name"}, - {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, +// {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, { "emulator", MD_PLIST, "", false, "emulator", "emulator" }, @@ -34,7 +34,7 @@ const std::vector gameMDD(gameDecls, gameDecls + sizeof(gameDecls) MetaDataDecl folderDecls[] = { {"name", MD_STRING, "", false, "name", "enter game name"}, - {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, +// {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, {"image", MD_PATH, "", false, "image", "enter path to image"}, {"thumbnail", MD_PATH, "", false, "thumbnail", "enter path to thumbnail"}, @@ -134,8 +134,8 @@ void MetaDataList::appendToXML(pugi::xml_node& parent, bool ignoreDefaults, cons void MetaDataList::set(const std::string& key, const std::string& value) { - if ((key == "sortname" || key == "name") && !value.empty()) - mSortName = Utils::String::toUpper(value); + if (key == "name") + mName = value; mMap[key] = value; mWasChanged = true; diff --git a/es-app/src/MetaData.h b/es-app/src/MetaData.h index c0c01f264d..3e23eab755 100644 --- a/es-app/src/MetaData.h +++ b/es-app/src/MetaData.h @@ -63,10 +63,7 @@ class MetaDataList inline MetaDataListType getType() const { return mType; } inline const std::vector& getMDD() const { return getMDDByType(getType()); } - - const std::string getSortName() const { - return mSortName; - } + const std::string getName() const { return mName; } private: MetaDataListType mType; @@ -78,7 +75,7 @@ class MetaDataList static std::map BuildDefaultMap(MetaDataListType type); - std::string mSortName; + std::string mName; }; #endif // ES_APP_META_DATA_H diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index b2cd549059..657012078f 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -24,6 +24,7 @@ std::vector SystemData::sSystemVector; SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) { + mViewModeChanged = false; mFilterIndex = new FileFilterIndex(); // if it's an actual system, initialize it, if not, just create the data structure @@ -56,6 +57,19 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys loadTheme(); } +bool SystemData::setSystemViewMode(std::string newViewMode) +{ + if (newViewMode == "automatic") + newViewMode = ""; + + if (mViewMode == newViewMode) + return false; + + mViewMode = newViewMode; + mViewModeChanged = true; + return true; +} + SystemData::~SystemData() { delete mRootFolder; @@ -108,7 +122,7 @@ void SystemData::populateFolder(FileData* folder) //filePath = *it; // skip hidden files and folders - if(!showHidden && fileInfo.readOnly) + if(!showHidden && fileInfo.hidden) continue; //this is a little complicated because we allow a list of extensions to be defined (delimited with a space) diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index fd0db7a977..59207eeede 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -1,56 +1,56 @@ -#pragma once -#ifndef ES_APP_SYSTEM_DATA_H -#define ES_APP_SYSTEM_DATA_H - -#include "PlatformId.h" -#include -#include -#include -#include - -class FileData; -class FileFilterIndex; -class ThemeData; -class Window; - -struct EmulatorData -{ - std::string mName; - std::string mCommandLine; - std::vector mCores; -}; - -struct SystemEnvironmentData -{ - std::string mStartPath; - std::vector mSearchExtensions; - std::string mLaunchCommand; - std::vector mPlatformIds; - -// std::string mDefaultCore; - - std::vector mEmulators; - - std::vector getCores(std::string emulatorName) - { +#pragma once +#ifndef ES_APP_SYSTEM_DATA_H +#define ES_APP_SYSTEM_DATA_H + +#include "PlatformId.h" +#include +#include +#include +#include + +class FileData; +class FileFilterIndex; +class ThemeData; +class Window; + +struct EmulatorData +{ + std::string mName; + std::string mCommandLine; + std::vector mCores; +}; + +struct SystemEnvironmentData +{ + std::string mStartPath; + std::vector mSearchExtensions; + std::string mLaunchCommand; + std::vector mPlatformIds; + +// std::string mDefaultCore; + + std::vector mEmulators; + + std::vector getCores(std::string emulatorName) + { std::vector list; for (auto& emulator : mEmulators) if (emulatorName == emulator.mName) return emulator.mCores; - - return list; - } - - std::string getDefaultEmulator() - { + + return list; + } + + std::string getDefaultEmulator() + { for (auto& emulator : mEmulators) return emulator.mName; - - return ""; - } - - std::string getDefaultCore(std::string emulatorName) + + return ""; + } + + std::string getDefaultCore(std::string emulatorName) { for (auto& emulator : mEmulators) { @@ -60,85 +60,91 @@ struct SystemEnvironmentData return core; } } - - return ""; - } - - std::string getEmulatorCommandLine(std::string emulatorName) + + return ""; + } + + std::string getEmulatorCommandLine(std::string emulatorName) { for (auto& emulator : mEmulators) if (emulatorName == emulator.mName) return emulator.mCommandLine; - - return ""; - } -}; - -class SystemData -{ -public: - SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem = false); - ~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 mEnvData->mStartPath; } - inline const std::vector& getExtensions() const { return mEnvData->mSearchExtensions; } - inline const std::string& getThemeFolder() const { return mThemeFolder; } - inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } - inline const std::vector& getPlatformIds() const { return mEnvData->mPlatformIds; } - inline bool hasPlatformId(PlatformIds::PlatformId id) { if (!mEnvData) return false; return std::find(mEnvData->mPlatformIds.cbegin(), mEnvData->mPlatformIds.cend(), id) != mEnvData->mPlatformIds.cend(); } - - inline const std::shared_ptr& getTheme() const { return mTheme; } - - std::string getGamelistPath(bool forWrite) const; - bool hasGamelist() const; - std::string getThemePath() const; - - unsigned int getGameCount() const; - unsigned int getDisplayedGameCount() const; - - static void deleteSystems(); - static bool loadConfig(Window* window); //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 sSystemVector; - - inline std::vector::const_iterator getIterator() const { return std::find(sSystemVector.cbegin(), sSystemVector.cend(), this); }; - inline std::vector::const_reverse_iterator getRevIterator() const { return std::find(sSystemVector.crbegin(), sSystemVector.crend(), this); }; - inline bool isCollection() { return mIsCollectionSystem; }; - inline bool isGameSystem() { return mIsGameSystem; }; - - bool isVisible(); - - SystemData* getNext() const; - SystemData* getPrev() const; - static SystemData* getRandomSystem(); - FileData* getRandomGame(); - - // Load or re-load theme. - void loadTheme(); - - FileFilterIndex* getIndex() { return mFilterIndex; }; - -private: - bool mIsCollectionSystem; - bool mIsGameSystem; - std::string mName; - std::string mFullName; - SystemEnvironmentData* mEnvData; - std::string mThemeFolder; - std::shared_ptr mTheme; - - void populateFolder(FileData* folder); - void indexAllGameFilters(const FileData* folder); - void setIsGameSystemStatus(); - - FileFilterIndex* mFilterIndex; - - FileData* mRootFolder; -}; - -#endif // ES_APP_SYSTEM_DATA_H + + return ""; + } +}; + +class SystemData +{ +public: + SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem = false); + ~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 mEnvData->mStartPath; } + inline const std::vector& getExtensions() const { return mEnvData->mSearchExtensions; } + inline const std::string& getThemeFolder() const { return mThemeFolder; } + inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } + inline const std::vector& getPlatformIds() const { return mEnvData->mPlatformIds; } + inline bool hasPlatformId(PlatformIds::PlatformId id) { if (!mEnvData) return false; return std::find(mEnvData->mPlatformIds.cbegin(), mEnvData->mPlatformIds.cend(), id) != mEnvData->mPlatformIds.cend(); } + + inline const std::shared_ptr& getTheme() const { return mTheme; } + + std::string getSystemViewMode() const { if (mViewMode == "automatic") return ""; else return mViewMode; }; + bool setSystemViewMode(std::string newViewMode); + + std::string getGamelistPath(bool forWrite) const; + bool hasGamelist() const; + std::string getThemePath() const; + + unsigned int getGameCount() const; + unsigned int getDisplayedGameCount() const; + + static void deleteSystems(); + static bool loadConfig(Window* window); //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 sSystemVector; + + inline std::vector::const_iterator getIterator() const { return std::find(sSystemVector.cbegin(), sSystemVector.cend(), this); }; + inline std::vector::const_reverse_iterator getRevIterator() const { return std::find(sSystemVector.crbegin(), sSystemVector.crend(), this); }; + inline bool isCollection() { return mIsCollectionSystem; }; + inline bool isGameSystem() { return mIsGameSystem; }; + + bool isVisible(); + + SystemData* getNext() const; + SystemData* getPrev() const; + static SystemData* getRandomSystem(); + FileData* getRandomGame(); + + // Load or re-load theme. + void loadTheme(); + + FileFilterIndex* getIndex() { return mFilterIndex; }; + +private: + bool mIsCollectionSystem; + bool mIsGameSystem; + std::string mName; + std::string mFullName; + SystemEnvironmentData* mEnvData; + std::string mThemeFolder; + std::shared_ptr mTheme; + + std::string mViewMode; + bool mViewModeChanged; + + void populateFolder(FileData* folder); + void indexAllGameFilters(const FileData* folder); + void setIsGameSystemStatus(); + + FileFilterIndex* mFilterIndex; + + FileData* mRootFolder; +}; + +#endif // ES_APP_SYSTEM_DATA_H diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 7a2d13c20f..44ec65f9b4 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -1,295 +1,325 @@ -#include "GuiGamelistOptions.h" - -#include "guis/GuiGamelistFilter.h" -#include "scrapers/Scraper.h" -#include "views/gamelist/IGameListView.h" -#include "views/UIModeController.h" -#include "views/ViewController.h" -#include "CollectionSystemManager.h" -#include "FileFilterIndex.h" -#include "FileSorts.h" -#include "GuiMetaDataEd.h" -#include "SystemData.h" - -#include "animations/LambdaAnimation.h" - -GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : GuiComponent(window), - mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false) -{ - addChild(&mMenu); - - // check it's not a placeholder folder - if it is, only show "Filter Options" - FileData* file = getGamelist()->getCursor(); - fromPlaceholder = file->isPlaceHolder(); - ComponentListRow row; - - if (!fromPlaceholder) { - // jump to letter - row.elements.clear(); - - // define supported character range - // this range includes all numbers, capital letters, and most reasonable symbols - char startChar = '!'; - char endChar = '_'; - - char curChar = (char)toupper(getGamelist()->getCursor()->getSortName()[0]); - if(curChar < startChar || curChar > endChar) - curChar = startChar; - - mJumpToLetterList = std::make_shared(mWindow, _T("JUMP TO LETTER"), false); - for (char c = startChar; c <= endChar; c++) - { - // check if c is a valid first letter in current list - const std::vector& files = getGamelist()->getCursor()->getParent()->getChildrenListToDisplay(); - for (auto file : files) - { - char candidate = (char)toupper(file->getSortName()[0]); - if (c == candidate) - { - mJumpToLetterList->add(std::string(1, c), c, c == curChar); - break; - } - } - } - - row.addElement(std::make_shared(mWindow, _T("JUMP TO LETTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); - row.addElement(mJumpToLetterList, false); - row.input_handler = [&](InputConfig* config, Input input) - { - if(config->isMappedTo("a", input) && input.value) - { - jumpToLetter(); - return true; - } - else if(mJumpToLetterList->input(config, input)) - { - return true; - } - return false; - }; - mMenu.addRow(row); - - // sort list by - mListSort = std::make_shared(mWindow, _T("SORT GAMES BY"), false); - for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) - { - const FileData::SortType& sort = FileSorts::SortTypes.at(i); - mListSort->add(_L(Utils::String::toUpper(sort.description)), &sort, i == 0); // TODO - actually make the sort type persistent - } - - mMenu.addWithLabel(_T("SORT GAMES BY"), mListSort); - } - // show filtered menu - if(!Settings::getInstance()->getBool("ForceDisableFilters")) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("APPLY FILTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); - row.addElement(makeArrow(mWindow), false); - row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); - mMenu.addRow(row); - } - - std::map customCollections = CollectionSystemManager::get()->getCustomCollectionSystems(); - - if(UIModeController::getInstance()->isUIModeFull() && - ((customCollections.find(system->getName()) != customCollections.cend() && CollectionSystemManager::get()->getEditingCollection() != system->getName()) || - CollectionSystemManager::get()->getCustomCollectionsBundle()->getName() == system->getName())) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, "ADD/REMOVE GAMES TO THIS GAME COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); - row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::startEditMode, this)); - mMenu.addRow(row); - } - - if(UIModeController::getInstance()->isUIModeFull() && CollectionSystemManager::get()->isEditing()) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); - row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::exitEditMode, this)); - mMenu.addRow(row); - } - - if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("EDIT THIS GAME'S METADATA"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), 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); - - float x0 = (mSize.x() - mMenu.getSize().x()) / 2; - +#include "GuiGamelistOptions.h" + +#include "guis/GuiGamelistFilter.h" +#include "scrapers/Scraper.h" +#include "views/gamelist/IGameListView.h" +#include "views/UIModeController.h" +#include "views/ViewController.h" +#include "CollectionSystemManager.h" +#include "FileFilterIndex.h" +#include "FileSorts.h" +#include "GuiMetaDataEd.h" +#include "SystemData.h" + +#include "animations/LambdaAnimation.h" + +GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : GuiComponent(window), + mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false) +{ + addChild(&mMenu); + + // check it's not a placeholder folder - if it is, only show "Filter Options" + FileData* file = getGamelist()->getCursor(); + fromPlaceholder = file->isPlaceHolder(); + ComponentListRow row; + + if (!fromPlaceholder) { + // jump to letter + row.elements.clear(); + + // define supported character range + // this range includes all numbers, capital letters, and most reasonable symbols + char startChar = '!'; + char endChar = '_'; + + char curChar = (char)toupper(getGamelist()->getCursor()->getName()[0]); + if(curChar < startChar || curChar > endChar) + curChar = startChar; + + mJumpToLetterList = std::make_shared(mWindow, _T("JUMP TO LETTER"), false); + for (char c = startChar; c <= endChar; c++) + { + // check if c is a valid first letter in current list + const std::vector& files = getGamelist()->getCursor()->getParent()->getChildrenListToDisplay(); + for (auto file : files) + { + char candidate = (char)toupper(file->getName()[0]); + if (c == candidate) + { + mJumpToLetterList->add(std::string(1, c), c, c == curChar); + break; + } + } + } + + row.addElement(std::make_shared(mWindow, _T("JUMP TO LETTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(mJumpToLetterList, false); + row.input_handler = [&](InputConfig* config, Input input) + { + if(config->isMappedTo("a", input) && input.value) + { + jumpToLetter(); + return true; + } + else if(mJumpToLetterList->input(config, input)) + { + return true; + } + return false; + }; + mMenu.addRow(row); + + // sort list by + mListSort = std::make_shared(mWindow, _T("SORT GAMES BY"), false); + for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) + { + const FileData::SortType& sort = FileSorts::SortTypes.at(i); + mListSort->add(_L(Utils::String::toUpper(sort.description)), &sort, i == 0); // TODO - actually make the sort type persistent + } + + mMenu.addWithLabel(_T("SORT GAMES BY"), mListSort); + } + + // GameList view style + mViewMode = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); + std::vector styles; + styles.push_back("automatic"); + + auto mViews = system->getTheme()->getViewsOfTheme(); + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + styles.push_back(*it); + + std::string viewMode = system->getSystemViewMode(); + + bool found = false; + for (auto it = styles.cbegin(); it != styles.cend(); it++) + { + bool sel = (viewMode.empty() && *it == "automatic") || viewMode == *it; + if (sel) + found = true; + + mViewMode->add(_L(*it), *it, sel); + } + + if (!found) + mViewMode->selectFirstItem(); + + mMenu.addWithLabel(_T("GAMELIST VIEW STYLE"), mViewMode); + + // show filtered menu + if(!Settings::getInstance()->getBool("ForceDisableFilters")) + { + row.elements.clear(); + row.addElement(std::make_shared(mWindow, _T("APPLY FILTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(makeArrow(mWindow), false); + row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); + mMenu.addRow(row); + } + + std::map customCollections = CollectionSystemManager::get()->getCustomCollectionSystems(); + + if(UIModeController::getInstance()->isUIModeFull() && + ((customCollections.find(system->getName()) != customCollections.cend() && CollectionSystemManager::get()->getEditingCollection() != system->getName()) || + CollectionSystemManager::get()->getCustomCollectionsBundle()->getName() == system->getName())) + { + row.elements.clear(); + row.addElement(std::make_shared(mWindow, "ADD/REMOVE GAMES TO THIS GAME COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::startEditMode, this)); + mMenu.addRow(row); + } + + if(UIModeController::getInstance()->isUIModeFull() && CollectionSystemManager::get()->isEditing()) + { + row.elements.clear(); + row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::exitEditMode, this)); + mMenu.addRow(row); + } + + if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) + { + row.elements.clear(); + row.addElement(std::make_shared(mWindow, _T("EDIT THIS GAME'S METADATA"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), 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()); + + float x0 = (mSize.x() - mMenu.getSize().x()) / 2; + float y1 = Renderer::getScreenHeight(); - float y2 = (mSize.y() - mMenu.getSize().y()) / 2; - //float y1 = mMenu.getSize().y(); - //float y2 = (mSize.y() - mMenu.getSize().y()) / 2; - - setPosition(x0, y1); - - auto fadeFunc = [this, x0, y1, y2](float t) { - - t -= 1; // cubic ease out - float pct = Math::lerp(0, 1, t*t*t + 1); - - float y = y1 * (1 - pct) + y2 * pct; - setPosition(x0, y); - }; - - setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, x0, y2] - { + float y2 = (mSize.y() - mMenu.getSize().y()) / 2; + + if (Settings::getInstance()->getString("PowerSaverMode") == "instant" || Settings::getInstance()->getString("TransitionStyle") == "instant") setPosition(x0, y2); - }); - - setPosition(x0, y2); -} - -GuiGamelistOptions::~GuiGamelistOptions() -{ - // apply sort - if (!fromPlaceholder) { - FileData* root = mSystem->getRootFolder(); - root->sort(*mListSort->getSelected()); // will also recursively sort children - - // notify that the root folder was sorted - getGamelist()->onFileChanged(root, FILE_SORTED); - } - if (mFiltersChanged) - { - // only reload full view if we came from a placeholder - // as we need to re-display the remaining elements for whatever new - // game is selected - ViewController::get()->reloadGameListView(mSystem); - } -} - -void GuiGamelistOptions::openGamelistFilter() -{ - mFiltersChanged = true; - GuiGamelistFilter* ggf = new GuiGamelistFilter(mWindow, mSystem); - mWindow->pushGui(ggf); -} - -void GuiGamelistOptions::startEditMode() -{ - std::string editingSystem = mSystem->getName(); - // need to check if we're editing the collections bundle, as we will want to edit the selected collection within - if(editingSystem == CollectionSystemManager::get()->getCustomCollectionsBundle()->getName()) - { - FileData* file = getGamelist()->getCursor(); - // do we have the cursor on a specific collection? - if (file->getType() == FOLDER) - { - editingSystem = file->getName(); - } - else - { - // we are inside a specific collection. We want to edit that one. - editingSystem = file->getSystem()->getName(); - } - } - CollectionSystemManager::get()->setEditMode(editingSystem); - delete this; -} - -void GuiGamelistOptions::exitEditMode() -{ - CollectionSystemManager::get()->exitEditMode(); - delete this; -} - -void GuiGamelistOptions::openMetaDataEd() -{ - // open metadata editor - // get the FileData that hosts the original metadata - FileData* file = getGamelist()->getCursor()->getSourceFileData(); - ScraperSearchParams p; - p.game = file; - p.system = file->getSystem(); - - std::function deleteBtnFunc; - - if (file->getType() == FOLDER) - { - deleteBtnFunc = NULL; - } - else - { - deleteBtnFunc = [this, file] { - CollectionSystemManager::get()->deleteCollectionFiles(file); - ViewController::get()->getGameListView(file->getSystem()).get()->remove(file, true); - }; - } - - mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(), p, Utils::FileSystem::getFileName(file->getPath()), - std::bind(&IGameListView::onFileChanged, ViewController::get()->getGameListView(file->getSystem()).get(), file, FILE_METADATA_CHANGED), deleteBtnFunc, file)); -} - -void GuiGamelistOptions::jumpToLetter() -{ - char letter = mJumpToLetterList->getSelected(); - IGameListView* gamelist = getGamelist(); - - // this is a really shitty way to get a list of files - const std::vector& files = gamelist->getCursor()->getParent()->getChildrenListToDisplay(); - - long min = 0; - long max = (long)files.size() - 1; - long mid = 0; - - while(max >= min) - { - mid = ((max - min) / 2) + min; - - // game somehow has no first character to check - if(files.at(mid)->getName().empty()) - continue; - - char checkLetter = (char)toupper(files.at(mid)->getSortName()[0]); - - if(checkLetter < letter) - min = mid + 1; - else if(checkLetter > letter || (mid > 0 && (letter == toupper(files.at(mid - 1)->getSortName()[0])))) - max = mid - 1; - else - break; //exact match found - } - - gamelist->setCursor(files.at(mid)); - - delete this; -} - -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); -} - -HelpStyle GuiGamelistOptions::getHelpStyle() -{ - HelpStyle style = HelpStyle(); - style.applyTheme(mSystem->getTheme(), "system"); - return style; -} - -std::vector GuiGamelistOptions::getHelpPrompts() -{ - auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("CLOSE"))); - return prompts; -} - -IGameListView* GuiGamelistOptions::getGamelist() -{ - return ViewController::get()->getGameListView(mSystem).get(); -} + else + { + setPosition(x0, y1); + + auto fadeFunc = [this, x0, y1, y2](float t) { + + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + float y = y1 * (1 - pct) + y2 * pct; + setPosition(x0, y); + }; + + setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, x0, y2] + { + setPosition(x0, y2); + }); + + setPosition(x0, y2); + } +} + +GuiGamelistOptions::~GuiGamelistOptions() +{ + // apply sort + if (!fromPlaceholder) { + FileData* root = mSystem->getRootFolder(); + root->sort(*mListSort->getSelected()); // will also recursively sort children + + // notify that the root folder was sorted + getGamelist()->onFileChanged(root, FILE_SORTED); + } + + if (mSystem->setSystemViewMode(mViewMode->getSelected()) || mFiltersChanged) + { + // only reload full view if we came from a placeholder + // as we need to re-display the remaining elements for whatever new + // game is selected + ViewController::get()->reloadGameListView(mSystem); + } +} + +void GuiGamelistOptions::openGamelistFilter() +{ + mFiltersChanged = true; + GuiGamelistFilter* ggf = new GuiGamelistFilter(mWindow, mSystem); + mWindow->pushGui(ggf); +} + +void GuiGamelistOptions::startEditMode() +{ + std::string editingSystem = mSystem->getName(); + // need to check if we're editing the collections bundle, as we will want to edit the selected collection within + if(editingSystem == CollectionSystemManager::get()->getCustomCollectionsBundle()->getName()) + { + FileData* file = getGamelist()->getCursor(); + // do we have the cursor on a specific collection? + if (file->getType() == FOLDER) + { + editingSystem = file->getName(); + } + else + { + // we are inside a specific collection. We want to edit that one. + editingSystem = file->getSystem()->getName(); + } + } + CollectionSystemManager::get()->setEditMode(editingSystem); + delete this; +} + +void GuiGamelistOptions::exitEditMode() +{ + CollectionSystemManager::get()->exitEditMode(); + delete this; +} + +void GuiGamelistOptions::openMetaDataEd() +{ + // open metadata editor + // get the FileData that hosts the original metadata + FileData* file = getGamelist()->getCursor()->getSourceFileData(); + ScraperSearchParams p; + p.game = file; + p.system = file->getSystem(); + + std::function deleteBtnFunc; + + if (file->getType() == FOLDER) + { + deleteBtnFunc = NULL; + } + else + { + deleteBtnFunc = [this, file] { + CollectionSystemManager::get()->deleteCollectionFiles(file); + ViewController::get()->getGameListView(file->getSystem()).get()->remove(file, true); + }; + } + + mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(), p, Utils::FileSystem::getFileName(file->getPath()), + std::bind(&IGameListView::onFileChanged, ViewController::get()->getGameListView(file->getSystem()).get(), file, FILE_METADATA_CHANGED), deleteBtnFunc, file)); +} + +void GuiGamelistOptions::jumpToLetter() +{ + char letter = mJumpToLetterList->getSelected(); + IGameListView* gamelist = getGamelist(); + + // this is a really shitty way to get a list of files + const std::vector& files = gamelist->getCursor()->getParent()->getChildrenListToDisplay(); + + long min = 0; + long max = (long)files.size() - 1; + long mid = 0; + + while(max >= min) + { + mid = ((max - min) / 2) + min; + + // game somehow has no first character to check + if(files.at(mid)->getName().empty()) + continue; + + char checkLetter = (char)toupper(files.at(mid)->getName()[0]); + + if(checkLetter < letter) + min = mid + 1; + else if(checkLetter > letter || (mid > 0 && (letter == toupper(files.at(mid - 1)->getName()[0])))) + max = mid - 1; + else + break; //exact match found + } + + gamelist->setCursor(files.at(mid)); + + delete this; +} + +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); +} + +HelpStyle GuiGamelistOptions::getHelpStyle() +{ + HelpStyle style = HelpStyle(); + style.applyTheme(mSystem->getTheme(), "system"); + return style; +} + +std::vector GuiGamelistOptions::getHelpPrompts() +{ + auto prompts = mMenu.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", _T("CLOSE"))); + return prompts; +} + +IGameListView* GuiGamelistOptions::getGamelist() +{ + return ViewController::get()->getGameListView(mSystem).get(); +} diff --git a/es-app/src/guis/GuiGamelistOptions.h b/es-app/src/guis/GuiGamelistOptions.h index acf861669a..d6bfaf781c 100644 --- a/es-app/src/guis/GuiGamelistOptions.h +++ b/es-app/src/guis/GuiGamelistOptions.h @@ -35,6 +35,9 @@ class GuiGamelistOptions : public GuiComponent typedef OptionListComponent SortList; std::shared_ptr mListSort; + std::shared_ptr> mViewMode; + + SystemData* mSystem; IGameListView* getGamelist(); bool fromPlaceholder; diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 8173bc36c1..ed37b73f28 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -29,25 +29,25 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN if (isFullUI) { - addEntry(_T("UI SETTINGS"), theme->Text.color, true, [this] { openUISettings(); }); - addEntry(_T("CONFIGURE INPUT"), theme->Text.color, true, [this] { openConfigInput(); }); + addEntry(_T("UI SETTINGS"), theme->Text.color, true, [this] { openUISettings(); }, theme->MenuIcons.ui); + addEntry(_T("CONFIGURE INPUT"), theme->Text.color, true, [this] { openConfigInput(); }, theme->MenuIcons.controllers); } - addEntry(_T("SOUND SETTINGS"), theme->Text.color, true, [this] { openSoundSettings(); }); + addEntry(_T("SOUND SETTINGS"), theme->Text.color, true, [this] { openSoundSettings(); }, theme->MenuIcons.sound); if (isFullUI) - addEntry(_T("SCRAPER"), theme->Text.color, true, [this] { openScraperSettings(); }); + addEntry(_T("SCRAPER"), theme->Text.color, true, [this] { openScraperSettings(); }, theme->MenuIcons.scraper); if (isFullUI) { - addEntry(_T("GAME COLLECTION SETTINGS"), theme->Text.color, true, [this] { openCollectionSystemSettings(); }); - addEntry(_T("ADVANCED SETTINGS"), theme->Text.color, true, [this] { openOtherSettings(); }); + addEntry(_T("GAME COLLECTION SETTINGS"), theme->Text.color, true, [this] { openCollectionSystemSettings(); }, theme->MenuIcons.games); + addEntry(_T("ADVANCED SETTINGS"), theme->Text.color, true, [this] { openOtherSettings(); }, theme->MenuIcons.advanced); } -#if defined(_WIN32) - addEntry(_T("QUIT"), theme->Text.color, false, [this] {openQuitMenu(); }); +#ifdef WIN32 + addEntry(_T("QUIT"), theme->Text.color, false, [this] {openQuitMenu(); }, theme->MenuIcons.quit); #else - addEntry(_T("QUIT"), theme->Text.color, true, [this] {openQuitMenu(); }); + addEntry(_T("QUIT"), theme->Text.color, true, [this] {openQuitMenu(); }, theme->MenuIcons.quit); #endif addChild(&mMenu); @@ -58,23 +58,28 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN float y1 = Renderer::getScreenHeight(); float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); + if (Settings::getInstance()->getString("PowerSaverMode") == "instant" || Settings::getInstance()->getString("TransitionStyle") == "instant") + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + else + { + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); - auto fadeFunc = [this, y1, y2](float t) { - - t -= 1; // cubic ease out - float pct = Math::lerp(0, 1, t*t*t + 1); + auto fadeFunc = [this, y1, y2](float t) { - float y = y1 * (1 - pct) + y2 * pct; - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y); - }; + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + float y = y1 * (1 - pct) + y2 * pct; + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y); + }; + + setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] + { + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + }); - setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] - { setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); - }); - - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + } } void GuiMenu::openScraperSettings() @@ -234,6 +239,8 @@ void GuiMenu::openUISettings() { auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); + auto pthis = this; + // theme set auto theme = ThemeData::getMenuTheme(); auto themeSets = ThemeData::getThemeSets(); @@ -250,31 +257,281 @@ void GuiMenu::openUISettings() s->addWithLabel(_T("THEME"), theme_set); Window* window = mWindow; - s->addSaveFunc([window, theme_set] + s->addSaveFunc([window, theme_set, pthis] { - bool needReload = false; std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); if (oldTheme != theme_set->getSelected()) - needReload = true; + { + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); + + auto themeSubSets = ThemeData::getThemeSubSets(theme_set->getSelected()); + auto themeColorSets = ThemeData::sortThemeSubSets(themeSubSets, "colorset"); + auto themeIconSets = ThemeData::sortThemeSubSets(themeSubSets, "iconset"); + auto themeMenus = ThemeData::sortThemeSubSets(themeSubSets, "menu"); + auto themeSystemviewSets = ThemeData::sortThemeSubSets(themeSubSets, "systemview"); + auto themeGamelistViewSets = ThemeData::sortThemeSubSets(themeSubSets, "gamelistview"); + auto themeRegions = ThemeData::sortThemeSubSets(themeSubSets, "region"); + + // theme changed without setting options, forcing options to avoid crash/blank theme + if (themeRegions.empty()) + Settings::getInstance()->setString("ThemeRegionName", ""); + else + Settings::getInstance()->setString("ThemeRegionName", themeRegions.begin()->first); + + if (themeColorSets.empty()) + Settings::getInstance()->setString("ThemeColorSet", ""); + else + Settings::getInstance()->setString("ThemeColorSet", themeColorSets.begin()->first); + + if (themeIconSets.empty()) + Settings::getInstance()->setString("ThemeIconSet", ""); + else + Settings::getInstance()->setString("ThemeIconSet", themeIconSets.begin()->first); + + if (themeMenus.empty()) + Settings::getInstance()->setString("ThemeMenu", ""); + else + Settings::getInstance()->setString("ThemeMenu", themeMenus.begin()->first); + + if (themeSystemviewSets.empty()) + Settings::getInstance()->setString("ThemeSystemView", ""); + else + Settings::getInstance()->setString("ThemeSystemView", themeSystemviewSets.begin()->first); + + if (themeGamelistViewSets.empty()) + Settings::getInstance()->setString("ThemeGamelistView", ""); + else + Settings::getInstance()->setString("ThemeGamelistView", themeGamelistViewSets.begin()->first); + - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - if (needReload) - { Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); CollectionSystemManager::get()->updateSystemsList(); ViewController::get()->goToStart(); ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + + delete pthis; + window->pushGui(new GuiMenu(window)); } }); + + + auto system = ViewController::get()->getState().getSystem(); + if (system != NULL && system->getTheme()->hasSubsets()) + { + + // theme config + std::function openGui = [this, theme_set, window, pthis] { + auto themeconfig = new GuiSettings(mWindow, _T("THEME CONFIGURATION").c_str()); + + auto SelectedTheme = theme_set->getSelected(); + + auto themeSubSets = ThemeData::getThemeSubSets(SelectedTheme); + auto themeColorSets = ThemeData::sortThemeSubSets(themeSubSets, "colorset"); + auto themeIconSets = ThemeData::sortThemeSubSets(themeSubSets, "iconset"); + auto themeMenus = ThemeData::sortThemeSubSets(themeSubSets, "menu"); + auto themeSystemviewSets = ThemeData::sortThemeSubSets(themeSubSets, "systemview"); + auto themeGamelistViewSets = ThemeData::sortThemeSubSets(themeSubSets, "gamelistview"); + auto themeRegions = ThemeData::sortThemeSubSets(themeSubSets, "region"); + + // colorset + + auto selectedColorSet = themeColorSets.find(Settings::getInstance()->getString("ThemeColorSet")); + if (selectedColorSet == themeColorSets.end()) + selectedColorSet = themeColorSets.begin(); + auto theme_colorset = std::make_shared >(mWindow, _T("THEME COLORSET"), false); + + for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) + theme_colorset->add(it->first, it->first, it == selectedColorSet); + + if (!themeColorSets.empty()) + themeconfig->addWithLabel(_T("THEME COLORSET"), theme_colorset); + + // iconset + + auto selectedIconSet = themeIconSets.find(Settings::getInstance()->getString("ThemeIconSet")); + if (selectedIconSet == themeIconSets.end()) + selectedIconSet = themeIconSets.begin(); + auto theme_iconset = std::make_shared >(mWindow, _T("THEME ICONSET"), false); + + for (auto it = themeIconSets.begin(); it != themeIconSets.end(); it++) + theme_iconset->add(it->first, it->first, it == selectedIconSet); + + if (!themeIconSets.empty()) + themeconfig->addWithLabel(_T("THEME ICONSET"), theme_iconset); + + // menu + + auto selectedMenu = themeMenus.find(Settings::getInstance()->getString("ThemeMenu")); + if (selectedMenu == themeMenus.end()) + selectedMenu = themeMenus.begin(); + auto theme_menu = std::make_shared >(mWindow, _T("THEME MENU"), false); + + for (auto it = themeMenus.begin(); it != themeMenus.end(); it++) + theme_menu->add(it->first, it->first, it == selectedMenu); + + if (!themeMenus.empty()) + themeconfig->addWithLabel(_T("THEME MENU"), theme_menu); + + // systemview + + auto selectedSystemviewSet = themeSystemviewSets.find(Settings::getInstance()->getString("ThemeSystemView")); + if (selectedSystemviewSet == themeSystemviewSets.end()) + selectedSystemviewSet = themeSystemviewSets.begin(); + + auto theme_systemview = std::make_shared >(mWindow, _T("THEME SYSTEMVIEW"), false); + + for (auto it = themeSystemviewSets.begin(); it != themeSystemviewSets.end(); it++) + theme_systemview->add(it->first, it->first, it == selectedSystemviewSet); + + if (!themeSystemviewSets.empty()) + themeconfig->addWithLabel(_T("THEME SYSTEMVIEW"), theme_systemview); + + // gamelistview + + auto selectedGamelistViewSet = themeGamelistViewSets.find(Settings::getInstance()->getString("ThemeGamelistView")); + if (selectedGamelistViewSet == themeGamelistViewSets.end()) + selectedGamelistViewSet = themeGamelistViewSets.begin(); + + auto theme_gamelistview = std::make_shared >(mWindow, _T("THEME GAMELISTVIEW"), false); + + for (auto it = themeGamelistViewSets.begin(); it != themeGamelistViewSets.end(); it++) + theme_gamelistview->add(it->first, it->first, it == selectedGamelistViewSet); + + if (!themeGamelistViewSets.empty()) + themeconfig->addWithLabel(_T("THEME GAMELISTVIEW"), theme_gamelistview); + + // themeregion + auto selectedRegion = themeRegions.find(Settings::getInstance()->getString("ThemeRegionName")); + if (selectedRegion == themeRegions.end()) + selectedRegion = themeRegions.begin(); + + auto theme_region = std::make_shared >(mWindow, _T("THEME GAMELISTVIEW"), false); + + for (auto it = themeRegions.begin(); it != themeRegions.end(); it++) + theme_region->add(it->first, it->first, it == selectedRegion); + + if (!themeRegions.empty()) + themeconfig->addWithLabel(_T("THEME REGION"), theme_region); + + themeconfig->addSaveFunc([this, window, theme_set, theme_colorset, theme_iconset, theme_menu, theme_systemview, theme_gamelistview, theme_region, pthis] { + bool needReload = false; + if (Settings::getInstance()->getString("ThemeColorSet") != theme_colorset->getSelected() && !theme_colorset->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeIconSet") != theme_iconset->getSelected() && !theme_iconset->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeMenu") != theme_menu->getSelected() && !theme_menu->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeSystemView") != theme_systemview->getSelected() && !theme_systemview->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeGamelistView") != theme_gamelistview->getSelected() && !theme_gamelistview->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeRegionName") != theme_region->getSelected() && !theme_region->getSelected().empty()) + needReload = true; + + if (needReload) { + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); + Settings::getInstance()->setString("ThemeColorSet", theme_colorset->getSelected()); + Settings::getInstance()->setString("ThemeIconSet", theme_iconset->getSelected()); + Settings::getInstance()->setString("ThemeMenu", theme_menu->getSelected()); + Settings::getInstance()->setString("ThemeSystemView", theme_systemview->getSelected()); + Settings::getInstance()->setString("ThemeGamelistView", theme_gamelistview->getSelected()); + Settings::getInstance()->setString("ThemeRegionName", theme_region->getSelected()); + //Settings::getInstance()->setBool("ThemeChanged", true); + + //reload theme + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + + delete pthis; + window->pushGui(new GuiMenu(window)); + //ReloadAll(); + } + }); + if (!themeRegions.empty() || !themeGamelistViewSets.empty() || !themeSystemviewSets.empty() || !themeIconSets.empty() || !themeMenus.empty() || !themeColorSets.empty()) + { + themeconfig->updatePosition(); + mWindow->pushGui(themeconfig); + } + else + mWindow->pushGui(new GuiMsgBox(window, _T("THIS THEME HAS NO OPTION"), _T("OK"))); + }; + + s->addSubMenu(_T("THEME CONFIGURATION"), openGui); + } + } + + // GameList view style + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); + std::vector styles; + styles.push_back("automatic"); + + + auto system = ViewController::get()->getState().getSystem(); + if (system != NULL) + { + auto mViews = system->getTheme()->getViewsOfTheme(); + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + styles.push_back(*it); } + else + { + styles.push_back("basic"); + styles.push_back("detailed"); + styles.push_back("video"); + styles.push_back("grid"); + } + + for (auto it = styles.cbegin(); it != styles.cend(); it++) + gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); + + s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); + s->addSaveFunc([gamelist_style] { + bool needReload = false; + if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) + needReload = true; + Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); + if (needReload) + ViewController::get()->reloadAll(); + }); + + + //#ifndef WIN32 + //UI mode + auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); + std::vector UImodes = UIModeController::getInstance()->getUIModes(); + for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) + UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); + s->addWithLabel("UI MODE", UImodeSelection); + Window* window = mWindow; + s->addSaveFunc([UImodeSelection, window] + { + std::string selectedMode = UImodeSelection->getSelected(); + if (selectedMode != "Full") + { + std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; + msg += "This will hide most menu-options to prevent changes to the system.\n"; + msg += "To unlock and return to the full UI, enter this code: \n"; + msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; + msg += "Do you want to proceed?"; + window->pushGui(new GuiMsgBox(window, msg, + "YES", [selectedMode] { + LOG(LogDebug) << "Setting UI mode to " << selectedMode; + Settings::getInstance()->setString("UIMode", selectedMode); + Settings::getInstance()->saveFile(); + }, "NO", nullptr)); + } + }); + //#endif // LANGUAGE std::vector langues; langues.push_back("en"); - std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); if (xmlpath.length() > 0) { @@ -354,29 +611,6 @@ void GuiMenu::openUISettings() Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); }); - // GameList view style - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); - std::vector styles; - styles.push_back("automatic"); - styles.push_back("basic"); - styles.push_back("detailed"); - styles.push_back("video"); - styles.push_back("grid"); - styles.push_back("gridex"); - - for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); - - s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); - s->addSaveFunc([gamelist_style] { - bool needReload = false; - if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) - needReload = true; - Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); - if (needReload) - ViewController::get()->reloadAll(); - }); - // Optionally start in selected system auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); @@ -392,43 +626,19 @@ void GuiMenu::openUISettings() s->addSaveFunc([systemfocus_list] { Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); }); - - - /* - //UI mode - auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); - std::vector UImodes = UIModeController::getInstance()->getUIModes(); - for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) - UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); - s->addWithLabel("UI MODE", UImodeSelection); - Window* window = mWindow; - s->addSaveFunc([ UImodeSelection, window] - { - std::string selectedMode = UImodeSelection->getSelected(); - if (selectedMode != "Full") - { - std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; - msg += "This will hide most menu-options to prevent changes to the system.\n"; - msg += "To unlock and return to the full UI, enter this code: \n"; - msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; - msg += "Do you want to proceed?"; - window->pushGui(new GuiMsgBox(window, msg, - "YES", [selectedMode] { - LOG(LogDebug) << "Setting UI mode to " << selectedMode; - Settings::getInstance()->setString("UIMode", selectedMode); - Settings::getInstance()->saveFile(); - }, "NO",nullptr)); - } - }); - */ - - // screensaver - ComponentListRow screensaver_row; - screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); - screensaver_row.addElement(makeArrow(mWindow), false); - screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); - s->addRow(screensaver_row); + + // Open gamelist at start + auto bootOnGamelist = std::make_shared(mWindow); + bootOnGamelist->setState(Settings::getInstance()->getBool("StartupOnGameList")); + s->addWithLabel(_T("BOOT ON GAMELIST"), bootOnGamelist); + s->addSaveFunc([bootOnGamelist] { Settings::getInstance()->setBool("StartupOnGameList", bootOnGamelist->getState()); }); + + // Hide system view + auto hideSystemView = std::make_shared(mWindow); + hideSystemView->setState(Settings::getInstance()->getBool("HideSystemView")); + s->addWithLabel(_T("HIDE SYSTEM VIEW"), hideSystemView); + s->addSaveFunc([hideSystemView] { Settings::getInstance()->setBool("HideSystemView", hideSystemView->getState()); }); + #if defined(_WIN32) // quick system select (left/right in game list view) @@ -475,9 +685,18 @@ void GuiMenu::openUISettings() if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); }); + + // screensaver + ComponentListRow screensaver_row; + screensaver_row.elements.clear(); + screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); + screensaver_row.addElement(makeArrow(mWindow), false); + screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); + s->addRow(screensaver_row); + + s->updatePosition(); mWindow->pushGui(s); - } void GuiMenu::openOtherSettings() @@ -504,6 +723,7 @@ void GuiMenu::openOtherSettings() s->addSaveFunc([this, power_saver] { if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { Settings::getInstance()->setString("TransitionStyle", "instant"); + Settings::getInstance()->setString("GameTransitionStyle", "instant"); Settings::getInstance()->setBool("MoveCarousel", false); Settings::getInstance()->setBool("EnableSounds", false); } @@ -521,12 +741,14 @@ void GuiMenu::openOtherSettings() parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); - /* + +#ifndef WIN32 auto local_art = std::make_shared(mWindow); local_art->setState(Settings::getInstance()->getBool("LocalArt")); s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); - */ +#endif + // hidden files auto hidden_files = std::make_shared(mWindow); hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); @@ -553,7 +775,13 @@ void GuiMenu::openOtherSettings() #endif -#if defined(_WIN32) + // preload UI + auto preloadUI = std::make_shared(mWindow); + preloadUI->setState(Settings::getInstance()->getBool("PreloadUI")); + s->addWithLabel(_T("PRELOAD UI"), preloadUI); + s->addSaveFunc([preloadUI] { Settings::getInstance()->setBool("PreloadUI", preloadUI->getState()); }); + +#ifdef WIN32 // vsync auto vsync = std::make_shared(mWindow); vsync->setState(Settings::getInstance()->getBool("VSync")); @@ -585,7 +813,6 @@ void GuiMenu::openOtherSettings() framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); s->addWithLabel(_T("SHOW FRAMERATE"), framerate); s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); - s->updatePosition(); mWindow->pushGui(s); @@ -607,7 +834,7 @@ void GuiMenu::openConfigInput() void GuiMenu::openQuitMenu() { -#if defined(_WIN32) +#ifdef WIN32 Scripting::fireEvent("quit"); quitES(""); return; @@ -705,7 +932,7 @@ void GuiMenu::onSizeChanged() mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); } -void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) +void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func, const std::string iconName) { auto theme = ThemeData::getMenuTheme(); std::shared_ptr font = theme->Text.font; @@ -713,6 +940,22 @@ void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, con // populate the list ComponentListRow row; + + if (!iconName.empty()) + { + // icon + auto icon = std::make_shared(mWindow); + icon->setImage(iconName); + icon->setColorShift(theme->Text.color); + icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); + row.addElement(icon, false); + + // spacer between icon and text + auto spacer = std::make_shared(mWindow); + spacer->setSize(10, 0); + row.addElement(spacer, false); + } + row.addElement(std::make_shared(mWindow, name, font, color), true); if (add_arrow) diff --git a/es-app/src/guis/GuiMenu.h b/es-app/src/guis/GuiMenu.h index 25fdcc2634..99c7ef5445 100644 --- a/es-app/src/guis/GuiMenu.h +++ b/es-app/src/guis/GuiMenu.h @@ -16,7 +16,7 @@ class GuiMenu : public GuiComponent HelpStyle getHelpStyle() override; private: - void addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func); + void addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func, const std::string iconName); void addVersionInfo(); void openCollectionSystemSettings(); void openConfigInput(); diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index ee9abc7a1f..285e5f0d92 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -209,7 +209,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector row.addElement(spacer, false); auto bracket = std::make_shared(mWindow); - bracket->setImage(":/arrow.svg"); + bracket->setImage(ThemeData::getMenuTheme()->Icons.arrow);// ":/arrow.svg"); bracket->setResize(Vector2f(0, lbl->getFont()->getLetterHeight())); row.addElement(bracket, false); diff --git a/es-app/src/guis/GuiSettings.h b/es-app/src/guis/GuiSettings.h index ac9a22d084..c9793156a5 100644 --- a/es-app/src/guis/GuiSettings.h +++ b/es-app/src/guis/GuiSettings.h @@ -18,6 +18,18 @@ class GuiSettings : public GuiComponent inline void addWithLabel(const std::string& label, const std::shared_ptr& comp) { mMenu.addWithLabel(label, comp); }; inline void addSaveFunc(const std::function& func) { mSaveFuncs.push_back(func); }; + inline void addSubMenu(const std::string& label, const std::function& func) { + ComponentListRow row; + row.makeAcceptInputHandler(func); + + auto theme = ThemeData::getMenuTheme(); + + auto entryMenu = std::make_shared(mWindow, label, theme->Text.font, theme->Text.color); + row.addElement(entryMenu, true); + row.addElement(makeArrow(mWindow), false); + mMenu.addRow(row); + }; + bool input(InputConfig* config, Input input) override; std::vector getHelpPrompts() override; HelpStyle getHelpStyle() override; diff --git a/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp b/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp index e29b3e2e9a..de06342efe 100644 --- a/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp +++ b/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp @@ -95,7 +95,7 @@ void GuiSlideshowScreensaverOptions::addEditableTextComponent(ComponentListRow r row.addElement(spacer, false); auto bracket = std::make_shared(mWindow); - bracket->setImage(":/arrow.svg"); + bracket->setImage(ThemeData::getMenuTheme()->Icons.arrow); // ":/arrow.svg"); bracket->setResize(Vector2f(0, lbl->getFont()->getLetterHeight())); row.addElement(bracket, false); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 2f42e183f5..cd8edc9cf0 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -338,7 +338,7 @@ int main(int argc, char* argv[]) if (!scrape_cmdline) { - if(!window.init()) + if(!window.init(true)) { LOG(LogError) << "Window failed to initialize!"; return 1; @@ -385,7 +385,9 @@ int main(int argc, char* argv[]) // 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(); + + if (Settings::getInstance()->getBool("PreloadUI")) + ViewController::get()->preload(); if (splashScreen && splashScreenProgress) window.renderLoadingScreen(_T("Starting UI")); @@ -417,7 +419,7 @@ int main(int argc, char* argv[]) displayFrequency = lpDevMode.dmDisplayFrequency; // default value if cannot retrieve from user settings. } - int timeLimit = (1000 / displayFrequency) - 5; // Margin for vsync + int timeLimit = (1000 / displayFrequency) - 6; // Margin for vsync #endif int lastTime = SDL_GetTicks(); @@ -502,7 +504,7 @@ int main(int argc, char* argv[]) while(window.peekGui() != ViewController::get()) delete window.peekGui(); - window.deinit(); + window.deinit(true); MameNames::deinit(); CollectionSystemManager::deinit(); diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 15fd7eb10a..9f8de5b5bf 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -45,6 +45,9 @@ ViewController::~ViewController() void ViewController::goToStart(bool forceImmediate) { + bool hideSystemView = Settings::getInstance()->getBool("HideSystemView"); + bool startOnGamelist = Settings::getInstance()->getBool("StartupOnGameList"); + // If specific system is requested, go directly to the game list auto requestedSystem = Settings::getInstance()->getString("StartupSystem"); if("" != requestedSystem && "retropie" != requestedSystem) @@ -52,8 +55,11 @@ void ViewController::goToStart(bool forceImmediate) for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++){ if ((*it)->getName() == requestedSystem) { - goToSystemView(*it, forceImmediate); -// goToGameList(*it); + if (hideSystemView || startOnGamelist) + goToGameList(*it); + else + goToSystemView(*it, forceImmediate); + return; } } @@ -62,7 +68,10 @@ void ViewController::goToStart(bool forceImmediate) Settings::getInstance()->setString("StartupSystem", ""); } - goToSystemView(SystemData::sSystemVector.at(0), forceImmediate); + if (hideSystemView || startOnGamelist) + goToGameList(SystemData::sSystemVector.at(0)); + else + goToSystemView(SystemData::sSystemVector.at(0), forceImmediate); } void ViewController::ReloadAndGoToStart() @@ -284,75 +293,86 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste std::shared_ptr view; bool themeHasVideoView = system->getTheme()->hasView("video"); - bool themeHasGridView = system->getTheme()->hasView("grid"); - bool themeHasGridExView = system->getTheme()->hasView("gridex"); //decide type GameListViewType selectedViewType = AUTOMATIC; - bool detailed = false; + bool allowDetailedDowngrade = false; + bool forceView = false; std::string viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + std::string customThemeName; + + if (!system->getSystemViewMode().empty() && system->getTheme()->hasView(system->getSystemViewMode())) + { + viewPreference = system->getSystemViewMode(); + forceView = true; + } + + if (viewPreference == "automatic") + { + auto defaultView = system->getTheme()->getDefaultView(); + if (!defaultView.empty() && system->getTheme()->hasView(defaultView)) + viewPreference = defaultView; + } + + if (system->getTheme()->isCustomView(viewPreference)) + { + auto baseClass = system->getTheme()->getCustomViewBaseType(viewPreference); + if (!baseClass.empty()) // this is a customView + { + customThemeName = viewPreference; + viewPreference = baseClass; + } + } if (viewPreference.compare("basic") == 0) selectedViewType = BASIC; else if (viewPreference.compare("detailed") == 0) { - detailed = true; + allowDetailedDowngrade = true; selectedViewType = DETAILED; } - else if (themeHasGridExView && viewPreference.compare("gridex") == 0) - selectedViewType = GRIDEX; else if (themeHasGridView && viewPreference.compare("grid") == 0) selectedViewType = GRID; else if (viewPreference.compare("video") == 0) selectedViewType = VIDEO; - if (selectedViewType == AUTOMATIC || detailed) - { - if (themeHasGridView && system->getTheme()->getDefaultView() == "grid" && !detailed) - selectedViewType = GRID; - else if (themeHasGridExView && system->getTheme()->getDefaultView() == "gridex" && !detailed) - selectedViewType = GRIDEX; - else - { - selectedViewType = BASIC; + if (!forceView && (selectedViewType == AUTOMATIC || allowDetailedDowngrade)) + { + selectedViewType = BASIC; - if (system->getTheme()->getDefaultView() == "detailed") - selectedViewType = DETAILED; - else if (system->getTheme()->getDefaultView() != "basic") + if (system->getTheme()->getDefaultView() != "basic") + { + std::vector files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER); + for (auto it = files.cbegin(); it != files.cend(); it++) { - std::vector files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER); - for (auto it = files.cbegin(); it != files.cend(); it++) + if (themeHasVideoView && !(*it)->getVideoPath().empty() && viewPreference.compare("detailed") != 0) { - if (themeHasVideoView && !(*it)->getVideoPath().empty() && viewPreference.compare("detailed") != 0) - { - selectedViewType = VIDEO; - break; - } - else if (!(*it)->getThumbnailPath().empty()) + selectedViewType = VIDEO; + break; + } + else if (!(*it)->getThumbnailPath().empty()) + { + /* + if (!allowDetailedDowngrade && (*it)->metadata.get("thumbnail").length() > 0) { - if (!detailed && (*it)->metadata.get("thumbnail").length() > 0) - { - if (themeHasGridExView && (*it)->metadata.get("image").length() > 0) - selectedViewType = GRIDEX; - else if (themeHasGridView) - selectedViewType = GRID; - else - selectedViewType = DETAILED; - } + if (themeHasGridView) + selectedViewType = GRID; else selectedViewType = DETAILED; + } + else*/ + selectedViewType = DETAILED; - if (!themeHasVideoView) - break; + if (!themeHasVideoView) + break; - // Don't break out in case any subsequent files have video - } + // Don't break out in case any subsequent files have video } } - } + } } // Create the view @@ -364,22 +384,18 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste case DETAILED: view = std::shared_ptr(new DetailedGameListView(mWindow, system->getRootFolder())); break; - case GRID: + case GRID: view = std::shared_ptr(new GridGameListView(mWindow, system->getRootFolder())); break; - case GRIDEX: - { - GridGameListView* listView = new GridGameListView(mWindow, system->getRootFolder()); - listView->setGridEx(); - view = std::shared_ptr(listView); - } - break; case BASIC: default: view = std::shared_ptr(new BasicGameListView(mWindow, system->getRootFolder())); break; } + if (!customThemeName.empty()) + view->setThemeName(customThemeName); + view->setTheme(system->getTheme()); std::vector& sysVec = SystemData::sSystemVector; @@ -536,7 +552,6 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) // Redisplay the current view if (mCurrentView) mCurrentView->onShow(); - } void ViewController::reloadAll() diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h index 7aef661c1c..27900588f9 100644 --- a/es-app/src/views/ViewController.h +++ b/es-app/src/views/ViewController.h @@ -62,8 +62,8 @@ class ViewController : public GuiComponent BASIC, DETAILED, GRID, - VIDEO, - GRIDEX + VIDEO + //,GRIDEX }; struct State diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index c697a09130..51e51c598a 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -18,6 +18,12 @@ BasicGameListView::BasicGameListView(Window* window, FileData* root) populateList(root->getChildrenListToDisplay()); } +void BasicGameListView::setThemeName(std::string name) +{ + ISimpleGameListView::setThemeName(name); + // mGrid.setThemeName(getName()); +} + void BasicGameListView::onThemeChanged(const std::shared_ptr& theme) { ISimpleGameListView::onThemeChanged(theme); diff --git a/es-app/src/views/gamelist/BasicGameListView.h b/es-app/src/views/gamelist/BasicGameListView.h index f1a216b0f5..7cc8351798 100644 --- a/es-app/src/views/gamelist/BasicGameListView.h +++ b/es-app/src/views/gamelist/BasicGameListView.h @@ -18,11 +18,19 @@ class BasicGameListView : public ISimpleGameListView virtual FileData* getCursor() override; virtual void setCursor(FileData* file) override; - virtual const char* getName() const override { return "basic"; } + virtual const char* getName() const override + { + if (!mCustomThemeName.empty()) + return mCustomThemeName.c_str(); + + return "basic"; + } virtual std::vector getHelpPrompts() override; virtual void launch(FileData* game) override; + virtual void setThemeName(std::string name); + protected: virtual std::string getQuickSystemSelectRightButton() override; virtual std::string getQuickSystemSelectLeftButton() override; diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index bfb26740ce..67e22bf3eb 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -1,13 +1,24 @@ #include "views/gamelist/DetailedGameListView.h" #include "animations/LambdaAnimation.h" + +#ifdef _RPI_ +#include "components/VideoPlayerComponent.h" +#endif +#include "components/VideoVlcComponent.h" +#include "utils/FileSystemUtil.h" #include "views/ViewController.h" +#ifdef _RPI_ +#include "Settings.h" +#endif DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : BasicGameListView(window, root), mDescContainer(window), mDescription(window), mImage(window), + mVideo(nullptr), + mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -17,6 +28,8 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : { //mHeaderImage.setPosition(mSize.x() * 0.25f, 0); + mVideoVisible = false; + const float padding = 0.01f; mList.setPosition(mSize.x() * (0.50f + padding), mList.getPosition().y()); @@ -24,12 +37,29 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mList.setAlignment(TextListComponent::ALIGN_LEFT); mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); + // Create the correct type of video window +#ifdef _RPI_ + if (Settings::getInstance()->getBool("VideoOmxPlayer")) + mVideo = new VideoPlayerComponent(window, ""); + else +#endif + mVideo = new VideoVlcComponent(window, getTitlePath()); + // 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); mImage.setDefaultZIndex(30); - addChild(&mImage); + addChild(&mImage); + + // video + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); + mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); + mVideo->setStartDelay(2000); + mVideo->setDefaultZIndex(30); + + //addChild(mVideo); -> Add only if present in theme later // metadata labels + values mLblRating.setText(_T("Rating") + ": "); @@ -81,6 +111,11 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : updateInfoPanel(); } +DetailedGameListView::~DetailedGameListView() +{ + delete mVideo; +} + void DetailedGameListView::onThemeChanged(const std::shared_ptr& theme) { BasicGameListView::onThemeChanged(theme); @@ -89,6 +124,18 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); mName.applyTheme(theme, getName(), "md_name", ALL); + if (theme->getElement(getName(), "md_video", "video")) + { + mVideoVisible = true; + mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION); + addChild(mVideo); + } + else + { + mVideoVisible = false; + removeChild(mVideo); + } + initMDLabels(); std::vector labels = getMDLabels(); assert(labels.size() == 8); @@ -202,17 +249,36 @@ void DetailedGameListView::updateInfoPanel() FileData* file = (mList.size() == 0 || mList.isScrolling()) ? NULL : mList.getSelected(); bool fadingOut; - if(file == NULL) + if (file == NULL) { + mVideo->setVideo(""); + mVideo->setImage(""); + //mImage.setImage(""); //mDescription.setText(""); fadingOut = true; }else{ + if (mVideoVisible) + { + if (!mVideo->setVideo(file->getVideoPath())) + mVideo->setDefaultVideo(); + } + if (file->getImagePath().empty()) - mImage.setImage(file->getThumbnailPath()); + { + if (mVideoVisible) + mVideo->setImage(file->getThumbnailPath(), false, mImage.getSize()); + + mImage.setImage(file->getThumbnailPath(), false, mImage.getSize()); + } else - mImage.setImage(file->getImagePath()); + { + if (mVideoVisible) + mVideo->setImage(file->getImagePath(), false, mImage.getSize()); + + mImage.setImage(file->getImagePath(), false, mImage.getSize()); + } mDescription.setText(getMetadata(file, "desc")); mDescContainer.reset(); @@ -236,6 +302,7 @@ void DetailedGameListView::updateInfoPanel() std::vector comps = getMDValues(); comps.push_back(&mImage); + comps.push_back(mVideo); comps.push_back(&mDescription); comps.push_back(&mName); std::vector labels = getMDLabels(); @@ -296,3 +363,16 @@ std::vector DetailedGameListView::getMDValues() ret.push_back(&mPlayCount); return ret; } + +void DetailedGameListView::update(int deltaTime) +{ + BasicGameListView::update(deltaTime); + + mImage.setVisible(mVideo == NULL || !(mVideo->isPlaying() && !mVideo->isFading())); +} + +void DetailedGameListView::onShow() +{ + GuiComponent::onShow(); + updateInfoPanel(); +} diff --git a/es-app/src/views/gamelist/DetailedGameListView.h b/es-app/src/views/gamelist/DetailedGameListView.h index 3c070c955f..9f2f00a044 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.h +++ b/es-app/src/views/gamelist/DetailedGameListView.h @@ -7,17 +7,30 @@ #include "components/ScrollableContainer.h" #include "views/gamelist/BasicGameListView.h" +class VideoComponent; + class DetailedGameListView : public BasicGameListView { public: DetailedGameListView(Window* window, FileData* root); + ~DetailedGameListView(); virtual void onThemeChanged(const std::shared_ptr& theme) override; + virtual void onShow() override; + + virtual const char* getName() const override + { + if (!mCustomThemeName.empty()) + return mCustomThemeName.c_str(); - virtual const char* getName() const override { return "detailed"; } + return "detailed"; + } virtual void launch(FileData* game) override; +protected: + virtual void update(int deltaTime) override; + private: void updateInfoPanel(); @@ -38,12 +51,15 @@ class DetailedGameListView : public BasicGameListView DateTimeComponent mLastPlayed; TextComponent mPlayCount; TextComponent mName; + VideoComponent* mVideo; std::vector getMDLabels(); std::vector getMDValues(); ScrollableContainer mDescContainer; TextComponent mDescription; + + bool mVideoVisible; }; #endif // ES_APP_VIEWS_GAME_LIST_DETAILED_GAME_LIST_VIEW_H diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index a2dbc27428..4d3ac60834 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -22,8 +22,6 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : { const float padding = 0.01f; - mGridEx = false; - mGrid.setPosition(mSize.x() * 0.1f, mSize.y() * 0.1f); mGrid.setDefaultZIndex(20); mGrid.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); @@ -88,9 +86,9 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : updateInfoPanel(); } -void GridGameListView::setGridEx() +void GridGameListView::setThemeName(std::string name) { - mGridEx = true; + ISimpleGameListView::setThemeName(name); mGrid.setThemeName(getName()); } @@ -139,19 +137,19 @@ void GridGameListView::populateList(const std::vector& files) if ((*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) { if (systemName == "favorites") - mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it); + mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), (*it)->getVideoPath(), *it); else - mGrid.add(_T("\uF006 ") + (*it)->getName(), (*it)->getThumbnailPath(), *it); + mGrid.add(_T("\uF006 ") + (*it)->getName(), (*it)->getThumbnailPath(), (*it)->getVideoPath(), *it); } for (auto it = files.cbegin(); it != files.cend(); it++) if (!(*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) - mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it); + mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), (*it)->getVideoPath(), *it); } else { addPlaceholder(); - } + } } void GridGameListView::onThemeChanged(const std::shared_ptr& theme) @@ -286,9 +284,9 @@ void GridGameListView::updateInfoPanel() if (mImageVisible) { if (file->getImagePath().empty()) - mImage.setImage(file->getThumbnailPath()); + mImage.setImage(file->getThumbnailPath(), false, mImage.getSize()); else - mImage.setImage(file->getImagePath()); + mImage.setImage(file->getImagePath(), false, mImage.getSize()); } else mImage.setImage(""); @@ -343,7 +341,7 @@ void GridGameListView::addPlaceholder() { // empty grid - add a placeholder FileData* placeholder = new FileData(PLACEHOLDER, "", this->mRoot->getSystem()->getSystemEnvData(), this->mRoot->getSystem()); - mGrid.add(placeholder->getName(), "", placeholder); + mGrid.add(placeholder->getName(), "", "", placeholder); } void GridGameListView::launch(FileData* game) @@ -380,6 +378,19 @@ void GridGameListView::remove(FileData *game, bool deleteFile) onFileChanged(parent, FILE_REMOVED); // update the view, with game removed } +void GridGameListView::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); +} + + std::vector GridGameListView::getMDLabels() { std::vector ret; @@ -412,20 +423,26 @@ std::vector GridGameListView::getHelpPrompts() { std::vector prompts; - if(Settings::getInstance()->getBool("QuickSystemSelect")) + if (Settings::getInstance()->getBool("QuickSystemSelect")) prompts.push_back(HelpPrompt("lr", _T("SYSTEM"))); prompts.push_back(HelpPrompt("up/down/left/right", _T("CHOOSE"))); prompts.push_back(HelpPrompt("a", _T("LAUNCH"))); - prompts.push_back(HelpPrompt("b", _T("BACK"))); - if(!UIModeController::getInstance()->isUIModeKid()) + + if (!Settings::getInstance()->getBool("HideSystemView")) + prompts.push_back(HelpPrompt("b", _T("BACK"))); + + if (!UIModeController::getInstance()->isUIModeKid()) prompts.push_back(HelpPrompt("select", _T("OPTIONS"))); - if(mRoot->getSystem()->isGameSystem()) + + if (mRoot->getSystem()->isGameSystem()) prompts.push_back(HelpPrompt("x", _T("RANDOM"))); - if(mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) + if (mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) { std::string prompt = CollectionSystemManager::get()->getEditingCollection(); prompts.push_back(HelpPrompt("y", prompt)); } return prompts; } + + diff --git a/es-app/src/views/gamelist/GridGameListView.h b/es-app/src/views/gamelist/GridGameListView.h index 50cfc24ab7..d7fb787bc5 100644 --- a/es-app/src/views/gamelist/GridGameListView.h +++ b/es-app/src/views/gamelist/GridGameListView.h @@ -23,16 +23,17 @@ class GridGameListView : public ISimpleGameListView virtual const char* getName() const override { - if (mGridEx) - return "gridex"; + if (!mCustomThemeName.empty()) + return mCustomThemeName.c_str(); return "grid"; } virtual std::vector getHelpPrompts() override; virtual void launch(FileData* game) override; + virtual void onFileChanged(FileData* file, FileChangeType change); - void setGridEx(); + virtual void setThemeName(std::string name); protected: virtual std::string getQuickSystemSelectRightButton() override; @@ -63,7 +64,6 @@ class GridGameListView : public ISimpleGameListView ImageComponent mImage; bool mImageVisible; - bool mGridEx; std::vector getMDLabels(); std::vector getMDValues(); diff --git a/es-app/src/views/gamelist/IGameListView.cpp b/es-app/src/views/gamelist/IGameListView.cpp index 722eeabcdd..f2335dbb95 100644 --- a/es-app/src/views/gamelist/IGameListView.cpp +++ b/es-app/src/views/gamelist/IGameListView.cpp @@ -6,6 +6,11 @@ #include "Sound.h" #include "Window.h" +void IGameListView::setThemeName(std::string name) +{ + mCustomThemeName = name; +} + bool IGameListView::input(InputConfig* config, Input input) { // select to open GuiGamelistOptions diff --git a/es-app/src/views/gamelist/IGameListView.h b/es-app/src/views/gamelist/IGameListView.h index 988981593b..63acf6a45d 100644 --- a/es-app/src/views/gamelist/IGameListView.h +++ b/es-app/src/views/gamelist/IGameListView.h @@ -41,7 +41,12 @@ class IGameListView : public GuiComponent virtual HelpStyle getHelpStyle() override; void render(const Transform4x4f& parentTrans) override; + + virtual void setThemeName(std::string name); + protected: + std::string mCustomThemeName; + FileData* mRoot; std::shared_ptr mTheme; }; diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 90b1c032ef..7425391c17 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -78,7 +78,9 @@ void ISimpleGameListView::onFileChanged(FileData* /*file*/, FileChangeType /*cha bool ISimpleGameListView::input(InputConfig* config, Input input) { - if(input.value != 0) + bool hideSystemView = Settings::getInstance()->getBool("HideSystemView"); + + if (input.value != 0) { if (config->isMappedTo("a", input)) { @@ -112,25 +114,26 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } else if(config->isMappedTo("b", input)) { - if(mCursorStack.size()) + if (mCursorStack.size()) { populateList(mCursorStack.top()->getParent()->getChildren()); setCursor(mCursorStack.top()); mCursorStack.pop(); Sound::getFromTheme(getTheme(), getName(), "back")->play(); - }else{ + } + else if (!hideSystemView) + { onFocusLost(); SystemData* systemToView = getCursor()->getSystem(); if (systemToView->isCollection()) - { systemToView = CollectionSystemManager::get()->getSystemToView(systemToView); - } + ViewController::get()->goToSystemView(systemToView); } return true; } - else if(config->isMappedLike(getQuickSystemSelectRightButton(), input)) + else if (config->isMappedLike(getQuickSystemSelectRightButton(), input) || config->isMappedLike("rightshoulder", input)) { if(Settings::getInstance()->getBool("QuickSystemSelect")) { @@ -139,7 +142,7 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) return true; } } - else if(config->isMappedLike(getQuickSystemSelectLeftButton(), input)) + else if (config->isMappedLike(getQuickSystemSelectLeftButton(), input) || config->isMappedLike("leftshoulder", input)) { if(Settings::getInstance()->getBool("QuickSystemSelect")) { diff --git a/es-app/src/views/gamelist/ISimpleGameListView.h b/es-app/src/views/gamelist/ISimpleGameListView.h index f161eadd06..6980dc2443 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.h +++ b/es-app/src/views/gamelist/ISimpleGameListView.h @@ -36,6 +36,7 @@ class ISimpleGameListView : public IGameListView ImageComponent mHeaderImage; ImageComponent mBackground; + std::vector mThemeExtras; std::stack mCursorStack; diff --git a/es-app/src/views/gamelist/VideoGameListView.h b/es-app/src/views/gamelist/VideoGameListView.h index 8a8335866b..1ff642ca2d 100644 --- a/es-app/src/views/gamelist/VideoGameListView.h +++ b/es-app/src/views/gamelist/VideoGameListView.h @@ -19,7 +19,14 @@ class VideoGameListView : public BasicGameListView virtual void onThemeChanged(const std::shared_ptr& theme) override; - virtual const char* getName() const override { return "video"; } + virtual const char* getName() const override + { + if (!mCustomThemeName.empty()) + return mCustomThemeName.c_str(); + + return "video"; + } + virtual void launch(FileData* game) override; protected: diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index d05f77c3d4..fe3c63ff50 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -47,7 +47,7 @@ void GuiTextTool::ensureLocalisation() mItems.clear(); std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/"+ mCurrentLanguage +"/emulationstation2.po"); - if (Utils::FileSystem::exists(xmlpath)) + if (Utils::FileSystem::exists(xmlpath)) { LocalizationItem* currentItem = NULL; @@ -96,10 +96,10 @@ const std::string GuiTextTool::localize(const std::string text) { ensureLocalisation(); - for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) - { - if (text == (*it)->msgid) - return (*it)->msgstr; + for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) + { + if (text == (*it)->msgid) + return (*it)->msgstr; } return text; diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index f21e6f8f11..1bb4fe2f2f 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -35,6 +35,7 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d { width = FreeImage_GetWidth(fiBitmap); height = FreeImage_GetHeight(fiBitmap); + //loop through scanlines and add all pixel data to the return vector //this is necessary, because width*height*bpp might not be == pitch @@ -81,7 +82,40 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d return rawData; } -unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height) +#include "math/Vector2i.h" + +//public static Rectangle GetPictureRect(Size imageSize, Rectangle rcPhoto, bool outerZooming = false, bool sourceRect = false) + +Vector2i ImageIO::adjustPictureSize(Vector2i imageSize, Vector2i maxSize) +{ + int cxDIB = imageSize.x(); + int cyDIB = imageSize.y(); + int iMaxX = maxSize.x(); + int iMaxY = maxSize.y(); + + double xCoef = (double)iMaxX / (double)cxDIB; + double yCoef = (double)iMaxY / (double)cyDIB; + + cyDIB = (int)((double)cyDIB * std::fmax(xCoef, yCoef)); + cxDIB = (int)((double)cxDIB * std::fmax(xCoef, yCoef)); + + if (cxDIB > iMaxX) + { + cyDIB = (int)((double)cyDIB * (double)iMaxX / (double)cxDIB); + cxDIB = iMaxX; + } + + if (cyDIB > iMaxY) + { + cxDIB = (int)((double)cxDIB * (double)iMaxY / (double)cyDIB); + cyDIB = iMaxY; + } + + return Vector2i(cxDIB, cyDIB); +} + + +unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight) { width = 0; height = 0; @@ -112,7 +146,21 @@ unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const { width = FreeImage_GetWidth(fiBitmap); height = FreeImage_GetHeight(fiBitmap); + + if (maxWidth > 0 && maxHeight > 0 && (width > maxWidth || height > maxHeight)) + { + Vector2i sz = adjustPictureSize(Vector2i(width, height), Vector2i(maxWidth, maxHeight)); + if (sz.x() != width || sz.y() != height) + { + FIBITMAP* imageRescaled = FreeImage_Rescale(fiBitmap, sz.x(), sz.y(), FILTER_BOX); + FreeImage_Unload(fiBitmap); + fiBitmap = imageRescaled; + width = FreeImage_GetWidth(fiBitmap); + height = FreeImage_GetHeight(fiBitmap); + } + } + //loop through scanlines and add all pixel data to the return vector //this is necessary, because width*height*bpp might not be == pitch diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index acbe6cb526..89b27909ad 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -5,13 +5,16 @@ #include #include +#include "math/Vector2i.h" + class ImageIO { public: static std::vector loadFromMemoryRGBA32(const unsigned char * data, const size_t size, size_t & width, size_t & height); - static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height); - + static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth=0, int maxHeight=0); + static void flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height); + static Vector2i adjustPictureSize(Vector2i imageSize, Vector2i maxSize); }; #endif // ES_CORE_IMAGE_IO diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index a21d117a44..a42dd323c9 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -67,6 +67,10 @@ void Settings::setDefaults() mBoolMap["Windowed"] = false; mBoolMap["SplashScreen"] = true; mBoolMap["SplashScreenProgress"] = true; + mBoolMap["PreloadUI"] = true; + mBoolMap["StartupOnGameList"] = false; + mBoolMap["HideSystemView"] = false; + mStringMap["StartupSystem"] = ""; #if defined(_WIN32) @@ -92,12 +96,17 @@ void Settings::setDefaults() mIntMap["ScreenSaverTime"] = 5*60*1000; // 5 minutes mIntMap["ScraperResizeWidth"] = 400; mIntMap["ScraperResizeHeight"] = 0; + +#if defined(_WIN32) + mIntMap["MaxVRAM"] = 256; +#else #ifdef _RPI_ mIntMap["MaxVRAM"] = 80; #else mIntMap["MaxVRAM"] = 100; #endif - +#endif + #if defined(_WIN32) mBoolMap["HideWindow"] = false; #else @@ -111,6 +120,13 @@ void Settings::setDefaults() mStringMap["Scraper"] = "TheGamesDB"; mStringMap["GamelistViewStyle"] = "automatic"; + mStringMap["ThemeColorSet"] = ""; + mStringMap["ThemeIconSet"] = ""; + mStringMap["ThemeMenu"] = ""; + mStringMap["ThemeSystemView"] = ""; + mStringMap["ThemeGamelistView"] = ""; + mStringMap["ThemeRegionName"] = "eu"; + mBoolMap["ScreenSaverControls"] = true; mStringMap["ScreenSaverGameInfo"] = "never"; mBoolMap["StretchVideoOnScreenSaver"] = false; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index bcc1a72180..1bb2cf8bb7 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -6,10 +6,14 @@ #include "Log.h" #include "platform.h" #include "Settings.h" -#include #include -std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" }, { "menu" } }; +#ifdef _RPI_ +#include "components/VideoPlayerComponent.h" +#endif +#include "components/VideoVlcComponent.h" + +std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "video" }, { "menu" } }; std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; std::map> ThemeData::sElementMap { @@ -29,8 +33,15 @@ std::map> The { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, { "margin", NORMALIZED_PAIR }, + + { "padding", NORMALIZED_PAIR }, + + { "autoLayout", NORMALIZED_PAIR }, + { "autoLayoutSelectedZoom", FLOAT }, + { "gameImage", PATH }, { "folderImage", PATH }, + { "showVideoAtDelay", FLOAT }, { "scrollDirection", STRING } } }, { "gridtile", { { "size", NORMALIZED_PAIR }, @@ -40,7 +51,7 @@ std::map> The { "backgroundCornerSize", NORMALIZED_PAIR }, { "backgroundColor", COLOR }, { "backgroundCenterColor", COLOR }, - { "backgroundEdgeColor", COLOR }, + { "backgroundEdgeColor", COLOR }, { "imageSizeMode", STRING } } }, { "text", { { "pos", NORMALIZED_PAIR }, @@ -132,6 +143,7 @@ std::map> The { "rotation", FLOAT }, { "rotationOrigin", NORMALIZED_PAIR }, { "default", PATH }, + { "path", PATH }, { "delay", FLOAT }, { "zIndex", FLOAT }, { "showSnapshotNoVideo", BOOLEAN }, @@ -167,7 +179,30 @@ std::map> The { "menuBackground", { { "path", PATH }, { "fadePath", PATH }, - { "color", COLOR } } } + { "color", COLOR } } }, + + { "menuIcons",{ + { "iconSystem", PATH }, + { "iconUpdates", PATH }, + { "iconControllers", PATH }, + { "iconGames", PATH }, + { "iconUI", PATH }, + { "iconSound", PATH }, + { "iconNetwork", PATH }, + { "iconScraper", PATH }, + { "iconAdvanced", PATH }, + { "iconQuit", PATH } } }, + + { "menuSwitch",{ + { "pathOn", PATH }, + { "pathOff", PATH } } }, + + { "menuSlider",{ + { "path", PATH } } }, + + { "menuButton",{ + { "path", PATH }, + { "filledPath", PATH } } }, }; std::shared_ptr ThemeData::MenuTheme; @@ -229,6 +264,14 @@ std::string resolvePlaceholders(const char* in) ThemeData::ThemeData() { + mHasSubsets = false; + + mColorset = Settings::getInstance()->getString("ThemeColorSet"); + mIconset = Settings::getInstance()->getString("ThemeIconSet"); + mMenu = Settings::getInstance()->getString("ThemeMenu"); + mSystemview = Settings::getInstance()->getString("ThemeSystemView"); + mGamelistview = Settings::getInstance()->getString("ThemeGamelistView"); + mVersion = 0; } @@ -242,6 +285,7 @@ void ThemeData::loadFile(std::string system, std::map if(!Utils::FileSystem::exists(path)) throw error << "File does not exist!"; + mHasSubsets = false; mVersion = 0; mViews.clear(); @@ -273,6 +317,7 @@ void ThemeData::loadFile(std::string system, std::map parseVariables(root); parseIncludes(root); parseViews(root); + parseCustomViews(root); parseFeatures(root); MenuTheme = std::shared_ptr(new ThemeMenu(*this)); @@ -296,11 +341,39 @@ bool ThemeData::parseSubset(const pugi::xml_node& node) if (node.attribute("subset")) { + mHasSubsets = true; + parse = false; const std::string subsetAttr = node.attribute("subset").as_string(); const std::string nameAttr = node.attribute("name").as_string(); - if (subsetAttr == "iconset" || subsetAttr == "gamelistview") + if (subsetAttr == "colorset" && nameAttr == mColorset) + { + parse = true; + return parse; + } + if (subsetAttr == "iconset" && nameAttr == mIconset) + { + parse = true; + return parse; + } + if (subsetAttr == "menu" && nameAttr == mMenu) + { + parse = true; + return parse; + } + if (subsetAttr == "systemview" && nameAttr == mSystemview) + { + parse = true; + return parse; + } + if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) + { + parse = true; + return parse; + } + /* + if (subsetAttr == "menu" || subsetAttr == "iconset" || subsetAttr == "gamelistview") { if (nameAttr.rfind("1-") != std::string::npos) { @@ -320,7 +393,7 @@ bool ThemeData::parseSubset(const pugi::xml_node& node) { parse = true; return parse; - } + }*/ /* if (subsetAttr == "colorset" && nameAttr == mColorset) { @@ -387,6 +460,7 @@ void ThemeData::parseIncludes(const pugi::xml_node& root) parseVariables(theme); parseIncludes(theme); parseViews(theme); + parseCustomViews(theme); parseFeatures(theme); mPaths.pop_back(); @@ -408,6 +482,7 @@ void ThemeData::parseFeatures(const pugi::xml_node& root) if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) { parseViews(node); + parseCustomViews(node); } } } @@ -441,37 +516,138 @@ void ThemeData::parseViews(const pugi::xml_node& root) // parse views for (pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) { - if(!node.attribute("name")) - throw error << "View missing \"name\" attribute!"; + if (!node.attribute("name")) + continue; const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); + const std::string nameAttr = node.attribute("name").as_string(); size_t prevOff = nameAttr.find_first_not_of(delim, 0); size_t off = nameAttr.find_first_of(delim, prevOff); std::string viewKey; - while(off != std::string::npos || prevOff != std::string::npos) + + while (off != std::string::npos || prevOff != std::string::npos) { viewKey = nameAttr.substr(prevOff, off - prevOff); prevOff = nameAttr.find_first_not_of(delim, off); off = nameAttr.find_first_of(delim, prevOff); if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) - { + { ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; parseView(node, view); - - if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) - { - ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; - parseView(node, view); + + + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + { + if (it->second.isCustomView && it->second.baseType == viewKey) + { + ThemeView& customView = (ThemeView&)it->second; + parseView(node, customView); + } } + + + + } } } } +void ThemeData::parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& view, std::string baseClass) +{ + bool found = false; + + // Import original view properties + for (pugi::xml_node nodec = root.child("view"); nodec; nodec = nodec.next_sibling("view")) + { + if (!nodec.attribute("name")) + continue; + + const char* delim = " \t\r\n,"; + const std::string nameAttr = nodec.attribute("name").as_string(); + + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + std::string viewKey; + while (off != std::string::npos || prevOff != std::string::npos) + { + viewKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + if (viewKey == baseClass) + { + found = true; + parseView(nodec, view); + } + } + } + + if (found) + return; + + // base class is a customview ? + for (pugi::xml_node nodec = root.child("customView"); nodec; nodec = nodec.next_sibling("customView")) + { + const std::string nameAttr = nodec.attribute("name").as_string(); + + if (!nameAttr.empty() && nameAttr == baseClass) + { + std::string inherits = nodec.attribute("inherits").as_string(); + if (!inherits.empty() && inherits != baseClass) + { + view.baseType = inherits; + parseCustomViewBaseClass(root, view, inherits); + } + + parseView(nodec, view); + } + } +} -void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) +void ThemeData::parseCustomViews(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + // parse views + for (pugi::xml_node node = root.child("customView"); node; node = node.next_sibling("customView")) + { + if (!node.attribute("name")) + continue; + + std::string viewKey = node.attribute("name").as_string(); + + ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; + view.isCustomView = true; + + std::string inherits = node.attribute("inherits").as_string(); + if (!inherits.empty()) + { + view.baseType = inherits; + parseCustomViewBaseClass(root, view, inherits); + } + + parseView(node, view); + } +} + +std::vector ThemeData::getViewsOfTheme() +{ + std::vector ret; + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + { + if (it->first == "menu" || it->first == "system") + continue; + + ret.push_back(it->first); + } + + return ret; +} + +void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view, bool overwriteElements) { ThemeException error; error.setFiles(mPaths); @@ -506,7 +682,7 @@ void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) off = nameAttr.find_first_of(delim, prevOff); parseElement(node, elemTypeIt->second, - view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); + view.elements.insert(std::pair(elemKey, ThemeElement())).first->second, overwriteElements); if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) view.orderedKeys.push_back(elemKey); @@ -521,7 +697,7 @@ bool ThemeData::parseRegion(const pugi::xml_node& node) if (node.attribute("region")) { - std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); + std::string regionsetting = Settings::getInstance()->getString("ThemeRegionName"); parse = false; const char* delim = " \t\r\n,"; @@ -542,11 +718,9 @@ bool ThemeData::parseRegion(const pugi::xml_node& node) } return parse; - } - -void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element) +void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element, bool overwrite) { ThemeException error; error.setFiles(mPaths); @@ -564,6 +738,9 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapsecond) @@ -652,6 +829,24 @@ bool ThemeData::hasView(const std::string& view) return (viewIt != mViews.cend()); } +std::string ThemeData::getCustomViewBaseType(const std::string& view) +{ + auto viewIt = mViews.find(view); + if (viewIt != mViews.cend()) + return viewIt->second.baseType; + + return ""; +} + +bool ThemeData::isCustomView(const std::string& view) +{ + auto viewIt = mViews.find(view); + if (viewIt != mViews.cend()) + return viewIt->second.isCustomView; + + return false; +} + const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const { auto viewIt = mViews.find(view); @@ -715,6 +910,15 @@ std::vector ThemeData::makeExtras(const std::shared_ptrgetBool("VideoOmxPlayer")) + comp = new VideoPlayerComponent(window, ""); + else +#endif + comp = new VideoVlcComponent(window, ""); + } comp->setDefaultZIndex(10); comp->applyTheme(theme, view, *it, ThemeFlags::ALL); @@ -846,4 +1050,156 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData& theme) if (elem->has("selectorColor")) Text.selectorColor = elem->get("selectorColor"); } -} \ No newline at end of file + + elem = theme.getElement("menu", "menubutton", "menuButton"); + + if (elem) + { + if (elem->has("path")) + Icons.button = elem->get("path"); + if (elem->has("filledPath")) + Icons.button_filled = elem->get("filledPath"); + } + + elem = theme.getElement("menu", "menuswitch", "menuSwitch"); + + if (elem) + { + if (elem->has("pathOn") && ResourceManager::getInstance()->fileExists(elem->get("pathOn"))) + Icons.on = elem->get("pathOn"); + if (elem->has("pathOff") && ResourceManager::getInstance()->fileExists(elem->get("pathOff"))) + Icons.off = elem->get("pathOff"); + } + + elem = theme.getElement("menu", "menuslider", "menuSlider"); + + if (elem && elem->has("path") && ResourceManager::getInstance()->fileExists(elem->get("path"))) + Icons.knob = elem->get("path"); + + elem = theme.getElement("menu", "menuicons", "menuIcons"); + + if (elem) + { + if (elem->has("iconSystem") && ResourceManager::getInstance()->fileExists(elem->get("iconSystem"))) + MenuIcons.system = elem->get("iconSystem"); + + if (elem->has("iconUpdates") && ResourceManager::getInstance()->fileExists(elem->get("iconUpdates"))) + MenuIcons.updates = elem->get("iconUpdates"); + + if (elem->has("iconGames") && ResourceManager::getInstance()->fileExists(elem->get("iconGames"))) + MenuIcons.games = elem->get("iconGames"); + + if (elem->has("iconControllers") && ResourceManager::getInstance()->fileExists(elem->get("iconControllers"))) + MenuIcons.controllers = elem->get("iconControllers"); + + if (elem->has("iconUI") && ResourceManager::getInstance()->fileExists(elem->get("iconUI"))) + MenuIcons.ui = elem->get("iconUI"); + + if (elem->has("iconSound") && ResourceManager::getInstance()->fileExists(elem->get("iconSound"))) + MenuIcons.sound = elem->get("iconSound"); + + if (elem->has("iconScraper") && ResourceManager::getInstance()->fileExists(elem->get("iconScraper"))) + MenuIcons.scraper = elem->get("iconScraper"); + + if (elem->has("iconAdvanced") && ResourceManager::getInstance()->fileExists(elem->get("iconAdvanced"))) + MenuIcons.advanced = elem->get("iconAdvanced"); + + if (elem->has("iconQuit") && ResourceManager::getInstance()->fileExists(elem->get("iconQuit"))) + MenuIcons.quit = elem->get("iconQuit"); + } +} + +std::map ThemeData::sortThemeSubSets(const std::map& subsetmap, const std::string& subset) +{ + std::map sortedsets; + + for (const auto& it : subsetmap) + { + if (it.second == subset) + sortedsets[it.first] = it.first; + } + return sortedsets; +} + + +void ThemeData::crawlIncludes(const pugi::xml_node& root, std::map& sets, std::deque& dequepath) +{ + for (pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) + { + sets[node.attribute("name").as_string()] = node.attribute("subset").as_string(); + + const char* relPath = node.text().get(); + std::string path = Utils::FileSystem::resolveRelativePath(relPath, dequepath.back(), true); + + dequepath.push_back(path); + pugi::xml_document includeDoc; + /*pugi::xml_parse_result result =*/ includeDoc.load_file(path.c_str()); + pugi::xml_node root = includeDoc.child("theme"); + crawlIncludes(root, sets, dequepath); + findRegion(includeDoc, sets); + dequepath.pop_back(); + } +} + +void ThemeData::findRegion(const pugi::xml_document& doc, std::map& sets) +{ + pugi::xpath_node_set regionattr = doc.select_nodes("//@region"); + for (auto xpath_node : regionattr) + { + if (xpath_node.attribute() != nullptr) + sets[xpath_node.attribute().value()] = "region"; + } +} + +std::map ThemeData::getThemeSubSets(const std::string& theme) +{ + std::map sets; + + std::deque dequepath; + + static const size_t pathCount = 2; + std::string paths[pathCount] = + { + "/etc/emulationstation/themes", + Utils::FileSystem::getHomePath() + "/.emulationstation/themes" + }; + + for (size_t i = 0; i < pathCount; i++) + { + if (!Utils::FileSystem::isDirectory(paths[i])) + continue; + + auto dirs = Utils::FileSystem::getDirInfo(paths[i] + "/" + theme, false); + for (auto it = dirs.cbegin(); it != dirs.cend(); ++it) + { + if (!it->directory || it->hidden) + continue; + + std::string path = it->path + "/theme.xml"; + if (!Utils::FileSystem::exists(path)) + continue; + + dequepath.push_back(path); + pugi::xml_document doc; + doc.load_file(path.c_str()); + pugi::xml_node root = doc.child("theme"); + crawlIncludes(root, sets, dequepath); + findRegion(doc, sets); + dequepath.pop_back(); + } + + std::string path = paths[i] + "/" + theme + "/theme.xml"; + if (!Utils::FileSystem::exists(path)) + continue; + + dequepath.push_back(path); + pugi::xml_document doc; + doc.load_file(path.c_str()); + pugi::xml_node root = doc.child("theme"); + crawlIncludes(root, sets, dequepath); + findRegion(doc, sets); + dequepath.pop_back(); + } + + return sets; +} diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 27fa912433..16d59d4fd8 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -4,6 +4,7 @@ #include "math/Vector2f.h" #include "utils/FileSystemUtil.h" +#include #include #include @@ -93,6 +94,29 @@ struct MenuElement { std::shared_ptr font; }; +struct IconElement { + std::string button; + std::string button_filled; + std::string on; + std::string off; + std::string option_arrow; + std::string arrow; + std::string knob; +}; + +struct MenuIconElement { + + std::string system; + std::string updates; + std::string games; + std::string controllers; + std::string ui; + std::string sound; + std::string scraper; + std::string advanced; + std::string quit; +}; + class ThemeData { public: @@ -106,6 +130,8 @@ class ThemeData MenuElement Text{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, "", "", nullptr }; MenuElement TextSmall{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, "", "", nullptr }; MenuElement Footer{ 0xC6C6C6FF, 0xC6C6C6FF, 0xC6C6C6FF, 0xFFFFFFFF, "", "", nullptr }; + MenuIconElement MenuIcons { "","","","","","","","","" }; + IconElement Icons { ":/button.png", ":/button_filled.png", ":/on.svg", ":/off.svg", ":/option_arrow.svg", ":/arrow.svg", ":/slider_knob.svg" }; }; @@ -150,8 +176,12 @@ class ThemeData class ThemeView { public: + ThemeView() { isCustomView = false; } + std::map elements; std::vector orderedKeys; + std::string baseType; + bool isCustomView; }; @@ -174,6 +204,9 @@ class ThemeData bool hasView(const std::string& view); + bool isCustomView(const std::string& view); + std::string getCustomViewBaseType(const std::string& view); + // If expectedType is an empty string, will do no type checking. const ThemeElement* getElement(const std::string& view, const std::string& element, const std::string& expectedType) const; @@ -185,10 +218,19 @@ class ThemeData static std::string getThemeFromCurrentSet(const std::string& system); std::string getDefaultView() { return mDefaultView; }; + + std::vector getViewsOfTheme(); + + bool hasSubsets() { return mHasSubsets; } static const std::shared_ptr& getMenuTheme() { return MenuTheme; } + static std::map sortThemeSubSets(const std::map& subsetmap, const std::string& subset); + static std::map ThemeData::getThemeSubSets(const std::string& theme); private: + static void crawlIncludes(const pugi::xml_node& root, std::map& sets, std::deque& dequepath); + static void findRegion(const pugi::xml_document& doc, std::map& sets); + static std::map< std::string, std::map > sElementMap; static std::vector sSupportedFeatures; static std::vector sSupportedViews; @@ -202,16 +244,28 @@ class ThemeData void parseIncludes(const pugi::xml_node& themeRoot); void parseVariables(const pugi::xml_node& root); void parseViews(const pugi::xml_node& themeRoot); - void parseView(const pugi::xml_node& viewNode, ThemeView& view); - void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element); + void parseCustomViews(const pugi::xml_node& root); + void parseView(const pugi::xml_node& viewNode, ThemeView& view, bool overwriteElements = true); + void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element, bool overwrite=true); bool parseRegion(const pugi::xml_node& node); bool parseSubset(const pugi::xml_node& node); + void parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& view, std::string baseClass); + std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); std::map mViews; + + std::string mColorset; + std::string mIconset; + std::string mMenu; + std::string mSystemview; + std::string mGamelistview; + std::string mSystemThemeFolder; + bool mHasSubsets; + static std::shared_ptr MenuTheme; }; diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 87f5118e5b..c887c29028 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -71,16 +71,18 @@ GuiComponent* Window::peekGui() return mGuiStack.back(); } -bool Window::init() +bool Window::init(bool initRenderer) { - if(!Renderer::init()) + if (initRenderer && !Renderer::init()) { LOG(LogError) << "Renderer failed to initialize!"; return false; } InputManager::getInstance()->init(); - ResourceManager::getInstance()->reloadAll(); + + if (initRenderer) + ResourceManager::getInstance()->reloadAll(); //keep a reference to the default fonts, so they don't keep getting destroyed/recreated if(mDefaultFonts.empty()) @@ -100,7 +102,7 @@ bool Window::init() return true; } -void Window::deinit() +void Window::deinit(bool deinitRenderer) { // Hide all GUI elements on uninitialisation - this disable for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) @@ -108,8 +110,12 @@ void Window::deinit() (*i)->onHide(); } InputManager::getInstance()->deinit(); - ResourceManager::getInstance()->unloadAll(); - Renderer::deinit(); + + if (deinitRenderer) + { + ResourceManager::getInstance()->unloadAll(); + Renderer::deinit(); + } } void Window::textInput(const char* text) diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 0c64a959fa..42a2498106 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -57,8 +57,8 @@ class Window void update(int deltaTime); void render(); - bool init(); - void deinit(); + bool init(bool initRenderer); + void deinit(bool deinitRenderer); void normalizeNextUpdate(); diff --git a/es-core/src/components/ButtonComponent.cpp b/es-core/src/components/ButtonComponent.cpp index 08eeeb34bc..b457c6aff5 100644 --- a/es-core/src/components/ButtonComponent.cpp +++ b/es-core/src/components/ButtonComponent.cpp @@ -5,7 +5,7 @@ #include "Renderer.h" ButtonComponent::ButtonComponent(Window* window, const std::string& text, const std::string& helpText, const std::function& func) : GuiComponent(window), - mBox(window, ":/button.png"), + mBox(window, ThemeData::getMenuTheme()->Icons.button), mFont(Font::get(FONT_SIZE_MEDIUM)), mFocused(false), mEnabled(true), @@ -78,18 +78,18 @@ void ButtonComponent::setEnabled(bool enabled) void ButtonComponent::updateImage() { - /* if(!mEnabled || !mPressedFunc) { - mBox.setImagePath(":/button_filled.png"); + mBox.setImagePath(ThemeData::getMenuTheme()->Icons.button_filled); mBox.setCenterColor(0x770000FF); mBox.setEdgeColor(0x770000FF); return; } - */ + mBox.setCenterColor(mColor); mBox.setEdgeColor(mColor); - mBox.setImagePath(mFocused ? ":/button_filled.png" : ":/button.png"); + mBox.setImagePath(mFocused ? ThemeData::getMenuTheme()->Icons.button_filled : ThemeData::getMenuTheme()->Icons.button); + //mBox.setImagePath(mFocused ? ":/button_filled.png" : ":/button.png"); } void ButtonComponent::render(const Transform4x4f& parentTrans) diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index ae85435063..de13836dac 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -8,8 +8,20 @@ #include "animations/LambdaAnimation.h" -GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window), mLabel(window) -{ + +#ifdef _RPI_ +#include "components/VideoPlayerComponent.h" +#endif +#include "components/VideoVlcComponent.h" +#include "utils/FileSystemUtil.h" +#ifdef _RPI_ +#include "Settings.h" +#endif + +#define VIDEODELAY 100 + +GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window), mLabel(window), mVideo(nullptr), mVideoPlaying(false) +{ mSelectedZoomPercent = 1.0f; mAnimPosition = Vector3f(0, 0); @@ -31,50 +43,81 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mSelectedProperties.mBackgroundCenterColor = 0xFFFFFFFF; mSelectedProperties.mBackgroundEdgeColor = 0xFFFFFFFF; - mLabelSize = Vector2f(1.0, 0.30); + mDefaultProperties.mLabelSize = Vector2f(1.0, 0.30); + mDefaultProperties.mLabelColor = 0xFFFFFFFF; + mDefaultProperties.mLabelBackColor = 0; + + mSelectedProperties.mLabelSize = Vector2f(1.0, 0.30); + mSelectedProperties.mLabelColor = 0xFFFFFFFF; + mSelectedProperties.mLabelBackColor = 0; mImage = std::make_shared(mWindow); - mImage->setOrigin(0.5f, 0.5f); - - //mBackground.setOrigin(0.5f, 0.5f); - - //mLabel.setOrigin(0.5f, 0.5f); - //mLabel.setOrigin(0.5f, 0.0f); - //mLabel.setSize(1.0f, 1.0f); - addChild(&mBackground); addChild(&(*mImage)); addChild(&mLabel); - + + // Create the correct type of video window +#ifdef _RPI_ + if (Settings::getInstance()->getBool("VideoOmxPlayer")) + mVideo = new VideoPlayerComponent(window, ""); + else +#endif + mVideo = new VideoVlcComponent(window, getTitlePath()); + + // video + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setStartDelay(VIDEODELAY); + mVideo->setDefaultZIndex(30); + addChild(mVideo); + setSelected(false); setVisible(true); } +void GridTileComponent::forceSize(Vector2f size, float selectedZoom) +{ + mDefaultProperties.mSize = size; + mSelectedProperties.mSize = size * selectedZoom; +} + +GridTileComponent::~GridTileComponent() +{ + if (mVideo != nullptr) + delete mVideo; + + mVideo = nullptr; +} + void GridTileComponent::resize() { const GridTileProperties& currentProperties = getCurrentProperties(); - setSize(currentProperties.mSize); + Vector2f size = currentProperties.mSize; + setSize(size); - float height = currentProperties.mSize.y() * mLabelSize.y(); + float height = (int) (size.y() * currentProperties.mLabelSize.y()); + float labelHeight = height; + + mLabel.setColor(currentProperties.mLabelColor); + mLabel.setBackgroundColor(currentProperties.mLabelBackColor); if (mLabelMerged) { mLabel.setPosition(currentProperties.mPadding.x(), mSize.y() - height - currentProperties.mPadding.y()); - mLabel.setSize(currentProperties.mSize.x() - 2 * currentProperties.mPadding.x(), height); + mLabel.setSize(size.x() - 2 * currentProperties.mPadding.x(), height); } else - { - mLabel.setPosition(0, mSize.y() - height); - mLabel.setSize(currentProperties.mSize.x(), height); + { + mLabel.setPosition(0, mSize.y() - height); + mLabel.setSize(size.x(), height); } if (!mLabelVisible || mLabelMerged) height = 0; - if (mLabelSize.x() == 0) + if (currentProperties.mLabelSize.x() == 0) height = 0; float topPadding = currentProperties.mPadding.y(); @@ -83,10 +126,7 @@ void GridTileComponent::resize() if (mSelectedZoomPercent != 1.0f) { - paddingX = mDefaultProperties.mPadding.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.x() * mSelectedZoomPercent; - topPadding = mDefaultProperties.mPadding.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.y() * mSelectedZoomPercent; - - if (mAnimPosition.x() != 0 && mAnimPosition.y() != 0) + if (mAnimPosition.x() != 0 && mAnimPosition.y() != 0 && mSelected) { int x = mAnimPosition.x() - mPosition.x(); int y = mAnimPosition.y() - mPosition.y(); @@ -102,31 +142,80 @@ void GridTileComponent::resize() else mBackground.setPosition(0, 0); + float imageWidth = size.x() - paddingX * 2.0; + float imageHeight = size.y() - topPadding - bottomPadding; - if (mImage != NULL) + if (mImage != nullptr) { mImage->setOrigin(0.5f, 0.5f); - mImage->setPosition(currentProperties.mSize.x() / 2.0f, (currentProperties.mSize.y() - height) / 2.0f); + mImage->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); if (currentProperties.mImageSizeMode == "minSize") - mImage->setMinSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + mImage->setMinSize(imageWidth, imageHeight); else if (currentProperties.mImageSizeMode == "size") - mImage->setSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + mImage->setSize(imageWidth, imageHeight); else - mImage->setMaxSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + mImage->setMaxSize(imageWidth, imageHeight); + + if (mLabelMerged) + { + mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, currentProperties.mPadding.y() + mImage->getSize().y() - labelHeight); + mLabel.setSize(mImage->getSize().x(), labelHeight); + } + else if (currentProperties.mPadding.x() == 0) + { + mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, mImage->getSize().y()); + mLabel.setSize(mImage->getSize().x(), labelHeight); + } } + if (mVideo != nullptr) + { + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); + + if (currentProperties.mImageSizeMode == "minSize") + { + auto vs = mVideo->getVideoSize(); + + double prop = vs == Vector2f(0,0) ? 640.0 / 480.0 : vs.x() / vs.y(); + double imgprop = imageWidth / imageHeight; + if (prop < imgprop) + { + double h = imageWidth / prop; // suppose video is 4:3 + + if (h < imageHeight) + h = imageHeight; + + mVideo->setSize(imageWidth, h); + } + else + { + double w = imageHeight / prop; // suppose video is 4:3 + + if (w < imageWidth) + w = imageWidth; + + mVideo->setSize(w, imageHeight); + } + } + else + if (currentProperties.mImageSizeMode == "size") + mVideo->setSize(imageWidth, size.y() - topPadding - bottomPadding ); + else + mVideo->setMaxSize(imageWidth, size.y() - topPadding - bottomPadding); + } if (!mLabelMerged && currentProperties.mImageSizeMode == "minSize") - mBackground.setSize(currentProperties.mSize.x(), currentProperties.mSize.y() - bottomPadding); + mBackground.setSize(size.x(), size.y() - bottomPadding + topPadding); else - mBackground.setSize(currentProperties.mSize); + mBackground.setSize(size); mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); } -void GridTileComponent::render(const Transform4x4f& parentTrans) +void GridTileComponent::renderBackground(const Transform4x4f& parentTrans) { if (!mVisible) return; @@ -137,24 +226,63 @@ void GridTileComponent::render(const Transform4x4f& parentTrans) if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) return; - //Renderer::setMatrix(trans); - //Renderer::drawRect(0.f, 0.f, mSize.x(), mSize.y(), 0xFF0000FF); - if (mBackground.getCornerSize().x() == 0) { - Renderer::setMatrix(trans); - Renderer::drawRect(mBackground.getPosition().x(), mBackground.getPosition().y(), mBackground.getSize().x(), mBackground.getSize().y(), mBackground.getCenterColor()); + Renderer::drawRect(mBackground.getPosition().x(), mBackground.getPosition().y(), mBackground.getSize().x() - 1, mBackground.getSize().y() - 1, mBackground.getCenterColor()); Renderer::setMatrix(parentTrans); } else mBackground.render(trans); +} + +void GridTileComponent::renderContent(const Transform4x4f& parentTrans) +{ + if (!mVisible) + return; + + Transform4x4f trans = getTransform() * parentTrans; + + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + + float padding = getCurrentProperties().mPadding.x(); + float topPadding = getCurrentProperties().mPadding.y(); + float bottomPadding = topPadding; + + if (mLabelVisible && !mLabelMerged) + bottomPadding = std::max((int)topPadding, (int)(mSize.y() * getCurrentProperties().mLabelSize.y())); + + Vector2i pos((int)Math::round(trans.translation()[0] + padding), (int)Math::round(trans.translation()[1] + topPadding)); + Vector2i size((int)Math::round(mSize.x() - 2 * padding), (int)Math::round(mSize.y() - topPadding - bottomPadding)); + + if (getCurrentProperties().mImageSizeMode == "minSize") + Renderer::pushClipRect(pos, size); if (mImage != NULL) mImage->render(trans); - - if (mLabelVisible) + + if (mSelected && !mVideoPath.empty() && mVideo != nullptr) + mVideo->render(trans); + + if (!mLabelMerged && getCurrentProperties().mImageSizeMode == "minSize") + Renderer::popClipRect(); + + if (mLabelVisible && getCurrentProperties().mLabelSize.y()>0) mLabel.render(trans); + + if (mLabelMerged && getCurrentProperties().mImageSizeMode == "minSize") + Renderer::popClipRect(); +} + +void GridTileComponent::render(const Transform4x4f& parentTrans) +{ + if (!mVisible) + return; + + renderBackground(parentTrans); + renderContent(parentTrans); } // Update all the tile properties to the new status (selected or default) @@ -255,17 +383,43 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons elem->get("imageSizeMode") : mDefaultProperties.mImageSizeMode; - elem = theme->getElement(view, element, "text"); + elem = theme->getElement(view, "gridtile", "text"); if (elem != NULL) { if (elem && elem->has("size")) { - mLabelSize = elem->get("size"); - mLabelMerged = mLabelSize.x() == 0; + mDefaultProperties.mLabelSize = elem->get("size"); + mSelectedProperties.mLabelSize = mDefaultProperties.mLabelSize; + mLabelMerged = mDefaultProperties.mLabelSize.x() == 0; + } + + if (elem && elem->has("color")) + { + mDefaultProperties.mLabelColor = elem->get("color"); + mSelectedProperties.mLabelColor = mDefaultProperties.mLabelColor; + } + + if (elem && elem->has("backgroundColor")) + { + mDefaultProperties.mLabelBackColor = elem->get("backgroundColor"); + mSelectedProperties.mLabelBackColor = mDefaultProperties.mLabelBackColor; } mLabelVisible = true; mLabel.applyTheme(theme, view, element, properties); + + elem = theme->getElement(view, "gridtile_selected", "text"); + if (elem != NULL) + { + if (elem && elem->has("size")) + mSelectedProperties.mLabelSize = elem->get("size"); + + if (elem && elem->has("color")) + mSelectedProperties.mLabelColor = elem->get("color"); + + if (elem && elem->has("backgroundColor")) + mSelectedProperties.mLabelBackColor = elem->get("backgroundColor"); + } } else mLabelVisible = false; @@ -290,18 +444,49 @@ bool GridTileComponent::isSelected() const return mSelected; } -void GridTileComponent::setImage(const std::string& path, std::string name) +void GridTileComponent::setImage(const std::string& path) { - if (mCurrentPath != path) - { - mCurrentPath = path; - mImage->setImage(path); - } - + if (mCurrentPath == path) + return; + + mCurrentPath = path; + mImage->setImage(path, false, mSize); + resize(); +} + +void GridTileComponent::reset() +{ + setLabel(""); + setVideo(""); + setImage(""); +} + +void GridTileComponent::setLabel(std::string name) +{ + if (mLabel.getText() == name) + return; + mLabel.setText(name); resize(); } +void GridTileComponent::setVideo(const std::string& path, float defaultDelay) +{ + if (mVideoPath == path) + return; + + mVideoPath = path; + + if (mVideo != nullptr) + { + if (defaultDelay >= 0.0) + mVideo->setStartDelay(defaultDelay); + + if (mVideoPath.empty()) + mVideo->setVideo(""); + } +} + void GridTileComponent::setImage(const std::shared_ptr& texture, std::string name) { mImage->setImage(texture); @@ -311,37 +496,86 @@ void GridTileComponent::setImage(const std::shared_ptr& texture resize(); } -void GridTileComponent::setSelected(bool selected, Vector3f* pPosition) +void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f* pPosition) { + if (mSelected == selected) + { + if (mSelected && mVideo != nullptr) + mVideo->setVideo(mVideoPath); + + return; + } + mSelected = selected; + + if (!mSelected && mVideo != nullptr) + mVideo->setVideo(""); - if (selected && pPosition != NULL) + if (selected) { - auto funeec = [this](float t) + if (pPosition == NULL || !allowAnimation) { - this->setSelectedZoom(t); - }; - } + cancelAnimation(3); - if (selected && (pPosition != NULL || mSelectedProperties.mPadding.x() != mDefaultProperties.mPadding.y())) - { - if (pPosition == NULL) + this->setSelectedZoom(1); mAnimPosition = Vector3f(0, 0, 0); + + if (mVideo != NULL) + mVideo->setVideo(mVideoPath); + } else - mAnimPosition = Vector3f(pPosition->x(), pPosition->y(), pPosition->z()); + { + if (pPosition == NULL) + mAnimPosition = Vector3f(0, 0, 0); + else + mAnimPosition = Vector3f(pPosition->x(), pPosition->y(), pPosition->z()); + + auto func = [this](float t) + { + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + this->setSelectedZoom(pct); + }; + + cancelAnimation(3); + setAnimation(new LambdaAnimation(func, 250), 0, [this] { + this->setSelectedZoom(1); + mAnimPosition = Vector3f(0, 0, 0); - auto func = [this](float t) + if (mVideo != NULL) + mVideo->setVideo(mVideoPath); + + }, false, 3); + } + } + else // if (!selected) + { + if (!allowAnimation) { - t -= 1; // cubic ease out - float pct = Math::lerp(0, 1, t*t*t + 1); + this->setSelectedZoom(0); - this->setSelectedZoom(pct); - }; + if (mVideo != NULL) + mVideo->setVideo(""); + } + else + { + auto func = [this](float t) + { + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + this->setSelectedZoom(1.0 - pct); + }; - setAnimation(new LambdaAnimation(func, 200), 0, [this] { - this->setSelectedZoom(1); - mAnimPosition = Vector3f(0, 0, 0); - }, false, 3); + cancelAnimation(3); + setAnimation(new LambdaAnimation(func, 250), 0, [this] { + this->setSelectedZoom(0); + + if (mVideo != NULL) + mVideo->setVideo(""); + + }, false, 3); + } } } @@ -356,7 +590,61 @@ void GridTileComponent::setVisible(bool visible) mVisible = visible; } -const GridTileProperties& GridTileComponent::getCurrentProperties() const +unsigned int mixColors(unsigned int first, unsigned int second, float percent) +{ + unsigned char alpha0 = (first >> 24) & 0xFF; + unsigned char blue0 = (first >> 16) & 0xFF; + unsigned char green0 = (first >> 8) & 0xFF; + unsigned char red0 = first & 0xFF; + + unsigned char alpha1 = (second >> 24) & 0xFF; + unsigned char blue1 = (second >> 16) & 0xFF; + unsigned char green1 = (second >> 8) & 0xFF; + unsigned char red1 = second & 0xFF; + + unsigned char alpha = (unsigned char)(alpha0 * (1.0 - percent) + alpha1 * percent); + unsigned char blue = (unsigned char)(blue0 * (1.0 - percent) + blue1 * percent); + unsigned char green = (unsigned char)(green0 * (1.0 - percent) + green1 * percent); + unsigned char red = (unsigned char)(red0 * (1.0 - percent) + red1 * percent); + + return (alpha << 24) | (blue << 16) | (green << 8) | red; +} + +const GridTileProperties& GridTileComponent::getCurrentProperties() { + if (mSelectedZoomPercent != 1.0f) + { + auto def = mSelected ? mSelectedProperties : mDefaultProperties; + + mMixedProperties = mSelected ? mSelectedProperties : mDefaultProperties; + + if (mDefaultProperties.mSize != mSelectedProperties.mSize) + { + float x = mDefaultProperties.mSize.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mSize.x() * mSelectedZoomPercent; + float y = mDefaultProperties.mSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mSize.y() * mSelectedZoomPercent; + mMixedProperties.mSize = Vector2f(x, y); + } + + if (mDefaultProperties.mPadding != mSelectedProperties.mPadding) + { + float x = mDefaultProperties.mPadding.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.x() * mSelectedZoomPercent; + float y = mDefaultProperties.mPadding.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.y() * mSelectedZoomPercent; + mMixedProperties.mPadding = Vector2f(x, y); + } + + if (mDefaultProperties.mImageColor != mSelectedProperties.mImageColor) + { + mMixedProperties.mImageColor = mixColors(mDefaultProperties.mImageColor, mSelectedProperties.mImageColor, mSelectedZoomPercent); + } + + if (mDefaultProperties.mLabelSize != mSelectedProperties.mLabelSize) + { + float y = mDefaultProperties.mLabelSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mLabelSize.y() * mSelectedZoomPercent; + mMixedProperties.mLabelSize = Vector2f(mDefaultProperties.mLabelSize.x(), y); + } + + return mMixedProperties; + } + return mSelected ? mSelectedProperties : mDefaultProperties; } \ No newline at end of file diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index 46fc6c4364..1502d7ca84 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -6,6 +6,8 @@ #include "ImageComponent.h" #include "TextComponent.h" + +class VideoComponent; struct GridTileProperties { Vector2f mSize; @@ -17,12 +19,17 @@ struct GridTileProperties unsigned int mBackgroundEdgeColor; std::string mImageSizeMode; + + Vector2f mLabelSize; + unsigned int mLabelColor; + unsigned int mLabelBackColor; }; class GridTileComponent : public GuiComponent { public: GridTileComponent(Window* window); + ~GridTileComponent(); void render(const Transform4x4f& parentTrans) override; void update(int deltaTime) override; @@ -35,18 +42,30 @@ class GridTileComponent : public GuiComponent Vector2f getSelectedTileSize() const; bool isSelected() const; - void setImage(const std::string& path, std::string name); + void reset(); + + void setLabel(std::string name); + void setVideo(const std::string& path, float defaultDelay = -1.0); + + void setImage(const std::string& path); void setImage(const std::shared_ptr& texture, std::string name); - void setSelected(bool selected, Vector3f* pPosition = NULL); + void setSelected(bool selected, bool allowAnimation = true, Vector3f* pPosition = NULL); void setVisible(bool visible); + void forceSize(Vector2f size, float selectedZoom = 1.0); + + void renderBackground(const Transform4x4f& parentTrans); + void renderContent(const Transform4x4f& parentTrans); + + bool shouldSplitRendering() { return isAnimationPlaying(3); }; + private: void resize(); - const GridTileProperties& getCurrentProperties() const; + const GridTileProperties& getCurrentProperties(); std::shared_ptr mImage; - TextComponent mLabel; - Vector2f mLabelSize; + + TextComponent mLabel; bool mLabelVisible; bool mLabelMerged; @@ -55,8 +74,10 @@ class GridTileComponent : public GuiComponent GridTileProperties mDefaultProperties; GridTileProperties mSelectedProperties; + GridTileProperties mMixedProperties; std::string mCurrentPath; + std::string mVideoPath; void setSelectedZoom(float percent); @@ -65,6 +86,10 @@ class GridTileComponent : public GuiComponent bool mVisible; Vector3f mAnimPosition; + + VideoComponent* mVideo; + bool mVideoPlaying; + }; #endif // ES_CORE_COMPONENTS_GRID_TILE_COMPONENT_H diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 3ec03372de..0df7e7e7dd 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -21,11 +21,10 @@ Vector2f ImageComponent::getSize() const ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : GuiComponent(window), mTargetIsMax(false), mTargetIsMin(false), mFlipX(false), mFlipY(false), mTargetSize(0, 0), mColorShift(0xFFFFFFFF), - mForceLoad(forceLoad), mDynamic(dynamic), mFadeOpacity(0), mFading(false), mRotateByTargetSize(false), + mForceLoad(forceLoad), mDynamic(dynamic), mFadeOpacity(0), mFading(false), mRotateByTargetSize(false), mVisible(true), mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f) { - updateColors(); - mSizeChanged = false; + updateColors(); } ImageComponent::~ImageComponent() @@ -122,11 +121,7 @@ void ImageComponent::resize() mSize[0] = Math::round(mSize.x()); mSize[1] = Math::round(mSize.y()); - if (ox != mSize.x() || oy != mSize.y()) - mSizeChanged = true; - - // mSize.y() should already be rounded - //mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); + mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); onSizeChanged(); } @@ -141,17 +136,17 @@ void ImageComponent::setDefaultImage(std::string path) mDefaultPath = path; } -void ImageComponent::setImage(std::string path, bool tile) +void ImageComponent::setImage(std::string path, bool tile, Vector2f maxSize) { if (path.empty() || !ResourceManager::getInstance()->fileExists(path)) { if (mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) mTexture.reset(); else - mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic); + mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic, maxSize); } else - mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic); + mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, maxSize); resize(); } @@ -173,6 +168,9 @@ void ImageComponent::setImage(const std::shared_ptr& texture) void ImageComponent::setResize(float width, float height) { + if (!mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) + return; + mTargetSize = Vector2f(width, height); mTargetIsMax = false; mTargetIsMin = false; @@ -181,6 +179,9 @@ void ImageComponent::setResize(float width, float height) void ImageComponent::setMaxSize(float width, float height) { + if (mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) + return; + mTargetSize = Vector2f(width, height); mTargetIsMax = true; mTargetIsMin = false; @@ -189,6 +190,9 @@ void ImageComponent::setMaxSize(float width, float height) void ImageComponent::setMinSize(float width, float height) { + if (mTargetIsMin && !mTargetIsMax && mTargetSize.x() == width && mTargetSize.y() == height) + return; + mTargetSize = Vector2f(width, height); mTargetIsMax = false; mTargetIsMin = true; @@ -330,6 +334,9 @@ void ImageComponent::updateColors() void ImageComponent::render(const Transform4x4f& parentTrans) { + if (!mVisible) + return; + Transform4x4f trans = parentTrans * getTransform(); Renderer::setMatrix(trans); @@ -339,12 +346,6 @@ void ImageComponent::render(const Transform4x4f& parentTrans) if (mTexture && mOpacity > 0) { - if (mSizeChanged) - { - mSizeChanged = false; - mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); - } - if (Settings::getInstance()->getBool("DebugImage")) { Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; @@ -476,7 +477,7 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s if(properties & PATH && elem->has("path")) { bool tile = (elem->has("tile") && elem->get("tile")); - setImage(elem->get("path"), tile); + setImage(elem->get("path"), tile, mTargetSize); } if(properties & COLOR && elem->has("color")) diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 3f5ce3138a..17ca7ac375 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -18,7 +18,7 @@ class ImageComponent : public GuiComponent void setDefaultImage(std::string path); //Loads the image at the given filepath. Will tile if tile is true (retrieves texture as tiling, creates vertices accordingly). - void setImage(std::string path, bool tile = false); + void setImage(std::string path, bool tile = false, Vector2f maxSize = Vector2f(0 ,0)); //Loads an image from memory. void setImage(const char* image, size_t length, bool tile = false); //Use an already existing texture. @@ -69,6 +69,13 @@ class ImageComponent : public GuiComponent Vector2f getSize() const override; + bool isVisible() { + return mVisible; + } + void setVisible(bool visible) { + mVisible = visible; + } + bool hasImage(); void render(const Transform4x4f& parentTrans) override; @@ -107,11 +114,10 @@ class ImageComponent : public GuiComponent bool mForceLoad; bool mDynamic; bool mRotateByTargetSize; + bool mVisible; Vector2f mTopLeftCrop; - Vector2f mBottomRightCrop; - - bool mSizeChanged; + Vector2f mBottomRightCrop; }; #endif // ES_CORE_COMPONENTS_IMAGE_COMPONENT_H diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 9dc51138dd..b28fa65bc8 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -7,15 +7,20 @@ #include "resources/TextureResource.h" #include "GridTileComponent.h" +#define EXTRAITEMS 2 + enum ScrollDirection { SCROLL_VERTICALLY, - SCROLL_HORIZONTALLY + SCROLL_HORIZONTALLY, + SCROLL_VERTICALLY_CENTER, + SCROLL_HORIZONTALLY_CENTER, }; struct ImageGridData { std::string texturePath; + std::string videoPath; }; template @@ -40,7 +45,7 @@ class ImageGridComponent : public IList ImageGridComponent(Window* window); - void add(const std::string& name, const std::string& imagePath, const T& obj); + void add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const T& obj); bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; @@ -52,17 +57,22 @@ class ImageGridComponent : public IList void setThemeName(std::string name) { mName = name; }; + virtual void topWindow(bool isTop); + virtual void onShow(); + virtual void onHide(); + protected: - virtual void onCursorChanged(const CursorState& state) override; + virtual void onCursorChanged(const CursorState& state) override; private: // TILES void buildTiles(); - void updateTiles(); - void updateTileAtPos(int tilePos, int imgPos); - int getStartPosition() const; + void updateTiles(bool ascending = true, bool allowAnimation = true); + void updateTileAtPos(int tilePos, int imgPos, bool allowAnimation = true); void calcGridDimension(); + bool isVertical() { return mScrollDirection == SCROLL_VERTICALLY || mScrollDirection == SCROLL_VERTICALLY_CENTER; }; + bool mEntriesDirty; int mLastCursor; std::string mDefaultGameTexture; @@ -70,6 +80,9 @@ class ImageGridComponent : public IList // TILES bool mLastRowPartial; + Vector2f mAutoLayout; + float mAutoLayoutZoom; + Vector2f mPadding; Vector2f mMargin; Vector2f mTileSize; Vector2i mGridDimension; @@ -79,6 +92,11 @@ class ImageGridComponent : public IList std::string mName; int mStartPosition; + bool mAllowVideo; + float mVideoDelay; + + float mCamera; + float mCameraDirection; // MISCELLANEOUS ScrollDirection mScrollDirection; @@ -90,6 +108,14 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList::ImageGridComponent(Window* window) : IList -void ImageGridComponent::add(const std::string& name, const std::string& imagePath, const T& obj) +void ImageGridComponent::add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const T& obj) { typename IList::Entry entry; entry.name = name; entry.object = obj; entry.data.texturePath = imagePath; + entry.data.videoPath = videoPath; static_cast*>(this)->add(entry); mEntriesDirty = true; @@ -121,19 +149,25 @@ bool ImageGridComponent::input(InputConfig* config, Input input) { if(input.value != 0) { + int idx = isVertical() ? 0 : 1; + Vector2i dir = Vector2i::Zero(); if(config->isMappedLike("up", input)) - dir[1 ^ mScrollDirection] = -1; + dir[1 ^ idx] = -1; else if(config->isMappedLike("down", input)) - dir[1 ^ mScrollDirection] = 1; + dir[1 ^ idx] = 1; else if(config->isMappedLike("left", input)) - dir[0 ^ mScrollDirection] = -1; + dir[0 ^ idx] = -1; else if(config->isMappedLike("right", input)) - dir[0 ^ mScrollDirection] = 1; + dir[0 ^ idx] = 1; if(dir != Vector2i::Zero()) { - listInput(dir.x() + dir.y() * mGridDimension.x()); + if (isVertical()) + listInput(dir.x() + dir.y() * mGridDimension.x()); + else + listInput(dir.x() + dir.y() * mGridDimension.y()); + return true; } }else{ @@ -149,18 +183,65 @@ bool ImageGridComponent::input(InputConfig* config, Input input) template void ImageGridComponent::update(int deltaTime) { + GuiComponent::update(deltaTime); listUpdate(deltaTime); for(auto it = mTiles.begin(); it != mTiles.end(); it++) (*it)->update(deltaTime); } +template +void ImageGridComponent::topWindow(bool isTop) +{ + GuiComponent::topWindow(isTop); + + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + std::shared_ptr tile = mTiles.at(ti); + tile->topWindow(isTop); + } +} + +template +void ImageGridComponent::onShow() +{ + GuiComponent::onShow(); + + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + std::shared_ptr tile = mTiles.at(ti); + tile->onShow(); + } +} + +template +void ImageGridComponent::onHide() +{ + GuiComponent::onHide(); + + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + std::shared_ptr tile = mTiles.at(ti); + tile->onHide(); + } +} + template void ImageGridComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = getTransform() * parentTrans; - if(mEntriesDirty) + float offsetX = isVertical() ? 0 : mCamera * mCameraDirection * (mTileSize.x() + mMargin.x()); + float offsetY = isVertical() ? mCamera * mCameraDirection * (mTileSize.y() + mMargin.y()) : 0; + + if (Settings::getInstance()->getBool("DebugImage")) + { + Renderer::setMatrix(trans); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0xFF000055); + Renderer::setMatrix(parentTrans); + } + + if (mEntriesDirty) { updateTiles(); mEntriesDirty = false; @@ -175,40 +256,53 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) Renderer::pushClipRect(pos, size); - // Render all the tiles but the selected one + if (mCamera != 0) + { + for (auto it = mTiles.begin(); it != mTiles.end(); it++) + (*it)->setPosition((*it)->getPosition().x() + offsetX, (*it)->getPosition().y() + offsetY); + } + + // Render the selected image background on bottom of the others if needed std::shared_ptr selectedTile = NULL; for(auto it = mTiles.begin(); it != mTiles.end(); it++) { std::shared_ptr tile = (*it); - - // If it's the selected image, keep it for later, otherwise render it now - if(tile->isSelected()) + if (tile->isSelected()) + { selectedTile = tile; - // else - // tile->render(trans); - } - + if (tile->shouldSplitRendering()) + tile->renderBackground(trans); - - // Render the selected image on top of the others - if (selectedTile != NULL) - selectedTile->render(trans); + break; + } + } for (auto it = mTiles.begin(); it != mTiles.end(); it++) { std::shared_ptr tile = (*it); - - // If it's the selected image, keep it for later, otherwise render it now - if (tile->isSelected()) - ; // selectedTile = tile; - else + if (!tile->isSelected()) tile->render(trans); } + // Render the selected image content on top of the others + if (selectedTile != NULL) + { + if (selectedTile->shouldSplitRendering()) + selectedTile->renderContent(trans); + else + selectedTile->render(trans); + } + Renderer::popClipRect(); listRenderTitleOverlay(trans); + if (mCamera != 0) + { + for (auto it = mTiles.begin(); it != mTiles.end(); it++) + (*it)->setPosition((*it)->getPosition().x() - offsetX, (*it)->getPosition().y() - offsetY); + } + GuiComponent::renderChildren(trans); } @@ -229,8 +323,35 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("margin")) mMargin = elem->get("margin") * screen; + if (elem->has("padding")) + mPadding = elem->get("padding") * screen; + + if (elem->has("autoLayout")) + mAutoLayout = elem->get("autoLayout"); + + if (elem->has("autoLayoutSelectedZoom")) + mAutoLayoutZoom = elem->get("autoLayoutSelectedZoom"); + if (elem->has("scrollDirection")) - mScrollDirection = (ScrollDirection)(elem->get("scrollDirection") == "horizontal"); + { + auto direction = elem->get("scrollDirection"); + if (direction == "horizontal") + mScrollDirection = SCROLL_HORIZONTALLY; + else if (direction == "horizontalCenter") + mScrollDirection = SCROLL_HORIZONTALLY_CENTER; + else if (direction == "verticalCenter") + mScrollDirection = SCROLL_VERTICALLY_CENTER; + else + mScrollDirection = SCROLL_VERTICALLY; + } + + if (elem->has("showVideoAtDelay")) + { + mVideoDelay = elem->get("showVideoAtDelay"); + mAllowVideo = true; + } + else + mAllowVideo = false; if (elem->has("gameImage")) { @@ -301,7 +422,122 @@ void ImageGridComponent::onSizeChanged() template void ImageGridComponent::onCursorChanged(const CursorState& state) { - if (mLastCursor != mCursor) + if (mLastCursor == mCursor) + return; + + if (true) + { + bool centerSel = (mScrollDirection == SCROLL_HORIZONTALLY_CENTER || mScrollDirection == SCROLL_VERTICALLY_CENTER); + + bool direction = mCursor >= mLastCursor; + + int oldStart = mStartPosition; + + float dimScrollable = isVertical() ? mGridDimension.y() - 2 * EXTRAITEMS: mGridDimension.x() - 2 * EXTRAITEMS; + float dimOpposite = isVertical() ? mGridDimension.x() : mGridDimension.y(); + + int centralCol = (int)(dimScrollable - 0.5) / 2; + int maxCentralCol = (int)(dimScrollable) / 2; + + int oldCol = (mLastCursor / dimOpposite); + int col = (mCursor / dimOpposite); + + int lastCol = ((mEntries.size() - 1) / dimOpposite); + + int lastScroll = std::max(0, (int) (lastCol + 1 - dimScrollable)); + + float startPos = 0; + float endPos = 1; + + if (isAnimationPlaying(2)) + { + startPos = -(mCamera*2.0/3.0); + if (startPos < -1) + startPos = -1; + + cancelAnimation(2); + updateTiles(direction, false); + } + + std::shared_ptr oldTile = nullptr; + std::shared_ptr newTile = nullptr; + + int oldIdx = mLastCursor - mStartPosition + (dimOpposite * EXTRAITEMS); + if (oldIdx >= 0 && oldIdx < mTiles.size()) + oldTile = mTiles[oldIdx]; + + int newIdx = mCursor - mStartPosition + (dimOpposite * EXTRAITEMS); + if (newIdx >= 0 && newIdx < mTiles.size()) + newTile = mTiles[newIdx]; + + Vector3f oldPos = Vector3f(0, 0); + + if (oldTile != nullptr) + { + oldPos = oldTile->getPosition(); + oldTile->setSelected(false); + } + + if (newTile != nullptr) + newTile->setSelected(true, true, oldPos == Vector3f(0, 0) ? nullptr : &oldPos); + + int firstVisibleCol = mStartPosition / dimOpposite; + + if ((col < centralCol || (col == 0 && col == centralCol)) && !centerSel) + mStartPosition = 0; + else if ((col - centralCol) > lastScroll && !centerSel) + mStartPosition = lastScroll * dimOpposite; + else if (maxCentralCol != centralCol && col == firstVisibleCol + maxCentralCol || col == firstVisibleCol + centralCol) + { + if (col == firstVisibleCol + maxCentralCol) + mStartPosition = (col - maxCentralCol) * dimOpposite; + else + mStartPosition = (col - centralCol) * dimOpposite; + } + else + { + if (oldCol == firstVisibleCol + maxCentralCol) + mStartPosition = (col - maxCentralCol) * dimOpposite; + else + mStartPosition = (col - centralCol) * dimOpposite; + } + + mLastCursor = mCursor; + + mCameraDirection = direction ? -1.0 : 1.0; + mCamera = 0; + + if (oldStart == mStartPosition) + { + updateTiles(direction, true); + + if (mCursorChangedCallback) + mCursorChangedCallback(state); + + return; + } + + if (mCursorChangedCallback) + mCursorChangedCallback(state); + + auto func = [this, startPos, endPos](float t) + { + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + float x = startPos * (1.0 - pct) + endPos * pct; + mCamera = x; // Math::lerp(0.0f, 1.0f, t); + }; + + setAnimation(new LambdaAnimation(func, 250), 0, [this, direction] { + + mCamera = 0; + updateTiles(direction, false); + }, false, 2); + + return; + } + else { // 1 if scrolling down, -1 if scrolling up int scrollDirection = mCursor >= mLastCursor ? 1 : -1; @@ -309,15 +545,26 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) int partialRow = (int)mLastRowPartial; int maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); + if (!isVertical()) + maxPos = mStartPosition + ((mGridDimension.x()-2) * (mGridDimension.y() - partialRow)); + if (mCursor == 0) mStartPosition = 0; else if (mCursor < mStartPosition) mStartPosition = std::max(0, mStartPosition - mGridDimension.x()); else if (mCursor >= maxPos) { - mStartPosition = mStartPosition + mGridDimension.x(); - - maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); + if (!isVertical()) + { + mStartPosition = mStartPosition + mGridDimension.y(); + maxPos = mStartPosition + ((mGridDimension.x()-2) * (mGridDimension.y() - partialRow)); + } + else + { + mStartPosition = mStartPosition + mGridDimension.x(); + maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); + } + if (mCursor >= maxPos) { int cursorRow = mCursor / mGridDimension.x(); @@ -329,62 +576,23 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) } } } + } - updateTiles(); + updateTiles(mCursor >= mLastCursor); - if (mCursorChangedCallback) - mCursorChangedCallback(state); - } + if (mCursorChangedCallback) + mCursorChangedCallback(state); mLastCursor = mCursor; } -// Create and position tiles (mTiles) -template -void ImageGridComponent::buildTiles() -{ - mStartPosition = 0; - mTiles.clear(); - - calcGridDimension(); - - Vector2f tileDistance = mTileSize + mMargin; - Vector2f bufferSize = Vector2f(0, 0); // mScrollDirection == SCROLL_HORIZONTALLY ? tileDistance.x() * texBuffersForward[3] : 0, mScrollDirection == SCROLL_VERTICALLY ? tileDistance.y() * texBuffersForward[3] : 0); - Vector2f startPosition = mTileSize / 2 - bufferSize; - - int X, Y; - - // Layout tile size and position - for(int y = 0; y < mGridDimension.y(); y++) - { - for(int x = 0; x < mGridDimension.x(); x++) - { - // Create tiles - auto tile = std::make_shared(mWindow); - - // In Vertical mod, tiles are ordered from left to right, then from top to bottom - // In Horizontal mod, tiles are ordered from top to bottom, then from left to right - X = mScrollDirection == SCROLL_VERTICALLY ? x : y; - Y = mScrollDirection == SCROLL_VERTICALLY ? y : x; - - tile->setPosition(X * tileDistance.x() + startPosition.x(), Y * tileDistance.y() + startPosition.y()); - tile->setOrigin(0.5f, 0.5f); - tile->setImage("", ""); - - if (mTheme) - tile->applyTheme(mTheme, mName, "gridtile", ThemeFlags::ALL); - - mTiles.push_back(tile); - } - } -} template -void ImageGridComponent::updateTiles() +void ImageGridComponent::updateTiles(bool ascending, bool allowAnimation) { if (!mTiles.size()) return; - + // Stop updating the tiles at highest scroll speed if (mScrollTier == 3) { @@ -392,41 +600,95 @@ void ImageGridComponent::updateTiles() { std::shared_ptr tile = mTiles.at(ti); - tile->setSelected(false); - tile->setImage(mDefaultGameTexture, ""); + tile->setSelected(false, allowAnimation); + tile->setLabel(""); + tile->setImage(mDefaultGameTexture); tile->setVisible(false); } return; } - int i = 0; - int end = (int)mTiles.size(); - int img = getStartPosition(); + if (!ascending) + { + int i = (int)mTiles.size() - 1; + int end = -1; + int img = mStartPosition + (int)mTiles.size() - 1; + + if (isVertical()) + img -= EXTRAITEMS * mGridDimension.x(); + else + img -= EXTRAITEMS * mGridDimension.y(); - while (i != end) + while (i != end) + { + updateTileAtPos(i, img, allowAnimation); + i--; img--; + } + } + else { - updateTileAtPos(i, img); - i++; img++; + int i = 0; + int end = (int)mTiles.size(); + int img = mStartPosition; + + if (isVertical()) + img -= EXTRAITEMS * mGridDimension.x(); + else + img -= EXTRAITEMS * mGridDimension.y(); + + while (i != end) + { + updateTileAtPos(i, img, allowAnimation); + i++; img++; + } } mLastCursor = mCursor; } + template -void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos) +void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowAnimation) { std::shared_ptr tile = mTiles.at(tilePos); // If we have more tiles than we have to display images on screen, hide them if(imgPos < 0 || imgPos >= size() || tilePos < 0 || tilePos >= (int) mTiles.size()) // Same for tiles out of the buffer { - tile->setSelected(false); - tile->setImage("", ""); + tile->setSelected(false, allowAnimation); + tile->reset(); tile->setVisible(false); } else { - + tile->setVisible(true); + + std::string name = mEntries.at(imgPos).name; // .object->getName(); + tile->setLabel(name); + + std::string imagePath = mEntries.at(imgPos).data.texturePath; + + if (ResourceManager::getInstance()->fileExists(imagePath)) + tile->setImage(imagePath); + else if (mEntries.at(imgPos).object->getType() == 2) + tile->setImage(mDefaultFolderTexture); + else + tile->setImage(mDefaultGameTexture); + + if (mAllowVideo) + { + std::string videoPath = mEntries.at(imgPos).data.videoPath; + + if (ResourceManager::getInstance()->fileExists(videoPath)) + tile->setVideo(videoPath, mVideoDelay); + else if (mEntries.at(imgPos).object->getType() == 2) + tile->setVideo(""); + else + tile->setVideo(""); + } + else + tile->setVideo(""); + if (imgPos == mCursor && mCursor != mLastCursor) { int dif = mCursor - tilePos; @@ -434,57 +696,81 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos) if (idx < 0 || idx >= mTiles.size()) idx = 0; - - tile->setSelected(true, &mTiles.at(idx)->getPosition()); - } - else - tile->setSelected(imgPos == mCursor); - tile->setVisible(true); - - std::string name = mEntries.at(imgPos).name; // .object->getName(); - std::string imagePath = mEntries.at(imgPos).data.texturePath; - - if (ResourceManager::getInstance()->fileExists(imagePath)) - tile->setImage(imagePath, name); - else if (mEntries.at(imgPos).object->getType() == 2) - tile->setImage(mDefaultFolderTexture, name); + tile->setSelected(true, allowAnimation, &mTiles.at(idx)->getPosition()); + } else - tile->setImage(mDefaultGameTexture, name); + tile->setSelected(imgPos == mCursor, allowAnimation); } } -// Return the starting position (the number of the game which will be displayed on top left of the screen) + +// Create and position tiles (mTiles) template -int ImageGridComponent::getStartPosition() const +void ImageGridComponent::buildTiles() { - return mStartPosition; + mStartPosition = 0; + mTiles.clear(); - // The "partialRow" variable exist because we want to keep the same positioning behavior in both - // case, whenever we have an integer number of rows or not (the last partial row is ignored when - // calculating position and the cursor shouldn't end up in this row when close to the end) - int partialRow = (int)mLastRowPartial; + calcGridDimension(); - int cursorRow = mCursor / mGridDimension.x(); - int cursorCol = mCursor % mGridDimension.x(); + Vector2f tileDistance = mTileSize + mMargin; + Vector2f tileSize = mTileSize; - int start = (cursorRow - ((mGridDimension.y() - partialRow) / 2)) * mGridDimension.x(); - if (start + (mGridDimension.x() * (mGridDimension.y() - partialRow)) >= (int)mEntries.size()) + if (mAutoLayout.x() != 0 && mAutoLayout.y() != 0) { - // If we are at the end put the row as close as we can and no higher, using the following formula - // Where E is the nb of entries, X the grid x dim (nb of column), Y the grid y dim (nb of line) - // start = first tile of last row - nb column * (nb line - 1) - // = (E - 1) / X * X - X * (Y - 1) - // = X * ((E - 1) / X - Y + 1) - start = mGridDimension.x() * (((int)mEntries.size() - 1) / mGridDimension.x() - mGridDimension.y() + 1 + partialRow); + auto x = (mSize.x() - (mMargin.x() * (mAutoLayout.x() - 1)) - 2 * mPadding.x()) / (int) mAutoLayout.x(); + auto y = (mSize.y() - (mMargin.y() * (mAutoLayout.y() - 1)) - 2 * mPadding.y()) / (int) mAutoLayout.y(); + + tileSize = Vector2f(x, y); + mTileSize = tileSize; + tileDistance = tileSize + mMargin; } - if (start < 0) - return 0; + bool vert = isVertical(); + + Vector2f bufferSize = Vector2f(/*vert && mGridDimension.y() == 1 ? tileDistance.x() :*/ 0, 0); + Vector2f startPosition = tileSize / 2 - bufferSize; + + startPosition += mPadding; + + int X, Y; + + // Layout tile size and position + for (int y = 0; y < (vert ? mGridDimension.y() : mGridDimension.x()); y++) + { + for (int x = 0; x < (vert ? mGridDimension.x() : mGridDimension.y()); x++) + { + // Create tiles + auto tile = std::make_shared(mWindow); + + // In Vertical mod, tiles are ordered from left to right, then from top to bottom + // In Horizontal mod, tiles are ordered from top to bottom, then from left to right + X = vert ? x : y - EXTRAITEMS; + Y = vert ? y - EXTRAITEMS : x; + + //if (!isVertical()) + // X--; + + tile->setPosition(X * tileDistance.x() + startPosition.x(), Y * tileDistance.y() + startPosition.y()); + tile->setOrigin(0.5f, 0.5f); + tile->reset(); + + if (mTheme) + tile->applyTheme(mTheme, mName, "gridtile", ThemeFlags::ALL); - return start; + if (mAutoLayout.x() != 0 && mAutoLayout.y() != 0) + tile->forceSize(mTileSize, mAutoLayoutZoom); + + mTiles.push_back(tile); + } + } + + mLastCursor = -1; + onCursorChanged(CURSOR_STOPPED); } + // Calculate how much tiles of size mTileSize we can fit in a grid of size mSize using a margin of size mMargin template void ImageGridComponent::calcGridDimension() @@ -492,15 +778,11 @@ void ImageGridComponent::calcGridDimension() // GRID_SIZE = COLUMNS * TILE_SIZE + (COLUMNS - 1) * MARGIN // <=> COLUMNS = (GRID_SIZE + MARGIN) / (TILE_SIZE + MARGIN) Vector2f gridDimension = (mSize + mMargin) / (mTileSize + mMargin); + if (mAutoLayout.x() != 0 && mAutoLayout.y() != 0) + gridDimension = mAutoLayout; mLastRowPartial = Math::floorf(gridDimension.y()) != gridDimension.y(); - - // Ceil y dim so we can display partial last row - mGridDimension = Vector2i(gridDimension.x(), Math::ceilf(gridDimension.y())); - - // Invert dimensions for horizontally scrolling grid - if (mScrollDirection == SCROLL_HORIZONTALLY) - mGridDimension = Vector2i((float) (int)gridDimension.y(), mGridDimension.x()); + mGridDimension = Vector2i((int) gridDimension.x(), (int) gridDimension.y()); // Grid dimension validation if (mGridDimension.x() < 1) @@ -508,8 +790,11 @@ void ImageGridComponent::calcGridDimension() if (mGridDimension.y() < 1) LOG(LogError) << "Theme defined grid Y dimension below 1"; - // Add extra tiles to both side depending on max texture buffer -// mGridDimension.y() += texBuffersForward[3] * 2; + // Add extra tiles to both sides : Add EXTRAITEMS before, EXTRAITEMS after + if (isVertical()) + mGridDimension.y() += 2 * EXTRAITEMS; + else + mGridDimension.x() += 2 * EXTRAITEMS; }; diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index f891c04490..1413e2fbe2 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -145,7 +145,7 @@ std::shared_ptr makeArrow(Window* window) auto menuTheme = ThemeData::getMenuTheme(); auto bracket = std::make_shared(window); - bracket->setImage(":/arrow.svg"); + bracket->setImage(ThemeData::getMenuTheme()->Icons.arrow); // ":/arrow.svg"); bracket->setColorShift(menuTheme->Text.color); bracket->setResize(0, round(menuTheme->Text.font->getLetterHeight())); //bracket->setResize(0, Math::round(Font::get(FONT_SIZE_MEDIUM)->getLetterHeight())); diff --git a/es-core/src/components/NinePatchComponent.h b/es-core/src/components/NinePatchComponent.h index 8d0dbf697e..446e8bbd23 100644 --- a/es-core/src/components/NinePatchComponent.h +++ b/es-core/src/components/NinePatchComponent.h @@ -36,6 +36,7 @@ class NinePatchComponent : public GuiComponent void setCenterColor(unsigned int centerColor); // Apply a color shift to the "center" part of the ninepatch. unsigned int getCenterColor() { return mCenterColor; }; + unsigned int getEdgeColor() { return mEdgeColor; }; virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index e1dc87eb7a..b52720343b 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -1,391 +1,391 @@ -#pragma once -#ifndef ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H -#define ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H - -#include "GuiComponent.h" -#include "Log.h" -#include "Window.h" - -//Used to display a list of options. -//Can select one or multiple options. - -// if !multiSelect -// * <- curEntry -> - -// always -// * press a -> open full list - -#define CHECKED_PATH ":/checkbox_checked.svg" -#define UNCHECKED_PATH ":/checkbox_unchecked.svg" - -template -class OptionListComponent : public GuiComponent -{ -private: - struct OptionListData - { - std::string name; - T object; - bool selected; - }; - - class OptionListPopup : public GuiComponent - { - private: - MenuComponent mMenu; - OptionListComponent* mParent; - - public: - OptionListPopup(Window* window, OptionListComponent* parent, const std::string& title) : GuiComponent(window), - mMenu(window, title.c_str()), mParent(parent) - { - auto menuTheme = ThemeData::getMenuTheme(); - auto font = menuTheme->Text.font; - auto color = menuTheme->Text.color; - - ComponentListRow row; - - // for select all/none - std::vector checkboxes; - - for(auto it = mParent->mEntries.begin(); it != mParent->mEntries.end(); it++) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, color), true); - - OptionListData& e = *it; - - if(mParent->mMultiSelect) - { - // add checkbox - auto checkbox = std::make_shared(mWindow); - checkbox->setImage(it->selected ? CHECKED_PATH : UNCHECKED_PATH); - checkbox->setResize(0, font->getLetterHeight()); - row.addElement(checkbox, false); - - // input handler - // update checkbox state & selected value - row.makeAcceptInputHandler([this, &e, checkbox] - { - e.selected = !e.selected; - checkbox->setImage(e.selected ? CHECKED_PATH : UNCHECKED_PATH); - mParent->onSelectedChanged(); - }); - - // for select all/none - checkboxes.push_back(checkbox.get()); - }else{ - // input handler for non-multiselect - // update selected value and close - row.makeAcceptInputHandler([this, &e] - { - mParent->mEntries.at(mParent->getSelectedId()).selected = false; - e.selected = true; - mParent->onSelectedChanged(); - delete this; - }); - } - - // also set cursor to this row if we're not multi-select and this row is selected - mMenu.addRow(row, (!mParent->mMultiSelect && it->selected)); - } - - mMenu.addButton(_T("BACK"), _T("accept"), [this] { delete this; }); - - if(mParent->mMultiSelect) - { - mMenu.addButton(_T("SELECT ALL"), _T("SELECT ALL"), [this, checkboxes] { - for(unsigned int i = 0; i < mParent->mEntries.size(); i++) - { - mParent->mEntries.at(i).selected = true; - checkboxes.at(i)->setImage(CHECKED_PATH); - } - mParent->onSelectedChanged(); - }); - - mMenu.addButton(_T("SELECT NONE"), _T("SELECT NONE"), [this, checkboxes] { - for(unsigned int i = 0; i < mParent->mEntries.size(); i++) - { - mParent->mEntries.at(i).selected = false; - checkboxes.at(i)->setImage(UNCHECKED_PATH); - } - mParent->onSelectedChanged(); - }); - } - - mMenu.setPosition( - (Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, - (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); - //Renderer::getScreenHeight() * 0.15f); - addChild(&mMenu); - } - - bool input(InputConfig* config, Input input) override - { - if(config->isMappedTo("b", input) && input.value != 0) - { - delete this; - return true; - } - - return GuiComponent::input(config, input); - } - - std::vector getHelpPrompts() override - { - auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); - return prompts; - } - }; - -public: - OptionListComponent(Window* window, const std::string& name, bool multiSelect = false) : GuiComponent(window), mMultiSelect(multiSelect), mName(name), - mText(window), mLeftArrow(window), mRightArrow(window) - { - auto theme = ThemeData::getMenuTheme(); - - // auto font = Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT); - mText.setFont(theme->Text.font); // font - mText.setColor(theme->Text.color); // 0x777777FF - mText.setHorizontalAlignment(ALIGN_CENTER); - addChild(&mText); - - mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); - mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); - - if (mMultiSelect) - { - mRightArrow.setImage(":/arrow.svg"); - mRightArrow.setColorShift(theme->Text.color); - addChild(&mRightArrow); - } else { - mLeftArrow.setImage(":/option_arrow.svg"); - mLeftArrow.setColorShift(theme->Text.color); - mLeftArrow.setFlipX(true); - addChild(&mLeftArrow); - - mRightArrow.setImage(":/option_arrow.svg"); - mLeftArrow.setColorShift(theme->Text.color); - addChild(&mRightArrow); - } - - setSize(mLeftArrow.getSize().x() + mRightArrow.getSize().x(), theme->Text.font->getHeight()); - } - - - virtual void setColor(unsigned int color) - { - mText.setColor(color); - mLeftArrow.setColorShift(color); - mRightArrow.setColorShift(color); - } - - // handles positioning/resizing of text and arrows - void onSizeChanged() override - { - mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); - mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); - - if(mSize.x() < (mLeftArrow.getSize().x() + mRightArrow.getSize().x())) - LOG(LogWarning) << "OptionListComponent too narrow!"; - - mText.setSize(mSize.x() - mLeftArrow.getSize().x() - mRightArrow.getSize().x(), mText.getFont()->getHeight()); - - // position - mLeftArrow.setPosition(0, (mSize.y() - mLeftArrow.getSize().y()) / 2); - mText.setPosition(mLeftArrow.getPosition().x() + mLeftArrow.getSize().x(), (mSize.y() - mText.getSize().y()) / 2); - mRightArrow.setPosition(mText.getPosition().x() + mText.getSize().x(), (mSize.y() - mRightArrow.getSize().y()) / 2); - } - - bool input(InputConfig* config, Input input) override - { - if(input.value != 0) - { - if(config->isMappedTo("a", input)) - { - open(); - return true; - } - if(!mMultiSelect) - { - if(config->isMappedLike("left", input)) - { - // move selection to previous - unsigned int i = getSelectedId(); - int next = (int)i - 1; - if(next < 0) - next += (int)mEntries.size(); - - mEntries.at(i).selected = false; - mEntries.at(next).selected = true; - onSelectedChanged(); - return true; - - }else if(config->isMappedLike("right", input)) - { - if (mEntries.size() == 0) - return true; - - // move selection to next - unsigned int i = getSelectedId(); - int next = (i + 1) % mEntries.size(); - mEntries.at(i).selected = false; - mEntries.at(next).selected = true; - onSelectedChanged(); - return true; - - } - } - } - return GuiComponent::input(config, input); - } - - std::vector getSelectedObjects() - { - std::vector ret; - for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) - { - if(it->selected) - ret.push_back(it->object); - } - - return ret; - } - - T getSelected() - { - assert(mMultiSelect == false); - auto selected = getSelectedObjects(); - assert(selected.size() == 1); - return selected.at(0); - } - - void add(const std::string& name, const T& obj, bool selected) - { - OptionListData e; - e.name = name; - e.object = obj; - e.selected = selected; - - mEntries.push_back(e); - onSelectedChanged(); - } - - void selectAll() - { - for(unsigned int i = 0; i < mEntries.size(); i++) - { - mEntries.at(i).selected = true; - } - onSelectedChanged(); - } - - void selectNone() - { - for(unsigned int i = 0; i < mEntries.size(); i++) - { - mEntries.at(i).selected = false; - } - onSelectedChanged(); - } - - void selectFirstItem() - { - for (unsigned int i = 0; i < mEntries.size(); i++) - mEntries.at(i).selected = false; - - if (mEntries.size() > 0) - mEntries.at(0).selected = true; - - onSelectedChanged(); - } - +#pragma once +#ifndef ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H +#define ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H + +#include "GuiComponent.h" +#include "Log.h" +#include "Window.h" + +//Used to display a list of options. +//Can select one or multiple options. + +// if !multiSelect +// * <- curEntry -> + +// always +// * press a -> open full list + +#define CHECKED_PATH ":/checkbox_checked.svg" +#define UNCHECKED_PATH ":/checkbox_unchecked.svg" + +template +class OptionListComponent : public GuiComponent +{ +private: + struct OptionListData + { + std::string name; + T object; + bool selected; + }; + + class OptionListPopup : public GuiComponent + { + private: + MenuComponent mMenu; + OptionListComponent* mParent; + + public: + OptionListPopup(Window* window, OptionListComponent* parent, const std::string& title) : GuiComponent(window), + mMenu(window, title.c_str()), mParent(parent) + { + auto menuTheme = ThemeData::getMenuTheme(); + auto font = menuTheme->Text.font; + auto color = menuTheme->Text.color; + + ComponentListRow row; + + // for select all/none + std::vector checkboxes; + + for(auto it = mParent->mEntries.begin(); it != mParent->mEntries.end(); it++) + { + row.elements.clear(); + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, color), true); + + OptionListData& e = *it; + + if(mParent->mMultiSelect) + { + // add checkbox + auto checkbox = std::make_shared(mWindow); + checkbox->setImage(it->selected ? CHECKED_PATH : UNCHECKED_PATH); + checkbox->setResize(0, font->getLetterHeight()); + row.addElement(checkbox, false); + + // input handler + // update checkbox state & selected value + row.makeAcceptInputHandler([this, &e, checkbox] + { + e.selected = !e.selected; + checkbox->setImage(e.selected ? CHECKED_PATH : UNCHECKED_PATH); + mParent->onSelectedChanged(); + }); + + // for select all/none + checkboxes.push_back(checkbox.get()); + }else{ + // input handler for non-multiselect + // update selected value and close + row.makeAcceptInputHandler([this, &e] + { + mParent->mEntries.at(mParent->getSelectedId()).selected = false; + e.selected = true; + mParent->onSelectedChanged(); + delete this; + }); + } + + // also set cursor to this row if we're not multi-select and this row is selected + mMenu.addRow(row, (!mParent->mMultiSelect && it->selected)); + } + + mMenu.addButton(_T("BACK"), _T("accept"), [this] { delete this; }); + + if(mParent->mMultiSelect) + { + mMenu.addButton(_T("SELECT ALL"), _T("SELECT ALL"), [this, checkboxes] { + for(unsigned int i = 0; i < mParent->mEntries.size(); i++) + { + mParent->mEntries.at(i).selected = true; + checkboxes.at(i)->setImage(CHECKED_PATH); + } + mParent->onSelectedChanged(); + }); + + mMenu.addButton(_T("SELECT NONE"), _T("SELECT NONE"), [this, checkboxes] { + for(unsigned int i = 0; i < mParent->mEntries.size(); i++) + { + mParent->mEntries.at(i).selected = false; + checkboxes.at(i)->setImage(UNCHECKED_PATH); + } + mParent->onSelectedChanged(); + }); + } + + mMenu.setPosition( + (Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, + (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); + //Renderer::getScreenHeight() * 0.15f); + addChild(&mMenu); + } + + bool input(InputConfig* config, Input input) override + { + if(config->isMappedTo("b", input) && input.value != 0) + { + delete this; + return true; + } + + return GuiComponent::input(config, input); + } + + std::vector getHelpPrompts() override + { + auto prompts = mMenu.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + return prompts; + } + }; + +public: + OptionListComponent(Window* window, const std::string& name, bool multiSelect = false) : GuiComponent(window), mMultiSelect(multiSelect), mName(name), + mText(window), mLeftArrow(window), mRightArrow(window) + { + auto theme = ThemeData::getMenuTheme(); + + // auto font = Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT); + mText.setFont(theme->Text.font); // font + mText.setColor(theme->Text.color); // 0x777777FF + mText.setHorizontalAlignment(ALIGN_CENTER); + addChild(&mText); + + mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); + mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); + + if (mMultiSelect) + { + mRightArrow.setImage(ThemeData::getMenuTheme()->Icons.arrow);// ":/arrow.svg"); + mRightArrow.setColorShift(theme->Text.color); + addChild(&mRightArrow); + } else { + mLeftArrow.setImage(ThemeData::getMenuTheme()->Icons.option_arrow); // ":/option_arrow.svg" + mLeftArrow.setColorShift(theme->Text.color); + mLeftArrow.setFlipX(true); + addChild(&mLeftArrow); + + mRightArrow.setImage(ThemeData::getMenuTheme()->Icons.option_arrow); // ":/option_arrow.svg"); + mLeftArrow.setColorShift(theme->Text.color); + addChild(&mRightArrow); + } + + setSize(mLeftArrow.getSize().x() + mRightArrow.getSize().x(), theme->Text.font->getHeight()); + } + + + virtual void setColor(unsigned int color) + { + mText.setColor(color); + mLeftArrow.setColorShift(color); + mRightArrow.setColorShift(color); + } + + // handles positioning/resizing of text and arrows + void onSizeChanged() override + { + mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); + mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); + + if(mSize.x() < (mLeftArrow.getSize().x() + mRightArrow.getSize().x())) + LOG(LogWarning) << "OptionListComponent too narrow!"; + + mText.setSize(mSize.x() - mLeftArrow.getSize().x() - mRightArrow.getSize().x(), mText.getFont()->getHeight()); + + // position + mLeftArrow.setPosition(0, (mSize.y() - mLeftArrow.getSize().y()) / 2); + mText.setPosition(mLeftArrow.getPosition().x() + mLeftArrow.getSize().x(), (mSize.y() - mText.getSize().y()) / 2); + mRightArrow.setPosition(mText.getPosition().x() + mText.getSize().x(), (mSize.y() - mRightArrow.getSize().y()) / 2); + } + + bool input(InputConfig* config, Input input) override + { + if(input.value != 0) + { + if(config->isMappedTo("a", input)) + { + open(); + return true; + } + if(!mMultiSelect) + { + if(config->isMappedLike("left", input)) + { + // move selection to previous + unsigned int i = getSelectedId(); + int next = (int)i - 1; + if(next < 0) + next += (int)mEntries.size(); + + mEntries.at(i).selected = false; + mEntries.at(next).selected = true; + onSelectedChanged(); + return true; + + }else if(config->isMappedLike("right", input)) + { + if (mEntries.size() == 0) + return true; + + // move selection to next + unsigned int i = getSelectedId(); + int next = (i + 1) % mEntries.size(); + mEntries.at(i).selected = false; + mEntries.at(next).selected = true; + onSelectedChanged(); + return true; + + } + } + } + return GuiComponent::input(config, input); + } + + std::vector getSelectedObjects() + { + std::vector ret; + for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) + { + if(it->selected) + ret.push_back(it->object); + } + + return ret; + } + + T getSelected() + { + assert(mMultiSelect == false); + auto selected = getSelectedObjects(); + assert(selected.size() == 1); + return selected.at(0); + } + + void add(const std::string& name, const T& obj, bool selected) + { + OptionListData e; + e.name = name; + e.object = obj; + e.selected = selected; + + mEntries.push_back(e); + onSelectedChanged(); + } + + void selectAll() + { + for(unsigned int i = 0; i < mEntries.size(); i++) + { + mEntries.at(i).selected = true; + } + onSelectedChanged(); + } + + void selectNone() + { + for(unsigned int i = 0; i < mEntries.size(); i++) + { + mEntries.at(i).selected = false; + } + onSelectedChanged(); + } + + void selectFirstItem() + { + for (unsigned int i = 0; i < mEntries.size(); i++) + mEntries.at(i).selected = false; + + if (mEntries.size() > 0) + mEntries.at(0).selected = true; + + onSelectedChanged(); + } + void clear() { mEntries.clear(); - } - + } + inline void invalidate() { onSelectedChanged(); - } - + } + void setSelectedChangedCallback(const std::function& callback) { mSelectedChangedCallback = callback; - } - - -private: - unsigned int getSelectedId() - { - assert(mMultiSelect == false); - for(unsigned int i = 0; i < mEntries.size(); i++) - { - if(mEntries.at(i).selected) - return i; - } - - LOG(LogWarning) << "OptionListComponent::getSelectedId() - no selected element found, defaulting to 0"; - return 0; - } - - void open() - { - mWindow->pushGui(new OptionListPopup(mWindow, this, mName)); - } - - - void onSelectedChanged() - { - if(mMultiSelect) - { - // display # selected - std::stringstream ss; - ss << getSelectedObjects().size() << " SELECTED"; - mText.setText(ss.str()); - mText.setSize(0, mText.getSize().y()); - setSize(mText.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); - if(mParent) // hack since theres no "on child size changed" callback atm... - mParent->onSizeChanged(); - }else{ - // display currently selected + l/r cursors - for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) - { - if (it->selected) - { - mText.setText(Utils::String::toUpper(it->name)); - mText.setSize(0, mText.getSize().y()); - setSize(mText.getSize().x() + mLeftArrow.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); - if (mParent) // hack since theres no "on child size changed" callback atm... - mParent->onSizeChanged(); - break; - } - } - } - + } + + +private: + unsigned int getSelectedId() + { + assert(mMultiSelect == false); + for(unsigned int i = 0; i < mEntries.size(); i++) + { + if(mEntries.at(i).selected) + return i; + } + + LOG(LogWarning) << "OptionListComponent::getSelectedId() - no selected element found, defaulting to 0"; + return 0; + } + + void open() + { + mWindow->pushGui(new OptionListPopup(mWindow, this, mName)); + } + + + void onSelectedChanged() + { + if(mMultiSelect) + { + // display # selected + std::stringstream ss; + ss << getSelectedObjects().size() << " SELECTED"; + mText.setText(ss.str()); + mText.setSize(0, mText.getSize().y()); + setSize(mText.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); + if(mParent) // hack since theres no "on child size changed" callback atm... + mParent->onSizeChanged(); + }else{ + // display currently selected + l/r cursors + for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) + { + if (it->selected) + { + mText.setText(Utils::String::toUpper(it->name)); + mText.setSize(0, mText.getSize().y()); + setSize(mText.getSize().x() + mLeftArrow.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); + if (mParent) // hack since theres no "on child size changed" callback atm... + mParent->onSizeChanged(); + break; + } + } + } + if (mSelectedChangedCallback) - mSelectedChangedCallback(mEntries.at(getSelectedId()).object); - } - - std::vector getHelpPrompts() override - { - std::vector prompts; - if(!mMultiSelect) - prompts.push_back(HelpPrompt("left/right", "MODIFIER")); - - prompts.push_back(HelpPrompt("a", "SELECTIONNER")); - return prompts; - } - - bool mMultiSelect; - - std::string mName; - TextComponent mText; - ImageComponent mLeftArrow; - ImageComponent mRightArrow; - - std::vector mEntries; - std::function mSelectedChangedCallback; -}; - -#endif // ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H + mSelectedChangedCallback(mEntries.at(getSelectedId()).object); + } + + std::vector getHelpPrompts() override + { + std::vector prompts; + if(!mMultiSelect) + prompts.push_back(HelpPrompt("left/right", "MODIFIER")); + + prompts.push_back(HelpPrompt("a", "SELECTIONNER")); + return prompts; + } + + bool mMultiSelect; + + std::string mName; + TextComponent mText; + ImageComponent mLeftArrow; + ImageComponent mRightArrow; + + std::vector mEntries; + std::function mSelectedChangedCallback; +}; + +#endif // ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H diff --git a/es-core/src/components/SliderComponent.cpp b/es-core/src/components/SliderComponent.cpp index a8e9657100..5139795cee 100644 --- a/es-core/src/components/SliderComponent.cpp +++ b/es-core/src/components/SliderComponent.cpp @@ -18,7 +18,7 @@ SliderComponent::SliderComponent(Window* window, float min, float max, float inc mColor = menuTheme->Text.color; mKnob.setOrigin(0.5f, 0.5f); - mKnob.setImage(":/slider_knob.svg"); + mKnob.setImage(ThemeData::getMenuTheme()->Icons.knob); // ":/slider_knob.svg"); mKnob.setColorShift(mColor); setSize(Renderer::getScreenWidth() * 0.15f, menuTheme->Text.font->getLetterHeight()); diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index 27ba63294e..62572d39a9 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -7,7 +7,7 @@ SwitchComponent::SwitchComponent(Window* window, bool state) : GuiComponent(wind { auto menuTheme = ThemeData::getMenuTheme(); - mImage.setImage(":/off.svg"); + mImage.setImage(ThemeData::getMenuTheme()->Icons.off); mImage.setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight()); mImage.setColorShift(menuTheme->Text.color); @@ -74,7 +74,9 @@ void SwitchComponent::setValue(const std::string& statestring) void SwitchComponent::onStateChanged() { - mImage.setImage(mState ? ":/on.svg" : ":/off.svg"); + auto theme = ThemeData::getMenuTheme(); + mImage.setImage(mState ? theme->Icons.on : theme->Icons.off); + //mImage.setImage(mState ? ":/on.svg" : ":/off.svg"); } std::vector SwitchComponent::getHelpPrompts() diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index 2800b41eaa..cd0949018f 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -41,6 +41,9 @@ void TextComponent::setFont(const std::shared_ptr& font) // Set the color of the font/text void TextComponent::setColor(unsigned int color) { + if (mColor == color) + return; + mColor = color; mColorOpacity = mColor & 0x000000FF; onColorChanged(); @@ -49,6 +52,9 @@ void TextComponent::setColor(unsigned int color) // Set the color of the background box void TextComponent::setBackgroundColor(unsigned int color) { + if (mBgColor == color) + return; + mBgColor = color; mBgColorOpacity = mBgColor & 0x000000FF; } diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index e472cec335..8f76273985 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -22,6 +22,8 @@ class TextComponent : public GuiComponent void setFont(const std::shared_ptr& font); void setUppercase(bool uppercase); void onSizeChanged() override; + + std::string getText() { return mText; } void setText(const std::string& text); virtual void setColor(unsigned int color); void setHorizontalAlignment(Alignment align); diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index fae35d643c..3586232b6a 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -8,7 +8,7 @@ #include "Window.h" #include -#define FADE_TIME_MS 200 +#define FADE_TIME_MS 900 std::string getTitlePath() { std::string titleFolder = getTitleFolder(); @@ -109,6 +109,7 @@ bool VideoComponent::setVideo(std::string path) // Store the path mVideoPath = fullPath; + mStartDelayed = false; // If the file exists then set the new video if (!fullPath.empty() && ResourceManager::getInstance()->fileExists(fullPath)) @@ -120,13 +121,13 @@ bool VideoComponent::setVideo(std::string path) return false; } -void VideoComponent::setImage(std::string path) +void VideoComponent::setImage(std::string path, bool tile, Vector2f maxSize) { // Check that the image has changed if (path == mStaticImagePath) return; - mStaticImage.setImage(path); + mStaticImage.setImage(path, tile, maxSize); mFadeIn = 0.0f; mStaticImagePath = path; } @@ -147,7 +148,7 @@ void VideoComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = parentTrans * getTransform(); GuiComponent::renderChildren(trans); - + Renderer::setMatrix(trans); // Handle the case where the video is delayed @@ -223,6 +224,9 @@ void VideoComponent::applyTheme(const std::shared_ptr& theme, const s setZIndex(elem->get("zIndex")); else setZIndex(getDefaultZIndex()); + + if (elem->has("path")) + mVideoPath = elem->get("path"); } std::vector VideoComponent::getHelpPrompts() @@ -232,79 +236,91 @@ std::vector VideoComponent::getHelpPrompts() return ret; } -void VideoComponent::handleStartDelay() + +void VideoComponent::handleLooping() { - // Only play if any delay has timed out - if (mStartDelayed) - { - if (mStartTime > SDL_GetTicks()) - { - // Timeout not yet completed - return; - } - // Completed - mStartDelayed = false; - // Clear the playing flag so startVideo works - mIsPlaying = false; - startVideo(); - } } -void VideoComponent::handleLooping() +void VideoComponent::handleStartDelay() { + // Only play if any delay has timed out + if (!mStartDelayed) + return; + + // Timeout not yet completed + if (mStartTime > SDL_GetTicks()) + return; + + // Completed + mStartDelayed = false; + // Clear the playing flag so startVideo works + mIsPlaying = false; + startVideo(); + + mFadeIn = 0.0f; + mIsPlaying = true; } void VideoComponent::startVideoWithDelay() { // If not playing then either start the video or initiate the delay - if (!mIsPlaying) + if (mIsPlaying || mStartDelayed) + return; + + // Set the video that we are going to be playing so we don't attempt to restart it + mPlayingVideoPath = mVideoPath; + + if (mConfig.startDelay == 0 || PowerSaver::getMode() == PowerSaver::INSTANT) { - // Set the video that we are going to be playing so we don't attempt to restart it - mPlayingVideoPath = mVideoPath; + // No delay. Just start the video + mStartDelayed = false; + mIsPlaying = false; + startVideo(); - if (mConfig.startDelay == 0 || PowerSaver::getMode() == PowerSaver::INSTANT) - { - // No delay. Just start the video - mStartDelayed = false; - startVideo(); - } - else - { - // Configure the start delay - mStartDelayed = true; - mFadeIn = 0.0f; - mStartTime = SDL_GetTicks() + mConfig.startDelay; - } + mFadeIn = 0.0f; mIsPlaying = true; } + else + { + // Configure the start delay + mStartDelayed = true; + mFadeIn = 0.0f; + mStartTime = SDL_GetTicks() + mConfig.startDelay; + } } void VideoComponent::update(int deltaTime) { manageState(); - // If the video start is delayed and there is less than the fade time then set the image fade - // accordingly - if (mStartDelayed) + if (mIsPlaying) { - Uint32 ticks = SDL_GetTicks(); - if (mStartTime > ticks) + // If the video start is delayed and there is less than the fade time then set the image fade + // accordingly + + if (mStartDelayed) { - Uint32 diff = mStartTime - ticks; - if (diff < FADE_TIME_MS) + Uint32 ticks = SDL_GetTicks(); + if (mStartTime > ticks) { - mFadeIn = (float)diff / (float)FADE_TIME_MS; - return; + Uint32 diff = mStartTime - ticks; + if (diff < FADE_TIME_MS) + { + mFadeIn = (float)diff / (float)FADE_TIME_MS; + return; + } } } + + // If the fade in is less than 1 then increment it + if (mFadeIn < 1.0f) + { + mFadeIn += deltaTime / (float)FADE_TIME_MS; + if (mFadeIn > 1.0f) + mFadeIn = 1.0f; + } } - // If the fade in is less than 1 then increment it - if (mFadeIn < 1.0f) - { - mFadeIn += deltaTime / (float)FADE_TIME_MS; - if (mFadeIn > 1.0f) - mFadeIn = 1.0f; - } + GuiComponent::update(deltaTime); } @@ -340,7 +356,7 @@ void VideoComponent::manageState() { startVideoWithDelay(); } - } + } } void VideoComponent::onShow() diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index 8d8207b1c7..0a306504ad 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -30,7 +30,7 @@ class VideoComponent : public GuiComponent // Loads the video at the given filepath bool setVideo(std::string path); // Loads a static image that is displayed if the video cannot be played - void setImage(std::string path); + void setImage(std::string path, bool tile = false, Vector2f maxSize = Vector2f(0, 0)); // Configures the component to show the default video void setDefaultVideo(); @@ -38,6 +38,8 @@ class VideoComponent : public GuiComponent // sets whether it's going to render in screensaver mode void setScreensaverMode(bool isScreensaver); + void setStartDelay(int delay) { mConfig.startDelay = delay; } + virtual void onShow() override; virtual void onHide() override; virtual void onScreenSaverActivate() override; @@ -70,6 +72,15 @@ class VideoComponent : public GuiComponent virtual void setMaxSize(float width, float height) = 0; inline void setMaxSize(const Vector2f& size) { setMaxSize(size.x(), size.y()); } + Vector2f getVideoSize() { return Vector2f(mVideoWidth, mVideoHeight); } + bool isPlaying() { + return mIsPlaying; + } + + bool isFading() { + return mFadeIn < 1.0f; + } + private: // Start the video Immediately virtual void startVideo() = 0; diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index ea712c0a60..54879c36f5 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -136,8 +136,6 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); - GuiComponent::renderChildren(trans); - Renderer::setMatrix(trans); if (mIsPlaying && mContext.valid) @@ -177,16 +175,19 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) vertices[3].tex[0] = 1.0f + tex_offs_x; vertices[3].tex[1] = -tex_offs_y; vertices[4].tex[0] = -tex_offs_x; vertices[4].tex[1] = 1.0f + tex_offs_y; vertices[5].tex[0] = 1.0f + tex_offs_x; vertices[5].tex[1] = 1.0f + tex_offs_y; - + // Colours - use this to fade the video in and out - for (int i = 0; i < (4 * 6); ++i) { - if ((i%4) < 3) + for (int i = 0; i < (4 * 6); ++i) + { + if ((i%4) == 3) vertices[i / 4].colour[i % 4] = mFadeIn; else vertices[i / 4].colour[i % 4] = 1.0f; } - + glEnable(GL_TEXTURE_2D); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Build a texture for the video frame mTexture->initFromPixels((unsigned char*)mContext.surface->pixels, mContext.surface->w, mContext.surface->h); @@ -208,6 +209,7 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) glDisableClientState(GL_COLOR_ARRAY); glDisable(GL_TEXTURE_2D); + glDisable(GL_BLEND); } else { VideoComponent::renderSnapshot(parentTrans); } @@ -346,10 +348,12 @@ void VideoVlcComponent::startVideo() libvlc_audio_set_mute(mMediaPlayer, 1); } - libvlc_media_player_play(mMediaPlayer); + libvlc_media_player_play(mMediaPlayer); libvlc_video_set_callbacks(mMediaPlayer, lock, unlock, display, (void*)&mContext); libvlc_video_set_format(mMediaPlayer, "RGBA", (int)mVideoWidth, (int)mVideoHeight, (int)mVideoWidth * 4); + // libvlc_media_player_set_position(mMediaPlayer, 0.15); + // Update the playing state mIsPlaying = true; mFadeIn = 0.0f; diff --git a/es-core/src/resources/ResourceManager.cpp b/es-core/src/resources/ResourceManager.cpp index 9f5014e432..3887a671a4 100644 --- a/es-core/src/resources/ResourceManager.cpp +++ b/es-core/src/resources/ResourceManager.cpp @@ -52,9 +52,10 @@ const ResourceData ResourceManager::getFileData(const std::string& path) const //check if its a resource const std::string respath = getResourcePath(path); - if(Utils::FileSystem::exists(respath)) + auto size = Utils::FileSystem::getFileSize(respath); + if (size > 0) { - ResourceData data = loadFile(respath); + ResourceData data = loadFile(respath, size); return data; } @@ -63,13 +64,16 @@ const ResourceData ResourceManager::getFileData(const std::string& path) const return data; } -ResourceData ResourceManager::loadFile(const std::string& path) const +ResourceData ResourceManager::loadFile(const std::string& path, size_t size) const { std::ifstream stream(path, std::ios::binary); - stream.seekg(0, stream.end); - size_t size = (size_t)stream.tellg(); - stream.seekg(0, stream.beg); + if (size == 0) + { + stream.seekg(0, stream.end); + size = (size_t)stream.tellg(); + stream.seekg(0, stream.beg); + } //supply custom deleter to properly free array std::shared_ptr data(new unsigned char[size], array_deleter); diff --git a/es-core/src/resources/ResourceManager.h b/es-core/src/resources/ResourceManager.h index 96eab6455e..ad3f20133e 100644 --- a/es-core/src/resources/ResourceManager.h +++ b/es-core/src/resources/ResourceManager.h @@ -43,7 +43,7 @@ class ResourceManager static std::shared_ptr sInstance; - ResourceData loadFile(const std::string& path) const; + ResourceData loadFile(const std::string& path, size_t size) const; std::list< std::weak_ptr > mReloadables; }; diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 48aae8932a..4c11b02198 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -14,8 +14,9 @@ #define DPI 96 TextureData::TextureData(bool tile) : mTile(tile), mTextureID(0), mDataRGBA(nullptr), mScalable(false), - mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f) + mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f), mMaxSize(Vector2f(0,0)) { + } TextureData::~TextureData() @@ -73,7 +74,14 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length // auto scale height to keep aspect mHeight = (size_t)Math::round(((float)mWidth / svgImage->width) * svgImage->height); } - + + if (mMaxSize.x() > 0 && mMaxSize.y() > 0 && (mWidth > mMaxSize.x() || mHeight > mMaxSize.y())) + { + Vector2i sz = ImageIO::adjustPictureSize(Vector2i(mWidth, mHeight), Vector2i(mMaxSize.x(), mMaxSize.y())); + mWidth = sz.x(); + mHeight = sz.y(); + } + unsigned char* dataRGBA = new unsigned char[mWidth * mHeight * 4]; NSVGrasterizer* rast = nsvgCreateRasterizer(); @@ -97,8 +105,8 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng if (mDataRGBA) return true; } - - unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height); + + unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height, mMaxSize.x(), mMaxSize.y()); // std::vector imageRGBA = ImageIO::loadFromMemoryRGBA32((const unsigned char*)(fileData), length, width, height); if (imageRGBA == NULL) diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 2355d439a9..7a0215de9a 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -7,6 +7,8 @@ #include #include +#include "math/Vector2f.h" + class TextureResource; class TextureData @@ -39,6 +41,8 @@ class TextureData // Release the texture from conventional RAM void releaseRAM(); + void setMaxSize(Vector2f maxSize) { mMaxSize = maxSize; }; + // Get the amount of VRAM currenty used by this texture size_t getVRAMUsage(); @@ -62,6 +66,8 @@ class TextureData float mSourceHeight; bool mScalable; bool mReloadable; + + Vector2f mMaxSize; }; #endif // ES_CORE_RESOURCES_TEXTURE_DATA_H diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index f64719003d..5755887187 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -113,6 +113,7 @@ void TextureDataManager::load(std::shared_ptr tex, bool block) { if (size < max_texture) break; + //size -= (*it)->getVRAMUsage(); (*it)->releaseVRAM(); (*it)->releaseRAM(); diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index c3d9eff0b0..3cb0c6f2b8 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -7,7 +7,7 @@ TextureDataManager TextureResource::sTextureDataManager; std::map< TextureResource::TextureKeyType, std::weak_ptr > TextureResource::sTextureMap; std::set TextureResource::sAllTextures; -TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic) : mTextureData(nullptr), mForceLoad(false) +TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic, Vector2f maxSize) : mTextureData(nullptr), mForceLoad(false) { // Create a texture data object for this texture if (!path.empty()) @@ -18,6 +18,7 @@ TextureResource::TextureResource(const std::string& path, bool tile, bool dynami if (dynamic) { data = sTextureDataManager.add(this, tile); + data->setMaxSize(maxSize); data->initFromPath(path); // Force the texture manager to load it using a blocking load sTextureDataManager.load(data, true); @@ -25,7 +26,9 @@ TextureResource::TextureResource(const std::string& path, bool tile, bool dynami else { mTextureData = std::shared_ptr(new TextureData(tile)); + data = mTextureData; + data->setMaxSize(maxSize); data->initFromPath(path); // Load it so we can read the width/height data->load(); @@ -100,14 +103,14 @@ bool TextureResource::bind() } } -std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic) +std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic, Vector2f maxSize) { std::shared_ptr& rm = ResourceManager::getInstance(); const std::string canonicalPath = Utils::FileSystem::getCanonicalPath(path); if(canonicalPath.empty()) { - std::shared_ptr tex(new TextureResource("", tile, false)); + std::shared_ptr tex(new TextureResource("", tile, false, maxSize)); rm->addReloadable(tex); //make sure we get properly deinitialized even though we do nothing on reinitialization return tex; } @@ -122,7 +125,7 @@ std::shared_ptr TextureResource::get(const std::string& path, b // need to create it std::shared_ptr tex; - tex = std::shared_ptr(new TextureResource(key.first, tile, dynamic)); + tex = std::shared_ptr(new TextureResource(key.first, tile, dynamic, maxSize)); std::shared_ptr data = sTextureDataManager.get(tex.get()); // is it an SVG? @@ -135,6 +138,9 @@ std::shared_ptr TextureResource::get(const std::string& path, b // Add it to the reloadable list rm->addReloadable(tex); + if (data != nullptr) + data->setMaxSize(maxSize); + // Force load it if necessary. Note that it may get dumped from VRAM if we run low if (forceLoad) { diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index c2be59f1d7..a6d9aee083 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -16,7 +16,7 @@ class TextureData; class TextureResource : public IReloadable { public: - static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true); + static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true, Vector2f maxSize = Vector2f(0, 0)); void initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height); virtual void initFromMemory(const char* file, size_t length); @@ -36,7 +36,7 @@ class TextureResource : public IReloadable static size_t getTotalTextureSize(); // returns the number of bytes that would be used if all textures were in memory protected: - TextureResource(const std::string& path, bool tile, bool dynamic); + TextureResource(const std::string& path, bool tile, bool dynamic, Vector2f maxSize); virtual void unload(std::shared_ptr& rm); virtual void reload(std::shared_ptr& rm); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 150a6191a2..c16aab4720 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -80,7 +80,7 @@ namespace Utils FileInfo fi; fi.path = fullName; - fi.readOnly = (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN; + fi.hidden = (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN; fi.directory = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY; contentList.push_back(fi); @@ -250,11 +250,19 @@ namespace Utils static std::string path; // only construct the homepath once - if(!path.length()) + if (!path.length()) { + // verify if .emulationstation/es_systems.cfg is under exe's path to make app portable + std::string portableDir = getExePath() + "/.emulationstation/es_systems.cfg"; + if (Utils::FileSystem::exists(portableDir)) + { + path = getExePath(); + return path; + } + // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows char* envHome = getenv("HOME"); - envHome = "H:/[Emulz]/EmulationStation/"; + #ifdef _DEBUG envHome = "H:/[Emulz]/EmulationStation/"; #endif @@ -303,8 +311,8 @@ namespace Utils if (result) { std::string ret = buffer; - path = getParent(ret); - return ret; + path = getGenericPath(getParent(ret)); + return path; } #endif path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); @@ -678,12 +686,24 @@ namespace Utils { std::string path = getGenericPath(_path); struct stat64 info; - + // check if stat64 succeeded return (stat64(path.c_str(), &info) == 0); } // exists + size_t getFileSize(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + if ((stat64(path.c_str(), &info) == 0)) + return (size_t) info.st_size; + + return 0; + } + bool isAbsolute(const std::string& _path) { std::string path = getGenericPath(_path); diff --git a/es-core/src/utils/FileSystemUtil.h b/es-core/src/utils/FileSystemUtil.h index e8b01741c0..8f995f7990 100644 --- a/es-core/src/utils/FileSystemUtil.h +++ b/es-core/src/utils/FileSystemUtil.h @@ -13,7 +13,7 @@ namespace Utils { public: std::string path; - bool readOnly; + bool hidden; bool directory; }; @@ -42,6 +42,7 @@ namespace Utils bool removeFile (const std::string& _path); bool createDirectory (const std::string& _path); bool exists (const std::string& _path); + size_t getFileSize(const std::string& _path); bool isAbsolute (const std::string& _path); bool isRegularFile (const std::string& _path); bool isDirectory (const std::string& _path); From 9dc589cb6aa269726b9aa54b15dda823118c1392 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Wed, 19 Jun 2019 15:19:26 +0200 Subject: [PATCH 018/336] Update README.md --- README.md | 142 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 109 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index b4c9a4f011..bfe59cc3a8 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,123 @@ -EmulationStation -================ +EmulationStation FCAMOD +======================= -This is a fork of EmulationStation from jrassa. -EmulationStation is a cross-platform graphical front-end for emulators with controller navigation. +This is a fork of EmulationStation containing many additions. +This has been primary developped for Windows platform, but can be compiled for Linux & Raspberry Pi. Changes in my branch ==================== +**System list :** - Support for Multiple Emulators/Cores in es_systems.cfg, and setting Emulator/Core per game. -```xml - %HOME%\RetroArch\retroarch.exe -L %HOME%\RetroArch\cores\%CORE%_libretro.dll %ROM% - - - - mame2003_plus - mame2003 - - - - - fbalpha2012 - - - -``` -- Better Grid view (animations, layout, selection...) -- Created a 'GridEx' mode, which inherits from grid, in order to create a 'preview bar' including a game screenshot. -- Changed 'Automatic' view mode. Switchs to grid if "thumbnails" exist in gamelist. Switchs to gridex if "thumbnails" and "image" exist in gamelist. -- Don't show Directories that contains only one Game : just Show the game. -- Don't show Games what are marked Hidden in gamelist. -- Cleaned menus + changed menu item order (by interest). Simplified "Quit" menu item ( no more popup ) -- Case insensitive file extensions. -- Skip parsing 'downloaded_images' and 'media' folders ( loading time boost ) + ```xml + %HOME%\RetroArch\retroarch.exe -L %HOME%\RetroArch\cores\%CORE%_libretro.dll %ROM% + + + + mame2003_plus + mame2003 + + + + + fbalpha2012 + + + + ``` +**Grid view :** +- Animations when size changes and during scrolling. +- Supports having a label. + ```xml + + 969A9E + 1 0.18 + + + F6FAFF + + ``` +- Layout can be defined by number of columns and rows ( you had to calculate manually the size of tiles in previous versions ). Zooming the selected item can also be defined simply. + ```xml + + 4 3 + 1.04 + ``` +- Supports extended padding (top, left, bottom, right) : + ```xml + + 0.03 0.13 0.03 0.08 + ``` + +- Supports video in the selected item (delay can be defined in the theme) + ```xml + + 700 + ``` + +- Theme can define which image to use (image, thumbnail or marquee). + ```xml + + marquee + ``` + +- Theme can define the image sizing mode (minSize, maxSize or size). Gridtile items can define a padding. + ```xml + + 24 24 + minSize + ``` + +- Supports md_image, md_video, md_name items... just like detailed view. +- Ability to override grid size by system. + +**Detailed view :** +- Supports md_video, md_marquee items like video view did : Video view is no longer useful. + +**Custom views & Theming:** +- Allow creation of custom views, which inherits from one of the basic theme items ( basic, detailed, grid ). + ```xml + + + ``` +- Ability to select the view (or customview) to use globally or by system. +- The theme can force the default view to use ( attribute defaultView ) +- Fully supports Retropie & Recalbox Themes. + +**Optimizations :** +- Really faster loading time, using multithreading. +- The loading sequence displays a progress bar. +- Reviewed SVG loading and size calculation mecanism. Previous versions unloaded/reloaded SVGs each time a new container needed to display it because of a size calculation problem. +- Ability to disable "Preload UI" mecanism. This mecanism is used to preload the UI of gamelists of every system. Disable it adds a small lags when opening +- Don't keep in memory the cache of image filenames when launching games -> It takes a lot of memory for nothing. +- Skip parsing 'downloaded_images' and 'media' folders ( better loading time ) +- Added option "Optimize images Vram Use" : Don't load an image in it source resolution if it needs to be displayed smaller -> Resize images in memory to save VRAM. Introduce longer image loading time, but less VRAM use. + +**Menus :** +- Cleaned menus + changed menu item order (by interest). +- Full support for menu Theming. +- Separated "Transition style" and "Game launch transition" +- Added option "Boot on gamelist" +- Added option "Hide system view" +- Added option "Display favorites first in gamelist" + +**General :** - Localisation (French actually supported) +- OSK : On-screen Keyboard. +- Fixed : Don't show Games what are marked Hidden in gamelist. - Corrected favorites ( and custom lists ) management. -- Don't load all fields in Medadata Editor ( too tricky to use on windows, better use an external tool ). +- Don't show Directories that contains only one Game : just Show the game. +- Case insensitive file extensions. + +**Windows specific :** +- Simplified "Quit" menu item ( no more popup asking to restart or turn off Windows ) - Windows is now "Windowed No border" by default. On Windows, Exclusive fullscreen can be annoying... -- Theming : Ability to force default view ( attribute defaultView ) -- Windows : Stop using _wsystem for launching games. Run games with ShellExecuteEx instead ( avoids command window ) -- Add an option to leave ES open with a black screen" Chargement en cours..." when launching games ( avoids showing windows desktop ) +- Stop using _wsystem for launching games. Run games with ShellExecuteEx instead ( avoids command window ) +- Add an option to leave ES open with a black screen "Loading..." when launching games ( avoids showing windows desktop ) +- Don't load all fields in Medadata Editor ( too tricky to use on windows, better use an external tool ). -Je crois que c'est à peu près tout.... +Je crois que c'est à peu près tout... Building ======== From 54d79d8a033ec5ccf935f7aa8d0c4cf189479c07 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Wed, 19 Jun 2019 16:41:49 +0200 Subject: [PATCH 019/336] Update README.md --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bfe59cc3a8..61ace5c931 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,11 @@ Changes in my branch - Ability to select the view (or customview) to use globally or by system. - The theme can force the default view to use ( attribute defaultView ) - Fully supports Retropie & Recalbox Themes. +- Carousel supports element "logoPos" : this allows the logo not to be inevitably centered. +- Image loading : the image bytes where duplicated 3 times in memory. +- In previous versions, if a xml element was unknown in the theme, nothing was loaded. -**Optimizations :** +**Optimizations & Fixes:** - Really faster loading time, using multithreading. - The loading sequence displays a progress bar. - Reviewed SVG loading and size calculation mecanism. Previous versions unloaded/reloaded SVGs each time a new container needed to display it because of a size calculation problem. @@ -93,6 +96,11 @@ Changes in my branch - Don't keep in memory the cache of image filenames when launching games -> It takes a lot of memory for nothing. - Skip parsing 'downloaded_images' and 'media' folders ( better loading time ) - Added option "Optimize images Vram Use" : Don't load an image in it source resolution if it needs to be displayed smaller -> Resize images in memory to save VRAM. Introduce longer image loading time, but less VRAM use. +- Fixed video starting : Videos started fading even if the video was not available yet ( but not really fading : there was no blending ). +- Software clipping : Avoid rendering clipped items -> They were previously clipped by OpenGl scissors. +- Carousel animation was corrupted if the carousel has to display only one item with 1 +- Font : Optimization when calculating text extend. +- If XML writer fails, the gamelist.xml file become empty and set to 0Kb -> Added a mecanism to secure that. Also, previous gamelist.xml version is saved as gamelist.xml.old. **Menus :** - Cleaned menus + changed menu item order (by interest). @@ -106,16 +114,20 @@ Changes in my branch - Localisation (French actually supported) - OSK : On-screen Keyboard. - Fixed : Don't show Games what are marked Hidden in gamelist. +- Added a star icon before the name of the game when it is a favorite. - Corrected favorites ( and custom lists ) management. - Don't show Directories that contains only one Game : just Show the game. - Case insensitive file extensions. +- Stop using "sortname" in gamelists. It is useful. **Windows specific :** +- Natively portable. If file ".emulationstation/es_systems.cfg" relative to the exe folder. - Simplified "Quit" menu item ( no more popup asking to restart or turn off Windows ) - Windows is now "Windowed No border" by default. On Windows, Exclusive fullscreen can be annoying... - Stop using _wsystem for launching games. Run games with ShellExecuteEx instead ( avoids command window ) - Add an option to leave ES open with a black screen "Loading..." when launching games ( avoids showing windows desktop ) - Don't load all fields in Medadata Editor ( too tricky to use on windows, better use an external tool ). +- With some Nvidia GPUs when VSYNC is active, SDL_GL_SwapWindow takes a lot of CPU : Introduce a smart calculation based on display frequency to reduce the time SDL_GL_SwapWindow has to wait. This saves a lot of CPU load. Je crois que c'est à peu près tout... @@ -221,7 +233,7 @@ You can use `--help` or `-h` to view a list of command-line options. Briefly out --no-splash don't show the splash screen --debug more logging, show console on Windows --scrape scrape using command line interface ---windowed not fullscreen, should be used with --resolution +--windowed not fullscreen, may be used with --resolution --vsync [1/on or 0/off] turn vsync on or off (default is on) --max-vram [size] Max VRAM to use in Mb before swapping. 0 for unlimited --force-kid Force the UI mode to be Kid From dcb29f18f83fdb193a82ded7cc25923f724c8bd7 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Wed, 19 Jun 2019 17:08:40 +0200 Subject: [PATCH 020/336] Update THEMES.md --- THEMES.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 11 deletions(-) diff --git a/THEMES.md b/THEMES.md index fc4ff063b1..75274239ca 100644 --- a/THEMES.md +++ b/THEMES.md @@ -69,8 +69,8 @@ How it works Everything must be inside a `` tag. -**The `` tag *must* be specified**. This is the version of the theming system the theme was designed for. The current version is 3. - +**The `` tag *must* be specified**. This is the version of the theming system the theme was designed for. +The current version is 4. A *view* can be thought of as a particular "screen" within EmulationStation. Views are defined like this: @@ -109,6 +109,14 @@ Or, you can create your own elements by adding `extra="true"` (as is done in the ValueHere ``` +A *customView* can be thought of as a particular "screen" within EmulationStation. +Custom Views must inherit one of the standard views and are defined like this: + +```xml + + ... define elements here ... + +``` @@ -124,7 +132,7 @@ You can include theme files within theme files, similar to `#include` in C (thou `~/.emulationstation/all_themes.xml`: ```xml - 3 + 4 ./all_themes/myfont.ttf @@ -137,7 +145,7 @@ You can include theme files within theme files, similar to `#include` in C (thou `~/.emulationstation/snes/theme.xml`: ```xml - 3 + 4 ./../all_themes.xml @@ -150,7 +158,7 @@ You can include theme files within theme files, similar to `#include` in C (thou Is equivalent to this `snes/theme.xml`: ```xml - 3 + 4 ./all_themes/myfont.ttf @@ -170,7 +178,7 @@ Sometimes you want to apply the same properties to the same elements across mult ```xml - 3 + 4 ./snes_art/snes_header.png @@ -284,7 +292,7 @@ You can now change the order in which elements are rendered by setting `zIndex` * `imagegrid name="gamegrid"` - 20 * Media * `image name="md_image"` - 30 - * `video name="md_video"` - 30 + * `video name="md_video"` - 31 * `image name="md_marquee"` - 35 * Metadata - 40 * Labels @@ -392,6 +400,10 @@ Reference * `image name="md_image"` - POSITION | SIZE | Z_INDEX - Path is the "image" metadata for the currently selected game. + * `video name="md_video"` - POSITION | SIZE | Z_INDEX + - Path is the "video" metadata for the currently selected game. + * `image name="md_marquee"` - POSITION | SIZE | Z_INDEX + - Path is the "marquee" metadata for the currently selected game. * `rating name="md_rating"` - ALL - The "rating" metadata. * `datetime name="md_releasedate"` - ALL @@ -534,10 +546,33 @@ Reference - Displays details of the system currently selected in the carousel. * You can use extra elements (elements with `extra="true"`) to add your own backgrounds, etc. They will be displayed behind the carousel, and scroll relative to the carousel. +#### menu +* `helpsystem name="help"` - ALL + - The help system style for this view. If not defined, menus will have the same helpsystem as defined in system view. +* `menuBackground name="menubg"` - COLOR | PATH | FADEPATH + - The background behind menus. you can set an image and/or change color (alpha supported) + +* `menuSwitch name="menuswitch"` - PATHON | PATHOFF + - Images for the on/off switch in menus +* `menuSlider name="menuslider"` - PATH + - Image for the slider knob in menus +* `menuButton name="menubutton"` - PATH | FILLEDPATH + - Images for menu buttons +* `menuText name="menutext"` - FONTPATH | FONTSIZE | COLOR + - text for all menu entries +* `menuText name="menutitle"` - FONTPATH | FONTSIZE | COLOR + - text for menu titles +* `menuText name="menufooter"` - FONTPATH | FONTSIZE | COLOR + - text for menu footers or subtitles +* `menuTextSmall name="menutextsmall"` - FONTPATH | FONTSIZE | COLOR + - text for menu entries in smallerfont + +menu is used to theme helpsystem and ES menus. ## Types of properties: * NORMALIZED_PAIR - two decimals, in the range [0..1], delimited by a space. For example, `0.25 0.5`. Most commonly used for position (x and y coordinates) and size (width and height). +* NORMALIZED_RECT - four decimals, in the range [0..1], delimited by a space. For example, `0.25 0.5 0.10 0.30`. Most commonly used for padding to store top, left, bottom and right coordinates. * PATH - a path. If the first character is a `~`, it will be expanded into the environment variable for the home path (`$HOME` for Linux or `%HOMEPATH%` for Windows). If the first character is a `.`, it will be expanded to the theme file's directory, allowing you to specify resources relative to the theme file, like so: `./../general_art/myfont.ttf`. * BOOLEAN - `true`/`1` or `false`/`0`. * COLOR - a hexidecimal RGB or RGBA color (6 or 8 digits). If 6 digits, will assume the alpha channel is `FF` (not transparent). @@ -584,14 +619,26 @@ Can be created as an extra. * `pos` - type: NORMALIZED_PAIR. * `size` - type: NORMALIZED_PAIR. - The size of the grid. Take care the selected tile can go out of the grid size, so don't position the grid too close to another element or the screen border. -* `margin` - type: NORMALIZED_PAIR. +* `margin` - type: NORMALIZED_PAIR. Margin between tiles. +* `padding` - type: NORMALIZED_RECT. + - NEW : Padding for displaying tiles. +* `autoLayout` - type: NORMALIZED_PAIR. + - NEW : Number of column and rows in the grid (integer values). +* `autoLayoutSelectedZoom` - type: FLOAT. + - NEW : Zoom factor to apply when a tile is selected. +* `imageSource` - type: STRING. + - NEW : Selects the image to display. `thumbnail` by default, can also be set to `image` or `marquee`. +* `showVideoAtDelay` - type: FLOAT. + - NEW : delay in millseconds to display video, when the tile is selected. * `gameImage` - type: PATH. - The default image used for games which doesn't have an image. * `folderImage` - type: PATH. - The default image used for folders which doesn't have an image. * `scrollDirection` - type: STRING. - `vertical` by default, can also be set to `horizontal`. Not that in `horizontal` mod, the tiles are ordered from top to bottom, then from left to right. - +* `zIndex` - type: FLOAT. + - NEW : z-index value for component. Components will be rendered in order of z-index value from low to high. + #### gridtile * `size` - type: NORMALIZED_PAIR. @@ -610,7 +657,11 @@ Can be created as an extra. - Set the color of the center part of the ninepatch. The default tile background center color and selected tile background center color have no influence on each others. * `backgroundEdgeColor` - type: COLOR. - Set the color of the edge parts of the ninepatch. The default tile background edge color and selected tile background edge color have no influence on each others. - +* `selectionMode` - type: STRING. + - NEW : Selects if the background is over the full tile or only the image. `full` by default, can also be set to `image`. +* `imageSizeMode` - type: STRING. + - NEW : Selects the image sizing mode. `maxSize` by default, can also be set to `minSize` (outer zoom) or `size` (stretch). + #### video * `pos` - type: NORMALIZED_PAIR. @@ -634,7 +685,9 @@ Can be created as an extra. - If true, playing of video will be delayed for `delayed` seconds, when game is selected. * `zIndex` - type: FLOAT. - z-index value for component. Components will be rendered in order of z-index value from low to high. - +* `path` - type: PATH. + - NEW : Path to video file if video is an extra. + #### text Can be created as an extra. @@ -813,6 +866,24 @@ EmulationStation borrows the concept of "nine patches" from Android (or "9-Slice - Default is 3 * `zIndex` - type: FLOAT. - z-index value for component. Components will be rendered in order of z-index value from low to high. +* `logoPos` - type: NORMALIZED_PAIR. + - NEW : Set the logo position if it is not centered. + +#### menuText & menuTextSmall + +* `color` - type: COLOR. + - Default is 777777FF +* `fontPath` - type: PATH. + - Path to a truetype font (.ttf). +* `fontSize` - type: FLOAT. + - Size of the font as a percentage of screen height (e.g. for a value of `0.1`, the text's height would be 10% of the screen height). Default is 0.085 for menutitle, 0.045 for menutext and 0.035 for menufooter and menutextsmall. +* `separatorColor` - type: COLOR. + - Default is C6C7C6FF. Color of lines that separates menu entries. +* `selectedColor` - type: COLOR. + - Default is FFFFFFFF. Color of text for selected menu entry. +* `selectorColor` - type: COLOR. + - Default is 878787FF. Color of the selector bar. + The help system is a special element that displays a context-sensitive list of actions the user can take at any time. You should try and keep the position constant throughout every screen. Keep in mind the "default" settings (including position) are used whenever the user opens a menu. From 1f8add9b1ac280e5a84bea838cd67650c03d05b0 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Wed, 19 Jun 2019 17:09:37 +0200 Subject: [PATCH 021/336] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 61ace5c931..0e2ee90026 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ Changes in my branch **General :** - Localisation (French actually supported) - OSK : On-screen Keyboard. +- Video elements can be added as extras. - Fixed : Don't show Games what are marked Hidden in gamelist. - Added a star icon before the name of the game when it is a favorite. - Corrected favorites ( and custom lists ) management. From 831cd9cdc8a9f8498768f209a27d07fb78ab0802 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 19 Jun 2019 17:28:14 +0200 Subject: [PATCH 022/336] - Faster loading time, using multithreading. - Reviewed SVG loading and size calculation mecanism. Previous versions unloaded/reloaded SVGs each time a new container needed to display it because of a size calculation problem. - Ability to override grid size by system. - Grid supports extended padding (top, left, bottom, right) : - Theme can define which image to use (image, thumbnail or marquee). - Don't keep in memory the cache of image filenames when launching games -> It takes a lot of memory for nothing. - If XML writer fails, the gamelist.xml file become empty and set to 0Kb -> Added a mecanism to secure that. Also, previous gamelist.xml version is saved as gamelist.xml.old. - OSK : On-screen Keyboard. - Video elements can be added as extras. --- es-app/src/CollectionSystemManager.cpp | 13 +- es-app/src/CollectionSystemManager.h | 2 +- es-app/src/EmulationStation.h | 16 +- es-app/src/Gamelist.cpp | 42 +- es-app/src/SystemData.cpp | 429 +++++++++++++----- es-app/src/SystemData.h | 12 +- .../src/components/ScraperSearchComponent.cpp | 17 +- es-app/src/components/TextListComponent.h | 2 +- .../src/guis/GuiCollectionSystemsOptions.cpp | 13 +- es-app/src/guis/GuiCollectionSystemsOptions.h | 3 + es-app/src/guis/GuiGamelistOptions.cpp | 125 ++++- es-app/src/guis/GuiGamelistOptions.h | 6 +- es-app/src/guis/GuiMenu.cpp | 25 +- es-app/src/guis/GuiMetaDataEd.cpp | 7 +- es-app/src/main.cpp | 4 + es-app/src/views/SystemView.cpp | 152 ++++--- es-app/src/views/ViewController.cpp | 38 +- .../src/views/gamelist/BasicGameListView.cpp | 53 ++- .../views/gamelist/DetailedGameListView.cpp | 107 +++-- .../src/views/gamelist/DetailedGameListView.h | 10 +- .../src/views/gamelist/GridGameListView.cpp | 159 ++++++- es-app/src/views/gamelist/GridGameListView.h | 13 +- .../views/gamelist/ISimpleGameListView.cpp | 2 +- .../src/views/gamelist/VideoGameListView.cpp | 2 +- es-core/CMakeLists.txt | 2 + es-core/src/GuiComponent.cpp | 15 +- es-core/src/GuiComponent.h | 14 + es-core/src/ImageIO.cpp | 12 +- es-core/src/ImageIO.h | 2 +- es-core/src/Settings.cpp | 8 +- es-core/src/ThemeData.cpp | 57 ++- es-core/src/ThemeData.h | 13 +- es-core/src/Window.cpp | 21 +- es-core/src/components/ButtonComponent.cpp | 16 +- es-core/src/components/ButtonComponent.h | 9 +- es-core/src/components/ComponentGrid.cpp | 16 +- es-core/src/components/ComponentGrid.h | 4 + es-core/src/components/GridTileComponent.cpp | 201 ++++---- es-core/src/components/GridTileComponent.h | 11 +- es-core/src/components/IList.h | 9 + es-core/src/components/ImageComponent.cpp | 23 +- es-core/src/components/ImageComponent.h | 4 + es-core/src/components/ImageGridComponent.h | 270 +++++------ es-core/src/components/MenuComponent.cpp | 56 +++ es-core/src/components/MenuComponent.h | 1 + es-core/src/components/NinePatchComponent.cpp | 11 + es-core/src/components/NinePatchComponent.h | 1 + es-core/src/components/SwitchComponent.cpp | 7 +- es-core/src/components/TextEditComponent.h | 3 +- es-core/src/components/VideoComponent.cpp | 62 ++- es-core/src/components/VideoComponent.h | 9 + .../src/components/VideoPlayerComponent.cpp | 4 +- es-core/src/components/VideoVlcComponent.cpp | 37 +- es-core/src/components/VideoVlcComponent.h | 1 + es-core/src/guis/GuiTextEditPopupKeyboard.cpp | 266 +++++++++++ es-core/src/guis/GuiTextEditPopupKeyboard.h | 73 +++ es-core/src/math/Vector2f.cpp | 22 + es-core/src/math/Vector2f.h | 3 + es-core/src/platform.h | 2 + es-core/src/resources/Font.cpp | 17 +- es-core/src/resources/Font.h | 7 +- es-core/src/resources/ResourceManager.cpp | 6 +- es-core/src/resources/ResourceManager.h | 4 +- es-core/src/resources/TextureData.cpp | 94 +++- es-core/src/resources/TextureData.h | 22 +- es-core/src/resources/TextureDataManager.cpp | 57 ++- es-core/src/resources/TextureResource.cpp | 76 +++- es-core/src/resources/TextureResource.h | 8 +- es-core/src/utils/FileSystemUtil.cpp | 3 + es-core/src/utils/StringUtil.cpp | 20 +- es-core/src/utils/StringUtil.h | 1 + 71 files changed, 2152 insertions(+), 680 deletions(-) create mode 100644 es-core/src/guis/GuiTextEditPopupKeyboard.cpp create mode 100644 es-core/src/guis/GuiTextEditPopupKeyboard.h diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 16af995b20..fb87f18a60 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -124,7 +124,7 @@ void CollectionSystemManager::saveCustomCollection(SystemData* sys) /* Methods to load all Collections into memory, and handle enabling the active ones */ // loads all Collection Systems -void CollectionSystemManager::loadCollectionSystems() +void CollectionSystemManager::loadCollectionSystems(bool async) { initAutoCollectionSystems(); CollectionSystemDecl decl = mCollectionSystemDeclsIndex[myCollectionsName]; @@ -135,8 +135,11 @@ void CollectionSystemManager::loadCollectionSystems() { // Now see which ones are enabled loadEnabledListFromSettings(); + + // add to the main System Vector, and create Views as needed - updateSystemsList(); + if (!async) + updateSystemsList(); } } @@ -170,7 +173,7 @@ void CollectionSystemManager::updateSystemsList() // add custom enabled ones addEnabledCollectionsToDisplayedSystems(&mCustomCollectionSystemsData); - if(Settings::getInstance()->getBool("SortAllSystems")) + if (Settings::getInstance()->getBool("SortAllSystems")) { // sort custom individual systems with other systems std::sort(SystemData::sSystemVector.begin(), SystemData::sSystemVector.end(), systemSort); @@ -200,7 +203,7 @@ void CollectionSystemManager::updateSystemsList() // add auto enabled ones addEnabledCollectionsToDisplayedSystems(&mAutoCollectionSystemsData); - + /* // create views for collections, before reload for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) { @@ -208,7 +211,7 @@ void CollectionSystemManager::updateSystemsList() { ViewController::get()->getGameListView((*sysIt)); } - } + }*/ // if we were editing a custom collection, and it's no longer enabled, exit edit mode if(mIsEditingCustom && !mEditingCollectionSystemData->isEnabled) diff --git a/es-app/src/CollectionSystemManager.h b/es-app/src/CollectionSystemManager.h index 23afe19320..50a113c366 100644 --- a/es-app/src/CollectionSystemManager.h +++ b/es-app/src/CollectionSystemManager.h @@ -49,7 +49,7 @@ class CollectionSystemManager static void deinit(); void saveCustomCollection(SystemData* sys); - void loadCollectionSystems(); + void loadCollectionSystems(bool async=false); void loadEnabledListFromSettings(); void updateSystemsList(); diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index 375b11d9cf..96c9c42201 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -4,15 +4,17 @@ // 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 10 -#define PROGRAM_VERSION_MAINTENANCE 0 -#define PROGRAM_VERSION_STRING "2.10.0 FCAMOD" -#define SCRAPER_VERSION_STRING "2.10.0" +#define PROGRAM_VERSION_MAJOR 2 +#define PROGRAM_VERSION_MINOR 10 +#define PROGRAM_VERSION_MAINTENANCE 0 +#define PROGRAM_VERSION_BUILD 4 + +#define PROGRAM_VERSION_STRING "2.10.0.4 FCAMOD" +#define SCRAPER_VERSION_STRING "2.10.0.4" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ -#define RESOURCE_VERSION_STRING "2,10,0\0" -#define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE +#define RESOURCE_VERSION_STRING "2,10,0,4\0" +#define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE,PROGRAM_VERSION_BUILD #endif // ES_APP_EMULATION_STATION_H diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index f0328caf76..ac56208496 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -1,6 +1,7 @@ #include "Gamelist.h" #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" #include "FileData.h" #include "FileFilterIndex.h" #include "Log.h" @@ -107,7 +108,8 @@ void parseGamelist(SystemData* system) return; } - system->setSystemViewMode(root.attribute("defaultView").value()); + Vector2f gridSizeOverride = Vector2f::parseString(root.attribute("gridSize").value()); + system->setSystemViewMode(root.attribute("defaultView").value(), gridSizeOverride, false); std::string relativeTo = system->getStartPath(); @@ -215,6 +217,19 @@ void updateGamelist(SystemData* system) else root.attribute("defaultView") = system->getSystemViewMode().c_str(); } + + Vector2f gridSize = Vector2f::parseString(root.attribute("gridSize").value()); + if (gridSize != system->getGridSizeOverride()) + { + numUpdated++; + + if (system->getGridSizeOverride() == Vector2f(0,0)) + root.remove_attribute("gridSize"); + else if (root.attribute("gridSize").empty()) + root.append_attribute("gridSize") = system->getGridSizeOverride().toString().c_str(); + else + root.attribute("gridSize") = system->getGridSizeOverride().toString().c_str(); + } if(!root) { @@ -288,9 +303,32 @@ void updateGamelist(SystemData* system) LOG(LogInfo) << "Added/Updated " << numUpdated << " entities in '" << xmlReadPath << "'"; - if (!doc.save_file(xmlWritePath.c_str())) { + // Secure XML writing -> Write to a temporary file first + std::string tmpFile = xmlWritePath + ".tmp"; + if (Utils::FileSystem::exists(tmpFile)) + Utils::FileSystem::removeFile(tmpFile); + + if (!doc.save_file(tmpFile.c_str())) { LOG(LogError) << "Error saving gamelist.xml to \"" << xmlWritePath << "\" (for system " << system->getName() << ")!"; } + else if (Utils::FileSystem::exists(tmpFile)) + { + // Secure XML writing + if ((int) Utils::FileSystem::getFileSize(tmpFile) > 0) + { + std::string savFile = xmlWritePath + ".old"; + + if (Utils::FileSystem::exists(savFile)) + Utils::FileSystem::removeFile(savFile); + + if (Utils::FileSystem::exists(xmlWritePath)) + std::rename(xmlWritePath.c_str(), savFile.c_str()); + + std::rename(tmpFile.c_str(), xmlWritePath.c_str()); + } + else + Utils::FileSystem::removeFile(tmpFile); + } } }else{ LOG(LogError) << "Found no root folder for system \"" << system->getName() << "\"!"; diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 657012078f..e12f7cb5bf 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -10,8 +10,10 @@ #include "Settings.h" #include "ThemeData.h" #include "views/UIModeController.h" -#include #include + +#include "utils/StringUtil.h" + #ifdef WIN32 #include #endif @@ -19,11 +21,16 @@ #include "GuiComponent.h" #include "Window.h" +#include + std::vector SystemData::sSystemVector; +#define USE_THREADING + SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) { + mGridSizeOverride = Vector2f(0, 0); mViewModeChanged = false; mFilterIndex = new FileFilterIndex(); @@ -57,19 +64,26 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys loadTheme(); } -bool SystemData::setSystemViewMode(std::string newViewMode) +bool SystemData::setSystemViewMode(std::string newViewMode, Vector2f gridSizeOverride, bool setChanged) { if (newViewMode == "automatic") newViewMode = ""; - if (mViewMode == newViewMode) + if (mViewMode == newViewMode && gridSizeOverride == mGridSizeOverride) return false; + mGridSizeOverride = gridSizeOverride; mViewMode = newViewMode; - mViewModeChanged = true; + mViewModeChanged = setChanged; + return true; } +Vector2f SystemData::getGridSizeOverride() +{ + return mGridSizeOverride; +} + SystemData::~SystemData() { delete mRootFolder; @@ -196,6 +210,234 @@ std::vector readList(const std::string& str, const char* delims = " return ret; } +SystemData* SystemData::loadSystem(pugi::xml_node system) +{ + std::vector emulatorList; + + std::string name, fullname, path, cmd, themeFolder, defaultCore; + + name = system.child("name").text().get(); + fullname = system.child("fullname").text().get(); + path = system.child("path").text().get(); + defaultCore = system.child("defaultCore").text().get(); + + pugi::xml_node emulators = system.child("emulators"); + if (emulators != NULL) + { + for (pugi::xml_node emulator : emulators.children()) + { + EmulatorData emulatorData; + emulatorData.mName = emulator.attribute("name").value(); + emulatorData.mCommandLine = emulator.attribute("command").value(); + + pugi::xml_node cores = emulator.child("cores"); + if (cores != NULL) + { + for (pugi::xml_node core : cores.children()) + { + const std::string& corename = core.text().get(); + + if (defaultCore.length() == 0) + defaultCore = corename; + + emulatorData.mCores.push_back(corename); + } + } + + emulatorList.push_back(emulatorData); + } + } + /* + if (window != NULL) + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; + */ + // convert extensions list from a string into a vector of strings + + std::vector list = readList(system.child("extension").text().get()); + std::vector extensions; + + for (auto extension = list.cbegin(); extension != list.cend(); extension++) + { + std::string xt = (*extension); + std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); + + if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) + extensions.push_back(xt); + } + + cmd = system.child("command").text().get(); + + // platform id list + const char* platformList = system.child("platform").text().get(); + std::vector platformStrs = readList(platformList); + std::vector platformIds; + for (auto it = platformStrs.cbegin(); it != platformStrs.cend(); 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!"; + return nullptr; + } + + //convert path to generic directory seperators + path = Utils::FileSystem::getGenericPath(path); + + //expand home symbol if the startpath contains ~ + if (path[0] == '~') + { + path.erase(0, 1); + path.insert(0, Utils::FileSystem::getHomePath()); + } + + //create the system runtime environment data + SystemEnvironmentData* envData = new SystemEnvironmentData; + envData->mStartPath = path; + envData->mSearchExtensions = extensions; + envData->mLaunchCommand = cmd; + envData->mPlatformIds = platformIds; + envData->mEmulators = emulatorList; + + SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); + if (newSys->getRootFolder()->getChildrenByFilename().size() == 0) + { + LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; + delete newSys; + + return nullptr; + } + + return newSys; +} + +#ifdef USE_THREADING +typedef std::function work_function; + +#include +#include +#include +#include + +class ThreadPool +{ +public: + ThreadPool() : mRunning(true), mWaiting(false), mNumWork(0) + { +#ifdef WIN32 + SYSTEM_INFO sysinfo; + GetSystemInfo(&sysinfo); + size_t num_threads = sysinfo.dwNumberOfProcessors; +#else + size_t num_threads = sysconf(_SC_NPROCESSORS_ONLN); +#endif + + auto doWork = [&](size_t id) + { + while (mRunning) + { + _mutex.lock(); + if (!mWorkQueue.empty()) + { + auto work = mWorkQueue.front(); + mWorkQueue.pop(); + _mutex.unlock(); + + try + { + work(); + } + catch (...) { } + + mNumWork--; + } + else + { + _mutex.unlock(); + + // Extra code : Exit finished threads + if (mWaiting) + return; + + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + }; + + mThreads.reserve(num_threads); + + for (size_t i = 0; i < num_threads; i++) + mThreads.push_back(std::thread(doWork, i)); + } + + ~ThreadPool() + { + mRunning = false; + for (std::thread& t : mThreads) + t.join(); + } + + void queueWorkItem(work_function work) + { + _mutex.lock(); + mWorkQueue.push(work); + mNumWork++; + _mutex.unlock(); + } + + void wait() + { + mWaiting = true; + while (mNumWork.load() > 0) + std::this_thread::yield(); + } + + void wait(work_function work, int delay = 50) + { + mWaiting = true; + while (mNumWork.load() > 0) + { + work(); + + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(delay)); + } + } + +private: + bool mRunning; + bool mWaiting; + std::queue mWorkQueue; + std::atomic mNumWork; + std::mutex _mutex; + std::vector mThreads; + +}; +#endif + //creates systems from information located in a config file bool SystemData::loadConfig(Window* window) { @@ -231,138 +473,97 @@ bool SystemData::loadConfig(Window* window) return false; } - float systemCount = 1; + std::vector systemsNames; + + int systemCount = 0; for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { + systemsNames.push_back(system.child("fullname").text().get()); systemCount++; + } - float currentSystem = 0; - for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - { - std::vector emulatorList; - - std::string name, fullname, path, cmd, themeFolder, defaultCore; - - name = system.child("name").text().get(); - fullname = system.child("fullname").text().get(); - path = system.child("path").text().get(); - defaultCore = system.child("defaultCore").text().get(); - - pugi::xml_node emulators = system.child("emulators"); - if (emulators != NULL) - { - for (pugi::xml_node emulator : emulators.children()) - { - EmulatorData emulatorData; - emulatorData.mName = emulator.attribute("name").value(); - emulatorData.mCommandLine = emulator.attribute("command").value(); - - pugi::xml_node cores = emulator.child("cores"); - if (cores != NULL) - { - for (pugi::xml_node core : cores.children()) - { - const std::string& corename = core.text().get(); - - if (defaultCore.length() == 0) - defaultCore = corename; - - emulatorData.mCores.push_back(corename); - } - } - - emulatorList.push_back(emulatorData); - } - } - - if (window != NULL) - window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); + int currentSystem = 0; - currentSystem++; +#ifdef WIN32 + unsigned int Ticks = GetTickCount(); +#endif - // convert extensions list from a string into a vector of strings +#ifdef USE_THREADING + ThreadPool threadPool; - std::vector list = readList(system.child("extension").text().get()); - std::vector extensions; + typedef SystemData* SystemDataPtr; + + SystemDataPtr* systems = new SystemDataPtr[systemCount]; + for (int i = 0; i < systemCount; i++) + systems[i] = nullptr; - for (auto extension = list.cbegin(); extension != list.cend(); extension++) - { - std::string xt = (*extension); - std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); - - if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) - extensions.push_back(xt); - } + threadPool.queueWorkItem([] { CollectionSystemManager::get()->loadCollectionSystems(true); }); - cmd = system.child("command").text().get(); +#endif - // platform id list - const char* platformList = system.child("platform").text().get(); - std::vector platformStrs = readList(platformList); - std::vector platformIds; - for(auto it = platformStrs.cbegin(); it != platformStrs.cend(); it++) + int processedSystem = 0; + + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { +#ifdef USE_THREADING + threadPool.queueWorkItem([system, currentSystem, systems, &processedSystem] { - 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; - } + systems[currentSystem] = loadSystem(system); + processedSystem++; + }); +#else + std::string fullname = system.child("fullname").text().get(); - // 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()); + if (window != NULL) + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : (float) currentSystem / (float) (systemCount + 1)); + + SystemData* pSystem = loadSystem(system); + if (pSystem != nullptr) + sSystemVector.push_back(pSystem); + +#endif + currentSystem++; + } - //validate - if(name.empty() || path.empty() || extensions.empty() || cmd.empty()) +#ifdef USE_THREADING + if (window != NULL) + { + threadPool.wait([window, &processedSystem, systemCount, systemsNames] { - LOG(LogError) << "System \"" << name << "\" is missing name, path, extension, or command!"; - continue; - } + int px = processedSystem; + auto name = px < 0 || px > systemsNames.size() ? "" : systemsNames.at(px); + window->renderLoadingScreen(name, (float)px / (float)(systemCount + 1)); + }, 50); + } + else + threadPool.wait(); - //convert path to generic directory seperators - path = Utils::FileSystem::getGenericPath(path); + for (int i = 0; i < systemCount; i++) + { + SystemData* pSystem = systems[i]; + if (pSystem != nullptr) + sSystemVector.push_back(pSystem); + } - //expand home symbol if the startpath contains ~ - if (path[0] == '~') - { - path.erase(0, 1); - path.insert(0, Utils::FileSystem::getHomePath()); - } + delete[] systems; - //create the system runtime environment data - SystemEnvironmentData* envData = new SystemEnvironmentData; - envData->mStartPath = path; - envData->mSearchExtensions = extensions; - envData->mLaunchCommand = cmd; - envData->mPlatformIds = platformIds; - envData->mEmulators = emulatorList; + if (window != NULL) + window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); - SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); - if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) - { - LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; - delete newSys; - } - else - sSystemVector.push_back(newSys); - } + CollectionSystemManager::get()->updateSystemsList(); +#else if (window != NULL) window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); - currentSystem++; - CollectionSystemManager::get()->loadCollectionSystems(); +#endif + +#ifdef WIN32 + Ticks = GetTickCount() - Ticks; + // ::MessageBox(0, std::to_string(Ticks).c_str(), NULL, NULL); +#endif + return true; } diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 59207eeede..d19aea5806 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -8,6 +8,9 @@ #include #include +#include +#include "math/Vector2f.h" + class FileData; class FileFilterIndex; class ThemeData; @@ -92,8 +95,10 @@ class SystemData inline const std::shared_ptr& getTheme() const { return mTheme; } - std::string getSystemViewMode() const { if (mViewMode == "automatic") return ""; else return mViewMode; }; - bool setSystemViewMode(std::string newViewMode); + std::string getSystemViewMode() const { if (mViewMode == "automatic") return ""; else return mViewMode; }; + bool setSystemViewMode(std::string newViewMode, Vector2f gridSizeOverride = Vector2f(0,0), bool setChanged = true); + + Vector2f getGridSizeOverride(); std::string getGamelistPath(bool forWrite) const; bool hasGamelist() const; @@ -127,6 +132,8 @@ class SystemData FileFilterIndex* getIndex() { return mFilterIndex; }; private: + static SystemData* loadSystem(pugi::xml_node system); + bool mIsCollectionSystem; bool mIsGameSystem; std::string mName; @@ -136,6 +143,7 @@ class SystemData std::shared_ptr mTheme; std::string mViewMode; + Vector2f mGridSizeOverride; bool mViewModeChanged; void populateFolder(FileData* folder); diff --git a/es-app/src/components/ScraperSearchComponent.cpp b/es-app/src/components/ScraperSearchComponent.cpp index 87aacba10e..788620b6db 100644 --- a/es-app/src/components/ScraperSearchComponent.cpp +++ b/es-app/src/components/ScraperSearchComponent.cpp @@ -8,6 +8,7 @@ #include "components/TextComponent.h" #include "guis/GuiMsgBox.h" #include "guis/GuiTextEditPopup.h" +#include "guis/GuiTextEditPopupKeyboard.h" #include "resources/Font.h" #include "utils/StringUtil.h" #include "FileData.h" @@ -235,12 +236,12 @@ void ScraperSearchComponent::onSearchDone(const std::vector if (!isValidConfiguredScraper()) { mWindow->pushGui(new GuiMsgBox(mWindow, Utils::String::toUpper("Configured scraper is no longer available.\nPlease change the scraping source in the settings."), - "FINISH", mSkipCallback)); + _T("FINISH"), mSkipCallback)); } else { ComponentListRow row; - row.addElement(std::make_shared(mWindow, "NO GAMES FOUND - SKIP", font, color), true); + row.addElement(std::make_shared(mWindow, _T("NO GAMES FOUND - SKIP"), font, color), true); if(mSkipCallback) row.makeAcceptInputHandler(mSkipCallback); @@ -455,17 +456,23 @@ void ScraperSearchComponent::openInputScreen(ScraperSearchParams& params) }; stop(); - mWindow->pushGui(new GuiTextEditPopup(mWindow, "SEARCH FOR", + + mWindow->pushGui(new GuiTextEditPopupKeyboard(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, + params.nameOverride.empty() ? params.game->getCleanName() : params.nameOverride, searchForFunc, false, "SEARCH")); + /* + mWindow->pushGui(new GuiTextEditPopup(mWindow, _T("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, _T("SEARCH")));*/ } std::vector ScraperSearchComponent::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); if(getSelectedIndex() != -1) - prompts.push_back(HelpPrompt("a", "accept result")); + prompts.push_back(HelpPrompt("a", _T("accept result"))); return prompts; } diff --git a/es-app/src/components/TextListComponent.h b/es-app/src/components/TextListComponent.h index c90329883b..eace2d6892 100644 --- a/es-app/src/components/TextListComponent.h +++ b/es-app/src/components/TextListComponent.h @@ -142,7 +142,7 @@ void TextListComponent::render(const Transform4x4f& parentTrans) int startEntry = 0; //number of entries that can fit on the screen simultaniously - int screenCount = (int)(mSize.y() / entrySize + 0.5f); + int screenCount = Math::round(mSize.y() / entrySize); // (int)(mSize.y() / entrySize); // + 0.5f -> avoid partial items if(size() >= screenCount) { diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index c47c7e7d5e..7d5ea965d8 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -77,6 +77,10 @@ void GuiCollectionSystemsOptions::initializeMenu() sortAllSystemsSwitch->setState(Settings::getInstance()->getBool("SortAllSystems")); mMenu.addWithLabel(_T("SORT CUSTOM COLLECTIONS AND SYSTEMS"), sortAllSystemsSwitch); + favoritesFirstSwitch = std::make_shared(mWindow); + favoritesFirstSwitch->setState(Settings::getInstance()->getBool("FavoritesFirst")); + mMenu.addWithLabel(_T("DISPLAY FAVORITES FIRST IN GAMELIST"), favoritesFirstSwitch); + if(CollectionSystemManager::get()->isEditing()) { row.elements.clear(); @@ -170,11 +174,16 @@ void GuiCollectionSystemsOptions::applySettings() std::string prevAuto = Settings::getInstance()->getString("CollectionSystemsAuto"); std::string outCustom = Utils::String::vectorToCommaString(customOptionList->getSelectedObjects()); std::string prevCustom = Settings::getInstance()->getString("CollectionSystemsCustom"); + bool outSort = sortAllSystemsSwitch->getState(); bool prevSort = Settings::getInstance()->getBool("SortAllSystems"); bool outBundle = bundleCustomCollections->getState(); bool prevBundle = Settings::getInstance()->getBool("UseCustomCollectionsSystem"); - bool needUpdateSettings = prevAuto != outAuto || prevCustom != outCustom || outSort != prevSort || outBundle != prevBundle; + + bool outFavoritesFirst = favoritesFirstSwitch->getState(); + bool prevFavoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); + + bool needUpdateSettings = prevAuto != outAuto || prevCustom != outCustom || outSort != prevSort || outBundle != prevBundle || outFavoritesFirst != prevFavoritesFirst; if (needUpdateSettings) { updateSettings(outAuto, outCustom); @@ -188,6 +197,8 @@ void GuiCollectionSystemsOptions::updateSettings(std::string newAutoSettings, st Settings::getInstance()->setString("CollectionSystemsCustom", newCustomSettings); Settings::getInstance()->setBool("SortAllSystems", sortAllSystemsSwitch->getState()); Settings::getInstance()->setBool("UseCustomCollectionsSystem", bundleCustomCollections->getState()); + Settings::getInstance()->setBool("FavoritesFirst", favoritesFirstSwitch->getState()); + Settings::getInstance()->saveFile(); CollectionSystemManager::get()->loadEnabledListFromSettings(); CollectionSystemManager::get()->updateSystemsList(); diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.h b/es-app/src/guis/GuiCollectionSystemsOptions.h index 0187920630..b54a8e92e7 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.h +++ b/es-app/src/guis/GuiCollectionSystemsOptions.h @@ -26,8 +26,11 @@ class GuiCollectionSystemsOptions : public GuiComponent void updateSettings(std::string newAutoSettings, std::string newCustomSettings); void createCollection(std::string inName); void exitEditMode(); + std::shared_ptr< OptionListComponent > autoOptionList; std::shared_ptr< OptionListComponent > customOptionList; + + std::shared_ptr favoritesFirstSwitch; std::shared_ptr sortAllSystemsSwitch; std::shared_ptr bundleCustomCollections; MenuComponent mMenu; diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 44ec65f9b4..2e4507a02b 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -13,9 +13,61 @@ #include "animations/LambdaAnimation.h" -GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : GuiComponent(window), +std::vector gridSizes { + "automatic", + + "2x2", + "2x3", + "2x4", + "2x5", + "2x6", + "2x7", + + "3x1", + "3x2", + "3x3", + "3x4", + "3x5", + "3x6", + "3x7", + + "4x1", + "4x2", + "4x3", + "4x4", + "4x5", + "4x6", + "4x7", + + "5x1", + "5x2", + "5x3", + "5x4", + "5x5", + "5x6", + "5x7", + + "6x1", + "6x2", + "6x3", + "6x4", + "6x5", + "6x6", + "6x7", + + "7x1", + "7x2", + "7x3", + "7x4", + "7x5", + "7x6", + "7x7" +}; + +GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool showGridFeatures) : GuiComponent(window), mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false) -{ +{ + mGridSize = NULL; addChild(&mMenu); // check it's not a placeholder folder - if it is, only show "Filter Options" @@ -106,6 +158,32 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui mMenu.addWithLabel(_T("GAMELIST VIEW STYLE"), mViewMode); + + + // Grid size override + if (showGridFeatures) + { + auto gridOverride = system->getGridSizeOverride(); + auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); + + mGridSize = std::make_shared>(mWindow, _T("GRID SIZE"), false); + + found = false; + for (auto it = gridSizes.cbegin(); it != gridSizes.cend(); it++) + { + bool sel = (gridOverride == Vector2f(0, 0) && *it == "automatic") || ovv == *it; + if (sel) + found = true; + + mGridSize->add(_L(*it), *it, sel); + } + + if (!found) + mGridSize->selectFirstItem(); + + mMenu.addWithLabel(_T("GRID SIZE"), mGridSize); + } + // show filtered menu if(!Settings::getInstance()->getBool("ForceDisableFilters")) { @@ -116,6 +194,31 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui mMenu.addRow(row); } + + + /* + // maximum vram + auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); + max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); + s->addWithLabel(_T("VRAM LIMIT"), max_vram); + s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); + */ + + + + + + + + + + + + + + + + std::map customCollections = CollectionSystemManager::get()->getCustomCollectionSystems(); if(UIModeController::getInstance()->isUIModeFull() && @@ -188,7 +291,23 @@ GuiGamelistOptions::~GuiGamelistOptions() getGamelist()->onFileChanged(root, FILE_SORTED); } - if (mSystem->setSystemViewMode(mViewMode->getSelected()) || mFiltersChanged) + Vector2f gridSizeOverride(0, 0); + + if (mGridSize != NULL) + { + auto str = mGridSize->getSelected(); + + size_t divider = str.find('x'); + if (divider != std::string::npos) + { + std::string first = str.substr(0, divider); + std::string second = str.substr(divider+1, std::string::npos); + + gridSizeOverride = Vector2f((float)atof(first.c_str()), (float)atof(second.c_str())); + } + } + + if (mSystem->setSystemViewMode(mViewMode->getSelected(), gridSizeOverride) || mFiltersChanged) { // only reload full view if we came from a placeholder // as we need to re-display the remaining elements for whatever new diff --git a/es-app/src/guis/GuiGamelistOptions.h b/es-app/src/guis/GuiGamelistOptions.h index d6bfaf781c..32dbabfce5 100644 --- a/es-app/src/guis/GuiGamelistOptions.h +++ b/es-app/src/guis/GuiGamelistOptions.h @@ -4,6 +4,7 @@ #include "components/MenuComponent.h" #include "components/OptionListComponent.h" +#include "components/SliderComponent.h" #include "FileData.h" #include "GuiComponent.h" @@ -13,7 +14,7 @@ class SystemData; class GuiGamelistOptions : public GuiComponent { public: - GuiGamelistOptions(Window* window, SystemData* system); + GuiGamelistOptions(Window* window, SystemData* system, bool showGridFeatures = false); virtual ~GuiGamelistOptions(); virtual bool input(InputConfig* config, Input input) override; @@ -36,7 +37,8 @@ class GuiGamelistOptions : public GuiComponent std::shared_ptr mListSort; std::shared_ptr> mViewMode; - + + std::shared_ptr> mGridSize; SystemData* mSystem; IGameListView* getGamelist(); diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index ed37b73f28..298336617d 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -19,6 +19,7 @@ #include #include +#include "resources/TextureData.h" #include "animations/LambdaAnimation.h" GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) @@ -485,13 +486,17 @@ void GuiMenu::openUISettings() styles.push_back("grid"); } + auto viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + if (!system->getTheme()->hasView(viewPreference)) + viewPreference == "automatic"; + for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); + gamelist_style->add(_L(*it), *it, viewPreference == *it); s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); - s->addSaveFunc([gamelist_style] { + s->addSaveFunc([gamelist_style, viewPreference] { bool needReload = false; - if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) + if (viewPreference != gamelist_style->getSelected()) needReload = true; Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); if (needReload) @@ -588,6 +593,7 @@ void GuiMenu::openUISettings() PowerSaver::init(); } Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); + GuiComponent::ALLOWANIMATIONS = Settings::getInstance()->getString("TransitionStyle") != "instant"; }); @@ -727,6 +733,9 @@ void GuiMenu::openOtherSettings() Settings::getInstance()->setBool("MoveCarousel", false); Settings::getInstance()->setBool("EnableSounds", false); } + + GuiComponent::ALLOWANIMATIONS = Settings::getInstance()->getString("TransitionStyle") != "instant"; + Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); PowerSaver::init(); }); @@ -780,6 +789,16 @@ void GuiMenu::openOtherSettings() preloadUI->setState(Settings::getInstance()->getBool("PreloadUI")); s->addWithLabel(_T("PRELOAD UI"), preloadUI); s->addSaveFunc([preloadUI] { Settings::getInstance()->setBool("PreloadUI", preloadUI->getState()); }); + + // optimizeVram + auto optimizeVram = std::make_shared(mWindow); + optimizeVram->setState(Settings::getInstance()->getBool("OptimizeVRAM")); + s->addWithLabel(_T("OPTIMIZE IMAGES VRAM USE"), optimizeVram); + s->addSaveFunc([optimizeVram] + { + TextureData::OPTIMIZEVRAM = optimizeVram->getState(); + Settings::getInstance()->setBool("OptimizeVRAM", optimizeVram->getState()); + }); #ifdef WIN32 // vsync diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 285e5f0d92..6979133cf9 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -18,6 +18,7 @@ #include "FileFilterIndex.h" #include "SystemData.h" #include "Window.h" +#include "guis/GuiTextEditPopupKeyboard.h" GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : GuiComponent(window), @@ -216,8 +217,10 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector 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)); + row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] + { + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, title, ed->getValue(), updateVal, multiLine)); + // mWindow->pushGui(new GuiTextEditPopup(mWindow, title, ed->getValue(), updateVal, multiLine)); }); break; } diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index cd8edc9cf0..db6980ae6e 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -25,6 +25,7 @@ #include #endif +#include "resources/TextureData.h" #include bool scrape_cmdline = false; @@ -333,6 +334,9 @@ int main(int argc, char* argv[]) MameNames::init(); window.pushGui(ViewController::get()); + TextureData::OPTIMIZEVRAM = Settings::getInstance()->getBool("OptimizeVRAM"); + GuiComponent::ALLOWANIMATIONS = Settings::getInstance()->getString("TransitionStyle") != "instant"; + bool splashScreen = Settings::getInstance()->getBool("SplashScreen"); bool splashScreenProgress = Settings::getInstance()->getBool("SplashScreenProgress"); diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 805b4f8cd2..0da080ad81 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -30,92 +30,98 @@ void SystemView::populate() { mEntries.clear(); - for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { const std::shared_ptr& theme = (*it)->getTheme(); - if(mViewNeedsReload) + if (mViewNeedsReload) getViewElements(theme); - if((*it)->isVisible()) - { - Entry e; - e.name = (*it)->getName(); - e.object = *it; + if (!(*it)->isVisible()) + continue; - // make logo - const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); - if(logoElem) - { - std::string path = logoElem->get("path"); - std::string defaultPath = logoElem->has("default") ? logoElem->get("default") : ""; - if((!path.empty() && ResourceManager::getInstance()->fileExists(path)) - || (!defaultPath.empty() && ResourceManager::getInstance()->fileExists(defaultPath))) - { - ImageComponent* logo = new ImageComponent(mWindow, false, false); - logo->setMaxSize(mCarousel.logoSize * mCarousel.logoScale); - logo->applyTheme(theme, "system", "logo", ThemeFlags::PATH | ThemeFlags::COLOR); - logo->setRotateByTargetSize(true); - e.data.logo = std::shared_ptr(logo); - } - } - if (!e.data.logo) - { - // no logo in theme; use text - TextComponent* text = new TextComponent(mWindow, - (*it)->getName(), - Font::get(FONT_SIZE_LARGE), - 0x000000FF, - ALIGN_CENTER); - text->setSize(mCarousel.logoSize * mCarousel.logoScale); - text->applyTheme((*it)->getTheme(), "system", "logoText", ThemeFlags::FONT_PATH | ThemeFlags::FONT_SIZE | ThemeFlags::COLOR | ThemeFlags::FORCE_UPPERCASE | ThemeFlags::LINE_SPACING | ThemeFlags::TEXT); - e.data.logo = std::shared_ptr(text); - - if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) - { - text->setHorizontalAlignment(mCarousel.logoAlignment); - text->setVerticalAlignment(ALIGN_CENTER); - } else { - text->setHorizontalAlignment(ALIGN_CENTER); - text->setVerticalAlignment(mCarousel.logoAlignment); - } + Entry e; + e.name = (*it)->getName(); + e.object = *it; + + // make logo + const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); + if (logoElem) + { + std::string path = logoElem->get("path"); + std::string defaultPath = logoElem->has("default") ? logoElem->get("default") : ""; + + if ((!path.empty() && ResourceManager::getInstance()->fileExists(path)) + || (!defaultPath.empty() && ResourceManager::getInstance()->fileExists(defaultPath))) + { + ImageComponent* logo = new ImageComponent(mWindow, false, false); + logo->setMaxSize(mCarousel.logoSize * mCarousel.logoScale); + logo->applyTheme(theme, "system", "logo", ThemeFlags::PATH | ThemeFlags::SIZE | ThemeFlags::COLOR); + logo->setRotateByTargetSize(true); + e.data.logo = std::shared_ptr(logo); } + } + + if (!e.data.logo) + { + // no logo in theme; use text + TextComponent* text = new TextComponent(mWindow, + (*it)->getName(), + Font::get(FONT_SIZE_LARGE), + 0x000000FF, + ALIGN_CENTER); + text->setSize(mCarousel.logoSize * mCarousel.logoScale); + text->applyTheme((*it)->getTheme(), "system", "logoText", ThemeFlags::FONT_PATH | ThemeFlags::FONT_SIZE | ThemeFlags::COLOR | ThemeFlags::FORCE_UPPERCASE | ThemeFlags::LINE_SPACING | ThemeFlags::TEXT); + e.data.logo = std::shared_ptr(text); if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) { - if (mCarousel.logoAlignment == ALIGN_LEFT) - e.data.logo->setOrigin(0, 0.5); - else if (mCarousel.logoAlignment == ALIGN_RIGHT) - e.data.logo->setOrigin(1.0, 0.5); - else - e.data.logo->setOrigin(0.5, 0.5); - } else { - if (mCarousel.logoAlignment == ALIGN_TOP) - e.data.logo->setOrigin(0.5, 0); - else if (mCarousel.logoAlignment == ALIGN_BOTTOM) - e.data.logo->setOrigin(0.5, 1); - else - e.data.logo->setOrigin(0.5, 0.5); + text->setHorizontalAlignment(mCarousel.logoAlignment); + text->setVerticalAlignment(ALIGN_CENTER); } - - Vector2f denormalized = mCarousel.logoSize * e.data.logo->getOrigin(); - e.data.logo->setPosition(denormalized.x(), denormalized.y(), 0.0); - // delete any existing extras - for (auto extra : e.data.backgroundExtras) - delete extra; - e.data.backgroundExtras.clear(); - - // make background extras - e.data.backgroundExtras = ThemeData::makeExtras((*it)->getTheme(), "system", mWindow); - - // sort the extras by z-index - std::stable_sort(e.data.backgroundExtras.begin(), e.data.backgroundExtras.end(), [](GuiComponent* a, GuiComponent* b) { - return b->getZIndex() > a->getZIndex(); - }); - - this->add(e); + else { + text->setHorizontalAlignment(ALIGN_CENTER); + text->setVerticalAlignment(mCarousel.logoAlignment); + } + } + + if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) + { + if (mCarousel.logoAlignment == ALIGN_LEFT) + e.data.logo->setOrigin(0, 0.5); + else if (mCarousel.logoAlignment == ALIGN_RIGHT) + e.data.logo->setOrigin(1.0, 0.5); + else + e.data.logo->setOrigin(0.5, 0.5); } + else { + if (mCarousel.logoAlignment == ALIGN_TOP) + e.data.logo->setOrigin(0.5, 0); + else if (mCarousel.logoAlignment == ALIGN_BOTTOM) + e.data.logo->setOrigin(0.5, 1); + else + e.data.logo->setOrigin(0.5, 0.5); + } + + + Vector2f denormalized = mCarousel.logoSize * e.data.logo->getOrigin(); + e.data.logo->setPosition(denormalized.x(), denormalized.y(), 0.0); + // delete any existing extras + for (auto extra : e.data.backgroundExtras) + delete extra; + e.data.backgroundExtras.clear(); + + // make background extras + e.data.backgroundExtras = ThemeData::makeExtras((*it)->getTheme(), "system", mWindow); + + // sort the extras by z-index + std::stable_sort(e.data.backgroundExtras.begin(), e.data.backgroundExtras.end(), [](GuiComponent* a, GuiComponent* b) { + return b->getZIndex() > a->getZIndex(); + }); + + this->add(e); } + if (mEntries.size() == 0) { // Something is wrong, there is not a single system to show, check if UI mode is not full diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 9f8de5b5bf..95a99f41d8 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -147,7 +147,7 @@ void ViewController::goToGameList(SystemData* system) mCurrentView = getGameListView(system); if (mCurrentView) - mCurrentView->onShow(); + mCurrentView->onShow(); playViewTransition(false); } @@ -302,11 +302,16 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste bool forceView = false; std::string viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + if (!system->getTheme()->hasView(viewPreference)) + viewPreference == "automatic"; + std::string customThemeName; + Vector2f gridSizeOverride = Vector2f(0,0); if (!system->getSystemViewMode().empty() && system->getTheme()->hasView(system->getSystemViewMode())) { viewPreference = system->getSystemViewMode(); + gridSizeOverride = system->getGridSizeOverride(); forceView = true; } @@ -385,7 +390,10 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste view = std::shared_ptr(new DetailedGameListView(mWindow, system->getRootFolder())); break; case GRID: - view = std::shared_ptr(new GridGameListView(mWindow, system->getRootFolder())); + { + view = std::shared_ptr(new GridGameListView(mWindow, system->getRootFolder(), system->getTheme(), customThemeName, gridSizeOverride)); + } + break; case BASIC: default: @@ -393,10 +401,15 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste break; } - if (!customThemeName.empty()) - view->setThemeName(customThemeName); + if (selectedViewType != GRID) + { + // GridGameListView theme needs to be loaded before populating. - view->setTheme(system->getTheme()); + if (!customThemeName.empty()) + view->setThemeName(customThemeName); + + view->setTheme(system->getTheme()); + } std::vector& sysVec = SystemData::sSystemVector; int id = (int)(std::find(sysVec.cbegin(), sysVec.cend(), system) - sysVec.cbegin()); @@ -507,13 +520,22 @@ void ViewController::render(const Transform4x4f& parentTrans) void ViewController::preload() { - uint32_t i = 0; + int i = 1; + int max = SystemData::sSystemVector.size() + 1; + + bool splash = Settings::getInstance()->getBool("SplashScreen") && Settings::getInstance()->getBool("SplashScreenProgress"); + if (splash) + mWindow->renderLoadingScreen(_T("Preloading UI"), (float)i / (float)max); + + // First load the system list + getSystemListView(); + for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { - if (Settings::getInstance()->getBool("SplashScreen") && Settings::getInstance()->getBool("SplashScreenProgress")) + if (splash) { i++; - mWindow->renderLoadingScreen(_T("Preloading UI"), (float) i / (float) SystemData::sSystemVector.size()); + mWindow->renderLoadingScreen(_T("Preloading UI"), (float) i / (float)max); } (*it)->getIndex()->resetFilters(); diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 51e51c598a..39eb026d60 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -53,35 +53,48 @@ void BasicGameListView::populateList(const std::vector& files) mHeaderText.setText(systemName); bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + bool favoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); + bool showFavoriteIcon = (systemName != "favorites"); + if (!showFavoriteIcon) + favoritesFirst = false; if (files.size() > 0) { - for (auto it = files.cbegin(); it != files.cend(); it++) + if (favoritesFirst) { - if ((*it)->getFavorite()) - if (showHiddenFiles || !(*it)->getHidden()) - { - if (systemName == "favorites") - mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); - else - - mList.add(_T("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); - } + for (auto it = files.cbegin(); it != files.cend(); it++) + { + if (!showHiddenFiles && (*it)->getHidden()) + continue; + + if (!(*it)->getFavorite()) + continue; + + if (showFavoriteIcon) + mList.add(_T("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + else + mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + } } - /* + for (auto it = files.cbegin(); it != files.cend(); it++) { - if (!(*it)->getFavorite() && ((*it)->getType() == FOLDER)) - if (showHiddenFiles || !(*it)->getHidden()) - mList.add("[Folder] "+(*it)->getName(), *it, ((*it)->getType() == FOLDER)); + if (!showHiddenFiles && (*it)->getHidden()) + continue; - }*/ + if ((*it)->getFavorite()) + { + if (favoritesFirst) + continue; - for (auto it = files.cbegin(); it != files.cend(); it++) - { - if (!(*it)->getFavorite()) - if (showHiddenFiles || !(*it)->getHidden()) - mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + if (showFavoriteIcon) + { + mList.add(_T("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + continue; + } + } + + mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); } } else diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 67e22bf3eb..6bb0311fc9 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -15,9 +15,7 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : BasicGameListView(window, root), mDescContainer(window), mDescription(window), - mImage(window), - - mVideo(nullptr), + mImage(window), mMarquee(window), mVideo(nullptr), mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -26,10 +24,6 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mGenre(window), mPlayers(window), mLastPlayed(window), mPlayCount(window), mName(window) { - //mHeaderImage.setPosition(mSize.x() * 0.25f, 0); - - mVideoVisible = false; - const float padding = 0.01f; mList.setPosition(mSize.x() * (0.50f + padding), mList.getPosition().y()); @@ -37,15 +31,14 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mList.setAlignment(TextListComponent::ALIGN_LEFT); mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); - // Create the correct type of video window -#ifdef _RPI_ - if (Settings::getInstance()->getBool("VideoOmxPlayer")) - mVideo = new VideoPlayerComponent(window, ""); - else -#endif - mVideo = new VideoVlcComponent(window, getTitlePath()); + // Marquee + mMarquee.setOrigin(0.5f, 0.5f); + mMarquee.setPosition(mSize.x() * 0.25f, mSize.y() * 0.10f); + mMarquee.setMaxSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.18f); + mMarquee.setDefaultZIndex(35); + // addChild(&mMarquee); - // image + // 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); @@ -53,11 +46,20 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : addChild(&mImage); // video + // Create the correct type of video window +#ifdef _RPI_ + if (Settings::getInstance()->getBool("VideoOmxPlayer")) + mVideo = new VideoPlayerComponent(window, ""); + else +#endif + mVideo = new VideoVlcComponent(window, getTitlePath()); + mVideo->setOrigin(0.5f, 0.5f); mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); mVideo->setStartDelay(2000); - mVideo->setDefaultZIndex(30); + mVideo->setDefaultZIndex(31); +// addChild(mVideo); //addChild(mVideo); -> Add only if present in theme later @@ -121,20 +123,27 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them BasicGameListView::onThemeChanged(theme); using namespace ThemeFlags; - mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + + mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); mName.applyTheme(theme, getName(), "md_name", ALL); if (theme->getElement(getName(), "md_video", "video")) { - mVideoVisible = true; mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION); - addChild(mVideo); + if (!isChild(mVideo)) + addChild(mVideo); } - else - { - mVideoVisible = false; + else if (isChild(mVideo)) removeChild(mVideo); + + if (theme->getElement(getName(), "md_marquee", "image")) + { + mMarquee.applyTheme(theme, getName(), "md_marquee", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + if (!isChild(&mMarquee)) + addChild(&mMarquee); } + else if (isChild(&mMarquee)) + removeChild(&mMarquee); initMDLabels(); std::vector labels = getMDLabels(); @@ -168,6 +177,7 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION)); sortChildren(); + updateInfoPanel(); } void DetailedGameListView::initMDLabels() @@ -250,35 +260,30 @@ void DetailedGameListView::updateInfoPanel() bool fadingOut; if (file == NULL) - { + { mVideo->setVideo(""); - mVideo->setImage(""); - + //mVideo->setImage(""); //mImage.setImage(""); //mDescription.setText(""); fadingOut = true; - }else{ + } + else + { + std::string imagePath = file->getImagePath().empty() ? file->getThumbnailPath() : file->getImagePath(); - if (mVideoVisible) + if (isChild(mVideo)) { if (!mVideo->setVideo(file->getVideoPath())) - mVideo->setDefaultVideo(); - } + mVideo->setDefaultVideo(); - if (file->getImagePath().empty()) - { - if (mVideoVisible) - mVideo->setImage(file->getThumbnailPath(), false, mImage.getSize()); - - mImage.setImage(file->getThumbnailPath(), false, mImage.getSize()); + mVideo->setImage(imagePath, false, mVideo->getSize()); } - else - { - if (mVideoVisible) - mVideo->setImage(file->getImagePath(), false, mImage.getSize()); - mImage.setImage(file->getImagePath(), false, mImage.getSize()); - } + if (isChild(&mImage)) + mImage.setImage(imagePath, false, mImage.getSize()); + + if (isChild(&mMarquee)) + mMarquee.setImage(file->getMarqueePath()); mDescription.setText(getMetadata(file, "desc")); mDescContainer.reset(); @@ -301,7 +306,13 @@ void DetailedGameListView::updateInfoPanel() } std::vector comps = getMDValues(); - comps.push_back(&mImage); + + if (isChild(&mMarquee)) + comps.push_back(&mMarquee); + + if (isChild(&mImage)) + comps.push_back(&mImage); + comps.push_back(mVideo); comps.push_back(&mDescription); comps.push_back(&mName); @@ -330,7 +341,10 @@ void DetailedGameListView::updateInfoPanel() void DetailedGameListView::launch(FileData* game) { Vector3f target(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0); - if(mImage.hasImage()) + + if (isChild(mVideo) && !isChild(&mImage)) + target = Vector3f(mVideo->getCenter().x(), mVideo->getCenter().y(), 0); + else if(mImage.hasImage()) target = Vector3f(mImage.getCenter().x(), mImage.getCenter().y(), 0); ViewController::get()->launch(game, target); @@ -364,13 +378,6 @@ std::vector DetailedGameListView::getMDValues() return ret; } -void DetailedGameListView::update(int deltaTime) -{ - BasicGameListView::update(deltaTime); - - mImage.setVisible(mVideo == NULL || !(mVideo->isPlaying() && !mVideo->isFading())); -} - void DetailedGameListView::onShow() { GuiComponent::onShow(); diff --git a/es-app/src/views/gamelist/DetailedGameListView.h b/es-app/src/views/gamelist/DetailedGameListView.h index 9f2f00a044..2916ad66ff 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.h +++ b/es-app/src/views/gamelist/DetailedGameListView.h @@ -28,9 +28,6 @@ class DetailedGameListView : public BasicGameListView virtual void launch(FileData* game) override; -protected: - virtual void update(int deltaTime) override; - private: void updateInfoPanel(); @@ -39,6 +36,8 @@ class DetailedGameListView : public BasicGameListView std::string getMetadata(FileData* file, std::string name); ImageComponent mImage; + ImageComponent mMarquee; + VideoComponent* mVideo; TextComponent mLblRating, mLblReleaseDate, mLblDeveloper, mLblPublisher, mLblGenre, mLblPlayers, mLblLastPlayed, mLblPlayCount; @@ -51,15 +50,14 @@ class DetailedGameListView : public BasicGameListView DateTimeComponent mLastPlayed; TextComponent mPlayCount; TextComponent mName; - VideoComponent* mVideo; - + std::vector getMDLabels(); std::vector getMDValues(); ScrollableContainer mDescContainer; TextComponent mDescription; - bool mVideoVisible; + }; #endif // ES_APP_VIEWS_GAME_LIST_DETAILED_GAME_LIST_VIEW_H diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 4d3ac60834..918bb88892 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -6,13 +6,20 @@ #include "CollectionSystemManager.h" #include "Settings.h" #include "SystemData.h" +#include "Window.h" +#include "guis/GuiGamelistOptions.h" -GridGameListView::GridGameListView(Window* window, FileData* root) : +#ifdef _RPI_ +#include "components/VideoPlayerComponent.h" +#endif +#include "components/VideoVlcComponent.h" + +GridGameListView::GridGameListView(Window* window, FileData* root, const std::shared_ptr& theme, std::string themeName, Vector2f gridSize) : ISimpleGameListView(window, root), mGrid(window), mDescContainer(window), mDescription(window), mImage(window), - + mVideo(nullptr), mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -22,13 +29,14 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : { const float padding = 0.01f; + mVideoVisible = false; + + mGrid.setGridSizeOverride(gridSize); mGrid.setPosition(mSize.x() * 0.1f, mSize.y() * 0.1f); mGrid.setDefaultZIndex(20); mGrid.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); addChild(&mGrid); - populateList(root->getChildrenListToDisplay()); - // image mImage.setOrigin(0.5f, 0.5f); //mImage.setPosition(mSize.x() * 0.25f, mList.getPosition().y() + mSize.y() * 0.2125f); @@ -36,6 +44,22 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : mImage.setDefaultZIndex(30); addChild(&mImage); + // video + // Create the correct type of video window +#ifdef _RPI_ + if (Settings::getInstance()->getBool("VideoOmxPlayer")) + mVideo = new VideoPlayerComponent(window, ""); + else +#endif + mVideo = new VideoVlcComponent(window, getTitlePath()); + + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); + mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); + mVideo->setStartDelay(2000); + mVideo->setDefaultZIndex(31); + // addChild(mVideo); + // metadata labels + values mLblRating.setText("Rating: "); addChild(&mLblRating); @@ -83,9 +107,20 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : initMDLabels(); initMDValues(); + + if (!themeName.empty()) + setThemeName(themeName); + + setTheme(theme); + + populateList(root->getChildrenListToDisplay()); updateInfoPanel(); } +GridGameListView::~GridGameListView() +{ + delete mVideo; +} void GridGameListView::setThemeName(std::string name) { ISimpleGameListView::setThemeName(name); @@ -118,12 +153,33 @@ std::string GridGameListView::getQuickSystemSelectLeftButton() bool GridGameListView::input(InputConfig* config, Input input) { + if (!UIModeController::getInstance()->isUIModeKid() && config->isMappedTo("select", input) && input.value) + { + Sound::getFromTheme(mTheme, getName(), "menuOpen")->play(); + mWindow->pushGui(new GuiGamelistOptions(mWindow, this->mRoot->getSystem(), true)); + return true; + + // Ctrl-R to reload a view when debugging + } + if(config->isMappedLike("left", input) || config->isMappedLike("right", input)) return GuiComponent::input(config, input); return ISimpleGameListView::input(config, input); } +const std::string GridGameListView::getImagePath(FileData* file) +{ + ImageSource src = mGrid.getImageSource(); + + if (src == ImageSource::IMAGE) + return file->getImagePath(); + else if (src == ImageSource::MARQUEE) + return file->getMarqueePath(); + + return file->getThumbnailPath(); +} + void GridGameListView::populateList(const std::vector& files) { mGrid.clear(); @@ -131,25 +187,51 @@ void GridGameListView::populateList(const std::vector& files) if (files.size() > 0) { std::string systemName = mRoot->getSystem()->getFullName(); + bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + bool favoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); + bool showFavoriteIcon = (systemName != "favorites"); + if (!showFavoriteIcon) + favoritesFirst = false; - for (auto it = files.cbegin(); it != files.cend(); it++) - if ((*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) + if (favoritesFirst) + { + for (auto it = files.cbegin(); it != files.cend(); it++) { - if (systemName == "favorites") - mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), (*it)->getVideoPath(), *it); - else - mGrid.add(_T("\uF006 ") + (*it)->getName(), (*it)->getThumbnailPath(), (*it)->getVideoPath(), *it); + if ((*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) + { + if (showFavoriteIcon) + mGrid.add(_T("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + else + mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + } } + } for (auto it = files.cbegin(); it != files.cend(); it++) - if (!(*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) - mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), (*it)->getVideoPath(), *it); + { + if (!showHiddenFiles && (*it)->getHidden()) + continue; + + if ((*it)->getFavorite()) + { + if (favoritesFirst) + continue; + + if (showFavoriteIcon) + { + mGrid.add(_T("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + continue; + } + } + + mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + } } else { addPlaceholder(); - } + } } void GridGameListView::onThemeChanged(const std::shared_ptr& theme) @@ -169,6 +251,18 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) else mImageVisible = false; + if (theme->getElement(getName(), "md_video", "video")) + { + mVideoVisible = true; + mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION); + addChild(mVideo); + } + else + { + mVideoVisible = false; + removeChild(mVideo); + } + initMDLabels(); std::vector labels = getMDLabels(); assert(labels.size() == 8); @@ -196,9 +290,19 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) values[i]->applyTheme(theme, getName(), valElements[i], ALL ^ ThemeFlags::TEXT); } - mDescContainer.applyTheme(theme, getName(), "md_description", POSITION | ThemeFlags::SIZE | Z_INDEX); - mDescription.setSize(mDescContainer.getSize().x(), 0); - mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION)); + + if (theme->getElement(getName(), "md_description", "text")) + { + mDescContainer.applyTheme(theme, getName(), "md_description", POSITION | ThemeFlags::SIZE | Z_INDEX); + mDescription.setSize(mDescContainer.getSize().x(), 0); + mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION)); + + if (!isChild(&mDescContainer)) + addChild(&mDescContainer); + } + else + removeChild(&mDescContainer); + sortChildren(); updateInfoPanel(); @@ -276,17 +380,36 @@ void GridGameListView::updateInfoPanel() bool fadingOut; if (file == NULL) { + mVideo->setVideo(""); + mVideo->setImage(""); + //mDescription.setText(""); fadingOut = true; } else { + if (mVideoVisible) + { + if (!mVideo->setVideo(file->getVideoPath())) + mVideo->setDefaultVideo(); + } + if (mImageVisible) { if (file->getImagePath().empty()) + { + if (mVideoVisible) + mVideo->setImage(file->getThumbnailPath(), false, mVideo->getSize()); + mImage.setImage(file->getThumbnailPath(), false, mImage.getSize()); + } else + { + if (mVideoVisible) + mVideo->setImage(file->getImagePath(), false, mVideo->getSize()); + mImage.setImage(file->getImagePath(), false, mImage.getSize()); + } } else mImage.setImage(""); @@ -313,6 +436,7 @@ void GridGameListView::updateInfoPanel() std::vector comps = getMDValues(); comps.push_back(&mImage); + comps.push_back(mVideo); comps.push_back(&mDescription); comps.push_back(&mName); std::vector labels = getMDLabels(); @@ -353,6 +477,7 @@ void GridGameListView::remove(FileData *game, bool deleteFile) { if (deleteFile) Utils::FileSystem::removeFile(game->getPath()); // actually delete the file on the filesystem + FileData* parent = game->getParent(); if (getCursor() == game) // Select next element in list, or prev if none { @@ -444,5 +569,3 @@ std::vector GridGameListView::getHelpPrompts() } return prompts; } - - diff --git a/es-app/src/views/gamelist/GridGameListView.h b/es-app/src/views/gamelist/GridGameListView.h index d7fb787bc5..53508b1574 100644 --- a/es-app/src/views/gamelist/GridGameListView.h +++ b/es-app/src/views/gamelist/GridGameListView.h @@ -9,10 +9,13 @@ #include "views/gamelist/ISimpleGameListView.h" #include "views/gamelist/BasicGameListView.h" +class VideoComponent; + class GridGameListView : public ISimpleGameListView { public: - GridGameListView(Window* window, FileData* root); + GridGameListView(Window* window, FileData* root, const std::shared_ptr& theme, std::string customThemeName, Vector2f gridSize); + ~GridGameListView(); virtual void onThemeChanged(const std::shared_ptr& theme) override; @@ -33,7 +36,7 @@ class GridGameListView : public ISimpleGameListView virtual void launch(FileData* game) override; virtual void onFileChanged(FileData* file, FileChangeType change); - virtual void setThemeName(std::string name); + virtual void setThemeName(std::string name); protected: virtual std::string getQuickSystemSelectRightButton() override; @@ -46,6 +49,7 @@ class GridGameListView : public ISimpleGameListView private: void updateInfoPanel(); + const std::string getImagePath(FileData* file); void initMDLabels(); void initMDValues(); @@ -61,10 +65,13 @@ class GridGameListView : public ISimpleGameListView DateTimeComponent mLastPlayed; TextComponent mPlayCount; TextComponent mName; - ImageComponent mImage; + ImageComponent mImage; bool mImageVisible; + VideoComponent* mVideo; + bool mVideoVisible; + std::vector getMDLabels(); std::vector getMDValues(); diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 7425391c17..bde490abf5 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -164,7 +164,7 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) return true; } } - else if (config->isMappedTo("y", input) && UIModeController::getInstance()->isUIModeFull()) + else if (config->isMappedTo("y", input) && UIModeController::getInstance()->isUIModeKid()) { if(mRoot->getSystem()->isGameSystem()) { diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index b57d669444..823d9feb10 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -62,7 +62,7 @@ VideoGameListView::VideoGameListView(Window* window, FileData* root) : mVideo->setOrigin(0.5f, 0.5f); mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); mVideo->setSize(mSize.x() * (0.5f - 2*padding), mSize.y() * 0.4f); - mVideo->setDefaultZIndex(30); + mVideo->setDefaultZIndex(31); addChild(mVideo); // metadata labels + values diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index 9e4302f1e3..1cbb204927 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -55,6 +55,7 @@ set(CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInputConfig.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMsgBox.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopup.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopupKeyboard.h # Math ${CMAKE_CURRENT_SOURCE_DIR}/src/math/Misc.h @@ -128,6 +129,7 @@ set(CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInputConfig.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMsgBox.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopup.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopupKeyboard.cpp # Math ${CMAKE_CURRENT_SOURCE_DIR}/src/math/Misc.cpp diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index fe3c63ff50..b8946fb7c1 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -105,6 +105,8 @@ const std::string GuiTextTool::localize(const std::string text) return text; } +bool GuiComponent::ALLOWANIMATIONS = true; + GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255), mPosition(Vector3f::Zero()), mOrigin(Vector2f::Zero()), mRotationOrigin(0.5, 0.5), mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false) @@ -211,7 +213,7 @@ Vector2f GuiComponent::getSize() const void GuiComponent::setSize(float w, float h) { mSize = Vector2f(w, h); - onSizeChanged(); + onSizeChanged(); } float GuiComponent::getRotation() const @@ -245,7 +247,7 @@ void GuiComponent::setZIndex(float z) } float GuiComponent::getDefaultZIndex() const -{ +{ return mDefaultZIndex; } @@ -260,6 +262,15 @@ Vector2f GuiComponent::getCenter() const mPosition.y() - (getSize().y() * mOrigin.y()) + getSize().y() / 2); } +bool GuiComponent::isChild(GuiComponent* cmp) +{ + for (auto i = mChildren.cbegin(); i != mChildren.cend(); i++) + if (*i == cmp) + return true; + + return false; +} + //Children stuff. void GuiComponent::addChild(GuiComponent* cmp) { diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 31774a179e..73623121db 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -27,6 +27,8 @@ class GuiComponent GuiComponent(Window* window); virtual ~GuiComponent(); + static bool ALLOWANIMATIONS; + virtual void textInput(const char* text); //Called when input is received. @@ -94,6 +96,7 @@ class GuiComponent void sortChildren(); unsigned int getChildCount() const; GuiComponent* getChild(unsigned int i) const; + bool isChild(GuiComponent* cmp); // animation will be automatically deleted when it completes or is stopped. bool isAnimationPlaying(unsigned char slot) const; @@ -206,6 +209,7 @@ class GuiTextTool static const std::string localize(const std::string text); static void setLanguage(std::string lang); + static const std::string getLanguage() { return mCurrentLanguage; } private: static std::vector mItems; @@ -214,9 +218,19 @@ class GuiTextTool }; #if defined(_WIN32) + +#define UNICODE_CHARTYPE wchar_t* +#define UNICODE_CHARS(x) L ## x +#define UNICODE_STRING(x) GuiTextTool::convertFromWideString(L ## x) + #define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) #define _L(x) GuiTextTool::localize(x) #else + +#define UNICODE_CHARTYPE char* +#define UNICODE_CHARS(x) x +#define UNICODE_STRING(x) x + #define _T(x) GuiTextTool::localize(x) #define _L(x) GuiTextTool::localize(x) #endif // _WIN32 diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index 1bb4fe2f2f..e7b8e1690f 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -115,8 +115,11 @@ Vector2i ImageIO::adjustPictureSize(Vector2i imageSize, Vector2i maxSize) } -unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight) +unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight, Vector2i& baseSize, Vector2i& packedSize) { + baseSize = Vector2i(0, 0); + packedSize = Vector2i(0, 0); + width = 0; height = 0; @@ -142,22 +145,27 @@ unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const fiBitmap = fiConverted; } } + if (fiBitmap != nullptr) { width = FreeImage_GetWidth(fiBitmap); height = FreeImage_GetHeight(fiBitmap); + + baseSize = Vector2i(width, height); if (maxWidth > 0 && maxHeight > 0 && (width > maxWidth || height > maxHeight)) { Vector2i sz = adjustPictureSize(Vector2i(width, height), Vector2i(maxWidth, maxHeight)); if (sz.x() != width || sz.y() != height) - { + { FIBITMAP* imageRescaled = FreeImage_Rescale(fiBitmap, sz.x(), sz.y(), FILTER_BOX); FreeImage_Unload(fiBitmap); fiBitmap = imageRescaled; width = FreeImage_GetWidth(fiBitmap); height = FreeImage_GetHeight(fiBitmap); + + packedSize = Vector2i(width, height); } } diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index 89b27909ad..40f371ec1b 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -11,7 +11,7 @@ class ImageIO { public: static std::vector loadFromMemoryRGBA32(const unsigned char * data, const size_t size, size_t & width, size_t & height); - static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth=0, int maxHeight=0); + static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight, Vector2i& baseSize, Vector2i& packedSize); static void flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height); static Vector2i adjustPictureSize(Vector2i imageSize, Vector2i maxSize); diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index a42dd323c9..5eb83fa380 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -73,12 +73,7 @@ void Settings::setDefaults() mStringMap["StartupSystem"] = ""; -#if defined(_WIN32) mBoolMap["VSync"] = true; -#else - mBoolMap["VSync"] = false; -#endif - mBoolMap["EnableSounds"] = true; mBoolMap["ShowHelpPrompts"] = true; mBoolMap["ScrapeRatings"] = true; @@ -87,6 +82,7 @@ void Settings::setDefaults() mBoolMap["QuickSystemSelect"] = true; mBoolMap["MoveCarousel"] = true; mBoolMap["SaveGamelistsOnExit"] = true; + mBoolMap["OptimizeVRAM"] = false; mBoolMap["Debug"] = false; mBoolMap["DebugGrid"] = false; @@ -160,6 +156,8 @@ void Settings::setDefaults() mStringMap["CollectionSystemsCustom"] = ""; mBoolMap["SortAllSystems"] = false; mBoolMap["UseCustomCollectionsSystem"] = true; + mBoolMap["FavoritesFirst"] = true; + mBoolMap["LocalArt"] = false; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 1bb2cf8bb7..8957c4e2ee 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -8,6 +8,8 @@ #include "Settings.h" #include +#include "utils/StringUtil.h" + #ifdef _RPI_ #include "components/VideoPlayerComponent.h" #endif @@ -34,11 +36,13 @@ std::map> The { "size", NORMALIZED_PAIR }, { "margin", NORMALIZED_PAIR }, - { "padding", NORMALIZED_PAIR }, + { "padding", NORMALIZED_RECT }, { "autoLayout", NORMALIZED_PAIR }, { "autoLayoutSelectedZoom", FLOAT }, + { "imageSource", STRING }, // image, thumbnail, marquee + { "zIndex", FLOAT }, { "gameImage", PATH }, { "folderImage", PATH }, { "showVideoAtDelay", FLOAT }, @@ -52,6 +56,7 @@ std::map> The { "backgroundColor", COLOR }, { "backgroundCenterColor", COLOR }, { "backgroundEdgeColor", COLOR }, + { "selectionMode", STRING }, { "imageSizeMode", STRING } } }, { "text", { { "pos", NORMALIZED_PAIR }, @@ -205,7 +210,8 @@ std::map> The { "filledPath", PATH } } }, }; -std::shared_ptr ThemeData::MenuTheme; +std::shared_ptr ThemeData::mMenuTheme; +ThemeData* ThemeData::mCurrentTheme = nullptr; #define MINIMUM_THEME_FORMAT_VERSION 3 #define CURRENT_THEME_FORMAT_VERSION 6 @@ -240,9 +246,7 @@ unsigned int getHexColor(const char* str) return val; } -std::map mVariables; - -std::string resolvePlaceholders(const char* in) +std::string ThemeData::resolvePlaceholders(const char* in) { std::string inStr(in); @@ -320,7 +324,18 @@ void ThemeData::loadFile(std::string system, std::map parseCustomViews(root); parseFeatures(root); - MenuTheme = std::shared_ptr(new ThemeMenu(*this)); + mMenuTheme = nullptr; + mCurrentTheme = this; +} + +const std::shared_ptr& ThemeData::getMenuTheme() +{ + if (mMenuTheme == nullptr && mCurrentTheme != nullptr) + mMenuTheme = std::shared_ptr(new ThemeMenu(*mCurrentTheme)); + else if (mMenuTheme == nullptr) + return std::shared_ptr(new ThemeMenu(ThemeData())); + + return mMenuTheme; } std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) @@ -536,7 +551,6 @@ void ThemeData::parseViews(const pugi::xml_node& root) ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; parseView(node, view); - for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) { if (it->second.isCustomView && it->second.baseType == viewKey) @@ -545,10 +559,6 @@ void ThemeData::parseViews(const pugi::xml_node& root) parseView(node, customView); } } - - - - } } } @@ -745,6 +755,25 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapsecond) { + case NORMALIZED_RECT: + { + Vector4f val; + + auto splits = Utils::String::split(str, ' '); + if (splits.size() == 2) + { + val = Vector4f((float)atof(splits.at(0).c_str()), (float)atof(splits.at(1).c_str()), + (float)atof(splits.at(0).c_str()), (float)atof(splits.at(1).c_str())); + } + else if (splits.size() == 4) + { + val = Vector4f((float)atof(splits.at(0).c_str()), (float)atof(splits.at(1).c_str()), + (float)atof(splits.at(2).c_str()), (float)atof(splits.at(3).c_str())); + } + + element.properties[node.name()] = val; + break; + } case NORMALIZED_PAIR: { size_t divider = str.find(' '); @@ -759,7 +788,7 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map ThemeData::makeExtras(const std::shared_ptr ThemeData::makeExtras(const std::shared_ptrsetDefaultZIndex(10); comp->applyTheme(theme, view, *it, ThemeFlags::ALL); comps.push_back(comp); diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 16d59d4fd8..eeed1a54c4 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -3,6 +3,7 @@ #define ES_CORE_THEME_DATA_H #include "math/Vector2f.h" +#include "math/Vector4f.h" #include "utils/FileSystemUtil.h" #include @@ -143,12 +144,14 @@ class ThemeData struct Property { + void operator= (const Vector4f& value) { r = value; v = Vector2f(value.x(), value.y()); } void operator= (const Vector2f& value) { v = value; } void operator= (const std::string& value) { s = value; } void operator= (const unsigned int& value) { i = value; } void operator= (const float& value) { f = value; } void operator= (const bool& value) { b = value; } + Vector4f r; Vector2f v; std::string s; unsigned int i; @@ -166,6 +169,7 @@ class ThemeData else if(std::is_same::value) return *(const T*)&properties.at(prop).i; else if(std::is_same::value) return *(const T*)&properties.at(prop).f; else if(std::is_same::value) return *(const T*)&properties.at(prop).b; + else if (std::is_same::value) return *(const T*)&properties.at(prop).r; return T(); } @@ -194,6 +198,7 @@ class ThemeData enum ElementPropertyType { + NORMALIZED_RECT, NORMALIZED_PAIR, PATH, STRING, @@ -223,7 +228,7 @@ class ThemeData bool hasSubsets() { return mHasSubsets; } - static const std::shared_ptr& getMenuTheme() { return MenuTheme; } + static const std::shared_ptr& getMenuTheme(); static std::map sortThemeSubSets(const std::map& subsetmap, const std::string& subset); static std::map ThemeData::getThemeSubSets(const std::string& theme); @@ -253,6 +258,7 @@ class ThemeData void parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& view, std::string baseClass); std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); + std::string resolvePlaceholders(const char* in); std::map mViews; @@ -264,9 +270,12 @@ class ThemeData std::string mSystemThemeFolder; + std::map mVariables; + bool mHasSubsets; - static std::shared_ptr MenuTheme; + static std::shared_ptr mMenuTheme; + static ThemeData* mCurrentTheme; }; #endif // ES_CORE_THEME_DATA_H diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index c887c29028..987a171ef3 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -81,7 +81,7 @@ bool Window::init(bool initRenderer) InputManager::getInstance()->init(); - if (initRenderer) + //if (initRenderer) ResourceManager::getInstance()->reloadAll(); //keep a reference to the default fonts, so they don't keep getting destroyed/recreated @@ -109,11 +109,14 @@ void Window::deinit(bool deinitRenderer) { (*i)->onHide(); } + InputManager::getInstance()->deinit(); - + TextureResource::resetCache(); + ResourceManager::getInstance()->unloadAll(); + if (deinitRenderer) { - ResourceManager::getInstance()->unloadAll(); + // ResourceManager::getInstance()->unloadAll(); Renderer::deinit(); } } @@ -309,6 +312,9 @@ void Window::setAllowSleep(bool sleep) void Window::endRenderLoadingScreen() { + if (mSplash != NULL) + mSplash->unload(); + mSplash = NULL; } @@ -344,7 +350,7 @@ void Window::renderLoadingScreen(std::string text, float percent) #if defined(_WIN32) if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); + mSplash = TextureResource::get(":/splash.svg", false, false, true); #endif if (mSplash != NULL) @@ -381,11 +387,14 @@ void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) #if defined(_WIN32) if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); + mSplash = TextureResource::get(":/splash.svg", false, false, true); #endif if (mSplash != NULL) + { + mSplash->reload(); // Ensure splash is loaded splash.setImage(mSplash); + } else splash.setImage(":/splash.svg"); @@ -394,6 +403,8 @@ void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) splash.render(trans); auto& font = mDefaultFonts.at(1); + font->reload(); // Ensure font is loaded + TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); diff --git a/es-core/src/components/ButtonComponent.cpp b/es-core/src/components/ButtonComponent.cpp index b457c6aff5..eb4e64294a 100644 --- a/es-core/src/components/ButtonComponent.cpp +++ b/es-core/src/components/ButtonComponent.cpp @@ -4,7 +4,7 @@ #include "utils/StringUtil.h" #include "Renderer.h" -ButtonComponent::ButtonComponent(Window* window, const std::string& text, const std::string& helpText, const std::function& func) : GuiComponent(window), +ButtonComponent::ButtonComponent(Window* window, const std::string& text, const std::string& helpText, const std::function& func, bool upperCase) : GuiComponent(window), mBox(window, ThemeData::getMenuTheme()->Icons.button), mFont(Font::get(FONT_SIZE_MEDIUM)), mFocused(false), @@ -19,7 +19,7 @@ ButtonComponent::ButtonComponent(Window* window, const std::string& text, const mColor = menuTheme->Text.color; setPressedFunc(func); - setText(text, helpText); + setText(text, helpText, upperCase); updateImage(); } @@ -45,9 +45,9 @@ bool ButtonComponent::input(InputConfig* config, Input input) return GuiComponent::input(config, input); } -void ButtonComponent::setText(const std::string& text, const std::string& helpText) +void ButtonComponent::setText(const std::string& text, const std::string& helpText, bool upperCase) { - mText = Utils::String::toUpper(text); + mText = upperCase ? Utils::String::toUpper(text) : text; mHelpText = helpText; mTextCache = std::unique_ptr(mFont->buildTextCache(mText, 0, 0, getCurTextColor())); @@ -86,6 +86,14 @@ void ButtonComponent::updateImage() return; } + // If a new color has been set. + if (mNewColor) { + mBox.setImagePath(ThemeData::getMenuTheme()->Icons.button_filled); + mBox.setCenterColor(mModdedColor); + mBox.setEdgeColor(mModdedColor); + return; + } + mBox.setCenterColor(mColor); mBox.setEdgeColor(mColor); mBox.setImagePath(mFocused ? ThemeData::getMenuTheme()->Icons.button_filled : ThemeData::getMenuTheme()->Icons.button); diff --git a/es-core/src/components/ButtonComponent.h b/es-core/src/components/ButtonComponent.h index 790bbc6d43..0825aed5e7 100644 --- a/es-core/src/components/ButtonComponent.h +++ b/es-core/src/components/ButtonComponent.h @@ -10,7 +10,7 @@ class TextCache; class ButtonComponent : public GuiComponent { public: - ButtonComponent(Window* window, const std::string& text = "", const std::string& helpText = "", const std::function& func = nullptr); + ButtonComponent(Window* window, const std::string& text = "", const std::string& helpText = "", const std::function& func = nullptr, bool upperCase = true); void setPressedFunc(std::function f); @@ -19,7 +19,7 @@ class ButtonComponent : public GuiComponent bool input(InputConfig* config, Input input) override; void render(const Transform4x4f& parentTrans) override; - void setText(const std::string& text, const std::string& helpText); + void setText(const std::string& text, const std::string& helpText, bool upperCase = true); inline const std::string& getText() const { return mText; }; inline const std::function& getPressedFunc() const { return mPressedFunc; }; @@ -28,6 +28,9 @@ class ButtonComponent : public GuiComponent void onFocusGained() override; void onFocusLost() override; + void setColorShift(unsigned int color) { mModdedColor = color; mNewColor = true; updateImage(); } + void removeColorShift() { mNewColor = false; updateImage(); } + virtual std::vector getHelpPrompts() override; private: @@ -36,8 +39,10 @@ class ButtonComponent : public GuiComponent bool mFocused; bool mEnabled; + bool mNewColor = false; unsigned int mTextColorFocused; unsigned int mTextColorUnfocused; + unsigned int mModdedColor; unsigned int getCurTextColor() const; void updateImage(); diff --git a/es-core/src/components/ComponentGrid.cpp b/es-core/src/components/ComponentGrid.cpp index 7ba5b0c1c3..795e59b8c9 100644 --- a/es-core/src/components/ComponentGrid.cpp +++ b/es-core/src/components/ComponentGrid.cpp @@ -234,24 +234,30 @@ bool ComponentGrid::input(InputConfig* config, Input input) if(!input.value) return false; + bool result = false; + if(config->isMappedLike("down", input)) { - return moveCursor(Vector2i(0, 1)); + result = moveCursor(Vector2i(0, 1)); } if(config->isMappedLike("up", input)) { - return moveCursor(Vector2i(0, -1)); + result = moveCursor(Vector2i(0, -1)); } if(config->isMappedLike("left", input)) { - return moveCursor(Vector2i(-1, 0)); + result = moveCursor(Vector2i(-1, 0)); } if(config->isMappedLike("right", input)) { - return moveCursor(Vector2i(1, 0)); + result = moveCursor(Vector2i(1, 0)); } - return false; + if (!result && mUnhandledInputCallback) { + return mUnhandledInputCallback(config, input); + } + + return result; } void ComponentGrid::resetCursor() diff --git a/es-core/src/components/ComponentGrid.h b/es-core/src/components/ComponentGrid.h index 15fe8e76df..3ff8ec09e3 100644 --- a/es-core/src/components/ComponentGrid.h +++ b/es-core/src/components/ComponentGrid.h @@ -71,6 +71,8 @@ class ComponentGrid : public GuiComponent virtual std::vector getHelpPrompts() override; + inline void setUnhandledInputCallback(const std::function& func) { mUnhandledInputCallback = func; } + private: class GridEntry { @@ -122,6 +124,8 @@ class ComponentGrid : public GuiComponent void onCursorMoved(Vector2i from, Vector2i to); Vector2i mCursor; + + std::function mUnhandledInputCallback; }; #endif // ES_CORE_COMPONENTS_COMPONENT_GRID_H diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index de13836dac..9229227416 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -20,7 +20,7 @@ #define VIDEODELAY 100 -GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window), mLabel(window), mVideo(nullptr), mVideoPlaying(false) +GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window), mLabel(window), mVideo(nullptr), mVideoPlaying(false), mShown(false) { mSelectedZoomPercent = 1.0f; mAnimPosition = Vector3f(0, 0); @@ -53,6 +53,7 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mImage = std::make_shared(mWindow); mImage->setOrigin(0.5f, 0.5f); + mImage->setAllowFading(false); addChild(&mBackground); addChild(&(*mImage)); @@ -124,24 +125,6 @@ void GridTileComponent::resize() float bottomPadding = std::max(topPadding, height); float paddingX = currentProperties.mPadding.x(); - if (mSelectedZoomPercent != 1.0f) - { - if (mAnimPosition.x() != 0 && mAnimPosition.y() != 0 && mSelected) - { - int x = mAnimPosition.x() - mPosition.x(); - int y = mAnimPosition.y() - mPosition.y(); - - x = x * (1.0 - mSelectedZoomPercent); - y = y * (1.0 - mSelectedZoomPercent); - - mBackground.setPosition(x, y); - } - else - mBackground.setPosition(0, 0); - } - else - mBackground.setPosition(0, 0); - float imageWidth = size.x() - paddingX * 2.0; float imageHeight = size.y() - topPadding - bottomPadding; @@ -149,7 +132,8 @@ void GridTileComponent::resize() { mImage->setOrigin(0.5f, 0.5f); mImage->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); - + mImage->setColorShift(currentProperties.mImageColor); + if (currentProperties.mImageSizeMode == "minSize") mImage->setMinSize(imageWidth, imageHeight); else if (currentProperties.mImageSizeMode == "size") @@ -207,12 +191,45 @@ void GridTileComponent::resize() mVideo->setMaxSize(imageWidth, size.y() - topPadding - bottomPadding); } - if (!mLabelMerged && currentProperties.mImageSizeMode == "minSize") - mBackground.setSize(size.x(), size.y() - bottomPadding + topPadding); - else - mBackground.setSize(size); + Vector3f bkposition = Vector3f(0, 0); + Vector2f bkSize = size; + + +// if (!mLabelMerged && currentProperties.mImageSizeMode == "minSize") +// bkSize = Vector2f(size.x(), size.y() - bottomPadding + topPadding); + + if (mImage != NULL && currentProperties.mSelectionMode == "image" && mImage->getSize() != Vector2f(0,0)) + { + if (currentProperties.mImageSizeMode == "minSize") + bkSize = Vector2f(size.x(), size.y() - bottomPadding + topPadding); + else + { + bkposition = Vector3f( + mImage->getPosition().x() - mImage->getSize().x() / 2 - currentProperties.mPadding.x(), + mImage->getPosition().y() - mImage->getSize().y() / 2 - currentProperties.mPadding.y(), 0); + + bkSize = Vector2f(mImage->getSize().x() + 2 * currentProperties.mPadding.x(), mImage->getSize().y() + 2 * currentProperties.mPadding.y()); + } + } + + if (mSelectedZoomPercent != 1.0f && mAnimPosition.x() != 0 && mAnimPosition.y() != 0 && mSelected) + { + float x = mPosition.x() + bkposition.x(); + float y = mPosition.y() + bkposition.y(); + + x = mAnimPosition.x() * (1.0 - mSelectedZoomPercent) + x * mSelectedZoomPercent; + y = mAnimPosition.y() * (1.0 - mSelectedZoomPercent) + y * mSelectedZoomPercent; + + bkposition = Vector3f(x - mPosition.x(), y - mPosition.y(), 0); + } + + mBackground.setPosition(bkposition); + mBackground.setSize(bkSize); mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); + mBackground.setCenterColor(currentProperties.mBackgroundCenterColor); + mBackground.setEdgeColor(currentProperties.mBackgroundEdgeColor); + mBackground.setImagePath(currentProperties.mBackgroundImage); } void GridTileComponent::renderBackground(const Transform4x4f& parentTrans) @@ -221,19 +238,7 @@ void GridTileComponent::renderBackground(const Transform4x4f& parentTrans) return; Transform4x4f trans = getTransform() * parentTrans; - - Vector2f clipPos(trans.translation().x(), trans.translation().y()); - if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) - return; - - if (mBackground.getCornerSize().x() == 0) - { - Renderer::setMatrix(trans); - Renderer::drawRect(mBackground.getPosition().x(), mBackground.getPosition().y(), mBackground.getSize().x() - 1, mBackground.getSize().y() - 1, mBackground.getCenterColor()); - Renderer::setMatrix(parentTrans); - } - else - mBackground.render(trans); + mBackground.render(trans); } void GridTileComponent::renderContent(const Transform4x4f& parentTrans) @@ -285,24 +290,6 @@ void GridTileComponent::render(const Transform4x4f& parentTrans) renderContent(parentTrans); } -// Update all the tile properties to the new status (selected or default) -void GridTileComponent::update(int deltaTime) -{ - GuiComponent::update(deltaTime); - - const GridTileProperties& currentProperties = getCurrentProperties(); - - mBackground.setImagePath(currentProperties.mBackgroundImage); - - if (mImage != NULL) - mImage->setColorShift(currentProperties.mImageColor); - - mBackground.setCenterColor(currentProperties.mBackgroundCenterColor); - mBackground.setEdgeColor(currentProperties.mBackgroundEdgeColor); - - resize(); -} - void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); @@ -339,7 +326,16 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mDefaultProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); if (elem && elem->has("imageSizeMode")) + { mDefaultProperties.mImageSizeMode = elem->get("imageSizeMode"); + mSelectedProperties.mImageSizeMode = mDefaultProperties.mImageSizeMode; + } + + if (elem && elem->has("selectionMode")) + { + mDefaultProperties.mSelectionMode = elem->get("selectionMode"); + mSelectedProperties.mSelectionMode = mDefaultProperties.mSelectionMode; + } } // Apply theme to the selected gridtile @@ -379,10 +375,6 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem && elem->has("backgroundEdgeColor")) mSelectedProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); - mSelectedProperties.mImageSizeMode = elem && elem->has("imageSizeMode") ? - elem->get("imageSizeMode") : - mDefaultProperties.mImageSizeMode; - elem = theme->getElement(view, "gridtile", "text"); if (elem != NULL) { @@ -450,7 +442,12 @@ void GridTileComponent::setImage(const std::string& path) return; mCurrentPath = path; - mImage->setImage(path, false, mSize); + + if (mSelectedProperties.mSize.x() > mSize.x()) + mImage->setImage(path, false, mSelectedProperties.mSize); + else + mImage->setImage(path, false, mSize); + resize(); } @@ -485,19 +482,27 @@ void GridTileComponent::setVideo(const std::string& path, float defaultDelay) if (mVideoPath.empty()) mVideo->setVideo(""); } + + resize(); } -void GridTileComponent::setImage(const std::shared_ptr& texture, std::string name) +void GridTileComponent::onShow() { - mImage->setImage(texture); - mLabel.setText(name); - - // Resize now to prevent flickering images when scrolling - resize(); + GuiComponent::onShow(); + mShown = true; +} + +void GridTileComponent::onHide() +{ + GuiComponent::onHide(); + mShown = false; } void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f* pPosition) { + if (!mShown || !GuiComponent::ALLOWANIMATIONS) + allowAnimation = false; + if (mSelected == selected) { if (mSelected && mVideo != nullptr) @@ -560,6 +565,9 @@ void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f } else { + if (mVideo != NULL) + mVideo->setVideo(""); + auto func = [this](float t) { t -= 1; // cubic ease out @@ -570,10 +578,6 @@ void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f cancelAnimation(3); setAnimation(new LambdaAnimation(func, 250), 0, [this] { this->setSelectedZoom(0); - - if (mVideo != NULL) - mVideo->setVideo(""); - }, false, 3); } } @@ -612,39 +616,42 @@ unsigned int mixColors(unsigned int first, unsigned int second, float percent) const GridTileProperties& GridTileComponent::getCurrentProperties() { - if (mSelectedZoomPercent != 1.0f) - { - auto def = mSelected ? mSelectedProperties : mDefaultProperties; + if (mSelectedZoomPercent == 0.0f || mSelectedZoomPercent == 1.0f) + return mSelected ? mSelectedProperties : mDefaultProperties; - mMixedProperties = mSelected ? mSelectedProperties : mDefaultProperties; + auto def = mSelected ? mSelectedProperties : mDefaultProperties; - if (mDefaultProperties.mSize != mSelectedProperties.mSize) - { - float x = mDefaultProperties.mSize.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mSize.x() * mSelectedZoomPercent; - float y = mDefaultProperties.mSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mSize.y() * mSelectedZoomPercent; - mMixedProperties.mSize = Vector2f(x, y); - } + mMixedProperties = mSelected ? mSelectedProperties : mDefaultProperties; - if (mDefaultProperties.mPadding != mSelectedProperties.mPadding) - { - float x = mDefaultProperties.mPadding.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.x() * mSelectedZoomPercent; - float y = mDefaultProperties.mPadding.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.y() * mSelectedZoomPercent; - mMixedProperties.mPadding = Vector2f(x, y); - } + if (mDefaultProperties.mSize != mSelectedProperties.mSize) + { + float x = mDefaultProperties.mSize.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mSize.x() * mSelectedZoomPercent; + float y = mDefaultProperties.mSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mSize.y() * mSelectedZoomPercent; + mMixedProperties.mSize = Vector2f(x, y); + } - if (mDefaultProperties.mImageColor != mSelectedProperties.mImageColor) - { - mMixedProperties.mImageColor = mixColors(mDefaultProperties.mImageColor, mSelectedProperties.mImageColor, mSelectedZoomPercent); - } + if (mDefaultProperties.mPadding != mSelectedProperties.mPadding) + { + float x = mDefaultProperties.mPadding.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.x() * mSelectedZoomPercent; + float y = mDefaultProperties.mPadding.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.y() * mSelectedZoomPercent; + mMixedProperties.mPadding = Vector2f(x, y); + } - if (mDefaultProperties.mLabelSize != mSelectedProperties.mLabelSize) - { - float y = mDefaultProperties.mLabelSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mLabelSize.y() * mSelectedZoomPercent; - mMixedProperties.mLabelSize = Vector2f(mDefaultProperties.mLabelSize.x(), y); - } + if (mDefaultProperties.mImageColor != mSelectedProperties.mImageColor) + { + mMixedProperties.mImageColor = mixColors(mDefaultProperties.mImageColor, mSelectedProperties.mImageColor, mSelectedZoomPercent); + } - return mMixedProperties; + if (mDefaultProperties.mLabelSize != mSelectedProperties.mLabelSize) + { + float y = mDefaultProperties.mLabelSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mLabelSize.y() * mSelectedZoomPercent; + mMixedProperties.mLabelSize = Vector2f(mDefaultProperties.mLabelSize.x(), y); } - return mSelected ? mSelectedProperties : mDefaultProperties; + return mMixedProperties; +} + +Vector3f GridTileComponent::getBackgroundPosition() +{ + return Vector3f(mBackground.getPosition().x() + mPosition.x(), mBackground.getPosition().y() + mPosition.y(), 0); } \ No newline at end of file diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index 1502d7ca84..0d8401b53f 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -19,6 +19,7 @@ struct GridTileProperties unsigned int mBackgroundEdgeColor; std::string mImageSizeMode; + std::string mSelectionMode; Vector2f mLabelSize; unsigned int mLabelColor; @@ -32,7 +33,6 @@ class GridTileComponent : public GuiComponent ~GridTileComponent(); void render(const Transform4x4f& parentTrans) override; - void update(int deltaTime) override; virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); @@ -48,7 +48,7 @@ class GridTileComponent : public GuiComponent void setVideo(const std::string& path, float defaultDelay = -1.0); void setImage(const std::string& path); - void setImage(const std::shared_ptr& texture, std::string name); + // void setImage(const std::shared_ptr& texture, std::string name); void setSelected(bool selected, bool allowAnimation = true, Vector3f* pPosition = NULL); void setVisible(bool visible); @@ -59,6 +59,11 @@ class GridTileComponent : public GuiComponent bool shouldSplitRendering() { return isAnimationPlaying(3); }; + Vector3f getBackgroundPosition(); + + virtual void onShow(); + virtual void onHide(); + private: void resize(); const GridTileProperties& getCurrentProperties(); @@ -89,7 +94,7 @@ class GridTileComponent : public GuiComponent VideoComponent* mVideo; bool mVideoPlaying; - + bool mShown; }; #endif // ES_CORE_COMPONENTS_GRID_TILE_COMPONENT_H diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index 4b75e0138f..050181f5b9 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -136,6 +136,15 @@ class IList : public GuiComponent onCursorChanged(CURSOR_STOPPED); } + void setCursorIndex(int index) + { + if (index >= 0 && index < (int)mEntries.size()) + { + mCursor = index; + onCursorChanged(CURSOR_STOPPED); + } + } + // returns true if successful (select is in our list), false if not bool setCursor(const UserData& obj) { diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 0df7e7e7dd..792639e4fa 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -168,7 +168,7 @@ void ImageComponent::setImage(const std::shared_ptr& texture) void ImageComponent::setResize(float width, float height) { - if (!mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) + if (mSize.x() != 0 && mSize.y() != 0 && !mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) return; mTargetSize = Vector2f(width, height); @@ -179,7 +179,7 @@ void ImageComponent::setResize(float width, float height) void ImageComponent::setMaxSize(float width, float height) { - if (mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) + if (mSize.x() != 0 && mSize.y() != 0 && mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) return; mTargetSize = Vector2f(width, height); @@ -190,7 +190,7 @@ void ImageComponent::setMaxSize(float width, float height) void ImageComponent::setMinSize(float width, float height) { - if (mTargetIsMin && !mTargetIsMax && mTargetSize.x() == width && mTargetSize.y() == height) + if (mSize.x() != 0 && mSize.y() != 0 && mTargetIsMin && !mTargetIsMax && mTargetSize.x() == width && mTargetSize.y() == height) return; mTargetSize = Vector2f(width, height); @@ -260,6 +260,9 @@ void ImageComponent::setFlipY(bool flip) void ImageComponent::setColorShift(unsigned int color) { + if (mColorShift == color) + return; + mColorShift = color; // Grab the opacity from the color shift because we may need to apply it if // fading textures in @@ -338,12 +341,13 @@ void ImageComponent::render(const Transform4x4f& parentTrans) return; Transform4x4f trans = parentTrans * getTransform(); - Renderer::setMatrix(trans); Vector2f clipPos(trans.translation().x(), trans.translation().y()); if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) return; + Renderer::setMatrix(trans); + if (mTexture && mOpacity > 0) { if (Settings::getInstance()->getBool("DebugImage")) @@ -394,6 +398,9 @@ void ImageComponent::render(const Transform4x4f& parentTrans) void ImageComponent::fadeIn(bool textureLoaded) { + if (!mAllowFading) + return; + if (!mForceLoad) { if (!textureLoaded) @@ -476,8 +483,12 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s if(properties & PATH && elem->has("path")) { - bool tile = (elem->has("tile") && elem->get("tile")); - setImage(elem->get("path"), tile, mTargetSize); + auto path = elem->get("path"); + if (Utils::FileSystem::exists(path)) + { + bool tile = (elem->has("tile") && elem->get("tile")); + setImage(path, tile, Vector2f(mTargetSize.x(), mTargetSize.y())); + } } if(properties & COLOR && elem->has("color")) diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 17ca7ac375..6233725460 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -80,6 +80,8 @@ class ImageComponent : public GuiComponent void render(const Transform4x4f& parentTrans) override; + void setAllowFading(bool fade) { mAllowFading = fade; }; + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; virtual std::vector getHelpPrompts() override; @@ -116,6 +118,8 @@ class ImageComponent : public GuiComponent bool mRotateByTargetSize; bool mVisible; + bool mAllowFading; + Vector2f mTopLeftCrop; Vector2f mBottomRightCrop; }; diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index b28fa65bc8..92450572ca 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -17,6 +17,13 @@ enum ScrollDirection SCROLL_HORIZONTALLY_CENTER, }; +enum ImageSource +{ + THUMBNAIL, + IMAGE, + MARQUEE +}; + struct ImageGridData { std::string texturePath; @@ -61,6 +68,10 @@ class ImageGridComponent : public IList virtual void onShow(); virtual void onHide(); + ImageSource getImageSource() { return mImageSource; }; + + void setGridSizeOverride(Vector2f size); + protected: virtual void onCursorChanged(const CursorState& state) override; @@ -82,10 +93,12 @@ class ImageGridComponent : public IList bool mLastRowPartial; Vector2f mAutoLayout; float mAutoLayoutZoom; - Vector2f mPadding; + Vector4f mPadding; Vector2f mMargin; Vector2f mTileSize; Vector2i mGridDimension; + Vector2f mGridSizeOverride; + std::shared_ptr mTheme; std::vector< std::shared_ptr > mTiles; @@ -100,6 +113,8 @@ class ImageGridComponent : public IList // MISCELLANEOUS ScrollDirection mScrollDirection; + ImageSource mImageSource; + std::function mCursorChangedCallback; }; @@ -111,10 +126,11 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList::ImageGridComponent(Window* window) : IList::topWindow(bool isTop) template void ImageGridComponent::onShow() { + if (mEntriesDirty) + { + updateTiles(); + mEntriesDirty = false; + } + GuiComponent::onShow(); for (int ti = 0; ti < (int)mTiles.size(); ti++) @@ -214,6 +237,12 @@ void ImageGridComponent::onShow() } } +template +void ImageGridComponent::setGridSizeOverride(Vector2f size) +{ + mGridSizeOverride = size; +} + template void ImageGridComponent::onHide() { @@ -231,6 +260,10 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = getTransform() * parentTrans; + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + float offsetX = isVertical() ? 0 : mCamera * mCameraDirection * (mTileSize.x() + mMargin.x()); float offsetY = isVertical() ? mCamera * mCameraDirection * (mTileSize.y() + mMargin.y()) : 0; @@ -324,14 +357,27 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, mMargin = elem->get("margin") * screen; if (elem->has("padding")) - mPadding = elem->get("padding") * screen; + mPadding = elem->get("padding") * Vector4f(screen.x(), screen.y(), screen.x(), screen.y()); if (elem->has("autoLayout")) - mAutoLayout = elem->get("autoLayout"); + mAutoLayout = elem->get("autoLayout"); if (elem->has("autoLayoutSelectedZoom")) mAutoLayoutZoom = elem->get("autoLayoutSelectedZoom"); + if (elem->has("imageSource")) + { + auto direction = elem->get("imageSource"); + if (direction == "image") + mImageSource = IMAGE; + else if (direction == "marquee") + mImageSource = MARQUEE; + else + mImageSource = THUMBNAIL; + } + else + mImageSource = THUMBNAIL; + if (elem->has("scrollDirection")) { auto direction = elem->get("scrollDirection"); @@ -405,7 +451,7 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, GridTileComponent::getDefaultTileSize(); // Apply size property, will trigger a call to onSizeChanged() which will build the tiles - GuiComponent::applyTheme(theme, view, element, ThemeFlags::SIZE); + GuiComponent::applyTheme(theme, view, element, ThemeFlags::SIZE | ThemeFlags::Z_INDEX); // Trigger the call manually if the theme have no "imagegrid" element if (!elem) @@ -425,43 +471,43 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) if (mLastCursor == mCursor) return; - if (true) - { - bool centerSel = (mScrollDirection == SCROLL_HORIZONTALLY_CENTER || mScrollDirection == SCROLL_VERTICALLY_CENTER); + bool centerSel = (mScrollDirection == SCROLL_HORIZONTALLY_CENTER || mScrollDirection == SCROLL_VERTICALLY_CENTER); - bool direction = mCursor >= mLastCursor; + bool direction = mCursor >= mLastCursor; - int oldStart = mStartPosition; + int oldStart = mStartPosition; - float dimScrollable = isVertical() ? mGridDimension.y() - 2 * EXTRAITEMS: mGridDimension.x() - 2 * EXTRAITEMS; - float dimOpposite = isVertical() ? mGridDimension.x() : mGridDimension.y(); + float dimScrollable = isVertical() ? mGridDimension.y() - 2 * EXTRAITEMS : mGridDimension.x() - 2 * EXTRAITEMS; + float dimOpposite = isVertical() ? mGridDimension.x() : mGridDimension.y(); - int centralCol = (int)(dimScrollable - 0.5) / 2; - int maxCentralCol = (int)(dimScrollable) / 2; - - int oldCol = (mLastCursor / dimOpposite); - int col = (mCursor / dimOpposite); + int centralCol = (int)(dimScrollable - 0.5) / 2; + int maxCentralCol = (int)(dimScrollable) / 2; - int lastCol = ((mEntries.size() - 1) / dimOpposite); - - int lastScroll = std::max(0, (int) (lastCol + 1 - dimScrollable)); - - float startPos = 0; - float endPos = 1; + int oldCol = (mLastCursor / dimOpposite); + int col = (mCursor / dimOpposite); - if (isAnimationPlaying(2)) - { - startPos = -(mCamera*2.0/3.0); - if (startPos < -1) - startPos = -1; + int lastCol = ((mEntries.size() - 1) / dimOpposite); - cancelAnimation(2); - updateTiles(direction, false); - } - + int lastScroll = std::max(0, (int)(lastCol + 1 - dimScrollable)); + + float startPos = 0; + float endPos = 1; + + if (isAnimationPlaying(2)) + { + startPos = -(mCamera*2.0 / 3.0); + if (startPos < -1) + startPos = -1; + + cancelAnimation(2); + updateTiles(direction, false); + } + + if (GuiComponent::ALLOWANIMATIONS) + { std::shared_ptr oldTile = nullptr; std::shared_ptr newTile = nullptr; - + int oldIdx = mLastCursor - mStartPosition + (dimOpposite * EXTRAITEMS); if (oldIdx >= 0 && oldIdx < mTiles.size()) oldTile = mTiles[oldIdx]; @@ -469,121 +515,73 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) int newIdx = mCursor - mStartPosition + (dimOpposite * EXTRAITEMS); if (newIdx >= 0 && newIdx < mTiles.size()) newTile = mTiles[newIdx]; - + Vector3f oldPos = Vector3f(0, 0); - + if (oldTile != nullptr) { - oldPos = oldTile->getPosition(); + oldPos = oldTile->getBackgroundPosition(); oldTile->setSelected(false); } - - if (newTile != nullptr) - newTile->setSelected(true, true, oldPos == Vector3f(0, 0) ? nullptr : &oldPos); - - int firstVisibleCol = mStartPosition / dimOpposite; - if ((col < centralCol || (col == 0 && col == centralCol)) && !centerSel) - mStartPosition = 0; - else if ((col - centralCol) > lastScroll && !centerSel) - mStartPosition = lastScroll * dimOpposite; - else if (maxCentralCol != centralCol && col == firstVisibleCol + maxCentralCol || col == firstVisibleCol + centralCol) - { - if (col == firstVisibleCol + maxCentralCol) - mStartPosition = (col - maxCentralCol) * dimOpposite; - else - mStartPosition = (col - centralCol) * dimOpposite; - } - else - { - if (oldCol == firstVisibleCol + maxCentralCol) - mStartPosition = (col - maxCentralCol) * dimOpposite; - else - mStartPosition = (col - centralCol) * dimOpposite; - } + if (newTile != nullptr) + newTile->setSelected(true, true, oldPos == Vector3f(0, 0) ? nullptr : &oldPos); + } - mLastCursor = mCursor; + int firstVisibleCol = mStartPosition / dimOpposite; - mCameraDirection = direction ? -1.0 : 1.0; - mCamera = 0; + if ((col < centralCol || (col == 0 && col == centralCol)) && !centerSel) + mStartPosition = 0; + else if ((col - centralCol) > lastScroll && !centerSel) + mStartPosition = lastScroll * dimOpposite; + else if (maxCentralCol != centralCol && col == firstVisibleCol + maxCentralCol || col == firstVisibleCol + centralCol) + { + if (col == firstVisibleCol + maxCentralCol) + mStartPosition = (col - maxCentralCol) * dimOpposite; + else + mStartPosition = (col - centralCol) * dimOpposite; + } + else + { + if (oldCol == firstVisibleCol + maxCentralCol) + mStartPosition = (col - maxCentralCol) * dimOpposite; + else + mStartPosition = (col - centralCol) * dimOpposite; + } - if (oldStart == mStartPosition) - { - updateTiles(direction, true); + mLastCursor = mCursor; - if (mCursorChangedCallback) - mCursorChangedCallback(state); + mCameraDirection = direction ? -1.0 : 1.0; + mCamera = 0; - return; - } + if (oldStart == mStartPosition || !GuiComponent::ALLOWANIMATIONS) + { + updateTiles(direction, true); if (mCursorChangedCallback) mCursorChangedCallback(state); - - auto func = [this, startPos, endPos](float t) - { - t -= 1; // cubic ease out - float pct = Math::lerp(0, 1, t*t*t + 1); - float x = startPos * (1.0 - pct) + endPos * pct; - mCamera = x; // Math::lerp(0.0f, 1.0f, t); - }; - - setAnimation(new LambdaAnimation(func, 250), 0, [this, direction] { - - mCamera = 0; - updateTiles(direction, false); - }, false, 2); - return; } - else - { - // 1 if scrolling down, -1 if scrolling up - int scrollDirection = mCursor >= mLastCursor ? 1 : -1; - int partialRow = (int)mLastRowPartial; - int maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); + if (mCursorChangedCallback) + mCursorChangedCallback(state); - if (!isVertical()) - maxPos = mStartPosition + ((mGridDimension.x()-2) * (mGridDimension.y() - partialRow)); + auto func = [this, startPos, endPos](float t) + { + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + t = startPos * (1.0 - pct) + endPos * pct; - if (mCursor == 0) - mStartPosition = 0; - else if (mCursor < mStartPosition) - mStartPosition = std::max(0, mStartPosition - mGridDimension.x()); - else if (mCursor >= maxPos) - { - if (!isVertical()) - { - mStartPosition = mStartPosition + mGridDimension.y(); - maxPos = mStartPosition + ((mGridDimension.x()-2) * (mGridDimension.y() - partialRow)); - } - else - { - mStartPosition = mStartPosition + mGridDimension.x(); - maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); - } - - if (mCursor >= maxPos) - { - int cursorRow = mCursor / mGridDimension.x(); - mStartPosition = cursorRow * mGridDimension.x(); - if (mStartPosition >= mGridDimension.x() && cursorRow > 0) - { - cursorRow--; - mStartPosition = cursorRow * mGridDimension.x(); - } - } - } - } + mCamera = t; + }; - updateTiles(mCursor >= mLastCursor); + setAnimation(new LambdaAnimation(func, 250), 0, [this, direction] { - if (mCursorChangedCallback) - mCursorChangedCallback(state); + mCamera = 0; + updateTiles(direction, false); - mLastCursor = mCursor; + }, false, 2); } @@ -644,6 +642,7 @@ void ImageGridComponent::updateTiles(bool ascending, bool allowAnimation) } mLastCursor = mCursor; + mEntriesDirty = false; } @@ -674,9 +673,9 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA tile->setImage(mDefaultFolderTexture); else tile->setImage(mDefaultGameTexture); - - if (mAllowVideo) - { + + if (mAllowVideo && tile->isSelected()) + { std::string videoPath = mEntries.at(imgPos).data.videoPath; if (ResourceManager::getInstance()->fileExists(videoPath)) @@ -688,7 +687,7 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA } else tile->setVideo(""); - + if (imgPos == mCursor && mCursor != mLastCursor) { int dif = mCursor - tilePos; @@ -697,7 +696,7 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA if (idx < 0 || idx >= mTiles.size()) idx = 0; - tile->setSelected(true, allowAnimation, &mTiles.at(idx)->getPosition()); + tile->setSelected(true, allowAnimation, &mTiles.at(idx)->getBackgroundPosition()); } else tile->setSelected(imgPos == mCursor, allowAnimation); @@ -709,6 +708,9 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA template void ImageGridComponent::buildTiles() { + if (mGridSizeOverride.x() != 0 && mGridSizeOverride.y() != 0) + mAutoLayout = mGridSizeOverride; + mStartPosition = 0; mTiles.clear(); @@ -719,8 +721,8 @@ void ImageGridComponent::buildTiles() if (mAutoLayout.x() != 0 && mAutoLayout.y() != 0) { - auto x = (mSize.x() - (mMargin.x() * (mAutoLayout.x() - 1)) - 2 * mPadding.x()) / (int) mAutoLayout.x(); - auto y = (mSize.y() - (mMargin.y() * (mAutoLayout.y() - 1)) - 2 * mPadding.y()) / (int) mAutoLayout.y(); + auto x = (mSize.x() - (mMargin.x() * (mAutoLayout.x() - 1)) - mPadding.x() - mPadding.z()) / (int) mAutoLayout.x(); + auto y = (mSize.y() - (mMargin.y() * (mAutoLayout.y() - 1)) - mPadding.y() - mPadding.w()) / (int) mAutoLayout.y(); tileSize = Vector2f(x, y); mTileSize = tileSize; @@ -732,7 +734,7 @@ void ImageGridComponent::buildTiles() Vector2f bufferSize = Vector2f(/*vert && mGridDimension.y() == 1 ? tileDistance.x() :*/ 0, 0); Vector2f startPosition = tileSize / 2 - bufferSize; - startPosition += mPadding; + startPosition += Vector2f(mPadding.x(), mPadding.y()); int X, Y; diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 1413e2fbe2..82753fb796 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -30,6 +30,25 @@ MenuComponent::MenuComponent(Window* window, std::string title, const std::share mList = std::make_shared(mWindow); mGrid.setEntry(mList, Vector2i(0, 1), true); + mGrid.setUnhandledInputCallback([this](InputConfig* config, Input input) -> bool { + if (config->isMappedLike("down", input)) { + mGrid.setCursorTo(mList); + mList->setCursorIndex(0); + return true; + } + if (config->isMappedLike("up", input)) { + mList->setCursorIndex(mList->size() - 1); + if (mButtons.size()) { + mGrid.moveCursor(Vector2i(0, 1)); + } + else { + mGrid.setCursorTo(mList); + } + return true; + } + return false; + }); + updateGrid(); updateSize(); @@ -119,6 +138,43 @@ std::vector MenuComponent::getHelpPrompts() return mGrid.getHelpPrompts(); } +std::shared_ptr makeMultiDimButtonGrid(Window* window, const std::vector< std::vector< std::shared_ptr > >& buttons, float outerWidth) +{ + + const int sizeX = (int)buttons.at(0).size(); + const int sizeY = (int)buttons.size(); + const float buttonHeight = buttons.at(0).at(0)->getSize().y(); + const float gridHeight = (buttonHeight + BUTTON_GRID_VERT_PADDING + 2) * sizeY; + + float horizPadding = (float)BUTTON_GRID_HORIZ_PADDING; + float gridWidth, buttonWidth; + +// do { + gridWidth = outerWidth - horizPadding; // to get centered because size * (button size + BUTTON_GRID_VERT_PADDING) let a half BUTTON_GRID_VERT_PADDING left / right marge + buttonWidth = (gridWidth / sizeX) - horizPadding; + // horizPadding -= 2; +// } while ((buttonWidth < 100) && (horizPadding > 2)); + + std::shared_ptr grid = std::make_shared(window, Vector2i(sizeX, sizeY)); + + grid->setSize(gridWidth, gridHeight); + + for (int x = 0; x < sizeX; x++) + grid->setColWidthPerc(x, (float)1 / sizeX); + + for (int y = 0; y < sizeY; y++) + { + for (int x = 0; x < sizeX; x++) + { + const std::shared_ptr& button = buttons.at(y).at(x); + button->setSize(buttonWidth, buttonHeight); + grid->setEntry(button, Vector2i(x, y), true, false); + } + } + + return grid; +} + std::shared_ptr makeButtonGrid(Window* window, const std::vector< std::shared_ptr >& buttons) { std::shared_ptr buttonGrid = std::make_shared(window, Vector2i((int)buttons.size(), 2)); diff --git a/es-core/src/components/MenuComponent.h b/es-core/src/components/MenuComponent.h index d5a8db473c..aa4928702c 100644 --- a/es-core/src/components/MenuComponent.h +++ b/es-core/src/components/MenuComponent.h @@ -13,6 +13,7 @@ class ButtonComponent; class ImageComponent; std::shared_ptr makeButtonGrid(Window* window, const std::vector< std::shared_ptr >& buttons); +std::shared_ptr makeMultiDimButtonGrid(Window* window, const std::vector< std::vector< std::shared_ptr > >& buttons, float outerWidth); std::shared_ptr makeArrow(Window* window); #define TITLE_VERT_PADDING (Renderer::getScreenHeight()*0.0637f) diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index f3ff0bba01..911f85a676 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -11,6 +11,7 @@ NinePatchComponent::NinePatchComponent(Window* window, const std::string& path, mPath(path), mVertices(NULL), mColors(NULL) { + mPreviousSize = Vector2f(0, 0); if(!mPath.empty()) buildVertices(); } @@ -26,6 +27,9 @@ NinePatchComponent::~NinePatchComponent() void NinePatchComponent::updateColors() { + if (mColors == NULL) + return; + Renderer::buildGLColorArray(mColors, mEdgeColor, 6 * 9); Renderer::buildGLColorArray(&mColors[4 * 6 * 4], mCenterColor, 6); } @@ -143,6 +147,10 @@ void NinePatchComponent::render(const Transform4x4f& parentTrans) void NinePatchComponent::onSizeChanged() { + if (mPreviousSize == mSize) + return; + + mPreviousSize = mSize; buildVertices(); } @@ -153,6 +161,9 @@ const Vector2f& NinePatchComponent::getCornerSize() const void NinePatchComponent::setCornerSize(int sizeX, int sizeY) { + if (mCornerSize.x() == sizeX && mCornerSize.y() == sizeY) + return; + mCornerSize = Vector2f(sizeX, sizeY); buildVertices(); } diff --git a/es-core/src/components/NinePatchComponent.h b/es-core/src/components/NinePatchComponent.h index 446e8bbd23..b197e9adfd 100644 --- a/es-core/src/components/NinePatchComponent.h +++ b/es-core/src/components/NinePatchComponent.h @@ -59,6 +59,7 @@ class NinePatchComponent : public GuiComponent std::string mPath; Vector2f mCornerSize; + Vector2f mPreviousSize; unsigned int mEdgeColor; unsigned int mCenterColor; std::shared_ptr mTexture; diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index 62572d39a9..3bc1f89e56 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -7,8 +7,11 @@ SwitchComponent::SwitchComponent(Window* window, bool state) : GuiComponent(wind { auto menuTheme = ThemeData::getMenuTheme(); - mImage.setImage(ThemeData::getMenuTheme()->Icons.off); - mImage.setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight()); + float height = Font::get(FONT_SIZE_MEDIUM)->getLetterHeight(); + Vector2f maxSize = Vector2f(height, height); + + mImage.setImage(ThemeData::getMenuTheme()->Icons.off, false, maxSize); + mImage.setResize(0, height); mImage.setColorShift(menuTheme->Text.color); mSize = mImage.getSize(); diff --git a/es-core/src/components/TextEditComponent.h b/es-core/src/components/TextEditComponent.h index 74e743a2e9..bcfbf96cae 100644 --- a/es-core/src/components/TextEditComponent.h +++ b/es-core/src/components/TextEditComponent.h @@ -34,10 +34,11 @@ class TextEditComponent : public GuiComponent virtual std::vector getHelpPrompts() override; -private: void startEditing(); void stopEditing(); +private: + void onTextChanged(); void onCursorChanged(); diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 3586232b6a..067103bb6a 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -65,6 +65,9 @@ VideoComponent::VideoComponent(Window* window) : mTargetIsMax(false), mTargetSize(0, 0) { + mFadeIn = 0.0f; + mIsWaitingForVideoToStart = false; + // Setup the default configuration mConfig.showSnapshotDelay = false; mConfig.showSnapshotNoVideo = false; @@ -94,7 +97,7 @@ void VideoComponent::onOriginChanged() void VideoComponent::onSizeChanged() { - // Update the embeded static image + // Update the embeded static image mStaticImage.onSizeChanged(); } @@ -149,6 +152,8 @@ void VideoComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); GuiComponent::renderChildren(trans); + VideoComponent::renderSnapshot(parentTrans); + Renderer::setMatrix(trans); // Handle the case where the video is delayed @@ -162,10 +167,13 @@ void VideoComponent::renderSnapshot(const Transform4x4f& parentTrans) { // This is the case where the video is not currently being displayed. Work out // if we need to display a static image - if ((mConfig.showSnapshotNoVideo && mVideoPath.empty()) || (mStartDelayed && mConfig.showSnapshotDelay)) - { - // Display the static image instead - mStaticImage.setOpacity((unsigned char)(mFadeIn * 255.0f)); + if ((mConfig.showSnapshotNoVideo && mVideoPath.empty()) || ((mStartDelayed || mFadeIn < 1.0) && mConfig.showSnapshotDelay)) + { + float t = 1.0 - mFadeIn; + t -= 1; // cubic ease out + t = Math::lerp(0, 1, t*t*t + 1); + + mStaticImage.setOpacity((unsigned char)(t * 255.0f)); mStaticImage.render(parentTrans); } } @@ -244,7 +252,7 @@ void VideoComponent::handleLooping() void VideoComponent::handleStartDelay() { // Only play if any delay has timed out - if (!mStartDelayed) + if (!mStartDelayed || mIsWaitingForVideoToStart) return; // Timeout not yet completed @@ -255,16 +263,38 @@ void VideoComponent::handleStartDelay() mStartDelayed = false; // Clear the playing flag so startVideo works mIsPlaying = false; + + mIsWaitingForVideoToStart = true; + startVideo(); - mFadeIn = 0.0f; - mIsPlaying = true; + if (mIsPlaying) + mIsWaitingForVideoToStart = false; + +// mFadeIn = 0.0f; +// mIsPlaying = true; +} + +void VideoComponent::onVideoStarted() +{ + mIsWaitingForVideoToStart = false; + + if (mConfig.startDelay == 0 || PowerSaver::getMode() == PowerSaver::INSTANT) + { + mFadeIn = 1.0f; + mIsPlaying = true; + } + else + { + mFadeIn = 0.0f; + mIsPlaying = true; + } } void VideoComponent::startVideoWithDelay() { // If not playing then either start the video or initiate the delay - if (mIsPlaying || mStartDelayed) + if (mIsPlaying || mStartDelayed || mIsWaitingForVideoToStart) return; // Set the video that we are going to be playing so we don't attempt to restart it @@ -275,10 +305,13 @@ void VideoComponent::startVideoWithDelay() // No delay. Just start the video mStartDelayed = false; mIsPlaying = false; + + mIsWaitingForVideoToStart = true; + startVideo(); - mFadeIn = 0.0f; - mIsPlaying = true; + if (mIsPlaying) + mIsWaitingForVideoToStart = false; } else { @@ -326,16 +359,20 @@ void VideoComponent::update(int deltaTime) void VideoComponent::manageState() { + if (mIsWaitingForVideoToStart && mIsPlaying) + mIsWaitingForVideoToStart = false; + // We will only show if the component is on display and the screensaver // is not active bool show = mShowing && !mScreensaverActive && !mDisable; // See if we're already playing - if (mIsPlaying) + if (mIsPlaying || mIsWaitingForVideoToStart) { // If we are not on display then stop the video from playing if (!show) { + mIsWaitingForVideoToStart = false; stopVideo(); } else @@ -344,6 +381,7 @@ void VideoComponent::manageState() { // Path changed. Stop the video. We will start it again below because // mIsPlaying will be modified by stopVideo to be false + mIsWaitingForVideoToStart = false; stopVideo(); } } diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index 0a306504ad..45c236d100 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -77,10 +77,16 @@ class VideoComponent : public GuiComponent return mIsPlaying; } + bool isWaitingForVideoToStart() { + return mIsWaitingForVideoToStart; + } + bool isFading() { return mFadeIn < 1.0f; } + void onVideoStarted(); + private: // Start the video Immediately virtual void startVideo() = 0; @@ -99,6 +105,7 @@ class VideoComponent : public GuiComponent void manageState(); protected: + unsigned mVideoWidth; unsigned mVideoHeight; Vector2f mTargetSize; @@ -118,6 +125,8 @@ class VideoComponent : public GuiComponent bool mScreensaverMode; bool mTargetIsMax; + bool mIsWaitingForVideoToStart; + Configuration mConfig; }; diff --git a/es-core/src/components/VideoPlayerComponent.cpp b/es-core/src/components/VideoPlayerComponent.cpp index eeaba91775..a98e8e097c 100644 --- a/es-core/src/components/VideoPlayerComponent.cpp +++ b/es-core/src/components/VideoPlayerComponent.cpp @@ -32,8 +32,8 @@ void VideoPlayerComponent::render(const Transform4x4f& parentTrans) { VideoComponent::render(parentTrans); - if (!mIsPlaying || mPlayerPid == -1) - VideoComponent::renderSnapshot(parentTrans); +// if (!mIsPlaying || mPlayerPid == -1) + // VideoComponent::renderSnapshot(parentTrans); } void VideoPlayerComponent::setResize(float width, float height) diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 54879c36f5..c15b676fce 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -31,8 +31,14 @@ static void unlock(void *data, void* /*id*/, void *const* /*p_pixels*/) { } // VLC wants to display a video frame. -static void display(void* /*data*/, void* /*id*/) { - //Data to be displayed +static void display(void* data, void* id) +{ + if (data == NULL) + return; + + struct VideoContext *c = (struct VideoContext *)data; + if (c->valid && c->component != NULL && !c->component->isPlaying() && c->component->isWaitingForVideoToStart()) + c->component->onVideoStarted(); } VideoVlcComponent::VideoVlcComponent(Window* window, std::string subtitles) : @@ -132,6 +138,7 @@ void VideoVlcComponent::resize() void VideoVlcComponent::render(const Transform4x4f& parentTrans) { VideoComponent::render(parentTrans); + float x, y; Transform4x4f trans = parentTrans * getTransform(); @@ -176,11 +183,20 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) vertices[4].tex[0] = -tex_offs_x; vertices[4].tex[1] = 1.0f + tex_offs_y; vertices[5].tex[0] = 1.0f + tex_offs_x; vertices[5].tex[1] = 1.0f + tex_offs_y; + float t = mFadeIn; + if (mFadeIn < 1.0) + { + t = 1.0 - mFadeIn; + t -= 1; // cubic ease in + t = Math::lerp(0, 1, t*t*t + 1); + t = 1.0 - t; + } + // Colours - use this to fade the video in and out for (int i = 0; i < (4 * 6); ++i) { if ((i%4) == 3) - vertices[i / 4].colour[i % 4] = mFadeIn; + vertices[i / 4].colour[i % 4] = t; else vertices[i / 4].colour[i % 4] = 1.0f; } @@ -210,9 +226,7 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) glDisable(GL_TEXTURE_2D); glDisable(GL_BLEND); - } else { - VideoComponent::renderSnapshot(parentTrans); - } + } } void VideoVlcComponent::setupContext() @@ -222,6 +236,7 @@ void VideoVlcComponent::setupContext() // Create an RGBA surface to render the video into mContext.surface = SDL_CreateRGBSurface(SDL_SWSURFACE, (int)mVideoWidth, (int)mVideoHeight, 32, 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff); mContext.mutex = SDL_CreateMutex(); + mContext.component = this; mContext.valid = true; resize(); } @@ -233,6 +248,8 @@ void VideoVlcComponent::freeContext() { SDL_FreeSurface(mContext.surface); SDL_DestroyMutex(mContext.mutex); + + mContext.component = NULL; mContext.valid = false; } } @@ -351,12 +368,10 @@ void VideoVlcComponent::startVideo() libvlc_media_player_play(mMediaPlayer); libvlc_video_set_callbacks(mMediaPlayer, lock, unlock, display, (void*)&mContext); libvlc_video_set_format(mMediaPlayer, "RGBA", (int)mVideoWidth, (int)mVideoHeight, (int)mVideoWidth * 4); - - // libvlc_media_player_set_position(mMediaPlayer, 0.15); - + // Update the playing state - mIsPlaying = true; - mFadeIn = 0.0f; + //mIsPlaying = true; + //mFadeIn = 0.0f; } } } diff --git a/es-core/src/components/VideoVlcComponent.h b/es-core/src/components/VideoVlcComponent.h index c350b8cb3c..4a1a27bcba 100644 --- a/es-core/src/components/VideoVlcComponent.h +++ b/es-core/src/components/VideoVlcComponent.h @@ -13,6 +13,7 @@ struct libvlc_media_player_t; struct VideoContext { SDL_Surface* surface; SDL_mutex* mutex; + VideoComponent* component; bool valid; }; diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp new file mode 100644 index 0000000000..c4a3b26f45 --- /dev/null +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp @@ -0,0 +1,266 @@ +#include "guis/GuiTextEditPopupKeyboard.h" +#include "components/MenuComponent.h" +#include "Log.h" +#include "utils/StringUtil.h" + +std::vector> kbFrench { + { UNICODE_CHARS("&"), UNICODE_CHARS("é"), UNICODE_CHARS("\""), UNICODE_CHARS("'"), UNICODE_CHARS("("), UNICODE_CHARS("#"), UNICODE_CHARS("è"), UNICODE_CHARS("!"), UNICODE_CHARS("ç"), UNICODE_CHARS("à"), UNICODE_CHARS(")"), UNICODE_CHARS("-") }, + { UNICODE_CHARS("1"), UNICODE_CHARS("2"), UNICODE_CHARS("3"), UNICODE_CHARS("4"), UNICODE_CHARS("5"), UNICODE_CHARS("6"), UNICODE_CHARS("7"), UNICODE_CHARS("8"), UNICODE_CHARS("9"), UNICODE_CHARS("0"), UNICODE_CHARS("@"), UNICODE_CHARS("_") }, + /* + { UNICODE_CHARS("à"), UNICODE_CHARS("ä"), UNICODE_CHARS("ë"), UNICODE_CHARS("ì"), UNICODE_CHARS("ï"), UNICODE_CHARS("ò"), UNICODE_CHARS("ö"), UNICODE_CHARS("ü"), UNICODE_CHARS("\\"), UNICODE_CHARS("|"), UNICODE_CHARS("§"), UNICODE_CHARS("°") }, + { UNICODE_CHARS("á"), UNICODE_CHARS("â"), UNICODE_CHARS("ê"), UNICODE_CHARS("í"), UNICODE_CHARS("î"), UNICODE_CHARS("ó"), UNICODE_CHARS("ô"), UNICODE_CHARS("ú", "û"), UNICODE_CHARS("ñ"), UNICODE_CHARS("¡"), UNICODE_CHARS("¿") }, + */ + { UNICODE_CHARS("a"), UNICODE_CHARS("z"), UNICODE_CHARS("e"), UNICODE_CHARS("r"), UNICODE_CHARS("t"), UNICODE_CHARS("y"), UNICODE_CHARS("u"), UNICODE_CHARS("i"), UNICODE_CHARS("o"), UNICODE_CHARS("p"), UNICODE_CHARS("^"), UNICODE_CHARS("$") }, + { UNICODE_CHARS("A"), UNICODE_CHARS("Z"), UNICODE_CHARS("E"), UNICODE_CHARS("R"), UNICODE_CHARS("T"), UNICODE_CHARS("Y"), UNICODE_CHARS("U"), UNICODE_CHARS("I"), UNICODE_CHARS("O"), UNICODE_CHARS("P"), UNICODE_CHARS("¨"), UNICODE_CHARS("*") }, + + { UNICODE_CHARS("q"), UNICODE_CHARS("s"), UNICODE_CHARS("d"), UNICODE_CHARS("f"), UNICODE_CHARS("g"), UNICODE_CHARS("h"), UNICODE_CHARS("j"), UNICODE_CHARS("k"), UNICODE_CHARS("l"), UNICODE_CHARS("m"), UNICODE_CHARS("ù"), UNICODE_CHARS("`") }, + { UNICODE_CHARS("Q"), UNICODE_CHARS("S"), UNICODE_CHARS("D"), UNICODE_CHARS("F"), UNICODE_CHARS("G"), UNICODE_CHARS("H"), UNICODE_CHARS("J"), UNICODE_CHARS("K"), UNICODE_CHARS("L"), UNICODE_CHARS("M"), UNICODE_CHARS("%"), UNICODE_CHARS("£") }, + + //SHIFT key at position 0 + { UNICODE_CHARS("SHIFT"), UNICODE_CHARS("<"), UNICODE_CHARS("w"), UNICODE_CHARS("x"), UNICODE_CHARS("c"), UNICODE_CHARS("v"), UNICODE_CHARS("b"), UNICODE_CHARS("n"), UNICODE_CHARS(","), UNICODE_CHARS(";"), UNICODE_CHARS(":"), UNICODE_CHARS("=") }, + { UNICODE_CHARS("SHIFT"), UNICODE_CHARS(">"), UNICODE_CHARS("W"), UNICODE_CHARS("X"), UNICODE_CHARS("C"), UNICODE_CHARS("V"), UNICODE_CHARS("B"), UNICODE_CHARS("N"), UNICODE_CHARS("?"), UNICODE_CHARS("."), UNICODE_CHARS("/"), UNICODE_CHARS("+") } +}; + +std::vector> kbUs { + + { UNICODE_CHARS("1"), UNICODE_CHARS("2"), UNICODE_CHARS("3"), UNICODE_CHARS("4"), UNICODE_CHARS("5"), UNICODE_CHARS("6"), UNICODE_CHARS("7"), UNICODE_CHARS("8"), UNICODE_CHARS("9"), UNICODE_CHARS("0"), UNICODE_CHARS("_"), UNICODE_CHARS("+") }, + { UNICODE_CHARS("!"), UNICODE_CHARS("@"), UNICODE_CHARS("#"), UNICODE_CHARS("$"), UNICODE_CHARS("%"), UNICODE_CHARS("^"), UNICODE_CHARS("&"), UNICODE_CHARS("*"), UNICODE_CHARS("("), UNICODE_CHARS(")"), UNICODE_CHARS("-"), UNICODE_CHARS("=") }, +/* + { UNICODE_CHARS("à"), UNICODE_CHARS("ä"), UNICODE_CHARS("è"), UNICODE_CHARS("ë"), UNICODE_CHARS("ì"), UNICODE_CHARS("ï"), UNICODE_CHARS("ò"), UNICODE_CHARS("ö"), UNICODE_CHARS("ù"), UNICODE_CHARS("ü"), UNICODE_CHARS("¨"), UNICODE_CHARS("¿") }, + { UNICODE_CHARS("á"), UNICODE_CHARS("â"), UNICODE_CHARS("é"), UNICODE_CHARS("ê"), UNICODE_CHARS("í"), UNICODE_CHARS("î"), UNICODE_CHARS("ó"), UNICODE_CHARS("ô"), UNICODE_CHARS("ú"), UNICODE_CHARS("û"), UNICODE_CHARS("ñ"), UNICODE_CHARS("¡") }, + */ + { UNICODE_CHARS("q"), UNICODE_CHARS("w"), UNICODE_CHARS("e"), UNICODE_CHARS("r"), UNICODE_CHARS("t"), UNICODE_CHARS("y"), UNICODE_CHARS("u"), UNICODE_CHARS("i"), UNICODE_CHARS("o"), UNICODE_CHARS("p"), UNICODE_CHARS("{"), UNICODE_CHARS("}") }, + { UNICODE_CHARS("Q"), UNICODE_CHARS("W"), UNICODE_CHARS("E"), UNICODE_CHARS("R"), UNICODE_CHARS("T"), UNICODE_CHARS("Y"), UNICODE_CHARS("U"), UNICODE_CHARS("I"), UNICODE_CHARS("O"), UNICODE_CHARS("P"), UNICODE_CHARS("["), UNICODE_CHARS("]") }, + + { UNICODE_CHARS("a"), UNICODE_CHARS("s"), UNICODE_CHARS("d"), UNICODE_CHARS("f"), UNICODE_CHARS("g"), UNICODE_CHARS("h"), UNICODE_CHARS("j"), UNICODE_CHARS("k"), UNICODE_CHARS("l"), UNICODE_CHARS(";"), UNICODE_CHARS("\""), UNICODE_CHARS("|") }, + { UNICODE_CHARS("A"), UNICODE_CHARS("S"), UNICODE_CHARS("D"), UNICODE_CHARS("F"), UNICODE_CHARS("G"), UNICODE_CHARS("H"), UNICODE_CHARS("J"), UNICODE_CHARS("K"), UNICODE_CHARS("L"), UNICODE_CHARS(":"), UNICODE_CHARS("'"), UNICODE_CHARS("\\") }, + + { UNICODE_CHARS("SHIFT"), UNICODE_CHARS("~"), UNICODE_CHARS("z"), UNICODE_CHARS("x"), UNICODE_CHARS("c"), UNICODE_CHARS("v"), UNICODE_CHARS("b"), UNICODE_CHARS("n"), UNICODE_CHARS("m"), UNICODE_CHARS(","), UNICODE_CHARS("."), UNICODE_CHARS("?") }, + { UNICODE_CHARS("SHIFT"), UNICODE_CHARS("`"), UNICODE_CHARS("Z"), UNICODE_CHARS("X"), UNICODE_CHARS("C"), UNICODE_CHARS("V"), UNICODE_CHARS("B"), UNICODE_CHARS("N"), UNICODE_CHARS("M"), UNICODE_CHARS("<"), UNICODE_CHARS(">"), UNICODE_CHARS("/") }, +}; + +GuiTextEditPopupKeyboard::GuiTextEditPopupKeyboard(Window* window, const std::string& title, const std::string& initValue, + const std::function& okCallback, bool multiLine, const char* acceptBtnText) + : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 7)), mMultiLine(multiLine) +{ + auto theme = ThemeData::getMenuTheme(); + + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + + addChild(&mBackground); + addChild(&mGrid); + + mTitle = std::make_shared(mWindow, Utils::String::toUpper(title), ThemeData::getMenuTheme()->Title.font, ThemeData::getMenuTheme()->Title.color, ALIGN_CENTER); + + mText = std::make_shared(mWindow); + mText->setValue(initValue); + + if(!multiLine) + mText->setCursor(initValue.size()); + + // Header + mGrid.setEntry(mTitle, Vector2i(0, 0), false, true); + + // Text edit add + mGrid.setEntry(mText, Vector2i(0, 1), true, false, Vector2i(1, 1)); + + std::vector< std::vector< std::shared_ptr > > buttonList; + + // Keyboard + // Case for if multiline is enabled, then don't create the keyboard. + if (!mMultiLine) + { + std::vector> &layout = kbUs; + + if (GuiTextTool::getLanguage() == "fr") + layout = kbFrench; + + for (unsigned int i = 0; i < layout.size() / 2; i++) + { + std::vector> buttons; + for (unsigned int j = 0; j < layout[2 * i].size(); j++) + { +#ifdef WIN32 + std::wstring toConvert = layout[2 * i][j]; + std::string atj = GuiTextTool::convertFromWideString(toConvert); + + toConvert = layout[2 * i + 1][j]; + std::string atjs = GuiTextTool::convertFromWideString(toConvert); +#else + std::string atj = layout[2 * i][j]; + std::string atjs = layout[2 * i + 1][j]; + +#endif + + if (atj == "SHIFT") + { + // Special case for shift key + mShiftButton = std::make_shared(mWindow, "SHIFT", _T("SHIFTS FOR UPPER,LOWER, AND SPECIAL"), [this] { + shiftKeys(); + }); + buttons.push_back(mShiftButton); + } + else + buttons.push_back(makeButton(atj, atjs)); + } + buttonList.push_back(buttons); + } + } + + const float gridWidth = Renderer::getScreenWidth() * 0.85f; + mKeyboardGrid = makeMultiDimButtonGrid(mWindow, buttonList, gridWidth - 20); + mGrid.setEntry(mKeyboardGrid, Vector2i(0, 2), true, false); + + // Accept/Cancel buttons + buttons.push_back(std::make_shared(mWindow, _L(acceptBtnText), _L(acceptBtnText), [this, okCallback] { okCallback(mText->getValue()); delete this; })); + + buttons.push_back(std::make_shared(mWindow, _T("SPACE"), _T("SPACE"), [this] { + mText->startEditing(); + mText->textInput(" "); + mText->stopEditing(); + })); + + buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE A CHAR"), [this] { + mText->startEditing(); + mText->textInput("\b"); + mText->stopEditing(); + })); + + buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("discard changes"), [this] { delete this; })); + + mButtons = makeButtonGrid(mWindow, buttons); + mGrid.setEntry(mButtons, Vector2i(0, 3), true, false); + + // Determine size from text size + float textHeight = mText->getFont()->getHeight(); + if (multiLine) + textHeight *= 6; + mText->setSize(gridWidth - 40, textHeight); + + // If multiline, set all diminsions back to default, else draw size for keyboard. + if (mMultiLine) + { + setSize(Renderer::getScreenWidth() * 0.5f, mTitle->getFont()->getHeight() + textHeight + mKeyboardGrid->getSize().y() + 40); + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); + } + else + { + setSize(gridWidth, mTitle->getFont()->getHeight() + textHeight + 40 + mKeyboardGrid->getSize().y() + mButtons->getSize().y()); + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); + } +} + +void GuiTextEditPopupKeyboard::onSizeChanged() +{ + mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); + + mText->setSize(mSize.x() - 40, mText->getSize().y()); + + float fullHeight = mTitle->getFont()->getHeight() + mText->getSize().y() + mKeyboardGrid->getSize().y() + mButtons->getSize().y(); + + // update grid + mGrid.setRowHeightPerc(0, mTitle->getFont()->getHeight() / fullHeight); + mGrid.setRowHeightPerc(1, mText->getSize().y() / fullHeight); + mGrid.setRowHeightPerc(2, mKeyboardGrid->getSize().y() / fullHeight); + mGrid.setRowHeightPerc(3, mButtons->getSize().y() / fullHeight); + + mGrid.setSize(mSize); + + mKeyboardGrid->onSizeChanged(); + /* + mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); + + mText->setSize(mSize.x() - 40, mText->getSize().y()); + + // update grid + mGrid.setRowHeightPerc(0, mTitle->getFont()->getHeight() / mSize.y()); + mGrid.setRowHeightPerc(2, mKeyboardGrid->getSize().y() / mSize.y()); + + mGrid.setSize(mSize);*/ +} + +bool GuiTextEditPopupKeyboard::input(InputConfig* config, Input input) +{ + if (GuiComponent::input(config, input)) + return true; + + // pressing back when not text editing closes us + if (config->isMappedTo("b", input) && input.value) + { + delete this; + return true; + } + + // For deleting a chara (Left Top Button) + if (config->isMappedTo("lefttop", input) && input.value) { + mText->startEditing(); + mText->textInput("\b"); + mText->stopEditing(); + } + + // For Adding a space (Right Top Button) + if (config->isMappedTo("righttop", input) && input.value) { + mText->startEditing(); + mText->textInput(" "); + } + + // For Shifting (X) + if (config->isMappedTo("x", input) && input.value) { + if (mShift) mShift = false; + else mShift = true; + shiftKeys(); + } + + + + return false; +} + +void GuiTextEditPopupKeyboard::update(int deltatime) { + +} + +std::shared_ptr GuiTextEditPopupKeyboard::makeButton(const std::string& key, const std::string& shiftedKey) +{ + std::shared_ptr button = std::make_shared(mWindow, key, key, [this, key, shiftedKey] { + mText->startEditing(); + if (mShift) + mText->textInput(shiftedKey.c_str()); + else + mText->textInput(key.c_str()); + mText->stopEditing(); + }, false); + KeyboardButton kb(button, key, shiftedKey); + keyboardButtons.push_back(kb); + return button; +} + +// Shifts the keys when user hits the shift button. +void GuiTextEditPopupKeyboard::shiftKeys() +{ + mShift = !mShift; + + if (mShift) + mShiftButton->setColorShift(0xFF0000FF); + else + mShiftButton->removeColorShift(); + + for (auto & kb : keyboardButtons) + { + const std::string& text = mShift ? kb.shiftedKey : kb.key; + kb.button->setText(text, text, false); + } +} + +std::vector GuiTextEditPopupKeyboard::getHelpPrompts() +{ + std::vector prompts = mGrid.getHelpPrompts(); + prompts.push_back(HelpPrompt("x", _T("SHIFT"))); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("r", _T("SPACE"))); + prompts.push_back(HelpPrompt("l", _T("DELETE"))); + return prompts; +} + diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.h b/es-core/src/guis/GuiTextEditPopupKeyboard.h new file mode 100644 index 0000000000..62e9813e3d --- /dev/null +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.h @@ -0,0 +1,73 @@ +#include "GuiComponent.h" + +#include "components/NinePatchComponent.h" +#include "components/ButtonComponent.h" +#include "components/ComponentGrid.h" +#include "components/TextEditComponent.h" +#include "components/TextComponent.h" + +class GuiTextEditPopupKeyboard : public GuiComponent +{ +public: + GuiTextEditPopupKeyboard(Window* window, const std::string& title, const std::string& initValue, + const std::function& okCallback, bool multiLine, const char* acceptBtnText = "OK"); + + bool input(InputConfig* config, Input input); + void update(int deltatime) override; + void onSizeChanged(); + std::vector getHelpPrompts() override; + +private: + + class KeyboardButton + { + public: + std::shared_ptr button; + const std::string key; + const std::string shiftedKey; + KeyboardButton(const std::shared_ptr b, const std::string& k, const std::string& sk) : button(b), key(k), shiftedKey(sk) {}; + }; + + std::shared_ptr makeButton(const std::string& key, const std::string& shiftedKey); + + void shiftKeys(); + + NinePatchComponent mBackground; + ComponentGrid mGrid; + + // Vectors for button rows + + std::vector< std::shared_ptr > buttons; + /* + std::vector< std::shared_ptr > kButtons; + std::vector< std::shared_ptr > hButtons; + std::vector< std::shared_ptr > bButtons; + std::vector< std::shared_ptr > digitButtons; + */ + + std::vector keyboardButtons; + std::shared_ptr mShiftButton; + + std::shared_ptr mTitle; + std::shared_ptr mText; + std::shared_ptr mKeyboardGrid; + std::shared_ptr mButtonGrid; + std::shared_ptr mNewGrid; + std::shared_ptr mButtons; + + // Define keyboard key rows. + const char* numRow[10] = { "1","2","3","4","5","6","7","8","9","0" }; + const char* numRowUp[10] = { "!", "@", "#", "$", "%", "^", "&", "*", "(", ")" }; + const char* topRow[10] = { "q","w","e","r","t","y","u","i","o","p" }; + const char* topRowUp[10] = { "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P" }; // Just so I don't have to deal with toupper + const char* homeRow[10] = { "a","s","d","f","g","h","j","k","l",";" }; + const char* homeRowUp[10] = { "A", "S", "D", "F", "G", "H", "J", "K", "L", ":" }; + const char* bottomRow[9] = { "z","x","c","v","b","n","m",",","." }; // Shift is handled in the constructor + const char* bottomRowUp[9] = { "Z", "X", "C", "V", "B", "N", "M", "<", ">" }; + + int mxIndex = 0; // Stores the X index and makes every grid the same. + + bool mMultiLine; + bool mShift = false; + bool mShiftChange = false; +}; diff --git a/es-core/src/math/Vector2f.cpp b/es-core/src/math/Vector2f.cpp index f98f17194c..ddf8fc351a 100644 --- a/es-core/src/math/Vector2f.cpp +++ b/es-core/src/math/Vector2f.cpp @@ -17,3 +17,25 @@ Vector2f& Vector2f::lerp(const Vector2f& _start, const Vector2f& _end, const flo return *this; } // lerp + +const Vector2f Vector2f::parseString(const std::string& _input) +{ + Vector2f ret = Vector2f(0, 0); + + size_t divider = _input.find(' '); + if (divider != std::string::npos) + { + std::string first = _input.substr(0, divider); + std::string second = _input.substr(divider, std::string::npos); + + ret = Vector2f((float)atof(first.c_str()), (float)atof(second.c_str())); + } + + return ret; +} + + +const std::string Vector2f::toString() +{ + return std::to_string(mX) + " " + std::to_string(mY); +} \ No newline at end of file diff --git a/es-core/src/math/Vector2f.h b/es-core/src/math/Vector2f.h index b9c91fda08..93e109d799 100644 --- a/es-core/src/math/Vector2f.h +++ b/es-core/src/math/Vector2f.h @@ -2,6 +2,7 @@ #ifndef ES_CORE_MATH_VECTOR2F_H #define ES_CORE_MATH_VECTOR2F_H +#include #include "math/Misc.h" #include @@ -11,6 +12,8 @@ class Vector4f; class Vector2f { public: + static const Vector2f parseString(const std::string& _input); + const std::string toString(); Vector2f() { } Vector2f(const float _f) : mX(_f), mY(_f) { } diff --git a/es-core/src/platform.h b/es-core/src/platform.h index 398f7c12e6..48b4a169b2 100644 --- a/es-core/src/platform.h +++ b/es-core/src/platform.h @@ -18,6 +18,8 @@ #define sleep Sleep #endif + #define GL_GLEXT_PROTOTYPES + #define GLHEADER #endif diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index fe2a1b6a08..216c2e3d92 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -22,7 +22,6 @@ Font::FontFace::FontFace(ResourceData&& d, int size) : data(d) Font::FontFace::~FontFace() { - if(face) FT_Done_Face(face); } @@ -73,6 +72,7 @@ Font::Font(int size, const std::string& path) : mSize(size), mPath(path) { assert(mSize > 0); + mLoaded = true; mMaxGlyphHeight = 0; if (!sLibrary) @@ -95,17 +95,24 @@ Font::~Font() for (auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); it++) delete it->second; - unload(ResourceManager::getInstance()); + unload(); } -void Font::reload(std::shared_ptr& /*rm*/) +void Font::reload() { + if (mLoaded) + return; + rebuildTextures(); + mLoaded = true; } -void Font::unload(std::shared_ptr& /*rm*/) +void Font::unload() { - unloadTextures(); + if (mLoaded) + unloadTextures(); + + mLoaded = false; } std::shared_ptr Font::get(int size, const std::string& path) diff --git a/es-core/src/resources/Font.h b/es-core/src/resources/Font.h index c9c81af643..2813ab1597 100644 --- a/es-core/src/resources/Font.h +++ b/es-core/src/resources/Font.h @@ -53,8 +53,8 @@ class Font : public IReloadable float getHeight(float lineSpacing = 1.5f) const; float getLetterHeight(); - void unload(std::shared_ptr& rm) override; - void reload(std::shared_ptr& rm) override; + void unload() override; + void reload() override; int getSize() const; inline const std::string& getPath() const { return mPath; } @@ -135,6 +135,9 @@ class Font : public IReloadable float getNewlineStartOffset(const std::string& text, const unsigned int& charStart, const float& xLen, const Alignment& alignment); + + bool mLoaded; + friend TextCache; }; diff --git a/es-core/src/resources/ResourceManager.cpp b/es-core/src/resources/ResourceManager.cpp index 3887a671a4..3f5035b6e7 100644 --- a/es-core/src/resources/ResourceManager.cpp +++ b/es-core/src/resources/ResourceManager.cpp @@ -98,9 +98,9 @@ void ResourceManager::unloadAll() auto iter = mReloadables.cbegin(); while(iter != mReloadables.cend()) { - if(!iter->expired()) + if (!iter->expired()) { - iter->lock()->unload(sInstance); + iter->lock()->unload(); iter++; }else{ iter = mReloadables.erase(iter); @@ -115,7 +115,7 @@ void ResourceManager::reloadAll() { if(!iter->expired()) { - iter->lock()->reload(sInstance); + iter->lock()->reload(); iter++; }else{ iter = mReloadables.erase(iter); diff --git a/es-core/src/resources/ResourceManager.h b/es-core/src/resources/ResourceManager.h index ad3f20133e..f66ed8c7fa 100644 --- a/es-core/src/resources/ResourceManager.h +++ b/es-core/src/resources/ResourceManager.h @@ -20,8 +20,8 @@ class ResourceManager; class IReloadable { public: - virtual void unload(std::shared_ptr& rm) = 0; - virtual void reload(std::shared_ptr& rm) = 0; + virtual void unload() = 0; + virtual void reload() = 0; }; class ResourceManager diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 4c11b02198..76857841cb 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -13,8 +13,20 @@ #define DPI 96 +#ifdef WIN32 +void traceOutput(std::string string) +{ +#if _DEBUG + OutputDebugStringA(string.c_str()); +#endif +} +#endif + + +bool TextureData::OPTIMIZEVRAM = false; + TextureData::TextureData(bool tile) : mTile(tile), mTextureID(0), mDataRGBA(nullptr), mScalable(false), - mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f), mMaxSize(Vector2f(0,0)) + mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f), mMaxSize(Vector2f(0,0)), mPackedSize(Vector2i(0,0)), mBaseSize(Vector2i(0, 0)) { } @@ -61,8 +73,14 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length mSourceWidth = svgImage->width; mSourceHeight = svgImage->height; } - mWidth = (size_t)Math::round(mSourceWidth); - mHeight = (size_t)Math::round(mSourceHeight); + else + mSourceWidth = (mSourceHeight * svgImage->width) / svgImage->height; // FCATMP : Always keep source aspect ratio + +// mWidth = (size_t)Math::round(mSourceWidth); +// mHeight = (size_t)Math::round(mSourceHeight); + + mWidth = (int) mSourceWidth; + mHeight = (int) mSourceHeight; if (mWidth == 0) { @@ -74,14 +92,29 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length // auto scale height to keep aspect mHeight = (size_t)Math::round(((float)mWidth / svgImage->width) * svgImage->height); } + + mBaseSize = Vector2i(mWidth, mHeight); - if (mMaxSize.x() > 0 && mMaxSize.y() > 0 && (mWidth > mMaxSize.x() || mHeight > mMaxSize.y())) + if (mMaxSize.x() > 0 && mMaxSize.y() > 0 && mHeight < mMaxSize.y() && mWidth < mMaxSize.y()) // FCATMP { Vector2i sz = ImageIO::adjustPictureSize(Vector2i(mWidth, mHeight), Vector2i(mMaxSize.x(), mMaxSize.y())); - mWidth = sz.x(); + mWidth = sz.x(); mHeight = sz.y(); + mWidth = (int)((mHeight * svgImage->width) / svgImage->height); } + if (OPTIMIZEVRAM && mMaxSize.x() > 0 && mMaxSize.y() > 0 && (mWidth > mMaxSize.x() || mHeight > mMaxSize.y())) + { + Vector2i sz = ImageIO::adjustPictureSize(Vector2i(mWidth, mHeight), Vector2i(mMaxSize.x(), mMaxSize.y())); + mWidth = sz.x(); + mHeight = sz.y(); + mWidth = (mHeight * svgImage->width) / svgImage->height; + + mPackedSize = Vector2i(mWidth, mHeight); + } + else + mPackedSize = Vector2i(0, 0); + unsigned char* dataRGBA = new unsigned char[mWidth * mHeight * 4]; NSVGrasterizer* rast = nsvgCreateRasterizer(); @@ -95,6 +128,29 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length return true; } +bool TextureData::isRequiredTextureSizeOk() +{ + if (!OPTIMIZEVRAM) + return true; + + if (mPackedSize == Vector2i(0, 0)) + return true; + + if (mBaseSize == Vector2i(0, 0)) + return true; + + if (mMaxSize == Vector2f(0, 0)) + return true; + + if ((int) mMaxSize.x() <= mPackedSize.x() || (int) mMaxSize.y() <= mPackedSize.y()) + return true; + + if (mBaseSize.x() <= mPackedSize.x() || mBaseSize.y() <= mPackedSize.y()) + return true; + + return false; +} + bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t length) { size_t width, height; @@ -106,9 +162,7 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng return true; } - unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height, mMaxSize.x(), mMaxSize.y()); - -// std::vector imageRGBA = ImageIO::loadFromMemoryRGBA32((const unsigned char*)(fileData), length, width, height); + unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height, OPTIMIZEVRAM ? mMaxSize.x() : 0, mMaxSize.y(), mBaseSize, mPackedSize); if (imageRGBA == NULL) { LOG(LogError) << "Could not initialize texture from memory, invalid data! (file path: " << mPath << ", data ptr: " << (size_t)fileData << ", reported size: " << length << ")"; @@ -148,6 +202,7 @@ bool TextureData::initFromRGBAEx(unsigned char* dataRGBA, size_t width, size_t h mDataRGBA = dataRGBA; mWidth = width; mHeight = height; + return true; } @@ -158,18 +213,27 @@ bool TextureData::load() // Need to load. See if there is a file if (!mPath.empty()) { +#ifdef WIN32 + char buffer[1000]; + sprintf_s(buffer, "TextureData::load(%s, %d)\n", mPath.c_str(), mTextureID); + traceOutput(buffer); +#endif + std::shared_ptr& rm = ResourceManager::getInstance(); const ResourceData& data = rm->getFileData(mPath); // is it an SVG? if (mPath.substr(mPath.size() - 4, std::string::npos) == ".svg") { - mScalable = true; + mScalable = true; // ??? interest ? retval = initSVGFromMemory((const unsigned char*)data.ptr.get(), data.length); } else retval = initImageFromMemory((const unsigned char*)data.ptr.get(), data.length); } + + + return retval; } @@ -178,6 +242,7 @@ bool TextureData::isLoaded() std::unique_lock lock(mMutex); if (mDataRGBA || (mTextureID != 0)) return true; + return false; } @@ -199,6 +264,7 @@ bool TextureData::uploadAndBind() // Make sure we're ready to upload if ((mWidth == 0) || (mHeight == 0) || (mDataRGBA == nullptr)) return false; + glGetError(); //now for the openGL texture stuff glGenTextures(1, &mTextureID); @@ -266,8 +332,16 @@ void TextureData::setSourceSize(float width, float height) { if (mScalable) { - if ((mSourceWidth != width) || (mSourceHeight != height)) + //if ((mSourceWidth != width) || (mSourceHeight != height)) + if (mSourceHeight < height) // FCATMP { + /* +#ifdef WIN32 + char buffer[1000]; + sprintf_s(buffer, "TextureData::setSourceSize(%s, %d)\n", mPath.c_str(), mTextureID); + traceOutput(buffer); +#endif*/ + mSourceWidth = width; mSourceHeight = height; releaseVRAM(); diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 7a0215de9a..3d378c0567 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -8,6 +8,7 @@ #include #include "math/Vector2f.h" +#include "math/Vector2i.h" class TextureResource; @@ -17,6 +18,8 @@ class TextureData TextureData(bool tile); ~TextureData(); + static bool OPTIMIZEVRAM; + // These functions populate mDataRGBA but do not upload the texture to VRAM //!!!! Needs to be canonical path. Caller should check for duplicates before calling this @@ -41,7 +44,11 @@ class TextureData // Release the texture from conventional RAM void releaseRAM(); - void setMaxSize(Vector2f maxSize) { mMaxSize = maxSize; }; + void setMaxSize(Vector2f maxSize) + { + if (mMaxSize.x() < maxSize.x() || mMaxSize.y() < maxSize.y()) + mMaxSize = maxSize; + }; // Get the amount of VRAM currenty used by this texture size_t getVRAMUsage(); @@ -54,20 +61,27 @@ class TextureData bool tiled() { return mTile; } + bool isRequiredTextureSizeOk(); + + std::string mPath; + GLuint mTextureID; + private: std::mutex mMutex; bool mTile; - std::string mPath; - GLuint mTextureID; unsigned char* mDataRGBA; size_t mWidth; size_t mHeight; float mSourceWidth; - float mSourceHeight; + float mSourceHeight; bool mScalable; bool mReloadable; + Vector2i mPackedSize; + Vector2i mBaseSize; Vector2f mMaxSize; }; +void traceOutput(std::string string); + #endif // ES_CORE_RESOURCES_TEXTURE_DATA_H diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index 5755887187..7f1cb7095b 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -4,6 +4,8 @@ #include "resources/TextureResource.h" #include "Settings.h" +#include + TextureDataManager::TextureDataManager() { unsigned char data[5 * 5 * 4]; @@ -63,7 +65,8 @@ std::shared_ptr TextureDataManager::get(const TextureResource* key) mTextureLookup[key] = mTextures.cbegin(); // Make sure it's loaded or queued for loading - load(tex); + if (!tex->isLoaded()) // FCATMP + load(tex); } return tex; } @@ -104,28 +107,76 @@ void TextureDataManager::load(std::shared_ptr tex, bool block) { // See if it's already loaded if (tex->isLoaded()) - return; + { + if (tex->isRequiredTextureSizeOk()) + return; + + tex->releaseVRAM(); + tex->releaseRAM(); + + mLoader->remove(tex); + block = true; // Reload instantly or other instances will fade again + } + // Not loaded. Make sure there is room size_t size = TextureResource::getTotalMemUsage(); size_t max_texture = (size_t)Settings::getInstance()->getInt("MaxVRAM") * 1024 * 1024; + int cleanedMemory = 0; + int lastTime = SDL_GetTicks(); + for (auto it = mTextures.crbegin(); it != mTextures.crend(); ++it) { if (size < max_texture) break; - //size -= (*it)->getVRAMUsage(); + if (!(*it)->isLoaded()) // FCA added to avoid calling TextureResource::getTotalMemUsage() if texture is not loaded + continue; + (*it)->releaseVRAM(); (*it)->releaseRAM(); + // It may be already in the loader queue. In this case it wouldn't have been using // any VRAM yet but it will be. Remove it from the loader queue mLoader->remove(*it); size = TextureResource::getTotalMemUsage(); + + cleanedMemory++; + } + +#ifdef WIN32 + if (cleanedMemory > 0) + { + lastTime = SDL_GetTicks() - lastTime; + + char buffer[1000]; + sprintf_s(buffer, "cleanedMemory : %d items in %d ms\n", cleanedMemory, lastTime); + traceOutput(buffer); } +#endif + if (!block) + { + /* +#ifdef WIN32 + char buffer[1000]; + sprintf_s(buffer, "ASYNC LOAD : %s\n", tex->mPath.c_str()); + traceOutput(buffer); +#endif*/ + mLoader->load(tex); + } else + { + /* +#ifdef WIN32 + char buffer[1000]; + sprintf_s(buffer, "SYNC LOAD : %s\n", tex->mPath.c_str()); + traceOutput(buffer); +#endif*/ + mLoader->remove(tex); tex->load(); + } } TextureLoader::TextureLoader() : mExit(false) diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index 3cb0c6f2b8..b6c8feb672 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -4,7 +4,10 @@ #include "resources/TextureData.h" TextureDataManager TextureResource::sTextureDataManager; -std::map< TextureResource::TextureKeyType, std::weak_ptr > TextureResource::sTextureMap; + +std::map< TextureResource::TextureKeyType, std::weak_ptr> TextureResource::sTextureMap; +std::map< TextureResource::TextureKeyType, std::shared_ptr> TextureResource::sPermanentTextureMap; // FCAWEAK + std::set TextureResource::sAllTextures; TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic, Vector2f maxSize) : mTextureData(nullptr), mForceLoad(false) @@ -16,12 +19,13 @@ TextureResource::TextureResource(const std::string& path, bool tile, bool dynami // data manager to manage loading/unloading of this texture std::shared_ptr data; if (dynamic) - { + { data = sTextureDataManager.add(this, tile); data->setMaxSize(maxSize); data->initFromPath(path); + // Force the texture manager to load it using a blocking load - sTextureDataManager.load(data, true); + sTextureDataManager.load(data, true); // } else { @@ -50,7 +54,12 @@ TextureResource::~TextureResource() if (mTextureData == nullptr) sTextureDataManager.remove(this); - sAllTextures.erase(sAllTextures.find(this)); + if (sAllTextures.size() > 0) + { + auto pthis = sAllTextures.find(this); + if (pthis != sAllTextures.end()) + sAllTextures.erase(pthis); + } } void TextureResource::initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height) @@ -103,6 +112,12 @@ bool TextureResource::bind() } } +void TextureResource::resetCache() +{ + sPermanentTextureMap.clear(); + sTextureMap.clear(); +} + std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic, Vector2f maxSize) { std::shared_ptr& rm = ResourceManager::getInstance(); @@ -119,20 +134,53 @@ std::shared_ptr TextureResource::get(const std::string& path, b auto foundTexture = sTextureMap.find(key); if(foundTexture != sTextureMap.cend()) { - if(!foundTexture->second.expired()) - return foundTexture->second.lock(); + if (!foundTexture->second.expired()) // FCAWEAK + { + std::shared_ptr rc = foundTexture->second.lock(); + + if (maxSize != Vector2f(0, 0) && TextureData::OPTIMIZEVRAM) + { + auto dt = sTextureDataManager.get(rc.get()); + if (dt != nullptr) + dt->setMaxSize(maxSize); + } + + return rc; + } + else + sTextureMap.erase(foundTexture); } + auto permanentTexture = sPermanentTextureMap.find(key); + if (permanentTexture != sPermanentTextureMap.cend()) + { + std::shared_ptr rc = permanentTexture->second; + + if (maxSize != Vector2f(0, 0) && TextureData::OPTIMIZEVRAM) + { + auto dt = sTextureDataManager.get(rc.get()); + if (dt != nullptr) + dt->setMaxSize(maxSize); + } + + return rc; + } + // need to create it std::shared_ptr tex; tex = std::shared_ptr(new TextureResource(key.first, tile, dynamic, maxSize)); std::shared_ptr data = sTextureDataManager.get(tex.get()); // is it an SVG? - if(key.first.substr(key.first.size() - 4, std::string::npos) != ".svg") + // if(key.first.substr(key.first.size() - 4, std::string::npos) != ".svg") // FCATMP { // Probably not. Add it to our map. We don't add SVGs because 2 svgs might be rasterized at different sizes - sTextureMap[key] = std::weak_ptr(tex); + // FCA useless -> If the svg is too small, it will be reloaded bigger with setSourceSize... + + if (canonicalPath.length() > 0 && canonicalPath[0] == ':') + sPermanentTextureMap[key] = std::shared_ptr(tex); // FCAWEAK + else + sTextureMap[key] = std::shared_ptr(tex); // FCAWEAK } // Add it to the reloadable list @@ -161,8 +209,10 @@ void TextureResource::rasterizeAt(size_t width, size_t height) data = sTextureDataManager.get(this); mSourceSize = Vector2f((float)width, (float)height); data->setSourceSize((float)width, (float)height); + if (mForceLoad || (mTextureData != nullptr)) - data->load(); + if (!data->isLoaded()) // FCATMP + data->load(); } Vector2f TextureResource::getSourceImageSize() const @@ -205,7 +255,7 @@ size_t TextureResource::getTotalTextureSize() return total; } -void TextureResource::unload(std::shared_ptr& /*rm*/) +void TextureResource::unload() { // Release the texture's resources std::shared_ptr data; @@ -218,10 +268,12 @@ void TextureResource::unload(std::shared_ptr& /*rm*/) data->releaseRAM(); } -void TextureResource::reload(std::shared_ptr& /*rm*/) +void TextureResource::reload() { // For dynamically loaded textures the texture manager will load them on demand. // For manually loaded textures we have to reload them here - if (mTextureData) + if (mTextureData && !mTextureData->isLoaded()) mTextureData->load(); + else if (mTextureData == nullptr) + sTextureDataManager.get(this); } diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index a6d9aee083..4e7b29b7fa 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -34,11 +34,14 @@ class TextureResource : public IReloadable static size_t getTotalMemUsage(); // returns an approximation of total VRAM used by textures (in bytes) static size_t getTotalTextureSize(); // returns the number of bytes that would be used if all textures were in memory + static void resetCache(); + +public: + virtual void unload(); + virtual void reload(); protected: TextureResource(const std::string& path, bool tile, bool dynamic, Vector2f maxSize); - virtual void unload(std::shared_ptr& rm); - virtual void reload(std::shared_ptr& rm); private: // mTextureData is used for textures that are not loaded from a file - these ones @@ -54,6 +57,7 @@ class TextureResource : public IReloadable typedef std::pair TextureKeyType; static std::map< TextureKeyType, std::weak_ptr > sTextureMap; // map of textures, used to prevent duplicate textures + static std::map< TextureKeyType, std::shared_ptr > sPermanentTextureMap; // map of textures, used to prevent duplicate textures // FCAWEAK static std::set sAllTextures; // Set of all textures, used for memory management }; diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index c16aab4720..80c737962e 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -263,7 +263,10 @@ namespace Utils // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows char* envHome = getenv("HOME"); + // envHome = "H:/[Emulz]/EmulationStation/"; + #ifdef _DEBUG + if (Utils::FileSystem::exists("H:/[Emulz]/EmulationStation/emulationstation.exe")) envHome = "H:/[Emulz]/EmulationStation/"; #endif diff --git a/es-core/src/utils/StringUtil.cpp b/es-core/src/utils/StringUtil.cpp index 0e1da37ea8..6d78501c6a 100644 --- a/es-core/src/utils/StringUtil.cpp +++ b/es-core/src/utils/StringUtil.cpp @@ -34,6 +34,24 @@ namespace Utils return wstring; } #endif + std::vector split(const std::string& s, char seperator) + { + std::vector output; + + std::string::size_type prev_pos = 0, pos = 0; + while ((pos = s.find(seperator, pos)) != std::string::npos) + { + std::string substring(s.substr(prev_pos, pos - prev_pos)); + + output.push_back(substring); + + prev_pos = ++pos; + } + + output.push_back(s.substr(prev_pos, pos - prev_pos)); // Last word + + return output; + } unsigned int chars2Unicode(const std::string& _string, size_t& _cursor) { @@ -329,7 +347,7 @@ namespace Utils return buffer; - } // scramble + } // scramble } // String:: diff --git a/es-core/src/utils/StringUtil.h b/es-core/src/utils/StringUtil.h index 3ffb5c5eac..baef504825 100644 --- a/es-core/src/utils/StringUtil.h +++ b/es-core/src/utils/StringUtil.h @@ -27,6 +27,7 @@ namespace Utils std::string vectorToCommaString(stringVector _vector); std::string format (const char* _string, ...); std::string scramble (const std::string& _input, const std::string& key); + std::vector split (const std::string& s, char seperator); #if defined(_WIN32) const std::string convertFromWideString(const std::wstring wstring); From d056f91c3c8e3c14bb370a1e7802ea4c1141182e Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 27 May 2019 13:39:13 +0200 Subject: [PATCH 023/336] [My Changes] --- .gitignore | 1 + es-app/src/CollectionSystemManager.cpp | 14 +- es-app/src/EmulationStation.h | 6 +- es-app/src/FileData.cpp | 118 +++- es-app/src/FileData.h | 9 + es-app/src/FileSorts.cpp | 4 +- es-app/src/Gamelist.cpp | 72 +- es-app/src/MetaData.cpp | 9 +- es-app/src/MetaData.h | 3 +- es-app/src/SystemData.cpp | 143 +++- es-app/src/SystemData.h | 56 +- .../src/guis/GuiCollectionSystemsOptions.cpp | 30 +- es-app/src/guis/GuiCollectionSystemsOptions.h | 2 +- es-app/src/guis/GuiGameScraper.cpp | 2 +- es-app/src/guis/GuiGamelistFilter.cpp | 8 +- es-app/src/guis/GuiGamelistOptions.cpp | 21 +- .../src/guis/GuiGeneralScreensaverOptions.cpp | 2 +- .../src/guis/GuiGeneralScreensaverOptions.h | 2 +- es-app/src/guis/GuiMenu.cpp | 348 ++++++---- es-app/src/guis/GuiMenu.h | 2 +- es-app/src/guis/GuiMetaDataEd.cpp | 163 ++++- es-app/src/guis/GuiMetaDataEd.h | 8 +- es-app/src/guis/GuiScraperMulti.cpp | 2 +- es-app/src/guis/GuiScraperStart.cpp | 8 +- es-app/src/guis/GuiScreensaverOptions.cpp | 8 +- es-app/src/guis/GuiScreensaverOptions.h | 2 +- es-app/src/guis/GuiSettings.cpp | 8 +- es-app/src/guis/GuiSettings.h | 2 +- es-app/src/main.cpp | 116 ++-- es-app/src/scrapers/ScreenScraper.cpp | 13 +- es-app/src/scrapers/ScreenScraper.h | 2 +- es-app/src/views/SystemView.cpp | 15 +- es-app/src/views/ViewController.cpp | 137 ++-- es-app/src/views/ViewController.h | 9 +- .../src/views/gamelist/BasicGameListView.cpp | 55 +- .../views/gamelist/DetailedGameListView.cpp | 52 +- .../src/views/gamelist/DetailedGameListView.h | 1 + .../src/views/gamelist/GridGameListView.cpp | 74 ++- es-app/src/views/gamelist/GridGameListView.h | 15 +- .../views/gamelist/ISimpleGameListView.cpp | 31 +- es-core/src/GuiComponent.cpp | 107 +++ es-core/src/GuiComponent.h | 53 ++ es-core/src/InputManager.cpp | 2 +- es-core/src/Renderer_init_sdlgl.cpp | 38 +- es-core/src/Scripting.cpp | 2 +- es-core/src/Settings.cpp | 9 +- es-core/src/ThemeData.cpp | 57 +- es-core/src/ThemeData.h | 10 +- es-core/src/Window.cpp | 41 +- es-core/src/Window.h | 1 + es-core/src/components/ComponentGrid.cpp | 6 +- es-core/src/components/ComponentList.cpp | 2 +- es-core/src/components/DateTimeComponent.cpp | 4 +- .../src/components/DateTimeEditComponent.cpp | 4 +- es-core/src/components/GridTileComponent.cpp | 188 +++++- es-core/src/components/GridTileComponent.h | 24 +- es-core/src/components/ImageComponent.cpp | 39 +- es-core/src/components/ImageGridComponent.h | 176 +++-- es-core/src/components/MenuComponent.cpp | 4 +- es-core/src/components/MenuComponent.h | 2 +- es-core/src/components/OptionListComponent.h | 45 +- es-core/src/components/SliderComponent.cpp | 2 +- es-core/src/components/SwitchComponent.cpp | 2 +- es-core/src/components/VideoComponent.cpp | 2 +- es-core/src/guis/GuiInputConfig.cpp | 22 +- es-core/src/guis/GuiInputConfig.h | 3 +- es-core/src/guis/GuiTextEditPopup.cpp | 2 +- es-core/src/platform.cpp | 103 ++- es-core/src/platform.h | 4 +- es-core/src/resources/Font.cpp | 8 +- es-core/src/resources/TextureData.cpp | 7 +- es-core/src/utils/FileSystemUtil.cpp | 30 +- es-core/src/utils/StringUtil.cpp | 629 +++++++++--------- es-core/src/utils/StringUtil.h | 4 + 74 files changed, 2242 insertions(+), 963 deletions(-) diff --git a/.gitignore b/.gitignore index 4055d624e6..19593e251e 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,4 @@ Makefile CPackConfig.cmake CPackSourceConfig.cmake *.cbp +/.vs diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 0ccdd9ee1d..bd6eb63e91 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -258,6 +258,9 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS if (name == "favorites" && file->metadata.get("favorite") == "false") { // need to check if still marked as favorite, if not remove ViewController::get()->getGameListView(curSys).get()->remove(collectionEntry, false); + + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); + ViewController::get()->getGameListView(curSys)->onFileChanged(collectionEntry, FILE_METADATA_CHANGED); } else { @@ -274,6 +277,7 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS CollectionFileData* newGame = new CollectionFileData(file, curSys); rootFolder->addChild(newGame); fileIndex->addToIndex(newGame); + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); ViewController::get()->getGameListView(curSys)->onFileChanged(newGame, FILE_METADATA_CHANGED); } @@ -453,6 +457,13 @@ void CollectionSystemManager::exitEditMode() // adds or removes a game from a specific collection bool CollectionSystemManager::toggleGameInCollection(FileData* file) { + /* + if (file->getType() == FOLDER) + { + if (file->findUniqueGameForFolder() == NULL) + return false; + } + */ if (file->getType() == GAME) { GuiInfoPopup* s; @@ -707,7 +718,8 @@ void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysDa for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) { // we won't iterate all collections - if ((*sysIt)->isGameSystem() && !(*sysIt)->isCollection()) { + if ((*sysIt)->isGameSystem() && !(*sysIt)->isCollection()) + { std::vector files = (*sysIt)->getRootFolder()->getFilesRecursive(GAME); for(auto gameIt = files.cbegin(); gameIt != files.cend(); gameIt++) { diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index 2bd27925bc..57389a1055 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -5,13 +5,13 @@ // 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 9 +#define PROGRAM_VERSION_MINOR 10 #define PROGRAM_VERSION_MAINTENANCE 0 -#define PROGRAM_VERSION_STRING "2.9.0rp-dev" +#define PROGRAM_VERSION_STRING "2.10.0 FCAMOD" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ -#define RESOURCE_VERSION_STRING "2,9,0\0" +#define RESOURCE_VERSION_STRING "2,10,0\0" #define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE #endif // ES_APP_EMULATION_STATION_H diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 16cdd164d3..532ea9a71d 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -22,7 +22,9 @@ FileData::FileData(FileType type, const std::string& path, SystemEnvironmentData // metadata needs at least a name field (since that's what getName() will return) if(metadata.get("name").empty()) metadata.set("name", getDisplayName()); - mSystemName = system->getName(); + + mSystemName = system->getName(); + // mDefaultCore = system->getSystemEnvData()->mDefaultCore; } FileData::~FileData() @@ -58,7 +60,7 @@ const std::string FileData::getThumbnailPath() const if(thumbnail.empty()) { thumbnail = metadata.get("image"); - + /* // no image, try to use local image if(thumbnail.empty() && Settings::getInstance()->getBool("LocalArt")) { @@ -72,12 +74,23 @@ const std::string FileData::getThumbnailPath() const thumbnail = path; } } - } + }*/ } return thumbnail; } +const bool FileData::getFavorite() +{ + return metadata.get("favorite") == "true"; +} + + +const bool FileData::getHidden() +{ + return metadata.get("hidden") == "true"; +} + const std::string& FileData::getName() { return metadata.get("name"); @@ -91,6 +104,26 @@ const std::string& FileData::getSortName() return metadata.get("sortname"); } +FileData* FileData::findUniqueGameForFolder() +{ + std::vector children = getChildren(); + + if (children.size() == 1 && children.at(0)->getType() == GAME) + return children.at(0); + + for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + if ((*it)->getType() == GAME) + return NULL; + + FileData* ret = (*it)->findUniqueGameForFolder(); + if (ret != NULL) + return ret; + } + + return NULL; +} + const std::vector& FileData::getChildrenListToDisplay() { FileFilterIndex* idx = CollectionSystemManager::get()->getSystemToView(mSystem)->getIndex(); @@ -111,10 +144,20 @@ const std::vector& FileData::getChildrenListToDisplay() { } } +const std::string FileData::getCore() const +{ + return metadata.get("core"); +} + +const std::string FileData::getEmulator() const +{ + return metadata.get("emulator"); +} + const std::string FileData::getVideoPath() const { std::string video = metadata.get("video"); - + /* // no video, try to use local video if(video.empty() && Settings::getInstance()->getBool("LocalArt")) { @@ -122,7 +165,7 @@ const std::string FileData::getVideoPath() const if(Utils::FileSystem::exists(path)) video = path; } - + */ return video; } @@ -199,12 +242,13 @@ std::string FileData::getKey() { const bool FileData::isArcadeAsset() { - const std::string stem = Utils::FileSystem::getStem(mPath); - return ( - (mSystem && (mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO))) - && - (MameNames::getInstance()->isBios(stem) || MameNames::getInstance()->isDevice(stem)) - ); + if (mSystem && (mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO))) + { + const std::string stem = Utils::FileSystem::getStem(mPath); + return MameNames::getInstance()->isBios(stem) || MameNames::getInstance()->isDevice(stem); + } + + return false; } FileData* FileData::getSourceFileData() @@ -271,44 +315,68 @@ void FileData::launchGame(Window* window) AudioManager::getInstance()->deinit(); VolumeControl::getInstance()->deinit(); - window->deinit(); + + bool hideWindow = Settings::getInstance()->getBool("HideWindow"); + if (hideWindow) + window->deinit(); std::string command = mEnvData->mLaunchCommand; - const std::string rom = Utils::FileSystem::getEscapedPath(getPath()); + const std::string rom = Utils::FileSystem::getEscapedPath(getPath()); const std::string basename = Utils::FileSystem::getStem(getPath()); - const std::string rom_raw = Utils::FileSystem::getPreferredPath(getPath()); + const std::string rom_raw = Utils::FileSystem::getPreferredPath(getPath()); + + std::string emulator = getEmulator(); + if (emulator.length() == 0) + emulator = mEnvData->getDefaultEmulator(); + + std::string core = getCore(); + if (core.length() == 0) + core = mEnvData->getDefaultCore(emulator); + + std::string customCommandLine = mEnvData->getEmulatorCommandLine(emulator); + if (customCommandLine.length() > 0) + command = customCommandLine; + + command = Utils::String::replace(command, "%EMULATOR%", emulator); + command = Utils::String::replace(command, "%CORE%", core); command = Utils::String::replace(command, "%ROM%", rom); command = Utils::String::replace(command, "%BASENAME%", basename); - command = Utils::String::replace(command, "%ROM_RAW%", rom_raw); + command = Utils::String::replace(command, "%ROM_RAW%", rom_raw); + command = Utils::String::replace(command, "%SYSTEM%", mSystemName); + command = Utils::String::replace(command, "%HOME%", Utils::FileSystem::getHomePath()); Scripting::fireEvent("game-start", rom, basename); LOG(LogInfo) << " " << command; - int exitCode = runSystemCommand(command); - if(exitCode != 0) + int exitCode = runSystemCommand(command, getDisplayName(), hideWindow ? NULL : window); + if (exitCode != 0) { LOG(LogWarning) << "...launch terminated with nonzero exit code " << exitCode << "!"; } Scripting::fireEvent("game-end"); - window->init(); + if (hideWindow) + window->init(); + VolumeControl::getInstance()->init(); window->normalizeNextUpdate(); //update number of times the game has been launched + if (exitCode == 0) + { + FileData* gameToUpdate = getSourceFileData(); - FileData* gameToUpdate = getSourceFileData(); - - int timesPlayed = gameToUpdate->metadata.getInt("playcount") + 1; - gameToUpdate->metadata.set("playcount", std::to_string(static_cast(timesPlayed))); + int timesPlayed = gameToUpdate->metadata.getInt("playcount") + 1; + gameToUpdate->metadata.set("playcount", std::to_string(static_cast(timesPlayed))); - //update last played time - gameToUpdate->metadata.set("lastplayed", Utils::Time::DateTime(Utils::Time::now())); - CollectionSystemManager::get()->refreshCollectionSystems(gameToUpdate); + //update last played time + gameToUpdate->metadata.set("lastplayed", Utils::Time::DateTime(Utils::Time::now())); + CollectionSystemManager::get()->refreshCollectionSystems(gameToUpdate); + } } CollectionFileData::CollectionFileData(FileData* file, SystemData* system) diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 63a9ce16e3..00fd4a923b 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -50,6 +50,14 @@ class FileData virtual const std::string getMarqueePath() const; virtual const std::string getImagePath() const; + virtual const std::string getCore() const; + virtual const std::string getEmulator() const; + + virtual const bool getHidden(); + virtual const bool getFavorite(); + + FileData* findUniqueGameForFolder(); + const std::vector& getChildrenListToDisplay(); std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; @@ -94,6 +102,7 @@ class FileData FileData* mSourceFileData; FileData* mParent; std::string mSystemName; + //std::string mDefaultCore; private: FileType mType; diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index 58b4ba90a1..5043656e11 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -17,8 +17,8 @@ namespace FileSorts FileData::SortType(&compareLastPlayed, true, "last played, ascending"), FileData::SortType(&compareLastPlayed, false, "last played, descending"), - FileData::SortType(&compareNumPlayers, true, "number players, ascending"), - FileData::SortType(&compareNumPlayers, false, "number players, descending"), + FileData::SortType(&compareNumPlayers, true, "number of players, ascending"), + FileData::SortType(&compareNumPlayers, false, "number of players, descending"), FileData::SortType(&compareReleaseDate, true, "release date, ascending"), FileData::SortType(&compareReleaseDate, false, "release date, descending"), diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 14af11757d..ea20baa94a 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -66,11 +66,13 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType LOG(LogWarning) << "gameList: folder doesn't already exist, won't create"; return NULL; } - + // create missing folder + + /* FCA TODO -> 1 seul jeu dans le folder ? FileData* folder = new FileData(FOLDER, Utils::FileSystem::getStem(treeNode->getPath()) + "/" + *path_it, system->getSystemEnvData(), system); treeNode->addChild(folder); - treeNode = folder; + treeNode = folder;*/ } path_it++; @@ -81,12 +83,12 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType void parseGamelist(SystemData* system) { - bool trustGamelist = Settings::getInstance()->getBool("ParseGamelistOnly"); std::string xmlpath = system->getGamelistPath(false); - - if(!Utils::FileSystem::exists(xmlpath)) + if (!Utils::FileSystem::exists(xmlpath)) return; + bool trustGamelist = Settings::getInstance()->getBool("ParseGamelistOnly"); + LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"..."; pugi::xml_document doc; @@ -107,39 +109,41 @@ void parseGamelist(SystemData* system) std::string relativeTo = system->getStartPath(); - const char* tagList[2] = { "game", "folder" }; - FileType typeList[2] = { GAME, FOLDER }; - for(int i = 0; i < 2; i++) + for (pugi::xml_node fileNode : root.children()) { - const char* tag = tagList[i]; - FileType type = typeList[i]; - for(pugi::xml_node fileNode = root.child(tag); fileNode; fileNode = fileNode.next_sibling(tag)) + FileType type = GAME; + + std::string tag = fileNode.name(); + + if (tag == "folder") + type = FOLDER; + else if (tag != "game") + continue; + + const std::string path = Utils::FileSystem::resolveRelativePath(fileNode.child("path").text().get(), relativeTo, false); + + if (!trustGamelist && !Utils::FileSystem::exists(path)) { - const std::string path = Utils::FileSystem::resolveRelativePath(fileNode.child("path").text().get(), relativeTo, false); - - if(!trustGamelist && !Utils::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; - } - else if(!file->isArcadeAsset()) - { - std::string defaultName = file->metadata.get("name"); - file->metadata = MetaDataList::createFromXML(GAME_METADATA, fileNode, relativeTo); + 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; + } + else if(!file->isArcadeAsset()) + { + 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); + //make sure name gets set if one didn't exist + if (file->metadata.get("name").empty()) + file->metadata.set("name", defaultName); - file->metadata.resetChangedFlag(); - } + file->metadata.resetChangedFlag(); } } } diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index 8a977e9509..3c9900d5a4 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -9,6 +9,10 @@ MetaDataDecl gameDecls[] = { {"name", MD_STRING, "", false, "name", "enter game name"}, {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, + + { "emulator", MD_PLIST, "", false, "emulator", "emulator" }, + { "core", MD_PLIST, "", false, "core", "core" }, + {"image", MD_PATH, "", false, "image", "enter path to image"}, {"video", MD_PATH , "", false, "video", "enter path to video"}, {"marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, @@ -40,7 +44,10 @@ MetaDataDecl folderDecls[] = { {"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"} + {"players", MD_INT, "1", false, "players", "enter number of players"}, + + { "favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on" }, + { "hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on" }, }; const std::vector folderMDD(folderDecls, folderDecls + sizeof(folderDecls) / sizeof(folderDecls[0])); diff --git a/es-app/src/MetaData.h b/es-app/src/MetaData.h index 6ed8f6d44c..7205511a2c 100644 --- a/es-app/src/MetaData.h +++ b/es-app/src/MetaData.h @@ -20,7 +20,8 @@ enum MetaDataType MD_PATH, MD_RATING, MD_DATE, - MD_TIME //used for lastplayed + MD_TIME, //used for lastplayed + MD_PLIST }; struct MetaDataDecl diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index d6e72cfd32..4052801b75 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -16,6 +16,8 @@ #include #endif +#include "Window.h" + std::vector SystemData::sSystemVector; SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : @@ -24,15 +26,19 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys mFilterIndex = new FileFilterIndex(); // if it's an actual system, initialize it, if not, just create the data structure - if(!CollectionSystem) + if (!CollectionSystem) { mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); mRootFolder->metadata.set("name", mFullName); - if(!Settings::getInstance()->getBool("ParseGamelistOnly")) + if (!Settings::getInstance()->getBool("ParseGamelistOnly")) + { populateFolder(mRootFolder); + if (mRootFolder->getChildren().size() == 0) + return; + } - if(!Settings::getInstance()->getBool("IgnoreGamelist")) + if (!Settings::getInstance()->getBool("IgnoreGamelist")) parseGamelist(this); mRootFolder->sort(FileSorts::SortTypes.at(0)); @@ -50,12 +56,6 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys SystemData::~SystemData() { - //save changed game data back to xml - if(!Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit") && !mIsCollectionSystem) - { - updateGamelist(this); - } - delete mRootFolder; delete mFilterIndex; } @@ -68,6 +68,12 @@ void SystemData::setIsGameSystemStatus() mIsGameSystem = (mName != "retropie"); } +char _easytolower(char in) { + if (in <= 'Z' && in >= 'A') + return in - ('Z' - 'z'); + return in; +} + void SystemData::populateFolder(FileData* folder) { const std::string& folderPath = folder->getPath(); @@ -77,6 +83,11 @@ void SystemData::populateFolder(FileData* folder) return; } + int di = folderPath.rfind("downloaded_images"); + int md = folderPath.rfind("media"); + if (di > 0 || md > 0) + return; + //make sure that this isn't a symlink to a thing we already have if(Utils::FileSystem::isSymlink(folderPath)) { @@ -87,7 +98,7 @@ void SystemData::populateFolder(FileData* folder) return; } } - + std::string filePath; std::string extension; bool isGame; @@ -104,6 +115,7 @@ void SystemData::populateFolder(FileData* folder) //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 = Utils::FileSystem::getExtension(filePath); + std::transform(extension.begin(), extension.end(), extension.begin(), ::_easytolower); //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 @@ -120,18 +132,23 @@ void SystemData::populateFolder(FileData* folder) isGame = true; } } - + //add directories that also do not match an extension as folders if(!isGame && Utils::FileSystem::isDirectory(filePath)) { - FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); - populateFolder(newFolder); - - //ignore folders that do not contain games - if(newFolder->getChildrenByFilename().size() == 0) - delete newFolder; - else - folder->addChild(newFolder); + if (filePath.rfind("downloaded_images") == std::string::npos && + filePath.rfind("media") == std::string::npos) + { + FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); + populateFolder(newFolder); + + if (newFolder->getChildrenByFilename().size() == 0) + delete newFolder; + else if (newFolder->findUniqueGameForFolder() != NULL) + delete newFolder; + else + folder->addChild(newFolder); + } } } } @@ -168,7 +185,7 @@ std::vector readList(const std::string& str, const char* delims = " } //creates systems from information located in a config file -bool SystemData::loadConfig() +bool SystemData::loadConfig(Window* window) { deleteSystems(); @@ -201,17 +218,65 @@ bool SystemData::loadConfig() LOG(LogError) << "es_systems.cfg is missing the tag!"; return false; } + + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { + std::vector emulatorList; + - for(pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - { - std::string name, fullname, path, cmd, themeFolder; + // std::vector coreList; + std::string name, fullname, path, cmd, themeFolder, defaultCore; + name = system.child("name").text().get(); fullname = system.child("fullname").text().get(); path = system.child("path").text().get(); + defaultCore = system.child("defaultCore").text().get(); + + pugi::xml_node emulators = system.child("emulators"); + if (emulators != NULL) + { + for (pugi::xml_node emulator : emulators.children()) + { + EmulatorData emulatorData; + emulatorData.mName = emulator.attribute("name").value(); + emulatorData.mCommandLine = emulator.attribute("command").value(); + + pugi::xml_node cores = emulator.child("cores"); + if (cores != NULL) + { + for (pugi::xml_node core : cores.children()) + { + const std::string& corename = core.text().get(); + + if (defaultCore.length() == 0) + defaultCore = corename; + + emulatorData.mCores.push_back(corename); + // coreList.push_back(corename); + } + } + + emulatorList.push_back(emulatorData); + } + } + + if (window != NULL) + window->renderLoadingScreen(fullname); // convert extensions list from a string into a vector of strings - std::vector extensions = readList(system.child("extension").text().get()); + + std::vector list = readList(system.child("extension").text().get()); + std::vector extensions; + + for (auto extension = list.cbegin(); extension != list.cend(); extension++) + { + std::string xt = (*extension); + std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); + + if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) + extensions.push_back(xt); + } cmd = system.child("command").text().get(); @@ -253,7 +318,7 @@ bool SystemData::loadConfig() path = Utils::FileSystem::getGenericPath(path); //expand home symbol if the startpath contains ~ - if(path[0] == '~') + if (path[0] == '~') { path.erase(0, 1); path.insert(0, Utils::FileSystem::getHomePath()); @@ -265,18 +330,25 @@ bool SystemData::loadConfig() envData->mSearchExtensions = extensions; envData->mLaunchCommand = cmd; envData->mPlatformIds = platformIds; + // envData->mDefaultCore = defaultCore; + envData->mEmulators = emulatorList; + + // envData->mCores = coreList; SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) { LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; delete newSys; - }else{ - sSystemVector.push_back(newSys); } + else + sSystemVector.push_back(newSys); } - CollectionSystemManager::get()->loadCollectionSystems(); + if (window != NULL) + window->renderLoadingScreen("Favoris"); + + CollectionSystemManager::get()->loadCollectionSystems(); return true; } @@ -328,10 +400,18 @@ void SystemData::writeExampleConfig(const std::string& path) void SystemData::deleteSystems() { + bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); + for(unsigned int i = 0; i < sSystemVector.size(); i++) { - delete sSystemVector.at(i); + SystemData* pData = sSystemVector.at(i); + + if (saveOnExit && !pData->mIsCollectionSystem) + updateGamelist(pData); + + delete pData; } + sSystemVector.clear(); } @@ -495,8 +575,9 @@ void SystemData::loadTheme() sysData.insert(std::pair("system.theme", getThemeFolder())); sysData.insert(std::pair("system.fullName", getFullName())); - mTheme->loadFile(sysData, path); - } catch(ThemeException& e) + mTheme->loadFile(getThemeFolder(), path); + } + catch(ThemeException& e) { LOG(LogError) << e.what(); mTheme = std::make_shared(); // reset to empty diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index ad0456b3c4..3925b74961 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -11,6 +11,14 @@ class FileData; class FileFilterIndex; class ThemeData; +class Window; + +struct EmulatorData +{ + std::string mName; + std::string mCommandLine; + std::vector mCores; +}; struct SystemEnvironmentData { @@ -18,6 +26,52 @@ struct SystemEnvironmentData std::vector mSearchExtensions; std::string mLaunchCommand; std::vector mPlatformIds; + +// std::string mDefaultCore; + + std::vector mEmulators; + + std::vector getCores(std::string emulatorName) + { + std::vector list; + + for (auto& emulator : mEmulators) + if (emulatorName == emulator.mName) + return emulator.mCores; + + return list; + } + + std::string getDefaultEmulator() + { + for (auto& emulator : mEmulators) + return emulator.mName; + + return ""; + } + + std::string getDefaultCore(std::string emulatorName) + { + for (auto& emulator : mEmulators) + { + if (emulatorName == emulator.mName) + { + for (auto core : emulator.mCores) + return core; + } + } + + return ""; + } + + std::string getEmulatorCommandLine(std::string emulatorName) + { + for (auto& emulator : mEmulators) + if (emulatorName == emulator.mName) + return emulator.mCommandLine; + + return ""; + } }; class SystemData @@ -46,7 +100,7 @@ class SystemData unsigned int getDisplayedGameCount() const; 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 bool loadConfig(Window* window); //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 diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index 27df3082eb..be30460aa4 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -9,7 +9,7 @@ #include "CollectionSystemManager.h" #include "Window.h" -GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) : GuiComponent(window), mMenu(window, "GAME COLLECTION SETTINGS") +GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) : GuiComponent(window), mMenu(window, _T("GAME COLLECTION SETTINGS")) { initializeMenu(); } @@ -27,9 +27,9 @@ void GuiCollectionSystemsOptions::initializeMenu() std::vector unusedFolders = CollectionSystemManager::get()->getUnusedSystemsFromTheme(); if (unusedFolders.size() > 0) { - addEntry("CREATE NEW CUSTOM COLLECTION FROM THEME", 0x777777FF, true, + addEntry(_T("CREATE NEW CUSTOM COLLECTION FROM THEME"), 0x777777FF, true, [this, unusedFolders] { - auto s = new GuiSettings(mWindow, "SELECT THEME FOLDER"); + auto s = new GuiSettings(mWindow, _T("SELECT THEME FOLDER")); std::shared_ptr< OptionListComponent > folderThemes = std::make_shared< OptionListComponent >(mWindow, "SELECT THEME FOLDER", true); // add Custom Systems @@ -52,7 +52,7 @@ void GuiCollectionSystemsOptions::initializeMenu() } ComponentListRow row; - row.addElement(std::make_shared(mWindow, "CREATE NEW CUSTOM COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("CREATE NEW CUSTOM COLLECTION"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); auto createCustomCollection = [this](const std::string& newVal) { std::string name = newVal; // we need to store the first Gui and remove it, as it'll be deleted by the actual Gui @@ -62,33 +62,33 @@ void GuiCollectionSystemsOptions::initializeMenu() createCollection(name); }; row.makeAcceptInputHandler([this, createCustomCollection] { - mWindow->pushGui(new GuiTextEditPopup(mWindow, "New Collection Name", "", createCustomCollection, false)); + mWindow->pushGui(new GuiTextEditPopup(mWindow, _T("New Collection Name"), "", createCustomCollection, false)); }); mMenu.addRow(row); bundleCustomCollections = std::make_shared(mWindow); bundleCustomCollections->setState(Settings::getInstance()->getBool("UseCustomCollectionsSystem")); - mMenu.addWithLabel("GROUP UNTHEMED CUSTOM COLLECTIONS", bundleCustomCollections); + mMenu.addWithLabel(_T("GROUP UNTHEMED CUSTOM COLLECTIONS"), bundleCustomCollections); sortAllSystemsSwitch = std::make_shared(mWindow); sortAllSystemsSwitch->setState(Settings::getInstance()->getBool("SortAllSystems")); - mMenu.addWithLabel("SORT CUSTOM COLLECTIONS AND SYSTEMS", sortAllSystemsSwitch); + mMenu.addWithLabel(_T("SORT CUSTOM COLLECTIONS AND SYSTEMS"), sortAllSystemsSwitch); if(CollectionSystemManager::get()->isEditing()) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("FINISH EDITING COLLECTION")+" '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "'", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.makeAcceptInputHandler(std::bind(&GuiCollectionSystemsOptions::exitEditMode, this)); mMenu.addRow(row); } - mMenu.addButton("BACK", "back", std::bind(&GuiCollectionSystemsOptions::applySettings, this)); + mMenu.addButton(_T("BACK"), _T("BACK"), std::bind(&GuiCollectionSystemsOptions::applySettings, this)); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); } -void GuiCollectionSystemsOptions::addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func) +void GuiCollectionSystemsOptions::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) { std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); @@ -139,25 +139,25 @@ void GuiCollectionSystemsOptions::addSystemsToMenu() std::map autoSystems = CollectionSystemManager::get()->getAutoCollectionSystems(); - autoOptionList = std::make_shared< OptionListComponent >(mWindow, "SELECT COLLECTIONS", true); + autoOptionList = std::make_shared< OptionListComponent >(mWindow, _T("SELECT COLLECTIONS"), true); // add Auto Systems for(std::map::const_iterator it = autoSystems.cbegin() ; it != autoSystems.cend() ; it++ ) { autoOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); } - mMenu.addWithLabel("AUTOMATIC GAME COLLECTIONS", autoOptionList); + mMenu.addWithLabel(_T("AUTOMATIC GAME COLLECTIONS"), autoOptionList); std::map customSystems = CollectionSystemManager::get()->getCustomCollectionSystems(); - customOptionList = std::make_shared< OptionListComponent >(mWindow, "SELECT COLLECTIONS", true); + customOptionList = std::make_shared< OptionListComponent >(mWindow, _T("SELECT COLLECTIONS"), true); // add Custom Systems for(std::map::const_iterator it = customSystems.cbegin() ; it != customSystems.cend() ; it++ ) { customOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); } - mMenu.addWithLabel("CUSTOM GAME COLLECTIONS", customOptionList); + mMenu.addWithLabel(_T("CUSTOM GAME COLLECTIONS"), customOptionList); } void GuiCollectionSystemsOptions::applySettings() @@ -209,6 +209,6 @@ bool GuiCollectionSystemsOptions::input(InputConfig* config, Input input) std::vector GuiCollectionSystemsOptions::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); return prompts; } diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.h b/es-app/src/guis/GuiCollectionSystemsOptions.h index fc6113e0a2..0187920630 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.h +++ b/es-app/src/guis/GuiCollectionSystemsOptions.h @@ -22,7 +22,7 @@ class GuiCollectionSystemsOptions : public GuiComponent void initializeMenu(); void applySettings(); void addSystemsToMenu(); - void addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func); + void addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func); void updateSettings(std::string newAutoSettings, std::string newCustomSettings); void createCollection(std::string inName); void exitEditMode(); diff --git a/es-app/src/guis/GuiGameScraper.cpp b/es-app/src/guis/GuiGameScraper.cpp index d8ebcb7160..c94eab6630 100644 --- a/es-app/src/guis/GuiGameScraper.cpp +++ b/es-app/src/guis/GuiGameScraper.cpp @@ -42,7 +42,7 @@ GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std:: mSearch->openInputScreen(mSearchParams); mGrid.resetCursor(); })); - buttons.push_back(std::make_shared(mWindow, "CANCEL", "cancel", [&] { delete this; })); + buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); mButtonGrid = makeButtonGrid(mWindow, buttons); mGrid.setEntry(mButtonGrid, Vector2i(0, 6), true, false); diff --git a/es-app/src/guis/GuiGamelistFilter.cpp b/es-app/src/guis/GuiGamelistFilter.cpp index 8a3625a1e3..1216775439 100644 --- a/es-app/src/guis/GuiGamelistFilter.cpp +++ b/es-app/src/guis/GuiGamelistFilter.cpp @@ -21,14 +21,14 @@ void GuiGamelistFilter::initializeMenu() // show filtered menu row.elements.clear(); - row.addElement(std::make_shared(mWindow, "RESET ALL FILTERS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("RESET ALL FILTERS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistFilter::resetAllFilters, this)); mMenu.addRow(row); row.elements.clear(); addFiltersToMenu(); - mMenu.addButton("BACK", "back", std::bind(&GuiGamelistFilter::applyFilters, this)); + mMenu.addButton(_T("BACK"), _T("BACK"), std::bind(&GuiGamelistFilter::applyFilters, this)); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); } @@ -61,7 +61,7 @@ void GuiGamelistFilter::addFiltersToMenu() FilterIndexType type = (*it).type; // type of filter std::map* allKeys = (*it).allIndexKeys; // all possible filters for this type - std::string menuLabel = (*it).menuLabel; // text to show in menu + std::string menuLabel = _L((*it).menuLabel); // text to show in menu std::shared_ptr< OptionListComponent > optionList; @@ -112,6 +112,6 @@ bool GuiGamelistFilter::input(InputConfig* config, Input input) std::vector GuiGamelistFilter::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); return prompts; } diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 7510f0c5c5..b789667da8 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -34,7 +34,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if(curChar < startChar || curChar > endChar) curChar = startChar; - mJumpToLetterList = std::make_shared(mWindow, "JUMP TO...", false); + mJumpToLetterList = std::make_shared(mWindow, _T("JUMP TO LETTER"), false); for (char c = startChar; c <= endChar; c++) { // check if c is a valid first letter in current list @@ -50,9 +50,10 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui } } - row.addElement(std::make_shared(mWindow, "JUMP TO...", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("JUMP TO LETTER"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.addElement(mJumpToLetterList, false); - row.input_handler = [&](InputConfig* config, Input input) { + row.input_handler = [&](InputConfig* config, Input input) + { if(config->isMappedTo("a", input) && input.value) { jumpToLetter(); @@ -67,20 +68,20 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui mMenu.addRow(row); // sort list by - mListSort = std::make_shared(mWindow, "SORT GAMES BY", false); + mListSort = std::make_shared(mWindow, _T("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 + mListSort->add(_L(Utils::String::toUpper(sort.description)), &sort, i == 0); // TODO - actually make the sort type persistent } - mMenu.addWithLabel("SORT GAMES BY", mListSort); + mMenu.addWithLabel(_T("SORT GAMES BY"), mListSort); } // show filtered menu if(!Settings::getInstance()->getBool("ForceDisableFilters")) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FILTER GAMELIST", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("APPLY FILTER"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); mMenu.addRow(row); @@ -109,7 +110,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "EDIT THIS GAME'S METADATA", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("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); @@ -198,7 +199,7 @@ void GuiGamelistOptions::openMetaDataEd() } mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(), p, Utils::FileSystem::getFileName(file->getPath()), - std::bind(&IGameListView::onFileChanged, ViewController::get()->getGameListView(file->getSystem()).get(), file, FILE_METADATA_CHANGED), deleteBtnFunc)); + std::bind(&IGameListView::onFileChanged, ViewController::get()->getGameListView(file->getSystem()).get(), file, FILE_METADATA_CHANGED), deleteBtnFunc, file)); } void GuiGamelistOptions::jumpToLetter() @@ -257,7 +258,7 @@ HelpStyle GuiGamelistOptions::getHelpStyle() std::vector GuiGamelistOptions::getHelpPrompts() { auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "close")); + prompts.push_back(HelpPrompt("b", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp index 30688c566c..e229aa87c6 100644 --- a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp +++ b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp @@ -8,7 +8,7 @@ #include "guis/GuiVideoScreensaverOptions.h" #include "Settings.h" -GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, const char* title) : GuiScreensaverOptions(window, title) +GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, std::string title) : GuiScreensaverOptions(window, title) { // screensaver time auto screensaver_time = std::make_shared(mWindow, 0.f, 30.f, 1.f, "m"); diff --git a/es-app/src/guis/GuiGeneralScreensaverOptions.h b/es-app/src/guis/GuiGeneralScreensaverOptions.h index 1992d9ad15..1a850e3871 100644 --- a/es-app/src/guis/GuiGeneralScreensaverOptions.h +++ b/es-app/src/guis/GuiGeneralScreensaverOptions.h @@ -7,7 +7,7 @@ class GuiGeneralScreensaverOptions : public GuiScreensaverOptions { public: - GuiGeneralScreensaverOptions(Window* window, const char* title); + GuiGeneralScreensaverOptions(Window* window, std::string title); virtual ~GuiGeneralScreensaverOptions(); private: diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 59d8ae4150..4b1cf2aea6 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -19,32 +19,35 @@ #include #include -GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, "MAIN MENU"), mVersion(window) +GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) + { bool isFullUI = UIModeController::getInstance()->isUIModeFull(); - if (isFullUI) - addEntry("SCRAPER", 0x777777FF, true, [this] { openScraperSettings(); }); - - addEntry("SOUND SETTINGS", 0x777777FF, true, [this] { openSoundSettings(); }); - if (isFullUI) - addEntry("UI SETTINGS", 0x777777FF, true, [this] { openUISettings(); }); + { + addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); + addEntry(_T("CONFIGURE INPUT"), 0x777777FF, true, [this] { openConfigInput(); }); + } - if (isFullUI) - addEntry("GAME COLLECTION SETTINGS", 0x777777FF, true, [this] { openCollectionSystemSettings(); }); + addEntry(_T("SOUND SETTINGS"), 0x777777FF, true, [this] { openSoundSettings(); }); if (isFullUI) - addEntry("OTHER SETTINGS", 0x777777FF, true, [this] { openOtherSettings(); }); + addEntry(_T("SCRAPER"), 0x777777FF, true, [this] { openScraperSettings(); }); if (isFullUI) - addEntry("CONFIGURE INPUT", 0x777777FF, true, [this] { openConfigInput(); }); + { + addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); + addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); + } - addEntry("QUIT", 0x777777FF, true, [this] {openQuitMenu(); }); + + addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); addChild(&mMenu); addVersionInfo(); + setSize(mMenu.getSize()); setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, Renderer::getScreenHeight() * 0.15f); } @@ -88,12 +91,12 @@ void GuiMenu::openScraperSettings() void GuiMenu::openSoundSettings() { - auto s = new GuiSettings(mWindow, "SOUND SETTINGS"); - + auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); + // volume auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); volume->setValue((float)VolumeControl::getInstance()->getVolume()); - s->addWithLabel("SYSTEM VOLUME", volume); + s->addWithLabel(_T("SYSTEM VOLUME"), volume); s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); if (UIModeController::getInstance()->isUIModeFull()) @@ -154,7 +157,7 @@ void GuiMenu::openSoundSettings() // disable sounds auto sounds_enabled = std::make_shared(mWindow); sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); - s->addWithLabel("ENABLE NAVIGATION SOUNDS", sounds_enabled); + s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); s->addSaveFunc([sounds_enabled] { if (sounds_enabled->getState() && !Settings::getInstance()->getBool("EnableSounds") @@ -168,7 +171,7 @@ void GuiMenu::openSoundSettings() auto video_audio = std::make_shared(mWindow); video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); - s->addWithLabel("ENABLE VIDEO AUDIO", video_audio); + s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); #ifdef _RPI_ @@ -202,72 +205,95 @@ void GuiMenu::openSoundSettings() void GuiMenu::openUISettings() { - auto s = new GuiSettings(mWindow, "UI SETTINGS"); + auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); - //UI mode - auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); - std::vector UImodes = UIModeController::getInstance()->getUIModes(); - for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) - UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); - s->addWithLabel("UI MODE", UImodeSelection); - Window* window = mWindow; - s->addSaveFunc([ UImodeSelection, window] + // theme set + auto themeSets = ThemeData::getThemeSets(); + + if (!themeSets.empty()) { - std::string selectedMode = UImodeSelection->getSelected(); - if (selectedMode != "Full") + std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if (selectedSet == themeSets.cend()) + selectedSet = themeSets.cbegin(); + + auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); + for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) + theme_set->add(it->first, it->first, it == selectedSet); + s->addWithLabel(_T("THEME"), theme_set); + + Window* window = mWindow; + s->addSaveFunc([window, theme_set] { - std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; - msg += "This will hide most menu-options to prevent changes to the system.\n"; - msg += "To unlock and return to the full UI, enter this code: \n"; - msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; - msg += "Do you want to proceed?"; - window->pushGui(new GuiMsgBox(window, msg, - "YES", [selectedMode] { - LOG(LogDebug) << "Setting UI mode to " << selectedMode; - Settings::getInstance()->setString("UIMode", selectedMode); - Settings::getInstance()->saveFile(); - }, "NO",nullptr)); - } - }); + bool needReload = false; + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + if (oldTheme != theme_set->getSelected()) + needReload = true; - // screensaver - ComponentListRow screensaver_row; - screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, "SCREENSAVER SETTINGS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - screensaver_row.addElement(makeArrow(mWindow), false); - screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); - s->addRow(screensaver_row); + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - // quick system select (left/right in game list view) - auto quick_sys_select = std::make_shared(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()); }); + if (needReload) + { + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + } + }); + } - // carousel transition option - auto move_carousel = std::make_shared(mWindow); - move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); - s->addWithLabel("CAROUSEL TRANSITIONS", move_carousel); - s->addSaveFunc([move_carousel] { - if (move_carousel->getState() - && !Settings::getInstance()->getBool("MoveCarousel") - && PowerSaver::getMode() == PowerSaver::INSTANT) + // LANGUAGE + + std::vector langues; + langues.push_back("en"); + + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); + if (xmlpath.length() > 0) + { + xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); + if (Utils::FileSystem::isDirectory(*it)) + continue; + + std::string name = *it; + + if (name.rfind("emulationstation2.po") == std::string::npos) + continue; + + name = Utils::FileSystem::getParent(name); + name = Utils::FileSystem::getFileName(name); + + if (name != "en") + langues.push_back(name); } - Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); - }); + + if (langues.size() > 1) + { + auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); + + for (auto it = langues.cbegin(); it != langues.cend(); it++) + language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); + + s->addWithLabel(_T("LANGUAGE"), language); + s->addSaveFunc([language] { + Settings::getInstance()->setString("Language", language->getSelected()); + }); + } + } // transition style - auto transition_style = std::make_shared< OptionListComponent >(mWindow, "TRANSITION STYLE", false); + auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); std::vector transitions; transitions.push_back("fade"); transitions.push_back("slide"); transitions.push_back("instant"); - for(auto it = transitions.cbegin(); it != transitions.cend(); it++) - transition_style->add(*it, *it, Settings::getInstance()->getString("TransitionStyle") == *it); - s->addWithLabel("TRANSITION STYLE", transition_style); + for (auto it = transitions.cbegin(); it != transitions.cend(); it++) + transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); + + s->addWithLabel(_T("TRANSITION STYLE"), transition_style); s->addSaveFunc([transition_style] { if (Settings::getInstance()->getString("TransitionStyle") == "instant" && transition_style->getSelected() != "instant" @@ -279,52 +305,41 @@ void GuiMenu::openUISettings() Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); }); - // theme set - auto themeSets = ThemeData::getThemeSets(); - - if(!themeSets.empty()) - { - std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(selectedSet == themeSets.cend()) - selectedSet = themeSets.cbegin(); - auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME SET", false); - for(auto it = themeSets.cbegin(); it != themeSets.cend(); it++) - theme_set->add(it->first, it->first, it == selectedSet); - s->addWithLabel("THEME SET", theme_set); + auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); + std::vector gameTransitions; + gameTransitions.push_back("fade"); + gameTransitions.push_back("slide"); + gameTransitions.push_back("instant"); + for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) + transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); - Window* window = mWindow; - s->addSaveFunc([window, theme_set] + s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); + s->addSaveFunc([transitionOfGames_style] { + if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" + && transitionOfGames_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) { - bool needReload = false; - std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - if(oldTheme != theme_set->getSelected()) - needReload = true; - - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - - if(needReload) - { - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation - } - }); - } + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); + }); // GameList view style - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, "GAMELIST VIEW STYLE", false); + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); std::vector styles; styles.push_back("automatic"); styles.push_back("basic"); styles.push_back("detailed"); styles.push_back("video"); styles.push_back("grid"); + styles.push_back("gridex"); for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(*it, *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); - s->addWithLabel("GAMELIST VIEW STYLE", gamelist_style); + gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); + + s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); s->addSaveFunc([gamelist_style] { bool needReload = false; if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) @@ -334,9 +349,10 @@ void GuiMenu::openUISettings() ViewController::get()->reloadAll(); }); + // Optionally start in selected system - auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, "START ON SYSTEM", false); - systemfocus_list->add("NONE", "", Settings::getInstance()->getString("StartupSystem") == ""); + auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); + systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { if ("retropie" != (*it)->getName()) @@ -344,21 +360,87 @@ void GuiMenu::openUISettings() systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); } } - s->addWithLabel("START ON SYSTEM", systemfocus_list); + s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); s->addSaveFunc([systemfocus_list] { Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); }); + + /* + //UI mode + auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); + std::vector UImodes = UIModeController::getInstance()->getUIModes(); + for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) + UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); + s->addWithLabel("UI MODE", UImodeSelection); + Window* window = mWindow; + s->addSaveFunc([ UImodeSelection, window] + { + std::string selectedMode = UImodeSelection->getSelected(); + if (selectedMode != "Full") + { + std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; + msg += "This will hide most menu-options to prevent changes to the system.\n"; + msg += "To unlock and return to the full UI, enter this code: \n"; + msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; + msg += "Do you want to proceed?"; + window->pushGui(new GuiMsgBox(window, msg, + "YES", [selectedMode] { + LOG(LogDebug) << "Setting UI mode to " << selectedMode; + Settings::getInstance()->setString("UIMode", selectedMode); + Settings::getInstance()->saveFile(); + }, "NO",nullptr)); + } + }); + */ + + // screensaver + ComponentListRow screensaver_row; + screensaver_row.elements.clear(); + screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + screensaver_row.addElement(makeArrow(mWindow), false); + screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); + s->addRow(screensaver_row); + + + // quick system select (left/right in game list view) + auto hideWindowScreen = std::make_shared(mWindow); + hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); + s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); + s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); + + + // quick system select (left/right in game list view) + auto quick_sys_select = std::make_shared(mWindow); + quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); + s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); + s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); + + // carousel transition option + auto move_carousel = std::make_shared(mWindow); + move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); + s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); + s->addSaveFunc([move_carousel] { + if (move_carousel->getState() + && !Settings::getInstance()->getBool("MoveCarousel") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); + }); + // show help auto show_help = std::make_shared(mWindow); show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); - s->addWithLabel("ON-SCREEN HELP", show_help); + s->addWithLabel(_T("ON-SCREEN HELP"), show_help); s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); // enable filters (ForceDisableFilters) auto enable_filter = std::make_shared(mWindow); enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); - s->addWithLabel("ENABLE FILTERS", enable_filter); + s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); s->addSaveFunc([enable_filter] { bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); @@ -371,16 +453,16 @@ void GuiMenu::openUISettings() void GuiMenu::openOtherSettings() { - auto s = new GuiSettings(mWindow, "OTHER SETTINGS"); + auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); // maximum vram - auto max_vram = std::make_shared(mWindow, 0.f, 1000.f, 10.f, "Mb"); + auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel("VRAM LIMIT", max_vram); + s->addWithLabel(_T("VRAM LIMIT"), max_vram); s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); // power saver - auto power_saver = std::make_shared< OptionListComponent >(mWindow, "POWER SAVER MODES", false); + auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); std::vector modes; modes.push_back("disabled"); modes.push_back("default"); @@ -388,7 +470,8 @@ void GuiMenu::openOtherSettings() modes.push_back("instant"); for (auto it = modes.cbegin(); it != modes.cend(); it++) power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); - s->addWithLabel("POWER SAVER MODES", power_saver); + + s->addWithLabel(_T("POWER SAVER MODES"), power_saver); s->addSaveFunc([this, power_saver] { if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { Settings::getInstance()->setString("TransitionStyle", "instant"); @@ -402,23 +485,23 @@ void GuiMenu::openOtherSettings() // gamelists auto save_gamelists = std::make_shared(mWindow); save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); - s->addWithLabel("SAVE METADATA ON EXIT", save_gamelists); + s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); auto parse_gamelists = std::make_shared(mWindow); parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); - s->addWithLabel("PARSE GAMESLISTS ONLY", parse_gamelists); + s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); - + /* auto local_art = std::make_shared(mWindow); local_art->setState(Settings::getInstance()->getBool("LocalArt")); - s->addWithLabel("SEARCH FOR LOCAL ART", local_art); + s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); - + */ // hidden files auto hidden_files = std::make_shared(mWindow); hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); - s->addWithLabel("SHOW HIDDEN FILES", hidden_files); + s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); #ifdef _RPI_ @@ -442,11 +525,12 @@ void GuiMenu::openOtherSettings() #endif // framerate + /* auto framerate = std::make_shared(mWindow); framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); - s->addWithLabel("SHOW FRAMERATE", framerate); + s->addWithLabel(_T("SHOW FRAMERATE"), framerate); s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); - + */ mWindow->pushGui(s); @@ -455,17 +539,23 @@ void GuiMenu::openOtherSettings() void GuiMenu::openConfigInput() { Window* window = mWindow; - window->pushGui(new GuiMsgBox(window, "ARE YOU SURE YOU WANT TO CONFIGURE INPUT?", "YES", + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + /* + window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), [window] { window->pushGui(new GuiDetectDevice(window, false, nullptr)); - }, "NO", nullptr) - ); + }, _T("NO"), nullptr) + );*/ } void GuiMenu::openQuitMenu() { - auto s = new GuiSettings(mWindow, "QUIT"); + Scripting::fireEvent("quit"); + quitES(""); + return; + + auto s = new GuiSettings(mWindow, _T("QUIT")); Window* window = mWindow; @@ -540,7 +630,7 @@ void GuiMenu::addVersionInfo() } void GuiMenu::openScreensaverOptions() { - mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, "SCREENSAVER SETTINGS")); + mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); } void GuiMenu::openCollectionSystemSettings() { @@ -553,7 +643,7 @@ void GuiMenu::onSizeChanged() mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); } -void GuiMenu::addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func) +void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) { std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); @@ -596,8 +686,8 @@ HelpStyle GuiMenu::getHelpStyle() std::vector GuiMenu::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("up/down", "choose")); - prompts.push_back(HelpPrompt("a", "select")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("SELECT"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiMenu.h b/es-app/src/guis/GuiMenu.h index da8406bb79..25fdcc2634 100644 --- a/es-app/src/guis/GuiMenu.h +++ b/es-app/src/guis/GuiMenu.h @@ -16,7 +16,7 @@ class GuiMenu : public GuiComponent HelpStyle getHelpStyle() override; private: - void addEntry(const char* name, unsigned int color, bool add_arrow, const std::function& func); + void addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func); void addVersionInfo(); void openCollectionSystemSettings(); void openConfigInput(); diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index f07a5061e6..5f0df4f525 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -20,7 +20,7 @@ #include "Window.h" GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, - const std::string& /*header*/, std::function saveCallback, std::function deleteFunc) : GuiComponent(window), + const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : GuiComponent(window), mScraperParams(scraperParams), mBackground(window, ":/frame.png"), @@ -35,9 +35,10 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector mHeaderGrid = std::make_shared(mWindow, Vector2i(1, 5)); - mTitle = std::make_shared(mWindow, "EDIT METADATA", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); + mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER); + mHeaderGrid->setEntry(mTitle, Vector2i(0, 1), false, true); mHeaderGrid->setEntry(mSubtitle, Vector2i(0, 3), false, true); @@ -46,6 +47,11 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector mList = std::make_shared(mWindow); mGrid.setEntry(mList, Vector2i(0, 1), true, true); + SystemData* system = file->getSystem(); + + auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); + auto core_choice = std::make_shared>(mWindow, _T("CORE"), false); + // populate list for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) { @@ -55,21 +61,104 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector if(iter->isStatistic) continue; + if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || + iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || + iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") + 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(mWindow, Utils::String::toUpper(iter->displayName), Font::get(FONT_SIZE_SMALL), 0x777777FF); + + if (iter->displayName == "emulator") + { + // if (file->getSystemEnvData()->mEmulators.size() <= 1) + // continue; + + std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); + + std::string currentEmul = file->getEmulator(); + + if (defaultEmul.length() == 0) + emul_choice->add(_T("DEFAULT"), "", false); + else + emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); + + for (auto core : file->getSystemEnvData()->mEmulators) + emul_choice->add(core.mName, core.mName, core.mName == currentEmul); + + row.addElement(std::make_shared(mWindow, _T("EMULATOR"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(emul_choice, false); + + mList->addRow(row); + emul_choice->setTag(iter->key); + mEditors.push_back(emul_choice); + + emul_choice->setSelectedChangedCallback([this, system, core_choice, file](std::string emulatorName) + { + std::string currentCore = file->getCore(); + + std::string defaultCore = system->getSystemEnvData()->getDefaultCore(emulatorName); + if (emulatorName.length() == 0) + defaultCore = system->getSystemEnvData()->getDefaultCore(system->getSystemEnvData()->getDefaultEmulator()); + + core_choice->clear(); + if (defaultCore.length() == 0) + core_choice->add(_T("DEFAULT"), "", false); + else + core_choice->add(_T("DEFAULT")+" ("+ defaultCore+")", "", false); + + std::vector cores = system->getSystemEnvData()->getCores(emulatorName); + + bool found = false; + + for (auto it = cores.begin(); it != cores.end(); it++) + { + std::string core = *it; + core_choice->add(core, core, currentCore == core); + if (currentCore == core) + found = true; + } + + if (!found) + core_choice->selectFirstItem(); + else + core_choice->invalidate(); + }); + + continue; + } + + if (iter->displayName == "core") + { + // core_choice->add(_T("DEFAULT"), "", true); + core_choice->setTag(iter->key); + + row.addElement(std::make_shared(mWindow, "CORE", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(core_choice, false); + + mList->addRow(row); + ed = core_choice; + + mEditors.push_back(core_choice); + + // force change event to load core list + emul_choice->invalidate(); + continue; + } + + auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); row.addElement(lbl, true); // label - switch(iter->type) + switch (iter->type) { - case MD_BOOL: + case MD_BOOL: { ed = std::make_shared(window); row.addElement(ed, false, true); break; } - case MD_RATING: + case MD_RATING: { ed = std::make_shared(window); const float height = lbl->getSize().y() * 0.71f; @@ -85,7 +174,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector break; } - case MD_DATE: + case MD_DATE: { ed = std::make_shared(window); row.addElement(ed, false); @@ -99,14 +188,14 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector break; } - case MD_TIME: + case MD_TIME: { ed = std::make_shared(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); row.addElement(ed, false); break; } - case MD_MULTILINE_STRING: - default: + case MD_MULTILINE_STRING: + default: { // MD_STRING ed = std::make_shared(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); @@ -129,27 +218,30 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector }); break; } - } + } assert(ed); mList->addRow(row); + + ed->setTag(iter->key); ed->setValue(mMetaData->get(iter->key)); + mEditors.push_back(ed); } std::vector< std::shared_ptr > buttons; - if(!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) - buttons.push_back(std::make_shared(mWindow, "SCRAPE", "scrape", std::bind(&GuiMetaDataEd::fetch, this))); + if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) + buttons.push_back(std::make_shared(mWindow, _T("SCRAPE"), _T("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); - buttons.push_back(std::make_shared(mWindow, "SAVE", "save", [&] { save(); delete this; })); - buttons.push_back(std::make_shared(mWindow, "CANCEL", "cancel", [&] { delete this; })); + buttons.push_back(std::make_shared(mWindow, _T("SAVE"), _T("SAVE"), [&] { save(); delete this; })); + buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); if(mDeleteFunc) { auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; - buttons.push_back(std::make_shared(mWindow, "DELETE", "delete", deleteBtnFunc)); + buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE"), deleteBtnFunc)); } mButtons = makeButtonGrid(mWindow, buttons); @@ -184,17 +276,26 @@ void GuiMetaDataEd::save() // remove game from index mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); - for(unsigned int i = 0; i < mEditors.size(); i++) - { - if(mMetaDataDecl.at(i).isStatistic) - continue; - mMetaData->set(mMetaDataDecl.at(i).key, mEditors.at(i)->getValue()); + for (unsigned int i = 0; i < mEditors.size(); i++) + { + std::shared_ptr ed = mEditors.at(i); + + auto val = ed->getValue(); + auto key = ed->getTag(); + + if (key == "core" || key == "emulator") + { + std::shared_ptr> list = std::static_pointer_cast>(ed); + val = list->getSelected(); + } + + mMetaData->set(key, val); } // enter game in index mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); - if(mSavedCallback) + if (mSavedCallback) mSavedCallback(); // update respective Collection Entries @@ -211,10 +312,8 @@ 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; + auto val = mEditors.at(i)->getValue(); + auto key = mEditors.at(i)->getTag(); mEditors.at(i)->setValue(result.mdl.get(key)); } } @@ -225,7 +324,7 @@ void GuiMetaDataEd::close(bool closeAllWindows) bool dirty = false; for(unsigned int i = 0; i < mEditors.size(); i++) { - const std::string& key = mMetaDataDecl.at(i).key; + auto key = mEditors.at(i)->getTag(); if(mMetaData->get(key) != mEditors.at(i)->getValue()) { dirty = true; @@ -250,9 +349,9 @@ void GuiMetaDataEd::close(bool closeAllWindows) { // 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 + "ENREGISTRER LES MODIFICATIONS ?", + "OUI", [this, closeFunc] { save(); closeFunc(); }, + "NON", closeFunc )); }else{ closeFunc(); @@ -277,7 +376,7 @@ bool GuiMetaDataEd::input(InputConfig* config, Input input) std::vector GuiMetaDataEd::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiMetaDataEd.h b/es-app/src/guis/GuiMetaDataEd.h index ee192f982b..8bdf3531d1 100644 --- a/es-app/src/guis/GuiMetaDataEd.h +++ b/es-app/src/guis/GuiMetaDataEd.h @@ -5,17 +5,20 @@ #include "components/ComponentGrid.h" #include "components/NinePatchComponent.h" #include "scrapers/Scraper.h" +#include "components/MenuComponent.h" +#include "components/OptionListComponent.h" #include "GuiComponent.h" #include "MetaData.h" class ComponentList; class TextComponent; +class FileData; class GuiMetaDataEd : public GuiComponent { public: GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams params, - const std::string& header, std::function savedCallback, std::function deleteFunc); + const std::string& header, std::function savedCallback, std::function deleteFunc, FileData* file); bool input(InputConfig* config, Input input) override; void onSizeChanged() override; @@ -38,6 +41,9 @@ class GuiMetaDataEd : public GuiComponent ScraperSearchParams mScraperParams; + // typedef OptionListComponent CoreList; + //std::shared_ptr mCoreList; + std::vector< std::shared_ptr > mEditors; std::vector mMetaDataDecl; diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 28b87db40f..7dcd2d16bd 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -32,7 +32,7 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queue(mWindow, "SCRAPING IN PROGRESS", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); mGrid.setEntry(mTitle, Vector2i(0, 0), false, true); - mSystem = std::make_shared(mWindow, "SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); + mSystem = std::make_shared(mWindow, _T("SYSTEM"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); mGrid.setEntry(mSystem, Vector2i(0, 1), false, true); mSubtitle = std::make_shared(mWindow, "subtitle text", Font::get(FONT_SIZE_SMALL), 0x888888FF, ALIGN_CENTER); diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index 10c2a9b867..2330f095f6 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -34,8 +34,8 @@ GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), 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.addButton("START", "LANCER", std::bind(&GuiScraperStart::pressedStart, this)); + mMenu.addButton(_T("BACK"), _T("BACK"), [&] { delete this; }); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); } @@ -122,7 +122,7 @@ bool GuiScraperStart::input(InputConfig* config, Input input) std::vector GuiScraperStart::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiScreensaverOptions.cpp b/es-app/src/guis/GuiScreensaverOptions.cpp index e9d2f068c3..f6d33f49f2 100644 --- a/es-app/src/guis/GuiScreensaverOptions.cpp +++ b/es-app/src/guis/GuiScreensaverOptions.cpp @@ -5,11 +5,11 @@ #include "SystemData.h" #include "Window.h" -GuiScreensaverOptions::GuiScreensaverOptions(Window* window, const char* title) : GuiComponent(window), mMenu(window, title) +GuiScreensaverOptions::GuiScreensaverOptions(Window* window, std::string title) : GuiComponent(window), mMenu(window, title) { addChild(&mMenu); - mMenu.addButton("BACK", "go back", [this] { delete this; }); + mMenu.addButton(_T("BACK"), _T("BACK"), [this] { delete this; }); setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); @@ -62,8 +62,8 @@ std::vector GuiScreensaverOptions::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiScreensaverOptions.h b/es-app/src/guis/GuiScreensaverOptions.h index 6f61489f8b..e8e4332e1d 100644 --- a/es-app/src/guis/GuiScreensaverOptions.h +++ b/es-app/src/guis/GuiScreensaverOptions.h @@ -8,7 +8,7 @@ class GuiScreensaverOptions : public GuiComponent { public: - GuiScreensaverOptions(Window* window, const char* title); + GuiScreensaverOptions(Window* window, std::string title); virtual ~GuiScreensaverOptions(); // just calls save(); virtual void save(); diff --git a/es-app/src/guis/GuiSettings.cpp b/es-app/src/guis/GuiSettings.cpp index 53f96c439e..f0d3dd697a 100644 --- a/es-app/src/guis/GuiSettings.cpp +++ b/es-app/src/guis/GuiSettings.cpp @@ -5,11 +5,11 @@ #include "SystemData.h" #include "Window.h" -GuiSettings::GuiSettings(Window* window, const char* title) : GuiComponent(window), mMenu(window, title) +GuiSettings::GuiSettings(Window* window, std::string title) : GuiComponent(window), mMenu(window, title) { addChild(&mMenu); - mMenu.addButton("BACK", "go back", [this] { delete this; }); + mMenu.addButton(_T("BACK"), _T("BACK"), [this] { delete this; }); setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); @@ -62,8 +62,8 @@ std::vector GuiSettings::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); - prompts.push_back(HelpPrompt("start", "close")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiSettings.h b/es-app/src/guis/GuiSettings.h index d5b0d97998..05ece518f2 100644 --- a/es-app/src/guis/GuiSettings.h +++ b/es-app/src/guis/GuiSettings.h @@ -8,7 +8,7 @@ class GuiSettings : public GuiComponent { public: - GuiSettings(Window* window, const char* title); + GuiSettings(Window* window, std::string title); virtual ~GuiSettings(); // just calls save(); void save(); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 6c1fe80ef6..136bee9434 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -35,7 +35,7 @@ bool parseArgs(int argc, char* argv[]) for(int i = 1; i < argc; i++) { - if(strcmp(argv[i], "--resolution") == 0) + if (strcmp(argv[i], "--resolution") == 0) { if(i >= argc - 2) { @@ -48,9 +48,10 @@ bool parseArgs(int argc, char* argv[]) i += 2; // skip the argument value Settings::getInstance()->setInt("WindowWidth", width); Settings::getInstance()->setInt("WindowHeight", height); - }else if(strcmp(argv[i], "--screensize") == 0) + } + else if (strcmp(argv[i], "--screensize") == 0) { - if(i >= argc - 2) + if (i >= argc - 2) { std::cerr << "Invalid screensize supplied."; return false; @@ -61,7 +62,8 @@ bool parseArgs(int argc, char* argv[]) i += 2; // skip the argument value Settings::getInstance()->setInt("ScreenWidth", width); Settings::getInstance()->setInt("ScreenHeight", height); - }else if(strcmp(argv[i], "--screenoffset") == 0) + } + else if (strcmp(argv[i], "--screenoffset") == 0) { if(i >= argc - 2) { @@ -74,7 +76,8 @@ bool parseArgs(int argc, char* argv[]) i += 2; // skip the argument value Settings::getInstance()->setInt("ScreenOffsetX", x); Settings::getInstance()->setInt("ScreenOffsetY", y); - }else if (strcmp(argv[i], "--screenrotate") == 0) + } + else if (strcmp(argv[i], "--screenrotate") == 0) { if (i >= argc - 1) { @@ -85,44 +88,60 @@ bool parseArgs(int argc, char* argv[]) int rotate = atoi(argv[i + 1]); ++i; // skip the argument value Settings::getInstance()->setInt("ScreenRotate", rotate); - }else if(strcmp(argv[i], "--gamelist-only") == 0) + } + else if (strcmp(argv[i], "--gamelist-only") == 0) { Settings::getInstance()->setBool("ParseGamelistOnly", true); - }else if(strcmp(argv[i], "--ignore-gamelist") == 0) + } + else if (strcmp(argv[i], "--ignore-gamelist") == 0) { Settings::getInstance()->setBool("IgnoreGamelist", true); - }else if(strcmp(argv[i], "--show-hidden-files") == 0) + } + else if (strcmp(argv[i], "--show-hidden-files") == 0) { Settings::getInstance()->setBool("ShowHiddenFiles", true); - }else if(strcmp(argv[i], "--draw-framerate") == 0) + } + else if (strcmp(argv[i], "--draw-framerate") == 0) { Settings::getInstance()->setBool("DrawFramerate", true); - }else if(strcmp(argv[i], "--no-exit") == 0) + } + else if (strcmp(argv[i], "--no-exit") == 0) { Settings::getInstance()->setBool("ShowExit", false); - }else if(strcmp(argv[i], "--no-splash") == 0) + } + else if (strcmp(argv[i], "--no-splash") == 0) { Settings::getInstance()->setBool("SplashScreen", false); - }else if(strcmp(argv[i], "--debug") == 0) + } + else if (strcmp(argv[i], "--debug") == 0) { Settings::getInstance()->setBool("Debug", true); Settings::getInstance()->setBool("HideConsole", false); Log::setReportingLevel(LogDebug); - }else if(strcmp(argv[i], "--fullscreen-borderless") == 0) + } + else if (strcmp(argv[i], "--fullscreen-borderless") == 0) { Settings::getInstance()->setBool("FullscreenBorderless", true); - }else if(strcmp(argv[i], "--windowed") == 0) + } + else if (strcmp(argv[i], "--fullscreen") == 0) { - Settings::getInstance()->setBool("Windowed", true); - }else if(strcmp(argv[i], "--vsync") == 0) + Settings::getInstance()->setBool("FullscreenBorderless", false); + } + else if (strcmp(argv[i], "--windowed") == 0 || strcmp(argv[i], "-windowed") == 0) { - bool vsync = (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "1") == 0) ? true : false; + Settings::getInstance()->setBool("Windowed", true); + } + else if (strcmp(argv[i], "--vsync") == 0 || strcmp(argv[i], "-vsync") == 0) + { + bool vsync = strlen(argv[i]) <= 7 ? true : (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "1") == 0) ? true : false; Settings::getInstance()->setBool("VSync", vsync); i++; // skip vsync value - }else if(strcmp(argv[i], "--scrape") == 0) + } + else if (strcmp(argv[i], "--scrape") == 0) { scrape_cmdline = true; - }else if(strcmp(argv[i], "--max-vram") == 0) + } + else if (strcmp(argv[i], "--max-vram") == 0) { int maxVRAM = atoi(argv[i + 1]); Settings::getInstance()->setInt("MaxVRAM", maxVRAM); @@ -197,25 +216,29 @@ bool verifyHomeFolderExists() } // Returns true if everything is OK, -bool loadSystemConfigFile(const char** errorString) +bool loadSystemConfigFile(Window* window, const char** errorString) { *errorString = NULL; - - if(!SystemData::loadConfig()) + + if(!SystemData::loadConfig(window)) { 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; } @@ -232,8 +255,9 @@ int main(int argc, char* argv[]) { srand((unsigned int)time(NULL)); - std::locale::global(std::locale("C")); - + //std::locale::global(std::locale("C")); + std::locale::global(std::locale("en-US")); + if(!parseArgs(argc, argv)) return 0; @@ -311,29 +335,33 @@ int main(int argc, char* argv[]) LOG(LogInfo) << " ARB_texture_non_power_of_two: " << (glExts.find("ARB_texture_non_power_of_two") != std::string::npos ? "ok" : "MISSING"); if(splashScreen) { - std::string progressText = "Loading..."; + std::string progressText = "Chargement"; + if (splashScreenProgress) - progressText = "Loading system config..."; + progressText = "Chargement de la configuration"; + window.renderLoadingScreen(progressText); } } const char* errorMsg = NULL; - if(!loadSystemConfigFile(&errorMsg)) + if(!loadSystemConfigFile(&window, &errorMsg)) { // something went terribly wrong - if(errorMsg == NULL) + if (errorMsg == NULL) { LOG(LogError) << "Unknown error occured while parsing system config file."; - if(!scrape_cmdline) + + 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", [] { + _T("QUIT"), [] { SDL_Event* quit = new SDL_Event(); quit->type = SDL_QUIT; SDL_PushEvent(quit); @@ -341,30 +369,26 @@ int main(int argc, char* argv[]) } //run the command line scraper then quit - if(scrape_cmdline) - { + 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(); - - if(splashScreen && splashScreenProgress) - window.renderLoadingScreen("Done."); + // ViewController::get()->preload(); + + if (splashScreen && splashScreenProgress) + window.renderLoadingScreen(_T("Starting UI")); //choose which GUI to open depending on if an input configuration already exists if(errorMsg == NULL) { - if(Utils::FileSystem::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0) - { - ViewController::get()->goToStart(); - }else{ - window.pushGui(new GuiDetectDevice(&window, true, [] { ViewController::get()->goToStart(); })); - } + if (Utils::FileSystem::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0) + ViewController::get()->goToStart(true); + else + window.pushGui(new GuiDetectDevice(&window, true, [] { ViewController::get()->goToStart(true); })); } //generate joystick events since we're done loading @@ -386,9 +410,10 @@ int main(int argc, char* argv[]) { InputManager::getInstance()->parseEvent(event, &window); - if(event.type == SDL_QUIT) + if (event.type == SDL_QUIT) running = false; - } while(SDL_PollEvent(&event)); + } + while(SDL_PollEvent(&event)); // triggered if exiting from SDL_WaitEvent due to event if (ps_standby) @@ -429,6 +454,7 @@ int main(int argc, char* argv[]) while(window.peekGui() != ViewController::get()) delete window.peekGui(); + window.deinit(); MameNames::deinit(); diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index e9eb53378b..31f32ea408 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -125,7 +125,17 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, ScreenScraperRequest::ScreenScraperConfig ssConfig; - path = ssConfig.getGameSearchUrl(params.game->getFileName()); + if (params.nameOverride.length() == 0) + { + path = ssConfig.getGameSearchUrl(params.game->getFileName()); + path += "&romtype=rom"; + } + else + { + path = ssConfig.getGameSearchUrl(params.nameOverride); + path += "&romtype=jeu"; + } + auto& platforms = params.system->getPlatformIds(); for (auto platformIt = platforms.cbegin(); platformIt != platforms.cend(); platformIt++) @@ -142,7 +152,6 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, requests.push(std::unique_ptr(new ScreenScraperRequest(requests, results, path))); } - } void ScreenScraperRequest::process(const std::unique_ptr& req, std::vector& results) diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index 4c749008f1..55c4a89c4f 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -28,7 +28,7 @@ class ScreenScraperRequest : public ScraperHttpRequest const std::string API_DEV_P = { 108, 28, 54, 55, 83, 43, 91, 44, 30, 22, 41, 12, 0, 108, 38, 29 }; const std::string API_DEV_KEY = { 54, 73, 115, 100, 101, 67, 111, 107, 79, 66, 68, 66, 67, 56, 118, 77, 54, 88, 101, 54 }; const std::string API_URL_BASE = "https://www.screenscraper.fr/api2"; - const std::string API_SOFT_NAME = "Emulationstation " + static_cast(PROGRAM_VERSION_STRING); + const std::string API_SOFT_NAME = "Emulationstation 2.9.0"; // +static_cast(PROGRAM_VERSION_STRING); /** Which type of image artwork we need. Possible values (not a comprehensive list): - ss: in-game screenshot diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index cdd5b1ea10..9ed1a22dc6 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -259,7 +259,7 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) if (!getSelected()->isGameSystem()) ss << "CONFIGURATION"; else - ss << gameCount << " GAMES AVAILABLE"; + ss << gameCount << " " << _T("GAMES AVAILABLE"); mSystemInfo.setText(ss.str()); }, false, 1); @@ -376,14 +376,15 @@ std::vector SystemView::getHelpPrompts() { std::vector prompts; if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) - prompts.push_back(HelpPrompt("up/down", "choose")); + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); else - prompts.push_back(HelpPrompt("left/right", "choose")); - prompts.push_back(HelpPrompt("a", "select")); - prompts.push_back(HelpPrompt("x", "random")); + prompts.push_back(HelpPrompt("left/right", _T("CHOOSE"))); - if (!UIModeController::getInstance()->isUIModeKid() && Settings::getInstance()->getBool("ScreenSaverControls")) - prompts.push_back(HelpPrompt("select", "launch screensaver")); + prompts.push_back(HelpPrompt("a", _T("SELECT"))); + //prompts.push_back(HelpPrompt("x", "random")); + +// if (!UIModeController::getInstance()->isUIModeKid() && Settings::getInstance()->getBool("ScreenSaverControls")) + // prompts.push_back(HelpPrompt("select", "launch screensaver")); return prompts; } diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index ada681e260..65fb1b4122 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -43,7 +43,7 @@ ViewController::~ViewController() sInstance = NULL; } -void ViewController::goToStart() +void ViewController::goToStart(bool forceImmediate) { // If specific system is requested, go directly to the game list auto requestedSystem = Settings::getInstance()->getString("StartupSystem"); @@ -52,7 +52,8 @@ void ViewController::goToStart() for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++){ if ((*it)->getName() == requestedSystem) { - goToGameList(*it); + goToSystemView(*it, forceImmediate); +// goToGameList(*it); return; } } @@ -60,14 +61,15 @@ void ViewController::goToStart() // Requested system doesn't exist Settings::getInstance()->setString("StartupSystem", ""); } - goToSystemView(SystemData::sSystemVector.at(0)); + goToSystemView(SystemData::sSystemVector.at(0), forceImmediate); } void ViewController::ReloadAndGoToStart() { - mWindow->renderLoadingScreen("Loading..."); + mWindow->renderLoadingScreen("Chargement"); + ViewController::get()->reloadAll(); - ViewController::get()->goToStart(); + ViewController::get()->goToStart(false); } int ViewController::getSystemId(SystemData* system) @@ -76,7 +78,7 @@ int ViewController::getSystemId(SystemData* system) return (int)(std::find(sysVec.cbegin(), sysVec.cend(), system) - sysVec.cbegin()); } -void ViewController::goToSystemView(SystemData* system) +void ViewController::goToSystemView(SystemData* system, bool forceImmediate) { // Tell any current view it's about to be hidden if (mCurrentView) @@ -95,7 +97,7 @@ void ViewController::goToSystemView(SystemData* system) mCurrentView->onShow(); PowerSaver::setState(true); - playViewTransition(); + playViewTransition(forceImmediate); } void ViewController::goToNextGameList() @@ -139,10 +141,10 @@ void ViewController::goToGameList(SystemData* system) { mCurrentView->onShow(); } - playViewTransition(); + playViewTransition(false); } -void ViewController::playViewTransition() +void ViewController::playViewTransition(bool forceImmediate) { Vector3f target(Vector3f::Zero()); if(mCurrentView) @@ -153,7 +155,7 @@ void ViewController::playViewTransition() return; std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); - if(transition_style == "fade") + if(!forceImmediate && transition_style == "fade") { // fade // stop whatever's currently playing, leaving mFadeOpacity wherever it is @@ -181,7 +183,7 @@ void ViewController::playViewTransition() }else{ advanceAnimation(0, (int)(mFadeOpacity * FADE_DURATION)); } - } else if (transition_style == "slide"){ + } else if (!forceImmediate && transition_style == "slide"){ // slide or simple slide setAnimation(new MoveCameraAnimation(mCamera, target)); updateHelpPrompts(); // update help prompts immediately @@ -212,8 +214,8 @@ void ViewController::launch(FileData* game, Vector3f center) } // Hide the current view - if (mCurrentView) - mCurrentView->onHide(); + //if (mCurrentView) + //mCurrentView->onHide(); Transform4x4f origCamera = mCamera; origCamera.translation() = -mCurrentView->getPosition(); @@ -223,7 +225,7 @@ void ViewController::launch(FileData* game, Vector3f center) mWindow->stopInfoPopup(); // make sure we disable any existing info popup mLockInput = true; - std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); + std::string transition_style = Settings::getInstance()->getString("GameTransitionStyle"); if(transition_style == "fade") { // fade out, launch game, fade back in @@ -240,7 +242,7 @@ void ViewController::launch(FileData* game, Vector3f center) // 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->launchGame(mWindow); + game->launchGame(mWindow); mCamera = origCamera; setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 600), 0, [this] { mLockInput = false; }, true); this->onFileChanged(game, FILE_METADATA_CHANGED); @@ -280,33 +282,72 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste bool themeHasVideoView = system->getTheme()->hasView("video"); + bool themeHasGridView = system->getTheme()->hasView("grid"); + bool themeHasGridExView = system->getTheme()->hasView("gridex"); + //decide type GameListViewType selectedViewType = AUTOMATIC; + bool detailed = false; + std::string viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + if (viewPreference.compare("basic") == 0) selectedViewType = BASIC; - if (viewPreference.compare("detailed") == 0) + else if (viewPreference.compare("detailed") == 0) + { + detailed = true; selectedViewType = DETAILED; - if (viewPreference.compare("grid") == 0) + } + else if (themeHasGridExView && viewPreference.compare("gridex") == 0) + selectedViewType = GRIDEX; + else if (themeHasGridView && viewPreference.compare("grid") == 0) selectedViewType = GRID; - if (viewPreference.compare("video") == 0) + else if (viewPreference.compare("video") == 0) selectedViewType = VIDEO; - if (selectedViewType == AUTOMATIC) + if (selectedViewType == AUTOMATIC || detailed) { - std::vector files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER); - for (auto it = files.cbegin(); it != files.cend(); it++) + if (themeHasGridView && system->getTheme()->getDefaultView() == "grid" && !detailed) + selectedViewType = GRID; + else if (themeHasGridExView && system->getTheme()->getDefaultView() == "gridex" && !detailed) + selectedViewType = GRIDEX; + else { - if (themeHasVideoView && !(*it)->getVideoPath().empty()) - { - selectedViewType = VIDEO; - break; - } - else if (!(*it)->getThumbnailPath().empty()) - { + selectedViewType = BASIC; + + if (system->getTheme()->getDefaultView() == "detailed") selectedViewType = DETAILED; - // Don't break out in case any subsequent files have video + else if (system->getTheme()->getDefaultView() != "basic") + { + std::vector files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER); + for (auto it = files.cbegin(); it != files.cend(); it++) + { + if (themeHasVideoView && !(*it)->getVideoPath().empty() && viewPreference.compare("detailed") != 0) + { + selectedViewType = VIDEO; + break; + } + else if (!(*it)->getThumbnailPath().empty()) + { + if (!detailed && (*it)->metadata.get("thumbnail").length() > 0) + { + if (themeHasGridExView && (*it)->metadata.get("image").length() > 0) + selectedViewType = GRIDEX; + else if (themeHasGridView) + selectedViewType = GRID; + else + selectedViewType = DETAILED; + } + else + selectedViewType = DETAILED; + + if (!themeHasVideoView) + break; + + // Don't break out in case any subsequent files have video + } + } } } } @@ -323,6 +364,13 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste case GRID: view = std::shared_ptr(new GridGameListView(mWindow, system->getRootFolder())); break; + case GRIDEX: + { + GridGameListView* listView = new GridGameListView(mWindow, system->getRootFolder()); + listView->setGridEx(); + view = std::shared_ptr(listView); + } + break; case BASIC: default: view = std::shared_ptr(new BasicGameListView(mWindow, system->getRootFolder())); @@ -356,7 +404,7 @@ std::shared_ptr ViewController::getSystemListView() bool ViewController::input(InputConfig* config, Input input) { - if(mLockInput) + if (mLockInput) return true; // open menu @@ -437,7 +485,7 @@ void ViewController::preload() { i++; char buffer[100]; - sprintf (buffer, "Loading '%s' (%d/%d)", + sprintf (buffer, "Chargement de '%s' (%d/%d)", (*it)->getFullName().c_str(), i, (int)SystemData::sSystemVector.size()); mWindow->renderLoadingScreen(std::string(buffer)); } @@ -454,20 +502,22 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) if(it->second.get() == view) { bool isCurrent = (mCurrentView == it->second); + SystemData* system = it->first; FileData* cursor = view->getCursor(); mGameListViews.erase(it); - if(reloadTheme) + if (reloadTheme) system->loadTheme(); + system->getIndex()->setUIModeFilters(); std::shared_ptr newView = getGameListView(system); // to counter having come from a placeholder - if (!cursor->isPlaceHolder()) { + if (!cursor->isPlaceHolder()) newView->setCursor(cursor); - } - if(isCurrent) + + if (isCurrent) mCurrentView = newView; break; @@ -483,19 +533,26 @@ void ViewController::reloadAll() { // clear all gamelistviews std::map cursorMap; + for(auto it = mGameListViews.cbegin(); it != mGameListViews.cend(); it++) - { cursorMap[it->first] = it->second->getCursor(); - } + mGameListViews.clear(); + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + { + if (cursorMap.find((*it)) == cursorMap.end()) + cursorMap[(*it)] = NULL; + } // load themes, create gamelistviews and reset filters for(auto it = cursorMap.cbegin(); it != cursorMap.cend(); it++) { it->first->loadTheme(); it->first->getIndex()->resetFilters(); - getGameListView(it->first)->setCursor(it->second); + + if (it->second != NULL) + getGameListView(it->first)->setCursor(it->second); } // Rebuild SystemListView @@ -509,11 +566,11 @@ void ViewController::reloadAll() }else if(mState.viewing == SYSTEM_SELECT) { SystemData* system = mState.getSystem(); - goToSystemView(SystemData::sSystemVector.front()); + goToSystemView(SystemData::sSystemVector.front(), false); mSystemListView->goToSystem(system, false); mCurrentView = mSystemListView; }else{ - goToSystemView(SystemData::sSystemVector.front()); + goToSystemView(SystemData::sSystemVector.front(), false); } updateHelpPrompts(); diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h index eec5e9fdb1..7aef661c1c 100644 --- a/es-app/src/views/ViewController.h +++ b/es-app/src/views/ViewController.h @@ -34,8 +34,8 @@ class ViewController : public GuiComponent void goToNextGameList(); void goToPrevGameList(); void goToGameList(SystemData* system); - void goToSystemView(SystemData* system); - void goToStart(); + void goToSystemView(SystemData* system, bool forceImmediate = false); + void goToStart(bool forceImmediate = false); void ReloadAndGoToStart(); void onFileChanged(FileData* file, FileChangeType change); @@ -62,7 +62,8 @@ class ViewController : public GuiComponent BASIC, DETAILED, GRID, - VIDEO + VIDEO, + GRIDEX }; struct State @@ -89,7 +90,7 @@ class ViewController : public GuiComponent ViewController(Window* window); static ViewController* sInstance; - void playViewTransition(); + void playViewTransition(bool forceImmediate); int getSystemId(SystemData* system); std::shared_ptr mCurrentView; diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 899f12578c..c697a09130 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -29,7 +29,7 @@ void BasicGameListView::onThemeChanged(const std::shared_ptr& theme) void BasicGameListView::onFileChanged(FileData* file, FileChangeType change) { - if(change == FILE_METADATA_CHANGED) + if (change == FILE_METADATA_CHANGED) { // might switch to a detailed view ViewController::get()->reloadGameListView(this); @@ -42,12 +42,40 @@ void BasicGameListView::onFileChanged(FileData* file, FileChangeType change) void BasicGameListView::populateList(const std::vector& files) { mList.clear(); - mHeaderText.setText(mRoot->getSystem()->getFullName()); + + std::string systemName = mRoot->getSystem()->getFullName(); + mHeaderText.setText(systemName); + + bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + if (files.size() > 0) { - for(auto it = files.cbegin(); it != files.cend(); it++) + for (auto it = files.cbegin(); it != files.cend(); it++) + { + if ((*it)->getFavorite()) + if (showHiddenFiles || !(*it)->getHidden()) + { + if (systemName == "favorites") + mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + else + + mList.add(_T("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + } + } + /* + for (auto it = files.cbegin(); it != files.cend(); it++) + { + if (!(*it)->getFavorite() && ((*it)->getType() == FOLDER)) + if (showHiddenFiles || !(*it)->getHidden()) + mList.add("[Folder] "+(*it)->getName(), *it, ((*it)->getType() == FOLDER)); + + }*/ + + for (auto it = files.cbegin(); it != files.cend(); it++) { - mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + if (!(*it)->getFavorite()) + if (showHiddenFiles || !(*it)->getHidden()) + mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); } } else @@ -146,17 +174,24 @@ std::vector BasicGameListView::getHelpPrompts() std::vector 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("left/right", _T("SYSTEM"))); + + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("LAUNCH"))); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + if(!UIModeController::getInstance()->isUIModeKid()) - prompts.push_back(HelpPrompt("select", "options")); + prompts.push_back(HelpPrompt("select", _T("options"))); + if(mRoot->getSystem()->isGameSystem()) - prompts.push_back(HelpPrompt("x", "random")); + prompts.push_back(HelpPrompt("x", _T("RANDOM"))); + if(mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) { std::string prompt = CollectionSystemManager::get()->getEditingCollection(); + if (prompt == "Favorites") + prompt = "FAVORIS"; + prompts.push_back(HelpPrompt("y", prompt)); } return prompts; diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 87d2893f56..bfb26740ce 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -32,29 +32,29 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : addChild(&mImage); // metadata labels + values - mLblRating.setText("Rating: "); + mLblRating.setText(_T("Rating") + ": "); addChild(&mLblRating); addChild(&mRating); - mLblReleaseDate.setText("Released: "); + mLblReleaseDate.setText(_T("Released") + ": "); addChild(&mLblReleaseDate); addChild(&mReleaseDate); - mLblDeveloper.setText("Developer: "); + mLblDeveloper.setText(_T("Developer") + ": "); addChild(&mLblDeveloper); addChild(&mDeveloper); - mLblPublisher.setText("Publisher: "); + mLblPublisher.setText(_T("Publisher")+": "); addChild(&mLblPublisher); addChild(&mPublisher); - mLblGenre.setText("Genre: "); + mLblGenre.setText(_T("Genre") + ": "); addChild(&mLblGenre); addChild(&mGenre); - mLblPlayers.setText("Players: "); + mLblPlayers.setText(_T("Players") + ": "); addChild(&mLblPlayers); addChild(&mPlayers); - mLblLastPlayed.setText("Last played: "); + mLblLastPlayed.setText(_T("Last played") + ": "); addChild(&mLblLastPlayed); mLastPlayed.setDisplayRelative(true); addChild(&mLastPlayed); - mLblPlayCount.setText("Times played: "); + mLblPlayCount.setText(_T("Times played")); addChild(&mLblPlayCount); addChild(&mPlayCount); @@ -188,6 +188,15 @@ void DetailedGameListView::initMDValues() mDescContainer.setSize(mDescContainer.getSize().x(), mSize.y() - mDescContainer.getPosition().y()); } +std::string DetailedGameListView::getMetadata(FileData* file, std::string name) +{ + std::string ret = file->metadata.get(name); + if (ret == "unknown") + return _L(ret); + + return ret; +} + void DetailedGameListView::updateInfoPanel() { FileData* file = (mList.size() == 0 || mList.isScrolling()) ? NULL : mList.getSelected(); @@ -199,22 +208,27 @@ void DetailedGameListView::updateInfoPanel() //mDescription.setText(""); fadingOut = true; }else{ - mImage.setImage(file->getImagePath()); - mDescription.setText(file->metadata.get("desc")); + + if (file->getImagePath().empty()) + mImage.setImage(file->getThumbnailPath()); + else + mImage.setImage(file->getImagePath()); + + mDescription.setText(getMetadata(file, "desc")); mDescContainer.reset(); - 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")); - mName.setValue(file->metadata.get("name")); + mRating.setValue(getMetadata(file, "rating")); + mReleaseDate.setValue(getMetadata(file, "releasedate")); + mDeveloper.setValue(getMetadata(file, "developer")); + mPublisher.setValue(getMetadata(file, "publisher")); + mGenre.setValue(getMetadata(file, "genre")); + mPlayers.setValue(getMetadata(file, "players")); + mName.setValue(getMetadata(file, "name")); if(file->getType() == GAME) { - mLastPlayed.setValue(file->metadata.get("lastplayed")); - mPlayCount.setValue(file->metadata.get("playcount")); + mLastPlayed.setValue(getMetadata(file, "lastplayed")); + mPlayCount.setValue(getMetadata(file, "playcount")); } fadingOut = false; diff --git a/es-app/src/views/gamelist/DetailedGameListView.h b/es-app/src/views/gamelist/DetailedGameListView.h index 76c8a93537..3c070c955f 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.h +++ b/es-app/src/views/gamelist/DetailedGameListView.h @@ -23,6 +23,7 @@ class DetailedGameListView : public BasicGameListView void initMDLabels(); void initMDValues(); + std::string getMetadata(FileData* file, std::string name); ImageComponent mImage; diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 91f9c08b0a..a2dbc27428 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -11,6 +11,7 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : ISimpleGameListView(window, root), mGrid(window), mDescContainer(window), mDescription(window), + mImage(window), mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -21,6 +22,8 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : { const float padding = 0.01f; + mGridEx = false; + mGrid.setPosition(mSize.x() * 0.1f, mSize.y() * 0.1f); mGrid.setDefaultZIndex(20); mGrid.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); @@ -28,6 +31,13 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : populateList(root->getChildrenListToDisplay()); + // 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); + mImage.setDefaultZIndex(30); + addChild(&mImage); + // metadata labels + values mLblRating.setText("Rating: "); addChild(&mLblRating); @@ -78,6 +88,12 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : updateInfoPanel(); } +void GridGameListView::setGridEx() +{ + mGridEx = true; + mGrid.setThemeName(getName()); +} + FileData* GridGameListView::getCursor() { return mGrid.getSelected(); @@ -116,10 +132,21 @@ void GridGameListView::populateList(const std::vector& files) mHeaderText.setText(mRoot->getSystem()->getFullName()); if (files.size() > 0) { + std::string systemName = mRoot->getSystem()->getFullName(); + bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + for (auto it = files.cbegin(); it != files.cend(); it++) - { - mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it); - } + if ((*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) + { + if (systemName == "favorites") + mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it); + else + mGrid.add(_T("\uF006 ") + (*it)->getName(), (*it)->getThumbnailPath(), *it); + } + + for (auto it = files.cbegin(); it != files.cend(); it++) + if (!(*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) + mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it); } else { @@ -136,6 +163,14 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) mGrid.applyTheme(theme, getName(), "gamegrid", ALL); mName.applyTheme(theme, getName(), "md_name", ALL); + if (theme->getElement(getName(), "md_image", "image")) + { + mImageVisible = true; + mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + } + else + mImageVisible = false; + initMDLabels(); std::vector labels = getMDLabels(); assert(labels.size() == 8); @@ -168,6 +203,7 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION)); sortChildren(); + updateInfoPanel(); } void GridGameListView::initMDLabels() @@ -240,11 +276,23 @@ void GridGameListView::updateInfoPanel() FileData* file = (mGrid.size() == 0 || mGrid.isScrolling()) ? NULL : mGrid.getSelected(); bool fadingOut; - if(file == NULL) + if (file == NULL) { //mDescription.setText(""); fadingOut = true; - }else{ + } + else + { + if (mImageVisible) + { + if (file->getImagePath().empty()) + mImage.setImage(file->getThumbnailPath()); + else + mImage.setImage(file->getImagePath()); + } + else + mImage.setImage(""); + mDescription.setText(file->metadata.get("desc")); mDescContainer.reset(); @@ -266,11 +314,12 @@ void GridGameListView::updateInfoPanel() } std::vector comps = getMDValues(); + comps.push_back(&mImage); comps.push_back(&mDescription); comps.push_back(&mName); std::vector labels = getMDLabels(); comps.insert(comps.cend(), labels.cbegin(), labels.cend()); - + for(auto it = comps.cbegin(); it != comps.cend(); it++) { GuiComponent* comp = *it; @@ -364,14 +413,15 @@ std::vector GridGameListView::getHelpPrompts() std::vector prompts; if(Settings::getInstance()->getBool("QuickSystemSelect")) - prompts.push_back(HelpPrompt("lr", "system")); - prompts.push_back(HelpPrompt("up/down/left/right", "choose")); - prompts.push_back(HelpPrompt("a", "launch")); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("lr", _T("SYSTEM"))); + prompts.push_back(HelpPrompt("up/down/left/right", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("LAUNCH"))); + prompts.push_back(HelpPrompt("b", _T("BACK"))); if(!UIModeController::getInstance()->isUIModeKid()) - prompts.push_back(HelpPrompt("select", "options")); + prompts.push_back(HelpPrompt("select", _T("OPTIONS"))); if(mRoot->getSystem()->isGameSystem()) - prompts.push_back(HelpPrompt("x", "random")); + prompts.push_back(HelpPrompt("x", _T("RANDOM"))); + if(mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) { std::string prompt = CollectionSystemManager::get()->getEditingCollection(); diff --git a/es-app/src/views/gamelist/GridGameListView.h b/es-app/src/views/gamelist/GridGameListView.h index da09c87903..50cfc24ab7 100644 --- a/es-app/src/views/gamelist/GridGameListView.h +++ b/es-app/src/views/gamelist/GridGameListView.h @@ -7,6 +7,7 @@ #include "components/ScrollableContainer.h" #include "components/ImageGridComponent.h" #include "views/gamelist/ISimpleGameListView.h" +#include "views/gamelist/BasicGameListView.h" class GridGameListView : public ISimpleGameListView { @@ -20,11 +21,19 @@ class GridGameListView : public ISimpleGameListView virtual bool input(InputConfig* config, Input input) override; - virtual const char* getName() const override { return "grid"; } + virtual const char* getName() const override + { + if (mGridEx) + return "gridex"; + + return "grid"; + } virtual std::vector getHelpPrompts() override; virtual void launch(FileData* game) override; + void setGridEx(); + protected: virtual std::string getQuickSystemSelectRightButton() override; virtual std::string getQuickSystemSelectLeftButton() override; @@ -51,6 +60,10 @@ class GridGameListView : public ISimpleGameListView DateTimeComponent mLastPlayed; TextComponent mPlayCount; TextComponent mName; + ImageComponent mImage; + + bool mImageVisible; + bool mGridEx; std::vector getMDLabels(); std::vector getMDValues(); diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 5a1a68ed35..90b1c032ef 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -80,14 +80,24 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) { if(input.value != 0) { - if(config->isMappedTo("a", input)) + if (config->isMappedTo("a", input)) { FileData* cursor = getCursor(); - if(cursor->getType() == GAME) + + if (cursor->getType() == FOLDER) + { + FileData* gameOfFolder = cursor->findUniqueGameForFolder(); + if (gameOfFolder != NULL) + cursor = gameOfFolder; + } + + if (cursor->getType() == GAME) { Sound::getFromTheme(getTheme(), getName(), "launch")->play(); launch(cursor); - }else{ + } + else + { // it's a folder if(cursor->getChildren().size() > 0) { @@ -99,7 +109,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } return true; - }else if(config->isMappedTo("b", input)) + } + else if(config->isMappedTo("b", input)) { if(mCursorStack.size()) { @@ -118,7 +129,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } return true; - }else if(config->isMappedLike(getQuickSystemSelectRightButton(), input)) + } + else if(config->isMappedLike(getQuickSystemSelectRightButton(), input)) { if(Settings::getInstance()->getBool("QuickSystemSelect")) { @@ -126,7 +138,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) ViewController::get()->goToNextGameList(); return true; } - }else if(config->isMappedLike(getQuickSystemSelectLeftButton(), input)) + } + else if(config->isMappedLike(getQuickSystemSelectLeftButton(), input)) { if(Settings::getInstance()->getBool("QuickSystemSelect")) { @@ -134,7 +147,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) ViewController::get()->goToPrevGameList(); return true; } - }else if (config->isMappedTo("x", input)) + } + else if (config->isMappedTo("x", input)) { if (mRoot->getSystem()->isGameSystem()) { @@ -146,7 +160,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } return true; } - }else if (config->isMappedTo("y", input) && UIModeController::getInstance()->isUIModeFull()) + } + else if (config->isMappedTo("y", input) && UIModeController::getInstance()->isUIModeFull()) { if(mRoot->getSystem()->isGameSystem()) { diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index 9b7d22aa22..7cd24b6f15 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -8,6 +8,103 @@ #include "Window.h" #include + +#if defined(_WIN32) +#define _conv(x) GuiTextTool::convertFromWideString(L ## x) +#else +#define _conv(x) x +#endif + +#include +#include +#include "resources\ResourceManager.h" + +std::vector GuiTextTool::mItems; +std::string GuiTextTool::mCurrentLanguage = "en"; +bool GuiTextTool::mCurrentLanguageLoaded = false; + +void GuiTextTool::setLanguage(std::string lang) +{ + mCurrentLanguage = lang; + mCurrentLanguageLoaded = false; +} + +void GuiTextTool::ensureLocalisation() +{ + if (mCurrentLanguageLoaded) + { + if (Settings::getInstance()->getString("Language") == mCurrentLanguage) + return; + + mCurrentLanguage = Settings::getInstance()->getString("Language"); + } + + mCurrentLanguageLoaded = true; + + for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) + delete (*it); + + mItems.clear(); + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/"+ mCurrentLanguage +"/emulationstation2.po"); + if (Utils::FileSystem::exists(xmlpath)) + { + LocalizationItem* currentItem = NULL; + + std::ifstream file(xmlpath); + std::string str; + while (std::getline(file, str)) + { + if (str.length() > 0 && str[0] == '#') + { + if (currentItem != NULL && currentItem->msgid.length() > 0 && currentItem->msgstr.length() > 0) + mItems.push_back(currentItem); + + currentItem = new LocalizationItem(); + } + + if (currentItem != NULL && str.find("msgid") == 0) + { + auto start = str.find("\""); + if (start != std::string::npos) + { + auto end = str.find("\"", start + 1); + if (end != std::string::npos) + currentItem->msgid = str.substr(start + 1, end - start - 1); + } + } + + if (currentItem != NULL && str.find("msgstr") == 0) + { + auto start = str.find("\""); + if (start != std::string::npos) + { + auto end = str.find("\"", start + 1); + if (end != std::string::npos) + currentItem->msgstr = str.substr(start + 1, end - start - 1); + } + } + } + + if (currentItem != NULL) + delete currentItem; + } +} + + +const std::string GuiTextTool::localize(const std::string text) +{ + ensureLocalisation(); + + for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) + { + if (text == (*it)->msgid) + return (*it)->msgstr; + } + + return text; +} + GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255), mPosition(Vector3f::Zero()), mOrigin(Vector2f::Zero()), mRotationOrigin(0.5, 0.5), mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false) @@ -281,6 +378,16 @@ std::string GuiComponent::getValue() const return ""; } +void GuiComponent::setTag(const std::string& value) +{ + mTag = value; +} + +std::string GuiComponent::getTag() const +{ + return mTag; +} + void GuiComponent::textInput(const char* text) { for(auto iter = mChildren.cbegin(); iter != mChildren.cend(); iter++) diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 4c15ba578b..4712ce32f5 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -10,6 +10,10 @@ #include #include +#if defined(_WIN32) +#include +#endif + class Animation; class AnimationController; class Font; @@ -109,6 +113,10 @@ class GuiComponent virtual std::string getValue() const; virtual void setValue(const std::string& value); + + virtual std::string getTag() const; + virtual void setTag(const std::string& value); + virtual void onFocusGained() {}; virtual void onFocusLost() {}; @@ -145,6 +153,8 @@ class GuiComponent GuiComponent* mParent; std::vector mChildren; + std::string mTag; + Vector3f mPosition; Vector2f mOrigin; Vector2f mRotationOrigin; @@ -166,4 +176,47 @@ class GuiComponent AnimationController* mAnimationMap[MAX_ANIMATIONS]; }; + + +class LocalizationItem +{ +public: + std::string msgid; + std::string msgstr; +}; + +class GuiTextTool +{ +public: +#if defined(_WIN32) + static const std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + + std::string string; + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return string; + } +#endif + static void ensureLocalisation(); + static const std::string localize(const std::string text); + + static void setLanguage(std::string lang); + +private: + static std::vector mItems; + static std::string mCurrentLanguage; + static bool mCurrentLanguageLoaded; +}; + +#if defined(_WIN32) +#define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) +#define _L(x) GuiTextTool::localize(x) +#else +#define _T(x) GuiTextTool::localize(x) +#define _L(x) GuiTextTool::localize(x) +#endif // _WIN32 + #endif // ES_CORE_GUI_COMPONENT_H diff --git a/es-core/src/InputManager.cpp b/es-core/src/InputManager.cpp index d074367ebf..59fc54bdac 100644 --- a/es-core/src/InputManager.cpp +++ b/es-core/src/InputManager.cpp @@ -426,7 +426,7 @@ void InputManager::doOnFinish() LOG(LogInfo) << " " << tocall; std::cout << "==============================================\ninput config finish command:\n"; - int exitCode = runSystemCommand(tocall); + int exitCode = runSystemCommand(tocall, "", NULL); std::cout << "==============================================\n"; if(exitCode != 0) diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index f87cddd0cc..fb33b2cea4 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -37,7 +37,7 @@ namespace Renderer { LOG(LogInfo) << "Creating surface..."; - if(SDL_Init(SDL_INIT_VIDEO) != 0) + if (SDL_Init(SDL_INIT_VIDEO) != 0) { LOG(LogError) << "Error initializing SDL!\n " << SDL_GetError(); return false; @@ -62,25 +62,27 @@ namespace Renderer SDL_DisplayMode dispMode; SDL_GetDesktopDisplayMode(0, &dispMode); - windowWidth = Settings::getInstance()->getInt("WindowWidth") ? Settings::getInstance()->getInt("WindowWidth") : dispMode.w; - windowHeight = Settings::getInstance()->getInt("WindowHeight") ? Settings::getInstance()->getInt("WindowHeight") : dispMode.h; - screenWidth = Settings::getInstance()->getInt("ScreenWidth") ? Settings::getInstance()->getInt("ScreenWidth") : windowWidth; - screenHeight = Settings::getInstance()->getInt("ScreenHeight") ? Settings::getInstance()->getInt("ScreenHeight") : windowHeight; + + windowWidth = Settings::getInstance()->getInt("WindowWidth") ? Settings::getInstance()->getInt("WindowWidth") : dispMode.w; + windowHeight = Settings::getInstance()->getInt("WindowHeight") ? Settings::getInstance()->getInt("WindowHeight") : dispMode.h; + screenWidth = Settings::getInstance()->getInt("ScreenWidth") ? Settings::getInstance()->getInt("ScreenWidth") : windowWidth; + screenHeight = Settings::getInstance()->getInt("ScreenHeight") ? Settings::getInstance()->getInt("ScreenHeight") : windowHeight; screenOffsetX = Settings::getInstance()->getInt("ScreenOffsetX") ? Settings::getInstance()->getInt("ScreenOffsetX") : 0; screenOffsetY = Settings::getInstance()->getInt("ScreenOffsetY") ? Settings::getInstance()->getInt("ScreenOffsetY") : 0; - screenRotate = Settings::getInstance()->getInt("ScreenRotate") ? Settings::getInstance()->getInt("ScreenRotate") : 0; - - sdlWindow = SDL_CreateWindow("EmulationStation", - SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, - windowWidth, windowHeight, + screenRotate = Settings::getInstance()->getInt("ScreenRotate") ? Settings::getInstance()->getInt("ScreenRotate") : 0; + + sdlWindow = SDL_CreateWindow("EmulationStation", + SDL_WINDOWPOS_UNDEFINED, + SDL_WINDOWPOS_UNDEFINED, + windowWidth, windowHeight, SDL_WINDOW_OPENGL | (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN))); - - if(sdlWindow == NULL) + + if (sdlWindow == NULL) { LOG(LogError) << "Error creating SDL window!\n\t" << SDL_GetError(); return false; } - + LOG(LogInfo) << "Created window successfully."; //support screen rotation @@ -125,7 +127,7 @@ namespace Renderer sdlContext = SDL_GL_CreateContext(sdlWindow); // vsync - if(Settings::getInstance()->getBool("VSync")) + // if(Settings::getInstance()->getBool("VSync")) { // SDL_GL_SetSwapInterval(0) for immediate updates (no vsync, default), // 1 for updates synchronized with the vertical retrace, @@ -133,11 +135,11 @@ namespace Renderer // SDL_GL_SetSwapInterval returns 0 on success, -1 on error. // if vsync is requested, try normal vsync; if that doesn't work, try late swap tearing // if that doesn't work, report an error - if(SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) + if (SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; } - else - SDL_GL_SetSwapInterval(0); + // else + // SDL_GL_SetSwapInterval(0); return true; } @@ -215,7 +217,7 @@ namespace Renderer } void swapBuffers() - { + { SDL_GL_SwapWindow(sdlWindow); glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } diff --git a/es-core/src/Scripting.cpp b/es-core/src/Scripting.cpp index 3038db0a40..421d4fdb7e 100644 --- a/es-core/src/Scripting.cpp +++ b/es-core/src/Scripting.cpp @@ -28,7 +28,7 @@ namespace Scripting // append folder to path std::string script = *it + " \"" + arg1 + "\" \"" + arg2 + "\""; LOG(LogDebug) << " executing: " << script; - runSystemCommand(script); + runSystemCommand(script, "", NULL); } } } diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 13c09a84a1..a63c75e9d3 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -61,7 +61,7 @@ void Settings::setDefaults() mBoolMap["ShowHiddenFiles"] = false; mBoolMap["DrawFramerate"] = false; mBoolMap["ShowExit"] = true; - mBoolMap["FullscreenBorderless"] = false; + mBoolMap["FullscreenBorderless"] = true; mBoolMap["Windowed"] = false; mBoolMap["SplashScreen"] = true; mBoolMap["SplashScreenProgress"] = true; @@ -91,8 +91,11 @@ void Settings::setDefaults() #else mIntMap["MaxVRAM"] = 100; #endif - - mStringMap["TransitionStyle"] = "fade"; + + mBoolMap["HideWindow"] = true; + mStringMap["GameTransitionStyle"] = "fade"; + mStringMap["TransitionStyle"] = "slide"; + mStringMap["Language"] = "en"; mStringMap["ThemeSet"] = ""; mStringMap["ScreenSaverBehavior"] = "dim"; mStringMap["Scraper"] = "TheGamesDB"; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index cf2548089f..52f75760cc 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -9,7 +9,7 @@ #include #include -std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" }, { "grid" }, { "video" } }; +std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" } }; std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; std::map> ThemeData::sElementMap { @@ -40,7 +40,8 @@ std::map> The { "backgroundCornerSize", NORMALIZED_PAIR }, { "backgroundColor", COLOR }, { "backgroundCenterColor", COLOR }, - { "backgroundEdgeColor", COLOR } } }, + { "backgroundEdgeColor", COLOR }, + { "imageSizeMode", STRING } } }, { "text", { { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, @@ -202,7 +203,7 @@ ThemeData::ThemeData() mVersion = 0; } -void ThemeData::loadFile(std::map sysDataMap, const std::string& path) +void ThemeData::loadFile(std::string system, const std::string& path) { mPaths.push_back(path); @@ -214,9 +215,12 @@ void ThemeData::loadFile(std::map sysDataMap, const st mVersion = 0; mViews.clear(); - mVariables.clear(); - mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); + mSystemThemeFolder = system; + + mVariables.clear(); + //mVariables.insert(system, system); + //mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); pugi::xml_document doc; pugi::xml_parse_result res = doc.load_file(path.c_str()); @@ -232,6 +236,9 @@ void ThemeData::loadFile(std::map sysDataMap, const st if(mVersion == -404) throw error << " tag missing!\n It's either out of date or you need to add " << CURRENT_THEME_FORMAT_VERSION << " inside your tag."; + if (root.attribute("defaultView")) + mDefaultView = root.attribute("defaultView").as_string(); + if(mVersion < MINIMUM_THEME_FORMAT_VERSION) throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; @@ -241,6 +248,22 @@ void ThemeData::loadFile(std::map sysDataMap, const st parseFeatures(root); } +std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) +{ + std::string result = path; + + size_t start_pos = result.find("$system"); + if (start_pos == std::string::npos) + return path; + + result.replace(start_pos, 7, systemThemeFolder); + + //result.replace("$system", systemThemeFolder); + //boost::algorithm::replace_first(result, "$system", systemThemeFolder); + return result; +} + + void ThemeData::parseIncludes(const pugi::xml_node& root) { ThemeException error; @@ -250,8 +273,11 @@ void ThemeData::parseIncludes(const pugi::xml_node& root) { std::string relPath = resolvePlaceholders(node.text().as_string()); std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); - if(!ResourceManager::getInstance()->fileExists(path)) - throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + path = resolveSystemVariable(mSystemThemeFolder, path); + + if (!ResourceManager::getInstance()->fileExists(path)) + continue; + //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; error << " from included file \"" << relPath << "\":\n "; @@ -340,6 +366,12 @@ void ThemeData::parseViews(const pugi::xml_node& root) { ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; parseView(node, view); + + if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) + { + ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; + parseView(node, view); + } } } } @@ -390,10 +422,11 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapsecond) { @@ -492,8 +525,8 @@ const std::shared_ptr& ThemeData::getDefault() { try { - std::map emptyMap; - theme->loadFile(emptyMap, path); + //std::map emptyMap; + theme->loadFile("", path); // emptyMap } catch(ThemeException& e) { LOG(LogError) << e.what(); diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 4fc9faa747..6bbcc01555 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -135,7 +135,7 @@ class ThemeData ThemeData(); // throws ThemeException - void loadFile(std::map sysDataMap, const std::string& path); + void loadFile(std::string system, const std::string& path); enum ElementPropertyType { @@ -159,6 +159,9 @@ class ThemeData static std::map getThemeSets(); static std::string getThemeFromCurrentSet(const std::string& system); + std::string getDefaultView() { return mDefaultView; }; + + private: static std::map< std::string, std::map > sElementMap; static std::vector sSupportedFeatures; @@ -167,6 +170,8 @@ class ThemeData std::deque mPaths; float mVersion; + std::string mDefaultView; + void parseFeatures(const pugi::xml_node& themeRoot); void parseIncludes(const pugi::xml_node& themeRoot); void parseVariables(const pugi::xml_node& root); @@ -174,7 +179,10 @@ class ThemeData void parseView(const pugi::xml_node& viewNode, ThemeView& view); void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element); + std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); + std::map mViews; + std::string mSystemThemeFolder; }; #endif // ES_CORE_THEME_DATA_H diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 0f33e5e240..05337b0e01 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -76,7 +76,6 @@ bool Window::init() } InputManager::getInstance()->init(); - ResourceManager::getInstance()->reloadAll(); //keep a reference to the default fonts, so they don't keep getting destroyed/recreated @@ -91,7 +90,7 @@ bool Window::init() mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); // update our help because font sizes probably changed - if(peekGui()) + if (peekGui()) peekGui()->updateHelpPrompts(); return true; @@ -299,15 +298,15 @@ void Window::setAllowSleep(bool sleep) } void Window::renderLoadingScreen(std::string text) -{ +{ Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); - + ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.6f, 0.0f); + splash.setResize(Renderer::getScreenWidth() * 0.5f, 0.0f); splash.setImage(":/splash.svg"); - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.6f); + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); splash.render(trans); auto& font = mDefaultFonts.at(1); @@ -323,6 +322,36 @@ void Window::renderLoadingScreen(std::string text) Renderer::swapBuffers(); } +void Window::renderBlackScreen(std::string text) +{ + Transform4x4f trans = Transform4x4f::Identity(); + Renderer::setMatrix(trans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); + /* + ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.5f, 0.0f); + splash.setImage(":/splash.svg"); + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.render(trans); + */ + + if (text.length() > 0) + { + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); + + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.835f); + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; + } + + Renderer::swapBuffers(); +} + + void Window::renderHelpPromptsEarly() { mHelp->render(Transform4x4f::Identity()); diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 3f104c648e..5fd238dc62 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -65,6 +65,7 @@ class Window void setAllowSleep(bool sleep); void renderLoadingScreen(std::string text); + void renderBlackScreen(std::string text); void renderHelpPromptsEarly(); // used to render HelpPrompts before a fade void setHelpPrompts(const std::vector& prompts, const HelpStyle& style); diff --git a/es-core/src/components/ComponentGrid.cpp b/es-core/src/components/ComponentGrid.cpp index 066f9a3f0c..e89e7f9c4e 100644 --- a/es-core/src/components/ComponentGrid.cpp +++ b/es-core/src/components/ComponentGrid.cpp @@ -445,11 +445,11 @@ std::vector ComponentGrid::getHelpPrompts() } if(canScrollHoriz && canScrollVert) - prompts.push_back(HelpPrompt("up/down/left/right", "choose")); + prompts.push_back(HelpPrompt("up/down/left/right", _T("CHOOSE"))); else if(canScrollHoriz) - prompts.push_back(HelpPrompt("left/right", "choose")); + prompts.push_back(HelpPrompt("left/right", _T("CHOOSE"))); else if(canScrollVert) - prompts.push_back(HelpPrompt("up/down", "choose")); + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); return prompts; } diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index c2c42d8f39..f8f1bcd518 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -328,7 +328,7 @@ std::vector ComponentList::getHelpPrompts() } if(addMovePrompt) - prompts.push_back(HelpPrompt("up/down", "choose")); + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); } return prompts; diff --git a/es-core/src/components/DateTimeComponent.cpp b/es-core/src/components/DateTimeComponent.cpp index 0052f11ad3..5fcfd2ba73 100644 --- a/es-core/src/components/DateTimeComponent.cpp +++ b/es-core/src/components/DateTimeComponent.cpp @@ -51,7 +51,7 @@ std::string DateTimeComponent::getDisplayString() const if (mDisplayRelative) { //relative time if(mTime.getTime() == 0) - return "never"; + return _T("never"); Utils::Time::DateTime now(Utils::Time::now()); Utils::Time::Duration dur(now.getTime() - mTime.getTime()); @@ -71,7 +71,7 @@ std::string DateTimeComponent::getDisplayString() const } if(mTime.getTime() == 0) - return "unknown"; + return _T("unknown"); return Utils::Time::timeToString(mTime.getTime(), mFormat); } diff --git a/es-core/src/components/DateTimeEditComponent.cpp b/es-core/src/components/DateTimeEditComponent.cpp index d4b6636a39..6676641af5 100644 --- a/es-core/src/components/DateTimeEditComponent.cpp +++ b/es-core/src/components/DateTimeEditComponent.cpp @@ -207,7 +207,7 @@ std::string DateTimeEditComponent::getDisplayString(DisplayMode mode) const { //relative time if(mTime.getTime() == 0) - return "never"; + return _T("never"); Utils::Time::DateTime now(Utils::Time::now()); Utils::Time::Duration dur(now.getTime() - mTime.getTime()); @@ -229,7 +229,7 @@ std::string DateTimeEditComponent::getDisplayString(DisplayMode mode) const } if(mTime.getTime() == 0) - return "unknown"; + return _T("unknown"); return Utils::Time::timeToString(mTime, fmt); } diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 745fede1a3..310830f4fb 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -4,11 +4,18 @@ #include "ThemeData.h" #include "Renderer.h" -GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window) +#include + +#include "animations/LambdaAnimation.h" + +GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window), mLabel(window) { + mSelectedZoomPercent = 1.0f; + mAnimPosition = Vector3f(0, 0); + mDefaultProperties.mSize = getDefaultTileSize(); mDefaultProperties.mPadding = Vector2f(16.0f, 16.0f); - mDefaultProperties.mImageColor = 0xAAAAAABB; + mDefaultProperties.mImageColor = 0xFFFFFFDD; // 0xAAAAAABB; mDefaultProperties.mBackgroundImage = ":/frame.png"; mDefaultProperties.mBackgroundCornerSize = Vector2f(16 ,16); mDefaultProperties.mBackgroundCenterColor = 0xAAAAEEFF; @@ -22,41 +29,126 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mSelectedProperties.mBackgroundCenterColor = 0xFFFFFFFF; mSelectedProperties.mBackgroundEdgeColor = 0xFFFFFFFF; + mLabelSize = Vector2f(1.0, 0.30); + mImage = std::make_shared(mWindow); + mImage->setOrigin(0.5f, 0.5f); + + //mBackground.setOrigin(0.5f, 0.5f); - mBackground.setOrigin(0.5f, 0.5f); + //mLabel.setOrigin(0.5f, 0.5f); + //mLabel.setOrigin(0.5f, 0.0f); + //mLabel.setSize(1.0f, 1.0f); + addChild(&mBackground); addChild(&(*mImage)); + addChild(&mLabel); setSelected(false); setVisible(true); } +void GridTileComponent::resize() +{ + const GridTileProperties& currentProperties = getCurrentProperties(); + + setSize(currentProperties.mSize); + + float height = currentProperties.mSize.y() * mLabelSize.y(); + if (!mLabelVisible) + height = 0; + + mLabel.setPosition(0, mSize.y() - height); + mLabel.setSize(currentProperties.mSize.x(), height); + + if (mLabelSize.x() == 0) + height = 0; + + float topPadding = currentProperties.mPadding.y(); + float bottomPadding = std::max(topPadding, height); + float paddingX = currentProperties.mPadding.x(); + + if (mSelectedZoomPercent != 1.0f) + { + paddingX = mDefaultProperties.mPadding.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.x() * mSelectedZoomPercent; + topPadding = mDefaultProperties.mPadding.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.y() * mSelectedZoomPercent; + + if (mAnimPosition.x() != 0 && mAnimPosition.y() != 0) + { + int x = mAnimPosition.x() - mPosition.x(); + int y = mAnimPosition.y() - mPosition.y(); + + x = x * (1.0 - mSelectedZoomPercent); + y = y * (1.0 - mSelectedZoomPercent); + + mBackground.setPosition(x, y); + } + else + mBackground.setPosition(0, 0); + } + else + mBackground.setPosition(0, 0); + + if (mImage != NULL) + { + mImage->setOrigin(0.5f, 0.5f); + mImage->setPosition(currentProperties.mSize.x() / 2.0f, (currentProperties.mSize.y() - height) / 2.0f); + + if (currentProperties.mImageSizeMode == "minSize") + mImage->setMinSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + else if (currentProperties.mImageSizeMode == "size") + mImage->setSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + else + mImage->setMaxSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + } + + + + + mBackground.setSize(currentProperties.mSize); + mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); +} + void GridTileComponent::render(const Transform4x4f& parentTrans) { + if (!mVisible) + return; + Transform4x4f trans = getTransform() * parentTrans; - if (mVisible) - renderChildren(trans); + //Renderer::setMatrix(trans); + //Renderer::drawRect(0.f, 0.f, mSize.x(), mSize.y(), 0xFF0000FF); + + mBackground.render(trans); + + if (mImage != NULL) + mImage->render(trans); + + if (mLabelVisible) + mLabel.render(trans); } // Update all the tile properties to the new status (selected or default) -void GridTileComponent::update() +void GridTileComponent::update(int deltaTime) { + GuiComponent::update(deltaTime); + const GridTileProperties& currentProperties = getCurrentProperties(); mBackground.setImagePath(currentProperties.mBackgroundImage); - mImage->setColorShift(currentProperties.mImageColor); + if (mImage != NULL) + mImage->setColorShift(currentProperties.mImageColor); + mBackground.setCenterColor(currentProperties.mBackgroundCenterColor); mBackground.setEdgeColor(currentProperties.mBackgroundEdgeColor); resize(); } -void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& /*element*/, unsigned int /*properties*/) +void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); @@ -90,6 +182,9 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem->has("backgroundEdgeColor")) mDefaultProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); + + if (elem && elem->has("imageSizeMode")) + mDefaultProperties.mImageSizeMode = elem->get("imageSizeMode"); } // Apply theme to the selected gridtile @@ -99,7 +194,8 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mSelectedProperties.mSize = elem && elem->has("size") ? elem->get("size") * screen : - getSelectedTileSize(); + mDefaultProperties.mSize; + //getSelectedTileSize(); mSelectedProperties.mPadding = elem && elem->has("padding") ? elem->get("padding") : @@ -127,6 +223,22 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem && elem->has("backgroundEdgeColor")) mSelectedProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); + + mSelectedProperties.mImageSizeMode = elem && elem->has("imageSizeMode") ? + elem->get("imageSizeMode") : + mDefaultProperties.mImageSizeMode; + + elem = theme->getElement(view, element, "text"); + if (elem != NULL) + { + if (elem && elem->has("size")) + mLabelSize = elem->get("size"); + + mLabelVisible = true; + mLabel.applyTheme(theme, view, element, properties); + } + else + mLabelVisible = false; } // Made this a static function because the ImageGridComponent need to know the default tile size @@ -148,39 +260,67 @@ bool GridTileComponent::isSelected() const return mSelected; } -void GridTileComponent::setImage(const std::string& path) +void GridTileComponent::setImage(const std::string& path, std::string name) { - mImage->setImage(path); - - // Resize now to prevent flickering images when scrolling + if (mCurrentPath != path) + { + mCurrentPath = path; + mImage->setImage(path); + } + + mLabel.setText(name); resize(); } -void GridTileComponent::setImage(const std::shared_ptr& texture) +void GridTileComponent::setImage(const std::shared_ptr& texture, std::string name) { mImage->setImage(texture); - + mLabel.setText(name); + // Resize now to prevent flickering images when scrolling resize(); } -void GridTileComponent::setSelected(bool selected) +void GridTileComponent::setSelected(bool selected, Vector3f* pPosition) { mSelected = selected; + + if (selected && pPosition != NULL) + { + auto funeec = [this](float t) + { + this->setSelectedZoom(t); + }; + } + + if (selected && (pPosition != NULL || mSelectedProperties.mPadding.x() != mDefaultProperties.mPadding.y())) + { + if (pPosition == NULL) + mAnimPosition = Vector3f(0, 0, 0); + else + mAnimPosition = Vector3f(pPosition->x(), pPosition->y(), pPosition->z()); + + auto func = [this](float t) + { + this->setSelectedZoom(t); + }; + + setAnimation(new LambdaAnimation(func, 100), 0, [this] { + this->setSelectedZoom(1); + mAnimPosition = Vector3f(0, 0, 0); + }, false, 3); + } } -void GridTileComponent::setVisible(bool visible) +void GridTileComponent::setSelectedZoom(float percent) { - mVisible = visible; + mSelectedZoomPercent = percent; + resize(); } -void GridTileComponent::resize() +void GridTileComponent::setVisible(bool visible) { - const GridTileProperties& currentProperties = getCurrentProperties(); - - mImage->setMaxSize(currentProperties.mSize - currentProperties.mPadding * 2); - mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); - mBackground.fitTo(currentProperties.mSize - mBackground.getCornerSize() * 2); + mVisible = visible; } const GridTileProperties& GridTileComponent::getCurrentProperties() const diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index c191ce94ab..bdaa611a16 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -4,6 +4,7 @@ #include "NinePatchComponent.h" #include "ImageComponent.h" +#include "TextComponent.h" struct GridTileProperties { @@ -14,6 +15,8 @@ struct GridTileProperties Vector2f mBackgroundCornerSize; unsigned int mBackgroundCenterColor; unsigned int mBackgroundEdgeColor; + + std::string mImageSizeMode; }; class GridTileComponent : public GuiComponent @@ -22,7 +25,8 @@ class GridTileComponent : public GuiComponent GridTileComponent(Window* window); void render(const Transform4x4f& parentTrans) override; - void update(); + void update(int deltaTime) override; + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); // Made this a static function because the ImageGridComponent need to know the default tile max size @@ -31,9 +35,9 @@ class GridTileComponent : public GuiComponent Vector2f getSelectedTileSize() const; bool isSelected() const; - void setImage(const std::string& path); - void setImage(const std::shared_ptr& texture); - void setSelected(bool selected); + void setImage(const std::string& path, std::string name); + void setImage(const std::shared_ptr& texture, std::string name); + void setSelected(bool selected, Vector3f* pPosition = NULL); void setVisible(bool visible); private: @@ -41,13 +45,25 @@ class GridTileComponent : public GuiComponent const GridTileProperties& getCurrentProperties() const; std::shared_ptr mImage; + TextComponent mLabel; + Vector2f mLabelSize; + + bool mLabelVisible; + NinePatchComponent mBackground; GridTileProperties mDefaultProperties; GridTileProperties mSelectedProperties; + std::string mCurrentPath; + + void setSelectedZoom(float percent); + + float mSelectedZoomPercent; bool mSelected; bool mVisible; + + Vector3f mAnimPosition; }; #endif // ES_CORE_COMPONENTS_GRID_TILE_COMPONENT_H diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 14f5fac134..8358ee6839 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -133,23 +133,22 @@ void ImageComponent::setDefaultImage(std::string path) void ImageComponent::setImage(std::string path, bool tile) { - if(path.empty() || !ResourceManager::getInstance()->fileExists(path)) + if (path.empty() || !ResourceManager::getInstance()->fileExists(path)) { - if(mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) + if (mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) mTexture.reset(); else mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic); - } else { + } + else mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic); - } - + resize(); } void ImageComponent::setImage(const char* path, size_t length, bool tile) { mTexture.reset(); - mTexture = TextureResource::get("", tile); mTexture->initFromMemory(path, length); @@ -263,7 +262,7 @@ void ImageComponent::setOpacity(unsigned char opacity) void ImageComponent::updateVertices() { - if(!mTexture || !mTexture->isInitialized()) + if (!mTexture || !mTexture->isInitialized()) return; // we go through this mess to make sure everything is properly rounded @@ -274,6 +273,7 @@ void ImageComponent::updateVertices() mVertices[0].pos = Vector2f(topLeft.x(), topLeft.y()); mVertices[1].pos = Vector2f(topLeft.x(), bottomRight.y()); + mVertices[2].pos = Vector2f(bottomRight.x(), topLeft.y()); mVertices[3].pos = Vector2f(bottomRight.x(), topLeft.y()); @@ -281,11 +281,13 @@ void ImageComponent::updateVertices() mVertices[5].pos = Vector2f(bottomRight.x(), bottomRight.y()); float px, py; - if(mTexture->isTiled()) + if (mTexture->isTiled()) { px = mSize.x() / getTextureSize().x(); py = mSize.y() / getTextureSize().y(); - }else{ + } + else + { px = 1; py = 1; } @@ -298,12 +300,13 @@ void ImageComponent::updateVertices() mVertices[4].tex = Vector2f(mTopLeftCrop.x(), 1 - mBottomRightCrop.y()); mVertices[5].tex = Vector2f(px * mBottomRightCrop.x(), 1 - mBottomRightCrop.y()); - if(mFlipX) + if (mFlipX) { for(int i = 0; i < 6; i++) mVertices[i].tex[0] = px - mVertices[i].tex[0]; } - if(mFlipY) + + if (mFlipY) { for(int i = 0; i < 6; i++) mVertices[i].tex[1] = py - mVertices[i].tex[1]; @@ -320,14 +323,16 @@ void ImageComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); Renderer::setMatrix(trans); - if(mTexture && mOpacity > 0) + if (mTexture && mOpacity > 0) { - if(Settings::getInstance()->getBool("DebugImage")) { + if (Settings::getInstance()->getBool("DebugImage")) + { Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; Renderer::drawRect(targetSizePos.x(), targetSizePos.y(), mTargetSize.x(), mTargetSize.y(), 0xFF000033); Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); } - if(mTexture->isInitialized()) + + if (mTexture->isInitialized()) { // actually draw the image // The bind() function returns false if the texture is not currently loaded. A blank @@ -355,7 +360,9 @@ void ImageComponent::render(const Transform4x4f& parentTrans) glDisable(GL_TEXTURE_2D); glDisable(GL_BLEND); - }else{ + } + else + { LOG(LogError) << "Image texture is not initialized!"; mTexture.reset(); } @@ -471,6 +478,6 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s std::vector ImageComponent::getHelpPrompts() { std::vector ret; - ret.push_back(HelpPrompt("a", "select")); + ret.push_back(HelpPrompt("a", "SELECTIONNER")); return ret; } diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 65b329adcb..9dc51138dd 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -50,6 +50,8 @@ class ImageGridComponent : public IList void onSizeChanged() override; inline void setCursorChangedCallback(const std::function& func) { mCursorChangedCallback = func; } + void setThemeName(std::string name) { mName = name; }; + protected: virtual void onCursorChanged(const CursorState& state) override; @@ -57,13 +59,10 @@ class ImageGridComponent : public IList // TILES void buildTiles(); void updateTiles(); - void updateTileAtPos(int tilePos, int imgPos, int bufferTop, int bufferBot); + void updateTileAtPos(int tilePos, int imgPos); int getStartPosition() const; void calcGridDimension(); - // IMAGES & ENTRIES - const int texBuffersBehind[4] = { 1, 1, 1, 1 }; - const int texBuffersForward[4] = { 1, 2, 3, 3 }; bool mEntriesDirty; int mLastCursor; std::string mDefaultGameTexture; @@ -77,6 +76,10 @@ class ImageGridComponent : public IList std::shared_ptr mTheme; std::vector< std::shared_ptr > mTiles; + std::string mName; + + int mStartPosition; + // MISCELLANEOUS ScrollDirection mScrollDirection; std::function mCursorChangedCallback; @@ -87,6 +90,8 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList void ImageGridComponent::update(int deltaTime) { listUpdate(deltaTime); - + for(auto it = mTiles.begin(); it != mTiles.end(); it++) - (*it)->update(); + (*it)->update(deltaTime); } template @@ -179,16 +184,29 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) // If it's the selected image, keep it for later, otherwise render it now if(tile->isSelected()) selectedTile = tile; - else - tile->render(trans); + // else + // tile->render(trans); } - Renderer::popClipRect(); + // Render the selected image on top of the others if (selectedTile != NULL) selectedTile->render(trans); + for (auto it = mTiles.begin(); it != mTiles.end(); it++) + { + std::shared_ptr tile = (*it); + + // If it's the selected image, keep it for later, otherwise render it now + if (tile->isSelected()) + ; // selectedTile = tile; + else + tile->render(trans); + } + + Renderer::popClipRect(); + listRenderTitleOverlay(trans); GuiComponent::renderChildren(trans); @@ -283,23 +301,55 @@ void ImageGridComponent::onSizeChanged() template void ImageGridComponent::onCursorChanged(const CursorState& state) { - updateTiles(); + if (mLastCursor != mCursor) + { + // 1 if scrolling down, -1 if scrolling up + int scrollDirection = mCursor >= mLastCursor ? 1 : -1; + + int partialRow = (int)mLastRowPartial; + int maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); + + if (mCursor == 0) + mStartPosition = 0; + else if (mCursor < mStartPosition) + mStartPosition = std::max(0, mStartPosition - mGridDimension.x()); + else if (mCursor >= maxPos) + { + mStartPosition = mStartPosition + mGridDimension.x(); + + maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); + if (mCursor >= maxPos) + { + int cursorRow = mCursor / mGridDimension.x(); + mStartPosition = cursorRow * mGridDimension.x(); + if (mStartPosition >= mGridDimension.x() && cursorRow > 0) + { + cursorRow--; + mStartPosition = cursorRow * mGridDimension.x(); + } + } + } - if(mCursorChangedCallback) - mCursorChangedCallback(state); + updateTiles(); + + if (mCursorChangedCallback) + mCursorChangedCallback(state); + } + + mLastCursor = mCursor; } // Create and position tiles (mTiles) template void ImageGridComponent::buildTiles() { + mStartPosition = 0; mTiles.clear(); calcGridDimension(); Vector2f tileDistance = mTileSize + mMargin; - Vector2f bufferSize = Vector2f(mScrollDirection == SCROLL_HORIZONTALLY ? tileDistance.x() * texBuffersForward[3] : 0, - mScrollDirection == SCROLL_VERTICALLY ? tileDistance.y() * texBuffersForward[3] : 0); + Vector2f bufferSize = Vector2f(0, 0); // mScrollDirection == SCROLL_HORIZONTALLY ? tileDistance.x() * texBuffersForward[3] : 0, mScrollDirection == SCROLL_VERTICALLY ? tileDistance.y() * texBuffersForward[3] : 0); Vector2f startPosition = mTileSize / 2 - bufferSize; int X, Y; @@ -319,10 +369,10 @@ void ImageGridComponent::buildTiles() tile->setPosition(X * tileDistance.x() + startPosition.x(), Y * tileDistance.y() + startPosition.y()); tile->setOrigin(0.5f, 0.5f); - tile->setImage(""); + tile->setImage("", ""); if (mTheme) - tile->applyTheme(mTheme, "grid", "gridtile", ThemeFlags::ALL); + tile->applyTheme(mTheme, mName, "gridtile", ThemeFlags::ALL); mTiles.push_back(tile); } @@ -334,84 +384,73 @@ void ImageGridComponent::updateTiles() { if (!mTiles.size()) return; - + // Stop updating the tiles at highest scroll speed if (mScrollTier == 3) { for (int ti = 0; ti < (int)mTiles.size(); ti++) { std::shared_ptr tile = mTiles.at(ti); - + tile->setSelected(false); - tile->setImage(mDefaultGameTexture); + tile->setImage(mDefaultGameTexture, ""); tile->setVisible(false); } return; } - // 1 if scrolling down, -1 if scrolling up - int scrollDirection = mCursor >= mLastCursor ? 1 : -1; - - // If going down, update from top to bottom - // If going up, update from bottom to top - int ti = scrollDirection == 1 ? 0 : (int)mTiles.size() - 1; - int end = scrollDirection == 1 ? (int)mTiles.size() : -1; - + int i = 0; + int end = (int)mTiles.size(); int img = getStartPosition(); - if (scrollDirection == -1) - img += (int)mTiles.size() - 1; - - // Calculate buffer size depending on scroll speed and direction - int bufferBehind = (texBuffersForward[3] - texBuffersBehind[mScrollTier]) * mGridDimension.x(); - int bufferForward = (texBuffersForward[3] - texBuffersForward[mScrollTier]) * mGridDimension.x(); - - int bufferTop = scrollDirection == 1 ? bufferBehind : bufferForward; - int bufferBot = scrollDirection == 1 ? bufferForward : bufferBehind; - // Update the tiles - while (ti != end) + while (i != end) { - updateTileAtPos(ti, img, bufferTop, bufferBot); - - ti += scrollDirection; - img += scrollDirection; + updateTileAtPos(i, img); + i++; img++; } mLastCursor = mCursor; } template -void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, int bufferTop, int bufferBot) +void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos) { std::shared_ptr tile = mTiles.at(tilePos); // If we have more tiles than we have to display images on screen, hide them - if(imgPos < 0 || imgPos >= size() - || tilePos < bufferTop || tilePos >= (int)mTiles.size() - bufferBot) // Same for tiles out of the buffer + if(imgPos < 0 || imgPos >= size() || tilePos < 0 || tilePos >= (int) mTiles.size()) // Same for tiles out of the buffer { tile->setSelected(false); - tile->setImage(""); + tile->setImage("", ""); tile->setVisible(false); } else { - tile->setSelected(imgPos == mCursor); + + if (imgPos == mCursor && mCursor != mLastCursor) + { + int dif = mCursor - tilePos; + int idx = mLastCursor - dif; + + if (idx < 0 || idx >= mTiles.size()) + idx = 0; + + tile->setSelected(true, &mTiles.at(idx)->getPosition()); + } + else + tile->setSelected(imgPos == mCursor); + tile->setVisible(true); + std::string name = mEntries.at(imgPos).name; // .object->getName(); std::string imagePath = mEntries.at(imgPos).data.texturePath; + if (ResourceManager::getInstance()->fileExists(imagePath)) - { - tile->setImage(imagePath); - } + tile->setImage(imagePath, name); + else if (mEntries.at(imgPos).object->getType() == 2) + tile->setImage(mDefaultFolderTexture, name); else - { - // FileType::FOLDER = 2, but FileData is our template parameter T, - // so we don't want to bring that dependence to FileData here - if (mEntries.at(imgPos).object->getType() == 2) - tile->setImage(mDefaultFolderTexture); - else - tile->setImage(mDefaultGameTexture); - } + tile->setImage(mDefaultGameTexture, name); } } @@ -419,32 +458,29 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, int bufferT template int ImageGridComponent::getStartPosition() const { + return mStartPosition; + // The "partialRow" variable exist because we want to keep the same positioning behavior in both // case, whenever we have an integer number of rows or not (the last partial row is ignored when // calculating position and the cursor shouldn't end up in this row when close to the end) int partialRow = (int)mLastRowPartial; int cursorRow = mCursor / mGridDimension.x(); + int cursorCol = mCursor % mGridDimension.x(); int start = (cursorRow - ((mGridDimension.y() - partialRow) / 2)) * mGridDimension.x(); - - // Number of tiles which are just used as a buffer for texture loading - int bufferSize = texBuffersForward[3] * mGridDimension.x(); - - if(start + (mGridDimension.x() * (mGridDimension.y() - partialRow)) >= (int)mEntries.size() + bufferSize) + if (start + (mGridDimension.x() * (mGridDimension.y() - partialRow)) >= (int)mEntries.size()) { // If we are at the end put the row as close as we can and no higher, using the following formula // Where E is the nb of entries, X the grid x dim (nb of column), Y the grid y dim (nb of line) // start = first tile of last row - nb column * (nb line - 1) // = (E - 1) / X * X - X * (Y - 1) // = X * ((E - 1) / X - Y + 1) - start = mGridDimension.x() * (((int)mEntries.size() - 1) / mGridDimension.x() - mGridDimension.y() + 1 + partialRow) + bufferSize; + start = mGridDimension.x() * (((int)mEntries.size() - 1) / mGridDimension.x() - mGridDimension.y() + 1 + partialRow); } - if(start < -bufferSize) - { - start = -bufferSize; - } + if (start < 0) + return 0; return start; } @@ -464,7 +500,7 @@ void ImageGridComponent::calcGridDimension() // Invert dimensions for horizontally scrolling grid if (mScrollDirection == SCROLL_HORIZONTALLY) - mGridDimension = Vector2i(mGridDimension.y(), mGridDimension.x()); + mGridDimension = Vector2i((float) (int)gridDimension.y(), mGridDimension.x()); // Grid dimension validation if (mGridDimension.x() < 1) @@ -473,7 +509,7 @@ void ImageGridComponent::calcGridDimension() LOG(LogError) << "Theme defined grid Y dimension below 1"; // Add extra tiles to both side depending on max texture buffer - mGridDimension.y() += texBuffersForward[3] * 2; +// mGridDimension.y() += texBuffersForward[3] * 2; }; diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 2e60b59459..343c86f618 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -7,7 +7,7 @@ #define TITLE_HEIGHT (mTitle->getFont()->getLetterHeight() + TITLE_VERT_PADDING) -MenuComponent::MenuComponent(Window* window, const char* title, const std::shared_ptr& titleFont) : GuiComponent(window), +MenuComponent::MenuComponent(Window* window, std::string title, const std::shared_ptr& titleFont) : GuiComponent(window), mBackground(window), mGrid(window, Vector2i(1, 3)) { addChild(&mBackground); @@ -19,7 +19,7 @@ MenuComponent::MenuComponent(Window* window, const char* title, const std::share mTitle = std::make_shared(mWindow); mTitle->setHorizontalAlignment(ALIGN_CENTER); mTitle->setColor(0x555555FF); - setTitle(title, titleFont); + setTitle(title.c_str(), titleFont); mGrid.setEntry(mTitle, Vector2i(0, 0), false); // set up list which will never change (externally, anyway) diff --git a/es-core/src/components/MenuComponent.h b/es-core/src/components/MenuComponent.h index 7babd2d3a5..e2c9ea31f2 100644 --- a/es-core/src/components/MenuComponent.h +++ b/es-core/src/components/MenuComponent.h @@ -20,7 +20,7 @@ std::shared_ptr makeArrow(Window* window); class MenuComponent : public GuiComponent { public: - MenuComponent(Window* window, const char* title, const std::shared_ptr& titleFont = Font::get(FONT_SIZE_LARGE)); + MenuComponent(Window* window, std::string title, const std::shared_ptr& titleFont = Font::get(FONT_SIZE_LARGE)); void onSizeChanged() override; diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index bf922acdcb..613a22c026 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -87,7 +87,7 @@ class OptionListComponent : public GuiComponent mMenu.addRow(row, (!mParent->mMultiSelect && it->selected)); } - mMenu.addButton("BACK", "accept", [this] { delete this; }); + mMenu.addButton(_T("BACK"), "accept", [this] { delete this; }); if(mParent->mMultiSelect) { @@ -128,7 +128,7 @@ class OptionListComponent : public GuiComponent std::vector getHelpPrompts() override { auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); return prompts; } }; @@ -205,6 +205,9 @@ class OptionListComponent : public GuiComponent }else if(config->isMappedLike("right", input)) { + if (mEntries.size() == 0) + return true; + // move selection to next unsigned int i = getSelectedId(); int next = (i + 1) % mEntries.size(); @@ -268,6 +271,31 @@ class OptionListComponent : public GuiComponent onSelectedChanged(); } + void selectFirstItem() + { + for (unsigned int i = 0; i < mEntries.size(); i++) + mEntries.at(i).selected = false; + + if (mEntries.size() > 0) + mEntries.at(0).selected = true; + + onSelectedChanged(); + } + + void clear() { + mEntries.clear(); + } + + inline void invalidate() { + onSelectedChanged(); + } + + void setSelectedChangedCallback(const std::function& callback) + { + mSelectedChangedCallback = callback; + } + + private: unsigned int getSelectedId() { @@ -287,6 +315,7 @@ class OptionListComponent : public GuiComponent mWindow->pushGui(new OptionListPopup(mWindow, this, mName)); } + void onSelectedChanged() { if(mMultiSelect) @@ -303,26 +332,29 @@ class OptionListComponent : public GuiComponent // display currently selected + l/r cursors for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) { - if(it->selected) + if (it->selected) { mText.setText(Utils::String::toUpper(it->name)); mText.setSize(0, mText.getSize().y()); setSize(mText.getSize().x() + mLeftArrow.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); - if(mParent) // hack since theres no "on child size changed" callback atm... + if (mParent) // hack since theres no "on child size changed" callback atm... mParent->onSizeChanged(); break; } } } + + if (mSelectedChangedCallback) + mSelectedChangedCallback(mEntries.at(getSelectedId()).object); } std::vector getHelpPrompts() override { std::vector prompts; if(!mMultiSelect) - prompts.push_back(HelpPrompt("left/right", "change")); + prompts.push_back(HelpPrompt("left/right", "MODIFIER")); - prompts.push_back(HelpPrompt("a", "select")); + prompts.push_back(HelpPrompt("a", "SELECTIONNER")); return prompts; } @@ -334,6 +366,7 @@ class OptionListComponent : public GuiComponent ImageComponent mRightArrow; std::vector mEntries; + std::function mSelectedChangedCallback; }; #endif // ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H diff --git a/es-core/src/components/SliderComponent.cpp b/es-core/src/components/SliderComponent.cpp index 619c798356..81f97e9b09 100644 --- a/es-core/src/components/SliderComponent.cpp +++ b/es-core/src/components/SliderComponent.cpp @@ -139,6 +139,6 @@ void SliderComponent::onValueChanged() std::vector SliderComponent::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("left/right", "change")); + prompts.push_back(HelpPrompt("left/right", "MODIFIER")); return prompts; } diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index ec549020bc..747e507fc3 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -72,6 +72,6 @@ void SwitchComponent::onStateChanged() std::vector SwitchComponent::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("a", "change")); + prompts.push_back(HelpPrompt("a", "MODIFIER")); return prompts; } diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 6cd5f082a1..fae35d643c 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -228,7 +228,7 @@ void VideoComponent::applyTheme(const std::shared_ptr& theme, const s std::vector VideoComponent::getHelpPrompts() { std::vector ret; - ret.push_back(HelpPrompt("a", "select")); + ret.push_back(HelpPrompt("a", "SELECTIONNER")); return ret; } diff --git a/es-core/src/guis/GuiInputConfig.cpp b/es-core/src/guis/GuiInputConfig.cpp index 29111a4841..3ec21f101f 100755 --- a/es-core/src/guis/GuiInputConfig.cpp +++ b/es-core/src/guis/GuiInputConfig.cpp @@ -133,8 +133,9 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi return false; } - // apply filtering for quirks related to trigger mapping - if(filterTrigger(input, config, i)) + + // filter for input quirks specific to Sony DualShock 3 + if(filterTrigger(input, config)) return false; // we are configuring @@ -336,26 +337,21 @@ void GuiInputConfig::clearAssignment(int inputId) mTargetConfig->unmapInput(GUI_INPUT_CONFIG_LIST[inputId].name); } -bool GuiInputConfig::filterTrigger(Input input, InputConfig* config, int inputId) +bool GuiInputConfig::filterTrigger(Input input, InputConfig* config) { #if defined(__linux__) // match PlayStation joystick with 6 axes only if((strstr(config->getDeviceName().c_str(), "PLAYSTATION") != NULL \ - || strstr(config->getDeviceName().c_str(), "PS3 Ga") != NULL \ - || strstr(config->getDeviceName().c_str(), "PS(R) Ga") != NULL) \ + || strstr(config->getDeviceName().c_str(), "PS3 Game") != NULL \ + || strstr(config->getDeviceName().c_str(), "PS(R) Game") != NULL) \ && InputManager::getInstance()->getAxisCountByDevice(config->getDeviceId()) == 6) { // digital triggers are unwanted if (input.type == TYPE_BUTTON && (input.id == 6 || input.id == 7)) return true; - } - - // ignore negative pole for axes 2/5 only when triggers are being configured - if((mSkipAxis || strstr(GUI_INPUT_CONFIG_LIST[inputId].name, "Trigger") != NULL) \ - && input.type == TYPE_AXIS && (input.id == 2 || input.id == 5) && input.value < 0) - { - mSkipAxis = true; - return true; + // ignore analog values < 0 + if (input.type == TYPE_AXIS && (input.id == 2 || input.id == 5) && input.value < 0) + return true; } #endif diff --git a/es-core/src/guis/GuiInputConfig.h b/es-core/src/guis/GuiInputConfig.h index d33a8fad7b..bef5f03eab 100644 --- a/es-core/src/guis/GuiInputConfig.h +++ b/es-core/src/guis/GuiInputConfig.h @@ -28,7 +28,7 @@ class GuiInputConfig : public GuiComponent bool assign(Input input, int inputId); void clearAssignment(int inputId); - bool filterTrigger(Input input, InputConfig* config, int inputId); + bool filterTrigger(Input input, InputConfig* config); void rowDone(); @@ -50,7 +50,6 @@ class GuiInputConfig : public GuiComponent Input mHeldInput; int mHeldTime; int mHeldInputId; - bool mSkipAxis; BusyComponent mBusyAnim; }; diff --git a/es-core/src/guis/GuiTextEditPopup.cpp b/es-core/src/guis/GuiTextEditPopup.cpp index 267939430e..de7390cbb8 100644 --- a/es-core/src/guis/GuiTextEditPopup.cpp +++ b/es-core/src/guis/GuiTextEditPopup.cpp @@ -68,6 +68,6 @@ bool GuiTextEditPopup::input(InputConfig* config, Input input) std::vector GuiTextEditPopup::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", "back")); + prompts.push_back(HelpPrompt("b", _T("BACK"))); return prompts; } diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index 81722a549c..d47f6204a0 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -8,6 +8,9 @@ #endif #include +#include +#include "Window.h" + int runShutdownCommand() { #ifdef WIN32 // windows @@ -26,15 +29,111 @@ int runRestartCommand() #endif } -int runSystemCommand(const std::string& cmd_utf8) +std::string trim(const std::string& str) +{ + size_t first = str.find_first_not_of(' '); + if (std::string::npos == first) + { + return str; + } + size_t last = str.find_last_not_of(' '); + return str.substr(first, (last - first + 1)); +} + +void split_cmd(const std::string& cmd, + std::string* executable, + std::string* parameters) +{ + std::string c(cmd); + size_t exec_end; + + c = trim(c); + //boost::trim_all(c); + + if (c[0] == '\"') + { + exec_end = c.find_first_of('\"', 1); + if (std::string::npos != exec_end) + { + *executable = c.substr(1, exec_end - 1); + *parameters = c.substr(exec_end + 1); + } + else + { + *executable = c.substr(1, exec_end); + std::string().swap(*parameters); + } + } + else + { + exec_end = c.find_first_of(' ', 0); + if (std::string::npos != exec_end) + { + *executable = c.substr(0, exec_end); + *parameters = c.substr(exec_end + 1); + } + else + { + *executable = c.substr(0, exec_end); + std::string().swap(*parameters); + } + } +} + +int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window) { #ifdef WIN32 + if (window != NULL) + window->renderBlackScreen("Chargement en cours..."); + // on Windows we use _wsystem to support non-ASCII paths // which requires converting from utf8 to a wstring typedef std::codecvt_utf8 convert_type; std::wstring_convert converter; std::wstring wchar_str = converter.from_bytes(cmd_utf8); - return _wsystem(wchar_str.c_str()); + + std::string exe; + std::string args; + + split_cmd(cmd_utf8, &exe, &args); + + SHELLEXECUTEINFO lpExecInfo; + lpExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); + lpExecInfo.lpFile = exe.c_str(); + lpExecInfo.fMask = SEE_MASK_DOENVSUBST | SEE_MASK_NOCLOSEPROCESS; + lpExecInfo.hwnd = NULL; + lpExecInfo.lpVerb = "open"; // to open program + lpExecInfo.lpParameters = args.c_str(); // file name as an argument + lpExecInfo.lpDirectory = NULL; + lpExecInfo.nShow = SW_SHOW; // show command prompt with normal window size + lpExecInfo.hInstApp = (HINSTANCE)SE_ERR_DDEFAIL; //WINSHELLAPI BOOL WINAPI result; + ShellExecuteEx(&lpExecInfo); + + if (lpExecInfo.hProcess != NULL) + { + if (window == NULL) + WaitForSingleObject(lpExecInfo.hProcess, INFINITE); + else + { + while (WaitForSingleObject(lpExecInfo.hProcess, 50) == 0x00000102L) + { + bool polled = false; + + SDL_Event event; + while (SDL_PollEvent(&event)) + polled = true; + + if (window != NULL && polled) + window->renderBlackScreen("Chargement en cours..."); + } + } + + CloseHandle(lpExecInfo.hProcess); + return 0; + } + + return 1; + //return _wsystem(wchar_str.c_str()); #else return system(cmd_utf8.c_str()); #endif diff --git a/es-core/src/platform.h b/es-core/src/platform.h index 7ffaf5d31e..398f7c12e6 100644 --- a/es-core/src/platform.h +++ b/es-core/src/platform.h @@ -21,9 +21,11 @@ #define GLHEADER #endif +class Window; + int runShutdownCommand(); // shut down the system (returns 0 if successful) int runRestartCommand(); // restart the system (returns 0 if successful) -int runSystemCommand(const std::string& cmd_utf8); // run a utf-8 encoded in the shell (requires wstring conversion on Windows) +int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window); // run a utf-8 encoded in the shell (requires wstring conversion on Windows) int quitES(const std::string& filename); void touch(const std::string& filename); diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index f0e93e61cc..d5086b7e05 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -78,8 +78,8 @@ Font::Font(int size, const std::string& path) : mSize(size), mPath(path) initLibrary(); // always initialize ASCII characters - for(unsigned int i = 32; i < 128; i++) - getGlyph(i); +// for(unsigned int i = 32; i < 128; i++) + // getGlyph(i); clearFaceCache(); } @@ -247,6 +247,10 @@ std::vector getFallbackFontPaths() for(unsigned int i = 0; i < sizeof(fontNames) / sizeof(fontNames[0]); i++) { std::string path = fontDir + fontNames[i]; + + if (i == 0) + path = ":/fontawesome_webfont.ttf"; + if(ResourceManager::getInstance()->fileExists(path)) fontPaths.push_back(path); } diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index f493a91c46..e2a613a859 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -135,6 +135,7 @@ bool TextureData::load() if (!mPath.empty()) { std::shared_ptr& rm = ResourceManager::getInstance(); + const ResourceData& data = rm->getFileData(mPath); // is it an SVG? if (mPath.substr(mPath.size() - 4, std::string::npos) == ".svg") @@ -163,6 +164,9 @@ bool TextureData::uploadAndBind() if (mTextureID != 0) { glBindTexture(GL_TEXTURE_2D, mTextureID); + + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } else { @@ -182,12 +186,13 @@ bool TextureData::uploadAndBind() glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)mWidth, (GLsizei)mHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, mDataRGBA); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); const GLint wrapMode = mTile ? GL_REPEAT : GL_CLAMP_TO_EDGE; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode); } + return true; } diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 5a0a351c97..e4af303eb3 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -147,7 +147,12 @@ namespace Utils { // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows char* envHome = getenv("HOME"); - if(envHome) + +#ifdef _DEBUG + envHome = "H:/[Emulz]/EmulationStation/"; +#endif + + if (envHome) path = getGenericPath(envHome); #if defined(_WIN32) @@ -174,10 +179,7 @@ namespace Utils std::string getCWDPath() { char temp[512]; - - // return current working directory path return (getcwd(temp, 512) ? getGenericPath(temp) : ""); - } // getCWDPath std::string getExePath() @@ -185,14 +187,22 @@ namespace Utils static std::string path; // only construct the exepath once - if(!path.length()) + if (!path.length()) { - path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); - - if(isRegularFile(path)) +#if defined(_WIN32) + char buffer[MAX_PATH]; + DWORD size = MAX_PATH; + DWORD result = GetModuleFileNameA(NULL, buffer, size); + if (result) { - path = getParent(path); + std::string ret = buffer; + path = getParent(ret); + return ret; } +#endif + path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); + if (isRegularFile(path)) + path = getParent(path); } // return constructed exepath @@ -644,7 +654,7 @@ namespace Utils #endif // _WIN32 // filenames starting with . are hidden in linux, we do this check for windows as well - if(getFileName(path)[0] == '.') + if (getFileName(path)[0] == '.') return true; // not hidden diff --git a/es-core/src/utils/StringUtil.cpp b/es-core/src/utils/StringUtil.cpp index 8d35f9701a..0e1da37ea8 100644 --- a/es-core/src/utils/StringUtil.cpp +++ b/es-core/src/utils/StringUtil.cpp @@ -1,299 +1,336 @@ -#include "utils/StringUtil.h" - -#include -#include - -namespace Utils -{ - namespace String - { - unsigned int chars2Unicode(const std::string& _string, size_t& _cursor) - { - const char& c = _string[_cursor]; - unsigned int result = '?'; - - if((c & 0x80) == 0) // 0xxxxxxx, one byte character - { - // 0xxxxxxx - result = ((_string[_cursor++] ) ); - } - else if((c & 0xE0) == 0xC0) // 110xxxxx, two byte character - { - // 110xxxxx 10xxxxxx - result = ((_string[_cursor++] & 0x1F) << 6) | - ((_string[_cursor++] & 0x3F) ); - } - else if((c & 0xF0) == 0xE0) // 1110xxxx, three byte character - { - // 1110xxxx 10xxxxxx 10xxxxxx - result = ((_string[_cursor++] & 0x0F) << 12) | - ((_string[_cursor++] & 0x3F) << 6) | - ((_string[_cursor++] & 0x3F) ); - } - else if((c & 0xF8) == 0xF0) // 11110xxx, four byte character - { - // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - result = ((_string[_cursor++] & 0x07) << 18) | - ((_string[_cursor++] & 0x3F) << 12) | - ((_string[_cursor++] & 0x3F) << 6) | - ((_string[_cursor++] & 0x3F) ); - } - else - { - // error, invalid unicode - ++_cursor; - } - - return result; - - } // chars2Unicode - - std::string unicode2Chars(const unsigned int _unicode) - { - std::string result; - - if(_unicode < 0x80) // one byte character - { - result += ((_unicode ) & 0xFF); - } - else if(_unicode < 0x800) // two byte character - { - result += ((_unicode >> 6) & 0xFF) | 0xC0; - result += ((_unicode ) & 0x3F) | 0x80; - } - else if(_unicode < 0xFFFF) // three byte character - { - result += ((_unicode >> 12) & 0xFF) | 0xE0; - result += ((_unicode >> 6) & 0x3F) | 0x80; - result += ((_unicode ) & 0x3F) | 0x80; - } - else if(_unicode <= 0x1fffff) // four byte character - { - result += ((_unicode >> 18) & 0xFF) | 0xF0; - result += ((_unicode >> 12) & 0x3F) | 0x80; - result += ((_unicode >> 6) & 0x3F) | 0x80; - result += ((_unicode ) & 0x3F) | 0x80; - } - else - { - // error, invalid unicode - result += '?'; - } - - return result; - - } // unicode2Chars - - size_t nextCursor(const std::string& _string, const size_t _cursor) - { - size_t result = _cursor; - - while(result < _string.length()) - { - ++result; - - if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx - break; - } - - return result; - - } // nextCursor - - size_t prevCursor(const std::string& _string, const size_t _cursor) - { - size_t result = _cursor; - - while(result > 0) - { - --result; - - if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx - break; - } - - return result; - - } // prevCursor - - size_t moveCursor(const std::string& _string, const size_t _cursor, const int _amount) - { - size_t result = _cursor; - - if(_amount > 0) - { - for(int i = 0; i < _amount; ++i) - result = nextCursor(_string, result); - } - else if(_amount < 0) - { - for(int i = _amount; i < 0; ++i) - result = prevCursor(_string, result); - } - - return result; - - } // moveCursor - - std::string toLower(const std::string& _string) - { - std::string string; - - for(size_t i = 0; i < _string.length(); ++i) - string += (char)tolower(_string[i]); - - return string; - - } // toLower - - std::string toUpper(const std::string& _string) - { - std::string string; - - for(size_t i = 0; i < _string.length(); ++i) - string += (char)toupper(_string[i]); - - return string; - - } // toUpper - - std::string trim(const std::string& _string) - { - const size_t strBegin = _string.find_first_not_of(" \t"); - const size_t strEnd = _string.find_last_not_of(" \t"); - - if(strBegin == std::string::npos) - return ""; - - return _string.substr(strBegin, strEnd - strBegin + 1); - - } // trim - - std::string replace(const std::string& _string, const std::string& _replace, const std::string& _with) - { - std::string string = _string; - size_t pos; - - while((pos = string.find(_replace)) != std::string::npos) - string = string.replace(pos, _replace.length(), _with.c_str(), _with.length()); - - return string; - - } // replace - - bool startsWith(const std::string& _string, const std::string& _start) - { - return (_string.find(_start) == 0); - - } // startsWith - - bool endsWith(const std::string& _string, const std::string& _end) - { - return (_string.find(_end) == (_string.size() - _end.size())); - - } // endsWith - - std::string removeParenthesis(const std::string& _string) - { - static const char remove[4] = { '(', ')', '[', ']' }; - std::string string = _string; - size_t start; - size_t end; - bool done = false; - - while(!done) - { - done = true; - - for(int i = 0; i < sizeof(remove); i += 2) - { - end = string.find_first_of(remove[i + 1]); - start = string.find_last_of( remove[i + 0], end); - - if((start != std::string::npos) && (end != std::string::npos)) - { - string.erase(start, end - start + 1); - done = false; - } - } - } - - return trim(string); - - } // removeParenthesis - - stringVector commaStringToVector(const std::string& _string) - { - stringVector vector; - size_t start = 0; - size_t comma = _string.find(","); - - while(comma != std::string::npos) - { - vector.push_back(_string.substr(start, comma - start)); - start = comma + 1; - comma = _string.find(",", start); - } - - vector.push_back(_string.substr(start)); - std::sort(vector.begin(), vector.end()); - - return vector; - - } // commaStringToVector - - std::string vectorToCommaString(stringVector _vector) - { - std::string string; - - std::sort(_vector.begin(), _vector.end()); - - for(stringVector::const_iterator it = _vector.cbegin(); it != _vector.cend(); ++it) - string += (string.length() ? "," : "") + (*it); - - return string; - - } // vectorToCommaString - - std::string format(const char* _format, ...) - { - va_list args; - va_list copy; - - va_start(args, _format); - - va_copy(copy, args); - const int length = vsnprintf(nullptr, 0, _format, copy); - va_end(copy); - - char* buffer = new char[length + 1]; - va_copy(copy, args); - vsnprintf(buffer, length + 1, _format, copy); - va_end(copy); - - va_end(args); - - std::string out(buffer); - delete buffer; - - return out; - - } // format - - // Simple XOR scrambling of a string, with an accompanying key - std::string scramble(const std::string& _input, const std::string& key) - { - std::string buffer = _input; +#include "utils/StringUtil.h" + +#include +#include + +#if defined(_WIN32) +#include +#endif + +namespace Utils +{ + namespace String + { +#if defined(_WIN32) + const std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + + std::string string; + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return string; + } + + const std::wstring convertToWideString(const std::string string) + { + int numBytes = MultiByteToWideChar(CP_UTF8, 0, string.c_str(), (int)string.length(), nullptr, 0); + + std::wstring wstring; + wstring.resize(numBytes); + MultiByteToWideChar(CP_UTF8, 0, string.c_str(), (int)string.length(), (WCHAR*)wstring.c_str(), numBytes); + + return wstring; + } +#endif + + unsigned int chars2Unicode(const std::string& _string, size_t& _cursor) + { + const char& c = _string[_cursor]; + unsigned int result = '?'; + + if((c & 0x80) == 0) // 0xxxxxxx, one byte character + { + // 0xxxxxxx + result = ((_string[_cursor++] ) ); + } + else if((c & 0xE0) == 0xC0) // 110xxxxx, two byte character + { + // 110xxxxx 10xxxxxx + result = ((_string[_cursor++] & 0x1F) << 6) | + ((_string[_cursor++] & 0x3F) ); + } + else if((c & 0xF0) == 0xE0) // 1110xxxx, three byte character + { + // 1110xxxx 10xxxxxx 10xxxxxx + result = ((_string[_cursor++] & 0x0F) << 12) | + ((_string[_cursor++] & 0x3F) << 6) | + ((_string[_cursor++] & 0x3F) ); + } + else if((c & 0xF8) == 0xF0) // 11110xxx, four byte character + { + // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx + result = ((_string[_cursor++] & 0x07) << 18) | + ((_string[_cursor++] & 0x3F) << 12) | + ((_string[_cursor++] & 0x3F) << 6) | + ((_string[_cursor++] & 0x3F) ); + } + else + { + // error, invalid unicode + ++_cursor; + } + + return result; + + } // chars2Unicode + + std::string unicode2Chars(const unsigned int _unicode) + { + std::string result; + + if(_unicode < 0x80) // one byte character + { + result += ((_unicode ) & 0xFF); + } + else if(_unicode < 0x800) // two byte character + { + result += ((_unicode >> 6) & 0xFF) | 0xC0; + result += ((_unicode ) & 0x3F) | 0x80; + } + else if(_unicode < 0xFFFF) // three byte character + { + result += ((_unicode >> 12) & 0xFF) | 0xE0; + result += ((_unicode >> 6) & 0x3F) | 0x80; + result += ((_unicode ) & 0x3F) | 0x80; + } + else if(_unicode <= 0x1fffff) // four byte character + { + result += ((_unicode >> 18) & 0xFF) | 0xF0; + result += ((_unicode >> 12) & 0x3F) | 0x80; + result += ((_unicode >> 6) & 0x3F) | 0x80; + result += ((_unicode ) & 0x3F) | 0x80; + } + else + { + // error, invalid unicode + result += '?'; + } + + return result; + + } // unicode2Chars + + size_t nextCursor(const std::string& _string, const size_t _cursor) + { + size_t result = _cursor; + + while(result < _string.length()) + { + ++result; + + if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx + break; + } + + return result; + + } // nextCursor + + size_t prevCursor(const std::string& _string, const size_t _cursor) + { + size_t result = _cursor; + + while(result > 0) + { + --result; + + if((_string[result] & 0xC0) != 0x80) // break if current character is not 10xxxxxx + break; + } + + return result; + + } // prevCursor + + size_t moveCursor(const std::string& _string, const size_t _cursor, const int _amount) + { + size_t result = _cursor; + + if(_amount > 0) + { + for(int i = 0; i < _amount; ++i) + result = nextCursor(_string, result); + } + else if(_amount < 0) + { + for(int i = _amount; i < 0; ++i) + result = prevCursor(_string, result); + } + + return result; + + } // moveCursor + + std::string toLower(const std::string& _string) + { + std::string string; + + for(size_t i = 0; i < _string.length(); ++i) + string += (char)tolower(_string[i]); + + return string; + + } // toLower + + std::string toUpper(const std::string& _string) + { + +#if defined(_WIN32) + std::wstring stringW = convertToWideString(_string); + + auto& f = std::use_facet>(std::locale()); + f.toupper(&stringW[0], &stringW[0] + stringW.size()); + + return convertFromWideString(stringW); +#else + std::string string; + + for(size_t i = 0; i < _string.length(); ++i) + string += (char)toupper(_string[i]); + + return string; +#endif + } // toUpper + + std::string trim(const std::string& _string) + { + const size_t strBegin = _string.find_first_not_of(" \t"); + const size_t strEnd = _string.find_last_not_of(" \t"); + + if(strBegin == std::string::npos) + return ""; + + return _string.substr(strBegin, strEnd - strBegin + 1); + + } // trim + + std::string replace(const std::string& _string, const std::string& _replace, const std::string& _with) + { + std::string string = _string; + size_t pos; + + while((pos = string.find(_replace)) != std::string::npos) + string = string.replace(pos, _replace.length(), _with.c_str(), _with.length()); + + return string; + + } // replace + + bool startsWith(const std::string& _string, const std::string& _start) + { + return (_string.find(_start) == 0); + + } // startsWith + + bool endsWith(const std::string& _string, const std::string& _end) + { + return (_string.find(_end) == (_string.size() - _end.size())); + + } // endsWith + + std::string removeParenthesis(const std::string& _string) + { + static const char remove[4] = { '(', ')', '[', ']' }; + std::string string = _string; + size_t start; + size_t end; + bool done = false; + + while(!done) + { + done = true; + + for(int i = 0; i < sizeof(remove); i += 2) + { + end = string.find_first_of(remove[i + 1]); + start = string.find_last_of( remove[i + 0], end); + + if((start != std::string::npos) && (end != std::string::npos)) + { + string.erase(start, end - start + 1); + done = false; + } + } + } + + return trim(string); + + } // removeParenthesis + + stringVector commaStringToVector(const std::string& _string) + { + stringVector vector; + size_t start = 0; + size_t comma = _string.find(","); + + while(comma != std::string::npos) + { + vector.push_back(_string.substr(start, comma - start)); + start = comma + 1; + comma = _string.find(",", start); + } + + vector.push_back(_string.substr(start)); + std::sort(vector.begin(), vector.end()); + + return vector; + + } // commaStringToVector + + std::string vectorToCommaString(stringVector _vector) + { + std::string string; + + std::sort(_vector.begin(), _vector.end()); + + for(stringVector::const_iterator it = _vector.cbegin(); it != _vector.cend(); ++it) + string += (string.length() ? "," : "") + (*it); + + return string; + + } // vectorToCommaString + + std::string format(const char* _format, ...) + { + va_list args; + va_list copy; + + va_start(args, _format); + + va_copy(copy, args); + const int length = vsnprintf(nullptr, 0, _format, copy); + va_end(copy); + + char* buffer = new char[length + 1]; + va_copy(copy, args); + vsnprintf(buffer, length + 1, _format, copy); + va_end(copy); + + va_end(args); + + std::string out(buffer); + delete buffer; + + return out; + + } // format + + // Simple XOR scrambling of a string, with an accompanying key + std::string scramble(const std::string& _input, const std::string& key) + { + std::string buffer = _input; for (size_t i = 0; i < _input.size(); ++i) { - buffer[i] = _input[i] ^ key[i]; - } - - return buffer; - - } // scramble - - } // String:: - -} // Utils:: + buffer[i] = _input[i] ^ key[i]; + } + + return buffer; + + } // scramble + + } // String:: + +} // Utils:: diff --git a/es-core/src/utils/StringUtil.h b/es-core/src/utils/StringUtil.h index 5fffa1d761..7ebcf0df22 100644 --- a/es-core/src/utils/StringUtil.h +++ b/es-core/src/utils/StringUtil.h @@ -28,6 +28,10 @@ namespace Utils std::string format (const char* _string, ...); std::string scramble (const std::string& _input, const std::string& key); +#if defined(_WIN32) + const std::string convertFromWideString(const std::wstring wstring); + const std::wstring convertToWideString(const std::string string); +#endif } // String:: } // Utils:: From 244c0fe57e74001f2a557c82e6290d56bb6e7c4b Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 27 May 2019 13:57:26 +0200 Subject: [PATCH 024/336] [Add French localization resources] --- resources/fontawesome_webfont.ttf | Bin 0 -> 120456 bytes resources/locale/fr/emulationstation2.po | 2047 ++++++++++++++++++++++ resources/locale/it/emulationstation2.po | 1884 ++++++++++++++++++++ 3 files changed, 3931 insertions(+) create mode 100644 resources/fontawesome_webfont.ttf create mode 100644 resources/locale/fr/emulationstation2.po create mode 100644 resources/locale/it/emulationstation2.po diff --git a/resources/fontawesome_webfont.ttf b/resources/fontawesome_webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..59dec23e2af3290758890eee55e9f619e6c48202 GIT binary patch literal 120456 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 literal 0 HcmV?d00001 diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po new file mode 100644 index 0000000000..3f34a69461 --- /dev/null +++ b/resources/locale/fr/emulationstation2.po @@ -0,0 +1,2047 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: recalbox-emulationstation\n" +"Language: fr\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" + +#: +msgid "GAME COLLECTION SETTINGS" +msgstr "LISTES PERSONNALISÉES" + +#: +msgid "CANCEL" +msgstr "ANNULER" + +#: +msgid "automatic" +msgstr "automatique" + +#: +msgid "basic" +msgstr "basique" + +#: +msgid "detailed" +msgstr "détaillée" + +#: +msgid "grid" +msgstr "grille" + +#: +msgid "fade" +msgstr "fondu" + +#: +msgid "instant" +msgstr "instantanée" + +#: +msgid "slide" +msgstr "animée" + +#: +msgid "gridex" +msgstr "grille détaillée" + +#: +msgid "ENABLE NAVIGATION SOUNDS" +msgstr "SONS LORS DE LA NAVIGATION" + +#: +msgid "ENABLE VIDEO AUDIO" +msgstr "SON DES VIDEOS" + +#: +msgid "APPLY FILTER" +msgstr "FILTER LES JEUX" + +#: +msgid "AUTOMATIC GAME COLLECTIONS" +msgstr "COLLECTIONS AUTOMATIQUES" + +#: +msgid "SELECT COLLECTIONS" +msgstr "SÉLECTIONNER LES COLLECTIONS" + +#: +msgid "CUSTOM GAME COLLECTIONS" +msgstr "COLLECTIONS PERSONNALISEES" + +#: +msgid "CREATE NEW CUSTOM COLLECTION FROM THEME" +msgstr "CRÉER UNE COLLECTION A PARTIR D'UN THEME" + +#: +msgid "GAME COLLECTION SETTINGS" +msgstr "PARAMÈTRES DES LISTES DE JEUX" + +#: +msgid "SELECT THEME FOLDER" +msgstr "SÉLECTIONNEZ LE DOSSIER" + +#: +msgid "CREATE NEW CUSTOM COLLECTION" +msgstr "CREER UNE COLLECTION PERSONNALISÉE" + +#: +msgid "New Collection Name" +msgstr "Nom de la collection" + +#: +msgid "GROUP UNTHEMED CUSTOM COLLECTIONS" +msgstr "GROUPER LES COLLECTIONS SANS THEME" + +#: +msgid "SORT CUSTOM COLLECTIONS AND SYSTEMS" +msgstr "TRIER LES COLLECTIONS ET LES SYSTÈMES" + +#: +msgid "FINISH EDITING COLLECTION" +msgstr "TERMINER L'ÉDITION DE LA COLLECTION" + +#: +msgid "PARSE GAMESLISTS ONLY" +msgstr "LISTES DE JEUX XML UNIQUEMENT" + +#: +msgid "POWER SAVER MODES" +msgstr "MODE D'ECONOMIE D'ENERGIE" + +#: +msgid "SAVE METADATA ON EXIT" +msgstr "SAUVER LES DONNÉES EN QUITTANT" + +#: +msgid "SHOW HIDDEN FILES" +msgstr "AFFICHER LES FICHIERS CACHÉS" + +#: +msgid "VRAM LIMIT" +msgstr "MÉMOIRE VIDÉO MAX" + +#: +msgid "RESET ALL FILTERS" +msgstr "RÉINITIALISER TOUS LES FILTRES" + +#: +msgid "GAMELIST VIEW STYLE" +msgstr "STYLE D'AFFICHAGE" + +#: +msgid "GAME LAUNCH TRANSITION" +msgstr "TRANSITION AU LANCEMENT D'UN JEU" + +#: +msgid "RANDOM" +msgstr "ALÉATOIRE" + +#: +msgid "Rating" +msgstr "Note" + +#: +msgid "Released" +msgstr "Sortie" + +#: +msgid "Developer" +msgstr "Développeur" + +#: +msgid "Publisher" +msgstr "Éditeur" + +#: +msgid "Genre" +msgstr "Genre" + +#: +msgid "Players" +msgstr "Joueurs" + +#: +msgid "NO GAMES FOUND - SKIP" +msgstr "AUCUN JEU CORRESPONDANT - PASSER" + +#: +msgid "RETRY" +msgstr "RECOMMENCER" + +#: +msgid "SKIP" +msgstr "PASSER" + +#: +msgid "SEARCH FOR" +msgstr "RECHERCHER" + +#: +msgid "SEARCH" +msgstr "RECHERCHER" + +#: +msgid "SCRAPING IN PROGRESS" +msgstr "SCRAPING EN COURS" + +#: +msgid "SYSTEM" +msgstr "SYSTÈME" + +#: +msgid "subtitle text" +msgstr "sous-titre" + +#: +msgid "INPUT" +msgstr "ÉDITER" + +#: +msgid "search" +msgstr "rechercher" + +#: +msgid "STOP" +msgstr "ARRÊTER" + +#: +msgid "stop (progress saved)" +msgstr "stop (progression sauvegardée)" + +#: +msgid "GAME %i OF %i" +msgstr "JEU %i SUR %i" + +#: +msgid "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 RECALBOX.COM FOR MORE INFORMATION." +msgstr "AUCUN JEU N'A ÉTÉ TROUVÉ SUR LE SYSTÈME !\n" +"VEUILLEZ AJOUTER DES JEUX EN SUIVANT LES INSTRUCTIONS DE LA NOTICE.\n" +"\n" +"RENDEZ-VOUS SUR RECALBOX.COM POUR PLUS D'INFORMATIONS" + +#: +msgid "%i GAME SUCCESSFULLY SCRAPED!" +msgid_plural "%i GAMES SUCCESSFULLY SCRAPED!" +msgstr[0] "%i JEU SCRAPPÉ !" +msgstr[1] "%i JEUX SCRAPPÉS !" + +#: +msgid "%i GAME SKIPPED." +msgid_plural "%i GAMES SKIPPED." +msgstr[0] "%i JEU PASSÉ." +msgstr[1] "%i JEUX PASSÉS." + +#: +msgid "OK" +msgstr "OK" + +#: +msgid "EDIT METADATA" +msgstr "ÉDITER LES DONNÉES" + +#: +msgid "SCRAPE" +msgstr "SCRAPPER" + +#: +msgid "SAVE" +msgstr "SAUVEGARDER" + +#: +msgid "THIS WILL DELETE A FILE!\n" +"ARE YOU SURE?" +msgstr "CELA VA EFFACER LE FICHIER\n" +"ÊTES-VOUS SUR ?" + +#: +msgid "YES" +msgstr "OUI" + +#: +msgid "NO" +msgstr "NON" + +#: +msgid "DELETE" +msgstr "SUPPRIMER" + +#: +msgid "SAVE CHANGES?" +msgstr "SAUVEGARDER ?" + +#: +msgid "BACK" +msgstr "RETOUR" + +#: +msgid "CLOSE" +msgstr "FERMER" + +#: +msgid "MAIN MENU" +msgstr "MENU PRINCIPAL" + +#: +msgid "KODI MEDIA CENTER" +msgstr "KODI MEDIA CENTER" + +#: +msgid "SYSTEM SETTINGS" +msgstr "OPTIONS SYSTÈME" + +#: +msgid "VERSION" +msgstr "VERSION" + +#: +msgid "DISK USAGE" +msgstr "ESPACE DISQUE" + +#: +msgid "STORAGE DEVICE" +msgstr "MÉDIA DE STOCKAGE" + +#: +msgid "LANGUAGE" +msgstr "LANGUE" + +#: +msgid "OVERCLOCK" +msgstr "OVERCLOCK" + +#: +msgid "EXTREM (1100Mhz)" +msgstr "EXTREM (1100Mhz)" + +#: +msgid "TURBO (1000Mhz)" +msgstr "TURBO (1000Mhz)" + +#: +msgid "HIGH (950Mhz)" +msgstr "HIGH (950Mhz)" + +#: +msgid "NONE (700Mhz)" +msgstr "AUCUN (700Mhz)" + +#: +msgid "TURBO (1050Mhz)+" +msgstr "TURBO (1050Mhz)+" + +#: +msgid "HIGH (1050Mhz)" +msgstr "HIGH (1050Mhz)" + +#: +msgid "NONE (900Mhz)" +msgstr "AUCUN (900Mhz)" + +#: +msgid "NONE (1200Mhz)" +msgstr "AUCUN (1200Mhz)" + +#: +msgid "NONE" +msgstr "AUCUN" + +#. NEW SETTINGS ORGANIZATION +#: +msgid "UPDATES" +msgstr "MISES À JOUR" + +#: +msgid "AUTO UPDATES" +msgstr "MISES À JOUR AUTO" + +#: +msgid "START UPDATE" +msgstr "LANCER LA MISE À JOUR" + +#: +msgid "KODI SETTINGS" +msgstr "OPTIONS KODI" + +#: +msgid "ENABLE KODI" +msgstr "ACTIVER KODI" + +#: +msgid "KODI AT START" +msgstr "AUTO DÉMARRAGE DE KODI" + +#: +msgid "START KODI WITH X" +msgstr "X POUR LANCER KODI" + +#: +msgid "SECURITY" +msgstr "SÉCURITÉ" + +#: +msgid "ENFORCE SECURITY" +msgstr "RENFORCER LA SÉCURITÉ" + +#: +msgid "ROOT PASSWORD" +msgstr "MOT DE PASSE ROOT" + +#: +msgid "THE SYSTEM WILL NOW REBOOT" +msgstr "LE SYSTÈME VA REDÉMARRER" + +#: +msgid "GAMES SETTINGS" +msgstr "OPTIONS DES JEUX" + +#: +msgid "GAME RATIO" +msgstr "FORMAT JEUX" + +#: +msgid "SMOOTH GAMES" +msgstr "LISSER LES JEUX" + +#: +msgid "REWIND" +msgstr "REMBOBINAGE" + +#: +msgid "AUTO SAVE/LOAD" +msgstr "SAUVEGARDE/CHARGEMENT AUTOMATIQUE" + +#: +msgid "SHADERS SET" +msgstr "SHADERS SET" + +#: +msgid "SCANLINES" +msgstr "SCANLINES" + +#: +msgid "RETRO" +msgstr "RETRO" + +#: +msgid "RETROACHIEVEMENTS SETTINGS" +msgstr "OPTIONS DE RETROACHIEVEMENTS" + +#: +msgid "RETROACHIEVEMENTS" +msgstr "RETROACHIEVEMENTS" + +#: +msgid "HARDCORE MODE" +msgstr "MODE HARDCORE" + +#: +msgid "USERNAME" +msgstr "NOM UTILISATEUR" + +#: +msgid "PASSWORD" +msgstr "MOT DE PASSE" + +#: +msgid "ADVANCED" +msgstr "AVANCÉES" + +#: +msgid "REALLY UPDATE GAMES LISTS ?" +msgstr "CONFIRMER LA MISE À JOUR DES LISTES DES JEUX ?" + +#: +msgid "UPDATE GAMES LISTS" +msgstr "METTRE À JOUR LES LISTES DES JEUX" + +#: +msgid "CONTROLLERS SETTINGS" +msgstr "OPTIONS MANETTES" + +#: +msgid "UI SETTINGS" +msgstr "OPTIONS DE L'INTERFACE" + +#: +msgid "OVERSCAN" +msgstr "OVERSCAN" + +#: +msgid "SCREENSAVER SETTINGS" +msgstr "ÉCRAN DE VEILLE" + +#: +msgid "SCREENSAVER AFTER" +msgstr "ÉCRAN DE VEILLE APRÈS" + +#: +msgid "TRANSITION STYLE" +msgstr "STYLE DE LA TRANSITION" + +#: +msgid "SCREENSAVER BEHAVIOR" +msgstr "COMPORTEMENT DE L'ÉCRAN DE VEILLE" + +#: +msgid "SHOW FRAMERATE" +msgstr "AFFICHER LE FRAMERATE" + +#: +msgid "ON-SCREEN HELP" +msgstr "AFFICHAGE DE L'AIDE" + +#: +msgid "HIDE WHEN RUNNING GAME" +msgstr "MASQUER AU LANCEMENT D'UN JEU" + +#: +msgid "QUICK SYSTEM SELECT" +msgstr "SÉLECTION RAPIDE DU SYSTÈME" + +#: +msgid "THEME SET" +msgstr "THÈME" + +#: +msgid "SOUND SETTINGS" +msgstr "OPTIONS DU SON" + +#: +msgid "SYSTEM VOLUME" +msgstr "VOLUME DU SYSTÈME" + +#: +msgid "FRONTEND MUSIC" +msgstr "MUSIQUE DE FOND" + +#: +msgid "OUTPUT DEVICE" +msgstr "SORTIE AUDIO" + +#: +msgid "HDMI" +msgstr "HDMI" + +#: +msgid "JACK" +msgstr "JACK" + +#: +msgid "AUTO" +msgstr "AUTO" + +#: +msgid "NETWORK SETTINGS" +msgstr "OPTIONS RÉSEAU" + +#: +msgid "CONNECTED" +msgstr "CONNECTÉ" + +#: +msgid "NOT CONNECTED" +msgstr "DÉCONNECTÉ" + +#: +msgid "STATUS" +msgstr "STATUT" + +#: +msgid "IP ADDRESS" +msgstr "ADRESSE IP" + +#: +msgid "HOSTNAME" +msgstr "NOM SUR LE RÉSEAU" + +#: +msgid "ENABLE WIFI" +msgstr "ACTIVER LE WIFI" + +#: +msgid "WIFI SSID" +msgstr "SSID WIFI" + +#: +msgid "WIFI KEY" +msgstr "CLÉ WIFI" + +#: +msgid "WIFI ENABLED" +msgstr "WIFI ACTIVÉ" + +#: +msgid "WIFI CONFIGURATION ERROR" +msgstr "ERREUR DE CONFIGURATION WIFI" + +#: +msgid "SCRAPER" +msgstr "SCRAPPEUR" + +#: +msgid "SCRAPE FROM" +msgstr "SCRAPPER DEPUIS" + +#: +msgid "SCRAPE RATINGS" +msgstr "SCRAPPER LES NOTES" + +#: +msgid "SCRAPE NOW" +msgstr "SCRAPPER MAINTENANT" + +#: +msgid "QUIT" +msgstr "QUITTER" + +#: +msgid "REALLY RESTART?" +msgstr "REDÉMARRER ?" + +#: +msgid "RESTART SYSTEM" +msgstr "REDÉMARRER" + +#: +msgid "REALLY SHUTDOWN?" +msgstr "ÉTEINDRE ?" + +#: +msgid "SHUTDOWN SYSTEM" +msgstr "ÉTEINDRE" + +#: +msgid "Emulator" +msgstr "Émulateur" + +#: +msgid "Core" +msgstr "Core" + +#: +msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." +msgstr "VOUS ALLEZ CONFIGURER UNE MANETTE. SI VOUS N'AVEZ QU'UN JOYSTICK OU JUSTE UNE CROIX DIRECTIONNELLE, CONFIGUREZ SEULEMENT LES DIRECTIONS ET PASSEZ LES JOYSTICKS EN RESTANT APPUYÉ SUR UN BOUTON. SI VOUS N'AVEZ PAS DE BOUTON SPECIAL POUR HOTKEY, UTILISEZ SELECT. IGNOREZ UN BOUTON QUE VOTRE MANETTE N'A PAS EN RESTANT APPUYÉ SUR UN BOUTON DÉJÀ CONFIGURÉ. LES NOMS DES BOUTONS SONT BASÉS SUR LA SUPER NINTENDO" + +#. GUIMENU +#: +msgid "CONFIGURE A CONTROLLER" +msgstr "CONFIGURER UNE MANETTE" + +#. Bluetooth +#: +msgid "CONTROLLER PAIRED" +msgstr "MANETTE ASSOCIÉE" + +#: +msgid "UNABLE TO PAIR CONTROLLER" +msgstr "IMPOSSIBLE D'ASSOCIER LA MANETTE" + +#: +msgid "AN ERROR OCCURED" +msgstr "UNE ERREUR EST SURVENUE" + +#: +msgid "NO CONTROLLERS FOUND" +msgstr "AUCUNE MANETTE DÉTECTÉE" + +#: +msgid "PAIR A BLUETOOTH CONTROLLER" +msgstr "ASSOCIER UNE MANETTE BLUETOOTH" + +#: +msgid "CONTROLLERS LINKS HAVE BEEN DELETED." +msgstr "LES LIENS VERS LES MANETTES ONT ÉTÉ SUPPRIMÉS." + +#: +msgid "FORGET BLUETOOTH CONTROLLERS" +msgstr "OUBLIER LES MANETTES BLUETOOTH" + +#: +msgid "INPUT P%i" +msgstr "JOUEUR %i" + +#: +msgid "CHOOSE" +msgstr "CHOISIR" + +#: +msgid "SELECT" +msgstr "SÉLECTIONNER" + +#: +msgid "OPTIONS" +msgstr "OPTIONS" + +#: +msgid "JUMP TO LETTER" +msgstr "ALLER À LA LETTRE" + +#: +msgid "SORT GAMES BY" +msgstr "TRIER LES JEUX PAR" + +#. FAVORITES +#: +msgid "FAVORITES ONLY" +msgstr "FAVORIS SEULEMENT" + +#: +msgid "EDIT THIS GAME'S METADATA" +msgstr "ÉDITER LES DONNÉES DU JEU" + +#: +msgid "SCRAPE THESE GAMES" +msgstr "SCRAPPER CES JEUX" + +#: +msgid "All Games" +msgstr "Tous les jeux" + +#. MISSING SCRAPPER TRANSLATIONS +#: +msgid "Only missing image" +msgstr "Seulement images manquantes" + +#: +msgid "FILTER" +msgstr "FILTRE" + +#: +msgid "SCRAPE THESE SYSTEMS" +msgstr "SCRAPPER CES SYSTÈMES" + +#: +msgid "SYSTEMS" +msgstr "SYSTÈMES" + +#: +msgid "USER DECIDES ON CONFLICTS" +msgstr "CHOISIR SI CONFLIT" + +#: +msgid "START" +msgstr "DÉMARRER" + +#: +msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" +"CONTINUE ANYWAY?" +msgstr "ATTENTION: CERTAINS DE VOS SYSTÈMES N'ONT PAS DE PLATEFORME. LES RÉSULTATS POURRAIENT ÊTRE INATTENDUS!\n" +"CONTINUER QUAND MÊME?" + +#: +msgid "NO GAMES FIT THAT CRITERIA." +msgstr "AUCUN JEU NE CORRESPOND." + +#: +msgid "REALLY UPDATE?" +msgstr "METTRE À JOUR LE SYSTÈME ?" + +#: +msgid "NETWORK CONNECTION NEEDED" +msgstr "VEUILLEZ BRANCHER UN CÂBLE RÉSEAU" + +#: +msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" +msgstr "MISE À JOUR TÉLÉCHARGÉE, REDÉMARRAGE EN COURS" + +#: +msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" +msgstr "ÉCHEC DE LA MISE À JOUR" + +#: +msgid "NO UPDATE AVAILABLE" +msgstr "AUCUNE MISE À JOUR DISPONIBLE" + +#: +msgid "enter emulator" +msgstr "entrer l'émulateur" + +#: +msgid "enter core" +msgstr "entrer le core" + +#: +msgid "Ratio" +msgstr "Ratio" + +#: +msgid "enter ratio" +msgstr "entrer le ratio" + +#: +msgid "Name" +msgstr "Nom" + +#: +msgid "enter game name" +msgstr "entrer le nom du jeu" + +#: +msgid "Description" +msgstr "Description" + +#: +msgid "enter description" +msgstr "entrer la description" + +#: +msgid "Image" +msgstr "Image" + +#: +msgid "enter path to image" +msgstr "entrer le chemin vers l'image" + +#: +msgid "Thumbnail" +msgstr "Miniature" + +#: +msgid "enter path to thumbnail" +msgstr "entrer le chemin vers la miniature" + +#: +msgid "enter rating" +msgstr "entrer les votes" + +#: +msgid "Release date" +msgstr "Date de sortie" + +#: +msgid "enter release date" +msgstr "entrer la date de sortie" + +#: +msgid "enter game developer" +msgstr "entrer le développeur du jeu" + +#: +msgid "enter game publisher" +msgstr "entrer l'éditeur du jeu" + +#: +msgid "enter game genre" +msgstr "entrer le genre du jeu" + +#: +msgid "enter number of players" +msgstr "entrer le nombre de joueurs" + +#: +msgid "Favorite" +msgstr "Favori" + +#: +msgid "enter favorite" +msgstr "entrer le favori" + +#: +msgid "Region" +msgstr "Région" + +#: +msgid "enter region" +msgstr "entrer la région" + +#: +msgid "Romtype" +msgstr "Type de rom" + +#: +msgid "enter romtype" +msgstr "entre le type de rom" + +#: +msgid "Hidden" +msgstr "Invisible" + +#: +msgid "HIDDEN" +msgstr "CACHÉ" + +#: +msgid "NAME" +msgstr "NOM" + +#: +msgid "EMULATOR" +msgstr "EMULATEUR" + +#: +msgid "FAVORITE" +msgstr "FAVORI" + +#: +msgid "set hidden" +msgstr "Mettre caché" + +#: +msgid "Play count" +msgstr "Nb parties" + +#: +msgid "enter number of times played" +msgstr "entrer le nombre de parties" + +#: +msgid "Last played" +msgstr "Dernier jeu" + +#: +msgid "enter last played date" +msgstr "entrer la date de la dernière partie" + +#: +msgid "GAMES AVAILABLE" +msgstr "JEUX DISPONIBLES" + +#: +msgid "%i FAVORITE" +msgid_plural "%i FAVORITES" +msgstr[0] "%i FAVORI" +msgstr[1] "%i FAVORIS" + +#: +msgid "SCROLL" +msgstr "SCROLLER" + +#: +msgid "LAUNCH" +msgstr "LANCER" + +#: +msgid "Times played" +msgstr "Nb parties" + +#: +msgid "MENU" +msgstr "MENU" + +#: +msgid "FILENAME, ASCENDING" +msgstr "FICHIER, ASCENDANT" + +#: +msgid "FILENAME, DESCENDING" +msgstr "FICHIER, DESCENDANT" + +#: +msgid "RATING, ASCENDING" +msgstr "VOTES, ASCENDANT" + +#: +msgid "RATING, DESCENDING" +msgstr "VOTES, DESCENDANT" + +#: +msgid "TIMES PLAYED, ASCENDING" +msgstr "PARTIES JOUÉES, ASCENDANT" + +#: +msgid "TIMES PLAYED, DESCENDING" +msgstr "PARTIES JOUÉES, DESCENDANT" + +#: +msgid "LAST PLAYED, ASCENDING" +msgstr "DERNIÈRE PARTIE, ASCENDANT" + +#: +msgid "LAST PLAYED, DESCENDING" +msgstr "DERNIÈRE PARTIE, DESCENDANT" + +#: +msgid "WORKING..." +msgstr "EN COURS..." + +#: +msgid "CHANGE" +msgstr "CHANGER" + +#: +msgid "never" +msgstr "jamais" + +#: +msgid "just now" +msgstr "à l'instant" + +#: +msgid "%i sec ago" +msgid_plural "%i secs ago" +msgstr[0] "il y a %i sec" +msgstr[1] "il y a %i secs" + +#: +msgid "%i min ago" +msgid_plural "%i mins ago" +msgstr[0] "il y a %i min" +msgstr[1] "il y a %i mins" + +#: +msgid "%i hour ago" +msgid_plural "%i hours ago" +msgstr[0] "il y a %i heure" +msgstr[1] "il y a %i heures" + +#: +msgid "%i day ago" +msgid_plural "%i days ago" +msgstr[0] "il y a %i jour" +msgstr[1] "il y a %i jours" + +#: +msgid "unknown" +msgstr "inconnu" + +#: +msgid "SELECT ALL" +msgstr "TOUT SÉLECTIONNER" + +#: +msgid "SELECT NONE" +msgstr "AUCUN" + +#: +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i SÉLECTIONNÉ" +msgstr[1] "%i SÉLECTIONNÉS" + +#: +msgid "UP" +msgstr "HAUT" + +#: +msgid "DOWN" +msgstr "BAS" + +#: +msgid "LEFT" +msgstr "GAUCHE" + +#: +msgid "RIGHT" +msgstr "DROITE" + +#: +msgid "JOYSTICK 1 UP" +msgstr "JOYSTICK 1 HAUT" + +#: +msgid "JOYSTICK 1 LEFT" +msgstr "JOYSTICK 1 GAUCHE" + +#: +msgid "JOYSTICK 2 UP" +msgstr "JOYSTICK 2 HAUT" + +#: +msgid "JOYSTICK 2 LEFT" +msgstr "JOYSTICK 2 GAUCHE" + +#: +msgid "PAGE UP" +msgstr "L1 (PAGE PRECEDENTE)" + +#: +msgid "PAGE DOWN" +msgstr "R1 (PAGE SUIVANTE)" + +#: +msgid "HOTKEY" +msgstr "HOTKEY" + +#: +msgid "CONFIGURING" +msgstr "CONFIGURATION" + +#: +msgid "KEYBOARD" +msgstr "CLAVIER" + +#: +msgid "GAMEPAD %i" +msgstr "JOYSTICK %i" + +#. Config controllers missing translation +#: +msgid "PRESS ANYTHING" +msgstr "APPUYEZ SUR UN BOUTON" + +#: +msgid "ALREADY TAKEN" +msgstr "DÉJÀ PRIS" + +#: +msgid "DISCARD CHANGES" +msgstr "ANNULER LES MODIFICATIONS" + +#: +msgid "WELCOME" +msgstr "BIENVENUE" + +#: +msgid "CONFIGURE INPUT" +msgstr "CONFIGURER LES MANETTES" + +#: +msgid "%i GAMEPAD DETECTED" +msgid_plural "%i GAMEPADS DETECTED" +msgstr[0] "%i MANETTE DÉTECTÉE" +msgstr[1] "%i MANETTES DÉTECTÉES" + +#: +msgid "NO GAMEPADS DETECTED" +msgstr "AUCUNE MANETTE DÉTECTÉE" + +#: +msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." +msgstr "MAINTENEZ UN BOUTON SUR LA MANETTE POUR LA CONFIGURER" + +#: +msgid "PRESS F4 TO QUIT AT ANY TIME." +msgstr "APPUYEZ SUR F4 POUR QUITTER À TOUT MOMENT" + +#: +msgid "PRESS ESC OR THE HOTKEY TO CANCEL." +msgstr "APPUYEZ SUR ECHAP OU LA TOUCHE HOTKEY POUR ANNULER" + +#: +msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" +msgstr "VOULEZ-VOUS LANCER LE MEDIA CENTER KODI ?" + +#: +msgid "LOADING..." +msgstr "CHARGEMENT..." + +#: +msgid "PLEASE WAIT..." +msgstr "VEUILLEZ PATIENTER..." + +#: +msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" +msgstr "VOULEZ-VOUS VRAIMENT ÉTEINDRE SANS SAUVEGARDER LES METADATAS ?" + +#: +msgid "FAST SHUTDOWN SYSTEM" +msgstr "EXTINCTION RAPIDE" + +#: +msgid "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 RECALBOX.FR FOR MORE INFORMATION." +msgstr "Aucun système trouvé !\n" +"\n" +"Visitez le site recalbox.com pour plus d'infos !" + +#: +msgid "ON SCREEN KEYBOARD" +msgstr "CLAVIER VIRTUEL" + +#: +msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" +msgstr "APPUYEZ SUR SHIFT POUR OBTENIR LES MAJUSCULES" + +#: +msgid "SPACE" +msgstr "ESPACE" + +#: +msgid "DELETE A CHAR" +msgstr "EFFACER UN CARACTERE" + +#: +msgid "SHIFT" +msgstr "MAJUSCULE" + +#: +msgid "STOP EDITING" +msgstr "ARRETER L'EDITION" + +#: +msgid "MOVE CURSOR" +msgstr "BOUGER LE CURSEUR" + +#: +msgid "EDIT" +msgstr "EDITER" + +#: +msgid "ACCEPT RESULT" +msgstr "ACCEPTER LE RESULTAT" + +#: +msgid "FILENAME" +msgstr "FICHIER" + +#: +msgid "RATING" +msgstr "VOTES" + +#: +msgid "TIMES PLAYED" +msgstr "PARTIES JOUÉES" + +#: +msgid "LAST PLAYED" +msgstr "DERNIÈRE PARTIE" + +#: +msgid "NUMBER OF PLAYERS" +msgstr "NOMBRE DE JOUEURS" + +#: +msgid "DEVELOPER" +msgstr "DÉVELOPPEUR" + +#: +msgid "GENRE" +msgstr "GENRE" + +#: +msgid "SHOW HIDDEN" +msgstr "AFFICHER LES FICHIERS CACHÉS" + +#: +msgid "EXTREM (1400Mhz)" +msgstr "EXTRÊME (1400MHz)" + +#: +msgid "TURBO (1350Mhz)" +msgstr "TURBO (1350MHz)" + +#: +msgid "HIGH (1300Mhz)" +msgstr "HIGH (1300MHz)" + +#: +msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" +"IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." +msgstr "LES PRÉRÉGLAGES TURBO ET EXTRÊME PEUVENT PROVOQUER DES INSTABILITÉS DU SYSTÈME, UTILISEZ-LES À VOS PROPRES RISQUES.\n" +"SI VOUS CONTINUEZ, LE SYSTÈME REDÉMARRERA MAINTENANT." + +#: +msgid "%i GAME HIDDEN" +msgid_plural "%i GAMES HIDDEN" +msgstr[0] "%i JEU CACHÉ" +msgstr[1] "%i JEUX CACHÉS" + +#: +msgid "Start kodi media player." +msgstr "Démarrer le media player Kodi" + +#: +msgid "Select the language for your recalbox, select an external drive to store your games and configurations, check your current version and the free space on your drive" +msgstr "Choisissez la langue de votre Recalbox, choisissez un lecteur externe pour stocker vos jeux et vos configurations, vérifiez la version actuelle et l'espace libre sur votre disque" + +#: +msgid "Shows your current recalboxOS version." +msgstr "Montre la version actuelle de RecalboxOS." + +#: +msgid "Show how much space is used on your SHARE partition, located either on the SDCARD or on an external drive. The information shows how much GB are used and how much GB your storage has overall (example 13GB/26GB)." +msgstr "Montre l'espace utilisé sur la partition SHARE située sur la carte SD ou le lecteur externe. L'information montre combien de Go sont utilisés sur la capacité totale du support (exemple 13Go/26Go)." + +#: +msgid "Select an external drive to store your roms, saves, configurations etc.\n" +"Use a FAT32 formatted drive. The system does not format the drive. On first boot, with this option enabled, recalbox will create a '/recalbox' folder with all system files inside." +msgstr "Choisissez un lecteur externe pour stocker vos roms, sauvegardes, configurations etc.\n" +"Utilisez un lecteur formaté en FAT32. Le système ne formate pas le lecteur. Au premier démarrage, avec cette option activée, Recalbox créera un dossier '/recalbox' avec tous les fichiers système dedans." + +#: +msgid "Select your language. A reboot is needed to set this configuration active." +msgstr "Choisissez votre langue. Un redémarrage est nécessaire pour activer cette option." + +#: +msgid "Manage your recalbox updates. Select the update type. Activate update check." +msgstr "Gérez les mises à jour de Recalbox. Choisissez le type de mise à jour. Activez la vérification de mise à jour." + +#: +msgid "Check if an update is available, and start the update process." +msgstr "Vérifiez si une mise à jour est disponible et lancer le processus de mise à jour." + +#: +msgid "Stable updates will check for updates on stable recalbox releases. Stable updates are tested and approved by the recalbox team and their testers.\n" +"Unstable updates allows you to get the latest recalbox features by checking our unstable repository. You can test and validate with us the very last version of recalbox.\n" +"If you choose unstable update, be so kind to report issues on the recalbox-os issue board (https://github.com/recalbox/recalbox-os/issues)" +msgstr "Stable vérifiera les mises à jour parmi les sorties stables de Recalbox.\n" +"Les mises à jour stables sont testées et approuvées par l'équipe Recalbox et ses testeurs.\n" +"Les mise à jour unstable vous permettent de profiter des dernières fonctionnalités de Recalbox en vérifiant le dépôt unstable. Vous pouvez tester et valider avec nous la toute dernière version de Recalbox.\n" +"Si vous choisissez les mise à jour unstable, merci de signaler les problèmes sur le forum dédié (https://github.com/recalbox/recalbox-os/issues)." + +#: +msgid "Automatically check if an update is avaialble. If so, it notifies you with a message." +msgstr "Vérifier automatiquement si une mise à jour est disponible. Si tel est le cas, un message vous en informe." + +#: +msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." +msgstr "Configurez l'affichage des jeux, le ratio, les filtres (shaders), sauvgarde/chargement auto et votre compte retroachievement." + +#: +msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." +msgstr "Le ratio est le rapport entre la largeur et la hauteur de l'image. Utilisez AUTO pour laisser l'émulateur choisir le ratio original du jeu, cela vous procurera la meilleur expérience de retrogaming." + +#: +msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." +msgstr "Lisser l'image du jeu. Cette option rend l'image plus lisse en utilisant un filtre bi-linéaire." + +#: +msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." +msgstr "Cette option vous permet de rembobiner le jeu si vous êtes tué par un monstre ou si vous avez fait une erreur. Utilisez la commande HOTKEY + GAUCHE dans le jeu pour rembobiner." + +#: +msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." +msgstr "Sauvegardez automatiquement l'état actuel quand vous quittez un jeu et lancez automatiquement la dernière sauvegarde quand vous lancez un jeu." + +#: +msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." +msgstr "Integer scale est une échelle par facteur de nombre entier comme 2x, 3x, 4x, etc. Cette option met à l'échelle l'image jusqu'au plus grand facteur entier en dessous de la résolution. Par exemple,avec une résolution de 1920x1080 et integer scale activé, une image de 320x240 sera mis à l'échelle jusqu'à 1280x960 en laissant des bords noirs tout autour. Ceci pour maintenir un ratio de pixels de 1:1 avec le format d'image original pour que les pixels ne soit pas inégalement dupliqués." + +#: +msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." +msgstr "Les \"shaders\" sont des filtres pour le rendu du jeu. Vous pouvez choisir un \"shader set\" ici qui est une collection de \"shaders\" sélectionnés pour chaque système. Vous pouvez aussi changer de \"shader\" en cours de jeu avec HOTKEY + L2 ou HOTKEY + R2" + +#: +msgid "Enable or disable RetroAchievements in games." +msgstr "Activez ou désactivez RetroAchievements dans les jeux." + +#: +msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." +msgstr "Le mode Hardcore désactive *toutes* les fonctions rembobinage et sauvegarde dans l'émulateur: vous ne pourrez pas sauvegarder et charger n'importe quand. Vous devrez finir le jeu et débloquer les succès du premier coup, comme sur la console originale. En récompense, vous gagnerez à la fois les succès hardcore et standard, doublant ainsi vos points! Un jeu normal valant 400 points vaudra désormais 800 points si vous le finissez en hardcore!\n" +"Par exemple: si vous finissez un jeu pour 40 points, vous avez ensuite la possibilité d'obtenir 400 points de plus en hardcore." + +#: +msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" +msgstr "Le site web retroachievements.org propose des challenges/succès/trophées sur les système comme NES, SNES, GB, GBC, GBA, Megadrive, TurboGrafx16/PCEngine et plus! Créez un compte sur retroachievements.org et lancer vous dans la quête de succès!" + +#: +msgid "Add and configure up to 5 controllers." +msgstr "Ajoutez et configurez jusqu'à 5 manettes." + +#: +msgid "Pair a bluetooth controller with your recalbox. Your controller must be in pairing mode." +msgstr "Appairez une manette Bluetooth avec votre Recalbox. Votre manette doit être en mode appairage." + +#: +msgid "Forget all paired bluetooth controllers. You will have to pair your controllers again, but this option can help if you have issues to reconnect a controller, which is already paired." +msgstr "Oublier toutes les manettes appairées. Vous devrez appairer votre manette de nouveau mais cette option est utile si vous rencontrez des problèmes pour reconnecter une manette déjà appairée." + +#: +msgid "Configure your EmulationStation experience. Select transition types, help prompts, screensaver behavior. You can also deactivate the onscreen keyboard if you have a real keyboard plugged into your recalbox.\n" +"If you've added games since the last boot, you can also refresh the gamelist from this menu." +msgstr "Configurez votre expérience EmulationStation. Sélectionnez le type de transition, l'aide, le comportement de l'économiseur d'écran. Vous pouvez également désactiver le clavier virtuel si vous avez un clavier branché sur votre Recalbox.\n" +"Si vous avez ajouté des jeux depuis le dernier démarrage, vous pouvez aussi rafraîchir les liste de jeux depuis ce menu." + +#: +msgid "Start the screensaver after N minutes." +msgstr "Démarrer l'économiseur d'écran après N minutes." + +#: +msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." +msgstr "Choisissez le comportement de l'économiseur d'écran. DIM réduira la luminosité de l'écran, BLACK rendra l'écran noir." + +#: +msgid "Shows a help at the bottom of the screen which displays commands you can use." +msgstr "Montre l'aide en bas de l'écran qui vous indique les commandes que vous pouvez utiliser." + +#: +msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." +msgstr "Si activé, vous pouvez changer de système tout en parcourant les listes de jeux en appuyant sur GAUCHE ou DROITE." + +#: +msgid "The onscreen keyboard is necessary to type text if you only have controllers plugged into your recalbox. You can disable it if you have a real keyboard connected." +msgstr "Le clavier virtuel est nécessaire pour taper du texte si vous n'avez que des manettes branchées sur votre Recalbox. Vous pouvez désactiver cette option si vous avez un clavier branché." + +#: +msgid "Select a theme for your recalbox." +msgstr "Sélectionnez un thème pour votre Recalbox." + +#: +msgid "Updates the gamelists, if you added games since the last boot." +msgstr "Mettez à jour les listes de jeux si vous avez ajouté des jeux depuis le dernier démarrage." + +#: +msgid "Configure the sound options of your recalbox." +msgstr "Configurez les options sonores de votre Recalbox." + +#: +msgid "Set the volume of the sound output for the frontend and the games." +msgstr "Réglez le volume de la sortie audio pour EmulationStation et les jeux." + +#: +msgid "Enable or disable the frontend music. You can add your own music as mp3, or ogg format in the 'musics' directory of your recalbox." +msgstr "Activez ou désactivez la musique dans EmulationStation. Vous pouvez ajouter vos propres musiques au format mp3 ou ogg dans le dossier 'musics' de votre Recalbox." + +#: +msgid "Select your output device. Only HDMI and JACK are supported." +msgstr "Sélectionnez votre périphérique de sortie. Seuls HDMI et JACK sont supportés." + +#: +msgid "Configure the network options of your recalbox.\n" +"Check your network status and IP address, set the hostname and configure the WIFI." +msgstr "Configurez les options réseau de votre recalbox. Vérifiez le statut de connexion et l'adresse IP, choisissez le nom réseau et configurez le WIFI." + +#: +msgid "Displays CONNECTED, if you are connected, by checking if your recalbox can access the recalbox.com update server." +msgstr "Affiche CONNECTÉ si vous êtes connecté en vérifiant si votre Recalbox peut accéder au serveur de mise à jour recalbox.com. " + +#: +msgid "The IP address of your recalbox within your local network." +msgstr "L'adresse IP de votre Recalbox dans votre réseau local." + +#: +msgid "Enable or disable WIFI.\n" +"If you disable WIFI, the SSID and the WIFI passwords are saved and can be used when you reactivate it" +msgstr "Activez ou désactivez le WIFI.\n" +"Si vous désactivez le WIFI, le SSID et la clé WIFI sont sauvegardés et réutilisés quand vous réactivez l'option." + +#: +msgid "The name of your recalbox in your local network" +msgstr "Le nom de votre Recalbox sur le réseau local." + +#: +msgid "SSID (WIFI Name) of your network." +msgstr "SSID (Nom WIFI) de votre réseau." + +#: +msgid "Private key of your WIFI network." +msgstr "Clé de sécurité de votre réseau WIFI." + +#: +msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." +msgstr "Récupérez des informations et visuels pour vos jeux. Le scraper télécharge les metadatas et images pour vos jeux depuis différents serveurs et améliore complètement l'expérience d'EmulationStation." + +#: +msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." +msgstr "Sélectionnez un serveur depuis lequel scraper. Le serveur SCREENSCRAPER est recommandé et est basé sur www.screenscraper.fr. Il scrape les données de jeu dans votre langue, si disponible." + +#: +msgid "Begin the scrape process with the configuration shown below." +msgstr "Démarrez le processus de scrape avec la configuration ci-dessous." + +#: +msgid "Scrape and display game ratings." +msgstr "Scrape et affiche la notation des jeux." + +#: +msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." +msgstr "Options avancées. Assurez-vous de savoir ce que vous faîtes avant de changer les valeurs dans ce menu." + +#: +msgid "Overclock your board to increase the performance.\n" +"Overclock settings are tested and validated by the community. Keep in mind that overclocking your board can void your warranty." +msgstr "Overclockez votre carte pour augmenter les performances.\n" +"Les paramètres d'overclocking sont testés et validés par la communauté.\n" +"Gardez à l'esprit qu'overclocker votre carte peut annuler votre garantie." + +#: +msgid "Select which system to show when the recalbox frontend starts. The default value is 'favorites'." +msgstr "Choisissez quel système montrer quand EmulationStation démarre.\n" +"La valeur par défaut est 'favorites'." + +#: +msgid "On boot, recalbox will show the list of games of the selected system rather than the system view." +msgstr "Au démarrage, Recalbox montrera la liste de jeux du système sélectionné plutôt que la vue systèmes." + +#: +msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" +"This option highly speeds up boot time, but new games will not be detected." +msgstr "Montrer seulement les jeux présent dans le fichier gamelist.xml (situé dans le dossier des roms).\n" +"Cette option accélère grandement le temps de démarrage mais les nouveaux jeux ne seront pas détectés." + +#: +msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." +msgstr "Cette option vous permet de mettre le système sélectionné en mode fixe.\n" +"Avec cette option activé, l'utilisateur ne peut pas accéder aux autres systèmes." + +#: +msgid "Always display the basic gamelist view, even if you have scraped your games." +msgstr "Toujours afficher la vue liste de jeux basique, même si vous avez scrappé vos jeux." + +#: +msgid "Override global options like emulator, core, ratio and more for each available system in your recalbox." +msgstr "Passer outre les options globales comme émulateur, core, ratio et plus pour chaque système disponible dans votre Recalbox." + +#: +msgid "Configure boot options that make your recalbox boot straight into a system or into Kodi, lock a user to a single system, or directly show the gamelist." +msgstr "Configurez les options de démarrage qui font démarrer votre Recalbox directement dans un système ou dans Kodi, bloquer un utilisateur dans un seul système ou montrer directement la liste de jeux." + +#: +msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" +msgstr "Activez ou désactivez Kodi, configurez le démarrage de Kodi, activez le bouton X pour démarrer Kodi." + +#: +msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." +msgstr "Activez ou désactivez Kodi. Si Kodi est désactivé, vous ne pourrez pas le lancer avec le bouton X ou le lancer automatiquement au démarrage. L'option du menu sera aussi retirée." + +#: +msgid "Use the X button to start Kodi." +msgstr "Utilisez le bouton X pour démarrer Kodi." + +#: +msgid "Automatically start into Kodi on boot." +msgstr "Lancer directement Kodi au démarrage." + +#: +msgid "Manage your recalbox security." +msgstr "Gérez la sécurité de votre Recalbox." + +#: +msgid "Change the SSH root password." +msgstr "Changez le mot de passe root SSH." + +#. not sure 'bout this one +#: +msgid "Enforce recalbox security." +msgstr "Renforcer la sécurité de Recalbox." + +#: +msgid "Enable or disable overscan.\n" +"Overscan can help you, if you have a black border, or if the image is bigger than your screen. Before setting the overscan, try to configure your TV to have a 1:1 pixel output.\n" +"More overscan settings can be defined in the boot.txt file, available when you plug your SD card into your computer." +msgstr "Activez ou désactivez l'overscan.\n" +"L'overscan peut vous aider si vous avez des bandes noires ou si l'image est plus grande que l'écran. Avant de régler l'overscan, essayez de régler votre TV pour avoir une sortie 1:1.\n" +"D'autres réglages d'overscan peuvent être définis dans le fichier boot.txt disponible lorsque vous branchez votre carte SD sur un ordinateur." + +#: +msgid "Show the framerate in EmulationStation and in game." +msgstr "Affichez le taux de rafraîchissement dans EmulationStation et dans les jeux." + +#: +msgid "Enable or disable the Recalbox Manager.\n" +"The Recalbox Manager is a web application available on http://recalbox , if you are on windows, http://recalbox.local , if you are on Linux or Mac, or directly with your recalbox IP : http://192.168.1.XX.\n" +"You can configure many options from within the manager, and even manage games, saves, and scrapes!" +msgstr "Activez ou désactivez le Gestionnaire Recalbox.\n" +"Le Gestionnaire Recalbox est une application web disponible sur http://recalbox si vous êtes sous Windows, http://recalbox.local si vous êtes sous Linux ou Mac ou directement avec l'adresse IP de la Recalbox: http://192.168.1.XX.\n" +"Vous pouvez configurer de nombreuses options dans le Gestionnaire et même gérer les jeux, les sauvegardes et les scraps!" + +#: +msgid "Enable or disable the recalbox API.\n" +"The Recalbox API is a REST API exposing endpoints to control your recalbox via http requests." +msgstr "Activez ou désactivez l'API Recalbox.\n" +"L'API Recalbox est une API REST permettant de contrôler votre Recalbox avec des requêtes http." + +#: +msgid "Select which emulator to use when you start a game for this system." +msgstr "Selectionnez quel émulateur utiliser quand vous lancer un jeu pour ce système." + +#: +msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." +msgstr "Selectionnez quel core utiliser pour l'émulateur sélectionné. Par exemple, l'émulateur LIBRETRO a plusieurs core pour les jeux Super Nintendo. Le core par défaut que vous choisissez peut aussi être outrepassé par les réglages spécifiques par jeu." + +#: +msgid "USE COMPOSED VISUALS" +msgstr "UTILISER DES IMAGES COMPOSÉES" + +#: +msgid "CHECK UPDATES" +msgstr "VÉRIFIER LES MISES À JOUR" + +#: +msgid "UPDATE TYPE" +msgstr "TYPE DE MISE À JOUR" + +#: +msgid "INTEGER SCALE (PIXEL PERFECT)" +msgstr "INTEGER SCALE (PIXEL PERFECT)" + +#: +msgid "ADVANCED SETTINGS" +msgstr "PARAMÈTRES AVANCÉS" + +#: +msgid "BOOT SETTINGS" +msgstr "PARAMÈTRES DE DÉMARRAGE" + +#: +msgid "GAMELIST ONLY" +msgstr "LISTE DE JEUX UNIQUEMENT" + +#: +msgid "START ON SYSTEM" +msgstr "DÉMARRER SUR UN SYSTÈME" + +#: +msgid "BOOT ON GAMELIST" +msgstr "DÉMARRER SUR LA LISTE DE JEUX" + +#: +msgid "HIDE SYSTEM VIEW" +msgstr "CACHER LA VUE SYSTÈME" + +#: +msgid "EMULATOR ADVANCED CONFIGURATION" +msgstr "CONFIGURATION AVANCÉE DE L'ÉMULATEUR" + +#: +msgid "ADVANCED EMULATOR CONFIGURATION" +msgstr "CONFIGURATION AVANCÉE DE L'ÉMULATEUR" + +#: +msgid "HELP" +msgstr "AIDE" + +#: +msgid "THE SYSTEM IS UP TO DATE" +msgstr "LE SYSTÈME EST À JOUR" + +#: +msgid "FORCE BASIC GAMELIST VIEW" +msgstr "FORCER LA VUE LISTE DE JEUX BASIQUE" + +#: +msgid "DOWNLOADED" +msgstr "TÉLÉCHARGÉ" + +#: +msgid "UPDATE VERSION:" +msgstr "VERSION MISE ­À JOUR:" + +#: +msgid "UPDATE CHANGELOG:" +msgstr "CHANGELOG DE LA MISE À JOUR:" + +#: +msgid "MORE DETAILS" +msgstr "PLUS DE DÉTAILS" + +#: +msgid "CAROUSEL TRANSITIONS" +msgstr "ANIMATION DU BANDEAU" + +#: +msgid "ENABLE FILTERS" +msgstr "FILTRES" + +#: +msgid "THEME CONFIGURATION" +msgstr "CONFIGURATION DU THÈME" + +#: +msgid "THEME COLORSET" +msgstr "COLORSET DU THÈME" + +#: +msgid "THEME ICONSET" +msgstr "ICONSET DU THÈME" + +#: +msgid "THEME MENU" +msgstr "MENU DU THÈME" + +#: +msgid "THEME SYSTEMVIEW" +msgstr "SYSTEMVIEW DU THÈME" + +#: +msgid "THEME GAMELISTVIEW" +msgstr "GAMELISTVIEW DU THÈME" + +#: +msgid "THEME REGION" +msgstr "RÉGION DU THÈME" + +#: +msgid "THIS THEME HAS NO OPTION" +msgstr "CE THÈME N'A PAS D'OPTIONS" + +#: +msgid "MANUAL INPUT" +msgstr "SAISIE MANUELLE" + +#: +msgid "AN ERROR OCCURED - DOWNLOADED" +msgstr "UNE ERREUR EST SURVENUE - TÉLÉCHARGÉ" + +#: +msgid "START KODI" +msgstr "DÉMARRER KODI" + +#: +msgid "Shows the current available update version." +msgstr "Montre les versions disponibles." + +#: +msgid "Shows the current available update changelog." +msgstr "Montre le changelog de la mise à jour disponible." + +#: +msgid "Configure an associated controller. Your controller has to be associated / plugged before." +msgstr "Configurer une manette appairée. Votre manette doit être appairée / branchée auparavant." + +#: +msgid "Choose if carousel will be animated or not during transitions" +msgstr "Choisissez si le bandeau sera animé ou non durant les transitions." + +#: +msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" +msgstr "Sélectionnez le type de transition qui se produit lorsque vous lancez un jeu. FADE fera un fondu au noir et SLIDE zoomera sur l'image du jeu (ou le nom si il n'y a pas de metadatas)." + +#: +msgid "Select exisiting colorset options for this theme." +msgstr "Sélectionnez un colorset disponible pour votre thème" + +#: +msgid "Select exisiting iconset options for this theme." +msgstr "Sélectionnez un iconset disponible pour votre thème" + +#: +msgid "Select exisiting menu style options for this theme." +msgstr "Sélectionnez un style de menu disponible pour votre thème" + +#: +msgid "Select exisiting system view options for this theme." +msgstr "Sélectionnez une vue système disponible pour votre thème" + +#: +msgid "Select exisiting gamelist view options for this theme." +msgstr "Sélectionnez une vue gamelist disponible pour votre thème" + +#: +msgid "Configure theme options if available." +msgstr "Configurez les options du thème si disponibles" + +#: +msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" +msgstr "Sélectionnez une région des logos et images pour les systèmes qui sont différents selon les pays. Ex: Megadrive en Europe / Genesis aux USA" + +#: +msgid "Type the name of your SSID if it is hidden or not listed" +msgstr "Saisissez le nom de votre SSID si il n'est pas dans la liste ou caché" + +#: +msgid "Select a letter and the listing will go directly on the first game starting with this letter." +msgstr "Sélectionnez une lettre et le listing ira directement sur le 1er jeu commençant par cette lettre." + +#: +msgid "Select the way the game list is sortered (alphabetically, by notation...)." +msgstr "Sélectionnez la façon dont la liste de jeux est triée (alphabétique, par note...)." + +#: +msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." +msgstr "Changer entre rendre visible ou non les jeux favoris. Pour ajouter un jeu dans les favoris, sélectionnez le jeu et changer son statut en utilisant 'Y'." + +#: +msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." +msgstr "Changer entre rendre visible ou non les jeux cachés. pour cacher un jeu, éditez ses données et sélectionnez 'Cacher'." + +#: +msgid "This option display a menu which allows to change game data and many others options." +msgstr "Cette option affiche un menu qui permet de changer les données du jeu et de nombreuses autres options." + +#: +msgid "AVAILABLE UPDATE" +msgstr "MISE À JOUR DISPONIBLE" + +#: +msgid "UPDATE CHANGELOG" +msgstr "CHANGELOG DE LA MISE À JOUR" + +#: +msgid "CLOCK IN MENU" +msgstr "HORLOGE DANS LE MENU" + +#: +msgid "Now playing" +msgstr "Lecture en cours" + +#: +msgid "DEFAULT (%1%)" +msgstr "DÉFAUT (%1%)" + +#: +msgid "INPUT REQUIRED" +msgstr "ENTRÉE NÉCESSAIRE" + +#: +msgid "(skipped)" +msgstr "(passé)" + +#: +msgid "UP/DOWN TO SKIP" +msgstr "HAUT/BAS POUR PASSER" + +#: +msgid "A TO UNSET" +msgstr "A POUR ANNULER" + +#: +msgid "DOWN TO SKIP AND KEEP [%1%]" +msgstr "BAS POUR PASSER ET GARDER [%1%]" + +#: +msgid "UP/DOWN TO SKIP AND KEEP [%1%]" +msgstr "HAUT/BAS POUR PASSER ET GARDER [%1%]" + +#: +msgid "Set duration of help popups, 0 means no popup." +msgstr "Régler la durée des popups d'aide, 0 pour désactiver." + +#: +msgid "HELP POPUP DURATION" +msgstr "DURÉE DES POPUPS D'AIDE" + +#: +msgid "Set duration of music popups, 0 means no popup." +msgstr "Régler la durée des popups de musique, 0 pour désactiver." + +#: +msgid "MUSIC POPUP DURATION" +msgstr "DURÉE DES POPUPS DE MUSIQUE" + +#: +msgid "POPUP SETTINGS" +msgstr "RÉGLAGE DES POPUPS" + +#: +msgid "POPUP POSITION" +msgstr "POSITION DES POPUPS" + +#: +msgid "Select the position of popups on screen." +msgstr "Choisissez la position des popups sur l'écran" + +#: +msgid "Set position and duration of popups." +msgstr "Réglez la position et la durée des popups" + +#: +msgid "TOP/RIGHT" +msgstr "HAUT/DROITE" + +#: +msgid "BOTTOM/RIGHT" +msgstr "BAS/DROITE" + +#: +msgid "BOTTOM/LEFT" +msgstr "BAS/GAUCHE" + +#: +msgid "TOP/LEFT" +msgstr "HAUT/GAUCHE" + +#: +msgid "SHOW FOLDERS CONTENT" +msgstr "MONTRER LE CONTENU DES DOSSIERS" + +#: +msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." +msgstr "Basculer entre l'affichage classique de répertoires et l'affichage de tous les jeux en une seule liste." + +#: +msgid "NETPLAY" +msgstr "NETPLAY" + +#: +msgid "NETPLAY SETTINGS" +msgstr "OPTIONS NETPLAY" + +#: +msgid "NETPLAY LOBBY" +msgstr "LOBBY NETPLAY" + +#: +msgid "Enable or disable Netplay in games." +msgstr "Activer ou désactiver Netplay dans les jeux." + +#: +msgid "PORT" +msgstr "PORT" + +#: +msgid "NICKNAME" +msgstr "SURNOM" + +#: +msgid "RELAY SERVER" +msgstr "SERVEUR DE RELAI" + +#: +msgid "Enable or disable connections throught relay servers." +msgstr "Activer ou désactiver les connexions au travers de serveurs de relai." + +#: +msgid "KODI/NETPLAY" +msgstr "KODI/NETPLAY" + +#: +msgid "NO GAMES OR NO CONNECTION" +msgstr "PAS DE JEUX OU PAS DE CONNEXION" + +#: +msgid "HASH NOW" +msgstr "HASHER MAINTENANT" + +#: +msgid "HASH THESE SYSTEMS" +msgstr "HASHER CES SYSTÈMES" + +#: +msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." +msgstr "Ajoutez les hashs des roms dans vos gamelists pour avoir un résultat plus précis en Netplay." + +#: +msgid "HASH ROMS" +msgstr "HASHER LES ROMS" + +#: +msgid "Only missing hashs" +msgstr "Seulement hashs manquants" + +#: +msgid "Username" +msgstr "Nom" + +#: +msgid "Country" +msgstr "Pays" + +#: +msgid "Latency" +msgstr "Latence" + +#: +msgid "Host arch." +msgstr "Arch. de l'hôte" + +#: +msgid "Core ver." +msgstr "Ver. du core" + +#: +msgid "RA ver." +msgstr "Ver. de RA" + +#: +msgid "Can join" +msgstr "Joignable" + +#: +msgid "Rom and core match" +msgstr "Corres. rom et core" + +#: +msgid "Rom found" +msgstr "Rom trouvée" + +#: +msgid "No rom match" +msgstr "Pas de corres. de rom" + +#: +msgid "Match" +msgstr "Correspondance" + +#: +msgid "No Match" +msgstr "Pas de corres." + +#: +msgid "Rom file" +msgstr "Fichier rom" + +#: +msgid "Rom hash" +msgstr "Hash de la rom" + +#: +msgid "THIS COULD TAKE A WHILE, CONFIRM?" +msgstr "CELA PEUX PRENDRE LONGTEMPS, CONFIRMER?" + +#: +msgid "good" +msgstr "bon" + +#: +msgid "bad" +msgstr "mauvais" + +#: +msgid "medium" +msgstr "moyen" + +#: +msgid "NETPLAY POPUP DURATION" +msgstr "DURÉE DES POPUPS NETPLAY" + +#: +msgid "Set duration of netplay popups, 0 means no popup." +msgstr "Régler la durée des popups Netplay, 0 pour désactiver." + +#: +msgid "Player" +msgstr "Joueur" + +#: +msgid "Game" +msgstr "Jeu" + +#: +msgid "A Recalbox friend has started a Netplay game!" +msgstr "Un ami Recalbox a démarré une partie Netplay!" + +#: +msgid "Play online on games running through Retroarch like NES, SNES, FBA, Genesis/Megadrive and more!" +msgstr "Jouez en ligne à des jeux tournant sous Retroarch comme NES, SNES, FBA, Megadrive et bien plus!" + +#: +msgid "Rom, hash and core match" +msgstr "Corres. rom, hash et core" + +#: +msgid "No core match" +msgstr "Pas de corres. du core" + +#: +msgid "Add a clock in the main menu." +msgstr "Ajouter une horloge dans le menu principal" + +#: +msgid "UPGRADING" +msgstr "Mise à jour" + +#: +msgid "PREPARING" +msgstr "Préparation" + +#: +msgid "Starting UI" +msgstr "Initialisation de l'interface" + +#: +msgid "VERIFYING" +msgstr "Verification" + +#: +msgid "EMPTY LIST" +msgstr "Liste vide" + + +#: Retroarch ratio +msgid "Auto" +msgstr "Automatique" + +msgid "Square pixel" +msgstr "Pixel carré" + +msgid "Retroarch Config" +msgstr "Retroarch mode 'Config'" + +msgid "Retroarch Custom" +msgstr "Retroarch mode 'Custom'" + +msgid "Core provided" +msgstr "Fourni par le Core" + +msgid "Do not set" +msgstr "Ne pas toucher" \ No newline at end of file diff --git a/resources/locale/it/emulationstation2.po b/resources/locale/it/emulationstation2.po new file mode 100644 index 0000000000..f74c4c50b7 --- /dev/null +++ b/resources/locale/it/emulationstation2.po @@ -0,0 +1,1884 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: recalbox-emulationstation\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: +msgid "AN UPDATE IS AVAILABLE FOR YOUR RECALBOX" +msgstr "HAY UNA ACTUALIZACIÓN DISPONIBLE PARA RECALBOX" + +#: +msgid "CANCEL" +msgstr "CANCELAR" + +#: +msgid "Rating" +msgstr "Calificación" + +#: +msgid "Released" +msgstr "Lanzado" + +#: +msgid "Developer" +msgstr "Desarrollador" + +#: +msgid "Publisher" +msgstr "Editor" + +#: +msgid "Genre" +msgstr "Género" + +#: +msgid "Players" +msgstr "Jugadores" + +#: +msgid "NO GAMES FOUND - SKIP" +msgstr "JUEGOS NO ENCONTRADOS - OMITIR" + +#: +msgid "RETRY" +msgstr "REINTENTAR" + +#: +msgid "SKIP" +msgstr "OMITIR" + +#: +msgid "SEARCH FOR" +msgstr "BUSCAR POR" + +#: +msgid "SEARCH" +msgstr "BUSCAR" + +#: +msgid "SCRAPING IN PROGRESS" +msgstr "OBTENCIÓN DE DATOS EN CURSO" + +#: +msgid "SYSTEM" +msgstr "SISTEMA" + +#: +msgid "subtitle text" +msgstr "subtítulo" + +#: +msgid "INPUT" +msgstr "ENTRADA" + +#: +msgid "search" +msgstr "buscar" + +#: +msgid "STOP" +msgstr "DETENER" + +#: +msgid "stop (progress saved)" +msgstr "detener (guardar progreso)" + +#: +msgid "GAME %i OF %i" +msgstr "JUEGO %i DE %i" + +#: +msgid "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 RECALBOX.COM FOR MORE INFORMATION." +msgstr "¡NO SE HAN ENCONTRADO SISTEMAS!\n" +"COMPROBAR RUTAS EN ARCHIVO DE CONFIGURACIÓN.\n" +"LA CARPETA DE JUEGOS DEBE CONTENER AL MENOS UNO Y CON LA EXTENSIÓN CORRECTA.\n" +"\n" +"VISITAR RECALBOX.COM PARA OBTENER MÃS INFORMACIÓN" + +#: +msgid "%i GAME SUCCESSFULLY SCRAPED!" +msgid_plural "%i GAMES SUCCESSFULLY SCRAPED!" +msgstr[0] "¡INFORMACIÓN DE %i JUEGO OBTENIDA CORRECTAMENTE!" +msgstr[1] "¡INFORMACIÓN DE %i JUEGOS OBTENIDA CORRECTAMENTE!" + +#: +msgid "%i GAME SKIPPED." +msgid_plural "%i GAMES SKIPPED." +msgstr[0] "%i JUEGO OMITIDO" +msgstr[1] "%i JUEGOS OMITIDOS" + +#: +msgid "OK" +msgstr "OK" + +#: +msgid "EDIT METADATA" +msgstr "EDITAR METADATOS" + +#: +msgid "SCRAPE" +msgstr "OBTENER INFORMACIÓN" + +#: +msgid "SAVE" +msgstr "GUARDAR" + +#: +msgid "THIS WILL DELETE A FILE!\n" +"ARE YOU SURE?" +msgstr "¿SEGURO DESEA BORRAR EL ARCHIVO?" + +#: +msgid "YES" +msgstr "SÃ" + +#: +msgid "NO" +msgstr "NO" + +#: +msgid "DELETE" +msgstr "BORRAR" + +#: +msgid "SAVE CHANGES?" +msgstr "¿GUARDAR CAMBIOS?" + +#: +msgid "BACK" +msgstr "VOLVER" + +#: +msgid "CLOSE" +msgstr "CERRAR" + +#: +msgid "MAIN MENU" +msgstr "MENÚ PRINCIPAL" + +#: +msgid "KODI MEDIA CENTER" +msgstr "KODI MEDIA CENTER" + +#: +msgid "SYSTEM SETTINGS" +msgstr "CONFIGURACIÓN DE SISTEMA" + +#: +msgid "VERSION" +msgstr "VERSIÓN" + +#: +msgid "DISK USAGE" +msgstr "USO DE DISCO" + +#: +msgid "STORAGE DEVICE" +msgstr "ALMACENAMIENTO" + +#: +msgid "LANGUAGE" +msgstr "IDIOMA" + +#: +msgid "OVERCLOCK" +msgstr "OVERCLOCK" + +#: +msgid "EXTREM (1100Mhz)" +msgstr "EXTREMO (1100Mhz)" + +#: +msgid "TURBO (1000Mhz)" +msgstr "TURBO (1000Mhz)" + +#: +msgid "HIGH (950Mhz)" +msgstr "ALTO (950Mhz)" + +#: +msgid "NONE (700Mhz)" +msgstr "NO (700Mhz)" + +#: +msgid "TURBO (1050Mhz)+" +msgstr "TURBO (1050Mhz)+" + +#: +msgid "HIGH (1050Mhz)" +msgstr "ALTO (1050Mhz)" + +#: +msgid "NONE (900Mhz)" +msgstr "NO (900Mhz)" + +#: +msgid "NONE (1200Mhz)" +msgstr "NO (1200Mhz)" + +#: +msgid "NONE" +msgstr "NO" + +#. NEW SETTINGS ORGANIZATION +#: +msgid "UPDATES" +msgstr "ACTUALIZACIONES" + +#: +msgid "AUTO UPDATES" +msgstr "ACTUALIZACIONES AUTOMÃTICAS" + +#: +msgid "START UPDATE" +msgstr "COMENZAR ACTUALIZACIÓN" + +#: +msgid "KODI SETTINGS" +msgstr "CONFIGURACIÓN DE KODI" + +#: +msgid "ENABLE KODI" +msgstr "ACTIVAR KODI" + +#: +msgid "KODI AT START" +msgstr "INICIAR KODI AL ARRANCAR" + +#: +msgid "START KODI WITH X" +msgstr "PULSAR X PARA INICIAR KODI" + +#: +msgid "SECURITY" +msgstr "SEGURIDAD" + +#: +msgid "ENFORCE SECURITY" +msgstr "REFORZAR SEGURIDAD" + +#: +msgid "ROOT PASSWORD" +msgstr "CONTRASEÑA ROOT" + +#: +msgid "THE SYSTEM WILL NOW REBOOT" +msgstr "EL SISTEMA SERà REINICIADO" + +#: +msgid "GAMES SETTINGS" +msgstr "CONFIGURACIÓN DE JUEGOS" + +#: +msgid "GAME RATIO" +msgstr "RELACIÓN DE ASPECTO" + +#: +msgid "SMOOTH GAMES" +msgstr "SUAVIZAR JUEGOS" + +#: +msgid "REWIND" +msgstr "REBOBINAR" + +#: +msgid "AUTO SAVE/LOAD" +msgstr "GUARDAR/CARGAR AUTOMÃTICAMENTE" + +#: +msgid "SHADERS SET" +msgstr "CONJUNTO DE SHADERS" + +#. Yo dejaría en español SCANLINES, ya que en españa también se refiere a este filtro como SCANLINES, y no como LINEAS.. +#: +msgid "SCANLINES" +msgstr "SCANLINES" + +#: +msgid "RETRO" +msgstr "RETRO" + +#: +msgid "RETROACHIEVEMENTS SETTINGS" +msgstr "CONFIGURACIÓN DE RETROLOGROS" + +#: +msgid "RETROACHIEVEMENTS" +msgstr "RETROLOGROS" + +#: +msgid "HARDCORE MODE" +msgstr "MODO HARDCORE" + +#: +msgid "USERNAME" +msgstr "USUARIO" + +#: +msgid "PASSWORD" +msgstr "CONTRASEÑA" + +#: +msgid "ADVANCED" +msgstr "AVANZADO" + +#: +msgid "REALLY UPDATE GAMES LISTS ?" +msgstr "¿ACTUALIZAR LISTA DE JUEGOS?" + +#: +msgid "UPDATE GAMES LISTS" +msgstr "ACTUALIZAR LISTA DE JUEGOS" + +#: +msgid "CONTROLLERS SETTINGS" +msgstr "CONFIGURACIÓN DE MANDOS" + +#: +msgid "UI SETTINGS" +msgstr "CONFIGURACIÓN DE INTERFAZ" + +#: +msgid "OVERSCAN" +msgstr "OVERSCAN" + +#: +msgid "SCREENSAVER AFTER" +msgstr "SALVAPANTALLA A LOS" + +#: +msgid "TRANSITION STYLE" +msgstr "TIPO DE TRANSICIÓN" + +#: +msgid "SCREENSAVER BEHAVIOR" +msgstr "COMPORTAMIENTO DE SALVAPANTALLA" + +#: +msgid "SHOW FRAMERATE" +msgstr "MOSTRAR TASA DE REFRESCO" + +#: +msgid "ON-SCREEN HELP" +msgstr "AYUDA EN PANTALLA" + +#: +msgid "QUICK SYSTEM SELECT" +msgstr "SELECCIÓN RÃPIDA DE SISTEMA" + +#: +msgid "THEME SET" +msgstr "TEMA" + +#: +msgid "SOUND SETTINGS" +msgstr "CONFIGURACIÓN DE AUDIO" + +#: +msgid "SYSTEM VOLUME" +msgstr "VOLUMEN DE SISTEMA" + +#: +msgid "FRONTEND MUSIC" +msgstr "MÚSICA DE FONDO" + +#: +msgid "OUTPUT DEVICE" +msgstr "SALIDA DE AUDIO" + +#: +msgid "HDMI" +msgstr "HDMI" + +#: +msgid "JACK" +msgstr "JACK" + +#: +msgid "AUTO" +msgstr "AUTO" + +#: +msgid "NETWORK SETTINGS" +msgstr "CONFIGURACIÓN DE RED" + +#: +msgid "CONNECTED" +msgstr "CONECTADO" + +#: +msgid "NOT CONNECTED" +msgstr "NO CONECTADO" + +#: +msgid "STATUS" +msgstr "ESTADO" + +#: +msgid "IP ADDRESS" +msgstr "DIRECCIÓN IP" + +#: +msgid "HOSTNAME" +msgstr "NOMBRE DE HOST" + +#: +msgid "ENABLE WIFI" +msgstr "ACTIVAR WIFI" + +#: +msgid "WIFI SSID" +msgstr "SSID WIFI" + +#: +msgid "WIFI KEY" +msgstr "CLAVE WIFI" + +#: +msgid "WIFI ENABLED" +msgstr "WIFI ACTIVADO" + +#: +msgid "WIFI CONFIGURATION ERROR" +msgstr "ERROR DE CONFIGURACIÓN WIFI" + +#: +msgid "SCRAPER" +msgstr "SCRAPER" + +#: +msgid "SCRAPE FROM" +msgstr "OBTENER DATOS DE" + +#: +msgid "SCRAPE RATINGS" +msgstr "OBTENER CALIFICACIONES" + +#: +msgid "SCRAPE NOW" +msgstr "OBTENER DATOS AHORA" + +#: +msgid "QUIT" +msgstr "SALIR" + +#: +msgid "REALLY RESTART?" +msgstr "¿REINICIAR?" + +#: +msgid "RESTART SYSTEM" +msgstr "REINICIAR SISTEMA" + +#: +msgid "REALLY SHUTDOWN?" +msgstr "¿APAGAR?" + +#: +msgid "SHUTDOWN SYSTEM" +msgstr "APAGAR SISTEMA" + +#: +msgid "Emulator" +msgstr "Emulador" + +#: +msgid "Core" +msgstr "Núcleo" + +#: +msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." +msgstr "SI SÓLO EXISTE UN MANDO, CONFIGURAR LAS DIRECCIONES Y OMITIR LA CONFIGURACIÓN, MANTENIENDO PRESIONADO UN BOTÓN.\n" +"SI CARECE DE BOTÓN PARA HOTKEY, UTILIZAR BOTÓN SELECT.\n" +"OMITIR BOTONES FALTANTES, MANTENIENDO PRESIONADO CUALQUIER OTRO BOTÓN.\n" +"LOS NOMBRES DE LOS BOTONES SE BASAN EN EL MANDO DE SNES." + +#. GUIMENU +#: +msgid "CONFIGURE A CONTROLLER" +msgstr "CONFIGURAR UN MANDO" + +#. Bluetooth +#: +msgid "CONTROLLER PAIRED" +msgstr "MANDO VINCULADO" + +#: +msgid "UNABLE TO PAIR CONTROLLER" +msgstr "IMPOSIBLE VINCULAR MANDO" + +#: +msgid "AN ERROR OCCURED" +msgstr "ERROR" + +#: +msgid "NO CONTROLLERS FOUND" +msgstr "NINGÚN MANDO DETECTADO" + +#: +msgid "PAIR A BLUETOOTH CONTROLLER" +msgstr "VINCULAR MANDO BLUETOOTH" + +#: +msgid "CONTROLLERS LINKS HAVE BEEN DELETED." +msgstr "MANDOS VINCULADOS BORRADOS" + +#: +msgid "FORGET BLUETOOTH CONTROLLERS" +msgstr "DESVINCULAR MANDOS BLUETOOTH" + +#: +msgid "INPUT P%i" +msgstr "ENTRADA P%i" + +#: +msgid "CHOOSE" +msgstr "SELECCIONAR" + +#: +msgid "SELECT" +msgstr "SELECCIONAR" + +#: +msgid "OPTIONS" +msgstr "OPCIONES" + +#: +msgid "JUMP TO LETTER" +msgstr "IR A LETRA" + +#: +msgid "SORT GAMES BY" +msgstr "ORDENAR JUEGOS POR" + +#. FAVORITES +#: +msgid "FAVORITES ONLY" +msgstr "SOLO FAVORITOS" + +#: +msgid "EDIT THIS GAME'S METADATA" +msgstr "EDITAR DATOS DEL JUEGO" + +#: +msgid "SCRAPE THESE GAMES" +msgstr "OBTENER DATOS DE JUEGOS" + +#: +msgid "All Games" +msgstr "Todos los juegos" + +#. MISSING SCRAPPER TRANSLATIONS +#: +msgid "Only missing image" +msgstr "Solo imágenes faltantes" + +#: +msgid "FILTER" +msgstr "FILTRO" + +#: +msgid "SCRAPE THESE SYSTEMS" +msgstr "OBTENER SISTEMAS" + +#: +msgid "SYSTEMS" +msgstr "SISTEMAS" + +#: +msgid "USER DECIDES ON CONFLICTS" +msgstr "DECIDIR EN CASO DE CONFLICTOS" + +#: +msgid "START" +msgstr "INICIAR" + +#: +msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" +"CONTINUE ANYWAY?" +msgstr "CUIDADO: ALGÚN SISTEMA SELECCIONADO NO TIENE PLATAFORMA DEFINIDA.\n" +"¡LOS RESULTADOS PODRÃAN SER MÃS INCORRECTOS DE LO HABITUAL!\n" +"¿ACEPTAR?" + +#: +msgid "NO GAMES FIT THAT CRITERIA." +msgstr "NINGÚN JUEGO SE AJUSTA AL CRITERIO" + +#: +msgid "REALLY UPDATE?" +msgstr "¿ACTUALIZAR?" + +#: +msgid "NETWORK CONNECTION NEEDED" +msgstr "SE NECESITA CONEXIÓN DE RED" + +#. Lo correcto sería: +#. +#. ACTUALIZACIÓN DESCARGADA, EL SISTEMA SE REINICIARà +#: +msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" +msgstr "ACTUALIZACIÓN DESCARGADA, EL SISTEMA SE REINICIARÃ" + +#. Lo correcto sería: +#. +#. ACTUALIZACIÓN FALLIDA, EL SISTEMA SE REINICIARà +#: +msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" +msgstr "ACTUALIZACIÓN FALLIDA, EL SISTEMA SE REINICIARÃ" + +#: +msgid "NO UPDATE AVAILABLE" +msgstr "SIN ACTUALIZACIONES DISPONIBLES" + +#: +msgid "enter emulator" +msgstr "ingresar emulador" + +#: +msgid "enter core" +msgstr "ingresar núcleo" + +#: +msgid "Ratio" +msgstr "Proporción" + +#: +msgid "enter ratio" +msgstr "ingresar proporción" + +#: +msgid "Name" +msgstr "Nombre" + +#: +msgid "enter game name" +msgstr "ingresar nombre del juego" + +#: +msgid "Description" +msgstr "Descripción" + +#: +msgid "enter description" +msgstr "ingresar descripción" + +#: +msgid "Image" +msgstr "Imagen" + +#: +msgid "enter path to image" +msgstr "ingresar ruta de imagen" + +#: +msgid "Thumbnail" +msgstr "Miniatura" + +#: +msgid "enter path to thumbnail" +msgstr "ingresar ruta de miniatura " + +#. the english translation has a space befor the word "enter", typo? +#: +msgid "enter rating" +msgstr " ingresar calificación" + +#: +msgid "Release date" +msgstr "Fecha de lanzamiento" + +#: +msgid "enter release date" +msgstr "ingresar fecha de lanzamiento" + +#: +msgid "enter game developer" +msgstr "ingresar desarrollador" + +#. La palabra correcta para "publisher" es "editor". +#: +msgid "enter game publisher" +msgstr "ingresar editor" + +#: +msgid "enter game genre" +msgstr "ingresar género" + +#: +msgid "enter number of players" +msgstr "ingresar número de jugadores" + +#: +msgid "Favorite" +msgstr "Favorito" + +#: +msgid "enter favorite" +msgstr "ingresar favorito" + +#: +msgid "Region" +msgstr "Región" + +#: +msgid "enter region" +msgstr "ingresar región" + +#: +msgid "Romtype" +msgstr "Tipo de ROM" + +#: +msgid "enter romtype" +msgstr "ingresar tipo de ROM" + +#: +msgid "Hidden" +msgstr "Oculto" + +#: +msgid "set hidden" +msgstr "ocultar" + +#: +msgid "Play count" +msgstr "Partidas jugadas" + +#: +msgid "enter number of times played" +msgstr "ingresar partidas jugadas" + +#: +msgid "Last played" +msgstr "Última partida" + +#: +msgid "enter last played date" +msgstr "ingresar fecha de última partida" + +#: +msgid "%i GAME AVAILABLE" +msgid_plural "%i GAMES AVAILABLE" +msgstr[0] "%i JUEGO DISPONIBLE" +msgstr[1] "%i JUEGOS DISPONIBLES" + +#: +msgid "%i FAVORITE" +msgid_plural "%i FAVORITES" +msgstr[0] "%i FAVORITO" +msgstr[1] "%i FAVORITOS" + +#. Imagino que aquí queda mejor utilizar el infinitivo, comparandolo con la siguiente entrada... +#: +msgid "SCROLL" +msgstr "DESPLAZAR" + +#: +msgid "LAUNCH" +msgstr "LANZAR" + +#: +msgid "Times played" +msgstr "Veces jugado" + +#: +msgid "MENU" +msgstr "MENÚ" + +#: +msgid "FILENAME, ASCENDING" +msgstr "NOMBRE, ASCENDENTE" + +#: +msgid "FILENAME, DESCENDING" +msgstr "NOMBRE, DESCENDENTE" + +#: +msgid "RATING, ASCENDING" +msgstr "CALIFICACIÓN, ASCENDENTE" + +#: +msgid "RATING, DESCENDING" +msgstr "CALIFICACIÓN, DESCENDENTE" + +#: +msgid "TIMES PLAYED, ASCENDING" +msgstr "PARTIDAS, ASCENDENTE" + +#: +msgid "TIMES PLAYED, DESCENDING" +msgstr "PARTIDAS, DESCENDENTE" + +#: +msgid "LAST PLAYED, ASCENDING" +msgstr "ÚLTIMA PARTIDA, ASCENDENTE" + +#: +msgid "LAST PLAYED, DESCENDING" +msgstr "ÚLTIMA PARTIDA, DESCENDENTE" + +#: +msgid "WORKING..." +msgstr "TRABAJANDO..." + +#: +msgid "CHANGE" +msgstr "CAMBIAR" + +#: +msgid "never" +msgstr "nunca" + +#: +msgid "just now" +msgstr "ahora mismo" + +#: +msgid "%i sec ago" +msgid_plural "%i secs ago" +msgstr[0] "Hace %i segundo" +msgstr[1] "Hace %i segundos" + +#: +msgid "%i min ago" +msgid_plural "%i mins ago" +msgstr[0] "Hace %i minuto" +msgstr[1] "Hace %i minutos" + +#: +msgid "%i hour ago" +msgid_plural "%i hours ago" +msgstr[0] "Hace %i hora" +msgstr[1] "Hace %i horas" + +#: +msgid "%i day ago" +msgid_plural "%i days ago" +msgstr[0] "Hace %i día" +msgstr[1] "Hace %i días" + +#: +msgid "unknown" +msgstr "desconocido" + +#: +msgid "SELECT ALL" +msgstr "SELECCIONAR TODO" + +#: +msgid "SELECT NONE" +msgstr "SELECCIONAR NINGUNO" + +#: +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i SELECCIONADO" +msgstr[1] "%i SELECCIONADOS" + +#: +msgid "UP" +msgstr "ARRIBA" + +#: +msgid "DOWN" +msgstr "ABAJO" + +#: +msgid "LEFT" +msgstr "IZQUIERDA" + +#: +msgid "RIGHT" +msgstr "DERECHA" + +#. @Claudio, mando se refiere a todo el control, y no queda claro que se trata del joystick/palanca. Voy a volver a poner joystick a todos estos. +#: +msgid "JOYSTICK 1 UP" +msgstr "JOYSTICK 1 ARRIBA" + +#: +msgid "JOYSTICK 1 LEFT" +msgstr "JOYSTICK 1 IZQUIERDA" + +#: +msgid "JOYSTICK 2 UP" +msgstr "JOYSTICK 2 ARRIBA" + +#: +msgid "JOYSTICK 2 LEFT" +msgstr "JOYSTICK 2 IZQUIERDA" + +#. Creo que esta traducción no es correcta, ya que al configurar los botones L1 y R1 debería ser: +#. +#. L1: PÃGINA ANTERIOR +#. R1: PÃGINA SIGUIENTE +#: +msgid "PAGE UP" +msgstr "PÃGINA ARRIBA" + +#: +msgid "PAGE DOWN" +msgstr "PÃGINA ABAJO" + +#: +msgid "HOTKEY" +msgstr "HOTKEY" + +#: +msgid "CONFIGURING" +msgstr "CONFIGURANDO" + +#: +msgid "KEYBOARD" +msgstr "TECLADO" + +#: +msgid "GAMEPAD %i" +msgstr "MANDO %i" + +#. Config controllers missing translation +#: +msgid "PRESS ANYTHING" +msgstr "PULSAR CUALQUIER BOTÓN" + +#: +msgid "ALREADY TAKEN" +msgstr "EN USO" + +#: +msgid "DISCARD CHANGES" +msgstr "DESCARTAR CAMBIOS" + +#: +msgid "WELCOME" +msgstr "BIENVENIDO" + +#: +msgid "CONFIGURE INPUT" +msgstr "CONFIGURAR MANDOS" + +#: +msgid "%i GAMEPAD DETECTED" +msgid_plural "%i GAMEPADS DETECTED" +msgstr[0] "%i MANDO DETECTADO" +msgstr[1] "%i MANDOS DETECTADOS" + +#: +msgid "NO GAMEPADS DETECTED" +msgstr "SIN MANDOS DETECTADOS" + +#: +msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." +msgstr "MANTENER PRESIONADO UN BOTÓN DEL MANDO PARA CONFIGURARLO" + +#: +msgid "PRESS F4 TO QUIT AT ANY TIME." +msgstr "PRESIONAR F4 PARA SALIR EN CUALQUIER MOMENTO" + +#: +msgid "PRESS ESC OR THE HOTKEY TO CANCEL." +msgstr "PRESIONAR ESC O HOTKEY PARA CANCELAR" + +#: +msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" +msgstr "¿INICIAR EL CENTRO MULTIMEDIA KODI?" + +#: +msgid "LOADING..." +msgstr "CARGANDO..." + +#: +msgid "PLEASE WAIT..." +msgstr "ESPERAR..." + +#: +msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" +msgstr "¿APAGAR SIN GUARDAR DATOS?" + +#: +msgid "FAST SHUTDOWN SYSTEM" +msgstr "APAGAR RÃPIDO" + +#. Please change RECALBOX.FR to RECALBOX.COM on the English version! +#: +msgid "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 RECALBOX.FR FOR MORE INFORMATION." +msgstr "¡NO SE HAN ENCONTRADO SISTEMAS!\n" +"COMPROBAR RUTAS EN ARCHIVO DE CONFIGURACIÓN.\n" +"LA CARPETA DE JUEGOS DEBE CONTENER AL MENOS UNO Y CON LA EXTENSIÓN CORRECTA.\n" +"\n" +"VISITAR RECALBOX.COM PARA OBTENER MÃS INFORMACIÓN" + +#: +msgid "ON SCREEN KEYBOARD" +msgstr "TECLADO EN PANTALLA" + +#: +msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" +msgstr "SHIFT PARA MAYÚSCULAS, MINÚSCULAS Y ESPECIALES" + +#: +msgid "SPACE" +msgstr "ESPACIO" + +#: +msgid "DELETE A CHAR" +msgstr "BORRAR UN CARÃCTER" + +#: +msgid "SHIFT" +msgstr "SHIFT" + +#: +msgid "STOP EDITING" +msgstr "TERMINAR EDICIÓN" + +#: +msgid "MOVE CURSOR" +msgstr "MOVER CURSOR" + +#: +msgid "EDIT" +msgstr "EDITAR" + +#: +msgid "ACCEPT RESULT" +msgstr "ACEPTAR RESULTADO" + +#: +msgid "FILENAME" +msgstr "NOMBRE DEL ARCHIVO" + +#: +msgid "RATING" +msgstr "CLASIFICACIÓN" + +#: +msgid "TIMES PLAYED" +msgstr "JUGADO" + +#: +msgid "LAST PLAYED" +msgstr "ÚLTIMA VEZ JUGADO" + +#: +msgid "NUMBER OF PLAYERS" +msgstr "NÚMERO DE JUGADORES" + +#: +msgid "DEVELOPER" +msgstr "DESARROLLADOR" + +#: +msgid "GENRE" +msgstr "GÉNERO" + +#: +msgid "SHOW HIDDEN" +msgstr "MOSTRAR OCULTOS" + +#: +msgid "EXTREM (1400Mhz)" +msgstr "EXTREMO (1400Mhz)" + +#: +msgid "TURBO (1350Mhz)" +msgstr "TURBO (1350Mhz)" + +#: +msgid "HIGH (1300Mhz)" +msgstr "ALTO (1300Mhz)" + +#: +msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" +"IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." +msgstr "ESTABLECER EL OVERCLOCK EN TURBO Y EXTREMO, PUEDE CAUSAR INESTABILIDAD DEL SISTEMA.\n" +"NO USAR SIN ESTAR SEGURO.\n" +"AL CONTINUAR, EL SISTEMA SE REINICIARÃ." + +#: +msgid "%i GAME HIDDEN" +msgid_plural "%i GAMES HIDDEN" +msgstr[0] "%i JUEGO OCULTO" +msgstr[1] "%i JUEGOS OCULTOS" + +#: +msgid "Start kodi media player." +msgstr "Iniciar KODI" + +#: +msgid "Select the language for your recalbox, select an external drive to store your games and configurations, check your current version and the free space on your drive" +msgstr "Seleccionar idioma, seleccionar disco externo para guardar juegos y configuraciones, comprobar versión actual y espacio libre" + +#: +msgid "Shows your current recalboxOS version." +msgstr "Mostrar versión actual de RecalboxOS." + +#: +msgid "Show how much space is used on your SHARE partition, located either on the SDCARD or on an external drive. The information shows how much GB are used and how much GB your storage has overall (example 13GB/26GB)." +msgstr "Mostrar espacio usado en partición SHARE, tanto en SD como en disco externo. La información se muestra en GB usados/total (ejemplo 13GB/26GB)." + +#: +msgid "Select an external drive to store your roms, saves, configurations etc.\n" +"Use a FAT32 formatted drive. The system does not format the drive. On first boot, with this option enabled, recalbox will create a '/recalbox' folder with all system files inside." +msgstr "Seleccionar dispositivo externo para guardar juegos, partidas guardadas, configuraciones y otros.\n" +"Dispositivo requerido en formato FAT32, no será formateado.\n" +"En el siguiente arranque, esta opción crea en el dispositivo externo, una carpeta llamada '/recalbox' con todos los archivos requeridos" + +#: +msgid "Select your language. A reboot is needed to set this configuration active." +msgstr "Seleccionar idioma. Reiniciar para aplicar" + +#: +msgid "Manage your recalbox updates. Select the update type. Activate update check." +msgstr "Gestionar actualizaciones. Seleccionar tipo de actualización. Activar revisión de actualizaciones" + +#: +msgid "Check if an update is available, and start the update process." +msgstr "Revisar si hay actualizaciones disponibles, e iniciar proceso de actualización" + +#. We must change the issues page from github to gitlab on every language! +#: +msgid "Stable updates will check for updates on stable recalbox releases. Stable updates are tested and approved by the recalbox team and their testers.\n" +"Unstable updates allows you to get the latest recalbox features by checking our unstable repository. You can test and validate with us the very last version of recalbox.\n" +"If you choose unstable update, be so kind to report issues on the recalbox-os issue board (https://github.com/recalbox/recalbox-os/issues)" +msgstr "Actualizaciones estables: Las actualizaciones estables se comprueban y aprueban por el equipo de Recalbox y sus testers.\n" +"Actualizaciones inestables: Permiten obtener últimas características de recalbox comprobando nuestro depósito de inestables. Se puede probar y validar con nosotros la última versión de Recalbox.\n" +"Al seleccionar una actualización inestable, por favor reportar los problemas en la plataforma de desarrollo de RecalboxOS (https://gitlab.com/recalbox/recalbox/issues)" + +#. Creo que así queda más parecido al significado original +#: +msgid "Automatically check if an update is avaialble. If so, it notifies you with a message." +msgstr "Comprueba automáticamente si hay alguna actualización disponible. Si es así, se notifica con un mensaje" + +#: +msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." +msgstr "Configurar visualización de juegos, relación de aspecto, filtros (shaders), autoguardado y autocargado, y cuenta de retrologros" + +#: +msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." +msgstr "La relación de aspecto, es la relación entre ancho y alto de imagen.\n" +"Configurar AUTO para que el emulador seleccione la relación de aspecto original del juego, proporcionando la mejor experiencia de retrojuego" + +#: +msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." +msgstr "Suavizar imagen del juego. Esta opción proporciona la imagen más suave, usando filtros bilineales" + +#: +msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." +msgstr "Esta opción permite rebobinar el juego. Usar el botón HOTKEY+IZQUIERDA en el juego para rebobinar" + +#: +msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." +msgstr "Autoguardar la partida al salir del juego, y autocargar la partida al iniciar el juego." + +#: +msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." +msgstr "La escala entera, es el escalado por un factor de un número entero, como 2x, 3x, 4x, etc. Esta opción escala la imagen hasta la escala entera más grande por debajo de la resolución establecida. Por ejemplo, al establecer la resolución de pantalla completa en 1920x1080 y se habilita el escalado entero, sólo escalará una imagen de 320x240 hasta 1280x960 y dejará los bordes negros alrededor. Esto es para mantener una relación de aspecto de píxeles de 1:1 con la imagen original, de modo que los píxeles no se dupliquen de manera desigual" + +#: +msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." +msgstr "Los shaders equivalen a filtros para el renderizado del juego. Puede seleccionar un conjunto de shaders, que es una colección de shaders seleccionados para cada sistema. También se puede cambiar el shader dentro del juego con HOTKEY+L2 o HOTKEY+R2" + +#: +msgid "Enable or disable RetroAchievements in games." +msgstr "Activar o desactivar los retrologros en los juegos" + +#: +msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." +msgstr "El modo hardcore deshabilita toda capacidad de guardar partidas y funciones de rebobinado. Se debe terminar el juego y conseguir logros de una sola vez, igual que en la máquina original. En compensación, se conseguirán tanto los logros estándar y hardcore, y el doble de puntos. Un juego regular de 400 puntos, generará 800 si se completa en hardcore" + +#: +msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" +msgstr "¡La página web retroachievements.org propone desafíos/logros/trofeos en plataformas como NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine y más! ¡Crear cuenta en retroachievements.org y empezar búsqueda de logros!" + +#: +msgid "Add and configure up to 5 controllers." +msgstr "Agregar y configurar hasta 5 mandos" + +#: +msgid "Pair a bluetooth controller with your recalbox. Your controller must be in pairing mode." +msgstr "Vincular mando por bluetooth. El mando debe estar en modo visible" + +#: +msgid "Forget all paired bluetooth controllers. You will have to pair your controllers again, but this option can help if you have issues to reconnect a controller, which is already paired." +msgstr "Olvidar todos los mandos vinculados por bluetooth. Tendrás que vincularlos nuevamente, pero esta opción puede ayudarte si tienes problemas al reconectar un mando que ya está vinculado" + +#: +msgid "Configure your EmulationStation experience. Select transition types, help prompts, screensaver behavior. You can also deactivate the onscreen keyboard if you have a real keyboard plugged into your recalbox.\n" +"If you've added games since the last boot, you can also refresh the gamelist from this menu." +msgstr "Configurar experiencia con EmulationStation. Seleccionar tipos de transición, mensajes de ayuda, comportamiento del salvapantallas. También se puede desactivar el teclado virtual en caso de tener uno de verdad conectado a Recalbox.\n" +"Si se han agregado juegos desde el último arranque, también se puede actualizar la lista de juegos desde este menú" + +#: +msgid "Start the screensaver after N minutes." +msgstr "Iniciar el salvapantallas tras N minutos" + +#: +msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." +msgstr "Configurar el comportamiento del protector de pantalla. DIM reducirá el brillo de pantalla, y BLACK pondrá la pantalla en negro" + +#: +msgid "Shows a help at the bottom of the screen which displays commands you can use." +msgstr "Desplegar una ayuda en la parte inferior de la pantalla que enseña comandos a usar" + +#: +msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." +msgstr "Activado, permite cambiar de sistema pulsando IZQUIERDA o DERECHA en la lista de juegos" + +#: +msgid "The onscreen keyboard is necessary to type text if you only have controllers plugged into your recalbox. You can disable it if you have a real keyboard connected." +msgstr "El teclado virtual es necesario para escribir si existen sólo mandos conectados. Desactivar en caso de tener un teclado real conectado" + +#: +msgid "Select a theme for your recalbox." +msgstr "Seleccionar tema para Recalbox" + +#: +msgid "Updates the gamelists, if you added games since the last boot." +msgstr "Actualizar lista de juegos, en caso de haber agregado juegos desde el último inicio del sistema" + +#: +msgid "Configure the sound options of your recalbox." +msgstr "Configuras las opciones de sonido de Recalbox" + +#: +msgid "Set the volume of the sound output for the frontend and the games." +msgstr "Configurar volumen para salida de audio en interfaz y juegos" + +#: +msgid "Enable or disable the frontend music. You can add your own music as mp3, or ogg format in the 'musics' directory of your recalbox." +msgstr "Activar o desactivar música de interfaz. Permite agregar música propia en formato mp3 u ogg en la carpeta 'musics' de Recalbox" + +#: +msgid "Select your output device. Only HDMI and JACK are supported." +msgstr "Seleccionar dispositivo de salida. Sólo compatible con HDMI y JACK" + +#: +msgid "Configure the network options of your recalbox.\n" +"Check your network status and IP address, set the hostname and configure the WIFI." +msgstr "Configurar opciones de red.\n" +"Comprobar estado de red y dirección IP, establecer nombre de host y configurar WIFI" + +#: +msgid "Displays CONNECTED, if you are connected, by checking if your recalbox can access the recalbox.com update server." +msgstr "Mostrar CONECTADO, comprobando si hay acceso al servidor de actualizaciones" + +#: +msgid "The IP address of your recalbox within your local network." +msgstr "Dirección IP de recalbox" + +#: +msgid "Enable or disable WIFI.\n" +"If you disable WIFI, the SSID and the WIFI passwords are saved and can be used when you reactivate it" +msgstr "Activar o desactivar WIFI.\n" +"Al desactivar WIFI, SSID y contraseñas son guardadas y utilizadas al reactivar" + +#: +msgid "The name of your recalbox in your local network" +msgstr "Nombre de Recalbox en la red de área local" + +#: +msgid "SSID (WIFI Name) of your network." +msgstr "SSID (Nombre del WIFI) de la red" + +#: +msgid "Private key of your WIFI network." +msgstr "Contraseña WIFI" + +#: +msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." +msgstr "Obtener información e imágenes de juegos. El scraper descarga información e imágenes de juegos desde diferentes servidores y mejora la experiencia de usuario completamente en EmulationStation" + +#: +msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." +msgstr "Seleccionar un servidor desde donde descargar información. Se recomienda el servidor SCREENSCRAPER que está basado en www.screenscraper.fr y descarga datos de juegos en tu idioma, si está disponible" + +#: +msgid "Begin the scrape process with the configuration shown below." +msgstr "Comienza el proceso de descarga con la configuración siguiente" + +#: +msgid "Scrape and display game ratings." +msgstr "Descargar y mostrar las puntuaciones del juego" + +#: +msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." +msgstr "Configuraciones avanzadas. Asegúrate de que sabes lo que haces antes de cambiar cualquier valor en este menú" + +#: +msgid "Overclock your board to increase the performance.\n" +"Overclock settings are tested and validated by the community. Keep in mind that overclocking your board can void your warranty." +msgstr "Aumentar la velocidad del reloj para incrementar el rendimiento.\n" +"Estas configuraciones están probadas y validadas por la comunidad.\n" +"Considerar que al aumentar la velocidad del reloj es posible perder la garantía" + +#: +msgid "Select which system to show when the recalbox frontend starts. The default value is 'favorites'." +msgstr "Seleccionar qué sistema se muestra cuando empieza la interfaz del Recalbox. El valor por defecto es 'favoritos'" + +#: +msgid "On boot, recalbox will show the list of games of the selected system rather than the system view." +msgstr "En el arranque, Recalbox mostrará la lista de juegos del sistema seleccionado, no la vista de todos sistemas" + +#: +msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" +"This option highly speeds up boot time, but new games will not be detected." +msgstr "Mostrar sólo los juegos que están en el archivo gamelist.xml (situado en las carpetas de los ROMs).\n" +"Esta opción acelera el tiempo de arranque, pero no detectará nuevos juegos" + +#: +msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." +msgstr "Esta opción te permite establecer el sistema seleccionado en modo fijo. Con esta opción activada, el usuario no puede acceder a otros sistemas" + +#: +msgid "Always display the basic gamelist view, even if you have scraped your games." +msgstr "Mostrar siempre la lista de juegos básica, incluso si has scrapeado tus juegos" + +#: +msgid "Override global options like emulator, core, ratio and more for each available system in your recalbox." +msgstr "Anula opciones globales tales como emulador, núcleo, relación de aspecto y más, para cada sistema disponible" + +#: +msgid "Configure boot options that make your recalbox boot straight into a system or into Kodi, lock a user to a single system, or directly show the gamelist." +msgstr "Configurar las opciones de arranque para que Recalbox inicie directamente en un sistema definido, iniciar en Kodi, bloquear un usuario para un solo sistema, o directamente mostrar la lista de juegos" + +#: +msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" +msgstr "Activar o desactivar KODI, personalizar el arranque de KODI, activar el botón X para iniciar KODI" + +#: +msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." +msgstr "Activa o desactiva Kodi. Si Kodi está desactivado, no podrá ser iniciado con el botón X, o automáticamente al arrancar. El menú de entrada será desactivado" + +#: +msgid "Use the X button to start Kodi." +msgstr "Usar el botón X para iniciar Kodi" + +#: +msgid "Automatically start into Kodi on boot." +msgstr "Iniciar Kodi automáticamente en el arranque" + +#: +msgid "Manage your recalbox security." +msgstr "Administrar la seguridad de Recalbox" + +#: +msgid "Change the SSH root password." +msgstr "Cambiar la contraseña root SSH" + +#: +msgid "Enforce recalbox security." +msgstr "Reforzar la seguridad de Recalbox" + +#. Ordenador? Computador? Computadora? ...en español existen varias acepciones para este tipo de dispositivo (Windows en español lo define como "PC" simplemente, ahora en Windows 10, utiizan el término "Equipo"... Windows 98/Me/2000/XP se refería a "sistema") +#: +msgid "Enable or disable overscan.\n" +"Overscan can help you, if you have a black border, or if the image is bigger than your screen. Before setting the overscan, try to configure your TV to have a 1:1 pixel output.\n" +"More overscan settings can be defined in the boot.txt file, available when you plug your SD card into your computer." +msgstr "Activar o desactivar overscan.\n" +"El overscan puede ayudar al presentar bordes negros, o en caso de imagen de mayor tamaño que la pantalla.\n" +"Antes de configurar overscan, intentar configurar la TV para tener una salida de píxeles 1:1.\n" +"Es posible definir más configuraciones de overscan en el archivo boot.txt, disponible al insertar la tarjeta SD en la computadora" + +#: +msgid "Show the framerate in EmulationStation and in game." +msgstr "Mostrar cuadros por segundo en EmulationStation y en el juego" + +#: +msgid "Enable or disable the Recalbox Manager.\n" +"The Recalbox Manager is a web application available on http://recalbox , if you are on windows, http://recalbox.local , if you are on Linux or Mac, or directly with your recalbox IP : http://192.168.1.XX.\n" +"You can configure many options from within the manager, and even manage games, saves, and scrapes!" +msgstr "Activar o desactivar el Administrador de Recalbox.\n" +"El Administrador de Recalbox es una aplicación web disponible en http://recalbox, en Windows, http://recalbox.local en Linux o Mac, o directamente con la IP de Recalbox: http://192.168.1.XX.\n" +"¡Es posible configurar muchas opciones desde el administrador, incluso juegos, partidas guardadas e información!" + +#: +msgid "Enable or disable the recalbox API.\n" +"The Recalbox API is a REST API exposing endpoints to control your recalbox via http requests." +msgstr "Activar o desactivar la API de Recalbox.\n" +"La API de Recalbox es una API REST que expone puntos finales para controlar Recalbox a través de solicitudes HTTP" + +#: +msgid "Select which emulator to use when you start a game for this system." +msgstr "Seleccionar qué emulador usar cuando se inicia un juego de este sistema" + +#: +msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." +msgstr "Seleccionar qué núcleo usar para el emulador seleccionado. Por ejemplo, el emulador LIBRETRO tiene varios núcleos para ejecutar juegos de Super Nintendo. La configuración del núcleo por defecto que se elija, también puede ser eliminada desde la configuración específica para juegos" + +#: +msgid "USE COMPOSED VISUALS" +msgstr "USAR IMÃGENES COMPUESTAS" + +#: +msgid "CHECK UPDATES" +msgstr "COMPROBAR ACTUALIZACIONES" + +#: +msgid "UPDATE TYPE" +msgstr "TIPO DE ACTUALIZACIÓN" + +#: +msgid "INTEGER SCALE (PIXEL PERFECT)" +msgstr "ESCALA COMPLETA (PIXEL PERFECT)" + +#: +msgid "ADVANCED SETTINGS" +msgstr "OPCIONES AVANZADAS" + +#: +msgid "BOOT SETTINGS" +msgstr "OPCIONES DE ARRANQUE" + +#: +msgid "GAMELIST ONLY" +msgstr "SÓLO LISTA DE JUEGOS" + +#: +msgid "BOOT ON SYSTEM" +msgstr "ARRANCAR EN SISTEMA" + +#: +msgid "BOOT ON GAMELIST" +msgstr "ARRANCAR EN LISTA DE JUEGOS" + +#: +msgid "HIDE SYSTEM VIEW" +msgstr "OCULTAR VISTA DE SISTEMA" + +#: +msgid "EMULATOR ADVANCED CONFIGURATION" +msgstr "CONFIGURACIÓN AVANZADA DEL EMULADOR" + +#: +msgid "ADVANCED EMULATOR CONFIGURATION" +msgstr "CONFIGURACIÓN DEL EMULADOR AVANZADA" + +#: +msgid "HELP" +msgstr "AYUDA" + +#: +msgid "THE SYSTEM IS UP TO DATE" +msgstr "EL SISTEMA ESTà ACTUALIZADO" + +#: +msgid "FORCE BASIC GAMELIST VIEW" +msgstr "FORZAR VISTA DE LISTA DE JUEGOS BÃSICA" + +#: +msgid "DOWNLOADED" +msgstr "DESCARGADO" + +#: +msgid "UPDATE VERSION:" +msgstr "VERSIÓN DE LA ACTUALIZACIÓN:" + +#: +msgid "UPDATE CHANGELOG:" +msgstr "REGISTRO DE CAMBIOS DE LA ACTUALIZACIÓN:" + +#: +msgid "MORE DETAILS" +msgstr "MÃS DETALLES" + +#: +msgid "CAROUSEL ANIMATION" +msgstr "ANIMACIÓN DEL CARRUSEL" + +#: +msgid "THEME CONFIGURATION" +msgstr "CONFIGURACIÓN DEL TEMA" + +#: +msgid "THEME COLORSET" +msgstr "CONJUNTO DE COLORES DEL TEMA" + +#: +msgid "THEME ICONSET" +msgstr "CONJUNTO DE ÃCONOS DEL TEMA" + +#: +msgid "THEME MENU" +msgstr "MENÚ DEL TEMA" + +#: +msgid "THEME SYSTEMVIEW" +msgstr "VISTA DE SISTEMAS DEL TEMA" + +#: +msgid "THEME GAMELISTVIEW" +msgstr "VISTA DE LISTA DE JUEGOS DEL TEMA" + +#: +msgid "THEME REGION" +msgstr "REGIÓN DEL TEMA" + +#: +msgid "THIS THEME HAS NO OPTION" +msgstr "ESTE TEMA NO TIENE OPCIONES" + +#: +msgid "MANUAL INPUT" +msgstr "ENTRADA MANUAL" + +#: +msgid "AN ERROR OCCURED - DOWNLOADED" +msgstr "HA HABIDO UN ERROR - DESCARGADO" + +#: +msgid "START KODI" +msgstr "INICIAR KODI" + +#: +msgid "Shows the current available update version." +msgstr "Mostrar la versión de actualización disponible actualmente" + +#: +msgid "Shows the current available update changelog." +msgstr "Mostrar el registro de cambios de actualización disponible actualmente" + +#: +msgid "Configure an associated controller. Your controller has to be associated / plugged before." +msgstr "Configurar mando conectado. El mando debe estar previamente vinculado/conectado" + +#: +msgid "Choose if carousel will be animated or not during transitions" +msgstr "Elegir si el carrusel estará animado o no durante transiciones" + +#: +msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" +msgstr "Seleccionar el tipo de transición que ocurre cuando comienza un juego. INSTANT no hará nada, FADE se desvanecerá a oscuro, y SLIDE hará un zoom en la cubierta del juego (o nombre, si no hay metadatos)" + +#: +msgid "Select exisiting colorset options for this theme." +msgstr "Seleccionar opciones de conjuntos de colores existentes para este tema" + +#: +msgid "Select exisiting iconset options for this theme." +msgstr "Seleccionar opciones de conjunto de iconos existentes para este tema" + +#: +msgid "Select exisiting menu style options for this theme." +msgstr "Seleccionar opciones de estilo de menú existentes para este tema" + +#: +msgid "Select exisiting system view options for this theme." +msgstr "Seleccionar opciones de vista de sistema existentes para este tema" + +#: +msgid "Select exisiting gamelist view options for this theme." +msgstr "Seleccionar opciones de vista de lista de juegos existentes para este tema" + +#: +msgid "Configure theme options if available." +msgstr "Configurar opciones del tema si está disponible" + +#: +msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" +msgstr "Seleccionar región de logotipos, imágenes para sistemas diferentes entre países Ejemplo, Megadrive en EU, Genesis en USA" + +#: +msgid "Type the name of your SSID if it is hidden or not listed" +msgstr "Ingresar nombre de SSID, si está oculto o no aparece" + +#: +msgid "Select a letter and the listing will go directly on the first game starting with this letter." +msgstr "Seleccionar una letra y la lista irá directamente al primer juego que comience con esta letra" + +#: +msgid "Select the way the game list is sortered (alphabetically, by notation...)." +msgstr "Seleccionar la forma en que se ordena la lista de juegos (alfabéticamente, mediante notación...)" + +#: +msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." +msgstr "Cambiar entre ver o no solo los juegos favoritos. Para agregar un juego a la lista de favoritos, seleccionar el juego y cambiar su estado con 'Y'" + +#: +msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." +msgstr "Cambiar entre ver o no los juegos ocultos. Para ocultar un juego, editar sus datos y seleccionar 'Ocultar'" + +#: +msgid "This option display a menu which allows to change game data and many others options." +msgstr "Esta opción muestra un menú que permite cambiar los datos del juego y muchas otras opciones" + +#: +msgid "AVAILABLE UPDATE" +msgstr "ACTUALIZACIÓN DISPONIBLE" + +#: +msgid "UPDATE CHANGELOG" +msgstr "REGISTRO DE CAMBIOS" + +#: +msgid "CLOCK IN MENU" +msgstr "RELOJ EN MENÚ" + +#: +msgid "Now playing" +msgstr "Reproduciendo ahora" + +#: +msgid "DEFAULT (%1%)" +msgstr "DEFECTO (%1%)" + +#: +msgid "INPUT REQUIRED" +msgstr "INGRESO REQUERIDO" + +#: +msgid "(skipped)" +msgstr "(omitido)" + +#: +msgid "UP/DOWN TO SKIP" +msgstr "ARRIBA/ABAJO PARA OMITIR" + +#. Explanation: you config the controller, you make a mistake (let's say right instead of left), you can press A to erase wrong config and redo +#: +msgid "A TO UNSET" +msgstr "A PARA CAMBIAR" + +#: +msgid "DOWN TO SKIP AND KEEP [%1%]" +msgstr "ABAJO PARA OMITIR Y MANTENER [%1%]" + +#: +msgid "UP/DOWN TO SKIP AND KEEP [%1%]" +msgstr "ARRIBA/ABAJO PARA OMITIR Y MANTENER [%1%]" + +#: +msgid "Set duration of help popups, 0 means no popup." +msgstr "Duración de popups de ayuda, 0 significa sin popup" + +#: +msgid "HELP POPUP DURATION" +msgstr "DURACIÓN DE POPUPS DE AYUDA" + +#: +msgid "Set duration of music popups, 0 means no popup." +msgstr "Duración de popups de música, 0 significa sin popup" + +#: +msgid "MUSIC POPUP DURATION" +msgstr "DURACIÓN DE POPUPS DE MÚSICA" + +#: +msgid "POPUP SETTINGS" +msgstr "CONFIGURACIÓN DE POPUPS" + +#: +msgid "POPUP POSITION" +msgstr "POSICIÓN DE POPUPS" + +#: +msgid "Select the position of popups on screen." +msgstr "Seleccionar posición de popups en pantalla" + +#: +msgid "Set position and duration of popups." +msgstr "Posición y duración de popups" + +#: +msgid "TOP/RIGHT" +msgstr "ARRIBA/DERECHA" + +#: +msgid "BOTTOM/RIGHT" +msgstr "ABAJO/DERECHA" + +#: +msgid "BOTTOM/LEFT" +msgstr "ABAJO/IZQUIERDA" + +#: +msgid "TOP/LEFT" +msgstr "ARRIBA/IZQUIERDA" + +#: +msgid "SHOW FOLDERS CONTENT" +msgstr "MOSTRAR CONTENIDO DE CARPETAS" + +#: +msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." +msgstr "Cambiar entre mostrar estructura de carpetas o ver todos los juegos en un único nivel superior" + +#: +msgid "NETPLAY" +msgstr "NETPLAY" + +#: +msgid "NETPLAY SETTINGS" +msgstr "CONFIGURACIÓN DE NETPLAY" + +#: +msgid "NETPLAY LOBBY" +msgstr "SALA DE ESPERA DE NETPLAY" + +#: +msgid "Enable or disable Netplay in games." +msgstr "Habilitar o deshabilitar Netplay en juegos" + +#: +msgid "PORT" +msgstr "PUERTO" + +#: +msgid "NICKNAME" +msgstr "ALIAS" + +#: +msgid "RELAY SERVER" +msgstr "SERVIDOR DE RETRANSMISIÓN" + +#: +msgid "Enable or disable connections throught relay servers." +msgstr "Habilitar o deshabilitar conexiones mediante servidores de retransmisión" + +#: +msgid "KODI/NETPLAY" +msgstr "KODI/NETPLAY" + +#: +msgid "NO GAMES OR NO CONNECTION" +msgstr "SIN JUEGOS O SIN CONEXIÓN" + +#: +msgid "HASH NOW" +msgstr "CALCULAR HASH AHORA" + +#: +msgid "HASH THESE SYSTEMS" +msgstr "CALCULAR HASH PARA ESTOS SISTEMAS" + +#: +msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." +msgstr "Agregar hash de roms a listas de juegos para obtener resultados más precisos en Netplay." + +#: +msgid "HASH ROMS" +msgstr "CALCULAR HASH DE ROMS" + +#: +msgid "Only missing hashs" +msgstr "Solo hash faltantes" + +#: +msgid "Username" +msgstr "Usuario" + +#: +msgid "Country" +msgstr "País" + +#: +msgid "Latency" +msgstr "Latencia" + +#: +msgid "Host arch." +msgstr "Arq. del anfitrión" + +#: +msgid "Core ver." +msgstr "Ver. núcleo" + +#: +msgid "RA ver." +msgstr "Ver. RA" + +#: +msgid "Can join" +msgstr "Puede unirse" + +#: +msgid "Rom and core match" +msgstr "Rom y núcleo coincidente" + +#: +msgid "Rom found" +msgstr "Rom encontrado" + +#: +msgid "No rom match" +msgstr "Rom sin coincidencia" + +#: +msgid "Match" +msgstr "Coincidencia" + +#: +msgid "No Match" +msgstr "Sin coincidencia" + +#: +msgid "Rom file" +msgstr "Archivo rom" + +#: +msgid "Rom hash" +msgstr "Hash del rom" + +#: +msgid "THIS COULD TAKE A WHILE, CONFIRM?" +msgstr "Esto podría tardar, ¿confirmar?" + +#: +msgid "good" +msgstr "bueno" + +#: +msgid "bad" +msgstr "malo" + +#: +msgid "medium" +msgstr "medio" + +#: +msgid "NETPLAY POPUP DURATION" +msgstr "DURACIÓN POPUPS PARA NETPLAY" + +#: +msgid "Set duration of netplay popups, 0 means no popup." +msgstr "Duración de popups para Netplay, 0 significa sin popups" + +#: +msgid "Player" +msgstr "Jugador" + +#: +msgid "Game" +msgstr "Juego" + +#: +msgid "A Recalbox friend has started a Netplay game!" +msgstr "!Un amigo de Recalbox ha iniciado un juego en Netplay!" + +#: +msgid "Play online on games running through Retroarch like NES, SNES, FBA, Genesis/Megadrive and more!" +msgstr "¡Juega en línea en juegos que funcionan con RetroArch como NES, SNES, FBA, Genesis/Megadrive y más!" + +#: +msgid "Rom, hash and core match" +msgstr "Rom, hash y núcleo coincidente" + +#: +msgid "No core match" +msgstr "Núcleo sin coincidencia" + +#: +msgid "Add a clock in the main menu." +msgstr "" + From f3e51f2ff30849371b31d89921e5938115853df8 Mon Sep 17 00:00:00 2001 From: fabricecaruso <51082152+fabricecaruso@users.noreply.github.com> Date: Mon, 27 May 2019 14:06:41 +0200 Subject: [PATCH 025/336] Update README.md --- README.md | 446 +++++++++++++++++++++++++++++------------------------- 1 file changed, 242 insertions(+), 204 deletions(-) diff --git a/README.md b/README.md index 49ff0c5338..52e38b50b0 100644 --- a/README.md +++ b/README.md @@ -1,103 +1,141 @@ -EmulationStation -================ - -This is a fork of EmulationStation for RetroPie. -EmulationStation is a cross-platform graphical front-end for emulators with controller navigation. - -Building -======== - -EmulationStation uses some C++11 code, which means you'll need to use at least g++-4.7 on Linux, or VS2010 on Windows, to compile. - -EmulationStation has a few dependencies. For building, you'll need CMake, SDL2, FreeImage, FreeType, cURL and RapidJSON. You also should probably install the `fonts-droid` package which contains fallback fonts for Chinese/Japanese/Korean characters, but ES will still work fine without it (this package is only used at run-time). - -**On Debian/Ubuntu:** -All of this be easily installed with `apt-get`: -```bash -sudo apt-get install libsdl2-dev libfreeimage-dev libfreetype6-dev libcurl4-openssl-dev rapidjson-dev \ - libasound2-dev libgl1-mesa-dev build-essential cmake fonts-droid-fallback libvlc-dev \ - libvlccore-dev vlc-bin -``` -**On Fedora:** -All of this be easily installed with `dnf` (with rpmfusion activated) : -```bash -sudo dnf install SDL2-devel freeimage-devel freetype-devel curl-devel \ - alsa-lib-devel mesa-libGL-devel cmake \ - vlc-devel rapidjson-devel -``` - -Note this Repository uses a git submodule - to checkout the source and all submodules, use - -```bash -git clone --recursive https://github.com/RetroPie/EmulationStation.git -``` - -or - -```bash -git clone https://github.com/RetroPie/EmulationStation.git -cd EmulationStation -git submodule update --init -``` - -Then, generate and build the Makefile with CMake: -```bash -cd YourEmulationStationDirectory -cmake . -make -``` - -**On the Raspberry Pi:** - -Complete Raspberry Pi build instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#install_rpi_standalone). - -**On Windows:** - -[FreeImage](http://downloads.sourceforge.net/freeimage/FreeImage3154Win32.zip) - -[FreeType2](http://download.savannah.gnu.org/releases/freetype/freetype-2.4.9.tar.bz2) (you'll need to compile) - -[SDL2](http://www.libsdl.org/release/SDL2-devel-2.0.8-VC.zip) - -[cURL](http://curl.haxx.se/download.html) (you'll need to compile or get the pre-compiled DLL version) - -[RapisJSON](https://github.com/tencent/rapidjson) (you'll need the `include/rapidsjon` added to the include path) - -(Remember to copy necessary .DLLs into the same folder as the executable: probably FreeImage.dll, freetype6.dll, SDL2.dll, libcurl.dll, and zlib1.dll. Exact list depends on if you built your libraries in "static" mode or not.) - -[CMake](http://www.cmake.org/cmake/resources/software.html) (this is used for generating the Visual Studio project) - -(If you don't know how to use CMake, here are some hints: run cmake-gui and point it at your EmulationStation folder. Point the "build" directory somewhere - I use EmulationStation/build. Click configure, choose "Visual Studio [year] Project", fill in red fields as they appear and keep clicking Configure (you may need to check "Advanced"), then click Generate.) - - -Configuring -=========== - -**~/.emulationstation/es_systems.cfg:** -When first run, an example systems configuration file will be created at `~/.emulationstation/es_systems.cfg`. `~` is `$HOME` on Linux, and `%HOMEPATH%` on Windows. This example has some comments explaining how to write the configuration file. See the "Writing an es_systems.cfg" section for more information. - -**Keep in mind you'll have to set up your emulator separately from EmulationStation!** - -**~/.emulationstation/es_input.cfg:** -When you first start EmulationStation, you will be prompted to configure an input device. The process is thus: - -1. Hold a button on the device you want to configure. This includes the keyboard. - -2. Press the buttons as they appear in the list. Some inputs can be skipped by holding any button down for a few seconds (e.g. page up/page down). - -3. You can review your mappings by pressing up and down, making any changes by pressing A. - -4. Choose "SAVE" to save this device and close the input configuration screen. - -The new configuration will be added to the `~/.emulationstation/es_input.cfg` file. - -**Both new and old devices can be (re)configured at any time by pressing the Start button and choosing "CONFIGURE INPUT".** From here, you may unplug the device you used to open the menu and plug in a new one, if necessary. New devices will be appended to the existing input configuration file, so your old devices will remain configured. - -**If your controller stops working, you can delete the `~/.emulationstation/es_input.cfg` file to make the input configuration screen re-appear on next run.** - - -You can use `--help` or `-h` to view a list of command-line options. Briefly outlined here: -``` +EmulationStation +================ + +This is a fork of EmulationStation from jrassa. +EmulationStation is a cross-platform graphical front-end for emulators with controller navigation. + +Changes in my branch +==================== + +- Support for Multiple Emulators/Cores in es_systems.cfg, and setting Emulator/Core per game. + + %HOME%\RetroArch\retroarch.exe -L %HOME%\RetroArch\cores\%CORE%_libretro.dll %ROM% + + + + mame2003_plus + mame2003 + + + + + fbalpha2012 + + + + +- Better Grid view (animations, layout, selection...) +- Created a 'GridEx' mode, which inherits from grid, in order to create a 'preview bar' including a game screenshot. +- Changed 'Automatic' view mode. Switchs to grid if "thumbnails" exist in gamelist. Switchs to gridex if "thumbnails" and "image" exist in gamelist. +- Don't show Directories that contains only one Game : just Show the game. +- Don't show Games what are marked Hidden in gamelist. +- Cleaned menus + changed menu item order (by interest). Simplified "Quit" menu item ( no more popup ) +- Case insensitive file extensions. +- Skip parsing 'downloaded_images' and 'media' folders ( loading time boost ) +- Localisation (French actually supported) +- Corrected favorites ( and custom lists ) management. +- Don't load all fields in Medadata Editor ( too tricky to use on windows, better use an external tool ). +- Windows is now "Windowed No border" by default. On Windows, Exclusive fullscreen can be annoying... +- Theming : Ability to force default view ( attribute defaultView ) +- Windows : Stop using _wsystem for launching games. Run games with ShellExecuteEx instead ( avoids command window ) +- Add an option to leave ES open with a black screen" Chargement en cours..." when launching games ( avoids showing windows desktop ) + +Je crois que c'est à peu près tout.... + +Building +======== + +EmulationStation uses some C++11 code, which means you'll need to use at least g++-4.7 on Linux, or VS2010 on Windows, to compile. + +EmulationStation has a few dependencies. For building, you'll need CMake, SDL2, FreeImage, FreeType, cURL and RapidJSON. You also should probably install the `fonts-droid` package which contains fallback fonts for Chinese/Japanese/Korean characters, but ES will still work fine without it (this package is only used at run-time). + +**On Debian/Ubuntu:** +All of this be easily installed with `apt-get`: +```bash +sudo apt-get install libsdl2-dev libfreeimage-dev libfreetype6-dev libcurl4-openssl-dev rapidjson-dev \ + libasound2-dev libgl1-mesa-dev build-essential cmake fonts-droid-fallback libvlc-dev \ + libvlccore-dev vlc-bin +``` +**On Fedora:** +All of this be easily installed with `dnf` (with rpmfusion activated) : +```bash +sudo dnf install SDL2-devel freeimage-devel freetype-devel curl-devel \ + alsa-lib-devel mesa-libGL-devel cmake \ + vlc-devel rapidjson-devel +``` + +Note this Repository uses a git submodule - to checkout the source and all submodules, use + +```bash +git clone --recursive https://github.com/RetroPie/EmulationStation.git +``` + +or + +```bash +git clone https://github.com/RetroPie/EmulationStation.git +cd EmulationStation +git submodule update --init +``` + +Then, generate and build the Makefile with CMake: +```bash +cd YourEmulationStationDirectory +cmake . +make +``` + +**On the Raspberry Pi:** + +Complete Raspberry Pi build instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#install_rpi_standalone). + +**On Windows:** + +[FreeImage](http://downloads.sourceforge.net/freeimage/FreeImage3154Win32.zip) + +[FreeType2](http://download.savannah.gnu.org/releases/freetype/freetype-2.4.9.tar.bz2) (you'll need to compile) + +[SDL2](http://www.libsdl.org/release/SDL2-devel-2.0.8-VC.zip) + +[cURL](http://curl.haxx.se/download.html) (you'll need to compile or get the pre-compiled DLL version) + +[RapisJSON](https://github.com/tencent/rapidjson) (you'll need the `include/rapidsjon` added to the include path) + +(Remember to copy necessary .DLLs into the same folder as the executable: probably FreeImage.dll, freetype6.dll, SDL2.dll, libcurl.dll, and zlib1.dll. Exact list depends on if you built your libraries in "static" mode or not.) + +[CMake](http://www.cmake.org/cmake/resources/software.html) (this is used for generating the Visual Studio project) + +(If you don't know how to use CMake, here are some hints: run cmake-gui and point it at your EmulationStation folder. Point the "build" directory somewhere - I use EmulationStation/build. Click configure, choose "Visual Studio [year] Project", fill in red fields as they appear and keep clicking Configure (you may need to check "Advanced"), then click Generate.) + + +Configuring +=========== + +**~/.emulationstation/es_systems.cfg:** +When first run, an example systems configuration file will be created at `~/.emulationstation/es_systems.cfg`. `~` is `$HOME` on Linux, and `%HOMEPATH%` on Windows. This example has some comments explaining how to write the configuration file. See the "Writing an es_systems.cfg" section for more information. + +**Keep in mind you'll have to set up your emulator separately from EmulationStation!** + +**~/.emulationstation/es_input.cfg:** +When you first start EmulationStation, you will be prompted to configure an input device. The process is thus: + +1. Hold a button on the device you want to configure. This includes the keyboard. + +2. Press the buttons as they appear in the list. Some inputs can be skipped by holding any button down for a few seconds (e.g. page up/page down). + +3. You can review your mappings by pressing up and down, making any changes by pressing A. + +4. Choose "SAVE" to save this device and close the input configuration screen. + +The new configuration will be added to the `~/.emulationstation/es_input.cfg` file. + +**Both new and old devices can be (re)configured at any time by pressing the Start button and choosing "CONFIGURE INPUT".** From here, you may unplug the device you used to open the menu and plug in a new one, if necessary. New devices will be appended to the existing input configuration file, so your old devices will remain configured. + +**If your controller stops working, you can delete the `~/.emulationstation/es_input.cfg` file to make the input configuration screen re-appear on next run.** + + +You can use `--help` or `-h` to view a list of command-line options. Briefly outlined here: +``` --resolution [width] [height] try and force a particular resolution --gamelist-only skip automatic game search, only read from gamelist.xml --ignore-gamelist ignore the gamelist (useful for troubleshooting) @@ -112,107 +150,107 @@ You can use `--help` or `-h` to view a list of command-line options. Briefly out --force-kid Force the UI mode to be Kid --force-kiosk Force the UI mode to be Kiosk --force-disable-filters Force the UI to ignore applied filters in gamelist ---help, -h summon a sentient, angry tuba -``` - -As long as ES hasn't frozen, you can always press F4 to close the application. - - -Writing an es_systems.cfg -========================= - -Complete configuration instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#config). - -The `es_systems.cfg` file contains the system configuration data for EmulationStation, written in XML. This tells EmulationStation what systems you have, what platform they correspond to (for scraping), and where the games are located. - -ES will check two places for an es_systems.cfg file, in the following order, stopping after it finds one that works: -* `~/.emulationstation/es_systems.cfg` -* `/etc/emulationstation/es_systems.cfg` - -The order EmulationStation displays systems reflects the order you define them in. - -**NOTE:** A system *must* have at least one game present in its "path" directory, or ES will ignore it! If no valid systems are found, ES will report an error and quit! - -Here's an example es_systems.cfg: - -```xml - - - - - - - snes - - - Super Nintendo Entertainment System - - - ~/roms/snes - - - .smc .sfc .SMC .SFC - - - snesemulator %ROM% - - - - snes - - - snes - - -``` - -The following "tags" are replaced by ES in launch commands: - -`%ROM%` - Replaced with absolute path to the selected ROM, with most Bash special characters escaped with a backslash. - -`%BASENAME%` - Replaced with the "base" name of the path to the selected ROM. For example, a path of "/foo/bar.rom", this tag would be "bar". This tag is useful for setting up AdvanceMAME. - -`%ROM_RAW%` - Replaced with the unescaped, absolute path to the selected ROM. If your emulator is picky about paths, you might want to use this instead of %ROM%, but enclosed in quotes. - -See [SYSTEMS.md](SYSTEMS.md) for some live examples in EmulationStation. - -gamelist.xml -============ - -The gamelist.xml file for a system defines metadata for games, such as a name, image (like a screenshot or box art), description, release date, and rating. - -If at least one game in a system has an image specified, ES will use the detailed view for that system (which displays metadata alongside the game list). - -*You can use ES's [scraping](http://en.wikipedia.org/wiki/Web_scraping) tools to avoid creating a gamelist.xml by hand.* There are two ways to run the scraper: - -* **If you want to scrape multiple games:** press start to open the menu and choose the "SCRAPER" option. Adjust your settings and press "SCRAPE NOW". -* **If you just want to scrape one game:** find the game on the game list in ES and press select. Choose "EDIT THIS GAME'S METADATA" and then press the "SCRAPE" button at the bottom of the metadata editor. - -You can also edit metadata within ES by using the metadata editor - just find the game you wish to edit on the gamelist, press Select, and choose "EDIT THIS GAME'S METADATA." - -A command-line version of the scraper is also provided - just run emulationstation with `--scrape` *(currently broken)*. - -The switch `--ignore-gamelist` can be used to ignore the gamelist and force ES to use the non-detailed view. - -If you're writing a tool to generate or parse gamelist.xml files, you should check out [GAMELISTS.md](GAMELISTS.md) for more detailed documentation. - - -Themes -====== - -By default, EmulationStation looks pretty ugly. You can fix that. If you want to know more about making your own themes (or editing existing ones), read [THEMES.md](THEMES.md)! - -I've put some themes up for download on my EmulationStation webpage: http://aloshi.com/emulationstation#themes - -If you're using RetroPie, you should already have a nice set of themes automatically installed! - - --Alec "Aloshi" Lofquist -http://www.aloshi.com -http://www.emulationstation.org +--help, -h summon a sentient, angry tuba +``` + +As long as ES hasn't frozen, you can always press F4 to close the application. + + +Writing an es_systems.cfg +========================= + +Complete configuration instructions at [emulationstation.org](http://emulationstation.org/gettingstarted.html#config). + +The `es_systems.cfg` file contains the system configuration data for EmulationStation, written in XML. This tells EmulationStation what systems you have, what platform they correspond to (for scraping), and where the games are located. + +ES will check two places for an es_systems.cfg file, in the following order, stopping after it finds one that works: +* `~/.emulationstation/es_systems.cfg` +* `/etc/emulationstation/es_systems.cfg` + +The order EmulationStation displays systems reflects the order you define them in. + +**NOTE:** A system *must* have at least one game present in its "path" directory, or ES will ignore it! If no valid systems are found, ES will report an error and quit! + +Here's an example es_systems.cfg: + +```xml + + + + + + + snes + + + Super Nintendo Entertainment System + + + ~/roms/snes + + + .smc .sfc .SMC .SFC + + + snesemulator %ROM% + + + + snes + + + snes + + +``` + +The following "tags" are replaced by ES in launch commands: + +`%ROM%` - Replaced with absolute path to the selected ROM, with most Bash special characters escaped with a backslash. + +`%BASENAME%` - Replaced with the "base" name of the path to the selected ROM. For example, a path of "/foo/bar.rom", this tag would be "bar". This tag is useful for setting up AdvanceMAME. + +`%ROM_RAW%` - Replaced with the unescaped, absolute path to the selected ROM. If your emulator is picky about paths, you might want to use this instead of %ROM%, but enclosed in quotes. + +See [SYSTEMS.md](SYSTEMS.md) for some live examples in EmulationStation. + +gamelist.xml +============ + +The gamelist.xml file for a system defines metadata for games, such as a name, image (like a screenshot or box art), description, release date, and rating. + +If at least one game in a system has an image specified, ES will use the detailed view for that system (which displays metadata alongside the game list). + +*You can use ES's [scraping](http://en.wikipedia.org/wiki/Web_scraping) tools to avoid creating a gamelist.xml by hand.* There are two ways to run the scraper: + +* **If you want to scrape multiple games:** press start to open the menu and choose the "SCRAPER" option. Adjust your settings and press "SCRAPE NOW". +* **If you just want to scrape one game:** find the game on the game list in ES and press select. Choose "EDIT THIS GAME'S METADATA" and then press the "SCRAPE" button at the bottom of the metadata editor. + +You can also edit metadata within ES by using the metadata editor - just find the game you wish to edit on the gamelist, press Select, and choose "EDIT THIS GAME'S METADATA." + +A command-line version of the scraper is also provided - just run emulationstation with `--scrape` *(currently broken)*. + +The switch `--ignore-gamelist` can be used to ignore the gamelist and force ES to use the non-detailed view. + +If you're writing a tool to generate or parse gamelist.xml files, you should check out [GAMELISTS.md](GAMELISTS.md) for more detailed documentation. + + +Themes +====== + +By default, EmulationStation looks pretty ugly. You can fix that. If you want to know more about making your own themes (or editing existing ones), read [THEMES.md](THEMES.md)! + +I've put some themes up for download on my EmulationStation webpage: http://aloshi.com/emulationstation#themes + +If you're using RetroPie, you should already have a nice set of themes automatically installed! + + +-Alec "Aloshi" Lofquist +http://www.aloshi.com +http://www.emulationstation.org From 33a855df3521a641d0291cac83077192287bcbd4 Mon Sep 17 00:00:00 2001 From: fabricecaruso <51082152+fabricecaruso@users.noreply.github.com> Date: Mon, 27 May 2019 14:10:32 +0200 Subject: [PATCH 026/336] Update README.md --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 52e38b50b0..b4c9a4f011 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Changes in my branch - Support for Multiple Emulators/Cores in es_systems.cfg, and setting Emulator/Core per game. +```xml %HOME%\RetroArch\retroarch.exe -L %HOME%\RetroArch\cores\%CORE%_libretro.dll %ROM% @@ -23,7 +24,7 @@ Changes in my branch - +``` - Better Grid view (animations, layout, selection...) - Created a 'GridEx' mode, which inherits from grid, in order to create a 'preview bar' including a game screenshot. - Changed 'Automatic' view mode. Switchs to grid if "thumbnails" exist in gamelist. Switchs to gridex if "thumbnails" and "image" exist in gamelist. From a37e5b5fd3a470938a815df088b4b6b4d6aa79b6 Mon Sep 17 00:00:00 2001 From: Fab Date: Mon, 27 May 2019 21:46:37 +0200 Subject: [PATCH 027/336] [Fonts] Change favorite icon with a simple glyph font --- es-core/src/resources/Font.cpp | 2 +- resources/fontawesome_webfont.ttf | Bin 120456 -> 0 bytes resources/glyphs.ttf | Bin 0 -> 1904 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 resources/fontawesome_webfont.ttf create mode 100644 resources/glyphs.ttf diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index d5086b7e05..b32cdec546 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -249,7 +249,7 @@ std::vector getFallbackFontPaths() std::string path = fontDir + fontNames[i]; if (i == 0) - path = ":/fontawesome_webfont.ttf"; + path = ":/glyphs.ttf"; if(ResourceManager::getInstance()->fileExists(path)) fontPaths.push_back(path); diff --git a/resources/fontawesome_webfont.ttf b/resources/fontawesome_webfont.ttf deleted file mode 100644 index 59dec23e2af3290758890eee55e9f619e6c48202..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120456 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 diff --git a/resources/glyphs.ttf b/resources/glyphs.ttf new file mode 100644 index 0000000000000000000000000000000000000000..f6cc4b6a511ac0af60b461af5364f5b398924d43 GIT binary patch literal 1904 zcmdT^TWDNG82)F^=4^L2mtC_-nl^1FsixX=H)qdocH1PC(6os~q)m)b1ZhvRvlrZ( zXHRZYFi>l4@j>t!TM&$Vu^R8TR zDLed?|0HzRz}NG_%7j*K{sH~R;Hf+WzNGdU^e=!vnlCwvdbjI;7d%v~%v#W4Ezb&g zq+~5t$wxzwT>$50t0Y2GZ@mHiI+hJqD>diI?%RvdcR_!YG_8fbO%1@0@A$rkbW02U ziybf685&*F*K|#63ToF7lX{T_G!J?1VB#*sK(x6*GD|y~7b30lTzu@7~`GMUqF+HowtsXfIJKvPCSF?mO6bQ1|J+_72^b zO1F3Pv53ne2YTz8zO-vJn8e(gN|+t(^7CBnfBo3%-pRd_Bddom26*av>Uw}RHJ`lN z_*h44(E&KPR-%Gvk-_!rv$m+i5x@L~C zi%sDZmnVMh(xWf-WSc_WXTM0Sgd5wm<}T*#?0o){0GB0o+bG8SecjC$@;ZI_Lc`gX zk-sSD=9a$ybmL8F{bTr5+(mU_+)td}t(&J?YU>Ri8l-G<)6IGtlR1{jV$|$fNbCWY zOkyukS3M5cZaSne!%aP|um|{*!Wwnb8HK%&7Zh$lovRA#)IuLB?4wEgTwy;2SU}+* zwXul8&9t9AA@}X|V-E(KP#Ak=3kqY;?2^J7nd}{fv43`3;Rf2xzEfC7%-?97ESkqy zp)xr%MS^lPM@2~OtvVj%W45pyk>LvkC(oyss)8qC=D0OKUMV|MA~#pGY{jCAMolAr zbZBtMOe@8Vuxo`%na5*>G5m~W&*phNahSvG$gm9KE>W?0JRTd288I_y#(63kJzsFP zMLts$R!wkQWQEP03U~5?k5#Hmb|IH{cumYYa+xebNlg}^xno-yQL^lpx#c)^;ryIy zQm#0KSy79fu;vAyu*#XGyyH}d1_q>$wIFJhl86;54-^g*;aaYV0pb)zA4AdvQs-dI zfG?o<4&{NSX$i#_KnaSGxiz&9OFQCjElL#L2F3B5hiDMpr1Z9EGg!x_8rGIY<*<#x z!`NoEJ+8X#^2YC1w{BG46g-S^$h`PF%F+F2t?76@R3Bj>A0hmhE+0|7-JcbGh;WO= K|DXJ4p8f$kvL^cg literal 0 HcmV?d00001 From d4d104d639601d3d6962fcfdbe46b1ed2247364a Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 27 May 2019 13:57:26 +0200 Subject: [PATCH 028/336] [Add French localization resources] --- resources/fontawesome_webfont.ttf | Bin 0 -> 120456 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 resources/fontawesome_webfont.ttf diff --git a/resources/fontawesome_webfont.ttf b/resources/fontawesome_webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..59dec23e2af3290758890eee55e9f619e6c48202 GIT binary patch literal 120456 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 literal 0 HcmV?d00001 From 6c2129562ecaaebfc59c0f6202461cc58576d34b Mon Sep 17 00:00:00 2001 From: Fab Date: Mon, 27 May 2019 21:46:37 +0200 Subject: [PATCH 029/336] [Fonts] Change favorite icon with a simple glyph font --- resources/fontawesome_webfont.ttf | Bin 120456 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 resources/fontawesome_webfont.ttf diff --git a/resources/fontawesome_webfont.ttf b/resources/fontawesome_webfont.ttf deleted file mode 100644 index 59dec23e2af3290758890eee55e9f619e6c48202..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 120456 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 From 7ebb400212f4e5197f9b7e0d7bf97b7a67f237ec Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 28 May 2019 16:33:14 +0200 Subject: [PATCH 030/336] [Theming] Bad loading for themes using variables [SplashScreen] Progressbar + simplified splash.svg [Favorites] Better icon + Refresh list when switching favorite [Main Menu] Animations when opening [GameLaunch] Show splash.svg when launching ( when option hide when launch game is disabled ) [Font] Bad sizing when favorite glyph is loaded with small fonts. --- es-app/src/CollectionSystemManager.cpp | 17 ++- es-app/src/EmulationStation.h | 1 + es-app/src/SystemData.cpp | 21 ++- es-app/src/guis/GuiGamelistOptions.cpp | 29 +++- es-app/src/guis/GuiMenu.cpp | 42 ++++-- es-app/src/main.cpp | 16 +-- es-app/src/scrapers/ScreenScraper.h | 2 +- es-app/src/views/SystemView.cpp | 2 +- es-app/src/views/ViewController.cpp | 24 ++-- es-core/src/Settings.cpp | 4 + es-core/src/ThemeData.cpp | 140 ++++++++++++++++--- es-core/src/ThemeData.h | 4 +- es-core/src/Window.cpp | 96 ++++++++++--- es-core/src/Window.h | 9 +- es-core/src/components/GridTileComponent.cpp | 7 +- es-core/src/platform.cpp | 10 +- es-core/src/resources/Font.cpp | 35 ++--- es-core/src/resources/TextureData.cpp | 5 +- es-core/src/utils/FileSystemUtil.cpp | 2 +- resources/locale/fr/emulationstation2.po | 4 + resources/splash.svg | 91 +++++------- 21 files changed, 396 insertions(+), 165 deletions(-) diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index bd6eb63e91..16af995b20 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -519,20 +519,29 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) } else { - file->getSourceFileData()->getSystem()->getIndex()->removeFromIndex(file); + SystemData* sysData = file->getSourceFileData()->getSystem(); + sysData->getIndex()->removeFromIndex(file); + MetaDataList* md = &file->getSourceFileData()->metadata; + std::string value = md->get("favorite"); if (value == "false") - { md->set("favorite", "true"); - } else { adding = false; md->set("favorite", "false"); } - file->getSourceFileData()->getSystem()->getIndex()->addToIndex(file); + sysData->getIndex()->addToIndex(file); + refreshCollectionSystems(file->getSourceFileData()); + + SystemData* systemViewToUpdate = getSystemToView(sysData); + if (systemViewToUpdate != NULL) + { + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); + ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(file, FILE_METADATA_CHANGED); + } } if (adding) { diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index 57389a1055..375b11d9cf 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -8,6 +8,7 @@ #define PROGRAM_VERSION_MINOR 10 #define PROGRAM_VERSION_MAINTENANCE 0 #define PROGRAM_VERSION_STRING "2.10.0 FCAMOD" +#define SCRAPER_VERSION_STRING "2.10.0" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 4052801b75..adae7852e0 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -16,6 +16,7 @@ #include #endif +#include "GuiComponent.h" #include "Window.h" std::vector SystemData::sSystemVector; @@ -218,11 +219,15 @@ bool SystemData::loadConfig(Window* window) LOG(LogError) << "es_systems.cfg is missing the tag!"; return false; } - + + float systemCount = 1; + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + systemCount++; + + float currentSystem = 0; for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) { - std::vector emulatorList; - + std::vector emulatorList; // std::vector coreList; @@ -262,7 +267,9 @@ bool SystemData::loadConfig(Window* window) } if (window != NULL) - window->renderLoadingScreen(fullname); + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; // convert extensions list from a string into a vector of strings @@ -346,7 +353,9 @@ bool SystemData::loadConfig(Window* window) } if (window != NULL) - window->renderLoadingScreen("Favoris"); + window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; CollectionSystemManager::get()->loadCollectionSystems(); return true; @@ -575,7 +584,7 @@ void SystemData::loadTheme() sysData.insert(std::pair("system.theme", getThemeFolder())); sysData.insert(std::pair("system.fullName", getFullName())); - mTheme->loadFile(getThemeFolder(), path); + mTheme->loadFile(getThemeFolder(), sysData, path); } catch(ThemeException& e) { diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index b789667da8..01c0741c21 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -11,6 +11,8 @@ #include "GuiMetaDataEd.h" #include "SystemData.h" +#include "animations/LambdaAnimation.h" + GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : GuiComponent(window), mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false) { @@ -118,7 +120,32 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui // center the menu setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); + //mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); + + float x0 = (mSize.x() - mMenu.getSize().x()) / 2; + + float y1 = Renderer::getScreenHeight(); + float y2 = (mSize.y() - mMenu.getSize().y()) / 2; + //float y1 = mMenu.getSize().y(); + //float y2 = (mSize.y() - mMenu.getSize().y()) / 2; + + setPosition(x0, y1); + + auto fadeFunc = [this, x0, y1, y2](float t) { + + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + float y = y1 * (1 - pct) + y2 * pct; + setPosition(x0, y); + }; + + setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, x0, y2] + { + setPosition(x0, y2); + }); + + setPosition(x0, y2); } GuiGamelistOptions::~GuiGamelistOptions() diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 4b1cf2aea6..a383ee2ff4 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -19,12 +19,13 @@ #include #include +#include "animations/LambdaAnimation.h" + GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) { bool isFullUI = UIModeController::getInstance()->isUIModeFull(); - - + if (isFullUI) { addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); @@ -41,15 +42,38 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); } - - + +#if defined(_WIN32) addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); +#else + addEntry(_T("QUIT"), 0x777777FF, true, [this] {openQuitMenu(); }); +#endif addChild(&mMenu); addVersionInfo(); setSize(mMenu.getSize()); - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, Renderer::getScreenHeight() * 0.15f); + + float y1 = Renderer::getScreenHeight(); + float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); + + auto fadeFunc = [this, y1, y2](float t) { + + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + float y = y1 * (1 - pct) + y2 * pct; + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y); + }; + + setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] + { + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + }); + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); } void GuiMenu::openScraperSettings() @@ -402,13 +426,13 @@ void GuiMenu::openUISettings() screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); s->addRow(screensaver_row); - +#if defined(_WIN32) // quick system select (left/right in game list view) auto hideWindowScreen = std::make_shared(mWindow); hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); - +#endif // quick system select (left/right in game list view) auto quick_sys_select = std::make_shared(mWindow); @@ -551,9 +575,11 @@ void GuiMenu::openConfigInput() void GuiMenu::openQuitMenu() { +#if defined(_WIN32) Scripting::fireEvent("quit"); quitES(""); return; +#endif auto s = new GuiSettings(mWindow, _T("QUIT")); @@ -573,8 +599,6 @@ void GuiMenu::openQuitMenu() row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); s->addRow(row); - - if(Settings::getInstance()->getBool("ShowExit")) { row.elements.clear(); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 136bee9434..2a08c2c583 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -322,7 +322,7 @@ int main(int argc, char* argv[]) bool splashScreen = Settings::getInstance()->getBool("SplashScreen"); bool splashScreenProgress = Settings::getInstance()->getBool("SplashScreenProgress"); - if(!scrape_cmdline) + if (!scrape_cmdline) { if(!window.init()) { @@ -333,15 +333,9 @@ int main(int argc, char* argv[]) std::string glExts = (const char*)glGetString(GL_EXTENSIONS); LOG(LogInfo) << "Checking available OpenGL extensions..."; LOG(LogInfo) << " ARB_texture_non_power_of_two: " << (glExts.find("ARB_texture_non_power_of_two") != std::string::npos ? "ok" : "MISSING"); - if(splashScreen) - { - std::string progressText = "Chargement"; - - if (splashScreenProgress) - progressText = "Chargement de la configuration"; - window.renderLoadingScreen(progressText); - } + if (splashScreen) + window.renderLoadingScreen(_T("Loading...")); } const char* errorMsg = NULL; @@ -383,7 +377,7 @@ int main(int argc, char* argv[]) window.renderLoadingScreen(_T("Starting UI")); //choose which GUI to open depending on if an input configuration already exists - if(errorMsg == NULL) + if (errorMsg == NULL) { if (Utils::FileSystem::exists(InputManager::getConfigPath()) && InputManager::getInstance()->getNumConfiguredDevices() > 0) ViewController::get()->goToStart(true); @@ -394,6 +388,8 @@ int main(int argc, char* argv[]) //generate joystick events since we're done loading SDL_JoystickEventState(SDL_ENABLE); + window.endRenderLoadingScreen(); + int lastTime = SDL_GetTicks(); int ps_time = SDL_GetTicks(); diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index 55c4a89c4f..24063744e2 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -28,7 +28,7 @@ class ScreenScraperRequest : public ScraperHttpRequest const std::string API_DEV_P = { 108, 28, 54, 55, 83, 43, 91, 44, 30, 22, 41, 12, 0, 108, 38, 29 }; const std::string API_DEV_KEY = { 54, 73, 115, 100, 101, 67, 111, 107, 79, 66, 68, 66, 67, 56, 118, 77, 54, 88, 101, 54 }; const std::string API_URL_BASE = "https://www.screenscraper.fr/api2"; - const std::string API_SOFT_NAME = "Emulationstation 2.9.0"; // +static_cast(PROGRAM_VERSION_STRING); + const std::string API_SOFT_NAME = "Emulationstation "+static_cast(SCRAPER_VERSION_STRING); /** Which type of image artwork we need. Possible values (not a comprehensive list): - ss: in-game screenshot diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 9ed1a22dc6..41bc9fddfe 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -271,7 +271,7 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) }, goFast ? 10 : 300); // wait 600ms to fade in - setAnimation(infoFadeIn, goFast ? 0 : 2000, nullptr, false, 2); + setAnimation(infoFadeIn, goFast ? 0 : 500, nullptr, false, 2); // no need to animate transition, we're not going anywhere (probably mEntries.size() == 1) if(endPos == mCamOffset && endPos == mExtrasCamOffset) diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 65fb1b4122..37c61dbbab 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -61,12 +61,13 @@ void ViewController::goToStart(bool forceImmediate) // Requested system doesn't exist Settings::getInstance()->setString("StartupSystem", ""); } + goToSystemView(SystemData::sSystemVector.at(0), forceImmediate); } void ViewController::ReloadAndGoToStart() { - mWindow->renderLoadingScreen("Chargement"); + mWindow->renderLoadingScreen(_T("Loading...")); ViewController::get()->reloadAll(); ViewController::get()->goToStart(false); @@ -235,8 +236,8 @@ void ViewController::launch(FileData* game, Vector3f center) setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this, game, fadeFunc] { game->launchGame(mWindow); - setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this] { mLockInput = false; }, true); - this->onFileChanged(game, FILE_METADATA_CHANGED); + setAnimation(new LambdaAnimation(fadeFunc, 800), 0, [this] { mLockInput = false; mWindow->endRenderLoadingScreen(); }, true); + this->onFileChanged(game, FILE_METADATA_CHANGED); }); } else if (transition_style == "slide"){ // move camera to zoom in on center + fade out, launch game, come back in @@ -244,7 +245,7 @@ void ViewController::launch(FileData* game, Vector3f center) { game->launchGame(mWindow); mCamera = origCamera; - setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 600), 0, [this] { mLockInput = false; }, true); + setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 600), 0, [this] { mLockInput = false; mWindow->endRenderLoadingScreen(); }, true); this->onFileChanged(game, FILE_METADATA_CHANGED); }); } else { // instant @@ -252,7 +253,7 @@ void ViewController::launch(FileData* game, Vector3f center) { game->launchGame(mWindow); mCamera = origCamera; - setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 10), 0, [this] { mLockInput = false; }, true); + setAnimation(new LaunchAnimation(mCamera, mFadeOpacity, center, 10), 0, [this] { mLockInput = false; mWindow->endRenderLoadingScreen(); }, true); this->onFileChanged(game, FILE_METADATA_CHANGED); }); } @@ -420,7 +421,7 @@ bool ViewController::input(InputConfig* config, Input input) return true; } - if(mCurrentView) + if (mCurrentView) return mCurrentView->input(config, input); return false; @@ -468,10 +469,15 @@ void ViewController::render(const Transform4x4f& parentTrans) mWindow->renderHelpPromptsEarly(); // fade out - if(mFadeOpacity) + if (mFadeOpacity) { - Renderer::setMatrix(parentTrans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(mFadeOpacity * 255)); + if (Settings::getInstance()->getBool("HideWindow")) + { + Renderer::setMatrix(parentTrans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(mFadeOpacity * 255)); + } + else + mWindow->renderGameLoadingScreen(mFadeOpacity, false); } } diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index a63c75e9d3..41f2e16de4 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -92,7 +92,11 @@ void Settings::setDefaults() mIntMap["MaxVRAM"] = 100; #endif +#if defined(_WIN32) + mBoolMap["HideWindow"] = false; +#else mBoolMap["HideWindow"] = true; +#endif mStringMap["GameTransitionStyle"] = "fade"; mStringMap["TransitionStyle"] = "slide"; mStringMap["Language"] = "en"; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 52f75760cc..9ee8ea2fc9 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -203,7 +203,7 @@ ThemeData::ThemeData() mVersion = 0; } -void ThemeData::loadFile(std::string system, const std::string& path) +void ThemeData::loadFile(std::string system, std::map sysDataMap, const std::string& path) { mPaths.push_back(path); @@ -219,8 +219,7 @@ void ThemeData::loadFile(std::string system, const std::string& path) mSystemThemeFolder = system; mVariables.clear(); - //mVariables.insert(system, system); - //mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); + mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); pugi::xml_document doc; pugi::xml_parse_result res = doc.load_file(path.c_str()); @@ -263,6 +262,67 @@ std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolde return result; } +bool ThemeData::parseSubset(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("subset")) + { + parse = false; + const std::string subsetAttr = node.attribute("subset").as_string(); + const std::string nameAttr = node.attribute("name").as_string(); + + if (subsetAttr == "iconset" || subsetAttr == "gamelistview") + { + if (nameAttr.rfind("1-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (subsetAttr == "systemview") + { + if (nameAttr.rfind("8-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (nameAttr.rfind("2-") != std::string::npos) + { + parse = true; + return parse; + } + /* + if (subsetAttr == "colorset" && nameAttr == mColorset) + { + parse = true; + return parse; + } + if (subsetAttr == "iconset" && nameAttr == mIconset) + { + parse = true; + return parse; + } + if (subsetAttr == "menu" && nameAttr == mMenu) + { + parse = true; + return parse; + } + if (subsetAttr == "systemview" && nameAttr == mSystemview) + { + parse = true; + return parse; + } + if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) + { + parse = true; + return parse; + }*/ + } + + return parse; +} void ThemeData::parseIncludes(const pugi::xml_node& root) { @@ -271,6 +331,9 @@ void ThemeData::parseIncludes(const pugi::xml_node& root) for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) { + if (!parseSubset(node)) + continue; + std::string relPath = resolvePlaceholders(node.text().as_string()); std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); path = resolveSystemVariable(mSystemThemeFolder, path); @@ -391,23 +454,56 @@ void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) if(elemTypeIt == sElementMap.cend()) throw error << "Unknown element of type \"" << node.name() << "\"!"; + if (parseRegion(node)) + { + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + parseElement(node, elemTypeIt->second, + view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); + + if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) + view.orderedKeys.push_back(elemKey); + } + } + } +} + +bool ThemeData::parseRegion(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("region")) + { + std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); + + parse = false; const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); + const std::string nameAttr = node.attribute("region").as_string(); size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while(off != std::string::npos || prevOff != std::string::npos) + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) { std::string elemKey = nameAttr.substr(prevOff, off - prevOff); prevOff = nameAttr.find_first_not_of(delim, off); off = nameAttr.find_first_of(delim, prevOff); - - parseElement(node, elemTypeIt->second, - view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); - - if(std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) - view.orderedKeys.push_back(elemKey); + if (elemKey == regionsetting) + { + parse = true; + return parse; + } } + } + return parse; + } @@ -450,16 +546,28 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapfileExists(path)) + + if (!ResourceManager::getInstance()->fileExists(path)) + { + std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); + if (ResourceManager::getInstance()->fileExists(rootPath)) + path = rootPath; + } + + if (!ResourceManager::getInstance()->fileExists(path)) { std::stringstream ss; ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda ss << "could not find file \"" << node.text().get() << "\" "; if(node.text().get() != path) ss << "(which resolved to \"" << path << "\") "; + LOG(LogWarning) << ss.str(); + OutputDebugString(ss.str().c_str()); } - element.properties[node.name()] = path; + else + element.properties[node.name()] = path; + break; } case COLOR: @@ -525,8 +633,8 @@ const std::shared_ptr& ThemeData::getDefault() { try { - //std::map emptyMap; - theme->loadFile("", path); // emptyMap + std::map emptyMap; + theme->loadFile("", emptyMap, path); } catch(ThemeException& e) { LOG(LogError) << e.what(); diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 6bbcc01555..aec760c4c7 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -135,7 +135,7 @@ class ThemeData ThemeData(); // throws ThemeException - void loadFile(std::string system, const std::string& path); + void loadFile(std::string system, std::map sysDataMap, const std::string& path); enum ElementPropertyType { @@ -178,6 +178,8 @@ class ThemeData void parseViews(const pugi::xml_node& themeRoot); void parseView(const pugi::xml_node& viewNode, ThemeView& view); void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element); + bool parseRegion(const pugi::xml_node& node); + bool parseSubset(const pugi::xml_node& node); std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 05337b0e01..1b361f408e 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -11,11 +11,15 @@ #include #include +#include + Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) { mHelp = new HelpComponent(this); mBackgroundOverlay = new ImageComponent(this); + mSplash = NULL; + } Window::~Window() @@ -297,58 +301,104 @@ void Window::setAllowSleep(bool sleep) mAllowSleep = sleep; } -void Window::renderLoadingScreen(std::string text) +void Window::endRenderLoadingScreen() +{ + mSplash = NULL; +} + +void Window::renderLoadingScreen(std::string text, float percent) { +#if defined(_WIN32) + // Avoid Window Freezing on Windows + SDL_Event event; + while (SDL_PollEvent(&event)) ; +#endif + Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); + if (percent > 0) + { + float baseHeight = 0.04f; + + float w = Renderer::getScreenWidth() / 2; + float h = Renderer::getScreenHeight() * baseHeight; + + float x = Renderer::getScreenWidth() / 2 - w / 2; + float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); + + Renderer::drawRect(x, y, w, h, 0x252525FF); + Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF + } + + ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.5f, 0.0f); - splash.setImage(":/splash.svg"); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); splash.render(trans); + auto& font = mDefaultFonts.at(1); TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.835f); + float y = Math::round(Renderer::getScreenHeight() * 0.8f); // 35 trans = trans.translate(Vector3f(x, y, 0.0f)); Renderer::setMatrix(trans); font->renderTextCache(cache); delete cache; + Renderer::swapBuffers(); } -void Window::renderBlackScreen(std::string text) +void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) { Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); - /* + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); + ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.5f, 0.0f); - splash.setImage(":/splash.svg"); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); splash.render(trans); - */ + + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); - if (text.length() > 0) - { - auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); - - float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.835f); - trans = trans.translate(Vector3f(x, y, 0.0f)); - Renderer::setMatrix(trans); - font->renderTextCache(cache); - delete cache; - } + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.835f); + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; - Renderer::swapBuffers(); + if (swapBuffers) + Renderer::swapBuffers(); } diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 5fd238dc62..0c64a959fa 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -16,6 +16,8 @@ class ImageComponent; class InputConfig; class TextCache; class Transform4x4f; +class TextureResource; + struct HelpStyle; class Window @@ -64,8 +66,9 @@ class Window bool getAllowSleep(); void setAllowSleep(bool sleep); - void renderLoadingScreen(std::string text); - void renderBlackScreen(std::string text); + void endRenderLoadingScreen(); + void renderLoadingScreen(std::string text, float percent = -1); + void renderGameLoadingScreen(float opacity=1, bool swapBuffers=true); void renderHelpPromptsEarly(); // used to render HelpPrompts before a fade void setHelpPrompts(const std::vector& prompts, const HelpStyle& style); @@ -91,6 +94,8 @@ class Window InfoPopup* mInfoPopup; bool mRenderScreenSaver; + std::shared_ptr mSplash; + std::vector mGuiStack; std::vector< std::shared_ptr > mDefaultFonts; diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 310830f4fb..cccf549cfe 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -302,10 +302,13 @@ void GridTileComponent::setSelected(bool selected, Vector3f* pPosition) auto func = [this](float t) { - this->setSelectedZoom(t); + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + this->setSelectedZoom(pct); }; - setAnimation(new LambdaAnimation(func, 100), 0, [this] { + setAnimation(new LambdaAnimation(func, 200), 0, [this] { this->setSelectedZoom(1); mAnimPosition = Vector3f(0, 0, 0); }, false, 3); diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index d47f6204a0..e80e589518 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -1,16 +1,18 @@ #include "platform.h" - #include + #ifdef WIN32 #include +#include #else #include #endif #include -#include #include "Window.h" +#include "GuiComponent.h" + int runShutdownCommand() { #ifdef WIN32 // windows @@ -84,7 +86,7 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo { #ifdef WIN32 if (window != NULL) - window->renderBlackScreen("Chargement en cours..."); + window->renderGameLoadingScreen(); // on Windows we use _wsystem to support non-ASCII paths // which requires converting from utf8 to a wstring @@ -124,7 +126,7 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo polled = true; if (window != NULL && polled) - window->renderBlackScreen("Chargement en cours..."); + window->renderGameLoadingScreen(); } } diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index b32cdec546..9890954a82 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -78,9 +78,11 @@ Font::Font(int size, const std::string& path) : mSize(size), mPath(path) initLibrary(); // always initialize ASCII characters -// for(unsigned int i = 32; i < 128; i++) - // getGlyph(i); + for (unsigned int i = 32; i < 128; i++) + getGlyph(i); + // getGlyph(61446); + clearFaceCache(); } @@ -140,7 +142,7 @@ Font::FontTexture::~FontTexture() bool Font::FontTexture::findEmpty(const Vector2i& size, Vector2i& cursor_out) { - if(size.x() >= textureSize.x() || size.y() >= textureSize.y()) + if (size.x() >= textureSize.x() || size.y() >= textureSize.y()) return false; if(writePos.x() + size.x() >= textureSize.x() && @@ -235,9 +237,10 @@ std::vector getFallbackFontPaths() fontDir += "\\Fonts\\"; const char* fontNames[] = { + "glyphs.ttf", // latin + "arial.ttf", // latin "meiryo.ttc", // japanese - "simhei.ttf", // chinese - "arial.ttf" // latin + "simhei.ttf" // chinese }; //prepend to font file names @@ -251,7 +254,7 @@ std::vector getFallbackFontPaths() if (i == 0) path = ":/glyphs.ttf"; - if(ResourceManager::getInstance()->fileExists(path)) + if (ResourceManager::getInstance()->fileExists(path)) fontPaths.push_back(path); } @@ -288,18 +291,17 @@ FT_Face Font::getFaceForChar(unsigned int id) for(unsigned int i = 0; i < fallbackFonts.size() + 1; i++) { auto fit = mFaceCache.find(i); - - if(fit == mFaceCache.cend()) // doesn't exist yet - { + if (fit == mFaceCache.cend()) // doesn't exist yet + { // i == 0 -> mPath // otherwise, take from fallbackFonts const std::string& path = (i == 0 ? mPath : fallbackFonts.at(i - 1)); - ResourceData data = ResourceManager::getInstance()->getFileData(path); - mFaceCache[i] = std::unique_ptr(new FontFace(std::move(data), mSize)); + ResourceData data = ResourceManager::getInstance()->getFileData(path); + mFaceCache[i] = std::unique_ptr(new FontFace(std::move(data), i == 1 && mMaxGlyphHeight > 0 ? mMaxGlyphHeight : mSize)); // Reduce size of gyphs ???? fit = mFaceCache.find(i); } - if(FT_Get_Char_Index(fit->second->face, id) != 0) + if (FT_Get_Char_Index(fit->second->face, id) != 0) return fit->second->face; } @@ -316,7 +318,7 @@ Font::Glyph* Font::getGlyph(unsigned int id) { // is it already loaded? auto it = mGlyphMap.find(id); - if(it != mGlyphMap.cend()) + if (it != mGlyphMap.cend()) return &it->second; // nope, need to make a glyph @@ -329,7 +331,7 @@ Font::Glyph* Font::getGlyph(unsigned int id) FT_GlyphSlot g = face->glyph; - if(FT_Load_Char(face, id, FT_LOAD_RENDER)) + if (FT_Load_Char(face, id, FT_LOAD_RENDER)) { LOG(LogError) << "Could not find glyph for character " << id << " for font " << mPath << ", size " << mSize << "!"; return NULL; @@ -352,9 +354,10 @@ Font::Glyph* Font::getGlyph(unsigned int id) Glyph& glyph = mGlyphMap[id]; glyph.texture = tex; + glyph.texPos = Vector2f(cursor.x() / (float)tex->textureSize.x(), cursor.y() / (float)tex->textureSize.y()); glyph.texSize = Vector2f(glyphSize.x() / (float)tex->textureSize.x(), glyphSize.y() / (float)tex->textureSize.y()); - + glyph.advance = Vector2f((float)g->metrics.horiAdvance / 64.0f, (float)g->metrics.vertAdvance / 64.0f); glyph.bearing = Vector2f((float)g->metrics.horiBearingX / 64.0f, (float)g->metrics.horiBearingY / 64.0f); @@ -364,7 +367,7 @@ Font::Glyph* Font::getGlyph(unsigned int id) glBindTexture(GL_TEXTURE_2D, 0); // update max glyph height - if(glyphSize.y() > mMaxGlyphHeight) + if (id != 61446 && glyphSize.y() > mMaxGlyphHeight) mMaxGlyphHeight = glyphSize.y(); // done diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index e2a613a859..6c472c79ed 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -164,9 +164,6 @@ bool TextureData::uploadAndBind() if (mTextureID != 0) { glBindTexture(GL_TEXTURE_2D, mTextureID); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); } else { @@ -186,7 +183,7 @@ bool TextureData::uploadAndBind() glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)mWidth, (GLsizei)mHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, mDataRGBA); glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); const GLint wrapMode = mTile ? GL_REPEAT : GL_CLAMP_TO_EDGE; glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index e4af303eb3..3c77caed8b 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -149,7 +149,7 @@ namespace Utils char* envHome = getenv("HOME"); #ifdef _DEBUG - envHome = "H:/[Emulz]/EmulationStation/"; + // envHome = "H:/[Emulz]/EmulationStation/"; #endif if (envHome) diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index 3f34a69461..ccd347f69a 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -92,6 +92,10 @@ msgstr "CREER UNE COLLECTION PERSONNALISÉE" msgid "New Collection Name" msgstr "Nom de la collection" +#: +msgid "Loading..." +msgstr "Chargement en cours..." + #: msgid "GROUP UNTHEMED CUSTOM COLLECTIONS" msgstr "GROUPER LES COLLECTIONS SANS THEME" diff --git a/resources/splash.svg b/resources/splash.svg index b15cdec4e2..7ae4f6bcc9 100644 --- a/resources/splash.svg +++ b/resources/splash.svg @@ -1,55 +1,36 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + + + + + + + + + + + + + + + + + Layer 1 + + + + + + + + + + + + + + + + + + From f2c6c19011f75db93142e380f5bad9ec6a102f6e Mon Sep 17 00:00:00 2001 From: Fab Date: Tue, 28 May 2019 18:32:28 +0200 Subject: [PATCH 031/336] [Theme] Fix theme loading --- es-app/src/FileData.h | 268 ++-- es-app/src/SystemData.cpp | 1188 +++++++------- es-app/src/SystemData.h | 40 +- es-app/src/guis/GuiGamelistOptions.cpp | 18 +- es-app/src/guis/GuiMenu.cpp | 1412 ++++++++--------- es-app/src/guis/GuiMetaDataEd.cpp | 764 ++++----- es-core/src/GuiComponent.cpp | 1164 +++++++------- es-core/src/GuiComponent.h | 444 +++--- es-core/src/ThemeData.cpp | 1457 +++++++++--------- es-core/src/Window.cpp | 1062 ++++++------- es-core/src/components/OptionListComponent.h | 16 +- es-core/src/platform.cpp | 328 ++-- es-core/src/utils/FileSystemUtil.cpp | 1334 ++++++++-------- es-core/src/utils/StringUtil.h | 2 +- 14 files changed, 4750 insertions(+), 4747 deletions(-) diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 00fd4a923b..a4a0d7e866 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -1,134 +1,134 @@ -#pragma once -#ifndef ES_APP_FILE_DATA_H -#define ES_APP_FILE_DATA_H - -#include "utils/FileSystemUtil.h" -#include "MetaData.h" -#include - -class SystemData; -class Window; -struct SystemEnvironmentData; - -enum FileType -{ - GAME = 1, // Cannot have children. - FOLDER = 2, - PLACEHOLDER = 3 -}; - -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); - -// A tree node that holds information for a file. -class FileData -{ -public: - FileData(FileType type, const std::string& path, SystemEnvironmentData* envData, SystemData* system); - virtual ~FileData(); - - virtual const std::string& getName(); - virtual const std::string& getSortName(); - inline FileType getType() const { return mType; } - inline const std::string& getPath() const { return mPath; } - inline FileData* getParent() const { return mParent; } - inline const std::unordered_map& getChildrenByFilename() const { return mChildrenByFilename; } - inline const std::vector& getChildren() const { return mChildren; } - inline SystemData* getSystem() const { return mSystem; } - inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } - virtual const std::string getThumbnailPath() const; - virtual const std::string getVideoPath() const; - virtual const std::string getMarqueePath() const; - virtual const std::string getImagePath() const; - - virtual const std::string getCore() const; - virtual const std::string getEmulator() const; - - virtual const bool getHidden(); - virtual const bool getFavorite(); - - FileData* findUniqueGameForFolder(); - - const std::vector& getChildrenListToDisplay(); - std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; - - void addChild(FileData* file); // Error if mType != FOLDER - void removeChild(FileData* file); //Error if mType != FOLDER - - inline bool isPlaceHolder() { return mType == PLACEHOLDER; }; - - virtual inline void refreshMetadata() { return; }; - - virtual std::string getKey(); - const bool isArcadeAsset(); - inline std::string getFullPath() { return getPath(); }; - inline std::string getFileName() { return Utils::FileSystem::getFileName(getPath()); }; - virtual FileData* getSourceFileData(); - inline std::string getSystemName() const { return mSystemName; }; - - // Returns our best guess at the "real" name for this file (will attempt to perform MAME name translation) - std::string getDisplayName() const; - - // As above, but also remove parenthesis - std::string getCleanName() const; - - void launchGame(Window* window); - - 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; - -protected: - FileData* mSourceFileData; - FileData* mParent; - std::string mSystemName; - //std::string mDefaultCore; - -private: - FileType mType; - std::string mPath; - SystemEnvironmentData* mEnvData; - SystemData* mSystem; - std::unordered_map mChildrenByFilename; - std::vector mChildren; - std::vector mFilteredChildren; -}; - -class CollectionFileData : public FileData -{ -public: - CollectionFileData(FileData* file, SystemData* system); - ~CollectionFileData(); - const std::string& getName(); - void refreshMetadata(); - FileData* getSourceFileData(); - std::string getKey(); -private: - // needs to be updated when metadata changes - std::string mCollectionFileName; - bool mDirty; -}; - -FileData::SortType getSortTypeFromString(std::string desc); - -#endif // ES_APP_FILE_DATA_H +#pragma once +#ifndef ES_APP_FILE_DATA_H +#define ES_APP_FILE_DATA_H + +#include "utils/FileSystemUtil.h" +#include "MetaData.h" +#include + +class SystemData; +class Window; +struct SystemEnvironmentData; + +enum FileType +{ + GAME = 1, // Cannot have children. + FOLDER = 2, + PLACEHOLDER = 3 +}; + +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); + +// A tree node that holds information for a file. +class FileData +{ +public: + FileData(FileType type, const std::string& path, SystemEnvironmentData* envData, SystemData* system); + virtual ~FileData(); + + virtual const std::string& getName(); + virtual const std::string& getSortName(); + inline FileType getType() const { return mType; } + inline const std::string& getPath() const { return mPath; } + inline FileData* getParent() const { return mParent; } + inline const std::unordered_map& getChildrenByFilename() const { return mChildrenByFilename; } + inline const std::vector& getChildren() const { return mChildren; } + inline SystemData* getSystem() const { return mSystem; } + inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } + virtual const std::string getThumbnailPath() const; + virtual const std::string getVideoPath() const; + virtual const std::string getMarqueePath() const; + virtual const std::string getImagePath() const; + + virtual const std::string getCore() const; + virtual const std::string getEmulator() const; + + virtual const bool getHidden(); + virtual const bool getFavorite(); + + FileData* findUniqueGameForFolder(); + + const std::vector& getChildrenListToDisplay(); + std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; + + void addChild(FileData* file); // Error if mType != FOLDER + void removeChild(FileData* file); //Error if mType != FOLDER + + inline bool isPlaceHolder() { return mType == PLACEHOLDER; }; + + virtual inline void refreshMetadata() { return; }; + + virtual std::string getKey(); + const bool isArcadeAsset(); + inline std::string getFullPath() { return getPath(); }; + inline std::string getFileName() { return Utils::FileSystem::getFileName(getPath()); }; + virtual FileData* getSourceFileData(); + inline std::string getSystemName() const { return mSystemName; }; + + // Returns our best guess at the "real" name for this file (will attempt to perform MAME name translation) + std::string getDisplayName() const; + + // As above, but also remove parenthesis + std::string getCleanName() const; + + void launchGame(Window* window); + + 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; + +protected: + FileData* mSourceFileData; + FileData* mParent; + std::string mSystemName; + //std::string mDefaultCore; + +private: + FileType mType; + std::string mPath; + SystemEnvironmentData* mEnvData; + SystemData* mSystem; + std::unordered_map mChildrenByFilename; + std::vector mChildren; + std::vector mFilteredChildren; +}; + +class CollectionFileData : public FileData +{ +public: + CollectionFileData(FileData* file, SystemData* system); + ~CollectionFileData(); + const std::string& getName(); + void refreshMetadata(); + FileData* getSourceFileData(); + std::string getKey(); +private: + // needs to be updated when metadata changes + std::string mCollectionFileName; + bool mDirty; +}; + +FileData::SortType getSortTypeFromString(std::string desc); + +#endif // ES_APP_FILE_DATA_H diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index adae7852e0..6389da9957 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -1,594 +1,594 @@ -#include "SystemData.h" - -#include "utils/FileSystemUtil.h" -#include "CollectionSystemManager.h" -#include "FileFilterIndex.h" -#include "FileSorts.h" -#include "Gamelist.h" -#include "Log.h" -#include "platform.h" -#include "Settings.h" -#include "ThemeData.h" -#include "views/UIModeController.h" -#include -#include -#ifdef WIN32 -#include -#endif - -#include "GuiComponent.h" -#include "Window.h" - -std::vector SystemData::sSystemVector; - -SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : - mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) -{ - mFilterIndex = new FileFilterIndex(); - - // if it's an actual system, initialize it, if not, just create the data structure - if (!CollectionSystem) - { - mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); - mRootFolder->metadata.set("name", mFullName); - - if (!Settings::getInstance()->getBool("ParseGamelistOnly")) - { - populateFolder(mRootFolder); - if (mRootFolder->getChildren().size() == 0) - return; - } - - if (!Settings::getInstance()->getBool("IgnoreGamelist")) - parseGamelist(this); - - mRootFolder->sort(FileSorts::SortTypes.at(0)); - - indexAllGameFilters(mRootFolder); - } - else - { - // virtual systems are updated afterwards, we're just creating the data structure - mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this); - } - setIsGameSystemStatus(); - loadTheme(); -} - -SystemData::~SystemData() -{ - delete mRootFolder; - delete mFilterIndex; -} - -void SystemData::setIsGameSystemStatus() -{ - // we exclude non-game systems from specific operations (i.e. the "RetroPie" system, at least) - // if/when there are more in the future, maybe this can be a more complex method, with a proper list - // but for now a simple string comparison is more performant - mIsGameSystem = (mName != "retropie"); -} - -char _easytolower(char in) { - if (in <= 'Z' && in >= 'A') - return in - ('Z' - 'z'); - return in; -} - -void SystemData::populateFolder(FileData* folder) -{ - const std::string& folderPath = folder->getPath(); - if(!Utils::FileSystem::isDirectory(folderPath)) - { - LOG(LogWarning) << "Error - folder with path \"" << folderPath << "\" is not a directory!"; - return; - } - - int di = folderPath.rfind("downloaded_images"); - int md = folderPath.rfind("media"); - if (di > 0 || md > 0) - return; - - //make sure that this isn't a symlink to a thing we already have - if(Utils::FileSystem::isSymlink(folderPath)) - { - //if this symlink resolves to somewhere that's at the beginning of our path, it's gonna recurse - if(folderPath.find(Utils::FileSystem::getCanonicalPath(folderPath)) == 0) - { - LOG(LogWarning) << "Skipping infinitely recursive symlink \"" << folderPath << "\""; - return; - } - } - - std::string filePath; - std::string extension; - bool isGame; - bool showHidden = Settings::getInstance()->getBool("ShowHiddenFiles"); - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(folderPath); - for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - filePath = *it; - - // skip hidden files and folders - if(!showHidden && Utils::FileSystem::isHidden(filePath)) - 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 = Utils::FileSystem::getExtension(filePath); - std::transform(extension.begin(), extension.end(), extension.begin(), ::_easytolower); - - //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(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) - { - FileData* newGame = new FileData(GAME, filePath, mEnvData, this); - - // preventing new arcade assets to be added - if(!newGame->isArcadeAsset()) - { - folder->addChild(newGame); - isGame = true; - } - } - - //add directories that also do not match an extension as folders - if(!isGame && Utils::FileSystem::isDirectory(filePath)) - { - if (filePath.rfind("downloaded_images") == std::string::npos && - filePath.rfind("media") == std::string::npos) - { - FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); - populateFolder(newFolder); - - if (newFolder->getChildrenByFilename().size() == 0) - delete newFolder; - else if (newFolder->findUniqueGameForFolder() != NULL) - delete newFolder; - else - folder->addChild(newFolder); - } - } - } -} - -void SystemData::indexAllGameFilters(const FileData* folder) -{ - const std::vector& children = folder->getChildren(); - - for(std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) - { - switch((*it)->getType()) - { - case GAME: { mFilterIndex->addToIndex(*it); } break; - case FOLDER: { indexAllGameFilters(*it); } break; - } - } -} - -std::vector readList(const std::string& str, const char* delims = " \t\r\n,") -{ - std::vector 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(Window* window) -{ - deleteSystems(); - - std::string path = getConfigPath(false); - - LOG(LogInfo) << "Loading system config file " << path << "..."; - - if(!Utils::FileSystem::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 tag!"; - return false; - } - - float systemCount = 1; - for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - systemCount++; - - float currentSystem = 0; - for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - { - std::vector emulatorList; - - // std::vector coreList; - - std::string name, fullname, path, cmd, themeFolder, defaultCore; - - name = system.child("name").text().get(); - fullname = system.child("fullname").text().get(); - path = system.child("path").text().get(); - defaultCore = system.child("defaultCore").text().get(); - - pugi::xml_node emulators = system.child("emulators"); - if (emulators != NULL) - { - for (pugi::xml_node emulator : emulators.children()) - { - EmulatorData emulatorData; - emulatorData.mName = emulator.attribute("name").value(); - emulatorData.mCommandLine = emulator.attribute("command").value(); - - pugi::xml_node cores = emulator.child("cores"); - if (cores != NULL) - { - for (pugi::xml_node core : cores.children()) - { - const std::string& corename = core.text().get(); - - if (defaultCore.length() == 0) - defaultCore = corename; - - emulatorData.mCores.push_back(corename); - // coreList.push_back(corename); - } - } - - emulatorList.push_back(emulatorData); - } - } - - if (window != NULL) - window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); - - currentSystem++; - - // convert extensions list from a string into a vector of strings - - std::vector list = readList(system.child("extension").text().get()); - std::vector extensions; - - for (auto extension = list.cbegin(); extension != list.cend(); extension++) - { - std::string xt = (*extension); - std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); - - if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) - extensions.push_back(xt); - } - - cmd = system.child("command").text().get(); - - // platform id list - const char* platformList = system.child("platform").text().get(); - std::vector platformStrs = readList(platformList); - std::vector platformIds; - for(auto it = platformStrs.cbegin(); it != platformStrs.cend(); 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 - path = Utils::FileSystem::getGenericPath(path); - - //expand home symbol if the startpath contains ~ - if (path[0] == '~') - { - path.erase(0, 1); - path.insert(0, Utils::FileSystem::getHomePath()); - } - - //create the system runtime environment data - SystemEnvironmentData* envData = new SystemEnvironmentData; - envData->mStartPath = path; - envData->mSearchExtensions = extensions; - envData->mLaunchCommand = cmd; - envData->mPlatformIds = platformIds; - // envData->mDefaultCore = defaultCore; - envData->mEmulators = emulatorList; - - // envData->mCores = coreList; - - SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); - if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) - { - LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; - delete newSys; - } - else - sSystemVector.push_back(newSys); - } - - if (window != NULL) - window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); - - currentSystem++; - - CollectionSystemManager::get()->loadCollectionSystems(); - return true; -} - -void SystemData::writeExampleConfig(const std::string& path) -{ - std::ofstream file(path.c_str()); - - file << "\n" - "\n" - "\n" - " \n" - " \n" - "\n" - " \n" - " nes\n" - "\n" - " \n" - " Nintendo Entertainment System\n" - "\n" - " \n" - " ~/roms/nes\n" - "\n" - " \n" - " .nes .NES\n" - "\n" - " \n" - " retroarch -L ~/cores/libretro-fceumm.so %ROM%\n" - "\n" - " \n" - " nes\n" - "\n" - " \n" - " nes\n" - " \n" - "\n"; - - file.close(); - - LOG(LogError) << "Example config written! Go read it at \"" << path << "\"!"; -} - -void SystemData::deleteSystems() -{ - bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); - - for(unsigned int i = 0; i < sSystemVector.size(); i++) - { - SystemData* pData = sSystemVector.at(i); - - if (saveOnExit && !pData->mIsCollectionSystem) - updateGamelist(pData); - - delete pData; - } - - sSystemVector.clear(); -} - -std::string SystemData::getConfigPath(bool forWrite) -{ - std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_systems.cfg"; - if(forWrite || Utils::FileSystem::exists(path)) - return path; - - return "/etc/emulationstation/es_systems.cfg"; -} - -bool SystemData::isVisible() -{ - return (getDisplayedGameCount() > 0 || - (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || - (mIsCollectionSystem && mName == "favorites")); -} - -SystemData* SystemData::getNext() const -{ - std::vector::const_iterator it = getIterator(); - - do { - it++; - if (it == sSystemVector.cend()) - it = sSystemVector.cbegin(); - } while (!(*it)->isVisible()); - // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. - - return *it; -} - -SystemData* SystemData::getPrev() const -{ - std::vector::const_reverse_iterator it = getRevIterator(); - - do { - it++; - if (it == sSystemVector.crend()) - it = sSystemVector.crbegin(); - } while (!(*it)->isVisible()); - // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. - - return *it; -} - -std::string SystemData::getGamelistPath(bool forWrite) const -{ - std::string filePath; - - filePath = mRootFolder->getPath() + "/gamelist.xml"; - if(Utils::FileSystem::exists(filePath)) - return filePath; - - filePath = Utils::FileSystem::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 - Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath)); - if(forWrite || Utils::FileSystem::exists(filePath)) - return filePath; - - 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. system theme from currently selected theme set [CURRENT_THEME_PATH]/[SYSTEM]/theme.xml - // 3. default system theme from currently selected theme set [CURRENT_THEME_PATH]/theme.xml - - // first, check game folder - std::string localThemePath = mRootFolder->getPath() + "/theme.xml"; - if(Utils::FileSystem::exists(localThemePath)) - return localThemePath; - - // not in game folder, try system theme in theme sets - localThemePath = ThemeData::getThemeFromCurrentSet(mThemeFolder); - - if (Utils::FileSystem::exists(localThemePath)) - return localThemePath; - - // not system theme, try default system theme in theme set - localThemePath = Utils::FileSystem::getParent(Utils::FileSystem::getParent(localThemePath)) + "/theme.xml"; - - return localThemePath; -} - -bool SystemData::hasGamelist() const -{ - return (Utils::FileSystem::exists(getGamelistPath(false))); -} - -unsigned int SystemData::getGameCount() const -{ - return (unsigned int)mRootFolder->getFilesRecursive(GAME).size(); -} - -SystemData* SystemData::getRandomSystem() -{ - // this is a bit brute force. It might be more efficient to just to a while (!gameSystem) do random again... - unsigned int total = 0; - for(auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) - { - if ((*it)->isGameSystem()) - total ++; - } - - // get random number in range - int target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); - for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) - { - if ((*it)->isGameSystem()) - { - if (target > 0) - { - target--; - } - else - { - return (*it); - } - } - } - - // if we end up here, there is no valid system - return NULL; -} - -FileData* SystemData::getRandomGame() -{ - std::vector list = mRootFolder->getFilesRecursive(GAME, true); - unsigned int total = (int)list.size(); - int target = 0; - // get random number in range - if (total == 0) - return NULL; - target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); - return list.at(target); -} - -unsigned int SystemData::getDisplayedGameCount() const -{ - return (unsigned int)mRootFolder->getFilesRecursive(GAME, true).size(); -} - -void SystemData::loadTheme() -{ - mTheme = std::make_shared(); - - std::string path = getThemePath(); - - if(!Utils::FileSystem::exists(path)) // no theme available for this platform - return; - - try - { - // build map with system variables for theme to use, - std::map sysData; - sysData.insert(std::pair("system.name", getName())); - sysData.insert(std::pair("system.theme", getThemeFolder())); - sysData.insert(std::pair("system.fullName", getFullName())); - - mTheme->loadFile(getThemeFolder(), sysData, path); - } - catch(ThemeException& e) - { - LOG(LogError) << e.what(); - mTheme = std::make_shared(); // reset to empty - } -} +#include "SystemData.h" + +#include "utils/FileSystemUtil.h" +#include "CollectionSystemManager.h" +#include "FileFilterIndex.h" +#include "FileSorts.h" +#include "Gamelist.h" +#include "Log.h" +#include "platform.h" +#include "Settings.h" +#include "ThemeData.h" +#include "views/UIModeController.h" +#include +#include +#ifdef WIN32 +#include +#endif + +#include "GuiComponent.h" +#include "Window.h" + +std::vector SystemData::sSystemVector; + +SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : + mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) +{ + mFilterIndex = new FileFilterIndex(); + + // if it's an actual system, initialize it, if not, just create the data structure + if (!CollectionSystem) + { + mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); + mRootFolder->metadata.set("name", mFullName); + + if (!Settings::getInstance()->getBool("ParseGamelistOnly")) + { + populateFolder(mRootFolder); + if (mRootFolder->getChildren().size() == 0) + return; + } + + if (!Settings::getInstance()->getBool("IgnoreGamelist")) + parseGamelist(this); + + mRootFolder->sort(FileSorts::SortTypes.at(0)); + + indexAllGameFilters(mRootFolder); + } + else + { + // virtual systems are updated afterwards, we're just creating the data structure + mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this); + } + setIsGameSystemStatus(); + loadTheme(); +} + +SystemData::~SystemData() +{ + delete mRootFolder; + delete mFilterIndex; +} + +void SystemData::setIsGameSystemStatus() +{ + // we exclude non-game systems from specific operations (i.e. the "RetroPie" system, at least) + // if/when there are more in the future, maybe this can be a more complex method, with a proper list + // but for now a simple string comparison is more performant + mIsGameSystem = (mName != "retropie"); +} + +char _easytolower(char in) { + if (in <= 'Z' && in >= 'A') + return in - ('Z' - 'z'); + return in; +} + +void SystemData::populateFolder(FileData* folder) +{ + const std::string& folderPath = folder->getPath(); + if(!Utils::FileSystem::isDirectory(folderPath)) + { + LOG(LogWarning) << "Error - folder with path \"" << folderPath << "\" is not a directory!"; + return; + } + + int di = folderPath.rfind("downloaded_images"); + int md = folderPath.rfind("media"); + if (di > 0 || md > 0) + return; + + //make sure that this isn't a symlink to a thing we already have + if(Utils::FileSystem::isSymlink(folderPath)) + { + //if this symlink resolves to somewhere that's at the beginning of our path, it's gonna recurse + if(folderPath.find(Utils::FileSystem::getCanonicalPath(folderPath)) == 0) + { + LOG(LogWarning) << "Skipping infinitely recursive symlink \"" << folderPath << "\""; + return; + } + } + + std::string filePath; + std::string extension; + bool isGame; + bool showHidden = Settings::getInstance()->getBool("ShowHiddenFiles"); + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(folderPath); + for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + filePath = *it; + + // skip hidden files and folders + if(!showHidden && Utils::FileSystem::isHidden(filePath)) + 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 = Utils::FileSystem::getExtension(filePath); + std::transform(extension.begin(), extension.end(), extension.begin(), ::_easytolower); + + //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(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) + { + FileData* newGame = new FileData(GAME, filePath, mEnvData, this); + + // preventing new arcade assets to be added + if(!newGame->isArcadeAsset()) + { + folder->addChild(newGame); + isGame = true; + } + } + + //add directories that also do not match an extension as folders + if(!isGame && Utils::FileSystem::isDirectory(filePath)) + { + if (filePath.rfind("downloaded_images") == std::string::npos && + filePath.rfind("media") == std::string::npos) + { + FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); + populateFolder(newFolder); + + if (newFolder->getChildrenByFilename().size() == 0) + delete newFolder; + else if (newFolder->findUniqueGameForFolder() != NULL) + delete newFolder; + else + folder->addChild(newFolder); + } + } + } +} + +void SystemData::indexAllGameFilters(const FileData* folder) +{ + const std::vector& children = folder->getChildren(); + + for(std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + switch((*it)->getType()) + { + case GAME: { mFilterIndex->addToIndex(*it); } break; + case FOLDER: { indexAllGameFilters(*it); } break; + } + } +} + +std::vector readList(const std::string& str, const char* delims = " \t\r\n,") +{ + std::vector 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(Window* window) +{ + deleteSystems(); + + std::string path = getConfigPath(false); + + LOG(LogInfo) << "Loading system config file " << path << "..."; + + if(!Utils::FileSystem::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 tag!"; + return false; + } + + float systemCount = 1; + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + systemCount++; + + float currentSystem = 0; + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { + std::vector emulatorList; + + // std::vector coreList; + + std::string name, fullname, path, cmd, themeFolder, defaultCore; + + name = system.child("name").text().get(); + fullname = system.child("fullname").text().get(); + path = system.child("path").text().get(); + defaultCore = system.child("defaultCore").text().get(); + + pugi::xml_node emulators = system.child("emulators"); + if (emulators != NULL) + { + for (pugi::xml_node emulator : emulators.children()) + { + EmulatorData emulatorData; + emulatorData.mName = emulator.attribute("name").value(); + emulatorData.mCommandLine = emulator.attribute("command").value(); + + pugi::xml_node cores = emulator.child("cores"); + if (cores != NULL) + { + for (pugi::xml_node core : cores.children()) + { + const std::string& corename = core.text().get(); + + if (defaultCore.length() == 0) + defaultCore = corename; + + emulatorData.mCores.push_back(corename); + // coreList.push_back(corename); + } + } + + emulatorList.push_back(emulatorData); + } + } + + if (window != NULL) + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; + + // convert extensions list from a string into a vector of strings + + std::vector list = readList(system.child("extension").text().get()); + std::vector extensions; + + for (auto extension = list.cbegin(); extension != list.cend(); extension++) + { + std::string xt = (*extension); + std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); + + if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) + extensions.push_back(xt); + } + + cmd = system.child("command").text().get(); + + // platform id list + const char* platformList = system.child("platform").text().get(); + std::vector platformStrs = readList(platformList); + std::vector platformIds; + for(auto it = platformStrs.cbegin(); it != platformStrs.cend(); 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 + path = Utils::FileSystem::getGenericPath(path); + + //expand home symbol if the startpath contains ~ + if (path[0] == '~') + { + path.erase(0, 1); + path.insert(0, Utils::FileSystem::getHomePath()); + } + + //create the system runtime environment data + SystemEnvironmentData* envData = new SystemEnvironmentData; + envData->mStartPath = path; + envData->mSearchExtensions = extensions; + envData->mLaunchCommand = cmd; + envData->mPlatformIds = platformIds; + // envData->mDefaultCore = defaultCore; + envData->mEmulators = emulatorList; + + // envData->mCores = coreList; + + SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); + if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) + { + LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; + delete newSys; + } + else + sSystemVector.push_back(newSys); + } + + if (window != NULL) + window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; + + CollectionSystemManager::get()->loadCollectionSystems(); + return true; +} + +void SystemData::writeExampleConfig(const std::string& path) +{ + std::ofstream file(path.c_str()); + + file << "\n" + "\n" + "\n" + " \n" + " \n" + "\n" + " \n" + " nes\n" + "\n" + " \n" + " Nintendo Entertainment System\n" + "\n" + " \n" + " ~/roms/nes\n" + "\n" + " \n" + " .nes .NES\n" + "\n" + " \n" + " retroarch -L ~/cores/libretro-fceumm.so %ROM%\n" + "\n" + " \n" + " nes\n" + "\n" + " \n" + " nes\n" + " \n" + "\n"; + + file.close(); + + LOG(LogError) << "Example config written! Go read it at \"" << path << "\"!"; +} + +void SystemData::deleteSystems() +{ + bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); + + for(unsigned int i = 0; i < sSystemVector.size(); i++) + { + SystemData* pData = sSystemVector.at(i); + + if (saveOnExit && !pData->mIsCollectionSystem) + updateGamelist(pData); + + delete pData; + } + + sSystemVector.clear(); +} + +std::string SystemData::getConfigPath(bool forWrite) +{ + std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_systems.cfg"; + if(forWrite || Utils::FileSystem::exists(path)) + return path; + + return "/etc/emulationstation/es_systems.cfg"; +} + +bool SystemData::isVisible() +{ + return (getDisplayedGameCount() > 0 || + (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || + (mIsCollectionSystem && mName == "favorites")); +} + +SystemData* SystemData::getNext() const +{ + std::vector::const_iterator it = getIterator(); + + do { + it++; + if (it == sSystemVector.cend()) + it = sSystemVector.cbegin(); + } while (!(*it)->isVisible()); + // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. + + return *it; +} + +SystemData* SystemData::getPrev() const +{ + std::vector::const_reverse_iterator it = getRevIterator(); + + do { + it++; + if (it == sSystemVector.crend()) + it = sSystemVector.crbegin(); + } while (!(*it)->isVisible()); + // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. + + return *it; +} + +std::string SystemData::getGamelistPath(bool forWrite) const +{ + std::string filePath; + + filePath = mRootFolder->getPath() + "/gamelist.xml"; + if(Utils::FileSystem::exists(filePath)) + return filePath; + + filePath = Utils::FileSystem::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 + Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath)); + if(forWrite || Utils::FileSystem::exists(filePath)) + return filePath; + + 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. system theme from currently selected theme set [CURRENT_THEME_PATH]/[SYSTEM]/theme.xml + // 3. default system theme from currently selected theme set [CURRENT_THEME_PATH]/theme.xml + + // first, check game folder + std::string localThemePath = mRootFolder->getPath() + "/theme.xml"; + if(Utils::FileSystem::exists(localThemePath)) + return localThemePath; + + // not in game folder, try system theme in theme sets + localThemePath = ThemeData::getThemeFromCurrentSet(mThemeFolder); + + if (Utils::FileSystem::exists(localThemePath)) + return localThemePath; + + // not system theme, try default system theme in theme set + localThemePath = Utils::FileSystem::getParent(Utils::FileSystem::getParent(localThemePath)) + "/theme.xml"; + + return localThemePath; +} + +bool SystemData::hasGamelist() const +{ + return (Utils::FileSystem::exists(getGamelistPath(false))); +} + +unsigned int SystemData::getGameCount() const +{ + return (unsigned int)mRootFolder->getFilesRecursive(GAME).size(); +} + +SystemData* SystemData::getRandomSystem() +{ + // this is a bit brute force. It might be more efficient to just to a while (!gameSystem) do random again... + unsigned int total = 0; + for(auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) + { + if ((*it)->isGameSystem()) + total ++; + } + + // get random number in range + int target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); + for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) + { + if ((*it)->isGameSystem()) + { + if (target > 0) + { + target--; + } + else + { + return (*it); + } + } + } + + // if we end up here, there is no valid system + return NULL; +} + +FileData* SystemData::getRandomGame() +{ + std::vector list = mRootFolder->getFilesRecursive(GAME, true); + unsigned int total = (int)list.size(); + int target = 0; + // get random number in range + if (total == 0) + return NULL; + target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); + return list.at(target); +} + +unsigned int SystemData::getDisplayedGameCount() const +{ + return (unsigned int)mRootFolder->getFilesRecursive(GAME, true).size(); +} + +void SystemData::loadTheme() +{ + mTheme = std::make_shared(); + + std::string path = getThemePath(); + + if(!Utils::FileSystem::exists(path)) // no theme available for this platform + return; + + try + { + // build map with system variables for theme to use, + std::map sysData; + sysData.insert(std::pair("system.name", getName())); + sysData.insert(std::pair("system.theme", getThemeFolder())); + sysData.insert(std::pair("system.fullName", getFullName())); + + mTheme->loadFile(getThemeFolder(), sysData, path); + } + catch(ThemeException& e) + { + LOG(LogError) << e.what(); + mTheme = std::make_shared(); // reset to empty + } +} diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 3925b74961..fd0db7a977 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -33,42 +33,42 @@ struct SystemEnvironmentData std::vector getCores(std::string emulatorName) { - std::vector list; - - for (auto& emulator : mEmulators) - if (emulatorName == emulator.mName) - return emulator.mCores; + std::vector list; + + for (auto& emulator : mEmulators) + if (emulatorName == emulator.mName) + return emulator.mCores; return list; } std::string getDefaultEmulator() { - for (auto& emulator : mEmulators) - return emulator.mName; + for (auto& emulator : mEmulators) + return emulator.mName; return ""; } std::string getDefaultCore(std::string emulatorName) - { - for (auto& emulator : mEmulators) - { - if (emulatorName == emulator.mName) - { - for (auto core : emulator.mCores) - return core; - } - } + { + for (auto& emulator : mEmulators) + { + if (emulatorName == emulator.mName) + { + for (auto core : emulator.mCores) + return core; + } + } return ""; } std::string getEmulatorCommandLine(std::string emulatorName) - { - for (auto& emulator : mEmulators) - if (emulatorName == emulator.mName) - return emulator.mCommandLine; + { + for (auto& emulator : mEmulators) + if (emulatorName == emulator.mName) + return emulator.mCommandLine; return ""; } diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 01c0741c21..fd42411e4a 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -124,13 +124,13 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui float x0 = (mSize.x() - mMenu.getSize().x()) / 2; - float y1 = Renderer::getScreenHeight(); + float y1 = Renderer::getScreenHeight(); float y2 = (mSize.y() - mMenu.getSize().y()) / 2; - //float y1 = mMenu.getSize().y(); - //float y2 = (mSize.y() - mMenu.getSize().y()) / 2; - - setPosition(x0, y1); - + //float y1 = mMenu.getSize().y(); + //float y2 = (mSize.y() - mMenu.getSize().y()) / 2; + + setPosition(x0, y1); + auto fadeFunc = [this, x0, y1, y2](float t) { t -= 1; // cubic ease out @@ -142,9 +142,9 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, x0, y2] { - setPosition(x0, y2); - }); - + setPosition(x0, y2); + }); + setPosition(x0, y2); } diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index a383ee2ff4..beaf8e8749 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -1,64 +1,64 @@ -#include "guis/GuiMenu.h" - -#include "components/OptionListComponent.h" -#include "components/SliderComponent.h" -#include "components/SwitchComponent.h" -#include "guis/GuiCollectionSystemsOptions.h" -#include "guis/GuiDetectDevice.h" -#include "guis/GuiGeneralScreensaverOptions.h" -#include "guis/GuiMsgBox.h" -#include "guis/GuiScraperStart.h" -#include "guis/GuiSettings.h" -#include "views/UIModeController.h" -#include "views/ViewController.h" -#include "CollectionSystemManager.h" -#include "EmulationStation.h" -#include "Scripting.h" -#include "SystemData.h" -#include "VolumeControl.h" -#include -#include - -#include "animations/LambdaAnimation.h" - -GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) - -{ - bool isFullUI = UIModeController::getInstance()->isUIModeFull(); - - if (isFullUI) - { - addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); - addEntry(_T("CONFIGURE INPUT"), 0x777777FF, true, [this] { openConfigInput(); }); - } - - addEntry(_T("SOUND SETTINGS"), 0x777777FF, true, [this] { openSoundSettings(); }); - - if (isFullUI) - addEntry(_T("SCRAPER"), 0x777777FF, true, [this] { openScraperSettings(); }); - - if (isFullUI) - { - addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); - addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); - } - -#if defined(_WIN32) - addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); -#else - addEntry(_T("QUIT"), 0x777777FF, true, [this] {openQuitMenu(); }); -#endif - - addChild(&mMenu); - addVersionInfo(); - - setSize(mMenu.getSize()); - - float y1 = Renderer::getScreenHeight(); - float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; - - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); - +#include "guis/GuiMenu.h" + +#include "components/OptionListComponent.h" +#include "components/SliderComponent.h" +#include "components/SwitchComponent.h" +#include "guis/GuiCollectionSystemsOptions.h" +#include "guis/GuiDetectDevice.h" +#include "guis/GuiGeneralScreensaverOptions.h" +#include "guis/GuiMsgBox.h" +#include "guis/GuiScraperStart.h" +#include "guis/GuiSettings.h" +#include "views/UIModeController.h" +#include "views/ViewController.h" +#include "CollectionSystemManager.h" +#include "EmulationStation.h" +#include "Scripting.h" +#include "SystemData.h" +#include "VolumeControl.h" +#include +#include + +#include "animations/LambdaAnimation.h" + +GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) + +{ + bool isFullUI = UIModeController::getInstance()->isUIModeFull(); + + if (isFullUI) + { + addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); + addEntry(_T("CONFIGURE INPUT"), 0x777777FF, true, [this] { openConfigInput(); }); + } + + addEntry(_T("SOUND SETTINGS"), 0x777777FF, true, [this] { openSoundSettings(); }); + + if (isFullUI) + addEntry(_T("SCRAPER"), 0x777777FF, true, [this] { openScraperSettings(); }); + + if (isFullUI) + { + addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); + addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); + } + +#if defined(_WIN32) + addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); +#else + addEntry(_T("QUIT"), 0x777777FF, true, [this] {openQuitMenu(); }); +#endif + + addChild(&mMenu); + addVersionInfo(); + + setSize(mMenu.getSize()); + + float y1 = Renderer::getScreenHeight(); + float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); + auto fadeFunc = [this, y1, y2](float t) { t -= 1; // cubic ease out @@ -70,648 +70,648 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] { - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); - }); - - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); -} - -void GuiMenu::openScraperSettings() -{ - auto s = new GuiSettings(mWindow, "SCRAPER"); - - // scrape from - auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, "SCRAPE FROM", false); - std::vector scrapers = getScraperList(); - - // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. - for(auto it = scrapers.cbegin(); it != scrapers.cend(); 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(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; - auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; - std::function openAndSave = openScrapeNow; - openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; - row.makeAcceptInputHandler(openAndSave); - - auto scrape_now = std::make_shared(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); -} - -void GuiMenu::openSoundSettings() -{ - auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); - - // volume - auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); - volume->setValue((float)VolumeControl::getInstance()->getVolume()); - s->addWithLabel(_T("SYSTEM VOLUME"), volume); - s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); - - if (UIModeController::getInstance()->isUIModeFull()) - { -#if defined(__linux__) - // audio card - auto audio_card = std::make_shared< OptionListComponent >(mWindow, "AUDIO CARD", false); - std::vector audio_cards; - #ifdef _RPI_ - // RPi Specific Audio Cards - audio_cards.push_back("local"); - audio_cards.push_back("hdmi"); - audio_cards.push_back("both"); - #endif - audio_cards.push_back("default"); - audio_cards.push_back("sysdefault"); - audio_cards.push_back("dmix"); - audio_cards.push_back("hw"); - audio_cards.push_back("plughw"); - audio_cards.push_back("null"); - if (Settings::getInstance()->getString("AudioCard") != "") { - if(std::find(audio_cards.begin(), audio_cards.end(), Settings::getInstance()->getString("AudioCard")) == audio_cards.end()) { - audio_cards.push_back(Settings::getInstance()->getString("AudioCard")); - } - } - for(auto ac = audio_cards.cbegin(); ac != audio_cards.cend(); ac++) - audio_card->add(*ac, *ac, Settings::getInstance()->getString("AudioCard") == *ac); - s->addWithLabel("AUDIO CARD", audio_card); - s->addSaveFunc([audio_card] { - Settings::getInstance()->setString("AudioCard", audio_card->getSelected()); - VolumeControl::getInstance()->deinit(); - VolumeControl::getInstance()->init(); - }); - - // volume control device - auto vol_dev = std::make_shared< OptionListComponent >(mWindow, "AUDIO DEVICE", false); - std::vector transitions; - transitions.push_back("PCM"); - transitions.push_back("Speaker"); - transitions.push_back("Master"); - transitions.push_back("Digital"); - transitions.push_back("Analogue"); - if (Settings::getInstance()->getString("AudioDevice") != "") { - if(std::find(transitions.begin(), transitions.end(), Settings::getInstance()->getString("AudioDevice")) == transitions.end()) { - transitions.push_back(Settings::getInstance()->getString("AudioDevice")); - } - } - for(auto it = transitions.cbegin(); it != transitions.cend(); it++) - vol_dev->add(*it, *it, Settings::getInstance()->getString("AudioDevice") == *it); - s->addWithLabel("AUDIO DEVICE", vol_dev); - s->addSaveFunc([vol_dev] { - Settings::getInstance()->setString("AudioDevice", vol_dev->getSelected()); - VolumeControl::getInstance()->deinit(); - VolumeControl::getInstance()->init(); - }); -#endif - - // disable sounds - auto sounds_enabled = std::make_shared(mWindow); - sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); - s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); - s->addSaveFunc([sounds_enabled] { - if (sounds_enabled->getState() - && !Settings::getInstance()->getBool("EnableSounds") - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState()); - }); - - auto video_audio = std::make_shared(mWindow); - video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); - s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); - s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); - -#ifdef _RPI_ - // OMX player Audio Device - auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, "OMX PLAYER AUDIO DEVICE", false); - std::vector omx_cards; - // RPi Specific Audio Cards - omx_cards.push_back("local"); - omx_cards.push_back("hdmi"); - omx_cards.push_back("both"); - omx_cards.push_back("alsa:hw:0,0"); - omx_cards.push_back("alsa:hw:1,0"); - if (Settings::getInstance()->getString("OMXAudioDev") != "") { - if (std::find(omx_cards.begin(), omx_cards.end(), Settings::getInstance()->getString("OMXAudioDev")) == omx_cards.end()) { - omx_cards.push_back(Settings::getInstance()->getString("OMXAudioDev")); - } - } - for (auto it = omx_cards.cbegin(); it != omx_cards.cend(); it++) - omx_audio_dev->add(*it, *it, Settings::getInstance()->getString("OMXAudioDev") == *it); - s->addWithLabel("OMX PLAYER AUDIO DEVICE", omx_audio_dev); - s->addSaveFunc([omx_audio_dev] { - if (Settings::getInstance()->getString("OMXAudioDev") != omx_audio_dev->getSelected()) - Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); - }); -#endif - } - - mWindow->pushGui(s); - -} - -void GuiMenu::openUISettings() -{ - auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); - - // theme set - auto themeSets = ThemeData::getThemeSets(); - - if (!themeSets.empty()) - { - std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if (selectedSet == themeSets.cend()) - selectedSet = themeSets.cbegin(); - - auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); - for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) - theme_set->add(it->first, it->first, it == selectedSet); - s->addWithLabel(_T("THEME"), theme_set); - - Window* window = mWindow; - s->addSaveFunc([window, theme_set] - { - bool needReload = false; - std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - if (oldTheme != theme_set->getSelected()) - needReload = true; - - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - - if (needReload) - { - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation - } - }); - } - - // LANGUAGE - - std::vector langues; - langues.push_back("en"); - - - std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); - if (xmlpath.length() > 0) - { - xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; - - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); - for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if (Utils::FileSystem::isDirectory(*it)) - continue; - - std::string name = *it; - - if (name.rfind("emulationstation2.po") == std::string::npos) - continue; - - name = Utils::FileSystem::getParent(name); - name = Utils::FileSystem::getFileName(name); - - if (name != "en") - langues.push_back(name); - } - - if (langues.size() > 1) - { - auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); - - for (auto it = langues.cbegin(); it != langues.cend(); it++) - language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); - - s->addWithLabel(_T("LANGUAGE"), language); - s->addSaveFunc([language] { - Settings::getInstance()->setString("Language", language->getSelected()); - }); - } - } - - // transition style - auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); - std::vector transitions; - transitions.push_back("fade"); - transitions.push_back("slide"); - transitions.push_back("instant"); - for (auto it = transitions.cbegin(); it != transitions.cend(); it++) - transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); - - s->addWithLabel(_T("TRANSITION STYLE"), transition_style); - s->addSaveFunc([transition_style] { - if (Settings::getInstance()->getString("TransitionStyle") == "instant" - && transition_style->getSelected() != "instant" - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); - }); - - - auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); - std::vector gameTransitions; - gameTransitions.push_back("fade"); - gameTransitions.push_back("slide"); - gameTransitions.push_back("instant"); - for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) - transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); - - s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); - s->addSaveFunc([transitionOfGames_style] { - if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" - && transitionOfGames_style->getSelected() != "instant" - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); - }); - - // GameList view style - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); - std::vector styles; - styles.push_back("automatic"); - styles.push_back("basic"); - styles.push_back("detailed"); - styles.push_back("video"); - styles.push_back("grid"); - styles.push_back("gridex"); - - for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); - - s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); - s->addSaveFunc([gamelist_style] { - bool needReload = false; - if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) - needReload = true; - Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); - if (needReload) - ViewController::get()->reloadAll(); - }); - - - // Optionally start in selected system - auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); - systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); - for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) - { - if ("retropie" != (*it)->getName()) - { - systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); - } - } - s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); - s->addSaveFunc([systemfocus_list] { - Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); - }); - - - /* - //UI mode - auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); - std::vector UImodes = UIModeController::getInstance()->getUIModes(); - for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) - UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); - s->addWithLabel("UI MODE", UImodeSelection); - Window* window = mWindow; - s->addSaveFunc([ UImodeSelection, window] - { - std::string selectedMode = UImodeSelection->getSelected(); - if (selectedMode != "Full") - { - std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; - msg += "This will hide most menu-options to prevent changes to the system.\n"; - msg += "To unlock and return to the full UI, enter this code: \n"; - msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; - msg += "Do you want to proceed?"; - window->pushGui(new GuiMsgBox(window, msg, - "YES", [selectedMode] { - LOG(LogDebug) << "Setting UI mode to " << selectedMode; - Settings::getInstance()->setString("UIMode", selectedMode); - Settings::getInstance()->saveFile(); - }, "NO",nullptr)); - } - }); - */ - - // screensaver - ComponentListRow screensaver_row; - screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - screensaver_row.addElement(makeArrow(mWindow), false); - screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); - s->addRow(screensaver_row); - -#if defined(_WIN32) - // quick system select (left/right in game list view) - auto hideWindowScreen = std::make_shared(mWindow); - hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); - s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); - s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); -#endif - - // quick system select (left/right in game list view) - auto quick_sys_select = std::make_shared(mWindow); - quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); - s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); - s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); - - // carousel transition option - auto move_carousel = std::make_shared(mWindow); - move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); - s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); - s->addSaveFunc([move_carousel] { - if (move_carousel->getState() - && !Settings::getInstance()->getBool("MoveCarousel") - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); - }); - - // show help - auto show_help = std::make_shared(mWindow); - show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); - s->addWithLabel(_T("ON-SCREEN HELP"), show_help); - s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); - - // enable filters (ForceDisableFilters) - auto enable_filter = std::make_shared(mWindow); - enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); - s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); - s->addSaveFunc([enable_filter] { - bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); - Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); - if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); - }); - - mWindow->pushGui(s); - -} - -void GuiMenu::openOtherSettings() -{ - auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); - - // maximum vram - auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); - max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel(_T("VRAM LIMIT"), max_vram); - s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); - - // power saver - auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); - std::vector modes; - modes.push_back("disabled"); - modes.push_back("default"); - modes.push_back("enhanced"); - modes.push_back("instant"); - for (auto it = modes.cbegin(); it != modes.cend(); it++) - power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); - - s->addWithLabel(_T("POWER SAVER MODES"), power_saver); - s->addSaveFunc([this, power_saver] { - if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { - Settings::getInstance()->setString("TransitionStyle", "instant"); - Settings::getInstance()->setBool("MoveCarousel", false); - Settings::getInstance()->setBool("EnableSounds", false); - } - Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); - PowerSaver::init(); - }); - - // gamelists - auto save_gamelists = std::make_shared(mWindow); - save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); - s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); - s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); - - auto parse_gamelists = std::make_shared(mWindow); - parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); - s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); - s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); - /* - auto local_art = std::make_shared(mWindow); - local_art->setState(Settings::getInstance()->getBool("LocalArt")); - s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); - s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); - */ - // hidden files - auto hidden_files = std::make_shared(mWindow); - hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); - s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); - s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); - -#ifdef _RPI_ - // Video Player - VideoOmxPlayer - auto omx_player = std::make_shared(mWindow); - omx_player->setState(Settings::getInstance()->getBool("VideoOmxPlayer")); - s->addWithLabel("USE OMX PLAYER (HW ACCELERATED)", omx_player); - s->addSaveFunc([omx_player] - { - // need to reload all views to re-create the right video components - bool needReload = false; - if(Settings::getInstance()->getBool("VideoOmxPlayer") != omx_player->getState()) - needReload = true; - - Settings::getInstance()->setBool("VideoOmxPlayer", omx_player->getState()); - - if(needReload) - ViewController::get()->reloadAll(); - }); - -#endif - - // framerate - /* - auto framerate = std::make_shared(mWindow); - framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); - s->addWithLabel(_T("SHOW FRAMERATE"), framerate); - s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); - */ - - mWindow->pushGui(s); - -} - -void GuiMenu::openConfigInput() -{ - Window* window = mWindow; - window->pushGui(new GuiDetectDevice(window, false, nullptr)); - /* - window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), - [window] { - window->pushGui(new GuiDetectDevice(window, false, nullptr)); - }, _T("NO"), nullptr) - );*/ - -} - -void GuiMenu::openQuitMenu() -{ -#if defined(_WIN32) - Scripting::fireEvent("quit"); - quitES(""); - return; -#endif - - auto s = new GuiSettings(mWindow, _T("QUIT")); - - Window* window = mWindow; - - ComponentListRow row; - if (UIModeController::getInstance()->isUIModeFull()) - { - row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", - [] { - Scripting::fireEvent("quit"); - if(quitES("/tmp/es-restart") != 0) - LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", 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", - [] { - Scripting::fireEvent("quit"); - quitES(""); - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - s->addRow(row); - } - } - row.elements.clear(); - row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", - [] { - Scripting::fireEvent("quit", "reboot"); - Scripting::fireEvent("reboot"); - if (quitES("/tmp/es-sysrestart") != 0) - LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(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", - [] { - Scripting::fireEvent("quit", "shutdown"); - Scripting::fireEvent("shutdown"); - if (quitES("/tmp/es-shutdown") != 0) - LOG(LogWarning) << "Shutdown terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - s->addRow(row); - - mWindow->pushGui(s); -} - -void GuiMenu::addVersionInfo() -{ - std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); - - mVersion.setFont(Font::get(FONT_SIZE_SMALL)); - mVersion.setColor(0x5E5E5EFF); - mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); - mVersion.setHorizontalAlignment(ALIGN_CENTER); - addChild(&mVersion); -} - -void GuiMenu::openScreensaverOptions() { - mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); -} - -void GuiMenu::openCollectionSystemSettings() { - mWindow->pushGui(new GuiCollectionSystemsOptions(mWindow)); -} - -void GuiMenu::onSizeChanged() -{ - mVersion.setSize(mSize.x(), 0); - mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); -} - -void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) -{ - std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); - - // populate the list - ComponentListRow row; - row.addElement(std::make_shared(mWindow, name, font, color), true); - - if(add_arrow) - { - std::shared_ptr 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; -} - -HelpStyle GuiMenu::getHelpStyle() -{ - HelpStyle style = HelpStyle(); - style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); - return style; -} - -std::vector GuiMenu::getHelpPrompts() -{ - std::vector prompts; - prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); - prompts.push_back(HelpPrompt("a", _T("SELECT"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); - return prompts; -} + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + }); + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); +} + +void GuiMenu::openScraperSettings() +{ + auto s = new GuiSettings(mWindow, "SCRAPER"); + + // scrape from + auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, "SCRAPE FROM", false); + std::vector scrapers = getScraperList(); + + // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. + for(auto it = scrapers.cbegin(); it != scrapers.cend(); 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(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; + auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; + std::function openAndSave = openScrapeNow; + openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; + row.makeAcceptInputHandler(openAndSave); + + auto scrape_now = std::make_shared(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); +} + +void GuiMenu::openSoundSettings() +{ + auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); + + // volume + auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); + volume->setValue((float)VolumeControl::getInstance()->getVolume()); + s->addWithLabel(_T("SYSTEM VOLUME"), volume); + s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); + + if (UIModeController::getInstance()->isUIModeFull()) + { +#if defined(__linux__) + // audio card + auto audio_card = std::make_shared< OptionListComponent >(mWindow, "AUDIO CARD", false); + std::vector audio_cards; + #ifdef _RPI_ + // RPi Specific Audio Cards + audio_cards.push_back("local"); + audio_cards.push_back("hdmi"); + audio_cards.push_back("both"); + #endif + audio_cards.push_back("default"); + audio_cards.push_back("sysdefault"); + audio_cards.push_back("dmix"); + audio_cards.push_back("hw"); + audio_cards.push_back("plughw"); + audio_cards.push_back("null"); + if (Settings::getInstance()->getString("AudioCard") != "") { + if(std::find(audio_cards.begin(), audio_cards.end(), Settings::getInstance()->getString("AudioCard")) == audio_cards.end()) { + audio_cards.push_back(Settings::getInstance()->getString("AudioCard")); + } + } + for(auto ac = audio_cards.cbegin(); ac != audio_cards.cend(); ac++) + audio_card->add(*ac, *ac, Settings::getInstance()->getString("AudioCard") == *ac); + s->addWithLabel("AUDIO CARD", audio_card); + s->addSaveFunc([audio_card] { + Settings::getInstance()->setString("AudioCard", audio_card->getSelected()); + VolumeControl::getInstance()->deinit(); + VolumeControl::getInstance()->init(); + }); + + // volume control device + auto vol_dev = std::make_shared< OptionListComponent >(mWindow, "AUDIO DEVICE", false); + std::vector transitions; + transitions.push_back("PCM"); + transitions.push_back("Speaker"); + transitions.push_back("Master"); + transitions.push_back("Digital"); + transitions.push_back("Analogue"); + if (Settings::getInstance()->getString("AudioDevice") != "") { + if(std::find(transitions.begin(), transitions.end(), Settings::getInstance()->getString("AudioDevice")) == transitions.end()) { + transitions.push_back(Settings::getInstance()->getString("AudioDevice")); + } + } + for(auto it = transitions.cbegin(); it != transitions.cend(); it++) + vol_dev->add(*it, *it, Settings::getInstance()->getString("AudioDevice") == *it); + s->addWithLabel("AUDIO DEVICE", vol_dev); + s->addSaveFunc([vol_dev] { + Settings::getInstance()->setString("AudioDevice", vol_dev->getSelected()); + VolumeControl::getInstance()->deinit(); + VolumeControl::getInstance()->init(); + }); +#endif + + // disable sounds + auto sounds_enabled = std::make_shared(mWindow); + sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); + s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); + s->addSaveFunc([sounds_enabled] { + if (sounds_enabled->getState() + && !Settings::getInstance()->getBool("EnableSounds") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState()); + }); + + auto video_audio = std::make_shared(mWindow); + video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); + s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); + s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); + +#ifdef _RPI_ + // OMX player Audio Device + auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, "OMX PLAYER AUDIO DEVICE", false); + std::vector omx_cards; + // RPi Specific Audio Cards + omx_cards.push_back("local"); + omx_cards.push_back("hdmi"); + omx_cards.push_back("both"); + omx_cards.push_back("alsa:hw:0,0"); + omx_cards.push_back("alsa:hw:1,0"); + if (Settings::getInstance()->getString("OMXAudioDev") != "") { + if (std::find(omx_cards.begin(), omx_cards.end(), Settings::getInstance()->getString("OMXAudioDev")) == omx_cards.end()) { + omx_cards.push_back(Settings::getInstance()->getString("OMXAudioDev")); + } + } + for (auto it = omx_cards.cbegin(); it != omx_cards.cend(); it++) + omx_audio_dev->add(*it, *it, Settings::getInstance()->getString("OMXAudioDev") == *it); + s->addWithLabel("OMX PLAYER AUDIO DEVICE", omx_audio_dev); + s->addSaveFunc([omx_audio_dev] { + if (Settings::getInstance()->getString("OMXAudioDev") != omx_audio_dev->getSelected()) + Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); + }); +#endif + } + + mWindow->pushGui(s); + +} + +void GuiMenu::openUISettings() +{ + auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); + + // theme set + auto themeSets = ThemeData::getThemeSets(); + + if (!themeSets.empty()) + { + std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if (selectedSet == themeSets.cend()) + selectedSet = themeSets.cbegin(); + + auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); + for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) + theme_set->add(it->first, it->first, it == selectedSet); + s->addWithLabel(_T("THEME"), theme_set); + + Window* window = mWindow; + s->addSaveFunc([window, theme_set] + { + bool needReload = false; + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + if (oldTheme != theme_set->getSelected()) + needReload = true; + + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); + + if (needReload) + { + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + } + }); + } + + // LANGUAGE + + std::vector langues; + langues.push_back("en"); + + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); + if (xmlpath.length() > 0) + { + xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if (Utils::FileSystem::isDirectory(*it)) + continue; + + std::string name = *it; + + if (name.rfind("emulationstation2.po") == std::string::npos) + continue; + + name = Utils::FileSystem::getParent(name); + name = Utils::FileSystem::getFileName(name); + + if (name != "en") + langues.push_back(name); + } + + if (langues.size() > 1) + { + auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); + + for (auto it = langues.cbegin(); it != langues.cend(); it++) + language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); + + s->addWithLabel(_T("LANGUAGE"), language); + s->addSaveFunc([language] { + Settings::getInstance()->setString("Language", language->getSelected()); + }); + } + } + + // transition style + auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); + std::vector transitions; + transitions.push_back("fade"); + transitions.push_back("slide"); + transitions.push_back("instant"); + for (auto it = transitions.cbegin(); it != transitions.cend(); it++) + transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); + + s->addWithLabel(_T("TRANSITION STYLE"), transition_style); + s->addSaveFunc([transition_style] { + if (Settings::getInstance()->getString("TransitionStyle") == "instant" + && transition_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); + }); + + + auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); + std::vector gameTransitions; + gameTransitions.push_back("fade"); + gameTransitions.push_back("slide"); + gameTransitions.push_back("instant"); + for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) + transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); + + s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); + s->addSaveFunc([transitionOfGames_style] { + if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" + && transitionOfGames_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); + }); + + // GameList view style + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); + std::vector styles; + styles.push_back("automatic"); + styles.push_back("basic"); + styles.push_back("detailed"); + styles.push_back("video"); + styles.push_back("grid"); + styles.push_back("gridex"); + + for (auto it = styles.cbegin(); it != styles.cend(); it++) + gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); + + s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); + s->addSaveFunc([gamelist_style] { + bool needReload = false; + if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) + needReload = true; + Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); + if (needReload) + ViewController::get()->reloadAll(); + }); + + + // Optionally start in selected system + auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); + systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + { + if ("retropie" != (*it)->getName()) + { + systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); + } + } + s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); + s->addSaveFunc([systemfocus_list] { + Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); + }); + + + /* + //UI mode + auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); + std::vector UImodes = UIModeController::getInstance()->getUIModes(); + for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) + UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); + s->addWithLabel("UI MODE", UImodeSelection); + Window* window = mWindow; + s->addSaveFunc([ UImodeSelection, window] + { + std::string selectedMode = UImodeSelection->getSelected(); + if (selectedMode != "Full") + { + std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; + msg += "This will hide most menu-options to prevent changes to the system.\n"; + msg += "To unlock and return to the full UI, enter this code: \n"; + msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; + msg += "Do you want to proceed?"; + window->pushGui(new GuiMsgBox(window, msg, + "YES", [selectedMode] { + LOG(LogDebug) << "Setting UI mode to " << selectedMode; + Settings::getInstance()->setString("UIMode", selectedMode); + Settings::getInstance()->saveFile(); + }, "NO",nullptr)); + } + }); + */ + + // screensaver + ComponentListRow screensaver_row; + screensaver_row.elements.clear(); + screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + screensaver_row.addElement(makeArrow(mWindow), false); + screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); + s->addRow(screensaver_row); + +#if defined(_WIN32) + // quick system select (left/right in game list view) + auto hideWindowScreen = std::make_shared(mWindow); + hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); + s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); + s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); +#endif + + // quick system select (left/right in game list view) + auto quick_sys_select = std::make_shared(mWindow); + quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); + s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); + s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); + + // carousel transition option + auto move_carousel = std::make_shared(mWindow); + move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); + s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); + s->addSaveFunc([move_carousel] { + if (move_carousel->getState() + && !Settings::getInstance()->getBool("MoveCarousel") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); + }); + + // show help + auto show_help = std::make_shared(mWindow); + show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); + s->addWithLabel(_T("ON-SCREEN HELP"), show_help); + s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); + + // enable filters (ForceDisableFilters) + auto enable_filter = std::make_shared(mWindow); + enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); + s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); + s->addSaveFunc([enable_filter] { + bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); + Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); + if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); + }); + + mWindow->pushGui(s); + +} + +void GuiMenu::openOtherSettings() +{ + auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); + + // maximum vram + auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); + max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); + s->addWithLabel(_T("VRAM LIMIT"), max_vram); + s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); + + // power saver + auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); + std::vector modes; + modes.push_back("disabled"); + modes.push_back("default"); + modes.push_back("enhanced"); + modes.push_back("instant"); + for (auto it = modes.cbegin(); it != modes.cend(); it++) + power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); + + s->addWithLabel(_T("POWER SAVER MODES"), power_saver); + s->addSaveFunc([this, power_saver] { + if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { + Settings::getInstance()->setString("TransitionStyle", "instant"); + Settings::getInstance()->setBool("MoveCarousel", false); + Settings::getInstance()->setBool("EnableSounds", false); + } + Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); + PowerSaver::init(); + }); + + // gamelists + auto save_gamelists = std::make_shared(mWindow); + save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); + s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); + s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); + + auto parse_gamelists = std::make_shared(mWindow); + parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); + s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); + s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); + /* + auto local_art = std::make_shared(mWindow); + local_art->setState(Settings::getInstance()->getBool("LocalArt")); + s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); + s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); + */ + // hidden files + auto hidden_files = std::make_shared(mWindow); + hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); + s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); + s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); + +#ifdef _RPI_ + // Video Player - VideoOmxPlayer + auto omx_player = std::make_shared(mWindow); + omx_player->setState(Settings::getInstance()->getBool("VideoOmxPlayer")); + s->addWithLabel("USE OMX PLAYER (HW ACCELERATED)", omx_player); + s->addSaveFunc([omx_player] + { + // need to reload all views to re-create the right video components + bool needReload = false; + if(Settings::getInstance()->getBool("VideoOmxPlayer") != omx_player->getState()) + needReload = true; + + Settings::getInstance()->setBool("VideoOmxPlayer", omx_player->getState()); + + if(needReload) + ViewController::get()->reloadAll(); + }); + +#endif + + // framerate + /* + auto framerate = std::make_shared(mWindow); + framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); + s->addWithLabel(_T("SHOW FRAMERATE"), framerate); + s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); + */ + + mWindow->pushGui(s); + +} + +void GuiMenu::openConfigInput() +{ + Window* window = mWindow; + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + /* + window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), + [window] { + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + }, _T("NO"), nullptr) + );*/ + +} + +void GuiMenu::openQuitMenu() +{ +#if defined(_WIN32) + Scripting::fireEvent("quit"); + quitES(""); + return; +#endif + + auto s = new GuiSettings(mWindow, _T("QUIT")); + + Window* window = mWindow; + + ComponentListRow row; + if (UIModeController::getInstance()->isUIModeFull()) + { + row.makeAcceptInputHandler([window] { + window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + [] { + Scripting::fireEvent("quit"); + if(quitES("/tmp/es-restart") != 0) + LOG(LogWarning) << "Restart terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", 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", + [] { + Scripting::fireEvent("quit"); + quitES(""); + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + s->addRow(row); + } + } + row.elements.clear(); + row.makeAcceptInputHandler([window] { + window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + [] { + Scripting::fireEvent("quit", "reboot"); + Scripting::fireEvent("reboot"); + if (quitES("/tmp/es-sysrestart") != 0) + LOG(LogWarning) << "Restart terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(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", + [] { + Scripting::fireEvent("quit", "shutdown"); + Scripting::fireEvent("shutdown"); + if (quitES("/tmp/es-shutdown") != 0) + LOG(LogWarning) << "Shutdown terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + s->addRow(row); + + mWindow->pushGui(s); +} + +void GuiMenu::addVersionInfo() +{ + std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); + + mVersion.setFont(Font::get(FONT_SIZE_SMALL)); + mVersion.setColor(0x5E5E5EFF); + mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); + mVersion.setHorizontalAlignment(ALIGN_CENTER); + addChild(&mVersion); +} + +void GuiMenu::openScreensaverOptions() { + mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); +} + +void GuiMenu::openCollectionSystemSettings() { + mWindow->pushGui(new GuiCollectionSystemsOptions(mWindow)); +} + +void GuiMenu::onSizeChanged() +{ + mVersion.setSize(mSize.x(), 0); + mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); +} + +void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) +{ + std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); + + // populate the list + ComponentListRow row; + row.addElement(std::make_shared(mWindow, name, font, color), true); + + if(add_arrow) + { + std::shared_ptr 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; +} + +HelpStyle GuiMenu::getHelpStyle() +{ + HelpStyle style = HelpStyle(); + style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); + return style; +} + +std::vector GuiMenu::getHelpPrompts() +{ + std::vector prompts; + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("SELECT"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + return prompts; +} diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 5f0df4f525..31835bfa1b 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -1,382 +1,382 @@ -#include "guis/GuiMetaDataEd.h" - -#include "components/ButtonComponent.h" -#include "components/ComponentList.h" -#include "components/DateTimeEditComponent.h" -#include "components/MenuComponent.h" -#include "components/RatingComponent.h" -#include "components/SwitchComponent.h" -#include "components/TextComponent.h" -#include "guis/GuiGameScraper.h" -#include "guis/GuiMsgBox.h" -#include "guis/GuiTextEditPopup.h" -#include "resources/Font.h" -#include "utils/StringUtil.h" -#include "views/ViewController.h" -#include "CollectionSystemManager.h" -#include "FileData.h" -#include "FileFilterIndex.h" -#include "SystemData.h" -#include "Window.h" - -GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, - const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : 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(mWindow, Vector2i(1, 5)); - - mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); - mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), - 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(mWindow); - mGrid.setEntry(mList, Vector2i(0, 1), true, true); - - SystemData* system = file->getSystem(); - - auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); - auto core_choice = std::make_shared>(mWindow, _T("CORE"), false); - - // populate list - for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) - { - std::shared_ptr ed; - - // don't add statistics - if(iter->isStatistic) - continue; - - if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || - iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || - iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") - continue; - - // create ed and add it (and any related components) to mMenu - // ed's value will be set below - ComponentListRow row; - - if (iter->displayName == "emulator") - { - // if (file->getSystemEnvData()->mEmulators.size() <= 1) - // continue; - - std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); - - std::string currentEmul = file->getEmulator(); - - if (defaultEmul.length() == 0) - emul_choice->add(_T("DEFAULT"), "", false); - else - emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); - - for (auto core : file->getSystemEnvData()->mEmulators) - emul_choice->add(core.mName, core.mName, core.mName == currentEmul); - - row.addElement(std::make_shared(mWindow, _T("EMULATOR"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(emul_choice, false); - - mList->addRow(row); - emul_choice->setTag(iter->key); - mEditors.push_back(emul_choice); - - emul_choice->setSelectedChangedCallback([this, system, core_choice, file](std::string emulatorName) - { - std::string currentCore = file->getCore(); - - std::string defaultCore = system->getSystemEnvData()->getDefaultCore(emulatorName); - if (emulatorName.length() == 0) - defaultCore = system->getSystemEnvData()->getDefaultCore(system->getSystemEnvData()->getDefaultEmulator()); - - core_choice->clear(); - if (defaultCore.length() == 0) - core_choice->add(_T("DEFAULT"), "", false); - else - core_choice->add(_T("DEFAULT")+" ("+ defaultCore+")", "", false); - - std::vector cores = system->getSystemEnvData()->getCores(emulatorName); - - bool found = false; - - for (auto it = cores.begin(); it != cores.end(); it++) - { - std::string core = *it; - core_choice->add(core, core, currentCore == core); - if (currentCore == core) - found = true; - } - - if (!found) - core_choice->selectFirstItem(); - else - core_choice->invalidate(); - }); - - continue; - } - - if (iter->displayName == "core") - { - // core_choice->add(_T("DEFAULT"), "", true); - core_choice->setTag(iter->key); - - row.addElement(std::make_shared(mWindow, "CORE", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(core_choice, false); - - mList->addRow(row); - ed = core_choice; - - mEditors.push_back(core_choice); - - // force change event to load core list - emul_choice->invalidate(); - continue; - } - - auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); - row.addElement(lbl, true); // label - - switch (iter->type) - { - case MD_BOOL: - { - ed = std::make_shared(window); - row.addElement(ed, false, true); - break; - } - case MD_RATING: - { - ed = std::make_shared(window); - const float height = lbl->getSize().y() * 0.71f; - ed->setSize(0, height); - row.addElement(ed, false, true); - - auto spacer = std::make_shared(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(window); - row.addElement(ed, false); - - auto spacer = std::make_shared(mWindow); - spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0); - row.addElement(spacer, false); - - // pass input to the actual DateTimeEditComponent 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(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); - row.addElement(ed, false); - break; - } - case MD_MULTILINE_STRING: - default: - { - // MD_STRING - ed = std::make_shared(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); - row.addElement(ed, true); - - auto spacer = std::make_shared(mWindow); - spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); - row.addElement(spacer, false); - - auto bracket = std::make_shared(mWindow); - bracket->setImage(":/arrow.svg"); - bracket->setResize(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->setTag(iter->key); - ed->setValue(mMetaData->get(iter->key)); - - mEditors.push_back(ed); - } - - std::vector< std::shared_ptr > buttons; - - if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) - buttons.push_back(std::make_shared(mWindow, _T("SCRAPE"), _T("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); - - buttons.push_back(std::make_shared(mWindow, _T("SAVE"), _T("SAVE"), [&] { save(); delete this; })); - buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); - - if(mDeleteFunc) - { - auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; - auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; - buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE"), deleteBtnFunc)); - } - - mButtons = makeButtonGrid(mWindow, buttons); - mGrid.setEntry(mButtons, Vector2i(0, 2), true, false); - - // resize + center - float width = (float)Math::min(Renderer::getScreenHeight(), (int)(Renderer::getScreenWidth() * 0.90f)); - setSize(width, 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() -{ - // remove game from index - mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); - - for (unsigned int i = 0; i < mEditors.size(); i++) - { - std::shared_ptr ed = mEditors.at(i); - - auto val = ed->getValue(); - auto key = ed->getTag(); - - if (key == "core" || key == "emulator") - { - std::shared_ptr> list = std::static_pointer_cast>(ed); - val = list->getSelected(); - } - - mMetaData->set(key, val); - } - - // enter game in index - mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); - - if (mSavedCallback) - mSavedCallback(); - - // update respective Collection Entries - CollectionSystemManager::get()->refreshCollectionSystems(mScraperParams.game); -} - -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++) - { - auto val = mEditors.at(i)->getValue(); - auto key = mEditors.at(i)->getTag(); - 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++) - { - auto key = mEditors.at(i)->getTag(); - if(mMetaData->get(key) != mEditors.at(i)->getValue()) - { - dirty = true; - break; - } - } - - std::function 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, - "ENREGISTRER LES MODIFICATIONS ?", - "OUI", [this, closeFunc] { save(); closeFunc(); }, - "NON", 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 GuiMetaDataEd::getHelpPrompts() -{ - std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); - return prompts; -} +#include "guis/GuiMetaDataEd.h" + +#include "components/ButtonComponent.h" +#include "components/ComponentList.h" +#include "components/DateTimeEditComponent.h" +#include "components/MenuComponent.h" +#include "components/RatingComponent.h" +#include "components/SwitchComponent.h" +#include "components/TextComponent.h" +#include "guis/GuiGameScraper.h" +#include "guis/GuiMsgBox.h" +#include "guis/GuiTextEditPopup.h" +#include "resources/Font.h" +#include "utils/StringUtil.h" +#include "views/ViewController.h" +#include "CollectionSystemManager.h" +#include "FileData.h" +#include "FileFilterIndex.h" +#include "SystemData.h" +#include "Window.h" + +GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, + const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : 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(mWindow, Vector2i(1, 5)); + + mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); + mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), + 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(mWindow); + mGrid.setEntry(mList, Vector2i(0, 1), true, true); + + SystemData* system = file->getSystem(); + + auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); + auto core_choice = std::make_shared>(mWindow, _T("CORE"), false); + + // populate list + for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + { + std::shared_ptr ed; + + // don't add statistics + if(iter->isStatistic) + continue; + + if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || + iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || + iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") + continue; + + // create ed and add it (and any related components) to mMenu + // ed's value will be set below + ComponentListRow row; + + if (iter->displayName == "emulator") + { + // if (file->getSystemEnvData()->mEmulators.size() <= 1) + // continue; + + std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); + + std::string currentEmul = file->getEmulator(); + + if (defaultEmul.length() == 0) + emul_choice->add(_T("DEFAULT"), "", false); + else + emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); + + for (auto core : file->getSystemEnvData()->mEmulators) + emul_choice->add(core.mName, core.mName, core.mName == currentEmul); + + row.addElement(std::make_shared(mWindow, _T("EMULATOR"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(emul_choice, false); + + mList->addRow(row); + emul_choice->setTag(iter->key); + mEditors.push_back(emul_choice); + + emul_choice->setSelectedChangedCallback([this, system, core_choice, file](std::string emulatorName) + { + std::string currentCore = file->getCore(); + + std::string defaultCore = system->getSystemEnvData()->getDefaultCore(emulatorName); + if (emulatorName.length() == 0) + defaultCore = system->getSystemEnvData()->getDefaultCore(system->getSystemEnvData()->getDefaultEmulator()); + + core_choice->clear(); + if (defaultCore.length() == 0) + core_choice->add(_T("DEFAULT"), "", false); + else + core_choice->add(_T("DEFAULT")+" ("+ defaultCore+")", "", false); + + std::vector cores = system->getSystemEnvData()->getCores(emulatorName); + + bool found = false; + + for (auto it = cores.begin(); it != cores.end(); it++) + { + std::string core = *it; + core_choice->add(core, core, currentCore == core); + if (currentCore == core) + found = true; + } + + if (!found) + core_choice->selectFirstItem(); + else + core_choice->invalidate(); + }); + + continue; + } + + if (iter->displayName == "core") + { + // core_choice->add(_T("DEFAULT"), "", true); + core_choice->setTag(iter->key); + + row.addElement(std::make_shared(mWindow, "CORE", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(core_choice, false); + + mList->addRow(row); + ed = core_choice; + + mEditors.push_back(core_choice); + + // force change event to load core list + emul_choice->invalidate(); + continue; + } + + auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + row.addElement(lbl, true); // label + + switch (iter->type) + { + case MD_BOOL: + { + ed = std::make_shared(window); + row.addElement(ed, false, true); + break; + } + case MD_RATING: + { + ed = std::make_shared(window); + const float height = lbl->getSize().y() * 0.71f; + ed->setSize(0, height); + row.addElement(ed, false, true); + + auto spacer = std::make_shared(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(window); + row.addElement(ed, false); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0); + row.addElement(spacer, false); + + // pass input to the actual DateTimeEditComponent 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(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); + row.addElement(ed, false); + break; + } + case MD_MULTILINE_STRING: + default: + { + // MD_STRING + ed = std::make_shared(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); + row.addElement(ed, true); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); + row.addElement(spacer, false); + + auto bracket = std::make_shared(mWindow); + bracket->setImage(":/arrow.svg"); + bracket->setResize(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->setTag(iter->key); + ed->setValue(mMetaData->get(iter->key)); + + mEditors.push_back(ed); + } + + std::vector< std::shared_ptr > buttons; + + if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) + buttons.push_back(std::make_shared(mWindow, _T("SCRAPE"), _T("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); + + buttons.push_back(std::make_shared(mWindow, _T("SAVE"), _T("SAVE"), [&] { save(); delete this; })); + buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); + + if(mDeleteFunc) + { + auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; + auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; + buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE"), deleteBtnFunc)); + } + + mButtons = makeButtonGrid(mWindow, buttons); + mGrid.setEntry(mButtons, Vector2i(0, 2), true, false); + + // resize + center + float width = (float)Math::min(Renderer::getScreenHeight(), (int)(Renderer::getScreenWidth() * 0.90f)); + setSize(width, 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() +{ + // remove game from index + mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); + + for (unsigned int i = 0; i < mEditors.size(); i++) + { + std::shared_ptr ed = mEditors.at(i); + + auto val = ed->getValue(); + auto key = ed->getTag(); + + if (key == "core" || key == "emulator") + { + std::shared_ptr> list = std::static_pointer_cast>(ed); + val = list->getSelected(); + } + + mMetaData->set(key, val); + } + + // enter game in index + mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); + + if (mSavedCallback) + mSavedCallback(); + + // update respective Collection Entries + CollectionSystemManager::get()->refreshCollectionSystems(mScraperParams.game); +} + +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++) + { + auto val = mEditors.at(i)->getValue(); + auto key = mEditors.at(i)->getTag(); + 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++) + { + auto key = mEditors.at(i)->getTag(); + if(mMetaData->get(key) != mEditors.at(i)->getValue()) + { + dirty = true; + break; + } + } + + std::function 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, + "ENREGISTRER LES MODIFICATIONS ?", + "OUI", [this, closeFunc] { save(); closeFunc(); }, + "NON", 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 GuiMetaDataEd::getHelpPrompts() +{ + std::vector prompts = mGrid.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + return prompts; +} diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index 7cd24b6f15..d05f77c3d4 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -1,588 +1,588 @@ -#include "GuiComponent.h" - -#include "animations/Animation.h" -#include "animations/AnimationController.h" -#include "Log.h" -#include "Renderer.h" -#include "ThemeData.h" -#include "Window.h" -#include - - -#if defined(_WIN32) -#define _conv(x) GuiTextTool::convertFromWideString(L ## x) -#else -#define _conv(x) x -#endif - -#include -#include -#include "resources\ResourceManager.h" - -std::vector GuiTextTool::mItems; -std::string GuiTextTool::mCurrentLanguage = "en"; -bool GuiTextTool::mCurrentLanguageLoaded = false; - -void GuiTextTool::setLanguage(std::string lang) -{ - mCurrentLanguage = lang; - mCurrentLanguageLoaded = false; -} - -void GuiTextTool::ensureLocalisation() -{ - if (mCurrentLanguageLoaded) - { - if (Settings::getInstance()->getString("Language") == mCurrentLanguage) - return; - - mCurrentLanguage = Settings::getInstance()->getString("Language"); - } - - mCurrentLanguageLoaded = true; - - for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) - delete (*it); - - mItems.clear(); - - std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/"+ mCurrentLanguage +"/emulationstation2.po"); +#include "GuiComponent.h" + +#include "animations/Animation.h" +#include "animations/AnimationController.h" +#include "Log.h" +#include "Renderer.h" +#include "ThemeData.h" +#include "Window.h" +#include + + +#if defined(_WIN32) +#define _conv(x) GuiTextTool::convertFromWideString(L ## x) +#else +#define _conv(x) x +#endif + +#include +#include +#include "resources\ResourceManager.h" + +std::vector GuiTextTool::mItems; +std::string GuiTextTool::mCurrentLanguage = "en"; +bool GuiTextTool::mCurrentLanguageLoaded = false; + +void GuiTextTool::setLanguage(std::string lang) +{ + mCurrentLanguage = lang; + mCurrentLanguageLoaded = false; +} + +void GuiTextTool::ensureLocalisation() +{ + if (mCurrentLanguageLoaded) + { + if (Settings::getInstance()->getString("Language") == mCurrentLanguage) + return; + + mCurrentLanguage = Settings::getInstance()->getString("Language"); + } + + mCurrentLanguageLoaded = true; + + for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) + delete (*it); + + mItems.clear(); + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/"+ mCurrentLanguage +"/emulationstation2.po"); if (Utils::FileSystem::exists(xmlpath)) - { - LocalizationItem* currentItem = NULL; - - std::ifstream file(xmlpath); - std::string str; - while (std::getline(file, str)) - { - if (str.length() > 0 && str[0] == '#') - { - if (currentItem != NULL && currentItem->msgid.length() > 0 && currentItem->msgstr.length() > 0) - mItems.push_back(currentItem); - - currentItem = new LocalizationItem(); - } - - if (currentItem != NULL && str.find("msgid") == 0) - { - auto start = str.find("\""); - if (start != std::string::npos) - { - auto end = str.find("\"", start + 1); - if (end != std::string::npos) - currentItem->msgid = str.substr(start + 1, end - start - 1); - } - } - - if (currentItem != NULL && str.find("msgstr") == 0) - { - auto start = str.find("\""); - if (start != std::string::npos) - { - auto end = str.find("\"", start + 1); - if (end != std::string::npos) - currentItem->msgstr = str.substr(start + 1, end - start - 1); - } - } - } - - if (currentItem != NULL) - delete currentItem; - } -} - - -const std::string GuiTextTool::localize(const std::string text) -{ - ensureLocalisation(); - + { + LocalizationItem* currentItem = NULL; + + std::ifstream file(xmlpath); + std::string str; + while (std::getline(file, str)) + { + if (str.length() > 0 && str[0] == '#') + { + if (currentItem != NULL && currentItem->msgid.length() > 0 && currentItem->msgstr.length() > 0) + mItems.push_back(currentItem); + + currentItem = new LocalizationItem(); + } + + if (currentItem != NULL && str.find("msgid") == 0) + { + auto start = str.find("\""); + if (start != std::string::npos) + { + auto end = str.find("\"", start + 1); + if (end != std::string::npos) + currentItem->msgid = str.substr(start + 1, end - start - 1); + } + } + + if (currentItem != NULL && str.find("msgstr") == 0) + { + auto start = str.find("\""); + if (start != std::string::npos) + { + auto end = str.find("\"", start + 1); + if (end != std::string::npos) + currentItem->msgstr = str.substr(start + 1, end - start - 1); + } + } + } + + if (currentItem != NULL) + delete currentItem; + } +} + + +const std::string GuiTextTool::localize(const std::string text) +{ + ensureLocalisation(); + for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) { if (text == (*it)->msgid) return (*it)->msgstr; - } - - return text; -} - -GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255), - mPosition(Vector3f::Zero()), mOrigin(Vector2f::Zero()), mRotationOrigin(0.5, 0.5), - mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false) -{ - for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) - mAnimationMap[i] = NULL; -} - -GuiComponent::~GuiComponent() -{ - mWindow->removeGui(this); - - cancelAllAnimations(); - - if(mParent) - mParent->removeChild(this); - - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->setParent(NULL); -} - -bool GuiComponent::input(InputConfig* config, Input input) -{ - for(unsigned int i = 0; i < getChildCount(); i++) - { - if(getChild(i)->input(config, input)) - return true; - } - - return false; -} - -void GuiComponent::updateSelf(int deltaTime) -{ - for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) - advanceAnimation(i, deltaTime); -} - -void GuiComponent::updateChildren(int deltaTime) -{ - for(unsigned int i = 0; i < getChildCount(); i++) - { - getChild(i)->update(deltaTime); - } -} - -void GuiComponent::update(int deltaTime) -{ - updateSelf(deltaTime); - updateChildren(deltaTime); -} - -void GuiComponent::render(const Transform4x4f& parentTrans) -{ - Transform4x4f trans = parentTrans * getTransform(); - renderChildren(trans); -} - -void GuiComponent::renderChildren(const Transform4x4f& transform) const -{ - for(unsigned int i = 0; i < getChildCount(); i++) - { - getChild(i)->render(transform); - } -} - -Vector3f GuiComponent::getPosition() const -{ - return mPosition; -} - -void GuiComponent::setPosition(float x, float y, float z) -{ - mPosition = Vector3f(x, y, z); - onPositionChanged(); -} - -Vector2f GuiComponent::getOrigin() const -{ - return mOrigin; -} - -void GuiComponent::setOrigin(float x, float y) -{ - mOrigin = Vector2f(x, y); - onOriginChanged(); -} - -Vector2f GuiComponent::getRotationOrigin() const -{ - return mRotationOrigin; -} - -void GuiComponent::setRotationOrigin(float x, float y) -{ - mRotationOrigin = Vector2f(x, y); -} - -Vector2f GuiComponent::getSize() const -{ - return mSize; -} - -void GuiComponent::setSize(float w, float h) -{ - mSize = Vector2f(w, h); - onSizeChanged(); -} - -float GuiComponent::getRotation() const -{ - return mRotation; -} - -void GuiComponent::setRotation(float rotation) -{ - mRotation = rotation; -} - -float GuiComponent::getScale() const -{ - return mScale; -} - -void GuiComponent::setScale(float scale) -{ - mScale = scale; -} - -float GuiComponent::getZIndex() const -{ - return mZIndex; -} - -void GuiComponent::setZIndex(float z) -{ - mZIndex = z; -} - -float GuiComponent::getDefaultZIndex() const -{ - return mDefaultZIndex; -} - -void GuiComponent::setDefaultZIndex(float z) -{ - mDefaultZIndex = z; -} - -Vector2f GuiComponent::getCenter() const -{ - return Vector2f(mPosition.x() - (getSize().x() * mOrigin.x()) + getSize().x() / 2, - mPosition.y() - (getSize().y() * mOrigin.y()) + getSize().y() / 2); -} - -//Children stuff. -void GuiComponent::addChild(GuiComponent* cmp) -{ - mChildren.push_back(cmp); - - if(cmp->getParent()) - cmp->getParent()->removeChild(cmp); - - cmp->setParent(this); -} - -void GuiComponent::removeChild(GuiComponent* cmp) -{ - if(!cmp->getParent()) - return; - - if(cmp->getParent() != this) - { - LOG(LogError) << "Tried to remove child from incorrect parent!"; - } - - cmp->setParent(NULL); - - for(auto i = mChildren.cbegin(); i != mChildren.cend(); i++) - { - if(*i == cmp) - { - mChildren.erase(i); - return; - } - } -} - -void GuiComponent::clearChildren() -{ - mChildren.clear(); -} - -void GuiComponent::sortChildren() -{ - std::stable_sort(mChildren.begin(), mChildren.end(), [](GuiComponent* a, GuiComponent* b) { - return b->getZIndex() > a->getZIndex(); - }); -} - -unsigned int GuiComponent::getChildCount() const -{ - return (int)mChildren.size(); -} - -GuiComponent* GuiComponent::getChild(unsigned int i) const -{ - return mChildren.at(i); -} - -void GuiComponent::setParent(GuiComponent* parent) -{ - mParent = parent; -} - -GuiComponent* GuiComponent::getParent() const -{ - return mParent; -} - -unsigned char GuiComponent::getOpacity() const -{ - return mOpacity; -} - -void GuiComponent::setOpacity(unsigned char opacity) -{ - mOpacity = opacity; - for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - (*it)->setOpacity(opacity); - } -} - -const Transform4x4f& GuiComponent::getTransform() -{ - mTransform = Transform4x4f::Identity(); - mTransform.translate(mPosition); - if (mScale != 1.0) - { - mTransform.scale(mScale); - } - if (mRotation != 0.0) - { - // Calculate offset as difference between origin and rotation origin - Vector2f rotationSize = getRotationSize(); - float xOff = (mOrigin.x() - mRotationOrigin.x()) * rotationSize.x(); - float yOff = (mOrigin.y() - mRotationOrigin.y()) * rotationSize.y(); - - // transform to offset point - if (xOff != 0.0 || yOff != 0.0) - mTransform.translate(Vector3f(xOff * -1, yOff * -1, 0.0f)); - - // apply rotation transform - mTransform.rotateZ(mRotation); - - // Tranform back to original point - if (xOff != 0.0 || yOff != 0.0) - mTransform.translate(Vector3f(xOff, yOff, 0.0f)); - } - mTransform.translate(Vector3f(mOrigin.x() * mSize.x() * -1, mOrigin.y() * mSize.y() * -1, 0.0f)); - return mTransform; -} - -void GuiComponent::setValue(const std::string& /*value*/) -{ -} - -std::string GuiComponent::getValue() const -{ - return ""; -} - -void GuiComponent::setTag(const std::string& value) -{ - mTag = value; -} - -std::string GuiComponent::getTag() const -{ - return mTag; -} - -void GuiComponent::textInput(const char* text) -{ - for(auto iter = mChildren.cbegin(); iter != mChildren.cend(); iter++) - { - (*iter)->textInput(text); - } -} - -void GuiComponent::setAnimation(Animation* anim, int delay, std::function finishedCallback, bool reverse, unsigned char slot) -{ - assert(slot < MAX_ANIMATIONS); - - AnimationController* oldAnim = mAnimationMap[slot]; - mAnimationMap[slot] = new AnimationController(anim, delay, finishedCallback, reverse); - - if(oldAnim) - delete oldAnim; -} - -bool GuiComponent::stopAnimation(unsigned char slot) -{ - assert(slot < MAX_ANIMATIONS); - if(mAnimationMap[slot]) - { - delete mAnimationMap[slot]; - mAnimationMap[slot] = NULL; - return true; - }else{ - return false; - } -} - -bool GuiComponent::cancelAnimation(unsigned char slot) -{ - assert(slot < MAX_ANIMATIONS); - if(mAnimationMap[slot]) - { - mAnimationMap[slot]->removeFinishedCallback(); - delete mAnimationMap[slot]; - mAnimationMap[slot] = NULL; - return true; - }else{ - return false; - } -} - -bool GuiComponent::finishAnimation(unsigned char slot) -{ - assert(slot < MAX_ANIMATIONS); - if(mAnimationMap[slot]) - { - // skip to animation's end - const bool done = mAnimationMap[slot]->update(mAnimationMap[slot]->getAnimation()->getDuration() - mAnimationMap[slot]->getTime()); - assert(done); - - delete mAnimationMap[slot]; // will also call finishedCallback - mAnimationMap[slot] = NULL; - return true; - }else{ - return false; - } -} - -bool GuiComponent::advanceAnimation(unsigned char slot, unsigned int time) -{ - assert(slot < MAX_ANIMATIONS); - AnimationController* anim = mAnimationMap[slot]; - if(anim) - { - bool done = anim->update(time); - if(done) - { - mAnimationMap[slot] = NULL; - delete anim; - } - return true; - }else{ - return false; - } -} - -void GuiComponent::stopAllAnimations() -{ - for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) - stopAnimation(i); -} - -void GuiComponent::cancelAllAnimations() -{ - for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) - cancelAnimation(i); -} - -bool GuiComponent::isAnimationPlaying(unsigned char slot) const -{ - return mAnimationMap[slot] != NULL; -} - -bool GuiComponent::isAnimationReversed(unsigned char slot) const -{ - assert(mAnimationMap[slot] != NULL); - return mAnimationMap[slot]->isReversed(); -} - -int GuiComponent::getAnimationTime(unsigned char slot) const -{ - assert(mAnimationMap[slot] != NULL); - return mAnimationMap[slot]->getTime(); -} - -void GuiComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) -{ - Vector2f scale = getParent() ? getParent()->getSize() : Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - - const ThemeData::ThemeElement* elem = theme->getElement(view, element, ""); - if(!elem) - return; - - using namespace ThemeFlags; - if(properties & POSITION && elem->has("pos")) - { - Vector2f denormalized = elem->get("pos") * scale; - setPosition(Vector3f(denormalized.x(), denormalized.y(), 0)); - } - - if(properties & ThemeFlags::SIZE && elem->has("size")) - setSize(elem->get("size") * scale); - - // position + size also implies origin - if((properties & ORIGIN || (properties & POSITION && properties & ThemeFlags::SIZE)) && elem->has("origin")) - setOrigin(elem->get("origin")); - - if(properties & ThemeFlags::ROTATION) { - if(elem->has("rotation")) - setRotationDegrees(elem->get("rotation")); - if(elem->has("rotationOrigin")) - setRotationOrigin(elem->get("rotationOrigin")); - } - - if(properties & ThemeFlags::Z_INDEX && elem->has("zIndex")) - setZIndex(elem->get("zIndex")); - else - setZIndex(getDefaultZIndex()); -} - -void GuiComponent::updateHelpPrompts() -{ - if(getParent()) - { - getParent()->updateHelpPrompts(); - return; - } - - std::vector prompts = getHelpPrompts(); - - if(mWindow->peekGui() == this) - mWindow->setHelpPrompts(prompts, getHelpStyle()); -} - -HelpStyle GuiComponent::getHelpStyle() -{ - return HelpStyle(); -} - -bool GuiComponent::isProcessing() const -{ - return mIsProcessing; -} - -void GuiComponent::onShow() -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->onShow(); -} - -void GuiComponent::onHide() -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->onHide(); -} - -void GuiComponent::onScreenSaverActivate() -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->onScreenSaverActivate(); -} - -void GuiComponent::onScreenSaverDeactivate() -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->onScreenSaverDeactivate(); -} - -void GuiComponent::topWindow(bool isTop) -{ - for(unsigned int i = 0; i < getChildCount(); i++) - getChild(i)->topWindow(isTop); + } + + return text; +} + +GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255), + mPosition(Vector3f::Zero()), mOrigin(Vector2f::Zero()), mRotationOrigin(0.5, 0.5), + mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false) +{ + for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) + mAnimationMap[i] = NULL; +} + +GuiComponent::~GuiComponent() +{ + mWindow->removeGui(this); + + cancelAllAnimations(); + + if(mParent) + mParent->removeChild(this); + + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->setParent(NULL); +} + +bool GuiComponent::input(InputConfig* config, Input input) +{ + for(unsigned int i = 0; i < getChildCount(); i++) + { + if(getChild(i)->input(config, input)) + return true; + } + + return false; +} + +void GuiComponent::updateSelf(int deltaTime) +{ + for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) + advanceAnimation(i, deltaTime); +} + +void GuiComponent::updateChildren(int deltaTime) +{ + for(unsigned int i = 0; i < getChildCount(); i++) + { + getChild(i)->update(deltaTime); + } +} + +void GuiComponent::update(int deltaTime) +{ + updateSelf(deltaTime); + updateChildren(deltaTime); +} + +void GuiComponent::render(const Transform4x4f& parentTrans) +{ + Transform4x4f trans = parentTrans * getTransform(); + renderChildren(trans); +} + +void GuiComponent::renderChildren(const Transform4x4f& transform) const +{ + for(unsigned int i = 0; i < getChildCount(); i++) + { + getChild(i)->render(transform); + } +} + +Vector3f GuiComponent::getPosition() const +{ + return mPosition; +} + +void GuiComponent::setPosition(float x, float y, float z) +{ + mPosition = Vector3f(x, y, z); + onPositionChanged(); +} + +Vector2f GuiComponent::getOrigin() const +{ + return mOrigin; +} + +void GuiComponent::setOrigin(float x, float y) +{ + mOrigin = Vector2f(x, y); + onOriginChanged(); +} + +Vector2f GuiComponent::getRotationOrigin() const +{ + return mRotationOrigin; +} + +void GuiComponent::setRotationOrigin(float x, float y) +{ + mRotationOrigin = Vector2f(x, y); +} + +Vector2f GuiComponent::getSize() const +{ + return mSize; +} + +void GuiComponent::setSize(float w, float h) +{ + mSize = Vector2f(w, h); + onSizeChanged(); +} + +float GuiComponent::getRotation() const +{ + return mRotation; +} + +void GuiComponent::setRotation(float rotation) +{ + mRotation = rotation; +} + +float GuiComponent::getScale() const +{ + return mScale; +} + +void GuiComponent::setScale(float scale) +{ + mScale = scale; +} + +float GuiComponent::getZIndex() const +{ + return mZIndex; +} + +void GuiComponent::setZIndex(float z) +{ + mZIndex = z; +} + +float GuiComponent::getDefaultZIndex() const +{ + return mDefaultZIndex; +} + +void GuiComponent::setDefaultZIndex(float z) +{ + mDefaultZIndex = z; +} + +Vector2f GuiComponent::getCenter() const +{ + return Vector2f(mPosition.x() - (getSize().x() * mOrigin.x()) + getSize().x() / 2, + mPosition.y() - (getSize().y() * mOrigin.y()) + getSize().y() / 2); +} + +//Children stuff. +void GuiComponent::addChild(GuiComponent* cmp) +{ + mChildren.push_back(cmp); + + if(cmp->getParent()) + cmp->getParent()->removeChild(cmp); + + cmp->setParent(this); +} + +void GuiComponent::removeChild(GuiComponent* cmp) +{ + if(!cmp->getParent()) + return; + + if(cmp->getParent() != this) + { + LOG(LogError) << "Tried to remove child from incorrect parent!"; + } + + cmp->setParent(NULL); + + for(auto i = mChildren.cbegin(); i != mChildren.cend(); i++) + { + if(*i == cmp) + { + mChildren.erase(i); + return; + } + } +} + +void GuiComponent::clearChildren() +{ + mChildren.clear(); +} + +void GuiComponent::sortChildren() +{ + std::stable_sort(mChildren.begin(), mChildren.end(), [](GuiComponent* a, GuiComponent* b) { + return b->getZIndex() > a->getZIndex(); + }); +} + +unsigned int GuiComponent::getChildCount() const +{ + return (int)mChildren.size(); +} + +GuiComponent* GuiComponent::getChild(unsigned int i) const +{ + return mChildren.at(i); +} + +void GuiComponent::setParent(GuiComponent* parent) +{ + mParent = parent; +} + +GuiComponent* GuiComponent::getParent() const +{ + return mParent; +} + +unsigned char GuiComponent::getOpacity() const +{ + return mOpacity; +} + +void GuiComponent::setOpacity(unsigned char opacity) +{ + mOpacity = opacity; + for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + (*it)->setOpacity(opacity); + } +} + +const Transform4x4f& GuiComponent::getTransform() +{ + mTransform = Transform4x4f::Identity(); + mTransform.translate(mPosition); + if (mScale != 1.0) + { + mTransform.scale(mScale); + } + if (mRotation != 0.0) + { + // Calculate offset as difference between origin and rotation origin + Vector2f rotationSize = getRotationSize(); + float xOff = (mOrigin.x() - mRotationOrigin.x()) * rotationSize.x(); + float yOff = (mOrigin.y() - mRotationOrigin.y()) * rotationSize.y(); + + // transform to offset point + if (xOff != 0.0 || yOff != 0.0) + mTransform.translate(Vector3f(xOff * -1, yOff * -1, 0.0f)); + + // apply rotation transform + mTransform.rotateZ(mRotation); + + // Tranform back to original point + if (xOff != 0.0 || yOff != 0.0) + mTransform.translate(Vector3f(xOff, yOff, 0.0f)); + } + mTransform.translate(Vector3f(mOrigin.x() * mSize.x() * -1, mOrigin.y() * mSize.y() * -1, 0.0f)); + return mTransform; +} + +void GuiComponent::setValue(const std::string& /*value*/) +{ +} + +std::string GuiComponent::getValue() const +{ + return ""; +} + +void GuiComponent::setTag(const std::string& value) +{ + mTag = value; +} + +std::string GuiComponent::getTag() const +{ + return mTag; +} + +void GuiComponent::textInput(const char* text) +{ + for(auto iter = mChildren.cbegin(); iter != mChildren.cend(); iter++) + { + (*iter)->textInput(text); + } +} + +void GuiComponent::setAnimation(Animation* anim, int delay, std::function finishedCallback, bool reverse, unsigned char slot) +{ + assert(slot < MAX_ANIMATIONS); + + AnimationController* oldAnim = mAnimationMap[slot]; + mAnimationMap[slot] = new AnimationController(anim, delay, finishedCallback, reverse); + + if(oldAnim) + delete oldAnim; +} + +bool GuiComponent::stopAnimation(unsigned char slot) +{ + assert(slot < MAX_ANIMATIONS); + if(mAnimationMap[slot]) + { + delete mAnimationMap[slot]; + mAnimationMap[slot] = NULL; + return true; + }else{ + return false; + } +} + +bool GuiComponent::cancelAnimation(unsigned char slot) +{ + assert(slot < MAX_ANIMATIONS); + if(mAnimationMap[slot]) + { + mAnimationMap[slot]->removeFinishedCallback(); + delete mAnimationMap[slot]; + mAnimationMap[slot] = NULL; + return true; + }else{ + return false; + } +} + +bool GuiComponent::finishAnimation(unsigned char slot) +{ + assert(slot < MAX_ANIMATIONS); + if(mAnimationMap[slot]) + { + // skip to animation's end + const bool done = mAnimationMap[slot]->update(mAnimationMap[slot]->getAnimation()->getDuration() - mAnimationMap[slot]->getTime()); + assert(done); + + delete mAnimationMap[slot]; // will also call finishedCallback + mAnimationMap[slot] = NULL; + return true; + }else{ + return false; + } +} + +bool GuiComponent::advanceAnimation(unsigned char slot, unsigned int time) +{ + assert(slot < MAX_ANIMATIONS); + AnimationController* anim = mAnimationMap[slot]; + if(anim) + { + bool done = anim->update(time); + if(done) + { + mAnimationMap[slot] = NULL; + delete anim; + } + return true; + }else{ + return false; + } +} + +void GuiComponent::stopAllAnimations() +{ + for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) + stopAnimation(i); +} + +void GuiComponent::cancelAllAnimations() +{ + for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) + cancelAnimation(i); +} + +bool GuiComponent::isAnimationPlaying(unsigned char slot) const +{ + return mAnimationMap[slot] != NULL; +} + +bool GuiComponent::isAnimationReversed(unsigned char slot) const +{ + assert(mAnimationMap[slot] != NULL); + return mAnimationMap[slot]->isReversed(); +} + +int GuiComponent::getAnimationTime(unsigned char slot) const +{ + assert(mAnimationMap[slot] != NULL); + return mAnimationMap[slot]->getTime(); +} + +void GuiComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) +{ + Vector2f scale = getParent() ? getParent()->getSize() : Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + + const ThemeData::ThemeElement* elem = theme->getElement(view, element, ""); + if(!elem) + return; + + using namespace ThemeFlags; + if(properties & POSITION && elem->has("pos")) + { + Vector2f denormalized = elem->get("pos") * scale; + setPosition(Vector3f(denormalized.x(), denormalized.y(), 0)); + } + + if(properties & ThemeFlags::SIZE && elem->has("size")) + setSize(elem->get("size") * scale); + + // position + size also implies origin + if((properties & ORIGIN || (properties & POSITION && properties & ThemeFlags::SIZE)) && elem->has("origin")) + setOrigin(elem->get("origin")); + + if(properties & ThemeFlags::ROTATION) { + if(elem->has("rotation")) + setRotationDegrees(elem->get("rotation")); + if(elem->has("rotationOrigin")) + setRotationOrigin(elem->get("rotationOrigin")); + } + + if(properties & ThemeFlags::Z_INDEX && elem->has("zIndex")) + setZIndex(elem->get("zIndex")); + else + setZIndex(getDefaultZIndex()); +} + +void GuiComponent::updateHelpPrompts() +{ + if(getParent()) + { + getParent()->updateHelpPrompts(); + return; + } + + std::vector prompts = getHelpPrompts(); + + if(mWindow->peekGui() == this) + mWindow->setHelpPrompts(prompts, getHelpStyle()); +} + +HelpStyle GuiComponent::getHelpStyle() +{ + return HelpStyle(); +} + +bool GuiComponent::isProcessing() const +{ + return mIsProcessing; +} + +void GuiComponent::onShow() +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->onShow(); +} + +void GuiComponent::onHide() +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->onHide(); +} + +void GuiComponent::onScreenSaverActivate() +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->onScreenSaverActivate(); +} + +void GuiComponent::onScreenSaverDeactivate() +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->onScreenSaverDeactivate(); +} + +void GuiComponent::topWindow(bool isTop) +{ + for(unsigned int i = 0; i < getChildCount(); i++) + getChild(i)->topWindow(isTop); } \ No newline at end of file diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 4712ce32f5..d850727610 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -1,222 +1,222 @@ -#pragma once -#ifndef ES_CORE_GUI_COMPONENT_H -#define ES_CORE_GUI_COMPONENT_H - -#include "math/Misc.h" -#include "math/Transform4x4f.h" -#include "HelpPrompt.h" -#include "HelpStyle.h" -#include "InputConfig.h" -#include -#include - -#if defined(_WIN32) -#include -#endif - -class Animation; -class AnimationController; -class Font; -class InputConfig; -class ThemeData; -class Window; - -class GuiComponent -{ -public: - GuiComponent(Window* window); - virtual ~GuiComponent(); - - virtual void textInput(const char* text); - - //Called when input is received. - //Return true if the input is consumed, false if it should continue to be passed to other children. - virtual bool input(InputConfig* config, Input input); - - //Called when time passes. Default implementation calls updateSelf(deltaTime) and updateChildren(deltaTime) - so you should probably call GuiComponent::update(deltaTime) at some point (or at least updateSelf so animations work). - virtual void update(int deltaTime); - - //Called when it's time to render. By default, just calls renderChildren(parentTrans * getTransform()). - //You probably want to override this like so: - //1. Calculate the new transform that your control will draw at with Transform4x4f t = parentTrans * getTransform(). - //2. Set the renderer to use that new transform as the model matrix - Renderer::setMatrix(t); - //3. Draw your component. - //4. Tell your children to render, based on your component's transform - renderChildren(t). - virtual void render(const Transform4x4f& parentTrans); - - Vector3f getPosition() const; - inline void setPosition(const Vector3f& offset) { setPosition(offset.x(), offset.y(), offset.z()); } - void setPosition(float x, float y, float z = 0.0f); - virtual void onPositionChanged() {}; - - //Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) - Vector2f getOrigin() const; - void setOrigin(float originX, float originY); - inline void setOrigin(Vector2f origin) { setOrigin(origin.x(), origin.y()); } - virtual void onOriginChanged() {}; - - //Sets the rotation origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) - Vector2f getRotationOrigin() const; - void setRotationOrigin(float originX, float originY); - inline void setRotationOrigin(Vector2f origin) { setRotationOrigin(origin.x(), origin.y()); } - - virtual Vector2f getSize() const; - inline void setSize(const Vector2f& size) { setSize(size.x(), size.y()); } - void setSize(float w, float h); - virtual void onSizeChanged() {}; - - virtual Vector2f getRotationSize() const { return getSize(); }; - - float getRotation() const; - void setRotation(float rotation); - inline void setRotationDegrees(float rotation) { setRotation((float)ES_DEG_TO_RAD(rotation)); } - - float getScale() const; - void setScale(float scale); - - float getZIndex() const; - void setZIndex(float zIndex); - - float getDefaultZIndex() const; - void setDefaultZIndex(float zIndex); - - // Returns the center point of the image (takes origin into account). - Vector2f getCenter() const; - - void setParent(GuiComponent* parent); - GuiComponent* getParent() const; - - void addChild(GuiComponent* cmp); - void removeChild(GuiComponent* cmp); - void clearChildren(); - void sortChildren(); - unsigned int getChildCount() const; - GuiComponent* getChild(unsigned int i) const; - - // animation will be automatically deleted when it completes or is stopped. - bool isAnimationPlaying(unsigned char slot) const; - bool isAnimationReversed(unsigned char slot) const; - int getAnimationTime(unsigned char slot) const; - void setAnimation(Animation* animation, int delay = 0, std::function finishedCallback = nullptr, bool reverse = false, unsigned char slot = 0); - bool stopAnimation(unsigned char slot); - bool cancelAnimation(unsigned char slot); // Like stopAnimation, but doesn't call finishedCallback - only removes the animation, leaving things in their current state. Returns true if successful (an animation was in this slot). - bool finishAnimation(unsigned char slot); // Calls update(1.f) and finishedCallback, then deletes the animation - basically skips to the end. Returns true if successful (an animation was in this slot). - bool advanceAnimation(unsigned char slot, unsigned int time); // Returns true if successful (an animation was in this slot). - void stopAllAnimations(); - void cancelAllAnimations(); - - virtual unsigned char getOpacity() const; - virtual void setOpacity(unsigned char opacity); - - const Transform4x4f& getTransform(); - - virtual std::string getValue() const; - virtual void setValue(const std::string& value); - - - virtual std::string getTag() const; - virtual void setTag(const std::string& value); - - virtual void onFocusGained() {}; - virtual void onFocusLost() {}; - - virtual void onShow(); - virtual void onHide(); - - virtual void onScreenSaverActivate(); - virtual void onScreenSaverDeactivate(); - virtual void topWindow(bool isTop); - - // Default implementation just handles and tags as normalized float pairs. - // You probably want to keep this behavior for any derived classes as well as add your own. - virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); - - // Returns a list of help prompts. - virtual std::vector getHelpPrompts() { return std::vector(); }; - - // Called whenever help prompts change. - void updateHelpPrompts(); - - virtual HelpStyle getHelpStyle(); - - // Returns true if the component is busy doing background processing (e.g. HTTP downloads) - bool isProcessing() const; - -protected: - void renderChildren(const Transform4x4f& transform) const; - void updateSelf(int deltaTime); // updates animations - void updateChildren(int deltaTime); // updates animations - - unsigned char mOpacity; - Window* mWindow; - - GuiComponent* mParent; - std::vector mChildren; - - std::string mTag; - - Vector3f mPosition; - Vector2f mOrigin; - Vector2f mRotationOrigin; - Vector2f mSize; - - float mRotation = 0.0; - float mScale = 1.0; - - float mDefaultZIndex = 0; - float mZIndex = 0; - - bool mIsProcessing; - -public: - const static unsigned char MAX_ANIMATIONS = 4; - -private: - Transform4x4f mTransform; //Don't access this directly! Use getTransform()! - AnimationController* mAnimationMap[MAX_ANIMATIONS]; -}; - - - -class LocalizationItem -{ -public: - std::string msgid; - std::string msgstr; -}; - -class GuiTextTool -{ -public: -#if defined(_WIN32) - static const std::string convertFromWideString(const std::wstring wstring) - { - int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); - - std::string string; - string.resize(numBytes); - WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); - - return string; - } -#endif - static void ensureLocalisation(); - static const std::string localize(const std::string text); - - static void setLanguage(std::string lang); - -private: - static std::vector mItems; - static std::string mCurrentLanguage; - static bool mCurrentLanguageLoaded; -}; - -#if defined(_WIN32) -#define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) -#define _L(x) GuiTextTool::localize(x) -#else -#define _T(x) GuiTextTool::localize(x) -#define _L(x) GuiTextTool::localize(x) -#endif // _WIN32 - -#endif // ES_CORE_GUI_COMPONENT_H +#pragma once +#ifndef ES_CORE_GUI_COMPONENT_H +#define ES_CORE_GUI_COMPONENT_H + +#include "math/Misc.h" +#include "math/Transform4x4f.h" +#include "HelpPrompt.h" +#include "HelpStyle.h" +#include "InputConfig.h" +#include +#include + +#if defined(_WIN32) +#include +#endif + +class Animation; +class AnimationController; +class Font; +class InputConfig; +class ThemeData; +class Window; + +class GuiComponent +{ +public: + GuiComponent(Window* window); + virtual ~GuiComponent(); + + virtual void textInput(const char* text); + + //Called when input is received. + //Return true if the input is consumed, false if it should continue to be passed to other children. + virtual bool input(InputConfig* config, Input input); + + //Called when time passes. Default implementation calls updateSelf(deltaTime) and updateChildren(deltaTime) - so you should probably call GuiComponent::update(deltaTime) at some point (or at least updateSelf so animations work). + virtual void update(int deltaTime); + + //Called when it's time to render. By default, just calls renderChildren(parentTrans * getTransform()). + //You probably want to override this like so: + //1. Calculate the new transform that your control will draw at with Transform4x4f t = parentTrans * getTransform(). + //2. Set the renderer to use that new transform as the model matrix - Renderer::setMatrix(t); + //3. Draw your component. + //4. Tell your children to render, based on your component's transform - renderChildren(t). + virtual void render(const Transform4x4f& parentTrans); + + Vector3f getPosition() const; + inline void setPosition(const Vector3f& offset) { setPosition(offset.x(), offset.y(), offset.z()); } + void setPosition(float x, float y, float z = 0.0f); + virtual void onPositionChanged() {}; + + //Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) + Vector2f getOrigin() const; + void setOrigin(float originX, float originY); + inline void setOrigin(Vector2f origin) { setOrigin(origin.x(), origin.y()); } + virtual void onOriginChanged() {}; + + //Sets the rotation origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) + Vector2f getRotationOrigin() const; + void setRotationOrigin(float originX, float originY); + inline void setRotationOrigin(Vector2f origin) { setRotationOrigin(origin.x(), origin.y()); } + + virtual Vector2f getSize() const; + inline void setSize(const Vector2f& size) { setSize(size.x(), size.y()); } + void setSize(float w, float h); + virtual void onSizeChanged() {}; + + virtual Vector2f getRotationSize() const { return getSize(); }; + + float getRotation() const; + void setRotation(float rotation); + inline void setRotationDegrees(float rotation) { setRotation((float)ES_DEG_TO_RAD(rotation)); } + + float getScale() const; + void setScale(float scale); + + float getZIndex() const; + void setZIndex(float zIndex); + + float getDefaultZIndex() const; + void setDefaultZIndex(float zIndex); + + // Returns the center point of the image (takes origin into account). + Vector2f getCenter() const; + + void setParent(GuiComponent* parent); + GuiComponent* getParent() const; + + void addChild(GuiComponent* cmp); + void removeChild(GuiComponent* cmp); + void clearChildren(); + void sortChildren(); + unsigned int getChildCount() const; + GuiComponent* getChild(unsigned int i) const; + + // animation will be automatically deleted when it completes or is stopped. + bool isAnimationPlaying(unsigned char slot) const; + bool isAnimationReversed(unsigned char slot) const; + int getAnimationTime(unsigned char slot) const; + void setAnimation(Animation* animation, int delay = 0, std::function finishedCallback = nullptr, bool reverse = false, unsigned char slot = 0); + bool stopAnimation(unsigned char slot); + bool cancelAnimation(unsigned char slot); // Like stopAnimation, but doesn't call finishedCallback - only removes the animation, leaving things in their current state. Returns true if successful (an animation was in this slot). + bool finishAnimation(unsigned char slot); // Calls update(1.f) and finishedCallback, then deletes the animation - basically skips to the end. Returns true if successful (an animation was in this slot). + bool advanceAnimation(unsigned char slot, unsigned int time); // Returns true if successful (an animation was in this slot). + void stopAllAnimations(); + void cancelAllAnimations(); + + virtual unsigned char getOpacity() const; + virtual void setOpacity(unsigned char opacity); + + const Transform4x4f& getTransform(); + + virtual std::string getValue() const; + virtual void setValue(const std::string& value); + + + virtual std::string getTag() const; + virtual void setTag(const std::string& value); + + virtual void onFocusGained() {}; + virtual void onFocusLost() {}; + + virtual void onShow(); + virtual void onHide(); + + virtual void onScreenSaverActivate(); + virtual void onScreenSaverDeactivate(); + virtual void topWindow(bool isTop); + + // Default implementation just handles and tags as normalized float pairs. + // You probably want to keep this behavior for any derived classes as well as add your own. + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); + + // Returns a list of help prompts. + virtual std::vector getHelpPrompts() { return std::vector(); }; + + // Called whenever help prompts change. + void updateHelpPrompts(); + + virtual HelpStyle getHelpStyle(); + + // Returns true if the component is busy doing background processing (e.g. HTTP downloads) + bool isProcessing() const; + +protected: + void renderChildren(const Transform4x4f& transform) const; + void updateSelf(int deltaTime); // updates animations + void updateChildren(int deltaTime); // updates animations + + unsigned char mOpacity; + Window* mWindow; + + GuiComponent* mParent; + std::vector mChildren; + + std::string mTag; + + Vector3f mPosition; + Vector2f mOrigin; + Vector2f mRotationOrigin; + Vector2f mSize; + + float mRotation = 0.0; + float mScale = 1.0; + + float mDefaultZIndex = 0; + float mZIndex = 0; + + bool mIsProcessing; + +public: + const static unsigned char MAX_ANIMATIONS = 4; + +private: + Transform4x4f mTransform; //Don't access this directly! Use getTransform()! + AnimationController* mAnimationMap[MAX_ANIMATIONS]; +}; + + + +class LocalizationItem +{ +public: + std::string msgid; + std::string msgstr; +}; + +class GuiTextTool +{ +public: +#if defined(_WIN32) + static const std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + + std::string string; + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return string; + } +#endif + static void ensureLocalisation(); + static const std::string localize(const std::string text); + + static void setLanguage(std::string lang); + +private: + static std::vector mItems; + static std::string mCurrentLanguage; + static bool mCurrentLanguageLoaded; +}; + +#if defined(_WIN32) +#define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) +#define _L(x) GuiTextTool::localize(x) +#else +#define _T(x) GuiTextTool::localize(x) +#define _L(x) GuiTextTool::localize(x) +#endif // _WIN32 + +#endif // ES_CORE_GUI_COMPONENT_H diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 9ee8ea2fc9..ba4121ce08 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -1,727 +1,730 @@ -#include "ThemeData.h" - -#include "components/ImageComponent.h" -#include "components/TextComponent.h" -#include "utils/FileSystemUtil.h" -#include "Log.h" -#include "platform.h" -#include "Settings.h" -#include -#include - -std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" } }; -std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; - -std::map> ThemeData::sElementMap { - { "image", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "maxSize", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "path", PATH }, - { "default", PATH }, - { "tile", BOOLEAN }, - { "color", COLOR }, - { "zIndex", FLOAT } } }, - { "imagegrid", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "margin", NORMALIZED_PAIR }, - { "gameImage", PATH }, - { "folderImage", PATH }, - { "scrollDirection", STRING } } }, - { "gridtile", { - { "size", NORMALIZED_PAIR }, - { "padding", NORMALIZED_PAIR }, - { "imageColor", COLOR }, - { "backgroundImage", PATH }, - { "backgroundCornerSize", NORMALIZED_PAIR }, - { "backgroundColor", COLOR }, - { "backgroundCenterColor", COLOR }, - { "backgroundEdgeColor", COLOR }, - { "imageSizeMode", STRING } } }, - { "text", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "text", STRING }, - { "backgroundColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "color", COLOR }, - { "alignment", STRING }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "value", STRING }, - { "zIndex", FLOAT } } }, - { "textlist", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "selectorHeight", FLOAT }, - { "selectorOffsetY", FLOAT }, - { "selectorColor", COLOR }, - { "selectorImagePath", PATH }, - { "selectorImageTile", BOOLEAN }, - { "selectedColor", COLOR }, - { "primaryColor", COLOR }, - { "secondaryColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "scrollSound", PATH }, - { "alignment", STRING }, - { "horizontalMargin", FLOAT }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "zIndex", FLOAT } } }, - { "container", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "zIndex", FLOAT } } }, - { "ninepatch", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "path", PATH }, - { "zIndex", FLOAT } } }, - { "datetime", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "backgroundColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "color", COLOR }, - { "alignment", STRING }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "value", STRING }, - { "format", STRING }, - { "displayRelative", BOOLEAN }, - { "zIndex", FLOAT } } }, - { "rating", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "color", COLOR }, - { "filledPath", PATH }, - { "unfilledPath", PATH }, - { "zIndex", FLOAT } } }, - { "sound", { - { "path", PATH } } }, - { "helpsystem", { - { "pos", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "textColor", COLOR }, - { "iconColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT } } }, - { "video", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "maxSize", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "default", PATH }, - { "delay", FLOAT }, - { "zIndex", FLOAT }, - { "showSnapshotNoVideo", BOOLEAN }, - { "showSnapshotDelay", BOOLEAN } } }, - { "carousel", { - { "type", STRING }, - { "size", NORMALIZED_PAIR }, - { "pos", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "color", COLOR }, - { "logoScale", FLOAT }, - { "logoRotation", FLOAT }, - { "logoRotationOrigin", NORMALIZED_PAIR }, - { "logoSize", NORMALIZED_PAIR }, - { "logoAlignment", STRING }, - { "maxLogoCount", FLOAT }, - { "zIndex", FLOAT } } } -}; - -#define MINIMUM_THEME_FORMAT_VERSION 3 -#define CURRENT_THEME_FORMAT_VERSION 6 - -// helper -unsigned int getHexColor(const char* str) -{ - ThemeException error; - if(!str) - throw error << "Empty color"; - - size_t len = strlen(str); - if(len != 6 && len != 8) - throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; - - unsigned int val; - std::stringstream ss; - ss << str; - ss >> std::hex >> val; - - if(len == 6) - val = (val << 8) | 0xFF; - - return val; -} - -std::map mVariables; - -std::string resolvePlaceholders(const char* in) -{ - std::string inStr(in); - - if(inStr.empty()) - return inStr; - - const size_t variableBegin = inStr.find("${"); - const size_t variableEnd = inStr.find("}", variableBegin); - - if((variableBegin == std::string::npos) || (variableEnd == std::string::npos)) - return inStr; - - std::string prefix = inStr.substr(0, variableBegin); - std::string replace = inStr.substr(variableBegin + 2, variableEnd - (variableBegin + 2)); - std::string suffix = resolvePlaceholders(inStr.substr(variableEnd + 1).c_str()); - - return prefix + mVariables[replace] + suffix; -} - -ThemeData::ThemeData() -{ - mVersion = 0; -} - -void ThemeData::loadFile(std::string system, std::map sysDataMap, const std::string& path) -{ - mPaths.push_back(path); - - ThemeException error; - error.setFiles(mPaths); - - if(!Utils::FileSystem::exists(path)) - throw error << "File does not exist!"; - - mVersion = 0; - mViews.clear(); - - mSystemThemeFolder = system; - - mVariables.clear(); - mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); - - pugi::xml_document doc; - pugi::xml_parse_result res = doc.load_file(path.c_str()); - if(!res) - throw error << "XML parsing error: \n " << res.description(); - - pugi::xml_node root = doc.child("theme"); - if(!root) - throw error << "Missing tag!"; - - // parse version - mVersion = root.child("formatVersion").text().as_float(-404); - if(mVersion == -404) - throw error << " tag missing!\n It's either out of date or you need to add " << CURRENT_THEME_FORMAT_VERSION << " inside your tag."; - - if (root.attribute("defaultView")) - mDefaultView = root.attribute("defaultView").as_string(); - - if(mVersion < MINIMUM_THEME_FORMAT_VERSION) - throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; - - parseVariables(root); - parseIncludes(root); - parseViews(root); - parseFeatures(root); -} - -std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) -{ - std::string result = path; - - size_t start_pos = result.find("$system"); - if (start_pos == std::string::npos) - return path; - - result.replace(start_pos, 7, systemThemeFolder); - - //result.replace("$system", systemThemeFolder); - //boost::algorithm::replace_first(result, "$system", systemThemeFolder); - return result; -} - -bool ThemeData::parseSubset(const pugi::xml_node& node) -{ - bool parse = true; - - if (node.attribute("subset")) - { - parse = false; - const std::string subsetAttr = node.attribute("subset").as_string(); - const std::string nameAttr = node.attribute("name").as_string(); - - if (subsetAttr == "iconset" || subsetAttr == "gamelistview") - { - if (nameAttr.rfind("1-") != std::string::npos) - { - parse = true; - return parse; - } - } - else if (subsetAttr == "systemview") - { - if (nameAttr.rfind("8-") != std::string::npos) - { - parse = true; - return parse; - } - } - else if (nameAttr.rfind("2-") != std::string::npos) - { - parse = true; - return parse; - } - /* - if (subsetAttr == "colorset" && nameAttr == mColorset) - { - parse = true; - return parse; - } - if (subsetAttr == "iconset" && nameAttr == mIconset) - { - parse = true; - return parse; - } - if (subsetAttr == "menu" && nameAttr == mMenu) - { - parse = true; - return parse; - } - if (subsetAttr == "systemview" && nameAttr == mSystemview) - { - parse = true; - return parse; - } - if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) - { - parse = true; - return parse; - }*/ - } - - return parse; -} - -void ThemeData::parseIncludes(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) - { - if (!parseSubset(node)) - continue; - - std::string relPath = resolvePlaceholders(node.text().as_string()); - std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); - path = resolveSystemVariable(mSystemThemeFolder, path); - - if (!ResourceManager::getInstance()->fileExists(path)) - continue; - //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; - - error << " from included file \"" << relPath << "\":\n "; - - mPaths.push_back(path); - - pugi::xml_document includeDoc; - pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); - if(!result) - throw error << "Error parsing file: \n " << result.description(); - - pugi::xml_node theme = includeDoc.child("theme"); - if(!theme) - throw error << "Missing tag!"; - - parseVariables(theme); - parseIncludes(theme); - parseViews(theme); - parseFeatures(theme); - - mPaths.pop_back(); - } -} - -void ThemeData::parseFeatures(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) - { - if(!node.attribute("supported")) - throw error << "Feature missing \"supported\" attribute!"; - - const std::string supportedAttr = node.attribute("supported").as_string(); - - if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) - { - parseViews(node); - } - } -} - -void ThemeData::parseVariables(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - pugi::xml_node variables = root.child("variables"); - - if(!variables) - return; - - for(pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) - { - std::string key = it->name(); - std::string val = it->text().as_string(); - - if (!val.empty()) - mVariables.insert(std::pair(key, val)); - } -} - -void ThemeData::parseViews(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - // parse views - for(pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) - { - if(!node.attribute("name")) - throw error << "View missing \"name\" attribute!"; - - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - std::string viewKey; - while(off != std::string::npos || prevOff != std::string::npos) - { - viewKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - - if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) - { - ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; - parseView(node, view); - - if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) - { - ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; - parseView(node, view); - } - } - } - } -} - -void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) - { - if(!node.attribute("name")) - throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; - - auto elemTypeIt = sElementMap.find(node.name()); - if(elemTypeIt == sElementMap.cend()) - throw error << "Unknown element of type \"" << node.name() << "\"!"; - - if (parseRegion(node)) - { - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while (off != std::string::npos || prevOff != std::string::npos) - { - std::string elemKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - - parseElement(node, elemTypeIt->second, - view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); - - if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) - view.orderedKeys.push_back(elemKey); - } - } - } -} - -bool ThemeData::parseRegion(const pugi::xml_node& node) -{ - bool parse = true; - - if (node.attribute("region")) - { - std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); - - parse = false; - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("region").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while (off != std::string::npos || prevOff != std::string::npos) - { - std::string elemKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - if (elemKey == regionsetting) - { - parse = true; - return parse; - } - } - - } - return parse; - -} - - -void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element) -{ - ThemeException error; - error.setFiles(mPaths); - - element.type = root.name(); - element.extra = root.attribute("extra").as_bool(false); - - for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) - { - auto typeIt = typeMap.find(node.name()); - if (typeIt == typeMap.cend()) - continue; - //throw error << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ")."; - - std::string str = resolveSystemVariable(mSystemThemeFolder, resolvePlaceholders(node.text().as_string())); - - switch(typeIt->second) - { - case NORMALIZED_PAIR: - { - size_t divider = str.find(' '); - if(divider == std::string::npos) - throw error << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")"; - - std::string first = str.substr(0, divider); - std::string second = str.substr(divider, std::string::npos); - - Vector2f val((float)atof(first.c_str()), (float)atof(second.c_str())); - - element.properties[node.name()] = val; - break; - } - case STRING: - element.properties[node.name()] = str; - break; - case PATH: - { - std::string path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true); - - if (!ResourceManager::getInstance()->fileExists(path)) - { - std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); - if (ResourceManager::getInstance()->fileExists(rootPath)) - path = rootPath; - } - - if (!ResourceManager::getInstance()->fileExists(path)) - { - std::stringstream ss; - ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda - ss << "could not find file \"" << node.text().get() << "\" "; - if(node.text().get() != path) - ss << "(which resolved to \"" << path << "\") "; - - LOG(LogWarning) << ss.str(); - OutputDebugString(ss.str().c_str()); - } - else - element.properties[node.name()] = path; - - break; - } - case COLOR: - element.properties[node.name()] = getHexColor(str.c_str()); - break; - case FLOAT: - { - float floatVal = static_cast(strtod(str.c_str(), 0)); - element.properties[node.name()] = floatVal; - break; - } - - case BOOLEAN: - { - // only look at first char - char first = str[0]; - // 1*, t* (true), T* (True), y* (yes), Y* (YES) - bool boolVal = (first == '1' || first == 't' || first == 'T' || first == 'y' || first == 'Y'); - - element.properties[node.name()] = boolVal; - break; - } - default: - throw error << "Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name(); - } - } -} - -bool ThemeData::hasView(const std::string& view) -{ - auto viewIt = mViews.find(view); - return (viewIt != mViews.cend()); -} - -const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const -{ - auto viewIt = mViews.find(view); - if(viewIt == mViews.cend()) - return NULL; // not found - - auto elemIt = viewIt->second.elements.find(element); - if(elemIt == viewIt->second.elements.cend()) return NULL; - - if(elemIt->second.type != expectedType && !expectedType.empty()) - { - LOG(LogWarning) << " requested mismatched theme type for [" << view << "." << element << "] - expected \"" - << expectedType << "\", got \"" << elemIt->second.type << "\""; - return NULL; - } - - return &elemIt->second; -} - -const std::shared_ptr& ThemeData::getDefault() -{ - static std::shared_ptr theme = nullptr; - if(theme == nullptr) - { - theme = std::shared_ptr(new ThemeData()); - - const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_theme_default.xml"; - if(Utils::FileSystem::exists(path)) - { - try - { - std::map emptyMap; - theme->loadFile("", emptyMap, path); - } catch(ThemeException& e) - { - LOG(LogError) << e.what(); - theme = std::shared_ptr(new ThemeData()); //reset to empty - } - } - } - - return theme; -} - -std::vector ThemeData::makeExtras(const std::shared_ptr& theme, const std::string& view, Window* window) -{ - std::vector comps; - - auto viewIt = theme->mViews.find(view); - if(viewIt == theme->mViews.cend()) - return comps; - - for(auto it = viewIt->second.orderedKeys.cbegin(); it != viewIt->second.orderedKeys.cend(); it++) - { - ThemeElement& elem = viewIt->second.elements.at(*it); - if(elem.extra) - { - GuiComponent* comp = NULL; - const std::string& t = elem.type; - if(t == "image") - comp = new ImageComponent(window); - else if(t == "text") - comp = new TextComponent(window); - - comp->setDefaultZIndex(10); - comp->applyTheme(theme, view, *it, ThemeFlags::ALL); - comps.push_back(comp); - } - } - - return comps; -} - -std::map ThemeData::getThemeSets() -{ - std::map sets; - - static const size_t pathCount = 2; - std::string paths[pathCount] = - { - "/etc/emulationstation/themes", - Utils::FileSystem::getHomePath() + "/.emulationstation/themes" - }; - - for(size_t i = 0; i < pathCount; i++) - { - if(!Utils::FileSystem::isDirectory(paths[i])) - continue; - - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(paths[i]); - - for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if(Utils::FileSystem::isDirectory(*it)) - { - ThemeSet set = {*it}; - sets[set.getName()] = set; - } - } - } - - return sets; -} - -std::string ThemeData::getThemeFromCurrentSet(const std::string& system) -{ - std::map themeSets = ThemeData::getThemeSets(); - if(themeSets.empty()) - { - // no theme sets available - return ""; - } - - std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(set == themeSets.cend()) - { - // currently selected theme set is missing, so just pick the first available set - set = themeSets.cbegin(); - Settings::getInstance()->setString("ThemeSet", set->first); - } - - return set->second.getThemePath(system); -} +#include "ThemeData.h" + +#include "components/ImageComponent.h" +#include "components/TextComponent.h" +#include "utils/FileSystemUtil.h" +#include "Log.h" +#include "platform.h" +#include "Settings.h" +#include +#include + +std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" } }; +std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; + +std::map> ThemeData::sElementMap { + { "image", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "maxSize", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "path", PATH }, + { "default", PATH }, + { "tile", BOOLEAN }, + { "color", COLOR }, + { "zIndex", FLOAT } } }, + { "imagegrid", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "margin", NORMALIZED_PAIR }, + { "gameImage", PATH }, + { "folderImage", PATH }, + { "scrollDirection", STRING } } }, + { "gridtile", { + { "size", NORMALIZED_PAIR }, + { "padding", NORMALIZED_PAIR }, + { "imageColor", COLOR }, + { "backgroundImage", PATH }, + { "backgroundCornerSize", NORMALIZED_PAIR }, + { "backgroundColor", COLOR }, + { "backgroundCenterColor", COLOR }, + { "backgroundEdgeColor", COLOR }, + { "imageSizeMode", STRING } } }, + { "text", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "text", STRING }, + { "backgroundColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "color", COLOR }, + { "alignment", STRING }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "value", STRING }, + { "zIndex", FLOAT } } }, + { "textlist", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "selectorHeight", FLOAT }, + { "selectorOffsetY", FLOAT }, + { "selectorColor", COLOR }, + { "selectorImagePath", PATH }, + { "selectorImageTile", BOOLEAN }, + { "selectedColor", COLOR }, + { "primaryColor", COLOR }, + { "secondaryColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "scrollSound", PATH }, + { "alignment", STRING }, + { "horizontalMargin", FLOAT }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "zIndex", FLOAT } } }, + { "container", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "zIndex", FLOAT } } }, + { "ninepatch", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "path", PATH }, + { "zIndex", FLOAT } } }, + { "datetime", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "backgroundColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "color", COLOR }, + { "alignment", STRING }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "value", STRING }, + { "format", STRING }, + { "displayRelative", BOOLEAN }, + { "zIndex", FLOAT } } }, + { "rating", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "color", COLOR }, + { "filledPath", PATH }, + { "unfilledPath", PATH }, + { "zIndex", FLOAT } } }, + { "sound", { + { "path", PATH } } }, + { "helpsystem", { + { "pos", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "textColor", COLOR }, + { "iconColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT } } }, + { "video", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "maxSize", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "default", PATH }, + { "delay", FLOAT }, + { "zIndex", FLOAT }, + { "showSnapshotNoVideo", BOOLEAN }, + { "showSnapshotDelay", BOOLEAN } } }, + { "carousel", { + { "type", STRING }, + { "size", NORMALIZED_PAIR }, + { "pos", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "color", COLOR }, + { "logoScale", FLOAT }, + { "logoRotation", FLOAT }, + { "logoRotationOrigin", NORMALIZED_PAIR }, + { "logoSize", NORMALIZED_PAIR }, + { "logoAlignment", STRING }, + { "maxLogoCount", FLOAT }, + { "zIndex", FLOAT } } } +}; + +#define MINIMUM_THEME_FORMAT_VERSION 3 +#define CURRENT_THEME_FORMAT_VERSION 6 + +// helper +unsigned int getHexColor(const char* str) +{ + ThemeException error; + if(!str) + throw error << "Empty color"; + + size_t len = strlen(str); + if(len != 6 && len != 8) + throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; + + unsigned int val; + std::stringstream ss; + ss << str; + ss >> std::hex >> val; + + if(len == 6) + val = (val << 8) | 0xFF; + + return val; +} + +std::map mVariables; + +std::string resolvePlaceholders(const char* in) +{ + std::string inStr(in); + + if(inStr.empty()) + return inStr; + + const size_t variableBegin = inStr.find("${"); + const size_t variableEnd = inStr.find("}", variableBegin); + + if((variableBegin == std::string::npos) || (variableEnd == std::string::npos)) + return inStr; + + std::string prefix = inStr.substr(0, variableBegin); + std::string replace = inStr.substr(variableBegin + 2, variableEnd - (variableBegin + 2)); + std::string suffix = resolvePlaceholders(inStr.substr(variableEnd + 1).c_str()); + + return prefix + mVariables[replace] + suffix; +} + +ThemeData::ThemeData() +{ + mVersion = 0; +} + +void ThemeData::loadFile(std::string system, std::map sysDataMap, const std::string& path) +{ + mPaths.push_back(path); + + ThemeException error; + error.setFiles(mPaths); + + if(!Utils::FileSystem::exists(path)) + throw error << "File does not exist!"; + + mVersion = 0; + mViews.clear(); + + mSystemThemeFolder = system; + + mVariables.clear(); + mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); + + pugi::xml_document doc; + pugi::xml_parse_result res = doc.load_file(path.c_str()); + if(!res) + throw error << "XML parsing error: \n " << res.description(); + + pugi::xml_node root = doc.child("theme"); + if(!root) + throw error << "Missing tag!"; + + // parse version + mVersion = root.child("formatVersion").text().as_float(-404); + if(mVersion == -404) + throw error << " tag missing!\n It's either out of date or you need to add " << CURRENT_THEME_FORMAT_VERSION << " inside your tag."; + + if (root.attribute("defaultView")) + mDefaultView = root.attribute("defaultView").as_string(); + + if(mVersion < MINIMUM_THEME_FORMAT_VERSION) + throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; + + parseVariables(root); + parseIncludes(root); + parseViews(root); + parseFeatures(root); +} + +std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) +{ + std::string result = path; + + size_t start_pos = result.find("$system"); + if (start_pos == std::string::npos) + return path; + + result.replace(start_pos, 7, systemThemeFolder); + + //result.replace("$system", systemThemeFolder); + //boost::algorithm::replace_first(result, "$system", systemThemeFolder); + return result; +} + +bool ThemeData::parseSubset(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("subset")) + { + parse = false; + const std::string subsetAttr = node.attribute("subset").as_string(); + const std::string nameAttr = node.attribute("name").as_string(); + + if (subsetAttr == "iconset" || subsetAttr == "gamelistview") + { + if (nameAttr.rfind("1-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (subsetAttr == "systemview") + { + if (nameAttr.rfind("8-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (nameAttr.rfind("2-") != std::string::npos) + { + parse = true; + return parse; + } + /* + if (subsetAttr == "colorset" && nameAttr == mColorset) + { + parse = true; + return parse; + } + if (subsetAttr == "iconset" && nameAttr == mIconset) + { + parse = true; + return parse; + } + if (subsetAttr == "menu" && nameAttr == mMenu) + { + parse = true; + return parse; + } + if (subsetAttr == "systemview" && nameAttr == mSystemview) + { + parse = true; + return parse; + } + if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) + { + parse = true; + return parse; + }*/ + } + + return parse; +} + +void ThemeData::parseIncludes(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) + { + if (!parseSubset(node)) + continue; + + std::string relPath = resolvePlaceholders(node.text().as_string()); + std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); + path = resolveSystemVariable(mSystemThemeFolder, path); + + if (!ResourceManager::getInstance()->fileExists(path)) + continue; + //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + + error << " from included file \"" << relPath << "\":\n "; + + mPaths.push_back(path); + + pugi::xml_document includeDoc; + pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); + if(!result) + throw error << "Error parsing file: \n " << result.description(); + + pugi::xml_node theme = includeDoc.child("theme"); + if(!theme) + throw error << "Missing tag!"; + + parseVariables(theme); + parseIncludes(theme); + parseViews(theme); + parseFeatures(theme); + + mPaths.pop_back(); + } +} + +void ThemeData::parseFeatures(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + for(pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) + { + if(!node.attribute("supported")) + throw error << "Feature missing \"supported\" attribute!"; + + const std::string supportedAttr = node.attribute("supported").as_string(); + + if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) + { + parseViews(node); + } + } +} + +void ThemeData::parseVariables(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + pugi::xml_node variables = root.child("variables"); + + if(!variables) + return; + + for(pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) + { + std::string key = it->name(); + std::string val = it->text().as_string(); + + if (!val.empty()) + mVariables.insert(std::pair(key, val)); + } +} + +void ThemeData::parseViews(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + // parse views + for(pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) + { + if(!node.attribute("name")) + throw error << "View missing \"name\" attribute!"; + + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + std::string viewKey; + while(off != std::string::npos || prevOff != std::string::npos) + { + viewKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) + { + ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; + parseView(node, view); + + if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) + { + ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; + parseView(node, view); + } + } + } + } +} + +void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) +{ + ThemeException error; + error.setFiles(mPaths); + + for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + { + if(!node.attribute("name")) + throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; + + auto elemTypeIt = sElementMap.find(node.name()); + if(elemTypeIt == sElementMap.cend()) + throw error << "Unknown element of type \"" << node.name() << "\"!"; + + if (parseRegion(node)) + { + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + parseElement(node, elemTypeIt->second, + view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); + + if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) + view.orderedKeys.push_back(elemKey); + } + } + } +} + +bool ThemeData::parseRegion(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("region")) + { + std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); + + parse = false; + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("region").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + if (elemKey == regionsetting) + { + parse = true; + return parse; + } + } + + } + return parse; + +} + + +void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element) +{ + ThemeException error; + error.setFiles(mPaths); + + element.type = root.name(); + element.extra = root.attribute("extra").as_bool(false); + + for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + { + auto typeIt = typeMap.find(node.name()); + if (typeIt == typeMap.cend()) + continue; + //throw error << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ")."; + + std::string str = resolveSystemVariable(mSystemThemeFolder, resolvePlaceholders(node.text().as_string())); + + switch(typeIt->second) + { + case NORMALIZED_PAIR: + { + size_t divider = str.find(' '); + if(divider == std::string::npos) + throw error << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")"; + + std::string first = str.substr(0, divider); + std::string second = str.substr(divider, std::string::npos); + + Vector2f val((float)atof(first.c_str()), (float)atof(second.c_str())); + + element.properties[node.name()] = val; + break; + } + case STRING: + element.properties[node.name()] = str; + break; + case PATH: + { + std::string path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true); + + if (!ResourceManager::getInstance()->fileExists(path)) + { + std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); + if (ResourceManager::getInstance()->fileExists(rootPath)) + path = rootPath; + } + + if (!ResourceManager::getInstance()->fileExists(path)) + { + std::stringstream ss; + ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda + ss << "could not find file \"" << node.text().get() << "\" "; + if(node.text().get() != path) + ss << "(which resolved to \"" << path << "\") "; + + LOG(LogWarning) << ss.str(); + + auto test = element.properties[node.name()]; + if (test.s.empty()) + element.properties[node.name()] = path; + } + else + element.properties[node.name()] = path; + + break; + } + case COLOR: + element.properties[node.name()] = getHexColor(str.c_str()); + break; + case FLOAT: + { + float floatVal = static_cast(strtod(str.c_str(), 0)); + element.properties[node.name()] = floatVal; + break; + } + + case BOOLEAN: + { + // only look at first char + char first = str[0]; + // 1*, t* (true), T* (True), y* (yes), Y* (YES) + bool boolVal = (first == '1' || first == 't' || first == 'T' || first == 'y' || first == 'Y'); + + element.properties[node.name()] = boolVal; + break; + } + default: + throw error << "Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name(); + } + } +} + +bool ThemeData::hasView(const std::string& view) +{ + auto viewIt = mViews.find(view); + return (viewIt != mViews.cend()); +} + +const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const +{ + auto viewIt = mViews.find(view); + if(viewIt == mViews.cend()) + return NULL; // not found + + auto elemIt = viewIt->second.elements.find(element); + if(elemIt == viewIt->second.elements.cend()) return NULL; + + if(elemIt->second.type != expectedType && !expectedType.empty()) + { + LOG(LogWarning) << " requested mismatched theme type for [" << view << "." << element << "] - expected \"" + << expectedType << "\", got \"" << elemIt->second.type << "\""; + return NULL; + } + + return &elemIt->second; +} + +const std::shared_ptr& ThemeData::getDefault() +{ + static std::shared_ptr theme = nullptr; + if(theme == nullptr) + { + theme = std::shared_ptr(new ThemeData()); + + const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_theme_default.xml"; + if(Utils::FileSystem::exists(path)) + { + try + { + std::map emptyMap; + theme->loadFile("", emptyMap, path); + } catch(ThemeException& e) + { + LOG(LogError) << e.what(); + theme = std::shared_ptr(new ThemeData()); //reset to empty + } + } + } + + return theme; +} + +std::vector ThemeData::makeExtras(const std::shared_ptr& theme, const std::string& view, Window* window) +{ + std::vector comps; + + auto viewIt = theme->mViews.find(view); + if(viewIt == theme->mViews.cend()) + return comps; + + for(auto it = viewIt->second.orderedKeys.cbegin(); it != viewIt->second.orderedKeys.cend(); it++) + { + ThemeElement& elem = viewIt->second.elements.at(*it); + if(elem.extra) + { + GuiComponent* comp = NULL; + const std::string& t = elem.type; + if(t == "image") + comp = new ImageComponent(window); + else if(t == "text") + comp = new TextComponent(window); + + comp->setDefaultZIndex(10); + comp->applyTheme(theme, view, *it, ThemeFlags::ALL); + comps.push_back(comp); + } + } + + return comps; +} + +std::map ThemeData::getThemeSets() +{ + std::map sets; + + static const size_t pathCount = 2; + std::string paths[pathCount] = + { + "/etc/emulationstation/themes", + Utils::FileSystem::getHomePath() + "/.emulationstation/themes" + }; + + for(size_t i = 0; i < pathCount; i++) + { + if(!Utils::FileSystem::isDirectory(paths[i])) + continue; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(paths[i]); + + for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if(Utils::FileSystem::isDirectory(*it)) + { + ThemeSet set = {*it}; + sets[set.getName()] = set; + } + } + } + + return sets; +} + +std::string ThemeData::getThemeFromCurrentSet(const std::string& system) +{ + std::map themeSets = ThemeData::getThemeSets(); + if(themeSets.empty()) + { + // no theme sets available + return ""; + } + + std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if(set == themeSets.cend()) + { + // currently selected theme set is missing, so just pick the first available set + set = themeSets.cbegin(); + Settings::getInstance()->setString("ThemeSet", set->first); + } + + return set->second.getThemePath(system); +} diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 1b361f408e..7a3164f882 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -1,531 +1,531 @@ -#include "Window.h" - -#include "components/HelpComponent.h" -#include "components/ImageComponent.h" -#include "resources/Font.h" -#include "resources/TextureResource.h" -#include "InputManager.h" -#include "Log.h" -#include "Renderer.h" -#include "Scripting.h" -#include -#include - -#include - -Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), - mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) -{ - mHelp = new HelpComponent(this); - mBackgroundOverlay = new ImageComponent(this); - mSplash = NULL; - -} - -Window::~Window() -{ - delete mBackgroundOverlay; - - // delete all our GUIs - while(peekGui()) - delete peekGui(); - - delete mHelp; -} - -void Window::pushGui(GuiComponent* gui) -{ - if (mGuiStack.size() > 0) - { - auto& top = mGuiStack.back(); - top->topWindow(false); - } - mGuiStack.push_back(gui); - gui->updateHelpPrompts(); -} - -void Window::removeGui(GuiComponent* gui) -{ - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - { - if(*i == gui) - { - i = mGuiStack.erase(i); - - if(i == mGuiStack.cend() && mGuiStack.size()) // we just popped the stack and the stack is not empty - { - mGuiStack.back()->updateHelpPrompts(); - mGuiStack.back()->topWindow(true); - } - - return; - } - } -} - -GuiComponent* Window::peekGui() -{ - if(mGuiStack.size() == 0) - return NULL; - - return mGuiStack.back(); -} - -bool Window::init() -{ - if(!Renderer::init()) - { - LOG(LogError) << "Renderer failed to initialize!"; - return false; - } - - InputManager::getInstance()->init(); - ResourceManager::getInstance()->reloadAll(); - - //keep a reference to the default fonts, so they don't keep getting destroyed/recreated - if(mDefaultFonts.empty()) - { - mDefaultFonts.push_back(Font::get(FONT_SIZE_SMALL)); - mDefaultFonts.push_back(Font::get(FONT_SIZE_MEDIUM)); - mDefaultFonts.push_back(Font::get(FONT_SIZE_LARGE)); - } - - mBackgroundOverlay->setImage(":/scroll_gradient.png"); - mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - - // update our help because font sizes probably changed - if (peekGui()) - peekGui()->updateHelpPrompts(); - - return true; -} - -void Window::deinit() -{ - // Hide all GUI elements on uninitialisation - this disable - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - { - (*i)->onHide(); - } - InputManager::getInstance()->deinit(); - ResourceManager::getInstance()->unloadAll(); - Renderer::deinit(); -} - -void Window::textInput(const char* text) -{ - if(peekGui()) - peekGui()->textInput(text); -} - -void Window::input(InputConfig* config, Input input) -{ - if (mScreenSaver) { - if(mScreenSaver->isScreenSaverActive() && Settings::getInstance()->getBool("ScreenSaverControls") && - (Settings::getInstance()->getString("ScreenSaverBehavior") == "random video")) - { - if(mScreenSaver->getCurrentGame() != NULL && (config->isMappedLike("right", input) || config->isMappedTo("start", input) || config->isMappedTo("select", input))) - { - if(config->isMappedLike("right", input) || config->isMappedTo("select", input)) - { - if (input.value != 0) { - // handle screensaver control - mScreenSaver->nextVideo(); - } - return; - } - else if(config->isMappedTo("start", input) && input.value != 0) - { - // launch game! - cancelScreenSaver(); - mScreenSaver->launchGame(); - // to force handling the wake up process - mSleeping = true; - } - } - } - } - - if(mSleeping) - { - // wake up - mTimeSinceLastInput = 0; - cancelScreenSaver(); - mSleeping = false; - onWake(); - return; - } - - mTimeSinceLastInput = 0; - if (cancelScreenSaver()) - return; - - if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle debug grid with Ctrl-G - Settings::getInstance()->setBool("DebugGrid", !Settings::getInstance()->getBool("DebugGrid")); - } - else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle TextComponent debug view with Ctrl-T - Settings::getInstance()->setBool("DebugText", !Settings::getInstance()->getBool("DebugText")); - } - else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle TextComponent debug view with Ctrl-I - Settings::getInstance()->setBool("DebugImage", !Settings::getInstance()->getBool("DebugImage")); - } - else - { - if (peekGui()) - { - this->peekGui()->input(config, input); // this is where the majority of inputs will be consumed: the GuiComponent Stack - } - } -} - -void Window::update(int deltaTime) -{ - if(mNormalizeNextUpdate) - { - mNormalizeNextUpdate = false; - if(deltaTime > mAverageDeltaTime) - deltaTime = mAverageDeltaTime; - } - - mFrameTimeElapsed += deltaTime; - mFrameCountElapsed++; - if(mFrameTimeElapsed > 500) - { - mAverageDeltaTime = mFrameTimeElapsed / mFrameCountElapsed; - - if(Settings::getInstance()->getBool("DrawFramerate")) - { - std::stringstream ss; - - // fps - ss << std::fixed << std::setprecision(1) << (1000.0f * (float)mFrameCountElapsed / (float)mFrameTimeElapsed) << "fps, "; - ss << std::fixed << std::setprecision(2) << ((float)mFrameTimeElapsed / (float)mFrameCountElapsed) << "ms"; - - // vram - float textureVramUsageMb = TextureResource::getTotalMemUsage() / 1000.0f / 1000.0f; - float textureTotalUsageMb = TextureResource::getTotalTextureSize() / 1000.0f / 1000.0f; - float fontVramUsageMb = Font::getTotalMemUsage() / 1000.0f / 1000.0f; - - ss << "\nFont VRAM: " << fontVramUsageMb << " Tex VRAM: " << textureVramUsageMb << - " Tex Max: " << textureTotalUsageMb; - mFrameDataText = std::unique_ptr(mDefaultFonts.at(1)->buildTextCache(ss.str(), 50.f, 50.f, 0xFF00FFFF)); - } - - mFrameTimeElapsed = 0; - mFrameCountElapsed = 0; - } - - mTimeSinceLastInput += deltaTime; - - if(peekGui()) - peekGui()->update(deltaTime); - - // Update the screensaver - if (mScreenSaver) - mScreenSaver->update(deltaTime); -} - -void Window::render() -{ - Transform4x4f transform = Transform4x4f::Identity(); - - mRenderedHelpPrompts = false; - - // draw only bottom and top of GuiStack (if they are different) - if(mGuiStack.size()) - { - auto& bottom = mGuiStack.front(); - auto& top = mGuiStack.back(); - - bottom->render(transform); - if(bottom != top) - { - mBackgroundOverlay->render(transform); - top->render(transform); - } - } - - if(!mRenderedHelpPrompts) - mHelp->render(transform); - - if(Settings::getInstance()->getBool("DrawFramerate") && mFrameDataText) - { - Renderer::setMatrix(Transform4x4f::Identity()); - mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); - } - - unsigned int screensaverTime = (unsigned int)Settings::getInstance()->getInt("ScreenSaverTime"); - if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) - startScreenSaver(); - - // Always call the screensaver render function regardless of whether the screensaver is active - // or not because it may perform a fade on transition - renderScreenSaver(); - - if(!mRenderScreenSaver && mInfoPopup) - { - mInfoPopup->render(transform); - } - - if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) - { - if (!isProcessing() && mAllowSleep && (!mScreenSaver || mScreenSaver->allowSleep())) - { - // go to sleep - if (mSleeping == false) { - mSleeping = true; - onSleep(); - } - } - } -} - -void Window::normalizeNextUpdate() -{ - mNormalizeNextUpdate = true; -} - -bool Window::getAllowSleep() -{ - return mAllowSleep; -} - -void Window::setAllowSleep(bool sleep) -{ - mAllowSleep = sleep; -} - -void Window::endRenderLoadingScreen() -{ - mSplash = NULL; -} - -void Window::renderLoadingScreen(std::string text, float percent) -{ -#if defined(_WIN32) - // Avoid Window Freezing on Windows - SDL_Event event; - while (SDL_PollEvent(&event)) ; -#endif - - Transform4x4f trans = Transform4x4f::Identity(); - Renderer::setMatrix(trans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); - - if (percent > 0) - { - float baseHeight = 0.04f; - - float w = Renderer::getScreenWidth() / 2; - float h = Renderer::getScreenHeight() * baseHeight; - - float x = Renderer::getScreenWidth() / 2 - w / 2; - float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); - - Renderer::drawRect(x, y, w, h, 0x252525FF); - Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF - } - - - ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); - -#if defined(_WIN32) - if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); -#endif - - if (mSplash != NULL) - splash.setImage(mSplash); - else - splash.setImage(":/splash.svg"); - - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); - splash.render(trans); - - - auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); - - float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.8f); // 35 - trans = trans.translate(Vector3f(x, y, 0.0f)); - Renderer::setMatrix(trans); - font->renderTextCache(cache); - delete cache; - - - Renderer::swapBuffers(); -} - -void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) -{ - Transform4x4f trans = Transform4x4f::Identity(); - Renderer::setMatrix(trans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); - - ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); - -#if defined(_WIN32) - if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); -#endif - - if (mSplash != NULL) - splash.setImage(mSplash); - else - splash.setImage(":/splash.svg"); - - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); - splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); - splash.render(trans); - - auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); - - float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.835f); - trans = trans.translate(Vector3f(x, y, 0.0f)); - Renderer::setMatrix(trans); - font->renderTextCache(cache); - delete cache; - - if (swapBuffers) - Renderer::swapBuffers(); -} - - -void Window::renderHelpPromptsEarly() -{ - mHelp->render(Transform4x4f::Identity()); - mRenderedHelpPrompts = true; -} - -void Window::setHelpPrompts(const std::vector& prompts, const HelpStyle& style) -{ - mHelp->clearPrompts(); - mHelp->setStyle(style); - - std::vector addPrompts; - - std::map inputSeenMap; - std::map mappedToSeenMap; - for(auto it = prompts.cbegin(); it != prompts.cend(); it++) - { - // only add it if the same icon hasn't already been added - if(inputSeenMap.emplace(it->first, true).second) - { - // this symbol hasn't been seen yet, what about the action name? - auto mappedTo = mappedToSeenMap.find(it->second); - if(mappedTo != mappedToSeenMap.cend()) - { - // yes, it has! - - // can we combine? (dpad only) - if((it->first == "up/down" && addPrompts.at(mappedTo->second).first != "left/right") || - (it->first == "left/right" && addPrompts.at(mappedTo->second).first != "up/down")) - { - // yes! - addPrompts.at(mappedTo->second).first = "up/down/left/right"; - // don't need to add this to addPrompts since we just merged - }else{ - // no, we can't combine! - addPrompts.push_back(*it); - } - }else{ - // no, it hasn't! - mappedToSeenMap.emplace(it->second, (int)addPrompts.size()); - addPrompts.push_back(*it); - } - } - } - - // sort prompts so it goes [dpad_all] [dpad_u/d] [dpad_l/r] [a/b/x/y/l/r] [start/select] - std::sort(addPrompts.begin(), addPrompts.end(), [](const HelpPrompt& a, const HelpPrompt& b) -> bool { - - static const char* map[] = { - "up/down/left/right", - "up/down", - "left/right", - "a", "b", "x", "y", "l", "r", - "start", "select", - NULL - }; - - int i = 0; - int aVal = 0; - int bVal = 0; - while(map[i] != NULL) - { - if(a.first == map[i]) - aVal = i; - if(b.first == map[i]) - bVal = i; - i++; - } - - return aVal > bVal; - }); - - mHelp->setPrompts(addPrompts); -} - - -void Window::onSleep() -{ - Scripting::fireEvent("sleep"); -} - -void Window::onWake() -{ - Scripting::fireEvent("wake"); -} - -bool Window::isProcessing() -{ - return count_if(mGuiStack.cbegin(), mGuiStack.cend(), [](GuiComponent* c) { return c->isProcessing(); }) > 0; -} - -void Window::startScreenSaver() -{ - if (mScreenSaver && !mRenderScreenSaver) - { - // Tell the GUI components the screensaver is starting - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - (*i)->onScreenSaverActivate(); - - mScreenSaver->startScreenSaver(); - mRenderScreenSaver = true; - } -} - -bool Window::cancelScreenSaver() -{ - if (mScreenSaver && mRenderScreenSaver) - { - mScreenSaver->stopScreenSaver(); - mRenderScreenSaver = false; - mScreenSaver->resetCounts(); - - // Tell the GUI components the screensaver has stopped - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - (*i)->onScreenSaverDeactivate(); - - return true; - } - - return false; -} - -void Window::renderScreenSaver() -{ - if (mScreenSaver) - mScreenSaver->renderScreenSaver(); -} +#include "Window.h" + +#include "components/HelpComponent.h" +#include "components/ImageComponent.h" +#include "resources/Font.h" +#include "resources/TextureResource.h" +#include "InputManager.h" +#include "Log.h" +#include "Renderer.h" +#include "Scripting.h" +#include +#include + +#include + +Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), + mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) +{ + mHelp = new HelpComponent(this); + mBackgroundOverlay = new ImageComponent(this); + mSplash = NULL; + +} + +Window::~Window() +{ + delete mBackgroundOverlay; + + // delete all our GUIs + while(peekGui()) + delete peekGui(); + + delete mHelp; +} + +void Window::pushGui(GuiComponent* gui) +{ + if (mGuiStack.size() > 0) + { + auto& top = mGuiStack.back(); + top->topWindow(false); + } + mGuiStack.push_back(gui); + gui->updateHelpPrompts(); +} + +void Window::removeGui(GuiComponent* gui) +{ + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + { + if(*i == gui) + { + i = mGuiStack.erase(i); + + if(i == mGuiStack.cend() && mGuiStack.size()) // we just popped the stack and the stack is not empty + { + mGuiStack.back()->updateHelpPrompts(); + mGuiStack.back()->topWindow(true); + } + + return; + } + } +} + +GuiComponent* Window::peekGui() +{ + if(mGuiStack.size() == 0) + return NULL; + + return mGuiStack.back(); +} + +bool Window::init() +{ + if(!Renderer::init()) + { + LOG(LogError) << "Renderer failed to initialize!"; + return false; + } + + InputManager::getInstance()->init(); + ResourceManager::getInstance()->reloadAll(); + + //keep a reference to the default fonts, so they don't keep getting destroyed/recreated + if(mDefaultFonts.empty()) + { + mDefaultFonts.push_back(Font::get(FONT_SIZE_SMALL)); + mDefaultFonts.push_back(Font::get(FONT_SIZE_MEDIUM)); + mDefaultFonts.push_back(Font::get(FONT_SIZE_LARGE)); + } + + mBackgroundOverlay->setImage(":/scroll_gradient.png"); + mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + + // update our help because font sizes probably changed + if (peekGui()) + peekGui()->updateHelpPrompts(); + + return true; +} + +void Window::deinit() +{ + // Hide all GUI elements on uninitialisation - this disable + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + { + (*i)->onHide(); + } + InputManager::getInstance()->deinit(); + ResourceManager::getInstance()->unloadAll(); + Renderer::deinit(); +} + +void Window::textInput(const char* text) +{ + if(peekGui()) + peekGui()->textInput(text); +} + +void Window::input(InputConfig* config, Input input) +{ + if (mScreenSaver) { + if(mScreenSaver->isScreenSaverActive() && Settings::getInstance()->getBool("ScreenSaverControls") && + (Settings::getInstance()->getString("ScreenSaverBehavior") == "random video")) + { + if(mScreenSaver->getCurrentGame() != NULL && (config->isMappedLike("right", input) || config->isMappedTo("start", input) || config->isMappedTo("select", input))) + { + if(config->isMappedLike("right", input) || config->isMappedTo("select", input)) + { + if (input.value != 0) { + // handle screensaver control + mScreenSaver->nextVideo(); + } + return; + } + else if(config->isMappedTo("start", input) && input.value != 0) + { + // launch game! + cancelScreenSaver(); + mScreenSaver->launchGame(); + // to force handling the wake up process + mSleeping = true; + } + } + } + } + + if(mSleeping) + { + // wake up + mTimeSinceLastInput = 0; + cancelScreenSaver(); + mSleeping = false; + onWake(); + return; + } + + mTimeSinceLastInput = 0; + if (cancelScreenSaver()) + return; + + if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle debug grid with Ctrl-G + Settings::getInstance()->setBool("DebugGrid", !Settings::getInstance()->getBool("DebugGrid")); + } + else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle TextComponent debug view with Ctrl-T + Settings::getInstance()->setBool("DebugText", !Settings::getInstance()->getBool("DebugText")); + } + else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle TextComponent debug view with Ctrl-I + Settings::getInstance()->setBool("DebugImage", !Settings::getInstance()->getBool("DebugImage")); + } + else + { + if (peekGui()) + { + this->peekGui()->input(config, input); // this is where the majority of inputs will be consumed: the GuiComponent Stack + } + } +} + +void Window::update(int deltaTime) +{ + if(mNormalizeNextUpdate) + { + mNormalizeNextUpdate = false; + if(deltaTime > mAverageDeltaTime) + deltaTime = mAverageDeltaTime; + } + + mFrameTimeElapsed += deltaTime; + mFrameCountElapsed++; + if(mFrameTimeElapsed > 500) + { + mAverageDeltaTime = mFrameTimeElapsed / mFrameCountElapsed; + + if(Settings::getInstance()->getBool("DrawFramerate")) + { + std::stringstream ss; + + // fps + ss << std::fixed << std::setprecision(1) << (1000.0f * (float)mFrameCountElapsed / (float)mFrameTimeElapsed) << "fps, "; + ss << std::fixed << std::setprecision(2) << ((float)mFrameTimeElapsed / (float)mFrameCountElapsed) << "ms"; + + // vram + float textureVramUsageMb = TextureResource::getTotalMemUsage() / 1000.0f / 1000.0f; + float textureTotalUsageMb = TextureResource::getTotalTextureSize() / 1000.0f / 1000.0f; + float fontVramUsageMb = Font::getTotalMemUsage() / 1000.0f / 1000.0f; + + ss << "\nFont VRAM: " << fontVramUsageMb << " Tex VRAM: " << textureVramUsageMb << + " Tex Max: " << textureTotalUsageMb; + mFrameDataText = std::unique_ptr(mDefaultFonts.at(1)->buildTextCache(ss.str(), 50.f, 50.f, 0xFF00FFFF)); + } + + mFrameTimeElapsed = 0; + mFrameCountElapsed = 0; + } + + mTimeSinceLastInput += deltaTime; + + if(peekGui()) + peekGui()->update(deltaTime); + + // Update the screensaver + if (mScreenSaver) + mScreenSaver->update(deltaTime); +} + +void Window::render() +{ + Transform4x4f transform = Transform4x4f::Identity(); + + mRenderedHelpPrompts = false; + + // draw only bottom and top of GuiStack (if they are different) + if(mGuiStack.size()) + { + auto& bottom = mGuiStack.front(); + auto& top = mGuiStack.back(); + + bottom->render(transform); + if(bottom != top) + { + mBackgroundOverlay->render(transform); + top->render(transform); + } + } + + if(!mRenderedHelpPrompts) + mHelp->render(transform); + + if(Settings::getInstance()->getBool("DrawFramerate") && mFrameDataText) + { + Renderer::setMatrix(Transform4x4f::Identity()); + mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); + } + + unsigned int screensaverTime = (unsigned int)Settings::getInstance()->getInt("ScreenSaverTime"); + if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) + startScreenSaver(); + + // Always call the screensaver render function regardless of whether the screensaver is active + // or not because it may perform a fade on transition + renderScreenSaver(); + + if(!mRenderScreenSaver && mInfoPopup) + { + mInfoPopup->render(transform); + } + + if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) + { + if (!isProcessing() && mAllowSleep && (!mScreenSaver || mScreenSaver->allowSleep())) + { + // go to sleep + if (mSleeping == false) { + mSleeping = true; + onSleep(); + } + } + } +} + +void Window::normalizeNextUpdate() +{ + mNormalizeNextUpdate = true; +} + +bool Window::getAllowSleep() +{ + return mAllowSleep; +} + +void Window::setAllowSleep(bool sleep) +{ + mAllowSleep = sleep; +} + +void Window::endRenderLoadingScreen() +{ + mSplash = NULL; +} + +void Window::renderLoadingScreen(std::string text, float percent) +{ +#if defined(_WIN32) + // Avoid Window Freezing on Windows + SDL_Event event; + while (SDL_PollEvent(&event)) ; +#endif + + Transform4x4f trans = Transform4x4f::Identity(); + Renderer::setMatrix(trans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); + + if (percent >= 0) + { + float baseHeight = 0.04f; + + float w = Renderer::getScreenWidth() / 2; + float h = Renderer::getScreenHeight() * baseHeight; + + float x = Renderer::getScreenWidth() / 2 - w / 2; + float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); + + Renderer::drawRect(x, y, w, h, 0x252525FF); + Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF + } + + + ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.render(trans); + + + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); + + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.78f); // 35 + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; + + + Renderer::swapBuffers(); +} + +void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) +{ + Transform4x4f trans = Transform4x4f::Identity(); + Renderer::setMatrix(trans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); + + ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); + splash.render(trans); + + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); + + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.835f); + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; + + if (swapBuffers) + Renderer::swapBuffers(); +} + + +void Window::renderHelpPromptsEarly() +{ + mHelp->render(Transform4x4f::Identity()); + mRenderedHelpPrompts = true; +} + +void Window::setHelpPrompts(const std::vector& prompts, const HelpStyle& style) +{ + mHelp->clearPrompts(); + mHelp->setStyle(style); + + std::vector addPrompts; + + std::map inputSeenMap; + std::map mappedToSeenMap; + for(auto it = prompts.cbegin(); it != prompts.cend(); it++) + { + // only add it if the same icon hasn't already been added + if(inputSeenMap.emplace(it->first, true).second) + { + // this symbol hasn't been seen yet, what about the action name? + auto mappedTo = mappedToSeenMap.find(it->second); + if(mappedTo != mappedToSeenMap.cend()) + { + // yes, it has! + + // can we combine? (dpad only) + if((it->first == "up/down" && addPrompts.at(mappedTo->second).first != "left/right") || + (it->first == "left/right" && addPrompts.at(mappedTo->second).first != "up/down")) + { + // yes! + addPrompts.at(mappedTo->second).first = "up/down/left/right"; + // don't need to add this to addPrompts since we just merged + }else{ + // no, we can't combine! + addPrompts.push_back(*it); + } + }else{ + // no, it hasn't! + mappedToSeenMap.emplace(it->second, (int)addPrompts.size()); + addPrompts.push_back(*it); + } + } + } + + // sort prompts so it goes [dpad_all] [dpad_u/d] [dpad_l/r] [a/b/x/y/l/r] [start/select] + std::sort(addPrompts.begin(), addPrompts.end(), [](const HelpPrompt& a, const HelpPrompt& b) -> bool { + + static const char* map[] = { + "up/down/left/right", + "up/down", + "left/right", + "a", "b", "x", "y", "l", "r", + "start", "select", + NULL + }; + + int i = 0; + int aVal = 0; + int bVal = 0; + while(map[i] != NULL) + { + if(a.first == map[i]) + aVal = i; + if(b.first == map[i]) + bVal = i; + i++; + } + + return aVal > bVal; + }); + + mHelp->setPrompts(addPrompts); +} + + +void Window::onSleep() +{ + Scripting::fireEvent("sleep"); +} + +void Window::onWake() +{ + Scripting::fireEvent("wake"); +} + +bool Window::isProcessing() +{ + return count_if(mGuiStack.cbegin(), mGuiStack.cend(), [](GuiComponent* c) { return c->isProcessing(); }) > 0; +} + +void Window::startScreenSaver() +{ + if (mScreenSaver && !mRenderScreenSaver) + { + // Tell the GUI components the screensaver is starting + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + (*i)->onScreenSaverActivate(); + + mScreenSaver->startScreenSaver(); + mRenderScreenSaver = true; + } +} + +bool Window::cancelScreenSaver() +{ + if (mScreenSaver && mRenderScreenSaver) + { + mScreenSaver->stopScreenSaver(); + mRenderScreenSaver = false; + mScreenSaver->resetCounts(); + + // Tell the GUI components the screensaver has stopped + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + (*i)->onScreenSaverDeactivate(); + + return true; + } + + return false; +} + +void Window::renderScreenSaver() +{ + if (mScreenSaver) + mScreenSaver->renderScreenSaver(); +} diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index 613a22c026..f2d36950d0 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -282,17 +282,17 @@ class OptionListComponent : public GuiComponent onSelectedChanged(); } - void clear() { - mEntries.clear(); + void clear() { + mEntries.clear(); } - inline void invalidate() { - onSelectedChanged(); + inline void invalidate() { + onSelectedChanged(); } - void setSelectedChangedCallback(const std::function& callback) - { - mSelectedChangedCallback = callback; + void setSelectedChangedCallback(const std::function& callback) + { + mSelectedChangedCallback = callback; } @@ -344,7 +344,7 @@ class OptionListComponent : public GuiComponent } } - if (mSelectedChangedCallback) + if (mSelectedChangedCallback) mSelectedChangedCallback(mEntries.at(getSelectedId()).object); } diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index e80e589518..bf07b39759 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -1,165 +1,165 @@ -#include "platform.h" -#include - -#ifdef WIN32 -#include -#include -#else -#include -#endif -#include - -#include "Window.h" - -#include "GuiComponent.h" - -int runShutdownCommand() -{ -#ifdef WIN32 // windows - return system("shutdown -s -t 0"); -#else // osx / linux - return system("sudo shutdown -h now"); -#endif -} - -int runRestartCommand() -{ -#ifdef WIN32 // windows - return system("shutdown -r -t 0"); -#else // osx / linux - return system("sudo shutdown -r now"); -#endif -} - -std::string trim(const std::string& str) -{ - size_t first = str.find_first_not_of(' '); - if (std::string::npos == first) - { - return str; - } - size_t last = str.find_last_not_of(' '); - return str.substr(first, (last - first + 1)); -} - -void split_cmd(const std::string& cmd, - std::string* executable, - std::string* parameters) -{ - std::string c(cmd); - size_t exec_end; - - c = trim(c); - //boost::trim_all(c); - - if (c[0] == '\"') - { - exec_end = c.find_first_of('\"', 1); - if (std::string::npos != exec_end) - { - *executable = c.substr(1, exec_end - 1); - *parameters = c.substr(exec_end + 1); - } - else - { - *executable = c.substr(1, exec_end); - std::string().swap(*parameters); - } - } - else - { - exec_end = c.find_first_of(' ', 0); - if (std::string::npos != exec_end) - { - *executable = c.substr(0, exec_end); - *parameters = c.substr(exec_end + 1); - } - else - { - *executable = c.substr(0, exec_end); - std::string().swap(*parameters); - } - } -} - -int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window) -{ -#ifdef WIN32 - if (window != NULL) - window->renderGameLoadingScreen(); - - // on Windows we use _wsystem to support non-ASCII paths - // which requires converting from utf8 to a wstring - typedef std::codecvt_utf8 convert_type; - std::wstring_convert converter; - std::wstring wchar_str = converter.from_bytes(cmd_utf8); - - std::string exe; - std::string args; - - split_cmd(cmd_utf8, &exe, &args); - - SHELLEXECUTEINFO lpExecInfo; - lpExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); - lpExecInfo.lpFile = exe.c_str(); - lpExecInfo.fMask = SEE_MASK_DOENVSUBST | SEE_MASK_NOCLOSEPROCESS; - lpExecInfo.hwnd = NULL; - lpExecInfo.lpVerb = "open"; // to open program - lpExecInfo.lpParameters = args.c_str(); // file name as an argument - lpExecInfo.lpDirectory = NULL; - lpExecInfo.nShow = SW_SHOW; // show command prompt with normal window size - lpExecInfo.hInstApp = (HINSTANCE)SE_ERR_DDEFAIL; //WINSHELLAPI BOOL WINAPI result; - ShellExecuteEx(&lpExecInfo); - - if (lpExecInfo.hProcess != NULL) - { - if (window == NULL) - WaitForSingleObject(lpExecInfo.hProcess, INFINITE); - else - { - while (WaitForSingleObject(lpExecInfo.hProcess, 50) == 0x00000102L) - { - bool polled = false; - - SDL_Event event; - while (SDL_PollEvent(&event)) - polled = true; - - if (window != NULL && polled) - window->renderGameLoadingScreen(); - } - } - - CloseHandle(lpExecInfo.hProcess); - return 0; - } - - return 1; - //return _wsystem(wchar_str.c_str()); -#else - return system(cmd_utf8.c_str()); -#endif -} - -int quitES(const std::string& filename) -{ - if (!filename.empty()) - touch(filename); - SDL_Event* quit = new SDL_Event(); - quit->type = SDL_QUIT; - SDL_PushEvent(quit); - return 0; -} - -void touch(const std::string& filename) -{ -#ifdef WIN32 - FILE* fp = fopen(filename.c_str(), "ab+"); - if (fp != NULL) - fclose(fp); -#else - int fd = open(filename.c_str(), O_CREAT|O_WRONLY, 0644); - if (fd >= 0) - close(fd); -#endif +#include "platform.h" +#include + +#ifdef WIN32 +#include +#include +#else +#include +#endif +#include + +#include "Window.h" + +#include "GuiComponent.h" + +int runShutdownCommand() +{ +#ifdef WIN32 // windows + return system("shutdown -s -t 0"); +#else // osx / linux + return system("sudo shutdown -h now"); +#endif +} + +int runRestartCommand() +{ +#ifdef WIN32 // windows + return system("shutdown -r -t 0"); +#else // osx / linux + return system("sudo shutdown -r now"); +#endif +} + +std::string trim(const std::string& str) +{ + size_t first = str.find_first_not_of(' '); + if (std::string::npos == first) + { + return str; + } + size_t last = str.find_last_not_of(' '); + return str.substr(first, (last - first + 1)); +} + +void split_cmd(const std::string& cmd, + std::string* executable, + std::string* parameters) +{ + std::string c(cmd); + size_t exec_end; + + c = trim(c); + //boost::trim_all(c); + + if (c[0] == '\"') + { + exec_end = c.find_first_of('\"', 1); + if (std::string::npos != exec_end) + { + *executable = c.substr(1, exec_end - 1); + *parameters = c.substr(exec_end + 1); + } + else + { + *executable = c.substr(1, exec_end); + std::string().swap(*parameters); + } + } + else + { + exec_end = c.find_first_of(' ', 0); + if (std::string::npos != exec_end) + { + *executable = c.substr(0, exec_end); + *parameters = c.substr(exec_end + 1); + } + else + { + *executable = c.substr(0, exec_end); + std::string().swap(*parameters); + } + } +} + +int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window) +{ +#ifdef WIN32 + if (window != NULL) + window->renderGameLoadingScreen(); + + // on Windows we use _wsystem to support non-ASCII paths + // which requires converting from utf8 to a wstring + typedef std::codecvt_utf8 convert_type; + std::wstring_convert converter; + std::wstring wchar_str = converter.from_bytes(cmd_utf8); + + std::string exe; + std::string args; + + split_cmd(cmd_utf8, &exe, &args); + + SHELLEXECUTEINFO lpExecInfo; + lpExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); + lpExecInfo.lpFile = exe.c_str(); + lpExecInfo.fMask = SEE_MASK_DOENVSUBST | SEE_MASK_NOCLOSEPROCESS; + lpExecInfo.hwnd = NULL; + lpExecInfo.lpVerb = "open"; // to open program + lpExecInfo.lpParameters = args.c_str(); // file name as an argument + lpExecInfo.lpDirectory = NULL; + lpExecInfo.nShow = SW_SHOW; // show command prompt with normal window size + lpExecInfo.hInstApp = (HINSTANCE)SE_ERR_DDEFAIL; //WINSHELLAPI BOOL WINAPI result; + ShellExecuteEx(&lpExecInfo); + + if (lpExecInfo.hProcess != NULL) + { + if (window == NULL) + WaitForSingleObject(lpExecInfo.hProcess, INFINITE); + else + { + while (WaitForSingleObject(lpExecInfo.hProcess, 50) == 0x00000102L) + { + bool polled = false; + + SDL_Event event; + while (SDL_PollEvent(&event)) + polled = true; + + if (window != NULL && polled) + window->renderGameLoadingScreen(); + } + } + + CloseHandle(lpExecInfo.hProcess); + return 0; + } + + return 1; + //return _wsystem(wchar_str.c_str()); +#else + return system(cmd_utf8.c_str()); +#endif +} + +int quitES(const std::string& filename) +{ + if (!filename.empty()) + touch(filename); + SDL_Event* quit = new SDL_Event(); + quit->type = SDL_QUIT; + SDL_PushEvent(quit); + return 0; +} + +void touch(const std::string& filename) +{ +#ifdef WIN32 + FILE* fp = fopen(filename.c_str(), "ab+"); + if (fp != NULL) + fclose(fp); +#else + int fd = open(filename.c_str(), O_CREAT|O_WRONLY, 0644); + if (fd >= 0) + close(fd); +#endif } \ No newline at end of file diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 3c77caed8b..83b5270cb1 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -1,667 +1,667 @@ -#define _FILE_OFFSET_BITS 64 - -#include "utils/FileSystemUtil.h" - -#include "Settings.h" -#include -#include - -#if defined(_WIN32) -// because windows... -#include -#include -#define getcwd _getcwd -#define mkdir(x,y) _mkdir(x) -#define snprintf _snprintf -#define stat64 _stat64 -#define unlink _unlink -#define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) -#define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) -#else // _WIN32 -#include -#include -#endif // _WIN32 - -namespace Utils -{ - namespace FileSystem - { - -#if defined(_WIN32) - static std::string convertFromWideString(const std::wstring wstring) - { - int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); - std::string string; - - string.resize(numBytes); - WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); - - return std::string(string); - - } // convertFromWideString -#endif // _WIN32 - - stringList getDirContent(const std::string& _path, const bool _recursive) - { - std::string path = getGenericPath(_path); - stringList contentList; - - // only parse the directory, if it's a directory - if(isDirectory(path)) - { - -#if defined(_WIN32) - WIN32_FIND_DATAW findData; - std::string wildcard = path + "/*"; - HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); - - if(hFind != INVALID_HANDLE_VALUE) - { - // loop over all files in the directory - do - { - std::string name = convertFromWideString(findData.cFileName); - - // ignore "." and ".." - if((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - contentList.push_back(fullName); - - if(_recursive && isDirectory(fullName)) - contentList.merge(getDirContent(fullName, true)); - } - } - while(FindNextFileW(hFind, &findData)); - - FindClose(hFind); - } -#else // _WIN32 - DIR* dir = opendir(path.c_str()); - - if(dir != NULL) - { - struct dirent* entry; - - // loop over all files in the directory - while((entry = readdir(dir)) != NULL) - { - std::string name(entry->d_name); - - // ignore "." and ".." - if((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - contentList.push_back(fullName); - - if(_recursive && isDirectory(fullName)) - contentList.merge(getDirContent(fullName, true)); - } - } - - closedir(dir); - } -#endif // _WIN32 - - } - - // sort the content list - contentList.sort(); - - // return the content list - return contentList; - - } // getDirContent - - stringList getPathList(const std::string& _path) - { - stringList pathList; - std::string path = getGenericPath(_path); - size_t start = 0; - size_t end = 0; - - // split at '/' - while((end = path.find("/", start)) != std::string::npos) - { - if(end != start) - pathList.push_back(std::string(path, start, end - start)); - - start = end + 1; - } - - // add last folder / file to pathList - if(start != path.size()) - pathList.push_back(std::string(path, start, path.size() - start)); - - // return the path list - return pathList; - - } // getPathList - - std::string getHomePath() - { - static std::string path; - - // only construct the homepath once - if(!path.length()) - { - // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows - char* envHome = getenv("HOME"); - -#ifdef _DEBUG - // envHome = "H:/[Emulz]/EmulationStation/"; -#endif - - if (envHome) - path = getGenericPath(envHome); - -#if defined(_WIN32) - // but does not seem to work for Windows XP or Vista, so try something else - if(!path.length()) - { - char* envHomeDrive = getenv("HOMEDRIVE"); - char* envHomePath = getenv("HOMEPATH"); - if(envHomeDrive && envHomePath) - path = getGenericPath(std::string(envHomeDrive) + "/" + envHomePath); - } -#endif // _WIN32 - - // no homepath found, fall back to current working directory - if(!path.length()) - path = getCWDPath(); - } - - // return constructed homepath - return path; - - } // getHomePath - - std::string getCWDPath() - { - char temp[512]; - return (getcwd(temp, 512) ? getGenericPath(temp) : ""); - } // getCWDPath - - std::string getExePath() - { - static std::string path; - - // only construct the exepath once - if (!path.length()) - { -#if defined(_WIN32) - char buffer[MAX_PATH]; - DWORD size = MAX_PATH; - DWORD result = GetModuleFileNameA(NULL, buffer, size); - if (result) - { - std::string ret = buffer; - path = getParent(ret); - return ret; - } -#endif - path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); - if (isRegularFile(path)) - path = getParent(path); - } - - // return constructed exepath - return path; - - } // getExePath - - std::string getPreferredPath(const std::string& _path) - { - std::string path = _path; - size_t offset = std::string::npos; -#if defined(_WIN32) - // convert '/' to '\\' - while((offset = path.find('/')) != std::string::npos) - path.replace(offset, 1, "\\"); -#endif // _WIN32 - return path; - } - - std::string getGenericPath(const std::string& _path) - { - std::string path = _path; - size_t offset = std::string::npos; - - // remove "\\\\?\\" - if((path.find("\\\\?\\")) == 0) - path.erase(0, 4); - - // convert '\\' to '/' - while((offset = path.find('\\')) != std::string::npos) - path.replace(offset, 1 ,"/"); - - // remove double '/' - while((offset = path.find("//")) != std::string::npos) - path.erase(offset, 1); - - // remove trailing '/' - while(path.length() && ((offset = path.find_last_of('/')) == (path.length() - 1))) - path.erase(offset, 1); - - // return generic path - return path; - - } // getGenericPath - - std::string getEscapedPath(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // windows escapes stuff by just putting everything in quotes - return '"' + getPreferredPath(path) + '"'; -#else // _WIN32 - // insert a backslash before most characters that would mess up a bash path - const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; - const char* invalidChar = invalidChars; - - while(*invalidChar) - { - size_t start = 0; - size_t offset = 0; - - while((offset = path.find(*invalidChar, start)) != std::string::npos) - { - start = offset + 1; - - if((offset == 0) || (path[offset - 1] != '\\')) - { - path.insert(offset, 1, '\\'); - ++start; - } - } - - ++invalidChar; - } - - // return escaped path - return path; -#endif // _WIN32 - - } // getEscapedPath - - std::string getCanonicalPath(const std::string& _path) - { - // temporary hack for builtin resources - if((_path[0] == ':') && (_path[1] == '/')) - return _path; - - std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); - - // cleanup path - bool scan = true; - while(scan) - { - stringList pathList = getPathList(path); - - path.clear(); - scan = false; - - for(stringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); ++it) - { - // ignore empty - if((*it).empty()) - continue; - - // remove "/./" - if((*it) == ".") - continue; - - // resolve "/../" - if((*it) == "..") - { - path = getParent(path); - continue; - } - -#if defined(_WIN32) - // append folder to path - path += (path.size() == 0) ? (*it) : ("/" + (*it)); -#else // _WIN32 - // append folder to path - path += ("/" + (*it)); -#endif // _WIN32 - - // resolve symlink - if(isSymlink(path)) - { - std::string resolved = resolveSymlink(path); - - if(resolved.empty()) - return ""; - - if(isAbsolute(resolved)) - path = resolved; - else - path = getParent(path) + "/" + resolved; - - for(++it; it != pathList.cend(); ++it) - path += (path.size() == 0) ? (*it) : ("/" + (*it)); - - scan = true; - break; - } - } - } - - // return canonical path - return path; - - } // getCanonicalPath - - std::string getAbsolutePath(const std::string& _path, const std::string& _base) - { - std::string path = getGenericPath(_path); - std::string base = isAbsolute(_base) ? getGenericPath(_base) : getAbsolutePath(_base); - - // return absolute path - return isAbsolute(path) ? path : getGenericPath(base + "/" + path); - - } // getAbsolutePath - - std::string getParent(const std::string& _path) - { - std::string path = getGenericPath(_path); - size_t offset = std::string::npos; - - // find last '/' and erase it - if((offset = path.find_last_of('/')) != std::string::npos) - return path.erase(offset); - - // no parent found - return path; - - } // getParent - - std::string getFileName(const std::string& _path) - { - std::string path = getGenericPath(_path); - size_t offset = std::string::npos; - - // find last '/' and return the filename - if((offset = path.find_last_of('/')) != std::string::npos) - return ((path[offset + 1] == 0) ? "." : std::string(path, offset + 1)); - - // no '/' found, entire path is a filename - return path; - - } // getFileName - - std::string getStem(const std::string& _path) - { - std::string fileName = getFileName(_path); - size_t offset = std::string::npos; - - // empty fileName - if(fileName == ".") - return fileName; - - // find last '.' and erase the extension - if((offset = fileName.find_last_of('.')) != std::string::npos) - return fileName.erase(offset); - - // no '.' found, filename has no extension - return fileName; - - } // getStem - - std::string getExtension(const std::string& _path) - { - std::string fileName = getFileName(_path); - size_t offset = std::string::npos; - - // empty fileName - if(fileName == ".") - return fileName; - - // find last '.' and return the extension - if((offset = fileName.find_last_of('.')) != std::string::npos) - return std::string(fileName, offset); - - // no '.' found, filename has no extension - return "."; - - } // getExtension - - std::string resolveRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) - { - std::string path = getGenericPath(_path); - std::string relativeTo = isDirectory(_relativeTo) ? getGenericPath(_relativeTo) : getParent(_relativeTo); - - // nothing to resolve - if(!path.length()) - return path; - - // replace '.' with relativeTo - if((path[0] == '.') && (path[1] == '/')) - return (relativeTo + &(path[1])); - - // replace '~' with homePath - if(_allowHome && (path[0] == '~') && (path[1] == '/')) - return (getHomePath() + &(path[1])); - - // nothing to resolve - return path; - - } // resolveRelativePath - - std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) - { - bool contains = false; - std::string path = removeCommonPath(_path, _relativeTo, contains); - - if(contains) - { - // success - return ("./" + path); - } - - if(_allowHome) - { - path = removeCommonPath(_path, getHomePath(), contains); - - if(contains) - { - // success - return ("~/" + path); - } - } - - // nothing to resolve - return path; - - } // createRelativePath - - std::string removeCommonPath(const std::string& _path, const std::string& _common, bool& _contains) - { - std::string path = getGenericPath(_path); - std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); - - // check if path contains common - if(path.find(common) == 0) - { - _contains = true; - return path.substr(common.length() + 1); - } - - // it didn't - _contains = false; - return path; - - } // removeCommonPath - - std::string resolveSymlink(const std::string& _path) - { - std::string path = getGenericPath(_path); - std::string resolved; - -#if defined(_WIN32) - HANDLE hFile = CreateFile(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); - - if(hFile != INVALID_HANDLE_VALUE) - { - resolved.resize(GetFinalPathNameByHandle(hFile, nullptr, 0, FILE_NAME_NORMALIZED) + 1); - if(GetFinalPathNameByHandle(hFile, (LPSTR)resolved.data(), (DWORD)resolved.size(), FILE_NAME_NORMALIZED) > 0) - { - resolved.resize(resolved.size() - 1); - resolved = getGenericPath(resolved); - } - CloseHandle(hFile); - } -#else // _WIN32 - struct stat info; - - // check if lstat succeeded - if(lstat(path.c_str(), &info) == 0) - { - resolved.resize(info.st_size); - if(readlink(path.c_str(), (char*)resolved.data(), resolved.size()) > 0) - resolved = getGenericPath(resolved); - } -#endif // _WIN32 - - // return resolved path - return resolved; - - } // resolveSymlink - - bool removeFile(const std::string& _path) - { - std::string path = getGenericPath(_path); - - // don't remove if it doesn't exists - if(!exists(path)) - return true; - - // try to remove file - return (unlink(path.c_str()) == 0); - - } // removeFile - - bool createDirectory(const std::string& _path) - { - std::string path = getGenericPath(_path); - - // don't create if it already exists - if(exists(path)) - return true; - - // try to create directory - if(mkdir(path.c_str(), 0755) == 0) - return true; - - // failed to create directory, try to create the parent - std::string parent = getParent(path); - - // only try to create parent if it's not identical to path - if(parent != path) - createDirectory(parent); - - // try to create directory again now that the parent should exist - return (mkdir(path.c_str(), 0755) == 0); - - } // createDirectory - - bool exists(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - return (stat64(path.c_str(), &info) == 0); - - } // exists - - bool isAbsolute(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - return ((path.size() > 1) && (path[1] == ':')); -#else // _WIN32 - return ((path.size() > 0) && (path[0] == '/')); -#endif // _WIN32 - - } // isAbsolute - - bool isRegularFile(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - if(stat64(path.c_str(), &info) != 0) - return false; - - // check for S_IFREG attribute - return (S_ISREG(info.st_mode)); - - } // isRegularFile - - bool isDirectory(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat info; - - // check if stat succeeded - if(stat(path.c_str(), &info) != 0) - return false; - - // check for S_IFDIR attribute - return (S_ISDIR(info.st_mode)); - - } // isDirectory - - bool isSymlink(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // check for symlink attribute - const DWORD Attributes = GetFileAttributes(path.c_str()); - if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT)) - return true; -#else // _WIN32 - struct stat info; - - // check if lstat succeeded - if(lstat(path.c_str(), &info) != 0) - return false; - - // check for S_IFLNK attribute - return (S_ISLNK(info.st_mode)); -#endif // _WIN32 - - // not a symlink - return false; - - } // isSymlink - - bool isHidden(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // check for hidden attribute - const DWORD Attributes = GetFileAttributes(path.c_str()); - if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_HIDDEN)) - return true; -#endif // _WIN32 - - // filenames starting with . are hidden in linux, we do this check for windows as well - if (getFileName(path)[0] == '.') - return true; - - // not hidden - return false; - - } // isHidden - - } // FileSystem:: - -} // Utils:: +#define _FILE_OFFSET_BITS 64 + +#include "utils/FileSystemUtil.h" + +#include "Settings.h" +#include +#include + +#if defined(_WIN32) +// because windows... +#include +#include +#define getcwd _getcwd +#define mkdir(x,y) _mkdir(x) +#define snprintf _snprintf +#define stat64 _stat64 +#define unlink _unlink +#define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) +#define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) +#else // _WIN32 +#include +#include +#endif // _WIN32 + +namespace Utils +{ + namespace FileSystem + { + +#if defined(_WIN32) + static std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + std::string string; + + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return std::string(string); + + } // convertFromWideString +#endif // _WIN32 + + stringList getDirContent(const std::string& _path, const bool _recursive) + { + std::string path = getGenericPath(_path); + stringList contentList; + + // only parse the directory, if it's a directory + if(isDirectory(path)) + { + +#if defined(_WIN32) + WIN32_FIND_DATAW findData; + std::string wildcard = path + "/*"; + HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); + + if(hFind != INVALID_HANDLE_VALUE) + { + // loop over all files in the directory + do + { + std::string name = convertFromWideString(findData.cFileName); + + // ignore "." and ".." + if((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + contentList.push_back(fullName); + + if(_recursive && isDirectory(fullName)) + contentList.merge(getDirContent(fullName, true)); + } + } + while(FindNextFileW(hFind, &findData)); + + FindClose(hFind); + } +#else // _WIN32 + DIR* dir = opendir(path.c_str()); + + if(dir != NULL) + { + struct dirent* entry; + + // loop over all files in the directory + while((entry = readdir(dir)) != NULL) + { + std::string name(entry->d_name); + + // ignore "." and ".." + if((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + contentList.push_back(fullName); + + if(_recursive && isDirectory(fullName)) + contentList.merge(getDirContent(fullName, true)); + } + } + + closedir(dir); + } +#endif // _WIN32 + + } + + // sort the content list + contentList.sort(); + + // return the content list + return contentList; + + } // getDirContent + + stringList getPathList(const std::string& _path) + { + stringList pathList; + std::string path = getGenericPath(_path); + size_t start = 0; + size_t end = 0; + + // split at '/' + while((end = path.find("/", start)) != std::string::npos) + { + if(end != start) + pathList.push_back(std::string(path, start, end - start)); + + start = end + 1; + } + + // add last folder / file to pathList + if(start != path.size()) + pathList.push_back(std::string(path, start, path.size() - start)); + + // return the path list + return pathList; + + } // getPathList + + std::string getHomePath() + { + static std::string path; + + // only construct the homepath once + if(!path.length()) + { + // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows + char* envHome = getenv("HOME"); + +#ifdef _DEBUG + // envHome = "H:/[Emulz]/EmulationStation/"; +#endif + + if (envHome) + path = getGenericPath(envHome); + +#if defined(_WIN32) + // but does not seem to work for Windows XP or Vista, so try something else + if(!path.length()) + { + char* envHomeDrive = getenv("HOMEDRIVE"); + char* envHomePath = getenv("HOMEPATH"); + if(envHomeDrive && envHomePath) + path = getGenericPath(std::string(envHomeDrive) + "/" + envHomePath); + } +#endif // _WIN32 + + // no homepath found, fall back to current working directory + if(!path.length()) + path = getCWDPath(); + } + + // return constructed homepath + return path; + + } // getHomePath + + std::string getCWDPath() + { + char temp[512]; + return (getcwd(temp, 512) ? getGenericPath(temp) : ""); + } // getCWDPath + + std::string getExePath() + { + static std::string path; + + // only construct the exepath once + if (!path.length()) + { +#if defined(_WIN32) + char buffer[MAX_PATH]; + DWORD size = MAX_PATH; + DWORD result = GetModuleFileNameA(NULL, buffer, size); + if (result) + { + std::string ret = buffer; + path = getParent(ret); + return ret; + } +#endif + path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); + if (isRegularFile(path)) + path = getParent(path); + } + + // return constructed exepath + return path; + + } // getExePath + + std::string getPreferredPath(const std::string& _path) + { + std::string path = _path; + size_t offset = std::string::npos; +#if defined(_WIN32) + // convert '/' to '\\' + while((offset = path.find('/')) != std::string::npos) + path.replace(offset, 1, "\\"); +#endif // _WIN32 + return path; + } + + std::string getGenericPath(const std::string& _path) + { + std::string path = _path; + size_t offset = std::string::npos; + + // remove "\\\\?\\" + if((path.find("\\\\?\\")) == 0) + path.erase(0, 4); + + // convert '\\' to '/' + while((offset = path.find('\\')) != std::string::npos) + path.replace(offset, 1 ,"/"); + + // remove double '/' + while((offset = path.find("//")) != std::string::npos) + path.erase(offset, 1); + + // remove trailing '/' + while(path.length() && ((offset = path.find_last_of('/')) == (path.length() - 1))) + path.erase(offset, 1); + + // return generic path + return path; + + } // getGenericPath + + std::string getEscapedPath(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // windows escapes stuff by just putting everything in quotes + return '"' + getPreferredPath(path) + '"'; +#else // _WIN32 + // insert a backslash before most characters that would mess up a bash path + const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; + const char* invalidChar = invalidChars; + + while(*invalidChar) + { + size_t start = 0; + size_t offset = 0; + + while((offset = path.find(*invalidChar, start)) != std::string::npos) + { + start = offset + 1; + + if((offset == 0) || (path[offset - 1] != '\\')) + { + path.insert(offset, 1, '\\'); + ++start; + } + } + + ++invalidChar; + } + + // return escaped path + return path; +#endif // _WIN32 + + } // getEscapedPath + + std::string getCanonicalPath(const std::string& _path) + { + // temporary hack for builtin resources + if((_path[0] == ':') && (_path[1] == '/')) + return _path; + + std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); + + // cleanup path + bool scan = true; + while(scan) + { + stringList pathList = getPathList(path); + + path.clear(); + scan = false; + + for(stringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); ++it) + { + // ignore empty + if((*it).empty()) + continue; + + // remove "/./" + if((*it) == ".") + continue; + + // resolve "/../" + if((*it) == "..") + { + path = getParent(path); + continue; + } + +#if defined(_WIN32) + // append folder to path + path += (path.size() == 0) ? (*it) : ("/" + (*it)); +#else // _WIN32 + // append folder to path + path += ("/" + (*it)); +#endif // _WIN32 + + // resolve symlink + if(isSymlink(path)) + { + std::string resolved = resolveSymlink(path); + + if(resolved.empty()) + return ""; + + if(isAbsolute(resolved)) + path = resolved; + else + path = getParent(path) + "/" + resolved; + + for(++it; it != pathList.cend(); ++it) + path += (path.size() == 0) ? (*it) : ("/" + (*it)); + + scan = true; + break; + } + } + } + + // return canonical path + return path; + + } // getCanonicalPath + + std::string getAbsolutePath(const std::string& _path, const std::string& _base) + { + std::string path = getGenericPath(_path); + std::string base = isAbsolute(_base) ? getGenericPath(_base) : getAbsolutePath(_base); + + // return absolute path + return isAbsolute(path) ? path : getGenericPath(base + "/" + path); + + } // getAbsolutePath + + std::string getParent(const std::string& _path) + { + std::string path = getGenericPath(_path); + size_t offset = std::string::npos; + + // find last '/' and erase it + if((offset = path.find_last_of('/')) != std::string::npos) + return path.erase(offset); + + // no parent found + return path; + + } // getParent + + std::string getFileName(const std::string& _path) + { + std::string path = getGenericPath(_path); + size_t offset = std::string::npos; + + // find last '/' and return the filename + if((offset = path.find_last_of('/')) != std::string::npos) + return ((path[offset + 1] == 0) ? "." : std::string(path, offset + 1)); + + // no '/' found, entire path is a filename + return path; + + } // getFileName + + std::string getStem(const std::string& _path) + { + std::string fileName = getFileName(_path); + size_t offset = std::string::npos; + + // empty fileName + if(fileName == ".") + return fileName; + + // find last '.' and erase the extension + if((offset = fileName.find_last_of('.')) != std::string::npos) + return fileName.erase(offset); + + // no '.' found, filename has no extension + return fileName; + + } // getStem + + std::string getExtension(const std::string& _path) + { + std::string fileName = getFileName(_path); + size_t offset = std::string::npos; + + // empty fileName + if(fileName == ".") + return fileName; + + // find last '.' and return the extension + if((offset = fileName.find_last_of('.')) != std::string::npos) + return std::string(fileName, offset); + + // no '.' found, filename has no extension + return "."; + + } // getExtension + + std::string resolveRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) + { + std::string path = getGenericPath(_path); + std::string relativeTo = isDirectory(_relativeTo) ? getGenericPath(_relativeTo) : getParent(_relativeTo); + + // nothing to resolve + if(!path.length()) + return path; + + // replace '.' with relativeTo + if((path[0] == '.') && (path[1] == '/')) + return (relativeTo + &(path[1])); + + // replace '~' with homePath + if(_allowHome && (path[0] == '~') && (path[1] == '/')) + return (getHomePath() + &(path[1])); + + // nothing to resolve + return path; + + } // resolveRelativePath + + std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) + { + bool contains = false; + std::string path = removeCommonPath(_path, _relativeTo, contains); + + if(contains) + { + // success + return ("./" + path); + } + + if(_allowHome) + { + path = removeCommonPath(_path, getHomePath(), contains); + + if(contains) + { + // success + return ("~/" + path); + } + } + + // nothing to resolve + return path; + + } // createRelativePath + + std::string removeCommonPath(const std::string& _path, const std::string& _common, bool& _contains) + { + std::string path = getGenericPath(_path); + std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); + + // check if path contains common + if(path.find(common) == 0) + { + _contains = true; + return path.substr(common.length() + 1); + } + + // it didn't + _contains = false; + return path; + + } // removeCommonPath + + std::string resolveSymlink(const std::string& _path) + { + std::string path = getGenericPath(_path); + std::string resolved; + +#if defined(_WIN32) + HANDLE hFile = CreateFile(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); + + if(hFile != INVALID_HANDLE_VALUE) + { + resolved.resize(GetFinalPathNameByHandle(hFile, nullptr, 0, FILE_NAME_NORMALIZED) + 1); + if(GetFinalPathNameByHandle(hFile, (LPSTR)resolved.data(), (DWORD)resolved.size(), FILE_NAME_NORMALIZED) > 0) + { + resolved.resize(resolved.size() - 1); + resolved = getGenericPath(resolved); + } + CloseHandle(hFile); + } +#else // _WIN32 + struct stat info; + + // check if lstat succeeded + if(lstat(path.c_str(), &info) == 0) + { + resolved.resize(info.st_size); + if(readlink(path.c_str(), (char*)resolved.data(), resolved.size()) > 0) + resolved = getGenericPath(resolved); + } +#endif // _WIN32 + + // return resolved path + return resolved; + + } // resolveSymlink + + bool removeFile(const std::string& _path) + { + std::string path = getGenericPath(_path); + + // don't remove if it doesn't exists + if(!exists(path)) + return true; + + // try to remove file + return (unlink(path.c_str()) == 0); + + } // removeFile + + bool createDirectory(const std::string& _path) + { + std::string path = getGenericPath(_path); + + // don't create if it already exists + if(exists(path)) + return true; + + // try to create directory + if(mkdir(path.c_str(), 0755) == 0) + return true; + + // failed to create directory, try to create the parent + std::string parent = getParent(path); + + // only try to create parent if it's not identical to path + if(parent != path) + createDirectory(parent); + + // try to create directory again now that the parent should exist + return (mkdir(path.c_str(), 0755) == 0); + + } // createDirectory + + bool exists(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + return (stat64(path.c_str(), &info) == 0); + + } // exists + + bool isAbsolute(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + return ((path.size() > 1) && (path[1] == ':')); +#else // _WIN32 + return ((path.size() > 0) && (path[0] == '/')); +#endif // _WIN32 + + } // isAbsolute + + bool isRegularFile(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + if(stat64(path.c_str(), &info) != 0) + return false; + + // check for S_IFREG attribute + return (S_ISREG(info.st_mode)); + + } // isRegularFile + + bool isDirectory(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat info; + + // check if stat succeeded + if(stat(path.c_str(), &info) != 0) + return false; + + // check for S_IFDIR attribute + return (S_ISDIR(info.st_mode)); + + } // isDirectory + + bool isSymlink(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // check for symlink attribute + const DWORD Attributes = GetFileAttributes(path.c_str()); + if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT)) + return true; +#else // _WIN32 + struct stat info; + + // check if lstat succeeded + if(lstat(path.c_str(), &info) != 0) + return false; + + // check for S_IFLNK attribute + return (S_ISLNK(info.st_mode)); +#endif // _WIN32 + + // not a symlink + return false; + + } // isSymlink + + bool isHidden(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // check for hidden attribute + const DWORD Attributes = GetFileAttributes(path.c_str()); + if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_HIDDEN)) + return true; +#endif // _WIN32 + + // filenames starting with . are hidden in linux, we do this check for windows as well + if (getFileName(path)[0] == '.') + return true; + + // not hidden + return false; + + } // isHidden + + } // FileSystem:: + +} // Utils:: diff --git a/es-core/src/utils/StringUtil.h b/es-core/src/utils/StringUtil.h index 7ebcf0df22..3ffb5c5eac 100644 --- a/es-core/src/utils/StringUtil.h +++ b/es-core/src/utils/StringUtil.h @@ -28,7 +28,7 @@ namespace Utils std::string format (const char* _string, ...); std::string scramble (const std::string& _input, const std::string& key); -#if defined(_WIN32) +#if defined(_WIN32) const std::string convertFromWideString(const std::wstring wstring); const std::wstring convertToWideString(const std::string string); #endif From c12d796044e1c980e2bce52159b5bbfc018fba38 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 3 Jun 2019 17:02:57 +0200 Subject: [PATCH 032/336] [Misc Optimisations] ( loading, fonts, metadata... ) [Theming] Menu themings [Clipping] Make Clip before OpenGL .... --- es-app/src/FileData.cpp | 6 +- es-app/src/FileData.h | 268 +-- es-app/src/FileSorts.cpp | 12 +- es-app/src/MetaData.cpp | 62 +- es-app/src/MetaData.h | 11 + es-app/src/SystemData.cpp | 1178 ++++++------ es-app/src/components/RatingComponent.cpp | 5 + .../src/guis/GuiCollectionSystemsOptions.cpp | 14 +- es-app/src/guis/GuiGamelistFilter.cpp | 3 +- es-app/src/guis/GuiGamelistOptions.cpp | 10 +- .../src/guis/GuiGeneralScreensaverOptions.cpp | 4 +- es-app/src/guis/GuiMenu.cpp | 1451 +++++++-------- es-app/src/guis/GuiMetaDataEd.cpp | 767 ++++---- es-app/src/guis/GuiScraperMulti.cpp | 5 + es-app/src/guis/GuiScraperStart.cpp | 29 +- es-app/src/guis/GuiSettings.cpp | 11 +- es-app/src/guis/GuiSettings.h | 2 + es-app/src/main.cpp | 69 +- es-app/src/views/SystemView.cpp | 22 +- es-app/src/views/SystemView.h | 1 + es-app/src/views/ViewController.cpp | 46 +- es-app/src/views/gamelist/IGameListView.cpp | 4 + es-core/src/GuiComponent.h | 446 ++--- es-core/src/ImageIO.cpp | 87 +- es-core/src/ImageIO.h | 2 + es-core/src/PowerSaver.cpp | 1 + es-core/src/Renderer.h | 2 + es-core/src/Renderer_draw_gl.cpp | 51 +- es-core/src/Renderer_init_sdlgl.cpp | 27 +- es-core/src/Settings.cpp | 6 + es-core/src/ThemeData.cpp | 1579 +++++++++-------- es-core/src/ThemeData.h | 32 +- es-core/src/Window.cpp | 1062 +++++------ es-core/src/components/ButtonComponent.cpp | 14 +- es-core/src/components/ButtonComponent.h | 3 +- es-core/src/components/ComponentGrid.cpp | 5 +- es-core/src/components/ComponentGrid.h | 5 +- es-core/src/components/ComponentList.cpp | 51 +- es-core/src/components/GridTileComponent.cpp | 52 +- es-core/src/components/GridTileComponent.h | 1 + es-core/src/components/ImageComponent.cpp | 32 +- es-core/src/components/ImageComponent.h | 4 + es-core/src/components/MenuComponent.cpp | 32 +- es-core/src/components/MenuComponent.h | 8 +- es-core/src/components/NinePatchComponent.cpp | 13 + es-core/src/components/NinePatchComponent.h | 2 + es-core/src/components/OptionListComponent.h | 43 +- es-core/src/components/SliderComponent.cpp | 22 +- es-core/src/components/SliderComponent.h | 3 + es-core/src/components/SwitchComponent.cpp | 8 + es-core/src/components/SwitchComponent.h | 2 + es-core/src/components/TextComponent.cpp | 14 +- es-core/src/components/TextComponent.h | 2 +- es-core/src/components/VideoVlcComponent.cpp | 1 + es-core/src/guis/GuiDetectDevice.cpp | 17 +- es-core/src/guis/GuiInputConfig.cpp | 16 +- es-core/src/guis/GuiMsgBox.cpp | 8 +- es-core/src/guis/GuiTextEditPopup.cpp | 5 + es-core/src/resources/Font.cpp | 77 +- es-core/src/resources/Font.h | 6 +- es-core/src/resources/TextureData.cpp | 22 +- es-core/src/resources/TextureData.h | 1 + es-core/src/utils/FileSystemUtil.cpp | 1441 ++++++++------- es-core/src/utils/FileSystemUtil.h | 14 +- 64 files changed, 5004 insertions(+), 4195 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 532ea9a71d..71cdc3352d 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -20,11 +20,10 @@ FileData::FileData(FileType type, const std::string& path, SystemEnvironmentData : mType(type), mPath(path), mSystem(system), mEnvData(envData), mSourceFileData(NULL), 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()) + if (metadata.get("name").empty()) metadata.set("name", getDisplayName()); mSystemName = system->getName(); - // mDefaultCore = system->getSystemEnvData()->mDefaultCore; } FileData::~FileData() @@ -85,7 +84,6 @@ const bool FileData::getFavorite() return metadata.get("favorite") == "true"; } - const bool FileData::getHidden() { return metadata.get("hidden") == "true"; @@ -291,7 +289,7 @@ void FileData::removeChild(FileData* file) } void FileData::sort(ComparisonFunction& comparator, bool ascending) -{ +{ std::stable_sort(mChildren.begin(), mChildren.end(), comparator); for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index a4a0d7e866..d8bafed6d2 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -1,134 +1,134 @@ -#pragma once -#ifndef ES_APP_FILE_DATA_H -#define ES_APP_FILE_DATA_H - -#include "utils/FileSystemUtil.h" -#include "MetaData.h" -#include - -class SystemData; -class Window; -struct SystemEnvironmentData; - -enum FileType -{ - GAME = 1, // Cannot have children. - FOLDER = 2, - PLACEHOLDER = 3 -}; - -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); - -// A tree node that holds information for a file. -class FileData -{ -public: - FileData(FileType type, const std::string& path, SystemEnvironmentData* envData, SystemData* system); - virtual ~FileData(); - - virtual const std::string& getName(); - virtual const std::string& getSortName(); - inline FileType getType() const { return mType; } - inline const std::string& getPath() const { return mPath; } - inline FileData* getParent() const { return mParent; } - inline const std::unordered_map& getChildrenByFilename() const { return mChildrenByFilename; } - inline const std::vector& getChildren() const { return mChildren; } - inline SystemData* getSystem() const { return mSystem; } - inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } - virtual const std::string getThumbnailPath() const; - virtual const std::string getVideoPath() const; - virtual const std::string getMarqueePath() const; - virtual const std::string getImagePath() const; - - virtual const std::string getCore() const; - virtual const std::string getEmulator() const; - - virtual const bool getHidden(); - virtual const bool getFavorite(); - - FileData* findUniqueGameForFolder(); - - const std::vector& getChildrenListToDisplay(); - std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; - - void addChild(FileData* file); // Error if mType != FOLDER - void removeChild(FileData* file); //Error if mType != FOLDER - - inline bool isPlaceHolder() { return mType == PLACEHOLDER; }; - - virtual inline void refreshMetadata() { return; }; - - virtual std::string getKey(); - const bool isArcadeAsset(); - inline std::string getFullPath() { return getPath(); }; - inline std::string getFileName() { return Utils::FileSystem::getFileName(getPath()); }; - virtual FileData* getSourceFileData(); - inline std::string getSystemName() const { return mSystemName; }; - - // Returns our best guess at the "real" name for this file (will attempt to perform MAME name translation) - std::string getDisplayName() const; - - // As above, but also remove parenthesis - std::string getCleanName() const; - - void launchGame(Window* window); - - 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; - -protected: - FileData* mSourceFileData; - FileData* mParent; - std::string mSystemName; - //std::string mDefaultCore; - -private: - FileType mType; - std::string mPath; - SystemEnvironmentData* mEnvData; - SystemData* mSystem; - std::unordered_map mChildrenByFilename; - std::vector mChildren; - std::vector mFilteredChildren; -}; - -class CollectionFileData : public FileData -{ -public: - CollectionFileData(FileData* file, SystemData* system); - ~CollectionFileData(); - const std::string& getName(); - void refreshMetadata(); - FileData* getSourceFileData(); - std::string getKey(); -private: - // needs to be updated when metadata changes - std::string mCollectionFileName; - bool mDirty; -}; - -FileData::SortType getSortTypeFromString(std::string desc); - -#endif // ES_APP_FILE_DATA_H +#pragma once +#ifndef ES_APP_FILE_DATA_H +#define ES_APP_FILE_DATA_H + +#include "utils/FileSystemUtil.h" +#include "MetaData.h" +#include + +class SystemData; +class Window; +struct SystemEnvironmentData; + +enum FileType +{ + GAME = 1, // Cannot have children. + FOLDER = 2, + PLACEHOLDER = 3 +}; + +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); + +// A tree node that holds information for a file. +class FileData +{ +public: + FileData(FileType type, const std::string& path, SystemEnvironmentData* envData, SystemData* system); + virtual ~FileData(); + + virtual const std::string& getName(); + virtual const std::string& getSortName(); + inline FileType getType() const { return mType; } + inline const std::string& getPath() const { return mPath; } + inline FileData* getParent() const { return mParent; } + inline const std::unordered_map& getChildrenByFilename() const { return mChildrenByFilename; } + inline const std::vector& getChildren() const { return mChildren; } + inline SystemData* getSystem() const { return mSystem; } + inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } + virtual const std::string getThumbnailPath() const; + virtual const std::string getVideoPath() const; + virtual const std::string getMarqueePath() const; + virtual const std::string getImagePath() const; + + virtual const std::string getCore() const; + virtual const std::string getEmulator() const; + + virtual const bool getHidden(); + virtual const bool getFavorite(); + + FileData* findUniqueGameForFolder(); + + const std::vector& getChildrenListToDisplay(); + std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; + + void addChild(FileData* file); // Error if mType != FOLDER + void removeChild(FileData* file); //Error if mType != FOLDER + + inline bool isPlaceHolder() { return mType == PLACEHOLDER; }; + + virtual inline void refreshMetadata() { return; }; + + virtual std::string getKey(); + const bool isArcadeAsset(); + inline std::string getFullPath() { return getPath(); }; + inline std::string getFileName() { return Utils::FileSystem::getFileName(getPath()); }; + virtual FileData* getSourceFileData(); + inline std::string getSystemName() const { return mSystemName; }; + + // Returns our best guess at the "real" name for this file (will attempt to perform MAME name translation) + std::string getDisplayName() const; + + // As above, but also remove parenthesis + std::string getCleanName() const; + + void launchGame(Window* window); + + 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; + +protected: + FileData* mSourceFileData; + FileData* mParent; + std::string mSystemName; + //std::string mDefaultCore; + +private: + FileType mType; + std::string mPath; + SystemEnvironmentData* mEnvData; + SystemData* mSystem; + std::unordered_map mChildrenByFilename; + std::vector mChildren; + std::vector mFilteredChildren; +}; + +class CollectionFileData : public FileData +{ +public: + CollectionFileData(FileData* file, SystemData* system); + ~CollectionFileData(); + const std::string& getName(); + void refreshMetadata(); + FileData* getSourceFileData(); + std::string getKey(); +private: + // needs to be updated when metadata changes + std::string mCollectionFileName; + bool mDirty; +}; + +FileData::SortType getSortTypeFromString(std::string desc); + +#endif // ES_APP_FILE_DATA_H diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index 5043656e11..d1237c0a2d 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -41,16 +41,8 @@ namespace FileSorts //returns if file1 should come before file2 bool compareName(const FileData* file1, const FileData* file2) { - // we compare the actual metadata name, as collection files have the system appended which messes up the order - std::string name1 = Utils::String::toUpper(file1->metadata.get("sortname")); - std::string name2 = Utils::String::toUpper(file2->metadata.get("sortname")); - if(name1.empty()){ - name1 = Utils::String::toUpper(file1->metadata.get("name")); - } - if(name2.empty()){ - name2 = Utils::String::toUpper(file2->metadata.get("name")); - } - return name1.compare(name2) < 0; + std::string name1 = file1->metadata.getSortName(); + return name1.compare(file2->metadata.getSortName()) < 0; } bool compareRating(const FileData* file1, const FileData* file2) diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index 3c9900d5a4..c9c1716188 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -1,6 +1,7 @@ #include "MetaData.h" #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" #include "Log.h" #include @@ -51,6 +52,20 @@ MetaDataDecl folderDecls[] = { }; const std::vector folderMDD(folderDecls, folderDecls + sizeof(folderDecls) / sizeof(folderDecls[0])); +std::map MetaDataList::mDefaultGameMap = MetaDataList::BuildDefaultMap(GAME_METADATA); +std::map MetaDataList::mDefaultFolderMap = MetaDataList::BuildDefaultMap(FOLDER_METADATA); + +std::map MetaDataList::BuildDefaultMap(MetaDataListType type) +{ + std::map ret; + + const std::vector& mdd = getMDDByType(type); + for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + ret[iter->key] = iter->defaultValue; + + return ret; +} + const std::vector& getMDDByType(MetaDataListType type) { switch(type) @@ -65,17 +80,11 @@ const std::vector& getMDDByType(MetaDataListType type) return gameMDD; } +MetaDataList::MetaDataList(MetaDataListType type) : mType(type), mWasChanged(false) +{ - -MetaDataList::MetaDataList(MetaDataListType type) - : mType(type), mWasChanged(false) -{ - const std::vector& mdd = getMDD(); - for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) - set(iter->key, iter->defaultValue); } - MetaDataList MetaDataList::createFromXML(MetaDataListType type, pugi::xml_node& node, const std::string& relativeTo) { MetaDataList mdl(type); @@ -85,17 +94,14 @@ MetaDataList MetaDataList::createFromXML(MetaDataListType type, pugi::xml_node& for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) { pugi::xml_node md = node.child(iter->key.c_str()); - if(md) - { - // if it's a path, resolve relative paths + if (md) + { std::string value = md.text().get(); - if (iter->type == MD_PATH) - { + + if (iter->type == MD_PATH) // if it's a path, resolve relative paths value = Utils::FileSystem::resolveRelativePath(value, relativeTo, true); - } + mdl.set(iter->key, value); - }else{ - mdl.set(iter->key, iter->defaultValue); } } @@ -113,7 +119,7 @@ void MetaDataList::appendToXML(pugi::xml_node& parent, bool ignoreDefaults, cons { // we have this value! // if it's just the default (and we ignore defaults), don't write it - if(ignoreDefaults && mapIter->second == mddIter->defaultValue) + if (ignoreDefaults && mapIter->second == mddIter->defaultValue) continue; // try and make paths relative if we can @@ -128,13 +134,23 @@ void MetaDataList::appendToXML(pugi::xml_node& parent, bool ignoreDefaults, cons void MetaDataList::set(const std::string& key, const std::string& value) { + if ((key == "sortname" || key == "name") && !value.empty()) + mSortName = Utils::String::toUpper(value); + mMap[key] = value; mWasChanged = true; } const std::string& MetaDataList::get(const std::string& key) const { - return mMap.at(key); + auto it = mMap.find(key); + if (it != mMap.end()) + return it->second; + + if (mType == GAME_METADATA) + return mDefaultGameMap.at(key); + + return mDefaultFolderMap.at(key); } int MetaDataList::getInt(const std::string& key) const @@ -150,9 +166,15 @@ float MetaDataList::getFloat(const std::string& key) const bool MetaDataList::isDefault() { const std::vector& mdd = getMDD(); + + for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + { + auto it = mMap.find(iter->key); + if (it == mMap.end()) + continue; - for (unsigned int i = 1; i < mMap.size(); i++) { - if (mMap.at(mdd[i].key) != mdd[i].defaultValue) return false; + if (it->second != iter->defaultValue) + return false; } return true; diff --git a/es-app/src/MetaData.h b/es-app/src/MetaData.h index 7205511a2c..c0c01f264d 100644 --- a/es-app/src/MetaData.h +++ b/es-app/src/MetaData.h @@ -64,10 +64,21 @@ class MetaDataList inline MetaDataListType getType() const { return mType; } inline const std::vector& getMDD() const { return getMDDByType(getType()); } + const std::string getSortName() const { + return mSortName; + } + private: MetaDataListType mType; std::map mMap; bool mWasChanged; + + static std::map mDefaultGameMap; + static std::map mDefaultFolderMap; + + static std::map BuildDefaultMap(MetaDataListType type); + + std::string mSortName; }; #endif // ES_APP_META_DATA_H diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 6389da9957..b2cd549059 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -1,594 +1,584 @@ -#include "SystemData.h" - -#include "utils/FileSystemUtil.h" -#include "CollectionSystemManager.h" -#include "FileFilterIndex.h" -#include "FileSorts.h" -#include "Gamelist.h" -#include "Log.h" -#include "platform.h" -#include "Settings.h" -#include "ThemeData.h" -#include "views/UIModeController.h" -#include -#include -#ifdef WIN32 -#include -#endif - -#include "GuiComponent.h" -#include "Window.h" - -std::vector SystemData::sSystemVector; - -SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : - mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) -{ - mFilterIndex = new FileFilterIndex(); - - // if it's an actual system, initialize it, if not, just create the data structure - if (!CollectionSystem) - { - mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); - mRootFolder->metadata.set("name", mFullName); - - if (!Settings::getInstance()->getBool("ParseGamelistOnly")) - { - populateFolder(mRootFolder); - if (mRootFolder->getChildren().size() == 0) - return; - } - - if (!Settings::getInstance()->getBool("IgnoreGamelist")) - parseGamelist(this); - - mRootFolder->sort(FileSorts::SortTypes.at(0)); - - indexAllGameFilters(mRootFolder); - } - else - { - // virtual systems are updated afterwards, we're just creating the data structure - mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this); - } - setIsGameSystemStatus(); - loadTheme(); -} - -SystemData::~SystemData() -{ - delete mRootFolder; - delete mFilterIndex; -} - -void SystemData::setIsGameSystemStatus() -{ - // we exclude non-game systems from specific operations (i.e. the "RetroPie" system, at least) - // if/when there are more in the future, maybe this can be a more complex method, with a proper list - // but for now a simple string comparison is more performant - mIsGameSystem = (mName != "retropie"); -} - -char _easytolower(char in) { - if (in <= 'Z' && in >= 'A') - return in - ('Z' - 'z'); - return in; -} - -void SystemData::populateFolder(FileData* folder) -{ - const std::string& folderPath = folder->getPath(); - if(!Utils::FileSystem::isDirectory(folderPath)) - { - LOG(LogWarning) << "Error - folder with path \"" << folderPath << "\" is not a directory!"; - return; - } - - int di = folderPath.rfind("downloaded_images"); - int md = folderPath.rfind("media"); - if (di > 0 || md > 0) - return; - - //make sure that this isn't a symlink to a thing we already have - if(Utils::FileSystem::isSymlink(folderPath)) - { - //if this symlink resolves to somewhere that's at the beginning of our path, it's gonna recurse - if(folderPath.find(Utils::FileSystem::getCanonicalPath(folderPath)) == 0) - { - LOG(LogWarning) << "Skipping infinitely recursive symlink \"" << folderPath << "\""; - return; - } - } - - std::string filePath; - std::string extension; - bool isGame; - bool showHidden = Settings::getInstance()->getBool("ShowHiddenFiles"); - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(folderPath); - for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - filePath = *it; - - // skip hidden files and folders - if(!showHidden && Utils::FileSystem::isHidden(filePath)) - 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 = Utils::FileSystem::getExtension(filePath); - std::transform(extension.begin(), extension.end(), extension.begin(), ::_easytolower); - - //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(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) - { - FileData* newGame = new FileData(GAME, filePath, mEnvData, this); - - // preventing new arcade assets to be added - if(!newGame->isArcadeAsset()) - { - folder->addChild(newGame); - isGame = true; - } - } - - //add directories that also do not match an extension as folders - if(!isGame && Utils::FileSystem::isDirectory(filePath)) - { - if (filePath.rfind("downloaded_images") == std::string::npos && - filePath.rfind("media") == std::string::npos) - { - FileData* newFolder = new FileData(FOLDER, filePath, mEnvData, this); - populateFolder(newFolder); - - if (newFolder->getChildrenByFilename().size() == 0) - delete newFolder; - else if (newFolder->findUniqueGameForFolder() != NULL) - delete newFolder; - else - folder->addChild(newFolder); - } - } - } -} - -void SystemData::indexAllGameFilters(const FileData* folder) -{ - const std::vector& children = folder->getChildren(); - - for(std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) - { - switch((*it)->getType()) - { - case GAME: { mFilterIndex->addToIndex(*it); } break; - case FOLDER: { indexAllGameFilters(*it); } break; - } - } -} - -std::vector readList(const std::string& str, const char* delims = " \t\r\n,") -{ - std::vector 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(Window* window) -{ - deleteSystems(); - - std::string path = getConfigPath(false); - - LOG(LogInfo) << "Loading system config file " << path << "..."; - - if(!Utils::FileSystem::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 tag!"; - return false; - } - - float systemCount = 1; - for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - systemCount++; - - float currentSystem = 0; - for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - { - std::vector emulatorList; - - // std::vector coreList; - - std::string name, fullname, path, cmd, themeFolder, defaultCore; - - name = system.child("name").text().get(); - fullname = system.child("fullname").text().get(); - path = system.child("path").text().get(); - defaultCore = system.child("defaultCore").text().get(); - - pugi::xml_node emulators = system.child("emulators"); - if (emulators != NULL) - { - for (pugi::xml_node emulator : emulators.children()) - { - EmulatorData emulatorData; - emulatorData.mName = emulator.attribute("name").value(); - emulatorData.mCommandLine = emulator.attribute("command").value(); - - pugi::xml_node cores = emulator.child("cores"); - if (cores != NULL) - { - for (pugi::xml_node core : cores.children()) - { - const std::string& corename = core.text().get(); - - if (defaultCore.length() == 0) - defaultCore = corename; - - emulatorData.mCores.push_back(corename); - // coreList.push_back(corename); - } - } - - emulatorList.push_back(emulatorData); - } - } - - if (window != NULL) - window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); - - currentSystem++; - - // convert extensions list from a string into a vector of strings - - std::vector list = readList(system.child("extension").text().get()); - std::vector extensions; - - for (auto extension = list.cbegin(); extension != list.cend(); extension++) - { - std::string xt = (*extension); - std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); - - if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) - extensions.push_back(xt); - } - - cmd = system.child("command").text().get(); - - // platform id list - const char* platformList = system.child("platform").text().get(); - std::vector platformStrs = readList(platformList); - std::vector platformIds; - for(auto it = platformStrs.cbegin(); it != platformStrs.cend(); 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 - path = Utils::FileSystem::getGenericPath(path); - - //expand home symbol if the startpath contains ~ - if (path[0] == '~') - { - path.erase(0, 1); - path.insert(0, Utils::FileSystem::getHomePath()); - } - - //create the system runtime environment data - SystemEnvironmentData* envData = new SystemEnvironmentData; - envData->mStartPath = path; - envData->mSearchExtensions = extensions; - envData->mLaunchCommand = cmd; - envData->mPlatformIds = platformIds; - // envData->mDefaultCore = defaultCore; - envData->mEmulators = emulatorList; - - // envData->mCores = coreList; - - SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); - if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) - { - LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; - delete newSys; - } - else - sSystemVector.push_back(newSys); - } - - if (window != NULL) - window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); - - currentSystem++; - - CollectionSystemManager::get()->loadCollectionSystems(); - return true; -} - -void SystemData::writeExampleConfig(const std::string& path) -{ - std::ofstream file(path.c_str()); - - file << "\n" - "\n" - "\n" - " \n" - " \n" - "\n" - " \n" - " nes\n" - "\n" - " \n" - " Nintendo Entertainment System\n" - "\n" - " \n" - " ~/roms/nes\n" - "\n" - " \n" - " .nes .NES\n" - "\n" - " \n" - " retroarch -L ~/cores/libretro-fceumm.so %ROM%\n" - "\n" - " \n" - " nes\n" - "\n" - " \n" - " nes\n" - " \n" - "\n"; - - file.close(); - - LOG(LogError) << "Example config written! Go read it at \"" << path << "\"!"; -} - -void SystemData::deleteSystems() -{ - bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); - - for(unsigned int i = 0; i < sSystemVector.size(); i++) - { - SystemData* pData = sSystemVector.at(i); - - if (saveOnExit && !pData->mIsCollectionSystem) - updateGamelist(pData); - - delete pData; - } - - sSystemVector.clear(); -} - -std::string SystemData::getConfigPath(bool forWrite) -{ - std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_systems.cfg"; - if(forWrite || Utils::FileSystem::exists(path)) - return path; - - return "/etc/emulationstation/es_systems.cfg"; -} - -bool SystemData::isVisible() -{ - return (getDisplayedGameCount() > 0 || - (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || - (mIsCollectionSystem && mName == "favorites")); -} - -SystemData* SystemData::getNext() const -{ - std::vector::const_iterator it = getIterator(); - - do { - it++; - if (it == sSystemVector.cend()) - it = sSystemVector.cbegin(); - } while (!(*it)->isVisible()); - // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. - - return *it; -} - -SystemData* SystemData::getPrev() const -{ - std::vector::const_reverse_iterator it = getRevIterator(); - - do { - it++; - if (it == sSystemVector.crend()) - it = sSystemVector.crbegin(); - } while (!(*it)->isVisible()); - // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. - - return *it; -} - -std::string SystemData::getGamelistPath(bool forWrite) const -{ - std::string filePath; - - filePath = mRootFolder->getPath() + "/gamelist.xml"; - if(Utils::FileSystem::exists(filePath)) - return filePath; - - filePath = Utils::FileSystem::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 - Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath)); - if(forWrite || Utils::FileSystem::exists(filePath)) - return filePath; - - 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. system theme from currently selected theme set [CURRENT_THEME_PATH]/[SYSTEM]/theme.xml - // 3. default system theme from currently selected theme set [CURRENT_THEME_PATH]/theme.xml - - // first, check game folder - std::string localThemePath = mRootFolder->getPath() + "/theme.xml"; - if(Utils::FileSystem::exists(localThemePath)) - return localThemePath; - - // not in game folder, try system theme in theme sets - localThemePath = ThemeData::getThemeFromCurrentSet(mThemeFolder); - - if (Utils::FileSystem::exists(localThemePath)) - return localThemePath; - - // not system theme, try default system theme in theme set - localThemePath = Utils::FileSystem::getParent(Utils::FileSystem::getParent(localThemePath)) + "/theme.xml"; - - return localThemePath; -} - -bool SystemData::hasGamelist() const -{ - return (Utils::FileSystem::exists(getGamelistPath(false))); -} - -unsigned int SystemData::getGameCount() const -{ - return (unsigned int)mRootFolder->getFilesRecursive(GAME).size(); -} - -SystemData* SystemData::getRandomSystem() -{ - // this is a bit brute force. It might be more efficient to just to a while (!gameSystem) do random again... - unsigned int total = 0; - for(auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) - { - if ((*it)->isGameSystem()) - total ++; - } - - // get random number in range - int target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); - for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) - { - if ((*it)->isGameSystem()) - { - if (target > 0) - { - target--; - } - else - { - return (*it); - } - } - } - - // if we end up here, there is no valid system - return NULL; -} - -FileData* SystemData::getRandomGame() -{ - std::vector list = mRootFolder->getFilesRecursive(GAME, true); - unsigned int total = (int)list.size(); - int target = 0; - // get random number in range - if (total == 0) - return NULL; - target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); - return list.at(target); -} - -unsigned int SystemData::getDisplayedGameCount() const -{ - return (unsigned int)mRootFolder->getFilesRecursive(GAME, true).size(); -} - -void SystemData::loadTheme() -{ - mTheme = std::make_shared(); - - std::string path = getThemePath(); - - if(!Utils::FileSystem::exists(path)) // no theme available for this platform - return; - - try - { - // build map with system variables for theme to use, - std::map sysData; - sysData.insert(std::pair("system.name", getName())); - sysData.insert(std::pair("system.theme", getThemeFolder())); - sysData.insert(std::pair("system.fullName", getFullName())); - - mTheme->loadFile(getThemeFolder(), sysData, path); - } - catch(ThemeException& e) - { - LOG(LogError) << e.what(); - mTheme = std::make_shared(); // reset to empty - } -} +#include "SystemData.h" + +#include "utils/FileSystemUtil.h" +#include "CollectionSystemManager.h" +#include "FileFilterIndex.h" +#include "FileSorts.h" +#include "Gamelist.h" +#include "Log.h" +#include "platform.h" +#include "Settings.h" +#include "ThemeData.h" +#include "views/UIModeController.h" +#include +#include +#ifdef WIN32 +#include +#endif + +#include "GuiComponent.h" +#include "Window.h" + +std::vector SystemData::sSystemVector; + +SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : + mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) +{ + mFilterIndex = new FileFilterIndex(); + + // if it's an actual system, initialize it, if not, just create the data structure + if (!CollectionSystem) + { + mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); + mRootFolder->metadata.set("name", mFullName); + + if (!Settings::getInstance()->getBool("ParseGamelistOnly")) + { + populateFolder(mRootFolder); + if (mRootFolder->getChildren().size() == 0) + return; + } + + if (!Settings::getInstance()->getBool("IgnoreGamelist")) + parseGamelist(this); + + mRootFolder->sort(FileSorts::SortTypes.at(0)); + + indexAllGameFilters(mRootFolder); + } + else + { + // virtual systems are updated afterwards, we're just creating the data structure + mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this); + } + + setIsGameSystemStatus(); + loadTheme(); +} + +SystemData::~SystemData() +{ + delete mRootFolder; + delete mFilterIndex; +} + +void SystemData::setIsGameSystemStatus() +{ + // we exclude non-game systems from specific operations (i.e. the "RetroPie" system, at least) + // if/when there are more in the future, maybe this can be a more complex method, with a proper list + // but for now a simple string comparison is more performant + mIsGameSystem = (mName != "retropie"); +} + +char _easytolower(char in) { + if (in <= 'Z' && in >= 'A') + return in - ('Z' - 'z'); + return in; +} + +void SystemData::populateFolder(FileData* folder) +{ + const std::string& folderPath = folder->getPath(); + if(!Utils::FileSystem::isDirectory(folderPath)) + { + LOG(LogWarning) << "Error - folder with path \"" << folderPath << "\" is not a directory!"; + return; + } + + //make sure that this isn't a symlink to a thing we already have + if(Utils::FileSystem::isSymlink(folderPath)) + { + //if this symlink resolves to somewhere that's at the beginning of our path, it's gonna recurse + if(folderPath.find(Utils::FileSystem::getCanonicalPath(folderPath)) == 0) + { + LOG(LogWarning) << "Skipping infinitely recursive symlink \"" << folderPath << "\""; + return; + } + } + +// std::string filePath; + std::string extension; + bool isGame; + bool showHidden = Settings::getInstance()->getBool("ShowHiddenFiles"); + + Utils::FileSystem::fileList dirContent = Utils::FileSystem::getDirInfo(folderPath, false); + for(Utils::FileSystem::fileList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + auto fileInfo = *it; + //filePath = *it; + + // skip hidden files and folders + if(!showHidden && fileInfo.readOnly) + 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 = Utils::FileSystem::getExtension(fileInfo.path); + std::transform(extension.begin(), extension.end(), extension.begin(), ::_easytolower); + + //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(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) + { + FileData* newGame = new FileData(GAME, fileInfo.path, mEnvData, this); + + // preventing new arcade assets to be added + if (extension != ".zip" || !newGame->isArcadeAsset()) + { + folder->addChild(newGame); + isGame = true; + } + } + + //add directories that also do not match an extension as folders + if (!isGame && fileInfo.directory) + { + if (fileInfo.path.rfind("downloaded_images") != std::string::npos || fileInfo.path.rfind("media") != std::string::npos) + continue; + + FileData* newFolder = new FileData(FOLDER, fileInfo.path, mEnvData, this); + populateFolder(newFolder); + + if (newFolder->getChildrenByFilename().size() == 0) + delete newFolder; + else if (newFolder->findUniqueGameForFolder() != NULL) + delete newFolder; + else + folder->addChild(newFolder); + } + } +} + +void SystemData::indexAllGameFilters(const FileData* folder) +{ + const std::vector& children = folder->getChildren(); + + for(std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + switch((*it)->getType()) + { + case GAME: { mFilterIndex->addToIndex(*it); } break; + case FOLDER: { indexAllGameFilters(*it); } break; + } + } +} + +std::vector readList(const std::string& str, const char* delims = " \t\r\n,") +{ + std::vector 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(Window* window) +{ + deleteSystems(); + + std::string path = getConfigPath(false); + + LOG(LogInfo) << "Loading system config file " << path << "..."; + + if(!Utils::FileSystem::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 tag!"; + return false; + } + + float systemCount = 1; + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + systemCount++; + + float currentSystem = 0; + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { + std::vector emulatorList; + + std::string name, fullname, path, cmd, themeFolder, defaultCore; + + name = system.child("name").text().get(); + fullname = system.child("fullname").text().get(); + path = system.child("path").text().get(); + defaultCore = system.child("defaultCore").text().get(); + + pugi::xml_node emulators = system.child("emulators"); + if (emulators != NULL) + { + for (pugi::xml_node emulator : emulators.children()) + { + EmulatorData emulatorData; + emulatorData.mName = emulator.attribute("name").value(); + emulatorData.mCommandLine = emulator.attribute("command").value(); + + pugi::xml_node cores = emulator.child("cores"); + if (cores != NULL) + { + for (pugi::xml_node core : cores.children()) + { + const std::string& corename = core.text().get(); + + if (defaultCore.length() == 0) + defaultCore = corename; + + emulatorData.mCores.push_back(corename); + } + } + + emulatorList.push_back(emulatorData); + } + } + + if (window != NULL) + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; + + // convert extensions list from a string into a vector of strings + + std::vector list = readList(system.child("extension").text().get()); + std::vector extensions; + + for (auto extension = list.cbegin(); extension != list.cend(); extension++) + { + std::string xt = (*extension); + std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); + + if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) + extensions.push_back(xt); + } + + cmd = system.child("command").text().get(); + + // platform id list + const char* platformList = system.child("platform").text().get(); + std::vector platformStrs = readList(platformList); + std::vector platformIds; + for(auto it = platformStrs.cbegin(); it != platformStrs.cend(); 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 + path = Utils::FileSystem::getGenericPath(path); + + //expand home symbol if the startpath contains ~ + if (path[0] == '~') + { + path.erase(0, 1); + path.insert(0, Utils::FileSystem::getHomePath()); + } + + //create the system runtime environment data + SystemEnvironmentData* envData = new SystemEnvironmentData; + envData->mStartPath = path; + envData->mSearchExtensions = extensions; + envData->mLaunchCommand = cmd; + envData->mPlatformIds = platformIds; + envData->mEmulators = emulatorList; + + SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); + if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) + { + LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; + delete newSys; + } + else + sSystemVector.push_back(newSys); + } + + if (window != NULL) + window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; + + CollectionSystemManager::get()->loadCollectionSystems(); + return true; +} + +void SystemData::writeExampleConfig(const std::string& path) +{ + std::ofstream file(path.c_str()); + + file << "\n" + "\n" + "\n" + " \n" + " \n" + "\n" + " \n" + " nes\n" + "\n" + " \n" + " Nintendo Entertainment System\n" + "\n" + " \n" + " ~/roms/nes\n" + "\n" + " \n" + " .nes .NES\n" + "\n" + " \n" + " retroarch -L ~/cores/libretro-fceumm.so %ROM%\n" + "\n" + " \n" + " nes\n" + "\n" + " \n" + " nes\n" + " \n" + "\n"; + + file.close(); + + LOG(LogError) << "Example config written! Go read it at \"" << path << "\"!"; +} + +void SystemData::deleteSystems() +{ + bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); + + for(unsigned int i = 0; i < sSystemVector.size(); i++) + { + SystemData* pData = sSystemVector.at(i); + + if (saveOnExit && !pData->mIsCollectionSystem) + updateGamelist(pData); + + delete pData; + } + + sSystemVector.clear(); +} + +std::string SystemData::getConfigPath(bool forWrite) +{ + std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_systems.cfg"; + if(forWrite || Utils::FileSystem::exists(path)) + return path; + + return "/etc/emulationstation/es_systems.cfg"; +} + +bool SystemData::isVisible() +{ + return (getDisplayedGameCount() > 0 || + (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || + (mIsCollectionSystem && mName == "favorites")); +} + +SystemData* SystemData::getNext() const +{ + std::vector::const_iterator it = getIterator(); + + do { + it++; + if (it == sSystemVector.cend()) + it = sSystemVector.cbegin(); + } while (!(*it)->isVisible()); + // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. + + return *it; +} + +SystemData* SystemData::getPrev() const +{ + std::vector::const_reverse_iterator it = getRevIterator(); + + do { + it++; + if (it == sSystemVector.crend()) + it = sSystemVector.crbegin(); + } while (!(*it)->isVisible()); + // as we are starting in a valid gamelistview, this will always succeed, even if we have to come full circle. + + return *it; +} + +std::string SystemData::getGamelistPath(bool forWrite) const +{ + std::string filePath; + + filePath = mRootFolder->getPath() + "/gamelist.xml"; + if(Utils::FileSystem::exists(filePath)) + return filePath; + + filePath = Utils::FileSystem::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 + Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath)); + if(forWrite || Utils::FileSystem::exists(filePath)) + return filePath; + + 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. system theme from currently selected theme set [CURRENT_THEME_PATH]/[SYSTEM]/theme.xml + // 3. default system theme from currently selected theme set [CURRENT_THEME_PATH]/theme.xml + + // first, check game folder + std::string localThemePath = mRootFolder->getPath() + "/theme.xml"; + if(Utils::FileSystem::exists(localThemePath)) + return localThemePath; + + // not in game folder, try system theme in theme sets + localThemePath = ThemeData::getThemeFromCurrentSet(mThemeFolder); + + if (Utils::FileSystem::exists(localThemePath)) + return localThemePath; + + // not system theme, try default system theme in theme set + localThemePath = Utils::FileSystem::getParent(Utils::FileSystem::getParent(localThemePath)) + "/theme.xml"; + + return localThemePath; +} + +bool SystemData::hasGamelist() const +{ + return (Utils::FileSystem::exists(getGamelistPath(false))); +} + +unsigned int SystemData::getGameCount() const +{ + return (unsigned int)mRootFolder->getFilesRecursive(GAME).size(); +} + +SystemData* SystemData::getRandomSystem() +{ + // this is a bit brute force. It might be more efficient to just to a while (!gameSystem) do random again... + unsigned int total = 0; + for(auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) + { + if ((*it)->isGameSystem()) + total ++; + } + + // get random number in range + int target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); + for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) + { + if ((*it)->isGameSystem()) + { + if (target > 0) + { + target--; + } + else + { + return (*it); + } + } + } + + // if we end up here, there is no valid system + return NULL; +} + +FileData* SystemData::getRandomGame() +{ + std::vector list = mRootFolder->getFilesRecursive(GAME, true); + unsigned int total = (int)list.size(); + int target = 0; + // get random number in range + if (total == 0) + return NULL; + target = (int)Math::round((std::rand() / (float)RAND_MAX) * (total - 1)); + return list.at(target); +} + +unsigned int SystemData::getDisplayedGameCount() const +{ + return (unsigned int)mRootFolder->getFilesRecursive(GAME, true).size(); +} + +void SystemData::loadTheme() +{ + mTheme = std::make_shared(); + + std::string path = getThemePath(); + if (!Utils::FileSystem::exists(path)) // no theme available for this platform + return; + + try + { + // build map with system variables for theme to use, + std::map sysData; + sysData.insert(std::pair("system.name", getName())); + sysData.insert(std::pair("system.theme", getThemeFolder())); + sysData.insert(std::pair("system.fullName", getFullName())); + + mTheme->loadFile(getThemeFolder(), sysData, path); + } + catch(ThemeException& e) + { + LOG(LogError) << e.what(); + mTheme = std::make_shared(); // reset to empty + } +} diff --git a/es-app/src/components/RatingComponent.cpp b/es-app/src/components/RatingComponent.cpp index 924a0cbccf..05063694ab 100644 --- a/es-app/src/components/RatingComponent.cpp +++ b/es-app/src/components/RatingComponent.cpp @@ -114,6 +114,11 @@ void RatingComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = parentTrans * getTransform(); trans.round(); + + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + Renderer::setMatrix(trans); glEnable(GL_TEXTURE_2D); diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index be30460aa4..c47c7e7d5e 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -27,7 +27,7 @@ void GuiCollectionSystemsOptions::initializeMenu() std::vector unusedFolders = CollectionSystemManager::get()->getUnusedSystemsFromTheme(); if (unusedFolders.size() > 0) { - addEntry(_T("CREATE NEW CUSTOM COLLECTION FROM THEME"), 0x777777FF, true, + addEntry(_T("CREATE NEW CUSTOM COLLECTION FROM THEME"), ThemeData::getMenuTheme()->Text.color, true, [this, unusedFolders] { auto s = new GuiSettings(mWindow, _T("SELECT THEME FOLDER")); std::shared_ptr< OptionListComponent > folderThemes = std::make_shared< OptionListComponent >(mWindow, "SELECT THEME FOLDER", true); @@ -47,12 +47,14 @@ void GuiCollectionSystemsOptions::initializeMenu() row.addElement(themeFolder, true); s->addRow(row); } + + s->updatePosition(); mWindow->pushGui(s); }); } ComponentListRow row; - row.addElement(std::make_shared(mWindow, _T("CREATE NEW CUSTOM COLLECTION"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("CREATE NEW CUSTOM COLLECTION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); auto createCustomCollection = [this](const std::string& newVal) { std::string name = newVal; // we need to store the first Gui and remove it, as it'll be deleted by the actual Gui @@ -85,16 +87,18 @@ void GuiCollectionSystemsOptions::initializeMenu() mMenu.addButton(_T("BACK"), _T("BACK"), std::bind(&GuiCollectionSystemsOptions::applySettings, this)); - mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); + mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); } void GuiCollectionSystemsOptions::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) { - std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); + auto theme = ThemeData::getMenuTheme(); + + std::shared_ptr font = theme->Text.font; // Font::get(FONT_SIZE_MEDIUM); // populate the list ComponentListRow row; - row.addElement(std::make_shared(mWindow, name, font, color), true); + row.addElement(std::make_shared(mWindow, name, font, theme->Text.color), true); if(add_arrow) { diff --git a/es-app/src/guis/GuiGamelistFilter.cpp b/es-app/src/guis/GuiGamelistFilter.cpp index 1216775439..ffdc11bc43 100644 --- a/es-app/src/guis/GuiGamelistFilter.cpp +++ b/es-app/src/guis/GuiGamelistFilter.cpp @@ -19,9 +19,10 @@ void GuiGamelistFilter::initializeMenu() ComponentListRow row; + // show filtered menu row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("RESET ALL FILTERS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("RESET ALL FILTERS"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistFilter::resetAllFilters, this)); mMenu.addRow(row); row.elements.clear(); diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index fd42411e4a..7a2d13c20f 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -52,7 +52,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui } } - row.addElement(std::make_shared(mWindow, _T("JUMP TO LETTER"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("JUMP TO LETTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(mJumpToLetterList, false); row.input_handler = [&](InputConfig* config, Input input) { @@ -83,7 +83,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if(!Settings::getInstance()->getBool("ForceDisableFilters")) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("APPLY FILTER"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("APPLY FILTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); mMenu.addRow(row); @@ -96,7 +96,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui CollectionSystemManager::get()->getCustomCollectionsBundle()->getName() == system->getName())) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "ADD/REMOVE GAMES TO THIS GAME COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, "ADD/REMOVE GAMES TO THIS GAME COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::startEditMode, this)); mMenu.addRow(row); } @@ -104,7 +104,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if(UIModeController::getInstance()->isUIModeFull() && CollectionSystemManager::get()->isEditing()) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::exitEditMode, this)); mMenu.addRow(row); } @@ -112,7 +112,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("EDIT THIS GAME'S METADATA"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("EDIT THIS GAME'S METADATA"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openMetaDataEd, this)); mMenu.addRow(row); diff --git a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp index e229aa87c6..e5c0c84409 100644 --- a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp +++ b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp @@ -50,13 +50,13 @@ GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, std:: // show filtered menu row.elements.clear(); - row.addElement(std::make_shared(mWindow, "VIDEO SCREENSAVER SETTINGS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, "VIDEO SCREENSAVER SETTINGS", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGeneralScreensaverOptions::openVideoScreensaverOptions, this)); addRow(row); row.elements.clear(); - row.addElement(std::make_shared(mWindow, "SLIDESHOW SCREENSAVER SETTINGS", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, "SLIDESHOW SCREENSAVER SETTINGS", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGeneralScreensaverOptions::openSlideshowScreensaverOptions, this)); addRow(row); diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index beaf8e8749..8173bc36c1 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -1,64 +1,65 @@ -#include "guis/GuiMenu.h" - -#include "components/OptionListComponent.h" -#include "components/SliderComponent.h" -#include "components/SwitchComponent.h" -#include "guis/GuiCollectionSystemsOptions.h" -#include "guis/GuiDetectDevice.h" -#include "guis/GuiGeneralScreensaverOptions.h" -#include "guis/GuiMsgBox.h" -#include "guis/GuiScraperStart.h" -#include "guis/GuiSettings.h" -#include "views/UIModeController.h" -#include "views/ViewController.h" -#include "CollectionSystemManager.h" -#include "EmulationStation.h" -#include "Scripting.h" -#include "SystemData.h" -#include "VolumeControl.h" -#include -#include - -#include "animations/LambdaAnimation.h" - -GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) - -{ - bool isFullUI = UIModeController::getInstance()->isUIModeFull(); - - if (isFullUI) - { - addEntry(_T("UI SETTINGS"), 0x777777FF, true, [this] { openUISettings(); }); - addEntry(_T("CONFIGURE INPUT"), 0x777777FF, true, [this] { openConfigInput(); }); - } - - addEntry(_T("SOUND SETTINGS"), 0x777777FF, true, [this] { openSoundSettings(); }); - - if (isFullUI) - addEntry(_T("SCRAPER"), 0x777777FF, true, [this] { openScraperSettings(); }); - - if (isFullUI) - { - addEntry(_T("GAME COLLECTION SETTINGS"), 0x777777FF, true, [this] { openCollectionSystemSettings(); }); - addEntry(_T("ADVANCED SETTINGS"), 0x777777FF, true, [this] { openOtherSettings(); }); - } - -#if defined(_WIN32) - addEntry(_T("QUIT"), 0x777777FF, false, [this] {openQuitMenu(); }); -#else - addEntry(_T("QUIT"), 0x777777FF, true, [this] {openQuitMenu(); }); -#endif - - addChild(&mMenu); - addVersionInfo(); - - setSize(mMenu.getSize()); - - float y1 = Renderer::getScreenHeight(); - float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; - - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); - +#include "guis/GuiMenu.h" + +#include "components/OptionListComponent.h" +#include "components/SliderComponent.h" +#include "components/SwitchComponent.h" +#include "guis/GuiCollectionSystemsOptions.h" +#include "guis/GuiDetectDevice.h" +#include "guis/GuiGeneralScreensaverOptions.h" +#include "guis/GuiMsgBox.h" +#include "guis/GuiScraperStart.h" +#include "guis/GuiSettings.h" +#include "views/UIModeController.h" +#include "views/ViewController.h" +#include "CollectionSystemManager.h" +#include "EmulationStation.h" +#include "Scripting.h" +#include "SystemData.h" +#include "VolumeControl.h" +#include +#include + +#include "animations/LambdaAnimation.h" + +GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) +{ + auto theme = ThemeData::getMenuTheme(); + + bool isFullUI = UIModeController::getInstance()->isUIModeFull(); + + if (isFullUI) + { + addEntry(_T("UI SETTINGS"), theme->Text.color, true, [this] { openUISettings(); }); + addEntry(_T("CONFIGURE INPUT"), theme->Text.color, true, [this] { openConfigInput(); }); + } + + addEntry(_T("SOUND SETTINGS"), theme->Text.color, true, [this] { openSoundSettings(); }); + + if (isFullUI) + addEntry(_T("SCRAPER"), theme->Text.color, true, [this] { openScraperSettings(); }); + + if (isFullUI) + { + addEntry(_T("GAME COLLECTION SETTINGS"), theme->Text.color, true, [this] { openCollectionSystemSettings(); }); + addEntry(_T("ADVANCED SETTINGS"), theme->Text.color, true, [this] { openOtherSettings(); }); + } + +#if defined(_WIN32) + addEntry(_T("QUIT"), theme->Text.color, false, [this] {openQuitMenu(); }); +#else + addEntry(_T("QUIT"), theme->Text.color, true, [this] {openQuitMenu(); }); +#endif + + addChild(&mMenu); + addVersionInfo(); + + setSize(mMenu.getSize()); + + float y1 = Renderer::getScreenHeight(); + float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); + auto fadeFunc = [this, y1, y2](float t) { t -= 1; // cubic ease out @@ -70,648 +71,686 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] { - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); - }); - - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); -} - -void GuiMenu::openScraperSettings() -{ - auto s = new GuiSettings(mWindow, "SCRAPER"); - - // scrape from - auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, "SCRAPE FROM", false); - std::vector scrapers = getScraperList(); - - // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. - for(auto it = scrapers.cbegin(); it != scrapers.cend(); 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(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; - auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; - std::function openAndSave = openScrapeNow; - openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; - row.makeAcceptInputHandler(openAndSave); - - auto scrape_now = std::make_shared(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); -} - -void GuiMenu::openSoundSettings() -{ - auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); - - // volume - auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); - volume->setValue((float)VolumeControl::getInstance()->getVolume()); - s->addWithLabel(_T("SYSTEM VOLUME"), volume); - s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); - - if (UIModeController::getInstance()->isUIModeFull()) - { -#if defined(__linux__) - // audio card - auto audio_card = std::make_shared< OptionListComponent >(mWindow, "AUDIO CARD", false); - std::vector audio_cards; - #ifdef _RPI_ - // RPi Specific Audio Cards - audio_cards.push_back("local"); - audio_cards.push_back("hdmi"); - audio_cards.push_back("both"); - #endif - audio_cards.push_back("default"); - audio_cards.push_back("sysdefault"); - audio_cards.push_back("dmix"); - audio_cards.push_back("hw"); - audio_cards.push_back("plughw"); - audio_cards.push_back("null"); - if (Settings::getInstance()->getString("AudioCard") != "") { - if(std::find(audio_cards.begin(), audio_cards.end(), Settings::getInstance()->getString("AudioCard")) == audio_cards.end()) { - audio_cards.push_back(Settings::getInstance()->getString("AudioCard")); - } - } - for(auto ac = audio_cards.cbegin(); ac != audio_cards.cend(); ac++) - audio_card->add(*ac, *ac, Settings::getInstance()->getString("AudioCard") == *ac); - s->addWithLabel("AUDIO CARD", audio_card); - s->addSaveFunc([audio_card] { - Settings::getInstance()->setString("AudioCard", audio_card->getSelected()); - VolumeControl::getInstance()->deinit(); - VolumeControl::getInstance()->init(); - }); - - // volume control device - auto vol_dev = std::make_shared< OptionListComponent >(mWindow, "AUDIO DEVICE", false); - std::vector transitions; - transitions.push_back("PCM"); - transitions.push_back("Speaker"); - transitions.push_back("Master"); - transitions.push_back("Digital"); - transitions.push_back("Analogue"); - if (Settings::getInstance()->getString("AudioDevice") != "") { - if(std::find(transitions.begin(), transitions.end(), Settings::getInstance()->getString("AudioDevice")) == transitions.end()) { - transitions.push_back(Settings::getInstance()->getString("AudioDevice")); - } - } - for(auto it = transitions.cbegin(); it != transitions.cend(); it++) - vol_dev->add(*it, *it, Settings::getInstance()->getString("AudioDevice") == *it); - s->addWithLabel("AUDIO DEVICE", vol_dev); - s->addSaveFunc([vol_dev] { - Settings::getInstance()->setString("AudioDevice", vol_dev->getSelected()); - VolumeControl::getInstance()->deinit(); - VolumeControl::getInstance()->init(); - }); -#endif - - // disable sounds - auto sounds_enabled = std::make_shared(mWindow); - sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); - s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); - s->addSaveFunc([sounds_enabled] { - if (sounds_enabled->getState() - && !Settings::getInstance()->getBool("EnableSounds") - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState()); - }); - - auto video_audio = std::make_shared(mWindow); - video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); - s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); - s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); - -#ifdef _RPI_ - // OMX player Audio Device - auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, "OMX PLAYER AUDIO DEVICE", false); - std::vector omx_cards; - // RPi Specific Audio Cards - omx_cards.push_back("local"); - omx_cards.push_back("hdmi"); - omx_cards.push_back("both"); - omx_cards.push_back("alsa:hw:0,0"); - omx_cards.push_back("alsa:hw:1,0"); - if (Settings::getInstance()->getString("OMXAudioDev") != "") { - if (std::find(omx_cards.begin(), omx_cards.end(), Settings::getInstance()->getString("OMXAudioDev")) == omx_cards.end()) { - omx_cards.push_back(Settings::getInstance()->getString("OMXAudioDev")); - } - } - for (auto it = omx_cards.cbegin(); it != omx_cards.cend(); it++) - omx_audio_dev->add(*it, *it, Settings::getInstance()->getString("OMXAudioDev") == *it); - s->addWithLabel("OMX PLAYER AUDIO DEVICE", omx_audio_dev); - s->addSaveFunc([omx_audio_dev] { - if (Settings::getInstance()->getString("OMXAudioDev") != omx_audio_dev->getSelected()) - Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); - }); -#endif - } - - mWindow->pushGui(s); - -} - -void GuiMenu::openUISettings() -{ - auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); - - // theme set - auto themeSets = ThemeData::getThemeSets(); - - if (!themeSets.empty()) - { - std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if (selectedSet == themeSets.cend()) - selectedSet = themeSets.cbegin(); - - auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); - for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) - theme_set->add(it->first, it->first, it == selectedSet); - s->addWithLabel(_T("THEME"), theme_set); - - Window* window = mWindow; - s->addSaveFunc([window, theme_set] - { - bool needReload = false; - std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - if (oldTheme != theme_set->getSelected()) - needReload = true; - - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - - if (needReload) - { - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation - } - }); - } - - // LANGUAGE - - std::vector langues; - langues.push_back("en"); - - - std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); - if (xmlpath.length() > 0) - { - xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; - - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); - for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if (Utils::FileSystem::isDirectory(*it)) - continue; - - std::string name = *it; - - if (name.rfind("emulationstation2.po") == std::string::npos) - continue; - - name = Utils::FileSystem::getParent(name); - name = Utils::FileSystem::getFileName(name); - - if (name != "en") - langues.push_back(name); - } - - if (langues.size() > 1) - { - auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); - - for (auto it = langues.cbegin(); it != langues.cend(); it++) - language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); - - s->addWithLabel(_T("LANGUAGE"), language); - s->addSaveFunc([language] { - Settings::getInstance()->setString("Language", language->getSelected()); - }); - } - } - - // transition style - auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); - std::vector transitions; - transitions.push_back("fade"); - transitions.push_back("slide"); - transitions.push_back("instant"); - for (auto it = transitions.cbegin(); it != transitions.cend(); it++) - transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); - - s->addWithLabel(_T("TRANSITION STYLE"), transition_style); - s->addSaveFunc([transition_style] { - if (Settings::getInstance()->getString("TransitionStyle") == "instant" - && transition_style->getSelected() != "instant" - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); - }); - - - auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); - std::vector gameTransitions; - gameTransitions.push_back("fade"); - gameTransitions.push_back("slide"); - gameTransitions.push_back("instant"); - for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) - transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); - - s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); - s->addSaveFunc([transitionOfGames_style] { - if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" - && transitionOfGames_style->getSelected() != "instant" - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); - }); - - // GameList view style - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); - std::vector styles; - styles.push_back("automatic"); - styles.push_back("basic"); - styles.push_back("detailed"); - styles.push_back("video"); - styles.push_back("grid"); - styles.push_back("gridex"); - - for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); - - s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); - s->addSaveFunc([gamelist_style] { - bool needReload = false; - if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) - needReload = true; - Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); - if (needReload) - ViewController::get()->reloadAll(); - }); - - - // Optionally start in selected system - auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); - systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); - for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) - { - if ("retropie" != (*it)->getName()) - { - systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); - } - } - s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); - s->addSaveFunc([systemfocus_list] { - Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); - }); - - - /* - //UI mode - auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); - std::vector UImodes = UIModeController::getInstance()->getUIModes(); - for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) - UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); - s->addWithLabel("UI MODE", UImodeSelection); - Window* window = mWindow; - s->addSaveFunc([ UImodeSelection, window] - { - std::string selectedMode = UImodeSelection->getSelected(); - if (selectedMode != "Full") - { - std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; - msg += "This will hide most menu-options to prevent changes to the system.\n"; - msg += "To unlock and return to the full UI, enter this code: \n"; - msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; - msg += "Do you want to proceed?"; - window->pushGui(new GuiMsgBox(window, msg, - "YES", [selectedMode] { - LOG(LogDebug) << "Setting UI mode to " << selectedMode; - Settings::getInstance()->setString("UIMode", selectedMode); - Settings::getInstance()->saveFile(); - }, "NO",nullptr)); - } - }); - */ - - // screensaver - ComponentListRow screensaver_row; - screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - screensaver_row.addElement(makeArrow(mWindow), false); - screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); - s->addRow(screensaver_row); - -#if defined(_WIN32) - // quick system select (left/right in game list view) - auto hideWindowScreen = std::make_shared(mWindow); - hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); - s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); - s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); -#endif - - // quick system select (left/right in game list view) - auto quick_sys_select = std::make_shared(mWindow); - quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); - s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); - s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); - - // carousel transition option - auto move_carousel = std::make_shared(mWindow); - move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); - s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); - s->addSaveFunc([move_carousel] { - if (move_carousel->getState() - && !Settings::getInstance()->getBool("MoveCarousel") - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); - }); - - // show help - auto show_help = std::make_shared(mWindow); - show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); - s->addWithLabel(_T("ON-SCREEN HELP"), show_help); - s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); - - // enable filters (ForceDisableFilters) - auto enable_filter = std::make_shared(mWindow); - enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); - s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); - s->addSaveFunc([enable_filter] { - bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); - Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); - if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); - }); - - mWindow->pushGui(s); - -} - -void GuiMenu::openOtherSettings() -{ - auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); - - // maximum vram - auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); - max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel(_T("VRAM LIMIT"), max_vram); - s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); - - // power saver - auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); - std::vector modes; - modes.push_back("disabled"); - modes.push_back("default"); - modes.push_back("enhanced"); - modes.push_back("instant"); - for (auto it = modes.cbegin(); it != modes.cend(); it++) - power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); - - s->addWithLabel(_T("POWER SAVER MODES"), power_saver); - s->addSaveFunc([this, power_saver] { - if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { - Settings::getInstance()->setString("TransitionStyle", "instant"); - Settings::getInstance()->setBool("MoveCarousel", false); - Settings::getInstance()->setBool("EnableSounds", false); - } - Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); - PowerSaver::init(); - }); - - // gamelists - auto save_gamelists = std::make_shared(mWindow); - save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); - s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); - s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); - - auto parse_gamelists = std::make_shared(mWindow); - parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); - s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); - s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); - /* - auto local_art = std::make_shared(mWindow); - local_art->setState(Settings::getInstance()->getBool("LocalArt")); - s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); - s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); - */ - // hidden files - auto hidden_files = std::make_shared(mWindow); - hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); - s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); - s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); - -#ifdef _RPI_ - // Video Player - VideoOmxPlayer - auto omx_player = std::make_shared(mWindow); - omx_player->setState(Settings::getInstance()->getBool("VideoOmxPlayer")); - s->addWithLabel("USE OMX PLAYER (HW ACCELERATED)", omx_player); - s->addSaveFunc([omx_player] - { - // need to reload all views to re-create the right video components - bool needReload = false; - if(Settings::getInstance()->getBool("VideoOmxPlayer") != omx_player->getState()) - needReload = true; - - Settings::getInstance()->setBool("VideoOmxPlayer", omx_player->getState()); - - if(needReload) - ViewController::get()->reloadAll(); - }); - -#endif - - // framerate - /* - auto framerate = std::make_shared(mWindow); - framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); - s->addWithLabel(_T("SHOW FRAMERATE"), framerate); - s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); - */ - - mWindow->pushGui(s); - -} - -void GuiMenu::openConfigInput() -{ - Window* window = mWindow; - window->pushGui(new GuiDetectDevice(window, false, nullptr)); - /* - window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), - [window] { - window->pushGui(new GuiDetectDevice(window, false, nullptr)); - }, _T("NO"), nullptr) - );*/ - -} - -void GuiMenu::openQuitMenu() -{ -#if defined(_WIN32) - Scripting::fireEvent("quit"); - quitES(""); - return; -#endif - - auto s = new GuiSettings(mWindow, _T("QUIT")); - - Window* window = mWindow; - - ComponentListRow row; - if (UIModeController::getInstance()->isUIModeFull()) - { - row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", - [] { - Scripting::fireEvent("quit"); - if(quitES("/tmp/es-restart") != 0) - LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", 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", - [] { - Scripting::fireEvent("quit"); - quitES(""); - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - s->addRow(row); - } - } - row.elements.clear(); - row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", - [] { - Scripting::fireEvent("quit", "reboot"); - Scripting::fireEvent("reboot"); - if (quitES("/tmp/es-sysrestart") != 0) - LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(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", - [] { - Scripting::fireEvent("quit", "shutdown"); - Scripting::fireEvent("shutdown"); - if (quitES("/tmp/es-shutdown") != 0) - LOG(LogWarning) << "Shutdown terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - s->addRow(row); - - mWindow->pushGui(s); -} - -void GuiMenu::addVersionInfo() -{ - std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); - - mVersion.setFont(Font::get(FONT_SIZE_SMALL)); - mVersion.setColor(0x5E5E5EFF); - mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); - mVersion.setHorizontalAlignment(ALIGN_CENTER); - addChild(&mVersion); -} - -void GuiMenu::openScreensaverOptions() { - mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); -} - -void GuiMenu::openCollectionSystemSettings() { - mWindow->pushGui(new GuiCollectionSystemsOptions(mWindow)); -} - -void GuiMenu::onSizeChanged() -{ - mVersion.setSize(mSize.x(), 0); - mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); -} - -void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) -{ - std::shared_ptr font = Font::get(FONT_SIZE_MEDIUM); - - // populate the list - ComponentListRow row; - row.addElement(std::make_shared(mWindow, name, font, color), true); - - if(add_arrow) - { - std::shared_ptr 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; -} - -HelpStyle GuiMenu::getHelpStyle() -{ - HelpStyle style = HelpStyle(); - style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); - return style; -} - -std::vector GuiMenu::getHelpPrompts() -{ - std::vector prompts; - prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); - prompts.push_back(HelpPrompt("a", _T("SELECT"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); - return prompts; -} + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + }); + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); +} + +void GuiMenu::openScraperSettings() +{ + auto s = new GuiSettings(mWindow, _T("SCRAPER")); + + // scrape from + auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, _T("SCRAPE FROM"), false); + std::vector scrapers = getScraperList(); + + // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. + for(auto it = scrapers.cbegin(); it != scrapers.cend(); it++) + scraper_list->add(*it, *it, *it == Settings::getInstance()->getString("Scraper")); + + s->addWithLabel(_T("SCRAPE FROM"), scraper_list); + s->addSaveFunc([scraper_list] { Settings::getInstance()->setString("Scraper", scraper_list->getSelected()); }); + + // scrape ratings + auto scrape_ratings = std::make_shared(mWindow); + scrape_ratings->setState(Settings::getInstance()->getBool("ScrapeRatings")); + s->addWithLabel(_T("SCRAPE RATINGS"), scrape_ratings); + s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); }); + + // scrape now + ComponentListRow row; + auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; + std::function openAndSave = openScrapeNow; + openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; + row.makeAcceptInputHandler(openAndSave); + + auto scrape_now = std::make_shared(mWindow, _T("SCRAPE NOW"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); + auto bracket = makeArrow(mWindow); + row.addElement(scrape_now, true); + row.addElement(bracket, false); + s->addRow(row); + + s->updatePosition(); + mWindow->pushGui(s); +} + +void GuiMenu::openSoundSettings() +{ + auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); + + // volume + auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); + volume->setValue((float)VolumeControl::getInstance()->getVolume()); + s->addWithLabel(_T("SYSTEM VOLUME"), volume); + s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); + + if (UIModeController::getInstance()->isUIModeFull()) + { +#if defined(__linux__) + // audio card + auto audio_card = std::make_shared< OptionListComponent >(mWindow, "AUDIO CARD", false); + std::vector audio_cards; + #ifdef _RPI_ + // RPi Specific Audio Cards + audio_cards.push_back("local"); + audio_cards.push_back("hdmi"); + audio_cards.push_back("both"); + #endif + audio_cards.push_back("default"); + audio_cards.push_back("sysdefault"); + audio_cards.push_back("dmix"); + audio_cards.push_back("hw"); + audio_cards.push_back("plughw"); + audio_cards.push_back("null"); + if (Settings::getInstance()->getString("AudioCard") != "") { + if(std::find(audio_cards.begin(), audio_cards.end(), Settings::getInstance()->getString("AudioCard")) == audio_cards.end()) { + audio_cards.push_back(Settings::getInstance()->getString("AudioCard")); + } + } + for(auto ac = audio_cards.cbegin(); ac != audio_cards.cend(); ac++) + audio_card->add(*ac, *ac, Settings::getInstance()->getString("AudioCard") == *ac); + s->addWithLabel("AUDIO CARD", audio_card); + s->addSaveFunc([audio_card] { + Settings::getInstance()->setString("AudioCard", audio_card->getSelected()); + VolumeControl::getInstance()->deinit(); + VolumeControl::getInstance()->init(); + }); + + // volume control device + auto vol_dev = std::make_shared< OptionListComponent >(mWindow, "AUDIO DEVICE", false); + std::vector transitions; + transitions.push_back("PCM"); + transitions.push_back("Speaker"); + transitions.push_back("Master"); + transitions.push_back("Digital"); + transitions.push_back("Analogue"); + if (Settings::getInstance()->getString("AudioDevice") != "") { + if(std::find(transitions.begin(), transitions.end(), Settings::getInstance()->getString("AudioDevice")) == transitions.end()) { + transitions.push_back(Settings::getInstance()->getString("AudioDevice")); + } + } + for(auto it = transitions.cbegin(); it != transitions.cend(); it++) + vol_dev->add(*it, *it, Settings::getInstance()->getString("AudioDevice") == *it); + s->addWithLabel("AUDIO DEVICE", vol_dev); + s->addSaveFunc([vol_dev] { + Settings::getInstance()->setString("AudioDevice", vol_dev->getSelected()); + VolumeControl::getInstance()->deinit(); + VolumeControl::getInstance()->init(); + }); +#endif + + // disable sounds + auto sounds_enabled = std::make_shared(mWindow); + sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); + s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); + s->addSaveFunc([sounds_enabled] { + if (sounds_enabled->getState() + && !Settings::getInstance()->getBool("EnableSounds") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState()); + }); + + auto video_audio = std::make_shared(mWindow); + video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); + s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); + s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); + +#ifdef _RPI_ + // OMX player Audio Device + auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, "OMX PLAYER AUDIO DEVICE", false); + std::vector omx_cards; + // RPi Specific Audio Cards + omx_cards.push_back("local"); + omx_cards.push_back("hdmi"); + omx_cards.push_back("both"); + omx_cards.push_back("alsa:hw:0,0"); + omx_cards.push_back("alsa:hw:1,0"); + if (Settings::getInstance()->getString("OMXAudioDev") != "") { + if (std::find(omx_cards.begin(), omx_cards.end(), Settings::getInstance()->getString("OMXAudioDev")) == omx_cards.end()) { + omx_cards.push_back(Settings::getInstance()->getString("OMXAudioDev")); + } + } + for (auto it = omx_cards.cbegin(); it != omx_cards.cend(); it++) + omx_audio_dev->add(*it, *it, Settings::getInstance()->getString("OMXAudioDev") == *it); + s->addWithLabel("OMX PLAYER AUDIO DEVICE", omx_audio_dev); + s->addSaveFunc([omx_audio_dev] { + if (Settings::getInstance()->getString("OMXAudioDev") != omx_audio_dev->getSelected()) + Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); + }); +#endif + } + + s->updatePosition(); + mWindow->pushGui(s); + +} + +void GuiMenu::openUISettings() +{ + auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); + + // theme set + auto theme = ThemeData::getMenuTheme(); + auto themeSets = ThemeData::getThemeSets(); + + if (!themeSets.empty()) + { + std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if (selectedSet == themeSets.cend()) + selectedSet = themeSets.cbegin(); + + auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); + for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) + theme_set->add(it->first, it->first, it == selectedSet); + s->addWithLabel(_T("THEME"), theme_set); + + Window* window = mWindow; + s->addSaveFunc([window, theme_set] + { + bool needReload = false; + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + if (oldTheme != theme_set->getSelected()) + needReload = true; + + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); + + if (needReload) + { + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + } + }); + } + + // LANGUAGE + + std::vector langues; + langues.push_back("en"); + + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); + if (xmlpath.length() > 0) + { + xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if (Utils::FileSystem::isDirectory(*it)) + continue; + + std::string name = *it; + + if (name.rfind("emulationstation2.po") == std::string::npos) + continue; + + name = Utils::FileSystem::getParent(name); + name = Utils::FileSystem::getFileName(name); + + if (name != "en") + langues.push_back(name); + } + + if (langues.size() > 1) + { + auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); + + for (auto it = langues.cbegin(); it != langues.cend(); it++) + language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); + + s->addWithLabel(_T("LANGUAGE"), language); + s->addSaveFunc([language] { + Settings::getInstance()->setString("Language", language->getSelected()); + }); + } + } + + // transition style + auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); + std::vector transitions; + transitions.push_back("fade"); + transitions.push_back("slide"); + transitions.push_back("instant"); + for (auto it = transitions.cbegin(); it != transitions.cend(); it++) + transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); + + s->addWithLabel(_T("TRANSITION STYLE"), transition_style); + s->addSaveFunc([transition_style] { + if (Settings::getInstance()->getString("TransitionStyle") == "instant" + && transition_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); + }); + + + auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); + std::vector gameTransitions; + gameTransitions.push_back("fade"); + gameTransitions.push_back("slide"); + gameTransitions.push_back("instant"); + for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) + transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); + + s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); + s->addSaveFunc([transitionOfGames_style] { + if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" + && transitionOfGames_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); + }); + + // GameList view style + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); + std::vector styles; + styles.push_back("automatic"); + styles.push_back("basic"); + styles.push_back("detailed"); + styles.push_back("video"); + styles.push_back("grid"); + styles.push_back("gridex"); + + for (auto it = styles.cbegin(); it != styles.cend(); it++) + gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); + + s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); + s->addSaveFunc([gamelist_style] { + bool needReload = false; + if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) + needReload = true; + Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); + if (needReload) + ViewController::get()->reloadAll(); + }); + + + // Optionally start in selected system + auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); + systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + { + if ("retropie" != (*it)->getName()) + { + systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); + } + } + s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); + s->addSaveFunc([systemfocus_list] { + Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); + }); + + + /* + //UI mode + auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); + std::vector UImodes = UIModeController::getInstance()->getUIModes(); + for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) + UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); + s->addWithLabel("UI MODE", UImodeSelection); + Window* window = mWindow; + s->addSaveFunc([ UImodeSelection, window] + { + std::string selectedMode = UImodeSelection->getSelected(); + if (selectedMode != "Full") + { + std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; + msg += "This will hide most menu-options to prevent changes to the system.\n"; + msg += "To unlock and return to the full UI, enter this code: \n"; + msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; + msg += "Do you want to proceed?"; + window->pushGui(new GuiMsgBox(window, msg, + "YES", [selectedMode] { + LOG(LogDebug) << "Setting UI mode to " << selectedMode; + Settings::getInstance()->setString("UIMode", selectedMode); + Settings::getInstance()->saveFile(); + }, "NO",nullptr)); + } + }); + */ + + // screensaver + ComponentListRow screensaver_row; + screensaver_row.elements.clear(); + screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); + screensaver_row.addElement(makeArrow(mWindow), false); + screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); + s->addRow(screensaver_row); + +#if defined(_WIN32) + // quick system select (left/right in game list view) + auto hideWindowScreen = std::make_shared(mWindow); + hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); + s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); + s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); +#endif + + // quick system select (left/right in game list view) + auto quick_sys_select = std::make_shared(mWindow); + quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); + s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); + s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); + + // carousel transition option + auto move_carousel = std::make_shared(mWindow); + move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); + s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); + s->addSaveFunc([move_carousel] { + if (move_carousel->getState() + && !Settings::getInstance()->getBool("MoveCarousel") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); + }); + + // show help + auto show_help = std::make_shared(mWindow); + show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); + s->addWithLabel(_T("ON-SCREEN HELP"), show_help); + s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); + + // enable filters (ForceDisableFilters) + auto enable_filter = std::make_shared(mWindow); + enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); + s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); + s->addSaveFunc([enable_filter] { + bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); + Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); + if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); + }); + + s->updatePosition(); + mWindow->pushGui(s); + +} + +void GuiMenu::openOtherSettings() +{ + auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); + + // maximum vram + auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); + max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); + s->addWithLabel(_T("VRAM LIMIT"), max_vram); + s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); + + // power saver + auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); + std::vector modes; + modes.push_back("disabled"); + modes.push_back("default"); + modes.push_back("enhanced"); + modes.push_back("instant"); + for (auto it = modes.cbegin(); it != modes.cend(); it++) + power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); + + s->addWithLabel(_T("POWER SAVER MODES"), power_saver); + s->addSaveFunc([this, power_saver] { + if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { + Settings::getInstance()->setString("TransitionStyle", "instant"); + Settings::getInstance()->setBool("MoveCarousel", false); + Settings::getInstance()->setBool("EnableSounds", false); + } + Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); + PowerSaver::init(); + }); + + // gamelists + auto save_gamelists = std::make_shared(mWindow); + save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); + s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); + s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); + + auto parse_gamelists = std::make_shared(mWindow); + parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); + s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); + s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); + /* + auto local_art = std::make_shared(mWindow); + local_art->setState(Settings::getInstance()->getBool("LocalArt")); + s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); + s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); + */ + // hidden files + auto hidden_files = std::make_shared(mWindow); + hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); + s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); + s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); + +#ifdef _RPI_ + // Video Player - VideoOmxPlayer + auto omx_player = std::make_shared(mWindow); + omx_player->setState(Settings::getInstance()->getBool("VideoOmxPlayer")); + s->addWithLabel("USE OMX PLAYER (HW ACCELERATED)", omx_player); + s->addSaveFunc([omx_player] + { + // need to reload all views to re-create the right video components + bool needReload = false; + if(Settings::getInstance()->getBool("VideoOmxPlayer") != omx_player->getState()) + needReload = true; + + Settings::getInstance()->setBool("VideoOmxPlayer", omx_player->getState()); + + if(needReload) + ViewController::get()->reloadAll(); + }); + +#endif + +#if defined(_WIN32) + // vsync + auto vsync = std::make_shared(mWindow); + vsync->setState(Settings::getInstance()->getBool("VSync")); + s->addWithLabel(_T("VSYNC"), vsync); + s->addSaveFunc([vsync] + { + Settings::getInstance()->setBool("VSync", vsync->getState()); + + // vsync + if (Settings::getInstance()->getBool("VSync")) + { + // SDL_GL_SetSwapInterval(0) for immediate updates (no vsync, default), + // 1 for updates synchronized with the vertical retrace, + // or -1 for late swap tearing. + // SDL_GL_SetSwapInterval returns 0 on success, -1 on error. + // if vsync is requested, try normal vsync; if that doesn't work, try late swap tearing + // if that doesn't work, report an error + if (SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) + LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; + } + else + SDL_GL_SetSwapInterval(0); + }); +#endif + + // framerate + + auto framerate = std::make_shared(mWindow); + framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); + s->addWithLabel(_T("SHOW FRAMERATE"), framerate); + s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); + + + s->updatePosition(); + mWindow->pushGui(s); + +} + +void GuiMenu::openConfigInput() +{ + Window* window = mWindow; + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + /* + window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), + [window] { + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + }, _T("NO"), nullptr) + );*/ + +} + +void GuiMenu::openQuitMenu() +{ +#if defined(_WIN32) + Scripting::fireEvent("quit"); + quitES(""); + return; +#endif + + auto s = new GuiSettings(mWindow, _T("QUIT")); + + Window* window = mWindow; + + ComponentListRow row; + if (UIModeController::getInstance()->isUIModeFull()) + { + row.makeAcceptInputHandler([window] { + window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + [] { + Scripting::fireEvent("quit"); + if(quitES("/tmp/es-restart") != 0) + LOG(LogWarning) << "Restart terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", 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", + [] { + Scripting::fireEvent("quit"); + quitES(""); + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + s->addRow(row); + } + } + row.elements.clear(); + row.makeAcceptInputHandler([window] { + window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + [] { + Scripting::fireEvent("quit", "reboot"); + Scripting::fireEvent("reboot"); + if (quitES("/tmp/es-sysrestart") != 0) + LOG(LogWarning) << "Restart terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(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", + [] { + Scripting::fireEvent("quit", "shutdown"); + Scripting::fireEvent("shutdown"); + if (quitES("/tmp/es-shutdown") != 0) + LOG(LogWarning) << "Shutdown terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + s->addRow(row); + + s->updatePosition(); + mWindow->pushGui(s); +} + +void GuiMenu::addVersionInfo() +{ + std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); + + auto theme = ThemeData::getMenuTheme(); +// mVersion.setFont(Font::get(FONT_SIZE_SMALL)); +// mVersion.setColor(0x5E5E5EFF); + + mVersion.setFont(theme->Footer.font); + mVersion.setColor(theme->Footer.color); + + mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); + mVersion.setHorizontalAlignment(ALIGN_CENTER); + addChild(&mVersion); +} + +void GuiMenu::openScreensaverOptions() { + mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); +} + +void GuiMenu::openCollectionSystemSettings() { + mWindow->pushGui(new GuiCollectionSystemsOptions(mWindow)); +} + +void GuiMenu::onSizeChanged() +{ + mVersion.setSize(mSize.x(), 0); + mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); +} + +void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) +{ + auto theme = ThemeData::getMenuTheme(); + std::shared_ptr font = theme->Text.font; + color = theme->Text.color; + + // populate the list + ComponentListRow row; + row.addElement(std::make_shared(mWindow, name, font, color), true); + + if (add_arrow) + { + std::shared_ptr 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; +} + +HelpStyle GuiMenu::getHelpStyle() +{ + HelpStyle style = HelpStyle(); + style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); + return style; +} + +std::vector GuiMenu::getHelpPrompts() +{ + std::vector prompts; + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("SELECT"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + return prompts; +} diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 31835bfa1b..ee9abc7a1f 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -1,382 +1,385 @@ -#include "guis/GuiMetaDataEd.h" - -#include "components/ButtonComponent.h" -#include "components/ComponentList.h" -#include "components/DateTimeEditComponent.h" -#include "components/MenuComponent.h" -#include "components/RatingComponent.h" -#include "components/SwitchComponent.h" -#include "components/TextComponent.h" -#include "guis/GuiGameScraper.h" -#include "guis/GuiMsgBox.h" -#include "guis/GuiTextEditPopup.h" -#include "resources/Font.h" -#include "utils/StringUtil.h" -#include "views/ViewController.h" -#include "CollectionSystemManager.h" -#include "FileData.h" -#include "FileFilterIndex.h" -#include "SystemData.h" -#include "Window.h" - -GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, - const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : 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(mWindow, Vector2i(1, 5)); - - mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); - mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), - 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(mWindow); - mGrid.setEntry(mList, Vector2i(0, 1), true, true); - - SystemData* system = file->getSystem(); - - auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); - auto core_choice = std::make_shared>(mWindow, _T("CORE"), false); - - // populate list - for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) - { - std::shared_ptr ed; - - // don't add statistics - if(iter->isStatistic) - continue; - - if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || - iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || - iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") - continue; - - // create ed and add it (and any related components) to mMenu - // ed's value will be set below - ComponentListRow row; - - if (iter->displayName == "emulator") - { - // if (file->getSystemEnvData()->mEmulators.size() <= 1) - // continue; - - std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); - - std::string currentEmul = file->getEmulator(); - - if (defaultEmul.length() == 0) - emul_choice->add(_T("DEFAULT"), "", false); - else - emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); - - for (auto core : file->getSystemEnvData()->mEmulators) - emul_choice->add(core.mName, core.mName, core.mName == currentEmul); - - row.addElement(std::make_shared(mWindow, _T("EMULATOR"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(emul_choice, false); - - mList->addRow(row); - emul_choice->setTag(iter->key); - mEditors.push_back(emul_choice); - - emul_choice->setSelectedChangedCallback([this, system, core_choice, file](std::string emulatorName) - { - std::string currentCore = file->getCore(); - - std::string defaultCore = system->getSystemEnvData()->getDefaultCore(emulatorName); - if (emulatorName.length() == 0) - defaultCore = system->getSystemEnvData()->getDefaultCore(system->getSystemEnvData()->getDefaultEmulator()); - - core_choice->clear(); - if (defaultCore.length() == 0) - core_choice->add(_T("DEFAULT"), "", false); - else - core_choice->add(_T("DEFAULT")+" ("+ defaultCore+")", "", false); - - std::vector cores = system->getSystemEnvData()->getCores(emulatorName); - - bool found = false; - - for (auto it = cores.begin(); it != cores.end(); it++) - { - std::string core = *it; - core_choice->add(core, core, currentCore == core); - if (currentCore == core) - found = true; - } - - if (!found) - core_choice->selectFirstItem(); - else - core_choice->invalidate(); - }); - - continue; - } - - if (iter->displayName == "core") - { - // core_choice->add(_T("DEFAULT"), "", true); - core_choice->setTag(iter->key); - - row.addElement(std::make_shared(mWindow, "CORE", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(core_choice, false); - - mList->addRow(row); - ed = core_choice; - - mEditors.push_back(core_choice); - - // force change event to load core list - emul_choice->invalidate(); - continue; - } - - auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); - row.addElement(lbl, true); // label - - switch (iter->type) - { - case MD_BOOL: - { - ed = std::make_shared(window); - row.addElement(ed, false, true); - break; - } - case MD_RATING: - { - ed = std::make_shared(window); - const float height = lbl->getSize().y() * 0.71f; - ed->setSize(0, height); - row.addElement(ed, false, true); - - auto spacer = std::make_shared(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(window); - row.addElement(ed, false); - - auto spacer = std::make_shared(mWindow); - spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0); - row.addElement(spacer, false); - - // pass input to the actual DateTimeEditComponent 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(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); - row.addElement(ed, false); - break; - } - case MD_MULTILINE_STRING: - default: - { - // MD_STRING - ed = std::make_shared(window, "", Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT), 0x777777FF, ALIGN_RIGHT); - row.addElement(ed, true); - - auto spacer = std::make_shared(mWindow); - spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); - row.addElement(spacer, false); - - auto bracket = std::make_shared(mWindow); - bracket->setImage(":/arrow.svg"); - bracket->setResize(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->setTag(iter->key); - ed->setValue(mMetaData->get(iter->key)); - - mEditors.push_back(ed); - } - - std::vector< std::shared_ptr > buttons; - - if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) - buttons.push_back(std::make_shared(mWindow, _T("SCRAPE"), _T("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); - - buttons.push_back(std::make_shared(mWindow, _T("SAVE"), _T("SAVE"), [&] { save(); delete this; })); - buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); - - if(mDeleteFunc) - { - auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; - auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; - buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE"), deleteBtnFunc)); - } - - mButtons = makeButtonGrid(mWindow, buttons); - mGrid.setEntry(mButtons, Vector2i(0, 2), true, false); - - // resize + center - float width = (float)Math::min(Renderer::getScreenHeight(), (int)(Renderer::getScreenWidth() * 0.90f)); - setSize(width, 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() -{ - // remove game from index - mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); - - for (unsigned int i = 0; i < mEditors.size(); i++) - { - std::shared_ptr ed = mEditors.at(i); - - auto val = ed->getValue(); - auto key = ed->getTag(); - - if (key == "core" || key == "emulator") - { - std::shared_ptr> list = std::static_pointer_cast>(ed); - val = list->getSelected(); - } - - mMetaData->set(key, val); - } - - // enter game in index - mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); - - if (mSavedCallback) - mSavedCallback(); - - // update respective Collection Entries - CollectionSystemManager::get()->refreshCollectionSystems(mScraperParams.game); -} - -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++) - { - auto val = mEditors.at(i)->getValue(); - auto key = mEditors.at(i)->getTag(); - 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++) - { - auto key = mEditors.at(i)->getTag(); - if(mMetaData->get(key) != mEditors.at(i)->getValue()) - { - dirty = true; - break; - } - } - - std::function 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, - "ENREGISTRER LES MODIFICATIONS ?", - "OUI", [this, closeFunc] { save(); closeFunc(); }, - "NON", 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 GuiMetaDataEd::getHelpPrompts() -{ - std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); - return prompts; -} +#include "guis/GuiMetaDataEd.h" + +#include "components/ButtonComponent.h" +#include "components/ComponentList.h" +#include "components/DateTimeEditComponent.h" +#include "components/MenuComponent.h" +#include "components/RatingComponent.h" +#include "components/SwitchComponent.h" +#include "components/TextComponent.h" +#include "guis/GuiGameScraper.h" +#include "guis/GuiMsgBox.h" +#include "guis/GuiTextEditPopup.h" +#include "resources/Font.h" +#include "utils/StringUtil.h" +#include "views/ViewController.h" +#include "CollectionSystemManager.h" +#include "FileData.h" +#include "FileFilterIndex.h" +#include "SystemData.h" +#include "Window.h" + +GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, + const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : GuiComponent(window), + mScraperParams(scraperParams), + + mBackground(window, ":/frame.png"), + mGrid(window, Vector2i(1, 3)), + + mMetaDataDecl(mdd), + mMetaData(md), + mSavedCallback(saveCallback), mDeleteFunc(deleteFunc) +{ + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + + addChild(&mBackground); + addChild(&mGrid); + + mHeaderGrid = std::make_shared(mWindow, Vector2i(1, 5)); + + mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), theme->Title.font, theme->Title.color, ALIGN_CENTER); + mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), + theme->TextSmall.font, theme->TextSmall.color, 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(mWindow); + mGrid.setEntry(mList, Vector2i(0, 1), true, true); + + SystemData* system = file->getSystem(); + + auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); + auto core_choice = std::make_shared>(mWindow, _T("CORE"), false); + + // populate list + for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + { + std::shared_ptr ed; + + // don't add statistics + if(iter->isStatistic) + continue; + +#if defined(_WIN32) + if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || + iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || + iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") + continue; +#endif + + // create ed and add it (and any related components) to mMenu + // ed's value will be set below + ComponentListRow row; + + if (iter->displayName == "emulator") + { + std::string defaultEmul = system->getSystemEnvData()->getDefaultEmulator(); + std::string currentEmul = file->getEmulator(); + + if (defaultEmul.length() == 0) + emul_choice->add(_T("DEFAULT"), "", true); + else + emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); + + for (auto core : file->getSystemEnvData()->mEmulators) + emul_choice->add(core.mName, core.mName, core.mName == currentEmul); + + row.addElement(std::make_shared(mWindow, _T("EMULATOR"), theme->Text.font, theme->Text.color), true); + row.addElement(emul_choice, false); + + mList->addRow(row); + emul_choice->setTag(iter->key); + mEditors.push_back(emul_choice); + + emul_choice->setSelectedChangedCallback([this, system, core_choice, file](std::string emulatorName) + { + std::string currentCore = file->getCore(); + + std::string defaultCore = system->getSystemEnvData()->getDefaultCore(emulatorName); + if (emulatorName.length() == 0) + defaultCore = system->getSystemEnvData()->getDefaultCore(system->getSystemEnvData()->getDefaultEmulator()); + + core_choice->clear(); + if (defaultCore.length() == 0) + core_choice->add(_T("DEFAULT"), "", false); + else + core_choice->add(_T("DEFAULT")+" ("+ defaultCore+")", "", false); + + std::vector cores = system->getSystemEnvData()->getCores(emulatorName); + + bool found = false; + + for (auto it = cores.begin(); it != cores.end(); it++) + { + std::string core = *it; + core_choice->add(core, core, currentCore == core); + if (currentCore == core) + found = true; + } + + if (!found) + core_choice->selectFirstItem(); + else + core_choice->invalidate(); + }); + + continue; + } + + if (iter->displayName == "core") + { + // core_choice->add(_T("DEFAULT"), "", true); + core_choice->setTag(iter->key); + + row.addElement(std::make_shared(mWindow, "CORE", theme->Text.font, theme->Text.color), true); + row.addElement(core_choice, false); + + mList->addRow(row); + ed = core_choice; + + mEditors.push_back(core_choice); + + // force change event to load core list + emul_choice->invalidate(); + continue; + } + + auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), theme->Text.font, theme->Text.color); + row.addElement(lbl, true); // label + + switch (iter->type) + { + case MD_BOOL: + { + ed = std::make_shared(window); + row.addElement(ed, false, true); + break; + } + case MD_RATING: + { + ed = std::make_shared(window); + const float height = lbl->getSize().y() * 0.71f; + ed->setSize(0, height); + row.addElement(ed, false, true); + + auto spacer = std::make_shared(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(window); + row.addElement(ed, false); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.0025f, 0); + row.addElement(spacer, false); + + // pass input to the actual DateTimeEditComponent 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(window, DateTimeEditComponent::DISP_RELATIVE_TO_NOW); + row.addElement(ed, false); + break; + } + case MD_MULTILINE_STRING: + default: + { + // MD_STRING + ed = std::make_shared(window, "", theme->Text.font, theme->Text.color, ALIGN_RIGHT); + row.addElement(ed, true); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); + row.addElement(spacer, false); + + auto bracket = std::make_shared(mWindow); + bracket->setImage(":/arrow.svg"); + bracket->setResize(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->setTag(iter->key); + ed->setValue(mMetaData->get(iter->key)); + + mEditors.push_back(ed); + } + + std::vector< std::shared_ptr > buttons; + + if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) + buttons.push_back(std::make_shared(mWindow, _T("SCRAPE"), _T("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); + + buttons.push_back(std::make_shared(mWindow, _T("SAVE"), _T("SAVE"), [&] { save(); delete this; })); + buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); + + if(mDeleteFunc) + { + auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; + auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; + buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE"), deleteBtnFunc)); + } + + mButtons = makeButtonGrid(mWindow, buttons); + mGrid.setEntry(mButtons, Vector2i(0, 2), true, false); + + // resize + center + float width = (float)Math::min(Renderer::getScreenHeight(), (int)(Renderer::getScreenWidth() * 0.90f)); + setSize(width, 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() +{ + // remove game from index + mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); + + for (unsigned int i = 0; i < mEditors.size(); i++) + { + std::shared_ptr ed = mEditors.at(i); + + auto val = ed->getValue(); + auto key = ed->getTag(); + + if (key == "core" || key == "emulator") + { + std::shared_ptr> list = std::static_pointer_cast>(ed); + val = list->getSelected(); + } + + mMetaData->set(key, val); + } + + // enter game in index + mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); + + if (mSavedCallback) + mSavedCallback(); + + // update respective Collection Entries + CollectionSystemManager::get()->refreshCollectionSystems(mScraperParams.game); +} + +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++) + { + auto val = mEditors.at(i)->getValue(); + auto key = mEditors.at(i)->getTag(); + 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++) + { + auto key = mEditors.at(i)->getTag(); + if(mMetaData->get(key) != mEditors.at(i)->getValue()) + { + dirty = true; + break; + } + } + + std::function 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, + "ENREGISTRER LES MODIFICATIONS ?", + "OUI", [this, closeFunc] { save(); closeFunc(); }, + "NON", 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 GuiMetaDataEd::getHelpPrompts() +{ + std::vector prompts = mGrid.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + return prompts; +} diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 7dcd2d16bd..210cac7ae6 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -15,6 +15,11 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queueBackground.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + assert(mSearchQueue.size()); addChild(&mBackground); diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index 2330f095f6..6762fea19a 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -9,32 +9,32 @@ #include "SystemData.h" GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), - mMenu(window, "SCRAPE NOW") + mMenu(window, _T("SCRAPE NOW")) { addChild(&mMenu); // add filters (with first one selected) - mFilters = std::make_shared< OptionListComponent >(mWindow, "SCRAPE THESE GAMES", false); - mFilters->add("All Games", + mFilters = std::make_shared< OptionListComponent >(mWindow, _T("SCRAPE THESE GAMES"), false); + mFilters->add(_T("All Games"), [](SystemData*, FileData*) -> bool { return true; }, false); - mFilters->add("Only missing image", + mFilters->add(_T("Only missing image"), [](SystemData*, FileData* g) -> bool { return g->metadata.get("image").empty(); }, true); - mMenu.addWithLabel("Filter", mFilters); + mMenu.addWithLabel(_T("FILTER"), mFilters); //add systems (all with a platformid specified selected) - mSystems = std::make_shared< OptionListComponent >(mWindow, "SCRAPE THESE SYSTEMS", true); + mSystems = std::make_shared< OptionListComponent >(mWindow, _T("SCRAPE THESE SYSTEMS"), true); for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { if(!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) mSystems->add((*it)->getFullName(), *it, !(*it)->getPlatformIds().empty()); } - mMenu.addWithLabel("Systems", mSystems); + mMenu.addWithLabel(_T("SYSTEMS"), mSystems); mApproveResults = std::make_shared(mWindow); mApproveResults->setState(true); - mMenu.addWithLabel("User decides on conflicts", mApproveResults); + mMenu.addWithLabel(_T("USER DECIDES ON CONFLICTS"), mApproveResults); - mMenu.addButton("START", "LANCER", std::bind(&GuiScraperStart::pressedStart, this)); + mMenu.addButton(_T("START"), _T("START"), std::bind(&GuiScraperStart::pressedStart, this)); mMenu.addButton(_T("BACK"), _T("BACK"), [&] { delete this; }); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); @@ -49,8 +49,8 @@ void GuiScraperStart::pressedStart() { mWindow->pushGui(new GuiMsgBox(mWindow, Utils::String::toUpper("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)); + _T("YES"), std::bind(&GuiScraperStart::start, this), + _T("NO"), nullptr)); return; } } @@ -64,9 +64,10 @@ void GuiScraperStart::start() if(searches.empty()) { - mWindow->pushGui(new GuiMsgBox(mWindow, - "NO GAMES FIT THAT CRITERIA.")); - }else{ + mWindow->pushGui(new GuiMsgBox(mWindow, _T("NO GAMES FIT THAT CRITERIA."))); + } + else + { GuiScraperMulti* gsm = new GuiScraperMulti(mWindow, searches, mApproveResults->getState()); mWindow->pushGui(gsm); delete this; diff --git a/es-app/src/guis/GuiSettings.cpp b/es-app/src/guis/GuiSettings.cpp index f0d3dd697a..0ffc6f65af 100644 --- a/es-app/src/guis/GuiSettings.cpp +++ b/es-app/src/guis/GuiSettings.cpp @@ -8,11 +8,8 @@ GuiSettings::GuiSettings(Window* window, std::string title) : GuiComponent(window), mMenu(window, title) { addChild(&mMenu); - mMenu.addButton(_T("BACK"), _T("BACK"), [this] { delete this; }); - - setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); + updatePosition(); } GuiSettings::~GuiSettings() @@ -20,6 +17,12 @@ GuiSettings::~GuiSettings() save(); } +void GuiSettings::updatePosition() +{ + setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); +} + void GuiSettings::save() { if(!mSaveFuncs.size()) diff --git a/es-app/src/guis/GuiSettings.h b/es-app/src/guis/GuiSettings.h index 05ece518f2..ac9a22d084 100644 --- a/es-app/src/guis/GuiSettings.h +++ b/es-app/src/guis/GuiSettings.h @@ -11,6 +11,8 @@ class GuiSettings : public GuiComponent GuiSettings(Window* window, std::string title); virtual ~GuiSettings(); // just calls save(); + + void updatePosition(); void save(); inline void addRow(const ComponentListRow& row) { mMenu.addRow(row); }; inline void addWithLabel(const std::string& label, const std::shared_ptr& comp) { mMenu.addWithLabel(label, comp); }; diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 2a08c2c583..2f42e183f5 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -133,9 +133,23 @@ bool parseArgs(int argc, char* argv[]) } else if (strcmp(argv[i], "--vsync") == 0 || strcmp(argv[i], "-vsync") == 0) { - bool vsync = strlen(argv[i]) <= 7 ? true : (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "1") == 0) ? true : false; + bool vsync = false; + if (i == argc - 1) + vsync = true; + else + { + std::string arg = argv[i + 1]; + if (arg.find("-") == 0) + vsync = true; + else + { + vsync = (strcmp(argv[i + 1], "on") == 0 || strcmp(argv[i + 1], "1") == 0) ? true : false; + i++; // skip vsync value + } + } + Settings::getInstance()->setBool("VSync", vsync); - i++; // skip vsync value + } else if (strcmp(argv[i], "--scrape") == 0) { @@ -390,6 +404,22 @@ int main(int argc, char* argv[]) window.endRenderLoadingScreen(); +#ifdef WIN32 + DWORD displayFrequency = 60; + + DEVMODE lpDevMode; + memset(&lpDevMode, 0, sizeof(DEVMODE)); + lpDevMode.dmSize = sizeof(DEVMODE); + lpDevMode.dmFields = DM_BITSPERPEL | DM_PELSWIDTH | DM_PELSHEIGHT | DM_DISPLAYFLAGS | DM_DISPLAYFREQUENCY; + lpDevMode.dmDriverExtra = 0; + + if (EnumDisplaySettings(NULL, ENUM_CURRENT_SETTINGS, &lpDevMode) != 0) { + displayFrequency = lpDevMode.dmDisplayFrequency; // default value if cannot retrieve from user settings. + } + + int timeLimit = (1000 / displayFrequency) - 5; // Margin for vsync +#endif + int lastTime = SDL_GetTicks(); int ps_time = SDL_GetTicks(); @@ -397,10 +427,12 @@ int main(int argc, char* argv[]) while(running) { + int processStart = SDL_GetTicks(); + SDL_Event event; bool ps_standby = PowerSaver::getState() && (int) SDL_GetTicks() - ps_time > PowerSaver::getMode(); - if(ps_standby ? SDL_WaitEventTimeout(&event, PowerSaver::getTimeout()) : SDL_PollEvent(&event)) + if (ps_standby ? SDL_WaitEventTimeout(&event, PowerSaver::getTimeout()) : SDL_PollEvent(&event)) { do { @@ -423,13 +455,13 @@ int main(int argc, char* argv[]) { // If exitting SDL_WaitEventTimeout due to timeout. Trail considering // timeout as an event - ps_time = SDL_GetTicks(); + ps_time = SDL_GetTicks(); } - if(window.isSleeping()) + 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 + SDL_Delay(10); // this doesn't need to be accurate, we're just giving up our CPU time until something wakes us up continue; } @@ -438,14 +470,33 @@ int main(int argc, char* argv[]) lastTime = curTime; // cap deltaTime if it ever goes negative - if(deltaTime < 0) + if (deltaTime < 0) deltaTime = 1000; window.update(deltaTime); window.render(); - Renderer::swapBuffers(); - + Log::flush(); + + int processDuration = SDL_GetTicks() - processStart; + +#ifdef WIN32 + if (processDuration < timeLimit) + Sleep(timeLimit - processDuration); + + int swapStart = SDL_GetTicks(); +#endif + + Renderer::swapBuffers(); +/* +#ifdef WIN32 + int swapDuration = SDL_GetTicks() - swapStart; + + char buffer[100]; + sprintf_s(buffer, "px=%d swap=%d, sleep=%d\n", processDuration, swapDuration, timeLimit - processDuration); + OutputDebugStringA(buffer); +#endif +*/ } while(window.peekGui() != ViewController::get()) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 41bc9fddfe..805b4f8cd2 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -234,7 +234,6 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) 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); @@ -270,8 +269,12 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) mSystemInfo.setOpacity((unsigned char)(Math::lerp(0.f, 1.f, t) * 255)); }, goFast ? 10 : 300); +// ViewController::get()->getGameListView(mEntries.at(mCursor).object); // fake preload + // wait 600ms to fade in - setAnimation(infoFadeIn, goFast ? 0 : 500, nullptr, false, 2); + setAnimation(infoFadeIn, goFast ? 0 : 500, [this] { + ViewController::get()->getGameListView(mEntries.at(mCursor).object); // fake preload + }, false, 2); // no need to animate transition, we're not going anywhere (probably mEntries.size() == 1) if(endPos == mCamOffset && endPos == mExtrasCamOffset) @@ -349,6 +352,10 @@ void SystemView::render(const Transform4x4f& parentTrans) Transform4x4f trans = getTransform() * parentTrans; + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + auto systemInfoZIndex = mSystemInfo.getZIndex(); auto minMax = std::minmax(mCarousel.zIndex, systemInfoZIndex); @@ -488,6 +495,12 @@ void SystemView::renderCarousel(const Transform4x4f& trans) break; } + if (mCarousel.logoPos.x() >= 0) + xOff = mCarousel.logoPos.x() - (mCarousel.type == HORIZONTAL ? (mCamOffset * logoSpacing[0]) : 0); + + if (mCarousel.logoPos.y() >= 0) + yOff = mCarousel.logoPos.y() - (mCarousel.type == VERTICAL ? (mCamOffset * logoSpacing[1]) : 0); + int center = (int)(mCamOffset); int logoCount = Math::min(mCarousel.maxLogoCount, (int)mEntries.size()); @@ -495,7 +508,7 @@ void SystemView::renderCarousel(const Transform4x4f& trans) int bufferIndex = getScrollingVelocity() + 1; int bufferLeft = logoBuffersLeft[bufferIndex]; int bufferRight = logoBuffersRight[bufferIndex]; - if (logoCount == 1) + if (logoCount == 1 && mCamOffset == 0) { bufferLeft = 0; bufferRight = 0; @@ -610,6 +623,7 @@ void SystemView::getDefaultElements(void) mCarousel.logoRotationOrigin.y() = 0.5; mCarousel.logoSize.x() = 0.25f * mSize.x(); mCarousel.logoSize.y() = 0.155f * mSize.y(); + mCarousel.logoPos = Vector2f(-1, -1); mCarousel.maxLogoCount = 3; mCarousel.zIndex = 40; @@ -649,6 +663,8 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) mCarousel.logoScale = elem->get("logoScale"); if (elem->has("logoSize")) mCarousel.logoSize = elem->get("logoSize") * mSize; + if (elem->has("logoPos")) + mCarousel.logoPos = elem->get("logoPos") * mSize; if (elem->has("maxLogoCount")) mCarousel.maxLogoCount = (int)Math::round(elem->get("maxLogoCount")); if (elem->has("zIndex")) diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index db1205424a..4de89cc918 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -38,6 +38,7 @@ struct SystemViewCarousel unsigned int color; int maxLogoCount; // number of logos shown on the carousel Vector2f logoSize; + Vector2f logoPos; float zIndex; }; diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 37c61dbbab..15fd7eb10a 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -134,14 +134,12 @@ void ViewController::goToGameList(SystemData* system) mState.system = system; if (mCurrentView) - { mCurrentView->onHide(); - } + mCurrentView = getGameListView(system); if (mCurrentView) - { - mCurrentView->onShow(); - } + mCurrentView->onShow(); + playViewTransition(false); } @@ -156,7 +154,7 @@ void ViewController::playViewTransition(bool forceImmediate) return; std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); - if(!forceImmediate && transition_style == "fade") + if (!forceImmediate && transition_style == "fade") { // fade // stop whatever's currently playing, leaving mFadeOpacity wherever it is @@ -175,20 +173,24 @@ void ViewController::playViewTransition(bool forceImmediate) }); // fast-forward animation if we're partway faded - if(target == -mCamera.translation()) + 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 if (!forceImmediate && transition_style == "slide"){ + else + advanceAnimation(0, (int)(mFadeOpacity * FADE_DURATION)); + } + else if (!forceImmediate && transition_style == "slide") + { // slide or simple slide setAnimation(new MoveCameraAnimation(mCamera, target)); updateHelpPrompts(); // update help prompts immediately - } else { + } + else + { // instant setAnimation(new LambdaAnimation( [this, target](float /*t*/) @@ -450,9 +452,15 @@ void ViewController::render(const Transform4x4f& parentTrans) // Keep track of UI mode changes. UIModeController::getInstance()->monitorUIMode(); - // draw systemview - getSystemListView()->render(trans); + // clipping + Vector3f sysStart = getSystemListView()->getPosition(); + Vector3f sysEnd = getSystemListView()->getPosition() + Vector3f(getSystemListView()->getSize().x(), getSystemListView()->getSize().y(), 0); + // draw systemview +// if ((sysStart.x() == viewStart.x() && sysStart.y() < viewStart.y() && sysEnd.x() > viewEnd.x() && sysEnd.y() > viewEnd.y()) || +// (sysStart.x() < viewEnd.x() && sysStart.y() < viewEnd.y() && sysEnd.x() > viewStart.x() && sysEnd.y() > viewStart.y())) + getSystemListView()->render(trans); + // draw gamelists for(auto it = mGameListViews.cbegin(); it != mGameListViews.cend(); it++) { @@ -460,8 +468,8 @@ void ViewController::render(const Transform4x4f& parentTrans) Vector3f guiStart = it->second->getPosition(); Vector3f guiEnd = it->second->getPosition() + 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()) +// if ((guiStart.x() == viewStart.x() && guiStart.y() < viewStart.y() && guiEnd.x() > viewEnd.x() && guiEnd.y() > viewEnd.y()) || +// (guiStart.x() < viewEnd.x() && guiStart.y() < viewEnd.y() && guiEnd.x() > viewStart.x() && guiEnd.y() > viewStart.y())) it->second->render(trans); } @@ -486,14 +494,10 @@ void ViewController::preload() uint32_t i = 0; for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { - if(Settings::getInstance()->getBool("SplashScreen") && - Settings::getInstance()->getBool("SplashScreenProgress")) + if (Settings::getInstance()->getBool("SplashScreen") && Settings::getInstance()->getBool("SplashScreenProgress")) { i++; - char buffer[100]; - sprintf (buffer, "Chargement de '%s' (%d/%d)", - (*it)->getFullName().c_str(), i, (int)SystemData::sSystemVector.size()); - mWindow->renderLoadingScreen(std::string(buffer)); + mWindow->renderLoadingScreen(_T("Preloading UI"), (float) i / (float) SystemData::sSystemVector.size()); } (*it)->getIndex()->resetFilters(); diff --git a/es-app/src/views/gamelist/IGameListView.cpp b/es-app/src/views/gamelist/IGameListView.cpp index 5fd5dbf076..722eeabcdd 100644 --- a/es-app/src/views/gamelist/IGameListView.cpp +++ b/es-app/src/views/gamelist/IGameListView.cpp @@ -44,6 +44,10 @@ void IGameListView::render(const Transform4x4f& parentTrans) { Transform4x4f trans = parentTrans * getTransform(); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + float scaleX = trans.r0().x(); float scaleY = trans.r1().y(); diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index d850727610..31774a179e 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -1,222 +1,224 @@ -#pragma once -#ifndef ES_CORE_GUI_COMPONENT_H -#define ES_CORE_GUI_COMPONENT_H - -#include "math/Misc.h" -#include "math/Transform4x4f.h" -#include "HelpPrompt.h" -#include "HelpStyle.h" -#include "InputConfig.h" -#include -#include - -#if defined(_WIN32) -#include -#endif - -class Animation; -class AnimationController; -class Font; -class InputConfig; -class ThemeData; -class Window; - -class GuiComponent -{ -public: - GuiComponent(Window* window); - virtual ~GuiComponent(); - - virtual void textInput(const char* text); - - //Called when input is received. - //Return true if the input is consumed, false if it should continue to be passed to other children. - virtual bool input(InputConfig* config, Input input); - - //Called when time passes. Default implementation calls updateSelf(deltaTime) and updateChildren(deltaTime) - so you should probably call GuiComponent::update(deltaTime) at some point (or at least updateSelf so animations work). - virtual void update(int deltaTime); - - //Called when it's time to render. By default, just calls renderChildren(parentTrans * getTransform()). - //You probably want to override this like so: - //1. Calculate the new transform that your control will draw at with Transform4x4f t = parentTrans * getTransform(). - //2. Set the renderer to use that new transform as the model matrix - Renderer::setMatrix(t); - //3. Draw your component. - //4. Tell your children to render, based on your component's transform - renderChildren(t). - virtual void render(const Transform4x4f& parentTrans); - - Vector3f getPosition() const; - inline void setPosition(const Vector3f& offset) { setPosition(offset.x(), offset.y(), offset.z()); } - void setPosition(float x, float y, float z = 0.0f); - virtual void onPositionChanged() {}; - - //Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) - Vector2f getOrigin() const; - void setOrigin(float originX, float originY); - inline void setOrigin(Vector2f origin) { setOrigin(origin.x(), origin.y()); } - virtual void onOriginChanged() {}; - - //Sets the rotation origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) - Vector2f getRotationOrigin() const; - void setRotationOrigin(float originX, float originY); - inline void setRotationOrigin(Vector2f origin) { setRotationOrigin(origin.x(), origin.y()); } - - virtual Vector2f getSize() const; - inline void setSize(const Vector2f& size) { setSize(size.x(), size.y()); } - void setSize(float w, float h); - virtual void onSizeChanged() {}; - - virtual Vector2f getRotationSize() const { return getSize(); }; - - float getRotation() const; - void setRotation(float rotation); - inline void setRotationDegrees(float rotation) { setRotation((float)ES_DEG_TO_RAD(rotation)); } - - float getScale() const; - void setScale(float scale); - - float getZIndex() const; - void setZIndex(float zIndex); - - float getDefaultZIndex() const; - void setDefaultZIndex(float zIndex); - - // Returns the center point of the image (takes origin into account). - Vector2f getCenter() const; - - void setParent(GuiComponent* parent); - GuiComponent* getParent() const; - - void addChild(GuiComponent* cmp); - void removeChild(GuiComponent* cmp); - void clearChildren(); - void sortChildren(); - unsigned int getChildCount() const; - GuiComponent* getChild(unsigned int i) const; - - // animation will be automatically deleted when it completes or is stopped. - bool isAnimationPlaying(unsigned char slot) const; - bool isAnimationReversed(unsigned char slot) const; - int getAnimationTime(unsigned char slot) const; - void setAnimation(Animation* animation, int delay = 0, std::function finishedCallback = nullptr, bool reverse = false, unsigned char slot = 0); - bool stopAnimation(unsigned char slot); - bool cancelAnimation(unsigned char slot); // Like stopAnimation, but doesn't call finishedCallback - only removes the animation, leaving things in their current state. Returns true if successful (an animation was in this slot). - bool finishAnimation(unsigned char slot); // Calls update(1.f) and finishedCallback, then deletes the animation - basically skips to the end. Returns true if successful (an animation was in this slot). - bool advanceAnimation(unsigned char slot, unsigned int time); // Returns true if successful (an animation was in this slot). - void stopAllAnimations(); - void cancelAllAnimations(); - - virtual unsigned char getOpacity() const; - virtual void setOpacity(unsigned char opacity); - - const Transform4x4f& getTransform(); - - virtual std::string getValue() const; - virtual void setValue(const std::string& value); - - - virtual std::string getTag() const; - virtual void setTag(const std::string& value); - - virtual void onFocusGained() {}; - virtual void onFocusLost() {}; - - virtual void onShow(); - virtual void onHide(); - - virtual void onScreenSaverActivate(); - virtual void onScreenSaverDeactivate(); - virtual void topWindow(bool isTop); - - // Default implementation just handles and tags as normalized float pairs. - // You probably want to keep this behavior for any derived classes as well as add your own. - virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); - - // Returns a list of help prompts. - virtual std::vector getHelpPrompts() { return std::vector(); }; - - // Called whenever help prompts change. - void updateHelpPrompts(); - - virtual HelpStyle getHelpStyle(); - - // Returns true if the component is busy doing background processing (e.g. HTTP downloads) - bool isProcessing() const; - -protected: - void renderChildren(const Transform4x4f& transform) const; - void updateSelf(int deltaTime); // updates animations - void updateChildren(int deltaTime); // updates animations - - unsigned char mOpacity; - Window* mWindow; - - GuiComponent* mParent; - std::vector mChildren; - - std::string mTag; - - Vector3f mPosition; - Vector2f mOrigin; - Vector2f mRotationOrigin; - Vector2f mSize; - - float mRotation = 0.0; - float mScale = 1.0; - - float mDefaultZIndex = 0; - float mZIndex = 0; - - bool mIsProcessing; - -public: - const static unsigned char MAX_ANIMATIONS = 4; - -private: - Transform4x4f mTransform; //Don't access this directly! Use getTransform()! - AnimationController* mAnimationMap[MAX_ANIMATIONS]; -}; - - - -class LocalizationItem -{ -public: - std::string msgid; - std::string msgstr; -}; - -class GuiTextTool -{ -public: -#if defined(_WIN32) - static const std::string convertFromWideString(const std::wstring wstring) - { - int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); - - std::string string; - string.resize(numBytes); - WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); - - return string; - } -#endif - static void ensureLocalisation(); - static const std::string localize(const std::string text); - - static void setLanguage(std::string lang); - -private: - static std::vector mItems; - static std::string mCurrentLanguage; - static bool mCurrentLanguageLoaded; -}; - -#if defined(_WIN32) -#define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) -#define _L(x) GuiTextTool::localize(x) -#else -#define _T(x) GuiTextTool::localize(x) -#define _L(x) GuiTextTool::localize(x) -#endif // _WIN32 - -#endif // ES_CORE_GUI_COMPONENT_H +#pragma once +#ifndef ES_CORE_GUI_COMPONENT_H +#define ES_CORE_GUI_COMPONENT_H + +#include "math/Misc.h" +#include "math/Transform4x4f.h" +#include "HelpPrompt.h" +#include "HelpStyle.h" +#include "InputConfig.h" +#include +#include + +#if defined(_WIN32) +#include +#endif + +class Animation; +class AnimationController; +class Font; +class InputConfig; +class ThemeData; +class Window; + +class GuiComponent +{ +public: + GuiComponent(Window* window); + virtual ~GuiComponent(); + + virtual void textInput(const char* text); + + //Called when input is received. + //Return true if the input is consumed, false if it should continue to be passed to other children. + virtual bool input(InputConfig* config, Input input); + + //Called when time passes. Default implementation calls updateSelf(deltaTime) and updateChildren(deltaTime) - so you should probably call GuiComponent::update(deltaTime) at some point (or at least updateSelf so animations work). + virtual void update(int deltaTime); + + //Called when it's time to render. By default, just calls renderChildren(parentTrans * getTransform()). + //You probably want to override this like so: + //1. Calculate the new transform that your control will draw at with Transform4x4f t = parentTrans * getTransform(). + //2. Set the renderer to use that new transform as the model matrix - Renderer::setMatrix(t); + //3. Draw your component. + //4. Tell your children to render, based on your component's transform - renderChildren(t). + virtual void render(const Transform4x4f& parentTrans); + + Vector3f getPosition() const; + inline void setPosition(const Vector3f& offset) { setPosition(offset.x(), offset.y(), offset.z()); } + void setPosition(float x, float y, float z = 0.0f); + virtual void onPositionChanged() {}; + + //Sets the origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) + Vector2f getOrigin() const; + void setOrigin(float originX, float originY); + inline void setOrigin(Vector2f origin) { setOrigin(origin.x(), origin.y()); } + virtual void onOriginChanged() {}; + + //Sets the rotation origin as a percentage of this image (e.g. (0, 0) is top left, (0.5, 0.5) is the center) + Vector2f getRotationOrigin() const; + void setRotationOrigin(float originX, float originY); + inline void setRotationOrigin(Vector2f origin) { setRotationOrigin(origin.x(), origin.y()); } + + virtual Vector2f getSize() const; + inline void setSize(const Vector2f& size) { setSize(size.x(), size.y()); } + void setSize(float w, float h); + virtual void onSizeChanged() {}; + + virtual void setColor(unsigned int color) {}; + + virtual Vector2f getRotationSize() const { return getSize(); }; + + float getRotation() const; + void setRotation(float rotation); + inline void setRotationDegrees(float rotation) { setRotation((float)ES_DEG_TO_RAD(rotation)); } + + float getScale() const; + void setScale(float scale); + + float getZIndex() const; + void setZIndex(float zIndex); + + float getDefaultZIndex() const; + void setDefaultZIndex(float zIndex); + + // Returns the center point of the image (takes origin into account). + Vector2f getCenter() const; + + void setParent(GuiComponent* parent); + GuiComponent* getParent() const; + + void addChild(GuiComponent* cmp); + void removeChild(GuiComponent* cmp); + void clearChildren(); + void sortChildren(); + unsigned int getChildCount() const; + GuiComponent* getChild(unsigned int i) const; + + // animation will be automatically deleted when it completes or is stopped. + bool isAnimationPlaying(unsigned char slot) const; + bool isAnimationReversed(unsigned char slot) const; + int getAnimationTime(unsigned char slot) const; + void setAnimation(Animation* animation, int delay = 0, std::function finishedCallback = nullptr, bool reverse = false, unsigned char slot = 0); + bool stopAnimation(unsigned char slot); + bool cancelAnimation(unsigned char slot); // Like stopAnimation, but doesn't call finishedCallback - only removes the animation, leaving things in their current state. Returns true if successful (an animation was in this slot). + bool finishAnimation(unsigned char slot); // Calls update(1.f) and finishedCallback, then deletes the animation - basically skips to the end. Returns true if successful (an animation was in this slot). + bool advanceAnimation(unsigned char slot, unsigned int time); // Returns true if successful (an animation was in this slot). + void stopAllAnimations(); + void cancelAllAnimations(); + + virtual unsigned char getOpacity() const; + virtual void setOpacity(unsigned char opacity); + + const Transform4x4f& getTransform(); + + virtual std::string getValue() const; + virtual void setValue(const std::string& value); + + + virtual std::string getTag() const; + virtual void setTag(const std::string& value); + + virtual void onFocusGained() {}; + virtual void onFocusLost() {}; + + virtual void onShow(); + virtual void onHide(); + + virtual void onScreenSaverActivate(); + virtual void onScreenSaverDeactivate(); + virtual void topWindow(bool isTop); + + // Default implementation just handles and tags as normalized float pairs. + // You probably want to keep this behavior for any derived classes as well as add your own. + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); + + // Returns a list of help prompts. + virtual std::vector getHelpPrompts() { return std::vector(); }; + + // Called whenever help prompts change. + void updateHelpPrompts(); + + virtual HelpStyle getHelpStyle(); + + // Returns true if the component is busy doing background processing (e.g. HTTP downloads) + bool isProcessing() const; + +protected: + void renderChildren(const Transform4x4f& transform) const; + void updateSelf(int deltaTime); // updates animations + void updateChildren(int deltaTime); // updates animations + + unsigned char mOpacity; + Window* mWindow; + + GuiComponent* mParent; + std::vector mChildren; + + std::string mTag; + + Vector3f mPosition; + Vector2f mOrigin; + Vector2f mRotationOrigin; + Vector2f mSize; + + float mRotation = 0.0; + float mScale = 1.0; + + float mDefaultZIndex = 0; + float mZIndex = 0; + + bool mIsProcessing; + +public: + const static unsigned char MAX_ANIMATIONS = 4; + +private: + Transform4x4f mTransform; //Don't access this directly! Use getTransform()! + AnimationController* mAnimationMap[MAX_ANIMATIONS]; +}; + + + +class LocalizationItem +{ +public: + std::string msgid; + std::string msgstr; +}; + +class GuiTextTool +{ +public: +#if defined(_WIN32) + static const std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + + std::string string; + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return string; + } +#endif + static void ensureLocalisation(); + static const std::string localize(const std::string text); + + static void setLanguage(std::string lang); + +private: + static std::vector mItems; + static std::string mCurrentLanguage; + static bool mCurrentLanguageLoaded; +}; + +#if defined(_WIN32) +#define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) +#define _L(x) GuiTextTool::localize(x) +#else +#define _T(x) GuiTextTool::localize(x) +#define _L(x) GuiTextTool::localize(x) +#endif // _WIN32 + +#endif // ES_CORE_GUI_COMPONENT_H diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index 3285308cd2..f21e6f8f11 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -10,7 +10,8 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d width = 0; height = 0; FIMEMORY * fiMemory = FreeImage_OpenMemory((BYTE *)data, (DWORD)size); - if (fiMemory != nullptr) { + if (fiMemory != nullptr) + { //detect the filetype from data FREE_IMAGE_FORMAT format = FreeImage_GetFileTypeFromMemory(fiMemory); if (format != FIF_UNKNOWN && FreeImage_FIFSupportsReading(format)) @@ -31,21 +32,26 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d } } if (fiBitmap != nullptr) - { + { width = FreeImage_GetWidth(fiBitmap); height = FreeImage_GetHeight(fiBitmap); //loop through scanlines and add all pixel data to the return vector //this is necessary, because width*height*bpp might not be == pitch + unsigned char * tempData = new unsigned char[width * height * 4]; + unsigned char * bytes = FreeImage_GetBits(fiBitmap); + + // memcpy(tempData, bytes, width * height * 4); +/* for (size_t i = 0; i < height; i++) { const BYTE * scanLine = FreeImage_GetScanLine(fiBitmap, (int)i); memcpy(tempData + (i * width * 4), scanLine, width * 4); - } + }*/ //convert from BGRA to RGBA for(size_t i = 0; i < width*height; i++) { - RGBQUAD bgra = ((RGBQUAD *)tempData)[i]; + RGBQUAD bgra = ((RGBQUAD *)bytes)[i]; RGBQUAD rgba; rgba.rgbBlue = bgra.rgbRed; rgba.rgbGreen = bgra.rgbGreen; @@ -53,6 +59,7 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d rgba.rgbReserved = bgra.rgbReserved; ((RGBQUAD *)tempData)[i] = rgba; } + rawData = std::vector(tempData, tempData + width * height * 4); //free bitmap data FreeImage_Unload(fiBitmap); @@ -74,6 +81,78 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d return rawData; } +unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height) +{ + width = 0; + height = 0; + + FIMEMORY * fiMemory = FreeImage_OpenMemory((BYTE *)data, (DWORD)size); + if (fiMemory != nullptr) + { + //detect the filetype from data + FREE_IMAGE_FORMAT format = FreeImage_GetFileTypeFromMemory(fiMemory); + if (format != FIF_UNKNOWN && FreeImage_FIFSupportsReading(format)) + { + //file type is supported. load image + FIBITMAP * fiBitmap = FreeImage_LoadFromMemory(format, fiMemory); + if (fiBitmap != nullptr) + { + //loaded. convert to 32bit if necessary + if (FreeImage_GetBPP(fiBitmap) != 32) + { + FIBITMAP * fiConverted = FreeImage_ConvertTo32Bits(fiBitmap); + if (fiConverted != nullptr) + { + //free original bitmap data + FreeImage_Unload(fiBitmap); + fiBitmap = fiConverted; + } + } + if (fiBitmap != nullptr) + { + width = FreeImage_GetWidth(fiBitmap); + height = FreeImage_GetHeight(fiBitmap); + + //loop through scanlines and add all pixel data to the return vector + //this is necessary, because width*height*bpp might not be == pitch + + unsigned char* tempData = new unsigned char[width * height * 4]; + + int w = (int)width; + + for (int y = (int)height; --y >= 0; ) + { + unsigned int* argb = (unsigned int*)FreeImage_GetScanLine(fiBitmap, y); + unsigned int* abgr = (unsigned int*)(tempData + (y * width * 4)); + for (int x = w; --x >= 0;) + { + unsigned int c = argb[x]; + abgr[x] = (c & 0xFF00FF00) | ((c & 0xFF) << 16) | ((c >> 16) & 0xFF); + } + } + + FreeImage_Unload(fiBitmap); + FreeImage_CloseMemory(fiMemory); + + return tempData; + } + } + else + { + LOG(LogError) << "Error - Failed to load image from memory!"; + } + } + else + { + LOG(LogError) << "Error - File type " << (format == FIF_UNKNOWN ? "unknown" : "unsupported") << "!"; + } + //free FIMEMORY again + FreeImage_CloseMemory(fiMemory); + } + + return NULL; +} + void ImageIO::flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height) { unsigned int temp; diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index ad8ab48069..acbe6cb526 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -9,6 +9,8 @@ class ImageIO { public: static std::vector loadFromMemoryRGBA32(const unsigned char * data, const size_t size, size_t & width, size_t & height); + static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height); + static void flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height); }; diff --git a/es-core/src/PowerSaver.cpp b/es-core/src/PowerSaver.cpp index 8c68955f90..bc1d651a5c 100644 --- a/es-core/src/PowerSaver.cpp +++ b/es-core/src/PowerSaver.cpp @@ -62,6 +62,7 @@ void PowerSaver::updateMode() } else { mMode = DEFAULT; } + updateTimeouts(); } diff --git a/es-core/src/Renderer.h b/es-core/src/Renderer.h index 358cf4031f..5d8b0bd3ba 100644 --- a/es-core/src/Renderer.h +++ b/es-core/src/Renderer.h @@ -38,6 +38,8 @@ namespace Renderer void drawRect(int x, int y, int w, int h, unsigned int color, GLenum blend_sfactor = GL_SRC_ALPHA, GLenum blend_dfactor = GL_ONE_MINUS_SRC_ALPHA); void drawRect(float x, float y, float w, float h, unsigned int color, GLenum blend_sfactor = GL_SRC_ALPHA, GLenum blend_dfactor = GL_ONE_MINUS_SRC_ALPHA); + + bool isVisibleOnScreen(float x, float y, float w, float h); } #endif // ES_CORE_RENDERER_H diff --git a/es-core/src/Renderer_draw_gl.cpp b/es-core/src/Renderer_draw_gl.cpp index cac28c5d4b..b584c618b1 100644 --- a/es-core/src/Renderer_draw_gl.cpp +++ b/es-core/src/Renderer_draw_gl.cpp @@ -15,6 +15,7 @@ namespace Renderer { }; std::stack clipStack; + std::stack nativeClipStack; void setColor4bArray(GLubyte* array, unsigned int color) { @@ -62,7 +63,7 @@ namespace Renderer { } //make sure the box fits within clipStack.top(), and clip further accordingly - if(clipStack.size()) + if (clipStack.size()) { const ClipRect& top = clipStack.top(); if(top.x > box.x) @@ -81,6 +82,7 @@ namespace Renderer { box.h = 0; clipStack.push(box); + nativeClipStack.push(ClipRect(pos.x(), pos.y(), dim.x(), dim.y())); glScissor(box.x, box.y, box.w, box.h); glEnable(GL_SCISSOR_TEST); @@ -95,15 +97,60 @@ namespace Renderer { } clipStack.pop(); + nativeClipStack.pop(); + if(clipStack.empty()) { glDisable(GL_SCISSOR_TEST); - }else{ + } + else + { const ClipRect& top = clipStack.top(); glScissor(top.x, top.y, top.w, top.h); } } + bool valueInRange(int value, int min, int max) + { + return (value >= min) && (value <= max); + } + + bool rectOverlap(ClipRect A, ClipRect B) + { + bool xOverlap = valueInRange(A.x, B.x, B.x + B.w) || + valueInRange(B.x, A.x, A.x + A.w); + + bool yOverlap = valueInRange(A.y, B.y, B.y + B.h) || + valueInRange(B.y, A.y, A.y + A.h); + + return xOverlap && yOverlap; + } + + bool isVisibleOnScreen(float x, float y, float w, float h) + { + ClipRect screen = ClipRect(0, 0, Renderer::getWindowWidth(), Renderer::getWindowHeight()); + ClipRect box = ClipRect(x, y, w, h); + + if (w > 0 && x + w <= 0) + return false; + + if (h > 0 && y + h <= 0) + return false; + + if (x == screen.w || y == screen.h) + return false; + + if (!rectOverlap(screen, box)) + return false; + + if (clipStack.empty()) + return true; + + screen = nativeClipStack.top(); + return rectOverlap(screen, box); + } + + void drawRect(float x, float y, float w, float h, unsigned int color, GLenum blend_sfactor, GLenum blend_dfactor) { drawRect((int)Math::round(x), (int)Math::round(y), (int)Math::round(w), (int)Math::round(h), color, blend_sfactor, blend_dfactor); diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index fb33b2cea4..c51fddf94a 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -75,14 +75,15 @@ namespace Renderer SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, windowWidth, windowHeight, - SDL_WINDOW_OPENGL | (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN))); + SDL_WINDOW_OPENGL | (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN)) + ); if (sdlWindow == NULL) { LOG(LogError) << "Error creating SDL window!\n\t" << SDL_GetError(); return false; } - + LOG(LogInfo) << "Created window successfully."; //support screen rotation @@ -127,7 +128,7 @@ namespace Renderer sdlContext = SDL_GL_CreateContext(sdlWindow); // vsync - // if(Settings::getInstance()->getBool("VSync")) + if (Settings::getInstance()->getBool("VSync")) { // SDL_GL_SetSwapInterval(0) for immediate updates (no vsync, default), // 1 for updates synchronized with the vertical retrace, @@ -135,12 +136,13 @@ namespace Renderer // SDL_GL_SetSwapInterval returns 0 on success, -1 on error. // if vsync is requested, try normal vsync; if that doesn't work, try late swap tearing // if that doesn't work, report an error + if (SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) - LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; + LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; } - // else - // SDL_GL_SetSwapInterval(0); - + else + SDL_GL_SetSwapInterval(0); + return true; } @@ -218,7 +220,18 @@ namespace Renderer void swapBuffers() { +#ifdef WIN32 + glFlush(); + glFinish(); + Sleep(0); +#endif + SDL_GL_SwapWindow(sdlWindow); + +#ifdef WIN32 + Sleep(0); +#endif + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } }; diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 41f2e16de4..a21d117a44 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -24,7 +24,9 @@ std::vector settings_dont_save { { "ShowExit" }, { "SplashScreen" }, { "SplashScreenProgress" }, +#if !defined(_WIN32) { "VSync" }, +#endif { "FullscreenBorderless" }, { "Windowed" }, { "WindowWidth" }, @@ -67,7 +69,11 @@ void Settings::setDefaults() mBoolMap["SplashScreenProgress"] = true; mStringMap["StartupSystem"] = ""; +#if defined(_WIN32) mBoolMap["VSync"] = true; +#else + mBoolMap["VSync"] = false; +#endif mBoolMap["EnableSounds"] = true; mBoolMap["ShowHelpPrompts"] = true; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index ba4121ce08..bcc1a72180 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -1,730 +1,849 @@ -#include "ThemeData.h" - -#include "components/ImageComponent.h" -#include "components/TextComponent.h" -#include "utils/FileSystemUtil.h" -#include "Log.h" -#include "platform.h" -#include "Settings.h" -#include -#include - -std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" } }; -std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; - -std::map> ThemeData::sElementMap { - { "image", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "maxSize", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "path", PATH }, - { "default", PATH }, - { "tile", BOOLEAN }, - { "color", COLOR }, - { "zIndex", FLOAT } } }, - { "imagegrid", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "margin", NORMALIZED_PAIR }, - { "gameImage", PATH }, - { "folderImage", PATH }, - { "scrollDirection", STRING } } }, - { "gridtile", { - { "size", NORMALIZED_PAIR }, - { "padding", NORMALIZED_PAIR }, - { "imageColor", COLOR }, - { "backgroundImage", PATH }, - { "backgroundCornerSize", NORMALIZED_PAIR }, - { "backgroundColor", COLOR }, - { "backgroundCenterColor", COLOR }, - { "backgroundEdgeColor", COLOR }, - { "imageSizeMode", STRING } } }, - { "text", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "text", STRING }, - { "backgroundColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "color", COLOR }, - { "alignment", STRING }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "value", STRING }, - { "zIndex", FLOAT } } }, - { "textlist", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "selectorHeight", FLOAT }, - { "selectorOffsetY", FLOAT }, - { "selectorColor", COLOR }, - { "selectorImagePath", PATH }, - { "selectorImageTile", BOOLEAN }, - { "selectedColor", COLOR }, - { "primaryColor", COLOR }, - { "secondaryColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "scrollSound", PATH }, - { "alignment", STRING }, - { "horizontalMargin", FLOAT }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "zIndex", FLOAT } } }, - { "container", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "zIndex", FLOAT } } }, - { "ninepatch", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "path", PATH }, - { "zIndex", FLOAT } } }, - { "datetime", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "backgroundColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT }, - { "color", COLOR }, - { "alignment", STRING }, - { "forceUppercase", BOOLEAN }, - { "lineSpacing", FLOAT }, - { "value", STRING }, - { "format", STRING }, - { "displayRelative", BOOLEAN }, - { "zIndex", FLOAT } } }, - { "rating", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "color", COLOR }, - { "filledPath", PATH }, - { "unfilledPath", PATH }, - { "zIndex", FLOAT } } }, - { "sound", { - { "path", PATH } } }, - { "helpsystem", { - { "pos", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "textColor", COLOR }, - { "iconColor", COLOR }, - { "fontPath", PATH }, - { "fontSize", FLOAT } } }, - { "video", { - { "pos", NORMALIZED_PAIR }, - { "size", NORMALIZED_PAIR }, - { "maxSize", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "rotation", FLOAT }, - { "rotationOrigin", NORMALIZED_PAIR }, - { "default", PATH }, - { "delay", FLOAT }, - { "zIndex", FLOAT }, - { "showSnapshotNoVideo", BOOLEAN }, - { "showSnapshotDelay", BOOLEAN } } }, - { "carousel", { - { "type", STRING }, - { "size", NORMALIZED_PAIR }, - { "pos", NORMALIZED_PAIR }, - { "origin", NORMALIZED_PAIR }, - { "color", COLOR }, - { "logoScale", FLOAT }, - { "logoRotation", FLOAT }, - { "logoRotationOrigin", NORMALIZED_PAIR }, - { "logoSize", NORMALIZED_PAIR }, - { "logoAlignment", STRING }, - { "maxLogoCount", FLOAT }, - { "zIndex", FLOAT } } } -}; - -#define MINIMUM_THEME_FORMAT_VERSION 3 -#define CURRENT_THEME_FORMAT_VERSION 6 - -// helper -unsigned int getHexColor(const char* str) -{ - ThemeException error; - if(!str) - throw error << "Empty color"; - - size_t len = strlen(str); - if(len != 6 && len != 8) - throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; - - unsigned int val; - std::stringstream ss; - ss << str; - ss >> std::hex >> val; - - if(len == 6) - val = (val << 8) | 0xFF; - - return val; -} - -std::map mVariables; - -std::string resolvePlaceholders(const char* in) -{ - std::string inStr(in); - - if(inStr.empty()) - return inStr; - - const size_t variableBegin = inStr.find("${"); - const size_t variableEnd = inStr.find("}", variableBegin); - - if((variableBegin == std::string::npos) || (variableEnd == std::string::npos)) - return inStr; - - std::string prefix = inStr.substr(0, variableBegin); - std::string replace = inStr.substr(variableBegin + 2, variableEnd - (variableBegin + 2)); - std::string suffix = resolvePlaceholders(inStr.substr(variableEnd + 1).c_str()); - - return prefix + mVariables[replace] + suffix; -} - -ThemeData::ThemeData() -{ - mVersion = 0; -} - -void ThemeData::loadFile(std::string system, std::map sysDataMap, const std::string& path) -{ - mPaths.push_back(path); - - ThemeException error; - error.setFiles(mPaths); - - if(!Utils::FileSystem::exists(path)) - throw error << "File does not exist!"; - - mVersion = 0; - mViews.clear(); - - mSystemThemeFolder = system; - - mVariables.clear(); - mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); - - pugi::xml_document doc; - pugi::xml_parse_result res = doc.load_file(path.c_str()); - if(!res) - throw error << "XML parsing error: \n " << res.description(); - - pugi::xml_node root = doc.child("theme"); - if(!root) - throw error << "Missing tag!"; - - // parse version - mVersion = root.child("formatVersion").text().as_float(-404); - if(mVersion == -404) - throw error << " tag missing!\n It's either out of date or you need to add " << CURRENT_THEME_FORMAT_VERSION << " inside your tag."; - - if (root.attribute("defaultView")) - mDefaultView = root.attribute("defaultView").as_string(); - - if(mVersion < MINIMUM_THEME_FORMAT_VERSION) - throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; - - parseVariables(root); - parseIncludes(root); - parseViews(root); - parseFeatures(root); -} - -std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) -{ - std::string result = path; - - size_t start_pos = result.find("$system"); - if (start_pos == std::string::npos) - return path; - - result.replace(start_pos, 7, systemThemeFolder); - - //result.replace("$system", systemThemeFolder); - //boost::algorithm::replace_first(result, "$system", systemThemeFolder); - return result; -} - -bool ThemeData::parseSubset(const pugi::xml_node& node) -{ - bool parse = true; - - if (node.attribute("subset")) - { - parse = false; - const std::string subsetAttr = node.attribute("subset").as_string(); - const std::string nameAttr = node.attribute("name").as_string(); - - if (subsetAttr == "iconset" || subsetAttr == "gamelistview") - { - if (nameAttr.rfind("1-") != std::string::npos) - { - parse = true; - return parse; - } - } - else if (subsetAttr == "systemview") - { - if (nameAttr.rfind("8-") != std::string::npos) - { - parse = true; - return parse; - } - } - else if (nameAttr.rfind("2-") != std::string::npos) - { - parse = true; - return parse; - } - /* - if (subsetAttr == "colorset" && nameAttr == mColorset) - { - parse = true; - return parse; - } - if (subsetAttr == "iconset" && nameAttr == mIconset) - { - parse = true; - return parse; - } - if (subsetAttr == "menu" && nameAttr == mMenu) - { - parse = true; - return parse; - } - if (subsetAttr == "systemview" && nameAttr == mSystemview) - { - parse = true; - return parse; - } - if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) - { - parse = true; - return parse; - }*/ - } - - return parse; -} - -void ThemeData::parseIncludes(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) - { - if (!parseSubset(node)) - continue; - - std::string relPath = resolvePlaceholders(node.text().as_string()); - std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); - path = resolveSystemVariable(mSystemThemeFolder, path); - - if (!ResourceManager::getInstance()->fileExists(path)) - continue; - //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; - - error << " from included file \"" << relPath << "\":\n "; - - mPaths.push_back(path); - - pugi::xml_document includeDoc; - pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); - if(!result) - throw error << "Error parsing file: \n " << result.description(); - - pugi::xml_node theme = includeDoc.child("theme"); - if(!theme) - throw error << "Missing tag!"; - - parseVariables(theme); - parseIncludes(theme); - parseViews(theme); - parseFeatures(theme); - - mPaths.pop_back(); - } -} - -void ThemeData::parseFeatures(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) - { - if(!node.attribute("supported")) - throw error << "Feature missing \"supported\" attribute!"; - - const std::string supportedAttr = node.attribute("supported").as_string(); - - if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) - { - parseViews(node); - } - } -} - -void ThemeData::parseVariables(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - pugi::xml_node variables = root.child("variables"); - - if(!variables) - return; - - for(pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) - { - std::string key = it->name(); - std::string val = it->text().as_string(); - - if (!val.empty()) - mVariables.insert(std::pair(key, val)); - } -} - -void ThemeData::parseViews(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - // parse views - for(pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) - { - if(!node.attribute("name")) - throw error << "View missing \"name\" attribute!"; - - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - std::string viewKey; - while(off != std::string::npos || prevOff != std::string::npos) - { - viewKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - - if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) - { - ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; - parseView(node, view); - - if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) - { - ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; - parseView(node, view); - } - } - } - } -} - -void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) - { - if(!node.attribute("name")) - throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; - - auto elemTypeIt = sElementMap.find(node.name()); - if(elemTypeIt == sElementMap.cend()) - throw error << "Unknown element of type \"" << node.name() << "\"!"; - - if (parseRegion(node)) - { - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while (off != std::string::npos || prevOff != std::string::npos) - { - std::string elemKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - - parseElement(node, elemTypeIt->second, - view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); - - if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) - view.orderedKeys.push_back(elemKey); - } - } - } -} - -bool ThemeData::parseRegion(const pugi::xml_node& node) -{ - bool parse = true; - - if (node.attribute("region")) - { - std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); - - parse = false; - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("region").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while (off != std::string::npos || prevOff != std::string::npos) - { - std::string elemKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - if (elemKey == regionsetting) - { - parse = true; - return parse; - } - } - - } - return parse; - -} - - -void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element) -{ - ThemeException error; - error.setFiles(mPaths); - - element.type = root.name(); - element.extra = root.attribute("extra").as_bool(false); - - for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) - { - auto typeIt = typeMap.find(node.name()); - if (typeIt == typeMap.cend()) - continue; - //throw error << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ")."; - - std::string str = resolveSystemVariable(mSystemThemeFolder, resolvePlaceholders(node.text().as_string())); - - switch(typeIt->second) - { - case NORMALIZED_PAIR: - { - size_t divider = str.find(' '); - if(divider == std::string::npos) - throw error << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")"; - - std::string first = str.substr(0, divider); - std::string second = str.substr(divider, std::string::npos); - - Vector2f val((float)atof(first.c_str()), (float)atof(second.c_str())); - - element.properties[node.name()] = val; - break; - } - case STRING: - element.properties[node.name()] = str; - break; - case PATH: - { - std::string path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true); - - if (!ResourceManager::getInstance()->fileExists(path)) - { - std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); - if (ResourceManager::getInstance()->fileExists(rootPath)) - path = rootPath; - } - - if (!ResourceManager::getInstance()->fileExists(path)) - { - std::stringstream ss; - ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda - ss << "could not find file \"" << node.text().get() << "\" "; - if(node.text().get() != path) - ss << "(which resolved to \"" << path << "\") "; - - LOG(LogWarning) << ss.str(); - - auto test = element.properties[node.name()]; - if (test.s.empty()) - element.properties[node.name()] = path; - } - else - element.properties[node.name()] = path; - - break; - } - case COLOR: - element.properties[node.name()] = getHexColor(str.c_str()); - break; - case FLOAT: - { - float floatVal = static_cast(strtod(str.c_str(), 0)); - element.properties[node.name()] = floatVal; - break; - } - - case BOOLEAN: - { - // only look at first char - char first = str[0]; - // 1*, t* (true), T* (True), y* (yes), Y* (YES) - bool boolVal = (first == '1' || first == 't' || first == 'T' || first == 'y' || first == 'Y'); - - element.properties[node.name()] = boolVal; - break; - } - default: - throw error << "Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name(); - } - } -} - -bool ThemeData::hasView(const std::string& view) -{ - auto viewIt = mViews.find(view); - return (viewIt != mViews.cend()); -} - -const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const -{ - auto viewIt = mViews.find(view); - if(viewIt == mViews.cend()) - return NULL; // not found - - auto elemIt = viewIt->second.elements.find(element); - if(elemIt == viewIt->second.elements.cend()) return NULL; - - if(elemIt->second.type != expectedType && !expectedType.empty()) - { - LOG(LogWarning) << " requested mismatched theme type for [" << view << "." << element << "] - expected \"" - << expectedType << "\", got \"" << elemIt->second.type << "\""; - return NULL; - } - - return &elemIt->second; -} - -const std::shared_ptr& ThemeData::getDefault() -{ - static std::shared_ptr theme = nullptr; - if(theme == nullptr) - { - theme = std::shared_ptr(new ThemeData()); - - const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_theme_default.xml"; - if(Utils::FileSystem::exists(path)) - { - try - { - std::map emptyMap; - theme->loadFile("", emptyMap, path); - } catch(ThemeException& e) - { - LOG(LogError) << e.what(); - theme = std::shared_ptr(new ThemeData()); //reset to empty - } - } - } - - return theme; -} - -std::vector ThemeData::makeExtras(const std::shared_ptr& theme, const std::string& view, Window* window) -{ - std::vector comps; - - auto viewIt = theme->mViews.find(view); - if(viewIt == theme->mViews.cend()) - return comps; - - for(auto it = viewIt->second.orderedKeys.cbegin(); it != viewIt->second.orderedKeys.cend(); it++) - { - ThemeElement& elem = viewIt->second.elements.at(*it); - if(elem.extra) - { - GuiComponent* comp = NULL; - const std::string& t = elem.type; - if(t == "image") - comp = new ImageComponent(window); - else if(t == "text") - comp = new TextComponent(window); - - comp->setDefaultZIndex(10); - comp->applyTheme(theme, view, *it, ThemeFlags::ALL); - comps.push_back(comp); - } - } - - return comps; -} - -std::map ThemeData::getThemeSets() -{ - std::map sets; - - static const size_t pathCount = 2; - std::string paths[pathCount] = - { - "/etc/emulationstation/themes", - Utils::FileSystem::getHomePath() + "/.emulationstation/themes" - }; - - for(size_t i = 0; i < pathCount; i++) - { - if(!Utils::FileSystem::isDirectory(paths[i])) - continue; - - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(paths[i]); - - for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if(Utils::FileSystem::isDirectory(*it)) - { - ThemeSet set = {*it}; - sets[set.getName()] = set; - } - } - } - - return sets; -} - -std::string ThemeData::getThemeFromCurrentSet(const std::string& system) -{ - std::map themeSets = ThemeData::getThemeSets(); - if(themeSets.empty()) - { - // no theme sets available - return ""; - } - - std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(set == themeSets.cend()) - { - // currently selected theme set is missing, so just pick the first available set - set = themeSets.cbegin(); - Settings::getInstance()->setString("ThemeSet", set->first); - } - - return set->second.getThemePath(system); -} +#include "ThemeData.h" + +#include "components/ImageComponent.h" +#include "components/TextComponent.h" +#include "utils/FileSystemUtil.h" +#include "Log.h" +#include "platform.h" +#include "Settings.h" +#include +#include + +std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" }, { "menu" } }; +std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; + +std::map> ThemeData::sElementMap { + { "image", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "maxSize", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "path", PATH }, + { "default", PATH }, + { "tile", BOOLEAN }, + { "color", COLOR }, + { "zIndex", FLOAT } } }, + { "imagegrid", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "margin", NORMALIZED_PAIR }, + { "gameImage", PATH }, + { "folderImage", PATH }, + { "scrollDirection", STRING } } }, + { "gridtile", { + { "size", NORMALIZED_PAIR }, + { "padding", NORMALIZED_PAIR }, + { "imageColor", COLOR }, + { "backgroundImage", PATH }, + { "backgroundCornerSize", NORMALIZED_PAIR }, + { "backgroundColor", COLOR }, + { "backgroundCenterColor", COLOR }, + { "backgroundEdgeColor", COLOR }, + { "imageSizeMode", STRING } } }, + { "text", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "text", STRING }, + { "backgroundColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "color", COLOR }, + { "alignment", STRING }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "value", STRING }, + { "zIndex", FLOAT } } }, + { "textlist", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "selectorHeight", FLOAT }, + { "selectorOffsetY", FLOAT }, + { "selectorColor", COLOR }, + { "selectorImagePath", PATH }, + { "selectorImageTile", BOOLEAN }, + { "selectedColor", COLOR }, + { "primaryColor", COLOR }, + { "secondaryColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "scrollSound", PATH }, + { "alignment", STRING }, + { "horizontalMargin", FLOAT }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "zIndex", FLOAT } } }, + { "container", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "zIndex", FLOAT } } }, + { "ninepatch", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "path", PATH }, + { "zIndex", FLOAT } } }, + { "datetime", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "backgroundColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "color", COLOR }, + { "alignment", STRING }, + { "forceUppercase", BOOLEAN }, + { "lineSpacing", FLOAT }, + { "value", STRING }, + { "format", STRING }, + { "displayRelative", BOOLEAN }, + { "zIndex", FLOAT } } }, + { "rating", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "color", COLOR }, + { "filledPath", PATH }, + { "unfilledPath", PATH }, + { "zIndex", FLOAT } } }, + { "sound", { + { "path", PATH } } }, + { "helpsystem", { + { "pos", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "textColor", COLOR }, + { "iconColor", COLOR }, + { "fontPath", PATH }, + { "fontSize", FLOAT } } }, + { "video", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "maxSize", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "rotation", FLOAT }, + { "rotationOrigin", NORMALIZED_PAIR }, + { "default", PATH }, + { "delay", FLOAT }, + { "zIndex", FLOAT }, + { "showSnapshotNoVideo", BOOLEAN }, + { "showSnapshotDelay", BOOLEAN } } }, + { "carousel", { + { "type", STRING }, + { "size", NORMALIZED_PAIR }, + { "pos", NORMALIZED_PAIR }, + { "origin", NORMALIZED_PAIR }, + { "color", COLOR }, + { "logoScale", FLOAT }, + { "logoRotation", FLOAT }, + { "logoRotationOrigin", NORMALIZED_PAIR }, + { "logoSize", NORMALIZED_PAIR }, + { "logoPos", NORMALIZED_PAIR }, + { "logoAlignment", STRING }, + { "maxLogoCount", FLOAT }, + { "zIndex", FLOAT } } }, + + { "menuText", { + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "separatorColor", COLOR }, + { "selectorColor", COLOR }, + { "selectedColor", COLOR }, + { "color", COLOR } } }, + + { "menuTextSmall", { + { "fontPath", PATH }, + { "fontSize", FLOAT }, + { "color", COLOR } } }, + + { "menuBackground", { + { "path", PATH }, + { "fadePath", PATH }, + { "color", COLOR } } } +}; + +std::shared_ptr ThemeData::MenuTheme; + +#define MINIMUM_THEME_FORMAT_VERSION 3 +#define CURRENT_THEME_FORMAT_VERSION 6 + +// helper +unsigned int getHexColor(const char* str) +{ + ThemeException error; + if (!str) + { + //throw error << "Empty color"; + LOG(LogWarning) << "Empty color"; + return 0; + } + + size_t len = strlen(str); + if (len != 6 && len != 8) + { + //throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; + LOG(LogWarning) << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; + return 0; + } + + unsigned int val; + std::stringstream ss; + ss << str; + ss >> std::hex >> val; + + if(len == 6) + val = (val << 8) | 0xFF; + + return val; +} + +std::map mVariables; + +std::string resolvePlaceholders(const char* in) +{ + std::string inStr(in); + + if(inStr.empty()) + return inStr; + + const size_t variableBegin = inStr.find("${"); + const size_t variableEnd = inStr.find("}", variableBegin); + + if((variableBegin == std::string::npos) || (variableEnd == std::string::npos)) + return inStr; + + std::string prefix = inStr.substr(0, variableBegin); + std::string replace = inStr.substr(variableBegin + 2, variableEnd - (variableBegin + 2)); + std::string suffix = resolvePlaceholders(inStr.substr(variableEnd + 1).c_str()); + + return prefix + mVariables[replace] + suffix; +} + +ThemeData::ThemeData() +{ + mVersion = 0; +} + +void ThemeData::loadFile(std::string system, std::map sysDataMap, const std::string& path) +{ + mPaths.push_back(path); + + ThemeException error; + error.setFiles(mPaths); + + if(!Utils::FileSystem::exists(path)) + throw error << "File does not exist!"; + + mVersion = 0; + mViews.clear(); + + mSystemThemeFolder = system; + + mVariables.clear(); + mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); + + pugi::xml_document doc; + pugi::xml_parse_result res = doc.load_file(path.c_str()); + if(!res) + throw error << "XML parsing error: \n " << res.description(); + + pugi::xml_node root = doc.child("theme"); + if(!root) + throw error << "Missing tag!"; + + // parse version + mVersion = root.child("formatVersion").text().as_float(-404); + if(mVersion == -404) + throw error << " tag missing!\n It's either out of date or you need to add " << CURRENT_THEME_FORMAT_VERSION << " inside your tag."; + + if(mVersion < MINIMUM_THEME_FORMAT_VERSION) + throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; + + if (root.attribute("defaultView")) + mDefaultView = root.attribute("defaultView").as_string(); + + parseVariables(root); + parseIncludes(root); + parseViews(root); + parseFeatures(root); + + MenuTheme = std::shared_ptr(new ThemeMenu(*this)); +} + +std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) +{ + std::string result = path; + + size_t start_pos = result.find("$system"); + if (start_pos == std::string::npos) + return path; + + result.replace(start_pos, 7, systemThemeFolder); + return result; +} + +bool ThemeData::parseSubset(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("subset")) + { + parse = false; + const std::string subsetAttr = node.attribute("subset").as_string(); + const std::string nameAttr = node.attribute("name").as_string(); + + if (subsetAttr == "iconset" || subsetAttr == "gamelistview") + { + if (nameAttr.rfind("1-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (subsetAttr == "systemview") + { + if (nameAttr.rfind("8-") != std::string::npos) + { + parse = true; + return parse; + } + } + else if (nameAttr.rfind("2-") != std::string::npos) + { + parse = true; + return parse; + } + /* + if (subsetAttr == "colorset" && nameAttr == mColorset) + { + parse = true; + return parse; + } + if (subsetAttr == "iconset" && nameAttr == mIconset) + { + parse = true; + return parse; + } + if (subsetAttr == "menu" && nameAttr == mMenu) + { + parse = true; + return parse; + } + if (subsetAttr == "systemview" && nameAttr == mSystemview) + { + parse = true; + return parse; + } + if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) + { + parse = true; + return parse; + }*/ + } + + return parse; +} + +void ThemeData::parseIncludes(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) + { + if (!parseSubset(node)) + continue; + + std::string relPath = resolvePlaceholders(node.text().as_string()); + std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); + path = resolveSystemVariable(mSystemThemeFolder, path); + + if (!ResourceManager::getInstance()->fileExists(path)) + { + //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + LOG(LogWarning) << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + continue; + } + + mPaths.push_back(path); + + pugi::xml_document includeDoc; + pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); + if(!result) + throw error << "Error parsing file: \n " << result.description(); + + pugi::xml_node theme = includeDoc.child("theme"); + if(!theme) + throw error << "Missing tag!"; + + parseVariables(theme); + parseIncludes(theme); + parseViews(theme); + parseFeatures(theme); + + mPaths.pop_back(); + } +} + +void ThemeData::parseFeatures(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + for(pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) + { + if(!node.attribute("supported")) + throw error << "Feature missing \"supported\" attribute!"; + + const std::string supportedAttr = node.attribute("supported").as_string(); + + if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) + { + parseViews(node); + } + } +} + +void ThemeData::parseVariables(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + pugi::xml_node variables = root.child("variables"); + + if(!variables) + return; + + for(pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) + { + std::string key = it->name(); + std::string val = it->text().as_string(); + + if (!val.empty()) + mVariables.insert(std::pair(key, val)); + } +} + + +void ThemeData::parseViews(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + // parse views + for (pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) + { + if(!node.attribute("name")) + throw error << "View missing \"name\" attribute!"; + + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + std::string viewKey; + while(off != std::string::npos || prevOff != std::string::npos) + { + viewKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) + { + ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; + parseView(node, view); + + if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) + { + ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; + parseView(node, view); + } + } + } + } +} + + +void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) +{ + ThemeException error; + error.setFiles(mPaths); + + for (pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + { + if (!node.attribute("name")) + { + //throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; + LOG(LogWarning) << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; + continue; + } + + auto elemTypeIt = sElementMap.find(node.name()); + if (elemTypeIt == sElementMap.cend()) + { + //throw error << "Unknown element of type \"" << node.name() << "\"!"; + LOG(LogWarning) << "Unknown element of type \"" << node.name() << "\"!"; + continue; + } + + if (parseRegion(node)) + { + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + parseElement(node, elemTypeIt->second, + view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); + + if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) + view.orderedKeys.push_back(elemKey); + } + } + } +} + +bool ThemeData::parseRegion(const pugi::xml_node& node) +{ + bool parse = true; + + if (node.attribute("region")) + { + std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); + + parse = false; + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("region").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + if (elemKey == regionsetting) + { + parse = true; + return parse; + } + } + + } + return parse; + +} + + +void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element) +{ + ThemeException error; + error.setFiles(mPaths); + + element.type = root.name(); + element.extra = root.attribute("extra").as_bool(false); + + for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + { + auto typeIt = typeMap.find(node.name()); + if (typeIt == typeMap.cend()) + { + //throw error << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ")."; + LOG(LogWarning) << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ")."; + continue; + } + + std::string str = resolveSystemVariable(mSystemThemeFolder, resolvePlaceholders(node.text().as_string())); + + switch(typeIt->second) + { + case NORMALIZED_PAIR: + { + size_t divider = str.find(' '); + if (divider == std::string::npos) + { + // throw error << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")"; + LOG(LogWarning) << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")"; + break; + } + + std::string first = str.substr(0, divider); + std::string second = str.substr(divider, std::string::npos); + + Vector2f val((float)atof(first.c_str()), (float)atof(second.c_str())); + + element.properties[node.name()] = val; + break; + } + case STRING: + element.properties[node.name()] = str; + break; + case PATH: + { + std::string path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true); + + if (!ResourceManager::getInstance()->fileExists(path)) + { + std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); + if (ResourceManager::getInstance()->fileExists(rootPath)) + path = rootPath; + } + + if (!ResourceManager::getInstance()->fileExists(path)) + { + std::stringstream ss; + ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda + ss << "could not find file \"" << node.text().get() << "\" "; + if(node.text().get() != path) + ss << "(which resolved to \"" << path << "\") "; + + LOG(LogWarning) << ss.str(); + + auto test = element.properties[node.name()]; + if (test.s.empty()) + element.properties[node.name()] = path; + } + else + element.properties[node.name()] = path; + + break; + } + case COLOR: + element.properties[node.name()] = getHexColor(str.c_str()); + break; + case FLOAT: + { + float floatVal = static_cast(strtod(str.c_str(), 0)); + element.properties[node.name()] = floatVal; + break; + } + + case BOOLEAN: + { + // only look at first char + char first = str[0]; + // 1*, t* (true), T* (True), y* (yes), Y* (YES) + bool boolVal = (first == '1' || first == 't' || first == 'T' || first == 'y' || first == 'Y'); + + element.properties[node.name()] = boolVal; + break; + } + default: + LOG(LogWarning) << "Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name(); + break; // throw error << "Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name(); + } + } +} + +bool ThemeData::hasView(const std::string& view) +{ + auto viewIt = mViews.find(view); + return (viewIt != mViews.cend()); +} + +const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const +{ + auto viewIt = mViews.find(view); + if(viewIt == mViews.cend()) + return NULL; // not found + + auto elemIt = viewIt->second.elements.find(element); + if(elemIt == viewIt->second.elements.cend()) return NULL; + + if(elemIt->second.type != expectedType && !expectedType.empty()) + { + LOG(LogWarning) << " requested mismatched theme type for [" << view << "." << element << "] - expected \"" + << expectedType << "\", got \"" << elemIt->second.type << "\""; + return NULL; + } + + return &elemIt->second; +} + +const std::shared_ptr& ThemeData::getDefault() +{ + static std::shared_ptr theme = nullptr; + if(theme == nullptr) + { + theme = std::shared_ptr(new ThemeData()); + + const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_theme_default.xml"; + if(Utils::FileSystem::exists(path)) + { + try + { + std::map emptyMap; + theme->loadFile("", emptyMap, path); + } catch(ThemeException& e) + { + LOG(LogError) << e.what(); + theme = std::shared_ptr(new ThemeData()); //reset to empty + } + } + } + + return theme; +} + +std::vector ThemeData::makeExtras(const std::shared_ptr& theme, const std::string& view, Window* window) +{ + std::vector comps; + + auto viewIt = theme->mViews.find(view); + if(viewIt == theme->mViews.cend()) + return comps; + + for(auto it = viewIt->second.orderedKeys.cbegin(); it != viewIt->second.orderedKeys.cend(); it++) + { + ThemeElement& elem = viewIt->second.elements.at(*it); + if(elem.extra) + { + GuiComponent* comp = NULL; + const std::string& t = elem.type; + if(t == "image") + comp = new ImageComponent(window); + else if(t == "text") + comp = new TextComponent(window); + + comp->setDefaultZIndex(10); + comp->applyTheme(theme, view, *it, ThemeFlags::ALL); + comps.push_back(comp); + } + } + + return comps; +} + +std::map ThemeData::getThemeSets() +{ + std::map sets; + + static const size_t pathCount = 2; + std::string paths[pathCount] = + { + "/etc/emulationstation/themes", + Utils::FileSystem::getHomePath() + "/.emulationstation/themes" + }; + + for(size_t i = 0; i < pathCount; i++) + { + if(!Utils::FileSystem::isDirectory(paths[i])) + continue; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(paths[i]); + + for(Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if(Utils::FileSystem::isDirectory(*it)) + { + ThemeSet set = {*it}; + sets[set.getName()] = set; + } + } + } + + return sets; +} + +std::string ThemeData::getThemeFromCurrentSet(const std::string& system) +{ + std::map themeSets = ThemeData::getThemeSets(); + if(themeSets.empty()) + { + // no theme sets available + return ""; + } + + std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if(set == themeSets.cend()) + { + // currently selected theme set is missing, so just pick the first available set + set = themeSets.cbegin(); + Settings::getInstance()->setString("ThemeSet", set->first); + } + + return set->second.getThemePath(system); +} + +ThemeData::ThemeMenu::ThemeMenu(ThemeData& theme) +{ + Title.font = Font::get(FONT_SIZE_LARGE); + Footer.font = Font::get(FONT_SIZE_SMALL); + Text.font = Font::get(FONT_SIZE_MEDIUM); + TextSmall.font = Font::get(FONT_SIZE_SMALL); + + auto elem = theme.getElement("menu", "menubg", "menuBackground"); + if (elem) + { + if (elem->has("path") && ResourceManager::getInstance()->fileExists(elem->get("path"))) + Background.path = elem->get("path"); + + if (elem->has("fadePath") && ResourceManager::getInstance()->fileExists(elem->get("fadePath"))) + Background.fadePath = elem->get("fadePath"); + + if (elem->has("color")) + Background.color = elem->get("color"); + } + + elem = theme.getElement("menu", "menutitle", "menuText"); + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + Title.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_LARGE)); + if (elem->has("color")) + Title.color = elem->get("color"); + } + + elem = theme.getElement("menu", "menufooter", "menuText"); + + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + Footer.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_SMALL)); + if (elem->has("color")) + Footer.color = elem->get("color"); + } + + elem = theme.getElement("menu", "menutextsmall", "menuTextSmall"); + + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + TextSmall.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_SMALL)); + + if (elem->has("color")) + TextSmall.color = elem->get("color"); + if (elem->has("selectedColor")) + Text.selectedColor = elem->get("selectedColor"); + if (elem->has("selectorColor")) + Text.selectedColor = elem->get("selectorColor"); + } + + elem = theme.getElement("menu", "menutext", "menuText"); + + if (elem) + { + if (elem->has("fontPath") || elem->has("fontSize")) + Text.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_MEDIUM)); + + if (elem->has("color")) + Text.color = elem->get("color"); + if (elem->has("separatorColor")) + Text.separatorColor = elem->get("separatorColor"); + if (elem->has("selectedColor")) + Text.selectedColor = elem->get("selectedColor"); + if (elem->has("selectorColor")) + Text.selectorColor = elem->get("selectorColor"); + } +} \ No newline at end of file diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index aec760c4c7..27fa912433 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -4,6 +4,7 @@ #include "math/Vector2f.h" #include "utils/FileSystemUtil.h" + #include #include #include @@ -21,6 +22,7 @@ class NinePatchComponent; class Sound; class TextComponent; class Window; +class Font; namespace ThemeFlags { @@ -81,9 +83,31 @@ struct ThemeSet inline std::string getThemePath(const std::string& system) const { return path + "/" + system + "/theme.xml"; } }; +struct MenuElement { + unsigned int color; + unsigned int selectedColor; + unsigned int selectorColor; + unsigned int separatorColor; + std::string path; + std::string fadePath; + std::shared_ptr font; +}; + class ThemeData { public: + class ThemeMenu + { + public: + ThemeMenu(ThemeData& theme); + + MenuElement Background{ 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, ":/frame.png", ":/scroll_gradient.png", nullptr }; + MenuElement Title{ 0x555555FF, 0x555555FF, 0x555555FF, 0xFFFFFFFF, "", "", nullptr }; + MenuElement Text{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, "", "", nullptr }; + MenuElement TextSmall{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, "", "", nullptr }; + MenuElement Footer{ 0xC6C6C6FF, 0xC6C6C6FF, 0xC6C6C6FF, 0xFFFFFFFF, "", "", nullptr }; + }; + class ThemeElement { @@ -130,6 +154,7 @@ class ThemeData std::vector orderedKeys; }; + public: ThemeData(); @@ -161,6 +186,7 @@ class ThemeData std::string getDefaultView() { return mDefaultView; }; + static const std::shared_ptr& getMenuTheme() { return MenuTheme; } private: static std::map< std::string, std::map > sElementMap; @@ -171,7 +197,7 @@ class ThemeData float mVersion; std::string mDefaultView; - + void parseFeatures(const pugi::xml_node& themeRoot); void parseIncludes(const pugi::xml_node& themeRoot); void parseVariables(const pugi::xml_node& root); @@ -180,11 +206,13 @@ class ThemeData void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element); bool parseRegion(const pugi::xml_node& node); bool parseSubset(const pugi::xml_node& node); - + std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); std::map mViews; std::string mSystemThemeFolder; + + static std::shared_ptr MenuTheme; }; #endif // ES_CORE_THEME_DATA_H diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 7a3164f882..87f5118e5b 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -1,531 +1,531 @@ -#include "Window.h" - -#include "components/HelpComponent.h" -#include "components/ImageComponent.h" -#include "resources/Font.h" -#include "resources/TextureResource.h" -#include "InputManager.h" -#include "Log.h" -#include "Renderer.h" -#include "Scripting.h" -#include -#include - -#include - -Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), - mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) -{ - mHelp = new HelpComponent(this); - mBackgroundOverlay = new ImageComponent(this); - mSplash = NULL; - -} - -Window::~Window() -{ - delete mBackgroundOverlay; - - // delete all our GUIs - while(peekGui()) - delete peekGui(); - - delete mHelp; -} - -void Window::pushGui(GuiComponent* gui) -{ - if (mGuiStack.size() > 0) - { - auto& top = mGuiStack.back(); - top->topWindow(false); - } - mGuiStack.push_back(gui); - gui->updateHelpPrompts(); -} - -void Window::removeGui(GuiComponent* gui) -{ - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - { - if(*i == gui) - { - i = mGuiStack.erase(i); - - if(i == mGuiStack.cend() && mGuiStack.size()) // we just popped the stack and the stack is not empty - { - mGuiStack.back()->updateHelpPrompts(); - mGuiStack.back()->topWindow(true); - } - - return; - } - } -} - -GuiComponent* Window::peekGui() -{ - if(mGuiStack.size() == 0) - return NULL; - - return mGuiStack.back(); -} - -bool Window::init() -{ - if(!Renderer::init()) - { - LOG(LogError) << "Renderer failed to initialize!"; - return false; - } - - InputManager::getInstance()->init(); - ResourceManager::getInstance()->reloadAll(); - - //keep a reference to the default fonts, so they don't keep getting destroyed/recreated - if(mDefaultFonts.empty()) - { - mDefaultFonts.push_back(Font::get(FONT_SIZE_SMALL)); - mDefaultFonts.push_back(Font::get(FONT_SIZE_MEDIUM)); - mDefaultFonts.push_back(Font::get(FONT_SIZE_LARGE)); - } - - mBackgroundOverlay->setImage(":/scroll_gradient.png"); - mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - - // update our help because font sizes probably changed - if (peekGui()) - peekGui()->updateHelpPrompts(); - - return true; -} - -void Window::deinit() -{ - // Hide all GUI elements on uninitialisation - this disable - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - { - (*i)->onHide(); - } - InputManager::getInstance()->deinit(); - ResourceManager::getInstance()->unloadAll(); - Renderer::deinit(); -} - -void Window::textInput(const char* text) -{ - if(peekGui()) - peekGui()->textInput(text); -} - -void Window::input(InputConfig* config, Input input) -{ - if (mScreenSaver) { - if(mScreenSaver->isScreenSaverActive() && Settings::getInstance()->getBool("ScreenSaverControls") && - (Settings::getInstance()->getString("ScreenSaverBehavior") == "random video")) - { - if(mScreenSaver->getCurrentGame() != NULL && (config->isMappedLike("right", input) || config->isMappedTo("start", input) || config->isMappedTo("select", input))) - { - if(config->isMappedLike("right", input) || config->isMappedTo("select", input)) - { - if (input.value != 0) { - // handle screensaver control - mScreenSaver->nextVideo(); - } - return; - } - else if(config->isMappedTo("start", input) && input.value != 0) - { - // launch game! - cancelScreenSaver(); - mScreenSaver->launchGame(); - // to force handling the wake up process - mSleeping = true; - } - } - } - } - - if(mSleeping) - { - // wake up - mTimeSinceLastInput = 0; - cancelScreenSaver(); - mSleeping = false; - onWake(); - return; - } - - mTimeSinceLastInput = 0; - if (cancelScreenSaver()) - return; - - if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle debug grid with Ctrl-G - Settings::getInstance()->setBool("DebugGrid", !Settings::getInstance()->getBool("DebugGrid")); - } - else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle TextComponent debug view with Ctrl-T - Settings::getInstance()->setBool("DebugText", !Settings::getInstance()->getBool("DebugText")); - } - else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) - { - // toggle TextComponent debug view with Ctrl-I - Settings::getInstance()->setBool("DebugImage", !Settings::getInstance()->getBool("DebugImage")); - } - else - { - if (peekGui()) - { - this->peekGui()->input(config, input); // this is where the majority of inputs will be consumed: the GuiComponent Stack - } - } -} - -void Window::update(int deltaTime) -{ - if(mNormalizeNextUpdate) - { - mNormalizeNextUpdate = false; - if(deltaTime > mAverageDeltaTime) - deltaTime = mAverageDeltaTime; - } - - mFrameTimeElapsed += deltaTime; - mFrameCountElapsed++; - if(mFrameTimeElapsed > 500) - { - mAverageDeltaTime = mFrameTimeElapsed / mFrameCountElapsed; - - if(Settings::getInstance()->getBool("DrawFramerate")) - { - std::stringstream ss; - - // fps - ss << std::fixed << std::setprecision(1) << (1000.0f * (float)mFrameCountElapsed / (float)mFrameTimeElapsed) << "fps, "; - ss << std::fixed << std::setprecision(2) << ((float)mFrameTimeElapsed / (float)mFrameCountElapsed) << "ms"; - - // vram - float textureVramUsageMb = TextureResource::getTotalMemUsage() / 1000.0f / 1000.0f; - float textureTotalUsageMb = TextureResource::getTotalTextureSize() / 1000.0f / 1000.0f; - float fontVramUsageMb = Font::getTotalMemUsage() / 1000.0f / 1000.0f; - - ss << "\nFont VRAM: " << fontVramUsageMb << " Tex VRAM: " << textureVramUsageMb << - " Tex Max: " << textureTotalUsageMb; - mFrameDataText = std::unique_ptr(mDefaultFonts.at(1)->buildTextCache(ss.str(), 50.f, 50.f, 0xFF00FFFF)); - } - - mFrameTimeElapsed = 0; - mFrameCountElapsed = 0; - } - - mTimeSinceLastInput += deltaTime; - - if(peekGui()) - peekGui()->update(deltaTime); - - // Update the screensaver - if (mScreenSaver) - mScreenSaver->update(deltaTime); -} - -void Window::render() -{ - Transform4x4f transform = Transform4x4f::Identity(); - - mRenderedHelpPrompts = false; - - // draw only bottom and top of GuiStack (if they are different) - if(mGuiStack.size()) - { - auto& bottom = mGuiStack.front(); - auto& top = mGuiStack.back(); - - bottom->render(transform); - if(bottom != top) - { - mBackgroundOverlay->render(transform); - top->render(transform); - } - } - - if(!mRenderedHelpPrompts) - mHelp->render(transform); - - if(Settings::getInstance()->getBool("DrawFramerate") && mFrameDataText) - { - Renderer::setMatrix(Transform4x4f::Identity()); - mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); - } - - unsigned int screensaverTime = (unsigned int)Settings::getInstance()->getInt("ScreenSaverTime"); - if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) - startScreenSaver(); - - // Always call the screensaver render function regardless of whether the screensaver is active - // or not because it may perform a fade on transition - renderScreenSaver(); - - if(!mRenderScreenSaver && mInfoPopup) - { - mInfoPopup->render(transform); - } - - if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) - { - if (!isProcessing() && mAllowSleep && (!mScreenSaver || mScreenSaver->allowSleep())) - { - // go to sleep - if (mSleeping == false) { - mSleeping = true; - onSleep(); - } - } - } -} - -void Window::normalizeNextUpdate() -{ - mNormalizeNextUpdate = true; -} - -bool Window::getAllowSleep() -{ - return mAllowSleep; -} - -void Window::setAllowSleep(bool sleep) -{ - mAllowSleep = sleep; -} - -void Window::endRenderLoadingScreen() -{ - mSplash = NULL; -} - -void Window::renderLoadingScreen(std::string text, float percent) -{ -#if defined(_WIN32) - // Avoid Window Freezing on Windows - SDL_Event event; - while (SDL_PollEvent(&event)) ; -#endif - - Transform4x4f trans = Transform4x4f::Identity(); - Renderer::setMatrix(trans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); - - if (percent >= 0) - { - float baseHeight = 0.04f; - - float w = Renderer::getScreenWidth() / 2; - float h = Renderer::getScreenHeight() * baseHeight; - - float x = Renderer::getScreenWidth() / 2 - w / 2; - float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); - - Renderer::drawRect(x, y, w, h, 0x252525FF); - Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF - } - - - ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); - -#if defined(_WIN32) - if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); -#endif - - if (mSplash != NULL) - splash.setImage(mSplash); - else - splash.setImage(":/splash.svg"); - - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); - splash.render(trans); - - - auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); - - float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.78f); // 35 - trans = trans.translate(Vector3f(x, y, 0.0f)); - Renderer::setMatrix(trans); - font->renderTextCache(cache); - delete cache; - - - Renderer::swapBuffers(); -} - -void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) -{ - Transform4x4f trans = Transform4x4f::Identity(); - Renderer::setMatrix(trans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); - - ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); - -#if defined(_WIN32) - if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); -#endif - - if (mSplash != NULL) - splash.setImage(mSplash); - else - splash.setImage(":/splash.svg"); - - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); - splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); - splash.render(trans); - - auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); - - float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); - float y = Math::round(Renderer::getScreenHeight() * 0.835f); - trans = trans.translate(Vector3f(x, y, 0.0f)); - Renderer::setMatrix(trans); - font->renderTextCache(cache); - delete cache; - - if (swapBuffers) - Renderer::swapBuffers(); -} - - -void Window::renderHelpPromptsEarly() -{ - mHelp->render(Transform4x4f::Identity()); - mRenderedHelpPrompts = true; -} - -void Window::setHelpPrompts(const std::vector& prompts, const HelpStyle& style) -{ - mHelp->clearPrompts(); - mHelp->setStyle(style); - - std::vector addPrompts; - - std::map inputSeenMap; - std::map mappedToSeenMap; - for(auto it = prompts.cbegin(); it != prompts.cend(); it++) - { - // only add it if the same icon hasn't already been added - if(inputSeenMap.emplace(it->first, true).second) - { - // this symbol hasn't been seen yet, what about the action name? - auto mappedTo = mappedToSeenMap.find(it->second); - if(mappedTo != mappedToSeenMap.cend()) - { - // yes, it has! - - // can we combine? (dpad only) - if((it->first == "up/down" && addPrompts.at(mappedTo->second).first != "left/right") || - (it->first == "left/right" && addPrompts.at(mappedTo->second).first != "up/down")) - { - // yes! - addPrompts.at(mappedTo->second).first = "up/down/left/right"; - // don't need to add this to addPrompts since we just merged - }else{ - // no, we can't combine! - addPrompts.push_back(*it); - } - }else{ - // no, it hasn't! - mappedToSeenMap.emplace(it->second, (int)addPrompts.size()); - addPrompts.push_back(*it); - } - } - } - - // sort prompts so it goes [dpad_all] [dpad_u/d] [dpad_l/r] [a/b/x/y/l/r] [start/select] - std::sort(addPrompts.begin(), addPrompts.end(), [](const HelpPrompt& a, const HelpPrompt& b) -> bool { - - static const char* map[] = { - "up/down/left/right", - "up/down", - "left/right", - "a", "b", "x", "y", "l", "r", - "start", "select", - NULL - }; - - int i = 0; - int aVal = 0; - int bVal = 0; - while(map[i] != NULL) - { - if(a.first == map[i]) - aVal = i; - if(b.first == map[i]) - bVal = i; - i++; - } - - return aVal > bVal; - }); - - mHelp->setPrompts(addPrompts); -} - - -void Window::onSleep() -{ - Scripting::fireEvent("sleep"); -} - -void Window::onWake() -{ - Scripting::fireEvent("wake"); -} - -bool Window::isProcessing() -{ - return count_if(mGuiStack.cbegin(), mGuiStack.cend(), [](GuiComponent* c) { return c->isProcessing(); }) > 0; -} - -void Window::startScreenSaver() -{ - if (mScreenSaver && !mRenderScreenSaver) - { - // Tell the GUI components the screensaver is starting - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - (*i)->onScreenSaverActivate(); - - mScreenSaver->startScreenSaver(); - mRenderScreenSaver = true; - } -} - -bool Window::cancelScreenSaver() -{ - if (mScreenSaver && mRenderScreenSaver) - { - mScreenSaver->stopScreenSaver(); - mRenderScreenSaver = false; - mScreenSaver->resetCounts(); - - // Tell the GUI components the screensaver has stopped - for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - (*i)->onScreenSaverDeactivate(); - - return true; - } - - return false; -} - -void Window::renderScreenSaver() -{ - if (mScreenSaver) - mScreenSaver->renderScreenSaver(); -} +#include "Window.h" + +#include "components/HelpComponent.h" +#include "components/ImageComponent.h" +#include "resources/Font.h" +#include "resources/TextureResource.h" +#include "InputManager.h" +#include "Log.h" +#include "Renderer.h" +#include "Scripting.h" +#include +#include + +#include + +Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), + mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) +{ + mHelp = new HelpComponent(this); + mBackgroundOverlay = new ImageComponent(this); + mSplash = NULL; + +} + +Window::~Window() +{ + delete mBackgroundOverlay; + + // delete all our GUIs + while(peekGui()) + delete peekGui(); + + delete mHelp; +} + +void Window::pushGui(GuiComponent* gui) +{ + if (mGuiStack.size() > 0) + { + auto& top = mGuiStack.back(); + top->topWindow(false); + } + mGuiStack.push_back(gui); + gui->updateHelpPrompts(); +} + +void Window::removeGui(GuiComponent* gui) +{ + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + { + if(*i == gui) + { + i = mGuiStack.erase(i); + + if(i == mGuiStack.cend() && mGuiStack.size()) // we just popped the stack and the stack is not empty + { + mGuiStack.back()->updateHelpPrompts(); + mGuiStack.back()->topWindow(true); + } + + return; + } + } +} + +GuiComponent* Window::peekGui() +{ + if(mGuiStack.size() == 0) + return NULL; + + return mGuiStack.back(); +} + +bool Window::init() +{ + if(!Renderer::init()) + { + LOG(LogError) << "Renderer failed to initialize!"; + return false; + } + + InputManager::getInstance()->init(); + ResourceManager::getInstance()->reloadAll(); + + //keep a reference to the default fonts, so they don't keep getting destroyed/recreated + if(mDefaultFonts.empty()) + { + mDefaultFonts.push_back(Font::get(FONT_SIZE_SMALL)); + mDefaultFonts.push_back(Font::get(FONT_SIZE_MEDIUM)); + mDefaultFonts.push_back(Font::get(FONT_SIZE_LARGE)); + } + + mBackgroundOverlay->setImage(":/scroll_gradient.png"); + mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + + // update our help because font sizes probably changed + if (peekGui()) + peekGui()->updateHelpPrompts(); + + return true; +} + +void Window::deinit() +{ + // Hide all GUI elements on uninitialisation - this disable + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + { + (*i)->onHide(); + } + InputManager::getInstance()->deinit(); + ResourceManager::getInstance()->unloadAll(); + Renderer::deinit(); +} + +void Window::textInput(const char* text) +{ + if(peekGui()) + peekGui()->textInput(text); +} + +void Window::input(InputConfig* config, Input input) +{ + if (mScreenSaver) { + if(mScreenSaver->isScreenSaverActive() && Settings::getInstance()->getBool("ScreenSaverControls") && + (Settings::getInstance()->getString("ScreenSaverBehavior") == "random video")) + { + if(mScreenSaver->getCurrentGame() != NULL && (config->isMappedLike("right", input) || config->isMappedTo("start", input) || config->isMappedTo("select", input))) + { + if(config->isMappedLike("right", input) || config->isMappedTo("select", input)) + { + if (input.value != 0) { + // handle screensaver control + mScreenSaver->nextVideo(); + } + return; + } + else if(config->isMappedTo("start", input) && input.value != 0) + { + // launch game! + cancelScreenSaver(); + mScreenSaver->launchGame(); + // to force handling the wake up process + mSleeping = true; + } + } + } + } + + if(mSleeping) + { + // wake up + mTimeSinceLastInput = 0; + cancelScreenSaver(); + mSleeping = false; + onWake(); + return; + } + + mTimeSinceLastInput = 0; + if (cancelScreenSaver()) + return; + + if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle debug grid with Ctrl-G + Settings::getInstance()->setBool("DebugGrid", !Settings::getInstance()->getBool("DebugGrid")); + } + else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle TextComponent debug view with Ctrl-T + Settings::getInstance()->setBool("DebugText", !Settings::getInstance()->getBool("DebugText")); + } + else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + { + // toggle TextComponent debug view with Ctrl-I + Settings::getInstance()->setBool("DebugImage", !Settings::getInstance()->getBool("DebugImage")); + } + else + { + if (peekGui()) + { + this->peekGui()->input(config, input); // this is where the majority of inputs will be consumed: the GuiComponent Stack + } + } +} + +void Window::update(int deltaTime) +{ + if(mNormalizeNextUpdate) + { + mNormalizeNextUpdate = false; + if(deltaTime > mAverageDeltaTime) + deltaTime = mAverageDeltaTime; + } + + mFrameTimeElapsed += deltaTime; + mFrameCountElapsed++; + if(mFrameTimeElapsed > 500) + { + mAverageDeltaTime = mFrameTimeElapsed / mFrameCountElapsed; + + if(Settings::getInstance()->getBool("DrawFramerate")) + { + std::stringstream ss; + + // fps + ss << std::fixed << std::setprecision(1) << (1000.0f * (float)mFrameCountElapsed / (float)mFrameTimeElapsed) << "fps, "; + ss << std::fixed << std::setprecision(2) << ((float)mFrameTimeElapsed / (float)mFrameCountElapsed) << "ms"; + + // vram + float textureVramUsageMb = TextureResource::getTotalMemUsage() / 1000.0f / 1000.0f; + float textureTotalUsageMb = TextureResource::getTotalTextureSize() / 1000.0f / 1000.0f; + float fontVramUsageMb = Font::getTotalMemUsage() / 1000.0f / 1000.0f; + + ss << "\nFont VRAM: " << fontVramUsageMb << " Tex VRAM: " << textureVramUsageMb << + " Tex Max: " << textureTotalUsageMb; + mFrameDataText = std::unique_ptr(mDefaultFonts.at(1)->buildTextCache(ss.str(), 50.f, 50.f, 0xFF00FFFF)); + } + + mFrameTimeElapsed = 0; + mFrameCountElapsed = 0; + } + + mTimeSinceLastInput += deltaTime; + + if(peekGui()) + peekGui()->update(deltaTime); + + // Update the screensaver + if (mScreenSaver) + mScreenSaver->update(deltaTime); +} + +void Window::render() +{ + Transform4x4f transform = Transform4x4f::Identity(); + + mRenderedHelpPrompts = false; + + // draw only bottom and top of GuiStack (if they are different) + if(mGuiStack.size()) + { + auto& bottom = mGuiStack.front(); + auto& top = mGuiStack.back(); + + bottom->render(transform); + if(bottom != top) + { + mBackgroundOverlay->render(transform); + top->render(transform); + } + } + + if(!mRenderedHelpPrompts) + mHelp->render(transform); + + if(Settings::getInstance()->getBool("DrawFramerate") && mFrameDataText) + { + Renderer::setMatrix(Transform4x4f::Identity()); + mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); + } + + unsigned int screensaverTime = (unsigned int)Settings::getInstance()->getInt("ScreenSaverTime"); + if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) + startScreenSaver(); + + // Always call the screensaver render function regardless of whether the screensaver is active + // or not because it may perform a fade on transition + renderScreenSaver(); + + if(!mRenderScreenSaver && mInfoPopup) + { + mInfoPopup->render(transform); + } + + if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) + { + if (!isProcessing() && mAllowSleep && (!mScreenSaver || mScreenSaver->allowSleep())) + { + // go to sleep + if (mSleeping == false) { + mSleeping = true; + onSleep(); + } + } + } +} + +void Window::normalizeNextUpdate() +{ + mNormalizeNextUpdate = true; +} + +bool Window::getAllowSleep() +{ + return mAllowSleep; +} + +void Window::setAllowSleep(bool sleep) +{ + mAllowSleep = sleep; +} + +void Window::endRenderLoadingScreen() +{ + mSplash = NULL; +} + +void Window::renderLoadingScreen(std::string text, float percent) +{ +#if defined(_WIN32) + // Avoid Window Freezing on Windows + SDL_Event event; + while (SDL_PollEvent(&event)) ; +#endif + + Transform4x4f trans = Transform4x4f::Identity(); + Renderer::setMatrix(trans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); + + if (percent >= 0) + { + float baseHeight = 0.04f; + + float w = Renderer::getScreenWidth() / 2; + float h = Renderer::getScreenHeight() * baseHeight; + + float x = Renderer::getScreenWidth() / 2 - w / 2; + float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); + + Renderer::drawRect(x, y, w, h, 0x252525FF); + Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF + } + + + ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.render(trans); + + + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); + + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.78f); // 35 + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; + + + Renderer::swapBuffers(); +} + +void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) +{ + Transform4x4f trans = Transform4x4f::Identity(); + Renderer::setMatrix(trans); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); + + ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + +#if defined(_WIN32) + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); +#endif + + if (mSplash != NULL) + splash.setImage(mSplash); + else + splash.setImage(":/splash.svg"); + + splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); + splash.render(trans); + + auto& font = mDefaultFonts.at(1); + TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); + + float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); + float y = Math::round(Renderer::getScreenHeight() * 0.835f); + trans = trans.translate(Vector3f(x, y, 0.0f)); + Renderer::setMatrix(trans); + font->renderTextCache(cache); + delete cache; + + if (swapBuffers) + Renderer::swapBuffers(); +} + + +void Window::renderHelpPromptsEarly() +{ + mHelp->render(Transform4x4f::Identity()); + mRenderedHelpPrompts = true; +} + +void Window::setHelpPrompts(const std::vector& prompts, const HelpStyle& style) +{ + mHelp->clearPrompts(); + mHelp->setStyle(style); + + std::vector addPrompts; + + std::map inputSeenMap; + std::map mappedToSeenMap; + for(auto it = prompts.cbegin(); it != prompts.cend(); it++) + { + // only add it if the same icon hasn't already been added + if(inputSeenMap.emplace(it->first, true).second) + { + // this symbol hasn't been seen yet, what about the action name? + auto mappedTo = mappedToSeenMap.find(it->second); + if(mappedTo != mappedToSeenMap.cend()) + { + // yes, it has! + + // can we combine? (dpad only) + if((it->first == "up/down" && addPrompts.at(mappedTo->second).first != "left/right") || + (it->first == "left/right" && addPrompts.at(mappedTo->second).first != "up/down")) + { + // yes! + addPrompts.at(mappedTo->second).first = "up/down/left/right"; + // don't need to add this to addPrompts since we just merged + }else{ + // no, we can't combine! + addPrompts.push_back(*it); + } + }else{ + // no, it hasn't! + mappedToSeenMap.emplace(it->second, (int)addPrompts.size()); + addPrompts.push_back(*it); + } + } + } + + // sort prompts so it goes [dpad_all] [dpad_u/d] [dpad_l/r] [a/b/x/y/l/r] [start/select] + std::sort(addPrompts.begin(), addPrompts.end(), [](const HelpPrompt& a, const HelpPrompt& b) -> bool { + + static const char* map[] = { + "up/down/left/right", + "up/down", + "left/right", + "a", "b", "x", "y", "l", "r", + "start", "select", + NULL + }; + + int i = 0; + int aVal = 0; + int bVal = 0; + while(map[i] != NULL) + { + if(a.first == map[i]) + aVal = i; + if(b.first == map[i]) + bVal = i; + i++; + } + + return aVal > bVal; + }); + + mHelp->setPrompts(addPrompts); +} + + +void Window::onSleep() +{ + Scripting::fireEvent("sleep"); +} + +void Window::onWake() +{ + Scripting::fireEvent("wake"); +} + +bool Window::isProcessing() +{ + return count_if(mGuiStack.cbegin(), mGuiStack.cend(), [](GuiComponent* c) { return c->isProcessing(); }) > 0; +} + +void Window::startScreenSaver() +{ + if (mScreenSaver && !mRenderScreenSaver) + { + // Tell the GUI components the screensaver is starting + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + (*i)->onScreenSaverActivate(); + + mScreenSaver->startScreenSaver(); + mRenderScreenSaver = true; + } +} + +bool Window::cancelScreenSaver() +{ + if (mScreenSaver && mRenderScreenSaver) + { + mScreenSaver->stopScreenSaver(); + mRenderScreenSaver = false; + mScreenSaver->resetCounts(); + + // Tell the GUI components the screensaver has stopped + for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + (*i)->onScreenSaverDeactivate(); + + return true; + } + + return false; +} + +void Window::renderScreenSaver() +{ + if (mScreenSaver) + mScreenSaver->renderScreenSaver(); +} diff --git a/es-core/src/components/ButtonComponent.cpp b/es-core/src/components/ButtonComponent.cpp index 9496046419..08eeeb34bc 100644 --- a/es-core/src/components/ButtonComponent.cpp +++ b/es-core/src/components/ButtonComponent.cpp @@ -11,6 +11,13 @@ ButtonComponent::ButtonComponent(Window* window, const std::string& text, const mEnabled(true), mTextColorFocused(0xFFFFFFFF), mTextColorUnfocused(0x777777FF) { + auto menuTheme = ThemeData::getMenuTheme(); + + mFont = menuTheme->Text.font; + mTextColorUnfocused = menuTheme->Text.color; + mTextColorFocused = menuTheme->Text.selectedColor; + mColor = menuTheme->Text.color; + setPressedFunc(func); setText(text, helpText); updateImage(); @@ -71,6 +78,7 @@ void ButtonComponent::setEnabled(bool enabled) void ButtonComponent::updateImage() { + /* if(!mEnabled || !mPressedFunc) { mBox.setImagePath(":/button_filled.png"); @@ -78,9 +86,9 @@ void ButtonComponent::updateImage() mBox.setEdgeColor(0x770000FF); return; } - - mBox.setCenterColor(0xFFFFFFFF); - mBox.setEdgeColor(0xFFFFFFFF); + */ + mBox.setCenterColor(mColor); + mBox.setEdgeColor(mColor); mBox.setImagePath(mFocused ? ":/button_filled.png" : ":/button.png"); } diff --git a/es-core/src/components/ButtonComponent.h b/es-core/src/components/ButtonComponent.h index 65288209cd..790bbc6d43 100644 --- a/es-core/src/components/ButtonComponent.h +++ b/es-core/src/components/ButtonComponent.h @@ -38,7 +38,7 @@ class ButtonComponent : public GuiComponent bool mEnabled; unsigned int mTextColorFocused; unsigned int mTextColorUnfocused; - + unsigned int getCurTextColor() const; void updateImage(); @@ -46,6 +46,7 @@ class ButtonComponent : public GuiComponent std::string mHelpText; std::unique_ptr mTextCache; NinePatchComponent mBox; + unsigned int mColor; }; #endif // ES_CORE_COMPONENTS_BUTTON_COMPONENT_H diff --git a/es-core/src/components/ComponentGrid.cpp b/es-core/src/components/ComponentGrid.cpp index e89e7f9c4e..7ba5b0c1c3 100644 --- a/es-core/src/components/ComponentGrid.cpp +++ b/es-core/src/components/ComponentGrid.cpp @@ -5,11 +5,12 @@ using namespace GridFlags; -ComponentGrid::ComponentGrid(Window* window, const Vector2i& gridDimensions) : GuiComponent(window), +ComponentGrid::ComponentGrid(Window* window, const Vector2i& gridDimensions, unsigned int separatorColor) : GuiComponent(window), mGridSize(gridDimensions), mCursor(0, 0) { assert(gridDimensions.x() > 0 && gridDimensions.y() > 0); + mSeparatorColor = separatorColor; mCells.reserve(gridDimensions.x() * gridDimensions.y()); mColWidths = new float[gridDimensions.x()]; @@ -195,7 +196,7 @@ void ComponentGrid::updateSeparators() } mLineColors.reserve(mLines.size()); - Renderer::buildGLColorArray((GLubyte*)mLineColors.data(), 0xC6C7C6FF, (unsigned int)mLines.size()); + Renderer::buildGLColorArray((GLubyte*)mLineColors.data(), mSeparatorColor, (unsigned int)mLines.size()); } void ComponentGrid::onSizeChanged() diff --git a/es-core/src/components/ComponentGrid.h b/es-core/src/components/ComponentGrid.h index aa6519175f..15fe8e76df 100644 --- a/es-core/src/components/ComponentGrid.h +++ b/es-core/src/components/ComponentGrid.h @@ -29,9 +29,11 @@ namespace GridFlags class ComponentGrid : public GuiComponent { public: - ComponentGrid(Window* window, const Vector2i& gridDimensions); + ComponentGrid(Window* window, const Vector2i& gridDimensions, unsigned int separatorColor = 0xC6C7C6FF); virtual ~ComponentGrid(); + void setSeparatorColor(unsigned int separatorColor) { mSeparatorColor = separatorColor; updateSeparators(); } + bool removeEntry(const std::shared_ptr& comp); void setEntry(const std::shared_ptr& comp, const Vector2i& pos, bool canFocus, bool resize = true, @@ -105,6 +107,7 @@ class ComponentGrid : public GuiComponent std::vector mLines; std::vector mLineColors; + unsigned int mSeparatorColor; // Update position & size void updateCellComponent(const GridEntry& cell); diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index f8f1bcd518..4befbde724 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -135,7 +135,7 @@ void ComponentList::updateCameraOffset() { // move the camera to scroll const float totalHeight = getTotalRowHeight(); - if(totalHeight > mSize.y()) + if (totalHeight > mSize.y() && mCursor < mEntries.size()) { float target = mSelectorBarOffset + getRowHeight(mEntries.at(mCursor).data)/2 - (mSize.y() / 2); @@ -162,10 +162,17 @@ void ComponentList::render(const Transform4x4f& parentTrans) if(!size()) return; + auto menuTheme = ThemeData::getMenuTheme(); + unsigned int selectorColor = menuTheme->Text.selectorColor; + unsigned int selectedColor = menuTheme->Text.selectedColor; + unsigned int bgColor = menuTheme->Background.color; + unsigned int separatorColor = menuTheme->Text.separatorColor; + unsigned int textColor = menuTheme->Text.color; + Transform4x4f trans = parentTrans * getTransform(); trans.round(); - // clip everything to be inside our bounds + // clip everything to be inside our boundsz Vector3f dim(mSize.x(), mSize.y(), 0); dim = trans * dim - trans.translation(); Renderer::pushClipRect(Vector2i((int)trans.translation().x(), (int)trans.translation().y()), @@ -183,10 +190,11 @@ void ComponentList::render(const Transform4x4f& parentTrans) drawAll = !mFocused || i != (unsigned int)mCursor; for(auto it = entry.data.elements.cbegin(); it != entry.data.elements.cend(); it++) { - if(drawAll || it->invert_when_selected) + if (drawAll || it->invert_when_selected) { + it->component->setColor(textColor); it->component->render(trans); - }else{ + } else { drawAfterCursor.push_back(it->component.get()); } } @@ -196,7 +204,7 @@ void ComponentList::render(const Transform4x4f& parentTrans) Renderer::setMatrix(trans); // draw selector bar - if(mFocused) + if (mFocused) { // inversion: src * (1 - dst) + dst * 0 = where src = 1 // need a function that goes roughly 0x777777 -> 0xFFFFFF @@ -204,20 +212,33 @@ void ComponentList::render(const Transform4x4f& parentTrans) // (1 - dst) + 0x77 const float selectedRowHeight = getRowHeight(mEntries.at(mCursor).data); - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0xFFFFFFFF, - GL_ONE_MINUS_DST_COLOR, GL_ZERO); - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0x777777FF, - GL_ONE, GL_ONE); - + + if ((selectorColor != bgColor) && ((selectorColor & 0xFF) != 0x00)) { + //Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, GL_ONE_MINUS_DST_COLOR, GL_ZERO); + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, GL_ZERO, GL_ONE_MINUS_SRC_COLOR); + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, GL_ONE, GL_ONE); + } + + // Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0xFFFFFFFF, GL_ONE_MINUS_DST_COLOR, GL_ZERO); + // Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0x777777FF, GL_ONE, GL_ONE); + // hack to draw 2px dark on left/right of the bar - Renderer::drawRect(0.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF); - Renderer::drawRect(mSize.x() - 2.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF); + //Renderer::drawRect(0.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF); + //Renderer::drawRect(mSize.x() - 2.0f, mSelectorBarOffset, 2.0f, selectedRowHeight, 0x878787FF); + + auto& entry = mEntries.at(mCursor); + for (auto& element : entry.data.elements) + { + element.component->setColor(selectedColor); + drawAfterCursor.push_back(element.component.get()); + } + for(auto it = drawAfterCursor.cbegin(); it != drawAfterCursor.cend(); it++) (*it)->render(trans); // reset matrix if one of these components changed it - if(drawAfterCursor.size()) + if (drawAfterCursor.size()) Renderer::setMatrix(trans); } @@ -225,10 +246,10 @@ void ComponentList::render(const Transform4x4f& parentTrans) float y = 0; for(unsigned int i = 0; i < mEntries.size(); i++) { - Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, 0xC6C7C6FF); + Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, separatorColor); y += getRowHeight(mEntries.at(i).data); } - Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, 0xC6C7C6FF); + Renderer::drawRect(0.0f, y, mSize.x(), 1.0f, separatorColor); Renderer::popClipRect(); } diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index cccf549cfe..ae85435063 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -13,6 +13,8 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mSelectedZoomPercent = 1.0f; mAnimPosition = Vector3f(0, 0); + mLabelMerged = false; + mDefaultProperties.mSize = getDefaultTileSize(); mDefaultProperties.mPadding = Vector2f(16.0f, 16.0f); mDefaultProperties.mImageColor = 0xFFFFFFDD; // 0xAAAAAABB; @@ -57,11 +59,20 @@ void GridTileComponent::resize() setSize(currentProperties.mSize); float height = currentProperties.mSize.y() * mLabelSize.y(); - if (!mLabelVisible) - height = 0; - mLabel.setPosition(0, mSize.y() - height); - mLabel.setSize(currentProperties.mSize.x(), height); + if (mLabelMerged) + { + mLabel.setPosition(currentProperties.mPadding.x(), mSize.y() - height - currentProperties.mPadding.y()); + mLabel.setSize(currentProperties.mSize.x() - 2 * currentProperties.mPadding.x(), height); + } + else + { + mLabel.setPosition(0, mSize.y() - height); + mLabel.setSize(currentProperties.mSize.x(), height); + } + + if (!mLabelVisible || mLabelMerged) + height = 0; if (mLabelSize.x() == 0) height = 0; @@ -90,7 +101,8 @@ void GridTileComponent::resize() } else mBackground.setPosition(0, 0); - + + if (mImage != NULL) { mImage->setOrigin(0.5f, 0.5f); @@ -106,8 +118,11 @@ void GridTileComponent::resize() + if (!mLabelMerged && currentProperties.mImageSizeMode == "minSize") + mBackground.setSize(currentProperties.mSize.x(), currentProperties.mSize.y() - bottomPadding); + else + mBackground.setSize(currentProperties.mSize); - mBackground.setSize(currentProperties.mSize); mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); } @@ -118,14 +133,26 @@ void GridTileComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = getTransform() * parentTrans; + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + //Renderer::setMatrix(trans); //Renderer::drawRect(0.f, 0.f, mSize.x(), mSize.y(), 0xFF0000FF); - - mBackground.render(trans); + + if (mBackground.getCornerSize().x() == 0) + { + + Renderer::setMatrix(trans); + Renderer::drawRect(mBackground.getPosition().x(), mBackground.getPosition().y(), mBackground.getSize().x(), mBackground.getSize().y(), mBackground.getCenterColor()); + Renderer::setMatrix(parentTrans); + } + else + mBackground.render(trans); if (mImage != NULL) mImage->render(trans); - + if (mLabelVisible) mLabel.render(trans); } @@ -155,7 +182,7 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons // Apply theme to the default gridtile const ThemeData::ThemeElement* elem = theme->getElement(view, "default", "gridtile"); if (elem) - { + { if (elem->has("size")) mDefaultProperties.mSize = elem->get("size") * screen; @@ -232,7 +259,10 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem != NULL) { if (elem && elem->has("size")) + { mLabelSize = elem->get("size"); + mLabelMerged = mLabelSize.x() == 0; + } mLabelVisible = true; mLabel.applyTheme(theme, view, element, properties); @@ -264,7 +294,7 @@ void GridTileComponent::setImage(const std::string& path, std::string name) { if (mCurrentPath != path) { - mCurrentPath = path; + mCurrentPath = path; mImage->setImage(path); } diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index bdaa611a16..46fc6c4364 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -49,6 +49,7 @@ class GridTileComponent : public GuiComponent Vector2f mLabelSize; bool mLabelVisible; + bool mLabelMerged; NinePatchComponent mBackground; diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 8358ee6839..3ec03372de 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -25,6 +25,7 @@ ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : G mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f) { updateColors(); + mSizeChanged = false; } ImageComponent::~ImageComponent() @@ -33,17 +34,22 @@ ImageComponent::~ImageComponent() void ImageComponent::resize() { - if(!mTexture) + if (!mTexture) return; const Vector2f textureSize = mTexture->getSourceImageSize(); - if(textureSize == Vector2f::Zero()) + if (textureSize == Vector2f::Zero()) return; - if(mTexture->isTiled()) + int ox = mSize.x(); + int oy = mSize.y(); + + if (mTexture->isTiled()) { mSize = mTargetSize; - }else{ + } + else + { // SVG rasterization is determined by height (see SVGResource.cpp), and rasterization is done in terms of pixels // if rounding is off enough in the rasterization step (for images with extreme aspect ratios), it can cause cutoff when the aspect ratio breaks // so, we always make sure the resultant height is an integer to make sure cutoff doesn't happen, and scale width from that @@ -115,8 +121,12 @@ void ImageComponent::resize() mSize[0] = Math::round(mSize.x()); mSize[1] = Math::round(mSize.y()); + + if (ox != mSize.x() || oy != mSize.y()) + mSizeChanged = true; + // mSize.y() should already be rounded - mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); + //mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); onSizeChanged(); } @@ -323,13 +333,23 @@ void ImageComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); Renderer::setMatrix(trans); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + if (mTexture && mOpacity > 0) { + if (mSizeChanged) + { + mSizeChanged = false; + mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); + } + if (Settings::getInstance()->getBool("DebugImage")) { Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; Renderer::drawRect(targetSizePos.x(), targetSizePos.y(), mTargetSize.x(), mTargetSize.y(), 0xFF000033); - Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); } if (mTexture->isInitialized()) diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 7a93a0d851..3f5ce3138a 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -57,6 +57,8 @@ class ImageComponent : public GuiComponent // Multiply all pixels in the image by this color when rendering. void setColorShift(unsigned int color); + virtual void setColor(unsigned int color) { setColorShift(color); } + void setFlipX(bool flip); // Mirror on the X axis. void setFlipY(bool flip); // Mirror on the Y axis. @@ -108,6 +110,8 @@ class ImageComponent : public GuiComponent Vector2f mTopLeftCrop; Vector2f mBottomRightCrop; + + bool mSizeChanged; }; #endif // ES_CORE_COMPONENTS_IMAGE_COMPONENT_H diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 343c86f618..f891c04490 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -10,16 +10,20 @@ MenuComponent::MenuComponent(Window* window, std::string title, const std::shared_ptr& titleFont) : GuiComponent(window), mBackground(window), mGrid(window, Vector2i(1, 3)) { + auto theme = ThemeData::getMenuTheme(); + addChild(&mBackground); addChild(&mGrid); - - mBackground.setImagePath(":/frame.png"); + + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); // set up title mTitle = std::make_shared(mWindow); mTitle->setHorizontalAlignment(ALIGN_CENTER); - mTitle->setColor(0x555555FF); - setTitle(title.c_str(), titleFont); + mTitle->setColor(theme->Title.color); // 0x555555FF + setTitle(title.c_str(), theme->Title.font); // titleFont mGrid.setEntry(mTitle, Vector2i(0, 0), false); // set up list which will never change (externally, anyway) @@ -32,6 +36,16 @@ MenuComponent::MenuComponent(Window* window, std::string title, const std::share mGrid.resetCursor(); } +void MenuComponent::addWithLabel(const std::string& label, const std::shared_ptr& comp, bool setCursorHere, bool invert_when_selected) +{ + auto theme = ThemeData::getMenuTheme(); + + ComponentListRow row; + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(label), theme->Text.font, theme->Text.color), true); + row.addElement(comp, false, invert_when_selected); + addRow(row, setCursorHere); +} + void MenuComponent::setTitle(const char* title, const std::shared_ptr& font) { mTitle->setText(Utils::String::toUpper(title)); @@ -40,7 +54,9 @@ void MenuComponent::setTitle(const char* title, const std::shared_ptr& fon float MenuComponent::getButtonGridHeight() const { - return (mButtonGrid ? mButtonGrid->getSize().y() : Font::get(FONT_SIZE_MEDIUM)->getHeight() + BUTTON_GRID_VERT_PADDING); + auto menuTheme = ThemeData::getMenuTheme(); + return (mButtonGrid ? mButtonGrid->getSize().y() : menuTheme->Text.font->getHeight() + BUTTON_GRID_VERT_PADDING); + //return (mButtonGrid ? mButtonGrid->getSize().y() : Font::get(FONT_SIZE_MEDIUM)->getHeight() + BUTTON_GRID_VERT_PADDING); } void MenuComponent::updateSize() @@ -126,8 +142,12 @@ std::shared_ptr makeButtonGrid(Window* window, const std::vector< std::shared_ptr makeArrow(Window* window) { + auto menuTheme = ThemeData::getMenuTheme(); + auto bracket = std::make_shared(window); bracket->setImage(":/arrow.svg"); - bracket->setResize(0, Math::round(Font::get(FONT_SIZE_MEDIUM)->getLetterHeight())); + bracket->setColorShift(menuTheme->Text.color); + bracket->setResize(0, round(menuTheme->Text.font->getLetterHeight())); + //bracket->setResize(0, Math::round(Font::get(FONT_SIZE_MEDIUM)->getLetterHeight())); return bracket; } diff --git a/es-core/src/components/MenuComponent.h b/es-core/src/components/MenuComponent.h index e2c9ea31f2..d5a8db473c 100644 --- a/es-core/src/components/MenuComponent.h +++ b/es-core/src/components/MenuComponent.h @@ -26,13 +26,7 @@ class MenuComponent : public GuiComponent inline void addRow(const ComponentListRow& row, bool setCursorHere = false) { mList->addRow(row, setCursorHere); updateSize(); } - inline void addWithLabel(const std::string& label, const std::shared_ptr& comp, bool setCursorHere = false, bool invert_when_selected = true) - { - ComponentListRow row; - row.addElement(std::make_shared(mWindow, Utils::String::toUpper(label), Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - row.addElement(comp, false, invert_when_selected); - addRow(row, setCursorHere); - } + void addWithLabel(const std::string& label, const std::shared_ptr& comp, bool setCursorHere = false, bool invert_when_selected = true); void addButton(const std::string& label, const std::string& helpText, const std::function& callback); diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index fedf95115b..f3ff0bba01 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -106,6 +106,10 @@ void NinePatchComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); trans.round(); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + if(mTexture && mVertices != NULL) { Renderer::setMatrix(trans); @@ -166,18 +170,27 @@ void NinePatchComponent::fitTo(Vector2f size, Vector3f position, Vector2f paddin void NinePatchComponent::setImagePath(const std::string& path) { + if (mPath == path) + return; + mPath = path; buildVertices(); } void NinePatchComponent::setEdgeColor(unsigned int edgeColor) { + if (mEdgeColor == edgeColor) + return; + mEdgeColor = edgeColor; updateColors(); } void NinePatchComponent::setCenterColor(unsigned int centerColor) { + if (mCenterColor == centerColor) + return; + mCenterColor = centerColor; updateColors(); } diff --git a/es-core/src/components/NinePatchComponent.h b/es-core/src/components/NinePatchComponent.h index af9e3b9343..8d0dbf697e 100644 --- a/es-core/src/components/NinePatchComponent.h +++ b/es-core/src/components/NinePatchComponent.h @@ -35,6 +35,8 @@ class NinePatchComponent : public GuiComponent void setEdgeColor(unsigned int edgeColor); // Apply a color shift to the "edge" parts of the ninepatch. void setCenterColor(unsigned int centerColor); // Apply a color shift to the "center" part of the ninepatch. + unsigned int getCenterColor() { return mCenterColor; }; + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; const Vector2f& getCornerSize() const; diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index f2d36950d0..e1dc87eb7a 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -39,7 +39,10 @@ class OptionListComponent : public GuiComponent OptionListPopup(Window* window, OptionListComponent* parent, const std::string& title) : GuiComponent(window), mMenu(window, title.c_str()), mParent(parent) { - auto font = Font::get(FONT_SIZE_MEDIUM); + auto menuTheme = ThemeData::getMenuTheme(); + auto font = menuTheme->Text.font; + auto color = menuTheme->Text.color; + ComponentListRow row; // for select all/none @@ -48,7 +51,7 @@ class OptionListComponent : public GuiComponent for(auto it = mParent->mEntries.begin(); it != mParent->mEntries.end(); it++) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, 0x777777FF), true); + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, color), true); OptionListData& e = *it; @@ -87,11 +90,11 @@ class OptionListComponent : public GuiComponent mMenu.addRow(row, (!mParent->mMultiSelect && it->selected)); } - mMenu.addButton(_T("BACK"), "accept", [this] { delete this; }); + mMenu.addButton(_T("BACK"), _T("accept"), [this] { delete this; }); if(mParent->mMultiSelect) { - mMenu.addButton("SELECT ALL", "select all", [this, checkboxes] { + mMenu.addButton(_T("SELECT ALL"), _T("SELECT ALL"), [this, checkboxes] { for(unsigned int i = 0; i < mParent->mEntries.size(); i++) { mParent->mEntries.at(i).selected = true; @@ -100,7 +103,7 @@ class OptionListComponent : public GuiComponent mParent->onSelectedChanged(); }); - mMenu.addButton("SELECT NONE", "select none", [this, checkboxes] { + mMenu.addButton(_T("SELECT NONE"), _T("SELECT NONE"), [this, checkboxes] { for(unsigned int i = 0; i < mParent->mEntries.size(); i++) { mParent->mEntries.at(i).selected = false; @@ -110,7 +113,10 @@ class OptionListComponent : public GuiComponent }); } - mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); + mMenu.setPosition( + (Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, + (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); + //Renderer::getScreenHeight() * 0.15f); addChild(&mMenu); } @@ -137,29 +143,42 @@ class OptionListComponent : public GuiComponent OptionListComponent(Window* window, const std::string& name, bool multiSelect = false) : GuiComponent(window), mMultiSelect(multiSelect), mName(name), mText(window), mLeftArrow(window), mRightArrow(window) { - auto font = Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT); - mText.setFont(font); - mText.setColor(0x777777FF); + auto theme = ThemeData::getMenuTheme(); + + // auto font = Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT); + mText.setFont(theme->Text.font); // font + mText.setColor(theme->Text.color); // 0x777777FF mText.setHorizontalAlignment(ALIGN_CENTER); addChild(&mText); mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); - if(mMultiSelect) + if (mMultiSelect) { mRightArrow.setImage(":/arrow.svg"); + mRightArrow.setColorShift(theme->Text.color); addChild(&mRightArrow); - }else{ + } else { mLeftArrow.setImage(":/option_arrow.svg"); + mLeftArrow.setColorShift(theme->Text.color); mLeftArrow.setFlipX(true); addChild(&mLeftArrow); mRightArrow.setImage(":/option_arrow.svg"); + mLeftArrow.setColorShift(theme->Text.color); addChild(&mRightArrow); } - setSize(mLeftArrow.getSize().x() + mRightArrow.getSize().x(), font->getHeight()); + setSize(mLeftArrow.getSize().x() + mRightArrow.getSize().x(), theme->Text.font->getHeight()); + } + + + virtual void setColor(unsigned int color) + { + mText.setColor(color); + mLeftArrow.setColorShift(color); + mRightArrow.setColorShift(color); } // handles positioning/resizing of text and arrows diff --git a/es-core/src/components/SliderComponent.cpp b/es-core/src/components/SliderComponent.cpp index 81f97e9b09..a8e9657100 100644 --- a/es-core/src/components/SliderComponent.cpp +++ b/es-core/src/components/SliderComponent.cpp @@ -14,10 +14,20 @@ SliderComponent::SliderComponent(Window* window, float min, float max, float inc // some sane default value mValue = (max + min) / 2; + auto menuTheme = ThemeData::getMenuTheme(); + mColor = menuTheme->Text.color; + mKnob.setOrigin(0.5f, 0.5f); mKnob.setImage(":/slider_knob.svg"); - - setSize(Renderer::getScreenWidth() * 0.15f, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight()); + mKnob.setColorShift(mColor); + + setSize(Renderer::getScreenWidth() * 0.15f, menuTheme->Text.font->getLetterHeight()); +} + +void SliderComponent::setColor(unsigned int color) { + mColor = color; + mKnob.setColorShift(mColor); + onValueChanged(); } bool SliderComponent::input(InputConfig* config, Input input) @@ -73,7 +83,7 @@ void SliderComponent::render(const Transform4x4f& parentTrans) //render line const float lineWidth = 2; - Renderer::drawRect(mKnob.getSize().x() / 2, mSize.y() / 2 - lineWidth / 2, width, lineWidth, 0x777777FF); + Renderer::drawRect(mKnob.getSize().x() / 2, mSize.y() / 2 - lineWidth / 2, width, lineWidth, mColor); //render knob mKnob.render(trans); @@ -108,7 +118,7 @@ void SliderComponent::onSizeChanged() void SliderComponent::onValueChanged() { // update suffix textcache - if(mFont) + if (mFont) { std::stringstream ss; ss << std::fixed; @@ -126,7 +136,7 @@ void SliderComponent::onValueChanged() const std::string max = ss.str(); Vector2f textSize = mFont->sizeText(max); - mValueCache = std::shared_ptr(mFont->buildTextCache(val, mSize.x() - textSize.x(), (mSize.y() - textSize.y()) / 2, 0x777777FF)); + mValueCache = std::shared_ptr(mFont->buildTextCache(val, mSize.x() - textSize.x(), (mSize.y() - textSize.y()) / 2, mColor)); mValueCache->metrics.size[0] = textSize.x(); // fudge the width } @@ -139,6 +149,6 @@ void SliderComponent::onValueChanged() std::vector SliderComponent::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("left/right", "MODIFIER")); + prompts.push_back(HelpPrompt("left/right", _T("CHANGE"))); return prompts; } diff --git a/es-core/src/components/SliderComponent.h b/es-core/src/components/SliderComponent.h index e4297d5b7b..c74d7e7d2e 100644 --- a/es-core/src/components/SliderComponent.h +++ b/es-core/src/components/SliderComponent.h @@ -25,6 +25,7 @@ class SliderComponent : public GuiComponent void onSizeChanged() override; virtual std::vector getHelpPrompts() override; + virtual void setColor(unsigned int color); private: void onValueChanged(); @@ -40,6 +41,8 @@ class SliderComponent : public GuiComponent std::string mSuffix; std::shared_ptr mFont; std::shared_ptr mValueCache; + + unsigned int mColor; }; #endif // ES_CORE_COMPONENTS_SLIDER_COMPONENT_H diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index 747e507fc3..27ba63294e 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -5,11 +5,19 @@ SwitchComponent::SwitchComponent(Window* window, bool state) : GuiComponent(window), mImage(window), mState(state) { + auto menuTheme = ThemeData::getMenuTheme(); + mImage.setImage(":/off.svg"); mImage.setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight()); + mImage.setColorShift(menuTheme->Text.color); + mSize = mImage.getSize(); } +void SwitchComponent::setColor(unsigned int color) { + mImage.setColorShift(color); +} + void SwitchComponent::onSizeChanged() { mImage.setSize(mSize); diff --git a/es-core/src/components/SwitchComponent.h b/es-core/src/components/SwitchComponent.h index 2e2d83f6b4..6cc981fb99 100644 --- a/es-core/src/components/SwitchComponent.h +++ b/es-core/src/components/SwitchComponent.h @@ -23,6 +23,8 @@ class SwitchComponent : public GuiComponent virtual std::vector getHelpPrompts() override; + void setColor(unsigned int color); + private: void onStateChanged(); diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index d1fc995f40..2800b41eaa 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -95,15 +95,22 @@ void TextComponent::setUppercase(bool uppercase) void TextComponent::render(const Transform4x4f& parentTrans) { + if (mColorOpacity == 0) + return; + Transform4x4f trans = parentTrans * getTransform(); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + if (mRenderBackground) { Renderer::setMatrix(trans); Renderer::drawRect(0.f, 0.f, mSize.x(), mSize.y(), mBgColor); } - - if(mTextCache) + + if (mTextCache) { const Vector2f& textSize = mTextCache->metrics.size; float yOff = 0; @@ -116,7 +123,7 @@ void TextComponent::render(const Transform4x4f& parentTrans) yOff = (getSize().y() - textSize.y()); break; case ALIGN_CENTER: - yOff = (getSize().y() - textSize.y()) / 2.0f; + yOff = (int) (getSize().y() - textSize.y()) / 2.0f; break; } Vector3f off(0, yOff, 0); @@ -148,6 +155,7 @@ void TextComponent::render(const Transform4x4f& parentTrans) break; } } + mFont->renderTextCache(mTextCache.get()); } } diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index 1721105772..e472cec335 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -23,7 +23,7 @@ class TextComponent : public GuiComponent void setUppercase(bool uppercase); void onSizeChanged() override; void setText(const std::string& text); - void setColor(unsigned int color); + virtual void setColor(unsigned int color); void setHorizontalAlignment(Alignment align); void setVerticalAlignment(Alignment align); void setLineSpacing(float spacing); diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 91f7532e40..ea712c0a60 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -135,6 +135,7 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) float x, y; Transform4x4f trans = parentTrans * getTransform(); + GuiComponent::renderChildren(trans); Renderer::setMatrix(trans); diff --git a/es-core/src/guis/GuiDetectDevice.cpp b/es-core/src/guis/GuiDetectDevice.cpp index faadbf4ac3..cd92f1664e 100644 --- a/es-core/src/guis/GuiDetectDevice.cpp +++ b/es-core/src/guis/GuiDetectDevice.cpp @@ -14,6 +14,12 @@ GuiDetectDevice::GuiDetectDevice(Window* window, bool firstRun, const std::function& doneCallback) : GuiComponent(window), mFirstRun(firstRun), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 5)) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + mGrid.setSeparatorColor(theme->Text.separatorColor); + mHoldingConfig = NULL; mHoldTime = 0; mDoneCallback = doneCallback; @@ -23,9 +29,10 @@ GuiDetectDevice::GuiDetectDevice(Window* window, bool firstRun, const std::funct // title mTitle = std::make_shared(mWindow, firstRun ? "WELCOME" : "CONFIGURE INPUT", - Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); - mGrid.setEntry(mTitle, Vector2i(0, 0), false, true, Vector2i(1, 1), GridFlags::BORDER_BOTTOM); + ThemeData::getMenuTheme()->Title.font, ThemeData::getMenuTheme()->Title.color, ALIGN_CENTER); + mGrid.setEntry(mTitle, Vector2i(0, 0), false, true, Vector2i(1, 1), GridFlags::BORDER_BOTTOM); + // device info std::stringstream deviceInfo; int numDevices = InputManager::getInstance()->getNumJoysticks(); @@ -34,15 +41,15 @@ GuiDetectDevice::GuiDetectDevice(Window* window, bool firstRun, const std::funct deviceInfo << numDevices << " GAMEPAD" << (numDevices > 1 ? "S" : "") << " DETECTED"; else deviceInfo << "NO GAMEPADS DETECTED"; - mDeviceInfo = std::make_shared(mWindow, deviceInfo.str(), Font::get(FONT_SIZE_SMALL), 0x999999FF, ALIGN_CENTER); + mDeviceInfo = std::make_shared(mWindow, deviceInfo.str(), ThemeData::getMenuTheme()->TextSmall.font, 0x999999FF, ALIGN_CENTER); mGrid.setEntry(mDeviceInfo, Vector2i(0, 1), false, true); // message - mMsg1 = std::make_shared(mWindow, "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT.", Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER); + mMsg1 = std::make_shared(mWindow, "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT.", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color, ALIGN_CENTER); mGrid.setEntry(mMsg1, Vector2i(0, 2), false, true); const char* msg2str = firstRun ? "PRESS F4 TO QUIT AT ANY TIME." : "PRESS ESC TO CANCEL."; - mMsg2 = std::make_shared(mWindow, msg2str, Font::get(FONT_SIZE_SMALL), 0x777777FF, ALIGN_CENTER); + mMsg2 = std::make_shared(mWindow, msg2str, ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color, ALIGN_CENTER); mGrid.setEntry(mMsg2, Vector2i(0, 3), false, true); // currently held device diff --git a/es-core/src/guis/GuiInputConfig.cpp b/es-core/src/guis/GuiInputConfig.cpp index 3ec21f101f..52cf142de2 100755 --- a/es-core/src/guis/GuiInputConfig.cpp +++ b/es-core/src/guis/GuiInputConfig.cpp @@ -51,9 +51,15 @@ static const InputConfigStructure GUI_INPUT_CONFIG_LIST[inputCount] = #define HOLD_TO_SKIP_MS 1000 GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfigureAll, const std::function& okCallback) : GuiComponent(window), - mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 7)), + mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 7)), mTargetConfig(target), mHoldingInput(false), mBusyAnim(window) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + mGrid.setSeparatorColor(theme->Text.separatorColor); + LOG(LogInfo) << "Configuring device " << target->getDeviceId() << " (" << target->getDeviceName() << ")."; if(reconfigureAll) @@ -95,7 +101,7 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi // icon auto icon = std::make_shared(mWindow); icon->setImage(GUI_INPUT_CONFIG_LIST[i].icon); - icon->setColorShift(0x777777FF); + icon->setColorShift(ThemeData::getMenuTheme()->Text.color); icon->setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight() * 1.25f); row.addElement(icon, false); @@ -104,7 +110,7 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi spacer->setSize(16, 0); row.addElement(spacer, false); - auto text = std::make_shared(mWindow, GUI_INPUT_CONFIG_LIST[i].dispName, Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + auto text = std::make_shared(mWindow, GUI_INPUT_CONFIG_LIST[i].dispName, ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); row.addElement(text, true); auto mapping = std::make_shared(mWindow, "-NOT DEFINED-", Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT), 0x999999FF, ALIGN_RIGHT); @@ -258,7 +264,7 @@ void GuiInputConfig::update(int deltaTime) std::stringstream ss; ss << "HOLD FOR " << HOLD_TO_SKIP_MS/1000 - curSec << "S TO SKIP"; text->setText(ss.str()); - text->setColor(0x777777FF); + text->setColor(ThemeData::getMenuTheme()->Text.color); } } } @@ -301,7 +307,7 @@ void GuiInputConfig::setNotDefined(const std::shared_ptr& text) void GuiInputConfig::setAssignedTo(const std::shared_ptr& text, Input input) { text->setText(Utils::String::toUpper(input.string())); - text->setColor(0x777777FF); + text->setColor(ThemeData::getMenuTheme()->Text.color); } void GuiInputConfig::error(const std::shared_ptr& text, const std::string& /*msg*/) diff --git a/es-core/src/guis/GuiMsgBox.cpp b/es-core/src/guis/GuiMsgBox.cpp index 7f05603200..843a844a48 100644 --- a/es-core/src/guis/GuiMsgBox.cpp +++ b/es-core/src/guis/GuiMsgBox.cpp @@ -11,10 +11,16 @@ GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, const std::string& name3, const std::function& func3) : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 2)) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + float width = Renderer::getScreenWidth() * 0.6f; // max width float minWidth = Renderer::getScreenWidth() * 0.3f; // minimum width - mMsg = std::make_shared(mWindow, text, Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); + + mMsg = std::make_shared(mWindow, text, ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color, ALIGN_CENTER); mGrid.setEntry(mMsg, Vector2i(0, 0), false, false); // create the buttons diff --git a/es-core/src/guis/GuiTextEditPopup.cpp b/es-core/src/guis/GuiTextEditPopup.cpp index de7390cbb8..1244ff0871 100644 --- a/es-core/src/guis/GuiTextEditPopup.cpp +++ b/es-core/src/guis/GuiTextEditPopup.cpp @@ -8,6 +8,11 @@ GuiTextEditPopup::GuiTextEditPopup(Window* window, const std::string& title, con const std::function& okCallback, bool multiLine, const char* acceptBtnText) : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 3)), mMultiLine(multiLine) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + addChild(&mBackground); addChild(&mGrid); diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index 9890954a82..fe2a1b6a08 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -22,6 +22,7 @@ Font::FontFace::FontFace(ResourceData&& d, int size) : data(d) Font::FontFace::~FontFace() { + if(face) FT_Done_Face(face); } @@ -74,9 +75,12 @@ Font::Font(int size, const std::string& path) : mSize(size), mPath(path) mMaxGlyphHeight = 0; - if(!sLibrary) + if (!sLibrary) initLibrary(); + for (unsigned int i = 0; i < 255; i++) + mGlyphCacheArray[i] = NULL; + // always initialize ASCII characters for (unsigned int i = 32; i < 128; i++) getGlyph(i); @@ -88,6 +92,9 @@ Font::Font(int size, const std::string& path) : mSize(size), mPath(path) Font::~Font() { + for (auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); it++) + delete it->second; + unload(ResourceManager::getInstance()); } @@ -316,10 +323,20 @@ void Font::clearFaceCache() Font::Glyph* Font::getGlyph(unsigned int id) { - // is it already loaded? - auto it = mGlyphMap.find(id); - if (it != mGlyphMap.cend()) - return &it->second; + if (id < 255) + { + // FCA : optimisation : array is always really fastest than a map + Glyph* fastCache = mGlyphCacheArray[id]; + if (fastCache != NULL) + return fastCache; + } + else + { + // is it already loaded? + auto it = mGlyphMap.find(id); + if (it != mGlyphMap.cend()) + return it->second; + } // nope, need to make a glyph FT_Face face = getFaceForChar(id); @@ -351,15 +368,13 @@ Font::Glyph* Font::getGlyph(unsigned int id) } // create glyph - Glyph& glyph = mGlyphMap[id]; + Glyph* pGlyph = new Glyph(); - glyph.texture = tex; - - glyph.texPos = Vector2f(cursor.x() / (float)tex->textureSize.x(), cursor.y() / (float)tex->textureSize.y()); - glyph.texSize = Vector2f(glyphSize.x() / (float)tex->textureSize.x(), glyphSize.y() / (float)tex->textureSize.y()); - - glyph.advance = Vector2f((float)g->metrics.horiAdvance / 64.0f, (float)g->metrics.vertAdvance / 64.0f); - glyph.bearing = Vector2f((float)g->metrics.horiBearingX / 64.0f, (float)g->metrics.horiBearingY / 64.0f); + pGlyph->texture = tex; + pGlyph->texPos = Vector2f(cursor.x() / (float)tex->textureSize.x(), cursor.y() / (float)tex->textureSize.y()); + pGlyph->texSize = Vector2f(glyphSize.x() / (float)tex->textureSize.x(), glyphSize.y() / (float)tex->textureSize.y()); + pGlyph->advance = Vector2f((float)g->metrics.horiAdvance / 64.0f, (float)g->metrics.vertAdvance / 64.0f); + pGlyph->bearing = Vector2f((float)g->metrics.horiBearingX / 64.0f, (float)g->metrics.horiBearingY / 64.0f); // upload glyph bitmap to texture glBindTexture(GL_TEXTURE_2D, tex->textureId); @@ -370,8 +385,12 @@ Font::Glyph* Font::getGlyph(unsigned int id) if (id != 61446 && glyphSize.y() > mMaxGlyphHeight) mMaxGlyphHeight = glyphSize.y(); - // done - return &glyph; + mGlyphMap[id] = pGlyph; + + if (id < 255) + mGlyphCacheArray[id] = pGlyph; + + return pGlyph; } // completely recreate the texture data for all textures based on mGlyphs information @@ -379,9 +398,7 @@ void Font::rebuildTextures() { // recreate OpenGL textures for(auto it = mTextures.begin(); it != mTextures.end(); it++) - { it->initTexture(); - } // reupload the texture data for(auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); it++) @@ -392,11 +409,11 @@ void Font::rebuildTextures() // load the glyph bitmap through FT FT_Load_Char(face, it->first, FT_LOAD_RENDER); - FontTexture* tex = it->second.texture; + FontTexture* tex = it->second->texture; // find the position/size - Vector2i cursor((int)(it->second.texPos.x() * tex->textureSize.x()), (int)(it->second.texPos.y() * tex->textureSize.y())); - Vector2i glyphSize((int)(it->second.texSize.x() * tex->textureSize.x()), (int)(it->second.texSize.y() * tex->textureSize.y())); + Vector2i cursor((int)(it->second->texPos.x() * tex->textureSize.x()), (int)(it->second->texPos.y() * tex->textureSize.y())); + Vector2i glyphSize((int)(it->second->texSize.x() * tex->textureSize.x()), (int)(it->second->texSize.y() * tex->textureSize.y())); // upload to texture glBindTexture(GL_TEXTURE_2D, tex->textureId); @@ -708,18 +725,26 @@ void TextCache::setColor(unsigned int color) std::shared_ptr Font::getFromTheme(const ThemeData::ThemeElement* elem, unsigned int properties, const std::shared_ptr& orig) { using namespace ThemeFlags; - if(!(properties & FONT_PATH) && !(properties & FONT_SIZE)) + if (!(properties & FONT_PATH) && !(properties & FONT_SIZE)) return orig; - + std::shared_ptr font; int size = (orig ? orig->mSize : FONT_SIZE_MEDIUM); std::string path = (orig ? orig->mPath : getDefaultPath()); float sh = (float)Renderer::getScreenHeight(); - if(properties & FONT_SIZE && elem->has("fontSize")) - size = (int)(sh * elem->get("fontSize")); - if(properties & FONT_PATH && elem->has("fontPath")) - path = elem->get("fontPath"); + if (properties & FONT_SIZE && elem->has("fontSize")) + { + if ((int)(sh * elem->get("fontSize")) > 0) + size = (int)(sh * elem->get("fontSize")); + } + + if (properties & FONT_PATH && elem->has("fontPath")) + { + std::string tmppath = elem->get("fontPath"); + if (ResourceManager::getInstance()->fileExists(tmppath)) + path = tmppath; + } return get(size, path); } diff --git a/es-core/src/resources/Font.h b/es-core/src/resources/Font.h index 585c892461..c9c81af643 100644 --- a/es-core/src/resources/Font.h +++ b/es-core/src/resources/Font.h @@ -120,7 +120,11 @@ class Font : public IReloadable Vector2f bearing; }; - std::map mGlyphMap; + // used to cache 255 first chars + Glyph* mGlyphCacheArray[255]; + + // used to cache every char + std::map mGlyphMap; Glyph* getGlyph(unsigned int id); diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 6c472c79ed..48aae8932a 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -98,8 +98,10 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng return true; } - std::vector imageRGBA = ImageIO::loadFromMemoryRGBA32((const unsigned char*)(fileData), length, width, height); - if (imageRGBA.size() == 0) + unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height); + +// std::vector imageRGBA = ImageIO::loadFromMemoryRGBA32((const unsigned char*)(fileData), length, width, height); + if (imageRGBA == NULL) { LOG(LogError) << "Could not initialize texture from memory, invalid data! (file path: " << mPath << ", data ptr: " << (size_t)fileData << ", reported size: " << length << ")"; return false; @@ -109,7 +111,7 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng mSourceHeight = (float) height; mScalable = false; - return initFromRGBA(imageRGBA.data(), width, height); + return initFromRGBAEx(imageRGBA, width, height); } bool TextureData::initFromRGBA(const unsigned char* dataRGBA, size_t width, size_t height) @@ -127,6 +129,20 @@ bool TextureData::initFromRGBA(const unsigned char* dataRGBA, size_t width, size return true; } +bool TextureData::initFromRGBAEx(unsigned char* dataRGBA, size_t width, size_t height) +{ + // If already initialised then don't read again + std::unique_lock lock(mMutex); + if (mDataRGBA) + return true; + + // Take a copy + mDataRGBA = dataRGBA; + mWidth = width; + mHeight = height; + return true; +} + bool TextureData::load() { bool retval = false; diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 3ac6f239ef..2355d439a9 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -22,6 +22,7 @@ class TextureData bool initSVGFromMemory(const unsigned char* fileData, size_t length); bool initImageFromMemory(const unsigned char* fileData, size_t length); bool initFromRGBA(const unsigned char* dataRGBA, size_t width, size_t height); + bool initFromRGBAEx(unsigned char* dataRGBA, size_t width, size_t height); // Read the data into memory if necessary bool load(); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 83b5270cb1..150a6191a2 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -1,667 +1,774 @@ -#define _FILE_OFFSET_BITS 64 - -#include "utils/FileSystemUtil.h" - -#include "Settings.h" -#include -#include - -#if defined(_WIN32) -// because windows... -#include -#include -#define getcwd _getcwd -#define mkdir(x,y) _mkdir(x) -#define snprintf _snprintf -#define stat64 _stat64 -#define unlink _unlink -#define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) -#define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) -#else // _WIN32 -#include -#include -#endif // _WIN32 - -namespace Utils -{ - namespace FileSystem - { - -#if defined(_WIN32) - static std::string convertFromWideString(const std::wstring wstring) - { - int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); - std::string string; - - string.resize(numBytes); - WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); - - return std::string(string); - - } // convertFromWideString -#endif // _WIN32 - - stringList getDirContent(const std::string& _path, const bool _recursive) - { - std::string path = getGenericPath(_path); - stringList contentList; - - // only parse the directory, if it's a directory - if(isDirectory(path)) - { - -#if defined(_WIN32) - WIN32_FIND_DATAW findData; - std::string wildcard = path + "/*"; - HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); - - if(hFind != INVALID_HANDLE_VALUE) - { - // loop over all files in the directory - do - { - std::string name = convertFromWideString(findData.cFileName); - - // ignore "." and ".." - if((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - contentList.push_back(fullName); - - if(_recursive && isDirectory(fullName)) - contentList.merge(getDirContent(fullName, true)); - } - } - while(FindNextFileW(hFind, &findData)); - - FindClose(hFind); - } -#else // _WIN32 - DIR* dir = opendir(path.c_str()); - - if(dir != NULL) - { - struct dirent* entry; - - // loop over all files in the directory - while((entry = readdir(dir)) != NULL) - { - std::string name(entry->d_name); - - // ignore "." and ".." - if((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - contentList.push_back(fullName); - - if(_recursive && isDirectory(fullName)) - contentList.merge(getDirContent(fullName, true)); - } - } - - closedir(dir); - } -#endif // _WIN32 - - } - - // sort the content list - contentList.sort(); - - // return the content list - return contentList; - - } // getDirContent - - stringList getPathList(const std::string& _path) - { - stringList pathList; - std::string path = getGenericPath(_path); - size_t start = 0; - size_t end = 0; - - // split at '/' - while((end = path.find("/", start)) != std::string::npos) - { - if(end != start) - pathList.push_back(std::string(path, start, end - start)); - - start = end + 1; - } - - // add last folder / file to pathList - if(start != path.size()) - pathList.push_back(std::string(path, start, path.size() - start)); - - // return the path list - return pathList; - - } // getPathList - - std::string getHomePath() - { - static std::string path; - - // only construct the homepath once - if(!path.length()) - { - // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows - char* envHome = getenv("HOME"); - -#ifdef _DEBUG - // envHome = "H:/[Emulz]/EmulationStation/"; -#endif - - if (envHome) - path = getGenericPath(envHome); - -#if defined(_WIN32) - // but does not seem to work for Windows XP or Vista, so try something else - if(!path.length()) - { - char* envHomeDrive = getenv("HOMEDRIVE"); - char* envHomePath = getenv("HOMEPATH"); - if(envHomeDrive && envHomePath) - path = getGenericPath(std::string(envHomeDrive) + "/" + envHomePath); - } -#endif // _WIN32 - - // no homepath found, fall back to current working directory - if(!path.length()) - path = getCWDPath(); - } - - // return constructed homepath - return path; - - } // getHomePath - - std::string getCWDPath() - { - char temp[512]; - return (getcwd(temp, 512) ? getGenericPath(temp) : ""); - } // getCWDPath - - std::string getExePath() - { - static std::string path; - - // only construct the exepath once - if (!path.length()) - { -#if defined(_WIN32) - char buffer[MAX_PATH]; - DWORD size = MAX_PATH; - DWORD result = GetModuleFileNameA(NULL, buffer, size); - if (result) - { - std::string ret = buffer; - path = getParent(ret); - return ret; - } -#endif - path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); - if (isRegularFile(path)) - path = getParent(path); - } - - // return constructed exepath - return path; - - } // getExePath - - std::string getPreferredPath(const std::string& _path) - { - std::string path = _path; - size_t offset = std::string::npos; -#if defined(_WIN32) - // convert '/' to '\\' - while((offset = path.find('/')) != std::string::npos) - path.replace(offset, 1, "\\"); -#endif // _WIN32 - return path; - } - - std::string getGenericPath(const std::string& _path) - { - std::string path = _path; - size_t offset = std::string::npos; - - // remove "\\\\?\\" - if((path.find("\\\\?\\")) == 0) - path.erase(0, 4); - - // convert '\\' to '/' - while((offset = path.find('\\')) != std::string::npos) - path.replace(offset, 1 ,"/"); - - // remove double '/' - while((offset = path.find("//")) != std::string::npos) - path.erase(offset, 1); - - // remove trailing '/' - while(path.length() && ((offset = path.find_last_of('/')) == (path.length() - 1))) - path.erase(offset, 1); - - // return generic path - return path; - - } // getGenericPath - - std::string getEscapedPath(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // windows escapes stuff by just putting everything in quotes - return '"' + getPreferredPath(path) + '"'; -#else // _WIN32 - // insert a backslash before most characters that would mess up a bash path - const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; - const char* invalidChar = invalidChars; - - while(*invalidChar) - { - size_t start = 0; - size_t offset = 0; - - while((offset = path.find(*invalidChar, start)) != std::string::npos) - { - start = offset + 1; - - if((offset == 0) || (path[offset - 1] != '\\')) - { - path.insert(offset, 1, '\\'); - ++start; - } - } - - ++invalidChar; - } - - // return escaped path - return path; -#endif // _WIN32 - - } // getEscapedPath - - std::string getCanonicalPath(const std::string& _path) - { - // temporary hack for builtin resources - if((_path[0] == ':') && (_path[1] == '/')) - return _path; - - std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); - - // cleanup path - bool scan = true; - while(scan) - { - stringList pathList = getPathList(path); - - path.clear(); - scan = false; - - for(stringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); ++it) - { - // ignore empty - if((*it).empty()) - continue; - - // remove "/./" - if((*it) == ".") - continue; - - // resolve "/../" - if((*it) == "..") - { - path = getParent(path); - continue; - } - -#if defined(_WIN32) - // append folder to path - path += (path.size() == 0) ? (*it) : ("/" + (*it)); -#else // _WIN32 - // append folder to path - path += ("/" + (*it)); -#endif // _WIN32 - - // resolve symlink - if(isSymlink(path)) - { - std::string resolved = resolveSymlink(path); - - if(resolved.empty()) - return ""; - - if(isAbsolute(resolved)) - path = resolved; - else - path = getParent(path) + "/" + resolved; - - for(++it; it != pathList.cend(); ++it) - path += (path.size() == 0) ? (*it) : ("/" + (*it)); - - scan = true; - break; - } - } - } - - // return canonical path - return path; - - } // getCanonicalPath - - std::string getAbsolutePath(const std::string& _path, const std::string& _base) - { - std::string path = getGenericPath(_path); - std::string base = isAbsolute(_base) ? getGenericPath(_base) : getAbsolutePath(_base); - - // return absolute path - return isAbsolute(path) ? path : getGenericPath(base + "/" + path); - - } // getAbsolutePath - - std::string getParent(const std::string& _path) - { - std::string path = getGenericPath(_path); - size_t offset = std::string::npos; - - // find last '/' and erase it - if((offset = path.find_last_of('/')) != std::string::npos) - return path.erase(offset); - - // no parent found - return path; - - } // getParent - - std::string getFileName(const std::string& _path) - { - std::string path = getGenericPath(_path); - size_t offset = std::string::npos; - - // find last '/' and return the filename - if((offset = path.find_last_of('/')) != std::string::npos) - return ((path[offset + 1] == 0) ? "." : std::string(path, offset + 1)); - - // no '/' found, entire path is a filename - return path; - - } // getFileName - - std::string getStem(const std::string& _path) - { - std::string fileName = getFileName(_path); - size_t offset = std::string::npos; - - // empty fileName - if(fileName == ".") - return fileName; - - // find last '.' and erase the extension - if((offset = fileName.find_last_of('.')) != std::string::npos) - return fileName.erase(offset); - - // no '.' found, filename has no extension - return fileName; - - } // getStem - - std::string getExtension(const std::string& _path) - { - std::string fileName = getFileName(_path); - size_t offset = std::string::npos; - - // empty fileName - if(fileName == ".") - return fileName; - - // find last '.' and return the extension - if((offset = fileName.find_last_of('.')) != std::string::npos) - return std::string(fileName, offset); - - // no '.' found, filename has no extension - return "."; - - } // getExtension - - std::string resolveRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) - { - std::string path = getGenericPath(_path); - std::string relativeTo = isDirectory(_relativeTo) ? getGenericPath(_relativeTo) : getParent(_relativeTo); - - // nothing to resolve - if(!path.length()) - return path; - - // replace '.' with relativeTo - if((path[0] == '.') && (path[1] == '/')) - return (relativeTo + &(path[1])); - - // replace '~' with homePath - if(_allowHome && (path[0] == '~') && (path[1] == '/')) - return (getHomePath() + &(path[1])); - - // nothing to resolve - return path; - - } // resolveRelativePath - - std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) - { - bool contains = false; - std::string path = removeCommonPath(_path, _relativeTo, contains); - - if(contains) - { - // success - return ("./" + path); - } - - if(_allowHome) - { - path = removeCommonPath(_path, getHomePath(), contains); - - if(contains) - { - // success - return ("~/" + path); - } - } - - // nothing to resolve - return path; - - } // createRelativePath - - std::string removeCommonPath(const std::string& _path, const std::string& _common, bool& _contains) - { - std::string path = getGenericPath(_path); - std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); - - // check if path contains common - if(path.find(common) == 0) - { - _contains = true; - return path.substr(common.length() + 1); - } - - // it didn't - _contains = false; - return path; - - } // removeCommonPath - - std::string resolveSymlink(const std::string& _path) - { - std::string path = getGenericPath(_path); - std::string resolved; - -#if defined(_WIN32) - HANDLE hFile = CreateFile(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); - - if(hFile != INVALID_HANDLE_VALUE) - { - resolved.resize(GetFinalPathNameByHandle(hFile, nullptr, 0, FILE_NAME_NORMALIZED) + 1); - if(GetFinalPathNameByHandle(hFile, (LPSTR)resolved.data(), (DWORD)resolved.size(), FILE_NAME_NORMALIZED) > 0) - { - resolved.resize(resolved.size() - 1); - resolved = getGenericPath(resolved); - } - CloseHandle(hFile); - } -#else // _WIN32 - struct stat info; - - // check if lstat succeeded - if(lstat(path.c_str(), &info) == 0) - { - resolved.resize(info.st_size); - if(readlink(path.c_str(), (char*)resolved.data(), resolved.size()) > 0) - resolved = getGenericPath(resolved); - } -#endif // _WIN32 - - // return resolved path - return resolved; - - } // resolveSymlink - - bool removeFile(const std::string& _path) - { - std::string path = getGenericPath(_path); - - // don't remove if it doesn't exists - if(!exists(path)) - return true; - - // try to remove file - return (unlink(path.c_str()) == 0); - - } // removeFile - - bool createDirectory(const std::string& _path) - { - std::string path = getGenericPath(_path); - - // don't create if it already exists - if(exists(path)) - return true; - - // try to create directory - if(mkdir(path.c_str(), 0755) == 0) - return true; - - // failed to create directory, try to create the parent - std::string parent = getParent(path); - - // only try to create parent if it's not identical to path - if(parent != path) - createDirectory(parent); - - // try to create directory again now that the parent should exist - return (mkdir(path.c_str(), 0755) == 0); - - } // createDirectory - - bool exists(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - return (stat64(path.c_str(), &info) == 0); - - } // exists - - bool isAbsolute(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - return ((path.size() > 1) && (path[1] == ':')); -#else // _WIN32 - return ((path.size() > 0) && (path[0] == '/')); -#endif // _WIN32 - - } // isAbsolute - - bool isRegularFile(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - if(stat64(path.c_str(), &info) != 0) - return false; - - // check for S_IFREG attribute - return (S_ISREG(info.st_mode)); - - } // isRegularFile - - bool isDirectory(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat info; - - // check if stat succeeded - if(stat(path.c_str(), &info) != 0) - return false; - - // check for S_IFDIR attribute - return (S_ISDIR(info.st_mode)); - - } // isDirectory - - bool isSymlink(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // check for symlink attribute - const DWORD Attributes = GetFileAttributes(path.c_str()); - if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT)) - return true; -#else // _WIN32 - struct stat info; - - // check if lstat succeeded - if(lstat(path.c_str(), &info) != 0) - return false; - - // check for S_IFLNK attribute - return (S_ISLNK(info.st_mode)); -#endif // _WIN32 - - // not a symlink - return false; - - } // isSymlink - - bool isHidden(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // check for hidden attribute - const DWORD Attributes = GetFileAttributes(path.c_str()); - if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_HIDDEN)) - return true; -#endif // _WIN32 - - // filenames starting with . are hidden in linux, we do this check for windows as well - if (getFileName(path)[0] == '.') - return true; - - // not hidden - return false; - - } // isHidden - - } // FileSystem:: - -} // Utils:: +#define _FILE_OFFSET_BITS 64 + +#include "utils/FileSystemUtil.h" + +#include "Settings.h" +#include +#include + +#if defined(_WIN32) +// because windows... +#include +#include +#define getcwd _getcwd +#define mkdir(x,y) _mkdir(x) +#define snprintf _snprintf +#define stat64 _stat64 +#define unlink _unlink +#define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) +#define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) +#else // _WIN32 +#include +#include +#endif // _WIN32 + +namespace Utils +{ + namespace FileSystem + { + +#if defined(_WIN32) + static std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + std::string string; + + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return std::string(string); + + } // convertFromWideString +#endif // _WIN32 + + bool compareFileInfo(const FileInfo& first, const FileInfo& second) + { + unsigned int i = 0; + while ((i < first.path.length()) && (i < second.path.length())) + { + if (tolower(first.path[i]) < tolower(second.path[i])) return true; + else if (tolower(first.path[i]) > tolower(second.path[i])) return false; + ++i; + } + return (first.path.length() < second.path.length()); + } + + fileList getDirInfo(const std::string& _path, const bool _recursive) + { + std::string path = getGenericPath(_path); + fileList contentList; + + // only parse the directory, if it's a directory + if (isDirectory(path)) + { +#if defined(_WIN32) + WIN32_FIND_DATAW findData; + std::string wildcard = path + "/*"; + HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); + + if (hFind != INVALID_HANDLE_VALUE) + { + // loop over all files in the directory + do + { + std::string name = convertFromWideString(findData.cFileName); + + // ignore "." and ".." + if ((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + + FileInfo fi; + fi.path = fullName; + fi.readOnly = (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN; + fi.directory = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY; + contentList.push_back(fi); + + if (_recursive && fi.directory) + { + fileList fl = getDirInfo(fullName, true); + for (auto it = fl.cbegin(); it != fl.cend(); ++it) + contentList.push_back(*it); + } + } + } while (FindNextFileW(hFind, &findData)); + + FindClose(hFind); + } +#else // _WIN32 + DIR* dir = opendir(path.c_str()); + + if (dir != NULL) + { + struct dirent* entry; + + // loop over all files in the directory + while ((entry = readdir(dir)) != NULL) + { + std::string name(entry->d_name); + + // ignore "." and ".." + if ((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + + FileInfo fi; + fi.path = fullName; + fi.readOnly = Utils::FileSystem::isHidden(fullName); + fi.directory = isDirectory(fullName); + contentList.push_back(fi); + + if (_recursive && fi.directory) + { + fileList fl = getDirInfo(fullName, true); + for (auto it = fl.cbegin(); it != fl.cend(); ++it) + contentList.push_back(*it); + } + } + } + + closedir(dir); + } +#endif // _WIN32 + + } + + // sort the content list + // Why loose time -> It will be sorted later ???? contentList.sort(compareFileInfo); + + // return the content list + return contentList; + + } // getDirContent + + stringList getDirContent(const std::string& _path, const bool _recursive, const bool includeHidden) + { + std::string path = getGenericPath(_path); + stringList contentList; + + // only parse the directory, if it's a directory + if(isDirectory(path)) + { +#if defined(_WIN32) + WIN32_FIND_DATAW findData; + std::string wildcard = path + "/*"; + HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); + + if(hFind != INVALID_HANDLE_VALUE) + { + // loop over all files in the directory + do + { + std::string name = convertFromWideString(findData.cFileName); + + // ignore "." and ".." + if((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + + if (!includeHidden && (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN) + continue; + + contentList.push_back(fullName); + + if(_recursive && (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) + contentList.merge(getDirContent(fullName, true, includeHidden)); + } + } + while(FindNextFileW(hFind, &findData)); + + FindClose(hFind); + } +#else // _WIN32 + DIR* dir = opendir(path.c_str()); + + if(dir != NULL) + { + struct dirent* entry; + + // loop over all files in the directory + while((entry = readdir(dir)) != NULL) + { + std::string name(entry->d_name); + + // ignore "." and ".." + if((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + + if (!includeHidden && Utils::FileSystem::isHidden(fullName)) + continue; + + contentList.push_back(fullName); + + if(_recursive && isDirectory(fullName)) + contentList.merge(getDirContent(fullName, true)); + } + } + + closedir(dir); + } +#endif // _WIN32 + + } + + // sort the content list +// Why loose time -> It will be sorted later ???? contentList.sort(); + + // return the content list + return contentList; + + } // getDirContent + + stringList getPathList(const std::string& _path) + { + stringList pathList; + std::string path = getGenericPath(_path); + size_t start = 0; + size_t end = 0; + + // split at '/' + while((end = path.find("/", start)) != std::string::npos) + { + if(end != start) + pathList.push_back(std::string(path, start, end - start)); + + start = end + 1; + } + + // add last folder / file to pathList + if(start != path.size()) + pathList.push_back(std::string(path, start, path.size() - start)); + + // return the path list + return pathList; + + } // getPathList + + std::string getHomePath() + { + static std::string path; + + // only construct the homepath once + if(!path.length()) + { + // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows + char* envHome = getenv("HOME"); + envHome = "H:/[Emulz]/EmulationStation/"; +#ifdef _DEBUG + envHome = "H:/[Emulz]/EmulationStation/"; +#endif + + if (envHome) + path = getGenericPath(envHome); + +#if defined(_WIN32) + // but does not seem to work for Windows XP or Vista, so try something else + if(!path.length()) + { + char* envHomeDrive = getenv("HOMEDRIVE"); + char* envHomePath = getenv("HOMEPATH"); + if(envHomeDrive && envHomePath) + path = getGenericPath(std::string(envHomeDrive) + "/" + envHomePath); + } +#endif // _WIN32 + + // no homepath found, fall back to current working directory + if(!path.length()) + path = getCWDPath(); + } + + // return constructed homepath + return path; + + } // getHomePath + + std::string getCWDPath() + { + char temp[512]; + return (getcwd(temp, 512) ? getGenericPath(temp) : ""); + } // getCWDPath + + std::string getExePath() + { + static std::string path; + + // only construct the exepath once + if (!path.length()) + { +#if defined(_WIN32) + char buffer[MAX_PATH]; + DWORD size = MAX_PATH; + DWORD result = GetModuleFileNameA(NULL, buffer, size); + if (result) + { + std::string ret = buffer; + path = getParent(ret); + return ret; + } +#endif + path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); + if (isRegularFile(path)) + path = getParent(path); + } + + // return constructed exepath + return path; + + } // getExePath + + std::string getPreferredPath(const std::string& _path) + { + std::string path = _path; + size_t offset = std::string::npos; +#if defined(_WIN32) + // convert '/' to '\\' + while((offset = path.find('/')) != std::string::npos) + path.replace(offset, 1, "\\"); +#endif // _WIN32 + return path; + } + + std::string getGenericPath(const std::string& _path) + { + std::string path = _path; + size_t offset = std::string::npos; + + // remove "\\\\?\\" + if((path.find("\\\\?\\")) == 0) + path.erase(0, 4); + + // convert '\\' to '/' + while((offset = path.find('\\')) != std::string::npos) + path.replace(offset, 1 ,"/"); + + // remove double '/' + while((offset = path.find("//")) != std::string::npos) + path.erase(offset, 1); + + // remove trailing '/' + while(path.length() && ((offset = path.find_last_of('/')) == (path.length() - 1))) + path.erase(offset, 1); + + // return generic path + return path; + + } // getGenericPath + + std::string getEscapedPath(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // windows escapes stuff by just putting everything in quotes + return '"' + getPreferredPath(path) + '"'; +#else // _WIN32 + // insert a backslash before most characters that would mess up a bash path + const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; + const char* invalidChar = invalidChars; + + while(*invalidChar) + { + size_t start = 0; + size_t offset = 0; + + while((offset = path.find(*invalidChar, start)) != std::string::npos) + { + start = offset + 1; + + if((offset == 0) || (path[offset - 1] != '\\')) + { + path.insert(offset, 1, '\\'); + ++start; + } + } + + ++invalidChar; + } + + // return escaped path + return path; +#endif // _WIN32 + + } // getEscapedPath + + std::string getCanonicalPath(const std::string& _path) + { + // temporary hack for builtin resources + if((_path[0] == ':') && (_path[1] == '/')) + return _path; + + std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); + + // cleanup path + bool scan = true; + while(scan) + { + stringList pathList = getPathList(path); + + path.clear(); + scan = false; + + for(stringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); ++it) + { + // ignore empty + if((*it).empty()) + continue; + + // remove "/./" + if((*it) == ".") + continue; + + // resolve "/../" + if((*it) == "..") + { + path = getParent(path); + continue; + } + +#if defined(_WIN32) + // append folder to path + path += (path.size() == 0) ? (*it) : ("/" + (*it)); +#else // _WIN32 + // append folder to path + path += ("/" + (*it)); +#endif // _WIN32 + + // resolve symlink + if(isSymlink(path)) + { + std::string resolved = resolveSymlink(path); + + if(resolved.empty()) + return ""; + + if(isAbsolute(resolved)) + path = resolved; + else + path = getParent(path) + "/" + resolved; + + for(++it; it != pathList.cend(); ++it) + path += (path.size() == 0) ? (*it) : ("/" + (*it)); + + scan = true; + break; + } + } + } + + // return canonical path + return path; + + } // getCanonicalPath + + std::string getAbsolutePath(const std::string& _path, const std::string& _base) + { + std::string path = getGenericPath(_path); + std::string base = isAbsolute(_base) ? getGenericPath(_base) : getAbsolutePath(_base); + + // return absolute path + return isAbsolute(path) ? path : getGenericPath(base + "/" + path); + + } // getAbsolutePath + + std::string getParent(const std::string& _path) + { + std::string path = getGenericPath(_path); + size_t offset = std::string::npos; + + // find last '/' and erase it + if((offset = path.find_last_of('/')) != std::string::npos) + return path.erase(offset); + + // no parent found + return path; + + } // getParent + + std::string getFileName(const std::string& _path) + { + std::string path = getGenericPath(_path); + size_t offset = std::string::npos; + + // find last '/' and return the filename + if((offset = path.find_last_of('/')) != std::string::npos) + return ((path[offset + 1] == 0) ? "." : std::string(path, offset + 1)); + + // no '/' found, entire path is a filename + return path; + + } // getFileName + + std::string getStem(const std::string& _path) + { + std::string fileName = getFileName(_path); + size_t offset = std::string::npos; + + // empty fileName + if(fileName == ".") + return fileName; + + // find last '.' and erase the extension + if((offset = fileName.find_last_of('.')) != std::string::npos) + return fileName.erase(offset); + + // no '.' found, filename has no extension + return fileName; + + } // getStem + + std::string getExtension(const std::string& _path) + { + std::string fileName = getFileName(_path); + size_t offset = std::string::npos; + + // empty fileName + if(fileName == ".") + return fileName; + + // find last '.' and return the extension + if((offset = fileName.find_last_of('.')) != std::string::npos) + return std::string(fileName, offset); + + // no '.' found, filename has no extension + return "."; + + } // getExtension + + std::string resolveRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) + { + std::string path = getGenericPath(_path); + std::string relativeTo = isDirectory(_relativeTo) ? getGenericPath(_relativeTo) : getParent(_relativeTo); + + // nothing to resolve + if(!path.length()) + return path; + + // replace '.' with relativeTo + if((path[0] == '.') && (path[1] == '/')) + return (relativeTo + &(path[1])); + + // replace '~' with homePath + if(_allowHome && (path[0] == '~') && (path[1] == '/')) + return (getHomePath() + &(path[1])); + + // nothing to resolve + return path; + + } // resolveRelativePath + + std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) + { + bool contains = false; + std::string path = removeCommonPath(_path, _relativeTo, contains); + + if(contains) + { + // success + return ("./" + path); + } + + if(_allowHome) + { + path = removeCommonPath(_path, getHomePath(), contains); + + if(contains) + { + // success + return ("~/" + path); + } + } + + // nothing to resolve + return path; + + } // createRelativePath + + std::string removeCommonPath(const std::string& _path, const std::string& _common, bool& _contains) + { + std::string path = getGenericPath(_path); + std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); + + // check if path contains common + if(path.find(common) == 0) + { + _contains = true; + return path.substr(common.length() + 1); + } + + // it didn't + _contains = false; + return path; + + } // removeCommonPath + + std::string resolveSymlink(const std::string& _path) + { + std::string path = getGenericPath(_path); + std::string resolved; + +#if defined(_WIN32) + HANDLE hFile = CreateFile(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); + + if(hFile != INVALID_HANDLE_VALUE) + { + resolved.resize(GetFinalPathNameByHandle(hFile, nullptr, 0, FILE_NAME_NORMALIZED) + 1); + if(GetFinalPathNameByHandle(hFile, (LPSTR)resolved.data(), (DWORD)resolved.size(), FILE_NAME_NORMALIZED) > 0) + { + resolved.resize(resolved.size() - 1); + resolved = getGenericPath(resolved); + } + CloseHandle(hFile); + } +#else // _WIN32 + struct stat info; + + // check if lstat succeeded + if(lstat(path.c_str(), &info) == 0) + { + resolved.resize(info.st_size); + if(readlink(path.c_str(), (char*)resolved.data(), resolved.size()) > 0) + resolved = getGenericPath(resolved); + } +#endif // _WIN32 + + // return resolved path + return resolved; + + } // resolveSymlink + + bool removeFile(const std::string& _path) + { + std::string path = getGenericPath(_path); + + // don't remove if it doesn't exists + if(!exists(path)) + return true; + + // try to remove file + return (unlink(path.c_str()) == 0); + + } // removeFile + + bool createDirectory(const std::string& _path) + { + std::string path = getGenericPath(_path); + + // don't create if it already exists + if(exists(path)) + return true; + + // try to create directory + if(mkdir(path.c_str(), 0755) == 0) + return true; + + // failed to create directory, try to create the parent + std::string parent = getParent(path); + + // only try to create parent if it's not identical to path + if(parent != path) + createDirectory(parent); + + // try to create directory again now that the parent should exist + return (mkdir(path.c_str(), 0755) == 0); + + } // createDirectory + + bool exists(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + return (stat64(path.c_str(), &info) == 0); + + } // exists + + bool isAbsolute(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + return ((path.size() > 1) && (path[1] == ':')); +#else // _WIN32 + return ((path.size() > 0) && (path[0] == '/')); +#endif // _WIN32 + + } // isAbsolute + + bool isRegularFile(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + if(stat64(path.c_str(), &info) != 0) + return false; + + // check for S_IFREG attribute + return (S_ISREG(info.st_mode)); + + } // isRegularFile + + bool isDirectory(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat info; + + // check if stat succeeded + if(stat(path.c_str(), &info) != 0) + return false; + + // check for S_IFDIR attribute + return (S_ISDIR(info.st_mode)); + + } // isDirectory + + bool isSymlink(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // check for symlink attribute + const DWORD Attributes = GetFileAttributes(path.c_str()); + if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT)) + return true; +#else // _WIN32 + struct stat info; + + // check if lstat succeeded + if(lstat(path.c_str(), &info) != 0) + return false; + + // check for S_IFLNK attribute + return (S_ISLNK(info.st_mode)); +#endif // _WIN32 + + // not a symlink + return false; + + } // isSymlink + + bool isHidden(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // check for hidden attribute + const DWORD Attributes = GetFileAttributes(path.c_str()); + if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_HIDDEN)) + return true; +#endif // _WIN32 + + // filenames starting with . are hidden in linux, we do this check for windows as well + if (getFileName(path)[0] == '.') + return true; + + // not hidden + return false; + + } // isHidden + + } // FileSystem:: + +} // Utils:: diff --git a/es-core/src/utils/FileSystemUtil.h b/es-core/src/utils/FileSystemUtil.h index 48c7f4f5ac..e8b01741c0 100644 --- a/es-core/src/utils/FileSystemUtil.h +++ b/es-core/src/utils/FileSystemUtil.h @@ -9,9 +9,19 @@ namespace Utils { namespace FileSystem { + struct FileInfo + { + public: + std::string path; + bool readOnly; + bool directory; + }; + typedef std::list stringList; + typedef std::list fileList; - stringList getDirContent (const std::string& _path, const bool _recursive = false); + fileList getDirInfo(const std::string& _path, const bool _recursive = false); + stringList getDirContent (const std::string& _path, const bool _recursive = false, const bool includeHidden = true); stringList getPathList (const std::string& _path); std::string getHomePath (); std::string getCWDPath (); @@ -40,6 +50,8 @@ namespace Utils } // FileSystem:: + + } // Utils:: #endif // ES_CORE_UTILS_FILE_SYSTEM_UTIL_H From ca3b3a03fb6411eff5b53ef5a461226c121ef41a Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 13 Jun 2019 17:44:12 +0200 Subject: [PATCH 033/336] - Better GridView (with animations) - Added Custom Views in Themes -> Enables creation of multiple view - Full Themable Menus - Support for Recalbox themes - Merge view "detailed" and "video" -> video is not longer needed to display a video, a video element can be created in detailed view. - Video / Theming -> Support video elements as extras - Images : Don't load oversized image in VRAM -> max size is set by the container. --- es-app/src/FileData.cpp | 26 +- es-app/src/FileData.h | 2 +- es-app/src/FileSorts.cpp | 4 +- es-app/src/Gamelist.cpp | 27 +- es-app/src/MetaData.cpp | 8 +- es-app/src/MetaData.h | 7 +- es-app/src/SystemData.cpp | 16 +- es-app/src/SystemData.h | 254 +++--- es-app/src/guis/GuiGamelistOptions.cpp | 616 +++++++------- es-app/src/guis/GuiGamelistOptions.h | 3 + es-app/src/guis/GuiMenu.cpp | 435 +++++++--- es-app/src/guis/GuiMenu.h | 2 +- es-app/src/guis/GuiMetaDataEd.cpp | 2 +- es-app/src/guis/GuiSettings.h | 12 + .../guis/GuiSlideshowScreensaverOptions.cpp | 2 +- es-app/src/main.cpp | 10 +- es-app/src/views/ViewController.cpp | 119 +-- es-app/src/views/ViewController.h | 4 +- .../src/views/gamelist/BasicGameListView.cpp | 6 + es-app/src/views/gamelist/BasicGameListView.h | 10 +- .../views/gamelist/DetailedGameListView.cpp | 88 +- .../src/views/gamelist/DetailedGameListView.h | 18 +- .../src/views/gamelist/GridGameListView.cpp | 49 +- es-app/src/views/gamelist/GridGameListView.h | 8 +- es-app/src/views/gamelist/IGameListView.cpp | 5 + es-app/src/views/gamelist/IGameListView.h | 5 + .../views/gamelist/ISimpleGameListView.cpp | 17 +- .../src/views/gamelist/ISimpleGameListView.h | 1 + es-app/src/views/gamelist/VideoGameListView.h | 9 +- es-core/src/GuiComponent.cpp | 10 +- es-core/src/ImageIO.cpp | 50 +- es-core/src/ImageIO.h | 7 +- es-core/src/Settings.cpp | 18 +- es-core/src/ThemeData.cpp | 402 ++++++++- es-core/src/ThemeData.h | 58 +- es-core/src/Window.cpp | 18 +- es-core/src/Window.h | 4 +- es-core/src/components/ButtonComponent.cpp | 10 +- es-core/src/components/GridTileComponent.cpp | 424 ++++++++-- es-core/src/components/GridTileComponent.h | 35 +- es-core/src/components/ImageComponent.cpp | 37 +- es-core/src/components/ImageComponent.h | 14 +- es-core/src/components/ImageGridComponent.h | 557 +++++++++---- es-core/src/components/MenuComponent.cpp | 2 +- es-core/src/components/NinePatchComponent.h | 1 + es-core/src/components/OptionListComponent.h | 766 +++++++++--------- es-core/src/components/SliderComponent.cpp | 2 +- es-core/src/components/SwitchComponent.cpp | 6 +- es-core/src/components/TextComponent.cpp | 6 + es-core/src/components/TextComponent.h | 2 + es-core/src/components/VideoComponent.cpp | 122 +-- es-core/src/components/VideoComponent.h | 13 +- es-core/src/components/VideoVlcComponent.cpp | 18 +- es-core/src/resources/ResourceManager.cpp | 16 +- es-core/src/resources/ResourceManager.h | 2 +- es-core/src/resources/TextureData.cpp | 16 +- es-core/src/resources/TextureData.h | 6 + es-core/src/resources/TextureDataManager.cpp | 1 + es-core/src/resources/TextureResource.cpp | 14 +- es-core/src/resources/TextureResource.h | 4 +- es-core/src/utils/FileSystemUtil.cpp | 32 +- es-core/src/utils/FileSystemUtil.h | 3 +- 62 files changed, 3054 insertions(+), 1387 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 71cdc3352d..a5485bb65a 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -59,7 +59,7 @@ const std::string FileData::getThumbnailPath() const if(thumbnail.empty()) { thumbnail = metadata.get("image"); - /* + // no image, try to use local image if(thumbnail.empty() && Settings::getInstance()->getBool("LocalArt")) { @@ -73,7 +73,7 @@ const std::string FileData::getThumbnailPath() const thumbnail = path; } } - }*/ + } } return thumbnail; @@ -93,15 +93,15 @@ const std::string& FileData::getName() { return metadata.get("name"); } - +/* const std::string& FileData::getSortName() { if (metadata.get("sortname").empty()) return metadata.get("name"); - else - return metadata.get("sortname"); + + return metadata.get("sortname"); } - +*/ FileData* FileData::findUniqueGameForFolder() { std::vector children = getChildren(); @@ -155,15 +155,15 @@ const std::string FileData::getEmulator() const const std::string FileData::getVideoPath() const { std::string video = metadata.get("video"); - /* + // no video, try to use local video if(video.empty() && Settings::getInstance()->getBool("LocalArt")) { std::string path = mEnvData->mStartPath + "/images/" + getDisplayName() + "-video.mp4"; - if(Utils::FileSystem::exists(path)) + if (Utils::FileSystem::exists(path)) video = path; } - */ + return video; } @@ -172,7 +172,7 @@ const std::string FileData::getMarqueePath() const std::string marquee = metadata.get("marquee"); // no marquee, try to use local marquee - if(marquee.empty() && Settings::getInstance()->getBool("LocalArt")) + if (marquee.empty() && Settings::getInstance()->getBool("LocalArt")) { const char* extList[2] = { ".png", ".jpg" }; for(int i = 0; i < 2; i++) @@ -315,8 +315,7 @@ void FileData::launchGame(Window* window) VolumeControl::getInstance()->deinit(); bool hideWindow = Settings::getInstance()->getBool("HideWindow"); - if (hideWindow) - window->deinit(); + window->deinit(hideWindow); std::string command = mEnvData->mLaunchCommand; @@ -357,8 +356,7 @@ void FileData::launchGame(Window* window) Scripting::fireEvent("game-end"); - if (hideWindow) - window->init(); + window->init(hideWindow); VolumeControl::getInstance()->init(); window->normalizeNextUpdate(); diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index d8bafed6d2..93b0cc2601 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -37,7 +37,7 @@ class FileData virtual ~FileData(); virtual const std::string& getName(); - virtual const std::string& getSortName(); + //virtual const std::string& getSortName(); inline FileType getType() const { return mType; } inline const std::string& getPath() const { return mPath; } inline FileData* getParent() const { return mParent; } diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index d1237c0a2d..046100cab4 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -41,8 +41,8 @@ namespace FileSorts //returns if file1 should come before file2 bool compareName(const FileData* file1, const FileData* file2) { - std::string name1 = file1->metadata.getSortName(); - return name1.compare(file2->metadata.getSortName()) < 0; + std::string name1 = file1->metadata.getName(); + return name1.compare(file2->metadata.getName()) < 0; } bool compareRating(const FileData* file1, const FileData* file2) diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index ea20baa94a..f0328caf76 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -106,6 +106,8 @@ void parseGamelist(SystemData* system) LOG(LogError) << "Could not find node in gamelist \"" << xmlpath << "\"!"; return; } + + system->setSystemViewMode(root.attribute("defaultView").value()); std::string relativeTo = system->getStartPath(); @@ -181,12 +183,15 @@ void updateGamelist(SystemData* system) if(Settings::getInstance()->getBool("IgnoreGamelist")) return; + int numUpdated = 0; + pugi::xml_document doc; pugi::xml_node root; std::string xmlReadPath = system->getGamelistPath(false); if(Utils::FileSystem::exists(xmlReadPath)) { + //parse an existing file first pugi::xml_parse_result result = doc.load_file(xmlReadPath.c_str()); @@ -197,6 +202,20 @@ void updateGamelist(SystemData* system) } root = doc.child("gameList"); + + std::string viewMode = root.attribute("defaultView").value(); + if (viewMode != system->getSystemViewMode()) + { + numUpdated++; + + if (system->getSystemViewMode().empty()) + root.remove_attribute("defaultView"); + else if (root.attribute("defaultView").empty()) + root.append_attribute("defaultView") = system->getSystemViewMode().c_str(); + else + root.attribute("defaultView") = system->getSystemViewMode().c_str(); + } + if(!root) { LOG(LogError) << "Could not find node in gamelist \"" << xmlReadPath << "\"!"; @@ -205,6 +224,12 @@ void updateGamelist(SystemData* system) }else{ //set up an empty gamelist to append to root = doc.append_child("gameList"); + + if (!system->getSystemViewMode().empty()) + { + numUpdated++; + root.append_attribute("defaultView") = system->getSystemViewMode().c_str(); + } } @@ -212,8 +237,6 @@ void updateGamelist(SystemData* system) FileData* rootFolder = system->getRootFolder(); if (rootFolder != nullptr) { - int numUpdated = 0; - //get only files, no folders std::vector files = rootFolder->getFilesRecursive(GAME | FOLDER); //iterate through all files, checking if they're already in the XML diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index c9c1716188..cdaa4dd151 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -8,7 +8,7 @@ MetaDataDecl gameDecls[] = { // key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd {"name", MD_STRING, "", false, "name", "enter game name"}, - {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, +// {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, { "emulator", MD_PLIST, "", false, "emulator", "emulator" }, @@ -34,7 +34,7 @@ const std::vector gameMDD(gameDecls, gameDecls + sizeof(gameDecls) MetaDataDecl folderDecls[] = { {"name", MD_STRING, "", false, "name", "enter game name"}, - {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, +// {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, {"image", MD_PATH, "", false, "image", "enter path to image"}, {"thumbnail", MD_PATH, "", false, "thumbnail", "enter path to thumbnail"}, @@ -134,8 +134,8 @@ void MetaDataList::appendToXML(pugi::xml_node& parent, bool ignoreDefaults, cons void MetaDataList::set(const std::string& key, const std::string& value) { - if ((key == "sortname" || key == "name") && !value.empty()) - mSortName = Utils::String::toUpper(value); + if (key == "name") + mName = value; mMap[key] = value; mWasChanged = true; diff --git a/es-app/src/MetaData.h b/es-app/src/MetaData.h index c0c01f264d..3e23eab755 100644 --- a/es-app/src/MetaData.h +++ b/es-app/src/MetaData.h @@ -63,10 +63,7 @@ class MetaDataList inline MetaDataListType getType() const { return mType; } inline const std::vector& getMDD() const { return getMDDByType(getType()); } - - const std::string getSortName() const { - return mSortName; - } + const std::string getName() const { return mName; } private: MetaDataListType mType; @@ -78,7 +75,7 @@ class MetaDataList static std::map BuildDefaultMap(MetaDataListType type); - std::string mSortName; + std::string mName; }; #endif // ES_APP_META_DATA_H diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index b2cd549059..657012078f 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -24,6 +24,7 @@ std::vector SystemData::sSystemVector; SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) { + mViewModeChanged = false; mFilterIndex = new FileFilterIndex(); // if it's an actual system, initialize it, if not, just create the data structure @@ -56,6 +57,19 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys loadTheme(); } +bool SystemData::setSystemViewMode(std::string newViewMode) +{ + if (newViewMode == "automatic") + newViewMode = ""; + + if (mViewMode == newViewMode) + return false; + + mViewMode = newViewMode; + mViewModeChanged = true; + return true; +} + SystemData::~SystemData() { delete mRootFolder; @@ -108,7 +122,7 @@ void SystemData::populateFolder(FileData* folder) //filePath = *it; // skip hidden files and folders - if(!showHidden && fileInfo.readOnly) + if(!showHidden && fileInfo.hidden) continue; //this is a little complicated because we allow a list of extensions to be defined (delimited with a space) diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index fd0db7a977..59207eeede 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -1,56 +1,56 @@ -#pragma once -#ifndef ES_APP_SYSTEM_DATA_H -#define ES_APP_SYSTEM_DATA_H - -#include "PlatformId.h" -#include -#include -#include -#include - -class FileData; -class FileFilterIndex; -class ThemeData; -class Window; - -struct EmulatorData -{ - std::string mName; - std::string mCommandLine; - std::vector mCores; -}; - -struct SystemEnvironmentData -{ - std::string mStartPath; - std::vector mSearchExtensions; - std::string mLaunchCommand; - std::vector mPlatformIds; - -// std::string mDefaultCore; - - std::vector mEmulators; - - std::vector getCores(std::string emulatorName) - { +#pragma once +#ifndef ES_APP_SYSTEM_DATA_H +#define ES_APP_SYSTEM_DATA_H + +#include "PlatformId.h" +#include +#include +#include +#include + +class FileData; +class FileFilterIndex; +class ThemeData; +class Window; + +struct EmulatorData +{ + std::string mName; + std::string mCommandLine; + std::vector mCores; +}; + +struct SystemEnvironmentData +{ + std::string mStartPath; + std::vector mSearchExtensions; + std::string mLaunchCommand; + std::vector mPlatformIds; + +// std::string mDefaultCore; + + std::vector mEmulators; + + std::vector getCores(std::string emulatorName) + { std::vector list; for (auto& emulator : mEmulators) if (emulatorName == emulator.mName) return emulator.mCores; - - return list; - } - - std::string getDefaultEmulator() - { + + return list; + } + + std::string getDefaultEmulator() + { for (auto& emulator : mEmulators) return emulator.mName; - - return ""; - } - - std::string getDefaultCore(std::string emulatorName) + + return ""; + } + + std::string getDefaultCore(std::string emulatorName) { for (auto& emulator : mEmulators) { @@ -60,85 +60,91 @@ struct SystemEnvironmentData return core; } } - - return ""; - } - - std::string getEmulatorCommandLine(std::string emulatorName) + + return ""; + } + + std::string getEmulatorCommandLine(std::string emulatorName) { for (auto& emulator : mEmulators) if (emulatorName == emulator.mName) return emulator.mCommandLine; - - return ""; - } -}; - -class SystemData -{ -public: - SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem = false); - ~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 mEnvData->mStartPath; } - inline const std::vector& getExtensions() const { return mEnvData->mSearchExtensions; } - inline const std::string& getThemeFolder() const { return mThemeFolder; } - inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } - inline const std::vector& getPlatformIds() const { return mEnvData->mPlatformIds; } - inline bool hasPlatformId(PlatformIds::PlatformId id) { if (!mEnvData) return false; return std::find(mEnvData->mPlatformIds.cbegin(), mEnvData->mPlatformIds.cend(), id) != mEnvData->mPlatformIds.cend(); } - - inline const std::shared_ptr& getTheme() const { return mTheme; } - - std::string getGamelistPath(bool forWrite) const; - bool hasGamelist() const; - std::string getThemePath() const; - - unsigned int getGameCount() const; - unsigned int getDisplayedGameCount() const; - - static void deleteSystems(); - static bool loadConfig(Window* window); //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 sSystemVector; - - inline std::vector::const_iterator getIterator() const { return std::find(sSystemVector.cbegin(), sSystemVector.cend(), this); }; - inline std::vector::const_reverse_iterator getRevIterator() const { return std::find(sSystemVector.crbegin(), sSystemVector.crend(), this); }; - inline bool isCollection() { return mIsCollectionSystem; }; - inline bool isGameSystem() { return mIsGameSystem; }; - - bool isVisible(); - - SystemData* getNext() const; - SystemData* getPrev() const; - static SystemData* getRandomSystem(); - FileData* getRandomGame(); - - // Load or re-load theme. - void loadTheme(); - - FileFilterIndex* getIndex() { return mFilterIndex; }; - -private: - bool mIsCollectionSystem; - bool mIsGameSystem; - std::string mName; - std::string mFullName; - SystemEnvironmentData* mEnvData; - std::string mThemeFolder; - std::shared_ptr mTheme; - - void populateFolder(FileData* folder); - void indexAllGameFilters(const FileData* folder); - void setIsGameSystemStatus(); - - FileFilterIndex* mFilterIndex; - - FileData* mRootFolder; -}; - -#endif // ES_APP_SYSTEM_DATA_H + + return ""; + } +}; + +class SystemData +{ +public: + SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem = false); + ~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 mEnvData->mStartPath; } + inline const std::vector& getExtensions() const { return mEnvData->mSearchExtensions; } + inline const std::string& getThemeFolder() const { return mThemeFolder; } + inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } + inline const std::vector& getPlatformIds() const { return mEnvData->mPlatformIds; } + inline bool hasPlatformId(PlatformIds::PlatformId id) { if (!mEnvData) return false; return std::find(mEnvData->mPlatformIds.cbegin(), mEnvData->mPlatformIds.cend(), id) != mEnvData->mPlatformIds.cend(); } + + inline const std::shared_ptr& getTheme() const { return mTheme; } + + std::string getSystemViewMode() const { if (mViewMode == "automatic") return ""; else return mViewMode; }; + bool setSystemViewMode(std::string newViewMode); + + std::string getGamelistPath(bool forWrite) const; + bool hasGamelist() const; + std::string getThemePath() const; + + unsigned int getGameCount() const; + unsigned int getDisplayedGameCount() const; + + static void deleteSystems(); + static bool loadConfig(Window* window); //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 sSystemVector; + + inline std::vector::const_iterator getIterator() const { return std::find(sSystemVector.cbegin(), sSystemVector.cend(), this); }; + inline std::vector::const_reverse_iterator getRevIterator() const { return std::find(sSystemVector.crbegin(), sSystemVector.crend(), this); }; + inline bool isCollection() { return mIsCollectionSystem; }; + inline bool isGameSystem() { return mIsGameSystem; }; + + bool isVisible(); + + SystemData* getNext() const; + SystemData* getPrev() const; + static SystemData* getRandomSystem(); + FileData* getRandomGame(); + + // Load or re-load theme. + void loadTheme(); + + FileFilterIndex* getIndex() { return mFilterIndex; }; + +private: + bool mIsCollectionSystem; + bool mIsGameSystem; + std::string mName; + std::string mFullName; + SystemEnvironmentData* mEnvData; + std::string mThemeFolder; + std::shared_ptr mTheme; + + std::string mViewMode; + bool mViewModeChanged; + + void populateFolder(FileData* folder); + void indexAllGameFilters(const FileData* folder); + void setIsGameSystemStatus(); + + FileFilterIndex* mFilterIndex; + + FileData* mRootFolder; +}; + +#endif // ES_APP_SYSTEM_DATA_H diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 7a2d13c20f..44ec65f9b4 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -1,295 +1,325 @@ -#include "GuiGamelistOptions.h" - -#include "guis/GuiGamelistFilter.h" -#include "scrapers/Scraper.h" -#include "views/gamelist/IGameListView.h" -#include "views/UIModeController.h" -#include "views/ViewController.h" -#include "CollectionSystemManager.h" -#include "FileFilterIndex.h" -#include "FileSorts.h" -#include "GuiMetaDataEd.h" -#include "SystemData.h" - -#include "animations/LambdaAnimation.h" - -GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : GuiComponent(window), - mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false) -{ - addChild(&mMenu); - - // check it's not a placeholder folder - if it is, only show "Filter Options" - FileData* file = getGamelist()->getCursor(); - fromPlaceholder = file->isPlaceHolder(); - ComponentListRow row; - - if (!fromPlaceholder) { - // jump to letter - row.elements.clear(); - - // define supported character range - // this range includes all numbers, capital letters, and most reasonable symbols - char startChar = '!'; - char endChar = '_'; - - char curChar = (char)toupper(getGamelist()->getCursor()->getSortName()[0]); - if(curChar < startChar || curChar > endChar) - curChar = startChar; - - mJumpToLetterList = std::make_shared(mWindow, _T("JUMP TO LETTER"), false); - for (char c = startChar; c <= endChar; c++) - { - // check if c is a valid first letter in current list - const std::vector& files = getGamelist()->getCursor()->getParent()->getChildrenListToDisplay(); - for (auto file : files) - { - char candidate = (char)toupper(file->getSortName()[0]); - if (c == candidate) - { - mJumpToLetterList->add(std::string(1, c), c, c == curChar); - break; - } - } - } - - row.addElement(std::make_shared(mWindow, _T("JUMP TO LETTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); - row.addElement(mJumpToLetterList, false); - row.input_handler = [&](InputConfig* config, Input input) - { - if(config->isMappedTo("a", input) && input.value) - { - jumpToLetter(); - return true; - } - else if(mJumpToLetterList->input(config, input)) - { - return true; - } - return false; - }; - mMenu.addRow(row); - - // sort list by - mListSort = std::make_shared(mWindow, _T("SORT GAMES BY"), false); - for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) - { - const FileData::SortType& sort = FileSorts::SortTypes.at(i); - mListSort->add(_L(Utils::String::toUpper(sort.description)), &sort, i == 0); // TODO - actually make the sort type persistent - } - - mMenu.addWithLabel(_T("SORT GAMES BY"), mListSort); - } - // show filtered menu - if(!Settings::getInstance()->getBool("ForceDisableFilters")) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("APPLY FILTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); - row.addElement(makeArrow(mWindow), false); - row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); - mMenu.addRow(row); - } - - std::map customCollections = CollectionSystemManager::get()->getCustomCollectionSystems(); - - if(UIModeController::getInstance()->isUIModeFull() && - ((customCollections.find(system->getName()) != customCollections.cend() && CollectionSystemManager::get()->getEditingCollection() != system->getName()) || - CollectionSystemManager::get()->getCustomCollectionsBundle()->getName() == system->getName())) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, "ADD/REMOVE GAMES TO THIS GAME COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); - row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::startEditMode, this)); - mMenu.addRow(row); - } - - if(UIModeController::getInstance()->isUIModeFull() && CollectionSystemManager::get()->isEditing()) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); - row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::exitEditMode, this)); - mMenu.addRow(row); - } - - if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("EDIT THIS GAME'S METADATA"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), 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); - - float x0 = (mSize.x() - mMenu.getSize().x()) / 2; - +#include "GuiGamelistOptions.h" + +#include "guis/GuiGamelistFilter.h" +#include "scrapers/Scraper.h" +#include "views/gamelist/IGameListView.h" +#include "views/UIModeController.h" +#include "views/ViewController.h" +#include "CollectionSystemManager.h" +#include "FileFilterIndex.h" +#include "FileSorts.h" +#include "GuiMetaDataEd.h" +#include "SystemData.h" + +#include "animations/LambdaAnimation.h" + +GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : GuiComponent(window), + mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false) +{ + addChild(&mMenu); + + // check it's not a placeholder folder - if it is, only show "Filter Options" + FileData* file = getGamelist()->getCursor(); + fromPlaceholder = file->isPlaceHolder(); + ComponentListRow row; + + if (!fromPlaceholder) { + // jump to letter + row.elements.clear(); + + // define supported character range + // this range includes all numbers, capital letters, and most reasonable symbols + char startChar = '!'; + char endChar = '_'; + + char curChar = (char)toupper(getGamelist()->getCursor()->getName()[0]); + if(curChar < startChar || curChar > endChar) + curChar = startChar; + + mJumpToLetterList = std::make_shared(mWindow, _T("JUMP TO LETTER"), false); + for (char c = startChar; c <= endChar; c++) + { + // check if c is a valid first letter in current list + const std::vector& files = getGamelist()->getCursor()->getParent()->getChildrenListToDisplay(); + for (auto file : files) + { + char candidate = (char)toupper(file->getName()[0]); + if (c == candidate) + { + mJumpToLetterList->add(std::string(1, c), c, c == curChar); + break; + } + } + } + + row.addElement(std::make_shared(mWindow, _T("JUMP TO LETTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(mJumpToLetterList, false); + row.input_handler = [&](InputConfig* config, Input input) + { + if(config->isMappedTo("a", input) && input.value) + { + jumpToLetter(); + return true; + } + else if(mJumpToLetterList->input(config, input)) + { + return true; + } + return false; + }; + mMenu.addRow(row); + + // sort list by + mListSort = std::make_shared(mWindow, _T("SORT GAMES BY"), false); + for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) + { + const FileData::SortType& sort = FileSorts::SortTypes.at(i); + mListSort->add(_L(Utils::String::toUpper(sort.description)), &sort, i == 0); // TODO - actually make the sort type persistent + } + + mMenu.addWithLabel(_T("SORT GAMES BY"), mListSort); + } + + // GameList view style + mViewMode = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); + std::vector styles; + styles.push_back("automatic"); + + auto mViews = system->getTheme()->getViewsOfTheme(); + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + styles.push_back(*it); + + std::string viewMode = system->getSystemViewMode(); + + bool found = false; + for (auto it = styles.cbegin(); it != styles.cend(); it++) + { + bool sel = (viewMode.empty() && *it == "automatic") || viewMode == *it; + if (sel) + found = true; + + mViewMode->add(_L(*it), *it, sel); + } + + if (!found) + mViewMode->selectFirstItem(); + + mMenu.addWithLabel(_T("GAMELIST VIEW STYLE"), mViewMode); + + // show filtered menu + if(!Settings::getInstance()->getBool("ForceDisableFilters")) + { + row.elements.clear(); + row.addElement(std::make_shared(mWindow, _T("APPLY FILTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(makeArrow(mWindow), false); + row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); + mMenu.addRow(row); + } + + std::map customCollections = CollectionSystemManager::get()->getCustomCollectionSystems(); + + if(UIModeController::getInstance()->isUIModeFull() && + ((customCollections.find(system->getName()) != customCollections.cend() && CollectionSystemManager::get()->getEditingCollection() != system->getName()) || + CollectionSystemManager::get()->getCustomCollectionsBundle()->getName() == system->getName())) + { + row.elements.clear(); + row.addElement(std::make_shared(mWindow, "ADD/REMOVE GAMES TO THIS GAME COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::startEditMode, this)); + mMenu.addRow(row); + } + + if(UIModeController::getInstance()->isUIModeFull() && CollectionSystemManager::get()->isEditing()) + { + row.elements.clear(); + row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::exitEditMode, this)); + mMenu.addRow(row); + } + + if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) + { + row.elements.clear(); + row.addElement(std::make_shared(mWindow, _T("EDIT THIS GAME'S METADATA"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), 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()); + + float x0 = (mSize.x() - mMenu.getSize().x()) / 2; + float y1 = Renderer::getScreenHeight(); - float y2 = (mSize.y() - mMenu.getSize().y()) / 2; - //float y1 = mMenu.getSize().y(); - //float y2 = (mSize.y() - mMenu.getSize().y()) / 2; - - setPosition(x0, y1); - - auto fadeFunc = [this, x0, y1, y2](float t) { - - t -= 1; // cubic ease out - float pct = Math::lerp(0, 1, t*t*t + 1); - - float y = y1 * (1 - pct) + y2 * pct; - setPosition(x0, y); - }; - - setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, x0, y2] - { + float y2 = (mSize.y() - mMenu.getSize().y()) / 2; + + if (Settings::getInstance()->getString("PowerSaverMode") == "instant" || Settings::getInstance()->getString("TransitionStyle") == "instant") setPosition(x0, y2); - }); - - setPosition(x0, y2); -} - -GuiGamelistOptions::~GuiGamelistOptions() -{ - // apply sort - if (!fromPlaceholder) { - FileData* root = mSystem->getRootFolder(); - root->sort(*mListSort->getSelected()); // will also recursively sort children - - // notify that the root folder was sorted - getGamelist()->onFileChanged(root, FILE_SORTED); - } - if (mFiltersChanged) - { - // only reload full view if we came from a placeholder - // as we need to re-display the remaining elements for whatever new - // game is selected - ViewController::get()->reloadGameListView(mSystem); - } -} - -void GuiGamelistOptions::openGamelistFilter() -{ - mFiltersChanged = true; - GuiGamelistFilter* ggf = new GuiGamelistFilter(mWindow, mSystem); - mWindow->pushGui(ggf); -} - -void GuiGamelistOptions::startEditMode() -{ - std::string editingSystem = mSystem->getName(); - // need to check if we're editing the collections bundle, as we will want to edit the selected collection within - if(editingSystem == CollectionSystemManager::get()->getCustomCollectionsBundle()->getName()) - { - FileData* file = getGamelist()->getCursor(); - // do we have the cursor on a specific collection? - if (file->getType() == FOLDER) - { - editingSystem = file->getName(); - } - else - { - // we are inside a specific collection. We want to edit that one. - editingSystem = file->getSystem()->getName(); - } - } - CollectionSystemManager::get()->setEditMode(editingSystem); - delete this; -} - -void GuiGamelistOptions::exitEditMode() -{ - CollectionSystemManager::get()->exitEditMode(); - delete this; -} - -void GuiGamelistOptions::openMetaDataEd() -{ - // open metadata editor - // get the FileData that hosts the original metadata - FileData* file = getGamelist()->getCursor()->getSourceFileData(); - ScraperSearchParams p; - p.game = file; - p.system = file->getSystem(); - - std::function deleteBtnFunc; - - if (file->getType() == FOLDER) - { - deleteBtnFunc = NULL; - } - else - { - deleteBtnFunc = [this, file] { - CollectionSystemManager::get()->deleteCollectionFiles(file); - ViewController::get()->getGameListView(file->getSystem()).get()->remove(file, true); - }; - } - - mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(), p, Utils::FileSystem::getFileName(file->getPath()), - std::bind(&IGameListView::onFileChanged, ViewController::get()->getGameListView(file->getSystem()).get(), file, FILE_METADATA_CHANGED), deleteBtnFunc, file)); -} - -void GuiGamelistOptions::jumpToLetter() -{ - char letter = mJumpToLetterList->getSelected(); - IGameListView* gamelist = getGamelist(); - - // this is a really shitty way to get a list of files - const std::vector& files = gamelist->getCursor()->getParent()->getChildrenListToDisplay(); - - long min = 0; - long max = (long)files.size() - 1; - long mid = 0; - - while(max >= min) - { - mid = ((max - min) / 2) + min; - - // game somehow has no first character to check - if(files.at(mid)->getName().empty()) - continue; - - char checkLetter = (char)toupper(files.at(mid)->getSortName()[0]); - - if(checkLetter < letter) - min = mid + 1; - else if(checkLetter > letter || (mid > 0 && (letter == toupper(files.at(mid - 1)->getSortName()[0])))) - max = mid - 1; - else - break; //exact match found - } - - gamelist->setCursor(files.at(mid)); - - delete this; -} - -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); -} - -HelpStyle GuiGamelistOptions::getHelpStyle() -{ - HelpStyle style = HelpStyle(); - style.applyTheme(mSystem->getTheme(), "system"); - return style; -} - -std::vector GuiGamelistOptions::getHelpPrompts() -{ - auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("CLOSE"))); - return prompts; -} - -IGameListView* GuiGamelistOptions::getGamelist() -{ - return ViewController::get()->getGameListView(mSystem).get(); -} + else + { + setPosition(x0, y1); + + auto fadeFunc = [this, x0, y1, y2](float t) { + + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + float y = y1 * (1 - pct) + y2 * pct; + setPosition(x0, y); + }; + + setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, x0, y2] + { + setPosition(x0, y2); + }); + + setPosition(x0, y2); + } +} + +GuiGamelistOptions::~GuiGamelistOptions() +{ + // apply sort + if (!fromPlaceholder) { + FileData* root = mSystem->getRootFolder(); + root->sort(*mListSort->getSelected()); // will also recursively sort children + + // notify that the root folder was sorted + getGamelist()->onFileChanged(root, FILE_SORTED); + } + + if (mSystem->setSystemViewMode(mViewMode->getSelected()) || mFiltersChanged) + { + // only reload full view if we came from a placeholder + // as we need to re-display the remaining elements for whatever new + // game is selected + ViewController::get()->reloadGameListView(mSystem); + } +} + +void GuiGamelistOptions::openGamelistFilter() +{ + mFiltersChanged = true; + GuiGamelistFilter* ggf = new GuiGamelistFilter(mWindow, mSystem); + mWindow->pushGui(ggf); +} + +void GuiGamelistOptions::startEditMode() +{ + std::string editingSystem = mSystem->getName(); + // need to check if we're editing the collections bundle, as we will want to edit the selected collection within + if(editingSystem == CollectionSystemManager::get()->getCustomCollectionsBundle()->getName()) + { + FileData* file = getGamelist()->getCursor(); + // do we have the cursor on a specific collection? + if (file->getType() == FOLDER) + { + editingSystem = file->getName(); + } + else + { + // we are inside a specific collection. We want to edit that one. + editingSystem = file->getSystem()->getName(); + } + } + CollectionSystemManager::get()->setEditMode(editingSystem); + delete this; +} + +void GuiGamelistOptions::exitEditMode() +{ + CollectionSystemManager::get()->exitEditMode(); + delete this; +} + +void GuiGamelistOptions::openMetaDataEd() +{ + // open metadata editor + // get the FileData that hosts the original metadata + FileData* file = getGamelist()->getCursor()->getSourceFileData(); + ScraperSearchParams p; + p.game = file; + p.system = file->getSystem(); + + std::function deleteBtnFunc; + + if (file->getType() == FOLDER) + { + deleteBtnFunc = NULL; + } + else + { + deleteBtnFunc = [this, file] { + CollectionSystemManager::get()->deleteCollectionFiles(file); + ViewController::get()->getGameListView(file->getSystem()).get()->remove(file, true); + }; + } + + mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(), p, Utils::FileSystem::getFileName(file->getPath()), + std::bind(&IGameListView::onFileChanged, ViewController::get()->getGameListView(file->getSystem()).get(), file, FILE_METADATA_CHANGED), deleteBtnFunc, file)); +} + +void GuiGamelistOptions::jumpToLetter() +{ + char letter = mJumpToLetterList->getSelected(); + IGameListView* gamelist = getGamelist(); + + // this is a really shitty way to get a list of files + const std::vector& files = gamelist->getCursor()->getParent()->getChildrenListToDisplay(); + + long min = 0; + long max = (long)files.size() - 1; + long mid = 0; + + while(max >= min) + { + mid = ((max - min) / 2) + min; + + // game somehow has no first character to check + if(files.at(mid)->getName().empty()) + continue; + + char checkLetter = (char)toupper(files.at(mid)->getName()[0]); + + if(checkLetter < letter) + min = mid + 1; + else if(checkLetter > letter || (mid > 0 && (letter == toupper(files.at(mid - 1)->getName()[0])))) + max = mid - 1; + else + break; //exact match found + } + + gamelist->setCursor(files.at(mid)); + + delete this; +} + +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); +} + +HelpStyle GuiGamelistOptions::getHelpStyle() +{ + HelpStyle style = HelpStyle(); + style.applyTheme(mSystem->getTheme(), "system"); + return style; +} + +std::vector GuiGamelistOptions::getHelpPrompts() +{ + auto prompts = mMenu.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", _T("CLOSE"))); + return prompts; +} + +IGameListView* GuiGamelistOptions::getGamelist() +{ + return ViewController::get()->getGameListView(mSystem).get(); +} diff --git a/es-app/src/guis/GuiGamelistOptions.h b/es-app/src/guis/GuiGamelistOptions.h index acf861669a..d6bfaf781c 100644 --- a/es-app/src/guis/GuiGamelistOptions.h +++ b/es-app/src/guis/GuiGamelistOptions.h @@ -35,6 +35,9 @@ class GuiGamelistOptions : public GuiComponent typedef OptionListComponent SortList; std::shared_ptr mListSort; + std::shared_ptr> mViewMode; + + SystemData* mSystem; IGameListView* getGamelist(); bool fromPlaceholder; diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 8173bc36c1..ed37b73f28 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -29,25 +29,25 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN if (isFullUI) { - addEntry(_T("UI SETTINGS"), theme->Text.color, true, [this] { openUISettings(); }); - addEntry(_T("CONFIGURE INPUT"), theme->Text.color, true, [this] { openConfigInput(); }); + addEntry(_T("UI SETTINGS"), theme->Text.color, true, [this] { openUISettings(); }, theme->MenuIcons.ui); + addEntry(_T("CONFIGURE INPUT"), theme->Text.color, true, [this] { openConfigInput(); }, theme->MenuIcons.controllers); } - addEntry(_T("SOUND SETTINGS"), theme->Text.color, true, [this] { openSoundSettings(); }); + addEntry(_T("SOUND SETTINGS"), theme->Text.color, true, [this] { openSoundSettings(); }, theme->MenuIcons.sound); if (isFullUI) - addEntry(_T("SCRAPER"), theme->Text.color, true, [this] { openScraperSettings(); }); + addEntry(_T("SCRAPER"), theme->Text.color, true, [this] { openScraperSettings(); }, theme->MenuIcons.scraper); if (isFullUI) { - addEntry(_T("GAME COLLECTION SETTINGS"), theme->Text.color, true, [this] { openCollectionSystemSettings(); }); - addEntry(_T("ADVANCED SETTINGS"), theme->Text.color, true, [this] { openOtherSettings(); }); + addEntry(_T("GAME COLLECTION SETTINGS"), theme->Text.color, true, [this] { openCollectionSystemSettings(); }, theme->MenuIcons.games); + addEntry(_T("ADVANCED SETTINGS"), theme->Text.color, true, [this] { openOtherSettings(); }, theme->MenuIcons.advanced); } -#if defined(_WIN32) - addEntry(_T("QUIT"), theme->Text.color, false, [this] {openQuitMenu(); }); +#ifdef WIN32 + addEntry(_T("QUIT"), theme->Text.color, false, [this] {openQuitMenu(); }, theme->MenuIcons.quit); #else - addEntry(_T("QUIT"), theme->Text.color, true, [this] {openQuitMenu(); }); + addEntry(_T("QUIT"), theme->Text.color, true, [this] {openQuitMenu(); }, theme->MenuIcons.quit); #endif addChild(&mMenu); @@ -58,23 +58,28 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN float y1 = Renderer::getScreenHeight(); float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); + if (Settings::getInstance()->getString("PowerSaverMode") == "instant" || Settings::getInstance()->getString("TransitionStyle") == "instant") + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + else + { + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); - auto fadeFunc = [this, y1, y2](float t) { - - t -= 1; // cubic ease out - float pct = Math::lerp(0, 1, t*t*t + 1); + auto fadeFunc = [this, y1, y2](float t) { - float y = y1 * (1 - pct) + y2 * pct; - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y); - }; + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + float y = y1 * (1 - pct) + y2 * pct; + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y); + }; + + setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] + { + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + }); - setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] - { setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); - }); - - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + } } void GuiMenu::openScraperSettings() @@ -234,6 +239,8 @@ void GuiMenu::openUISettings() { auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); + auto pthis = this; + // theme set auto theme = ThemeData::getMenuTheme(); auto themeSets = ThemeData::getThemeSets(); @@ -250,31 +257,281 @@ void GuiMenu::openUISettings() s->addWithLabel(_T("THEME"), theme_set); Window* window = mWindow; - s->addSaveFunc([window, theme_set] + s->addSaveFunc([window, theme_set, pthis] { - bool needReload = false; std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); if (oldTheme != theme_set->getSelected()) - needReload = true; + { + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); + + auto themeSubSets = ThemeData::getThemeSubSets(theme_set->getSelected()); + auto themeColorSets = ThemeData::sortThemeSubSets(themeSubSets, "colorset"); + auto themeIconSets = ThemeData::sortThemeSubSets(themeSubSets, "iconset"); + auto themeMenus = ThemeData::sortThemeSubSets(themeSubSets, "menu"); + auto themeSystemviewSets = ThemeData::sortThemeSubSets(themeSubSets, "systemview"); + auto themeGamelistViewSets = ThemeData::sortThemeSubSets(themeSubSets, "gamelistview"); + auto themeRegions = ThemeData::sortThemeSubSets(themeSubSets, "region"); + + // theme changed without setting options, forcing options to avoid crash/blank theme + if (themeRegions.empty()) + Settings::getInstance()->setString("ThemeRegionName", ""); + else + Settings::getInstance()->setString("ThemeRegionName", themeRegions.begin()->first); + + if (themeColorSets.empty()) + Settings::getInstance()->setString("ThemeColorSet", ""); + else + Settings::getInstance()->setString("ThemeColorSet", themeColorSets.begin()->first); + + if (themeIconSets.empty()) + Settings::getInstance()->setString("ThemeIconSet", ""); + else + Settings::getInstance()->setString("ThemeIconSet", themeIconSets.begin()->first); + + if (themeMenus.empty()) + Settings::getInstance()->setString("ThemeMenu", ""); + else + Settings::getInstance()->setString("ThemeMenu", themeMenus.begin()->first); + + if (themeSystemviewSets.empty()) + Settings::getInstance()->setString("ThemeSystemView", ""); + else + Settings::getInstance()->setString("ThemeSystemView", themeSystemviewSets.begin()->first); + + if (themeGamelistViewSets.empty()) + Settings::getInstance()->setString("ThemeGamelistView", ""); + else + Settings::getInstance()->setString("ThemeGamelistView", themeGamelistViewSets.begin()->first); + - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - if (needReload) - { Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); CollectionSystemManager::get()->updateSystemsList(); ViewController::get()->goToStart(); ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + + delete pthis; + window->pushGui(new GuiMenu(window)); } }); + + + auto system = ViewController::get()->getState().getSystem(); + if (system != NULL && system->getTheme()->hasSubsets()) + { + + // theme config + std::function openGui = [this, theme_set, window, pthis] { + auto themeconfig = new GuiSettings(mWindow, _T("THEME CONFIGURATION").c_str()); + + auto SelectedTheme = theme_set->getSelected(); + + auto themeSubSets = ThemeData::getThemeSubSets(SelectedTheme); + auto themeColorSets = ThemeData::sortThemeSubSets(themeSubSets, "colorset"); + auto themeIconSets = ThemeData::sortThemeSubSets(themeSubSets, "iconset"); + auto themeMenus = ThemeData::sortThemeSubSets(themeSubSets, "menu"); + auto themeSystemviewSets = ThemeData::sortThemeSubSets(themeSubSets, "systemview"); + auto themeGamelistViewSets = ThemeData::sortThemeSubSets(themeSubSets, "gamelistview"); + auto themeRegions = ThemeData::sortThemeSubSets(themeSubSets, "region"); + + // colorset + + auto selectedColorSet = themeColorSets.find(Settings::getInstance()->getString("ThemeColorSet")); + if (selectedColorSet == themeColorSets.end()) + selectedColorSet = themeColorSets.begin(); + auto theme_colorset = std::make_shared >(mWindow, _T("THEME COLORSET"), false); + + for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) + theme_colorset->add(it->first, it->first, it == selectedColorSet); + + if (!themeColorSets.empty()) + themeconfig->addWithLabel(_T("THEME COLORSET"), theme_colorset); + + // iconset + + auto selectedIconSet = themeIconSets.find(Settings::getInstance()->getString("ThemeIconSet")); + if (selectedIconSet == themeIconSets.end()) + selectedIconSet = themeIconSets.begin(); + auto theme_iconset = std::make_shared >(mWindow, _T("THEME ICONSET"), false); + + for (auto it = themeIconSets.begin(); it != themeIconSets.end(); it++) + theme_iconset->add(it->first, it->first, it == selectedIconSet); + + if (!themeIconSets.empty()) + themeconfig->addWithLabel(_T("THEME ICONSET"), theme_iconset); + + // menu + + auto selectedMenu = themeMenus.find(Settings::getInstance()->getString("ThemeMenu")); + if (selectedMenu == themeMenus.end()) + selectedMenu = themeMenus.begin(); + auto theme_menu = std::make_shared >(mWindow, _T("THEME MENU"), false); + + for (auto it = themeMenus.begin(); it != themeMenus.end(); it++) + theme_menu->add(it->first, it->first, it == selectedMenu); + + if (!themeMenus.empty()) + themeconfig->addWithLabel(_T("THEME MENU"), theme_menu); + + // systemview + + auto selectedSystemviewSet = themeSystemviewSets.find(Settings::getInstance()->getString("ThemeSystemView")); + if (selectedSystemviewSet == themeSystemviewSets.end()) + selectedSystemviewSet = themeSystemviewSets.begin(); + + auto theme_systemview = std::make_shared >(mWindow, _T("THEME SYSTEMVIEW"), false); + + for (auto it = themeSystemviewSets.begin(); it != themeSystemviewSets.end(); it++) + theme_systemview->add(it->first, it->first, it == selectedSystemviewSet); + + if (!themeSystemviewSets.empty()) + themeconfig->addWithLabel(_T("THEME SYSTEMVIEW"), theme_systemview); + + // gamelistview + + auto selectedGamelistViewSet = themeGamelistViewSets.find(Settings::getInstance()->getString("ThemeGamelistView")); + if (selectedGamelistViewSet == themeGamelistViewSets.end()) + selectedGamelistViewSet = themeGamelistViewSets.begin(); + + auto theme_gamelistview = std::make_shared >(mWindow, _T("THEME GAMELISTVIEW"), false); + + for (auto it = themeGamelistViewSets.begin(); it != themeGamelistViewSets.end(); it++) + theme_gamelistview->add(it->first, it->first, it == selectedGamelistViewSet); + + if (!themeGamelistViewSets.empty()) + themeconfig->addWithLabel(_T("THEME GAMELISTVIEW"), theme_gamelistview); + + // themeregion + auto selectedRegion = themeRegions.find(Settings::getInstance()->getString("ThemeRegionName")); + if (selectedRegion == themeRegions.end()) + selectedRegion = themeRegions.begin(); + + auto theme_region = std::make_shared >(mWindow, _T("THEME GAMELISTVIEW"), false); + + for (auto it = themeRegions.begin(); it != themeRegions.end(); it++) + theme_region->add(it->first, it->first, it == selectedRegion); + + if (!themeRegions.empty()) + themeconfig->addWithLabel(_T("THEME REGION"), theme_region); + + themeconfig->addSaveFunc([this, window, theme_set, theme_colorset, theme_iconset, theme_menu, theme_systemview, theme_gamelistview, theme_region, pthis] { + bool needReload = false; + if (Settings::getInstance()->getString("ThemeColorSet") != theme_colorset->getSelected() && !theme_colorset->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeIconSet") != theme_iconset->getSelected() && !theme_iconset->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeMenu") != theme_menu->getSelected() && !theme_menu->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeSystemView") != theme_systemview->getSelected() && !theme_systemview->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeGamelistView") != theme_gamelistview->getSelected() && !theme_gamelistview->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeRegionName") != theme_region->getSelected() && !theme_region->getSelected().empty()) + needReload = true; + + if (needReload) { + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); + Settings::getInstance()->setString("ThemeColorSet", theme_colorset->getSelected()); + Settings::getInstance()->setString("ThemeIconSet", theme_iconset->getSelected()); + Settings::getInstance()->setString("ThemeMenu", theme_menu->getSelected()); + Settings::getInstance()->setString("ThemeSystemView", theme_systemview->getSelected()); + Settings::getInstance()->setString("ThemeGamelistView", theme_gamelistview->getSelected()); + Settings::getInstance()->setString("ThemeRegionName", theme_region->getSelected()); + //Settings::getInstance()->setBool("ThemeChanged", true); + + //reload theme + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + + delete pthis; + window->pushGui(new GuiMenu(window)); + //ReloadAll(); + } + }); + if (!themeRegions.empty() || !themeGamelistViewSets.empty() || !themeSystemviewSets.empty() || !themeIconSets.empty() || !themeMenus.empty() || !themeColorSets.empty()) + { + themeconfig->updatePosition(); + mWindow->pushGui(themeconfig); + } + else + mWindow->pushGui(new GuiMsgBox(window, _T("THIS THEME HAS NO OPTION"), _T("OK"))); + }; + + s->addSubMenu(_T("THEME CONFIGURATION"), openGui); + } + } + + // GameList view style + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); + std::vector styles; + styles.push_back("automatic"); + + + auto system = ViewController::get()->getState().getSystem(); + if (system != NULL) + { + auto mViews = system->getTheme()->getViewsOfTheme(); + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + styles.push_back(*it); } + else + { + styles.push_back("basic"); + styles.push_back("detailed"); + styles.push_back("video"); + styles.push_back("grid"); + } + + for (auto it = styles.cbegin(); it != styles.cend(); it++) + gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); + + s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); + s->addSaveFunc([gamelist_style] { + bool needReload = false; + if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) + needReload = true; + Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); + if (needReload) + ViewController::get()->reloadAll(); + }); + + + //#ifndef WIN32 + //UI mode + auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); + std::vector UImodes = UIModeController::getInstance()->getUIModes(); + for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) + UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); + s->addWithLabel("UI MODE", UImodeSelection); + Window* window = mWindow; + s->addSaveFunc([UImodeSelection, window] + { + std::string selectedMode = UImodeSelection->getSelected(); + if (selectedMode != "Full") + { + std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; + msg += "This will hide most menu-options to prevent changes to the system.\n"; + msg += "To unlock and return to the full UI, enter this code: \n"; + msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; + msg += "Do you want to proceed?"; + window->pushGui(new GuiMsgBox(window, msg, + "YES", [selectedMode] { + LOG(LogDebug) << "Setting UI mode to " << selectedMode; + Settings::getInstance()->setString("UIMode", selectedMode); + Settings::getInstance()->saveFile(); + }, "NO", nullptr)); + } + }); + //#endif // LANGUAGE std::vector langues; langues.push_back("en"); - std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); if (xmlpath.length() > 0) { @@ -354,29 +611,6 @@ void GuiMenu::openUISettings() Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); }); - // GameList view style - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); - std::vector styles; - styles.push_back("automatic"); - styles.push_back("basic"); - styles.push_back("detailed"); - styles.push_back("video"); - styles.push_back("grid"); - styles.push_back("gridex"); - - for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); - - s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); - s->addSaveFunc([gamelist_style] { - bool needReload = false; - if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) - needReload = true; - Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); - if (needReload) - ViewController::get()->reloadAll(); - }); - // Optionally start in selected system auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); @@ -392,43 +626,19 @@ void GuiMenu::openUISettings() s->addSaveFunc([systemfocus_list] { Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); }); - - - /* - //UI mode - auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); - std::vector UImodes = UIModeController::getInstance()->getUIModes(); - for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) - UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); - s->addWithLabel("UI MODE", UImodeSelection); - Window* window = mWindow; - s->addSaveFunc([ UImodeSelection, window] - { - std::string selectedMode = UImodeSelection->getSelected(); - if (selectedMode != "Full") - { - std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; - msg += "This will hide most menu-options to prevent changes to the system.\n"; - msg += "To unlock and return to the full UI, enter this code: \n"; - msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; - msg += "Do you want to proceed?"; - window->pushGui(new GuiMsgBox(window, msg, - "YES", [selectedMode] { - LOG(LogDebug) << "Setting UI mode to " << selectedMode; - Settings::getInstance()->setString("UIMode", selectedMode); - Settings::getInstance()->saveFile(); - }, "NO",nullptr)); - } - }); - */ - - // screensaver - ComponentListRow screensaver_row; - screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); - screensaver_row.addElement(makeArrow(mWindow), false); - screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); - s->addRow(screensaver_row); + + // Open gamelist at start + auto bootOnGamelist = std::make_shared(mWindow); + bootOnGamelist->setState(Settings::getInstance()->getBool("StartupOnGameList")); + s->addWithLabel(_T("BOOT ON GAMELIST"), bootOnGamelist); + s->addSaveFunc([bootOnGamelist] { Settings::getInstance()->setBool("StartupOnGameList", bootOnGamelist->getState()); }); + + // Hide system view + auto hideSystemView = std::make_shared(mWindow); + hideSystemView->setState(Settings::getInstance()->getBool("HideSystemView")); + s->addWithLabel(_T("HIDE SYSTEM VIEW"), hideSystemView); + s->addSaveFunc([hideSystemView] { Settings::getInstance()->setBool("HideSystemView", hideSystemView->getState()); }); + #if defined(_WIN32) // quick system select (left/right in game list view) @@ -475,9 +685,18 @@ void GuiMenu::openUISettings() if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); }); + + // screensaver + ComponentListRow screensaver_row; + screensaver_row.elements.clear(); + screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); + screensaver_row.addElement(makeArrow(mWindow), false); + screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); + s->addRow(screensaver_row); + + s->updatePosition(); mWindow->pushGui(s); - } void GuiMenu::openOtherSettings() @@ -504,6 +723,7 @@ void GuiMenu::openOtherSettings() s->addSaveFunc([this, power_saver] { if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { Settings::getInstance()->setString("TransitionStyle", "instant"); + Settings::getInstance()->setString("GameTransitionStyle", "instant"); Settings::getInstance()->setBool("MoveCarousel", false); Settings::getInstance()->setBool("EnableSounds", false); } @@ -521,12 +741,14 @@ void GuiMenu::openOtherSettings() parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); - /* + +#ifndef WIN32 auto local_art = std::make_shared(mWindow); local_art->setState(Settings::getInstance()->getBool("LocalArt")); s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); - */ +#endif + // hidden files auto hidden_files = std::make_shared(mWindow); hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); @@ -553,7 +775,13 @@ void GuiMenu::openOtherSettings() #endif -#if defined(_WIN32) + // preload UI + auto preloadUI = std::make_shared(mWindow); + preloadUI->setState(Settings::getInstance()->getBool("PreloadUI")); + s->addWithLabel(_T("PRELOAD UI"), preloadUI); + s->addSaveFunc([preloadUI] { Settings::getInstance()->setBool("PreloadUI", preloadUI->getState()); }); + +#ifdef WIN32 // vsync auto vsync = std::make_shared(mWindow); vsync->setState(Settings::getInstance()->getBool("VSync")); @@ -585,7 +813,6 @@ void GuiMenu::openOtherSettings() framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); s->addWithLabel(_T("SHOW FRAMERATE"), framerate); s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); - s->updatePosition(); mWindow->pushGui(s); @@ -607,7 +834,7 @@ void GuiMenu::openConfigInput() void GuiMenu::openQuitMenu() { -#if defined(_WIN32) +#ifdef WIN32 Scripting::fireEvent("quit"); quitES(""); return; @@ -705,7 +932,7 @@ void GuiMenu::onSizeChanged() mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); } -void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) +void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func, const std::string iconName) { auto theme = ThemeData::getMenuTheme(); std::shared_ptr font = theme->Text.font; @@ -713,6 +940,22 @@ void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, con // populate the list ComponentListRow row; + + if (!iconName.empty()) + { + // icon + auto icon = std::make_shared(mWindow); + icon->setImage(iconName); + icon->setColorShift(theme->Text.color); + icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); + row.addElement(icon, false); + + // spacer between icon and text + auto spacer = std::make_shared(mWindow); + spacer->setSize(10, 0); + row.addElement(spacer, false); + } + row.addElement(std::make_shared(mWindow, name, font, color), true); if (add_arrow) diff --git a/es-app/src/guis/GuiMenu.h b/es-app/src/guis/GuiMenu.h index 25fdcc2634..99c7ef5445 100644 --- a/es-app/src/guis/GuiMenu.h +++ b/es-app/src/guis/GuiMenu.h @@ -16,7 +16,7 @@ class GuiMenu : public GuiComponent HelpStyle getHelpStyle() override; private: - void addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func); + void addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func, const std::string iconName); void addVersionInfo(); void openCollectionSystemSettings(); void openConfigInput(); diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index ee9abc7a1f..285e5f0d92 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -209,7 +209,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector row.addElement(spacer, false); auto bracket = std::make_shared(mWindow); - bracket->setImage(":/arrow.svg"); + bracket->setImage(ThemeData::getMenuTheme()->Icons.arrow);// ":/arrow.svg"); bracket->setResize(Vector2f(0, lbl->getFont()->getLetterHeight())); row.addElement(bracket, false); diff --git a/es-app/src/guis/GuiSettings.h b/es-app/src/guis/GuiSettings.h index ac9a22d084..c9793156a5 100644 --- a/es-app/src/guis/GuiSettings.h +++ b/es-app/src/guis/GuiSettings.h @@ -18,6 +18,18 @@ class GuiSettings : public GuiComponent inline void addWithLabel(const std::string& label, const std::shared_ptr& comp) { mMenu.addWithLabel(label, comp); }; inline void addSaveFunc(const std::function& func) { mSaveFuncs.push_back(func); }; + inline void addSubMenu(const std::string& label, const std::function& func) { + ComponentListRow row; + row.makeAcceptInputHandler(func); + + auto theme = ThemeData::getMenuTheme(); + + auto entryMenu = std::make_shared(mWindow, label, theme->Text.font, theme->Text.color); + row.addElement(entryMenu, true); + row.addElement(makeArrow(mWindow), false); + mMenu.addRow(row); + }; + bool input(InputConfig* config, Input input) override; std::vector getHelpPrompts() override; HelpStyle getHelpStyle() override; diff --git a/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp b/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp index e29b3e2e9a..de06342efe 100644 --- a/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp +++ b/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp @@ -95,7 +95,7 @@ void GuiSlideshowScreensaverOptions::addEditableTextComponent(ComponentListRow r row.addElement(spacer, false); auto bracket = std::make_shared(mWindow); - bracket->setImage(":/arrow.svg"); + bracket->setImage(ThemeData::getMenuTheme()->Icons.arrow); // ":/arrow.svg"); bracket->setResize(Vector2f(0, lbl->getFont()->getLetterHeight())); row.addElement(bracket, false); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 2f42e183f5..cd8edc9cf0 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -338,7 +338,7 @@ int main(int argc, char* argv[]) if (!scrape_cmdline) { - if(!window.init()) + if(!window.init(true)) { LOG(LogError) << "Window failed to initialize!"; return 1; @@ -385,7 +385,9 @@ int main(int argc, char* argv[]) // 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(); + + if (Settings::getInstance()->getBool("PreloadUI")) + ViewController::get()->preload(); if (splashScreen && splashScreenProgress) window.renderLoadingScreen(_T("Starting UI")); @@ -417,7 +419,7 @@ int main(int argc, char* argv[]) displayFrequency = lpDevMode.dmDisplayFrequency; // default value if cannot retrieve from user settings. } - int timeLimit = (1000 / displayFrequency) - 5; // Margin for vsync + int timeLimit = (1000 / displayFrequency) - 6; // Margin for vsync #endif int lastTime = SDL_GetTicks(); @@ -502,7 +504,7 @@ int main(int argc, char* argv[]) while(window.peekGui() != ViewController::get()) delete window.peekGui(); - window.deinit(); + window.deinit(true); MameNames::deinit(); CollectionSystemManager::deinit(); diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 15fd7eb10a..9f8de5b5bf 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -45,6 +45,9 @@ ViewController::~ViewController() void ViewController::goToStart(bool forceImmediate) { + bool hideSystemView = Settings::getInstance()->getBool("HideSystemView"); + bool startOnGamelist = Settings::getInstance()->getBool("StartupOnGameList"); + // If specific system is requested, go directly to the game list auto requestedSystem = Settings::getInstance()->getString("StartupSystem"); if("" != requestedSystem && "retropie" != requestedSystem) @@ -52,8 +55,11 @@ void ViewController::goToStart(bool forceImmediate) for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++){ if ((*it)->getName() == requestedSystem) { - goToSystemView(*it, forceImmediate); -// goToGameList(*it); + if (hideSystemView || startOnGamelist) + goToGameList(*it); + else + goToSystemView(*it, forceImmediate); + return; } } @@ -62,7 +68,10 @@ void ViewController::goToStart(bool forceImmediate) Settings::getInstance()->setString("StartupSystem", ""); } - goToSystemView(SystemData::sSystemVector.at(0), forceImmediate); + if (hideSystemView || startOnGamelist) + goToGameList(SystemData::sSystemVector.at(0)); + else + goToSystemView(SystemData::sSystemVector.at(0), forceImmediate); } void ViewController::ReloadAndGoToStart() @@ -284,75 +293,86 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste std::shared_ptr view; bool themeHasVideoView = system->getTheme()->hasView("video"); - bool themeHasGridView = system->getTheme()->hasView("grid"); - bool themeHasGridExView = system->getTheme()->hasView("gridex"); //decide type GameListViewType selectedViewType = AUTOMATIC; - bool detailed = false; + bool allowDetailedDowngrade = false; + bool forceView = false; std::string viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + std::string customThemeName; + + if (!system->getSystemViewMode().empty() && system->getTheme()->hasView(system->getSystemViewMode())) + { + viewPreference = system->getSystemViewMode(); + forceView = true; + } + + if (viewPreference == "automatic") + { + auto defaultView = system->getTheme()->getDefaultView(); + if (!defaultView.empty() && system->getTheme()->hasView(defaultView)) + viewPreference = defaultView; + } + + if (system->getTheme()->isCustomView(viewPreference)) + { + auto baseClass = system->getTheme()->getCustomViewBaseType(viewPreference); + if (!baseClass.empty()) // this is a customView + { + customThemeName = viewPreference; + viewPreference = baseClass; + } + } if (viewPreference.compare("basic") == 0) selectedViewType = BASIC; else if (viewPreference.compare("detailed") == 0) { - detailed = true; + allowDetailedDowngrade = true; selectedViewType = DETAILED; } - else if (themeHasGridExView && viewPreference.compare("gridex") == 0) - selectedViewType = GRIDEX; else if (themeHasGridView && viewPreference.compare("grid") == 0) selectedViewType = GRID; else if (viewPreference.compare("video") == 0) selectedViewType = VIDEO; - if (selectedViewType == AUTOMATIC || detailed) - { - if (themeHasGridView && system->getTheme()->getDefaultView() == "grid" && !detailed) - selectedViewType = GRID; - else if (themeHasGridExView && system->getTheme()->getDefaultView() == "gridex" && !detailed) - selectedViewType = GRIDEX; - else - { - selectedViewType = BASIC; + if (!forceView && (selectedViewType == AUTOMATIC || allowDetailedDowngrade)) + { + selectedViewType = BASIC; - if (system->getTheme()->getDefaultView() == "detailed") - selectedViewType = DETAILED; - else if (system->getTheme()->getDefaultView() != "basic") + if (system->getTheme()->getDefaultView() != "basic") + { + std::vector files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER); + for (auto it = files.cbegin(); it != files.cend(); it++) { - std::vector files = system->getRootFolder()->getFilesRecursive(GAME | FOLDER); - for (auto it = files.cbegin(); it != files.cend(); it++) + if (themeHasVideoView && !(*it)->getVideoPath().empty() && viewPreference.compare("detailed") != 0) { - if (themeHasVideoView && !(*it)->getVideoPath().empty() && viewPreference.compare("detailed") != 0) - { - selectedViewType = VIDEO; - break; - } - else if (!(*it)->getThumbnailPath().empty()) + selectedViewType = VIDEO; + break; + } + else if (!(*it)->getThumbnailPath().empty()) + { + /* + if (!allowDetailedDowngrade && (*it)->metadata.get("thumbnail").length() > 0) { - if (!detailed && (*it)->metadata.get("thumbnail").length() > 0) - { - if (themeHasGridExView && (*it)->metadata.get("image").length() > 0) - selectedViewType = GRIDEX; - else if (themeHasGridView) - selectedViewType = GRID; - else - selectedViewType = DETAILED; - } + if (themeHasGridView) + selectedViewType = GRID; else selectedViewType = DETAILED; + } + else*/ + selectedViewType = DETAILED; - if (!themeHasVideoView) - break; + if (!themeHasVideoView) + break; - // Don't break out in case any subsequent files have video - } + // Don't break out in case any subsequent files have video } } - } + } } // Create the view @@ -364,22 +384,18 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste case DETAILED: view = std::shared_ptr(new DetailedGameListView(mWindow, system->getRootFolder())); break; - case GRID: + case GRID: view = std::shared_ptr(new GridGameListView(mWindow, system->getRootFolder())); break; - case GRIDEX: - { - GridGameListView* listView = new GridGameListView(mWindow, system->getRootFolder()); - listView->setGridEx(); - view = std::shared_ptr(listView); - } - break; case BASIC: default: view = std::shared_ptr(new BasicGameListView(mWindow, system->getRootFolder())); break; } + if (!customThemeName.empty()) + view->setThemeName(customThemeName); + view->setTheme(system->getTheme()); std::vector& sysVec = SystemData::sSystemVector; @@ -536,7 +552,6 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) // Redisplay the current view if (mCurrentView) mCurrentView->onShow(); - } void ViewController::reloadAll() diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h index 7aef661c1c..27900588f9 100644 --- a/es-app/src/views/ViewController.h +++ b/es-app/src/views/ViewController.h @@ -62,8 +62,8 @@ class ViewController : public GuiComponent BASIC, DETAILED, GRID, - VIDEO, - GRIDEX + VIDEO + //,GRIDEX }; struct State diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index c697a09130..51e51c598a 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -18,6 +18,12 @@ BasicGameListView::BasicGameListView(Window* window, FileData* root) populateList(root->getChildrenListToDisplay()); } +void BasicGameListView::setThemeName(std::string name) +{ + ISimpleGameListView::setThemeName(name); + // mGrid.setThemeName(getName()); +} + void BasicGameListView::onThemeChanged(const std::shared_ptr& theme) { ISimpleGameListView::onThemeChanged(theme); diff --git a/es-app/src/views/gamelist/BasicGameListView.h b/es-app/src/views/gamelist/BasicGameListView.h index f1a216b0f5..7cc8351798 100644 --- a/es-app/src/views/gamelist/BasicGameListView.h +++ b/es-app/src/views/gamelist/BasicGameListView.h @@ -18,11 +18,19 @@ class BasicGameListView : public ISimpleGameListView virtual FileData* getCursor() override; virtual void setCursor(FileData* file) override; - virtual const char* getName() const override { return "basic"; } + virtual const char* getName() const override + { + if (!mCustomThemeName.empty()) + return mCustomThemeName.c_str(); + + return "basic"; + } virtual std::vector getHelpPrompts() override; virtual void launch(FileData* game) override; + virtual void setThemeName(std::string name); + protected: virtual std::string getQuickSystemSelectRightButton() override; virtual std::string getQuickSystemSelectLeftButton() override; diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index bfb26740ce..67e22bf3eb 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -1,13 +1,24 @@ #include "views/gamelist/DetailedGameListView.h" #include "animations/LambdaAnimation.h" + +#ifdef _RPI_ +#include "components/VideoPlayerComponent.h" +#endif +#include "components/VideoVlcComponent.h" +#include "utils/FileSystemUtil.h" #include "views/ViewController.h" +#ifdef _RPI_ +#include "Settings.h" +#endif DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : BasicGameListView(window, root), mDescContainer(window), mDescription(window), mImage(window), + mVideo(nullptr), + mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -17,6 +28,8 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : { //mHeaderImage.setPosition(mSize.x() * 0.25f, 0); + mVideoVisible = false; + const float padding = 0.01f; mList.setPosition(mSize.x() * (0.50f + padding), mList.getPosition().y()); @@ -24,12 +37,29 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mList.setAlignment(TextListComponent::ALIGN_LEFT); mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); + // Create the correct type of video window +#ifdef _RPI_ + if (Settings::getInstance()->getBool("VideoOmxPlayer")) + mVideo = new VideoPlayerComponent(window, ""); + else +#endif + mVideo = new VideoVlcComponent(window, getTitlePath()); + // 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); mImage.setDefaultZIndex(30); - addChild(&mImage); + addChild(&mImage); + + // video + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); + mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); + mVideo->setStartDelay(2000); + mVideo->setDefaultZIndex(30); + + //addChild(mVideo); -> Add only if present in theme later // metadata labels + values mLblRating.setText(_T("Rating") + ": "); @@ -81,6 +111,11 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : updateInfoPanel(); } +DetailedGameListView::~DetailedGameListView() +{ + delete mVideo; +} + void DetailedGameListView::onThemeChanged(const std::shared_ptr& theme) { BasicGameListView::onThemeChanged(theme); @@ -89,6 +124,18 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); mName.applyTheme(theme, getName(), "md_name", ALL); + if (theme->getElement(getName(), "md_video", "video")) + { + mVideoVisible = true; + mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION); + addChild(mVideo); + } + else + { + mVideoVisible = false; + removeChild(mVideo); + } + initMDLabels(); std::vector labels = getMDLabels(); assert(labels.size() == 8); @@ -202,17 +249,36 @@ void DetailedGameListView::updateInfoPanel() FileData* file = (mList.size() == 0 || mList.isScrolling()) ? NULL : mList.getSelected(); bool fadingOut; - if(file == NULL) + if (file == NULL) { + mVideo->setVideo(""); + mVideo->setImage(""); + //mImage.setImage(""); //mDescription.setText(""); fadingOut = true; }else{ + if (mVideoVisible) + { + if (!mVideo->setVideo(file->getVideoPath())) + mVideo->setDefaultVideo(); + } + if (file->getImagePath().empty()) - mImage.setImage(file->getThumbnailPath()); + { + if (mVideoVisible) + mVideo->setImage(file->getThumbnailPath(), false, mImage.getSize()); + + mImage.setImage(file->getThumbnailPath(), false, mImage.getSize()); + } else - mImage.setImage(file->getImagePath()); + { + if (mVideoVisible) + mVideo->setImage(file->getImagePath(), false, mImage.getSize()); + + mImage.setImage(file->getImagePath(), false, mImage.getSize()); + } mDescription.setText(getMetadata(file, "desc")); mDescContainer.reset(); @@ -236,6 +302,7 @@ void DetailedGameListView::updateInfoPanel() std::vector comps = getMDValues(); comps.push_back(&mImage); + comps.push_back(mVideo); comps.push_back(&mDescription); comps.push_back(&mName); std::vector labels = getMDLabels(); @@ -296,3 +363,16 @@ std::vector DetailedGameListView::getMDValues() ret.push_back(&mPlayCount); return ret; } + +void DetailedGameListView::update(int deltaTime) +{ + BasicGameListView::update(deltaTime); + + mImage.setVisible(mVideo == NULL || !(mVideo->isPlaying() && !mVideo->isFading())); +} + +void DetailedGameListView::onShow() +{ + GuiComponent::onShow(); + updateInfoPanel(); +} diff --git a/es-app/src/views/gamelist/DetailedGameListView.h b/es-app/src/views/gamelist/DetailedGameListView.h index 3c070c955f..9f2f00a044 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.h +++ b/es-app/src/views/gamelist/DetailedGameListView.h @@ -7,17 +7,30 @@ #include "components/ScrollableContainer.h" #include "views/gamelist/BasicGameListView.h" +class VideoComponent; + class DetailedGameListView : public BasicGameListView { public: DetailedGameListView(Window* window, FileData* root); + ~DetailedGameListView(); virtual void onThemeChanged(const std::shared_ptr& theme) override; + virtual void onShow() override; + + virtual const char* getName() const override + { + if (!mCustomThemeName.empty()) + return mCustomThemeName.c_str(); - virtual const char* getName() const override { return "detailed"; } + return "detailed"; + } virtual void launch(FileData* game) override; +protected: + virtual void update(int deltaTime) override; + private: void updateInfoPanel(); @@ -38,12 +51,15 @@ class DetailedGameListView : public BasicGameListView DateTimeComponent mLastPlayed; TextComponent mPlayCount; TextComponent mName; + VideoComponent* mVideo; std::vector getMDLabels(); std::vector getMDValues(); ScrollableContainer mDescContainer; TextComponent mDescription; + + bool mVideoVisible; }; #endif // ES_APP_VIEWS_GAME_LIST_DETAILED_GAME_LIST_VIEW_H diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index a2dbc27428..4d3ac60834 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -22,8 +22,6 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : { const float padding = 0.01f; - mGridEx = false; - mGrid.setPosition(mSize.x() * 0.1f, mSize.y() * 0.1f); mGrid.setDefaultZIndex(20); mGrid.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); @@ -88,9 +86,9 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : updateInfoPanel(); } -void GridGameListView::setGridEx() +void GridGameListView::setThemeName(std::string name) { - mGridEx = true; + ISimpleGameListView::setThemeName(name); mGrid.setThemeName(getName()); } @@ -139,19 +137,19 @@ void GridGameListView::populateList(const std::vector& files) if ((*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) { if (systemName == "favorites") - mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it); + mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), (*it)->getVideoPath(), *it); else - mGrid.add(_T("\uF006 ") + (*it)->getName(), (*it)->getThumbnailPath(), *it); + mGrid.add(_T("\uF006 ") + (*it)->getName(), (*it)->getThumbnailPath(), (*it)->getVideoPath(), *it); } for (auto it = files.cbegin(); it != files.cend(); it++) if (!(*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) - mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), *it); + mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), (*it)->getVideoPath(), *it); } else { addPlaceholder(); - } + } } void GridGameListView::onThemeChanged(const std::shared_ptr& theme) @@ -286,9 +284,9 @@ void GridGameListView::updateInfoPanel() if (mImageVisible) { if (file->getImagePath().empty()) - mImage.setImage(file->getThumbnailPath()); + mImage.setImage(file->getThumbnailPath(), false, mImage.getSize()); else - mImage.setImage(file->getImagePath()); + mImage.setImage(file->getImagePath(), false, mImage.getSize()); } else mImage.setImage(""); @@ -343,7 +341,7 @@ void GridGameListView::addPlaceholder() { // empty grid - add a placeholder FileData* placeholder = new FileData(PLACEHOLDER, "", this->mRoot->getSystem()->getSystemEnvData(), this->mRoot->getSystem()); - mGrid.add(placeholder->getName(), "", placeholder); + mGrid.add(placeholder->getName(), "", "", placeholder); } void GridGameListView::launch(FileData* game) @@ -380,6 +378,19 @@ void GridGameListView::remove(FileData *game, bool deleteFile) onFileChanged(parent, FILE_REMOVED); // update the view, with game removed } +void GridGameListView::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); +} + + std::vector GridGameListView::getMDLabels() { std::vector ret; @@ -412,20 +423,26 @@ std::vector GridGameListView::getHelpPrompts() { std::vector prompts; - if(Settings::getInstance()->getBool("QuickSystemSelect")) + if (Settings::getInstance()->getBool("QuickSystemSelect")) prompts.push_back(HelpPrompt("lr", _T("SYSTEM"))); prompts.push_back(HelpPrompt("up/down/left/right", _T("CHOOSE"))); prompts.push_back(HelpPrompt("a", _T("LAUNCH"))); - prompts.push_back(HelpPrompt("b", _T("BACK"))); - if(!UIModeController::getInstance()->isUIModeKid()) + + if (!Settings::getInstance()->getBool("HideSystemView")) + prompts.push_back(HelpPrompt("b", _T("BACK"))); + + if (!UIModeController::getInstance()->isUIModeKid()) prompts.push_back(HelpPrompt("select", _T("OPTIONS"))); - if(mRoot->getSystem()->isGameSystem()) + + if (mRoot->getSystem()->isGameSystem()) prompts.push_back(HelpPrompt("x", _T("RANDOM"))); - if(mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) + if (mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) { std::string prompt = CollectionSystemManager::get()->getEditingCollection(); prompts.push_back(HelpPrompt("y", prompt)); } return prompts; } + + diff --git a/es-app/src/views/gamelist/GridGameListView.h b/es-app/src/views/gamelist/GridGameListView.h index 50cfc24ab7..d7fb787bc5 100644 --- a/es-app/src/views/gamelist/GridGameListView.h +++ b/es-app/src/views/gamelist/GridGameListView.h @@ -23,16 +23,17 @@ class GridGameListView : public ISimpleGameListView virtual const char* getName() const override { - if (mGridEx) - return "gridex"; + if (!mCustomThemeName.empty()) + return mCustomThemeName.c_str(); return "grid"; } virtual std::vector getHelpPrompts() override; virtual void launch(FileData* game) override; + virtual void onFileChanged(FileData* file, FileChangeType change); - void setGridEx(); + virtual void setThemeName(std::string name); protected: virtual std::string getQuickSystemSelectRightButton() override; @@ -63,7 +64,6 @@ class GridGameListView : public ISimpleGameListView ImageComponent mImage; bool mImageVisible; - bool mGridEx; std::vector getMDLabels(); std::vector getMDValues(); diff --git a/es-app/src/views/gamelist/IGameListView.cpp b/es-app/src/views/gamelist/IGameListView.cpp index 722eeabcdd..f2335dbb95 100644 --- a/es-app/src/views/gamelist/IGameListView.cpp +++ b/es-app/src/views/gamelist/IGameListView.cpp @@ -6,6 +6,11 @@ #include "Sound.h" #include "Window.h" +void IGameListView::setThemeName(std::string name) +{ + mCustomThemeName = name; +} + bool IGameListView::input(InputConfig* config, Input input) { // select to open GuiGamelistOptions diff --git a/es-app/src/views/gamelist/IGameListView.h b/es-app/src/views/gamelist/IGameListView.h index 988981593b..63acf6a45d 100644 --- a/es-app/src/views/gamelist/IGameListView.h +++ b/es-app/src/views/gamelist/IGameListView.h @@ -41,7 +41,12 @@ class IGameListView : public GuiComponent virtual HelpStyle getHelpStyle() override; void render(const Transform4x4f& parentTrans) override; + + virtual void setThemeName(std::string name); + protected: + std::string mCustomThemeName; + FileData* mRoot; std::shared_ptr mTheme; }; diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 90b1c032ef..7425391c17 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -78,7 +78,9 @@ void ISimpleGameListView::onFileChanged(FileData* /*file*/, FileChangeType /*cha bool ISimpleGameListView::input(InputConfig* config, Input input) { - if(input.value != 0) + bool hideSystemView = Settings::getInstance()->getBool("HideSystemView"); + + if (input.value != 0) { if (config->isMappedTo("a", input)) { @@ -112,25 +114,26 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } else if(config->isMappedTo("b", input)) { - if(mCursorStack.size()) + if (mCursorStack.size()) { populateList(mCursorStack.top()->getParent()->getChildren()); setCursor(mCursorStack.top()); mCursorStack.pop(); Sound::getFromTheme(getTheme(), getName(), "back")->play(); - }else{ + } + else if (!hideSystemView) + { onFocusLost(); SystemData* systemToView = getCursor()->getSystem(); if (systemToView->isCollection()) - { systemToView = CollectionSystemManager::get()->getSystemToView(systemToView); - } + ViewController::get()->goToSystemView(systemToView); } return true; } - else if(config->isMappedLike(getQuickSystemSelectRightButton(), input)) + else if (config->isMappedLike(getQuickSystemSelectRightButton(), input) || config->isMappedLike("rightshoulder", input)) { if(Settings::getInstance()->getBool("QuickSystemSelect")) { @@ -139,7 +142,7 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) return true; } } - else if(config->isMappedLike(getQuickSystemSelectLeftButton(), input)) + else if (config->isMappedLike(getQuickSystemSelectLeftButton(), input) || config->isMappedLike("leftshoulder", input)) { if(Settings::getInstance()->getBool("QuickSystemSelect")) { diff --git a/es-app/src/views/gamelist/ISimpleGameListView.h b/es-app/src/views/gamelist/ISimpleGameListView.h index f161eadd06..6980dc2443 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.h +++ b/es-app/src/views/gamelist/ISimpleGameListView.h @@ -36,6 +36,7 @@ class ISimpleGameListView : public IGameListView ImageComponent mHeaderImage; ImageComponent mBackground; + std::vector mThemeExtras; std::stack mCursorStack; diff --git a/es-app/src/views/gamelist/VideoGameListView.h b/es-app/src/views/gamelist/VideoGameListView.h index 8a8335866b..1ff642ca2d 100644 --- a/es-app/src/views/gamelist/VideoGameListView.h +++ b/es-app/src/views/gamelist/VideoGameListView.h @@ -19,7 +19,14 @@ class VideoGameListView : public BasicGameListView virtual void onThemeChanged(const std::shared_ptr& theme) override; - virtual const char* getName() const override { return "video"; } + virtual const char* getName() const override + { + if (!mCustomThemeName.empty()) + return mCustomThemeName.c_str(); + + return "video"; + } + virtual void launch(FileData* game) override; protected: diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index d05f77c3d4..fe3c63ff50 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -47,7 +47,7 @@ void GuiTextTool::ensureLocalisation() mItems.clear(); std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/"+ mCurrentLanguage +"/emulationstation2.po"); - if (Utils::FileSystem::exists(xmlpath)) + if (Utils::FileSystem::exists(xmlpath)) { LocalizationItem* currentItem = NULL; @@ -96,10 +96,10 @@ const std::string GuiTextTool::localize(const std::string text) { ensureLocalisation(); - for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) - { - if (text == (*it)->msgid) - return (*it)->msgstr; + for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) + { + if (text == (*it)->msgid) + return (*it)->msgstr; } return text; diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index f21e6f8f11..1bb4fe2f2f 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -35,6 +35,7 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d { width = FreeImage_GetWidth(fiBitmap); height = FreeImage_GetHeight(fiBitmap); + //loop through scanlines and add all pixel data to the return vector //this is necessary, because width*height*bpp might not be == pitch @@ -81,7 +82,40 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d return rawData; } -unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height) +#include "math/Vector2i.h" + +//public static Rectangle GetPictureRect(Size imageSize, Rectangle rcPhoto, bool outerZooming = false, bool sourceRect = false) + +Vector2i ImageIO::adjustPictureSize(Vector2i imageSize, Vector2i maxSize) +{ + int cxDIB = imageSize.x(); + int cyDIB = imageSize.y(); + int iMaxX = maxSize.x(); + int iMaxY = maxSize.y(); + + double xCoef = (double)iMaxX / (double)cxDIB; + double yCoef = (double)iMaxY / (double)cyDIB; + + cyDIB = (int)((double)cyDIB * std::fmax(xCoef, yCoef)); + cxDIB = (int)((double)cxDIB * std::fmax(xCoef, yCoef)); + + if (cxDIB > iMaxX) + { + cyDIB = (int)((double)cyDIB * (double)iMaxX / (double)cxDIB); + cxDIB = iMaxX; + } + + if (cyDIB > iMaxY) + { + cxDIB = (int)((double)cxDIB * (double)iMaxY / (double)cyDIB); + cyDIB = iMaxY; + } + + return Vector2i(cxDIB, cyDIB); +} + + +unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight) { width = 0; height = 0; @@ -112,7 +146,21 @@ unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const { width = FreeImage_GetWidth(fiBitmap); height = FreeImage_GetHeight(fiBitmap); + + if (maxWidth > 0 && maxHeight > 0 && (width > maxWidth || height > maxHeight)) + { + Vector2i sz = adjustPictureSize(Vector2i(width, height), Vector2i(maxWidth, maxHeight)); + if (sz.x() != width || sz.y() != height) + { + FIBITMAP* imageRescaled = FreeImage_Rescale(fiBitmap, sz.x(), sz.y(), FILTER_BOX); + FreeImage_Unload(fiBitmap); + fiBitmap = imageRescaled; + width = FreeImage_GetWidth(fiBitmap); + height = FreeImage_GetHeight(fiBitmap); + } + } + //loop through scanlines and add all pixel data to the return vector //this is necessary, because width*height*bpp might not be == pitch diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index acbe6cb526..89b27909ad 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -5,13 +5,16 @@ #include #include +#include "math/Vector2i.h" + class ImageIO { public: static std::vector loadFromMemoryRGBA32(const unsigned char * data, const size_t size, size_t & width, size_t & height); - static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height); - + static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth=0, int maxHeight=0); + static void flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height); + static Vector2i adjustPictureSize(Vector2i imageSize, Vector2i maxSize); }; #endif // ES_CORE_IMAGE_IO diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index a21d117a44..a42dd323c9 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -67,6 +67,10 @@ void Settings::setDefaults() mBoolMap["Windowed"] = false; mBoolMap["SplashScreen"] = true; mBoolMap["SplashScreenProgress"] = true; + mBoolMap["PreloadUI"] = true; + mBoolMap["StartupOnGameList"] = false; + mBoolMap["HideSystemView"] = false; + mStringMap["StartupSystem"] = ""; #if defined(_WIN32) @@ -92,12 +96,17 @@ void Settings::setDefaults() mIntMap["ScreenSaverTime"] = 5*60*1000; // 5 minutes mIntMap["ScraperResizeWidth"] = 400; mIntMap["ScraperResizeHeight"] = 0; + +#if defined(_WIN32) + mIntMap["MaxVRAM"] = 256; +#else #ifdef _RPI_ mIntMap["MaxVRAM"] = 80; #else mIntMap["MaxVRAM"] = 100; #endif - +#endif + #if defined(_WIN32) mBoolMap["HideWindow"] = false; #else @@ -111,6 +120,13 @@ void Settings::setDefaults() mStringMap["Scraper"] = "TheGamesDB"; mStringMap["GamelistViewStyle"] = "automatic"; + mStringMap["ThemeColorSet"] = ""; + mStringMap["ThemeIconSet"] = ""; + mStringMap["ThemeMenu"] = ""; + mStringMap["ThemeSystemView"] = ""; + mStringMap["ThemeGamelistView"] = ""; + mStringMap["ThemeRegionName"] = "eu"; + mBoolMap["ScreenSaverControls"] = true; mStringMap["ScreenSaverGameInfo"] = "never"; mBoolMap["StretchVideoOnScreenSaver"] = false; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index bcc1a72180..1bb2cf8bb7 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -6,10 +6,14 @@ #include "Log.h" #include "platform.h" #include "Settings.h" -#include #include -std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "gridex" },{ "video" }, { "menu" } }; +#ifdef _RPI_ +#include "components/VideoPlayerComponent.h" +#endif +#include "components/VideoVlcComponent.h" + +std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "video" }, { "menu" } }; std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; std::map> ThemeData::sElementMap { @@ -29,8 +33,15 @@ std::map> The { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, { "margin", NORMALIZED_PAIR }, + + { "padding", NORMALIZED_PAIR }, + + { "autoLayout", NORMALIZED_PAIR }, + { "autoLayoutSelectedZoom", FLOAT }, + { "gameImage", PATH }, { "folderImage", PATH }, + { "showVideoAtDelay", FLOAT }, { "scrollDirection", STRING } } }, { "gridtile", { { "size", NORMALIZED_PAIR }, @@ -40,7 +51,7 @@ std::map> The { "backgroundCornerSize", NORMALIZED_PAIR }, { "backgroundColor", COLOR }, { "backgroundCenterColor", COLOR }, - { "backgroundEdgeColor", COLOR }, + { "backgroundEdgeColor", COLOR }, { "imageSizeMode", STRING } } }, { "text", { { "pos", NORMALIZED_PAIR }, @@ -132,6 +143,7 @@ std::map> The { "rotation", FLOAT }, { "rotationOrigin", NORMALIZED_PAIR }, { "default", PATH }, + { "path", PATH }, { "delay", FLOAT }, { "zIndex", FLOAT }, { "showSnapshotNoVideo", BOOLEAN }, @@ -167,7 +179,30 @@ std::map> The { "menuBackground", { { "path", PATH }, { "fadePath", PATH }, - { "color", COLOR } } } + { "color", COLOR } } }, + + { "menuIcons",{ + { "iconSystem", PATH }, + { "iconUpdates", PATH }, + { "iconControllers", PATH }, + { "iconGames", PATH }, + { "iconUI", PATH }, + { "iconSound", PATH }, + { "iconNetwork", PATH }, + { "iconScraper", PATH }, + { "iconAdvanced", PATH }, + { "iconQuit", PATH } } }, + + { "menuSwitch",{ + { "pathOn", PATH }, + { "pathOff", PATH } } }, + + { "menuSlider",{ + { "path", PATH } } }, + + { "menuButton",{ + { "path", PATH }, + { "filledPath", PATH } } }, }; std::shared_ptr ThemeData::MenuTheme; @@ -229,6 +264,14 @@ std::string resolvePlaceholders(const char* in) ThemeData::ThemeData() { + mHasSubsets = false; + + mColorset = Settings::getInstance()->getString("ThemeColorSet"); + mIconset = Settings::getInstance()->getString("ThemeIconSet"); + mMenu = Settings::getInstance()->getString("ThemeMenu"); + mSystemview = Settings::getInstance()->getString("ThemeSystemView"); + mGamelistview = Settings::getInstance()->getString("ThemeGamelistView"); + mVersion = 0; } @@ -242,6 +285,7 @@ void ThemeData::loadFile(std::string system, std::map if(!Utils::FileSystem::exists(path)) throw error << "File does not exist!"; + mHasSubsets = false; mVersion = 0; mViews.clear(); @@ -273,6 +317,7 @@ void ThemeData::loadFile(std::string system, std::map parseVariables(root); parseIncludes(root); parseViews(root); + parseCustomViews(root); parseFeatures(root); MenuTheme = std::shared_ptr(new ThemeMenu(*this)); @@ -296,11 +341,39 @@ bool ThemeData::parseSubset(const pugi::xml_node& node) if (node.attribute("subset")) { + mHasSubsets = true; + parse = false; const std::string subsetAttr = node.attribute("subset").as_string(); const std::string nameAttr = node.attribute("name").as_string(); - if (subsetAttr == "iconset" || subsetAttr == "gamelistview") + if (subsetAttr == "colorset" && nameAttr == mColorset) + { + parse = true; + return parse; + } + if (subsetAttr == "iconset" && nameAttr == mIconset) + { + parse = true; + return parse; + } + if (subsetAttr == "menu" && nameAttr == mMenu) + { + parse = true; + return parse; + } + if (subsetAttr == "systemview" && nameAttr == mSystemview) + { + parse = true; + return parse; + } + if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) + { + parse = true; + return parse; + } + /* + if (subsetAttr == "menu" || subsetAttr == "iconset" || subsetAttr == "gamelistview") { if (nameAttr.rfind("1-") != std::string::npos) { @@ -320,7 +393,7 @@ bool ThemeData::parseSubset(const pugi::xml_node& node) { parse = true; return parse; - } + }*/ /* if (subsetAttr == "colorset" && nameAttr == mColorset) { @@ -387,6 +460,7 @@ void ThemeData::parseIncludes(const pugi::xml_node& root) parseVariables(theme); parseIncludes(theme); parseViews(theme); + parseCustomViews(theme); parseFeatures(theme); mPaths.pop_back(); @@ -408,6 +482,7 @@ void ThemeData::parseFeatures(const pugi::xml_node& root) if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) { parseViews(node); + parseCustomViews(node); } } } @@ -441,37 +516,138 @@ void ThemeData::parseViews(const pugi::xml_node& root) // parse views for (pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) { - if(!node.attribute("name")) - throw error << "View missing \"name\" attribute!"; + if (!node.attribute("name")) + continue; const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); + const std::string nameAttr = node.attribute("name").as_string(); size_t prevOff = nameAttr.find_first_not_of(delim, 0); size_t off = nameAttr.find_first_of(delim, prevOff); std::string viewKey; - while(off != std::string::npos || prevOff != std::string::npos) + + while (off != std::string::npos || prevOff != std::string::npos) { viewKey = nameAttr.substr(prevOff, off - prevOff); prevOff = nameAttr.find_first_not_of(delim, off); off = nameAttr.find_first_of(delim, prevOff); if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) - { + { ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; parseView(node, view); - - if (viewKey == "grid" && (int) nameAttr.find("gridex") < 0) - { - ThemeView& view = mViews.insert(std::pair("gridex", ThemeView())).first->second; - parseView(node, view); + + + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + { + if (it->second.isCustomView && it->second.baseType == viewKey) + { + ThemeView& customView = (ThemeView&)it->second; + parseView(node, customView); + } } + + + + } } } } +void ThemeData::parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& view, std::string baseClass) +{ + bool found = false; + + // Import original view properties + for (pugi::xml_node nodec = root.child("view"); nodec; nodec = nodec.next_sibling("view")) + { + if (!nodec.attribute("name")) + continue; + + const char* delim = " \t\r\n,"; + const std::string nameAttr = nodec.attribute("name").as_string(); + + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + std::string viewKey; + while (off != std::string::npos || prevOff != std::string::npos) + { + viewKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + if (viewKey == baseClass) + { + found = true; + parseView(nodec, view); + } + } + } + + if (found) + return; + + // base class is a customview ? + for (pugi::xml_node nodec = root.child("customView"); nodec; nodec = nodec.next_sibling("customView")) + { + const std::string nameAttr = nodec.attribute("name").as_string(); + + if (!nameAttr.empty() && nameAttr == baseClass) + { + std::string inherits = nodec.attribute("inherits").as_string(); + if (!inherits.empty() && inherits != baseClass) + { + view.baseType = inherits; + parseCustomViewBaseClass(root, view, inherits); + } + + parseView(nodec, view); + } + } +} -void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) +void ThemeData::parseCustomViews(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + // parse views + for (pugi::xml_node node = root.child("customView"); node; node = node.next_sibling("customView")) + { + if (!node.attribute("name")) + continue; + + std::string viewKey = node.attribute("name").as_string(); + + ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; + view.isCustomView = true; + + std::string inherits = node.attribute("inherits").as_string(); + if (!inherits.empty()) + { + view.baseType = inherits; + parseCustomViewBaseClass(root, view, inherits); + } + + parseView(node, view); + } +} + +std::vector ThemeData::getViewsOfTheme() +{ + std::vector ret; + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + { + if (it->first == "menu" || it->first == "system") + continue; + + ret.push_back(it->first); + } + + return ret; +} + +void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view, bool overwriteElements) { ThemeException error; error.setFiles(mPaths); @@ -506,7 +682,7 @@ void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view) off = nameAttr.find_first_of(delim, prevOff); parseElement(node, elemTypeIt->second, - view.elements.insert(std::pair(elemKey, ThemeElement())).first->second); + view.elements.insert(std::pair(elemKey, ThemeElement())).first->second, overwriteElements); if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) view.orderedKeys.push_back(elemKey); @@ -521,7 +697,7 @@ bool ThemeData::parseRegion(const pugi::xml_node& node) if (node.attribute("region")) { - std::string regionsetting = "us"; // Settings::getInstance()->getString("ThemeRegionName"); + std::string regionsetting = Settings::getInstance()->getString("ThemeRegionName"); parse = false; const char* delim = " \t\r\n,"; @@ -542,11 +718,9 @@ bool ThemeData::parseRegion(const pugi::xml_node& node) } return parse; - } - -void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element) +void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element, bool overwrite) { ThemeException error; error.setFiles(mPaths); @@ -564,6 +738,9 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapsecond) @@ -652,6 +829,24 @@ bool ThemeData::hasView(const std::string& view) return (viewIt != mViews.cend()); } +std::string ThemeData::getCustomViewBaseType(const std::string& view) +{ + auto viewIt = mViews.find(view); + if (viewIt != mViews.cend()) + return viewIt->second.baseType; + + return ""; +} + +bool ThemeData::isCustomView(const std::string& view) +{ + auto viewIt = mViews.find(view); + if (viewIt != mViews.cend()) + return viewIt->second.isCustomView; + + return false; +} + const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view, const std::string& element, const std::string& expectedType) const { auto viewIt = mViews.find(view); @@ -715,6 +910,15 @@ std::vector ThemeData::makeExtras(const std::shared_ptrgetBool("VideoOmxPlayer")) + comp = new VideoPlayerComponent(window, ""); + else +#endif + comp = new VideoVlcComponent(window, ""); + } comp->setDefaultZIndex(10); comp->applyTheme(theme, view, *it, ThemeFlags::ALL); @@ -846,4 +1050,156 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData& theme) if (elem->has("selectorColor")) Text.selectorColor = elem->get("selectorColor"); } -} \ No newline at end of file + + elem = theme.getElement("menu", "menubutton", "menuButton"); + + if (elem) + { + if (elem->has("path")) + Icons.button = elem->get("path"); + if (elem->has("filledPath")) + Icons.button_filled = elem->get("filledPath"); + } + + elem = theme.getElement("menu", "menuswitch", "menuSwitch"); + + if (elem) + { + if (elem->has("pathOn") && ResourceManager::getInstance()->fileExists(elem->get("pathOn"))) + Icons.on = elem->get("pathOn"); + if (elem->has("pathOff") && ResourceManager::getInstance()->fileExists(elem->get("pathOff"))) + Icons.off = elem->get("pathOff"); + } + + elem = theme.getElement("menu", "menuslider", "menuSlider"); + + if (elem && elem->has("path") && ResourceManager::getInstance()->fileExists(elem->get("path"))) + Icons.knob = elem->get("path"); + + elem = theme.getElement("menu", "menuicons", "menuIcons"); + + if (elem) + { + if (elem->has("iconSystem") && ResourceManager::getInstance()->fileExists(elem->get("iconSystem"))) + MenuIcons.system = elem->get("iconSystem"); + + if (elem->has("iconUpdates") && ResourceManager::getInstance()->fileExists(elem->get("iconUpdates"))) + MenuIcons.updates = elem->get("iconUpdates"); + + if (elem->has("iconGames") && ResourceManager::getInstance()->fileExists(elem->get("iconGames"))) + MenuIcons.games = elem->get("iconGames"); + + if (elem->has("iconControllers") && ResourceManager::getInstance()->fileExists(elem->get("iconControllers"))) + MenuIcons.controllers = elem->get("iconControllers"); + + if (elem->has("iconUI") && ResourceManager::getInstance()->fileExists(elem->get("iconUI"))) + MenuIcons.ui = elem->get("iconUI"); + + if (elem->has("iconSound") && ResourceManager::getInstance()->fileExists(elem->get("iconSound"))) + MenuIcons.sound = elem->get("iconSound"); + + if (elem->has("iconScraper") && ResourceManager::getInstance()->fileExists(elem->get("iconScraper"))) + MenuIcons.scraper = elem->get("iconScraper"); + + if (elem->has("iconAdvanced") && ResourceManager::getInstance()->fileExists(elem->get("iconAdvanced"))) + MenuIcons.advanced = elem->get("iconAdvanced"); + + if (elem->has("iconQuit") && ResourceManager::getInstance()->fileExists(elem->get("iconQuit"))) + MenuIcons.quit = elem->get("iconQuit"); + } +} + +std::map ThemeData::sortThemeSubSets(const std::map& subsetmap, const std::string& subset) +{ + std::map sortedsets; + + for (const auto& it : subsetmap) + { + if (it.second == subset) + sortedsets[it.first] = it.first; + } + return sortedsets; +} + + +void ThemeData::crawlIncludes(const pugi::xml_node& root, std::map& sets, std::deque& dequepath) +{ + for (pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) + { + sets[node.attribute("name").as_string()] = node.attribute("subset").as_string(); + + const char* relPath = node.text().get(); + std::string path = Utils::FileSystem::resolveRelativePath(relPath, dequepath.back(), true); + + dequepath.push_back(path); + pugi::xml_document includeDoc; + /*pugi::xml_parse_result result =*/ includeDoc.load_file(path.c_str()); + pugi::xml_node root = includeDoc.child("theme"); + crawlIncludes(root, sets, dequepath); + findRegion(includeDoc, sets); + dequepath.pop_back(); + } +} + +void ThemeData::findRegion(const pugi::xml_document& doc, std::map& sets) +{ + pugi::xpath_node_set regionattr = doc.select_nodes("//@region"); + for (auto xpath_node : regionattr) + { + if (xpath_node.attribute() != nullptr) + sets[xpath_node.attribute().value()] = "region"; + } +} + +std::map ThemeData::getThemeSubSets(const std::string& theme) +{ + std::map sets; + + std::deque dequepath; + + static const size_t pathCount = 2; + std::string paths[pathCount] = + { + "/etc/emulationstation/themes", + Utils::FileSystem::getHomePath() + "/.emulationstation/themes" + }; + + for (size_t i = 0; i < pathCount; i++) + { + if (!Utils::FileSystem::isDirectory(paths[i])) + continue; + + auto dirs = Utils::FileSystem::getDirInfo(paths[i] + "/" + theme, false); + for (auto it = dirs.cbegin(); it != dirs.cend(); ++it) + { + if (!it->directory || it->hidden) + continue; + + std::string path = it->path + "/theme.xml"; + if (!Utils::FileSystem::exists(path)) + continue; + + dequepath.push_back(path); + pugi::xml_document doc; + doc.load_file(path.c_str()); + pugi::xml_node root = doc.child("theme"); + crawlIncludes(root, sets, dequepath); + findRegion(doc, sets); + dequepath.pop_back(); + } + + std::string path = paths[i] + "/" + theme + "/theme.xml"; + if (!Utils::FileSystem::exists(path)) + continue; + + dequepath.push_back(path); + pugi::xml_document doc; + doc.load_file(path.c_str()); + pugi::xml_node root = doc.child("theme"); + crawlIncludes(root, sets, dequepath); + findRegion(doc, sets); + dequepath.pop_back(); + } + + return sets; +} diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 27fa912433..16d59d4fd8 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -4,6 +4,7 @@ #include "math/Vector2f.h" #include "utils/FileSystemUtil.h" +#include #include #include @@ -93,6 +94,29 @@ struct MenuElement { std::shared_ptr font; }; +struct IconElement { + std::string button; + std::string button_filled; + std::string on; + std::string off; + std::string option_arrow; + std::string arrow; + std::string knob; +}; + +struct MenuIconElement { + + std::string system; + std::string updates; + std::string games; + std::string controllers; + std::string ui; + std::string sound; + std::string scraper; + std::string advanced; + std::string quit; +}; + class ThemeData { public: @@ -106,6 +130,8 @@ class ThemeData MenuElement Text{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, "", "", nullptr }; MenuElement TextSmall{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, "", "", nullptr }; MenuElement Footer{ 0xC6C6C6FF, 0xC6C6C6FF, 0xC6C6C6FF, 0xFFFFFFFF, "", "", nullptr }; + MenuIconElement MenuIcons { "","","","","","","","","" }; + IconElement Icons { ":/button.png", ":/button_filled.png", ":/on.svg", ":/off.svg", ":/option_arrow.svg", ":/arrow.svg", ":/slider_knob.svg" }; }; @@ -150,8 +176,12 @@ class ThemeData class ThemeView { public: + ThemeView() { isCustomView = false; } + std::map elements; std::vector orderedKeys; + std::string baseType; + bool isCustomView; }; @@ -174,6 +204,9 @@ class ThemeData bool hasView(const std::string& view); + bool isCustomView(const std::string& view); + std::string getCustomViewBaseType(const std::string& view); + // If expectedType is an empty string, will do no type checking. const ThemeElement* getElement(const std::string& view, const std::string& element, const std::string& expectedType) const; @@ -185,10 +218,19 @@ class ThemeData static std::string getThemeFromCurrentSet(const std::string& system); std::string getDefaultView() { return mDefaultView; }; + + std::vector getViewsOfTheme(); + + bool hasSubsets() { return mHasSubsets; } static const std::shared_ptr& getMenuTheme() { return MenuTheme; } + static std::map sortThemeSubSets(const std::map& subsetmap, const std::string& subset); + static std::map ThemeData::getThemeSubSets(const std::string& theme); private: + static void crawlIncludes(const pugi::xml_node& root, std::map& sets, std::deque& dequepath); + static void findRegion(const pugi::xml_document& doc, std::map& sets); + static std::map< std::string, std::map > sElementMap; static std::vector sSupportedFeatures; static std::vector sSupportedViews; @@ -202,16 +244,28 @@ class ThemeData void parseIncludes(const pugi::xml_node& themeRoot); void parseVariables(const pugi::xml_node& root); void parseViews(const pugi::xml_node& themeRoot); - void parseView(const pugi::xml_node& viewNode, ThemeView& view); - void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element); + void parseCustomViews(const pugi::xml_node& root); + void parseView(const pugi::xml_node& viewNode, ThemeView& view, bool overwriteElements = true); + void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element, bool overwrite=true); bool parseRegion(const pugi::xml_node& node); bool parseSubset(const pugi::xml_node& node); + void parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& view, std::string baseClass); + std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); std::map mViews; + + std::string mColorset; + std::string mIconset; + std::string mMenu; + std::string mSystemview; + std::string mGamelistview; + std::string mSystemThemeFolder; + bool mHasSubsets; + static std::shared_ptr MenuTheme; }; diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 87f5118e5b..c887c29028 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -71,16 +71,18 @@ GuiComponent* Window::peekGui() return mGuiStack.back(); } -bool Window::init() +bool Window::init(bool initRenderer) { - if(!Renderer::init()) + if (initRenderer && !Renderer::init()) { LOG(LogError) << "Renderer failed to initialize!"; return false; } InputManager::getInstance()->init(); - ResourceManager::getInstance()->reloadAll(); + + if (initRenderer) + ResourceManager::getInstance()->reloadAll(); //keep a reference to the default fonts, so they don't keep getting destroyed/recreated if(mDefaultFonts.empty()) @@ -100,7 +102,7 @@ bool Window::init() return true; } -void Window::deinit() +void Window::deinit(bool deinitRenderer) { // Hide all GUI elements on uninitialisation - this disable for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) @@ -108,8 +110,12 @@ void Window::deinit() (*i)->onHide(); } InputManager::getInstance()->deinit(); - ResourceManager::getInstance()->unloadAll(); - Renderer::deinit(); + + if (deinitRenderer) + { + ResourceManager::getInstance()->unloadAll(); + Renderer::deinit(); + } } void Window::textInput(const char* text) diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 0c64a959fa..42a2498106 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -57,8 +57,8 @@ class Window void update(int deltaTime); void render(); - bool init(); - void deinit(); + bool init(bool initRenderer); + void deinit(bool deinitRenderer); void normalizeNextUpdate(); diff --git a/es-core/src/components/ButtonComponent.cpp b/es-core/src/components/ButtonComponent.cpp index 08eeeb34bc..b457c6aff5 100644 --- a/es-core/src/components/ButtonComponent.cpp +++ b/es-core/src/components/ButtonComponent.cpp @@ -5,7 +5,7 @@ #include "Renderer.h" ButtonComponent::ButtonComponent(Window* window, const std::string& text, const std::string& helpText, const std::function& func) : GuiComponent(window), - mBox(window, ":/button.png"), + mBox(window, ThemeData::getMenuTheme()->Icons.button), mFont(Font::get(FONT_SIZE_MEDIUM)), mFocused(false), mEnabled(true), @@ -78,18 +78,18 @@ void ButtonComponent::setEnabled(bool enabled) void ButtonComponent::updateImage() { - /* if(!mEnabled || !mPressedFunc) { - mBox.setImagePath(":/button_filled.png"); + mBox.setImagePath(ThemeData::getMenuTheme()->Icons.button_filled); mBox.setCenterColor(0x770000FF); mBox.setEdgeColor(0x770000FF); return; } - */ + mBox.setCenterColor(mColor); mBox.setEdgeColor(mColor); - mBox.setImagePath(mFocused ? ":/button_filled.png" : ":/button.png"); + mBox.setImagePath(mFocused ? ThemeData::getMenuTheme()->Icons.button_filled : ThemeData::getMenuTheme()->Icons.button); + //mBox.setImagePath(mFocused ? ":/button_filled.png" : ":/button.png"); } void ButtonComponent::render(const Transform4x4f& parentTrans) diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index ae85435063..de13836dac 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -8,8 +8,20 @@ #include "animations/LambdaAnimation.h" -GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window), mLabel(window) -{ + +#ifdef _RPI_ +#include "components/VideoPlayerComponent.h" +#endif +#include "components/VideoVlcComponent.h" +#include "utils/FileSystemUtil.h" +#ifdef _RPI_ +#include "Settings.h" +#endif + +#define VIDEODELAY 100 + +GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window), mLabel(window), mVideo(nullptr), mVideoPlaying(false) +{ mSelectedZoomPercent = 1.0f; mAnimPosition = Vector3f(0, 0); @@ -31,50 +43,81 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mSelectedProperties.mBackgroundCenterColor = 0xFFFFFFFF; mSelectedProperties.mBackgroundEdgeColor = 0xFFFFFFFF; - mLabelSize = Vector2f(1.0, 0.30); + mDefaultProperties.mLabelSize = Vector2f(1.0, 0.30); + mDefaultProperties.mLabelColor = 0xFFFFFFFF; + mDefaultProperties.mLabelBackColor = 0; + + mSelectedProperties.mLabelSize = Vector2f(1.0, 0.30); + mSelectedProperties.mLabelColor = 0xFFFFFFFF; + mSelectedProperties.mLabelBackColor = 0; mImage = std::make_shared(mWindow); - mImage->setOrigin(0.5f, 0.5f); - - //mBackground.setOrigin(0.5f, 0.5f); - - //mLabel.setOrigin(0.5f, 0.5f); - //mLabel.setOrigin(0.5f, 0.0f); - //mLabel.setSize(1.0f, 1.0f); - addChild(&mBackground); addChild(&(*mImage)); addChild(&mLabel); - + + // Create the correct type of video window +#ifdef _RPI_ + if (Settings::getInstance()->getBool("VideoOmxPlayer")) + mVideo = new VideoPlayerComponent(window, ""); + else +#endif + mVideo = new VideoVlcComponent(window, getTitlePath()); + + // video + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setStartDelay(VIDEODELAY); + mVideo->setDefaultZIndex(30); + addChild(mVideo); + setSelected(false); setVisible(true); } +void GridTileComponent::forceSize(Vector2f size, float selectedZoom) +{ + mDefaultProperties.mSize = size; + mSelectedProperties.mSize = size * selectedZoom; +} + +GridTileComponent::~GridTileComponent() +{ + if (mVideo != nullptr) + delete mVideo; + + mVideo = nullptr; +} + void GridTileComponent::resize() { const GridTileProperties& currentProperties = getCurrentProperties(); - setSize(currentProperties.mSize); + Vector2f size = currentProperties.mSize; + setSize(size); - float height = currentProperties.mSize.y() * mLabelSize.y(); + float height = (int) (size.y() * currentProperties.mLabelSize.y()); + float labelHeight = height; + + mLabel.setColor(currentProperties.mLabelColor); + mLabel.setBackgroundColor(currentProperties.mLabelBackColor); if (mLabelMerged) { mLabel.setPosition(currentProperties.mPadding.x(), mSize.y() - height - currentProperties.mPadding.y()); - mLabel.setSize(currentProperties.mSize.x() - 2 * currentProperties.mPadding.x(), height); + mLabel.setSize(size.x() - 2 * currentProperties.mPadding.x(), height); } else - { - mLabel.setPosition(0, mSize.y() - height); - mLabel.setSize(currentProperties.mSize.x(), height); + { + mLabel.setPosition(0, mSize.y() - height); + mLabel.setSize(size.x(), height); } if (!mLabelVisible || mLabelMerged) height = 0; - if (mLabelSize.x() == 0) + if (currentProperties.mLabelSize.x() == 0) height = 0; float topPadding = currentProperties.mPadding.y(); @@ -83,10 +126,7 @@ void GridTileComponent::resize() if (mSelectedZoomPercent != 1.0f) { - paddingX = mDefaultProperties.mPadding.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.x() * mSelectedZoomPercent; - topPadding = mDefaultProperties.mPadding.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.y() * mSelectedZoomPercent; - - if (mAnimPosition.x() != 0 && mAnimPosition.y() != 0) + if (mAnimPosition.x() != 0 && mAnimPosition.y() != 0 && mSelected) { int x = mAnimPosition.x() - mPosition.x(); int y = mAnimPosition.y() - mPosition.y(); @@ -102,31 +142,80 @@ void GridTileComponent::resize() else mBackground.setPosition(0, 0); + float imageWidth = size.x() - paddingX * 2.0; + float imageHeight = size.y() - topPadding - bottomPadding; - if (mImage != NULL) + if (mImage != nullptr) { mImage->setOrigin(0.5f, 0.5f); - mImage->setPosition(currentProperties.mSize.x() / 2.0f, (currentProperties.mSize.y() - height) / 2.0f); + mImage->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); if (currentProperties.mImageSizeMode == "minSize") - mImage->setMinSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + mImage->setMinSize(imageWidth, imageHeight); else if (currentProperties.mImageSizeMode == "size") - mImage->setSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + mImage->setSize(imageWidth, imageHeight); else - mImage->setMaxSize(currentProperties.mSize.x() - paddingX * 2, currentProperties.mSize.y() - topPadding - bottomPadding); + mImage->setMaxSize(imageWidth, imageHeight); + + if (mLabelMerged) + { + mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, currentProperties.mPadding.y() + mImage->getSize().y() - labelHeight); + mLabel.setSize(mImage->getSize().x(), labelHeight); + } + else if (currentProperties.mPadding.x() == 0) + { + mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, mImage->getSize().y()); + mLabel.setSize(mImage->getSize().x(), labelHeight); + } } + if (mVideo != nullptr) + { + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); + + if (currentProperties.mImageSizeMode == "minSize") + { + auto vs = mVideo->getVideoSize(); + + double prop = vs == Vector2f(0,0) ? 640.0 / 480.0 : vs.x() / vs.y(); + double imgprop = imageWidth / imageHeight; + if (prop < imgprop) + { + double h = imageWidth / prop; // suppose video is 4:3 + + if (h < imageHeight) + h = imageHeight; + + mVideo->setSize(imageWidth, h); + } + else + { + double w = imageHeight / prop; // suppose video is 4:3 + + if (w < imageWidth) + w = imageWidth; + + mVideo->setSize(w, imageHeight); + } + } + else + if (currentProperties.mImageSizeMode == "size") + mVideo->setSize(imageWidth, size.y() - topPadding - bottomPadding ); + else + mVideo->setMaxSize(imageWidth, size.y() - topPadding - bottomPadding); + } if (!mLabelMerged && currentProperties.mImageSizeMode == "minSize") - mBackground.setSize(currentProperties.mSize.x(), currentProperties.mSize.y() - bottomPadding); + mBackground.setSize(size.x(), size.y() - bottomPadding + topPadding); else - mBackground.setSize(currentProperties.mSize); + mBackground.setSize(size); mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); } -void GridTileComponent::render(const Transform4x4f& parentTrans) +void GridTileComponent::renderBackground(const Transform4x4f& parentTrans) { if (!mVisible) return; @@ -137,24 +226,63 @@ void GridTileComponent::render(const Transform4x4f& parentTrans) if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) return; - //Renderer::setMatrix(trans); - //Renderer::drawRect(0.f, 0.f, mSize.x(), mSize.y(), 0xFF0000FF); - if (mBackground.getCornerSize().x() == 0) { - Renderer::setMatrix(trans); - Renderer::drawRect(mBackground.getPosition().x(), mBackground.getPosition().y(), mBackground.getSize().x(), mBackground.getSize().y(), mBackground.getCenterColor()); + Renderer::drawRect(mBackground.getPosition().x(), mBackground.getPosition().y(), mBackground.getSize().x() - 1, mBackground.getSize().y() - 1, mBackground.getCenterColor()); Renderer::setMatrix(parentTrans); } else mBackground.render(trans); +} + +void GridTileComponent::renderContent(const Transform4x4f& parentTrans) +{ + if (!mVisible) + return; + + Transform4x4f trans = getTransform() * parentTrans; + + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + + float padding = getCurrentProperties().mPadding.x(); + float topPadding = getCurrentProperties().mPadding.y(); + float bottomPadding = topPadding; + + if (mLabelVisible && !mLabelMerged) + bottomPadding = std::max((int)topPadding, (int)(mSize.y() * getCurrentProperties().mLabelSize.y())); + + Vector2i pos((int)Math::round(trans.translation()[0] + padding), (int)Math::round(trans.translation()[1] + topPadding)); + Vector2i size((int)Math::round(mSize.x() - 2 * padding), (int)Math::round(mSize.y() - topPadding - bottomPadding)); + + if (getCurrentProperties().mImageSizeMode == "minSize") + Renderer::pushClipRect(pos, size); if (mImage != NULL) mImage->render(trans); - - if (mLabelVisible) + + if (mSelected && !mVideoPath.empty() && mVideo != nullptr) + mVideo->render(trans); + + if (!mLabelMerged && getCurrentProperties().mImageSizeMode == "minSize") + Renderer::popClipRect(); + + if (mLabelVisible && getCurrentProperties().mLabelSize.y()>0) mLabel.render(trans); + + if (mLabelMerged && getCurrentProperties().mImageSizeMode == "minSize") + Renderer::popClipRect(); +} + +void GridTileComponent::render(const Transform4x4f& parentTrans) +{ + if (!mVisible) + return; + + renderBackground(parentTrans); + renderContent(parentTrans); } // Update all the tile properties to the new status (selected or default) @@ -255,17 +383,43 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons elem->get("imageSizeMode") : mDefaultProperties.mImageSizeMode; - elem = theme->getElement(view, element, "text"); + elem = theme->getElement(view, "gridtile", "text"); if (elem != NULL) { if (elem && elem->has("size")) { - mLabelSize = elem->get("size"); - mLabelMerged = mLabelSize.x() == 0; + mDefaultProperties.mLabelSize = elem->get("size"); + mSelectedProperties.mLabelSize = mDefaultProperties.mLabelSize; + mLabelMerged = mDefaultProperties.mLabelSize.x() == 0; + } + + if (elem && elem->has("color")) + { + mDefaultProperties.mLabelColor = elem->get("color"); + mSelectedProperties.mLabelColor = mDefaultProperties.mLabelColor; + } + + if (elem && elem->has("backgroundColor")) + { + mDefaultProperties.mLabelBackColor = elem->get("backgroundColor"); + mSelectedProperties.mLabelBackColor = mDefaultProperties.mLabelBackColor; } mLabelVisible = true; mLabel.applyTheme(theme, view, element, properties); + + elem = theme->getElement(view, "gridtile_selected", "text"); + if (elem != NULL) + { + if (elem && elem->has("size")) + mSelectedProperties.mLabelSize = elem->get("size"); + + if (elem && elem->has("color")) + mSelectedProperties.mLabelColor = elem->get("color"); + + if (elem && elem->has("backgroundColor")) + mSelectedProperties.mLabelBackColor = elem->get("backgroundColor"); + } } else mLabelVisible = false; @@ -290,18 +444,49 @@ bool GridTileComponent::isSelected() const return mSelected; } -void GridTileComponent::setImage(const std::string& path, std::string name) +void GridTileComponent::setImage(const std::string& path) { - if (mCurrentPath != path) - { - mCurrentPath = path; - mImage->setImage(path); - } - + if (mCurrentPath == path) + return; + + mCurrentPath = path; + mImage->setImage(path, false, mSize); + resize(); +} + +void GridTileComponent::reset() +{ + setLabel(""); + setVideo(""); + setImage(""); +} + +void GridTileComponent::setLabel(std::string name) +{ + if (mLabel.getText() == name) + return; + mLabel.setText(name); resize(); } +void GridTileComponent::setVideo(const std::string& path, float defaultDelay) +{ + if (mVideoPath == path) + return; + + mVideoPath = path; + + if (mVideo != nullptr) + { + if (defaultDelay >= 0.0) + mVideo->setStartDelay(defaultDelay); + + if (mVideoPath.empty()) + mVideo->setVideo(""); + } +} + void GridTileComponent::setImage(const std::shared_ptr& texture, std::string name) { mImage->setImage(texture); @@ -311,37 +496,86 @@ void GridTileComponent::setImage(const std::shared_ptr& texture resize(); } -void GridTileComponent::setSelected(bool selected, Vector3f* pPosition) +void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f* pPosition) { + if (mSelected == selected) + { + if (mSelected && mVideo != nullptr) + mVideo->setVideo(mVideoPath); + + return; + } + mSelected = selected; + + if (!mSelected && mVideo != nullptr) + mVideo->setVideo(""); - if (selected && pPosition != NULL) + if (selected) { - auto funeec = [this](float t) + if (pPosition == NULL || !allowAnimation) { - this->setSelectedZoom(t); - }; - } + cancelAnimation(3); - if (selected && (pPosition != NULL || mSelectedProperties.mPadding.x() != mDefaultProperties.mPadding.y())) - { - if (pPosition == NULL) + this->setSelectedZoom(1); mAnimPosition = Vector3f(0, 0, 0); + + if (mVideo != NULL) + mVideo->setVideo(mVideoPath); + } else - mAnimPosition = Vector3f(pPosition->x(), pPosition->y(), pPosition->z()); + { + if (pPosition == NULL) + mAnimPosition = Vector3f(0, 0, 0); + else + mAnimPosition = Vector3f(pPosition->x(), pPosition->y(), pPosition->z()); + + auto func = [this](float t) + { + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + this->setSelectedZoom(pct); + }; + + cancelAnimation(3); + setAnimation(new LambdaAnimation(func, 250), 0, [this] { + this->setSelectedZoom(1); + mAnimPosition = Vector3f(0, 0, 0); - auto func = [this](float t) + if (mVideo != NULL) + mVideo->setVideo(mVideoPath); + + }, false, 3); + } + } + else // if (!selected) + { + if (!allowAnimation) { - t -= 1; // cubic ease out - float pct = Math::lerp(0, 1, t*t*t + 1); + this->setSelectedZoom(0); - this->setSelectedZoom(pct); - }; + if (mVideo != NULL) + mVideo->setVideo(""); + } + else + { + auto func = [this](float t) + { + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + this->setSelectedZoom(1.0 - pct); + }; - setAnimation(new LambdaAnimation(func, 200), 0, [this] { - this->setSelectedZoom(1); - mAnimPosition = Vector3f(0, 0, 0); - }, false, 3); + cancelAnimation(3); + setAnimation(new LambdaAnimation(func, 250), 0, [this] { + this->setSelectedZoom(0); + + if (mVideo != NULL) + mVideo->setVideo(""); + + }, false, 3); + } } } @@ -356,7 +590,61 @@ void GridTileComponent::setVisible(bool visible) mVisible = visible; } -const GridTileProperties& GridTileComponent::getCurrentProperties() const +unsigned int mixColors(unsigned int first, unsigned int second, float percent) +{ + unsigned char alpha0 = (first >> 24) & 0xFF; + unsigned char blue0 = (first >> 16) & 0xFF; + unsigned char green0 = (first >> 8) & 0xFF; + unsigned char red0 = first & 0xFF; + + unsigned char alpha1 = (second >> 24) & 0xFF; + unsigned char blue1 = (second >> 16) & 0xFF; + unsigned char green1 = (second >> 8) & 0xFF; + unsigned char red1 = second & 0xFF; + + unsigned char alpha = (unsigned char)(alpha0 * (1.0 - percent) + alpha1 * percent); + unsigned char blue = (unsigned char)(blue0 * (1.0 - percent) + blue1 * percent); + unsigned char green = (unsigned char)(green0 * (1.0 - percent) + green1 * percent); + unsigned char red = (unsigned char)(red0 * (1.0 - percent) + red1 * percent); + + return (alpha << 24) | (blue << 16) | (green << 8) | red; +} + +const GridTileProperties& GridTileComponent::getCurrentProperties() { + if (mSelectedZoomPercent != 1.0f) + { + auto def = mSelected ? mSelectedProperties : mDefaultProperties; + + mMixedProperties = mSelected ? mSelectedProperties : mDefaultProperties; + + if (mDefaultProperties.mSize != mSelectedProperties.mSize) + { + float x = mDefaultProperties.mSize.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mSize.x() * mSelectedZoomPercent; + float y = mDefaultProperties.mSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mSize.y() * mSelectedZoomPercent; + mMixedProperties.mSize = Vector2f(x, y); + } + + if (mDefaultProperties.mPadding != mSelectedProperties.mPadding) + { + float x = mDefaultProperties.mPadding.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.x() * mSelectedZoomPercent; + float y = mDefaultProperties.mPadding.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.y() * mSelectedZoomPercent; + mMixedProperties.mPadding = Vector2f(x, y); + } + + if (mDefaultProperties.mImageColor != mSelectedProperties.mImageColor) + { + mMixedProperties.mImageColor = mixColors(mDefaultProperties.mImageColor, mSelectedProperties.mImageColor, mSelectedZoomPercent); + } + + if (mDefaultProperties.mLabelSize != mSelectedProperties.mLabelSize) + { + float y = mDefaultProperties.mLabelSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mLabelSize.y() * mSelectedZoomPercent; + mMixedProperties.mLabelSize = Vector2f(mDefaultProperties.mLabelSize.x(), y); + } + + return mMixedProperties; + } + return mSelected ? mSelectedProperties : mDefaultProperties; } \ No newline at end of file diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index 46fc6c4364..1502d7ca84 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -6,6 +6,8 @@ #include "ImageComponent.h" #include "TextComponent.h" + +class VideoComponent; struct GridTileProperties { Vector2f mSize; @@ -17,12 +19,17 @@ struct GridTileProperties unsigned int mBackgroundEdgeColor; std::string mImageSizeMode; + + Vector2f mLabelSize; + unsigned int mLabelColor; + unsigned int mLabelBackColor; }; class GridTileComponent : public GuiComponent { public: GridTileComponent(Window* window); + ~GridTileComponent(); void render(const Transform4x4f& parentTrans) override; void update(int deltaTime) override; @@ -35,18 +42,30 @@ class GridTileComponent : public GuiComponent Vector2f getSelectedTileSize() const; bool isSelected() const; - void setImage(const std::string& path, std::string name); + void reset(); + + void setLabel(std::string name); + void setVideo(const std::string& path, float defaultDelay = -1.0); + + void setImage(const std::string& path); void setImage(const std::shared_ptr& texture, std::string name); - void setSelected(bool selected, Vector3f* pPosition = NULL); + void setSelected(bool selected, bool allowAnimation = true, Vector3f* pPosition = NULL); void setVisible(bool visible); + void forceSize(Vector2f size, float selectedZoom = 1.0); + + void renderBackground(const Transform4x4f& parentTrans); + void renderContent(const Transform4x4f& parentTrans); + + bool shouldSplitRendering() { return isAnimationPlaying(3); }; + private: void resize(); - const GridTileProperties& getCurrentProperties() const; + const GridTileProperties& getCurrentProperties(); std::shared_ptr mImage; - TextComponent mLabel; - Vector2f mLabelSize; + + TextComponent mLabel; bool mLabelVisible; bool mLabelMerged; @@ -55,8 +74,10 @@ class GridTileComponent : public GuiComponent GridTileProperties mDefaultProperties; GridTileProperties mSelectedProperties; + GridTileProperties mMixedProperties; std::string mCurrentPath; + std::string mVideoPath; void setSelectedZoom(float percent); @@ -65,6 +86,10 @@ class GridTileComponent : public GuiComponent bool mVisible; Vector3f mAnimPosition; + + VideoComponent* mVideo; + bool mVideoPlaying; + }; #endif // ES_CORE_COMPONENTS_GRID_TILE_COMPONENT_H diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 3ec03372de..0df7e7e7dd 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -21,11 +21,10 @@ Vector2f ImageComponent::getSize() const ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : GuiComponent(window), mTargetIsMax(false), mTargetIsMin(false), mFlipX(false), mFlipY(false), mTargetSize(0, 0), mColorShift(0xFFFFFFFF), - mForceLoad(forceLoad), mDynamic(dynamic), mFadeOpacity(0), mFading(false), mRotateByTargetSize(false), + mForceLoad(forceLoad), mDynamic(dynamic), mFadeOpacity(0), mFading(false), mRotateByTargetSize(false), mVisible(true), mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f) { - updateColors(); - mSizeChanged = false; + updateColors(); } ImageComponent::~ImageComponent() @@ -122,11 +121,7 @@ void ImageComponent::resize() mSize[0] = Math::round(mSize.x()); mSize[1] = Math::round(mSize.y()); - if (ox != mSize.x() || oy != mSize.y()) - mSizeChanged = true; - - // mSize.y() should already be rounded - //mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); + mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); onSizeChanged(); } @@ -141,17 +136,17 @@ void ImageComponent::setDefaultImage(std::string path) mDefaultPath = path; } -void ImageComponent::setImage(std::string path, bool tile) +void ImageComponent::setImage(std::string path, bool tile, Vector2f maxSize) { if (path.empty() || !ResourceManager::getInstance()->fileExists(path)) { if (mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) mTexture.reset(); else - mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic); + mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic, maxSize); } else - mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic); + mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, maxSize); resize(); } @@ -173,6 +168,9 @@ void ImageComponent::setImage(const std::shared_ptr& texture) void ImageComponent::setResize(float width, float height) { + if (!mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) + return; + mTargetSize = Vector2f(width, height); mTargetIsMax = false; mTargetIsMin = false; @@ -181,6 +179,9 @@ void ImageComponent::setResize(float width, float height) void ImageComponent::setMaxSize(float width, float height) { + if (mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) + return; + mTargetSize = Vector2f(width, height); mTargetIsMax = true; mTargetIsMin = false; @@ -189,6 +190,9 @@ void ImageComponent::setMaxSize(float width, float height) void ImageComponent::setMinSize(float width, float height) { + if (mTargetIsMin && !mTargetIsMax && mTargetSize.x() == width && mTargetSize.y() == height) + return; + mTargetSize = Vector2f(width, height); mTargetIsMax = false; mTargetIsMin = true; @@ -330,6 +334,9 @@ void ImageComponent::updateColors() void ImageComponent::render(const Transform4x4f& parentTrans) { + if (!mVisible) + return; + Transform4x4f trans = parentTrans * getTransform(); Renderer::setMatrix(trans); @@ -339,12 +346,6 @@ void ImageComponent::render(const Transform4x4f& parentTrans) if (mTexture && mOpacity > 0) { - if (mSizeChanged) - { - mSizeChanged = false; - mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); - } - if (Settings::getInstance()->getBool("DebugImage")) { Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; @@ -476,7 +477,7 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s if(properties & PATH && elem->has("path")) { bool tile = (elem->has("tile") && elem->get("tile")); - setImage(elem->get("path"), tile); + setImage(elem->get("path"), tile, mTargetSize); } if(properties & COLOR && elem->has("color")) diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 3f5ce3138a..17ca7ac375 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -18,7 +18,7 @@ class ImageComponent : public GuiComponent void setDefaultImage(std::string path); //Loads the image at the given filepath. Will tile if tile is true (retrieves texture as tiling, creates vertices accordingly). - void setImage(std::string path, bool tile = false); + void setImage(std::string path, bool tile = false, Vector2f maxSize = Vector2f(0 ,0)); //Loads an image from memory. void setImage(const char* image, size_t length, bool tile = false); //Use an already existing texture. @@ -69,6 +69,13 @@ class ImageComponent : public GuiComponent Vector2f getSize() const override; + bool isVisible() { + return mVisible; + } + void setVisible(bool visible) { + mVisible = visible; + } + bool hasImage(); void render(const Transform4x4f& parentTrans) override; @@ -107,11 +114,10 @@ class ImageComponent : public GuiComponent bool mForceLoad; bool mDynamic; bool mRotateByTargetSize; + bool mVisible; Vector2f mTopLeftCrop; - Vector2f mBottomRightCrop; - - bool mSizeChanged; + Vector2f mBottomRightCrop; }; #endif // ES_CORE_COMPONENTS_IMAGE_COMPONENT_H diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 9dc51138dd..b28fa65bc8 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -7,15 +7,20 @@ #include "resources/TextureResource.h" #include "GridTileComponent.h" +#define EXTRAITEMS 2 + enum ScrollDirection { SCROLL_VERTICALLY, - SCROLL_HORIZONTALLY + SCROLL_HORIZONTALLY, + SCROLL_VERTICALLY_CENTER, + SCROLL_HORIZONTALLY_CENTER, }; struct ImageGridData { std::string texturePath; + std::string videoPath; }; template @@ -40,7 +45,7 @@ class ImageGridComponent : public IList ImageGridComponent(Window* window); - void add(const std::string& name, const std::string& imagePath, const T& obj); + void add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const T& obj); bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; @@ -52,17 +57,22 @@ class ImageGridComponent : public IList void setThemeName(std::string name) { mName = name; }; + virtual void topWindow(bool isTop); + virtual void onShow(); + virtual void onHide(); + protected: - virtual void onCursorChanged(const CursorState& state) override; + virtual void onCursorChanged(const CursorState& state) override; private: // TILES void buildTiles(); - void updateTiles(); - void updateTileAtPos(int tilePos, int imgPos); - int getStartPosition() const; + void updateTiles(bool ascending = true, bool allowAnimation = true); + void updateTileAtPos(int tilePos, int imgPos, bool allowAnimation = true); void calcGridDimension(); + bool isVertical() { return mScrollDirection == SCROLL_VERTICALLY || mScrollDirection == SCROLL_VERTICALLY_CENTER; }; + bool mEntriesDirty; int mLastCursor; std::string mDefaultGameTexture; @@ -70,6 +80,9 @@ class ImageGridComponent : public IList // TILES bool mLastRowPartial; + Vector2f mAutoLayout; + float mAutoLayoutZoom; + Vector2f mPadding; Vector2f mMargin; Vector2f mTileSize; Vector2i mGridDimension; @@ -79,6 +92,11 @@ class ImageGridComponent : public IList std::string mName; int mStartPosition; + bool mAllowVideo; + float mVideoDelay; + + float mCamera; + float mCameraDirection; // MISCELLANEOUS ScrollDirection mScrollDirection; @@ -90,6 +108,14 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList::ImageGridComponent(Window* window) : IList -void ImageGridComponent::add(const std::string& name, const std::string& imagePath, const T& obj) +void ImageGridComponent::add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const T& obj) { typename IList::Entry entry; entry.name = name; entry.object = obj; entry.data.texturePath = imagePath; + entry.data.videoPath = videoPath; static_cast*>(this)->add(entry); mEntriesDirty = true; @@ -121,19 +149,25 @@ bool ImageGridComponent::input(InputConfig* config, Input input) { if(input.value != 0) { + int idx = isVertical() ? 0 : 1; + Vector2i dir = Vector2i::Zero(); if(config->isMappedLike("up", input)) - dir[1 ^ mScrollDirection] = -1; + dir[1 ^ idx] = -1; else if(config->isMappedLike("down", input)) - dir[1 ^ mScrollDirection] = 1; + dir[1 ^ idx] = 1; else if(config->isMappedLike("left", input)) - dir[0 ^ mScrollDirection] = -1; + dir[0 ^ idx] = -1; else if(config->isMappedLike("right", input)) - dir[0 ^ mScrollDirection] = 1; + dir[0 ^ idx] = 1; if(dir != Vector2i::Zero()) { - listInput(dir.x() + dir.y() * mGridDimension.x()); + if (isVertical()) + listInput(dir.x() + dir.y() * mGridDimension.x()); + else + listInput(dir.x() + dir.y() * mGridDimension.y()); + return true; } }else{ @@ -149,18 +183,65 @@ bool ImageGridComponent::input(InputConfig* config, Input input) template void ImageGridComponent::update(int deltaTime) { + GuiComponent::update(deltaTime); listUpdate(deltaTime); for(auto it = mTiles.begin(); it != mTiles.end(); it++) (*it)->update(deltaTime); } +template +void ImageGridComponent::topWindow(bool isTop) +{ + GuiComponent::topWindow(isTop); + + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + std::shared_ptr tile = mTiles.at(ti); + tile->topWindow(isTop); + } +} + +template +void ImageGridComponent::onShow() +{ + GuiComponent::onShow(); + + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + std::shared_ptr tile = mTiles.at(ti); + tile->onShow(); + } +} + +template +void ImageGridComponent::onHide() +{ + GuiComponent::onHide(); + + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + std::shared_ptr tile = mTiles.at(ti); + tile->onHide(); + } +} + template void ImageGridComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = getTransform() * parentTrans; - if(mEntriesDirty) + float offsetX = isVertical() ? 0 : mCamera * mCameraDirection * (mTileSize.x() + mMargin.x()); + float offsetY = isVertical() ? mCamera * mCameraDirection * (mTileSize.y() + mMargin.y()) : 0; + + if (Settings::getInstance()->getBool("DebugImage")) + { + Renderer::setMatrix(trans); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0xFF000055); + Renderer::setMatrix(parentTrans); + } + + if (mEntriesDirty) { updateTiles(); mEntriesDirty = false; @@ -175,40 +256,53 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) Renderer::pushClipRect(pos, size); - // Render all the tiles but the selected one + if (mCamera != 0) + { + for (auto it = mTiles.begin(); it != mTiles.end(); it++) + (*it)->setPosition((*it)->getPosition().x() + offsetX, (*it)->getPosition().y() + offsetY); + } + + // Render the selected image background on bottom of the others if needed std::shared_ptr selectedTile = NULL; for(auto it = mTiles.begin(); it != mTiles.end(); it++) { std::shared_ptr tile = (*it); - - // If it's the selected image, keep it for later, otherwise render it now - if(tile->isSelected()) + if (tile->isSelected()) + { selectedTile = tile; - // else - // tile->render(trans); - } - + if (tile->shouldSplitRendering()) + tile->renderBackground(trans); - - // Render the selected image on top of the others - if (selectedTile != NULL) - selectedTile->render(trans); + break; + } + } for (auto it = mTiles.begin(); it != mTiles.end(); it++) { std::shared_ptr tile = (*it); - - // If it's the selected image, keep it for later, otherwise render it now - if (tile->isSelected()) - ; // selectedTile = tile; - else + if (!tile->isSelected()) tile->render(trans); } + // Render the selected image content on top of the others + if (selectedTile != NULL) + { + if (selectedTile->shouldSplitRendering()) + selectedTile->renderContent(trans); + else + selectedTile->render(trans); + } + Renderer::popClipRect(); listRenderTitleOverlay(trans); + if (mCamera != 0) + { + for (auto it = mTiles.begin(); it != mTiles.end(); it++) + (*it)->setPosition((*it)->getPosition().x() - offsetX, (*it)->getPosition().y() - offsetY); + } + GuiComponent::renderChildren(trans); } @@ -229,8 +323,35 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("margin")) mMargin = elem->get("margin") * screen; + if (elem->has("padding")) + mPadding = elem->get("padding") * screen; + + if (elem->has("autoLayout")) + mAutoLayout = elem->get("autoLayout"); + + if (elem->has("autoLayoutSelectedZoom")) + mAutoLayoutZoom = elem->get("autoLayoutSelectedZoom"); + if (elem->has("scrollDirection")) - mScrollDirection = (ScrollDirection)(elem->get("scrollDirection") == "horizontal"); + { + auto direction = elem->get("scrollDirection"); + if (direction == "horizontal") + mScrollDirection = SCROLL_HORIZONTALLY; + else if (direction == "horizontalCenter") + mScrollDirection = SCROLL_HORIZONTALLY_CENTER; + else if (direction == "verticalCenter") + mScrollDirection = SCROLL_VERTICALLY_CENTER; + else + mScrollDirection = SCROLL_VERTICALLY; + } + + if (elem->has("showVideoAtDelay")) + { + mVideoDelay = elem->get("showVideoAtDelay"); + mAllowVideo = true; + } + else + mAllowVideo = false; if (elem->has("gameImage")) { @@ -301,7 +422,122 @@ void ImageGridComponent::onSizeChanged() template void ImageGridComponent::onCursorChanged(const CursorState& state) { - if (mLastCursor != mCursor) + if (mLastCursor == mCursor) + return; + + if (true) + { + bool centerSel = (mScrollDirection == SCROLL_HORIZONTALLY_CENTER || mScrollDirection == SCROLL_VERTICALLY_CENTER); + + bool direction = mCursor >= mLastCursor; + + int oldStart = mStartPosition; + + float dimScrollable = isVertical() ? mGridDimension.y() - 2 * EXTRAITEMS: mGridDimension.x() - 2 * EXTRAITEMS; + float dimOpposite = isVertical() ? mGridDimension.x() : mGridDimension.y(); + + int centralCol = (int)(dimScrollable - 0.5) / 2; + int maxCentralCol = (int)(dimScrollable) / 2; + + int oldCol = (mLastCursor / dimOpposite); + int col = (mCursor / dimOpposite); + + int lastCol = ((mEntries.size() - 1) / dimOpposite); + + int lastScroll = std::max(0, (int) (lastCol + 1 - dimScrollable)); + + float startPos = 0; + float endPos = 1; + + if (isAnimationPlaying(2)) + { + startPos = -(mCamera*2.0/3.0); + if (startPos < -1) + startPos = -1; + + cancelAnimation(2); + updateTiles(direction, false); + } + + std::shared_ptr oldTile = nullptr; + std::shared_ptr newTile = nullptr; + + int oldIdx = mLastCursor - mStartPosition + (dimOpposite * EXTRAITEMS); + if (oldIdx >= 0 && oldIdx < mTiles.size()) + oldTile = mTiles[oldIdx]; + + int newIdx = mCursor - mStartPosition + (dimOpposite * EXTRAITEMS); + if (newIdx >= 0 && newIdx < mTiles.size()) + newTile = mTiles[newIdx]; + + Vector3f oldPos = Vector3f(0, 0); + + if (oldTile != nullptr) + { + oldPos = oldTile->getPosition(); + oldTile->setSelected(false); + } + + if (newTile != nullptr) + newTile->setSelected(true, true, oldPos == Vector3f(0, 0) ? nullptr : &oldPos); + + int firstVisibleCol = mStartPosition / dimOpposite; + + if ((col < centralCol || (col == 0 && col == centralCol)) && !centerSel) + mStartPosition = 0; + else if ((col - centralCol) > lastScroll && !centerSel) + mStartPosition = lastScroll * dimOpposite; + else if (maxCentralCol != centralCol && col == firstVisibleCol + maxCentralCol || col == firstVisibleCol + centralCol) + { + if (col == firstVisibleCol + maxCentralCol) + mStartPosition = (col - maxCentralCol) * dimOpposite; + else + mStartPosition = (col - centralCol) * dimOpposite; + } + else + { + if (oldCol == firstVisibleCol + maxCentralCol) + mStartPosition = (col - maxCentralCol) * dimOpposite; + else + mStartPosition = (col - centralCol) * dimOpposite; + } + + mLastCursor = mCursor; + + mCameraDirection = direction ? -1.0 : 1.0; + mCamera = 0; + + if (oldStart == mStartPosition) + { + updateTiles(direction, true); + + if (mCursorChangedCallback) + mCursorChangedCallback(state); + + return; + } + + if (mCursorChangedCallback) + mCursorChangedCallback(state); + + auto func = [this, startPos, endPos](float t) + { + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + float x = startPos * (1.0 - pct) + endPos * pct; + mCamera = x; // Math::lerp(0.0f, 1.0f, t); + }; + + setAnimation(new LambdaAnimation(func, 250), 0, [this, direction] { + + mCamera = 0; + updateTiles(direction, false); + }, false, 2); + + return; + } + else { // 1 if scrolling down, -1 if scrolling up int scrollDirection = mCursor >= mLastCursor ? 1 : -1; @@ -309,15 +545,26 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) int partialRow = (int)mLastRowPartial; int maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); + if (!isVertical()) + maxPos = mStartPosition + ((mGridDimension.x()-2) * (mGridDimension.y() - partialRow)); + if (mCursor == 0) mStartPosition = 0; else if (mCursor < mStartPosition) mStartPosition = std::max(0, mStartPosition - mGridDimension.x()); else if (mCursor >= maxPos) { - mStartPosition = mStartPosition + mGridDimension.x(); - - maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); + if (!isVertical()) + { + mStartPosition = mStartPosition + mGridDimension.y(); + maxPos = mStartPosition + ((mGridDimension.x()-2) * (mGridDimension.y() - partialRow)); + } + else + { + mStartPosition = mStartPosition + mGridDimension.x(); + maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); + } + if (mCursor >= maxPos) { int cursorRow = mCursor / mGridDimension.x(); @@ -329,62 +576,23 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) } } } + } - updateTiles(); + updateTiles(mCursor >= mLastCursor); - if (mCursorChangedCallback) - mCursorChangedCallback(state); - } + if (mCursorChangedCallback) + mCursorChangedCallback(state); mLastCursor = mCursor; } -// Create and position tiles (mTiles) -template -void ImageGridComponent::buildTiles() -{ - mStartPosition = 0; - mTiles.clear(); - - calcGridDimension(); - - Vector2f tileDistance = mTileSize + mMargin; - Vector2f bufferSize = Vector2f(0, 0); // mScrollDirection == SCROLL_HORIZONTALLY ? tileDistance.x() * texBuffersForward[3] : 0, mScrollDirection == SCROLL_VERTICALLY ? tileDistance.y() * texBuffersForward[3] : 0); - Vector2f startPosition = mTileSize / 2 - bufferSize; - - int X, Y; - - // Layout tile size and position - for(int y = 0; y < mGridDimension.y(); y++) - { - for(int x = 0; x < mGridDimension.x(); x++) - { - // Create tiles - auto tile = std::make_shared(mWindow); - - // In Vertical mod, tiles are ordered from left to right, then from top to bottom - // In Horizontal mod, tiles are ordered from top to bottom, then from left to right - X = mScrollDirection == SCROLL_VERTICALLY ? x : y; - Y = mScrollDirection == SCROLL_VERTICALLY ? y : x; - - tile->setPosition(X * tileDistance.x() + startPosition.x(), Y * tileDistance.y() + startPosition.y()); - tile->setOrigin(0.5f, 0.5f); - tile->setImage("", ""); - - if (mTheme) - tile->applyTheme(mTheme, mName, "gridtile", ThemeFlags::ALL); - - mTiles.push_back(tile); - } - } -} template -void ImageGridComponent::updateTiles() +void ImageGridComponent::updateTiles(bool ascending, bool allowAnimation) { if (!mTiles.size()) return; - + // Stop updating the tiles at highest scroll speed if (mScrollTier == 3) { @@ -392,41 +600,95 @@ void ImageGridComponent::updateTiles() { std::shared_ptr tile = mTiles.at(ti); - tile->setSelected(false); - tile->setImage(mDefaultGameTexture, ""); + tile->setSelected(false, allowAnimation); + tile->setLabel(""); + tile->setImage(mDefaultGameTexture); tile->setVisible(false); } return; } - int i = 0; - int end = (int)mTiles.size(); - int img = getStartPosition(); + if (!ascending) + { + int i = (int)mTiles.size() - 1; + int end = -1; + int img = mStartPosition + (int)mTiles.size() - 1; + + if (isVertical()) + img -= EXTRAITEMS * mGridDimension.x(); + else + img -= EXTRAITEMS * mGridDimension.y(); - while (i != end) + while (i != end) + { + updateTileAtPos(i, img, allowAnimation); + i--; img--; + } + } + else { - updateTileAtPos(i, img); - i++; img++; + int i = 0; + int end = (int)mTiles.size(); + int img = mStartPosition; + + if (isVertical()) + img -= EXTRAITEMS * mGridDimension.x(); + else + img -= EXTRAITEMS * mGridDimension.y(); + + while (i != end) + { + updateTileAtPos(i, img, allowAnimation); + i++; img++; + } } mLastCursor = mCursor; } + template -void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos) +void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowAnimation) { std::shared_ptr tile = mTiles.at(tilePos); // If we have more tiles than we have to display images on screen, hide them if(imgPos < 0 || imgPos >= size() || tilePos < 0 || tilePos >= (int) mTiles.size()) // Same for tiles out of the buffer { - tile->setSelected(false); - tile->setImage("", ""); + tile->setSelected(false, allowAnimation); + tile->reset(); tile->setVisible(false); } else { - + tile->setVisible(true); + + std::string name = mEntries.at(imgPos).name; // .object->getName(); + tile->setLabel(name); + + std::string imagePath = mEntries.at(imgPos).data.texturePath; + + if (ResourceManager::getInstance()->fileExists(imagePath)) + tile->setImage(imagePath); + else if (mEntries.at(imgPos).object->getType() == 2) + tile->setImage(mDefaultFolderTexture); + else + tile->setImage(mDefaultGameTexture); + + if (mAllowVideo) + { + std::string videoPath = mEntries.at(imgPos).data.videoPath; + + if (ResourceManager::getInstance()->fileExists(videoPath)) + tile->setVideo(videoPath, mVideoDelay); + else if (mEntries.at(imgPos).object->getType() == 2) + tile->setVideo(""); + else + tile->setVideo(""); + } + else + tile->setVideo(""); + if (imgPos == mCursor && mCursor != mLastCursor) { int dif = mCursor - tilePos; @@ -434,57 +696,81 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos) if (idx < 0 || idx >= mTiles.size()) idx = 0; - - tile->setSelected(true, &mTiles.at(idx)->getPosition()); - } - else - tile->setSelected(imgPos == mCursor); - tile->setVisible(true); - - std::string name = mEntries.at(imgPos).name; // .object->getName(); - std::string imagePath = mEntries.at(imgPos).data.texturePath; - - if (ResourceManager::getInstance()->fileExists(imagePath)) - tile->setImage(imagePath, name); - else if (mEntries.at(imgPos).object->getType() == 2) - tile->setImage(mDefaultFolderTexture, name); + tile->setSelected(true, allowAnimation, &mTiles.at(idx)->getPosition()); + } else - tile->setImage(mDefaultGameTexture, name); + tile->setSelected(imgPos == mCursor, allowAnimation); } } -// Return the starting position (the number of the game which will be displayed on top left of the screen) + +// Create and position tiles (mTiles) template -int ImageGridComponent::getStartPosition() const +void ImageGridComponent::buildTiles() { - return mStartPosition; + mStartPosition = 0; + mTiles.clear(); - // The "partialRow" variable exist because we want to keep the same positioning behavior in both - // case, whenever we have an integer number of rows or not (the last partial row is ignored when - // calculating position and the cursor shouldn't end up in this row when close to the end) - int partialRow = (int)mLastRowPartial; + calcGridDimension(); - int cursorRow = mCursor / mGridDimension.x(); - int cursorCol = mCursor % mGridDimension.x(); + Vector2f tileDistance = mTileSize + mMargin; + Vector2f tileSize = mTileSize; - int start = (cursorRow - ((mGridDimension.y() - partialRow) / 2)) * mGridDimension.x(); - if (start + (mGridDimension.x() * (mGridDimension.y() - partialRow)) >= (int)mEntries.size()) + if (mAutoLayout.x() != 0 && mAutoLayout.y() != 0) { - // If we are at the end put the row as close as we can and no higher, using the following formula - // Where E is the nb of entries, X the grid x dim (nb of column), Y the grid y dim (nb of line) - // start = first tile of last row - nb column * (nb line - 1) - // = (E - 1) / X * X - X * (Y - 1) - // = X * ((E - 1) / X - Y + 1) - start = mGridDimension.x() * (((int)mEntries.size() - 1) / mGridDimension.x() - mGridDimension.y() + 1 + partialRow); + auto x = (mSize.x() - (mMargin.x() * (mAutoLayout.x() - 1)) - 2 * mPadding.x()) / (int) mAutoLayout.x(); + auto y = (mSize.y() - (mMargin.y() * (mAutoLayout.y() - 1)) - 2 * mPadding.y()) / (int) mAutoLayout.y(); + + tileSize = Vector2f(x, y); + mTileSize = tileSize; + tileDistance = tileSize + mMargin; } - if (start < 0) - return 0; + bool vert = isVertical(); + + Vector2f bufferSize = Vector2f(/*vert && mGridDimension.y() == 1 ? tileDistance.x() :*/ 0, 0); + Vector2f startPosition = tileSize / 2 - bufferSize; + + startPosition += mPadding; + + int X, Y; + + // Layout tile size and position + for (int y = 0; y < (vert ? mGridDimension.y() : mGridDimension.x()); y++) + { + for (int x = 0; x < (vert ? mGridDimension.x() : mGridDimension.y()); x++) + { + // Create tiles + auto tile = std::make_shared(mWindow); + + // In Vertical mod, tiles are ordered from left to right, then from top to bottom + // In Horizontal mod, tiles are ordered from top to bottom, then from left to right + X = vert ? x : y - EXTRAITEMS; + Y = vert ? y - EXTRAITEMS : x; + + //if (!isVertical()) + // X--; + + tile->setPosition(X * tileDistance.x() + startPosition.x(), Y * tileDistance.y() + startPosition.y()); + tile->setOrigin(0.5f, 0.5f); + tile->reset(); + + if (mTheme) + tile->applyTheme(mTheme, mName, "gridtile", ThemeFlags::ALL); - return start; + if (mAutoLayout.x() != 0 && mAutoLayout.y() != 0) + tile->forceSize(mTileSize, mAutoLayoutZoom); + + mTiles.push_back(tile); + } + } + + mLastCursor = -1; + onCursorChanged(CURSOR_STOPPED); } + // Calculate how much tiles of size mTileSize we can fit in a grid of size mSize using a margin of size mMargin template void ImageGridComponent::calcGridDimension() @@ -492,15 +778,11 @@ void ImageGridComponent::calcGridDimension() // GRID_SIZE = COLUMNS * TILE_SIZE + (COLUMNS - 1) * MARGIN // <=> COLUMNS = (GRID_SIZE + MARGIN) / (TILE_SIZE + MARGIN) Vector2f gridDimension = (mSize + mMargin) / (mTileSize + mMargin); + if (mAutoLayout.x() != 0 && mAutoLayout.y() != 0) + gridDimension = mAutoLayout; mLastRowPartial = Math::floorf(gridDimension.y()) != gridDimension.y(); - - // Ceil y dim so we can display partial last row - mGridDimension = Vector2i(gridDimension.x(), Math::ceilf(gridDimension.y())); - - // Invert dimensions for horizontally scrolling grid - if (mScrollDirection == SCROLL_HORIZONTALLY) - mGridDimension = Vector2i((float) (int)gridDimension.y(), mGridDimension.x()); + mGridDimension = Vector2i((int) gridDimension.x(), (int) gridDimension.y()); // Grid dimension validation if (mGridDimension.x() < 1) @@ -508,8 +790,11 @@ void ImageGridComponent::calcGridDimension() if (mGridDimension.y() < 1) LOG(LogError) << "Theme defined grid Y dimension below 1"; - // Add extra tiles to both side depending on max texture buffer -// mGridDimension.y() += texBuffersForward[3] * 2; + // Add extra tiles to both sides : Add EXTRAITEMS before, EXTRAITEMS after + if (isVertical()) + mGridDimension.y() += 2 * EXTRAITEMS; + else + mGridDimension.x() += 2 * EXTRAITEMS; }; diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index f891c04490..1413e2fbe2 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -145,7 +145,7 @@ std::shared_ptr makeArrow(Window* window) auto menuTheme = ThemeData::getMenuTheme(); auto bracket = std::make_shared(window); - bracket->setImage(":/arrow.svg"); + bracket->setImage(ThemeData::getMenuTheme()->Icons.arrow); // ":/arrow.svg"); bracket->setColorShift(menuTheme->Text.color); bracket->setResize(0, round(menuTheme->Text.font->getLetterHeight())); //bracket->setResize(0, Math::round(Font::get(FONT_SIZE_MEDIUM)->getLetterHeight())); diff --git a/es-core/src/components/NinePatchComponent.h b/es-core/src/components/NinePatchComponent.h index 8d0dbf697e..446e8bbd23 100644 --- a/es-core/src/components/NinePatchComponent.h +++ b/es-core/src/components/NinePatchComponent.h @@ -36,6 +36,7 @@ class NinePatchComponent : public GuiComponent void setCenterColor(unsigned int centerColor); // Apply a color shift to the "center" part of the ninepatch. unsigned int getCenterColor() { return mCenterColor; }; + unsigned int getEdgeColor() { return mEdgeColor; }; virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index e1dc87eb7a..b52720343b 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -1,391 +1,391 @@ -#pragma once -#ifndef ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H -#define ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H - -#include "GuiComponent.h" -#include "Log.h" -#include "Window.h" - -//Used to display a list of options. -//Can select one or multiple options. - -// if !multiSelect -// * <- curEntry -> - -// always -// * press a -> open full list - -#define CHECKED_PATH ":/checkbox_checked.svg" -#define UNCHECKED_PATH ":/checkbox_unchecked.svg" - -template -class OptionListComponent : public GuiComponent -{ -private: - struct OptionListData - { - std::string name; - T object; - bool selected; - }; - - class OptionListPopup : public GuiComponent - { - private: - MenuComponent mMenu; - OptionListComponent* mParent; - - public: - OptionListPopup(Window* window, OptionListComponent* parent, const std::string& title) : GuiComponent(window), - mMenu(window, title.c_str()), mParent(parent) - { - auto menuTheme = ThemeData::getMenuTheme(); - auto font = menuTheme->Text.font; - auto color = menuTheme->Text.color; - - ComponentListRow row; - - // for select all/none - std::vector checkboxes; - - for(auto it = mParent->mEntries.begin(); it != mParent->mEntries.end(); it++) - { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, color), true); - - OptionListData& e = *it; - - if(mParent->mMultiSelect) - { - // add checkbox - auto checkbox = std::make_shared(mWindow); - checkbox->setImage(it->selected ? CHECKED_PATH : UNCHECKED_PATH); - checkbox->setResize(0, font->getLetterHeight()); - row.addElement(checkbox, false); - - // input handler - // update checkbox state & selected value - row.makeAcceptInputHandler([this, &e, checkbox] - { - e.selected = !e.selected; - checkbox->setImage(e.selected ? CHECKED_PATH : UNCHECKED_PATH); - mParent->onSelectedChanged(); - }); - - // for select all/none - checkboxes.push_back(checkbox.get()); - }else{ - // input handler for non-multiselect - // update selected value and close - row.makeAcceptInputHandler([this, &e] - { - mParent->mEntries.at(mParent->getSelectedId()).selected = false; - e.selected = true; - mParent->onSelectedChanged(); - delete this; - }); - } - - // also set cursor to this row if we're not multi-select and this row is selected - mMenu.addRow(row, (!mParent->mMultiSelect && it->selected)); - } - - mMenu.addButton(_T("BACK"), _T("accept"), [this] { delete this; }); - - if(mParent->mMultiSelect) - { - mMenu.addButton(_T("SELECT ALL"), _T("SELECT ALL"), [this, checkboxes] { - for(unsigned int i = 0; i < mParent->mEntries.size(); i++) - { - mParent->mEntries.at(i).selected = true; - checkboxes.at(i)->setImage(CHECKED_PATH); - } - mParent->onSelectedChanged(); - }); - - mMenu.addButton(_T("SELECT NONE"), _T("SELECT NONE"), [this, checkboxes] { - for(unsigned int i = 0; i < mParent->mEntries.size(); i++) - { - mParent->mEntries.at(i).selected = false; - checkboxes.at(i)->setImage(UNCHECKED_PATH); - } - mParent->onSelectedChanged(); - }); - } - - mMenu.setPosition( - (Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, - (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); - //Renderer::getScreenHeight() * 0.15f); - addChild(&mMenu); - } - - bool input(InputConfig* config, Input input) override - { - if(config->isMappedTo("b", input) && input.value != 0) - { - delete this; - return true; - } - - return GuiComponent::input(config, input); - } - - std::vector getHelpPrompts() override - { - auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); - return prompts; - } - }; - -public: - OptionListComponent(Window* window, const std::string& name, bool multiSelect = false) : GuiComponent(window), mMultiSelect(multiSelect), mName(name), - mText(window), mLeftArrow(window), mRightArrow(window) - { - auto theme = ThemeData::getMenuTheme(); - - // auto font = Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT); - mText.setFont(theme->Text.font); // font - mText.setColor(theme->Text.color); // 0x777777FF - mText.setHorizontalAlignment(ALIGN_CENTER); - addChild(&mText); - - mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); - mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); - - if (mMultiSelect) - { - mRightArrow.setImage(":/arrow.svg"); - mRightArrow.setColorShift(theme->Text.color); - addChild(&mRightArrow); - } else { - mLeftArrow.setImage(":/option_arrow.svg"); - mLeftArrow.setColorShift(theme->Text.color); - mLeftArrow.setFlipX(true); - addChild(&mLeftArrow); - - mRightArrow.setImage(":/option_arrow.svg"); - mLeftArrow.setColorShift(theme->Text.color); - addChild(&mRightArrow); - } - - setSize(mLeftArrow.getSize().x() + mRightArrow.getSize().x(), theme->Text.font->getHeight()); - } - - - virtual void setColor(unsigned int color) - { - mText.setColor(color); - mLeftArrow.setColorShift(color); - mRightArrow.setColorShift(color); - } - - // handles positioning/resizing of text and arrows - void onSizeChanged() override - { - mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); - mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); - - if(mSize.x() < (mLeftArrow.getSize().x() + mRightArrow.getSize().x())) - LOG(LogWarning) << "OptionListComponent too narrow!"; - - mText.setSize(mSize.x() - mLeftArrow.getSize().x() - mRightArrow.getSize().x(), mText.getFont()->getHeight()); - - // position - mLeftArrow.setPosition(0, (mSize.y() - mLeftArrow.getSize().y()) / 2); - mText.setPosition(mLeftArrow.getPosition().x() + mLeftArrow.getSize().x(), (mSize.y() - mText.getSize().y()) / 2); - mRightArrow.setPosition(mText.getPosition().x() + mText.getSize().x(), (mSize.y() - mRightArrow.getSize().y()) / 2); - } - - bool input(InputConfig* config, Input input) override - { - if(input.value != 0) - { - if(config->isMappedTo("a", input)) - { - open(); - return true; - } - if(!mMultiSelect) - { - if(config->isMappedLike("left", input)) - { - // move selection to previous - unsigned int i = getSelectedId(); - int next = (int)i - 1; - if(next < 0) - next += (int)mEntries.size(); - - mEntries.at(i).selected = false; - mEntries.at(next).selected = true; - onSelectedChanged(); - return true; - - }else if(config->isMappedLike("right", input)) - { - if (mEntries.size() == 0) - return true; - - // move selection to next - unsigned int i = getSelectedId(); - int next = (i + 1) % mEntries.size(); - mEntries.at(i).selected = false; - mEntries.at(next).selected = true; - onSelectedChanged(); - return true; - - } - } - } - return GuiComponent::input(config, input); - } - - std::vector getSelectedObjects() - { - std::vector ret; - for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) - { - if(it->selected) - ret.push_back(it->object); - } - - return ret; - } - - T getSelected() - { - assert(mMultiSelect == false); - auto selected = getSelectedObjects(); - assert(selected.size() == 1); - return selected.at(0); - } - - void add(const std::string& name, const T& obj, bool selected) - { - OptionListData e; - e.name = name; - e.object = obj; - e.selected = selected; - - mEntries.push_back(e); - onSelectedChanged(); - } - - void selectAll() - { - for(unsigned int i = 0; i < mEntries.size(); i++) - { - mEntries.at(i).selected = true; - } - onSelectedChanged(); - } - - void selectNone() - { - for(unsigned int i = 0; i < mEntries.size(); i++) - { - mEntries.at(i).selected = false; - } - onSelectedChanged(); - } - - void selectFirstItem() - { - for (unsigned int i = 0; i < mEntries.size(); i++) - mEntries.at(i).selected = false; - - if (mEntries.size() > 0) - mEntries.at(0).selected = true; - - onSelectedChanged(); - } - +#pragma once +#ifndef ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H +#define ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H + +#include "GuiComponent.h" +#include "Log.h" +#include "Window.h" + +//Used to display a list of options. +//Can select one or multiple options. + +// if !multiSelect +// * <- curEntry -> + +// always +// * press a -> open full list + +#define CHECKED_PATH ":/checkbox_checked.svg" +#define UNCHECKED_PATH ":/checkbox_unchecked.svg" + +template +class OptionListComponent : public GuiComponent +{ +private: + struct OptionListData + { + std::string name; + T object; + bool selected; + }; + + class OptionListPopup : public GuiComponent + { + private: + MenuComponent mMenu; + OptionListComponent* mParent; + + public: + OptionListPopup(Window* window, OptionListComponent* parent, const std::string& title) : GuiComponent(window), + mMenu(window, title.c_str()), mParent(parent) + { + auto menuTheme = ThemeData::getMenuTheme(); + auto font = menuTheme->Text.font; + auto color = menuTheme->Text.color; + + ComponentListRow row; + + // for select all/none + std::vector checkboxes; + + for(auto it = mParent->mEntries.begin(); it != mParent->mEntries.end(); it++) + { + row.elements.clear(); + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, color), true); + + OptionListData& e = *it; + + if(mParent->mMultiSelect) + { + // add checkbox + auto checkbox = std::make_shared(mWindow); + checkbox->setImage(it->selected ? CHECKED_PATH : UNCHECKED_PATH); + checkbox->setResize(0, font->getLetterHeight()); + row.addElement(checkbox, false); + + // input handler + // update checkbox state & selected value + row.makeAcceptInputHandler([this, &e, checkbox] + { + e.selected = !e.selected; + checkbox->setImage(e.selected ? CHECKED_PATH : UNCHECKED_PATH); + mParent->onSelectedChanged(); + }); + + // for select all/none + checkboxes.push_back(checkbox.get()); + }else{ + // input handler for non-multiselect + // update selected value and close + row.makeAcceptInputHandler([this, &e] + { + mParent->mEntries.at(mParent->getSelectedId()).selected = false; + e.selected = true; + mParent->onSelectedChanged(); + delete this; + }); + } + + // also set cursor to this row if we're not multi-select and this row is selected + mMenu.addRow(row, (!mParent->mMultiSelect && it->selected)); + } + + mMenu.addButton(_T("BACK"), _T("accept"), [this] { delete this; }); + + if(mParent->mMultiSelect) + { + mMenu.addButton(_T("SELECT ALL"), _T("SELECT ALL"), [this, checkboxes] { + for(unsigned int i = 0; i < mParent->mEntries.size(); i++) + { + mParent->mEntries.at(i).selected = true; + checkboxes.at(i)->setImage(CHECKED_PATH); + } + mParent->onSelectedChanged(); + }); + + mMenu.addButton(_T("SELECT NONE"), _T("SELECT NONE"), [this, checkboxes] { + for(unsigned int i = 0; i < mParent->mEntries.size(); i++) + { + mParent->mEntries.at(i).selected = false; + checkboxes.at(i)->setImage(UNCHECKED_PATH); + } + mParent->onSelectedChanged(); + }); + } + + mMenu.setPosition( + (Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, + (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); + //Renderer::getScreenHeight() * 0.15f); + addChild(&mMenu); + } + + bool input(InputConfig* config, Input input) override + { + if(config->isMappedTo("b", input) && input.value != 0) + { + delete this; + return true; + } + + return GuiComponent::input(config, input); + } + + std::vector getHelpPrompts() override + { + auto prompts = mMenu.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + return prompts; + } + }; + +public: + OptionListComponent(Window* window, const std::string& name, bool multiSelect = false) : GuiComponent(window), mMultiSelect(multiSelect), mName(name), + mText(window), mLeftArrow(window), mRightArrow(window) + { + auto theme = ThemeData::getMenuTheme(); + + // auto font = Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT); + mText.setFont(theme->Text.font); // font + mText.setColor(theme->Text.color); // 0x777777FF + mText.setHorizontalAlignment(ALIGN_CENTER); + addChild(&mText); + + mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); + mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); + + if (mMultiSelect) + { + mRightArrow.setImage(ThemeData::getMenuTheme()->Icons.arrow);// ":/arrow.svg"); + mRightArrow.setColorShift(theme->Text.color); + addChild(&mRightArrow); + } else { + mLeftArrow.setImage(ThemeData::getMenuTheme()->Icons.option_arrow); // ":/option_arrow.svg" + mLeftArrow.setColorShift(theme->Text.color); + mLeftArrow.setFlipX(true); + addChild(&mLeftArrow); + + mRightArrow.setImage(ThemeData::getMenuTheme()->Icons.option_arrow); // ":/option_arrow.svg"); + mLeftArrow.setColorShift(theme->Text.color); + addChild(&mRightArrow); + } + + setSize(mLeftArrow.getSize().x() + mRightArrow.getSize().x(), theme->Text.font->getHeight()); + } + + + virtual void setColor(unsigned int color) + { + mText.setColor(color); + mLeftArrow.setColorShift(color); + mRightArrow.setColorShift(color); + } + + // handles positioning/resizing of text and arrows + void onSizeChanged() override + { + mLeftArrow.setResize(0, mText.getFont()->getLetterHeight()); + mRightArrow.setResize(0, mText.getFont()->getLetterHeight()); + + if(mSize.x() < (mLeftArrow.getSize().x() + mRightArrow.getSize().x())) + LOG(LogWarning) << "OptionListComponent too narrow!"; + + mText.setSize(mSize.x() - mLeftArrow.getSize().x() - mRightArrow.getSize().x(), mText.getFont()->getHeight()); + + // position + mLeftArrow.setPosition(0, (mSize.y() - mLeftArrow.getSize().y()) / 2); + mText.setPosition(mLeftArrow.getPosition().x() + mLeftArrow.getSize().x(), (mSize.y() - mText.getSize().y()) / 2); + mRightArrow.setPosition(mText.getPosition().x() + mText.getSize().x(), (mSize.y() - mRightArrow.getSize().y()) / 2); + } + + bool input(InputConfig* config, Input input) override + { + if(input.value != 0) + { + if(config->isMappedTo("a", input)) + { + open(); + return true; + } + if(!mMultiSelect) + { + if(config->isMappedLike("left", input)) + { + // move selection to previous + unsigned int i = getSelectedId(); + int next = (int)i - 1; + if(next < 0) + next += (int)mEntries.size(); + + mEntries.at(i).selected = false; + mEntries.at(next).selected = true; + onSelectedChanged(); + return true; + + }else if(config->isMappedLike("right", input)) + { + if (mEntries.size() == 0) + return true; + + // move selection to next + unsigned int i = getSelectedId(); + int next = (i + 1) % mEntries.size(); + mEntries.at(i).selected = false; + mEntries.at(next).selected = true; + onSelectedChanged(); + return true; + + } + } + } + return GuiComponent::input(config, input); + } + + std::vector getSelectedObjects() + { + std::vector ret; + for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) + { + if(it->selected) + ret.push_back(it->object); + } + + return ret; + } + + T getSelected() + { + assert(mMultiSelect == false); + auto selected = getSelectedObjects(); + assert(selected.size() == 1); + return selected.at(0); + } + + void add(const std::string& name, const T& obj, bool selected) + { + OptionListData e; + e.name = name; + e.object = obj; + e.selected = selected; + + mEntries.push_back(e); + onSelectedChanged(); + } + + void selectAll() + { + for(unsigned int i = 0; i < mEntries.size(); i++) + { + mEntries.at(i).selected = true; + } + onSelectedChanged(); + } + + void selectNone() + { + for(unsigned int i = 0; i < mEntries.size(); i++) + { + mEntries.at(i).selected = false; + } + onSelectedChanged(); + } + + void selectFirstItem() + { + for (unsigned int i = 0; i < mEntries.size(); i++) + mEntries.at(i).selected = false; + + if (mEntries.size() > 0) + mEntries.at(0).selected = true; + + onSelectedChanged(); + } + void clear() { mEntries.clear(); - } - + } + inline void invalidate() { onSelectedChanged(); - } - + } + void setSelectedChangedCallback(const std::function& callback) { mSelectedChangedCallback = callback; - } - - -private: - unsigned int getSelectedId() - { - assert(mMultiSelect == false); - for(unsigned int i = 0; i < mEntries.size(); i++) - { - if(mEntries.at(i).selected) - return i; - } - - LOG(LogWarning) << "OptionListComponent::getSelectedId() - no selected element found, defaulting to 0"; - return 0; - } - - void open() - { - mWindow->pushGui(new OptionListPopup(mWindow, this, mName)); - } - - - void onSelectedChanged() - { - if(mMultiSelect) - { - // display # selected - std::stringstream ss; - ss << getSelectedObjects().size() << " SELECTED"; - mText.setText(ss.str()); - mText.setSize(0, mText.getSize().y()); - setSize(mText.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); - if(mParent) // hack since theres no "on child size changed" callback atm... - mParent->onSizeChanged(); - }else{ - // display currently selected + l/r cursors - for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) - { - if (it->selected) - { - mText.setText(Utils::String::toUpper(it->name)); - mText.setSize(0, mText.getSize().y()); - setSize(mText.getSize().x() + mLeftArrow.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); - if (mParent) // hack since theres no "on child size changed" callback atm... - mParent->onSizeChanged(); - break; - } - } - } - + } + + +private: + unsigned int getSelectedId() + { + assert(mMultiSelect == false); + for(unsigned int i = 0; i < mEntries.size(); i++) + { + if(mEntries.at(i).selected) + return i; + } + + LOG(LogWarning) << "OptionListComponent::getSelectedId() - no selected element found, defaulting to 0"; + return 0; + } + + void open() + { + mWindow->pushGui(new OptionListPopup(mWindow, this, mName)); + } + + + void onSelectedChanged() + { + if(mMultiSelect) + { + // display # selected + std::stringstream ss; + ss << getSelectedObjects().size() << " SELECTED"; + mText.setText(ss.str()); + mText.setSize(0, mText.getSize().y()); + setSize(mText.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); + if(mParent) // hack since theres no "on child size changed" callback atm... + mParent->onSizeChanged(); + }else{ + // display currently selected + l/r cursors + for(auto it = mEntries.cbegin(); it != mEntries.cend(); it++) + { + if (it->selected) + { + mText.setText(Utils::String::toUpper(it->name)); + mText.setSize(0, mText.getSize().y()); + setSize(mText.getSize().x() + mLeftArrow.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); + if (mParent) // hack since theres no "on child size changed" callback atm... + mParent->onSizeChanged(); + break; + } + } + } + if (mSelectedChangedCallback) - mSelectedChangedCallback(mEntries.at(getSelectedId()).object); - } - - std::vector getHelpPrompts() override - { - std::vector prompts; - if(!mMultiSelect) - prompts.push_back(HelpPrompt("left/right", "MODIFIER")); - - prompts.push_back(HelpPrompt("a", "SELECTIONNER")); - return prompts; - } - - bool mMultiSelect; - - std::string mName; - TextComponent mText; - ImageComponent mLeftArrow; - ImageComponent mRightArrow; - - std::vector mEntries; - std::function mSelectedChangedCallback; -}; - -#endif // ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H + mSelectedChangedCallback(mEntries.at(getSelectedId()).object); + } + + std::vector getHelpPrompts() override + { + std::vector prompts; + if(!mMultiSelect) + prompts.push_back(HelpPrompt("left/right", "MODIFIER")); + + prompts.push_back(HelpPrompt("a", "SELECTIONNER")); + return prompts; + } + + bool mMultiSelect; + + std::string mName; + TextComponent mText; + ImageComponent mLeftArrow; + ImageComponent mRightArrow; + + std::vector mEntries; + std::function mSelectedChangedCallback; +}; + +#endif // ES_CORE_COMPONENTS_OPTION_LIST_COMPONENT_H diff --git a/es-core/src/components/SliderComponent.cpp b/es-core/src/components/SliderComponent.cpp index a8e9657100..5139795cee 100644 --- a/es-core/src/components/SliderComponent.cpp +++ b/es-core/src/components/SliderComponent.cpp @@ -18,7 +18,7 @@ SliderComponent::SliderComponent(Window* window, float min, float max, float inc mColor = menuTheme->Text.color; mKnob.setOrigin(0.5f, 0.5f); - mKnob.setImage(":/slider_knob.svg"); + mKnob.setImage(ThemeData::getMenuTheme()->Icons.knob); // ":/slider_knob.svg"); mKnob.setColorShift(mColor); setSize(Renderer::getScreenWidth() * 0.15f, menuTheme->Text.font->getLetterHeight()); diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index 27ba63294e..62572d39a9 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -7,7 +7,7 @@ SwitchComponent::SwitchComponent(Window* window, bool state) : GuiComponent(wind { auto menuTheme = ThemeData::getMenuTheme(); - mImage.setImage(":/off.svg"); + mImage.setImage(ThemeData::getMenuTheme()->Icons.off); mImage.setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight()); mImage.setColorShift(menuTheme->Text.color); @@ -74,7 +74,9 @@ void SwitchComponent::setValue(const std::string& statestring) void SwitchComponent::onStateChanged() { - mImage.setImage(mState ? ":/on.svg" : ":/off.svg"); + auto theme = ThemeData::getMenuTheme(); + mImage.setImage(mState ? theme->Icons.on : theme->Icons.off); + //mImage.setImage(mState ? ":/on.svg" : ":/off.svg"); } std::vector SwitchComponent::getHelpPrompts() diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index 2800b41eaa..cd0949018f 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -41,6 +41,9 @@ void TextComponent::setFont(const std::shared_ptr& font) // Set the color of the font/text void TextComponent::setColor(unsigned int color) { + if (mColor == color) + return; + mColor = color; mColorOpacity = mColor & 0x000000FF; onColorChanged(); @@ -49,6 +52,9 @@ void TextComponent::setColor(unsigned int color) // Set the color of the background box void TextComponent::setBackgroundColor(unsigned int color) { + if (mBgColor == color) + return; + mBgColor = color; mBgColorOpacity = mBgColor & 0x000000FF; } diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index e472cec335..8f76273985 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -22,6 +22,8 @@ class TextComponent : public GuiComponent void setFont(const std::shared_ptr& font); void setUppercase(bool uppercase); void onSizeChanged() override; + + std::string getText() { return mText; } void setText(const std::string& text); virtual void setColor(unsigned int color); void setHorizontalAlignment(Alignment align); diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index fae35d643c..3586232b6a 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -8,7 +8,7 @@ #include "Window.h" #include -#define FADE_TIME_MS 200 +#define FADE_TIME_MS 900 std::string getTitlePath() { std::string titleFolder = getTitleFolder(); @@ -109,6 +109,7 @@ bool VideoComponent::setVideo(std::string path) // Store the path mVideoPath = fullPath; + mStartDelayed = false; // If the file exists then set the new video if (!fullPath.empty() && ResourceManager::getInstance()->fileExists(fullPath)) @@ -120,13 +121,13 @@ bool VideoComponent::setVideo(std::string path) return false; } -void VideoComponent::setImage(std::string path) +void VideoComponent::setImage(std::string path, bool tile, Vector2f maxSize) { // Check that the image has changed if (path == mStaticImagePath) return; - mStaticImage.setImage(path); + mStaticImage.setImage(path, tile, maxSize); mFadeIn = 0.0f; mStaticImagePath = path; } @@ -147,7 +148,7 @@ void VideoComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = parentTrans * getTransform(); GuiComponent::renderChildren(trans); - + Renderer::setMatrix(trans); // Handle the case where the video is delayed @@ -223,6 +224,9 @@ void VideoComponent::applyTheme(const std::shared_ptr& theme, const s setZIndex(elem->get("zIndex")); else setZIndex(getDefaultZIndex()); + + if (elem->has("path")) + mVideoPath = elem->get("path"); } std::vector VideoComponent::getHelpPrompts() @@ -232,79 +236,91 @@ std::vector VideoComponent::getHelpPrompts() return ret; } -void VideoComponent::handleStartDelay() + +void VideoComponent::handleLooping() { - // Only play if any delay has timed out - if (mStartDelayed) - { - if (mStartTime > SDL_GetTicks()) - { - // Timeout not yet completed - return; - } - // Completed - mStartDelayed = false; - // Clear the playing flag so startVideo works - mIsPlaying = false; - startVideo(); - } } -void VideoComponent::handleLooping() +void VideoComponent::handleStartDelay() { + // Only play if any delay has timed out + if (!mStartDelayed) + return; + + // Timeout not yet completed + if (mStartTime > SDL_GetTicks()) + return; + + // Completed + mStartDelayed = false; + // Clear the playing flag so startVideo works + mIsPlaying = false; + startVideo(); + + mFadeIn = 0.0f; + mIsPlaying = true; } void VideoComponent::startVideoWithDelay() { // If not playing then either start the video or initiate the delay - if (!mIsPlaying) + if (mIsPlaying || mStartDelayed) + return; + + // Set the video that we are going to be playing so we don't attempt to restart it + mPlayingVideoPath = mVideoPath; + + if (mConfig.startDelay == 0 || PowerSaver::getMode() == PowerSaver::INSTANT) { - // Set the video that we are going to be playing so we don't attempt to restart it - mPlayingVideoPath = mVideoPath; + // No delay. Just start the video + mStartDelayed = false; + mIsPlaying = false; + startVideo(); - if (mConfig.startDelay == 0 || PowerSaver::getMode() == PowerSaver::INSTANT) - { - // No delay. Just start the video - mStartDelayed = false; - startVideo(); - } - else - { - // Configure the start delay - mStartDelayed = true; - mFadeIn = 0.0f; - mStartTime = SDL_GetTicks() + mConfig.startDelay; - } + mFadeIn = 0.0f; mIsPlaying = true; } + else + { + // Configure the start delay + mStartDelayed = true; + mFadeIn = 0.0f; + mStartTime = SDL_GetTicks() + mConfig.startDelay; + } } void VideoComponent::update(int deltaTime) { manageState(); - // If the video start is delayed and there is less than the fade time then set the image fade - // accordingly - if (mStartDelayed) + if (mIsPlaying) { - Uint32 ticks = SDL_GetTicks(); - if (mStartTime > ticks) + // If the video start is delayed and there is less than the fade time then set the image fade + // accordingly + + if (mStartDelayed) { - Uint32 diff = mStartTime - ticks; - if (diff < FADE_TIME_MS) + Uint32 ticks = SDL_GetTicks(); + if (mStartTime > ticks) { - mFadeIn = (float)diff / (float)FADE_TIME_MS; - return; + Uint32 diff = mStartTime - ticks; + if (diff < FADE_TIME_MS) + { + mFadeIn = (float)diff / (float)FADE_TIME_MS; + return; + } } } + + // If the fade in is less than 1 then increment it + if (mFadeIn < 1.0f) + { + mFadeIn += deltaTime / (float)FADE_TIME_MS; + if (mFadeIn > 1.0f) + mFadeIn = 1.0f; + } } - // If the fade in is less than 1 then increment it - if (mFadeIn < 1.0f) - { - mFadeIn += deltaTime / (float)FADE_TIME_MS; - if (mFadeIn > 1.0f) - mFadeIn = 1.0f; - } + GuiComponent::update(deltaTime); } @@ -340,7 +356,7 @@ void VideoComponent::manageState() { startVideoWithDelay(); } - } + } } void VideoComponent::onShow() diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index 8d8207b1c7..0a306504ad 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -30,7 +30,7 @@ class VideoComponent : public GuiComponent // Loads the video at the given filepath bool setVideo(std::string path); // Loads a static image that is displayed if the video cannot be played - void setImage(std::string path); + void setImage(std::string path, bool tile = false, Vector2f maxSize = Vector2f(0, 0)); // Configures the component to show the default video void setDefaultVideo(); @@ -38,6 +38,8 @@ class VideoComponent : public GuiComponent // sets whether it's going to render in screensaver mode void setScreensaverMode(bool isScreensaver); + void setStartDelay(int delay) { mConfig.startDelay = delay; } + virtual void onShow() override; virtual void onHide() override; virtual void onScreenSaverActivate() override; @@ -70,6 +72,15 @@ class VideoComponent : public GuiComponent virtual void setMaxSize(float width, float height) = 0; inline void setMaxSize(const Vector2f& size) { setMaxSize(size.x(), size.y()); } + Vector2f getVideoSize() { return Vector2f(mVideoWidth, mVideoHeight); } + bool isPlaying() { + return mIsPlaying; + } + + bool isFading() { + return mFadeIn < 1.0f; + } + private: // Start the video Immediately virtual void startVideo() = 0; diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index ea712c0a60..54879c36f5 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -136,8 +136,6 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); - GuiComponent::renderChildren(trans); - Renderer::setMatrix(trans); if (mIsPlaying && mContext.valid) @@ -177,16 +175,19 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) vertices[3].tex[0] = 1.0f + tex_offs_x; vertices[3].tex[1] = -tex_offs_y; vertices[4].tex[0] = -tex_offs_x; vertices[4].tex[1] = 1.0f + tex_offs_y; vertices[5].tex[0] = 1.0f + tex_offs_x; vertices[5].tex[1] = 1.0f + tex_offs_y; - + // Colours - use this to fade the video in and out - for (int i = 0; i < (4 * 6); ++i) { - if ((i%4) < 3) + for (int i = 0; i < (4 * 6); ++i) + { + if ((i%4) == 3) vertices[i / 4].colour[i % 4] = mFadeIn; else vertices[i / 4].colour[i % 4] = 1.0f; } - + glEnable(GL_TEXTURE_2D); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); // Build a texture for the video frame mTexture->initFromPixels((unsigned char*)mContext.surface->pixels, mContext.surface->w, mContext.surface->h); @@ -208,6 +209,7 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) glDisableClientState(GL_COLOR_ARRAY); glDisable(GL_TEXTURE_2D); + glDisable(GL_BLEND); } else { VideoComponent::renderSnapshot(parentTrans); } @@ -346,10 +348,12 @@ void VideoVlcComponent::startVideo() libvlc_audio_set_mute(mMediaPlayer, 1); } - libvlc_media_player_play(mMediaPlayer); + libvlc_media_player_play(mMediaPlayer); libvlc_video_set_callbacks(mMediaPlayer, lock, unlock, display, (void*)&mContext); libvlc_video_set_format(mMediaPlayer, "RGBA", (int)mVideoWidth, (int)mVideoHeight, (int)mVideoWidth * 4); + // libvlc_media_player_set_position(mMediaPlayer, 0.15); + // Update the playing state mIsPlaying = true; mFadeIn = 0.0f; diff --git a/es-core/src/resources/ResourceManager.cpp b/es-core/src/resources/ResourceManager.cpp index 9f5014e432..3887a671a4 100644 --- a/es-core/src/resources/ResourceManager.cpp +++ b/es-core/src/resources/ResourceManager.cpp @@ -52,9 +52,10 @@ const ResourceData ResourceManager::getFileData(const std::string& path) const //check if its a resource const std::string respath = getResourcePath(path); - if(Utils::FileSystem::exists(respath)) + auto size = Utils::FileSystem::getFileSize(respath); + if (size > 0) { - ResourceData data = loadFile(respath); + ResourceData data = loadFile(respath, size); return data; } @@ -63,13 +64,16 @@ const ResourceData ResourceManager::getFileData(const std::string& path) const return data; } -ResourceData ResourceManager::loadFile(const std::string& path) const +ResourceData ResourceManager::loadFile(const std::string& path, size_t size) const { std::ifstream stream(path, std::ios::binary); - stream.seekg(0, stream.end); - size_t size = (size_t)stream.tellg(); - stream.seekg(0, stream.beg); + if (size == 0) + { + stream.seekg(0, stream.end); + size = (size_t)stream.tellg(); + stream.seekg(0, stream.beg); + } //supply custom deleter to properly free array std::shared_ptr data(new unsigned char[size], array_deleter); diff --git a/es-core/src/resources/ResourceManager.h b/es-core/src/resources/ResourceManager.h index 96eab6455e..ad3f20133e 100644 --- a/es-core/src/resources/ResourceManager.h +++ b/es-core/src/resources/ResourceManager.h @@ -43,7 +43,7 @@ class ResourceManager static std::shared_ptr sInstance; - ResourceData loadFile(const std::string& path) const; + ResourceData loadFile(const std::string& path, size_t size) const; std::list< std::weak_ptr > mReloadables; }; diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 48aae8932a..4c11b02198 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -14,8 +14,9 @@ #define DPI 96 TextureData::TextureData(bool tile) : mTile(tile), mTextureID(0), mDataRGBA(nullptr), mScalable(false), - mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f) + mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f), mMaxSize(Vector2f(0,0)) { + } TextureData::~TextureData() @@ -73,7 +74,14 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length // auto scale height to keep aspect mHeight = (size_t)Math::round(((float)mWidth / svgImage->width) * svgImage->height); } - + + if (mMaxSize.x() > 0 && mMaxSize.y() > 0 && (mWidth > mMaxSize.x() || mHeight > mMaxSize.y())) + { + Vector2i sz = ImageIO::adjustPictureSize(Vector2i(mWidth, mHeight), Vector2i(mMaxSize.x(), mMaxSize.y())); + mWidth = sz.x(); + mHeight = sz.y(); + } + unsigned char* dataRGBA = new unsigned char[mWidth * mHeight * 4]; NSVGrasterizer* rast = nsvgCreateRasterizer(); @@ -97,8 +105,8 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng if (mDataRGBA) return true; } - - unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height); + + unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height, mMaxSize.x(), mMaxSize.y()); // std::vector imageRGBA = ImageIO::loadFromMemoryRGBA32((const unsigned char*)(fileData), length, width, height); if (imageRGBA == NULL) diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 2355d439a9..7a0215de9a 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -7,6 +7,8 @@ #include #include +#include "math/Vector2f.h" + class TextureResource; class TextureData @@ -39,6 +41,8 @@ class TextureData // Release the texture from conventional RAM void releaseRAM(); + void setMaxSize(Vector2f maxSize) { mMaxSize = maxSize; }; + // Get the amount of VRAM currenty used by this texture size_t getVRAMUsage(); @@ -62,6 +66,8 @@ class TextureData float mSourceHeight; bool mScalable; bool mReloadable; + + Vector2f mMaxSize; }; #endif // ES_CORE_RESOURCES_TEXTURE_DATA_H diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index f64719003d..5755887187 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -113,6 +113,7 @@ void TextureDataManager::load(std::shared_ptr tex, bool block) { if (size < max_texture) break; + //size -= (*it)->getVRAMUsage(); (*it)->releaseVRAM(); (*it)->releaseRAM(); diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index c3d9eff0b0..3cb0c6f2b8 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -7,7 +7,7 @@ TextureDataManager TextureResource::sTextureDataManager; std::map< TextureResource::TextureKeyType, std::weak_ptr > TextureResource::sTextureMap; std::set TextureResource::sAllTextures; -TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic) : mTextureData(nullptr), mForceLoad(false) +TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic, Vector2f maxSize) : mTextureData(nullptr), mForceLoad(false) { // Create a texture data object for this texture if (!path.empty()) @@ -18,6 +18,7 @@ TextureResource::TextureResource(const std::string& path, bool tile, bool dynami if (dynamic) { data = sTextureDataManager.add(this, tile); + data->setMaxSize(maxSize); data->initFromPath(path); // Force the texture manager to load it using a blocking load sTextureDataManager.load(data, true); @@ -25,7 +26,9 @@ TextureResource::TextureResource(const std::string& path, bool tile, bool dynami else { mTextureData = std::shared_ptr(new TextureData(tile)); + data = mTextureData; + data->setMaxSize(maxSize); data->initFromPath(path); // Load it so we can read the width/height data->load(); @@ -100,14 +103,14 @@ bool TextureResource::bind() } } -std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic) +std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic, Vector2f maxSize) { std::shared_ptr& rm = ResourceManager::getInstance(); const std::string canonicalPath = Utils::FileSystem::getCanonicalPath(path); if(canonicalPath.empty()) { - std::shared_ptr tex(new TextureResource("", tile, false)); + std::shared_ptr tex(new TextureResource("", tile, false, maxSize)); rm->addReloadable(tex); //make sure we get properly deinitialized even though we do nothing on reinitialization return tex; } @@ -122,7 +125,7 @@ std::shared_ptr TextureResource::get(const std::string& path, b // need to create it std::shared_ptr tex; - tex = std::shared_ptr(new TextureResource(key.first, tile, dynamic)); + tex = std::shared_ptr(new TextureResource(key.first, tile, dynamic, maxSize)); std::shared_ptr data = sTextureDataManager.get(tex.get()); // is it an SVG? @@ -135,6 +138,9 @@ std::shared_ptr TextureResource::get(const std::string& path, b // Add it to the reloadable list rm->addReloadable(tex); + if (data != nullptr) + data->setMaxSize(maxSize); + // Force load it if necessary. Note that it may get dumped from VRAM if we run low if (forceLoad) { diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index c2be59f1d7..a6d9aee083 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -16,7 +16,7 @@ class TextureData; class TextureResource : public IReloadable { public: - static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true); + static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true, Vector2f maxSize = Vector2f(0, 0)); void initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height); virtual void initFromMemory(const char* file, size_t length); @@ -36,7 +36,7 @@ class TextureResource : public IReloadable static size_t getTotalTextureSize(); // returns the number of bytes that would be used if all textures were in memory protected: - TextureResource(const std::string& path, bool tile, bool dynamic); + TextureResource(const std::string& path, bool tile, bool dynamic, Vector2f maxSize); virtual void unload(std::shared_ptr& rm); virtual void reload(std::shared_ptr& rm); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 150a6191a2..c16aab4720 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -80,7 +80,7 @@ namespace Utils FileInfo fi; fi.path = fullName; - fi.readOnly = (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN; + fi.hidden = (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN; fi.directory = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY; contentList.push_back(fi); @@ -250,11 +250,19 @@ namespace Utils static std::string path; // only construct the homepath once - if(!path.length()) + if (!path.length()) { + // verify if .emulationstation/es_systems.cfg is under exe's path to make app portable + std::string portableDir = getExePath() + "/.emulationstation/es_systems.cfg"; + if (Utils::FileSystem::exists(portableDir)) + { + path = getExePath(); + return path; + } + // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows char* envHome = getenv("HOME"); - envHome = "H:/[Emulz]/EmulationStation/"; + #ifdef _DEBUG envHome = "H:/[Emulz]/EmulationStation/"; #endif @@ -303,8 +311,8 @@ namespace Utils if (result) { std::string ret = buffer; - path = getParent(ret); - return ret; + path = getGenericPath(getParent(ret)); + return path; } #endif path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); @@ -678,12 +686,24 @@ namespace Utils { std::string path = getGenericPath(_path); struct stat64 info; - + // check if stat64 succeeded return (stat64(path.c_str(), &info) == 0); } // exists + size_t getFileSize(const std::string& _path) + { + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + if ((stat64(path.c_str(), &info) == 0)) + return (size_t) info.st_size; + + return 0; + } + bool isAbsolute(const std::string& _path) { std::string path = getGenericPath(_path); diff --git a/es-core/src/utils/FileSystemUtil.h b/es-core/src/utils/FileSystemUtil.h index e8b01741c0..8f995f7990 100644 --- a/es-core/src/utils/FileSystemUtil.h +++ b/es-core/src/utils/FileSystemUtil.h @@ -13,7 +13,7 @@ namespace Utils { public: std::string path; - bool readOnly; + bool hidden; bool directory; }; @@ -42,6 +42,7 @@ namespace Utils bool removeFile (const std::string& _path); bool createDirectory (const std::string& _path); bool exists (const std::string& _path); + size_t getFileSize(const std::string& _path); bool isAbsolute (const std::string& _path); bool isRegularFile (const std::string& _path); bool isDirectory (const std::string& _path); From 8d7b19259bb643b4e40d910813b32a96d874f58e Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Wed, 19 Jun 2019 15:19:26 +0200 Subject: [PATCH 034/336] Update README.md --- README.md | 142 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 109 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index b4c9a4f011..bfe59cc3a8 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,123 @@ -EmulationStation -================ +EmulationStation FCAMOD +======================= -This is a fork of EmulationStation from jrassa. -EmulationStation is a cross-platform graphical front-end for emulators with controller navigation. +This is a fork of EmulationStation containing many additions. +This has been primary developped for Windows platform, but can be compiled for Linux & Raspberry Pi. Changes in my branch ==================== +**System list :** - Support for Multiple Emulators/Cores in es_systems.cfg, and setting Emulator/Core per game. -```xml - %HOME%\RetroArch\retroarch.exe -L %HOME%\RetroArch\cores\%CORE%_libretro.dll %ROM% - - - - mame2003_plus - mame2003 - - - - - fbalpha2012 - - - -``` -- Better Grid view (animations, layout, selection...) -- Created a 'GridEx' mode, which inherits from grid, in order to create a 'preview bar' including a game screenshot. -- Changed 'Automatic' view mode. Switchs to grid if "thumbnails" exist in gamelist. Switchs to gridex if "thumbnails" and "image" exist in gamelist. -- Don't show Directories that contains only one Game : just Show the game. -- Don't show Games what are marked Hidden in gamelist. -- Cleaned menus + changed menu item order (by interest). Simplified "Quit" menu item ( no more popup ) -- Case insensitive file extensions. -- Skip parsing 'downloaded_images' and 'media' folders ( loading time boost ) + ```xml + %HOME%\RetroArch\retroarch.exe -L %HOME%\RetroArch\cores\%CORE%_libretro.dll %ROM% + + + + mame2003_plus + mame2003 + + + + + fbalpha2012 + + + + ``` +**Grid view :** +- Animations when size changes and during scrolling. +- Supports having a label. + ```xml + + 969A9E + 1 0.18 + + + F6FAFF + + ``` +- Layout can be defined by number of columns and rows ( you had to calculate manually the size of tiles in previous versions ). Zooming the selected item can also be defined simply. + ```xml + + 4 3 + 1.04 + ``` +- Supports extended padding (top, left, bottom, right) : + ```xml + + 0.03 0.13 0.03 0.08 + ``` + +- Supports video in the selected item (delay can be defined in the theme) + ```xml + + 700 + ``` + +- Theme can define which image to use (image, thumbnail or marquee). + ```xml + + marquee + ``` + +- Theme can define the image sizing mode (minSize, maxSize or size). Gridtile items can define a padding. + ```xml + + 24 24 + minSize + ``` + +- Supports md_image, md_video, md_name items... just like detailed view. +- Ability to override grid size by system. + +**Detailed view :** +- Supports md_video, md_marquee items like video view did : Video view is no longer useful. + +**Custom views & Theming:** +- Allow creation of custom views, which inherits from one of the basic theme items ( basic, detailed, grid ). + ```xml + + + ``` +- Ability to select the view (or customview) to use globally or by system. +- The theme can force the default view to use ( attribute defaultView ) +- Fully supports Retropie & Recalbox Themes. + +**Optimizations :** +- Really faster loading time, using multithreading. +- The loading sequence displays a progress bar. +- Reviewed SVG loading and size calculation mecanism. Previous versions unloaded/reloaded SVGs each time a new container needed to display it because of a size calculation problem. +- Ability to disable "Preload UI" mecanism. This mecanism is used to preload the UI of gamelists of every system. Disable it adds a small lags when opening +- Don't keep in memory the cache of image filenames when launching games -> It takes a lot of memory for nothing. +- Skip parsing 'downloaded_images' and 'media' folders ( better loading time ) +- Added option "Optimize images Vram Use" : Don't load an image in it source resolution if it needs to be displayed smaller -> Resize images in memory to save VRAM. Introduce longer image loading time, but less VRAM use. + +**Menus :** +- Cleaned menus + changed menu item order (by interest). +- Full support for menu Theming. +- Separated "Transition style" and "Game launch transition" +- Added option "Boot on gamelist" +- Added option "Hide system view" +- Added option "Display favorites first in gamelist" + +**General :** - Localisation (French actually supported) +- OSK : On-screen Keyboard. +- Fixed : Don't show Games what are marked Hidden in gamelist. - Corrected favorites ( and custom lists ) management. -- Don't load all fields in Medadata Editor ( too tricky to use on windows, better use an external tool ). +- Don't show Directories that contains only one Game : just Show the game. +- Case insensitive file extensions. + +**Windows specific :** +- Simplified "Quit" menu item ( no more popup asking to restart or turn off Windows ) - Windows is now "Windowed No border" by default. On Windows, Exclusive fullscreen can be annoying... -- Theming : Ability to force default view ( attribute defaultView ) -- Windows : Stop using _wsystem for launching games. Run games with ShellExecuteEx instead ( avoids command window ) -- Add an option to leave ES open with a black screen" Chargement en cours..." when launching games ( avoids showing windows desktop ) +- Stop using _wsystem for launching games. Run games with ShellExecuteEx instead ( avoids command window ) +- Add an option to leave ES open with a black screen "Loading..." when launching games ( avoids showing windows desktop ) +- Don't load all fields in Medadata Editor ( too tricky to use on windows, better use an external tool ). -Je crois que c'est à peu près tout.... +Je crois que c'est à peu près tout... Building ======== From eec88917a6af611f11a33f8d67a2c7558d32e353 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Wed, 19 Jun 2019 16:41:49 +0200 Subject: [PATCH 035/336] Update README.md --- README.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index bfe59cc3a8..61ace5c931 100644 --- a/README.md +++ b/README.md @@ -84,8 +84,11 @@ Changes in my branch - Ability to select the view (or customview) to use globally or by system. - The theme can force the default view to use ( attribute defaultView ) - Fully supports Retropie & Recalbox Themes. +- Carousel supports element "logoPos" : this allows the logo not to be inevitably centered. +- Image loading : the image bytes where duplicated 3 times in memory. +- In previous versions, if a xml element was unknown in the theme, nothing was loaded. -**Optimizations :** +**Optimizations & Fixes:** - Really faster loading time, using multithreading. - The loading sequence displays a progress bar. - Reviewed SVG loading and size calculation mecanism. Previous versions unloaded/reloaded SVGs each time a new container needed to display it because of a size calculation problem. @@ -93,6 +96,11 @@ Changes in my branch - Don't keep in memory the cache of image filenames when launching games -> It takes a lot of memory for nothing. - Skip parsing 'downloaded_images' and 'media' folders ( better loading time ) - Added option "Optimize images Vram Use" : Don't load an image in it source resolution if it needs to be displayed smaller -> Resize images in memory to save VRAM. Introduce longer image loading time, but less VRAM use. +- Fixed video starting : Videos started fading even if the video was not available yet ( but not really fading : there was no blending ). +- Software clipping : Avoid rendering clipped items -> They were previously clipped by OpenGl scissors. +- Carousel animation was corrupted if the carousel has to display only one item with 1 +- Font : Optimization when calculating text extend. +- If XML writer fails, the gamelist.xml file become empty and set to 0Kb -> Added a mecanism to secure that. Also, previous gamelist.xml version is saved as gamelist.xml.old. **Menus :** - Cleaned menus + changed menu item order (by interest). @@ -106,16 +114,20 @@ Changes in my branch - Localisation (French actually supported) - OSK : On-screen Keyboard. - Fixed : Don't show Games what are marked Hidden in gamelist. +- Added a star icon before the name of the game when it is a favorite. - Corrected favorites ( and custom lists ) management. - Don't show Directories that contains only one Game : just Show the game. - Case insensitive file extensions. +- Stop using "sortname" in gamelists. It is useful. **Windows specific :** +- Natively portable. If file ".emulationstation/es_systems.cfg" relative to the exe folder. - Simplified "Quit" menu item ( no more popup asking to restart or turn off Windows ) - Windows is now "Windowed No border" by default. On Windows, Exclusive fullscreen can be annoying... - Stop using _wsystem for launching games. Run games with ShellExecuteEx instead ( avoids command window ) - Add an option to leave ES open with a black screen "Loading..." when launching games ( avoids showing windows desktop ) - Don't load all fields in Medadata Editor ( too tricky to use on windows, better use an external tool ). +- With some Nvidia GPUs when VSYNC is active, SDL_GL_SwapWindow takes a lot of CPU : Introduce a smart calculation based on display frequency to reduce the time SDL_GL_SwapWindow has to wait. This saves a lot of CPU load. Je crois que c'est à peu près tout... @@ -221,7 +233,7 @@ You can use `--help` or `-h` to view a list of command-line options. Briefly out --no-splash don't show the splash screen --debug more logging, show console on Windows --scrape scrape using command line interface ---windowed not fullscreen, should be used with --resolution +--windowed not fullscreen, may be used with --resolution --vsync [1/on or 0/off] turn vsync on or off (default is on) --max-vram [size] Max VRAM to use in Mb before swapping. 0 for unlimited --force-kid Force the UI mode to be Kid From 626ac2c9a6206e88de404c25a9b3526bf88fb5b3 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Wed, 19 Jun 2019 17:08:40 +0200 Subject: [PATCH 036/336] Update THEMES.md --- THEMES.md | 93 ++++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 82 insertions(+), 11 deletions(-) diff --git a/THEMES.md b/THEMES.md index fc4ff063b1..75274239ca 100644 --- a/THEMES.md +++ b/THEMES.md @@ -69,8 +69,8 @@ How it works Everything must be inside a `` tag. -**The `` tag *must* be specified**. This is the version of the theming system the theme was designed for. The current version is 3. - +**The `` tag *must* be specified**. This is the version of the theming system the theme was designed for. +The current version is 4. A *view* can be thought of as a particular "screen" within EmulationStation. Views are defined like this: @@ -109,6 +109,14 @@ Or, you can create your own elements by adding `extra="true"` (as is done in the ValueHere ``` +A *customView* can be thought of as a particular "screen" within EmulationStation. +Custom Views must inherit one of the standard views and are defined like this: + +```xml + + ... define elements here ... + +``` @@ -124,7 +132,7 @@ You can include theme files within theme files, similar to `#include` in C (thou `~/.emulationstation/all_themes.xml`: ```xml - 3 + 4 ./all_themes/myfont.ttf @@ -137,7 +145,7 @@ You can include theme files within theme files, similar to `#include` in C (thou `~/.emulationstation/snes/theme.xml`: ```xml - 3 + 4 ./../all_themes.xml @@ -150,7 +158,7 @@ You can include theme files within theme files, similar to `#include` in C (thou Is equivalent to this `snes/theme.xml`: ```xml - 3 + 4 ./all_themes/myfont.ttf @@ -170,7 +178,7 @@ Sometimes you want to apply the same properties to the same elements across mult ```xml - 3 + 4 ./snes_art/snes_header.png @@ -284,7 +292,7 @@ You can now change the order in which elements are rendered by setting `zIndex` * `imagegrid name="gamegrid"` - 20 * Media * `image name="md_image"` - 30 - * `video name="md_video"` - 30 + * `video name="md_video"` - 31 * `image name="md_marquee"` - 35 * Metadata - 40 * Labels @@ -392,6 +400,10 @@ Reference * `image name="md_image"` - POSITION | SIZE | Z_INDEX - Path is the "image" metadata for the currently selected game. + * `video name="md_video"` - POSITION | SIZE | Z_INDEX + - Path is the "video" metadata for the currently selected game. + * `image name="md_marquee"` - POSITION | SIZE | Z_INDEX + - Path is the "marquee" metadata for the currently selected game. * `rating name="md_rating"` - ALL - The "rating" metadata. * `datetime name="md_releasedate"` - ALL @@ -534,10 +546,33 @@ Reference - Displays details of the system currently selected in the carousel. * You can use extra elements (elements with `extra="true"`) to add your own backgrounds, etc. They will be displayed behind the carousel, and scroll relative to the carousel. +#### menu +* `helpsystem name="help"` - ALL + - The help system style for this view. If not defined, menus will have the same helpsystem as defined in system view. +* `menuBackground name="menubg"` - COLOR | PATH | FADEPATH + - The background behind menus. you can set an image and/or change color (alpha supported) + +* `menuSwitch name="menuswitch"` - PATHON | PATHOFF + - Images for the on/off switch in menus +* `menuSlider name="menuslider"` - PATH + - Image for the slider knob in menus +* `menuButton name="menubutton"` - PATH | FILLEDPATH + - Images for menu buttons +* `menuText name="menutext"` - FONTPATH | FONTSIZE | COLOR + - text for all menu entries +* `menuText name="menutitle"` - FONTPATH | FONTSIZE | COLOR + - text for menu titles +* `menuText name="menufooter"` - FONTPATH | FONTSIZE | COLOR + - text for menu footers or subtitles +* `menuTextSmall name="menutextsmall"` - FONTPATH | FONTSIZE | COLOR + - text for menu entries in smallerfont + +menu is used to theme helpsystem and ES menus. ## Types of properties: * NORMALIZED_PAIR - two decimals, in the range [0..1], delimited by a space. For example, `0.25 0.5`. Most commonly used for position (x and y coordinates) and size (width and height). +* NORMALIZED_RECT - four decimals, in the range [0..1], delimited by a space. For example, `0.25 0.5 0.10 0.30`. Most commonly used for padding to store top, left, bottom and right coordinates. * PATH - a path. If the first character is a `~`, it will be expanded into the environment variable for the home path (`$HOME` for Linux or `%HOMEPATH%` for Windows). If the first character is a `.`, it will be expanded to the theme file's directory, allowing you to specify resources relative to the theme file, like so: `./../general_art/myfont.ttf`. * BOOLEAN - `true`/`1` or `false`/`0`. * COLOR - a hexidecimal RGB or RGBA color (6 or 8 digits). If 6 digits, will assume the alpha channel is `FF` (not transparent). @@ -584,14 +619,26 @@ Can be created as an extra. * `pos` - type: NORMALIZED_PAIR. * `size` - type: NORMALIZED_PAIR. - The size of the grid. Take care the selected tile can go out of the grid size, so don't position the grid too close to another element or the screen border. -* `margin` - type: NORMALIZED_PAIR. +* `margin` - type: NORMALIZED_PAIR. Margin between tiles. +* `padding` - type: NORMALIZED_RECT. + - NEW : Padding for displaying tiles. +* `autoLayout` - type: NORMALIZED_PAIR. + - NEW : Number of column and rows in the grid (integer values). +* `autoLayoutSelectedZoom` - type: FLOAT. + - NEW : Zoom factor to apply when a tile is selected. +* `imageSource` - type: STRING. + - NEW : Selects the image to display. `thumbnail` by default, can also be set to `image` or `marquee`. +* `showVideoAtDelay` - type: FLOAT. + - NEW : delay in millseconds to display video, when the tile is selected. * `gameImage` - type: PATH. - The default image used for games which doesn't have an image. * `folderImage` - type: PATH. - The default image used for folders which doesn't have an image. * `scrollDirection` - type: STRING. - `vertical` by default, can also be set to `horizontal`. Not that in `horizontal` mod, the tiles are ordered from top to bottom, then from left to right. - +* `zIndex` - type: FLOAT. + - NEW : z-index value for component. Components will be rendered in order of z-index value from low to high. + #### gridtile * `size` - type: NORMALIZED_PAIR. @@ -610,7 +657,11 @@ Can be created as an extra. - Set the color of the center part of the ninepatch. The default tile background center color and selected tile background center color have no influence on each others. * `backgroundEdgeColor` - type: COLOR. - Set the color of the edge parts of the ninepatch. The default tile background edge color and selected tile background edge color have no influence on each others. - +* `selectionMode` - type: STRING. + - NEW : Selects if the background is over the full tile or only the image. `full` by default, can also be set to `image`. +* `imageSizeMode` - type: STRING. + - NEW : Selects the image sizing mode. `maxSize` by default, can also be set to `minSize` (outer zoom) or `size` (stretch). + #### video * `pos` - type: NORMALIZED_PAIR. @@ -634,7 +685,9 @@ Can be created as an extra. - If true, playing of video will be delayed for `delayed` seconds, when game is selected. * `zIndex` - type: FLOAT. - z-index value for component. Components will be rendered in order of z-index value from low to high. - +* `path` - type: PATH. + - NEW : Path to video file if video is an extra. + #### text Can be created as an extra. @@ -813,6 +866,24 @@ EmulationStation borrows the concept of "nine patches" from Android (or "9-Slice - Default is 3 * `zIndex` - type: FLOAT. - z-index value for component. Components will be rendered in order of z-index value from low to high. +* `logoPos` - type: NORMALIZED_PAIR. + - NEW : Set the logo position if it is not centered. + +#### menuText & menuTextSmall + +* `color` - type: COLOR. + - Default is 777777FF +* `fontPath` - type: PATH. + - Path to a truetype font (.ttf). +* `fontSize` - type: FLOAT. + - Size of the font as a percentage of screen height (e.g. for a value of `0.1`, the text's height would be 10% of the screen height). Default is 0.085 for menutitle, 0.045 for menutext and 0.035 for menufooter and menutextsmall. +* `separatorColor` - type: COLOR. + - Default is C6C7C6FF. Color of lines that separates menu entries. +* `selectedColor` - type: COLOR. + - Default is FFFFFFFF. Color of text for selected menu entry. +* `selectorColor` - type: COLOR. + - Default is 878787FF. Color of the selector bar. + The help system is a special element that displays a context-sensitive list of actions the user can take at any time. You should try and keep the position constant throughout every screen. Keep in mind the "default" settings (including position) are used whenever the user opens a menu. From b3fdc3a659c77cf93f6e6a42b8d5f99cffaccef4 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Wed, 19 Jun 2019 17:09:37 +0200 Subject: [PATCH 037/336] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 61ace5c931..0e2ee90026 100644 --- a/README.md +++ b/README.md @@ -113,6 +113,7 @@ Changes in my branch **General :** - Localisation (French actually supported) - OSK : On-screen Keyboard. +- Video elements can be added as extras. - Fixed : Don't show Games what are marked Hidden in gamelist. - Added a star icon before the name of the game when it is a favorite. - Corrected favorites ( and custom lists ) management. From 3a48d3d698bbee4a97a0ca439abd04fb4343cfb3 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 19 Jun 2019 17:28:14 +0200 Subject: [PATCH 038/336] - Faster loading time, using multithreading. - Reviewed SVG loading and size calculation mecanism. Previous versions unloaded/reloaded SVGs each time a new container needed to display it because of a size calculation problem. - Ability to override grid size by system. - Grid supports extended padding (top, left, bottom, right) : - Theme can define which image to use (image, thumbnail or marquee). - Don't keep in memory the cache of image filenames when launching games -> It takes a lot of memory for nothing. - If XML writer fails, the gamelist.xml file become empty and set to 0Kb -> Added a mecanism to secure that. Also, previous gamelist.xml version is saved as gamelist.xml.old. - OSK : On-screen Keyboard. - Video elements can be added as extras. --- es-app/src/CollectionSystemManager.cpp | 13 +- es-app/src/CollectionSystemManager.h | 2 +- es-app/src/EmulationStation.h | 16 +- es-app/src/Gamelist.cpp | 42 +- es-app/src/SystemData.cpp | 429 +++++++++++++----- es-app/src/SystemData.h | 12 +- .../src/components/ScraperSearchComponent.cpp | 17 +- es-app/src/components/TextListComponent.h | 2 +- .../src/guis/GuiCollectionSystemsOptions.cpp | 13 +- es-app/src/guis/GuiCollectionSystemsOptions.h | 3 + es-app/src/guis/GuiGamelistOptions.cpp | 125 ++++- es-app/src/guis/GuiGamelistOptions.h | 6 +- es-app/src/guis/GuiMenu.cpp | 25 +- es-app/src/guis/GuiMetaDataEd.cpp | 7 +- es-app/src/main.cpp | 4 + es-app/src/views/SystemView.cpp | 152 ++++--- es-app/src/views/ViewController.cpp | 38 +- .../src/views/gamelist/BasicGameListView.cpp | 53 ++- .../views/gamelist/DetailedGameListView.cpp | 107 +++-- .../src/views/gamelist/DetailedGameListView.h | 10 +- .../src/views/gamelist/GridGameListView.cpp | 159 ++++++- es-app/src/views/gamelist/GridGameListView.h | 13 +- .../views/gamelist/ISimpleGameListView.cpp | 2 +- .../src/views/gamelist/VideoGameListView.cpp | 2 +- es-core/CMakeLists.txt | 2 + es-core/src/GuiComponent.cpp | 15 +- es-core/src/GuiComponent.h | 14 + es-core/src/ImageIO.cpp | 12 +- es-core/src/ImageIO.h | 2 +- es-core/src/Settings.cpp | 8 +- es-core/src/ThemeData.cpp | 57 ++- es-core/src/ThemeData.h | 13 +- es-core/src/Window.cpp | 21 +- es-core/src/components/ButtonComponent.cpp | 16 +- es-core/src/components/ButtonComponent.h | 9 +- es-core/src/components/ComponentGrid.cpp | 16 +- es-core/src/components/ComponentGrid.h | 4 + es-core/src/components/GridTileComponent.cpp | 201 ++++---- es-core/src/components/GridTileComponent.h | 11 +- es-core/src/components/IList.h | 9 + es-core/src/components/ImageComponent.cpp | 23 +- es-core/src/components/ImageComponent.h | 4 + es-core/src/components/ImageGridComponent.h | 270 +++++------ es-core/src/components/MenuComponent.cpp | 56 +++ es-core/src/components/MenuComponent.h | 1 + es-core/src/components/NinePatchComponent.cpp | 11 + es-core/src/components/NinePatchComponent.h | 1 + es-core/src/components/SwitchComponent.cpp | 7 +- es-core/src/components/TextEditComponent.h | 3 +- es-core/src/components/VideoComponent.cpp | 62 ++- es-core/src/components/VideoComponent.h | 9 + .../src/components/VideoPlayerComponent.cpp | 4 +- es-core/src/components/VideoVlcComponent.cpp | 37 +- es-core/src/components/VideoVlcComponent.h | 1 + es-core/src/guis/GuiTextEditPopupKeyboard.cpp | 266 +++++++++++ es-core/src/guis/GuiTextEditPopupKeyboard.h | 73 +++ es-core/src/math/Vector2f.cpp | 22 + es-core/src/math/Vector2f.h | 3 + es-core/src/platform.h | 2 + es-core/src/resources/Font.cpp | 17 +- es-core/src/resources/Font.h | 7 +- es-core/src/resources/ResourceManager.cpp | 6 +- es-core/src/resources/ResourceManager.h | 4 +- es-core/src/resources/TextureData.cpp | 94 +++- es-core/src/resources/TextureData.h | 22 +- es-core/src/resources/TextureDataManager.cpp | 57 ++- es-core/src/resources/TextureResource.cpp | 76 +++- es-core/src/resources/TextureResource.h | 8 +- es-core/src/utils/FileSystemUtil.cpp | 3 + es-core/src/utils/StringUtil.cpp | 20 +- es-core/src/utils/StringUtil.h | 1 + 71 files changed, 2152 insertions(+), 680 deletions(-) create mode 100644 es-core/src/guis/GuiTextEditPopupKeyboard.cpp create mode 100644 es-core/src/guis/GuiTextEditPopupKeyboard.h diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 16af995b20..fb87f18a60 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -124,7 +124,7 @@ void CollectionSystemManager::saveCustomCollection(SystemData* sys) /* Methods to load all Collections into memory, and handle enabling the active ones */ // loads all Collection Systems -void CollectionSystemManager::loadCollectionSystems() +void CollectionSystemManager::loadCollectionSystems(bool async) { initAutoCollectionSystems(); CollectionSystemDecl decl = mCollectionSystemDeclsIndex[myCollectionsName]; @@ -135,8 +135,11 @@ void CollectionSystemManager::loadCollectionSystems() { // Now see which ones are enabled loadEnabledListFromSettings(); + + // add to the main System Vector, and create Views as needed - updateSystemsList(); + if (!async) + updateSystemsList(); } } @@ -170,7 +173,7 @@ void CollectionSystemManager::updateSystemsList() // add custom enabled ones addEnabledCollectionsToDisplayedSystems(&mCustomCollectionSystemsData); - if(Settings::getInstance()->getBool("SortAllSystems")) + if (Settings::getInstance()->getBool("SortAllSystems")) { // sort custom individual systems with other systems std::sort(SystemData::sSystemVector.begin(), SystemData::sSystemVector.end(), systemSort); @@ -200,7 +203,7 @@ void CollectionSystemManager::updateSystemsList() // add auto enabled ones addEnabledCollectionsToDisplayedSystems(&mAutoCollectionSystemsData); - + /* // create views for collections, before reload for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) { @@ -208,7 +211,7 @@ void CollectionSystemManager::updateSystemsList() { ViewController::get()->getGameListView((*sysIt)); } - } + }*/ // if we were editing a custom collection, and it's no longer enabled, exit edit mode if(mIsEditingCustom && !mEditingCollectionSystemData->isEnabled) diff --git a/es-app/src/CollectionSystemManager.h b/es-app/src/CollectionSystemManager.h index 23afe19320..50a113c366 100644 --- a/es-app/src/CollectionSystemManager.h +++ b/es-app/src/CollectionSystemManager.h @@ -49,7 +49,7 @@ class CollectionSystemManager static void deinit(); void saveCustomCollection(SystemData* sys); - void loadCollectionSystems(); + void loadCollectionSystems(bool async=false); void loadEnabledListFromSettings(); void updateSystemsList(); diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index 375b11d9cf..96c9c42201 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -4,15 +4,17 @@ // 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 10 -#define PROGRAM_VERSION_MAINTENANCE 0 -#define PROGRAM_VERSION_STRING "2.10.0 FCAMOD" -#define SCRAPER_VERSION_STRING "2.10.0" +#define PROGRAM_VERSION_MAJOR 2 +#define PROGRAM_VERSION_MINOR 10 +#define PROGRAM_VERSION_MAINTENANCE 0 +#define PROGRAM_VERSION_BUILD 4 + +#define PROGRAM_VERSION_STRING "2.10.0.4 FCAMOD" +#define SCRAPER_VERSION_STRING "2.10.0.4" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ -#define RESOURCE_VERSION_STRING "2,10,0\0" -#define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE +#define RESOURCE_VERSION_STRING "2,10,0,4\0" +#define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE,PROGRAM_VERSION_BUILD #endif // ES_APP_EMULATION_STATION_H diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index f0328caf76..ac56208496 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -1,6 +1,7 @@ #include "Gamelist.h" #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" #include "FileData.h" #include "FileFilterIndex.h" #include "Log.h" @@ -107,7 +108,8 @@ void parseGamelist(SystemData* system) return; } - system->setSystemViewMode(root.attribute("defaultView").value()); + Vector2f gridSizeOverride = Vector2f::parseString(root.attribute("gridSize").value()); + system->setSystemViewMode(root.attribute("defaultView").value(), gridSizeOverride, false); std::string relativeTo = system->getStartPath(); @@ -215,6 +217,19 @@ void updateGamelist(SystemData* system) else root.attribute("defaultView") = system->getSystemViewMode().c_str(); } + + Vector2f gridSize = Vector2f::parseString(root.attribute("gridSize").value()); + if (gridSize != system->getGridSizeOverride()) + { + numUpdated++; + + if (system->getGridSizeOverride() == Vector2f(0,0)) + root.remove_attribute("gridSize"); + else if (root.attribute("gridSize").empty()) + root.append_attribute("gridSize") = system->getGridSizeOverride().toString().c_str(); + else + root.attribute("gridSize") = system->getGridSizeOverride().toString().c_str(); + } if(!root) { @@ -288,9 +303,32 @@ void updateGamelist(SystemData* system) LOG(LogInfo) << "Added/Updated " << numUpdated << " entities in '" << xmlReadPath << "'"; - if (!doc.save_file(xmlWritePath.c_str())) { + // Secure XML writing -> Write to a temporary file first + std::string tmpFile = xmlWritePath + ".tmp"; + if (Utils::FileSystem::exists(tmpFile)) + Utils::FileSystem::removeFile(tmpFile); + + if (!doc.save_file(tmpFile.c_str())) { LOG(LogError) << "Error saving gamelist.xml to \"" << xmlWritePath << "\" (for system " << system->getName() << ")!"; } + else if (Utils::FileSystem::exists(tmpFile)) + { + // Secure XML writing + if ((int) Utils::FileSystem::getFileSize(tmpFile) > 0) + { + std::string savFile = xmlWritePath + ".old"; + + if (Utils::FileSystem::exists(savFile)) + Utils::FileSystem::removeFile(savFile); + + if (Utils::FileSystem::exists(xmlWritePath)) + std::rename(xmlWritePath.c_str(), savFile.c_str()); + + std::rename(tmpFile.c_str(), xmlWritePath.c_str()); + } + else + Utils::FileSystem::removeFile(tmpFile); + } } }else{ LOG(LogError) << "Found no root folder for system \"" << system->getName() << "\"!"; diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 657012078f..e12f7cb5bf 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -10,8 +10,10 @@ #include "Settings.h" #include "ThemeData.h" #include "views/UIModeController.h" -#include #include + +#include "utils/StringUtil.h" + #ifdef WIN32 #include #endif @@ -19,11 +21,16 @@ #include "GuiComponent.h" #include "Window.h" +#include + std::vector SystemData::sSystemVector; +#define USE_THREADING + SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) { + mGridSizeOverride = Vector2f(0, 0); mViewModeChanged = false; mFilterIndex = new FileFilterIndex(); @@ -57,19 +64,26 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys loadTheme(); } -bool SystemData::setSystemViewMode(std::string newViewMode) +bool SystemData::setSystemViewMode(std::string newViewMode, Vector2f gridSizeOverride, bool setChanged) { if (newViewMode == "automatic") newViewMode = ""; - if (mViewMode == newViewMode) + if (mViewMode == newViewMode && gridSizeOverride == mGridSizeOverride) return false; + mGridSizeOverride = gridSizeOverride; mViewMode = newViewMode; - mViewModeChanged = true; + mViewModeChanged = setChanged; + return true; } +Vector2f SystemData::getGridSizeOverride() +{ + return mGridSizeOverride; +} + SystemData::~SystemData() { delete mRootFolder; @@ -196,6 +210,234 @@ std::vector readList(const std::string& str, const char* delims = " return ret; } +SystemData* SystemData::loadSystem(pugi::xml_node system) +{ + std::vector emulatorList; + + std::string name, fullname, path, cmd, themeFolder, defaultCore; + + name = system.child("name").text().get(); + fullname = system.child("fullname").text().get(); + path = system.child("path").text().get(); + defaultCore = system.child("defaultCore").text().get(); + + pugi::xml_node emulators = system.child("emulators"); + if (emulators != NULL) + { + for (pugi::xml_node emulator : emulators.children()) + { + EmulatorData emulatorData; + emulatorData.mName = emulator.attribute("name").value(); + emulatorData.mCommandLine = emulator.attribute("command").value(); + + pugi::xml_node cores = emulator.child("cores"); + if (cores != NULL) + { + for (pugi::xml_node core : cores.children()) + { + const std::string& corename = core.text().get(); + + if (defaultCore.length() == 0) + defaultCore = corename; + + emulatorData.mCores.push_back(corename); + } + } + + emulatorList.push_back(emulatorData); + } + } + /* + if (window != NULL) + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); + + currentSystem++; + */ + // convert extensions list from a string into a vector of strings + + std::vector list = readList(system.child("extension").text().get()); + std::vector extensions; + + for (auto extension = list.cbegin(); extension != list.cend(); extension++) + { + std::string xt = (*extension); + std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); + + if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) + extensions.push_back(xt); + } + + cmd = system.child("command").text().get(); + + // platform id list + const char* platformList = system.child("platform").text().get(); + std::vector platformStrs = readList(platformList); + std::vector platformIds; + for (auto it = platformStrs.cbegin(); it != platformStrs.cend(); 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!"; + return nullptr; + } + + //convert path to generic directory seperators + path = Utils::FileSystem::getGenericPath(path); + + //expand home symbol if the startpath contains ~ + if (path[0] == '~') + { + path.erase(0, 1); + path.insert(0, Utils::FileSystem::getHomePath()); + } + + //create the system runtime environment data + SystemEnvironmentData* envData = new SystemEnvironmentData; + envData->mStartPath = path; + envData->mSearchExtensions = extensions; + envData->mLaunchCommand = cmd; + envData->mPlatformIds = platformIds; + envData->mEmulators = emulatorList; + + SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); + if (newSys->getRootFolder()->getChildrenByFilename().size() == 0) + { + LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; + delete newSys; + + return nullptr; + } + + return newSys; +} + +#ifdef USE_THREADING +typedef std::function work_function; + +#include +#include +#include +#include + +class ThreadPool +{ +public: + ThreadPool() : mRunning(true), mWaiting(false), mNumWork(0) + { +#ifdef WIN32 + SYSTEM_INFO sysinfo; + GetSystemInfo(&sysinfo); + size_t num_threads = sysinfo.dwNumberOfProcessors; +#else + size_t num_threads = sysconf(_SC_NPROCESSORS_ONLN); +#endif + + auto doWork = [&](size_t id) + { + while (mRunning) + { + _mutex.lock(); + if (!mWorkQueue.empty()) + { + auto work = mWorkQueue.front(); + mWorkQueue.pop(); + _mutex.unlock(); + + try + { + work(); + } + catch (...) { } + + mNumWork--; + } + else + { + _mutex.unlock(); + + // Extra code : Exit finished threads + if (mWaiting) + return; + + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + }; + + mThreads.reserve(num_threads); + + for (size_t i = 0; i < num_threads; i++) + mThreads.push_back(std::thread(doWork, i)); + } + + ~ThreadPool() + { + mRunning = false; + for (std::thread& t : mThreads) + t.join(); + } + + void queueWorkItem(work_function work) + { + _mutex.lock(); + mWorkQueue.push(work); + mNumWork++; + _mutex.unlock(); + } + + void wait() + { + mWaiting = true; + while (mNumWork.load() > 0) + std::this_thread::yield(); + } + + void wait(work_function work, int delay = 50) + { + mWaiting = true; + while (mNumWork.load() > 0) + { + work(); + + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(delay)); + } + } + +private: + bool mRunning; + bool mWaiting; + std::queue mWorkQueue; + std::atomic mNumWork; + std::mutex _mutex; + std::vector mThreads; + +}; +#endif + //creates systems from information located in a config file bool SystemData::loadConfig(Window* window) { @@ -231,138 +473,97 @@ bool SystemData::loadConfig(Window* window) return false; } - float systemCount = 1; + std::vector systemsNames; + + int systemCount = 0; for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { + systemsNames.push_back(system.child("fullname").text().get()); systemCount++; + } - float currentSystem = 0; - for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - { - std::vector emulatorList; - - std::string name, fullname, path, cmd, themeFolder, defaultCore; - - name = system.child("name").text().get(); - fullname = system.child("fullname").text().get(); - path = system.child("path").text().get(); - defaultCore = system.child("defaultCore").text().get(); - - pugi::xml_node emulators = system.child("emulators"); - if (emulators != NULL) - { - for (pugi::xml_node emulator : emulators.children()) - { - EmulatorData emulatorData; - emulatorData.mName = emulator.attribute("name").value(); - emulatorData.mCommandLine = emulator.attribute("command").value(); - - pugi::xml_node cores = emulator.child("cores"); - if (cores != NULL) - { - for (pugi::xml_node core : cores.children()) - { - const std::string& corename = core.text().get(); - - if (defaultCore.length() == 0) - defaultCore = corename; - - emulatorData.mCores.push_back(corename); - } - } - - emulatorList.push_back(emulatorData); - } - } - - if (window != NULL) - window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : currentSystem / systemCount); + int currentSystem = 0; - currentSystem++; +#ifdef WIN32 + unsigned int Ticks = GetTickCount(); +#endif - // convert extensions list from a string into a vector of strings +#ifdef USE_THREADING + ThreadPool threadPool; - std::vector list = readList(system.child("extension").text().get()); - std::vector extensions; + typedef SystemData* SystemDataPtr; + + SystemDataPtr* systems = new SystemDataPtr[systemCount]; + for (int i = 0; i < systemCount; i++) + systems[i] = nullptr; - for (auto extension = list.cbegin(); extension != list.cend(); extension++) - { - std::string xt = (*extension); - std::transform(xt.begin(), xt.end(), xt.begin(), ::_easytolower); - - if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) - extensions.push_back(xt); - } + threadPool.queueWorkItem([] { CollectionSystemManager::get()->loadCollectionSystems(true); }); - cmd = system.child("command").text().get(); +#endif - // platform id list - const char* platformList = system.child("platform").text().get(); - std::vector platformStrs = readList(platformList); - std::vector platformIds; - for(auto it = platformStrs.cbegin(); it != platformStrs.cend(); it++) + int processedSystem = 0; + + for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { +#ifdef USE_THREADING + threadPool.queueWorkItem([system, currentSystem, systems, &processedSystem] { - 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; - } + systems[currentSystem] = loadSystem(system); + processedSystem++; + }); +#else + std::string fullname = system.child("fullname").text().get(); - // 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()); + if (window != NULL) + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : (float) currentSystem / (float) (systemCount + 1)); + + SystemData* pSystem = loadSystem(system); + if (pSystem != nullptr) + sSystemVector.push_back(pSystem); + +#endif + currentSystem++; + } - //validate - if(name.empty() || path.empty() || extensions.empty() || cmd.empty()) +#ifdef USE_THREADING + if (window != NULL) + { + threadPool.wait([window, &processedSystem, systemCount, systemsNames] { - LOG(LogError) << "System \"" << name << "\" is missing name, path, extension, or command!"; - continue; - } + int px = processedSystem; + auto name = px < 0 || px > systemsNames.size() ? "" : systemsNames.at(px); + window->renderLoadingScreen(name, (float)px / (float)(systemCount + 1)); + }, 50); + } + else + threadPool.wait(); - //convert path to generic directory seperators - path = Utils::FileSystem::getGenericPath(path); + for (int i = 0; i < systemCount; i++) + { + SystemData* pSystem = systems[i]; + if (pSystem != nullptr) + sSystemVector.push_back(pSystem); + } - //expand home symbol if the startpath contains ~ - if (path[0] == '~') - { - path.erase(0, 1); - path.insert(0, Utils::FileSystem::getHomePath()); - } + delete[] systems; - //create the system runtime environment data - SystemEnvironmentData* envData = new SystemEnvironmentData; - envData->mStartPath = path; - envData->mSearchExtensions = extensions; - envData->mLaunchCommand = cmd; - envData->mPlatformIds = platformIds; - envData->mEmulators = emulatorList; + if (window != NULL) + window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); - SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); - if(newSys->getRootFolder()->getChildrenByFilename().size() == 0) - { - LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; - delete newSys; - } - else - sSystemVector.push_back(newSys); - } + CollectionSystemManager::get()->updateSystemsList(); +#else if (window != NULL) window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); - currentSystem++; - CollectionSystemManager::get()->loadCollectionSystems(); +#endif + +#ifdef WIN32 + Ticks = GetTickCount() - Ticks; + // ::MessageBox(0, std::to_string(Ticks).c_str(), NULL, NULL); +#endif + return true; } diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 59207eeede..d19aea5806 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -8,6 +8,9 @@ #include #include +#include +#include "math/Vector2f.h" + class FileData; class FileFilterIndex; class ThemeData; @@ -92,8 +95,10 @@ class SystemData inline const std::shared_ptr& getTheme() const { return mTheme; } - std::string getSystemViewMode() const { if (mViewMode == "automatic") return ""; else return mViewMode; }; - bool setSystemViewMode(std::string newViewMode); + std::string getSystemViewMode() const { if (mViewMode == "automatic") return ""; else return mViewMode; }; + bool setSystemViewMode(std::string newViewMode, Vector2f gridSizeOverride = Vector2f(0,0), bool setChanged = true); + + Vector2f getGridSizeOverride(); std::string getGamelistPath(bool forWrite) const; bool hasGamelist() const; @@ -127,6 +132,8 @@ class SystemData FileFilterIndex* getIndex() { return mFilterIndex; }; private: + static SystemData* loadSystem(pugi::xml_node system); + bool mIsCollectionSystem; bool mIsGameSystem; std::string mName; @@ -136,6 +143,7 @@ class SystemData std::shared_ptr mTheme; std::string mViewMode; + Vector2f mGridSizeOverride; bool mViewModeChanged; void populateFolder(FileData* folder); diff --git a/es-app/src/components/ScraperSearchComponent.cpp b/es-app/src/components/ScraperSearchComponent.cpp index 87aacba10e..788620b6db 100644 --- a/es-app/src/components/ScraperSearchComponent.cpp +++ b/es-app/src/components/ScraperSearchComponent.cpp @@ -8,6 +8,7 @@ #include "components/TextComponent.h" #include "guis/GuiMsgBox.h" #include "guis/GuiTextEditPopup.h" +#include "guis/GuiTextEditPopupKeyboard.h" #include "resources/Font.h" #include "utils/StringUtil.h" #include "FileData.h" @@ -235,12 +236,12 @@ void ScraperSearchComponent::onSearchDone(const std::vector if (!isValidConfiguredScraper()) { mWindow->pushGui(new GuiMsgBox(mWindow, Utils::String::toUpper("Configured scraper is no longer available.\nPlease change the scraping source in the settings."), - "FINISH", mSkipCallback)); + _T("FINISH"), mSkipCallback)); } else { ComponentListRow row; - row.addElement(std::make_shared(mWindow, "NO GAMES FOUND - SKIP", font, color), true); + row.addElement(std::make_shared(mWindow, _T("NO GAMES FOUND - SKIP"), font, color), true); if(mSkipCallback) row.makeAcceptInputHandler(mSkipCallback); @@ -455,17 +456,23 @@ void ScraperSearchComponent::openInputScreen(ScraperSearchParams& params) }; stop(); - mWindow->pushGui(new GuiTextEditPopup(mWindow, "SEARCH FOR", + + mWindow->pushGui(new GuiTextEditPopupKeyboard(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, + params.nameOverride.empty() ? params.game->getCleanName() : params.nameOverride, searchForFunc, false, "SEARCH")); + /* + mWindow->pushGui(new GuiTextEditPopup(mWindow, _T("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, _T("SEARCH")));*/ } std::vector ScraperSearchComponent::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); if(getSelectedIndex() != -1) - prompts.push_back(HelpPrompt("a", "accept result")); + prompts.push_back(HelpPrompt("a", _T("accept result"))); return prompts; } diff --git a/es-app/src/components/TextListComponent.h b/es-app/src/components/TextListComponent.h index c90329883b..eace2d6892 100644 --- a/es-app/src/components/TextListComponent.h +++ b/es-app/src/components/TextListComponent.h @@ -142,7 +142,7 @@ void TextListComponent::render(const Transform4x4f& parentTrans) int startEntry = 0; //number of entries that can fit on the screen simultaniously - int screenCount = (int)(mSize.y() / entrySize + 0.5f); + int screenCount = Math::round(mSize.y() / entrySize); // (int)(mSize.y() / entrySize); // + 0.5f -> avoid partial items if(size() >= screenCount) { diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index c47c7e7d5e..7d5ea965d8 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -77,6 +77,10 @@ void GuiCollectionSystemsOptions::initializeMenu() sortAllSystemsSwitch->setState(Settings::getInstance()->getBool("SortAllSystems")); mMenu.addWithLabel(_T("SORT CUSTOM COLLECTIONS AND SYSTEMS"), sortAllSystemsSwitch); + favoritesFirstSwitch = std::make_shared(mWindow); + favoritesFirstSwitch->setState(Settings::getInstance()->getBool("FavoritesFirst")); + mMenu.addWithLabel(_T("DISPLAY FAVORITES FIRST IN GAMELIST"), favoritesFirstSwitch); + if(CollectionSystemManager::get()->isEditing()) { row.elements.clear(); @@ -170,11 +174,16 @@ void GuiCollectionSystemsOptions::applySettings() std::string prevAuto = Settings::getInstance()->getString("CollectionSystemsAuto"); std::string outCustom = Utils::String::vectorToCommaString(customOptionList->getSelectedObjects()); std::string prevCustom = Settings::getInstance()->getString("CollectionSystemsCustom"); + bool outSort = sortAllSystemsSwitch->getState(); bool prevSort = Settings::getInstance()->getBool("SortAllSystems"); bool outBundle = bundleCustomCollections->getState(); bool prevBundle = Settings::getInstance()->getBool("UseCustomCollectionsSystem"); - bool needUpdateSettings = prevAuto != outAuto || prevCustom != outCustom || outSort != prevSort || outBundle != prevBundle; + + bool outFavoritesFirst = favoritesFirstSwitch->getState(); + bool prevFavoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); + + bool needUpdateSettings = prevAuto != outAuto || prevCustom != outCustom || outSort != prevSort || outBundle != prevBundle || outFavoritesFirst != prevFavoritesFirst; if (needUpdateSettings) { updateSettings(outAuto, outCustom); @@ -188,6 +197,8 @@ void GuiCollectionSystemsOptions::updateSettings(std::string newAutoSettings, st Settings::getInstance()->setString("CollectionSystemsCustom", newCustomSettings); Settings::getInstance()->setBool("SortAllSystems", sortAllSystemsSwitch->getState()); Settings::getInstance()->setBool("UseCustomCollectionsSystem", bundleCustomCollections->getState()); + Settings::getInstance()->setBool("FavoritesFirst", favoritesFirstSwitch->getState()); + Settings::getInstance()->saveFile(); CollectionSystemManager::get()->loadEnabledListFromSettings(); CollectionSystemManager::get()->updateSystemsList(); diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.h b/es-app/src/guis/GuiCollectionSystemsOptions.h index 0187920630..b54a8e92e7 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.h +++ b/es-app/src/guis/GuiCollectionSystemsOptions.h @@ -26,8 +26,11 @@ class GuiCollectionSystemsOptions : public GuiComponent void updateSettings(std::string newAutoSettings, std::string newCustomSettings); void createCollection(std::string inName); void exitEditMode(); + std::shared_ptr< OptionListComponent > autoOptionList; std::shared_ptr< OptionListComponent > customOptionList; + + std::shared_ptr favoritesFirstSwitch; std::shared_ptr sortAllSystemsSwitch; std::shared_ptr bundleCustomCollections; MenuComponent mMenu; diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 44ec65f9b4..2e4507a02b 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -13,9 +13,61 @@ #include "animations/LambdaAnimation.h" -GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : GuiComponent(window), +std::vector gridSizes { + "automatic", + + "2x2", + "2x3", + "2x4", + "2x5", + "2x6", + "2x7", + + "3x1", + "3x2", + "3x3", + "3x4", + "3x5", + "3x6", + "3x7", + + "4x1", + "4x2", + "4x3", + "4x4", + "4x5", + "4x6", + "4x7", + + "5x1", + "5x2", + "5x3", + "5x4", + "5x5", + "5x6", + "5x7", + + "6x1", + "6x2", + "6x3", + "6x4", + "6x5", + "6x6", + "6x7", + + "7x1", + "7x2", + "7x3", + "7x4", + "7x5", + "7x6", + "7x7" +}; + +GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool showGridFeatures) : GuiComponent(window), mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false) -{ +{ + mGridSize = NULL; addChild(&mMenu); // check it's not a placeholder folder - if it is, only show "Filter Options" @@ -106,6 +158,32 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui mMenu.addWithLabel(_T("GAMELIST VIEW STYLE"), mViewMode); + + + // Grid size override + if (showGridFeatures) + { + auto gridOverride = system->getGridSizeOverride(); + auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); + + mGridSize = std::make_shared>(mWindow, _T("GRID SIZE"), false); + + found = false; + for (auto it = gridSizes.cbegin(); it != gridSizes.cend(); it++) + { + bool sel = (gridOverride == Vector2f(0, 0) && *it == "automatic") || ovv == *it; + if (sel) + found = true; + + mGridSize->add(_L(*it), *it, sel); + } + + if (!found) + mGridSize->selectFirstItem(); + + mMenu.addWithLabel(_T("GRID SIZE"), mGridSize); + } + // show filtered menu if(!Settings::getInstance()->getBool("ForceDisableFilters")) { @@ -116,6 +194,31 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system) : Gui mMenu.addRow(row); } + + + /* + // maximum vram + auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); + max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); + s->addWithLabel(_T("VRAM LIMIT"), max_vram); + s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); + */ + + + + + + + + + + + + + + + + std::map customCollections = CollectionSystemManager::get()->getCustomCollectionSystems(); if(UIModeController::getInstance()->isUIModeFull() && @@ -188,7 +291,23 @@ GuiGamelistOptions::~GuiGamelistOptions() getGamelist()->onFileChanged(root, FILE_SORTED); } - if (mSystem->setSystemViewMode(mViewMode->getSelected()) || mFiltersChanged) + Vector2f gridSizeOverride(0, 0); + + if (mGridSize != NULL) + { + auto str = mGridSize->getSelected(); + + size_t divider = str.find('x'); + if (divider != std::string::npos) + { + std::string first = str.substr(0, divider); + std::string second = str.substr(divider+1, std::string::npos); + + gridSizeOverride = Vector2f((float)atof(first.c_str()), (float)atof(second.c_str())); + } + } + + if (mSystem->setSystemViewMode(mViewMode->getSelected(), gridSizeOverride) || mFiltersChanged) { // only reload full view if we came from a placeholder // as we need to re-display the remaining elements for whatever new diff --git a/es-app/src/guis/GuiGamelistOptions.h b/es-app/src/guis/GuiGamelistOptions.h index d6bfaf781c..32dbabfce5 100644 --- a/es-app/src/guis/GuiGamelistOptions.h +++ b/es-app/src/guis/GuiGamelistOptions.h @@ -4,6 +4,7 @@ #include "components/MenuComponent.h" #include "components/OptionListComponent.h" +#include "components/SliderComponent.h" #include "FileData.h" #include "GuiComponent.h" @@ -13,7 +14,7 @@ class SystemData; class GuiGamelistOptions : public GuiComponent { public: - GuiGamelistOptions(Window* window, SystemData* system); + GuiGamelistOptions(Window* window, SystemData* system, bool showGridFeatures = false); virtual ~GuiGamelistOptions(); virtual bool input(InputConfig* config, Input input) override; @@ -36,7 +37,8 @@ class GuiGamelistOptions : public GuiComponent std::shared_ptr mListSort; std::shared_ptr> mViewMode; - + + std::shared_ptr> mGridSize; SystemData* mSystem; IGameListView* getGamelist(); diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index ed37b73f28..298336617d 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -19,6 +19,7 @@ #include #include +#include "resources/TextureData.h" #include "animations/LambdaAnimation.h" GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) @@ -485,13 +486,17 @@ void GuiMenu::openUISettings() styles.push_back("grid"); } + auto viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + if (!system->getTheme()->hasView(viewPreference)) + viewPreference == "automatic"; + for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_L(*it), *it, Settings::getInstance()->getString("GamelistViewStyle") == *it); + gamelist_style->add(_L(*it), *it, viewPreference == *it); s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); - s->addSaveFunc([gamelist_style] { + s->addSaveFunc([gamelist_style, viewPreference] { bool needReload = false; - if (Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected()) + if (viewPreference != gamelist_style->getSelected()) needReload = true; Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); if (needReload) @@ -588,6 +593,7 @@ void GuiMenu::openUISettings() PowerSaver::init(); } Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); + GuiComponent::ALLOWANIMATIONS = Settings::getInstance()->getString("TransitionStyle") != "instant"; }); @@ -727,6 +733,9 @@ void GuiMenu::openOtherSettings() Settings::getInstance()->setBool("MoveCarousel", false); Settings::getInstance()->setBool("EnableSounds", false); } + + GuiComponent::ALLOWANIMATIONS = Settings::getInstance()->getString("TransitionStyle") != "instant"; + Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); PowerSaver::init(); }); @@ -780,6 +789,16 @@ void GuiMenu::openOtherSettings() preloadUI->setState(Settings::getInstance()->getBool("PreloadUI")); s->addWithLabel(_T("PRELOAD UI"), preloadUI); s->addSaveFunc([preloadUI] { Settings::getInstance()->setBool("PreloadUI", preloadUI->getState()); }); + + // optimizeVram + auto optimizeVram = std::make_shared(mWindow); + optimizeVram->setState(Settings::getInstance()->getBool("OptimizeVRAM")); + s->addWithLabel(_T("OPTIMIZE IMAGES VRAM USE"), optimizeVram); + s->addSaveFunc([optimizeVram] + { + TextureData::OPTIMIZEVRAM = optimizeVram->getState(); + Settings::getInstance()->setBool("OptimizeVRAM", optimizeVram->getState()); + }); #ifdef WIN32 // vsync diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 285e5f0d92..6979133cf9 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -18,6 +18,7 @@ #include "FileFilterIndex.h" #include "SystemData.h" #include "Window.h" +#include "guis/GuiTextEditPopupKeyboard.h" GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector& mdd, ScraperSearchParams scraperParams, const std::string& /*header*/, std::function saveCallback, std::function deleteFunc, FileData* file) : GuiComponent(window), @@ -216,8 +217,10 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector 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)); + row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] + { + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, title, ed->getValue(), updateVal, multiLine)); + // mWindow->pushGui(new GuiTextEditPopup(mWindow, title, ed->getValue(), updateVal, multiLine)); }); break; } diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index cd8edc9cf0..db6980ae6e 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -25,6 +25,7 @@ #include #endif +#include "resources/TextureData.h" #include bool scrape_cmdline = false; @@ -333,6 +334,9 @@ int main(int argc, char* argv[]) MameNames::init(); window.pushGui(ViewController::get()); + TextureData::OPTIMIZEVRAM = Settings::getInstance()->getBool("OptimizeVRAM"); + GuiComponent::ALLOWANIMATIONS = Settings::getInstance()->getString("TransitionStyle") != "instant"; + bool splashScreen = Settings::getInstance()->getBool("SplashScreen"); bool splashScreenProgress = Settings::getInstance()->getBool("SplashScreenProgress"); diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 805b4f8cd2..0da080ad81 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -30,92 +30,98 @@ void SystemView::populate() { mEntries.clear(); - for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { const std::shared_ptr& theme = (*it)->getTheme(); - if(mViewNeedsReload) + if (mViewNeedsReload) getViewElements(theme); - if((*it)->isVisible()) - { - Entry e; - e.name = (*it)->getName(); - e.object = *it; + if (!(*it)->isVisible()) + continue; - // make logo - const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); - if(logoElem) - { - std::string path = logoElem->get("path"); - std::string defaultPath = logoElem->has("default") ? logoElem->get("default") : ""; - if((!path.empty() && ResourceManager::getInstance()->fileExists(path)) - || (!defaultPath.empty() && ResourceManager::getInstance()->fileExists(defaultPath))) - { - ImageComponent* logo = new ImageComponent(mWindow, false, false); - logo->setMaxSize(mCarousel.logoSize * mCarousel.logoScale); - logo->applyTheme(theme, "system", "logo", ThemeFlags::PATH | ThemeFlags::COLOR); - logo->setRotateByTargetSize(true); - e.data.logo = std::shared_ptr(logo); - } - } - if (!e.data.logo) - { - // no logo in theme; use text - TextComponent* text = new TextComponent(mWindow, - (*it)->getName(), - Font::get(FONT_SIZE_LARGE), - 0x000000FF, - ALIGN_CENTER); - text->setSize(mCarousel.logoSize * mCarousel.logoScale); - text->applyTheme((*it)->getTheme(), "system", "logoText", ThemeFlags::FONT_PATH | ThemeFlags::FONT_SIZE | ThemeFlags::COLOR | ThemeFlags::FORCE_UPPERCASE | ThemeFlags::LINE_SPACING | ThemeFlags::TEXT); - e.data.logo = std::shared_ptr(text); - - if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) - { - text->setHorizontalAlignment(mCarousel.logoAlignment); - text->setVerticalAlignment(ALIGN_CENTER); - } else { - text->setHorizontalAlignment(ALIGN_CENTER); - text->setVerticalAlignment(mCarousel.logoAlignment); - } + Entry e; + e.name = (*it)->getName(); + e.object = *it; + + // make logo + const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); + if (logoElem) + { + std::string path = logoElem->get("path"); + std::string defaultPath = logoElem->has("default") ? logoElem->get("default") : ""; + + if ((!path.empty() && ResourceManager::getInstance()->fileExists(path)) + || (!defaultPath.empty() && ResourceManager::getInstance()->fileExists(defaultPath))) + { + ImageComponent* logo = new ImageComponent(mWindow, false, false); + logo->setMaxSize(mCarousel.logoSize * mCarousel.logoScale); + logo->applyTheme(theme, "system", "logo", ThemeFlags::PATH | ThemeFlags::SIZE | ThemeFlags::COLOR); + logo->setRotateByTargetSize(true); + e.data.logo = std::shared_ptr(logo); } + } + + if (!e.data.logo) + { + // no logo in theme; use text + TextComponent* text = new TextComponent(mWindow, + (*it)->getName(), + Font::get(FONT_SIZE_LARGE), + 0x000000FF, + ALIGN_CENTER); + text->setSize(mCarousel.logoSize * mCarousel.logoScale); + text->applyTheme((*it)->getTheme(), "system", "logoText", ThemeFlags::FONT_PATH | ThemeFlags::FONT_SIZE | ThemeFlags::COLOR | ThemeFlags::FORCE_UPPERCASE | ThemeFlags::LINE_SPACING | ThemeFlags::TEXT); + e.data.logo = std::shared_ptr(text); if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) { - if (mCarousel.logoAlignment == ALIGN_LEFT) - e.data.logo->setOrigin(0, 0.5); - else if (mCarousel.logoAlignment == ALIGN_RIGHT) - e.data.logo->setOrigin(1.0, 0.5); - else - e.data.logo->setOrigin(0.5, 0.5); - } else { - if (mCarousel.logoAlignment == ALIGN_TOP) - e.data.logo->setOrigin(0.5, 0); - else if (mCarousel.logoAlignment == ALIGN_BOTTOM) - e.data.logo->setOrigin(0.5, 1); - else - e.data.logo->setOrigin(0.5, 0.5); + text->setHorizontalAlignment(mCarousel.logoAlignment); + text->setVerticalAlignment(ALIGN_CENTER); } - - Vector2f denormalized = mCarousel.logoSize * e.data.logo->getOrigin(); - e.data.logo->setPosition(denormalized.x(), denormalized.y(), 0.0); - // delete any existing extras - for (auto extra : e.data.backgroundExtras) - delete extra; - e.data.backgroundExtras.clear(); - - // make background extras - e.data.backgroundExtras = ThemeData::makeExtras((*it)->getTheme(), "system", mWindow); - - // sort the extras by z-index - std::stable_sort(e.data.backgroundExtras.begin(), e.data.backgroundExtras.end(), [](GuiComponent* a, GuiComponent* b) { - return b->getZIndex() > a->getZIndex(); - }); - - this->add(e); + else { + text->setHorizontalAlignment(ALIGN_CENTER); + text->setVerticalAlignment(mCarousel.logoAlignment); + } + } + + if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) + { + if (mCarousel.logoAlignment == ALIGN_LEFT) + e.data.logo->setOrigin(0, 0.5); + else if (mCarousel.logoAlignment == ALIGN_RIGHT) + e.data.logo->setOrigin(1.0, 0.5); + else + e.data.logo->setOrigin(0.5, 0.5); } + else { + if (mCarousel.logoAlignment == ALIGN_TOP) + e.data.logo->setOrigin(0.5, 0); + else if (mCarousel.logoAlignment == ALIGN_BOTTOM) + e.data.logo->setOrigin(0.5, 1); + else + e.data.logo->setOrigin(0.5, 0.5); + } + + + Vector2f denormalized = mCarousel.logoSize * e.data.logo->getOrigin(); + e.data.logo->setPosition(denormalized.x(), denormalized.y(), 0.0); + // delete any existing extras + for (auto extra : e.data.backgroundExtras) + delete extra; + e.data.backgroundExtras.clear(); + + // make background extras + e.data.backgroundExtras = ThemeData::makeExtras((*it)->getTheme(), "system", mWindow); + + // sort the extras by z-index + std::stable_sort(e.data.backgroundExtras.begin(), e.data.backgroundExtras.end(), [](GuiComponent* a, GuiComponent* b) { + return b->getZIndex() > a->getZIndex(); + }); + + this->add(e); } + if (mEntries.size() == 0) { // Something is wrong, there is not a single system to show, check if UI mode is not full diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 9f8de5b5bf..95a99f41d8 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -147,7 +147,7 @@ void ViewController::goToGameList(SystemData* system) mCurrentView = getGameListView(system); if (mCurrentView) - mCurrentView->onShow(); + mCurrentView->onShow(); playViewTransition(false); } @@ -302,11 +302,16 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste bool forceView = false; std::string viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + if (!system->getTheme()->hasView(viewPreference)) + viewPreference == "automatic"; + std::string customThemeName; + Vector2f gridSizeOverride = Vector2f(0,0); if (!system->getSystemViewMode().empty() && system->getTheme()->hasView(system->getSystemViewMode())) { viewPreference = system->getSystemViewMode(); + gridSizeOverride = system->getGridSizeOverride(); forceView = true; } @@ -385,7 +390,10 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste view = std::shared_ptr(new DetailedGameListView(mWindow, system->getRootFolder())); break; case GRID: - view = std::shared_ptr(new GridGameListView(mWindow, system->getRootFolder())); + { + view = std::shared_ptr(new GridGameListView(mWindow, system->getRootFolder(), system->getTheme(), customThemeName, gridSizeOverride)); + } + break; case BASIC: default: @@ -393,10 +401,15 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste break; } - if (!customThemeName.empty()) - view->setThemeName(customThemeName); + if (selectedViewType != GRID) + { + // GridGameListView theme needs to be loaded before populating. - view->setTheme(system->getTheme()); + if (!customThemeName.empty()) + view->setThemeName(customThemeName); + + view->setTheme(system->getTheme()); + } std::vector& sysVec = SystemData::sSystemVector; int id = (int)(std::find(sysVec.cbegin(), sysVec.cend(), system) - sysVec.cbegin()); @@ -507,13 +520,22 @@ void ViewController::render(const Transform4x4f& parentTrans) void ViewController::preload() { - uint32_t i = 0; + int i = 1; + int max = SystemData::sSystemVector.size() + 1; + + bool splash = Settings::getInstance()->getBool("SplashScreen") && Settings::getInstance()->getBool("SplashScreenProgress"); + if (splash) + mWindow->renderLoadingScreen(_T("Preloading UI"), (float)i / (float)max); + + // First load the system list + getSystemListView(); + for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { - if (Settings::getInstance()->getBool("SplashScreen") && Settings::getInstance()->getBool("SplashScreenProgress")) + if (splash) { i++; - mWindow->renderLoadingScreen(_T("Preloading UI"), (float) i / (float) SystemData::sSystemVector.size()); + mWindow->renderLoadingScreen(_T("Preloading UI"), (float) i / (float)max); } (*it)->getIndex()->resetFilters(); diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 51e51c598a..39eb026d60 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -53,35 +53,48 @@ void BasicGameListView::populateList(const std::vector& files) mHeaderText.setText(systemName); bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + bool favoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); + bool showFavoriteIcon = (systemName != "favorites"); + if (!showFavoriteIcon) + favoritesFirst = false; if (files.size() > 0) { - for (auto it = files.cbegin(); it != files.cend(); it++) + if (favoritesFirst) { - if ((*it)->getFavorite()) - if (showHiddenFiles || !(*it)->getHidden()) - { - if (systemName == "favorites") - mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); - else - - mList.add(_T("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); - } + for (auto it = files.cbegin(); it != files.cend(); it++) + { + if (!showHiddenFiles && (*it)->getHidden()) + continue; + + if (!(*it)->getFavorite()) + continue; + + if (showFavoriteIcon) + mList.add(_T("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + else + mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + } } - /* + for (auto it = files.cbegin(); it != files.cend(); it++) { - if (!(*it)->getFavorite() && ((*it)->getType() == FOLDER)) - if (showHiddenFiles || !(*it)->getHidden()) - mList.add("[Folder] "+(*it)->getName(), *it, ((*it)->getType() == FOLDER)); + if (!showHiddenFiles && (*it)->getHidden()) + continue; - }*/ + if ((*it)->getFavorite()) + { + if (favoritesFirst) + continue; - for (auto it = files.cbegin(); it != files.cend(); it++) - { - if (!(*it)->getFavorite()) - if (showHiddenFiles || !(*it)->getHidden()) - mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + if (showFavoriteIcon) + { + mList.add(_T("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + continue; + } + } + + mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); } } else diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 67e22bf3eb..6bb0311fc9 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -15,9 +15,7 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : BasicGameListView(window, root), mDescContainer(window), mDescription(window), - mImage(window), - - mVideo(nullptr), + mImage(window), mMarquee(window), mVideo(nullptr), mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -26,10 +24,6 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mGenre(window), mPlayers(window), mLastPlayed(window), mPlayCount(window), mName(window) { - //mHeaderImage.setPosition(mSize.x() * 0.25f, 0); - - mVideoVisible = false; - const float padding = 0.01f; mList.setPosition(mSize.x() * (0.50f + padding), mList.getPosition().y()); @@ -37,15 +31,14 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mList.setAlignment(TextListComponent::ALIGN_LEFT); mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); - // Create the correct type of video window -#ifdef _RPI_ - if (Settings::getInstance()->getBool("VideoOmxPlayer")) - mVideo = new VideoPlayerComponent(window, ""); - else -#endif - mVideo = new VideoVlcComponent(window, getTitlePath()); + // Marquee + mMarquee.setOrigin(0.5f, 0.5f); + mMarquee.setPosition(mSize.x() * 0.25f, mSize.y() * 0.10f); + mMarquee.setMaxSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.18f); + mMarquee.setDefaultZIndex(35); + // addChild(&mMarquee); - // image + // 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); @@ -53,11 +46,20 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : addChild(&mImage); // video + // Create the correct type of video window +#ifdef _RPI_ + if (Settings::getInstance()->getBool("VideoOmxPlayer")) + mVideo = new VideoPlayerComponent(window, ""); + else +#endif + mVideo = new VideoVlcComponent(window, getTitlePath()); + mVideo->setOrigin(0.5f, 0.5f); mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); mVideo->setStartDelay(2000); - mVideo->setDefaultZIndex(30); + mVideo->setDefaultZIndex(31); +// addChild(mVideo); //addChild(mVideo); -> Add only if present in theme later @@ -121,20 +123,27 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them BasicGameListView::onThemeChanged(theme); using namespace ThemeFlags; - mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + + mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); mName.applyTheme(theme, getName(), "md_name", ALL); if (theme->getElement(getName(), "md_video", "video")) { - mVideoVisible = true; mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION); - addChild(mVideo); + if (!isChild(mVideo)) + addChild(mVideo); } - else - { - mVideoVisible = false; + else if (isChild(mVideo)) removeChild(mVideo); + + if (theme->getElement(getName(), "md_marquee", "image")) + { + mMarquee.applyTheme(theme, getName(), "md_marquee", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + if (!isChild(&mMarquee)) + addChild(&mMarquee); } + else if (isChild(&mMarquee)) + removeChild(&mMarquee); initMDLabels(); std::vector labels = getMDLabels(); @@ -168,6 +177,7 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION)); sortChildren(); + updateInfoPanel(); } void DetailedGameListView::initMDLabels() @@ -250,35 +260,30 @@ void DetailedGameListView::updateInfoPanel() bool fadingOut; if (file == NULL) - { + { mVideo->setVideo(""); - mVideo->setImage(""); - + //mVideo->setImage(""); //mImage.setImage(""); //mDescription.setText(""); fadingOut = true; - }else{ + } + else + { + std::string imagePath = file->getImagePath().empty() ? file->getThumbnailPath() : file->getImagePath(); - if (mVideoVisible) + if (isChild(mVideo)) { if (!mVideo->setVideo(file->getVideoPath())) - mVideo->setDefaultVideo(); - } + mVideo->setDefaultVideo(); - if (file->getImagePath().empty()) - { - if (mVideoVisible) - mVideo->setImage(file->getThumbnailPath(), false, mImage.getSize()); - - mImage.setImage(file->getThumbnailPath(), false, mImage.getSize()); + mVideo->setImage(imagePath, false, mVideo->getSize()); } - else - { - if (mVideoVisible) - mVideo->setImage(file->getImagePath(), false, mImage.getSize()); - mImage.setImage(file->getImagePath(), false, mImage.getSize()); - } + if (isChild(&mImage)) + mImage.setImage(imagePath, false, mImage.getSize()); + + if (isChild(&mMarquee)) + mMarquee.setImage(file->getMarqueePath()); mDescription.setText(getMetadata(file, "desc")); mDescContainer.reset(); @@ -301,7 +306,13 @@ void DetailedGameListView::updateInfoPanel() } std::vector comps = getMDValues(); - comps.push_back(&mImage); + + if (isChild(&mMarquee)) + comps.push_back(&mMarquee); + + if (isChild(&mImage)) + comps.push_back(&mImage); + comps.push_back(mVideo); comps.push_back(&mDescription); comps.push_back(&mName); @@ -330,7 +341,10 @@ void DetailedGameListView::updateInfoPanel() void DetailedGameListView::launch(FileData* game) { Vector3f target(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0); - if(mImage.hasImage()) + + if (isChild(mVideo) && !isChild(&mImage)) + target = Vector3f(mVideo->getCenter().x(), mVideo->getCenter().y(), 0); + else if(mImage.hasImage()) target = Vector3f(mImage.getCenter().x(), mImage.getCenter().y(), 0); ViewController::get()->launch(game, target); @@ -364,13 +378,6 @@ std::vector DetailedGameListView::getMDValues() return ret; } -void DetailedGameListView::update(int deltaTime) -{ - BasicGameListView::update(deltaTime); - - mImage.setVisible(mVideo == NULL || !(mVideo->isPlaying() && !mVideo->isFading())); -} - void DetailedGameListView::onShow() { GuiComponent::onShow(); diff --git a/es-app/src/views/gamelist/DetailedGameListView.h b/es-app/src/views/gamelist/DetailedGameListView.h index 9f2f00a044..2916ad66ff 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.h +++ b/es-app/src/views/gamelist/DetailedGameListView.h @@ -28,9 +28,6 @@ class DetailedGameListView : public BasicGameListView virtual void launch(FileData* game) override; -protected: - virtual void update(int deltaTime) override; - private: void updateInfoPanel(); @@ -39,6 +36,8 @@ class DetailedGameListView : public BasicGameListView std::string getMetadata(FileData* file, std::string name); ImageComponent mImage; + ImageComponent mMarquee; + VideoComponent* mVideo; TextComponent mLblRating, mLblReleaseDate, mLblDeveloper, mLblPublisher, mLblGenre, mLblPlayers, mLblLastPlayed, mLblPlayCount; @@ -51,15 +50,14 @@ class DetailedGameListView : public BasicGameListView DateTimeComponent mLastPlayed; TextComponent mPlayCount; TextComponent mName; - VideoComponent* mVideo; - + std::vector getMDLabels(); std::vector getMDValues(); ScrollableContainer mDescContainer; TextComponent mDescription; - bool mVideoVisible; + }; #endif // ES_APP_VIEWS_GAME_LIST_DETAILED_GAME_LIST_VIEW_H diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 4d3ac60834..918bb88892 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -6,13 +6,20 @@ #include "CollectionSystemManager.h" #include "Settings.h" #include "SystemData.h" +#include "Window.h" +#include "guis/GuiGamelistOptions.h" -GridGameListView::GridGameListView(Window* window, FileData* root) : +#ifdef _RPI_ +#include "components/VideoPlayerComponent.h" +#endif +#include "components/VideoVlcComponent.h" + +GridGameListView::GridGameListView(Window* window, FileData* root, const std::shared_ptr& theme, std::string themeName, Vector2f gridSize) : ISimpleGameListView(window, root), mGrid(window), mDescContainer(window), mDescription(window), mImage(window), - + mVideo(nullptr), mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -22,13 +29,14 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : { const float padding = 0.01f; + mVideoVisible = false; + + mGrid.setGridSizeOverride(gridSize); mGrid.setPosition(mSize.x() * 0.1f, mSize.y() * 0.1f); mGrid.setDefaultZIndex(20); mGrid.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); addChild(&mGrid); - populateList(root->getChildrenListToDisplay()); - // image mImage.setOrigin(0.5f, 0.5f); //mImage.setPosition(mSize.x() * 0.25f, mList.getPosition().y() + mSize.y() * 0.2125f); @@ -36,6 +44,22 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : mImage.setDefaultZIndex(30); addChild(&mImage); + // video + // Create the correct type of video window +#ifdef _RPI_ + if (Settings::getInstance()->getBool("VideoOmxPlayer")) + mVideo = new VideoPlayerComponent(window, ""); + else +#endif + mVideo = new VideoVlcComponent(window, getTitlePath()); + + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); + mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); + mVideo->setStartDelay(2000); + mVideo->setDefaultZIndex(31); + // addChild(mVideo); + // metadata labels + values mLblRating.setText("Rating: "); addChild(&mLblRating); @@ -83,9 +107,20 @@ GridGameListView::GridGameListView(Window* window, FileData* root) : initMDLabels(); initMDValues(); + + if (!themeName.empty()) + setThemeName(themeName); + + setTheme(theme); + + populateList(root->getChildrenListToDisplay()); updateInfoPanel(); } +GridGameListView::~GridGameListView() +{ + delete mVideo; +} void GridGameListView::setThemeName(std::string name) { ISimpleGameListView::setThemeName(name); @@ -118,12 +153,33 @@ std::string GridGameListView::getQuickSystemSelectLeftButton() bool GridGameListView::input(InputConfig* config, Input input) { + if (!UIModeController::getInstance()->isUIModeKid() && config->isMappedTo("select", input) && input.value) + { + Sound::getFromTheme(mTheme, getName(), "menuOpen")->play(); + mWindow->pushGui(new GuiGamelistOptions(mWindow, this->mRoot->getSystem(), true)); + return true; + + // Ctrl-R to reload a view when debugging + } + if(config->isMappedLike("left", input) || config->isMappedLike("right", input)) return GuiComponent::input(config, input); return ISimpleGameListView::input(config, input); } +const std::string GridGameListView::getImagePath(FileData* file) +{ + ImageSource src = mGrid.getImageSource(); + + if (src == ImageSource::IMAGE) + return file->getImagePath(); + else if (src == ImageSource::MARQUEE) + return file->getMarqueePath(); + + return file->getThumbnailPath(); +} + void GridGameListView::populateList(const std::vector& files) { mGrid.clear(); @@ -131,25 +187,51 @@ void GridGameListView::populateList(const std::vector& files) if (files.size() > 0) { std::string systemName = mRoot->getSystem()->getFullName(); + bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + bool favoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); + bool showFavoriteIcon = (systemName != "favorites"); + if (!showFavoriteIcon) + favoritesFirst = false; - for (auto it = files.cbegin(); it != files.cend(); it++) - if ((*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) + if (favoritesFirst) + { + for (auto it = files.cbegin(); it != files.cend(); it++) { - if (systemName == "favorites") - mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), (*it)->getVideoPath(), *it); - else - mGrid.add(_T("\uF006 ") + (*it)->getName(), (*it)->getThumbnailPath(), (*it)->getVideoPath(), *it); + if ((*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) + { + if (showFavoriteIcon) + mGrid.add(_T("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + else + mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + } } + } for (auto it = files.cbegin(); it != files.cend(); it++) - if (!(*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) - mGrid.add((*it)->getName(), (*it)->getThumbnailPath(), (*it)->getVideoPath(), *it); + { + if (!showHiddenFiles && (*it)->getHidden()) + continue; + + if ((*it)->getFavorite()) + { + if (favoritesFirst) + continue; + + if (showFavoriteIcon) + { + mGrid.add(_T("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + continue; + } + } + + mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + } } else { addPlaceholder(); - } + } } void GridGameListView::onThemeChanged(const std::shared_ptr& theme) @@ -169,6 +251,18 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) else mImageVisible = false; + if (theme->getElement(getName(), "md_video", "video")) + { + mVideoVisible = true; + mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION); + addChild(mVideo); + } + else + { + mVideoVisible = false; + removeChild(mVideo); + } + initMDLabels(); std::vector labels = getMDLabels(); assert(labels.size() == 8); @@ -196,9 +290,19 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) values[i]->applyTheme(theme, getName(), valElements[i], ALL ^ ThemeFlags::TEXT); } - mDescContainer.applyTheme(theme, getName(), "md_description", POSITION | ThemeFlags::SIZE | Z_INDEX); - mDescription.setSize(mDescContainer.getSize().x(), 0); - mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION)); + + if (theme->getElement(getName(), "md_description", "text")) + { + mDescContainer.applyTheme(theme, getName(), "md_description", POSITION | ThemeFlags::SIZE | Z_INDEX); + mDescription.setSize(mDescContainer.getSize().x(), 0); + mDescription.applyTheme(theme, getName(), "md_description", ALL ^ (POSITION | ThemeFlags::SIZE | ThemeFlags::ORIGIN | TEXT | ROTATION)); + + if (!isChild(&mDescContainer)) + addChild(&mDescContainer); + } + else + removeChild(&mDescContainer); + sortChildren(); updateInfoPanel(); @@ -276,17 +380,36 @@ void GridGameListView::updateInfoPanel() bool fadingOut; if (file == NULL) { + mVideo->setVideo(""); + mVideo->setImage(""); + //mDescription.setText(""); fadingOut = true; } else { + if (mVideoVisible) + { + if (!mVideo->setVideo(file->getVideoPath())) + mVideo->setDefaultVideo(); + } + if (mImageVisible) { if (file->getImagePath().empty()) + { + if (mVideoVisible) + mVideo->setImage(file->getThumbnailPath(), false, mVideo->getSize()); + mImage.setImage(file->getThumbnailPath(), false, mImage.getSize()); + } else + { + if (mVideoVisible) + mVideo->setImage(file->getImagePath(), false, mVideo->getSize()); + mImage.setImage(file->getImagePath(), false, mImage.getSize()); + } } else mImage.setImage(""); @@ -313,6 +436,7 @@ void GridGameListView::updateInfoPanel() std::vector comps = getMDValues(); comps.push_back(&mImage); + comps.push_back(mVideo); comps.push_back(&mDescription); comps.push_back(&mName); std::vector labels = getMDLabels(); @@ -353,6 +477,7 @@ void GridGameListView::remove(FileData *game, bool deleteFile) { if (deleteFile) Utils::FileSystem::removeFile(game->getPath()); // actually delete the file on the filesystem + FileData* parent = game->getParent(); if (getCursor() == game) // Select next element in list, or prev if none { @@ -444,5 +569,3 @@ std::vector GridGameListView::getHelpPrompts() } return prompts; } - - diff --git a/es-app/src/views/gamelist/GridGameListView.h b/es-app/src/views/gamelist/GridGameListView.h index d7fb787bc5..53508b1574 100644 --- a/es-app/src/views/gamelist/GridGameListView.h +++ b/es-app/src/views/gamelist/GridGameListView.h @@ -9,10 +9,13 @@ #include "views/gamelist/ISimpleGameListView.h" #include "views/gamelist/BasicGameListView.h" +class VideoComponent; + class GridGameListView : public ISimpleGameListView { public: - GridGameListView(Window* window, FileData* root); + GridGameListView(Window* window, FileData* root, const std::shared_ptr& theme, std::string customThemeName, Vector2f gridSize); + ~GridGameListView(); virtual void onThemeChanged(const std::shared_ptr& theme) override; @@ -33,7 +36,7 @@ class GridGameListView : public ISimpleGameListView virtual void launch(FileData* game) override; virtual void onFileChanged(FileData* file, FileChangeType change); - virtual void setThemeName(std::string name); + virtual void setThemeName(std::string name); protected: virtual std::string getQuickSystemSelectRightButton() override; @@ -46,6 +49,7 @@ class GridGameListView : public ISimpleGameListView private: void updateInfoPanel(); + const std::string getImagePath(FileData* file); void initMDLabels(); void initMDValues(); @@ -61,10 +65,13 @@ class GridGameListView : public ISimpleGameListView DateTimeComponent mLastPlayed; TextComponent mPlayCount; TextComponent mName; - ImageComponent mImage; + ImageComponent mImage; bool mImageVisible; + VideoComponent* mVideo; + bool mVideoVisible; + std::vector getMDLabels(); std::vector getMDValues(); diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 7425391c17..bde490abf5 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -164,7 +164,7 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) return true; } } - else if (config->isMappedTo("y", input) && UIModeController::getInstance()->isUIModeFull()) + else if (config->isMappedTo("y", input) && UIModeController::getInstance()->isUIModeKid()) { if(mRoot->getSystem()->isGameSystem()) { diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index b57d669444..823d9feb10 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -62,7 +62,7 @@ VideoGameListView::VideoGameListView(Window* window, FileData* root) : mVideo->setOrigin(0.5f, 0.5f); mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); mVideo->setSize(mSize.x() * (0.5f - 2*padding), mSize.y() * 0.4f); - mVideo->setDefaultZIndex(30); + mVideo->setDefaultZIndex(31); addChild(mVideo); // metadata labels + values diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index 9e4302f1e3..1cbb204927 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -55,6 +55,7 @@ set(CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInputConfig.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMsgBox.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopup.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopupKeyboard.h # Math ${CMAKE_CURRENT_SOURCE_DIR}/src/math/Misc.h @@ -128,6 +129,7 @@ set(CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInputConfig.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMsgBox.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopup.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopupKeyboard.cpp # Math ${CMAKE_CURRENT_SOURCE_DIR}/src/math/Misc.cpp diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index fe3c63ff50..b8946fb7c1 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -105,6 +105,8 @@ const std::string GuiTextTool::localize(const std::string text) return text; } +bool GuiComponent::ALLOWANIMATIONS = true; + GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255), mPosition(Vector3f::Zero()), mOrigin(Vector2f::Zero()), mRotationOrigin(0.5, 0.5), mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false) @@ -211,7 +213,7 @@ Vector2f GuiComponent::getSize() const void GuiComponent::setSize(float w, float h) { mSize = Vector2f(w, h); - onSizeChanged(); + onSizeChanged(); } float GuiComponent::getRotation() const @@ -245,7 +247,7 @@ void GuiComponent::setZIndex(float z) } float GuiComponent::getDefaultZIndex() const -{ +{ return mDefaultZIndex; } @@ -260,6 +262,15 @@ Vector2f GuiComponent::getCenter() const mPosition.y() - (getSize().y() * mOrigin.y()) + getSize().y() / 2); } +bool GuiComponent::isChild(GuiComponent* cmp) +{ + for (auto i = mChildren.cbegin(); i != mChildren.cend(); i++) + if (*i == cmp) + return true; + + return false; +} + //Children stuff. void GuiComponent::addChild(GuiComponent* cmp) { diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 31774a179e..73623121db 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -27,6 +27,8 @@ class GuiComponent GuiComponent(Window* window); virtual ~GuiComponent(); + static bool ALLOWANIMATIONS; + virtual void textInput(const char* text); //Called when input is received. @@ -94,6 +96,7 @@ class GuiComponent void sortChildren(); unsigned int getChildCount() const; GuiComponent* getChild(unsigned int i) const; + bool isChild(GuiComponent* cmp); // animation will be automatically deleted when it completes or is stopped. bool isAnimationPlaying(unsigned char slot) const; @@ -206,6 +209,7 @@ class GuiTextTool static const std::string localize(const std::string text); static void setLanguage(std::string lang); + static const std::string getLanguage() { return mCurrentLanguage; } private: static std::vector mItems; @@ -214,9 +218,19 @@ class GuiTextTool }; #if defined(_WIN32) + +#define UNICODE_CHARTYPE wchar_t* +#define UNICODE_CHARS(x) L ## x +#define UNICODE_STRING(x) GuiTextTool::convertFromWideString(L ## x) + #define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) #define _L(x) GuiTextTool::localize(x) #else + +#define UNICODE_CHARTYPE char* +#define UNICODE_CHARS(x) x +#define UNICODE_STRING(x) x + #define _T(x) GuiTextTool::localize(x) #define _L(x) GuiTextTool::localize(x) #endif // _WIN32 diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index 1bb4fe2f2f..e7b8e1690f 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -115,8 +115,11 @@ Vector2i ImageIO::adjustPictureSize(Vector2i imageSize, Vector2i maxSize) } -unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight) +unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight, Vector2i& baseSize, Vector2i& packedSize) { + baseSize = Vector2i(0, 0); + packedSize = Vector2i(0, 0); + width = 0; height = 0; @@ -142,22 +145,27 @@ unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const fiBitmap = fiConverted; } } + if (fiBitmap != nullptr) { width = FreeImage_GetWidth(fiBitmap); height = FreeImage_GetHeight(fiBitmap); + + baseSize = Vector2i(width, height); if (maxWidth > 0 && maxHeight > 0 && (width > maxWidth || height > maxHeight)) { Vector2i sz = adjustPictureSize(Vector2i(width, height), Vector2i(maxWidth, maxHeight)); if (sz.x() != width || sz.y() != height) - { + { FIBITMAP* imageRescaled = FreeImage_Rescale(fiBitmap, sz.x(), sz.y(), FILTER_BOX); FreeImage_Unload(fiBitmap); fiBitmap = imageRescaled; width = FreeImage_GetWidth(fiBitmap); height = FreeImage_GetHeight(fiBitmap); + + packedSize = Vector2i(width, height); } } diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index 89b27909ad..40f371ec1b 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -11,7 +11,7 @@ class ImageIO { public: static std::vector loadFromMemoryRGBA32(const unsigned char * data, const size_t size, size_t & width, size_t & height); - static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth=0, int maxHeight=0); + static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight, Vector2i& baseSize, Vector2i& packedSize); static void flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height); static Vector2i adjustPictureSize(Vector2i imageSize, Vector2i maxSize); diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index a42dd323c9..5eb83fa380 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -73,12 +73,7 @@ void Settings::setDefaults() mStringMap["StartupSystem"] = ""; -#if defined(_WIN32) mBoolMap["VSync"] = true; -#else - mBoolMap["VSync"] = false; -#endif - mBoolMap["EnableSounds"] = true; mBoolMap["ShowHelpPrompts"] = true; mBoolMap["ScrapeRatings"] = true; @@ -87,6 +82,7 @@ void Settings::setDefaults() mBoolMap["QuickSystemSelect"] = true; mBoolMap["MoveCarousel"] = true; mBoolMap["SaveGamelistsOnExit"] = true; + mBoolMap["OptimizeVRAM"] = false; mBoolMap["Debug"] = false; mBoolMap["DebugGrid"] = false; @@ -160,6 +156,8 @@ void Settings::setDefaults() mStringMap["CollectionSystemsCustom"] = ""; mBoolMap["SortAllSystems"] = false; mBoolMap["UseCustomCollectionsSystem"] = true; + mBoolMap["FavoritesFirst"] = true; + mBoolMap["LocalArt"] = false; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 1bb2cf8bb7..8957c4e2ee 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -8,6 +8,8 @@ #include "Settings.h" #include +#include "utils/StringUtil.h" + #ifdef _RPI_ #include "components/VideoPlayerComponent.h" #endif @@ -34,11 +36,13 @@ std::map> The { "size", NORMALIZED_PAIR }, { "margin", NORMALIZED_PAIR }, - { "padding", NORMALIZED_PAIR }, + { "padding", NORMALIZED_RECT }, { "autoLayout", NORMALIZED_PAIR }, { "autoLayoutSelectedZoom", FLOAT }, + { "imageSource", STRING }, // image, thumbnail, marquee + { "zIndex", FLOAT }, { "gameImage", PATH }, { "folderImage", PATH }, { "showVideoAtDelay", FLOAT }, @@ -52,6 +56,7 @@ std::map> The { "backgroundColor", COLOR }, { "backgroundCenterColor", COLOR }, { "backgroundEdgeColor", COLOR }, + { "selectionMode", STRING }, { "imageSizeMode", STRING } } }, { "text", { { "pos", NORMALIZED_PAIR }, @@ -205,7 +210,8 @@ std::map> The { "filledPath", PATH } } }, }; -std::shared_ptr ThemeData::MenuTheme; +std::shared_ptr ThemeData::mMenuTheme; +ThemeData* ThemeData::mCurrentTheme = nullptr; #define MINIMUM_THEME_FORMAT_VERSION 3 #define CURRENT_THEME_FORMAT_VERSION 6 @@ -240,9 +246,7 @@ unsigned int getHexColor(const char* str) return val; } -std::map mVariables; - -std::string resolvePlaceholders(const char* in) +std::string ThemeData::resolvePlaceholders(const char* in) { std::string inStr(in); @@ -320,7 +324,18 @@ void ThemeData::loadFile(std::string system, std::map parseCustomViews(root); parseFeatures(root); - MenuTheme = std::shared_ptr(new ThemeMenu(*this)); + mMenuTheme = nullptr; + mCurrentTheme = this; +} + +const std::shared_ptr& ThemeData::getMenuTheme() +{ + if (mMenuTheme == nullptr && mCurrentTheme != nullptr) + mMenuTheme = std::shared_ptr(new ThemeMenu(*mCurrentTheme)); + else if (mMenuTheme == nullptr) + return std::shared_ptr(new ThemeMenu(ThemeData())); + + return mMenuTheme; } std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) @@ -536,7 +551,6 @@ void ThemeData::parseViews(const pugi::xml_node& root) ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; parseView(node, view); - for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) { if (it->second.isCustomView && it->second.baseType == viewKey) @@ -545,10 +559,6 @@ void ThemeData::parseViews(const pugi::xml_node& root) parseView(node, customView); } } - - - - } } } @@ -745,6 +755,25 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapsecond) { + case NORMALIZED_RECT: + { + Vector4f val; + + auto splits = Utils::String::split(str, ' '); + if (splits.size() == 2) + { + val = Vector4f((float)atof(splits.at(0).c_str()), (float)atof(splits.at(1).c_str()), + (float)atof(splits.at(0).c_str()), (float)atof(splits.at(1).c_str())); + } + else if (splits.size() == 4) + { + val = Vector4f((float)atof(splits.at(0).c_str()), (float)atof(splits.at(1).c_str()), + (float)atof(splits.at(2).c_str()), (float)atof(splits.at(3).c_str())); + } + + element.properties[node.name()] = val; + break; + } case NORMALIZED_PAIR: { size_t divider = str.find(' '); @@ -759,7 +788,7 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map ThemeData::makeExtras(const std::shared_ptr ThemeData::makeExtras(const std::shared_ptrsetDefaultZIndex(10); comp->applyTheme(theme, view, *it, ThemeFlags::ALL); comps.push_back(comp); diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 16d59d4fd8..eeed1a54c4 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -3,6 +3,7 @@ #define ES_CORE_THEME_DATA_H #include "math/Vector2f.h" +#include "math/Vector4f.h" #include "utils/FileSystemUtil.h" #include @@ -143,12 +144,14 @@ class ThemeData struct Property { + void operator= (const Vector4f& value) { r = value; v = Vector2f(value.x(), value.y()); } void operator= (const Vector2f& value) { v = value; } void operator= (const std::string& value) { s = value; } void operator= (const unsigned int& value) { i = value; } void operator= (const float& value) { f = value; } void operator= (const bool& value) { b = value; } + Vector4f r; Vector2f v; std::string s; unsigned int i; @@ -166,6 +169,7 @@ class ThemeData else if(std::is_same::value) return *(const T*)&properties.at(prop).i; else if(std::is_same::value) return *(const T*)&properties.at(prop).f; else if(std::is_same::value) return *(const T*)&properties.at(prop).b; + else if (std::is_same::value) return *(const T*)&properties.at(prop).r; return T(); } @@ -194,6 +198,7 @@ class ThemeData enum ElementPropertyType { + NORMALIZED_RECT, NORMALIZED_PAIR, PATH, STRING, @@ -223,7 +228,7 @@ class ThemeData bool hasSubsets() { return mHasSubsets; } - static const std::shared_ptr& getMenuTheme() { return MenuTheme; } + static const std::shared_ptr& getMenuTheme(); static std::map sortThemeSubSets(const std::map& subsetmap, const std::string& subset); static std::map ThemeData::getThemeSubSets(const std::string& theme); @@ -253,6 +258,7 @@ class ThemeData void parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& view, std::string baseClass); std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); + std::string resolvePlaceholders(const char* in); std::map mViews; @@ -264,9 +270,12 @@ class ThemeData std::string mSystemThemeFolder; + std::map mVariables; + bool mHasSubsets; - static std::shared_ptr MenuTheme; + static std::shared_ptr mMenuTheme; + static ThemeData* mCurrentTheme; }; #endif // ES_CORE_THEME_DATA_H diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index c887c29028..987a171ef3 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -81,7 +81,7 @@ bool Window::init(bool initRenderer) InputManager::getInstance()->init(); - if (initRenderer) + //if (initRenderer) ResourceManager::getInstance()->reloadAll(); //keep a reference to the default fonts, so they don't keep getting destroyed/recreated @@ -109,11 +109,14 @@ void Window::deinit(bool deinitRenderer) { (*i)->onHide(); } + InputManager::getInstance()->deinit(); - + TextureResource::resetCache(); + ResourceManager::getInstance()->unloadAll(); + if (deinitRenderer) { - ResourceManager::getInstance()->unloadAll(); + // ResourceManager::getInstance()->unloadAll(); Renderer::deinit(); } } @@ -309,6 +312,9 @@ void Window::setAllowSleep(bool sleep) void Window::endRenderLoadingScreen() { + if (mSplash != NULL) + mSplash->unload(); + mSplash = NULL; } @@ -344,7 +350,7 @@ void Window::renderLoadingScreen(std::string text, float percent) #if defined(_WIN32) if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); + mSplash = TextureResource::get(":/splash.svg", false, false, true); #endif if (mSplash != NULL) @@ -381,11 +387,14 @@ void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) #if defined(_WIN32) if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); + mSplash = TextureResource::get(":/splash.svg", false, false, true); #endif if (mSplash != NULL) + { + mSplash->reload(); // Ensure splash is loaded splash.setImage(mSplash); + } else splash.setImage(":/splash.svg"); @@ -394,6 +403,8 @@ void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) splash.render(trans); auto& font = mDefaultFonts.at(1); + font->reload(); // Ensure font is loaded + TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); diff --git a/es-core/src/components/ButtonComponent.cpp b/es-core/src/components/ButtonComponent.cpp index b457c6aff5..eb4e64294a 100644 --- a/es-core/src/components/ButtonComponent.cpp +++ b/es-core/src/components/ButtonComponent.cpp @@ -4,7 +4,7 @@ #include "utils/StringUtil.h" #include "Renderer.h" -ButtonComponent::ButtonComponent(Window* window, const std::string& text, const std::string& helpText, const std::function& func) : GuiComponent(window), +ButtonComponent::ButtonComponent(Window* window, const std::string& text, const std::string& helpText, const std::function& func, bool upperCase) : GuiComponent(window), mBox(window, ThemeData::getMenuTheme()->Icons.button), mFont(Font::get(FONT_SIZE_MEDIUM)), mFocused(false), @@ -19,7 +19,7 @@ ButtonComponent::ButtonComponent(Window* window, const std::string& text, const mColor = menuTheme->Text.color; setPressedFunc(func); - setText(text, helpText); + setText(text, helpText, upperCase); updateImage(); } @@ -45,9 +45,9 @@ bool ButtonComponent::input(InputConfig* config, Input input) return GuiComponent::input(config, input); } -void ButtonComponent::setText(const std::string& text, const std::string& helpText) +void ButtonComponent::setText(const std::string& text, const std::string& helpText, bool upperCase) { - mText = Utils::String::toUpper(text); + mText = upperCase ? Utils::String::toUpper(text) : text; mHelpText = helpText; mTextCache = std::unique_ptr(mFont->buildTextCache(mText, 0, 0, getCurTextColor())); @@ -86,6 +86,14 @@ void ButtonComponent::updateImage() return; } + // If a new color has been set. + if (mNewColor) { + mBox.setImagePath(ThemeData::getMenuTheme()->Icons.button_filled); + mBox.setCenterColor(mModdedColor); + mBox.setEdgeColor(mModdedColor); + return; + } + mBox.setCenterColor(mColor); mBox.setEdgeColor(mColor); mBox.setImagePath(mFocused ? ThemeData::getMenuTheme()->Icons.button_filled : ThemeData::getMenuTheme()->Icons.button); diff --git a/es-core/src/components/ButtonComponent.h b/es-core/src/components/ButtonComponent.h index 790bbc6d43..0825aed5e7 100644 --- a/es-core/src/components/ButtonComponent.h +++ b/es-core/src/components/ButtonComponent.h @@ -10,7 +10,7 @@ class TextCache; class ButtonComponent : public GuiComponent { public: - ButtonComponent(Window* window, const std::string& text = "", const std::string& helpText = "", const std::function& func = nullptr); + ButtonComponent(Window* window, const std::string& text = "", const std::string& helpText = "", const std::function& func = nullptr, bool upperCase = true); void setPressedFunc(std::function f); @@ -19,7 +19,7 @@ class ButtonComponent : public GuiComponent bool input(InputConfig* config, Input input) override; void render(const Transform4x4f& parentTrans) override; - void setText(const std::string& text, const std::string& helpText); + void setText(const std::string& text, const std::string& helpText, bool upperCase = true); inline const std::string& getText() const { return mText; }; inline const std::function& getPressedFunc() const { return mPressedFunc; }; @@ -28,6 +28,9 @@ class ButtonComponent : public GuiComponent void onFocusGained() override; void onFocusLost() override; + void setColorShift(unsigned int color) { mModdedColor = color; mNewColor = true; updateImage(); } + void removeColorShift() { mNewColor = false; updateImage(); } + virtual std::vector getHelpPrompts() override; private: @@ -36,8 +39,10 @@ class ButtonComponent : public GuiComponent bool mFocused; bool mEnabled; + bool mNewColor = false; unsigned int mTextColorFocused; unsigned int mTextColorUnfocused; + unsigned int mModdedColor; unsigned int getCurTextColor() const; void updateImage(); diff --git a/es-core/src/components/ComponentGrid.cpp b/es-core/src/components/ComponentGrid.cpp index 7ba5b0c1c3..795e59b8c9 100644 --- a/es-core/src/components/ComponentGrid.cpp +++ b/es-core/src/components/ComponentGrid.cpp @@ -234,24 +234,30 @@ bool ComponentGrid::input(InputConfig* config, Input input) if(!input.value) return false; + bool result = false; + if(config->isMappedLike("down", input)) { - return moveCursor(Vector2i(0, 1)); + result = moveCursor(Vector2i(0, 1)); } if(config->isMappedLike("up", input)) { - return moveCursor(Vector2i(0, -1)); + result = moveCursor(Vector2i(0, -1)); } if(config->isMappedLike("left", input)) { - return moveCursor(Vector2i(-1, 0)); + result = moveCursor(Vector2i(-1, 0)); } if(config->isMappedLike("right", input)) { - return moveCursor(Vector2i(1, 0)); + result = moveCursor(Vector2i(1, 0)); } - return false; + if (!result && mUnhandledInputCallback) { + return mUnhandledInputCallback(config, input); + } + + return result; } void ComponentGrid::resetCursor() diff --git a/es-core/src/components/ComponentGrid.h b/es-core/src/components/ComponentGrid.h index 15fe8e76df..3ff8ec09e3 100644 --- a/es-core/src/components/ComponentGrid.h +++ b/es-core/src/components/ComponentGrid.h @@ -71,6 +71,8 @@ class ComponentGrid : public GuiComponent virtual std::vector getHelpPrompts() override; + inline void setUnhandledInputCallback(const std::function& func) { mUnhandledInputCallback = func; } + private: class GridEntry { @@ -122,6 +124,8 @@ class ComponentGrid : public GuiComponent void onCursorMoved(Vector2i from, Vector2i to); Vector2i mCursor; + + std::function mUnhandledInputCallback; }; #endif // ES_CORE_COMPONENTS_COMPONENT_GRID_H diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index de13836dac..9229227416 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -20,7 +20,7 @@ #define VIDEODELAY 100 -GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window), mLabel(window), mVideo(nullptr), mVideoPlaying(false) +GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window), mLabel(window), mVideo(nullptr), mVideoPlaying(false), mShown(false) { mSelectedZoomPercent = 1.0f; mAnimPosition = Vector3f(0, 0); @@ -53,6 +53,7 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mImage = std::make_shared(mWindow); mImage->setOrigin(0.5f, 0.5f); + mImage->setAllowFading(false); addChild(&mBackground); addChild(&(*mImage)); @@ -124,24 +125,6 @@ void GridTileComponent::resize() float bottomPadding = std::max(topPadding, height); float paddingX = currentProperties.mPadding.x(); - if (mSelectedZoomPercent != 1.0f) - { - if (mAnimPosition.x() != 0 && mAnimPosition.y() != 0 && mSelected) - { - int x = mAnimPosition.x() - mPosition.x(); - int y = mAnimPosition.y() - mPosition.y(); - - x = x * (1.0 - mSelectedZoomPercent); - y = y * (1.0 - mSelectedZoomPercent); - - mBackground.setPosition(x, y); - } - else - mBackground.setPosition(0, 0); - } - else - mBackground.setPosition(0, 0); - float imageWidth = size.x() - paddingX * 2.0; float imageHeight = size.y() - topPadding - bottomPadding; @@ -149,7 +132,8 @@ void GridTileComponent::resize() { mImage->setOrigin(0.5f, 0.5f); mImage->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); - + mImage->setColorShift(currentProperties.mImageColor); + if (currentProperties.mImageSizeMode == "minSize") mImage->setMinSize(imageWidth, imageHeight); else if (currentProperties.mImageSizeMode == "size") @@ -207,12 +191,45 @@ void GridTileComponent::resize() mVideo->setMaxSize(imageWidth, size.y() - topPadding - bottomPadding); } - if (!mLabelMerged && currentProperties.mImageSizeMode == "minSize") - mBackground.setSize(size.x(), size.y() - bottomPadding + topPadding); - else - mBackground.setSize(size); + Vector3f bkposition = Vector3f(0, 0); + Vector2f bkSize = size; + + +// if (!mLabelMerged && currentProperties.mImageSizeMode == "minSize") +// bkSize = Vector2f(size.x(), size.y() - bottomPadding + topPadding); + + if (mImage != NULL && currentProperties.mSelectionMode == "image" && mImage->getSize() != Vector2f(0,0)) + { + if (currentProperties.mImageSizeMode == "minSize") + bkSize = Vector2f(size.x(), size.y() - bottomPadding + topPadding); + else + { + bkposition = Vector3f( + mImage->getPosition().x() - mImage->getSize().x() / 2 - currentProperties.mPadding.x(), + mImage->getPosition().y() - mImage->getSize().y() / 2 - currentProperties.mPadding.y(), 0); + + bkSize = Vector2f(mImage->getSize().x() + 2 * currentProperties.mPadding.x(), mImage->getSize().y() + 2 * currentProperties.mPadding.y()); + } + } + + if (mSelectedZoomPercent != 1.0f && mAnimPosition.x() != 0 && mAnimPosition.y() != 0 && mSelected) + { + float x = mPosition.x() + bkposition.x(); + float y = mPosition.y() + bkposition.y(); + + x = mAnimPosition.x() * (1.0 - mSelectedZoomPercent) + x * mSelectedZoomPercent; + y = mAnimPosition.y() * (1.0 - mSelectedZoomPercent) + y * mSelectedZoomPercent; + + bkposition = Vector3f(x - mPosition.x(), y - mPosition.y(), 0); + } + + mBackground.setPosition(bkposition); + mBackground.setSize(bkSize); mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); + mBackground.setCenterColor(currentProperties.mBackgroundCenterColor); + mBackground.setEdgeColor(currentProperties.mBackgroundEdgeColor); + mBackground.setImagePath(currentProperties.mBackgroundImage); } void GridTileComponent::renderBackground(const Transform4x4f& parentTrans) @@ -221,19 +238,7 @@ void GridTileComponent::renderBackground(const Transform4x4f& parentTrans) return; Transform4x4f trans = getTransform() * parentTrans; - - Vector2f clipPos(trans.translation().x(), trans.translation().y()); - if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) - return; - - if (mBackground.getCornerSize().x() == 0) - { - Renderer::setMatrix(trans); - Renderer::drawRect(mBackground.getPosition().x(), mBackground.getPosition().y(), mBackground.getSize().x() - 1, mBackground.getSize().y() - 1, mBackground.getCenterColor()); - Renderer::setMatrix(parentTrans); - } - else - mBackground.render(trans); + mBackground.render(trans); } void GridTileComponent::renderContent(const Transform4x4f& parentTrans) @@ -285,24 +290,6 @@ void GridTileComponent::render(const Transform4x4f& parentTrans) renderContent(parentTrans); } -// Update all the tile properties to the new status (selected or default) -void GridTileComponent::update(int deltaTime) -{ - GuiComponent::update(deltaTime); - - const GridTileProperties& currentProperties = getCurrentProperties(); - - mBackground.setImagePath(currentProperties.mBackgroundImage); - - if (mImage != NULL) - mImage->setColorShift(currentProperties.mImageColor); - - mBackground.setCenterColor(currentProperties.mBackgroundCenterColor); - mBackground.setEdgeColor(currentProperties.mBackgroundEdgeColor); - - resize(); -} - void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); @@ -339,7 +326,16 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mDefaultProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); if (elem && elem->has("imageSizeMode")) + { mDefaultProperties.mImageSizeMode = elem->get("imageSizeMode"); + mSelectedProperties.mImageSizeMode = mDefaultProperties.mImageSizeMode; + } + + if (elem && elem->has("selectionMode")) + { + mDefaultProperties.mSelectionMode = elem->get("selectionMode"); + mSelectedProperties.mSelectionMode = mDefaultProperties.mSelectionMode; + } } // Apply theme to the selected gridtile @@ -379,10 +375,6 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem && elem->has("backgroundEdgeColor")) mSelectedProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); - mSelectedProperties.mImageSizeMode = elem && elem->has("imageSizeMode") ? - elem->get("imageSizeMode") : - mDefaultProperties.mImageSizeMode; - elem = theme->getElement(view, "gridtile", "text"); if (elem != NULL) { @@ -450,7 +442,12 @@ void GridTileComponent::setImage(const std::string& path) return; mCurrentPath = path; - mImage->setImage(path, false, mSize); + + if (mSelectedProperties.mSize.x() > mSize.x()) + mImage->setImage(path, false, mSelectedProperties.mSize); + else + mImage->setImage(path, false, mSize); + resize(); } @@ -485,19 +482,27 @@ void GridTileComponent::setVideo(const std::string& path, float defaultDelay) if (mVideoPath.empty()) mVideo->setVideo(""); } + + resize(); } -void GridTileComponent::setImage(const std::shared_ptr& texture, std::string name) +void GridTileComponent::onShow() { - mImage->setImage(texture); - mLabel.setText(name); - - // Resize now to prevent flickering images when scrolling - resize(); + GuiComponent::onShow(); + mShown = true; +} + +void GridTileComponent::onHide() +{ + GuiComponent::onHide(); + mShown = false; } void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f* pPosition) { + if (!mShown || !GuiComponent::ALLOWANIMATIONS) + allowAnimation = false; + if (mSelected == selected) { if (mSelected && mVideo != nullptr) @@ -560,6 +565,9 @@ void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f } else { + if (mVideo != NULL) + mVideo->setVideo(""); + auto func = [this](float t) { t -= 1; // cubic ease out @@ -570,10 +578,6 @@ void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f cancelAnimation(3); setAnimation(new LambdaAnimation(func, 250), 0, [this] { this->setSelectedZoom(0); - - if (mVideo != NULL) - mVideo->setVideo(""); - }, false, 3); } } @@ -612,39 +616,42 @@ unsigned int mixColors(unsigned int first, unsigned int second, float percent) const GridTileProperties& GridTileComponent::getCurrentProperties() { - if (mSelectedZoomPercent != 1.0f) - { - auto def = mSelected ? mSelectedProperties : mDefaultProperties; + if (mSelectedZoomPercent == 0.0f || mSelectedZoomPercent == 1.0f) + return mSelected ? mSelectedProperties : mDefaultProperties; - mMixedProperties = mSelected ? mSelectedProperties : mDefaultProperties; + auto def = mSelected ? mSelectedProperties : mDefaultProperties; - if (mDefaultProperties.mSize != mSelectedProperties.mSize) - { - float x = mDefaultProperties.mSize.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mSize.x() * mSelectedZoomPercent; - float y = mDefaultProperties.mSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mSize.y() * mSelectedZoomPercent; - mMixedProperties.mSize = Vector2f(x, y); - } + mMixedProperties = mSelected ? mSelectedProperties : mDefaultProperties; - if (mDefaultProperties.mPadding != mSelectedProperties.mPadding) - { - float x = mDefaultProperties.mPadding.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.x() * mSelectedZoomPercent; - float y = mDefaultProperties.mPadding.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.y() * mSelectedZoomPercent; - mMixedProperties.mPadding = Vector2f(x, y); - } + if (mDefaultProperties.mSize != mSelectedProperties.mSize) + { + float x = mDefaultProperties.mSize.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mSize.x() * mSelectedZoomPercent; + float y = mDefaultProperties.mSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mSize.y() * mSelectedZoomPercent; + mMixedProperties.mSize = Vector2f(x, y); + } - if (mDefaultProperties.mImageColor != mSelectedProperties.mImageColor) - { - mMixedProperties.mImageColor = mixColors(mDefaultProperties.mImageColor, mSelectedProperties.mImageColor, mSelectedZoomPercent); - } + if (mDefaultProperties.mPadding != mSelectedProperties.mPadding) + { + float x = mDefaultProperties.mPadding.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.x() * mSelectedZoomPercent; + float y = mDefaultProperties.mPadding.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.y() * mSelectedZoomPercent; + mMixedProperties.mPadding = Vector2f(x, y); + } - if (mDefaultProperties.mLabelSize != mSelectedProperties.mLabelSize) - { - float y = mDefaultProperties.mLabelSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mLabelSize.y() * mSelectedZoomPercent; - mMixedProperties.mLabelSize = Vector2f(mDefaultProperties.mLabelSize.x(), y); - } + if (mDefaultProperties.mImageColor != mSelectedProperties.mImageColor) + { + mMixedProperties.mImageColor = mixColors(mDefaultProperties.mImageColor, mSelectedProperties.mImageColor, mSelectedZoomPercent); + } - return mMixedProperties; + if (mDefaultProperties.mLabelSize != mSelectedProperties.mLabelSize) + { + float y = mDefaultProperties.mLabelSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mLabelSize.y() * mSelectedZoomPercent; + mMixedProperties.mLabelSize = Vector2f(mDefaultProperties.mLabelSize.x(), y); } - return mSelected ? mSelectedProperties : mDefaultProperties; + return mMixedProperties; +} + +Vector3f GridTileComponent::getBackgroundPosition() +{ + return Vector3f(mBackground.getPosition().x() + mPosition.x(), mBackground.getPosition().y() + mPosition.y(), 0); } \ No newline at end of file diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index 1502d7ca84..0d8401b53f 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -19,6 +19,7 @@ struct GridTileProperties unsigned int mBackgroundEdgeColor; std::string mImageSizeMode; + std::string mSelectionMode; Vector2f mLabelSize; unsigned int mLabelColor; @@ -32,7 +33,6 @@ class GridTileComponent : public GuiComponent ~GridTileComponent(); void render(const Transform4x4f& parentTrans) override; - void update(int deltaTime) override; virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); @@ -48,7 +48,7 @@ class GridTileComponent : public GuiComponent void setVideo(const std::string& path, float defaultDelay = -1.0); void setImage(const std::string& path); - void setImage(const std::shared_ptr& texture, std::string name); + // void setImage(const std::shared_ptr& texture, std::string name); void setSelected(bool selected, bool allowAnimation = true, Vector3f* pPosition = NULL); void setVisible(bool visible); @@ -59,6 +59,11 @@ class GridTileComponent : public GuiComponent bool shouldSplitRendering() { return isAnimationPlaying(3); }; + Vector3f getBackgroundPosition(); + + virtual void onShow(); + virtual void onHide(); + private: void resize(); const GridTileProperties& getCurrentProperties(); @@ -89,7 +94,7 @@ class GridTileComponent : public GuiComponent VideoComponent* mVideo; bool mVideoPlaying; - + bool mShown; }; #endif // ES_CORE_COMPONENTS_GRID_TILE_COMPONENT_H diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index 4b75e0138f..050181f5b9 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -136,6 +136,15 @@ class IList : public GuiComponent onCursorChanged(CURSOR_STOPPED); } + void setCursorIndex(int index) + { + if (index >= 0 && index < (int)mEntries.size()) + { + mCursor = index; + onCursorChanged(CURSOR_STOPPED); + } + } + // returns true if successful (select is in our list), false if not bool setCursor(const UserData& obj) { diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 0df7e7e7dd..792639e4fa 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -168,7 +168,7 @@ void ImageComponent::setImage(const std::shared_ptr& texture) void ImageComponent::setResize(float width, float height) { - if (!mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) + if (mSize.x() != 0 && mSize.y() != 0 && !mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) return; mTargetSize = Vector2f(width, height); @@ -179,7 +179,7 @@ void ImageComponent::setResize(float width, float height) void ImageComponent::setMaxSize(float width, float height) { - if (mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) + if (mSize.x() != 0 && mSize.y() != 0 && mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) return; mTargetSize = Vector2f(width, height); @@ -190,7 +190,7 @@ void ImageComponent::setMaxSize(float width, float height) void ImageComponent::setMinSize(float width, float height) { - if (mTargetIsMin && !mTargetIsMax && mTargetSize.x() == width && mTargetSize.y() == height) + if (mSize.x() != 0 && mSize.y() != 0 && mTargetIsMin && !mTargetIsMax && mTargetSize.x() == width && mTargetSize.y() == height) return; mTargetSize = Vector2f(width, height); @@ -260,6 +260,9 @@ void ImageComponent::setFlipY(bool flip) void ImageComponent::setColorShift(unsigned int color) { + if (mColorShift == color) + return; + mColorShift = color; // Grab the opacity from the color shift because we may need to apply it if // fading textures in @@ -338,12 +341,13 @@ void ImageComponent::render(const Transform4x4f& parentTrans) return; Transform4x4f trans = parentTrans * getTransform(); - Renderer::setMatrix(trans); Vector2f clipPos(trans.translation().x(), trans.translation().y()); if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) return; + Renderer::setMatrix(trans); + if (mTexture && mOpacity > 0) { if (Settings::getInstance()->getBool("DebugImage")) @@ -394,6 +398,9 @@ void ImageComponent::render(const Transform4x4f& parentTrans) void ImageComponent::fadeIn(bool textureLoaded) { + if (!mAllowFading) + return; + if (!mForceLoad) { if (!textureLoaded) @@ -476,8 +483,12 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s if(properties & PATH && elem->has("path")) { - bool tile = (elem->has("tile") && elem->get("tile")); - setImage(elem->get("path"), tile, mTargetSize); + auto path = elem->get("path"); + if (Utils::FileSystem::exists(path)) + { + bool tile = (elem->has("tile") && elem->get("tile")); + setImage(path, tile, Vector2f(mTargetSize.x(), mTargetSize.y())); + } } if(properties & COLOR && elem->has("color")) diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 17ca7ac375..6233725460 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -80,6 +80,8 @@ class ImageComponent : public GuiComponent void render(const Transform4x4f& parentTrans) override; + void setAllowFading(bool fade) { mAllowFading = fade; }; + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; virtual std::vector getHelpPrompts() override; @@ -116,6 +118,8 @@ class ImageComponent : public GuiComponent bool mRotateByTargetSize; bool mVisible; + bool mAllowFading; + Vector2f mTopLeftCrop; Vector2f mBottomRightCrop; }; diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index b28fa65bc8..92450572ca 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -17,6 +17,13 @@ enum ScrollDirection SCROLL_HORIZONTALLY_CENTER, }; +enum ImageSource +{ + THUMBNAIL, + IMAGE, + MARQUEE +}; + struct ImageGridData { std::string texturePath; @@ -61,6 +68,10 @@ class ImageGridComponent : public IList virtual void onShow(); virtual void onHide(); + ImageSource getImageSource() { return mImageSource; }; + + void setGridSizeOverride(Vector2f size); + protected: virtual void onCursorChanged(const CursorState& state) override; @@ -82,10 +93,12 @@ class ImageGridComponent : public IList bool mLastRowPartial; Vector2f mAutoLayout; float mAutoLayoutZoom; - Vector2f mPadding; + Vector4f mPadding; Vector2f mMargin; Vector2f mTileSize; Vector2i mGridDimension; + Vector2f mGridSizeOverride; + std::shared_ptr mTheme; std::vector< std::shared_ptr > mTiles; @@ -100,6 +113,8 @@ class ImageGridComponent : public IList // MISCELLANEOUS ScrollDirection mScrollDirection; + ImageSource mImageSource; + std::function mCursorChangedCallback; }; @@ -111,10 +126,11 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList::ImageGridComponent(Window* window) : IList::topWindow(bool isTop) template void ImageGridComponent::onShow() { + if (mEntriesDirty) + { + updateTiles(); + mEntriesDirty = false; + } + GuiComponent::onShow(); for (int ti = 0; ti < (int)mTiles.size(); ti++) @@ -214,6 +237,12 @@ void ImageGridComponent::onShow() } } +template +void ImageGridComponent::setGridSizeOverride(Vector2f size) +{ + mGridSizeOverride = size; +} + template void ImageGridComponent::onHide() { @@ -231,6 +260,10 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = getTransform() * parentTrans; + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + float offsetX = isVertical() ? 0 : mCamera * mCameraDirection * (mTileSize.x() + mMargin.x()); float offsetY = isVertical() ? mCamera * mCameraDirection * (mTileSize.y() + mMargin.y()) : 0; @@ -324,14 +357,27 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, mMargin = elem->get("margin") * screen; if (elem->has("padding")) - mPadding = elem->get("padding") * screen; + mPadding = elem->get("padding") * Vector4f(screen.x(), screen.y(), screen.x(), screen.y()); if (elem->has("autoLayout")) - mAutoLayout = elem->get("autoLayout"); + mAutoLayout = elem->get("autoLayout"); if (elem->has("autoLayoutSelectedZoom")) mAutoLayoutZoom = elem->get("autoLayoutSelectedZoom"); + if (elem->has("imageSource")) + { + auto direction = elem->get("imageSource"); + if (direction == "image") + mImageSource = IMAGE; + else if (direction == "marquee") + mImageSource = MARQUEE; + else + mImageSource = THUMBNAIL; + } + else + mImageSource = THUMBNAIL; + if (elem->has("scrollDirection")) { auto direction = elem->get("scrollDirection"); @@ -405,7 +451,7 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, GridTileComponent::getDefaultTileSize(); // Apply size property, will trigger a call to onSizeChanged() which will build the tiles - GuiComponent::applyTheme(theme, view, element, ThemeFlags::SIZE); + GuiComponent::applyTheme(theme, view, element, ThemeFlags::SIZE | ThemeFlags::Z_INDEX); // Trigger the call manually if the theme have no "imagegrid" element if (!elem) @@ -425,43 +471,43 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) if (mLastCursor == mCursor) return; - if (true) - { - bool centerSel = (mScrollDirection == SCROLL_HORIZONTALLY_CENTER || mScrollDirection == SCROLL_VERTICALLY_CENTER); + bool centerSel = (mScrollDirection == SCROLL_HORIZONTALLY_CENTER || mScrollDirection == SCROLL_VERTICALLY_CENTER); - bool direction = mCursor >= mLastCursor; + bool direction = mCursor >= mLastCursor; - int oldStart = mStartPosition; + int oldStart = mStartPosition; - float dimScrollable = isVertical() ? mGridDimension.y() - 2 * EXTRAITEMS: mGridDimension.x() - 2 * EXTRAITEMS; - float dimOpposite = isVertical() ? mGridDimension.x() : mGridDimension.y(); + float dimScrollable = isVertical() ? mGridDimension.y() - 2 * EXTRAITEMS : mGridDimension.x() - 2 * EXTRAITEMS; + float dimOpposite = isVertical() ? mGridDimension.x() : mGridDimension.y(); - int centralCol = (int)(dimScrollable - 0.5) / 2; - int maxCentralCol = (int)(dimScrollable) / 2; - - int oldCol = (mLastCursor / dimOpposite); - int col = (mCursor / dimOpposite); + int centralCol = (int)(dimScrollable - 0.5) / 2; + int maxCentralCol = (int)(dimScrollable) / 2; - int lastCol = ((mEntries.size() - 1) / dimOpposite); - - int lastScroll = std::max(0, (int) (lastCol + 1 - dimScrollable)); - - float startPos = 0; - float endPos = 1; + int oldCol = (mLastCursor / dimOpposite); + int col = (mCursor / dimOpposite); - if (isAnimationPlaying(2)) - { - startPos = -(mCamera*2.0/3.0); - if (startPos < -1) - startPos = -1; + int lastCol = ((mEntries.size() - 1) / dimOpposite); - cancelAnimation(2); - updateTiles(direction, false); - } - + int lastScroll = std::max(0, (int)(lastCol + 1 - dimScrollable)); + + float startPos = 0; + float endPos = 1; + + if (isAnimationPlaying(2)) + { + startPos = -(mCamera*2.0 / 3.0); + if (startPos < -1) + startPos = -1; + + cancelAnimation(2); + updateTiles(direction, false); + } + + if (GuiComponent::ALLOWANIMATIONS) + { std::shared_ptr oldTile = nullptr; std::shared_ptr newTile = nullptr; - + int oldIdx = mLastCursor - mStartPosition + (dimOpposite * EXTRAITEMS); if (oldIdx >= 0 && oldIdx < mTiles.size()) oldTile = mTiles[oldIdx]; @@ -469,121 +515,73 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) int newIdx = mCursor - mStartPosition + (dimOpposite * EXTRAITEMS); if (newIdx >= 0 && newIdx < mTiles.size()) newTile = mTiles[newIdx]; - + Vector3f oldPos = Vector3f(0, 0); - + if (oldTile != nullptr) { - oldPos = oldTile->getPosition(); + oldPos = oldTile->getBackgroundPosition(); oldTile->setSelected(false); } - - if (newTile != nullptr) - newTile->setSelected(true, true, oldPos == Vector3f(0, 0) ? nullptr : &oldPos); - - int firstVisibleCol = mStartPosition / dimOpposite; - if ((col < centralCol || (col == 0 && col == centralCol)) && !centerSel) - mStartPosition = 0; - else if ((col - centralCol) > lastScroll && !centerSel) - mStartPosition = lastScroll * dimOpposite; - else if (maxCentralCol != centralCol && col == firstVisibleCol + maxCentralCol || col == firstVisibleCol + centralCol) - { - if (col == firstVisibleCol + maxCentralCol) - mStartPosition = (col - maxCentralCol) * dimOpposite; - else - mStartPosition = (col - centralCol) * dimOpposite; - } - else - { - if (oldCol == firstVisibleCol + maxCentralCol) - mStartPosition = (col - maxCentralCol) * dimOpposite; - else - mStartPosition = (col - centralCol) * dimOpposite; - } + if (newTile != nullptr) + newTile->setSelected(true, true, oldPos == Vector3f(0, 0) ? nullptr : &oldPos); + } - mLastCursor = mCursor; + int firstVisibleCol = mStartPosition / dimOpposite; - mCameraDirection = direction ? -1.0 : 1.0; - mCamera = 0; + if ((col < centralCol || (col == 0 && col == centralCol)) && !centerSel) + mStartPosition = 0; + else if ((col - centralCol) > lastScroll && !centerSel) + mStartPosition = lastScroll * dimOpposite; + else if (maxCentralCol != centralCol && col == firstVisibleCol + maxCentralCol || col == firstVisibleCol + centralCol) + { + if (col == firstVisibleCol + maxCentralCol) + mStartPosition = (col - maxCentralCol) * dimOpposite; + else + mStartPosition = (col - centralCol) * dimOpposite; + } + else + { + if (oldCol == firstVisibleCol + maxCentralCol) + mStartPosition = (col - maxCentralCol) * dimOpposite; + else + mStartPosition = (col - centralCol) * dimOpposite; + } - if (oldStart == mStartPosition) - { - updateTiles(direction, true); + mLastCursor = mCursor; - if (mCursorChangedCallback) - mCursorChangedCallback(state); + mCameraDirection = direction ? -1.0 : 1.0; + mCamera = 0; - return; - } + if (oldStart == mStartPosition || !GuiComponent::ALLOWANIMATIONS) + { + updateTiles(direction, true); if (mCursorChangedCallback) mCursorChangedCallback(state); - - auto func = [this, startPos, endPos](float t) - { - t -= 1; // cubic ease out - float pct = Math::lerp(0, 1, t*t*t + 1); - float x = startPos * (1.0 - pct) + endPos * pct; - mCamera = x; // Math::lerp(0.0f, 1.0f, t); - }; - - setAnimation(new LambdaAnimation(func, 250), 0, [this, direction] { - - mCamera = 0; - updateTiles(direction, false); - }, false, 2); - return; } - else - { - // 1 if scrolling down, -1 if scrolling up - int scrollDirection = mCursor >= mLastCursor ? 1 : -1; - int partialRow = (int)mLastRowPartial; - int maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); + if (mCursorChangedCallback) + mCursorChangedCallback(state); - if (!isVertical()) - maxPos = mStartPosition + ((mGridDimension.x()-2) * (mGridDimension.y() - partialRow)); + auto func = [this, startPos, endPos](float t) + { + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + t = startPos * (1.0 - pct) + endPos * pct; - if (mCursor == 0) - mStartPosition = 0; - else if (mCursor < mStartPosition) - mStartPosition = std::max(0, mStartPosition - mGridDimension.x()); - else if (mCursor >= maxPos) - { - if (!isVertical()) - { - mStartPosition = mStartPosition + mGridDimension.y(); - maxPos = mStartPosition + ((mGridDimension.x()-2) * (mGridDimension.y() - partialRow)); - } - else - { - mStartPosition = mStartPosition + mGridDimension.x(); - maxPos = mStartPosition + (mGridDimension.x() * (mGridDimension.y() - partialRow)); - } - - if (mCursor >= maxPos) - { - int cursorRow = mCursor / mGridDimension.x(); - mStartPosition = cursorRow * mGridDimension.x(); - if (mStartPosition >= mGridDimension.x() && cursorRow > 0) - { - cursorRow--; - mStartPosition = cursorRow * mGridDimension.x(); - } - } - } - } + mCamera = t; + }; - updateTiles(mCursor >= mLastCursor); + setAnimation(new LambdaAnimation(func, 250), 0, [this, direction] { - if (mCursorChangedCallback) - mCursorChangedCallback(state); + mCamera = 0; + updateTiles(direction, false); - mLastCursor = mCursor; + }, false, 2); } @@ -644,6 +642,7 @@ void ImageGridComponent::updateTiles(bool ascending, bool allowAnimation) } mLastCursor = mCursor; + mEntriesDirty = false; } @@ -674,9 +673,9 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA tile->setImage(mDefaultFolderTexture); else tile->setImage(mDefaultGameTexture); - - if (mAllowVideo) - { + + if (mAllowVideo && tile->isSelected()) + { std::string videoPath = mEntries.at(imgPos).data.videoPath; if (ResourceManager::getInstance()->fileExists(videoPath)) @@ -688,7 +687,7 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA } else tile->setVideo(""); - + if (imgPos == mCursor && mCursor != mLastCursor) { int dif = mCursor - tilePos; @@ -697,7 +696,7 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA if (idx < 0 || idx >= mTiles.size()) idx = 0; - tile->setSelected(true, allowAnimation, &mTiles.at(idx)->getPosition()); + tile->setSelected(true, allowAnimation, &mTiles.at(idx)->getBackgroundPosition()); } else tile->setSelected(imgPos == mCursor, allowAnimation); @@ -709,6 +708,9 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA template void ImageGridComponent::buildTiles() { + if (mGridSizeOverride.x() != 0 && mGridSizeOverride.y() != 0) + mAutoLayout = mGridSizeOverride; + mStartPosition = 0; mTiles.clear(); @@ -719,8 +721,8 @@ void ImageGridComponent::buildTiles() if (mAutoLayout.x() != 0 && mAutoLayout.y() != 0) { - auto x = (mSize.x() - (mMargin.x() * (mAutoLayout.x() - 1)) - 2 * mPadding.x()) / (int) mAutoLayout.x(); - auto y = (mSize.y() - (mMargin.y() * (mAutoLayout.y() - 1)) - 2 * mPadding.y()) / (int) mAutoLayout.y(); + auto x = (mSize.x() - (mMargin.x() * (mAutoLayout.x() - 1)) - mPadding.x() - mPadding.z()) / (int) mAutoLayout.x(); + auto y = (mSize.y() - (mMargin.y() * (mAutoLayout.y() - 1)) - mPadding.y() - mPadding.w()) / (int) mAutoLayout.y(); tileSize = Vector2f(x, y); mTileSize = tileSize; @@ -732,7 +734,7 @@ void ImageGridComponent::buildTiles() Vector2f bufferSize = Vector2f(/*vert && mGridDimension.y() == 1 ? tileDistance.x() :*/ 0, 0); Vector2f startPosition = tileSize / 2 - bufferSize; - startPosition += mPadding; + startPosition += Vector2f(mPadding.x(), mPadding.y()); int X, Y; diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 1413e2fbe2..82753fb796 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -30,6 +30,25 @@ MenuComponent::MenuComponent(Window* window, std::string title, const std::share mList = std::make_shared(mWindow); mGrid.setEntry(mList, Vector2i(0, 1), true); + mGrid.setUnhandledInputCallback([this](InputConfig* config, Input input) -> bool { + if (config->isMappedLike("down", input)) { + mGrid.setCursorTo(mList); + mList->setCursorIndex(0); + return true; + } + if (config->isMappedLike("up", input)) { + mList->setCursorIndex(mList->size() - 1); + if (mButtons.size()) { + mGrid.moveCursor(Vector2i(0, 1)); + } + else { + mGrid.setCursorTo(mList); + } + return true; + } + return false; + }); + updateGrid(); updateSize(); @@ -119,6 +138,43 @@ std::vector MenuComponent::getHelpPrompts() return mGrid.getHelpPrompts(); } +std::shared_ptr makeMultiDimButtonGrid(Window* window, const std::vector< std::vector< std::shared_ptr > >& buttons, float outerWidth) +{ + + const int sizeX = (int)buttons.at(0).size(); + const int sizeY = (int)buttons.size(); + const float buttonHeight = buttons.at(0).at(0)->getSize().y(); + const float gridHeight = (buttonHeight + BUTTON_GRID_VERT_PADDING + 2) * sizeY; + + float horizPadding = (float)BUTTON_GRID_HORIZ_PADDING; + float gridWidth, buttonWidth; + +// do { + gridWidth = outerWidth - horizPadding; // to get centered because size * (button size + BUTTON_GRID_VERT_PADDING) let a half BUTTON_GRID_VERT_PADDING left / right marge + buttonWidth = (gridWidth / sizeX) - horizPadding; + // horizPadding -= 2; +// } while ((buttonWidth < 100) && (horizPadding > 2)); + + std::shared_ptr grid = std::make_shared(window, Vector2i(sizeX, sizeY)); + + grid->setSize(gridWidth, gridHeight); + + for (int x = 0; x < sizeX; x++) + grid->setColWidthPerc(x, (float)1 / sizeX); + + for (int y = 0; y < sizeY; y++) + { + for (int x = 0; x < sizeX; x++) + { + const std::shared_ptr& button = buttons.at(y).at(x); + button->setSize(buttonWidth, buttonHeight); + grid->setEntry(button, Vector2i(x, y), true, false); + } + } + + return grid; +} + std::shared_ptr makeButtonGrid(Window* window, const std::vector< std::shared_ptr >& buttons) { std::shared_ptr buttonGrid = std::make_shared(window, Vector2i((int)buttons.size(), 2)); diff --git a/es-core/src/components/MenuComponent.h b/es-core/src/components/MenuComponent.h index d5a8db473c..aa4928702c 100644 --- a/es-core/src/components/MenuComponent.h +++ b/es-core/src/components/MenuComponent.h @@ -13,6 +13,7 @@ class ButtonComponent; class ImageComponent; std::shared_ptr makeButtonGrid(Window* window, const std::vector< std::shared_ptr >& buttons); +std::shared_ptr makeMultiDimButtonGrid(Window* window, const std::vector< std::vector< std::shared_ptr > >& buttons, float outerWidth); std::shared_ptr makeArrow(Window* window); #define TITLE_VERT_PADDING (Renderer::getScreenHeight()*0.0637f) diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index f3ff0bba01..911f85a676 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -11,6 +11,7 @@ NinePatchComponent::NinePatchComponent(Window* window, const std::string& path, mPath(path), mVertices(NULL), mColors(NULL) { + mPreviousSize = Vector2f(0, 0); if(!mPath.empty()) buildVertices(); } @@ -26,6 +27,9 @@ NinePatchComponent::~NinePatchComponent() void NinePatchComponent::updateColors() { + if (mColors == NULL) + return; + Renderer::buildGLColorArray(mColors, mEdgeColor, 6 * 9); Renderer::buildGLColorArray(&mColors[4 * 6 * 4], mCenterColor, 6); } @@ -143,6 +147,10 @@ void NinePatchComponent::render(const Transform4x4f& parentTrans) void NinePatchComponent::onSizeChanged() { + if (mPreviousSize == mSize) + return; + + mPreviousSize = mSize; buildVertices(); } @@ -153,6 +161,9 @@ const Vector2f& NinePatchComponent::getCornerSize() const void NinePatchComponent::setCornerSize(int sizeX, int sizeY) { + if (mCornerSize.x() == sizeX && mCornerSize.y() == sizeY) + return; + mCornerSize = Vector2f(sizeX, sizeY); buildVertices(); } diff --git a/es-core/src/components/NinePatchComponent.h b/es-core/src/components/NinePatchComponent.h index 446e8bbd23..b197e9adfd 100644 --- a/es-core/src/components/NinePatchComponent.h +++ b/es-core/src/components/NinePatchComponent.h @@ -59,6 +59,7 @@ class NinePatchComponent : public GuiComponent std::string mPath; Vector2f mCornerSize; + Vector2f mPreviousSize; unsigned int mEdgeColor; unsigned int mCenterColor; std::shared_ptr mTexture; diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index 62572d39a9..3bc1f89e56 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -7,8 +7,11 @@ SwitchComponent::SwitchComponent(Window* window, bool state) : GuiComponent(wind { auto menuTheme = ThemeData::getMenuTheme(); - mImage.setImage(ThemeData::getMenuTheme()->Icons.off); - mImage.setResize(0, Font::get(FONT_SIZE_MEDIUM)->getLetterHeight()); + float height = Font::get(FONT_SIZE_MEDIUM)->getLetterHeight(); + Vector2f maxSize = Vector2f(height, height); + + mImage.setImage(ThemeData::getMenuTheme()->Icons.off, false, maxSize); + mImage.setResize(0, height); mImage.setColorShift(menuTheme->Text.color); mSize = mImage.getSize(); diff --git a/es-core/src/components/TextEditComponent.h b/es-core/src/components/TextEditComponent.h index 74e743a2e9..bcfbf96cae 100644 --- a/es-core/src/components/TextEditComponent.h +++ b/es-core/src/components/TextEditComponent.h @@ -34,10 +34,11 @@ class TextEditComponent : public GuiComponent virtual std::vector getHelpPrompts() override; -private: void startEditing(); void stopEditing(); +private: + void onTextChanged(); void onCursorChanged(); diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 3586232b6a..067103bb6a 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -65,6 +65,9 @@ VideoComponent::VideoComponent(Window* window) : mTargetIsMax(false), mTargetSize(0, 0) { + mFadeIn = 0.0f; + mIsWaitingForVideoToStart = false; + // Setup the default configuration mConfig.showSnapshotDelay = false; mConfig.showSnapshotNoVideo = false; @@ -94,7 +97,7 @@ void VideoComponent::onOriginChanged() void VideoComponent::onSizeChanged() { - // Update the embeded static image + // Update the embeded static image mStaticImage.onSizeChanged(); } @@ -149,6 +152,8 @@ void VideoComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); GuiComponent::renderChildren(trans); + VideoComponent::renderSnapshot(parentTrans); + Renderer::setMatrix(trans); // Handle the case where the video is delayed @@ -162,10 +167,13 @@ void VideoComponent::renderSnapshot(const Transform4x4f& parentTrans) { // This is the case where the video is not currently being displayed. Work out // if we need to display a static image - if ((mConfig.showSnapshotNoVideo && mVideoPath.empty()) || (mStartDelayed && mConfig.showSnapshotDelay)) - { - // Display the static image instead - mStaticImage.setOpacity((unsigned char)(mFadeIn * 255.0f)); + if ((mConfig.showSnapshotNoVideo && mVideoPath.empty()) || ((mStartDelayed || mFadeIn < 1.0) && mConfig.showSnapshotDelay)) + { + float t = 1.0 - mFadeIn; + t -= 1; // cubic ease out + t = Math::lerp(0, 1, t*t*t + 1); + + mStaticImage.setOpacity((unsigned char)(t * 255.0f)); mStaticImage.render(parentTrans); } } @@ -244,7 +252,7 @@ void VideoComponent::handleLooping() void VideoComponent::handleStartDelay() { // Only play if any delay has timed out - if (!mStartDelayed) + if (!mStartDelayed || mIsWaitingForVideoToStart) return; // Timeout not yet completed @@ -255,16 +263,38 @@ void VideoComponent::handleStartDelay() mStartDelayed = false; // Clear the playing flag so startVideo works mIsPlaying = false; + + mIsWaitingForVideoToStart = true; + startVideo(); - mFadeIn = 0.0f; - mIsPlaying = true; + if (mIsPlaying) + mIsWaitingForVideoToStart = false; + +// mFadeIn = 0.0f; +// mIsPlaying = true; +} + +void VideoComponent::onVideoStarted() +{ + mIsWaitingForVideoToStart = false; + + if (mConfig.startDelay == 0 || PowerSaver::getMode() == PowerSaver::INSTANT) + { + mFadeIn = 1.0f; + mIsPlaying = true; + } + else + { + mFadeIn = 0.0f; + mIsPlaying = true; + } } void VideoComponent::startVideoWithDelay() { // If not playing then either start the video or initiate the delay - if (mIsPlaying || mStartDelayed) + if (mIsPlaying || mStartDelayed || mIsWaitingForVideoToStart) return; // Set the video that we are going to be playing so we don't attempt to restart it @@ -275,10 +305,13 @@ void VideoComponent::startVideoWithDelay() // No delay. Just start the video mStartDelayed = false; mIsPlaying = false; + + mIsWaitingForVideoToStart = true; + startVideo(); - mFadeIn = 0.0f; - mIsPlaying = true; + if (mIsPlaying) + mIsWaitingForVideoToStart = false; } else { @@ -326,16 +359,20 @@ void VideoComponent::update(int deltaTime) void VideoComponent::manageState() { + if (mIsWaitingForVideoToStart && mIsPlaying) + mIsWaitingForVideoToStart = false; + // We will only show if the component is on display and the screensaver // is not active bool show = mShowing && !mScreensaverActive && !mDisable; // See if we're already playing - if (mIsPlaying) + if (mIsPlaying || mIsWaitingForVideoToStart) { // If we are not on display then stop the video from playing if (!show) { + mIsWaitingForVideoToStart = false; stopVideo(); } else @@ -344,6 +381,7 @@ void VideoComponent::manageState() { // Path changed. Stop the video. We will start it again below because // mIsPlaying will be modified by stopVideo to be false + mIsWaitingForVideoToStart = false; stopVideo(); } } diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index 0a306504ad..45c236d100 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -77,10 +77,16 @@ class VideoComponent : public GuiComponent return mIsPlaying; } + bool isWaitingForVideoToStart() { + return mIsWaitingForVideoToStart; + } + bool isFading() { return mFadeIn < 1.0f; } + void onVideoStarted(); + private: // Start the video Immediately virtual void startVideo() = 0; @@ -99,6 +105,7 @@ class VideoComponent : public GuiComponent void manageState(); protected: + unsigned mVideoWidth; unsigned mVideoHeight; Vector2f mTargetSize; @@ -118,6 +125,8 @@ class VideoComponent : public GuiComponent bool mScreensaverMode; bool mTargetIsMax; + bool mIsWaitingForVideoToStart; + Configuration mConfig; }; diff --git a/es-core/src/components/VideoPlayerComponent.cpp b/es-core/src/components/VideoPlayerComponent.cpp index eeaba91775..a98e8e097c 100644 --- a/es-core/src/components/VideoPlayerComponent.cpp +++ b/es-core/src/components/VideoPlayerComponent.cpp @@ -32,8 +32,8 @@ void VideoPlayerComponent::render(const Transform4x4f& parentTrans) { VideoComponent::render(parentTrans); - if (!mIsPlaying || mPlayerPid == -1) - VideoComponent::renderSnapshot(parentTrans); +// if (!mIsPlaying || mPlayerPid == -1) + // VideoComponent::renderSnapshot(parentTrans); } void VideoPlayerComponent::setResize(float width, float height) diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 54879c36f5..c15b676fce 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -31,8 +31,14 @@ static void unlock(void *data, void* /*id*/, void *const* /*p_pixels*/) { } // VLC wants to display a video frame. -static void display(void* /*data*/, void* /*id*/) { - //Data to be displayed +static void display(void* data, void* id) +{ + if (data == NULL) + return; + + struct VideoContext *c = (struct VideoContext *)data; + if (c->valid && c->component != NULL && !c->component->isPlaying() && c->component->isWaitingForVideoToStart()) + c->component->onVideoStarted(); } VideoVlcComponent::VideoVlcComponent(Window* window, std::string subtitles) : @@ -132,6 +138,7 @@ void VideoVlcComponent::resize() void VideoVlcComponent::render(const Transform4x4f& parentTrans) { VideoComponent::render(parentTrans); + float x, y; Transform4x4f trans = parentTrans * getTransform(); @@ -176,11 +183,20 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) vertices[4].tex[0] = -tex_offs_x; vertices[4].tex[1] = 1.0f + tex_offs_y; vertices[5].tex[0] = 1.0f + tex_offs_x; vertices[5].tex[1] = 1.0f + tex_offs_y; + float t = mFadeIn; + if (mFadeIn < 1.0) + { + t = 1.0 - mFadeIn; + t -= 1; // cubic ease in + t = Math::lerp(0, 1, t*t*t + 1); + t = 1.0 - t; + } + // Colours - use this to fade the video in and out for (int i = 0; i < (4 * 6); ++i) { if ((i%4) == 3) - vertices[i / 4].colour[i % 4] = mFadeIn; + vertices[i / 4].colour[i % 4] = t; else vertices[i / 4].colour[i % 4] = 1.0f; } @@ -210,9 +226,7 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) glDisable(GL_TEXTURE_2D); glDisable(GL_BLEND); - } else { - VideoComponent::renderSnapshot(parentTrans); - } + } } void VideoVlcComponent::setupContext() @@ -222,6 +236,7 @@ void VideoVlcComponent::setupContext() // Create an RGBA surface to render the video into mContext.surface = SDL_CreateRGBSurface(SDL_SWSURFACE, (int)mVideoWidth, (int)mVideoHeight, 32, 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff); mContext.mutex = SDL_CreateMutex(); + mContext.component = this; mContext.valid = true; resize(); } @@ -233,6 +248,8 @@ void VideoVlcComponent::freeContext() { SDL_FreeSurface(mContext.surface); SDL_DestroyMutex(mContext.mutex); + + mContext.component = NULL; mContext.valid = false; } } @@ -351,12 +368,10 @@ void VideoVlcComponent::startVideo() libvlc_media_player_play(mMediaPlayer); libvlc_video_set_callbacks(mMediaPlayer, lock, unlock, display, (void*)&mContext); libvlc_video_set_format(mMediaPlayer, "RGBA", (int)mVideoWidth, (int)mVideoHeight, (int)mVideoWidth * 4); - - // libvlc_media_player_set_position(mMediaPlayer, 0.15); - + // Update the playing state - mIsPlaying = true; - mFadeIn = 0.0f; + //mIsPlaying = true; + //mFadeIn = 0.0f; } } } diff --git a/es-core/src/components/VideoVlcComponent.h b/es-core/src/components/VideoVlcComponent.h index c350b8cb3c..4a1a27bcba 100644 --- a/es-core/src/components/VideoVlcComponent.h +++ b/es-core/src/components/VideoVlcComponent.h @@ -13,6 +13,7 @@ struct libvlc_media_player_t; struct VideoContext { SDL_Surface* surface; SDL_mutex* mutex; + VideoComponent* component; bool valid; }; diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp new file mode 100644 index 0000000000..c4a3b26f45 --- /dev/null +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp @@ -0,0 +1,266 @@ +#include "guis/GuiTextEditPopupKeyboard.h" +#include "components/MenuComponent.h" +#include "Log.h" +#include "utils/StringUtil.h" + +std::vector> kbFrench { + { UNICODE_CHARS("&"), UNICODE_CHARS("é"), UNICODE_CHARS("\""), UNICODE_CHARS("'"), UNICODE_CHARS("("), UNICODE_CHARS("#"), UNICODE_CHARS("è"), UNICODE_CHARS("!"), UNICODE_CHARS("ç"), UNICODE_CHARS("à"), UNICODE_CHARS(")"), UNICODE_CHARS("-") }, + { UNICODE_CHARS("1"), UNICODE_CHARS("2"), UNICODE_CHARS("3"), UNICODE_CHARS("4"), UNICODE_CHARS("5"), UNICODE_CHARS("6"), UNICODE_CHARS("7"), UNICODE_CHARS("8"), UNICODE_CHARS("9"), UNICODE_CHARS("0"), UNICODE_CHARS("@"), UNICODE_CHARS("_") }, + /* + { UNICODE_CHARS("à"), UNICODE_CHARS("ä"), UNICODE_CHARS("ë"), UNICODE_CHARS("ì"), UNICODE_CHARS("ï"), UNICODE_CHARS("ò"), UNICODE_CHARS("ö"), UNICODE_CHARS("ü"), UNICODE_CHARS("\\"), UNICODE_CHARS("|"), UNICODE_CHARS("§"), UNICODE_CHARS("°") }, + { UNICODE_CHARS("á"), UNICODE_CHARS("â"), UNICODE_CHARS("ê"), UNICODE_CHARS("í"), UNICODE_CHARS("î"), UNICODE_CHARS("ó"), UNICODE_CHARS("ô"), UNICODE_CHARS("ú", "û"), UNICODE_CHARS("ñ"), UNICODE_CHARS("¡"), UNICODE_CHARS("¿") }, + */ + { UNICODE_CHARS("a"), UNICODE_CHARS("z"), UNICODE_CHARS("e"), UNICODE_CHARS("r"), UNICODE_CHARS("t"), UNICODE_CHARS("y"), UNICODE_CHARS("u"), UNICODE_CHARS("i"), UNICODE_CHARS("o"), UNICODE_CHARS("p"), UNICODE_CHARS("^"), UNICODE_CHARS("$") }, + { UNICODE_CHARS("A"), UNICODE_CHARS("Z"), UNICODE_CHARS("E"), UNICODE_CHARS("R"), UNICODE_CHARS("T"), UNICODE_CHARS("Y"), UNICODE_CHARS("U"), UNICODE_CHARS("I"), UNICODE_CHARS("O"), UNICODE_CHARS("P"), UNICODE_CHARS("¨"), UNICODE_CHARS("*") }, + + { UNICODE_CHARS("q"), UNICODE_CHARS("s"), UNICODE_CHARS("d"), UNICODE_CHARS("f"), UNICODE_CHARS("g"), UNICODE_CHARS("h"), UNICODE_CHARS("j"), UNICODE_CHARS("k"), UNICODE_CHARS("l"), UNICODE_CHARS("m"), UNICODE_CHARS("ù"), UNICODE_CHARS("`") }, + { UNICODE_CHARS("Q"), UNICODE_CHARS("S"), UNICODE_CHARS("D"), UNICODE_CHARS("F"), UNICODE_CHARS("G"), UNICODE_CHARS("H"), UNICODE_CHARS("J"), UNICODE_CHARS("K"), UNICODE_CHARS("L"), UNICODE_CHARS("M"), UNICODE_CHARS("%"), UNICODE_CHARS("£") }, + + //SHIFT key at position 0 + { UNICODE_CHARS("SHIFT"), UNICODE_CHARS("<"), UNICODE_CHARS("w"), UNICODE_CHARS("x"), UNICODE_CHARS("c"), UNICODE_CHARS("v"), UNICODE_CHARS("b"), UNICODE_CHARS("n"), UNICODE_CHARS(","), UNICODE_CHARS(";"), UNICODE_CHARS(":"), UNICODE_CHARS("=") }, + { UNICODE_CHARS("SHIFT"), UNICODE_CHARS(">"), UNICODE_CHARS("W"), UNICODE_CHARS("X"), UNICODE_CHARS("C"), UNICODE_CHARS("V"), UNICODE_CHARS("B"), UNICODE_CHARS("N"), UNICODE_CHARS("?"), UNICODE_CHARS("."), UNICODE_CHARS("/"), UNICODE_CHARS("+") } +}; + +std::vector> kbUs { + + { UNICODE_CHARS("1"), UNICODE_CHARS("2"), UNICODE_CHARS("3"), UNICODE_CHARS("4"), UNICODE_CHARS("5"), UNICODE_CHARS("6"), UNICODE_CHARS("7"), UNICODE_CHARS("8"), UNICODE_CHARS("9"), UNICODE_CHARS("0"), UNICODE_CHARS("_"), UNICODE_CHARS("+") }, + { UNICODE_CHARS("!"), UNICODE_CHARS("@"), UNICODE_CHARS("#"), UNICODE_CHARS("$"), UNICODE_CHARS("%"), UNICODE_CHARS("^"), UNICODE_CHARS("&"), UNICODE_CHARS("*"), UNICODE_CHARS("("), UNICODE_CHARS(")"), UNICODE_CHARS("-"), UNICODE_CHARS("=") }, +/* + { UNICODE_CHARS("à"), UNICODE_CHARS("ä"), UNICODE_CHARS("è"), UNICODE_CHARS("ë"), UNICODE_CHARS("ì"), UNICODE_CHARS("ï"), UNICODE_CHARS("ò"), UNICODE_CHARS("ö"), UNICODE_CHARS("ù"), UNICODE_CHARS("ü"), UNICODE_CHARS("¨"), UNICODE_CHARS("¿") }, + { UNICODE_CHARS("á"), UNICODE_CHARS("â"), UNICODE_CHARS("é"), UNICODE_CHARS("ê"), UNICODE_CHARS("í"), UNICODE_CHARS("î"), UNICODE_CHARS("ó"), UNICODE_CHARS("ô"), UNICODE_CHARS("ú"), UNICODE_CHARS("û"), UNICODE_CHARS("ñ"), UNICODE_CHARS("¡") }, + */ + { UNICODE_CHARS("q"), UNICODE_CHARS("w"), UNICODE_CHARS("e"), UNICODE_CHARS("r"), UNICODE_CHARS("t"), UNICODE_CHARS("y"), UNICODE_CHARS("u"), UNICODE_CHARS("i"), UNICODE_CHARS("o"), UNICODE_CHARS("p"), UNICODE_CHARS("{"), UNICODE_CHARS("}") }, + { UNICODE_CHARS("Q"), UNICODE_CHARS("W"), UNICODE_CHARS("E"), UNICODE_CHARS("R"), UNICODE_CHARS("T"), UNICODE_CHARS("Y"), UNICODE_CHARS("U"), UNICODE_CHARS("I"), UNICODE_CHARS("O"), UNICODE_CHARS("P"), UNICODE_CHARS("["), UNICODE_CHARS("]") }, + + { UNICODE_CHARS("a"), UNICODE_CHARS("s"), UNICODE_CHARS("d"), UNICODE_CHARS("f"), UNICODE_CHARS("g"), UNICODE_CHARS("h"), UNICODE_CHARS("j"), UNICODE_CHARS("k"), UNICODE_CHARS("l"), UNICODE_CHARS(";"), UNICODE_CHARS("\""), UNICODE_CHARS("|") }, + { UNICODE_CHARS("A"), UNICODE_CHARS("S"), UNICODE_CHARS("D"), UNICODE_CHARS("F"), UNICODE_CHARS("G"), UNICODE_CHARS("H"), UNICODE_CHARS("J"), UNICODE_CHARS("K"), UNICODE_CHARS("L"), UNICODE_CHARS(":"), UNICODE_CHARS("'"), UNICODE_CHARS("\\") }, + + { UNICODE_CHARS("SHIFT"), UNICODE_CHARS("~"), UNICODE_CHARS("z"), UNICODE_CHARS("x"), UNICODE_CHARS("c"), UNICODE_CHARS("v"), UNICODE_CHARS("b"), UNICODE_CHARS("n"), UNICODE_CHARS("m"), UNICODE_CHARS(","), UNICODE_CHARS("."), UNICODE_CHARS("?") }, + { UNICODE_CHARS("SHIFT"), UNICODE_CHARS("`"), UNICODE_CHARS("Z"), UNICODE_CHARS("X"), UNICODE_CHARS("C"), UNICODE_CHARS("V"), UNICODE_CHARS("B"), UNICODE_CHARS("N"), UNICODE_CHARS("M"), UNICODE_CHARS("<"), UNICODE_CHARS(">"), UNICODE_CHARS("/") }, +}; + +GuiTextEditPopupKeyboard::GuiTextEditPopupKeyboard(Window* window, const std::string& title, const std::string& initValue, + const std::function& okCallback, bool multiLine, const char* acceptBtnText) + : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 7)), mMultiLine(multiLine) +{ + auto theme = ThemeData::getMenuTheme(); + + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + + addChild(&mBackground); + addChild(&mGrid); + + mTitle = std::make_shared(mWindow, Utils::String::toUpper(title), ThemeData::getMenuTheme()->Title.font, ThemeData::getMenuTheme()->Title.color, ALIGN_CENTER); + + mText = std::make_shared(mWindow); + mText->setValue(initValue); + + if(!multiLine) + mText->setCursor(initValue.size()); + + // Header + mGrid.setEntry(mTitle, Vector2i(0, 0), false, true); + + // Text edit add + mGrid.setEntry(mText, Vector2i(0, 1), true, false, Vector2i(1, 1)); + + std::vector< std::vector< std::shared_ptr > > buttonList; + + // Keyboard + // Case for if multiline is enabled, then don't create the keyboard. + if (!mMultiLine) + { + std::vector> &layout = kbUs; + + if (GuiTextTool::getLanguage() == "fr") + layout = kbFrench; + + for (unsigned int i = 0; i < layout.size() / 2; i++) + { + std::vector> buttons; + for (unsigned int j = 0; j < layout[2 * i].size(); j++) + { +#ifdef WIN32 + std::wstring toConvert = layout[2 * i][j]; + std::string atj = GuiTextTool::convertFromWideString(toConvert); + + toConvert = layout[2 * i + 1][j]; + std::string atjs = GuiTextTool::convertFromWideString(toConvert); +#else + std::string atj = layout[2 * i][j]; + std::string atjs = layout[2 * i + 1][j]; + +#endif + + if (atj == "SHIFT") + { + // Special case for shift key + mShiftButton = std::make_shared(mWindow, "SHIFT", _T("SHIFTS FOR UPPER,LOWER, AND SPECIAL"), [this] { + shiftKeys(); + }); + buttons.push_back(mShiftButton); + } + else + buttons.push_back(makeButton(atj, atjs)); + } + buttonList.push_back(buttons); + } + } + + const float gridWidth = Renderer::getScreenWidth() * 0.85f; + mKeyboardGrid = makeMultiDimButtonGrid(mWindow, buttonList, gridWidth - 20); + mGrid.setEntry(mKeyboardGrid, Vector2i(0, 2), true, false); + + // Accept/Cancel buttons + buttons.push_back(std::make_shared(mWindow, _L(acceptBtnText), _L(acceptBtnText), [this, okCallback] { okCallback(mText->getValue()); delete this; })); + + buttons.push_back(std::make_shared(mWindow, _T("SPACE"), _T("SPACE"), [this] { + mText->startEditing(); + mText->textInput(" "); + mText->stopEditing(); + })); + + buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE A CHAR"), [this] { + mText->startEditing(); + mText->textInput("\b"); + mText->stopEditing(); + })); + + buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("discard changes"), [this] { delete this; })); + + mButtons = makeButtonGrid(mWindow, buttons); + mGrid.setEntry(mButtons, Vector2i(0, 3), true, false); + + // Determine size from text size + float textHeight = mText->getFont()->getHeight(); + if (multiLine) + textHeight *= 6; + mText->setSize(gridWidth - 40, textHeight); + + // If multiline, set all diminsions back to default, else draw size for keyboard. + if (mMultiLine) + { + setSize(Renderer::getScreenWidth() * 0.5f, mTitle->getFont()->getHeight() + textHeight + mKeyboardGrid->getSize().y() + 40); + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); + } + else + { + setSize(gridWidth, mTitle->getFont()->getHeight() + textHeight + 40 + mKeyboardGrid->getSize().y() + mButtons->getSize().y()); + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); + } +} + +void GuiTextEditPopupKeyboard::onSizeChanged() +{ + mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); + + mText->setSize(mSize.x() - 40, mText->getSize().y()); + + float fullHeight = mTitle->getFont()->getHeight() + mText->getSize().y() + mKeyboardGrid->getSize().y() + mButtons->getSize().y(); + + // update grid + mGrid.setRowHeightPerc(0, mTitle->getFont()->getHeight() / fullHeight); + mGrid.setRowHeightPerc(1, mText->getSize().y() / fullHeight); + mGrid.setRowHeightPerc(2, mKeyboardGrid->getSize().y() / fullHeight); + mGrid.setRowHeightPerc(3, mButtons->getSize().y() / fullHeight); + + mGrid.setSize(mSize); + + mKeyboardGrid->onSizeChanged(); + /* + mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); + + mText->setSize(mSize.x() - 40, mText->getSize().y()); + + // update grid + mGrid.setRowHeightPerc(0, mTitle->getFont()->getHeight() / mSize.y()); + mGrid.setRowHeightPerc(2, mKeyboardGrid->getSize().y() / mSize.y()); + + mGrid.setSize(mSize);*/ +} + +bool GuiTextEditPopupKeyboard::input(InputConfig* config, Input input) +{ + if (GuiComponent::input(config, input)) + return true; + + // pressing back when not text editing closes us + if (config->isMappedTo("b", input) && input.value) + { + delete this; + return true; + } + + // For deleting a chara (Left Top Button) + if (config->isMappedTo("lefttop", input) && input.value) { + mText->startEditing(); + mText->textInput("\b"); + mText->stopEditing(); + } + + // For Adding a space (Right Top Button) + if (config->isMappedTo("righttop", input) && input.value) { + mText->startEditing(); + mText->textInput(" "); + } + + // For Shifting (X) + if (config->isMappedTo("x", input) && input.value) { + if (mShift) mShift = false; + else mShift = true; + shiftKeys(); + } + + + + return false; +} + +void GuiTextEditPopupKeyboard::update(int deltatime) { + +} + +std::shared_ptr GuiTextEditPopupKeyboard::makeButton(const std::string& key, const std::string& shiftedKey) +{ + std::shared_ptr button = std::make_shared(mWindow, key, key, [this, key, shiftedKey] { + mText->startEditing(); + if (mShift) + mText->textInput(shiftedKey.c_str()); + else + mText->textInput(key.c_str()); + mText->stopEditing(); + }, false); + KeyboardButton kb(button, key, shiftedKey); + keyboardButtons.push_back(kb); + return button; +} + +// Shifts the keys when user hits the shift button. +void GuiTextEditPopupKeyboard::shiftKeys() +{ + mShift = !mShift; + + if (mShift) + mShiftButton->setColorShift(0xFF0000FF); + else + mShiftButton->removeColorShift(); + + for (auto & kb : keyboardButtons) + { + const std::string& text = mShift ? kb.shiftedKey : kb.key; + kb.button->setText(text, text, false); + } +} + +std::vector GuiTextEditPopupKeyboard::getHelpPrompts() +{ + std::vector prompts = mGrid.getHelpPrompts(); + prompts.push_back(HelpPrompt("x", _T("SHIFT"))); + prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("r", _T("SPACE"))); + prompts.push_back(HelpPrompt("l", _T("DELETE"))); + return prompts; +} + diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.h b/es-core/src/guis/GuiTextEditPopupKeyboard.h new file mode 100644 index 0000000000..62e9813e3d --- /dev/null +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.h @@ -0,0 +1,73 @@ +#include "GuiComponent.h" + +#include "components/NinePatchComponent.h" +#include "components/ButtonComponent.h" +#include "components/ComponentGrid.h" +#include "components/TextEditComponent.h" +#include "components/TextComponent.h" + +class GuiTextEditPopupKeyboard : public GuiComponent +{ +public: + GuiTextEditPopupKeyboard(Window* window, const std::string& title, const std::string& initValue, + const std::function& okCallback, bool multiLine, const char* acceptBtnText = "OK"); + + bool input(InputConfig* config, Input input); + void update(int deltatime) override; + void onSizeChanged(); + std::vector getHelpPrompts() override; + +private: + + class KeyboardButton + { + public: + std::shared_ptr button; + const std::string key; + const std::string shiftedKey; + KeyboardButton(const std::shared_ptr b, const std::string& k, const std::string& sk) : button(b), key(k), shiftedKey(sk) {}; + }; + + std::shared_ptr makeButton(const std::string& key, const std::string& shiftedKey); + + void shiftKeys(); + + NinePatchComponent mBackground; + ComponentGrid mGrid; + + // Vectors for button rows + + std::vector< std::shared_ptr > buttons; + /* + std::vector< std::shared_ptr > kButtons; + std::vector< std::shared_ptr > hButtons; + std::vector< std::shared_ptr > bButtons; + std::vector< std::shared_ptr > digitButtons; + */ + + std::vector keyboardButtons; + std::shared_ptr mShiftButton; + + std::shared_ptr mTitle; + std::shared_ptr mText; + std::shared_ptr mKeyboardGrid; + std::shared_ptr mButtonGrid; + std::shared_ptr mNewGrid; + std::shared_ptr mButtons; + + // Define keyboard key rows. + const char* numRow[10] = { "1","2","3","4","5","6","7","8","9","0" }; + const char* numRowUp[10] = { "!", "@", "#", "$", "%", "^", "&", "*", "(", ")" }; + const char* topRow[10] = { "q","w","e","r","t","y","u","i","o","p" }; + const char* topRowUp[10] = { "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P" }; // Just so I don't have to deal with toupper + const char* homeRow[10] = { "a","s","d","f","g","h","j","k","l",";" }; + const char* homeRowUp[10] = { "A", "S", "D", "F", "G", "H", "J", "K", "L", ":" }; + const char* bottomRow[9] = { "z","x","c","v","b","n","m",",","." }; // Shift is handled in the constructor + const char* bottomRowUp[9] = { "Z", "X", "C", "V", "B", "N", "M", "<", ">" }; + + int mxIndex = 0; // Stores the X index and makes every grid the same. + + bool mMultiLine; + bool mShift = false; + bool mShiftChange = false; +}; diff --git a/es-core/src/math/Vector2f.cpp b/es-core/src/math/Vector2f.cpp index f98f17194c..ddf8fc351a 100644 --- a/es-core/src/math/Vector2f.cpp +++ b/es-core/src/math/Vector2f.cpp @@ -17,3 +17,25 @@ Vector2f& Vector2f::lerp(const Vector2f& _start, const Vector2f& _end, const flo return *this; } // lerp + +const Vector2f Vector2f::parseString(const std::string& _input) +{ + Vector2f ret = Vector2f(0, 0); + + size_t divider = _input.find(' '); + if (divider != std::string::npos) + { + std::string first = _input.substr(0, divider); + std::string second = _input.substr(divider, std::string::npos); + + ret = Vector2f((float)atof(first.c_str()), (float)atof(second.c_str())); + } + + return ret; +} + + +const std::string Vector2f::toString() +{ + return std::to_string(mX) + " " + std::to_string(mY); +} \ No newline at end of file diff --git a/es-core/src/math/Vector2f.h b/es-core/src/math/Vector2f.h index b9c91fda08..93e109d799 100644 --- a/es-core/src/math/Vector2f.h +++ b/es-core/src/math/Vector2f.h @@ -2,6 +2,7 @@ #ifndef ES_CORE_MATH_VECTOR2F_H #define ES_CORE_MATH_VECTOR2F_H +#include #include "math/Misc.h" #include @@ -11,6 +12,8 @@ class Vector4f; class Vector2f { public: + static const Vector2f parseString(const std::string& _input); + const std::string toString(); Vector2f() { } Vector2f(const float _f) : mX(_f), mY(_f) { } diff --git a/es-core/src/platform.h b/es-core/src/platform.h index 398f7c12e6..48b4a169b2 100644 --- a/es-core/src/platform.h +++ b/es-core/src/platform.h @@ -18,6 +18,8 @@ #define sleep Sleep #endif + #define GL_GLEXT_PROTOTYPES + #define GLHEADER #endif diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index fe2a1b6a08..216c2e3d92 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -22,7 +22,6 @@ Font::FontFace::FontFace(ResourceData&& d, int size) : data(d) Font::FontFace::~FontFace() { - if(face) FT_Done_Face(face); } @@ -73,6 +72,7 @@ Font::Font(int size, const std::string& path) : mSize(size), mPath(path) { assert(mSize > 0); + mLoaded = true; mMaxGlyphHeight = 0; if (!sLibrary) @@ -95,17 +95,24 @@ Font::~Font() for (auto it = mGlyphMap.cbegin(); it != mGlyphMap.cend(); it++) delete it->second; - unload(ResourceManager::getInstance()); + unload(); } -void Font::reload(std::shared_ptr& /*rm*/) +void Font::reload() { + if (mLoaded) + return; + rebuildTextures(); + mLoaded = true; } -void Font::unload(std::shared_ptr& /*rm*/) +void Font::unload() { - unloadTextures(); + if (mLoaded) + unloadTextures(); + + mLoaded = false; } std::shared_ptr Font::get(int size, const std::string& path) diff --git a/es-core/src/resources/Font.h b/es-core/src/resources/Font.h index c9c81af643..2813ab1597 100644 --- a/es-core/src/resources/Font.h +++ b/es-core/src/resources/Font.h @@ -53,8 +53,8 @@ class Font : public IReloadable float getHeight(float lineSpacing = 1.5f) const; float getLetterHeight(); - void unload(std::shared_ptr& rm) override; - void reload(std::shared_ptr& rm) override; + void unload() override; + void reload() override; int getSize() const; inline const std::string& getPath() const { return mPath; } @@ -135,6 +135,9 @@ class Font : public IReloadable float getNewlineStartOffset(const std::string& text, const unsigned int& charStart, const float& xLen, const Alignment& alignment); + + bool mLoaded; + friend TextCache; }; diff --git a/es-core/src/resources/ResourceManager.cpp b/es-core/src/resources/ResourceManager.cpp index 3887a671a4..3f5035b6e7 100644 --- a/es-core/src/resources/ResourceManager.cpp +++ b/es-core/src/resources/ResourceManager.cpp @@ -98,9 +98,9 @@ void ResourceManager::unloadAll() auto iter = mReloadables.cbegin(); while(iter != mReloadables.cend()) { - if(!iter->expired()) + if (!iter->expired()) { - iter->lock()->unload(sInstance); + iter->lock()->unload(); iter++; }else{ iter = mReloadables.erase(iter); @@ -115,7 +115,7 @@ void ResourceManager::reloadAll() { if(!iter->expired()) { - iter->lock()->reload(sInstance); + iter->lock()->reload(); iter++; }else{ iter = mReloadables.erase(iter); diff --git a/es-core/src/resources/ResourceManager.h b/es-core/src/resources/ResourceManager.h index ad3f20133e..f66ed8c7fa 100644 --- a/es-core/src/resources/ResourceManager.h +++ b/es-core/src/resources/ResourceManager.h @@ -20,8 +20,8 @@ class ResourceManager; class IReloadable { public: - virtual void unload(std::shared_ptr& rm) = 0; - virtual void reload(std::shared_ptr& rm) = 0; + virtual void unload() = 0; + virtual void reload() = 0; }; class ResourceManager diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 4c11b02198..76857841cb 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -13,8 +13,20 @@ #define DPI 96 +#ifdef WIN32 +void traceOutput(std::string string) +{ +#if _DEBUG + OutputDebugStringA(string.c_str()); +#endif +} +#endif + + +bool TextureData::OPTIMIZEVRAM = false; + TextureData::TextureData(bool tile) : mTile(tile), mTextureID(0), mDataRGBA(nullptr), mScalable(false), - mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f), mMaxSize(Vector2f(0,0)) + mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f), mMaxSize(Vector2f(0,0)), mPackedSize(Vector2i(0,0)), mBaseSize(Vector2i(0, 0)) { } @@ -61,8 +73,14 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length mSourceWidth = svgImage->width; mSourceHeight = svgImage->height; } - mWidth = (size_t)Math::round(mSourceWidth); - mHeight = (size_t)Math::round(mSourceHeight); + else + mSourceWidth = (mSourceHeight * svgImage->width) / svgImage->height; // FCATMP : Always keep source aspect ratio + +// mWidth = (size_t)Math::round(mSourceWidth); +// mHeight = (size_t)Math::round(mSourceHeight); + + mWidth = (int) mSourceWidth; + mHeight = (int) mSourceHeight; if (mWidth == 0) { @@ -74,14 +92,29 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length // auto scale height to keep aspect mHeight = (size_t)Math::round(((float)mWidth / svgImage->width) * svgImage->height); } + + mBaseSize = Vector2i(mWidth, mHeight); - if (mMaxSize.x() > 0 && mMaxSize.y() > 0 && (mWidth > mMaxSize.x() || mHeight > mMaxSize.y())) + if (mMaxSize.x() > 0 && mMaxSize.y() > 0 && mHeight < mMaxSize.y() && mWidth < mMaxSize.y()) // FCATMP { Vector2i sz = ImageIO::adjustPictureSize(Vector2i(mWidth, mHeight), Vector2i(mMaxSize.x(), mMaxSize.y())); - mWidth = sz.x(); + mWidth = sz.x(); mHeight = sz.y(); + mWidth = (int)((mHeight * svgImage->width) / svgImage->height); } + if (OPTIMIZEVRAM && mMaxSize.x() > 0 && mMaxSize.y() > 0 && (mWidth > mMaxSize.x() || mHeight > mMaxSize.y())) + { + Vector2i sz = ImageIO::adjustPictureSize(Vector2i(mWidth, mHeight), Vector2i(mMaxSize.x(), mMaxSize.y())); + mWidth = sz.x(); + mHeight = sz.y(); + mWidth = (mHeight * svgImage->width) / svgImage->height; + + mPackedSize = Vector2i(mWidth, mHeight); + } + else + mPackedSize = Vector2i(0, 0); + unsigned char* dataRGBA = new unsigned char[mWidth * mHeight * 4]; NSVGrasterizer* rast = nsvgCreateRasterizer(); @@ -95,6 +128,29 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length return true; } +bool TextureData::isRequiredTextureSizeOk() +{ + if (!OPTIMIZEVRAM) + return true; + + if (mPackedSize == Vector2i(0, 0)) + return true; + + if (mBaseSize == Vector2i(0, 0)) + return true; + + if (mMaxSize == Vector2f(0, 0)) + return true; + + if ((int) mMaxSize.x() <= mPackedSize.x() || (int) mMaxSize.y() <= mPackedSize.y()) + return true; + + if (mBaseSize.x() <= mPackedSize.x() || mBaseSize.y() <= mPackedSize.y()) + return true; + + return false; +} + bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t length) { size_t width, height; @@ -106,9 +162,7 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng return true; } - unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height, mMaxSize.x(), mMaxSize.y()); - -// std::vector imageRGBA = ImageIO::loadFromMemoryRGBA32((const unsigned char*)(fileData), length, width, height); + unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height, OPTIMIZEVRAM ? mMaxSize.x() : 0, mMaxSize.y(), mBaseSize, mPackedSize); if (imageRGBA == NULL) { LOG(LogError) << "Could not initialize texture from memory, invalid data! (file path: " << mPath << ", data ptr: " << (size_t)fileData << ", reported size: " << length << ")"; @@ -148,6 +202,7 @@ bool TextureData::initFromRGBAEx(unsigned char* dataRGBA, size_t width, size_t h mDataRGBA = dataRGBA; mWidth = width; mHeight = height; + return true; } @@ -158,18 +213,27 @@ bool TextureData::load() // Need to load. See if there is a file if (!mPath.empty()) { +#ifdef WIN32 + char buffer[1000]; + sprintf_s(buffer, "TextureData::load(%s, %d)\n", mPath.c_str(), mTextureID); + traceOutput(buffer); +#endif + std::shared_ptr& rm = ResourceManager::getInstance(); const ResourceData& data = rm->getFileData(mPath); // is it an SVG? if (mPath.substr(mPath.size() - 4, std::string::npos) == ".svg") { - mScalable = true; + mScalable = true; // ??? interest ? retval = initSVGFromMemory((const unsigned char*)data.ptr.get(), data.length); } else retval = initImageFromMemory((const unsigned char*)data.ptr.get(), data.length); } + + + return retval; } @@ -178,6 +242,7 @@ bool TextureData::isLoaded() std::unique_lock lock(mMutex); if (mDataRGBA || (mTextureID != 0)) return true; + return false; } @@ -199,6 +264,7 @@ bool TextureData::uploadAndBind() // Make sure we're ready to upload if ((mWidth == 0) || (mHeight == 0) || (mDataRGBA == nullptr)) return false; + glGetError(); //now for the openGL texture stuff glGenTextures(1, &mTextureID); @@ -266,8 +332,16 @@ void TextureData::setSourceSize(float width, float height) { if (mScalable) { - if ((mSourceWidth != width) || (mSourceHeight != height)) + //if ((mSourceWidth != width) || (mSourceHeight != height)) + if (mSourceHeight < height) // FCATMP { + /* +#ifdef WIN32 + char buffer[1000]; + sprintf_s(buffer, "TextureData::setSourceSize(%s, %d)\n", mPath.c_str(), mTextureID); + traceOutput(buffer); +#endif*/ + mSourceWidth = width; mSourceHeight = height; releaseVRAM(); diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 7a0215de9a..3d378c0567 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -8,6 +8,7 @@ #include #include "math/Vector2f.h" +#include "math/Vector2i.h" class TextureResource; @@ -17,6 +18,8 @@ class TextureData TextureData(bool tile); ~TextureData(); + static bool OPTIMIZEVRAM; + // These functions populate mDataRGBA but do not upload the texture to VRAM //!!!! Needs to be canonical path. Caller should check for duplicates before calling this @@ -41,7 +44,11 @@ class TextureData // Release the texture from conventional RAM void releaseRAM(); - void setMaxSize(Vector2f maxSize) { mMaxSize = maxSize; }; + void setMaxSize(Vector2f maxSize) + { + if (mMaxSize.x() < maxSize.x() || mMaxSize.y() < maxSize.y()) + mMaxSize = maxSize; + }; // Get the amount of VRAM currenty used by this texture size_t getVRAMUsage(); @@ -54,20 +61,27 @@ class TextureData bool tiled() { return mTile; } + bool isRequiredTextureSizeOk(); + + std::string mPath; + GLuint mTextureID; + private: std::mutex mMutex; bool mTile; - std::string mPath; - GLuint mTextureID; unsigned char* mDataRGBA; size_t mWidth; size_t mHeight; float mSourceWidth; - float mSourceHeight; + float mSourceHeight; bool mScalable; bool mReloadable; + Vector2i mPackedSize; + Vector2i mBaseSize; Vector2f mMaxSize; }; +void traceOutput(std::string string); + #endif // ES_CORE_RESOURCES_TEXTURE_DATA_H diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index 5755887187..7f1cb7095b 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -4,6 +4,8 @@ #include "resources/TextureResource.h" #include "Settings.h" +#include + TextureDataManager::TextureDataManager() { unsigned char data[5 * 5 * 4]; @@ -63,7 +65,8 @@ std::shared_ptr TextureDataManager::get(const TextureResource* key) mTextureLookup[key] = mTextures.cbegin(); // Make sure it's loaded or queued for loading - load(tex); + if (!tex->isLoaded()) // FCATMP + load(tex); } return tex; } @@ -104,28 +107,76 @@ void TextureDataManager::load(std::shared_ptr tex, bool block) { // See if it's already loaded if (tex->isLoaded()) - return; + { + if (tex->isRequiredTextureSizeOk()) + return; + + tex->releaseVRAM(); + tex->releaseRAM(); + + mLoader->remove(tex); + block = true; // Reload instantly or other instances will fade again + } + // Not loaded. Make sure there is room size_t size = TextureResource::getTotalMemUsage(); size_t max_texture = (size_t)Settings::getInstance()->getInt("MaxVRAM") * 1024 * 1024; + int cleanedMemory = 0; + int lastTime = SDL_GetTicks(); + for (auto it = mTextures.crbegin(); it != mTextures.crend(); ++it) { if (size < max_texture) break; - //size -= (*it)->getVRAMUsage(); + if (!(*it)->isLoaded()) // FCA added to avoid calling TextureResource::getTotalMemUsage() if texture is not loaded + continue; + (*it)->releaseVRAM(); (*it)->releaseRAM(); + // It may be already in the loader queue. In this case it wouldn't have been using // any VRAM yet but it will be. Remove it from the loader queue mLoader->remove(*it); size = TextureResource::getTotalMemUsage(); + + cleanedMemory++; + } + +#ifdef WIN32 + if (cleanedMemory > 0) + { + lastTime = SDL_GetTicks() - lastTime; + + char buffer[1000]; + sprintf_s(buffer, "cleanedMemory : %d items in %d ms\n", cleanedMemory, lastTime); + traceOutput(buffer); } +#endif + if (!block) + { + /* +#ifdef WIN32 + char buffer[1000]; + sprintf_s(buffer, "ASYNC LOAD : %s\n", tex->mPath.c_str()); + traceOutput(buffer); +#endif*/ + mLoader->load(tex); + } else + { + /* +#ifdef WIN32 + char buffer[1000]; + sprintf_s(buffer, "SYNC LOAD : %s\n", tex->mPath.c_str()); + traceOutput(buffer); +#endif*/ + mLoader->remove(tex); tex->load(); + } } TextureLoader::TextureLoader() : mExit(false) diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index 3cb0c6f2b8..b6c8feb672 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -4,7 +4,10 @@ #include "resources/TextureData.h" TextureDataManager TextureResource::sTextureDataManager; -std::map< TextureResource::TextureKeyType, std::weak_ptr > TextureResource::sTextureMap; + +std::map< TextureResource::TextureKeyType, std::weak_ptr> TextureResource::sTextureMap; +std::map< TextureResource::TextureKeyType, std::shared_ptr> TextureResource::sPermanentTextureMap; // FCAWEAK + std::set TextureResource::sAllTextures; TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic, Vector2f maxSize) : mTextureData(nullptr), mForceLoad(false) @@ -16,12 +19,13 @@ TextureResource::TextureResource(const std::string& path, bool tile, bool dynami // data manager to manage loading/unloading of this texture std::shared_ptr data; if (dynamic) - { + { data = sTextureDataManager.add(this, tile); data->setMaxSize(maxSize); data->initFromPath(path); + // Force the texture manager to load it using a blocking load - sTextureDataManager.load(data, true); + sTextureDataManager.load(data, true); // } else { @@ -50,7 +54,12 @@ TextureResource::~TextureResource() if (mTextureData == nullptr) sTextureDataManager.remove(this); - sAllTextures.erase(sAllTextures.find(this)); + if (sAllTextures.size() > 0) + { + auto pthis = sAllTextures.find(this); + if (pthis != sAllTextures.end()) + sAllTextures.erase(pthis); + } } void TextureResource::initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height) @@ -103,6 +112,12 @@ bool TextureResource::bind() } } +void TextureResource::resetCache() +{ + sPermanentTextureMap.clear(); + sTextureMap.clear(); +} + std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic, Vector2f maxSize) { std::shared_ptr& rm = ResourceManager::getInstance(); @@ -119,20 +134,53 @@ std::shared_ptr TextureResource::get(const std::string& path, b auto foundTexture = sTextureMap.find(key); if(foundTexture != sTextureMap.cend()) { - if(!foundTexture->second.expired()) - return foundTexture->second.lock(); + if (!foundTexture->second.expired()) // FCAWEAK + { + std::shared_ptr rc = foundTexture->second.lock(); + + if (maxSize != Vector2f(0, 0) && TextureData::OPTIMIZEVRAM) + { + auto dt = sTextureDataManager.get(rc.get()); + if (dt != nullptr) + dt->setMaxSize(maxSize); + } + + return rc; + } + else + sTextureMap.erase(foundTexture); } + auto permanentTexture = sPermanentTextureMap.find(key); + if (permanentTexture != sPermanentTextureMap.cend()) + { + std::shared_ptr rc = permanentTexture->second; + + if (maxSize != Vector2f(0, 0) && TextureData::OPTIMIZEVRAM) + { + auto dt = sTextureDataManager.get(rc.get()); + if (dt != nullptr) + dt->setMaxSize(maxSize); + } + + return rc; + } + // need to create it std::shared_ptr tex; tex = std::shared_ptr(new TextureResource(key.first, tile, dynamic, maxSize)); std::shared_ptr data = sTextureDataManager.get(tex.get()); // is it an SVG? - if(key.first.substr(key.first.size() - 4, std::string::npos) != ".svg") + // if(key.first.substr(key.first.size() - 4, std::string::npos) != ".svg") // FCATMP { // Probably not. Add it to our map. We don't add SVGs because 2 svgs might be rasterized at different sizes - sTextureMap[key] = std::weak_ptr(tex); + // FCA useless -> If the svg is too small, it will be reloaded bigger with setSourceSize... + + if (canonicalPath.length() > 0 && canonicalPath[0] == ':') + sPermanentTextureMap[key] = std::shared_ptr(tex); // FCAWEAK + else + sTextureMap[key] = std::shared_ptr(tex); // FCAWEAK } // Add it to the reloadable list @@ -161,8 +209,10 @@ void TextureResource::rasterizeAt(size_t width, size_t height) data = sTextureDataManager.get(this); mSourceSize = Vector2f((float)width, (float)height); data->setSourceSize((float)width, (float)height); + if (mForceLoad || (mTextureData != nullptr)) - data->load(); + if (!data->isLoaded()) // FCATMP + data->load(); } Vector2f TextureResource::getSourceImageSize() const @@ -205,7 +255,7 @@ size_t TextureResource::getTotalTextureSize() return total; } -void TextureResource::unload(std::shared_ptr& /*rm*/) +void TextureResource::unload() { // Release the texture's resources std::shared_ptr data; @@ -218,10 +268,12 @@ void TextureResource::unload(std::shared_ptr& /*rm*/) data->releaseRAM(); } -void TextureResource::reload(std::shared_ptr& /*rm*/) +void TextureResource::reload() { // For dynamically loaded textures the texture manager will load them on demand. // For manually loaded textures we have to reload them here - if (mTextureData) + if (mTextureData && !mTextureData->isLoaded()) mTextureData->load(); + else if (mTextureData == nullptr) + sTextureDataManager.get(this); } diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index a6d9aee083..4e7b29b7fa 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -34,11 +34,14 @@ class TextureResource : public IReloadable static size_t getTotalMemUsage(); // returns an approximation of total VRAM used by textures (in bytes) static size_t getTotalTextureSize(); // returns the number of bytes that would be used if all textures were in memory + static void resetCache(); + +public: + virtual void unload(); + virtual void reload(); protected: TextureResource(const std::string& path, bool tile, bool dynamic, Vector2f maxSize); - virtual void unload(std::shared_ptr& rm); - virtual void reload(std::shared_ptr& rm); private: // mTextureData is used for textures that are not loaded from a file - these ones @@ -54,6 +57,7 @@ class TextureResource : public IReloadable typedef std::pair TextureKeyType; static std::map< TextureKeyType, std::weak_ptr > sTextureMap; // map of textures, used to prevent duplicate textures + static std::map< TextureKeyType, std::shared_ptr > sPermanentTextureMap; // map of textures, used to prevent duplicate textures // FCAWEAK static std::set sAllTextures; // Set of all textures, used for memory management }; diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index c16aab4720..80c737962e 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -263,7 +263,10 @@ namespace Utils // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows char* envHome = getenv("HOME"); + // envHome = "H:/[Emulz]/EmulationStation/"; + #ifdef _DEBUG + if (Utils::FileSystem::exists("H:/[Emulz]/EmulationStation/emulationstation.exe")) envHome = "H:/[Emulz]/EmulationStation/"; #endif diff --git a/es-core/src/utils/StringUtil.cpp b/es-core/src/utils/StringUtil.cpp index 0e1da37ea8..6d78501c6a 100644 --- a/es-core/src/utils/StringUtil.cpp +++ b/es-core/src/utils/StringUtil.cpp @@ -34,6 +34,24 @@ namespace Utils return wstring; } #endif + std::vector split(const std::string& s, char seperator) + { + std::vector output; + + std::string::size_type prev_pos = 0, pos = 0; + while ((pos = s.find(seperator, pos)) != std::string::npos) + { + std::string substring(s.substr(prev_pos, pos - prev_pos)); + + output.push_back(substring); + + prev_pos = ++pos; + } + + output.push_back(s.substr(prev_pos, pos - prev_pos)); // Last word + + return output; + } unsigned int chars2Unicode(const std::string& _string, size_t& _cursor) { @@ -329,7 +347,7 @@ namespace Utils return buffer; - } // scramble + } // scramble } // String:: diff --git a/es-core/src/utils/StringUtil.h b/es-core/src/utils/StringUtil.h index 3ffb5c5eac..baef504825 100644 --- a/es-core/src/utils/StringUtil.h +++ b/es-core/src/utils/StringUtil.h @@ -27,6 +27,7 @@ namespace Utils std::string vectorToCommaString(stringVector _vector); std::string format (const char* _string, ...); std::string scramble (const std::string& _input, const std::string& key); + std::vector split (const std::string& s, char seperator); #if defined(_WIN32) const std::string convertFromWideString(const std::wstring wstring); From f1e72ef8baf26dcb0900b06a7d132a2bd4799df4 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 20 Jun 2019 17:50:32 +0200 Subject: [PATCH 039/336] Memory usage optimizations (FileData, Metadata, FileFilterIndex) --- README.md | 3 +- es-app/src/CollectionSystemManager.cpp | 125 +++--- es-app/src/CollectionSystemManager.h | 3 +- es-app/src/FileData.cpp | 385 +++++++++++------- es-app/src/FileData.h | 111 +++-- es-app/src/FileFilterIndex.cpp | 5 +- es-app/src/FileSorts.cpp | 44 +- es-app/src/FileSorts.h | 2 +- es-app/src/Gamelist.cpp | 64 +-- es-app/src/Gamelist.h | 5 +- es-app/src/MetaData.cpp | 195 ++++++--- es-app/src/MetaData.h | 44 +- es-app/src/SystemData.cpp | 71 +++- es-app/src/SystemData.h | 36 +- es-app/src/SystemScreenSaver.cpp | 4 +- es-app/src/guis/GuiFastSelect.cpp | 6 +- es-app/src/guis/GuiGamelistFilter.cpp | 3 +- es-app/src/guis/GuiGamelistOptions.cpp | 4 +- es-app/src/guis/GuiGamelistOptions.h | 2 +- es-app/src/guis/GuiMetaDataEd.cpp | 4 +- es-app/src/views/ViewController.cpp | 18 +- .../src/views/gamelist/BasicGameListView.cpp | 32 +- es-app/src/views/gamelist/BasicGameListView.h | 4 +- .../views/gamelist/DetailedGameListView.cpp | 179 +++++--- .../src/views/gamelist/DetailedGameListView.h | 10 +- .../src/views/gamelist/GridGameListView.cpp | 121 +++--- es-app/src/views/gamelist/GridGameListView.h | 7 +- es-app/src/views/gamelist/IGameListView.h | 4 +- .../views/gamelist/ISimpleGameListView.cpp | 13 +- .../src/views/gamelist/ISimpleGameListView.h | 2 +- .../src/views/gamelist/VideoGameListView.cpp | 2 +- es-app/src/views/gamelist/VideoGameListView.h | 2 +- es-core/src/Window.cpp | 24 +- es-core/src/components/ImageGridComponent.h | 3 +- es-core/src/platform.h | 17 + es-core/src/resources/TextureData.cpp | 21 +- es-core/src/resources/TextureData.h | 2 - es-core/src/resources/TextureDataManager.cpp | 20 +- es-core/src/utils/FileSystemUtil.cpp | 62 ++- es-core/src/utils/FileSystemUtil.h | 1 + 40 files changed, 1027 insertions(+), 633 deletions(-) diff --git a/README.md b/README.md index 0e2ee90026..c0899f110e 100644 --- a/README.md +++ b/README.md @@ -89,7 +89,8 @@ Changes in my branch - In previous versions, if a xml element was unknown in the theme, nothing was loaded. **Optimizations & Fixes:** -- Really faster loading time, using multithreading. +- Faster loading time, using multithreading. +- Optimized memory usage for files and gamelists. - The loading sequence displays a progress bar. - Reviewed SVG loading and size calculation mecanism. Previous versions unloaded/reloaded SVGs each time a new container needed to display it because of a size calculation problem. - Ability to disable "Preload UI" mecanism. This mecanism is used to preload the UI of gamelists of every system. Disable it adds a small lags when opening diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index fb87f18a60..295f530a08 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -99,8 +99,9 @@ void CollectionSystemManager::deinit() void CollectionSystemManager::saveCustomCollection(SystemData* sys) { - std::string name = sys->getName(); - std::unordered_map games = sys->getRootFolder()->getChildrenByFilename(); + std::string name = sys->getName(); + auto games = sys->getRootFolder()->getChildren(); + bool found = mCustomCollectionSystemsData.find(name) != mCustomCollectionSystemsData.cend(); if (found) { CollectionSystemData sysData = mCustomCollectionSystemsData.at(name); @@ -108,9 +109,9 @@ void CollectionSystemManager::saveCustomCollection(SystemData* sys) { std::ofstream configFile; configFile.open(getCustomCollectionConfigPath(name)); - for(std::unordered_map::const_iterator iter = games.cbegin(); iter != games.cend(); ++iter) + for(auto iter = games.cbegin(); iter != games.cend(); ++iter) { - std::string path = iter->first; + std::string path = (*iter)->getKey(); configFile << path << std::endl; } configFile.close(); @@ -244,18 +245,18 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS // collection files use the full path as key, to avoid clashes std::string key = file->getFullPath(); - SystemData* curSys = sysData.system; - const std::unordered_map& children = curSys->getRootFolder()->getChildrenByFilename(); - bool found = children.find(key) != children.cend(); - FileData* rootFolder = curSys->getRootFolder(); - FileFilterIndex* fileIndex = curSys->getIndex(); + SystemData* curSys = sysData.system; + FileData* collectionEntry = curSys->getRootFolder()->FindByPath(key); + + FolderData* rootFolder = curSys->getRootFolder(); + std::string name = curSys->getName(); - if (found) { + if (collectionEntry != nullptr) + { // if we found it, we need to update it - FileData* collectionEntry = children.at(key); // remove from index, so we can re-index metadata after refreshing - fileIndex->removeFromIndex(collectionEntry); + curSys->removeFromIndex(collectionEntry); collectionEntry->refreshMetadata(); // found and we are removing if (name == "favorites" && file->metadata.get("favorite") == "false") { @@ -268,7 +269,7 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS else { // re-index with new metadata - fileIndex->addToIndex(collectionEntry); + curSys->addToIndex(collectionEntry); ViewController::get()->onFileChanged(collectionEntry, FILE_METADATA_CHANGED); } } @@ -279,7 +280,7 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS name == "favorites" && file->metadata.get("favorite") == "true") { CollectionFileData* newGame = new CollectionFileData(file, curSys); rootFolder->addChild(newGame); - fileIndex->addToIndex(newGame); + curSys->addToIndex(newGame); ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); ViewController::get()->getGameListView(curSys)->onFileChanged(newGame, FILE_METADATA_CHANGED); @@ -296,7 +297,7 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS } } -void CollectionSystemManager::trimCollectionCount(FileData* rootFolder, int limit) +void CollectionSystemManager::trimCollectionCount(FolderData* rootFolder, int limit) { SystemData* curSys = rootFolder->getSystem(); while ((int)rootFolder->getChildrenListToDisplay().size() > limit) @@ -320,12 +321,10 @@ void CollectionSystemManager::deleteCollectionFiles(FileData* file) { if (sysDataIt->second.isPopulated) { - const std::unordered_map& children = (sysDataIt->second.system)->getRootFolder()->getChildrenByFilename(); - - bool found = children.find(key) != children.cend(); - if (found) { - sysDataIt->second.needsSave = true; - FileData* collectionEntry = children.at(key); + FileData* collectionEntry = (sysDataIt->second.system)->getRootFolder()->FindByPath(key); + if (collectionEntry != nullptr) + { + sysDataIt->second.needsSave = true; SystemData* systemViewToUpdate = getSystemToView(sysDataIt->second.system); ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(collectionEntry, false); } @@ -482,25 +481,23 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) populateCustomCollection(mEditingCollectionSystemData); } std::string key = file->getFullPath(); - FileData* rootFolder = sysData->getRootFolder(); - const std::unordered_map& children = rootFolder->getChildrenByFilename(); - bool found = children.find(key) != children.cend(); - FileFilterIndex* fileIndex = sysData->getIndex(); + FolderData* rootFolder = sysData->getRootFolder(); + + FileData* collectionEntry = rootFolder->FindByPath(key); + std::string name = sysData->getName(); SystemData* systemViewToUpdate = getSystemToView(sysData); - if (found) { + if (collectionEntry != nullptr) { adding = false; - // if we found it, we need to remove it - FileData* collectionEntry = children.at(key); + // if we found it, we need to remove it // remove from index - fileIndex->removeFromIndex(collectionEntry); + sysData->removeFromIndex(collectionEntry); // remove from bundle index as well, if needed - if(systemViewToUpdate != sysData) - { - systemViewToUpdate->getIndex()->removeFromIndex(collectionEntry); - } + if (systemViewToUpdate != sysData) + systemViewToUpdate->removeFromIndex(collectionEntry); + ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(collectionEntry, false); } else @@ -508,14 +505,14 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) // we didn't find it here, we should add it CollectionFileData* newGame = new CollectionFileData(file, sysData); rootFolder->addChild(newGame); - fileIndex->addToIndex(newGame); + sysData->addToIndex(newGame); ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(newGame, FILE_METADATA_CHANGED); rootFolder->sort(getSortTypeFromString(mEditingCollectionSystemData->decl.defaultSort)); ViewController::get()->onFileChanged(systemViewToUpdate->getRootFolder(), FILE_SORTED); // add to bundle index as well, if needed if(systemViewToUpdate != sysData) { - systemViewToUpdate->getIndex()->addToIndex(newGame); + systemViewToUpdate->addToIndex(newGame); } } updateCollectionFolderMetadata(sysData); @@ -523,7 +520,7 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) else { SystemData* sysData = file->getSourceFileData()->getSystem(); - sysData->getIndex()->removeFromIndex(file); + sysData->removeFromIndex(file); MetaDataList* md = &file->getSourceFileData()->metadata; @@ -535,7 +532,7 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) adding = false; md->set("favorite", "false"); } - sysData->getIndex()->addToIndex(file); + sysData->addToIndex(file); refreshCollectionSystems(file->getSourceFileData()); @@ -565,12 +562,10 @@ SystemData* CollectionSystemManager::getSystemToView(SystemData* sys) SystemData* systemToView = sys; FileData* rootFolder = sys->getRootFolder(); - FileData* bundleRootFolder = mCustomCollectionsBundle->getRootFolder(); - const std::unordered_map& bundleChildren = bundleRootFolder->getChildrenByFilename(); + FolderData* bundleRootFolder = mCustomCollectionsBundle->getRootFolder(); // is the rootFolder bundled in the "My Collections" system? - bool sysFoundInBundle = bundleChildren.find(rootFolder->getKey()) != bundleChildren.cend(); - + bool sysFoundInBundle = bundleRootFolder->FindByPath(rootFolder->getKey()) != nullptr; if (sysFoundInBundle && sys->isCollection()) { systemToView = mCustomCollectionsBundle; @@ -596,7 +591,7 @@ void CollectionSystemManager::initAutoCollectionSystems() // automatically generate metadata for a folder void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) { - FileData* rootFolder = sys->getRootFolder(); + FolderData* rootFolder = sys->getRootFolder(); std::string desc = "This collection is empty."; std::string rating = "0"; @@ -608,16 +603,16 @@ void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) std::string thumbnail = ""; std::string image = ""; - std::unordered_map games = rootFolder->getChildrenByFilename(); + auto games = rootFolder->getChildren(); if(games.size() > 0) { std::string games_list = ""; int games_counter = 0; - for(std::unordered_map::const_iterator iter = games.cbegin(); iter != games.cend(); ++iter) + for(auto iter = games.cbegin(); iter != games.cend(); ++iter) { games_counter++; - FileData* file = iter->second; + FileData* file = *iter; std::string new_rating = file->metadata.get("rating"); std::string new_releasedate = file->metadata.get("releasedate"); @@ -647,10 +642,12 @@ void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) desc = "This collection contains " + std::to_string(games_counter) + " games, including " + games_list; FileData* randomGame = sys->getRandomGame(); - - video = randomGame->getVideoPath(); - thumbnail = randomGame->getThumbnailPath(); - image = randomGame->getImagePath(); + if (randomGame != nullptr) + { + video = randomGame->getVideoPath(); + thumbnail = randomGame->getThumbnailPath(); + image = randomGame->getImagePath(); + } } @@ -725,8 +722,8 @@ void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysDa { SystemData* newSys = sysData->system; CollectionSystemDecl sysDecl = sysData->decl; - FileData* rootFolder = newSys->getRootFolder(); - FileFilterIndex* index = newSys->getIndex(); + FolderData* rootFolder = newSys->getRootFolder(); + for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) { // we won't iterate all collections @@ -749,7 +746,7 @@ void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysDa if (include) { CollectionFileData* newGame = new CollectionFileData(*gameIt, newSys); rootFolder->addChild(newGame); - index->addToIndex(newGame); + newSys->addToIndex(newGame); } } } @@ -775,24 +772,22 @@ void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sys } LOG(LogInfo) << "Loading custom collection config file at " << path; - FileData* rootFolder = newSys->getRootFolder(); - FileFilterIndex* index = newSys->getIndex(); - + FolderData* rootFolder = newSys->getRootFolder(); + // get Configuration for this Custom System std::ifstream input(path); - // get all files map - std::unordered_map allFilesMap = getAllGamesCollection()->getRootFolder()->getChildrenByFilename(); + FolderData* folder = getAllGamesCollection()->getRootFolder(); // iterate list of files in config file - for(std::string gameKey; getline(input, gameKey); ) { - std::unordered_map::const_iterator it = allFilesMap.find(gameKey); - if (it != allFilesMap.cend()) { - CollectionFileData* newGame = new CollectionFileData(it->second, newSys); + FileData* file = folder->FindByPath(gameKey); + if (file != nullptr) + { + CollectionFileData* newGame = new CollectionFileData(file, newSys); rootFolder->addChild(newGame); - index->addToIndex(newGame); + newSys->addToIndex(newGame); } else { @@ -821,14 +816,14 @@ void CollectionSystemManager::removeCollectionsFromDisplayedSystems() // remove all custom collections in bundle // this should not delete the objects from memory! - FileData* customRoot = mCustomCollectionsBundle->getRootFolder(); + FolderData* customRoot = mCustomCollectionsBundle->getRootFolder(); std::vector mChildren = customRoot->getChildren(); for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) { customRoot->removeChild(*it); } // clear index - mCustomCollectionsBundle->getIndex()->resetIndex(); + mCustomCollectionsBundle->resetIndex(); // remove view so it's re-created as needed ViewController::get()->removeGameListView(mCustomCollectionsBundle); } @@ -863,7 +858,7 @@ void CollectionSystemManager::addEnabledCollectionsToDisplayedSystems(std::mapsecond.system->getRootFolder(); mCustomCollectionsBundle->getRootFolder()->addChild(newSysRootFolder); - mCustomCollectionsBundle->getIndex()->importIndex(it->second.system->getIndex()); + mCustomCollectionsBundle->getIndex(true)->importIndex(it->second.system->getIndex(true)); } } } diff --git a/es-app/src/CollectionSystemManager.h b/es-app/src/CollectionSystemManager.h index 50a113c366..a973c0db39 100644 --- a/es-app/src/CollectionSystemManager.h +++ b/es-app/src/CollectionSystemManager.h @@ -7,6 +7,7 @@ #include class FileData; +class FolderData; class SystemData; class Window; struct SystemEnvironmentData; @@ -103,7 +104,7 @@ class CollectionSystemManager std::vector getCollectionThemeFolders(bool custom); std::vector getUserCollectionThemeFolders(); - void trimCollectionCount(FileData* rootFolder, int limit); + void trimCollectionCount(FolderData* rootFolder, int limit); bool themeFolderExists(std::string folder); diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index a5485bb65a..90f8087a73 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -14,16 +14,37 @@ #include "SystemData.h" #include "VolumeControl.h" #include "Window.h" +#include "views/UIModeController.h" #include -FileData::FileData(FileType type, const std::string& path, SystemEnvironmentData* envData, SystemData* system) - : mType(type), mPath(path), mSystem(system), mEnvData(envData), mSourceFileData(NULL), mParent(NULL), metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor! +FileData::FileData(FileType type, const std::string& path, SystemData* system) + : mType(type), mSystem(system), mParent(NULL), metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor! { + mPath = Utils::FileSystem::createRelativePath(path, getSystemEnvData()->mStartPath, false); + + TRACE("FileData : " << mPath); + // metadata needs at least a name field (since that's what getName() will return) if (metadata.get("name").empty()) - metadata.set("name", getDisplayName()); + metadata.set("name", getDisplayName()); +} + +const std::string FileData::getPath() const +{ + if (mPath.empty()) + return getSystemEnvData()->mStartPath; + + return Utils::FileSystem::resolveRelativePath(mPath, getSystemEnvData()->mStartPath, true); +} + +inline SystemEnvironmentData* FileData::getSystemEnvData() const +{ + return mSystem->getSystemEnvData(); +} - mSystemName = system->getName(); +std::string FileData::getSystemName() const +{ + return mSystem->getName(); } FileData::~FileData() @@ -32,14 +53,12 @@ FileData::~FileData() mParent->removeChild(this); if(mType == GAME) - mSystem->getIndex()->removeFromIndex(this); - - mChildren.clear(); + mSystem->removeFromIndex(this); } std::string FileData::getDisplayName() const { - std::string stem = Utils::FileSystem::getStem(mPath); + std::string stem = Utils::FileSystem::getStem(getPath()); if(mSystem && mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO)) stem = MameNames::getInstance()->getRealName(stem); @@ -68,7 +87,7 @@ const std::string FileData::getThumbnailPath() const { if(thumbnail.empty()) { - std::string path = mEnvData->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; if(Utils::FileSystem::exists(path)) thumbnail = path; } @@ -89,57 +108,14 @@ const bool FileData::getHidden() return metadata.get("hidden") == "true"; } -const std::string& FileData::getName() +const bool FileData::getKidGame() { - return metadata.get("name"); -} -/* -const std::string& FileData::getSortName() -{ - if (metadata.get("sortname").empty()) - return metadata.get("name"); - - return metadata.get("sortname"); -} -*/ -FileData* FileData::findUniqueGameForFolder() -{ - std::vector children = getChildren(); - - if (children.size() == 1 && children.at(0)->getType() == GAME) - return children.at(0); - - for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) - { - if ((*it)->getType() == GAME) - return NULL; - - FileData* ret = (*it)->findUniqueGameForFolder(); - if (ret != NULL) - return ret; - } - - return NULL; + return metadata.get("kidgame") != "false"; } -const std::vector& FileData::getChildrenListToDisplay() { - - FileFilterIndex* idx = CollectionSystemManager::get()->getSystemToView(mSystem)->getIndex(); - if (idx->isFiltered()) { - mFilteredChildren.clear(); - for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if (idx->showFile((*it))) { - mFilteredChildren.push_back(*it); - } - } - - return mFilteredChildren; - } - else - { - return mChildren; - } +const std::string& FileData::getName() +{ + return metadata.getName(); } const std::string FileData::getCore() const @@ -159,7 +135,7 @@ const std::string FileData::getVideoPath() const // no video, try to use local video if(video.empty() && Settings::getInstance()->getBool("LocalArt")) { - std::string path = mEnvData->mStartPath + "/images/" + getDisplayName() + "-video.mp4"; + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-video.mp4"; if (Utils::FileSystem::exists(path)) video = path; } @@ -179,7 +155,7 @@ const std::string FileData::getMarqueePath() const { if(marquee.empty()) { - std::string path = mEnvData->mStartPath + "/images/" + getDisplayName() + "-marquee" + extList[i]; + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-marquee" + extList[i]; if(Utils::FileSystem::exists(path)) marquee = path; } @@ -201,7 +177,7 @@ const std::string FileData::getImagePath() const { if(image.empty()) { - std::string path = mEnvData->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; if(Utils::FileSystem::exists(path)) image = path; } @@ -211,29 +187,6 @@ const std::string FileData::getImagePath() const return image; } -std::vector FileData::getFilesRecursive(unsigned int typeMask, bool displayedOnly) const -{ - std::vector out; - FileFilterIndex* idx = mSystem->getIndex(); - - for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if((*it)->getType() & typeMask) - { - if (!displayedOnly || !idx->isFiltered() || idx->showFile(*it)) - out.push_back(*it); - } - - if((*it)->getChildren().size() > 0) - { - std::vector subchildren = (*it)->getFilesRecursive(typeMask, displayedOnly); - out.insert(out.cend(), subchildren.cbegin(), subchildren.cend()); - } - } - - return out; -} - std::string FileData::getKey() { return getFileName(); } @@ -242,7 +195,7 @@ const bool FileData::isArcadeAsset() { if (mSystem && (mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO))) { - const std::string stem = Utils::FileSystem::getStem(mPath); + const std::string stem = Utils::FileSystem::getStem(getPath()); return MameNames::getInstance()->isBios(stem) || MameNames::getInstance()->isDevice(stem); } @@ -254,59 +207,6 @@ FileData* FileData::getSourceFileData() return this; } -void FileData::addChild(FileData* file) -{ - assert(mType == FOLDER); - assert(file->getParent() == NULL); - - const std::string key = file->getKey(); - if (mChildrenByFilename.find(key) == mChildrenByFilename.cend()) - { - mChildrenByFilename[key] = file; - mChildren.push_back(file); - file->mParent = this; - } -} - -void FileData::removeChild(FileData* file) -{ - assert(mType == FOLDER); - assert(file->getParent() == this); - mChildrenByFilename.erase(file->getKey()); - for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if(*it == file) - { - file->mParent = NULL; - mChildren.erase(it); - return; - } - } - - // File somehow wasn't in our children. - assert(false); - -} - -void FileData::sort(ComparisonFunction& comparator, bool ascending) -{ - std::stable_sort(mChildren.begin(), mChildren.end(), comparator); - - for(auto it = mChildren.cbegin(); it != mChildren.cend(); 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); -} - void FileData::launchGame(Window* window) { LOG(LogInfo) << "Attempting to launch game..."; @@ -317,7 +217,7 @@ void FileData::launchGame(Window* window) bool hideWindow = Settings::getInstance()->getBool("HideWindow"); window->deinit(hideWindow); - std::string command = mEnvData->mLaunchCommand; + std::string command = getSystemEnvData()->mLaunchCommand; const std::string rom = Utils::FileSystem::getEscapedPath(getPath()); const std::string basename = Utils::FileSystem::getStem(getPath()); @@ -325,13 +225,13 @@ void FileData::launchGame(Window* window) std::string emulator = getEmulator(); if (emulator.length() == 0) - emulator = mEnvData->getDefaultEmulator(); + emulator = getSystemEnvData()->getDefaultEmulator(); std::string core = getCore(); if (core.length() == 0) - core = mEnvData->getDefaultCore(emulator); + core = getSystemEnvData()->getDefaultCore(emulator); - std::string customCommandLine = mEnvData->getEmulatorCommandLine(emulator); + std::string customCommandLine = getSystemEnvData()->getEmulatorCommandLine(emulator); if (customCommandLine.length() > 0) command = customCommandLine; @@ -341,7 +241,7 @@ void FileData::launchGame(Window* window) command = Utils::String::replace(command, "%ROM%", rom); command = Utils::String::replace(command, "%BASENAME%", basename); command = Utils::String::replace(command, "%ROM_RAW%", rom_raw); - command = Utils::String::replace(command, "%SYSTEM%", mSystemName); + command = Utils::String::replace(command, "%SYSTEM%", getSystemName()); command = Utils::String::replace(command, "%HOME%", Utils::FileSystem::getHomePath()); Scripting::fireEvent("game-start", rom, basename); @@ -376,14 +276,26 @@ void FileData::launchGame(Window* window) } CollectionFileData::CollectionFileData(FileData* file, SystemData* system) - : FileData(file->getSourceFileData()->getType(), file->getSourceFileData()->getPath(), file->getSourceFileData()->getSystemEnvData(), system) + : FileData(file->getSourceFileData()->getType(), "", system) { - // we use this constructor to create a clone of the filedata, and change its system mSourceFileData = file->getSourceFileData(); - refreshMetadata(); mParent = NULL; - metadata = mSourceFileData->metadata; - mSystemName = mSourceFileData->getSystem()->getName(); + metadata = mSourceFileData->metadata; +} + +SystemEnvironmentData* CollectionFileData::getSystemEnvData() const +{ + return mSourceFileData->getSystemEnvData(); +} + +const std::string CollectionFileData::getPath() const +{ + return mSourceFileData->getPath(); +} + +std::string CollectionFileData::getSystemName() const +{ + return mSourceFileData->getSystem()->getName(); } CollectionFileData::~CollectionFileData() @@ -420,12 +332,12 @@ const std::string& CollectionFileData::getName() } // returns Sort Type based on a string description -FileData::SortType getSortTypeFromString(std::string desc) { - std::vector SortTypes = FileSorts::SortTypes; +FolderData::SortType getSortTypeFromString(std::string desc) { + std::vector SortTypes = FileSorts::SortTypes; // find it for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) { - const FileData::SortType& sort = FileSorts::SortTypes.at(i); + const FolderData::SortType& sort = FileSorts::SortTypes.at(i); if(sort.description == desc) { return sort; @@ -434,3 +346,176 @@ FileData::SortType getSortTypeFromString(std::string desc) { // if not found default to name, ascending return FileSorts::SortTypes.at(0); } + +FileData* FolderData::findUniqueGameForFolder() +{ + std::vector children = getChildren(); + + if (children.size() == 1 && children.at(0)->getType() == GAME) + return children.at(0); + + for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + if ((*it)->getType() == GAME) + return NULL; + + FolderData* folder = (FolderData*)(*it); + FileData* ret = folder->findUniqueGameForFolder(); + if (ret != NULL) + return ret; + } + + return NULL; +} + +const std::vector FolderData::getChildrenListToDisplay() +{ + std::vector ret; + + bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + bool filterKidGame = false; + + if (!Settings::getInstance()->getBool("ForceDisableFilters")) + { + if (UIModeController::getInstance()->isUIModeKiosk()) + showHiddenFiles = false; + + if (UIModeController::getInstance()->isUIModeKid()) + filterKidGame = true; + } + + auto sys = CollectionSystemManager::get()->getSystemToView(mSystem); + + FileFilterIndex* idx = sys->getIndex(); + if (idx != nullptr && idx->isFiltered()) + { + for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + if (idx->showFile((*it))) { + ret.push_back(*it); + } + } + } + else + { + bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + if (showHiddenFiles) + return mChildren; + + for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + if (!showHiddenFiles && (*it)->getHidden()) + continue; + + if (filterKidGame && !(*it)->getKidGame()) + continue; + + ret.push_back(*it); + } + } + + return ret; +} + +std::vector FolderData::getFilesRecursive(unsigned int typeMask, bool displayedOnly) const +{ + std::vector out; + + FileFilterIndex* idx = mSystem->getIndex(); + + for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + if ((*it)->getType() & typeMask) + { + if (!displayedOnly || idx == nullptr || !idx->isFiltered() || idx->showFile(*it)) + out.push_back(*it); + } + + if ((*it)->getType() != FOLDER) + continue; + + FolderData* folder = (FolderData*)(*it); + if (folder->getChildren().size() > 0) + { + std::vector subchildren = folder->getFilesRecursive(typeMask, displayedOnly); + out.insert(out.cend(), subchildren.cbegin(), subchildren.cend()); + } + } + + return out; +} + +void FolderData::addChild(FileData* file) +{ + assert(mType == FOLDER); + assert(file->getParent() == NULL); + + const std::string key = file->getKey(); + + mChildren.push_back(file); + file->setParent(this); +} + +void FolderData::removeChild(FileData* file) +{ + assert(mType == FOLDER); + assert(file->getParent() == this); + + for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + if (*it == file) + { + file->setParent(NULL); + mChildren.erase(it); + return; + } + } + + // File somehow wasn't in our children. + assert(false); + +} + +void FolderData::sort(ComparisonFunction& comparator, bool ascending) +{ + std::stable_sort(mChildren.begin(), mChildren.end(), comparator); + + for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + if ((*it)->getType() != FOLDER) + continue; + + FolderData* folder = (FolderData*)(*it); + + if (folder->getChildren().size() > 0) + folder->sort(comparator, ascending); + } + + if (!ascending) + std::reverse(mChildren.begin(), mChildren.end()); +} + +void FolderData::sort(const SortType& type) +{ + sort(*type.comparisonFunction, type.ascending); +} + +FileData* FolderData::FindByPath(const std::string& path) +{ + std::vector children = getChildren(); + + for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + if ((*it)->getPath() == path) + return (*it); + + if ((*it)->getType() != FOLDER) + continue; + + auto item = ((FolderData*)(*it))->FindByPath(path); + if (item != nullptr) + return item; + } + + return nullptr; +} \ No newline at end of file diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 93b0cc2601..5293da3c4b 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -25,26 +25,28 @@ enum FileChangeType FILE_SORTED }; -// Used for loading/saving gamelist.xml. -const char* fileTypeToString(FileType type); -FileType stringToFileType(const char* str); +class FolderData; // A tree node that holds information for a file. class FileData { public: - FileData(FileType type, const std::string& path, SystemEnvironmentData* envData, SystemData* system); + FileData(FileType type, const std::string& path, SystemData* system); virtual ~FileData(); virtual const std::string& getName(); - //virtual const std::string& getSortName(); + inline FileType getType() const { return mType; } - inline const std::string& getPath() const { return mPath; } - inline FileData* getParent() const { return mParent; } - inline const std::unordered_map& getChildrenByFilename() const { return mChildrenByFilename; } - inline const std::vector& getChildren() const { return mChildren; } + + inline FolderData* getParent() const { return mParent; } + void setParent(FolderData* parent) { mParent = parent; } + inline SystemData* getSystem() const { return mSystem; } - inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } + + virtual const std::string getPath() const; + + virtual SystemEnvironmentData* getSystemEnvData() const; + virtual const std::string getThumbnailPath() const; virtual const std::string getVideoPath() const; virtual const std::string getMarqueePath() const; @@ -55,14 +57,7 @@ class FileData virtual const bool getHidden(); virtual const bool getFavorite(); - - FileData* findUniqueGameForFolder(); - - const std::vector& getChildrenListToDisplay(); - std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; - - void addChild(FileData* file); // Error if mType != FOLDER - void removeChild(FileData* file); //Error if mType != FOLDER + virtual const bool getKidGame(); inline bool isPlaceHolder() { return mType == PLACEHOLDER; }; @@ -73,7 +68,7 @@ class FileData inline std::string getFullPath() { return getPath(); }; inline std::string getFileName() { return Utils::FileSystem::getFileName(getPath()); }; virtual FileData* getSourceFileData(); - inline std::string getSystemName() const { return mSystemName; }; + virtual std::string getSystemName() const; // Returns our best guess at the "real" name for this file (will attempt to perform MAME name translation) std::string getDisplayName() const; @@ -83,35 +78,13 @@ class FileData void launchGame(Window* window); - 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; -protected: - FileData* mSourceFileData; - FileData* mParent; - std::string mSystemName; - //std::string mDefaultCore; - -private: - FileType mType; +protected: + FolderData* mParent; std::string mPath; - SystemEnvironmentData* mEnvData; + FileType mType; SystemData* mSystem; - std::unordered_map mChildrenByFilename; - std::vector mChildren; - std::vector mFilteredChildren; }; class CollectionFileData : public FileData @@ -123,12 +96,60 @@ class CollectionFileData : public FileData void refreshMetadata(); FileData* getSourceFileData(); std::string getKey(); + virtual const std::string getPath() const; + + virtual std::string getSystemName() const; + virtual SystemEnvironmentData* getSystemEnvData() const; + private: // needs to be updated when metadata changes std::string mCollectionFileName; + FileData* mSourceFileData; + bool mDirty; }; -FileData::SortType getSortTypeFromString(std::string desc); +class FolderData : public FileData +{ +public: + FolderData(const std::string& startpath, SystemData* system) : FileData(FOLDER, startpath, system) + { + } + + ~FolderData() + { + mChildren.clear(); + } + + 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); + + FileData* findUniqueGameForFolder(); + + FileData* FindByPath(const std::string& path); + + inline const std::vector& getChildren() const { return mChildren; } + const std::vector getChildrenListToDisplay(); + std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; + + void addChild(FileData* file); // Error if mType != FOLDER + void removeChild(FileData* file); //Error if mType != FOLDER + +private: + std::vector mChildren; +}; + +FolderData::SortType getSortTypeFromString(std::string desc); #endif // ES_APP_FILE_DATA_H diff --git a/es-app/src/FileFilterIndex.cpp b/es-app/src/FileFilterIndex.cpp index bb077f8782..03ecd73c68 100644 --- a/es-app/src/FileFilterIndex.cpp +++ b/es-app/src/FileFilterIndex.cpp @@ -297,8 +297,9 @@ bool FileFilterIndex::showFile(FileData* game) // if folder, needs further inspection - i.e. see if folder contains at least one element // that should be shown - if (game->getType() == FOLDER) { - std::vector children = game->getChildren(); + if (game->getType() == FOLDER) + { + std::vector children = ((FolderData*) game)->getChildren(); // iterate through all of the children, until there's a match for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it ) { diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index 046100cab4..25c3c6a66f 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -4,39 +4,39 @@ namespace FileSorts { - const FileData::SortType typesArr[] = { - FileData::SortType(&compareName, true, "filename, ascending"), - FileData::SortType(&compareName, false, "filename, descending"), + const FolderData::SortType typesArr[] = { + FolderData::SortType(&compareName, true, "filename, ascending"), + FolderData::SortType(&compareName, false, "filename, descending"), - FileData::SortType(&compareRating, true, "rating, ascending"), - FileData::SortType(&compareRating, false, "rating, descending"), + FolderData::SortType(&compareRating, true, "rating, ascending"), + FolderData::SortType(&compareRating, false, "rating, descending"), - FileData::SortType(&compareTimesPlayed, true, "times played, ascending"), - FileData::SortType(&compareTimesPlayed, false, "times played, descending"), + FolderData::SortType(&compareTimesPlayed, true, "times played, ascending"), + FolderData::SortType(&compareTimesPlayed, false, "times played, descending"), - FileData::SortType(&compareLastPlayed, true, "last played, ascending"), - FileData::SortType(&compareLastPlayed, false, "last played, descending"), + FolderData::SortType(&compareLastPlayed, true, "last played, ascending"), + FolderData::SortType(&compareLastPlayed, false, "last played, descending"), - FileData::SortType(&compareNumPlayers, true, "number of players, ascending"), - FileData::SortType(&compareNumPlayers, false, "number of players, descending"), + FolderData::SortType(&compareNumPlayers, true, "number of players, ascending"), + FolderData::SortType(&compareNumPlayers, false, "number of players, descending"), - FileData::SortType(&compareReleaseDate, true, "release date, ascending"), - FileData::SortType(&compareReleaseDate, false, "release date, descending"), + FolderData::SortType(&compareReleaseDate, true, "release date, ascending"), + FolderData::SortType(&compareReleaseDate, false, "release date, descending"), - FileData::SortType(&compareGenre, true, "genre, ascending"), - FileData::SortType(&compareGenre, false, "genre, descending"), + FolderData::SortType(&compareGenre, true, "genre, ascending"), + FolderData::SortType(&compareGenre, false, "genre, descending"), - FileData::SortType(&compareDeveloper, true, "developer, ascending"), - FileData::SortType(&compareDeveloper, false, "developer, descending"), + FolderData::SortType(&compareDeveloper, true, "developer, ascending"), + FolderData::SortType(&compareDeveloper, false, "developer, descending"), - FileData::SortType(&comparePublisher, true, "publisher, ascending"), - FileData::SortType(&comparePublisher, false, "publisher, descending"), + FolderData::SortType(&comparePublisher, true, "publisher, ascending"), + FolderData::SortType(&comparePublisher, false, "publisher, descending"), - FileData::SortType(&compareSystem, true, "system, ascending"), - FileData::SortType(&compareSystem, false, "system, descending") + FolderData::SortType(&compareSystem, true, "system, ascending"), + FolderData::SortType(&compareSystem, false, "system, descending") }; - const std::vector SortTypes(typesArr, typesArr + sizeof(typesArr)/sizeof(typesArr[0])); + const std::vector SortTypes(typesArr, typesArr + sizeof(typesArr)/sizeof(typesArr[0])); //returns if file1 should come before file2 bool compareName(const FileData* file1, const FileData* file2) diff --git a/es-app/src/FileSorts.h b/es-app/src/FileSorts.h index d821ba0f9d..9851cbd534 100644 --- a/es-app/src/FileSorts.h +++ b/es-app/src/FileSorts.h @@ -18,7 +18,7 @@ namespace FileSorts bool comparePublisher(const FileData* file1, const FileData* file2); bool compareSystem(const FileData* file1, const FileData* file2); - extern const std::vector SortTypes; + extern const std::vector SortTypes; }; #endif // ES_APP_FILE_SORTS_H diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index ac56208496..cb1a9f1e7e 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -9,10 +9,10 @@ #include "SystemData.h" #include -FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType type) +FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType type, std::unordered_map& fileMap) { // first, verify that path is within the system's root folder - FileData* root = system->getRootFolder(); + FolderData* root = system->getRootFolder(); bool contains = false; std::string relative = Utils::FileSystem::removeCommonPath(path, root->getPath(), contains); @@ -24,40 +24,41 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType Utils::FileSystem::stringList pathList = Utils::FileSystem::getPathList(relative); auto path_it = pathList.begin(); - FileData* treeNode = root; - bool found = false; + FolderData* treeNode = root; + + // bool found = false; while(path_it != pathList.end()) { - const std::unordered_map& children = treeNode->getChildrenByFilename(); - - std::string key = *path_it; - found = children.find(key) != children.cend(); - if (found) { - treeNode = children.at(key); - } + std::string key = Utils::FileSystem::combine(treeNode->getPath(), *path_it); + FileData* item = (fileMap.find(key) != fileMap.end()) ? fileMap[key] : nullptr; // this is the end if(path_it == --pathList.end()) { - if(found) - return treeNode; - - if(type == FOLDER) + if (type == FOLDER) { - LOG(LogWarning) << "gameList: folder doesn't already exist, won't create"; return NULL; + LOG(LogWarning) << "gameList: folder doesn't already exist, won't create"; } - FileData* file = new FileData(type, path, system->getSystemEnvData(), system); - - // skipping arcade assets from gamelist - if(!file->isArcadeAsset()) + if (type == GAME) // Final file { - treeNode->addChild(file); + if (item == nullptr) + { + // Add final game + item = new FileData(GAME, path, system); + if (!item->isArcadeAsset()) + { + fileMap[key] = item; + treeNode->addChild(item); + } + } + + return item; } - return file; } + /* if(!found) { // don't create folders unless it's leading up to a game @@ -69,20 +70,19 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType } // create missing folder - - /* FCA TODO -> 1 seul jeu dans le folder ? - FileData* folder = new FileData(FOLDER, Utils::FileSystem::getStem(treeNode->getPath()) + "/" + *path_it, system->getSystemEnvData(), system); - treeNode->addChild(folder); - treeNode = folder;*/ + //FCA TODO -> 1 seul jeu dans le folder ? + //FileData* folder = new FileData(FOLDER, Utils::FileSystem::getStem(treeNode->getPath()) + "/" + *path_it, system->getSystemEnvData(), system); + //treeNode->addChild(folder); + //treeNode = folder; } - + */ path_it++; } return NULL; } -void parseGamelist(SystemData* system) +void parseGamelist(SystemData* system, std::unordered_map& fileMap) { std::string xmlpath = system->getGamelistPath(false); if (!Utils::FileSystem::exists(xmlpath)) @@ -132,7 +132,7 @@ void parseGamelist(SystemData* system) continue; } - FileData* file = findOrCreateFile(system, path, type); + FileData* file = findOrCreateFile(system, path, type, fileMap); if(!file) { LOG(LogError) << "Error finding/creating FileData for \"" << path << "\", skipping."; @@ -141,7 +141,7 @@ void parseGamelist(SystemData* system) else if(!file->isArcadeAsset()) { std::string defaultName = file->metadata.get("name"); - file->metadata = MetaDataList::createFromXML(GAME_METADATA, fileNode, relativeTo); + file->metadata = MetaDataList::createFromXML(GAME_METADATA, fileNode, system); //make sure name gets set if one didn't exist if (file->metadata.get("name").empty()) @@ -249,7 +249,7 @@ void updateGamelist(SystemData* system) //now we have all the information from the XML. now iterate through all our games and add information from there - FileData* rootFolder = system->getRootFolder(); + FolderData* rootFolder = system->getRootFolder(); if (rootFolder != nullptr) { //get only files, no folders diff --git a/es-app/src/Gamelist.h b/es-app/src/Gamelist.h index d9502a196d..efc402b792 100644 --- a/es-app/src/Gamelist.h +++ b/es-app/src/Gamelist.h @@ -2,10 +2,13 @@ #ifndef ES_APP_GAME_LIST_H #define ES_APP_GAME_LIST_H +#include + class SystemData; +class FileData; // Loads gamelist.xml data into a SystemData. -void parseGamelist(SystemData* system); +void parseGamelist(SystemData* system, std::unordered_map& fileMap); // Writes currently loaded metadata for a SystemData to gamelist.xml. void updateGamelist(SystemData* system); diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index cdaa4dd151..519ff393e3 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -4,68 +4,112 @@ #include "utils/StringUtil.h" #include "Log.h" #include +#include "SystemData.h" MetaDataDecl gameDecls[] = { // key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd - {"name", MD_STRING, "", false, "name", "enter game name"}, -// {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, - {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, - - { "emulator", MD_PLIST, "", false, "emulator", "emulator" }, - { "core", MD_PLIST, "", false, "core", "core" }, - - {"image", MD_PATH, "", false, "image", "enter path to image"}, - {"video", MD_PATH , "", false, "video", "enter path to video"}, - {"marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, - {"thumbnail", MD_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"}, - {"favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on"}, - {"hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on" }, - {"kidgame", MD_BOOL, "false", false, "kidgame", "enter kidgame off/on" }, - {"playcount", MD_INT, "0", true, "play count", "enter number of times played"}, - {"lastplayed", MD_TIME, "0", true, "last played", "enter last played date"} + { 0, "name", MD_STRING, "", false, "name", "enter game name"}, +// { 1, "sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, + { 2, "desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, + { 3, "emulator", MD_PLIST, "", false, "emulator", "emulator" }, + { 4, "core", MD_PLIST, "", false, "core", "core" }, + { 5, "image", MD_PATH, "", false, "image", "enter path to image"}, + { 6, "video", MD_PATH , "", false, "video", "enter path to video"}, + { 7, "marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, + { 8, "thumbnail", MD_PATH, "", false, "thumbnail", "enter path to thumbnail"}, + { 9, "rating", MD_RATING, "0.000000", false, "rating", "enter rating"}, + { 10, "releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date"}, + { 11, "developer", MD_STRING, "unknown", false, "developer", "enter game developer"}, + { 12, "publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher"}, + { 13, "genre", MD_STRING, "unknown", false, "genre", "enter game genre"}, + { 14, "players", MD_INT, "1", false, "players", "enter number of players"}, + { 15, "favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on"}, + { 16, "hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on" }, + { 17, "kidgame", MD_BOOL, "false", false, "kidgame", "enter kidgame off/on" }, + { 18, "playcount", MD_INT, "0", true, "play count", "enter number of times played"}, + { 19, "lastplayed", MD_TIME, "0", true, "last played", "enter last played date"} }; + const std::vector gameMDD(gameDecls, gameDecls + sizeof(gameDecls) / sizeof(gameDecls[0])); MetaDataDecl folderDecls[] = { - {"name", MD_STRING, "", false, "name", "enter game name"}, -// {"sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, - {"desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, - {"image", MD_PATH, "", false, "image", "enter path to image"}, - {"thumbnail", MD_PATH, "", false, "thumbnail", "enter path to thumbnail"}, - {"video", MD_PATH, "", false, "video", "enter path to video"}, - {"marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, - {"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"}, - - { "favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on" }, - { "hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on" }, + { 0, "name", MD_STRING, "", false, "name", "enter game name"}, +// { 1, "sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, + { 2, "desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, + { 3, "image", MD_PATH, "", false, "image", "enter path to image"}, + { 4, "thumbnail", MD_PATH, "", false, "thumbnail", "enter path to thumbnail"}, + { 5, "video", MD_PATH, "", false, "video", "enter path to video"}, + { 6, "marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, + { 7, "rating", MD_RATING, "0.000000", false, "rating", "enter rating"}, + { 8, "releasedate", MD_DATE, "not-a-date-time", false, "release date", "enter release date"}, + { 9, "developer", MD_STRING, "unknown", false, "developer", "enter game developer"}, + { 10, "publisher", MD_STRING, "unknown", false, "publisher", "enter game publisher"}, + { 11, "genre", MD_STRING, "unknown", false, "genre", "enter game genre"}, + { 12, "players", MD_INT, "1", false, "players", "enter number of players"}, + { 13, "favorite", MD_BOOL, "false", false, "favorite", "enter favorite off/on" }, + { 14, "hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on" }, }; + const std::vector folderMDD(folderDecls, folderDecls + sizeof(folderDecls) / sizeof(folderDecls[0])); -std::map MetaDataList::mDefaultGameMap = MetaDataList::BuildDefaultMap(GAME_METADATA); -std::map MetaDataList::mDefaultFolderMap = MetaDataList::BuildDefaultMap(FOLDER_METADATA); +std::map MetaDataList::mGameIdMap = MetaDataList::BuildIdMap(GAME_METADATA); +std::map MetaDataList::mFolderIdMap = MetaDataList::BuildIdMap(FOLDER_METADATA); -std::map MetaDataList::BuildDefaultMap(MetaDataListType type) +std::map MetaDataList::mGameTypeMap = MetaDataList::BuildTypeMap(GAME_METADATA); +std::map MetaDataList::mFolderTypeMap = MetaDataList::BuildTypeMap(FOLDER_METADATA); + +std::map MetaDataList::mDefaultGameMap = MetaDataList::BuildDefaultMap(GAME_METADATA); +std::map MetaDataList::mDefaultFolderMap = MetaDataList::BuildDefaultMap(FOLDER_METADATA); + +std::map MetaDataList::BuildTypeMap(MetaDataListType type) { - std::map ret; + std::map ret; const std::vector& mdd = getMDDByType(type); for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) - ret[iter->key] = iter->defaultValue; + ret[iter->id] = iter->type; return ret; } +std::map MetaDataList::BuildIdMap(MetaDataListType type) +{ + std::map ret; + + const std::vector& mdd = getMDDByType(type); + for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + ret[iter->key] = iter->id; + + return ret; +} + +std::map MetaDataList::BuildDefaultMap(MetaDataListType type) +{ + std::map ret; + + const std::vector& mdd = getMDDByType(type); + for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) + ret[iter->id] = iter->defaultValue; + + return ret; +} + +MetaDataType MetaDataList::getType(unsigned char id) const +{ + if (mType == GAME_METADATA) + return mGameTypeMap[id]; + + return mFolderTypeMap[id]; +} + +unsigned char MetaDataList::getId(const std::string& key) const +{ + if (mType == GAME_METADATA) + return mGameIdMap[key]; + + return mFolderIdMap[key]; +} + const std::vector& getMDDByType(MetaDataListType type) { switch(type) @@ -85,9 +129,12 @@ MetaDataList::MetaDataList(MetaDataListType type) : mType(type), mWasChanged(fal } -MetaDataList MetaDataList::createFromXML(MetaDataListType type, pugi::xml_node& node, const std::string& relativeTo) +MetaDataList MetaDataList::createFromXML(MetaDataListType type, pugi::xml_node& node, SystemData* system) { MetaDataList mdl(type); + mdl.mRelativeTo = system; + + auto sz = sizeof(MetaDataList); const std::vector& mdd = mdl.getMDD(); @@ -98,10 +145,19 @@ MetaDataList MetaDataList::createFromXML(MetaDataListType type, pugi::xml_node& { std::string value = md.text().get(); - if (iter->type == MD_PATH) // if it's a path, resolve relative paths - value = Utils::FileSystem::resolveRelativePath(value, relativeTo, true); + // if (iter->type == MD_PATH) // if it's a path, resolve relative paths + // value = Utils::FileSystem::resolveRelativePath(value, relativeTo, true); + + if (value == iter->defaultValue) + continue; + + if (iter->type == MD_BOOL) + value = Utils::String::toLower(value); - mdl.set(iter->key, value); + if (iter->id == 0) + mdl.mName = value; + else + mdl.mMap[iter->id] = value; } } @@ -114,8 +170,14 @@ void MetaDataList::appendToXML(pugi::xml_node& parent, bool ignoreDefaults, cons for(auto mddIter = mdd.cbegin(); mddIter != mdd.cend(); mddIter++) { - auto mapIter = mMap.find(mddIter->key); - if(mapIter != mMap.cend()) + if (mddIter->id == 0) + { + parent.append_child("name").text().set(mName.c_str()); + continue; + } + + auto mapIter = mMap.find(mddIter->id); + if (mapIter != mMap.cend()) { // we have this value! // if it's just the default (and we ignore defaults), don't write it @@ -127,30 +189,46 @@ void MetaDataList::appendToXML(pugi::xml_node& parent, bool ignoreDefaults, cons if (mddIter->type == MD_PATH) value = Utils::FileSystem::createRelativePath(value, relativeTo, true); - parent.append_child(mapIter->first.c_str()).text().set(value.c_str()); + parent.append_child(mddIter->key.c_str()).text().set(value.c_str()); // mapIter->first } } } +const std::string& MetaDataList::getName() const +{ + return mName; +} + void MetaDataList::set(const std::string& key, const std::string& value) { if (key == "name") mName = value; + else + mMap[getId(key)] = value; - mMap[key] = value; mWasChanged = true; } -const std::string& MetaDataList::get(const std::string& key) const +const std::string MetaDataList::get(const std::string& key) const { - auto it = mMap.find(key); + if (key == "name") + return mName; + + auto id = getId(key); + + auto it = mMap.find(id); if (it != mMap.end()) + { + if (getType(id) == MD_PATH && mRelativeTo != nullptr) // if it's a path, resolve relative paths + return Utils::FileSystem::resolveRelativePath(it->second, mRelativeTo->getStartPath(), true); + return it->second; + } if (mType == GAME_METADATA) - return mDefaultGameMap.at(key); + return mDefaultGameMap.at(id); - return mDefaultFolderMap.at(key); + return mDefaultFolderMap.at(id); } int MetaDataList::getInt(const std::string& key) const @@ -165,11 +243,14 @@ float MetaDataList::getFloat(const std::string& key) const bool MetaDataList::isDefault() { + if (!mName.empty()) + return false; + const std::vector& mdd = getMDD(); for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) - { - auto it = mMap.find(iter->key); + { + auto it = mMap.find(iter->id); if (it == mMap.end()) continue; diff --git a/es-app/src/MetaData.h b/es-app/src/MetaData.h index 3e23eab755..9ee580f562 100644 --- a/es-app/src/MetaData.h +++ b/es-app/src/MetaData.h @@ -5,6 +5,8 @@ #include #include +class SystemData; + namespace pugi { class xml_node; } enum MetaDataType @@ -26,6 +28,8 @@ enum MetaDataType struct MetaDataDecl { + unsigned char id; + std::string key; MetaDataType type; std::string defaultValue; @@ -45,14 +49,14 @@ const std::vector& getMDDByType(MetaDataListType type); class MetaDataList { public: - static MetaDataList createFromXML(MetaDataListType type, pugi::xml_node& node, const std::string& relativeTo); + static MetaDataList createFromXML(MetaDataListType type, pugi::xml_node& node, SystemData* system); void appendToXML(pugi::xml_node& parent, bool ignoreDefaults, const std::string& relativeTo) const; MetaDataList(MetaDataListType type); - + void set(const std::string& key, const std::string& value); - const std::string& get(const std::string& key) const; + const std::string get(const std::string& key) const; int getInt(const std::string& key) const; float getFloat(const std::string& key) const; @@ -61,21 +65,37 @@ class MetaDataList bool wasChanged() const; void resetChangedFlag(); - inline MetaDataListType getType() const { return mType; } + inline MetaDataListType getType() const { return (MetaDataListType) mType; } inline const std::vector& getMDD() const { return getMDDByType(getType()); } - const std::string getName() const { return mName; } + const std::string& getName() const; private: - MetaDataListType mType; - std::map mMap; - bool mWasChanged; + std::string mName; + unsigned char mType; + bool mWasChanged; + SystemData* mRelativeTo; + + std::map mMap; + + unsigned char getId(const std::string& key) const; + MetaDataType getType(unsigned char id) const; + + +private: // Static maps + + static std::map mDefaultGameMap; + static std::map mDefaultFolderMap; + + static std::map mGameIdMap; + static std::map mFolderIdMap; - static std::map mDefaultGameMap; - static std::map mDefaultFolderMap; + static std::map mGameTypeMap; + static std::map mFolderTypeMap; - static std::map BuildDefaultMap(MetaDataListType type); + static std::map BuildDefaultMap(MetaDataListType type); + static std::map BuildIdMap(MetaDataListType type); + static std::map BuildTypeMap(MetaDataListType type); - std::string mName; }; #endif // ES_APP_META_DATA_H diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index e12f7cb5bf..a4e7c70221 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -32,32 +32,34 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys { mGridSizeOverride = Vector2f(0, 0); mViewModeChanged = false; - mFilterIndex = new FileFilterIndex(); + mFilterIndex = nullptr;// new FileFilterIndex(); // if it's an actual system, initialize it, if not, just create the data structure if (!CollectionSystem) { - mRootFolder = new FileData(FOLDER, mEnvData->mStartPath, mEnvData, this); + mRootFolder = new FolderData(mEnvData->mStartPath, this); mRootFolder->metadata.set("name", mFullName); + std::unordered_map fileMap; + if (!Settings::getInstance()->getBool("ParseGamelistOnly")) - { - populateFolder(mRootFolder); + { + populateFolder(mRootFolder, fileMap); if (mRootFolder->getChildren().size() == 0) return; } if (!Settings::getInstance()->getBool("IgnoreGamelist")) - parseGamelist(this); + parseGamelist(this, fileMap); mRootFolder->sort(FileSorts::SortTypes.at(0)); - indexAllGameFilters(mRootFolder); + //indexAllGameFilters(mRootFolder); } else { // virtual systems are updated afterwards, we're just creating the data structure - mRootFolder = new FileData(FOLDER, "" + name, mEnvData, this); + mRootFolder = new FolderData("" + name, this); } setIsGameSystemStatus(); @@ -87,7 +89,9 @@ Vector2f SystemData::getGridSizeOverride() SystemData::~SystemData() { delete mRootFolder; - delete mFilterIndex; + + if (mFilterIndex != nullptr) + delete mFilterIndex; } void SystemData::setIsGameSystemStatus() @@ -104,7 +108,7 @@ char _easytolower(char in) { return in; } -void SystemData::populateFolder(FileData* folder) +void SystemData::populateFolder(FolderData* folder, std::unordered_map& fileMap) { const std::string& folderPath = folder->getPath(); if(!Utils::FileSystem::isDirectory(folderPath)) @@ -150,13 +154,17 @@ void SystemData::populateFolder(FileData* folder) isGame = false; if(std::find(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) { - FileData* newGame = new FileData(GAME, fileInfo.path, mEnvData, this); - - // preventing new arcade assets to be added - if (extension != ".zip" || !newGame->isArcadeAsset()) + if (fileMap.find(fileInfo.path) == fileMap.end()) { - folder->addChild(newGame); - isGame = true; + FileData* newGame = new FileData(GAME, fileInfo.path, this); + + // preventing new arcade assets to be added + if (extension != ".zip" || !newGame->isArcadeAsset()) + { + folder->addChild(newGame); + fileMap[fileInfo.path] = newGame; + isGame = true; + } } } @@ -166,20 +174,39 @@ void SystemData::populateFolder(FileData* folder) if (fileInfo.path.rfind("downloaded_images") != std::string::npos || fileInfo.path.rfind("media") != std::string::npos) continue; - FileData* newFolder = new FileData(FOLDER, fileInfo.path, mEnvData, this); - populateFolder(newFolder); + FolderData* newFolder = new FolderData(fileInfo.path, this); + populateFolder(newFolder, fileMap); - if (newFolder->getChildrenByFilename().size() == 0) + if (newFolder->getChildren().size() == 0) delete newFolder; else if (newFolder->findUniqueGameForFolder() != NULL) delete newFolder; else - folder->addChild(newFolder); + { + const std::string& key = newFolder->getPath(); + if (fileMap.find(key) == fileMap.end()) + { + folder->addChild(newFolder); + fileMap[key] = newFolder; + } + } } } } -void SystemData::indexAllGameFilters(const FileData* folder) +FileFilterIndex* SystemData::getIndex(bool createIndex) +{ + if (mFilterIndex == nullptr && createIndex) + { + mFilterIndex = new FileFilterIndex(); + indexAllGameFilters(mRootFolder); + mFilterIndex->setUIModeFilters(); + } + + return mFilterIndex; +} + +void SystemData::indexAllGameFilters(const FolderData* folder) { const std::vector& children = folder->getChildren(); @@ -188,7 +215,7 @@ void SystemData::indexAllGameFilters(const FileData* folder) switch((*it)->getType()) { case GAME: { mFilterIndex->addToIndex(*it); } break; - case FOLDER: { indexAllGameFilters(*it); } break; + case FOLDER: { indexAllGameFilters((FolderData*)*it); } break; } } } @@ -322,7 +349,7 @@ SystemData* SystemData::loadSystem(pugi::xml_node system) envData->mEmulators = emulatorList; SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); - if (newSys->getRootFolder()->getChildrenByFilename().size() == 0) + if (newSys->getRootFolder()->getChildren().size() == 0) { LOG(LogWarning) << "System \"" << name << "\" has no games! Ignoring it."; delete newSys; diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index d19aea5806..51a8bbed68 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -10,9 +10,12 @@ #include #include "math/Vector2f.h" +#include + +#include "FileFilterIndex.h" class FileData; -class FileFilterIndex; +class FolderData; class ThemeData; class Window; @@ -83,7 +86,7 @@ class SystemData SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem = false); ~SystemData(); - inline FileData* getRootFolder() const { return mRootFolder; }; + inline FolderData* 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 mEnvData->mStartPath; } @@ -129,7 +132,28 @@ class SystemData // Load or re-load theme. void loadTheme(); - FileFilterIndex* getIndex() { return mFilterIndex; }; + FileFilterIndex* getIndex(bool createIndex = false); + + void removeFromIndex(FileData* game) { + if (mFilterIndex != nullptr) mFilterIndex->removeFromIndex(game); + }; + + void addToIndex(FileData* game) { + if (mFilterIndex != nullptr) mFilterIndex->addToIndex(game); + }; + + void resetFilters() { + if (mFilterIndex != nullptr) mFilterIndex->resetFilters(); + }; + + void resetIndex() { + if (mFilterIndex != nullptr) mFilterIndex->resetIndex(); + }; + + + void setUIModeFilters() { + if (mFilterIndex != nullptr) mFilterIndex->setUIModeFilters(); + } private: static SystemData* loadSystem(pugi::xml_node system); @@ -146,13 +170,13 @@ class SystemData Vector2f mGridSizeOverride; bool mViewModeChanged; - void populateFolder(FileData* folder); - void indexAllGameFilters(const FileData* folder); + void populateFolder(FolderData* folder, std::unordered_map& fileMap); + void indexAllGameFilters(const FolderData* folder); void setIsGameSystemStatus(); FileFilterIndex* mFilterIndex; - FileData* mRootFolder; + FolderData* mRootFolder; }; #endif // ES_APP_SYSTEM_DATA_H diff --git a/es-app/src/SystemScreenSaver.cpp b/es-app/src/SystemScreenSaver.cpp index 1837b94aa1..b3e8e87fb2 100644 --- a/es-app/src/SystemScreenSaver.cpp +++ b/es-app/src/SystemScreenSaver.cpp @@ -264,7 +264,7 @@ unsigned long SystemScreenSaver::countGameListNodes(const char *nodeName) if (!(*it)->isGameSystem() || (*it)->isCollection()) continue; - FileData* rootFileData = (*it)->getRootFolder(); + FolderData* rootFileData = (*it)->getRootFolder(); FileType type = GAME; std::vector allFiles = rootFileData->getFilesRecursive(type, true); @@ -308,7 +308,7 @@ void SystemScreenSaver::pickGameListNode(unsigned long index, const char *nodeNa if (!(*it)->isGameSystem() || (*it)->isCollection()) continue; - FileData* rootFileData = (*it)->getRootFolder(); + FolderData* rootFileData = (*it)->getRootFolder(); FileType type = GAME; std::vector allFiles = rootFileData->getFilesRecursive(type, true); diff --git a/es-app/src/guis/GuiFastSelect.cpp b/es-app/src/guis/GuiFastSelect.cpp index 3bf0af8cdb..f0d733265a 100644 --- a/es-app/src/guis/GuiFastSelect.cpp +++ b/es-app/src/guis/GuiFastSelect.cpp @@ -131,9 +131,9 @@ void GuiFastSelect::updateSortText() void GuiFastSelect::updateGameListSort() { - const FileData::SortType& sort = FileSorts::SortTypes.at(mSortId); + const FolderData::SortType& sort = FileSorts::SortTypes.at(mSortId); - FileData* root = mGameList->getCursor()->getSystem()->getRootFolder(); + FolderData* root = mGameList->getCursor()->getSystem()->getRootFolder(); root->sort(sort); // will also recursively sort children // notify that the root folder was sorted @@ -145,7 +145,7 @@ void GuiFastSelect::updateGameListCursor() const std::vector& list = mGameList->getCursor()->getParent()->getChildren(); // only skip by letter when the sort mode is alphabetical - const FileData::SortType& sort = FileSorts::SortTypes.at(mSortId); + const FolderData::SortType& sort = FileSorts::SortTypes.at(mSortId); if(sort.comparisonFunction != &FileSorts::compareName) return; diff --git a/es-app/src/guis/GuiGamelistFilter.cpp b/es-app/src/guis/GuiGamelistFilter.cpp index ffdc11bc43..3160b4bc97 100644 --- a/es-app/src/guis/GuiGamelistFilter.cpp +++ b/es-app/src/guis/GuiGamelistFilter.cpp @@ -15,10 +15,9 @@ void GuiGamelistFilter::initializeMenu() // get filters from system - mFilterIndex = mSystem->getIndex(); + mFilterIndex = mSystem->getIndex(true); ComponentListRow row; - // show filtered menu row.elements.clear(); diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 2e4507a02b..2f50022817 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -125,7 +125,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool mListSort = std::make_shared(mWindow, _T("SORT GAMES BY"), false); for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) { - const FileData::SortType& sort = FileSorts::SortTypes.at(i); + const FolderData::SortType& sort = FileSorts::SortTypes.at(i); mListSort->add(_L(Utils::String::toUpper(sort.description)), &sort, i == 0); // TODO - actually make the sort type persistent } @@ -284,7 +284,7 @@ GuiGamelistOptions::~GuiGamelistOptions() { // apply sort if (!fromPlaceholder) { - FileData* root = mSystem->getRootFolder(); + FolderData* root = mSystem->getRootFolder(); root->sort(*mListSort->getSelected()); // will also recursively sort children // notify that the root folder was sorted diff --git a/es-app/src/guis/GuiGamelistOptions.h b/es-app/src/guis/GuiGamelistOptions.h index 32dbabfce5..be6bfc7589 100644 --- a/es-app/src/guis/GuiGamelistOptions.h +++ b/es-app/src/guis/GuiGamelistOptions.h @@ -33,7 +33,7 @@ class GuiGamelistOptions : public GuiComponent typedef OptionListComponent LetterList; std::shared_ptr mJumpToLetterList; - typedef OptionListComponent SortList; + typedef OptionListComponent SortList; std::shared_ptr mListSort; std::shared_ptr> mViewMode; diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 6979133cf9..7fcd00cc69 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -280,7 +280,7 @@ void GuiMetaDataEd::onSizeChanged() void GuiMetaDataEd::save() { // remove game from index - mScraperParams.system->getIndex()->removeFromIndex(mScraperParams.game); + mScraperParams.system->removeFromIndex(mScraperParams.game); for (unsigned int i = 0; i < mEditors.size(); i++) { @@ -299,7 +299,7 @@ void GuiMetaDataEd::save() } // enter game in index - mScraperParams.system->getIndex()->addToIndex(mScraperParams.game); + mScraperParams.system->addToIndex(mScraperParams.game); if (mSavedCallback) mSavedCallback(); diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 95a99f41d8..17cac4dee0 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -288,7 +288,8 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste if(exists != mGameListViews.cend()) return exists->second; - system->getIndex()->setUIModeFilters(); + system->setUIModeFilters(); + //if we didn't, make it, remember it, and return it std::shared_ptr view; @@ -400,7 +401,7 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste view = std::shared_ptr(new BasicGameListView(mWindow, system->getRootFolder())); break; } - + if (selectedViewType != GRID) { // GridGameListView theme needs to be loaded before populating. @@ -410,7 +411,7 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste view->setTheme(system->getTheme()); } - + std::vector& sysVec = SystemData::sSystemVector; int id = (int)(std::find(sysVec.cbegin(), sysVec.cend(), system) - sysVec.cbegin()); view->setPosition(id * (float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight() * 2); @@ -538,7 +539,7 @@ void ViewController::preload() mWindow->renderLoadingScreen(_T("Preloading UI"), (float) i / (float)max); } - (*it)->getIndex()->resetFilters(); + (*it)->resetFilters(); getGameListView(*it); } } @@ -549,16 +550,21 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) { if(it->second.get() == view) { + + // addPlaceholder() + bool isCurrent = (mCurrentView == it->second); SystemData* system = it->first; + FileData* cursor = view->getCursor(); + mGameListViews.erase(it); if (reloadTheme) system->loadTheme(); - system->getIndex()->setUIModeFilters(); + system->setUIModeFilters(); std::shared_ptr newView = getGameListView(system); // to counter having come from a placeholder @@ -596,7 +602,7 @@ void ViewController::reloadAll() for(auto it = cursorMap.cbegin(); it != cursorMap.cend(); it++) { it->first->loadTheme(); - it->first->getIndex()->resetFilters(); + it->first->resetFilters(); if (it->second != NULL) getGameListView(it->first)->setCursor(it->second); diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 39eb026d60..f6f8e98a6a 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -7,15 +7,26 @@ #include "Settings.h" #include "SystemData.h" -BasicGameListView::BasicGameListView(Window* window, FileData* root) +BasicGameListView::BasicGameListView(Window* window, FolderData* root) : ISimpleGameListView(window, root), mList(window) { + mLoaded = false; + mList.setSize(mSize.x(), mSize.y() * 0.8f); mList.setPosition(0, mSize.y() * 0.2f); mList.setDefaultZIndex(20); - addChild(&mList); + addChild(&mList); +} + +void BasicGameListView::onShow() +{ + if (!mLoaded) + { + populateList(mRoot->getChildrenListToDisplay()); + mLoaded = true; + } - populateList(root->getChildrenListToDisplay()); + ISimpleGameListView::onShow(); } void BasicGameListView::setThemeName(std::string name) @@ -52,7 +63,6 @@ void BasicGameListView::populateList(const std::vector& files) std::string systemName = mRoot->getSystem()->getFullName(); mHeaderText.setText(systemName); - bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); bool favoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); bool showFavoriteIcon = (systemName != "favorites"); if (!showFavoriteIcon) @@ -64,9 +74,6 @@ void BasicGameListView::populateList(const std::vector& files) { for (auto it = files.cbegin(); it != files.cend(); it++) { - if (!showHiddenFiles && (*it)->getHidden()) - continue; - if (!(*it)->getFavorite()) continue; @@ -79,9 +86,6 @@ void BasicGameListView::populateList(const std::vector& files) for (auto it = files.cbegin(); it != files.cend(); it++) { - if (!showHiddenFiles && (*it)->getHidden()) - continue; - if ((*it)->getFavorite()) { if (favoritesFirst) @@ -105,6 +109,9 @@ void BasicGameListView::populateList(const std::vector& files) FileData* BasicGameListView::getCursor() { + if (mList.size() == 0) + return nullptr; + return mList.getSelected(); } @@ -140,7 +147,7 @@ void BasicGameListView::setCursor(FileData* cursor) void BasicGameListView::addPlaceholder() { // empty list - add a placeholder - FileData* placeholder = new FileData(PLACEHOLDER, "", this->mRoot->getSystem()->getSystemEnvData(), this->mRoot->getSystem()); + FileData* placeholder = new FileData(PLACEHOLDER, "", this->mRoot->getSystem()); mList.add(placeholder->getName(), placeholder, (placeholder->getType() == PLACEHOLDER)); } @@ -163,7 +170,8 @@ void BasicGameListView::remove(FileData *game, bool deleteFile) { if (deleteFile) Utils::FileSystem::removeFile(game->getPath()); // actually delete the file on the filesystem - FileData* parent = game->getParent(); + + FolderData* parent = game->getParent(); if (getCursor() == game) // Select next element in list, or prev if none { std::vector siblings = parent->getChildrenListToDisplay(); diff --git a/es-app/src/views/gamelist/BasicGameListView.h b/es-app/src/views/gamelist/BasicGameListView.h index 7cc8351798..1123f842fd 100644 --- a/es-app/src/views/gamelist/BasicGameListView.h +++ b/es-app/src/views/gamelist/BasicGameListView.h @@ -8,7 +8,7 @@ class BasicGameListView : public ISimpleGameListView { public: - BasicGameListView(Window* window, FileData* root); + BasicGameListView(Window* window, FolderData* root); // Called when a FileData* is added, has its metadata changed, or is removed virtual void onFileChanged(FileData* file, FileChangeType change); @@ -30,6 +30,7 @@ class BasicGameListView : public ISimpleGameListView virtual void launch(FileData* game) override; virtual void setThemeName(std::string name); + virtual void onShow() override; protected: virtual std::string getQuickSystemSelectRightButton() override; @@ -39,6 +40,7 @@ class BasicGameListView : public ISimpleGameListView virtual void addPlaceholder(); TextListComponent mList; + bool mLoaded; }; #endif // ES_APP_VIEWS_GAME_LIST_BASIC_GAME_LIST_VIEW_H diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 6bb0311fc9..7029fc3e6b 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -12,10 +12,10 @@ #include "Settings.h" #endif -DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : +DetailedGameListView::DetailedGameListView(Window* window, FolderData* root) : BasicGameListView(window, root), mDescContainer(window), mDescription(window), - mImage(window), mMarquee(window), mVideo(nullptr), + mImage(nullptr), mMarquee(nullptr), mVideo(nullptr), mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -31,37 +31,7 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mList.setAlignment(TextListComponent::ALIGN_LEFT); mList.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); - // Marquee - mMarquee.setOrigin(0.5f, 0.5f); - mMarquee.setPosition(mSize.x() * 0.25f, mSize.y() * 0.10f); - mMarquee.setMaxSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.18f); - mMarquee.setDefaultZIndex(35); - // addChild(&mMarquee); - - // 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); - mImage.setDefaultZIndex(30); - addChild(&mImage); - - // video - // Create the correct type of video window -#ifdef _RPI_ - if (Settings::getInstance()->getBool("VideoOmxPlayer")) - mVideo = new VideoPlayerComponent(window, ""); - else -#endif - mVideo = new VideoVlcComponent(window, getTitlePath()); - - mVideo->setOrigin(0.5f, 0.5f); - mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); - mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); - mVideo->setStartDelay(2000); - mVideo->setDefaultZIndex(31); -// addChild(mVideo); - - //addChild(mVideo); -> Add only if present in theme later + createImage(); // metadata labels + values mLblRating.setText(_T("Rating") + ": "); @@ -107,15 +77,74 @@ DetailedGameListView::DetailedGameListView(Window* window, FileData* root) : mDescription.setSize(mDescContainer.getSize().x(), 0); mDescContainer.addChild(&mDescription); - initMDLabels(); initMDValues(); - updateInfoPanel(); } DetailedGameListView::~DetailedGameListView() { - delete mVideo; + if (mImage != nullptr) + delete mImage; + + if (mMarquee != nullptr) + delete mMarquee; + + if (mVideo != nullptr) + delete mVideo; +} + +void DetailedGameListView::createImage() +{ + if (mImage != nullptr) + return; + + const float padding = 0.01f; + + // Image + mImage = new ImageComponent(mWindow); + 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); + mImage->setDefaultZIndex(30); + addChild(mImage); +} + +void DetailedGameListView::createVideo() +{ + if (mVideo != nullptr) + return; + + const float padding = 0.01f; + + // video +// Create the correct type of video window +#ifdef _RPI_ + if (Settings::getInstance()->getBool("VideoOmxPlayer")) + mVideo = new VideoPlayerComponent(window, ""); + else +#endif + mVideo = new VideoVlcComponent(mWindow, getTitlePath()); + + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); + mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); + mVideo->setStartDelay(2000); + mVideo->setDefaultZIndex(31); + addChild(mVideo); +} + +void DetailedGameListView::createMarquee() +{ + const float padding = 0.01f; + + // Marquee + mMarquee = new ImageComponent(mWindow); + mMarquee->setOrigin(0.5f, 0.5f); + mMarquee->setPosition(mSize.x() * 0.25f, mSize.y() * 0.10f); + mMarquee->setMaxSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.18f); + mMarquee->setDefaultZIndex(35); + addChild(mMarquee); + } void DetailedGameListView::onThemeChanged(const std::shared_ptr& theme) @@ -123,42 +152,59 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them BasicGameListView::onThemeChanged(theme); using namespace ThemeFlags; - - mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + mName.applyTheme(theme, getName(), "md_name", ALL); if (theme->getElement(getName(), "md_video", "video")) { + createVideo(); mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION); - if (!isChild(mVideo)) - addChild(mVideo); } - else if (isChild(mVideo)) + else if (mVideo != nullptr) + { removeChild(mVideo); + delete mVideo; + mVideo = nullptr; + } + + if (mVideo == nullptr || theme->getElement(getName(), "md_image", "image")) + { + createImage(); + mImage->applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + } + else if (mImage != nullptr) + { + removeChild(mImage); + delete mImage; + mImage = nullptr; + } if (theme->getElement(getName(), "md_marquee", "image")) { - mMarquee.applyTheme(theme, getName(), "md_marquee", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); - if (!isChild(&mMarquee)) - addChild(&mMarquee); + createMarquee(); + mMarquee->applyTheme(theme, getName(), "md_marquee", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + } + else if (mMarquee != nullptr) + { + removeChild(mMarquee); + delete mMarquee; + mMarquee = nullptr; } - else if (isChild(&mMarquee)) - removeChild(&mMarquee); initMDLabels(); std::vector 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++) + + for (unsigned int i = 0; i < labels.size(); i++) { labels[i]->applyTheme(theme, getName(), lblElements[i], ALL); } - initMDValues(); std::vector values = getMDValues(); assert(values.size() == 8); @@ -261,8 +307,9 @@ void DetailedGameListView::updateInfoPanel() bool fadingOut; if (file == NULL) { - mVideo->setVideo(""); - //mVideo->setImage(""); + if (mVideo != nullptr) + mVideo->setVideo(""); + //mImage.setImage(""); //mDescription.setText(""); fadingOut = true; @@ -271,7 +318,7 @@ void DetailedGameListView::updateInfoPanel() { std::string imagePath = file->getImagePath().empty() ? file->getThumbnailPath() : file->getImagePath(); - if (isChild(mVideo)) + if (mVideo != nullptr) { if (!mVideo->setVideo(file->getVideoPath())) mVideo->setDefaultVideo(); @@ -279,11 +326,11 @@ void DetailedGameListView::updateInfoPanel() mVideo->setImage(imagePath, false, mVideo->getSize()); } - if (isChild(&mImage)) - mImage.setImage(imagePath, false, mImage.getSize()); + if (mImage != nullptr) + mImage->setImage(imagePath, false, mImage->getSize()); - if (isChild(&mMarquee)) - mMarquee.setImage(file->getMarqueePath()); + if (mMarquee != nullptr) + mMarquee->setImage(file->getMarqueePath()); mDescription.setText(getMetadata(file, "desc")); mDescContainer.reset(); @@ -307,13 +354,15 @@ void DetailedGameListView::updateInfoPanel() std::vector comps = getMDValues(); - if (isChild(&mMarquee)) - comps.push_back(&mMarquee); + if (mVideo != nullptr) + comps.push_back(mVideo); + + if (mImage != nullptr) + comps.push_back(mImage); - if (isChild(&mImage)) - comps.push_back(&mImage); + if (mMarquee != nullptr) + comps.push_back(mMarquee); - comps.push_back(mVideo); comps.push_back(&mDescription); comps.push_back(&mName); std::vector labels = getMDLabels(); @@ -342,10 +391,10 @@ void DetailedGameListView::launch(FileData* game) { Vector3f target(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0); - if (isChild(mVideo) && !isChild(&mImage)) + if (mImage != nullptr && mImage->hasImage()) + target = Vector3f(mImage->getCenter().x(), mImage->getCenter().y(), 0); + else if (mVideo != nullptr) target = Vector3f(mVideo->getCenter().x(), mVideo->getCenter().y(), 0); - else if(mImage.hasImage()) - target = Vector3f(mImage.getCenter().x(), mImage.getCenter().y(), 0); ViewController::get()->launch(game, target); } @@ -380,6 +429,6 @@ std::vector DetailedGameListView::getMDValues() void DetailedGameListView::onShow() { - GuiComponent::onShow(); + BasicGameListView::onShow(); updateInfoPanel(); } diff --git a/es-app/src/views/gamelist/DetailedGameListView.h b/es-app/src/views/gamelist/DetailedGameListView.h index 2916ad66ff..1b1cdb6fb5 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.h +++ b/es-app/src/views/gamelist/DetailedGameListView.h @@ -12,7 +12,7 @@ class VideoComponent; class DetailedGameListView : public BasicGameListView { public: - DetailedGameListView(Window* window, FileData* root); + DetailedGameListView(Window* window, FolderData* root); ~DetailedGameListView(); virtual void onThemeChanged(const std::shared_ptr& theme) override; @@ -30,13 +30,17 @@ class DetailedGameListView : public BasicGameListView private: void updateInfoPanel(); + + void createVideo(); + void createMarquee(); + void createImage(); void initMDLabels(); void initMDValues(); std::string getMetadata(FileData* file, std::string name); - ImageComponent mImage; - ImageComponent mMarquee; + ImageComponent* mImage; + ImageComponent* mMarquee; VideoComponent* mVideo; TextComponent mLblRating, mLblReleaseDate, mLblDeveloper, mLblPublisher, mLblGenre, mLblPlayers, mLblLastPlayed, mLblPlayCount; diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 918bb88892..290354f4e3 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -14,7 +14,7 @@ #endif #include "components/VideoVlcComponent.h" -GridGameListView::GridGameListView(Window* window, FileData* root, const std::shared_ptr& theme, std::string themeName, Vector2f gridSize) : +GridGameListView::GridGameListView(Window* window, FolderData* root, const std::shared_ptr& theme, std::string themeName, Vector2f gridSize) : ISimpleGameListView(window, root), mGrid(window), mDescContainer(window), mDescription(window), @@ -29,7 +29,7 @@ GridGameListView::GridGameListView(Window* window, FileData* root, const std::sh { const float padding = 0.01f; - mVideoVisible = false; + mLoaded = false; mGrid.setGridSizeOverride(gridSize); mGrid.setPosition(mSize.x() * 0.1f, mSize.y() * 0.1f); @@ -44,22 +44,6 @@ GridGameListView::GridGameListView(Window* window, FileData* root, const std::sh mImage.setDefaultZIndex(30); addChild(&mImage); - // video - // Create the correct type of video window -#ifdef _RPI_ - if (Settings::getInstance()->getBool("VideoOmxPlayer")) - mVideo = new VideoPlayerComponent(window, ""); - else -#endif - mVideo = new VideoVlcComponent(window, getTitlePath()); - - mVideo->setOrigin(0.5f, 0.5f); - mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); - mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); - mVideo->setStartDelay(2000); - mVideo->setDefaultZIndex(31); - // addChild(mVideo); - // metadata labels + values mLblRating.setText("Rating: "); addChild(&mLblRating); @@ -103,8 +87,7 @@ GridGameListView::GridGameListView(Window* window, FileData* root, const std::sh mDescription.setFont(Font::get(FONT_SIZE_SMALL)); mDescription.setSize(mDescContainer.getSize().x(), 0); mDescContainer.addChild(&mDescription); - - + initMDLabels(); initMDValues(); @@ -112,15 +95,51 @@ GridGameListView::GridGameListView(Window* window, FileData* root, const std::sh setThemeName(themeName); setTheme(theme); +} - populateList(root->getChildrenListToDisplay()); - updateInfoPanel(); +void GridGameListView::createVideo() +{ + if (mVideo != nullptr) + return; + + const float padding = 0.01f; + + // video +// Create the correct type of video window +#ifdef _RPI_ + if (Settings::getInstance()->getBool("VideoOmxPlayer")) + mVideo = new VideoPlayerComponent(window, ""); + else +#endif + mVideo = new VideoVlcComponent(mWindow, getTitlePath()); + + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); + mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); + mVideo->setStartDelay(2000); + mVideo->setDefaultZIndex(31); + addChild(mVideo); } +void GridGameListView::onShow() +{ + if (!mLoaded) + { + populateList(mRoot->getChildrenListToDisplay()); + updateInfoPanel(); + mLoaded = true; + } + + ISimpleGameListView::onShow(); +} + + GridGameListView::~GridGameListView() { - delete mVideo; + if (mVideo != nullptr) + delete mVideo; } + void GridGameListView::setThemeName(std::string name) { ISimpleGameListView::setThemeName(name); @@ -129,6 +148,9 @@ void GridGameListView::setThemeName(std::string name) FileData* GridGameListView::getCursor() { + if (mGrid.size() == 0) + return nullptr; + return mGrid.getSelected(); } @@ -184,11 +206,11 @@ void GridGameListView::populateList(const std::vector& files) { mGrid.clear(); mHeaderText.setText(mRoot->getSystem()->getFullName()); + if (files.size() > 0) { std::string systemName = mRoot->getSystem()->getFullName(); - bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); bool favoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); bool showFavoriteIcon = (systemName != "favorites"); if (!showFavoriteIcon) @@ -198,21 +220,18 @@ void GridGameListView::populateList(const std::vector& files) { for (auto it = files.cbegin(); it != files.cend(); it++) { - if ((*it)->getFavorite() && (showHiddenFiles || !(*it)->getHidden())) - { - if (showFavoriteIcon) - mGrid.add(_T("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); - else - mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); - } + if (!(*it)->getFavorite()) + continue; + + if (showFavoriteIcon) + mGrid.add(_T("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + else + mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); } } for (auto it = files.cbegin(); it != files.cend(); it++) { - if (!showHiddenFiles && (*it)->getHidden()) - continue; - if ((*it)->getFavorite()) { if (favoritesFirst) @@ -253,14 +272,14 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) if (theme->getElement(getName(), "md_video", "video")) { - mVideoVisible = true; - mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION); - addChild(mVideo); + createVideo(); + mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION); } - else + else if (mVideo != nullptr) { - mVideoVisible = false; removeChild(mVideo); + delete mVideo; + mVideo = nullptr; } initMDLabels(); @@ -380,15 +399,16 @@ void GridGameListView::updateInfoPanel() bool fadingOut; if (file == NULL) { - mVideo->setVideo(""); - mVideo->setImage(""); - - //mDescription.setText(""); + if (mVideo != nullptr) + mVideo->setVideo(""); + + // mVideo->setImage(""); + // mDescription.setText(""); fadingOut = true; } else { - if (mVideoVisible) + if (mVideo != nullptr) { if (!mVideo->setVideo(file->getVideoPath())) mVideo->setDefaultVideo(); @@ -398,14 +418,14 @@ void GridGameListView::updateInfoPanel() { if (file->getImagePath().empty()) { - if (mVideoVisible) + if (mVideo != nullptr) mVideo->setImage(file->getThumbnailPath(), false, mVideo->getSize()); mImage.setImage(file->getThumbnailPath(), false, mImage.getSize()); } else { - if (mVideoVisible) + if (mVideo != nullptr) mVideo->setImage(file->getImagePath(), false, mVideo->getSize()); mImage.setImage(file->getImagePath(), false, mImage.getSize()); @@ -435,8 +455,11 @@ void GridGameListView::updateInfoPanel() } std::vector comps = getMDValues(); + + if (mVideo != nullptr) + comps.push_back(mVideo); + comps.push_back(&mImage); - comps.push_back(mVideo); comps.push_back(&mDescription); comps.push_back(&mName); std::vector labels = getMDLabels(); @@ -464,7 +487,7 @@ void GridGameListView::updateInfoPanel() void GridGameListView::addPlaceholder() { // empty grid - add a placeholder - FileData* placeholder = new FileData(PLACEHOLDER, "", this->mRoot->getSystem()->getSystemEnvData(), this->mRoot->getSystem()); + FileData* placeholder = new FileData(PLACEHOLDER, "", this->mRoot->getSystem()); mGrid.add(placeholder->getName(), "", "", placeholder); } @@ -478,7 +501,7 @@ void GridGameListView::remove(FileData *game, bool deleteFile) if (deleteFile) Utils::FileSystem::removeFile(game->getPath()); // actually delete the file on the filesystem - FileData* parent = game->getParent(); + FolderData* parent = game->getParent(); if (getCursor() == game) // Select next element in list, or prev if none { std::vector siblings = parent->getChildrenListToDisplay(); diff --git a/es-app/src/views/gamelist/GridGameListView.h b/es-app/src/views/gamelist/GridGameListView.h index 53508b1574..cf270692c8 100644 --- a/es-app/src/views/gamelist/GridGameListView.h +++ b/es-app/src/views/gamelist/GridGameListView.h @@ -14,7 +14,7 @@ class VideoComponent; class GridGameListView : public ISimpleGameListView { public: - GridGameListView(Window* window, FileData* root, const std::shared_ptr& theme, std::string customThemeName, Vector2f gridSize); + GridGameListView(Window* window, FolderData* root, const std::shared_ptr& theme, std::string customThemeName, Vector2f gridSize); ~GridGameListView(); virtual void onThemeChanged(const std::shared_ptr& theme) override; @@ -37,6 +37,7 @@ class GridGameListView : public ISimpleGameListView virtual void onFileChanged(FileData* file, FileChangeType change); virtual void setThemeName(std::string name); + virtual void onShow(); protected: virtual std::string getQuickSystemSelectRightButton() override; @@ -50,6 +51,8 @@ class GridGameListView : public ISimpleGameListView private: void updateInfoPanel(); const std::string getImagePath(FileData* file); + + void createVideo(); void initMDLabels(); void initMDValues(); @@ -77,6 +80,8 @@ class GridGameListView : public ISimpleGameListView ScrollableContainer mDescContainer; TextComponent mDescription; + + bool mLoaded; }; #endif // ES_APP_VIEWS_GAME_LIST_GRID_GAME_LIST_VIEW_H diff --git a/es-app/src/views/gamelist/IGameListView.h b/es-app/src/views/gamelist/IGameListView.h index 63acf6a45d..75c444355d 100644 --- a/es-app/src/views/gamelist/IGameListView.h +++ b/es-app/src/views/gamelist/IGameListView.h @@ -13,7 +13,7 @@ class Window; class IGameListView : public GuiComponent { public: - IGameListView(Window* window, FileData* root) : GuiComponent(window), mRoot(root) + IGameListView(Window* window, FolderData* root) : GuiComponent(window), mRoot(root) { setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); } virtual ~IGameListView() {} @@ -47,7 +47,7 @@ class IGameListView : public GuiComponent protected: std::string mCustomThemeName; - FileData* mRoot; + FolderData* mRoot; std::shared_ptr mTheme; }; diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 52775f4265..d20cde7d40 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -7,7 +7,7 @@ #include "Sound.h" #include "SystemData.h" -ISimpleGameListView::ISimpleGameListView(Window* window, FileData* root) : IGameListView(window, root), +ISimpleGameListView::ISimpleGameListView(Window* window, FolderData* root) : IGameListView(window, root), mHeaderText(window), mHeaderImage(window), mBackground(window) { mHeaderText.setText("Logo Text"); @@ -85,10 +85,13 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) if (config->isMappedTo("a", input)) { FileData* cursor = getCursor(); + FolderData* folder = NULL; if (cursor->getType() == FOLDER) { - FileData* gameOfFolder = cursor->findUniqueGameForFolder(); + folder = (FolderData*)cursor; + + FileData* gameOfFolder = folder->findUniqueGameForFolder(); if (gameOfFolder != NULL) cursor = gameOfFolder; } @@ -101,10 +104,10 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) else { // it's a folder - if(cursor->getChildren().size() > 0) + if(folder->getChildren().size() > 0) { mCursorStack.push(cursor); - populateList(cursor->getChildrenListToDisplay()); + populateList(folder->getChildrenListToDisplay()); FileData* cursor = getCursor(); setCursor(cursor); } @@ -164,7 +167,7 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) return true; } } - else if (config->isMappedTo("y", input) && UIModeController::getInstance()->isUIModeKid()) + else if (config->isMappedTo("y", input) && !UIModeController::getInstance()->isUIModeKid()) { if(mRoot->getSystem()->isGameSystem()) { diff --git a/es-app/src/views/gamelist/ISimpleGameListView.h b/es-app/src/views/gamelist/ISimpleGameListView.h index 6980dc2443..4a88b3f274 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.h +++ b/es-app/src/views/gamelist/ISimpleGameListView.h @@ -10,7 +10,7 @@ class ISimpleGameListView : public IGameListView { public: - ISimpleGameListView(Window* window, FileData* root); + ISimpleGameListView(Window* window, FolderData* 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. diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index 823d9feb10..ef9973f36b 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -11,7 +11,7 @@ #include "Settings.h" #endif -VideoGameListView::VideoGameListView(Window* window, FileData* root) : +VideoGameListView::VideoGameListView(Window* window, FolderData* root) : BasicGameListView(window, root), mDescContainer(window), mDescription(window), mMarquee(window), diff --git a/es-app/src/views/gamelist/VideoGameListView.h b/es-app/src/views/gamelist/VideoGameListView.h index 1ff642ca2d..585663a38b 100644 --- a/es-app/src/views/gamelist/VideoGameListView.h +++ b/es-app/src/views/gamelist/VideoGameListView.h @@ -12,7 +12,7 @@ class VideoComponent; class VideoGameListView : public BasicGameListView { public: - VideoGameListView(Window* window, FileData* root); + VideoGameListView(Window* window, FolderData* root); virtual ~VideoGameListView(); virtual void onShow() override; diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 987a171ef3..2bcca44e05 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -19,7 +19,6 @@ Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCoun mHelp = new HelpComponent(this); mBackgroundOverlay = new ImageComponent(this); mSplash = NULL; - } Window::~Window() @@ -320,12 +319,6 @@ void Window::endRenderLoadingScreen() void Window::renderLoadingScreen(std::string text, float percent) { -#if defined(_WIN32) - // Avoid Window Freezing on Windows - SDL_Event event; - while (SDL_PollEvent(&event)) ; -#endif - Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); @@ -343,15 +336,12 @@ void Window::renderLoadingScreen(std::string text, float percent) Renderer::drawRect(x, y, w, h, 0x252525FF); Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF } - - + ImageComponent splash(this, true); splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); -#if defined(_WIN32) if (mSplash == NULL) mSplash = TextureResource::get(":/splash.svg", false, false, true); -#endif if (mSplash != NULL) splash.setImage(mSplash); @@ -360,8 +350,7 @@ void Window::renderLoadingScreen(std::string text, float percent) splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); splash.render(trans); - - + auto& font = mDefaultFonts.at(1); TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); @@ -369,11 +358,16 @@ void Window::renderLoadingScreen(std::string text, float percent) float y = Math::round(Renderer::getScreenHeight() * 0.78f); // 35 trans = trans.translate(Vector3f(x, y, 0.0f)); Renderer::setMatrix(trans); - font->renderTextCache(cache); + font->renderTextCache(cache); delete cache; - Renderer::swapBuffers(); + +#if defined(_WIN32) + // Avoid Window Freezing on Windows + SDL_Event event; + while (SDL_PollEvent(&event)); +#endif } void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 92450572ca..d4debc24bd 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -549,12 +549,13 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) mStartPosition = (col - centralCol) * dimOpposite; } + auto lastCursor = mLastCursor; mLastCursor = mCursor; mCameraDirection = direction ? -1.0 : 1.0; mCamera = 0; - if (oldStart == mStartPosition || !GuiComponent::ALLOWANIMATIONS) + if (lastCursor < 0 || oldStart == mStartPosition || !GuiComponent::ALLOWANIMATIONS) { updateTiles(direction, true); diff --git a/es-core/src/platform.h b/es-core/src/platform.h index 48b4a169b2..efc74417f6 100644 --- a/es-core/src/platform.h +++ b/es-core/src/platform.h @@ -31,4 +31,21 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo int quitES(const std::string& filename); void touch(const std::string& filename); + +#if !defined(TRACE) +#if defined(WIN32) && defined(_DEBUG) + #include + #include + + #define TRACE( s ) \ + { \ + std::ostringstream os_; \ + os_ << s << std::endl; \ + OutputDebugStringA( os_.str().c_str() ); \ + } +#else + #define TRACE(s) +#endif +#endif + #endif // ES_CORE_PLATFORM_H diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 76857841cb..7ab09f40e1 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -13,16 +13,6 @@ #define DPI 96 -#ifdef WIN32 -void traceOutput(std::string string) -{ -#if _DEBUG - OutputDebugStringA(string.c_str()); -#endif -} -#endif - - bool TextureData::OPTIMIZEVRAM = false; TextureData::TextureData(bool tile) : mTile(tile), mTextureID(0), mDataRGBA(nullptr), mScalable(false), @@ -214,9 +204,7 @@ bool TextureData::load() if (!mPath.empty()) { #ifdef WIN32 - char buffer[1000]; - sprintf_s(buffer, "TextureData::load(%s, %d)\n", mPath.c_str(), mTextureID); - traceOutput(buffer); + TRACE("TextureData::load(" << mPath << ", " << mTextureID << ")") #endif std::shared_ptr& rm = ResourceManager::getInstance(); @@ -335,13 +323,6 @@ void TextureData::setSourceSize(float width, float height) //if ((mSourceWidth != width) || (mSourceHeight != height)) if (mSourceHeight < height) // FCATMP { - /* -#ifdef WIN32 - char buffer[1000]; - sprintf_s(buffer, "TextureData::setSourceSize(%s, %d)\n", mPath.c_str(), mTextureID); - traceOutput(buffer); -#endif*/ - mSourceWidth = width; mSourceHeight = height; releaseVRAM(); diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 3d378c0567..a2aaee863e 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -82,6 +82,4 @@ class TextureData Vector2f mMaxSize; }; -void traceOutput(std::string string); - #endif // ES_CORE_RESOURCES_TEXTURE_DATA_H diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index 7f1cb7095b..94306277a6 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -148,32 +148,16 @@ void TextureDataManager::load(std::shared_ptr tex, bool block) if (cleanedMemory > 0) { lastTime = SDL_GetTicks() - lastTime; - - char buffer[1000]; - sprintf_s(buffer, "cleanedMemory : %d items in %d ms\n", cleanedMemory, lastTime); - traceOutput(buffer); + TRACE("cleanedMemory : " << cleanedMemory << " items in " << lastTime << " ms") } #endif if (!block) { - /* -#ifdef WIN32 - char buffer[1000]; - sprintf_s(buffer, "ASYNC LOAD : %s\n", tex->mPath.c_str()); - traceOutput(buffer); -#endif*/ - mLoader->load(tex); } else - { - /* -#ifdef WIN32 - char buffer[1000]; - sprintf_s(buffer, "SYNC LOAD : %s\n", tex->mPath.c_str()); - traceOutput(buffer); -#endif*/ + { mLoader->remove(tex); tex->load(); } diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 80c737962e..064a8955a9 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -1,6 +1,7 @@ #define _FILE_OFFSET_BITS 64 #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" #include "Settings.h" #include @@ -263,7 +264,7 @@ namespace Utils // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows char* envHome = getenv("HOME"); - // envHome = "H:/[Emulz]/EmulationStation/"; + envHome = "H:/[Emulz]/EmulationStation/"; #ifdef _DEBUG if (Utils::FileSystem::exists("H:/[Emulz]/EmulationStation/emulationstation.exe")) @@ -570,6 +571,12 @@ namespace Utils std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) { + if (_relativeTo.empty()) + return _path; + + if (_path == _relativeTo) + return ""; + bool contains = false; std::string path = removeCommonPath(_path, _relativeTo, contains); @@ -792,6 +799,59 @@ namespace Utils } // isHidden + std::string combine(const std::string& _path, const std::string& filename) + { + std::string gp = getGenericPath(_path); + + if (Utils::String::startsWith(filename, "/..")) + { + auto f = getPathList(filename); + + int count = 0; + for (auto it = f.cbegin(); it != f.cend(); ++it) + { + if (*it != "..") + break; + + count++; + } + + if (count > 0) + { + auto list = getPathList(gp); + std::vector p(list.begin(), list.end()); + + std::string result; + + for (int i = 0; i < p.size() - count; i++) + { + if (result.empty()) + result = p.at(i); + else + result = result + "/" + p.at(i); + } + + std::vector fn(f.begin(), f.end()); + for (int i = count; i < fn.size(); i++) + { + if (result.empty()) + result = fn.at(i); + else + result = result + "/" + fn.at(i); + } + + return result; + } + } + + + if (!Utils::String::endsWith(gp, "/") && !Utils::String::endsWith(gp, "\\")) + if (!Utils::String::startsWith(filename, "/") && !Utils::String::startsWith(filename, "\\")) + gp += "/"; + + return gp + filename; + } + } // FileSystem:: } // Utils:: diff --git a/es-core/src/utils/FileSystemUtil.h b/es-core/src/utils/FileSystemUtil.h index 8f995f7990..3eb611cd0e 100644 --- a/es-core/src/utils/FileSystemUtil.h +++ b/es-core/src/utils/FileSystemUtil.h @@ -39,6 +39,7 @@ namespace Utils std::string createRelativePath (const std::string& _path, const std::string& _relativeTo, const bool _allowHome); std::string removeCommonPath (const std::string& _path, const std::string& _common, bool& _contains); std::string resolveSymlink (const std::string& _path); + std::string combine(const std::string& _path, const std::string& filename); bool removeFile (const std::string& _path); bool createDirectory (const std::string& _path); bool exists (const std::string& _path); From 625012692eb49b5c273a8dbb92d62e19b2a48e3e Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 20 Jun 2019 18:50:48 +0200 Subject: [PATCH 040/336] Windows only - TextureResource : exlude splash.svg from reloadables which is managed manually. --- es-core/src/resources/TextureResource.cpp | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index b6c8feb672..f9d1097467 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -183,8 +183,11 @@ std::shared_ptr TextureResource::get(const std::string& path, b sTextureMap[key] = std::shared_ptr(tex); // FCAWEAK } - // Add it to the reloadable list - rm->addReloadable(tex); + // Add it to the reloadable list, exclusion for splash.svg manually managed +#ifdef WIN32 + if (path != ":/splash.svg") +#endif + rm->addReloadable(tex); if (data != nullptr) data->setMaxSize(maxSize); From c5f8219f2e976f2ceb130d3b7f07c1dff0f01075 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 21 Jun 2019 17:25:29 +0200 Subject: [PATCH 041/336] - Grid bugfixes & optimizations - Support for text glows ( glowColor and glowSize in text element ) --- es-app/src/FileData.cpp | 2 +- es-app/src/Gamelist.cpp | 41 +---- es-app/src/SystemData.cpp | 10 ++ es-app/src/guis/GuiGamelistOptions.cpp | 8 +- es-app/src/guis/GuiGamelistOptions.h | 2 + es-app/src/guis/GuiMenu.cpp | 110 ++++++++++---- es-app/src/views/ViewController.cpp | 9 +- .../src/views/gamelist/BasicGameListView.cpp | 5 +- .../src/views/gamelist/GridGameListView.cpp | 19 ++- es-core/src/ImageIO.cpp | 15 ++ es-core/src/ImageIO.h | 2 + es-core/src/Settings.cpp | 44 ++++-- es-core/src/Settings.h | 6 + es-core/src/ThemeData.cpp | 4 + es-core/src/components/GridTileComponent.cpp | 141 +++++++++++++----- es-core/src/components/GridTileComponent.h | 13 +- es-core/src/components/ImageComponent.cpp | 12 +- es-core/src/components/ImageGridComponent.h | 86 +++++++---- es-core/src/components/TextComponent.cpp | 71 ++++++++- es-core/src/components/TextComponent.h | 8 + es-core/src/components/VideoComponent.cpp | 11 +- es-core/src/components/VideoComponent.h | 4 - es-core/src/resources/ResourceManager.cpp | 2 +- es-core/src/resources/TextureData.cpp | 2 +- es-core/src/resources/TextureResource.cpp | 8 +- 25 files changed, 460 insertions(+), 175 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 90f8087a73..1be6ec0233 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -22,7 +22,7 @@ FileData::FileData(FileType type, const std::string& path, SystemData* system) { mPath = Utils::FileSystem::createRelativePath(path, getSystemEnvData()->mStartPath, false); - TRACE("FileData : " << mPath); +// TRACE("FileData : " << mPath); // metadata needs at least a name field (since that's what getName() will return) if (metadata.get("name").empty()) diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index cb1a9f1e7e..7ffd37964a 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -108,9 +108,6 @@ void parseGamelist(SystemData* system, std::unordered_mapsetSystemViewMode(root.attribute("defaultView").value(), gridSizeOverride, false); - std::string relativeTo = system->getStartPath(); for (pugi::xml_node fileNode : root.children()) @@ -191,9 +188,8 @@ void updateGamelist(SystemData* system) pugi::xml_node root; std::string xmlReadPath = system->getGamelistPath(false); - if(Utils::FileSystem::exists(xmlReadPath)) + if (Utils::FileSystem::exists(xmlReadPath)) { - //parse an existing file first pugi::xml_parse_result result = doc.load_file(xmlReadPath.c_str()); @@ -203,34 +199,7 @@ void updateGamelist(SystemData* system) return; } - root = doc.child("gameList"); - - std::string viewMode = root.attribute("defaultView").value(); - if (viewMode != system->getSystemViewMode()) - { - numUpdated++; - - if (system->getSystemViewMode().empty()) - root.remove_attribute("defaultView"); - else if (root.attribute("defaultView").empty()) - root.append_attribute("defaultView") = system->getSystemViewMode().c_str(); - else - root.attribute("defaultView") = system->getSystemViewMode().c_str(); - } - - Vector2f gridSize = Vector2f::parseString(root.attribute("gridSize").value()); - if (gridSize != system->getGridSizeOverride()) - { - numUpdated++; - - if (system->getGridSizeOverride() == Vector2f(0,0)) - root.remove_attribute("gridSize"); - else if (root.attribute("gridSize").empty()) - root.append_attribute("gridSize") = system->getGridSizeOverride().toString().c_str(); - else - root.attribute("gridSize") = system->getGridSizeOverride().toString().c_str(); - } - + root = doc.child("gameList"); if(!root) { LOG(LogError) << "Could not find node in gamelist \"" << xmlReadPath << "\"!"; @@ -239,12 +208,6 @@ void updateGamelist(SystemData* system) }else{ //set up an empty gamelist to append to root = doc.append_child("gameList"); - - if (!system->getSystemViewMode().empty()) - { - numUpdated++; - root.append_attribute("defaultView") = system->getSystemViewMode().c_str(); - } } diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index a4e7c70221..93e94142ad 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -62,6 +62,10 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys mRootFolder = new FolderData("" + name, this); } + auto defaultView = Settings::getInstance()->getString(getName() + ".defaultView"); + auto gridSizeOverride = Vector2f::parseString(Settings::getInstance()->getString(getName() + ".gridSize")); + setSystemViewMode(defaultView, gridSizeOverride, false); + setIsGameSystemStatus(); loadTheme(); } @@ -78,6 +82,12 @@ bool SystemData::setSystemViewMode(std::string newViewMode, Vector2f gridSizeOve mViewMode = newViewMode; mViewModeChanged = setChanged; + if (setChanged) + { + Settings::getInstance()->setString(getName() + ".defaultView", mViewMode); + Settings::getInstance()->setString(getName() + ".gridSize", Utils::String::replace(Utils::String::replace(mGridSizeOverride.toString(), ".000000", ""), "0 0", "")); + } + return true; } diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 2f50022817..87cc2b3cb1 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -13,7 +13,7 @@ #include "animations/LambdaAnimation.h" -std::vector gridSizes { +std::vector GuiGamelistOptions::gridSizes { "automatic", "2x2", @@ -307,8 +307,12 @@ GuiGamelistOptions::~GuiGamelistOptions() } } - if (mSystem->setSystemViewMode(mViewMode->getSelected(), gridSizeOverride) || mFiltersChanged) + bool viewModeChanged = mSystem->setSystemViewMode(mViewMode->getSelected(), gridSizeOverride); + if (viewModeChanged || mFiltersChanged) { + if (viewModeChanged) + Settings::getInstance()->saveFile(); + // only reload full view if we came from a placeholder // as we need to re-display the remaining elements for whatever new // game is selected diff --git a/es-app/src/guis/GuiGamelistOptions.h b/es-app/src/guis/GuiGamelistOptions.h index be6bfc7589..5d04361156 100644 --- a/es-app/src/guis/GuiGamelistOptions.h +++ b/es-app/src/guis/GuiGamelistOptions.h @@ -21,6 +21,8 @@ class GuiGamelistOptions : public GuiComponent virtual std::vector getHelpPrompts() override; virtual HelpStyle getHelpStyle() override; + static std::vector gridSizes; + private: void openGamelistFilter(); void openMetaDataEd(); diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 298336617d..09d267e926 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -21,6 +21,7 @@ #include "resources/TextureData.h" #include "animations/LambdaAnimation.h" +#include "GuiGamelistOptions.h" // grid sizes GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) { @@ -245,6 +246,7 @@ void GuiMenu::openUISettings() // theme set auto theme = ThemeData::getMenuTheme(); auto themeSets = ThemeData::getThemeSets(); + auto system = ViewController::get()->getState().getSystem(); if (!themeSets.empty()) { @@ -315,9 +317,7 @@ void GuiMenu::openUISettings() window->pushGui(new GuiMenu(window)); } }); - - - auto system = ViewController::get()->getState().getSystem(); + if (system != NULL && system->getTheme()->hasSubsets()) { @@ -466,43 +466,89 @@ void GuiMenu::openUISettings() } // GameList view style - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); - std::vector styles; - styles.push_back("automatic"); + if (system != NULL && !system->getTheme()->hasSubsets()) + { + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); + std::vector styles; + styles.push_back("automatic"); + auto system = ViewController::get()->getState().getSystem(); + if (system != NULL) + { + auto mViews = system->getTheme()->getViewsOfTheme(); + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + styles.push_back(*it); + } + else + { + styles.push_back("basic"); + styles.push_back("detailed"); + styles.push_back("video"); + styles.push_back("grid"); + } - auto system = ViewController::get()->getState().getSystem(); - if (system != NULL) - { - auto mViews = system->getTheme()->getViewsOfTheme(); - for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) - styles.push_back(*it); + auto viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + if (!system->getTheme()->hasView(viewPreference)) + viewPreference = "automatic"; + + for (auto it = styles.cbegin(); it != styles.cend(); it++) + gamelist_style->add(_L(*it), *it, viewPreference == *it); + + s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); + s->addSaveFunc([gamelist_style, viewPreference] { + bool needReload = false; + if (viewPreference != gamelist_style->getSelected()) + needReload = true; + Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); + if (needReload) + ViewController::get()->reloadAll(); + }); } - else + + // Default grid size + if (system != NULL && system->getTheme()->hasView("grid")) { - styles.push_back("basic"); - styles.push_back("detailed"); - styles.push_back("video"); - styles.push_back("grid"); - } + Vector2f gridOverride = Vector2f::parseString(Settings::getInstance()->getString("DefaultGridSize")); + auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); - auto viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); - if (!system->getTheme()->hasView(viewPreference)) - viewPreference == "automatic"; + auto mGridSize = std::make_shared>(mWindow, _T("DEFAULT GRID SIZE"), false); - for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_L(*it), *it, viewPreference == *it); + bool found = false; + for (auto it = GuiGamelistOptions::gridSizes.cbegin(); it != GuiGamelistOptions::gridSizes.cend(); it++) + { + bool sel = (gridOverride == Vector2f(0, 0) && *it == "automatic") || ovv == *it; + if (sel) + found = true; - s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); - s->addSaveFunc([gamelist_style, viewPreference] { - bool needReload = false; - if (viewPreference != gamelist_style->getSelected()) - needReload = true; - Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); - if (needReload) - ViewController::get()->reloadAll(); - }); + mGridSize->add(_L(*it), *it, sel); + } + if (!found) + mGridSize->selectFirstItem(); + + s->addWithLabel(_T("DEFAULT GRID SIZE"), mGridSize); + s->addSaveFunc([mGridSize] + { + std::string str = mGridSize->getSelected(); + std::string value = ""; + + size_t divider = str.find('x'); + if (divider != std::string::npos) + { + std::string first = str.substr(0, divider); + std::string second = str.substr(divider + 1, std::string::npos); + + Vector2f gridSizeOverride = Vector2f((float)atof(first.c_str()), (float)atof(second.c_str())); + value = Utils::String::replace(Utils::String::replace(gridSizeOverride.toString(), ".000000", ""), "0 0", ""); + } + + if (value != Settings::getInstance()->getString("DefaultGridSize")) + { + Settings::getInstance()->setString("DefaultGridSize", value); + ViewController::get()->reloadAll(); + } + }); + } //#ifndef WIN32 //UI mode diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 17cac4dee0..598b2a9476 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -304,15 +304,18 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste bool forceView = false; std::string viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); if (!system->getTheme()->hasView(viewPreference)) - viewPreference == "automatic"; + viewPreference = "automatic"; std::string customThemeName; - Vector2f gridSizeOverride = Vector2f(0,0); + Vector2f gridSizeOverride = Vector2f::parseString(Settings::getInstance()->getString("DefaultGridSize")); + + Vector2f bySystemGridOverride = system->getGridSizeOverride(); //Vector2f(0,0); + if (bySystemGridOverride != Vector2f(0, 0)) + gridSizeOverride = bySystemGridOverride; if (!system->getSystemViewMode().empty() && system->getTheme()->hasView(system->getSystemViewMode())) { viewPreference = system->getSystemViewMode(); - gridSizeOverride = system->getGridSizeOverride(); forceView = true; } diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index f6f8e98a6a..b139273d91 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -16,16 +16,19 @@ BasicGameListView::BasicGameListView(Window* window, FolderData* root) mList.setPosition(0, mSize.y() * 0.2f); mList.setDefaultZIndex(20); addChild(&mList); + + populateList(mRoot->getChildrenListToDisplay()); } void BasicGameListView::onShow() { + /* if (!mLoaded) { populateList(mRoot->getChildrenListToDisplay()); mLoaded = true; } - + */ ISimpleGameListView::onShow(); } diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 290354f4e3..a4d7f760e8 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -95,6 +95,9 @@ GridGameListView::GridGameListView(Window* window, FolderData* root, const std:: setThemeName(themeName); setTheme(theme); + + populateList(mRoot->getChildrenListToDisplay()); + updateInfoPanel(); } void GridGameListView::createVideo() @@ -118,19 +121,13 @@ void GridGameListView::createVideo() mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); mVideo->setStartDelay(2000); mVideo->setDefaultZIndex(31); - addChild(mVideo); + addChild(mVideo); } void GridGameListView::onShow() { - if (!mLoaded) - { - populateList(mRoot->getChildrenListToDisplay()); - updateInfoPanel(); - mLoaded = true; - } - ISimpleGameListView::onShow(); + updateInfoPanel(); } @@ -396,12 +393,18 @@ void GridGameListView::updateInfoPanel() { FileData* file = (mGrid.size() == 0 || mGrid.isScrolling()) ? NULL : mGrid.getSelected(); + TRACE("GridGameListView::updateInfoPanel " << (file ? "null" : "ok")); + bool fadingOut; if (file == NULL) { if (mVideo != nullptr) + { mVideo->setVideo(""); + mVideo->setImage(""); + } + mImage.setImage(""); // mVideo->setImage(""); // mDescription.setText(""); fadingOut = true; diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index e7b8e1690f..085b7ed9d0 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -114,6 +114,21 @@ Vector2i ImageIO::adjustPictureSize(Vector2i imageSize, Vector2i maxSize) return Vector2i(cxDIB, cyDIB); } +Vector2f ImageIO::adjustExternPictureSizef(Vector2f imageSize, Vector2f maxSize) +{ + float cxDIB = maxSize.x(); + float cyDIB = maxSize.y(); + + float xCoef = maxSize.x() / imageSize.x(); + float yCoef = maxSize.y() / imageSize.y(); + + if (imageSize.x() * yCoef < maxSize.x()) + cyDIB = imageSize.y() * xCoef; + else + cxDIB = imageSize.x() * yCoef; + + return Vector2f(cxDIB, cyDIB); +} unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight, Vector2i& baseSize, Vector2i& packedSize) { diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index 40f371ec1b..820c60824f 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -6,6 +6,7 @@ #include #include "math/Vector2i.h" +#include "math/Vector2f.h" class ImageIO { @@ -15,6 +16,7 @@ class ImageIO static void flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height); static Vector2i adjustPictureSize(Vector2i imageSize, Vector2i maxSize); + static Vector2f adjustExternPictureSizef(Vector2f imageSize, Vector2f maxSize); }; #endif // ES_CORE_IMAGE_IO diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 5eb83fa380..8b49393375 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -9,6 +9,7 @@ #include Settings* Settings::sInstance = NULL; +static std::string mEmptyString = ""; // these values are NOT saved to es_settings.xml // since they're set through command-line arguments, and not the in-program settings menu @@ -115,6 +116,7 @@ void Settings::setDefaults() mStringMap["ScreenSaverBehavior"] = "dim"; mStringMap["Scraper"] = "TheGamesDB"; mStringMap["GamelistViewStyle"] = "automatic"; + mStringMap["DefaultGridSize"] = ""; mStringMap["ThemeColorSet"] = ""; mStringMap["ThemeIconSet"] = ""; @@ -184,10 +186,16 @@ void Settings::setDefaults() mIntMap["ScreenRotate"] = 0; mStringMap["ExePath"] = ""; + + + mDefaultBoolMap = mBoolMap; + mDefaultIntMap = mIntMap; + mDefaultFloatMap = mFloatMap; + mDefaultStringMap = mStringMap; } template -void saveMap(pugi::xml_document& doc, std::map& map, const char* type) +void saveMap(pugi::xml_document& doc, std::map& map, const char* type, std::map& defaultMap) { for(auto iter = map.cbegin(); iter != map.cend(); iter++) { @@ -195,6 +203,10 @@ void saveMap(pugi::xml_document& doc, std::map& map, const char* type) if(std::find(settings_dont_save.cbegin(), settings_dont_save.cend(), iter->first) != settings_dont_save.cend()) continue; + auto def = defaultMap.find(iter->first); + if (def != defaultMap.cend() && def->second == iter->second) + continue; + pugi::xml_node node = doc.append_child(type); node.append_attribute("name").set_value(iter->first.c_str()); node.append_attribute("value").set_value(iter->second); @@ -208,13 +220,24 @@ void Settings::saveFile() pugi::xml_document doc; - saveMap(doc, mBoolMap, "bool"); - saveMap(doc, mIntMap, "int"); - saveMap(doc, mFloatMap, "float"); + saveMap(doc, mBoolMap, "bool", mDefaultBoolMap); + saveMap(doc, mIntMap, "int", mDefaultIntMap); + saveMap(doc, mFloatMap, "float", mDefaultFloatMap); //saveMap(doc, mStringMap, "string"); for(auto iter = mStringMap.cbegin(); iter != mStringMap.cend(); iter++) { + // key is on the "don't save" list, so don't save it + if (std::find(settings_dont_save.cbegin(), settings_dont_save.cend(), iter->first) != settings_dont_save.cend()) + continue; + + auto def = mDefaultStringMap.find(iter->first); + if (def == mDefaultStringMap.cend() && iter->second.empty()) + continue; + + if (def != mDefaultStringMap.cend() && def->second == iter->second) + continue; + pugi::xml_node node = doc.append_child("string"); node.append_attribute("name").set_value(iter->first.c_str()); node.append_attribute("value").set_value(iter->second.c_str()); @@ -252,11 +275,12 @@ void Settings::loadFile() } //Print a warning message if the setting we're trying to get doesn't already exist in the map, then return the value in the map. -#define SETTINGS_GETSET(type, mapName, getMethodName, setMethodName) type Settings::getMethodName(const std::string& name) \ +#define SETTINGS_GETSET(type, mapName, getMethodName, setMethodName, defaultValue) type Settings::getMethodName(const std::string& name) \ { \ if(mapName.find(name) == mapName.cend()) \ { \ - LOG(LogError) << "Tried to use unset setting " << name << "!"; \ + /*LOG(LogError) << "Tried to use unset setting " << name << "!";*/ \ + return defaultValue; \ } \ return mapName[name]; \ } \ @@ -265,7 +289,7 @@ void Settings::setMethodName(const std::string& name, type value) \ mapName[name] = value; \ } -SETTINGS_GETSET(bool, mBoolMap, getBool, setBool); -SETTINGS_GETSET(int, mIntMap, getInt, setInt); -SETTINGS_GETSET(float, mFloatMap, getFloat, setFloat); -SETTINGS_GETSET(const std::string&, mStringMap, getString, setString); +SETTINGS_GETSET(bool, mBoolMap, getBool, setBool, false); +SETTINGS_GETSET(int, mIntMap, getInt, setInt, 0); +SETTINGS_GETSET(float, mFloatMap, getFloat, setFloat, 0.0f); +SETTINGS_GETSET(const std::string&, mStringMap, getString, setString, mEmptyString); diff --git a/es-core/src/Settings.h b/es-core/src/Settings.h index 413694b719..8050879d55 100644 --- a/es-core/src/Settings.h +++ b/es-core/src/Settings.h @@ -36,6 +36,12 @@ class Settings std::map mIntMap; std::map mFloatMap; std::map mStringMap; + + + std::map mDefaultBoolMap; + std::map mDefaultIntMap; + std::map mDefaultFloatMap; + std::map mDefaultStringMap; }; #endif // ES_CORE_SETTINGS_H diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 8957c4e2ee..1fdcf69b99 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -73,6 +73,10 @@ std::map> The { "forceUppercase", BOOLEAN }, { "lineSpacing", FLOAT }, { "value", STRING }, + + { "glowColor", COLOR }, + { "glowSize", FLOAT }, + { "zIndex", FLOAT } } }, { "textlist", { { "pos", NORMALIZED_PAIR }, diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 9229227416..fdcd8ed52d 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -7,7 +7,7 @@ #include #include "animations/LambdaAnimation.h" - +#include "ImageIO.h" #ifdef _RPI_ #include "components/VideoPlayerComponent.h" @@ -42,14 +42,26 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mSelectedProperties.mBackgroundCornerSize = mDefaultProperties.mBackgroundCornerSize; mSelectedProperties.mBackgroundCenterColor = 0xFFFFFFFF; mSelectedProperties.mBackgroundEdgeColor = 0xFFFFFFFF; + + mDefaultProperties.mLabelSize = Vector2f(1.0, 0.30); mDefaultProperties.mLabelColor = 0xFFFFFFFF; mDefaultProperties.mLabelBackColor = 0; - + mDefaultProperties.mLabelGlowColor = 0; + mDefaultProperties.mLabelGlowSize = 2; + + mDefaultProperties.mFontPath = ""; + mDefaultProperties.mFontSize = 0; + mSelectedProperties.mLabelSize = Vector2f(1.0, 0.30); mSelectedProperties.mLabelColor = 0xFFFFFFFF; mSelectedProperties.mLabelBackColor = 0; + mSelectedProperties.mLabelGlowColor = 0; + mSelectedProperties.mLabelGlowSize = 2; + + mSelectedProperties.mFontPath = ""; + mSelectedProperties.mFontSize = 0; mImage = std::make_shared(mWindow); mImage->setOrigin(0.5f, 0.5f); @@ -103,6 +115,11 @@ void GridTileComponent::resize() mLabel.setColor(currentProperties.mLabelColor); mLabel.setBackgroundColor(currentProperties.mLabelBackColor); + mLabel.setGlowColor(currentProperties.mLabelGlowColor); + mLabel.setGlowSize(currentProperties.mLabelGlowSize); + + if (mDefaultProperties.mFontPath != mSelectedProperties.mFontPath || mDefaultProperties.mFontSize != mSelectedProperties.mFontSize) + mLabel.setFont(currentProperties.mFontPath, currentProperties.mFontSize); if (mLabelMerged) { @@ -153,7 +170,7 @@ void GridTileComponent::resize() } } - if (mVideo != nullptr) + if (mVideo != nullptr && mVideo->isPlaying()) { mVideo->setOrigin(0.5f, 0.5f); mVideo->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); @@ -161,28 +178,10 @@ void GridTileComponent::resize() if (currentProperties.mImageSizeMode == "minSize") { auto vs = mVideo->getVideoSize(); + if (vs == Vector2f(0, 0)) + vs = Vector2f(640, 480); - double prop = vs == Vector2f(0,0) ? 640.0 / 480.0 : vs.x() / vs.y(); - double imgprop = imageWidth / imageHeight; - - if (prop < imgprop) - { - double h = imageWidth / prop; // suppose video is 4:3 - - if (h < imageHeight) - h = imageHeight; - - mVideo->setSize(imageWidth, h); - } - else - { - double w = imageHeight / prop; // suppose video is 4:3 - - if (w < imageWidth) - w = imageWidth; - - mVideo->setSize(w, imageHeight); - } + mVideo->setSize(ImageIO::adjustExternPictureSizef(vs, Vector2f(imageWidth, imageHeight))); } else if (currentProperties.mImageSizeMode == "size") @@ -232,6 +231,14 @@ void GridTileComponent::resize() mBackground.setImagePath(currentProperties.mBackgroundImage); } +void GridTileComponent::update(int deltaTime) +{ + GuiComponent::update(deltaTime); + + if ((mVideo != nullptr && mVideo->isPlaying())) + resize(); +} + void GridTileComponent::renderBackground(const Transform4x4f& parentTrans) { if (!mVisible) @@ -378,6 +385,8 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons elem = theme->getElement(view, "gridtile", "text"); if (elem != NULL) { + float sh = (float)Renderer::getScreenHeight(); + if (elem && elem->has("size")) { mDefaultProperties.mLabelSize = elem->get("size"); @@ -397,6 +406,30 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mSelectedProperties.mLabelBackColor = mDefaultProperties.mLabelBackColor; } + if (elem && elem->has("glowSize")) + { + mDefaultProperties.mLabelGlowSize = (unsigned int) elem->get("glowSize"); + mSelectedProperties.mLabelGlowSize = mDefaultProperties.mLabelGlowSize; + } + + if (elem && elem->has("glowColor")) + { + mDefaultProperties.mLabelGlowColor = elem->get("glowColor"); + mSelectedProperties.mLabelGlowColor = mDefaultProperties.mLabelGlowColor; + } + + if (elem && elem->has("fontSize")) + { + mDefaultProperties.mFontSize = elem->get("fontSize") * sh; + mSelectedProperties.mFontSize = mDefaultProperties.mFontSize; + } + + if (elem && elem->has("fontPath")) + { + mDefaultProperties.mFontPath = elem->get("fontPath"); + mSelectedProperties.mFontPath = mDefaultProperties.mFontPath; + } + mLabelVisible = true; mLabel.applyTheme(theme, view, element, properties); @@ -411,6 +444,18 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem && elem->has("backgroundColor")) mSelectedProperties.mLabelBackColor = elem->get("backgroundColor"); + + if (elem && elem->has("glowSize")) + mSelectedProperties.mLabelGlowSize = (unsigned int)elem->get("glowSize"); + + if (elem && elem->has("glowColor")) + mSelectedProperties.mLabelGlowColor = elem->get("glowColor"); + + if (elem && elem->has("fontSize")) + mSelectedProperties.mFontSize = elem->get("fontSize") * sh; + + if (elem && elem->has("fontPath")) + mSelectedProperties.mFontPath = elem->get("fontPath"); } } else @@ -498,12 +543,12 @@ void GridTileComponent::onHide() mShown = false; } -void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f* pPosition) +void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f* pPosition, bool force) { if (!mShown || !GuiComponent::ALLOWANIMATIONS) allowAnimation = false; - if (mSelected == selected) + if (mSelected == selected && !force) { if (mSelected && mVideo != nullptr) mVideo->setVideo(mVideoPath); @@ -511,22 +556,24 @@ void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f return; } - mSelected = selected; - + mSelected = selected; + if (!mSelected && mVideo != nullptr) mVideo->setVideo(""); if (selected) { if (pPosition == NULL || !allowAnimation) - { + { cancelAnimation(3); - + this->setSelectedZoom(1); mAnimPosition = Vector3f(0, 0, 0); if (mVideo != NULL) - mVideo->setVideo(mVideoPath); + mVideo->setVideo(mVideoPath); + + resize(); } else { @@ -558,13 +605,18 @@ void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f { if (!allowAnimation) { + cancelAnimation(3); this->setSelectedZoom(0); if (mVideo != NULL) mVideo->setVideo(""); + + resize(); } else { + this->setSelectedZoom(1); + if (mVideo != NULL) mVideo->setVideo(""); @@ -574,7 +626,7 @@ void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f float pct = Math::lerp(0, 1, t*t*t + 1); this->setSelectedZoom(1.0 - pct); }; - + cancelAnimation(3); setAnimation(new LambdaAnimation(func, 250), 0, [this] { this->setSelectedZoom(0); @@ -585,6 +637,9 @@ void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f void GridTileComponent::setSelectedZoom(float percent) { + if (mSelectedZoomPercent == percent) + return; + mSelectedZoomPercent = percent; resize(); } @@ -643,10 +698,24 @@ const GridTileProperties& GridTileComponent::getCurrentProperties() } if (mDefaultProperties.mLabelSize != mSelectedProperties.mLabelSize) - { - float y = mDefaultProperties.mLabelSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mLabelSize.y() * mSelectedZoomPercent; - mMixedProperties.mLabelSize = Vector2f(mDefaultProperties.mLabelSize.x(), y); - } + mMixedProperties.mLabelSize = Vector2f(mDefaultProperties.mLabelSize.x(), + mDefaultProperties.mLabelSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mLabelSize.y() * mSelectedZoomPercent); + + if (mDefaultProperties.mLabelColor != mSelectedProperties.mLabelColor) + mMixedProperties.mLabelColor = mixColors(mDefaultProperties.mLabelColor, mSelectedProperties.mLabelColor, mSelectedZoomPercent); + + if (mDefaultProperties.mLabelBackColor != mSelectedProperties.mLabelBackColor) + mMixedProperties.mLabelBackColor = mixColors(mDefaultProperties.mLabelBackColor, mSelectedProperties.mLabelBackColor, mSelectedZoomPercent); + + if (mDefaultProperties.mLabelGlowColor != mSelectedProperties.mLabelGlowColor) + mMixedProperties.mLabelGlowColor = mixColors(mDefaultProperties.mLabelGlowColor, mSelectedProperties.mLabelGlowColor, mSelectedZoomPercent); + + if (mDefaultProperties.mLabelGlowSize != mSelectedProperties.mLabelGlowSize) + mMixedProperties.mLabelGlowSize = mDefaultProperties.mLabelGlowSize * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mLabelGlowSize * mSelectedZoomPercent; + +// Avoid to multiply font sizes in mem + it create strange sizings +// if (mDefaultProperties.mFontSize != mSelectedProperties.mFontSize) + // mMixedProperties.mFontSize = mDefaultProperties.mFontSize * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mFontSize * mSelectedZoomPercent; return mMixedProperties; } diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index 0d8401b53f..0576b7ac8f 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -24,6 +24,16 @@ struct GridTileProperties Vector2f mLabelSize; unsigned int mLabelColor; unsigned int mLabelBackColor; + + unsigned int mLabelGlowColor; + unsigned int mLabelGlowSize; + + std::string mFontPath; + unsigned int mFontSize; + + /* + . / main / fonts / Dosis - Bold.ttf< / fontPath> + 0.025< / fontSize>*/ }; class GridTileComponent : public GuiComponent @@ -49,7 +59,7 @@ class GridTileComponent : public GuiComponent void setImage(const std::string& path); // void setImage(const std::shared_ptr& texture, std::string name); - void setSelected(bool selected, bool allowAnimation = true, Vector3f* pPosition = NULL); + void setSelected(bool selected, bool allowAnimation = true, Vector3f* pPosition = NULL, bool force=false); void setVisible(bool visible); void forceSize(Vector2f size, float selectedZoom = 1.0); @@ -63,6 +73,7 @@ class GridTileComponent : public GuiComponent virtual void onShow(); virtual void onHide(); + virtual void update(int deltaTime); private: void resize(); diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 792639e4fa..7888b2f051 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -6,6 +6,8 @@ #include "Settings.h" #include "ThemeData.h" +#include "resources/TextureData.h" + Vector2i ImageComponent::getTextureSize() const { if(mTexture) @@ -365,6 +367,12 @@ void ImageComponent::render(const Transform4x4f& parentTrans) // when it finally loads fadeIn(mTexture->bind()); + GLfloat color[16] = { + 0.5,0,0,0, + 0,1,0,0, + 0,0,1,0, + 0,0,0,1 }; + glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); @@ -376,9 +384,9 @@ void ImageComponent::render(const Transform4x4f& parentTrans) glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].pos); glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].tex); glColorPointer(4, GL_UNSIGNED_BYTE, 0, mColors); - + glDrawArrays(GL_TRIANGLES, 0, 6); - + glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); glDisableClientState(GL_COLOR_ARRAY); diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index d4debc24bd..af4cf4b516 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -78,8 +78,8 @@ class ImageGridComponent : public IList private: // TILES void buildTiles(); - void updateTiles(bool ascending = true, bool allowAnimation = true); - void updateTileAtPos(int tilePos, int imgPos, bool allowAnimation = true); + void updateTiles(bool ascending = true, bool allowAnimation = true, bool updateSelectedState = true); + void updateTileAtPos(int tilePos, int imgPos, bool allowAnimation = true, bool updateSelectedState = true); void calcGridDimension(); bool isVertical() { return mScrollDirection == SCROLL_VERTICALLY || mScrollDirection == SCROLL_VERTICALLY_CENTER; }; @@ -468,8 +468,19 @@ void ImageGridComponent::onSizeChanged() template void ImageGridComponent::onCursorChanged(const CursorState& state) { + + if (mLastCursor == mCursor) + { + if (state == CURSOR_STOPPED && mCursorChangedCallback) + mCursorChangedCallback(state); + + TRACE("skip ImageGridComponent::onCursorChanged " << state); + return; + } + + TRACE("ImageGridComponent::onCursorChanged " << state); bool centerSel = (mScrollDirection == SCROLL_HORIZONTALLY_CENTER || mScrollDirection == SCROLL_VERTICALLY_CENTER); @@ -500,9 +511,9 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) startPos = -1; cancelAnimation(2); - updateTiles(direction, false); + updateTiles(direction, false, !GuiComponent::ALLOWANIMATIONS); } - + if (GuiComponent::ALLOWANIMATIONS) { std::shared_ptr oldTile = nullptr; @@ -516,18 +527,27 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) if (newIdx >= 0 && newIdx < mTiles.size()) newTile = mTiles[newIdx]; + for (auto it = mTiles.begin(); it != mTiles.end(); it++) + { + if ((*it)->isSelected() && *it != oldTile && *it != newTile) + { + startPos = 0; + (*it)->setSelected(false, false, nullptr); + } + } + Vector3f oldPos = Vector3f(0, 0); - if (oldTile != nullptr) + if (oldTile != nullptr && oldTile != newTile) { oldPos = oldTile->getBackgroundPosition(); - oldTile->setSelected(false); + oldTile->setSelected(false, true, nullptr, true); } if (newTile != nullptr) - newTile->setSelected(true, true, oldPos == Vector3f(0, 0) ? nullptr : &oldPos); + newTile->setSelected(true, true, oldPos == Vector3f(0, 0) ? nullptr : &oldPos, true); } - + int firstVisibleCol = mStartPosition / dimOpposite; if ((col < centralCol || (col == 0 && col == centralCol)) && !centerSel) @@ -555,9 +575,9 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) mCameraDirection = direction ? -1.0 : 1.0; mCamera = 0; - if (lastCursor < 0 || oldStart == mStartPosition || !GuiComponent::ALLOWANIMATIONS) + if (lastCursor < 0 || !GuiComponent::ALLOWANIMATIONS) { - updateTiles(direction, true); + updateTiles(direction, lastCursor >= 0 && GuiComponent::ALLOWANIMATIONS); if (mCursorChangedCallback) mCursorChangedCallback(state); @@ -568,8 +588,13 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) if (mCursorChangedCallback) mCursorChangedCallback(state); - auto func = [this, startPos, endPos](float t) + bool moveCamera = (oldStart != mStartPosition); + + auto func = [this, startPos, endPos, moveCamera](float t) { + if (!moveCamera) + return; + t -= 1; // cubic ease out float pct = Math::lerp(0, 1, t*t*t + 1); t = startPos * (1.0 - pct) + endPos * pct; @@ -578,16 +603,14 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) }; setAnimation(new LambdaAnimation(func, 250), 0, [this, direction] { - mCamera = 0; updateTiles(direction, false); - }, false, 2); } template -void ImageGridComponent::updateTiles(bool ascending, bool allowAnimation) +void ImageGridComponent::updateTiles(bool ascending, bool allowAnimation, bool updateSelectedState = true) { if (!mTiles.size()) return; @@ -620,7 +643,7 @@ void ImageGridComponent::updateTiles(bool ascending, bool allowAnimation) while (i != end) { - updateTileAtPos(i, img, allowAnimation); + updateTileAtPos(i, img, allowAnimation, updateSelectedState); i--; img--; } } @@ -637,25 +660,29 @@ void ImageGridComponent::updateTiles(bool ascending, bool allowAnimation) while (i != end) { - updateTileAtPos(i, img, allowAnimation); + updateTileAtPos(i, img, allowAnimation, updateSelectedState); i++; img++; } } - mLastCursor = mCursor; + if (updateSelectedState) + mLastCursor = mCursor; + mEntriesDirty = false; } template -void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowAnimation) +void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowAnimation, bool updateSelectedState) { std::shared_ptr tile = mTiles.at(tilePos); // If we have more tiles than we have to display images on screen, hide them if(imgPos < 0 || imgPos >= size() || tilePos < 0 || tilePos >= (int) mTiles.size()) // Same for tiles out of the buffer { - tile->setSelected(false, allowAnimation); + if (updateSelectedState) + tile->setSelected(false, allowAnimation); + tile->reset(); tile->setVisible(false); } @@ -675,7 +702,7 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA else tile->setImage(mDefaultGameTexture); - if (mAllowVideo && tile->isSelected()) + if (mAllowVideo && imgPos == mCursor) { std::string videoPath = mEntries.at(imgPos).data.videoPath; @@ -689,18 +716,21 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA else tile->setVideo(""); - if (imgPos == mCursor && mCursor != mLastCursor) + if (updateSelectedState) { - int dif = mCursor - tilePos; - int idx = mLastCursor - dif; + if (imgPos == mCursor && mCursor != mLastCursor) + { + int dif = mCursor - tilePos; + int idx = mLastCursor - dif; - if (idx < 0 || idx >= mTiles.size()) - idx = 0; + if (idx < 0 || idx >= mTiles.size()) + idx = 0; - tile->setSelected(true, allowAnimation, &mTiles.at(idx)->getBackgroundPosition()); + tile->setSelected(true, allowAnimation, &mTiles.at(idx)->getBackgroundPosition()); + } + else + tile->setSelected(imgPos == mCursor, allowAnimation); } - else - tile->setSelected(imgPos == mCursor, allowAnimation); } } diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index cd0949018f..8b7612dd9f 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -8,7 +8,7 @@ TextComponent::TextComponent(Window* window) : GuiComponent(window), mFont(Font::get(FONT_SIZE_MEDIUM)), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), mHorizontalAlignment(ALIGN_LEFT), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), - mRenderBackground(false) + mRenderBackground(false), mGlowColor(0), mGlowSize(2) { } @@ -16,7 +16,7 @@ TextComponent::TextComponent(Window* window, const std::string& text, const std: Vector3f pos, Vector2f size, unsigned int bgcolor) : GuiComponent(window), mFont(NULL), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), mHorizontalAlignment(align), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), - mRenderBackground(false) + mRenderBackground(false), mGlowColor(0), mGlowSize(2) { setFont(font); setColor(color); @@ -34,10 +34,27 @@ void TextComponent::onSizeChanged() void TextComponent::setFont(const std::shared_ptr& font) { + if (mFont == font) + return; + mFont = font; onTextChanged(); } +void TextComponent::setFont(std::string path, int size) +{ + std::shared_ptr font; + int fontSize = size > 0 ? size : (mFont ? mFont->getSize() : FONT_SIZE_MEDIUM); + std::string fontPath = !path.empty() ? path : (mFont ? mFont->getPath() : Font::getDefaultPath()); + + font = Font::get(fontSize, fontPath); + if (mFont != font) + { + mFont = font; + onTextChanged(); + } +} + // Set the color of the font/text void TextComponent::setColor(unsigned int color) { @@ -141,6 +158,45 @@ void TextComponent::render(const Transform4x4f& parentTrans) Renderer::drawRect(0.f, 0.f, mSize.x(), mSize.y(), 0xFF000033); } + if ((mGlowColor & 0x000000FF) != 0 && mGlowSize > 0) + { + auto draw = [this, off, yOff, parentTrans](int margin) + { + auto func = [this, off, yOff, parentTrans](float x, float y) + { + Vector3f off = Vector3f(x, yOff + y, 0); + Transform4x4f trans = parentTrans * getTransform(); + + trans.translate(off); + trans.round(); + + Renderer::setMatrix(trans); + + mTextCache->setColor(mGlowColor); + mFont->renderTextCache(mTextCache.get()); + mTextCache->setColor(mColor); + }; + + int x = -margin; + int y = -margin; + func(x, y); + + for (int i = 0; i < 2 * margin; i++) + func(++x, y); + + for (int i = 0; i < 2 * margin; i++) + func(x, ++y); + + for (int i = 0; i < 2 * margin; i++) + func(--x, y); + + for (int i = 0; i < 2 * margin; i++) + func(x, --y); + }; + + draw(mGlowSize); + } + trans.translate(off); trans.round(); Renderer::setMatrix(trans); @@ -163,6 +219,8 @@ void TextComponent::render(const Transform4x4f& parentTrans) } mFont->renderTextCache(mTextCache.get()); + + } } @@ -300,5 +358,14 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, const st if(properties & LINE_SPACING && elem->has("lineSpacing")) setLineSpacing(elem->get("lineSpacing")); + if (properties & COLOR && elem->has("color")) + setColor(elem->get("color")); + + if (properties & COLOR && elem->has("glowColor")) + mGlowColor = elem->get("glowColor"); + + if (properties & COLOR && elem->has("glowSize")) + mGlowSize = (int) elem->get("glowSize"); + setFont(Font::getFromTheme(elem, properties, mFont)); } diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index 8f76273985..ff03a45274 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -44,6 +44,11 @@ class TextComponent : public GuiComponent virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; + void setGlowColor(unsigned int color) { mGlowColor = color; }; + void setGlowSize(unsigned int size) { mGlowSize = size; }; + + void setFont(std::string path, int size); + protected: virtual void onTextChanged(); @@ -67,6 +72,9 @@ class TextComponent : public GuiComponent Alignment mHorizontalAlignment; Alignment mVerticalAlignment; float mLineSpacing; + + unsigned int mGlowColor; + unsigned int mGlowSize; }; #endif // ES_CORE_COMPONENTS_TEXT_COMPONENT_H diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 067103bb6a..07780738fa 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -103,6 +103,9 @@ void VideoComponent::onSizeChanged() bool VideoComponent::setVideo(std::string path) { + if (path == mVideoPath) + return !path.empty(); + // Convert the path into a generic format std::string fullPath = Utils::FileSystem::getCanonicalPath(path); @@ -172,9 +175,13 @@ void VideoComponent::renderSnapshot(const Transform4x4f& parentTrans) float t = 1.0 - mFadeIn; t -= 1; // cubic ease out t = Math::lerp(0, 1, t*t*t + 1); + t = (t * (float)mOpacity); - mStaticImage.setOpacity((unsigned char)(t * 255.0f)); - mStaticImage.render(parentTrans); + if (t == 0.0) + return; + + mStaticImage.setOpacity((unsigned char)t); + mStaticImage.render(parentTrans); } } diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index 45c236d100..87675eaafd 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -81,10 +81,6 @@ class VideoComponent : public GuiComponent return mIsWaitingForVideoToStart; } - bool isFading() { - return mFadeIn < 1.0f; - } - void onVideoStarted(); private: diff --git a/es-core/src/resources/ResourceManager.cpp b/es-core/src/resources/ResourceManager.cpp index 3f5035b6e7..f06603a6b5 100644 --- a/es-core/src/resources/ResourceManager.cpp +++ b/es-core/src/resources/ResourceManager.cpp @@ -99,7 +99,7 @@ void ResourceManager::unloadAll() while(iter != mReloadables.cend()) { if (!iter->expired()) - { + { iter->lock()->unload(); iter++; }else{ diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 7ab09f40e1..36d3a945d8 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -204,7 +204,7 @@ bool TextureData::load() if (!mPath.empty()) { #ifdef WIN32 - TRACE("TextureData::load(" << mPath << ", " << mTextureID << ")") + //TRACE("TextureData::load(" << mPath << ", " << mTextureID << ")") #endif std::shared_ptr& rm = ResourceManager::getInstance(); diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index b6c8feb672..bf64eba249 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -183,8 +183,12 @@ std::shared_ptr TextureResource::get(const std::string& path, b sTextureMap[key] = std::shared_ptr(tex); // FCAWEAK } - // Add it to the reloadable list - rm->addReloadable(tex); + + // Add it to the reloadable list, exclusion for splash.svg manually managed +#ifdef WIN32 + if (path != ":/splash.svg") +#endif + rm->addReloadable(tex); if (data != nullptr) data->setMaxSize(maxSize); From a9f4b473fb451e04c77b87d7ca537a6afc62abdf Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Fri, 21 Jun 2019 17:28:26 +0200 Subject: [PATCH 042/336] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c0899f110e..86ba62b633 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,7 @@ Changes in my branch - Carousel supports element "logoPos" : this allows the logo not to be inevitably centered. - Image loading : the image bytes where duplicated 3 times in memory. - In previous versions, if a xml element was unknown in the theme, nothing was loaded. +- Support for glows around text **Optimizations & Fixes:** - Faster loading time, using multithreading. From 04557c51396691043da502aa25858bd4c67d1840 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Fri, 21 Jun 2019 17:30:21 +0200 Subject: [PATCH 043/336] Update THEMES.md --- THEMES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/THEMES.md b/THEMES.md index 75274239ca..3a01d422c7 100644 --- a/THEMES.md +++ b/THEMES.md @@ -717,6 +717,10 @@ Can be created as an extra. * `lineSpacing` - type: FLOAT. Controls the space between lines (as a multiple of font height). Default is 1.5. * `zIndex` - type: FLOAT. - z-index value for component. Components will be rendered in order of z-index value from low to high. +* `glowColor` - type: COLOR; + - NEW : Defines the color of the glow around the text. +* `glowSize` - type: FLOAT. + - NEW : Defines the size of the glow around the text. #### textlist From 5d19a8737ab0bef631778ce9c240402a5391680f Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 21 Jun 2019 19:01:06 +0200 Subject: [PATCH 044/336] Resources & french locale update --- es-app/src/guis/GuiMenu.cpp | 2124 +++++++++++----------- resources/OpenSans-CondBold.ttf | Bin 0 -> 264372 bytes resources/OpenSans-CondLight.ttf | Bin 0 -> 221108 bytes resources/arrow.svg | 5 +- resources/button.png | Bin 1231 -> 356 bytes resources/button_filled.png | Bin 1168 -> 275 bytes resources/checkbox_checked.svg | 7 +- resources/checkbox_unchecked.svg | 5 +- resources/fav_add.svg | 6 +- resources/fav_remove.svg | 5 +- resources/glyphs.ttf | Bin 1904 -> 1904 bytes resources/locale/fr/emulationstation2.po | 218 +-- resources/locale/it/emulationstation2.po | 1884 ------------------- resources/off.svg | 10 +- resources/on.svg | 6 +- resources/option_arrow.svg | 5 +- resources/slider_knob.svg | 9 +- resources/star_filled.svg | 5 +- resources/star_unfilled.svg | 5 +- 19 files changed, 1131 insertions(+), 3163 deletions(-) create mode 100644 resources/OpenSans-CondBold.ttf create mode 100644 resources/OpenSans-CondLight.ttf delete mode 100644 resources/locale/it/emulationstation2.po diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 09d267e926..57b6bee889 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -1,1064 +1,1064 @@ -#include "guis/GuiMenu.h" - -#include "components/OptionListComponent.h" -#include "components/SliderComponent.h" -#include "components/SwitchComponent.h" -#include "guis/GuiCollectionSystemsOptions.h" -#include "guis/GuiDetectDevice.h" -#include "guis/GuiGeneralScreensaverOptions.h" -#include "guis/GuiMsgBox.h" -#include "guis/GuiScraperStart.h" -#include "guis/GuiSettings.h" -#include "views/UIModeController.h" -#include "views/ViewController.h" -#include "CollectionSystemManager.h" -#include "EmulationStation.h" -#include "Scripting.h" -#include "SystemData.h" -#include "VolumeControl.h" -#include -#include - -#include "resources/TextureData.h" -#include "animations/LambdaAnimation.h" +#include "guis/GuiMenu.h" + +#include "components/OptionListComponent.h" +#include "components/SliderComponent.h" +#include "components/SwitchComponent.h" +#include "guis/GuiCollectionSystemsOptions.h" +#include "guis/GuiDetectDevice.h" +#include "guis/GuiGeneralScreensaverOptions.h" +#include "guis/GuiMsgBox.h" +#include "guis/GuiScraperStart.h" +#include "guis/GuiSettings.h" +#include "views/UIModeController.h" +#include "views/ViewController.h" +#include "CollectionSystemManager.h" +#include "EmulationStation.h" +#include "Scripting.h" +#include "SystemData.h" +#include "VolumeControl.h" +#include +#include + +#include "resources/TextureData.h" +#include "animations/LambdaAnimation.h" #include "GuiGamelistOptions.h" // grid sizes - -GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) -{ - auto theme = ThemeData::getMenuTheme(); - - bool isFullUI = UIModeController::getInstance()->isUIModeFull(); - - if (isFullUI) - { - addEntry(_T("UI SETTINGS"), theme->Text.color, true, [this] { openUISettings(); }, theme->MenuIcons.ui); - addEntry(_T("CONFIGURE INPUT"), theme->Text.color, true, [this] { openConfigInput(); }, theme->MenuIcons.controllers); - } - - addEntry(_T("SOUND SETTINGS"), theme->Text.color, true, [this] { openSoundSettings(); }, theme->MenuIcons.sound); - - if (isFullUI) - addEntry(_T("SCRAPER"), theme->Text.color, true, [this] { openScraperSettings(); }, theme->MenuIcons.scraper); - - if (isFullUI) - { - addEntry(_T("GAME COLLECTION SETTINGS"), theme->Text.color, true, [this] { openCollectionSystemSettings(); }, theme->MenuIcons.games); - addEntry(_T("ADVANCED SETTINGS"), theme->Text.color, true, [this] { openOtherSettings(); }, theme->MenuIcons.advanced); - } - -#ifdef WIN32 - addEntry(_T("QUIT"), theme->Text.color, false, [this] {openQuitMenu(); }, theme->MenuIcons.quit); -#else - addEntry(_T("QUIT"), theme->Text.color, true, [this] {openQuitMenu(); }, theme->MenuIcons.quit); -#endif - - addChild(&mMenu); - addVersionInfo(); - - setSize(mMenu.getSize()); - - float y1 = Renderer::getScreenHeight(); - float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; - - if (Settings::getInstance()->getString("PowerSaverMode") == "instant" || Settings::getInstance()->getString("TransitionStyle") == "instant") - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); - else - { - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); - - auto fadeFunc = [this, y1, y2](float t) { - - t -= 1; // cubic ease out - float pct = Math::lerp(0, 1, t*t*t + 1); - - float y = y1 * (1 - pct) + y2 * pct; - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y); - }; - - setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] - { - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); - }); - - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); - } -} - -void GuiMenu::openScraperSettings() -{ - auto s = new GuiSettings(mWindow, _T("SCRAPER")); - - // scrape from - auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, _T("SCRAPE FROM"), false); - std::vector scrapers = getScraperList(); - - // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. - for(auto it = scrapers.cbegin(); it != scrapers.cend(); it++) - scraper_list->add(*it, *it, *it == Settings::getInstance()->getString("Scraper")); - - s->addWithLabel(_T("SCRAPE FROM"), scraper_list); - s->addSaveFunc([scraper_list] { Settings::getInstance()->setString("Scraper", scraper_list->getSelected()); }); - - // scrape ratings - auto scrape_ratings = std::make_shared(mWindow); - scrape_ratings->setState(Settings::getInstance()->getBool("ScrapeRatings")); - s->addWithLabel(_T("SCRAPE RATINGS"), scrape_ratings); - s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); }); - - // scrape now - ComponentListRow row; - auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; - std::function openAndSave = openScrapeNow; - openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; - row.makeAcceptInputHandler(openAndSave); - - auto scrape_now = std::make_shared(mWindow, _T("SCRAPE NOW"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); - auto bracket = makeArrow(mWindow); - row.addElement(scrape_now, true); - row.addElement(bracket, false); - s->addRow(row); - - s->updatePosition(); - mWindow->pushGui(s); -} - -void GuiMenu::openSoundSettings() -{ - auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); - - // volume - auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); - volume->setValue((float)VolumeControl::getInstance()->getVolume()); - s->addWithLabel(_T("SYSTEM VOLUME"), volume); - s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); - - if (UIModeController::getInstance()->isUIModeFull()) - { -#if defined(__linux__) - // audio card - auto audio_card = std::make_shared< OptionListComponent >(mWindow, "AUDIO CARD", false); - std::vector audio_cards; - #ifdef _RPI_ - // RPi Specific Audio Cards - audio_cards.push_back("local"); - audio_cards.push_back("hdmi"); - audio_cards.push_back("both"); - #endif - audio_cards.push_back("default"); - audio_cards.push_back("sysdefault"); - audio_cards.push_back("dmix"); - audio_cards.push_back("hw"); - audio_cards.push_back("plughw"); - audio_cards.push_back("null"); - if (Settings::getInstance()->getString("AudioCard") != "") { - if(std::find(audio_cards.begin(), audio_cards.end(), Settings::getInstance()->getString("AudioCard")) == audio_cards.end()) { - audio_cards.push_back(Settings::getInstance()->getString("AudioCard")); - } - } - for(auto ac = audio_cards.cbegin(); ac != audio_cards.cend(); ac++) - audio_card->add(*ac, *ac, Settings::getInstance()->getString("AudioCard") == *ac); - s->addWithLabel("AUDIO CARD", audio_card); - s->addSaveFunc([audio_card] { - Settings::getInstance()->setString("AudioCard", audio_card->getSelected()); - VolumeControl::getInstance()->deinit(); - VolumeControl::getInstance()->init(); - }); - - // volume control device - auto vol_dev = std::make_shared< OptionListComponent >(mWindow, "AUDIO DEVICE", false); - std::vector transitions; - transitions.push_back("PCM"); - transitions.push_back("Speaker"); - transitions.push_back("Master"); - transitions.push_back("Digital"); - transitions.push_back("Analogue"); - if (Settings::getInstance()->getString("AudioDevice") != "") { - if(std::find(transitions.begin(), transitions.end(), Settings::getInstance()->getString("AudioDevice")) == transitions.end()) { - transitions.push_back(Settings::getInstance()->getString("AudioDevice")); - } - } - for(auto it = transitions.cbegin(); it != transitions.cend(); it++) - vol_dev->add(*it, *it, Settings::getInstance()->getString("AudioDevice") == *it); - s->addWithLabel("AUDIO DEVICE", vol_dev); - s->addSaveFunc([vol_dev] { - Settings::getInstance()->setString("AudioDevice", vol_dev->getSelected()); - VolumeControl::getInstance()->deinit(); - VolumeControl::getInstance()->init(); - }); -#endif - - // disable sounds - auto sounds_enabled = std::make_shared(mWindow); - sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); - s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); - s->addSaveFunc([sounds_enabled] { - if (sounds_enabled->getState() - && !Settings::getInstance()->getBool("EnableSounds") - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState()); - }); - - auto video_audio = std::make_shared(mWindow); - video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); - s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); - s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); - -#ifdef _RPI_ - // OMX player Audio Device - auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, "OMX PLAYER AUDIO DEVICE", false); - std::vector omx_cards; - // RPi Specific Audio Cards - omx_cards.push_back("local"); - omx_cards.push_back("hdmi"); - omx_cards.push_back("both"); - omx_cards.push_back("alsa:hw:0,0"); - omx_cards.push_back("alsa:hw:1,0"); - if (Settings::getInstance()->getString("OMXAudioDev") != "") { - if (std::find(omx_cards.begin(), omx_cards.end(), Settings::getInstance()->getString("OMXAudioDev")) == omx_cards.end()) { - omx_cards.push_back(Settings::getInstance()->getString("OMXAudioDev")); - } - } - for (auto it = omx_cards.cbegin(); it != omx_cards.cend(); it++) - omx_audio_dev->add(*it, *it, Settings::getInstance()->getString("OMXAudioDev") == *it); - s->addWithLabel("OMX PLAYER AUDIO DEVICE", omx_audio_dev); - s->addSaveFunc([omx_audio_dev] { - if (Settings::getInstance()->getString("OMXAudioDev") != omx_audio_dev->getSelected()) - Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); - }); -#endif - } - - s->updatePosition(); - mWindow->pushGui(s); - -} - -void GuiMenu::openUISettings() -{ - auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); - - auto pthis = this; - - // theme set - auto theme = ThemeData::getMenuTheme(); - auto themeSets = ThemeData::getThemeSets(); - auto system = ViewController::get()->getState().getSystem(); - - if (!themeSets.empty()) - { - std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if (selectedSet == themeSets.cend()) - selectedSet = themeSets.cbegin(); - - auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); - for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) - theme_set->add(it->first, it->first, it == selectedSet); - s->addWithLabel(_T("THEME"), theme_set); - - Window* window = mWindow; - s->addSaveFunc([window, theme_set, pthis] - { - std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - if (oldTheme != theme_set->getSelected()) - { - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - - auto themeSubSets = ThemeData::getThemeSubSets(theme_set->getSelected()); - auto themeColorSets = ThemeData::sortThemeSubSets(themeSubSets, "colorset"); - auto themeIconSets = ThemeData::sortThemeSubSets(themeSubSets, "iconset"); - auto themeMenus = ThemeData::sortThemeSubSets(themeSubSets, "menu"); - auto themeSystemviewSets = ThemeData::sortThemeSubSets(themeSubSets, "systemview"); - auto themeGamelistViewSets = ThemeData::sortThemeSubSets(themeSubSets, "gamelistview"); - auto themeRegions = ThemeData::sortThemeSubSets(themeSubSets, "region"); - - // theme changed without setting options, forcing options to avoid crash/blank theme - if (themeRegions.empty()) - Settings::getInstance()->setString("ThemeRegionName", ""); - else - Settings::getInstance()->setString("ThemeRegionName", themeRegions.begin()->first); - - if (themeColorSets.empty()) - Settings::getInstance()->setString("ThemeColorSet", ""); - else - Settings::getInstance()->setString("ThemeColorSet", themeColorSets.begin()->first); - - if (themeIconSets.empty()) - Settings::getInstance()->setString("ThemeIconSet", ""); - else - Settings::getInstance()->setString("ThemeIconSet", themeIconSets.begin()->first); - - if (themeMenus.empty()) - Settings::getInstance()->setString("ThemeMenu", ""); - else - Settings::getInstance()->setString("ThemeMenu", themeMenus.begin()->first); - - if (themeSystemviewSets.empty()) - Settings::getInstance()->setString("ThemeSystemView", ""); - else - Settings::getInstance()->setString("ThemeSystemView", themeSystemviewSets.begin()->first); - - if (themeGamelistViewSets.empty()) - Settings::getInstance()->setString("ThemeGamelistView", ""); - else - Settings::getInstance()->setString("ThemeGamelistView", themeGamelistViewSets.begin()->first); - - - - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation - - delete pthis; - window->pushGui(new GuiMenu(window)); - } - }); - - if (system != NULL && system->getTheme()->hasSubsets()) - { - - // theme config - std::function openGui = [this, theme_set, window, pthis] { - auto themeconfig = new GuiSettings(mWindow, _T("THEME CONFIGURATION").c_str()); - - auto SelectedTheme = theme_set->getSelected(); - - auto themeSubSets = ThemeData::getThemeSubSets(SelectedTheme); - auto themeColorSets = ThemeData::sortThemeSubSets(themeSubSets, "colorset"); - auto themeIconSets = ThemeData::sortThemeSubSets(themeSubSets, "iconset"); - auto themeMenus = ThemeData::sortThemeSubSets(themeSubSets, "menu"); - auto themeSystemviewSets = ThemeData::sortThemeSubSets(themeSubSets, "systemview"); - auto themeGamelistViewSets = ThemeData::sortThemeSubSets(themeSubSets, "gamelistview"); - auto themeRegions = ThemeData::sortThemeSubSets(themeSubSets, "region"); - - // colorset - - auto selectedColorSet = themeColorSets.find(Settings::getInstance()->getString("ThemeColorSet")); - if (selectedColorSet == themeColorSets.end()) - selectedColorSet = themeColorSets.begin(); - auto theme_colorset = std::make_shared >(mWindow, _T("THEME COLORSET"), false); - - for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) - theme_colorset->add(it->first, it->first, it == selectedColorSet); - - if (!themeColorSets.empty()) - themeconfig->addWithLabel(_T("THEME COLORSET"), theme_colorset); - - // iconset - - auto selectedIconSet = themeIconSets.find(Settings::getInstance()->getString("ThemeIconSet")); - if (selectedIconSet == themeIconSets.end()) - selectedIconSet = themeIconSets.begin(); - auto theme_iconset = std::make_shared >(mWindow, _T("THEME ICONSET"), false); - - for (auto it = themeIconSets.begin(); it != themeIconSets.end(); it++) - theme_iconset->add(it->first, it->first, it == selectedIconSet); - - if (!themeIconSets.empty()) - themeconfig->addWithLabel(_T("THEME ICONSET"), theme_iconset); - - // menu - - auto selectedMenu = themeMenus.find(Settings::getInstance()->getString("ThemeMenu")); - if (selectedMenu == themeMenus.end()) - selectedMenu = themeMenus.begin(); - auto theme_menu = std::make_shared >(mWindow, _T("THEME MENU"), false); - - for (auto it = themeMenus.begin(); it != themeMenus.end(); it++) - theme_menu->add(it->first, it->first, it == selectedMenu); - - if (!themeMenus.empty()) - themeconfig->addWithLabel(_T("THEME MENU"), theme_menu); - - // systemview - - auto selectedSystemviewSet = themeSystemviewSets.find(Settings::getInstance()->getString("ThemeSystemView")); - if (selectedSystemviewSet == themeSystemviewSets.end()) - selectedSystemviewSet = themeSystemviewSets.begin(); - - auto theme_systemview = std::make_shared >(mWindow, _T("THEME SYSTEMVIEW"), false); - - for (auto it = themeSystemviewSets.begin(); it != themeSystemviewSets.end(); it++) - theme_systemview->add(it->first, it->first, it == selectedSystemviewSet); - - if (!themeSystemviewSets.empty()) - themeconfig->addWithLabel(_T("THEME SYSTEMVIEW"), theme_systemview); - - // gamelistview - - auto selectedGamelistViewSet = themeGamelistViewSets.find(Settings::getInstance()->getString("ThemeGamelistView")); - if (selectedGamelistViewSet == themeGamelistViewSets.end()) - selectedGamelistViewSet = themeGamelistViewSets.begin(); - - auto theme_gamelistview = std::make_shared >(mWindow, _T("THEME GAMELISTVIEW"), false); - - for (auto it = themeGamelistViewSets.begin(); it != themeGamelistViewSets.end(); it++) - theme_gamelistview->add(it->first, it->first, it == selectedGamelistViewSet); - - if (!themeGamelistViewSets.empty()) - themeconfig->addWithLabel(_T("THEME GAMELISTVIEW"), theme_gamelistview); - - // themeregion - auto selectedRegion = themeRegions.find(Settings::getInstance()->getString("ThemeRegionName")); - if (selectedRegion == themeRegions.end()) - selectedRegion = themeRegions.begin(); - - auto theme_region = std::make_shared >(mWindow, _T("THEME GAMELISTVIEW"), false); - - for (auto it = themeRegions.begin(); it != themeRegions.end(); it++) - theme_region->add(it->first, it->first, it == selectedRegion); - - if (!themeRegions.empty()) - themeconfig->addWithLabel(_T("THEME REGION"), theme_region); - - themeconfig->addSaveFunc([this, window, theme_set, theme_colorset, theme_iconset, theme_menu, theme_systemview, theme_gamelistview, theme_region, pthis] { - bool needReload = false; - if (Settings::getInstance()->getString("ThemeColorSet") != theme_colorset->getSelected() && !theme_colorset->getSelected().empty()) - needReload = true; - if (Settings::getInstance()->getString("ThemeIconSet") != theme_iconset->getSelected() && !theme_iconset->getSelected().empty()) - needReload = true; - if (Settings::getInstance()->getString("ThemeMenu") != theme_menu->getSelected() && !theme_menu->getSelected().empty()) - needReload = true; - if (Settings::getInstance()->getString("ThemeSystemView") != theme_systemview->getSelected() && !theme_systemview->getSelected().empty()) - needReload = true; - if (Settings::getInstance()->getString("ThemeGamelistView") != theme_gamelistview->getSelected() && !theme_gamelistview->getSelected().empty()) - needReload = true; - if (Settings::getInstance()->getString("ThemeRegionName") != theme_region->getSelected() && !theme_region->getSelected().empty()) - needReload = true; - - if (needReload) { - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - Settings::getInstance()->setString("ThemeColorSet", theme_colorset->getSelected()); - Settings::getInstance()->setString("ThemeIconSet", theme_iconset->getSelected()); - Settings::getInstance()->setString("ThemeMenu", theme_menu->getSelected()); - Settings::getInstance()->setString("ThemeSystemView", theme_systemview->getSelected()); - Settings::getInstance()->setString("ThemeGamelistView", theme_gamelistview->getSelected()); - Settings::getInstance()->setString("ThemeRegionName", theme_region->getSelected()); - //Settings::getInstance()->setBool("ThemeChanged", true); - - //reload theme - std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation - - delete pthis; - window->pushGui(new GuiMenu(window)); - //ReloadAll(); - } - }); - if (!themeRegions.empty() || !themeGamelistViewSets.empty() || !themeSystemviewSets.empty() || !themeIconSets.empty() || !themeMenus.empty() || !themeColorSets.empty()) - { - themeconfig->updatePosition(); - mWindow->pushGui(themeconfig); - } - else - mWindow->pushGui(new GuiMsgBox(window, _T("THIS THEME HAS NO OPTION"), _T("OK"))); - }; - - s->addSubMenu(_T("THEME CONFIGURATION"), openGui); - } - } - - // GameList view style - if (system != NULL && !system->getTheme()->hasSubsets()) - { - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); - std::vector styles; - styles.push_back("automatic"); - - auto system = ViewController::get()->getState().getSystem(); - if (system != NULL) - { - auto mViews = system->getTheme()->getViewsOfTheme(); - for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) - styles.push_back(*it); - } - else - { - styles.push_back("basic"); - styles.push_back("detailed"); - styles.push_back("video"); - styles.push_back("grid"); - } - - auto viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + +GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) +{ + auto theme = ThemeData::getMenuTheme(); + + bool isFullUI = UIModeController::getInstance()->isUIModeFull(); + + if (isFullUI) + { + addEntry(_T("UI SETTINGS"), theme->Text.color, true, [this] { openUISettings(); }, theme->MenuIcons.ui); + addEntry(_T("CONFIGURE INPUT"), theme->Text.color, true, [this] { openConfigInput(); }, theme->MenuIcons.controllers); + } + + addEntry(_T("SOUND SETTINGS"), theme->Text.color, true, [this] { openSoundSettings(); }, theme->MenuIcons.sound); + + if (isFullUI) + addEntry(_T("SCRAPER"), theme->Text.color, true, [this] { openScraperSettings(); }, theme->MenuIcons.scraper); + + if (isFullUI) + { + addEntry(_T("GAME COLLECTION SETTINGS"), theme->Text.color, true, [this] { openCollectionSystemSettings(); }, theme->MenuIcons.games); + addEntry(_T("ADVANCED SETTINGS"), theme->Text.color, true, [this] { openOtherSettings(); }, theme->MenuIcons.advanced); + } + +#ifdef WIN32 + addEntry(_T("QUIT"), theme->Text.color, false, [this] {openQuitMenu(); }, theme->MenuIcons.quit); +#else + addEntry(_T("QUIT"), theme->Text.color, true, [this] {openQuitMenu(); }, theme->MenuIcons.quit); +#endif + + addChild(&mMenu); + addVersionInfo(); + + setSize(mMenu.getSize()); + + float y1 = Renderer::getScreenHeight(); + float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; + + if (Settings::getInstance()->getString("PowerSaverMode") == "instant" || Settings::getInstance()->getString("TransitionStyle") == "instant") + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + else + { + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); + + auto fadeFunc = [this, y1, y2](float t) { + + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + float y = y1 * (1 - pct) + y2 * pct; + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y); + }; + + setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] + { + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + }); + + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); + } +} + +void GuiMenu::openScraperSettings() +{ + auto s = new GuiSettings(mWindow, _T("SCRAPER")); + + // scrape from + auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, _T("SCRAPE FROM"), false); + std::vector scrapers = getScraperList(); + + // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. + for(auto it = scrapers.cbegin(); it != scrapers.cend(); it++) + scraper_list->add(*it, *it, *it == Settings::getInstance()->getString("Scraper")); + + s->addWithLabel(_T("SCRAPE FROM"), scraper_list); + s->addSaveFunc([scraper_list] { Settings::getInstance()->setString("Scraper", scraper_list->getSelected()); }); + + // scrape ratings + auto scrape_ratings = std::make_shared(mWindow); + scrape_ratings->setState(Settings::getInstance()->getBool("ScrapeRatings")); + s->addWithLabel(_T("SCRAPE RATINGS"), scrape_ratings); + s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); }); + + // scrape now + ComponentListRow row; + auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; + std::function openAndSave = openScrapeNow; + openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; + row.makeAcceptInputHandler(openAndSave); + + auto scrape_now = std::make_shared(mWindow, _T("SCRAPE NOW"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); + auto bracket = makeArrow(mWindow); + row.addElement(scrape_now, true); + row.addElement(bracket, false); + s->addRow(row); + + s->updatePosition(); + mWindow->pushGui(s); +} + +void GuiMenu::openSoundSettings() +{ + auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); + + // volume + auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); + volume->setValue((float)VolumeControl::getInstance()->getVolume()); + s->addWithLabel(_T("SYSTEM VOLUME"), volume); + s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); + + if (UIModeController::getInstance()->isUIModeFull()) + { +#if defined(__linux__) + // audio card + auto audio_card = std::make_shared< OptionListComponent >(mWindow, "AUDIO CARD", false); + std::vector audio_cards; + #ifdef _RPI_ + // RPi Specific Audio Cards + audio_cards.push_back("local"); + audio_cards.push_back("hdmi"); + audio_cards.push_back("both"); + #endif + audio_cards.push_back("default"); + audio_cards.push_back("sysdefault"); + audio_cards.push_back("dmix"); + audio_cards.push_back("hw"); + audio_cards.push_back("plughw"); + audio_cards.push_back("null"); + if (Settings::getInstance()->getString("AudioCard") != "") { + if(std::find(audio_cards.begin(), audio_cards.end(), Settings::getInstance()->getString("AudioCard")) == audio_cards.end()) { + audio_cards.push_back(Settings::getInstance()->getString("AudioCard")); + } + } + for(auto ac = audio_cards.cbegin(); ac != audio_cards.cend(); ac++) + audio_card->add(*ac, *ac, Settings::getInstance()->getString("AudioCard") == *ac); + s->addWithLabel("AUDIO CARD", audio_card); + s->addSaveFunc([audio_card] { + Settings::getInstance()->setString("AudioCard", audio_card->getSelected()); + VolumeControl::getInstance()->deinit(); + VolumeControl::getInstance()->init(); + }); + + // volume control device + auto vol_dev = std::make_shared< OptionListComponent >(mWindow, "AUDIO DEVICE", false); + std::vector transitions; + transitions.push_back("PCM"); + transitions.push_back("Speaker"); + transitions.push_back("Master"); + transitions.push_back("Digital"); + transitions.push_back("Analogue"); + if (Settings::getInstance()->getString("AudioDevice") != "") { + if(std::find(transitions.begin(), transitions.end(), Settings::getInstance()->getString("AudioDevice")) == transitions.end()) { + transitions.push_back(Settings::getInstance()->getString("AudioDevice")); + } + } + for(auto it = transitions.cbegin(); it != transitions.cend(); it++) + vol_dev->add(*it, *it, Settings::getInstance()->getString("AudioDevice") == *it); + s->addWithLabel("AUDIO DEVICE", vol_dev); + s->addSaveFunc([vol_dev] { + Settings::getInstance()->setString("AudioDevice", vol_dev->getSelected()); + VolumeControl::getInstance()->deinit(); + VolumeControl::getInstance()->init(); + }); +#endif + + // disable sounds + auto sounds_enabled = std::make_shared(mWindow); + sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); + s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); + s->addSaveFunc([sounds_enabled] { + if (sounds_enabled->getState() + && !Settings::getInstance()->getBool("EnableSounds") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("EnableSounds", sounds_enabled->getState()); + }); + + auto video_audio = std::make_shared(mWindow); + video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); + s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); + s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); + +#ifdef _RPI_ + // OMX player Audio Device + auto omx_audio_dev = std::make_shared< OptionListComponent >(mWindow, "OMX PLAYER AUDIO DEVICE", false); + std::vector omx_cards; + // RPi Specific Audio Cards + omx_cards.push_back("local"); + omx_cards.push_back("hdmi"); + omx_cards.push_back("both"); + omx_cards.push_back("alsa:hw:0,0"); + omx_cards.push_back("alsa:hw:1,0"); + if (Settings::getInstance()->getString("OMXAudioDev") != "") { + if (std::find(omx_cards.begin(), omx_cards.end(), Settings::getInstance()->getString("OMXAudioDev")) == omx_cards.end()) { + omx_cards.push_back(Settings::getInstance()->getString("OMXAudioDev")); + } + } + for (auto it = omx_cards.cbegin(); it != omx_cards.cend(); it++) + omx_audio_dev->add(*it, *it, Settings::getInstance()->getString("OMXAudioDev") == *it); + s->addWithLabel("OMX PLAYER AUDIO DEVICE", omx_audio_dev); + s->addSaveFunc([omx_audio_dev] { + if (Settings::getInstance()->getString("OMXAudioDev") != omx_audio_dev->getSelected()) + Settings::getInstance()->setString("OMXAudioDev", omx_audio_dev->getSelected()); + }); +#endif + } + + s->updatePosition(); + mWindow->pushGui(s); + +} + +void GuiMenu::openUISettings() +{ + auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); + + auto pthis = this; + + // theme set + auto theme = ThemeData::getMenuTheme(); + auto themeSets = ThemeData::getThemeSets(); + auto system = ViewController::get()->getState().getSystem(); + + if (!themeSets.empty()) + { + std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if (selectedSet == themeSets.cend()) + selectedSet = themeSets.cbegin(); + + auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); + for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) + theme_set->add(it->first, it->first, it == selectedSet); + s->addWithLabel(_T("THEME"), theme_set); + + Window* window = mWindow; + s->addSaveFunc([window, theme_set, pthis] + { + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + if (oldTheme != theme_set->getSelected()) + { + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); + + auto themeSubSets = ThemeData::getThemeSubSets(theme_set->getSelected()); + auto themeColorSets = ThemeData::sortThemeSubSets(themeSubSets, "colorset"); + auto themeIconSets = ThemeData::sortThemeSubSets(themeSubSets, "iconset"); + auto themeMenus = ThemeData::sortThemeSubSets(themeSubSets, "menu"); + auto themeSystemviewSets = ThemeData::sortThemeSubSets(themeSubSets, "systemview"); + auto themeGamelistViewSets = ThemeData::sortThemeSubSets(themeSubSets, "gamelistview"); + auto themeRegions = ThemeData::sortThemeSubSets(themeSubSets, "region"); + + // theme changed without setting options, forcing options to avoid crash/blank theme + if (themeRegions.empty()) + Settings::getInstance()->setString("ThemeRegionName", ""); + else + Settings::getInstance()->setString("ThemeRegionName", themeRegions.begin()->first); + + if (themeColorSets.empty()) + Settings::getInstance()->setString("ThemeColorSet", ""); + else + Settings::getInstance()->setString("ThemeColorSet", themeColorSets.begin()->first); + + if (themeIconSets.empty()) + Settings::getInstance()->setString("ThemeIconSet", ""); + else + Settings::getInstance()->setString("ThemeIconSet", themeIconSets.begin()->first); + + if (themeMenus.empty()) + Settings::getInstance()->setString("ThemeMenu", ""); + else + Settings::getInstance()->setString("ThemeMenu", themeMenus.begin()->first); + + if (themeSystemviewSets.empty()) + Settings::getInstance()->setString("ThemeSystemView", ""); + else + Settings::getInstance()->setString("ThemeSystemView", themeSystemviewSets.begin()->first); + + if (themeGamelistViewSets.empty()) + Settings::getInstance()->setString("ThemeGamelistView", ""); + else + Settings::getInstance()->setString("ThemeGamelistView", themeGamelistViewSets.begin()->first); + + + + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + + delete pthis; + window->pushGui(new GuiMenu(window)); + } + }); + + if (system != NULL && system->getTheme()->hasSubsets()) + { + + // theme config + std::function openGui = [this, theme_set, window, pthis] { + auto themeconfig = new GuiSettings(mWindow, _T("THEME CONFIGURATION").c_str()); + + auto SelectedTheme = theme_set->getSelected(); + + auto themeSubSets = ThemeData::getThemeSubSets(SelectedTheme); + auto themeColorSets = ThemeData::sortThemeSubSets(themeSubSets, "colorset"); + auto themeIconSets = ThemeData::sortThemeSubSets(themeSubSets, "iconset"); + auto themeMenus = ThemeData::sortThemeSubSets(themeSubSets, "menu"); + auto themeSystemviewSets = ThemeData::sortThemeSubSets(themeSubSets, "systemview"); + auto themeGamelistViewSets = ThemeData::sortThemeSubSets(themeSubSets, "gamelistview"); + auto themeRegions = ThemeData::sortThemeSubSets(themeSubSets, "region"); + + // colorset + + auto selectedColorSet = themeColorSets.find(Settings::getInstance()->getString("ThemeColorSet")); + if (selectedColorSet == themeColorSets.end()) + selectedColorSet = themeColorSets.begin(); + auto theme_colorset = std::make_shared >(mWindow, _T("THEME COLORSET"), false); + + for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) + theme_colorset->add(it->first, it->first, it == selectedColorSet); + + if (!themeColorSets.empty()) + themeconfig->addWithLabel(_T("THEME COLORSET"), theme_colorset); + + // iconset + + auto selectedIconSet = themeIconSets.find(Settings::getInstance()->getString("ThemeIconSet")); + if (selectedIconSet == themeIconSets.end()) + selectedIconSet = themeIconSets.begin(); + auto theme_iconset = std::make_shared >(mWindow, _T("THEME ICONSET"), false); + + for (auto it = themeIconSets.begin(); it != themeIconSets.end(); it++) + theme_iconset->add(it->first, it->first, it == selectedIconSet); + + if (!themeIconSets.empty()) + themeconfig->addWithLabel(_T("THEME ICONSET"), theme_iconset); + + // menu + + auto selectedMenu = themeMenus.find(Settings::getInstance()->getString("ThemeMenu")); + if (selectedMenu == themeMenus.end()) + selectedMenu = themeMenus.begin(); + auto theme_menu = std::make_shared >(mWindow, _T("THEME MENU"), false); + + for (auto it = themeMenus.begin(); it != themeMenus.end(); it++) + theme_menu->add(it->first, it->first, it == selectedMenu); + + if (!themeMenus.empty()) + themeconfig->addWithLabel(_T("THEME MENU"), theme_menu); + + // systemview + + auto selectedSystemviewSet = themeSystemviewSets.find(Settings::getInstance()->getString("ThemeSystemView")); + if (selectedSystemviewSet == themeSystemviewSets.end()) + selectedSystemviewSet = themeSystemviewSets.begin(); + + auto theme_systemview = std::make_shared >(mWindow, _T("THEME SYSTEMVIEW"), false); + + for (auto it = themeSystemviewSets.begin(); it != themeSystemviewSets.end(); it++) + theme_systemview->add(it->first, it->first, it == selectedSystemviewSet); + + if (!themeSystemviewSets.empty()) + themeconfig->addWithLabel(_T("THEME SYSTEMVIEW"), theme_systemview); + + // gamelistview + + auto selectedGamelistViewSet = themeGamelistViewSets.find(Settings::getInstance()->getString("ThemeGamelistView")); + if (selectedGamelistViewSet == themeGamelistViewSets.end()) + selectedGamelistViewSet = themeGamelistViewSets.begin(); + + auto theme_gamelistview = std::make_shared >(mWindow, _T("THEME GAMELISTVIEW"), false); + + for (auto it = themeGamelistViewSets.begin(); it != themeGamelistViewSets.end(); it++) + theme_gamelistview->add(it->first, it->first, it == selectedGamelistViewSet); + + if (!themeGamelistViewSets.empty()) + themeconfig->addWithLabel(_T("THEME GAMELISTVIEW"), theme_gamelistview); + + // themeregion + auto selectedRegion = themeRegions.find(Settings::getInstance()->getString("ThemeRegionName")); + if (selectedRegion == themeRegions.end()) + selectedRegion = themeRegions.begin(); + + auto theme_region = std::make_shared >(mWindow, _T("THEME GAMELISTVIEW"), false); + + for (auto it = themeRegions.begin(); it != themeRegions.end(); it++) + theme_region->add(it->first, it->first, it == selectedRegion); + + if (!themeRegions.empty()) + themeconfig->addWithLabel(_T("THEME REGION"), theme_region); + + themeconfig->addSaveFunc([this, window, theme_set, theme_colorset, theme_iconset, theme_menu, theme_systemview, theme_gamelistview, theme_region, pthis] { + bool needReload = false; + if (Settings::getInstance()->getString("ThemeColorSet") != theme_colorset->getSelected() && !theme_colorset->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeIconSet") != theme_iconset->getSelected() && !theme_iconset->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeMenu") != theme_menu->getSelected() && !theme_menu->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeSystemView") != theme_systemview->getSelected() && !theme_systemview->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeGamelistView") != theme_gamelistview->getSelected() && !theme_gamelistview->getSelected().empty()) + needReload = true; + if (Settings::getInstance()->getString("ThemeRegionName") != theme_region->getSelected() && !theme_region->getSelected().empty()) + needReload = true; + + if (needReload) { + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); + Settings::getInstance()->setString("ThemeColorSet", theme_colorset->getSelected()); + Settings::getInstance()->setString("ThemeIconSet", theme_iconset->getSelected()); + Settings::getInstance()->setString("ThemeMenu", theme_menu->getSelected()); + Settings::getInstance()->setString("ThemeSystemView", theme_systemview->getSelected()); + Settings::getInstance()->setString("ThemeGamelistView", theme_gamelistview->getSelected()); + Settings::getInstance()->setString("ThemeRegionName", theme_region->getSelected()); + //Settings::getInstance()->setBool("ThemeChanged", true); + + //reload theme + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + + delete pthis; + window->pushGui(new GuiMenu(window)); + //ReloadAll(); + } + }); + if (!themeRegions.empty() || !themeGamelistViewSets.empty() || !themeSystemviewSets.empty() || !themeIconSets.empty() || !themeMenus.empty() || !themeColorSets.empty()) + { + themeconfig->updatePosition(); + mWindow->pushGui(themeconfig); + } + else + mWindow->pushGui(new GuiMsgBox(window, _T("THIS THEME HAS NO OPTION"), _T("OK"))); + }; + + s->addSubMenu(_T("THEME CONFIGURATION"), openGui); + } + } + + // GameList view style + if (system != NULL && !system->getTheme()->hasSubsets()) + { + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); + std::vector styles; + styles.push_back("automatic"); + + auto system = ViewController::get()->getState().getSystem(); + if (system != NULL) + { + auto mViews = system->getTheme()->getViewsOfTheme(); + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + styles.push_back(*it); + } + else + { + styles.push_back("basic"); + styles.push_back("detailed"); + styles.push_back("video"); + styles.push_back("grid"); + } + + auto viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); if (!system->getTheme()->hasView(viewPreference)) - viewPreference = "automatic"; - - for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_L(*it), *it, viewPreference == *it); - - s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); - s->addSaveFunc([gamelist_style, viewPreference] { - bool needReload = false; - if (viewPreference != gamelist_style->getSelected()) - needReload = true; - Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); - if (needReload) - ViewController::get()->reloadAll(); - }); - } - - // Default grid size - if (system != NULL && system->getTheme()->hasView("grid")) - { - Vector2f gridOverride = Vector2f::parseString(Settings::getInstance()->getString("DefaultGridSize")); - auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); - - auto mGridSize = std::make_shared>(mWindow, _T("DEFAULT GRID SIZE"), false); - - bool found = false; - for (auto it = GuiGamelistOptions::gridSizes.cbegin(); it != GuiGamelistOptions::gridSizes.cend(); it++) - { - bool sel = (gridOverride == Vector2f(0, 0) && *it == "automatic") || ovv == *it; - if (sel) - found = true; - - mGridSize->add(_L(*it), *it, sel); - } - - if (!found) - mGridSize->selectFirstItem(); - - s->addWithLabel(_T("DEFAULT GRID SIZE"), mGridSize); - s->addSaveFunc([mGridSize] - { - std::string str = mGridSize->getSelected(); - std::string value = ""; - - size_t divider = str.find('x'); - if (divider != std::string::npos) - { - std::string first = str.substr(0, divider); - std::string second = str.substr(divider + 1, std::string::npos); - - Vector2f gridSizeOverride = Vector2f((float)atof(first.c_str()), (float)atof(second.c_str())); - value = Utils::String::replace(Utils::String::replace(gridSizeOverride.toString(), ".000000", ""), "0 0", ""); - } - - if (value != Settings::getInstance()->getString("DefaultGridSize")) - { - Settings::getInstance()->setString("DefaultGridSize", value); - ViewController::get()->reloadAll(); - } - }); - } - - //#ifndef WIN32 - //UI mode - auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, "UI MODE", false); - std::vector UImodes = UIModeController::getInstance()->getUIModes(); - for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) - UImodeSelection->add(*it, *it, Settings::getInstance()->getString("UIMode") == *it); - s->addWithLabel("UI MODE", UImodeSelection); - Window* window = mWindow; - s->addSaveFunc([UImodeSelection, window] - { - std::string selectedMode = UImodeSelection->getSelected(); - if (selectedMode != "Full") - { - std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; - msg += "This will hide most menu-options to prevent changes to the system.\n"; - msg += "To unlock and return to the full UI, enter this code: \n"; - msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; - msg += "Do you want to proceed?"; - window->pushGui(new GuiMsgBox(window, msg, - "YES", [selectedMode] { - LOG(LogDebug) << "Setting UI mode to " << selectedMode; - Settings::getInstance()->setString("UIMode", selectedMode); - Settings::getInstance()->saveFile(); - }, "NO", nullptr)); - } - }); - //#endif - - // LANGUAGE - - std::vector langues; - langues.push_back("en"); - - std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); - if (xmlpath.length() > 0) - { - xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; - - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); - for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if (Utils::FileSystem::isDirectory(*it)) - continue; - - std::string name = *it; - - if (name.rfind("emulationstation2.po") == std::string::npos) - continue; - - name = Utils::FileSystem::getParent(name); - name = Utils::FileSystem::getFileName(name); - - if (name != "en") - langues.push_back(name); - } - - if (langues.size() > 1) - { - auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); - - for (auto it = langues.cbegin(); it != langues.cend(); it++) - language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); - - s->addWithLabel(_T("LANGUAGE"), language); - s->addSaveFunc([language] { - Settings::getInstance()->setString("Language", language->getSelected()); - }); - } - } - - // transition style - auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); - std::vector transitions; - transitions.push_back("fade"); - transitions.push_back("slide"); - transitions.push_back("instant"); - for (auto it = transitions.cbegin(); it != transitions.cend(); it++) - transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); - - s->addWithLabel(_T("TRANSITION STYLE"), transition_style); - s->addSaveFunc([transition_style] { - if (Settings::getInstance()->getString("TransitionStyle") == "instant" - && transition_style->getSelected() != "instant" - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); - GuiComponent::ALLOWANIMATIONS = Settings::getInstance()->getString("TransitionStyle") != "instant"; - }); - - - auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); - std::vector gameTransitions; - gameTransitions.push_back("fade"); - gameTransitions.push_back("slide"); - gameTransitions.push_back("instant"); - for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) - transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); - - s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); - s->addSaveFunc([transitionOfGames_style] { - if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" - && transitionOfGames_style->getSelected() != "instant" - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); - }); - - - // Optionally start in selected system - auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); - systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); - for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) - { - if ("retropie" != (*it)->getName()) - { - systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); - } - } - s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); - s->addSaveFunc([systemfocus_list] { - Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); - }); - - // Open gamelist at start - auto bootOnGamelist = std::make_shared(mWindow); - bootOnGamelist->setState(Settings::getInstance()->getBool("StartupOnGameList")); - s->addWithLabel(_T("BOOT ON GAMELIST"), bootOnGamelist); - s->addSaveFunc([bootOnGamelist] { Settings::getInstance()->setBool("StartupOnGameList", bootOnGamelist->getState()); }); - - // Hide system view - auto hideSystemView = std::make_shared(mWindow); - hideSystemView->setState(Settings::getInstance()->getBool("HideSystemView")); - s->addWithLabel(_T("HIDE SYSTEM VIEW"), hideSystemView); - s->addSaveFunc([hideSystemView] { Settings::getInstance()->setBool("HideSystemView", hideSystemView->getState()); }); - - -#if defined(_WIN32) - // quick system select (left/right in game list view) - auto hideWindowScreen = std::make_shared(mWindow); - hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); - s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); - s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); -#endif - - // quick system select (left/right in game list view) - auto quick_sys_select = std::make_shared(mWindow); - quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); - s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); - s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); - - // carousel transition option - auto move_carousel = std::make_shared(mWindow); - move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); - s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); - s->addSaveFunc([move_carousel] { - if (move_carousel->getState() - && !Settings::getInstance()->getBool("MoveCarousel") - && PowerSaver::getMode() == PowerSaver::INSTANT) - { - Settings::getInstance()->setString("PowerSaverMode", "default"); - PowerSaver::init(); - } - Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); - }); - - // show help - auto show_help = std::make_shared(mWindow); - show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); - s->addWithLabel(_T("ON-SCREEN HELP"), show_help); - s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); - - // enable filters (ForceDisableFilters) - auto enable_filter = std::make_shared(mWindow); - enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); - s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); - s->addSaveFunc([enable_filter] { - bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); - Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); - if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); - }); - - - // screensaver - ComponentListRow screensaver_row; - screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); - screensaver_row.addElement(makeArrow(mWindow), false); - screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); - s->addRow(screensaver_row); - - - s->updatePosition(); - mWindow->pushGui(s); -} - -void GuiMenu::openOtherSettings() -{ - auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); - - // maximum vram - auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); - max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel(_T("VRAM LIMIT"), max_vram); - s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); - - // power saver - auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); - std::vector modes; - modes.push_back("disabled"); - modes.push_back("default"); - modes.push_back("enhanced"); - modes.push_back("instant"); - for (auto it = modes.cbegin(); it != modes.cend(); it++) - power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); - - s->addWithLabel(_T("POWER SAVER MODES"), power_saver); - s->addSaveFunc([this, power_saver] { - if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { - Settings::getInstance()->setString("TransitionStyle", "instant"); - Settings::getInstance()->setString("GameTransitionStyle", "instant"); - Settings::getInstance()->setBool("MoveCarousel", false); - Settings::getInstance()->setBool("EnableSounds", false); - } - - GuiComponent::ALLOWANIMATIONS = Settings::getInstance()->getString("TransitionStyle") != "instant"; - - Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); - PowerSaver::init(); - }); - - // gamelists - auto save_gamelists = std::make_shared(mWindow); - save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); - s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); - s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); - - auto parse_gamelists = std::make_shared(mWindow); - parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); - s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); - s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); - -#ifndef WIN32 - auto local_art = std::make_shared(mWindow); - local_art->setState(Settings::getInstance()->getBool("LocalArt")); - s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); - s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); -#endif - - // hidden files - auto hidden_files = std::make_shared(mWindow); - hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); - s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); - s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); - -#ifdef _RPI_ - // Video Player - VideoOmxPlayer - auto omx_player = std::make_shared(mWindow); - omx_player->setState(Settings::getInstance()->getBool("VideoOmxPlayer")); - s->addWithLabel("USE OMX PLAYER (HW ACCELERATED)", omx_player); - s->addSaveFunc([omx_player] - { - // need to reload all views to re-create the right video components - bool needReload = false; - if(Settings::getInstance()->getBool("VideoOmxPlayer") != omx_player->getState()) - needReload = true; - - Settings::getInstance()->setBool("VideoOmxPlayer", omx_player->getState()); - - if(needReload) - ViewController::get()->reloadAll(); - }); - -#endif - - // preload UI - auto preloadUI = std::make_shared(mWindow); - preloadUI->setState(Settings::getInstance()->getBool("PreloadUI")); - s->addWithLabel(_T("PRELOAD UI"), preloadUI); - s->addSaveFunc([preloadUI] { Settings::getInstance()->setBool("PreloadUI", preloadUI->getState()); }); - - // optimizeVram - auto optimizeVram = std::make_shared(mWindow); - optimizeVram->setState(Settings::getInstance()->getBool("OptimizeVRAM")); - s->addWithLabel(_T("OPTIMIZE IMAGES VRAM USE"), optimizeVram); - s->addSaveFunc([optimizeVram] - { - TextureData::OPTIMIZEVRAM = optimizeVram->getState(); - Settings::getInstance()->setBool("OptimizeVRAM", optimizeVram->getState()); - }); - -#ifdef WIN32 - // vsync - auto vsync = std::make_shared(mWindow); - vsync->setState(Settings::getInstance()->getBool("VSync")); - s->addWithLabel(_T("VSYNC"), vsync); - s->addSaveFunc([vsync] - { - Settings::getInstance()->setBool("VSync", vsync->getState()); - - // vsync - if (Settings::getInstance()->getBool("VSync")) - { - // SDL_GL_SetSwapInterval(0) for immediate updates (no vsync, default), - // 1 for updates synchronized with the vertical retrace, - // or -1 for late swap tearing. - // SDL_GL_SetSwapInterval returns 0 on success, -1 on error. - // if vsync is requested, try normal vsync; if that doesn't work, try late swap tearing - // if that doesn't work, report an error - if (SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) - LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; - } - else - SDL_GL_SetSwapInterval(0); - }); -#endif - - // framerate - - auto framerate = std::make_shared(mWindow); - framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); - s->addWithLabel(_T("SHOW FRAMERATE"), framerate); - s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); - - s->updatePosition(); - mWindow->pushGui(s); - -} - -void GuiMenu::openConfigInput() -{ - Window* window = mWindow; - window->pushGui(new GuiDetectDevice(window, false, nullptr)); - /* - window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), - [window] { - window->pushGui(new GuiDetectDevice(window, false, nullptr)); - }, _T("NO"), nullptr) - );*/ - -} - -void GuiMenu::openQuitMenu() -{ -#ifdef WIN32 - Scripting::fireEvent("quit"); - quitES(""); - return; -#endif - - auto s = new GuiSettings(mWindow, _T("QUIT")); - - Window* window = mWindow; - - ComponentListRow row; - if (UIModeController::getInstance()->isUIModeFull()) - { - row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", - [] { - Scripting::fireEvent("quit"); - if(quitES("/tmp/es-restart") != 0) - LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", 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", - [] { - Scripting::fireEvent("quit"); - quitES(""); - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - s->addRow(row); - } - } - row.elements.clear(); - row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", - [] { - Scripting::fireEvent("quit", "reboot"); - Scripting::fireEvent("reboot"); - if (quitES("/tmp/es-sysrestart") != 0) - LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(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", - [] { - Scripting::fireEvent("quit", "shutdown"); - Scripting::fireEvent("shutdown"); - if (quitES("/tmp/es-shutdown") != 0) - LOG(LogWarning) << "Shutdown terminated with non-zero result!"; - }, "NO", nullptr)); - }); - row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); - s->addRow(row); - - s->updatePosition(); - mWindow->pushGui(s); -} - -void GuiMenu::addVersionInfo() -{ - std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); - - auto theme = ThemeData::getMenuTheme(); -// mVersion.setFont(Font::get(FONT_SIZE_SMALL)); -// mVersion.setColor(0x5E5E5EFF); - - mVersion.setFont(theme->Footer.font); - mVersion.setColor(theme->Footer.color); - - mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); - mVersion.setHorizontalAlignment(ALIGN_CENTER); - addChild(&mVersion); -} - -void GuiMenu::openScreensaverOptions() { - mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); -} - -void GuiMenu::openCollectionSystemSettings() { - mWindow->pushGui(new GuiCollectionSystemsOptions(mWindow)); -} - -void GuiMenu::onSizeChanged() -{ - mVersion.setSize(mSize.x(), 0); - mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); -} - -void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func, const std::string iconName) -{ - auto theme = ThemeData::getMenuTheme(); - std::shared_ptr font = theme->Text.font; - color = theme->Text.color; - - // populate the list - ComponentListRow row; - - if (!iconName.empty()) - { - // icon - auto icon = std::make_shared(mWindow); - icon->setImage(iconName); - icon->setColorShift(theme->Text.color); - icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); - row.addElement(icon, false); - - // spacer between icon and text - auto spacer = std::make_shared(mWindow); - spacer->setSize(10, 0); - row.addElement(spacer, false); - } - - row.addElement(std::make_shared(mWindow, name, font, color), true); - - if (add_arrow) - { - std::shared_ptr 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; -} - -HelpStyle GuiMenu::getHelpStyle() -{ - HelpStyle style = HelpStyle(); - style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); - return style; -} - -std::vector GuiMenu::getHelpPrompts() -{ - std::vector prompts; - prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); - prompts.push_back(HelpPrompt("a", _T("SELECT"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); - return prompts; -} + viewPreference = "automatic"; + + for (auto it = styles.cbegin(); it != styles.cend(); it++) + gamelist_style->add(_L(*it), *it, viewPreference == *it); + + s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); + s->addSaveFunc([gamelist_style, viewPreference] { + bool needReload = false; + if (viewPreference != gamelist_style->getSelected()) + needReload = true; + Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); + if (needReload) + ViewController::get()->reloadAll(); + }); + } + + // Default grid size + if (system != NULL && system->getTheme()->hasView("grid")) + { + Vector2f gridOverride = Vector2f::parseString(Settings::getInstance()->getString("DefaultGridSize")); + auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); + + auto mGridSize = std::make_shared>(mWindow, _T("DEFAULT GRID SIZE"), false); + + bool found = false; + for (auto it = GuiGamelistOptions::gridSizes.cbegin(); it != GuiGamelistOptions::gridSizes.cend(); it++) + { + bool sel = (gridOverride == Vector2f(0, 0) && *it == "automatic") || ovv == *it; + if (sel) + found = true; + + mGridSize->add(_L(*it), *it, sel); + } + + if (!found) + mGridSize->selectFirstItem(); + + s->addWithLabel(_T("DEFAULT GRID SIZE"), mGridSize); + s->addSaveFunc([mGridSize] + { + std::string str = mGridSize->getSelected(); + std::string value = ""; + + size_t divider = str.find('x'); + if (divider != std::string::npos) + { + std::string first = str.substr(0, divider); + std::string second = str.substr(divider + 1, std::string::npos); + + Vector2f gridSizeOverride = Vector2f((float)atof(first.c_str()), (float)atof(second.c_str())); + value = Utils::String::replace(Utils::String::replace(gridSizeOverride.toString(), ".000000", ""), "0 0", ""); + } + + if (value != Settings::getInstance()->getString("DefaultGridSize")) + { + Settings::getInstance()->setString("DefaultGridSize", value); + ViewController::get()->reloadAll(); + } + }); + } + + //#ifndef WIN32 + //UI mode + auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, _T("UI MODE"), false); + std::vector UImodes = UIModeController::getInstance()->getUIModes(); + for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) + UImodeSelection->add(_L(*it), *it, Settings::getInstance()->getString("UIMode") == *it); + s->addWithLabel(_T("UI MODE"), UImodeSelection); + Window* window = mWindow; + s->addSaveFunc([UImodeSelection, window] + { + std::string selectedMode = UImodeSelection->getSelected(); + if (selectedMode != "Full") + { + std::string msg = "You are changing the UI to a restricted mode:\n" + selectedMode + "\n"; + msg += "This will hide most menu-options to prevent changes to the system.\n"; + msg += "To unlock and return to the full UI, enter this code: \n"; + msg += "\"" + UIModeController::getInstance()->getFormattedPassKeyStr() + "\"\n\n"; + msg += "Do you want to proceed?"; + window->pushGui(new GuiMsgBox(window, msg, + "YES", [selectedMode] { + LOG(LogDebug) << "Setting UI mode to " << selectedMode; + Settings::getInstance()->setString("UIMode", selectedMode); + Settings::getInstance()->saveFile(); + }, "NO", nullptr)); + } + }); + //#endif + + // LANGUAGE + + std::vector langues; + langues.push_back("en"); + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); + if (xmlpath.length() > 0) + { + xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if (Utils::FileSystem::isDirectory(*it)) + continue; + + std::string name = *it; + + if (name.rfind("emulationstation2.po") == std::string::npos) + continue; + + name = Utils::FileSystem::getParent(name); + name = Utils::FileSystem::getFileName(name); + + if (name != "en") + langues.push_back(name); + } + + if (langues.size() > 1) + { + auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); + + for (auto it = langues.cbegin(); it != langues.cend(); it++) + language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); + + s->addWithLabel(_T("LANGUAGE"), language); + s->addSaveFunc([language] { + Settings::getInstance()->setString("Language", language->getSelected()); + }); + } + } + + // transition style + auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); + std::vector transitions; + transitions.push_back("fade"); + transitions.push_back("slide"); + transitions.push_back("instant"); + for (auto it = transitions.cbegin(); it != transitions.cend(); it++) + transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); + + s->addWithLabel(_T("TRANSITION STYLE"), transition_style); + s->addSaveFunc([transition_style] { + if (Settings::getInstance()->getString("TransitionStyle") == "instant" + && transition_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); + GuiComponent::ALLOWANIMATIONS = Settings::getInstance()->getString("TransitionStyle") != "instant"; + }); + + + auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); + std::vector gameTransitions; + gameTransitions.push_back("fade"); + gameTransitions.push_back("slide"); + gameTransitions.push_back("instant"); + for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) + transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); + + s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); + s->addSaveFunc([transitionOfGames_style] { + if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" + && transitionOfGames_style->getSelected() != "instant" + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setString("GameTransitionStyle", transitionOfGames_style->getSelected()); + }); + + + // Optionally start in selected system + auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); + systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + { + if ("retropie" != (*it)->getName()) + { + systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); + } + } + s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); + s->addSaveFunc([systemfocus_list] { + Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); + }); + + // Open gamelist at start + auto bootOnGamelist = std::make_shared(mWindow); + bootOnGamelist->setState(Settings::getInstance()->getBool("StartupOnGameList")); + s->addWithLabel(_T("BOOT ON GAMELIST"), bootOnGamelist); + s->addSaveFunc([bootOnGamelist] { Settings::getInstance()->setBool("StartupOnGameList", bootOnGamelist->getState()); }); + + // Hide system view + auto hideSystemView = std::make_shared(mWindow); + hideSystemView->setState(Settings::getInstance()->getBool("HideSystemView")); + s->addWithLabel(_T("HIDE SYSTEM VIEW"), hideSystemView); + s->addSaveFunc([hideSystemView] { Settings::getInstance()->setBool("HideSystemView", hideSystemView->getState()); }); + + +#if defined(_WIN32) + // quick system select (left/right in game list view) + auto hideWindowScreen = std::make_shared(mWindow); + hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); + s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); + s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); +#endif + + // quick system select (left/right in game list view) + auto quick_sys_select = std::make_shared(mWindow); + quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); + s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); + s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); + + // carousel transition option + auto move_carousel = std::make_shared(mWindow); + move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); + s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); + s->addSaveFunc([move_carousel] { + if (move_carousel->getState() + && !Settings::getInstance()->getBool("MoveCarousel") + && PowerSaver::getMode() == PowerSaver::INSTANT) + { + Settings::getInstance()->setString("PowerSaverMode", "default"); + PowerSaver::init(); + } + Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); + }); + + // show help + auto show_help = std::make_shared(mWindow); + show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); + s->addWithLabel(_T("ON-SCREEN HELP"), show_help); + s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); + + // enable filters (ForceDisableFilters) + auto enable_filter = std::make_shared(mWindow); + enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); + s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); + s->addSaveFunc([enable_filter] { + bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); + Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); + if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); + }); + + + // screensaver + ComponentListRow screensaver_row; + screensaver_row.elements.clear(); + screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); + screensaver_row.addElement(makeArrow(mWindow), false); + screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); + s->addRow(screensaver_row); + + + s->updatePosition(); + mWindow->pushGui(s); +} + +void GuiMenu::openOtherSettings() +{ + auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); + + // maximum vram + auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); + max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); + s->addWithLabel(_T("VRAM LIMIT"), max_vram); + s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); + + // power saver + auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); + std::vector modes; + modes.push_back("disabled"); + modes.push_back("default"); + modes.push_back("enhanced"); + modes.push_back("instant"); + for (auto it = modes.cbegin(); it != modes.cend(); it++) + power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); + + s->addWithLabel(_T("POWER SAVER MODES"), power_saver); + s->addSaveFunc([this, power_saver] { + if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { + Settings::getInstance()->setString("TransitionStyle", "instant"); + Settings::getInstance()->setString("GameTransitionStyle", "instant"); + Settings::getInstance()->setBool("MoveCarousel", false); + Settings::getInstance()->setBool("EnableSounds", false); + } + + GuiComponent::ALLOWANIMATIONS = Settings::getInstance()->getString("TransitionStyle") != "instant"; + + Settings::getInstance()->setString("PowerSaverMode", power_saver->getSelected()); + PowerSaver::init(); + }); + + // gamelists + auto save_gamelists = std::make_shared(mWindow); + save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); + s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); + s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); + + auto parse_gamelists = std::make_shared(mWindow); + parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); + s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); + s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); + +#ifndef WIN32 + auto local_art = std::make_shared(mWindow); + local_art->setState(Settings::getInstance()->getBool("LocalArt")); + s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); + s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); +#endif + + // hidden files + auto hidden_files = std::make_shared(mWindow); + hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); + s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); + s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); + +#ifdef _RPI_ + // Video Player - VideoOmxPlayer + auto omx_player = std::make_shared(mWindow); + omx_player->setState(Settings::getInstance()->getBool("VideoOmxPlayer")); + s->addWithLabel("USE OMX PLAYER (HW ACCELERATED)", omx_player); + s->addSaveFunc([omx_player] + { + // need to reload all views to re-create the right video components + bool needReload = false; + if(Settings::getInstance()->getBool("VideoOmxPlayer") != omx_player->getState()) + needReload = true; + + Settings::getInstance()->setBool("VideoOmxPlayer", omx_player->getState()); + + if(needReload) + ViewController::get()->reloadAll(); + }); + +#endif + + // preload UI + auto preloadUI = std::make_shared(mWindow); + preloadUI->setState(Settings::getInstance()->getBool("PreloadUI")); + s->addWithLabel(_T("PRELOAD UI"), preloadUI); + s->addSaveFunc([preloadUI] { Settings::getInstance()->setBool("PreloadUI", preloadUI->getState()); }); + + // optimizeVram + auto optimizeVram = std::make_shared(mWindow); + optimizeVram->setState(Settings::getInstance()->getBool("OptimizeVRAM")); + s->addWithLabel(_T("OPTIMIZE IMAGES VRAM USE"), optimizeVram); + s->addSaveFunc([optimizeVram] + { + TextureData::OPTIMIZEVRAM = optimizeVram->getState(); + Settings::getInstance()->setBool("OptimizeVRAM", optimizeVram->getState()); + }); + +#ifdef WIN32 + // vsync + auto vsync = std::make_shared(mWindow); + vsync->setState(Settings::getInstance()->getBool("VSync")); + s->addWithLabel(_T("VSYNC"), vsync); + s->addSaveFunc([vsync] + { + Settings::getInstance()->setBool("VSync", vsync->getState()); + + // vsync + if (Settings::getInstance()->getBool("VSync")) + { + // SDL_GL_SetSwapInterval(0) for immediate updates (no vsync, default), + // 1 for updates synchronized with the vertical retrace, + // or -1 for late swap tearing. + // SDL_GL_SetSwapInterval returns 0 on success, -1 on error. + // if vsync is requested, try normal vsync; if that doesn't work, try late swap tearing + // if that doesn't work, report an error + if (SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) + LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; + } + else + SDL_GL_SetSwapInterval(0); + }); +#endif + + // framerate + + auto framerate = std::make_shared(mWindow); + framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); + s->addWithLabel(_T("SHOW FRAMERATE"), framerate); + s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); + + s->updatePosition(); + mWindow->pushGui(s); + +} + +void GuiMenu::openConfigInput() +{ + Window* window = mWindow; + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + /* + window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), + [window] { + window->pushGui(new GuiDetectDevice(window, false, nullptr)); + }, _T("NO"), nullptr) + );*/ + +} + +void GuiMenu::openQuitMenu() +{ +#ifdef WIN32 + Scripting::fireEvent("quit"); + quitES(""); + return; +#endif + + auto s = new GuiSettings(mWindow, _T("QUIT")); + + Window* window = mWindow; + + ComponentListRow row; + if (UIModeController::getInstance()->isUIModeFull()) + { + row.makeAcceptInputHandler([window] { + window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + [] { + Scripting::fireEvent("quit"); + if(quitES("/tmp/es-restart") != 0) + LOG(LogWarning) << "Restart terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", 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", + [] { + Scripting::fireEvent("quit"); + quitES(""); + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + s->addRow(row); + } + } + row.elements.clear(); + row.makeAcceptInputHandler([window] { + window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + [] { + Scripting::fireEvent("quit", "reboot"); + Scripting::fireEvent("reboot"); + if (quitES("/tmp/es-sysrestart") != 0) + LOG(LogWarning) << "Restart terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(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", + [] { + Scripting::fireEvent("quit", "shutdown"); + Scripting::fireEvent("shutdown"); + if (quitES("/tmp/es-shutdown") != 0) + LOG(LogWarning) << "Shutdown terminated with non-zero result!"; + }, "NO", nullptr)); + }); + row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + s->addRow(row); + + s->updatePosition(); + mWindow->pushGui(s); +} + +void GuiMenu::addVersionInfo() +{ + std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); + + auto theme = ThemeData::getMenuTheme(); +// mVersion.setFont(Font::get(FONT_SIZE_SMALL)); +// mVersion.setColor(0x5E5E5EFF); + + mVersion.setFont(theme->Footer.font); + mVersion.setColor(theme->Footer.color); + + mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); + mVersion.setHorizontalAlignment(ALIGN_CENTER); + addChild(&mVersion); +} + +void GuiMenu::openScreensaverOptions() { + mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); +} + +void GuiMenu::openCollectionSystemSettings() { + mWindow->pushGui(new GuiCollectionSystemsOptions(mWindow)); +} + +void GuiMenu::onSizeChanged() +{ + mVersion.setSize(mSize.x(), 0); + mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); +} + +void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func, const std::string iconName) +{ + auto theme = ThemeData::getMenuTheme(); + std::shared_ptr font = theme->Text.font; + color = theme->Text.color; + + // populate the list + ComponentListRow row; + + if (!iconName.empty()) + { + // icon + auto icon = std::make_shared(mWindow); + icon->setImage(iconName); + icon->setColorShift(theme->Text.color); + icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); + row.addElement(icon, false); + + // spacer between icon and text + auto spacer = std::make_shared(mWindow); + spacer->setSize(10, 0); + row.addElement(spacer, false); + } + + row.addElement(std::make_shared(mWindow, name, font, color), true); + + if (add_arrow) + { + std::shared_ptr 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; +} + +HelpStyle GuiMenu::getHelpStyle() +{ + HelpStyle style = HelpStyle(); + style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); + return style; +} + +std::vector GuiMenu::getHelpPrompts() +{ + std::vector prompts; + prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _T("SELECT"))); + prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + return prompts; +} diff --git a/resources/OpenSans-CondBold.ttf b/resources/OpenSans-CondBold.ttf new file mode 100644 index 0000000000000000000000000000000000000000..83966f21381f35a1ee69f30bd8d7a0ef954fe17b GIT binary patch literal 264372 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%0A%+X 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 k7j>sqZ~0%-bRgA6c5xW;*l6N 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 literal 0 HcmV?d00001 diff --git a/resources/arrow.svg b/resources/arrow.svg index 505356adee..e0a9f008da 100644 --- a/resources/arrow.svg +++ b/resources/arrow.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/resources/button.png b/resources/button.png index 7ac2297f9308a4a4d1a14687d244c0a7d3310c16..69a88cbaf5681e6aa3004197717ad75923b86c36 100644 GIT binary patch literal 356 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC#^NA%Cx&(BWL^R}Ea{HEjtmSN z`?>!lvI6;>1s;*b3=DjSL74G){)!Z!phSslL`iUdT1k0gQ7S`0VrE{6US4X6f{C7i zo@I`4ogYxmRZkbkkPPRyvmCh&8Hlhpv%dX$<(jdG{)ETJ|EIP@-P!LFm#~1d-q3lP z@5-%e%OBiii+tHtTzR_K?he;?O=h!il^a;5KPWcdJ28)Y*UR0a`-(p&WUz#7xowwV z$Mf|}&$sXc>ksBR-exqqP}ab+MOJ}1m;HnTS=|5H2|w5hW46K32*3xq68pHF_1f1wh>l3^w)^1&PVosU-?Y zsp*+{wo31J?^jaDOtDo8H}y5}EpSfF$n>ZxN)4{^3rViZPPR-@vbR&PsjvbXkegbP zs8ErclUHn2VXFi-*9yo63F|8KW+g=7RhMR$W{Yl!|Z$R@KEJl?AE#L8-<0 zrA5iW_()TRX$FQJev3c~fv&OgFUkZ)N@9*nesXDUYF>$_i>(q+MlU5Z#md~oz|h>% z)Y8D!$iU3P(9pub!r0l|)!5M0(bUo0)EH(4HocY>E*1usu7;K_7UqVAuEvg*hAvJn zZZ0MUmX=1Yu5i7cdBr7(dC93TdozLdLiHNp)obNkl$uzQUlfv`p94z)0U7xv`NbLe z1q#l=W(pdZIEG{rNBjj_4-V|~|O&{nO zeNgg1N-i)VU>XE5;fWu}foGr8JYb$F0%i^?eY-WljP=*k#WAGf)|*+5dL-x=A$y}uuM={)?uL*Fv9-sjxQ?DR!8=VjH{ zyeBl7WmwuTXDJV8&|M)t#lB(X=lVAXza5bJa?s{ku=!8+8I3G1^Cn&R==i)fH{swe zQH!d5jg$HW@6WnocHwD%y7;-6inmAX<=+@DXjq@EUDnvoB(i{kRl|XiE1&@v^Fu$Q z>ifwJd-kte%6g|lf>p%e$=5VSR(@#%M&JIUXLEVVY{Yrlc(`ffr=PUS3j3^P6GZx^prw85kH?(j9#r85lP9bN@+X1@buyJR*x382Ao@Fyrz3 z6)8YLi4xa{lHmNblJdl&REC1Y%)Ao4ywnl}6Fmbx%N*l6KcJcxPZ!6K4ClAE47m;{ z@HhneKep|Dt}9zQ@k&Ga50-z?Qrl}@%n%c-eP8f*&DMR!(_io<)fK0XvJPRZpj2qGz&N9qmG-1xbDG+AA;Ll3OpUM57&lb-P{BS4D#pOTa YVXv!Uk6!v@0UgBP>FVdQ&MBb@08cqz5&!@I literal 1168 zcmbVLO-K}B7@iWu1SzQ??cgvxxQ0?Ci|9?&QvoGvB(a zC#i!(-3vMtU4jCkh=Tq#BDw^UfpyWKLkCGDibRUOSzYU31`RXc&pYq)y#MFBJC9bB zSC?ZLRuO3zVrZwFudEcUecM+i&{hS-e%NEAU{10C7E+8Pz$2P82x34|Mu%U4CJZY{ ztMPu=AMM~}Lvu@xjytcJ2#sM)EqPOthXBNrU{KYA#L~m31g7^S36cWNnHI(N8qG5MnA+l`jWb6-jdQ~j4^p=R5= z<)4}H89L#6wy=+C9G`f;d~YQ+KET}U>tcGF#v1mo-dXuknZWL6t3yN6l^KUAB6bRs It%O?EnA( diff --git a/resources/checkbox_checked.svg b/resources/checkbox_checked.svg index 5f19b53021..e61e9f234d 100644 --- a/resources/checkbox_checked.svg +++ b/resources/checkbox_checked.svg @@ -1,6 +1 @@ - - - - - - + \ No newline at end of file diff --git a/resources/checkbox_unchecked.svg b/resources/checkbox_unchecked.svg index 5808eb13df..110977c6a9 100644 --- a/resources/checkbox_unchecked.svg +++ b/resources/checkbox_unchecked.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/resources/fav_add.svg b/resources/fav_add.svg index 34402f462b..ca9d965f9c 100644 --- a/resources/fav_add.svg +++ b/resources/fav_add.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/resources/fav_remove.svg b/resources/fav_remove.svg index bb5f3f2a72..7d78461644 100644 --- a/resources/fav_remove.svg +++ b/resources/fav_remove.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/resources/glyphs.ttf b/resources/glyphs.ttf index f6cc4b6a511ac0af60b461af5364f5b398924d43..238aacf77559719c876b83163fad7c471f284416 100644 GIT binary patch delta 214 zcmeys_kmA^fsuiMft#U$ftkU;KUm+WQ07bqP-G7fCnV=47CcCNa&w}J9@EX)6GMC% zy(ccRVr2#DVVw9-MpEGe8*}RaR0c8T3I?DF5WvL1#IRY6aUP?L*Dr^k@%%Pl8Ms-% zQdc(o>i{us3Ql`B`2y2jPX-oFAlHD=9cVd-&B(yVSPo?~0okokHZy|}<76nC1t@+L Z%4TJdV7v@sZ~n%t$;8O8S%EEp5deJ*D~SLA delta 214 zcmeys_kmA^fsuiMft#U$ftkU;KUm)=$KSgPD6$8L6OwZi3mzmsxiwKmk7>`Ri6Op> zK@%5Qu>z$S7$!cHk!1V8#+>>;l|hWT0w|&cGz18k7?{p(7Gs>pC==3N!y3{ z1th_6Wy8M?5Pegy%60Murn{buESx~D0i!$6auA!5fse5q%4PzxTcK=b1|!DFP&Nxt b{3w*o${@jb8N}ZFjaie4k#VyETL2>f9gZrB diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index ccd347f69a..a1afce5e64 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -4,10 +4,58 @@ msgstr "" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "X-Generator: POEditor.com\n" -"Project-Id-Version: recalbox-emulationstation\n" +"Project-Id-Version: emulationstation\n" "Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +#: +msgid "Loading..." +msgstr "Chargement en cours" + +#: +msgid "Preloading UI" +msgstr "Chargement de l'interface" + +#: +msgid "GRID SIZE" +msgstr "DIMENSIONS DE LA GRILLE" + +#: +msgid "DEFAULT GRID SIZE" +msgstr "DIMENSIONS DE LA GRILLE PAR DÉFAUT" + +#: +msgid "UI MODE" +msgstr "TYPE D'INTERFACE" + +#: +msgid "PRELOAD UI" +msgstr "PRECHARGEMENT DE L'INTERFACE" + +#: +msgid "VSYNC" +msgstr "SYNCHRONISATION VERTICALE" + +#: +msgid "DISPLAY FAVORITES FIRST IN GAMELIST" +msgstr "AFFICHER LES FAVORIS EN PREMIER" + +#: +msgid "OPTIMIZE IMAGES VRAM USE" +msgstr "OPTIMISER LE BESOIN MÉMOIRE DES IMAGES" + +#: +msgid "Full" +msgstr "Complet" + +#: +msgid "Kiosk" +msgstr "Kiosque" + +#: +msgid "Kid" +msgstr "Enfant" + #: msgid "GAME COLLECTION SETTINGS" msgstr "LISTES PERSONNALISÉES" @@ -224,11 +272,11 @@ msgstr "JEU %i SUR %i" msgid "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 RECALBOX.COM FOR MORE INFORMATION." +"VISIT EMULATIONSTATION.ORG FOR MORE INFORMATION." msgstr "AUCUN JEU N'A ÉTÉ TROUVÉ SUR LE SYSTÈME !\n" "VEUILLEZ AJOUTER DES JEUX EN SUIVANT LES INSTRUCTIONS DE LA NOTICE.\n" "\n" -"RENDEZ-VOUS SUR RECALBOX.COM POUR PLUS D'INFORMATIONS" +"RENDEZ-VOUS SUR EMULATIONSTATION.ORG POUR PLUS D'INFORMATIONS" #: msgid "%i GAME SUCCESSFULLY SCRAPED!" @@ -1142,10 +1190,10 @@ msgstr "EXTINCTION RAPIDE" msgid "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 RECALBOX.FR FOR MORE INFORMATION." +"VISIT EMULATIONSTATION.ORG FOR MORE INFORMATION." msgstr "Aucun système trouvé !\n" "\n" -"Visitez le site recalbox.com pour plus d'infos !" +"Visitez le site emulationstation.org pour plus d'infos !" #: msgid "ON SCREEN KEYBOARD" @@ -1243,49 +1291,10 @@ msgstr[1] "%i JEUX CACHÉS" msgid "Start kodi media player." msgstr "Démarrer le media player Kodi" -#: -msgid "Select the language for your recalbox, select an external drive to store your games and configurations, check your current version and the free space on your drive" -msgstr "Choisissez la langue de votre Recalbox, choisissez un lecteur externe pour stocker vos jeux et vos configurations, vérifiez la version actuelle et l'espace libre sur votre disque" - -#: -msgid "Shows your current recalboxOS version." -msgstr "Montre la version actuelle de RecalboxOS." - #: msgid "Show how much space is used on your SHARE partition, located either on the SDCARD or on an external drive. The information shows how much GB are used and how much GB your storage has overall (example 13GB/26GB)." msgstr "Montre l'espace utilisé sur la partition SHARE située sur la carte SD ou le lecteur externe. L'information montre combien de Go sont utilisés sur la capacité totale du support (exemple 13Go/26Go)." -#: -msgid "Select an external drive to store your roms, saves, configurations etc.\n" -"Use a FAT32 formatted drive. The system does not format the drive. On first boot, with this option enabled, recalbox will create a '/recalbox' folder with all system files inside." -msgstr "Choisissez un lecteur externe pour stocker vos roms, sauvegardes, configurations etc.\n" -"Utilisez un lecteur formaté en FAT32. Le système ne formate pas le lecteur. Au premier démarrage, avec cette option activée, Recalbox créera un dossier '/recalbox' avec tous les fichiers système dedans." - -#: -msgid "Select your language. A reboot is needed to set this configuration active." -msgstr "Choisissez votre langue. Un redémarrage est nécessaire pour activer cette option." - -#: -msgid "Manage your recalbox updates. Select the update type. Activate update check." -msgstr "Gérez les mises à jour de Recalbox. Choisissez le type de mise à jour. Activez la vérification de mise à jour." - -#: -msgid "Check if an update is available, and start the update process." -msgstr "Vérifiez si une mise à jour est disponible et lancer le processus de mise à jour." - -#: -msgid "Stable updates will check for updates on stable recalbox releases. Stable updates are tested and approved by the recalbox team and their testers.\n" -"Unstable updates allows you to get the latest recalbox features by checking our unstable repository. You can test and validate with us the very last version of recalbox.\n" -"If you choose unstable update, be so kind to report issues on the recalbox-os issue board (https://github.com/recalbox/recalbox-os/issues)" -msgstr "Stable vérifiera les mises à jour parmi les sorties stables de Recalbox.\n" -"Les mises à jour stables sont testées et approuvées par l'équipe Recalbox et ses testeurs.\n" -"Les mise à jour unstable vous permettent de profiter des dernières fonctionnalités de Recalbox en vérifiant le dépôt unstable. Vous pouvez tester et valider avec nous la toute dernière version de Recalbox.\n" -"Si vous choisissez les mise à jour unstable, merci de signaler les problèmes sur le forum dédié (https://github.com/recalbox/recalbox-os/issues)." - -#: -msgid "Automatically check if an update is avaialble. If so, it notifies you with a message." -msgstr "Vérifier automatiquement si une mise à jour est disponible. Si tel est le cas, un message vous en informe." - #: msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." msgstr "Configurez l'affichage des jeux, le ratio, les filtres (shaders), sauvgarde/chargement auto et votre compte retroachievement." @@ -1331,20 +1340,6 @@ msgstr "Le site web retroachievements.org propose des challenges/succès/trophé msgid "Add and configure up to 5 controllers." msgstr "Ajoutez et configurez jusqu'à 5 manettes." -#: -msgid "Pair a bluetooth controller with your recalbox. Your controller must be in pairing mode." -msgstr "Appairez une manette Bluetooth avec votre Recalbox. Votre manette doit être en mode appairage." - -#: -msgid "Forget all paired bluetooth controllers. You will have to pair your controllers again, but this option can help if you have issues to reconnect a controller, which is already paired." -msgstr "Oublier toutes les manettes appairées. Vous devrez appairer votre manette de nouveau mais cette option est utile si vous rencontrez des problèmes pour reconnecter une manette déjà appairée." - -#: -msgid "Configure your EmulationStation experience. Select transition types, help prompts, screensaver behavior. You can also deactivate the onscreen keyboard if you have a real keyboard plugged into your recalbox.\n" -"If you've added games since the last boot, you can also refresh the gamelist from this menu." -msgstr "Configurez votre expérience EmulationStation. Sélectionnez le type de transition, l'aide, le comportement de l'économiseur d'écran. Vous pouvez également désactiver le clavier virtuel si vous avez un clavier branché sur votre Recalbox.\n" -"Si vous avez ajouté des jeux depuis le dernier démarrage, vous pouvez aussi rafraîchir les liste de jeux depuis ce menu." - #: msgid "Start the screensaver after N minutes." msgstr "Démarrer l'économiseur d'écran après N minutes." @@ -1361,65 +1356,14 @@ msgstr "Montre l'aide en bas de l'écran qui vous indique les commandes que vous msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." msgstr "Si activé, vous pouvez changer de système tout en parcourant les listes de jeux en appuyant sur GAUCHE ou DROITE." -#: -msgid "The onscreen keyboard is necessary to type text if you only have controllers plugged into your recalbox. You can disable it if you have a real keyboard connected." -msgstr "Le clavier virtuel est nécessaire pour taper du texte si vous n'avez que des manettes branchées sur votre Recalbox. Vous pouvez désactiver cette option si vous avez un clavier branché." - -#: -msgid "Select a theme for your recalbox." -msgstr "Sélectionnez un thème pour votre Recalbox." - #: msgid "Updates the gamelists, if you added games since the last boot." msgstr "Mettez à jour les listes de jeux si vous avez ajouté des jeux depuis le dernier démarrage." -#: -msgid "Configure the sound options of your recalbox." -msgstr "Configurez les options sonores de votre Recalbox." - #: msgid "Set the volume of the sound output for the frontend and the games." msgstr "Réglez le volume de la sortie audio pour EmulationStation et les jeux." -#: -msgid "Enable or disable the frontend music. You can add your own music as mp3, or ogg format in the 'musics' directory of your recalbox." -msgstr "Activez ou désactivez la musique dans EmulationStation. Vous pouvez ajouter vos propres musiques au format mp3 ou ogg dans le dossier 'musics' de votre Recalbox." - -#: -msgid "Select your output device. Only HDMI and JACK are supported." -msgstr "Sélectionnez votre périphérique de sortie. Seuls HDMI et JACK sont supportés." - -#: -msgid "Configure the network options of your recalbox.\n" -"Check your network status and IP address, set the hostname and configure the WIFI." -msgstr "Configurez les options réseau de votre recalbox. Vérifiez le statut de connexion et l'adresse IP, choisissez le nom réseau et configurez le WIFI." - -#: -msgid "Displays CONNECTED, if you are connected, by checking if your recalbox can access the recalbox.com update server." -msgstr "Affiche CONNECTÉ si vous êtes connecté en vérifiant si votre Recalbox peut accéder au serveur de mise à jour recalbox.com. " - -#: -msgid "The IP address of your recalbox within your local network." -msgstr "L'adresse IP de votre Recalbox dans votre réseau local." - -#: -msgid "Enable or disable WIFI.\n" -"If you disable WIFI, the SSID and the WIFI passwords are saved and can be used when you reactivate it" -msgstr "Activez ou désactivez le WIFI.\n" -"Si vous désactivez le WIFI, le SSID et la clé WIFI sont sauvegardés et réutilisés quand vous réactivez l'option." - -#: -msgid "The name of your recalbox in your local network" -msgstr "Le nom de votre Recalbox sur le réseau local." - -#: -msgid "SSID (WIFI Name) of your network." -msgstr "SSID (Nom WIFI) de votre réseau." - -#: -msgid "Private key of your WIFI network." -msgstr "Clé de sécurité de votre réseau WIFI." - #: msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." msgstr "Récupérez des informations et visuels pour vos jeux. Le scraper télécharge les metadatas et images pour vos jeux depuis différents serveurs et améliore complètement l'expérience d'EmulationStation." @@ -1447,15 +1391,6 @@ msgstr "Overclockez votre carte pour augmenter les performances.\n" "Les paramètres d'overclocking sont testés et validés par la communauté.\n" "Gardez à l'esprit qu'overclocker votre carte peut annuler votre garantie." -#: -msgid "Select which system to show when the recalbox frontend starts. The default value is 'favorites'." -msgstr "Choisissez quel système montrer quand EmulationStation démarre.\n" -"La valeur par défaut est 'favorites'." - -#: -msgid "On boot, recalbox will show the list of games of the selected system rather than the system view." -msgstr "Au démarrage, Recalbox montrera la liste de jeux du système sélectionné plutôt que la vue systèmes." - #: msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" "This option highly speeds up boot time, but new games will not be detected." @@ -1471,14 +1406,6 @@ msgstr "Cette option vous permet de mettre le système sélectionné en mode fix msgid "Always display the basic gamelist view, even if you have scraped your games." msgstr "Toujours afficher la vue liste de jeux basique, même si vous avez scrappé vos jeux." -#: -msgid "Override global options like emulator, core, ratio and more for each available system in your recalbox." -msgstr "Passer outre les options globales comme émulateur, core, ratio et plus pour chaque système disponible dans votre Recalbox." - -#: -msgid "Configure boot options that make your recalbox boot straight into a system or into Kodi, lock a user to a single system, or directly show the gamelist." -msgstr "Configurez les options de démarrage qui font démarrer votre Recalbox directement dans un système ou dans Kodi, bloquer un utilisateur dans un seul système ou montrer directement la liste de jeux." - #: msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" msgstr "Activez ou désactivez Kodi, configurez le démarrage de Kodi, activez le bouton X pour démarrer Kodi." @@ -1495,45 +1422,10 @@ msgstr "Utilisez le bouton X pour démarrer Kodi." msgid "Automatically start into Kodi on boot." msgstr "Lancer directement Kodi au démarrage." -#: -msgid "Manage your recalbox security." -msgstr "Gérez la sécurité de votre Recalbox." - -#: -msgid "Change the SSH root password." -msgstr "Changez le mot de passe root SSH." - -#. not sure 'bout this one -#: -msgid "Enforce recalbox security." -msgstr "Renforcer la sécurité de Recalbox." - -#: -msgid "Enable or disable overscan.\n" -"Overscan can help you, if you have a black border, or if the image is bigger than your screen. Before setting the overscan, try to configure your TV to have a 1:1 pixel output.\n" -"More overscan settings can be defined in the boot.txt file, available when you plug your SD card into your computer." -msgstr "Activez ou désactivez l'overscan.\n" -"L'overscan peut vous aider si vous avez des bandes noires ou si l'image est plus grande que l'écran. Avant de régler l'overscan, essayez de régler votre TV pour avoir une sortie 1:1.\n" -"D'autres réglages d'overscan peuvent être définis dans le fichier boot.txt disponible lorsque vous branchez votre carte SD sur un ordinateur." - #: msgid "Show the framerate in EmulationStation and in game." msgstr "Affichez le taux de rafraîchissement dans EmulationStation et dans les jeux." -#: -msgid "Enable or disable the Recalbox Manager.\n" -"The Recalbox Manager is a web application available on http://recalbox , if you are on windows, http://recalbox.local , if you are on Linux or Mac, or directly with your recalbox IP : http://192.168.1.XX.\n" -"You can configure many options from within the manager, and even manage games, saves, and scrapes!" -msgstr "Activez ou désactivez le Gestionnaire Recalbox.\n" -"Le Gestionnaire Recalbox est une application web disponible sur http://recalbox si vous êtes sous Windows, http://recalbox.local si vous êtes sous Linux ou Mac ou directement avec l'adresse IP de la Recalbox: http://192.168.1.XX.\n" -"Vous pouvez configurer de nombreuses options dans le Gestionnaire et même gérer les jeux, les sauvegardes et les scraps!" - -#: -msgid "Enable or disable the recalbox API.\n" -"The Recalbox API is a REST API exposing endpoints to control your recalbox via http requests." -msgstr "Activez ou désactivez l'API Recalbox.\n" -"L'API Recalbox est une API REST permettant de contrôler votre Recalbox avec des requêtes http." - #: msgid "Select which emulator to use when you start a game for this system." msgstr "Selectionnez quel émulateur utiliser quand vous lancer un jeu pour ce système." @@ -1990,10 +1882,6 @@ msgstr "Joueur" msgid "Game" msgstr "Jeu" -#: -msgid "A Recalbox friend has started a Netplay game!" -msgstr "Un ami Recalbox a démarré une partie Netplay!" - #: msgid "Play online on games running through Retroarch like NES, SNES, FBA, Genesis/Megadrive and more!" msgstr "Jouez en ligne à des jeux tournant sous Retroarch comme NES, SNES, FBA, Megadrive et bien plus!" diff --git a/resources/locale/it/emulationstation2.po b/resources/locale/it/emulationstation2.po deleted file mode 100644 index f74c4c50b7..0000000000 --- a/resources/locale/it/emulationstation2.po +++ /dev/null @@ -1,1884 +0,0 @@ -msgid "" -msgstr "" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"X-Generator: POEditor.com\n" -"Project-Id-Version: recalbox-emulationstation\n" -"Language: es\n" -"Plural-Forms: nplurals=2; plural=(n != 1);\n" - -#: -msgid "AN UPDATE IS AVAILABLE FOR YOUR RECALBOX" -msgstr "HAY UNA ACTUALIZACIÓN DISPONIBLE PARA RECALBOX" - -#: -msgid "CANCEL" -msgstr "CANCELAR" - -#: -msgid "Rating" -msgstr "Calificación" - -#: -msgid "Released" -msgstr "Lanzado" - -#: -msgid "Developer" -msgstr "Desarrollador" - -#: -msgid "Publisher" -msgstr "Editor" - -#: -msgid "Genre" -msgstr "Género" - -#: -msgid "Players" -msgstr "Jugadores" - -#: -msgid "NO GAMES FOUND - SKIP" -msgstr "JUEGOS NO ENCONTRADOS - OMITIR" - -#: -msgid "RETRY" -msgstr "REINTENTAR" - -#: -msgid "SKIP" -msgstr "OMITIR" - -#: -msgid "SEARCH FOR" -msgstr "BUSCAR POR" - -#: -msgid "SEARCH" -msgstr "BUSCAR" - -#: -msgid "SCRAPING IN PROGRESS" -msgstr "OBTENCIÓN DE DATOS EN CURSO" - -#: -msgid "SYSTEM" -msgstr "SISTEMA" - -#: -msgid "subtitle text" -msgstr "subtítulo" - -#: -msgid "INPUT" -msgstr "ENTRADA" - -#: -msgid "search" -msgstr "buscar" - -#: -msgid "STOP" -msgstr "DETENER" - -#: -msgid "stop (progress saved)" -msgstr "detener (guardar progreso)" - -#: -msgid "GAME %i OF %i" -msgstr "JUEGO %i DE %i" - -#: -msgid "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 RECALBOX.COM FOR MORE INFORMATION." -msgstr "¡NO SE HAN ENCONTRADO SISTEMAS!\n" -"COMPROBAR RUTAS EN ARCHIVO DE CONFIGURACIÓN.\n" -"LA CARPETA DE JUEGOS DEBE CONTENER AL MENOS UNO Y CON LA EXTENSIÓN CORRECTA.\n" -"\n" -"VISITAR RECALBOX.COM PARA OBTENER MÃS INFORMACIÓN" - -#: -msgid "%i GAME SUCCESSFULLY SCRAPED!" -msgid_plural "%i GAMES SUCCESSFULLY SCRAPED!" -msgstr[0] "¡INFORMACIÓN DE %i JUEGO OBTENIDA CORRECTAMENTE!" -msgstr[1] "¡INFORMACIÓN DE %i JUEGOS OBTENIDA CORRECTAMENTE!" - -#: -msgid "%i GAME SKIPPED." -msgid_plural "%i GAMES SKIPPED." -msgstr[0] "%i JUEGO OMITIDO" -msgstr[1] "%i JUEGOS OMITIDOS" - -#: -msgid "OK" -msgstr "OK" - -#: -msgid "EDIT METADATA" -msgstr "EDITAR METADATOS" - -#: -msgid "SCRAPE" -msgstr "OBTENER INFORMACIÓN" - -#: -msgid "SAVE" -msgstr "GUARDAR" - -#: -msgid "THIS WILL DELETE A FILE!\n" -"ARE YOU SURE?" -msgstr "¿SEGURO DESEA BORRAR EL ARCHIVO?" - -#: -msgid "YES" -msgstr "SÃ" - -#: -msgid "NO" -msgstr "NO" - -#: -msgid "DELETE" -msgstr "BORRAR" - -#: -msgid "SAVE CHANGES?" -msgstr "¿GUARDAR CAMBIOS?" - -#: -msgid "BACK" -msgstr "VOLVER" - -#: -msgid "CLOSE" -msgstr "CERRAR" - -#: -msgid "MAIN MENU" -msgstr "MENÚ PRINCIPAL" - -#: -msgid "KODI MEDIA CENTER" -msgstr "KODI MEDIA CENTER" - -#: -msgid "SYSTEM SETTINGS" -msgstr "CONFIGURACIÓN DE SISTEMA" - -#: -msgid "VERSION" -msgstr "VERSIÓN" - -#: -msgid "DISK USAGE" -msgstr "USO DE DISCO" - -#: -msgid "STORAGE DEVICE" -msgstr "ALMACENAMIENTO" - -#: -msgid "LANGUAGE" -msgstr "IDIOMA" - -#: -msgid "OVERCLOCK" -msgstr "OVERCLOCK" - -#: -msgid "EXTREM (1100Mhz)" -msgstr "EXTREMO (1100Mhz)" - -#: -msgid "TURBO (1000Mhz)" -msgstr "TURBO (1000Mhz)" - -#: -msgid "HIGH (950Mhz)" -msgstr "ALTO (950Mhz)" - -#: -msgid "NONE (700Mhz)" -msgstr "NO (700Mhz)" - -#: -msgid "TURBO (1050Mhz)+" -msgstr "TURBO (1050Mhz)+" - -#: -msgid "HIGH (1050Mhz)" -msgstr "ALTO (1050Mhz)" - -#: -msgid "NONE (900Mhz)" -msgstr "NO (900Mhz)" - -#: -msgid "NONE (1200Mhz)" -msgstr "NO (1200Mhz)" - -#: -msgid "NONE" -msgstr "NO" - -#. NEW SETTINGS ORGANIZATION -#: -msgid "UPDATES" -msgstr "ACTUALIZACIONES" - -#: -msgid "AUTO UPDATES" -msgstr "ACTUALIZACIONES AUTOMÃTICAS" - -#: -msgid "START UPDATE" -msgstr "COMENZAR ACTUALIZACIÓN" - -#: -msgid "KODI SETTINGS" -msgstr "CONFIGURACIÓN DE KODI" - -#: -msgid "ENABLE KODI" -msgstr "ACTIVAR KODI" - -#: -msgid "KODI AT START" -msgstr "INICIAR KODI AL ARRANCAR" - -#: -msgid "START KODI WITH X" -msgstr "PULSAR X PARA INICIAR KODI" - -#: -msgid "SECURITY" -msgstr "SEGURIDAD" - -#: -msgid "ENFORCE SECURITY" -msgstr "REFORZAR SEGURIDAD" - -#: -msgid "ROOT PASSWORD" -msgstr "CONTRASEÑA ROOT" - -#: -msgid "THE SYSTEM WILL NOW REBOOT" -msgstr "EL SISTEMA SERà REINICIADO" - -#: -msgid "GAMES SETTINGS" -msgstr "CONFIGURACIÓN DE JUEGOS" - -#: -msgid "GAME RATIO" -msgstr "RELACIÓN DE ASPECTO" - -#: -msgid "SMOOTH GAMES" -msgstr "SUAVIZAR JUEGOS" - -#: -msgid "REWIND" -msgstr "REBOBINAR" - -#: -msgid "AUTO SAVE/LOAD" -msgstr "GUARDAR/CARGAR AUTOMÃTICAMENTE" - -#: -msgid "SHADERS SET" -msgstr "CONJUNTO DE SHADERS" - -#. Yo dejaría en español SCANLINES, ya que en españa también se refiere a este filtro como SCANLINES, y no como LINEAS.. -#: -msgid "SCANLINES" -msgstr "SCANLINES" - -#: -msgid "RETRO" -msgstr "RETRO" - -#: -msgid "RETROACHIEVEMENTS SETTINGS" -msgstr "CONFIGURACIÓN DE RETROLOGROS" - -#: -msgid "RETROACHIEVEMENTS" -msgstr "RETROLOGROS" - -#: -msgid "HARDCORE MODE" -msgstr "MODO HARDCORE" - -#: -msgid "USERNAME" -msgstr "USUARIO" - -#: -msgid "PASSWORD" -msgstr "CONTRASEÑA" - -#: -msgid "ADVANCED" -msgstr "AVANZADO" - -#: -msgid "REALLY UPDATE GAMES LISTS ?" -msgstr "¿ACTUALIZAR LISTA DE JUEGOS?" - -#: -msgid "UPDATE GAMES LISTS" -msgstr "ACTUALIZAR LISTA DE JUEGOS" - -#: -msgid "CONTROLLERS SETTINGS" -msgstr "CONFIGURACIÓN DE MANDOS" - -#: -msgid "UI SETTINGS" -msgstr "CONFIGURACIÓN DE INTERFAZ" - -#: -msgid "OVERSCAN" -msgstr "OVERSCAN" - -#: -msgid "SCREENSAVER AFTER" -msgstr "SALVAPANTALLA A LOS" - -#: -msgid "TRANSITION STYLE" -msgstr "TIPO DE TRANSICIÓN" - -#: -msgid "SCREENSAVER BEHAVIOR" -msgstr "COMPORTAMIENTO DE SALVAPANTALLA" - -#: -msgid "SHOW FRAMERATE" -msgstr "MOSTRAR TASA DE REFRESCO" - -#: -msgid "ON-SCREEN HELP" -msgstr "AYUDA EN PANTALLA" - -#: -msgid "QUICK SYSTEM SELECT" -msgstr "SELECCIÓN RÃPIDA DE SISTEMA" - -#: -msgid "THEME SET" -msgstr "TEMA" - -#: -msgid "SOUND SETTINGS" -msgstr "CONFIGURACIÓN DE AUDIO" - -#: -msgid "SYSTEM VOLUME" -msgstr "VOLUMEN DE SISTEMA" - -#: -msgid "FRONTEND MUSIC" -msgstr "MÚSICA DE FONDO" - -#: -msgid "OUTPUT DEVICE" -msgstr "SALIDA DE AUDIO" - -#: -msgid "HDMI" -msgstr "HDMI" - -#: -msgid "JACK" -msgstr "JACK" - -#: -msgid "AUTO" -msgstr "AUTO" - -#: -msgid "NETWORK SETTINGS" -msgstr "CONFIGURACIÓN DE RED" - -#: -msgid "CONNECTED" -msgstr "CONECTADO" - -#: -msgid "NOT CONNECTED" -msgstr "NO CONECTADO" - -#: -msgid "STATUS" -msgstr "ESTADO" - -#: -msgid "IP ADDRESS" -msgstr "DIRECCIÓN IP" - -#: -msgid "HOSTNAME" -msgstr "NOMBRE DE HOST" - -#: -msgid "ENABLE WIFI" -msgstr "ACTIVAR WIFI" - -#: -msgid "WIFI SSID" -msgstr "SSID WIFI" - -#: -msgid "WIFI KEY" -msgstr "CLAVE WIFI" - -#: -msgid "WIFI ENABLED" -msgstr "WIFI ACTIVADO" - -#: -msgid "WIFI CONFIGURATION ERROR" -msgstr "ERROR DE CONFIGURACIÓN WIFI" - -#: -msgid "SCRAPER" -msgstr "SCRAPER" - -#: -msgid "SCRAPE FROM" -msgstr "OBTENER DATOS DE" - -#: -msgid "SCRAPE RATINGS" -msgstr "OBTENER CALIFICACIONES" - -#: -msgid "SCRAPE NOW" -msgstr "OBTENER DATOS AHORA" - -#: -msgid "QUIT" -msgstr "SALIR" - -#: -msgid "REALLY RESTART?" -msgstr "¿REINICIAR?" - -#: -msgid "RESTART SYSTEM" -msgstr "REINICIAR SISTEMA" - -#: -msgid "REALLY SHUTDOWN?" -msgstr "¿APAGAR?" - -#: -msgid "SHUTDOWN SYSTEM" -msgstr "APAGAR SISTEMA" - -#: -msgid "Emulator" -msgstr "Emulador" - -#: -msgid "Core" -msgstr "Núcleo" - -#: -msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." -msgstr "SI SÓLO EXISTE UN MANDO, CONFIGURAR LAS DIRECCIONES Y OMITIR LA CONFIGURACIÓN, MANTENIENDO PRESIONADO UN BOTÓN.\n" -"SI CARECE DE BOTÓN PARA HOTKEY, UTILIZAR BOTÓN SELECT.\n" -"OMITIR BOTONES FALTANTES, MANTENIENDO PRESIONADO CUALQUIER OTRO BOTÓN.\n" -"LOS NOMBRES DE LOS BOTONES SE BASAN EN EL MANDO DE SNES." - -#. GUIMENU -#: -msgid "CONFIGURE A CONTROLLER" -msgstr "CONFIGURAR UN MANDO" - -#. Bluetooth -#: -msgid "CONTROLLER PAIRED" -msgstr "MANDO VINCULADO" - -#: -msgid "UNABLE TO PAIR CONTROLLER" -msgstr "IMPOSIBLE VINCULAR MANDO" - -#: -msgid "AN ERROR OCCURED" -msgstr "ERROR" - -#: -msgid "NO CONTROLLERS FOUND" -msgstr "NINGÚN MANDO DETECTADO" - -#: -msgid "PAIR A BLUETOOTH CONTROLLER" -msgstr "VINCULAR MANDO BLUETOOTH" - -#: -msgid "CONTROLLERS LINKS HAVE BEEN DELETED." -msgstr "MANDOS VINCULADOS BORRADOS" - -#: -msgid "FORGET BLUETOOTH CONTROLLERS" -msgstr "DESVINCULAR MANDOS BLUETOOTH" - -#: -msgid "INPUT P%i" -msgstr "ENTRADA P%i" - -#: -msgid "CHOOSE" -msgstr "SELECCIONAR" - -#: -msgid "SELECT" -msgstr "SELECCIONAR" - -#: -msgid "OPTIONS" -msgstr "OPCIONES" - -#: -msgid "JUMP TO LETTER" -msgstr "IR A LETRA" - -#: -msgid "SORT GAMES BY" -msgstr "ORDENAR JUEGOS POR" - -#. FAVORITES -#: -msgid "FAVORITES ONLY" -msgstr "SOLO FAVORITOS" - -#: -msgid "EDIT THIS GAME'S METADATA" -msgstr "EDITAR DATOS DEL JUEGO" - -#: -msgid "SCRAPE THESE GAMES" -msgstr "OBTENER DATOS DE JUEGOS" - -#: -msgid "All Games" -msgstr "Todos los juegos" - -#. MISSING SCRAPPER TRANSLATIONS -#: -msgid "Only missing image" -msgstr "Solo imágenes faltantes" - -#: -msgid "FILTER" -msgstr "FILTRO" - -#: -msgid "SCRAPE THESE SYSTEMS" -msgstr "OBTENER SISTEMAS" - -#: -msgid "SYSTEMS" -msgstr "SISTEMAS" - -#: -msgid "USER DECIDES ON CONFLICTS" -msgstr "DECIDIR EN CASO DE CONFLICTOS" - -#: -msgid "START" -msgstr "INICIAR" - -#: -msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" -"CONTINUE ANYWAY?" -msgstr "CUIDADO: ALGÚN SISTEMA SELECCIONADO NO TIENE PLATAFORMA DEFINIDA.\n" -"¡LOS RESULTADOS PODRÃAN SER MÃS INCORRECTOS DE LO HABITUAL!\n" -"¿ACEPTAR?" - -#: -msgid "NO GAMES FIT THAT CRITERIA." -msgstr "NINGÚN JUEGO SE AJUSTA AL CRITERIO" - -#: -msgid "REALLY UPDATE?" -msgstr "¿ACTUALIZAR?" - -#: -msgid "NETWORK CONNECTION NEEDED" -msgstr "SE NECESITA CONEXIÓN DE RED" - -#. Lo correcto sería: -#. -#. ACTUALIZACIÓN DESCARGADA, EL SISTEMA SE REINICIARà -#: -msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" -msgstr "ACTUALIZACIÓN DESCARGADA, EL SISTEMA SE REINICIARÃ" - -#. Lo correcto sería: -#. -#. ACTUALIZACIÓN FALLIDA, EL SISTEMA SE REINICIARà -#: -msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" -msgstr "ACTUALIZACIÓN FALLIDA, EL SISTEMA SE REINICIARÃ" - -#: -msgid "NO UPDATE AVAILABLE" -msgstr "SIN ACTUALIZACIONES DISPONIBLES" - -#: -msgid "enter emulator" -msgstr "ingresar emulador" - -#: -msgid "enter core" -msgstr "ingresar núcleo" - -#: -msgid "Ratio" -msgstr "Proporción" - -#: -msgid "enter ratio" -msgstr "ingresar proporción" - -#: -msgid "Name" -msgstr "Nombre" - -#: -msgid "enter game name" -msgstr "ingresar nombre del juego" - -#: -msgid "Description" -msgstr "Descripción" - -#: -msgid "enter description" -msgstr "ingresar descripción" - -#: -msgid "Image" -msgstr "Imagen" - -#: -msgid "enter path to image" -msgstr "ingresar ruta de imagen" - -#: -msgid "Thumbnail" -msgstr "Miniatura" - -#: -msgid "enter path to thumbnail" -msgstr "ingresar ruta de miniatura " - -#. the english translation has a space befor the word "enter", typo? -#: -msgid "enter rating" -msgstr " ingresar calificación" - -#: -msgid "Release date" -msgstr "Fecha de lanzamiento" - -#: -msgid "enter release date" -msgstr "ingresar fecha de lanzamiento" - -#: -msgid "enter game developer" -msgstr "ingresar desarrollador" - -#. La palabra correcta para "publisher" es "editor". -#: -msgid "enter game publisher" -msgstr "ingresar editor" - -#: -msgid "enter game genre" -msgstr "ingresar género" - -#: -msgid "enter number of players" -msgstr "ingresar número de jugadores" - -#: -msgid "Favorite" -msgstr "Favorito" - -#: -msgid "enter favorite" -msgstr "ingresar favorito" - -#: -msgid "Region" -msgstr "Región" - -#: -msgid "enter region" -msgstr "ingresar región" - -#: -msgid "Romtype" -msgstr "Tipo de ROM" - -#: -msgid "enter romtype" -msgstr "ingresar tipo de ROM" - -#: -msgid "Hidden" -msgstr "Oculto" - -#: -msgid "set hidden" -msgstr "ocultar" - -#: -msgid "Play count" -msgstr "Partidas jugadas" - -#: -msgid "enter number of times played" -msgstr "ingresar partidas jugadas" - -#: -msgid "Last played" -msgstr "Última partida" - -#: -msgid "enter last played date" -msgstr "ingresar fecha de última partida" - -#: -msgid "%i GAME AVAILABLE" -msgid_plural "%i GAMES AVAILABLE" -msgstr[0] "%i JUEGO DISPONIBLE" -msgstr[1] "%i JUEGOS DISPONIBLES" - -#: -msgid "%i FAVORITE" -msgid_plural "%i FAVORITES" -msgstr[0] "%i FAVORITO" -msgstr[1] "%i FAVORITOS" - -#. Imagino que aquí queda mejor utilizar el infinitivo, comparandolo con la siguiente entrada... -#: -msgid "SCROLL" -msgstr "DESPLAZAR" - -#: -msgid "LAUNCH" -msgstr "LANZAR" - -#: -msgid "Times played" -msgstr "Veces jugado" - -#: -msgid "MENU" -msgstr "MENÚ" - -#: -msgid "FILENAME, ASCENDING" -msgstr "NOMBRE, ASCENDENTE" - -#: -msgid "FILENAME, DESCENDING" -msgstr "NOMBRE, DESCENDENTE" - -#: -msgid "RATING, ASCENDING" -msgstr "CALIFICACIÓN, ASCENDENTE" - -#: -msgid "RATING, DESCENDING" -msgstr "CALIFICACIÓN, DESCENDENTE" - -#: -msgid "TIMES PLAYED, ASCENDING" -msgstr "PARTIDAS, ASCENDENTE" - -#: -msgid "TIMES PLAYED, DESCENDING" -msgstr "PARTIDAS, DESCENDENTE" - -#: -msgid "LAST PLAYED, ASCENDING" -msgstr "ÚLTIMA PARTIDA, ASCENDENTE" - -#: -msgid "LAST PLAYED, DESCENDING" -msgstr "ÚLTIMA PARTIDA, DESCENDENTE" - -#: -msgid "WORKING..." -msgstr "TRABAJANDO..." - -#: -msgid "CHANGE" -msgstr "CAMBIAR" - -#: -msgid "never" -msgstr "nunca" - -#: -msgid "just now" -msgstr "ahora mismo" - -#: -msgid "%i sec ago" -msgid_plural "%i secs ago" -msgstr[0] "Hace %i segundo" -msgstr[1] "Hace %i segundos" - -#: -msgid "%i min ago" -msgid_plural "%i mins ago" -msgstr[0] "Hace %i minuto" -msgstr[1] "Hace %i minutos" - -#: -msgid "%i hour ago" -msgid_plural "%i hours ago" -msgstr[0] "Hace %i hora" -msgstr[1] "Hace %i horas" - -#: -msgid "%i day ago" -msgid_plural "%i days ago" -msgstr[0] "Hace %i día" -msgstr[1] "Hace %i días" - -#: -msgid "unknown" -msgstr "desconocido" - -#: -msgid "SELECT ALL" -msgstr "SELECCIONAR TODO" - -#: -msgid "SELECT NONE" -msgstr "SELECCIONAR NINGUNO" - -#: -msgid "%i SELECTED" -msgid_plural "%i SELECTED" -msgstr[0] "%i SELECCIONADO" -msgstr[1] "%i SELECCIONADOS" - -#: -msgid "UP" -msgstr "ARRIBA" - -#: -msgid "DOWN" -msgstr "ABAJO" - -#: -msgid "LEFT" -msgstr "IZQUIERDA" - -#: -msgid "RIGHT" -msgstr "DERECHA" - -#. @Claudio, mando se refiere a todo el control, y no queda claro que se trata del joystick/palanca. Voy a volver a poner joystick a todos estos. -#: -msgid "JOYSTICK 1 UP" -msgstr "JOYSTICK 1 ARRIBA" - -#: -msgid "JOYSTICK 1 LEFT" -msgstr "JOYSTICK 1 IZQUIERDA" - -#: -msgid "JOYSTICK 2 UP" -msgstr "JOYSTICK 2 ARRIBA" - -#: -msgid "JOYSTICK 2 LEFT" -msgstr "JOYSTICK 2 IZQUIERDA" - -#. Creo que esta traducción no es correcta, ya que al configurar los botones L1 y R1 debería ser: -#. -#. L1: PÃGINA ANTERIOR -#. R1: PÃGINA SIGUIENTE -#: -msgid "PAGE UP" -msgstr "PÃGINA ARRIBA" - -#: -msgid "PAGE DOWN" -msgstr "PÃGINA ABAJO" - -#: -msgid "HOTKEY" -msgstr "HOTKEY" - -#: -msgid "CONFIGURING" -msgstr "CONFIGURANDO" - -#: -msgid "KEYBOARD" -msgstr "TECLADO" - -#: -msgid "GAMEPAD %i" -msgstr "MANDO %i" - -#. Config controllers missing translation -#: -msgid "PRESS ANYTHING" -msgstr "PULSAR CUALQUIER BOTÓN" - -#: -msgid "ALREADY TAKEN" -msgstr "EN USO" - -#: -msgid "DISCARD CHANGES" -msgstr "DESCARTAR CAMBIOS" - -#: -msgid "WELCOME" -msgstr "BIENVENIDO" - -#: -msgid "CONFIGURE INPUT" -msgstr "CONFIGURAR MANDOS" - -#: -msgid "%i GAMEPAD DETECTED" -msgid_plural "%i GAMEPADS DETECTED" -msgstr[0] "%i MANDO DETECTADO" -msgstr[1] "%i MANDOS DETECTADOS" - -#: -msgid "NO GAMEPADS DETECTED" -msgstr "SIN MANDOS DETECTADOS" - -#: -msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." -msgstr "MANTENER PRESIONADO UN BOTÓN DEL MANDO PARA CONFIGURARLO" - -#: -msgid "PRESS F4 TO QUIT AT ANY TIME." -msgstr "PRESIONAR F4 PARA SALIR EN CUALQUIER MOMENTO" - -#: -msgid "PRESS ESC OR THE HOTKEY TO CANCEL." -msgstr "PRESIONAR ESC O HOTKEY PARA CANCELAR" - -#: -msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" -msgstr "¿INICIAR EL CENTRO MULTIMEDIA KODI?" - -#: -msgid "LOADING..." -msgstr "CARGANDO..." - -#: -msgid "PLEASE WAIT..." -msgstr "ESPERAR..." - -#: -msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" -msgstr "¿APAGAR SIN GUARDAR DATOS?" - -#: -msgid "FAST SHUTDOWN SYSTEM" -msgstr "APAGAR RÃPIDO" - -#. Please change RECALBOX.FR to RECALBOX.COM on the English version! -#: -msgid "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 RECALBOX.FR FOR MORE INFORMATION." -msgstr "¡NO SE HAN ENCONTRADO SISTEMAS!\n" -"COMPROBAR RUTAS EN ARCHIVO DE CONFIGURACIÓN.\n" -"LA CARPETA DE JUEGOS DEBE CONTENER AL MENOS UNO Y CON LA EXTENSIÓN CORRECTA.\n" -"\n" -"VISITAR RECALBOX.COM PARA OBTENER MÃS INFORMACIÓN" - -#: -msgid "ON SCREEN KEYBOARD" -msgstr "TECLADO EN PANTALLA" - -#: -msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" -msgstr "SHIFT PARA MAYÚSCULAS, MINÚSCULAS Y ESPECIALES" - -#: -msgid "SPACE" -msgstr "ESPACIO" - -#: -msgid "DELETE A CHAR" -msgstr "BORRAR UN CARÃCTER" - -#: -msgid "SHIFT" -msgstr "SHIFT" - -#: -msgid "STOP EDITING" -msgstr "TERMINAR EDICIÓN" - -#: -msgid "MOVE CURSOR" -msgstr "MOVER CURSOR" - -#: -msgid "EDIT" -msgstr "EDITAR" - -#: -msgid "ACCEPT RESULT" -msgstr "ACEPTAR RESULTADO" - -#: -msgid "FILENAME" -msgstr "NOMBRE DEL ARCHIVO" - -#: -msgid "RATING" -msgstr "CLASIFICACIÓN" - -#: -msgid "TIMES PLAYED" -msgstr "JUGADO" - -#: -msgid "LAST PLAYED" -msgstr "ÚLTIMA VEZ JUGADO" - -#: -msgid "NUMBER OF PLAYERS" -msgstr "NÚMERO DE JUGADORES" - -#: -msgid "DEVELOPER" -msgstr "DESARROLLADOR" - -#: -msgid "GENRE" -msgstr "GÉNERO" - -#: -msgid "SHOW HIDDEN" -msgstr "MOSTRAR OCULTOS" - -#: -msgid "EXTREM (1400Mhz)" -msgstr "EXTREMO (1400Mhz)" - -#: -msgid "TURBO (1350Mhz)" -msgstr "TURBO (1350Mhz)" - -#: -msgid "HIGH (1300Mhz)" -msgstr "ALTO (1300Mhz)" - -#: -msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" -"IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." -msgstr "ESTABLECER EL OVERCLOCK EN TURBO Y EXTREMO, PUEDE CAUSAR INESTABILIDAD DEL SISTEMA.\n" -"NO USAR SIN ESTAR SEGURO.\n" -"AL CONTINUAR, EL SISTEMA SE REINICIARÃ." - -#: -msgid "%i GAME HIDDEN" -msgid_plural "%i GAMES HIDDEN" -msgstr[0] "%i JUEGO OCULTO" -msgstr[1] "%i JUEGOS OCULTOS" - -#: -msgid "Start kodi media player." -msgstr "Iniciar KODI" - -#: -msgid "Select the language for your recalbox, select an external drive to store your games and configurations, check your current version and the free space on your drive" -msgstr "Seleccionar idioma, seleccionar disco externo para guardar juegos y configuraciones, comprobar versión actual y espacio libre" - -#: -msgid "Shows your current recalboxOS version." -msgstr "Mostrar versión actual de RecalboxOS." - -#: -msgid "Show how much space is used on your SHARE partition, located either on the SDCARD or on an external drive. The information shows how much GB are used and how much GB your storage has overall (example 13GB/26GB)." -msgstr "Mostrar espacio usado en partición SHARE, tanto en SD como en disco externo. La información se muestra en GB usados/total (ejemplo 13GB/26GB)." - -#: -msgid "Select an external drive to store your roms, saves, configurations etc.\n" -"Use a FAT32 formatted drive. The system does not format the drive. On first boot, with this option enabled, recalbox will create a '/recalbox' folder with all system files inside." -msgstr "Seleccionar dispositivo externo para guardar juegos, partidas guardadas, configuraciones y otros.\n" -"Dispositivo requerido en formato FAT32, no será formateado.\n" -"En el siguiente arranque, esta opción crea en el dispositivo externo, una carpeta llamada '/recalbox' con todos los archivos requeridos" - -#: -msgid "Select your language. A reboot is needed to set this configuration active." -msgstr "Seleccionar idioma. Reiniciar para aplicar" - -#: -msgid "Manage your recalbox updates. Select the update type. Activate update check." -msgstr "Gestionar actualizaciones. Seleccionar tipo de actualización. Activar revisión de actualizaciones" - -#: -msgid "Check if an update is available, and start the update process." -msgstr "Revisar si hay actualizaciones disponibles, e iniciar proceso de actualización" - -#. We must change the issues page from github to gitlab on every language! -#: -msgid "Stable updates will check for updates on stable recalbox releases. Stable updates are tested and approved by the recalbox team and their testers.\n" -"Unstable updates allows you to get the latest recalbox features by checking our unstable repository. You can test and validate with us the very last version of recalbox.\n" -"If you choose unstable update, be so kind to report issues on the recalbox-os issue board (https://github.com/recalbox/recalbox-os/issues)" -msgstr "Actualizaciones estables: Las actualizaciones estables se comprueban y aprueban por el equipo de Recalbox y sus testers.\n" -"Actualizaciones inestables: Permiten obtener últimas características de recalbox comprobando nuestro depósito de inestables. Se puede probar y validar con nosotros la última versión de Recalbox.\n" -"Al seleccionar una actualización inestable, por favor reportar los problemas en la plataforma de desarrollo de RecalboxOS (https://gitlab.com/recalbox/recalbox/issues)" - -#. Creo que así queda más parecido al significado original -#: -msgid "Automatically check if an update is avaialble. If so, it notifies you with a message." -msgstr "Comprueba automáticamente si hay alguna actualización disponible. Si es así, se notifica con un mensaje" - -#: -msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." -msgstr "Configurar visualización de juegos, relación de aspecto, filtros (shaders), autoguardado y autocargado, y cuenta de retrologros" - -#: -msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." -msgstr "La relación de aspecto, es la relación entre ancho y alto de imagen.\n" -"Configurar AUTO para que el emulador seleccione la relación de aspecto original del juego, proporcionando la mejor experiencia de retrojuego" - -#: -msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." -msgstr "Suavizar imagen del juego. Esta opción proporciona la imagen más suave, usando filtros bilineales" - -#: -msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." -msgstr "Esta opción permite rebobinar el juego. Usar el botón HOTKEY+IZQUIERDA en el juego para rebobinar" - -#: -msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." -msgstr "Autoguardar la partida al salir del juego, y autocargar la partida al iniciar el juego." - -#: -msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." -msgstr "La escala entera, es el escalado por un factor de un número entero, como 2x, 3x, 4x, etc. Esta opción escala la imagen hasta la escala entera más grande por debajo de la resolución establecida. Por ejemplo, al establecer la resolución de pantalla completa en 1920x1080 y se habilita el escalado entero, sólo escalará una imagen de 320x240 hasta 1280x960 y dejará los bordes negros alrededor. Esto es para mantener una relación de aspecto de píxeles de 1:1 con la imagen original, de modo que los píxeles no se dupliquen de manera desigual" - -#: -msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." -msgstr "Los shaders equivalen a filtros para el renderizado del juego. Puede seleccionar un conjunto de shaders, que es una colección de shaders seleccionados para cada sistema. También se puede cambiar el shader dentro del juego con HOTKEY+L2 o HOTKEY+R2" - -#: -msgid "Enable or disable RetroAchievements in games." -msgstr "Activar o desactivar los retrologros en los juegos" - -#: -msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." -msgstr "El modo hardcore deshabilita toda capacidad de guardar partidas y funciones de rebobinado. Se debe terminar el juego y conseguir logros de una sola vez, igual que en la máquina original. En compensación, se conseguirán tanto los logros estándar y hardcore, y el doble de puntos. Un juego regular de 400 puntos, generará 800 si se completa en hardcore" - -#: -msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" -msgstr "¡La página web retroachievements.org propone desafíos/logros/trofeos en plataformas como NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine y más! ¡Crear cuenta en retroachievements.org y empezar búsqueda de logros!" - -#: -msgid "Add and configure up to 5 controllers." -msgstr "Agregar y configurar hasta 5 mandos" - -#: -msgid "Pair a bluetooth controller with your recalbox. Your controller must be in pairing mode." -msgstr "Vincular mando por bluetooth. El mando debe estar en modo visible" - -#: -msgid "Forget all paired bluetooth controllers. You will have to pair your controllers again, but this option can help if you have issues to reconnect a controller, which is already paired." -msgstr "Olvidar todos los mandos vinculados por bluetooth. Tendrás que vincularlos nuevamente, pero esta opción puede ayudarte si tienes problemas al reconectar un mando que ya está vinculado" - -#: -msgid "Configure your EmulationStation experience. Select transition types, help prompts, screensaver behavior. You can also deactivate the onscreen keyboard if you have a real keyboard plugged into your recalbox.\n" -"If you've added games since the last boot, you can also refresh the gamelist from this menu." -msgstr "Configurar experiencia con EmulationStation. Seleccionar tipos de transición, mensajes de ayuda, comportamiento del salvapantallas. También se puede desactivar el teclado virtual en caso de tener uno de verdad conectado a Recalbox.\n" -"Si se han agregado juegos desde el último arranque, también se puede actualizar la lista de juegos desde este menú" - -#: -msgid "Start the screensaver after N minutes." -msgstr "Iniciar el salvapantallas tras N minutos" - -#: -msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." -msgstr "Configurar el comportamiento del protector de pantalla. DIM reducirá el brillo de pantalla, y BLACK pondrá la pantalla en negro" - -#: -msgid "Shows a help at the bottom of the screen which displays commands you can use." -msgstr "Desplegar una ayuda en la parte inferior de la pantalla que enseña comandos a usar" - -#: -msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." -msgstr "Activado, permite cambiar de sistema pulsando IZQUIERDA o DERECHA en la lista de juegos" - -#: -msgid "The onscreen keyboard is necessary to type text if you only have controllers plugged into your recalbox. You can disable it if you have a real keyboard connected." -msgstr "El teclado virtual es necesario para escribir si existen sólo mandos conectados. Desactivar en caso de tener un teclado real conectado" - -#: -msgid "Select a theme for your recalbox." -msgstr "Seleccionar tema para Recalbox" - -#: -msgid "Updates the gamelists, if you added games since the last boot." -msgstr "Actualizar lista de juegos, en caso de haber agregado juegos desde el último inicio del sistema" - -#: -msgid "Configure the sound options of your recalbox." -msgstr "Configuras las opciones de sonido de Recalbox" - -#: -msgid "Set the volume of the sound output for the frontend and the games." -msgstr "Configurar volumen para salida de audio en interfaz y juegos" - -#: -msgid "Enable or disable the frontend music. You can add your own music as mp3, or ogg format in the 'musics' directory of your recalbox." -msgstr "Activar o desactivar música de interfaz. Permite agregar música propia en formato mp3 u ogg en la carpeta 'musics' de Recalbox" - -#: -msgid "Select your output device. Only HDMI and JACK are supported." -msgstr "Seleccionar dispositivo de salida. Sólo compatible con HDMI y JACK" - -#: -msgid "Configure the network options of your recalbox.\n" -"Check your network status and IP address, set the hostname and configure the WIFI." -msgstr "Configurar opciones de red.\n" -"Comprobar estado de red y dirección IP, establecer nombre de host y configurar WIFI" - -#: -msgid "Displays CONNECTED, if you are connected, by checking if your recalbox can access the recalbox.com update server." -msgstr "Mostrar CONECTADO, comprobando si hay acceso al servidor de actualizaciones" - -#: -msgid "The IP address of your recalbox within your local network." -msgstr "Dirección IP de recalbox" - -#: -msgid "Enable or disable WIFI.\n" -"If you disable WIFI, the SSID and the WIFI passwords are saved and can be used when you reactivate it" -msgstr "Activar o desactivar WIFI.\n" -"Al desactivar WIFI, SSID y contraseñas son guardadas y utilizadas al reactivar" - -#: -msgid "The name of your recalbox in your local network" -msgstr "Nombre de Recalbox en la red de área local" - -#: -msgid "SSID (WIFI Name) of your network." -msgstr "SSID (Nombre del WIFI) de la red" - -#: -msgid "Private key of your WIFI network." -msgstr "Contraseña WIFI" - -#: -msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." -msgstr "Obtener información e imágenes de juegos. El scraper descarga información e imágenes de juegos desde diferentes servidores y mejora la experiencia de usuario completamente en EmulationStation" - -#: -msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." -msgstr "Seleccionar un servidor desde donde descargar información. Se recomienda el servidor SCREENSCRAPER que está basado en www.screenscraper.fr y descarga datos de juegos en tu idioma, si está disponible" - -#: -msgid "Begin the scrape process with the configuration shown below." -msgstr "Comienza el proceso de descarga con la configuración siguiente" - -#: -msgid "Scrape and display game ratings." -msgstr "Descargar y mostrar las puntuaciones del juego" - -#: -msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." -msgstr "Configuraciones avanzadas. Asegúrate de que sabes lo que haces antes de cambiar cualquier valor en este menú" - -#: -msgid "Overclock your board to increase the performance.\n" -"Overclock settings are tested and validated by the community. Keep in mind that overclocking your board can void your warranty." -msgstr "Aumentar la velocidad del reloj para incrementar el rendimiento.\n" -"Estas configuraciones están probadas y validadas por la comunidad.\n" -"Considerar que al aumentar la velocidad del reloj es posible perder la garantía" - -#: -msgid "Select which system to show when the recalbox frontend starts. The default value is 'favorites'." -msgstr "Seleccionar qué sistema se muestra cuando empieza la interfaz del Recalbox. El valor por defecto es 'favoritos'" - -#: -msgid "On boot, recalbox will show the list of games of the selected system rather than the system view." -msgstr "En el arranque, Recalbox mostrará la lista de juegos del sistema seleccionado, no la vista de todos sistemas" - -#: -msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" -"This option highly speeds up boot time, but new games will not be detected." -msgstr "Mostrar sólo los juegos que están en el archivo gamelist.xml (situado en las carpetas de los ROMs).\n" -"Esta opción acelera el tiempo de arranque, pero no detectará nuevos juegos" - -#: -msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." -msgstr "Esta opción te permite establecer el sistema seleccionado en modo fijo. Con esta opción activada, el usuario no puede acceder a otros sistemas" - -#: -msgid "Always display the basic gamelist view, even if you have scraped your games." -msgstr "Mostrar siempre la lista de juegos básica, incluso si has scrapeado tus juegos" - -#: -msgid "Override global options like emulator, core, ratio and more for each available system in your recalbox." -msgstr "Anula opciones globales tales como emulador, núcleo, relación de aspecto y más, para cada sistema disponible" - -#: -msgid "Configure boot options that make your recalbox boot straight into a system or into Kodi, lock a user to a single system, or directly show the gamelist." -msgstr "Configurar las opciones de arranque para que Recalbox inicie directamente en un sistema definido, iniciar en Kodi, bloquear un usuario para un solo sistema, o directamente mostrar la lista de juegos" - -#: -msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" -msgstr "Activar o desactivar KODI, personalizar el arranque de KODI, activar el botón X para iniciar KODI" - -#: -msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." -msgstr "Activa o desactiva Kodi. Si Kodi está desactivado, no podrá ser iniciado con el botón X, o automáticamente al arrancar. El menú de entrada será desactivado" - -#: -msgid "Use the X button to start Kodi." -msgstr "Usar el botón X para iniciar Kodi" - -#: -msgid "Automatically start into Kodi on boot." -msgstr "Iniciar Kodi automáticamente en el arranque" - -#: -msgid "Manage your recalbox security." -msgstr "Administrar la seguridad de Recalbox" - -#: -msgid "Change the SSH root password." -msgstr "Cambiar la contraseña root SSH" - -#: -msgid "Enforce recalbox security." -msgstr "Reforzar la seguridad de Recalbox" - -#. Ordenador? Computador? Computadora? ...en español existen varias acepciones para este tipo de dispositivo (Windows en español lo define como "PC" simplemente, ahora en Windows 10, utiizan el término "Equipo"... Windows 98/Me/2000/XP se refería a "sistema") -#: -msgid "Enable or disable overscan.\n" -"Overscan can help you, if you have a black border, or if the image is bigger than your screen. Before setting the overscan, try to configure your TV to have a 1:1 pixel output.\n" -"More overscan settings can be defined in the boot.txt file, available when you plug your SD card into your computer." -msgstr "Activar o desactivar overscan.\n" -"El overscan puede ayudar al presentar bordes negros, o en caso de imagen de mayor tamaño que la pantalla.\n" -"Antes de configurar overscan, intentar configurar la TV para tener una salida de píxeles 1:1.\n" -"Es posible definir más configuraciones de overscan en el archivo boot.txt, disponible al insertar la tarjeta SD en la computadora" - -#: -msgid "Show the framerate in EmulationStation and in game." -msgstr "Mostrar cuadros por segundo en EmulationStation y en el juego" - -#: -msgid "Enable or disable the Recalbox Manager.\n" -"The Recalbox Manager is a web application available on http://recalbox , if you are on windows, http://recalbox.local , if you are on Linux or Mac, or directly with your recalbox IP : http://192.168.1.XX.\n" -"You can configure many options from within the manager, and even manage games, saves, and scrapes!" -msgstr "Activar o desactivar el Administrador de Recalbox.\n" -"El Administrador de Recalbox es una aplicación web disponible en http://recalbox, en Windows, http://recalbox.local en Linux o Mac, o directamente con la IP de Recalbox: http://192.168.1.XX.\n" -"¡Es posible configurar muchas opciones desde el administrador, incluso juegos, partidas guardadas e información!" - -#: -msgid "Enable or disable the recalbox API.\n" -"The Recalbox API is a REST API exposing endpoints to control your recalbox via http requests." -msgstr "Activar o desactivar la API de Recalbox.\n" -"La API de Recalbox es una API REST que expone puntos finales para controlar Recalbox a través de solicitudes HTTP" - -#: -msgid "Select which emulator to use when you start a game for this system." -msgstr "Seleccionar qué emulador usar cuando se inicia un juego de este sistema" - -#: -msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." -msgstr "Seleccionar qué núcleo usar para el emulador seleccionado. Por ejemplo, el emulador LIBRETRO tiene varios núcleos para ejecutar juegos de Super Nintendo. La configuración del núcleo por defecto que se elija, también puede ser eliminada desde la configuración específica para juegos" - -#: -msgid "USE COMPOSED VISUALS" -msgstr "USAR IMÃGENES COMPUESTAS" - -#: -msgid "CHECK UPDATES" -msgstr "COMPROBAR ACTUALIZACIONES" - -#: -msgid "UPDATE TYPE" -msgstr "TIPO DE ACTUALIZACIÓN" - -#: -msgid "INTEGER SCALE (PIXEL PERFECT)" -msgstr "ESCALA COMPLETA (PIXEL PERFECT)" - -#: -msgid "ADVANCED SETTINGS" -msgstr "OPCIONES AVANZADAS" - -#: -msgid "BOOT SETTINGS" -msgstr "OPCIONES DE ARRANQUE" - -#: -msgid "GAMELIST ONLY" -msgstr "SÓLO LISTA DE JUEGOS" - -#: -msgid "BOOT ON SYSTEM" -msgstr "ARRANCAR EN SISTEMA" - -#: -msgid "BOOT ON GAMELIST" -msgstr "ARRANCAR EN LISTA DE JUEGOS" - -#: -msgid "HIDE SYSTEM VIEW" -msgstr "OCULTAR VISTA DE SISTEMA" - -#: -msgid "EMULATOR ADVANCED CONFIGURATION" -msgstr "CONFIGURACIÓN AVANZADA DEL EMULADOR" - -#: -msgid "ADVANCED EMULATOR CONFIGURATION" -msgstr "CONFIGURACIÓN DEL EMULADOR AVANZADA" - -#: -msgid "HELP" -msgstr "AYUDA" - -#: -msgid "THE SYSTEM IS UP TO DATE" -msgstr "EL SISTEMA ESTà ACTUALIZADO" - -#: -msgid "FORCE BASIC GAMELIST VIEW" -msgstr "FORZAR VISTA DE LISTA DE JUEGOS BÃSICA" - -#: -msgid "DOWNLOADED" -msgstr "DESCARGADO" - -#: -msgid "UPDATE VERSION:" -msgstr "VERSIÓN DE LA ACTUALIZACIÓN:" - -#: -msgid "UPDATE CHANGELOG:" -msgstr "REGISTRO DE CAMBIOS DE LA ACTUALIZACIÓN:" - -#: -msgid "MORE DETAILS" -msgstr "MÃS DETALLES" - -#: -msgid "CAROUSEL ANIMATION" -msgstr "ANIMACIÓN DEL CARRUSEL" - -#: -msgid "THEME CONFIGURATION" -msgstr "CONFIGURACIÓN DEL TEMA" - -#: -msgid "THEME COLORSET" -msgstr "CONJUNTO DE COLORES DEL TEMA" - -#: -msgid "THEME ICONSET" -msgstr "CONJUNTO DE ÃCONOS DEL TEMA" - -#: -msgid "THEME MENU" -msgstr "MENÚ DEL TEMA" - -#: -msgid "THEME SYSTEMVIEW" -msgstr "VISTA DE SISTEMAS DEL TEMA" - -#: -msgid "THEME GAMELISTVIEW" -msgstr "VISTA DE LISTA DE JUEGOS DEL TEMA" - -#: -msgid "THEME REGION" -msgstr "REGIÓN DEL TEMA" - -#: -msgid "THIS THEME HAS NO OPTION" -msgstr "ESTE TEMA NO TIENE OPCIONES" - -#: -msgid "MANUAL INPUT" -msgstr "ENTRADA MANUAL" - -#: -msgid "AN ERROR OCCURED - DOWNLOADED" -msgstr "HA HABIDO UN ERROR - DESCARGADO" - -#: -msgid "START KODI" -msgstr "INICIAR KODI" - -#: -msgid "Shows the current available update version." -msgstr "Mostrar la versión de actualización disponible actualmente" - -#: -msgid "Shows the current available update changelog." -msgstr "Mostrar el registro de cambios de actualización disponible actualmente" - -#: -msgid "Configure an associated controller. Your controller has to be associated / plugged before." -msgstr "Configurar mando conectado. El mando debe estar previamente vinculado/conectado" - -#: -msgid "Choose if carousel will be animated or not during transitions" -msgstr "Elegir si el carrusel estará animado o no durante transiciones" - -#: -msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" -msgstr "Seleccionar el tipo de transición que ocurre cuando comienza un juego. INSTANT no hará nada, FADE se desvanecerá a oscuro, y SLIDE hará un zoom en la cubierta del juego (o nombre, si no hay metadatos)" - -#: -msgid "Select exisiting colorset options for this theme." -msgstr "Seleccionar opciones de conjuntos de colores existentes para este tema" - -#: -msgid "Select exisiting iconset options for this theme." -msgstr "Seleccionar opciones de conjunto de iconos existentes para este tema" - -#: -msgid "Select exisiting menu style options for this theme." -msgstr "Seleccionar opciones de estilo de menú existentes para este tema" - -#: -msgid "Select exisiting system view options for this theme." -msgstr "Seleccionar opciones de vista de sistema existentes para este tema" - -#: -msgid "Select exisiting gamelist view options for this theme." -msgstr "Seleccionar opciones de vista de lista de juegos existentes para este tema" - -#: -msgid "Configure theme options if available." -msgstr "Configurar opciones del tema si está disponible" - -#: -msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" -msgstr "Seleccionar región de logotipos, imágenes para sistemas diferentes entre países Ejemplo, Megadrive en EU, Genesis en USA" - -#: -msgid "Type the name of your SSID if it is hidden or not listed" -msgstr "Ingresar nombre de SSID, si está oculto o no aparece" - -#: -msgid "Select a letter and the listing will go directly on the first game starting with this letter." -msgstr "Seleccionar una letra y la lista irá directamente al primer juego que comience con esta letra" - -#: -msgid "Select the way the game list is sortered (alphabetically, by notation...)." -msgstr "Seleccionar la forma en que se ordena la lista de juegos (alfabéticamente, mediante notación...)" - -#: -msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." -msgstr "Cambiar entre ver o no solo los juegos favoritos. Para agregar un juego a la lista de favoritos, seleccionar el juego y cambiar su estado con 'Y'" - -#: -msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." -msgstr "Cambiar entre ver o no los juegos ocultos. Para ocultar un juego, editar sus datos y seleccionar 'Ocultar'" - -#: -msgid "This option display a menu which allows to change game data and many others options." -msgstr "Esta opción muestra un menú que permite cambiar los datos del juego y muchas otras opciones" - -#: -msgid "AVAILABLE UPDATE" -msgstr "ACTUALIZACIÓN DISPONIBLE" - -#: -msgid "UPDATE CHANGELOG" -msgstr "REGISTRO DE CAMBIOS" - -#: -msgid "CLOCK IN MENU" -msgstr "RELOJ EN MENÚ" - -#: -msgid "Now playing" -msgstr "Reproduciendo ahora" - -#: -msgid "DEFAULT (%1%)" -msgstr "DEFECTO (%1%)" - -#: -msgid "INPUT REQUIRED" -msgstr "INGRESO REQUERIDO" - -#: -msgid "(skipped)" -msgstr "(omitido)" - -#: -msgid "UP/DOWN TO SKIP" -msgstr "ARRIBA/ABAJO PARA OMITIR" - -#. Explanation: you config the controller, you make a mistake (let's say right instead of left), you can press A to erase wrong config and redo -#: -msgid "A TO UNSET" -msgstr "A PARA CAMBIAR" - -#: -msgid "DOWN TO SKIP AND KEEP [%1%]" -msgstr "ABAJO PARA OMITIR Y MANTENER [%1%]" - -#: -msgid "UP/DOWN TO SKIP AND KEEP [%1%]" -msgstr "ARRIBA/ABAJO PARA OMITIR Y MANTENER [%1%]" - -#: -msgid "Set duration of help popups, 0 means no popup." -msgstr "Duración de popups de ayuda, 0 significa sin popup" - -#: -msgid "HELP POPUP DURATION" -msgstr "DURACIÓN DE POPUPS DE AYUDA" - -#: -msgid "Set duration of music popups, 0 means no popup." -msgstr "Duración de popups de música, 0 significa sin popup" - -#: -msgid "MUSIC POPUP DURATION" -msgstr "DURACIÓN DE POPUPS DE MÚSICA" - -#: -msgid "POPUP SETTINGS" -msgstr "CONFIGURACIÓN DE POPUPS" - -#: -msgid "POPUP POSITION" -msgstr "POSICIÓN DE POPUPS" - -#: -msgid "Select the position of popups on screen." -msgstr "Seleccionar posición de popups en pantalla" - -#: -msgid "Set position and duration of popups." -msgstr "Posición y duración de popups" - -#: -msgid "TOP/RIGHT" -msgstr "ARRIBA/DERECHA" - -#: -msgid "BOTTOM/RIGHT" -msgstr "ABAJO/DERECHA" - -#: -msgid "BOTTOM/LEFT" -msgstr "ABAJO/IZQUIERDA" - -#: -msgid "TOP/LEFT" -msgstr "ARRIBA/IZQUIERDA" - -#: -msgid "SHOW FOLDERS CONTENT" -msgstr "MOSTRAR CONTENIDO DE CARPETAS" - -#: -msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." -msgstr "Cambiar entre mostrar estructura de carpetas o ver todos los juegos en un único nivel superior" - -#: -msgid "NETPLAY" -msgstr "NETPLAY" - -#: -msgid "NETPLAY SETTINGS" -msgstr "CONFIGURACIÓN DE NETPLAY" - -#: -msgid "NETPLAY LOBBY" -msgstr "SALA DE ESPERA DE NETPLAY" - -#: -msgid "Enable or disable Netplay in games." -msgstr "Habilitar o deshabilitar Netplay en juegos" - -#: -msgid "PORT" -msgstr "PUERTO" - -#: -msgid "NICKNAME" -msgstr "ALIAS" - -#: -msgid "RELAY SERVER" -msgstr "SERVIDOR DE RETRANSMISIÓN" - -#: -msgid "Enable or disable connections throught relay servers." -msgstr "Habilitar o deshabilitar conexiones mediante servidores de retransmisión" - -#: -msgid "KODI/NETPLAY" -msgstr "KODI/NETPLAY" - -#: -msgid "NO GAMES OR NO CONNECTION" -msgstr "SIN JUEGOS O SIN CONEXIÓN" - -#: -msgid "HASH NOW" -msgstr "CALCULAR HASH AHORA" - -#: -msgid "HASH THESE SYSTEMS" -msgstr "CALCULAR HASH PARA ESTOS SISTEMAS" - -#: -msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." -msgstr "Agregar hash de roms a listas de juegos para obtener resultados más precisos en Netplay." - -#: -msgid "HASH ROMS" -msgstr "CALCULAR HASH DE ROMS" - -#: -msgid "Only missing hashs" -msgstr "Solo hash faltantes" - -#: -msgid "Username" -msgstr "Usuario" - -#: -msgid "Country" -msgstr "País" - -#: -msgid "Latency" -msgstr "Latencia" - -#: -msgid "Host arch." -msgstr "Arq. del anfitrión" - -#: -msgid "Core ver." -msgstr "Ver. núcleo" - -#: -msgid "RA ver." -msgstr "Ver. RA" - -#: -msgid "Can join" -msgstr "Puede unirse" - -#: -msgid "Rom and core match" -msgstr "Rom y núcleo coincidente" - -#: -msgid "Rom found" -msgstr "Rom encontrado" - -#: -msgid "No rom match" -msgstr "Rom sin coincidencia" - -#: -msgid "Match" -msgstr "Coincidencia" - -#: -msgid "No Match" -msgstr "Sin coincidencia" - -#: -msgid "Rom file" -msgstr "Archivo rom" - -#: -msgid "Rom hash" -msgstr "Hash del rom" - -#: -msgid "THIS COULD TAKE A WHILE, CONFIRM?" -msgstr "Esto podría tardar, ¿confirmar?" - -#: -msgid "good" -msgstr "bueno" - -#: -msgid "bad" -msgstr "malo" - -#: -msgid "medium" -msgstr "medio" - -#: -msgid "NETPLAY POPUP DURATION" -msgstr "DURACIÓN POPUPS PARA NETPLAY" - -#: -msgid "Set duration of netplay popups, 0 means no popup." -msgstr "Duración de popups para Netplay, 0 significa sin popups" - -#: -msgid "Player" -msgstr "Jugador" - -#: -msgid "Game" -msgstr "Juego" - -#: -msgid "A Recalbox friend has started a Netplay game!" -msgstr "!Un amigo de Recalbox ha iniciado un juego en Netplay!" - -#: -msgid "Play online on games running through Retroarch like NES, SNES, FBA, Genesis/Megadrive and more!" -msgstr "¡Juega en línea en juegos que funcionan con RetroArch como NES, SNES, FBA, Genesis/Megadrive y más!" - -#: -msgid "Rom, hash and core match" -msgstr "Rom, hash y núcleo coincidente" - -#: -msgid "No core match" -msgstr "Núcleo sin coincidencia" - -#: -msgid "Add a clock in the main menu." -msgstr "" - diff --git a/resources/off.svg b/resources/off.svg index 5dacfb38a4..0f6c8a8b8e 100644 --- a/resources/off.svg +++ b/resources/off.svg @@ -1,9 +1 @@ - - - - - - - - - + \ No newline at end of file diff --git a/resources/on.svg b/resources/on.svg index ab4d5d9d5a..01702df8fe 100644 --- a/resources/on.svg +++ b/resources/on.svg @@ -1,5 +1 @@ - - - - - + \ No newline at end of file diff --git a/resources/option_arrow.svg b/resources/option_arrow.svg index 3027a4bc9b..3587e74823 100644 --- a/resources/option_arrow.svg +++ b/resources/option_arrow.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/resources/slider_knob.svg b/resources/slider_knob.svg index 6600ecb6c3..1a0a9b59bc 100644 --- a/resources/slider_knob.svg +++ b/resources/slider_knob.svg @@ -1,4 +1,7 @@ - - - + + + + Layer 1 + + diff --git a/resources/star_filled.svg b/resources/star_filled.svg index 0be43f7911..808a40955f 100644 --- a/resources/star_filled.svg +++ b/resources/star_filled.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file diff --git a/resources/star_unfilled.svg b/resources/star_unfilled.svg index 9d3a2f7370..c59a17c61c 100644 --- a/resources/star_unfilled.svg +++ b/resources/star_unfilled.svg @@ -1,4 +1 @@ - - - - + \ No newline at end of file From 9460fd3d2689a5235d95d957b9d888b917e2947f Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 28 Jun 2019 16:14:55 +0200 Subject: [PATCH 045/336] v2.10.0.5 - Reflection for images ( table reflection effect ) - Gradients for selected menu and list items. - Bug Fixes --- README.md | 2 + THEMES.md | 11 + es-app/src/EmulationStation.h | 8 +- es-app/src/FileData.cpp | 1 + es-app/src/Gamelist.cpp | 2 +- es-app/src/MetaData.cpp | 2 +- es-app/src/SystemData.cpp | 247 ++++++------------ es-app/src/components/TextListComponent.h | 11 +- es-app/src/guis/GuiGameScraper.cpp | 11 +- es-app/src/guis/GuiGamelistOptions.cpp | 5 +- es-app/src/guis/GuiMenu.cpp | 9 +- es-app/src/main.cpp | 16 ++ es-app/src/views/SystemView.cpp | 14 +- es-app/src/views/SystemView.h | 2 + .../src/views/gamelist/GridGameListView.cpp | 6 +- es-core/CMakeLists.txt | 2 + es-core/src/HttpReq.cpp | 11 +- es-core/src/Renderer.h | 2 + es-core/src/Renderer_draw_gl.cpp | 56 ++++ es-core/src/Settings.cpp | 6 +- es-core/src/ThemeData.cpp | 21 +- es-core/src/ThemeData.h | 13 +- es-core/src/Window.cpp | 11 +- es-core/src/components/ComponentList.cpp | 15 +- es-core/src/components/GridTileComponent.cpp | 25 +- es-core/src/components/GridTileComponent.h | 2 + es-core/src/components/ImageComponent.cpp | 113 ++++++-- es-core/src/components/ImageComponent.h | 9 +- es-core/src/components/TextComponent.cpp | 5 +- es-core/src/platform.h | 44 +++- es-core/src/utils/FileSystemUtil.cpp | 123 +++++---- es-core/src/utils/FileSystemUtil.h | 10 +- es-core/src/utils/ThreadPool.cpp | 106 ++++++++ es-core/src/utils/ThreadPool.h | 33 +++ resources/locale/fr/emulationstation2.po | 4 + 35 files changed, 662 insertions(+), 296 deletions(-) create mode 100644 es-core/src/utils/ThreadPool.cpp create mode 100644 es-core/src/utils/ThreadPool.h diff --git a/README.md b/README.md index c0899f110e..4778cb99a4 100644 --- a/README.md +++ b/README.md @@ -87,6 +87,8 @@ Changes in my branch - Carousel supports element "logoPos" : this allows the logo not to be inevitably centered. - Image loading : the image bytes where duplicated 3 times in memory. - In previous versions, if a xml element was unknown in the theme, nothing was loaded. +- Reflection for images ( table reflection effect ) +- Gradients for selected menu and list items. **Optimizations & Fixes:** - Faster loading time, using multithreading. diff --git a/THEMES.md b/THEMES.md index 75274239ca..7fde8043c1 100644 --- a/THEMES.md +++ b/THEMES.md @@ -613,6 +613,9 @@ Can be created as an extra. - Multiply each pixel's color by this color. For example, an all-white image with `FF0000` would become completely red. You can also control the transparency of an image with `FFFFFFAA` - keeping all the pixels their normal color and only affecting the alpha channel. * `zIndex` - type: FLOAT. - z-index value for component. Components will be rendered in order of z-index value from low to high. +* `reflexion` - type: NORMALIZED_PAIR. + - NEW : table reflexion effect. First item is top position alpha, second is bottom alpha. + #### imagegrid @@ -661,6 +664,8 @@ Can be created as an extra. - NEW : Selects if the background is over the full tile or only the image. `full` by default, can also be set to `image`. * `imageSizeMode` - type: STRING. - NEW : Selects the image sizing mode. `maxSize` by default, can also be set to `minSize` (outer zoom) or `size` (stretch). +* `reflexion` - type: NORMALIZED_PAIR. + - NEW : table reflexion effect. First item is top position alpha, second is bottom alpha. #### video @@ -726,6 +731,8 @@ Can be created as an extra. - Where on the component `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the component exactly in the middle of the screen. If the "POSITION" and "SIZE" attributes are themable, "ORIGIN" is implied. * `selectorColor` - type: COLOR. - Color of the "selector bar." +* `selectorGradientColor` - type: NORMALIZED_PAIR. + - NEW : Bottom color for the gradient of the "selector bar." * `selectorImagePath` - type: PATH. - Path to image to render in place of "selector bar." * `selectorImageTile` - type: BOOLEAN. @@ -845,6 +852,10 @@ EmulationStation borrows the concept of "nine patches" from Android (or "9-Slice * `color` - type: COLOR. - Controls the color of the carousel background. - Default is FFFFFFD8 +* `colorGradient` - type: COLOR. + - NEW : Color for the end of gradient +* `gradientType` - type: STRING. + - NEW : Sets the gradient direction. Accepted values are "horizontal" and "vertical". * `logoSize` - type: NORMALIZED_PAIR. Default is "0.25 0.155" * `logoScale` - type: FLOAT. - Selected logo is increased in size by this scale diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index 96c9c42201..5bb5922f1c 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -7,14 +7,14 @@ #define PROGRAM_VERSION_MAJOR 2 #define PROGRAM_VERSION_MINOR 10 #define PROGRAM_VERSION_MAINTENANCE 0 -#define PROGRAM_VERSION_BUILD 4 +#define PROGRAM_VERSION_BUILD 5 -#define PROGRAM_VERSION_STRING "2.10.0.4 FCAMOD" -#define SCRAPER_VERSION_STRING "2.10.0.4" +#define PROGRAM_VERSION_STRING "2.10.0.5 FCAMOD" +#define SCRAPER_VERSION_STRING "2.10.0.5" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ -#define RESOURCE_VERSION_STRING "2,10,0,4\0" +#define RESOURCE_VERSION_STRING "2,10,0,5\0" #define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE,PROGRAM_VERSION_BUILD #endif // ES_APP_EMULATION_STATION_H diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 1be6ec0233..8b215830a9 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -281,6 +281,7 @@ CollectionFileData::CollectionFileData(FileData* file, SystemData* system) mSourceFileData = file->getSourceFileData(); mParent = NULL; metadata = mSourceFileData->metadata; + mDirty = true; } SystemEnvironmentData* CollectionFileData::getSystemEnvData() const diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 7ffd37964a..03740ee722 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -192,7 +192,7 @@ void updateGamelist(SystemData* system) { //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(); diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index 519ff393e3..f2bbb3d78f 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -124,7 +124,7 @@ const std::vector& getMDDByType(MetaDataListType type) return gameMDD; } -MetaDataList::MetaDataList(MetaDataListType type) : mType(type), mWasChanged(false) +MetaDataList::MetaDataList(MetaDataListType type) : mType(type), mWasChanged(false), mRelativeTo(nullptr) { } diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 93e94142ad..91d8894a20 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -11,22 +11,15 @@ #include "ThemeData.h" #include "views/UIModeController.h" #include - #include "utils/StringUtil.h" - -#ifdef WIN32 -#include -#endif - +#include "utils/ThreadPool.h" #include "GuiComponent.h" #include "Window.h" -#include +using namespace Utils; std::vector SystemData::sSystemVector; -#define USE_THREADING - SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) { @@ -41,7 +34,7 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys mRootFolder->metadata.set("name", mFullName); std::unordered_map fileMap; - + if (!Settings::getInstance()->getBool("ParseGamelistOnly")) { populateFolder(mRootFolder, fileMap); @@ -51,7 +44,8 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys if (!Settings::getInstance()->getBool("IgnoreGamelist")) parseGamelist(this, fileMap); - + + //StopWatch ws("sort " + mName); mRootFolder->sort(FileSorts::SortTypes.at(0)); //indexAllGameFilters(mRootFolder); @@ -61,7 +55,7 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys // virtual systems are updated afterwards, we're just creating the data structure mRootFolder = new FolderData("" + name, this); } - + auto defaultView = Settings::getInstance()->getString(getName() + ".defaultView"); auto gridSizeOverride = Vector2f::parseString(Settings::getInstance()->getString(getName() + ".gridSize")); setSystemViewMode(defaultView, gridSizeOverride, false); @@ -143,7 +137,8 @@ void SystemData::populateFolder(FolderData* folder, std::unordered_mapgetBool("ShowHiddenFiles"); - Utils::FileSystem::fileList dirContent = Utils::FileSystem::getDirInfo(folderPath, false); + Utils::FileSystem::fileList dirContent = Utils::FileSystem::getDirInfo(folderPath); + for(Utils::FileSystem::fileList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) { auto fileInfo = *it; @@ -370,111 +365,6 @@ SystemData* SystemData::loadSystem(pugi::xml_node system) return newSys; } -#ifdef USE_THREADING -typedef std::function work_function; - -#include -#include -#include -#include - -class ThreadPool -{ -public: - ThreadPool() : mRunning(true), mWaiting(false), mNumWork(0) - { -#ifdef WIN32 - SYSTEM_INFO sysinfo; - GetSystemInfo(&sysinfo); - size_t num_threads = sysinfo.dwNumberOfProcessors; -#else - size_t num_threads = sysconf(_SC_NPROCESSORS_ONLN); -#endif - - auto doWork = [&](size_t id) - { - while (mRunning) - { - _mutex.lock(); - if (!mWorkQueue.empty()) - { - auto work = mWorkQueue.front(); - mWorkQueue.pop(); - _mutex.unlock(); - - try - { - work(); - } - catch (...) { } - - mNumWork--; - } - else - { - _mutex.unlock(); - - // Extra code : Exit finished threads - if (mWaiting) - return; - - std::this_thread::yield(); - std::this_thread::sleep_for(std::chrono::milliseconds(1)); - } - } - }; - - mThreads.reserve(num_threads); - - for (size_t i = 0; i < num_threads; i++) - mThreads.push_back(std::thread(doWork, i)); - } - - ~ThreadPool() - { - mRunning = false; - for (std::thread& t : mThreads) - t.join(); - } - - void queueWorkItem(work_function work) - { - _mutex.lock(); - mWorkQueue.push(work); - mNumWork++; - _mutex.unlock(); - } - - void wait() - { - mWaiting = true; - while (mNumWork.load() > 0) - std::this_thread::yield(); - } - - void wait(work_function work, int delay = 50) - { - mWaiting = true; - while (mNumWork.load() > 0) - { - work(); - - std::this_thread::yield(); - std::this_thread::sleep_for(std::chrono::milliseconds(delay)); - } - } - -private: - bool mRunning; - bool mWaiting; - std::queue mWorkQueue; - std::atomic mNumWork; - std::mutex _mutex; - std::vector mThreads; - -}; -#endif - //creates systems from information located in a config file bool SystemData::loadConfig(Window* window) { @@ -525,80 +415,99 @@ bool SystemData::loadConfig(Window* window) unsigned int Ticks = GetTickCount(); #endif -#ifdef USE_THREADING - ThreadPool threadPool; - typedef SystemData* SystemDataPtr; + + ThreadPool* pThreadPool = NULL; + SystemDataPtr* systems = NULL; - SystemDataPtr* systems = new SystemDataPtr[systemCount]; - for (int i = 0; i < systemCount; i++) - systems[i] = nullptr; + if (ThreadPool::getProcessorCount() > 2 && Settings::getInstance()->getBool("ThreadedLoading")) + { + pThreadPool = new ThreadPool(); - threadPool.queueWorkItem([] { CollectionSystemManager::get()->loadCollectionSystems(true); }); + systems = new SystemDataPtr[systemCount]; + for (int i = 0; i < systemCount; i++) + systems[i] = nullptr; -#endif + pThreadPool->queueWorkItem([] { CollectionSystemManager::get()->loadCollectionSystems(true); }); + } int processedSystem = 0; for (pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) { -#ifdef USE_THREADING - threadPool.queueWorkItem([system, currentSystem, systems, &processedSystem] + if (pThreadPool != NULL) { - systems[currentSystem] = loadSystem(system); - processedSystem++; - }); -#else - std::string fullname = system.child("fullname").text().get(); + pThreadPool->queueWorkItem([system, currentSystem, systems, &processedSystem] + { + systems[currentSystem] = loadSystem(system); + processedSystem++; + }); + } + else + { + std::string fullname = system.child("fullname").text().get(); + + if (window != NULL) + window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : (float)currentSystem / (float)(systemCount + 1)); + + std::string nm = system.child("name").text().get(); + StopWatch watch("SystemData " + nm); + + SystemData* pSystem = loadSystem(system); + if (pSystem != nullptr) + sSystemVector.push_back(pSystem); + } - if (window != NULL) - window->renderLoadingScreen(fullname, systemCount == 0 ? 0 : (float) currentSystem / (float) (systemCount + 1)); - - SystemData* pSystem = loadSystem(system); - if (pSystem != nullptr) - sSystemVector.push_back(pSystem); - -#endif currentSystem++; } -#ifdef USE_THREADING - if (window != NULL) - { - threadPool.wait([window, &processedSystem, systemCount, systemsNames] + if (pThreadPool != NULL) + { + if (window != NULL) { - int px = processedSystem; - auto name = px < 0 || px > systemsNames.size() ? "" : systemsNames.at(px); - window->renderLoadingScreen(name, (float)px / (float)(systemCount + 1)); - }, 50); - } - else - threadPool.wait(); + int cnt = 0; - for (int i = 0; i < systemCount; i++) - { - SystemData* pSystem = systems[i]; - if (pSystem != nullptr) - sSystemVector.push_back(pSystem); - } + pThreadPool->wait([window, &processedSystem, systemCount, systemsNames, &cnt] + { + int px = processedSystem; + if (px == cnt) + return; - delete[] systems; + cnt = px; - if (window != NULL) - window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + auto name = px < 0 || px > systemsNames.size() ? "" : systemsNames.at(px); + window->renderLoadingScreen(name, (float)px / (float)(systemCount + 1)); + }, 10); + } + else + pThreadPool->wait(); + + for (int i = 0; i < systemCount; i++) + { + SystemData* pSystem = systems[i]; + if (pSystem != nullptr) + sSystemVector.push_back(pSystem); + } - CollectionSystemManager::get()->updateSystemsList(); -#else + delete[] systems; + delete pThreadPool; - if (window != NULL) - window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + if (window != NULL) + window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); - CollectionSystemManager::get()->loadCollectionSystems(); -#endif + CollectionSystemManager::get()->updateSystemsList(); + } + else + { + if (window != NULL) + window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + + CollectionSystemManager::get()->loadCollectionSystems(); + } #ifdef WIN32 Ticks = GetTickCount() - Ticks; - // ::MessageBox(0, std::to_string(Ticks).c_str(), NULL, NULL); +// ::MessageBox(0, std::to_string(Ticks).c_str(), NULL, NULL); #endif return true; @@ -812,6 +721,8 @@ unsigned int SystemData::getDisplayedGameCount() const void SystemData::loadTheme() { + //StopWatch watch("SystemData::loadTheme " + getName()); + mTheme = std::make_shared(); std::string path = getThemePath(); diff --git a/es-app/src/components/TextListComponent.h b/es-app/src/components/TextListComponent.h index eace2d6892..282ac14f2f 100644 --- a/es-app/src/components/TextListComponent.h +++ b/es-app/src/components/TextListComponent.h @@ -73,6 +73,7 @@ class TextListComponent : public IList inline void setSelectorHeight(float selectorScale) { mSelectorHeight = selectorScale; } inline void setSelectorOffsetY(float selectorOffsetY) { mSelectorOffsetY = selectorOffsetY; } inline void setSelectorColor(unsigned int color) { mSelectorColor = color; } + inline void setSelectorGradientColor(unsigned int color) { mSelectorGradientColor = color; } inline void setSelectedColor(unsigned int color) { mSelectedColor = color; } inline void setColor(unsigned int id, unsigned int color) { mColors[id] = color; } inline void setLineSpacing(float lineSpacing) { mLineSpacing = lineSpacing; } @@ -97,6 +98,7 @@ class TextListComponent : public IList float mSelectorHeight; float mSelectorOffsetY; unsigned int mSelectorColor; + unsigned int mSelectorGradientColor; unsigned int mSelectedColor; std::string mScrollSound; static const unsigned int COLOR_ID_COUNT = 2; @@ -122,6 +124,7 @@ TextListComponent::TextListComponent(Window* window) : mSelectorHeight = mFont->getSize() * 1.5f; mSelectorOffsetY = 0; mSelectorColor = 0x000000FF; + mSelectorGradientColor = 0; mSelectedColor = 0; mColors[0] = 0x0000FFFF; mColors[1] = 0x00FF00FF; @@ -167,7 +170,11 @@ void TextListComponent::render(const Transform4x4f& parentTrans) mSelectorImage.render(trans); } else { Renderer::setMatrix(trans); - Renderer::drawRect(0.f, (mCursor - startEntry)*entrySize + mSelectorOffsetY, mSize.x(), mSelectorHeight, mSelectorColor); + + if (mSelectorGradientColor != 0) + Renderer::drawGradientRect(0.f, (mCursor - startEntry)*entrySize + mSelectorOffsetY, mSize.x(), mSelectorHeight, mSelectorColor, mSelectorGradientColor); + else + Renderer::drawRect(0.f, (mCursor - startEntry)*entrySize + mSelectorOffsetY, mSize.x(), mSelectorHeight, mSelectorColor); } } @@ -370,6 +377,8 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, c setColor(0, elem->get("primaryColor")); if(elem->has("secondaryColor")) setColor(1, elem->get("secondaryColor")); + if (elem->has("selectorGradientColor")) + setSelectorGradientColor(elem->get("selectorGradientColor")); } setFont(Font::getFromTheme(elem, properties, mFont)); diff --git a/es-app/src/guis/GuiGameScraper.cpp b/es-app/src/guis/GuiGameScraper.cpp index c94eab6630..c9c63d943a 100644 --- a/es-app/src/guis/GuiGameScraper.cpp +++ b/es-app/src/guis/GuiGameScraper.cpp @@ -13,6 +13,11 @@ GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std:: mSearchParams(params), mClose(false) { + auto theme = ThemeData::getMenuTheme(); + mBox.setImagePath(theme->Background.path); // ":/frame.png" + mBox.setCenterColor(theme->Background.color); + mBox.setEdgeColor(theme->Background.color); + PowerSaver::pause(); addChild(&mBox); addChild(&mGrid); @@ -20,13 +25,13 @@ GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std:: // row 0 is a spacer mGameName = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(mSearchParams.game->getPath())), - Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); + theme->Text.font, theme->Text.color, ALIGN_CENTER); mGrid.setEntry(mGameName, Vector2i(0, 1), false, true); // row 2 is a spacer - mSystemName = std::make_shared(mWindow, Utils::String::toUpper(mSearchParams.system->getFullName()), Font::get(FONT_SIZE_SMALL), - 0x888888FF, ALIGN_CENTER); + mSystemName = std::make_shared(mWindow, Utils::String::toUpper(mSearchParams.system->getFullName()), + theme->TextSmall.font, theme->TextSmall.color , ALIGN_CENTER); mGrid.setEntry(mSystemName, Vector2i(0, 3), false, true); // row 4 is a spacer diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 87cc2b3cb1..030a8c6844 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -16,7 +16,10 @@ std::vector GuiGamelistOptions::gridSizes { "automatic", - "2x2", + "1x1", + + "2x1", + "2x2", "2x3", "2x4", "2x5", diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 09d267e926..144ed824e1 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -872,13 +872,18 @@ void GuiMenu::openOtherSettings() }); #endif - // framerate - + // framerate auto framerate = std::make_shared(mWindow); framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); s->addWithLabel(_T("SHOW FRAMERATE"), framerate); s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); + // threaded loading + auto threadedLoading = std::make_shared(mWindow); + threadedLoading->setState(Settings::getInstance()->getBool("ThreadedLoading")); + s->addWithLabel(_T("THREADED LOADING"), threadedLoading); + s->addSaveFunc([threadedLoading] { Settings::getInstance()->setBool("ThreadedLoading", threadedLoading->getState()); }); + s->updatePosition(); mWindow->pushGui(s); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index db6980ae6e..b842413256 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -32,6 +32,22 @@ bool scrape_cmdline = false; bool parseArgs(int argc, char* argv[]) { + for (int i = 1; i < argc; i++) + { + if (strcmp(argv[i], "--home") == 0) + { + if (i == argc - 1) + continue; + + std::string arg = argv[i + 1]; + if (arg.find("-") == 0) + continue; + + Utils::FileSystem::setHomePath(argv[i + 1]); + i++; // skip vsync value + } + } + Settings::getInstance()->setString("ExePath", argv[0]); for(int i = 1; i < argc; i++) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 0da080ad81..71737653f1 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -449,7 +449,11 @@ void SystemView::renderCarousel(const Transform4x4f& trans) Renderer::pushClipRect(Vector2i((int)clipPos.x(), (int)clipPos.y()), Vector2i((int)mCarousel.size.x(), (int)mCarousel.size.y())); Renderer::setMatrix(carouselTrans); - Renderer::drawRect(0.0, 0.0, mCarousel.size.x(), mCarousel.size.y(), mCarousel.color); + + if (mCarousel.colorGradient != 0) + Renderer::drawGradientRect(0.0, 0.0, mCarousel.size.x(), mCarousel.size.y(), mCarousel.color, mCarousel.colorGradient, mCarousel.colorGradientHorz); + else + Renderer::drawRect(0.0, 0.0, mCarousel.size.x(), mCarousel.size.y(), mCarousel.color); // draw logos Vector2f logoSpacing(0.0, 0.0); // NB: logoSpacing will include the size of the logo itself as well! @@ -623,6 +627,8 @@ void SystemView::getDefaultElements(void) mCarousel.origin.x() = 0.0f; mCarousel.origin.y() = 0.0f; mCarousel.color = 0xFFFFFFD8; + mCarousel.colorGradient = 0; + mCarousel.colorGradientHorz = false; mCarousel.logoScale = 1.2f; mCarousel.logoRotation = 7.5; mCarousel.logoRotationOrigin.x() = -5; @@ -665,6 +671,12 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) mCarousel.origin = elem->get("origin"); if (elem->has("color")) mCarousel.color = elem->get("color"); + + if (elem->has("colorGradient")) + mCarousel.colorGradient = elem->get("colorGradient"); + + mCarousel.colorGradientHorz = (elem->has("gradientType") && !(elem->get("type").compare("horizontal"))); + if (elem->has("logoScale")) mCarousel.logoScale = elem->get("logoScale"); if (elem->has("logoSize")) diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index 4de89cc918..2e4a585896 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -36,6 +36,8 @@ struct SystemViewCarousel Vector2f logoRotationOrigin; Alignment logoAlignment; unsigned int color; + unsigned int colorGradient; + bool colorGradientHorz; int maxLogoCount; // number of logos shown on the carousel Vector2f logoSize; Vector2f logoPos; diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index a4d7f760e8..6f94251d85 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -456,7 +456,7 @@ void GridGameListView::updateInfoPanel() fadingOut = false; } - + std::vector comps = getMDValues(); if (mVideo != nullptr) @@ -465,6 +465,7 @@ void GridGameListView::updateInfoPanel() comps.push_back(&mImage); comps.push_back(&mDescription); comps.push_back(&mName); + std::vector labels = getMDLabels(); comps.insert(comps.cend(), labels.cbegin(), labels.cend()); @@ -475,8 +476,7 @@ void GridGameListView::updateInfoPanel() // 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))) + if((comp->isAnimationPlaying(0) && comp->isAnimationReversed(0) != fadingOut) || (!comp->isAnimationPlaying(0) && comp->getOpacity() != (fadingOut ? 0 : 255))) { auto func = [comp](float t) { diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index 1cbb204927..ae52f7ad45 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -75,6 +75,7 @@ set(CORE_HEADERS # Utils ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/FileSystemUtil.h ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/StringUtil.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/ThreadPool.h ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/TimeUtil.h ) @@ -149,6 +150,7 @@ set(CORE_SOURCES # Utils ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/FileSystemUtil.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/StringUtil.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/ThreadPool.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/utils/TimeUtil.cpp ) diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index 9bdeedf797..ce307a43a6 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -102,7 +102,16 @@ HttpReq::HttpReq(const std::string& url) onError(curl_easy_strerror(err)); return; } - + /* +#ifdef WIN32 +#ifdef _DEBUG + + CURLcode ret; + curl_easy_setopt(mHandle, CURLOPT_PROXY, "127.0.0.1:8081"); + curl_easy_setopt(mHandle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); +#endif +#endif + */ //add the handle to our multi CURLMcode merr = curl_multi_add_handle(s_multi_handle, mHandle); if(merr != CURLM_OK) diff --git a/es-core/src/Renderer.h b/es-core/src/Renderer.h index 5d8b0bd3ba..bbe34b9cc0 100644 --- a/es-core/src/Renderer.h +++ b/es-core/src/Renderer.h @@ -33,11 +33,13 @@ namespace Renderer void pushClipRect(Vector2i pos, Vector2i dim); void popClipRect(); + bool isClippingEnabled(); void setMatrix(const Transform4x4f& transform); void drawRect(int x, int y, int w, int h, unsigned int color, GLenum blend_sfactor = GL_SRC_ALPHA, GLenum blend_dfactor = GL_ONE_MINUS_SRC_ALPHA); void drawRect(float x, float y, float w, float h, unsigned int color, GLenum blend_sfactor = GL_SRC_ALPHA, GLenum blend_dfactor = GL_ONE_MINUS_SRC_ALPHA); + void drawGradientRect(int x, int y, int w, int h, unsigned int color, unsigned int colorBottom, bool horz = false, GLenum blend_sfactor = GL_SRC_ALPHA, GLenum blend_dfactor = GL_ONE_MINUS_SRC_ALPHA); bool isVisibleOnScreen(float x, float y, float w, float h); } diff --git a/es-core/src/Renderer_draw_gl.cpp b/es-core/src/Renderer_draw_gl.cpp index b584c618b1..5fbd0cefb3 100644 --- a/es-core/src/Renderer_draw_gl.cpp +++ b/es-core/src/Renderer_draw_gl.cpp @@ -110,6 +110,8 @@ namespace Renderer { } } + bool isClippingEnabled() { return !clipStack.empty(); } + bool valueInRange(int value, int min, int max) { return (value >= min) && (value <= max); @@ -156,8 +158,62 @@ namespace Renderer { drawRect((int)Math::round(x), (int)Math::round(y), (int)Math::round(w), (int)Math::round(h), color, blend_sfactor, blend_dfactor); } + #define MAKEQUAD(x) (((x) & 0xff000000) >> 24) / 255.0, (((x) & 0x00ff0000) >> 16) / 255.0, (((x) & 0x0000ff00) >> 8) / 255.0, (((x) & 0x000000ff)) / 255.0 + + void drawGradientRect(int x, int y, int w, int h, unsigned int color, unsigned int colorBottom, bool horz, GLenum blend_sfactor, GLenum blend_dfactor) + { + glEnable(GL_BLEND); + glBlendFunc(blend_sfactor, blend_dfactor); + + glBegin(GL_QUADS); + + glColor4f(MAKEQUAD(horz ? colorBottom : color)); + glVertex2f(x, y); + + glColor4f(MAKEQUAD(color)); + glVertex2f(x + w, y); + + glColor4f(MAKEQUAD(horz ? color : colorBottom)); + glVertex2f(x + w, y + h); + + glColor4f(MAKEQUAD(colorBottom)); + glVertex2f(x, y + h); + + glEnd(); + + glDisable(GL_BLEND); + + return; + } + void drawRect(int x, int y, int w, int h, unsigned int color, GLenum blend_sfactor, GLenum blend_dfactor) { + glEnable(GL_BLEND); + glBlendFunc(blend_sfactor, blend_dfactor); + glBegin(GL_QUADS); + + GLfloat red = ((color & 0xff000000) >> 24) / 255.0; + GLfloat green = ((color & 0x00ff0000) >> 16) / 255.0; + GLfloat blue = ((color & 0x0000ff00) >> 8) / 255.0; + GLfloat alpha = ((color & 0x000000ff)) / 255.0; + + glColor4f(red, green, blue, alpha); + glVertex2f(x, y); + + glColor4f(red, green, blue, alpha); + glVertex2f(x+w, y); + + glColor4f(red, green, blue, alpha); + glVertex2f(x+w, y+h); + + glColor4f(red, green, blue, alpha); + glVertex2f(x, y+h); + + glEnd(); + glDisable(GL_BLEND); + + return; + #ifdef USE_OPENGL_ES GLshort points[12]; #else diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 8b49393375..5f655de062 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -37,7 +37,8 @@ std::vector settings_dont_save { { "ScreenOffsetX" }, { "ScreenOffsetY" }, { "ScreenRotate" }, - { "ExePath" } + { "ExePath" }, + { "HomePath" } }; Settings::Settings() @@ -84,7 +85,8 @@ void Settings::setDefaults() mBoolMap["MoveCarousel"] = true; mBoolMap["SaveGamelistsOnExit"] = true; mBoolMap["OptimizeVRAM"] = false; - + mBoolMap["ThreadedLoading"] = true; + mBoolMap["Debug"] = false; mBoolMap["DebugGrid"] = false; mBoolMap["DebugText"] = false; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 1fdcf69b99..5c9b876524 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -30,6 +30,7 @@ std::map> The { "default", PATH }, { "tile", BOOLEAN }, { "color", COLOR }, + { "reflexion", NORMALIZED_PAIR }, { "zIndex", FLOAT } } }, { "imagegrid", { { "pos", NORMALIZED_PAIR }, @@ -57,6 +58,7 @@ std::map> The { "backgroundCenterColor", COLOR }, { "backgroundEdgeColor", COLOR }, { "selectionMode", STRING }, + { "reflexion", NORMALIZED_PAIR }, { "imageSizeMode", STRING } } }, { "text", { { "pos", NORMALIZED_PAIR }, @@ -85,6 +87,7 @@ std::map> The { "selectorHeight", FLOAT }, { "selectorOffsetY", FLOAT }, { "selectorColor", COLOR }, + { "selectorGradientColor", COLOR }, { "selectorImagePath", PATH }, { "selectorImageTile", BOOLEAN }, { "selectedColor", COLOR }, @@ -163,6 +166,8 @@ std::map> The { "pos", NORMALIZED_PAIR }, { "origin", NORMALIZED_PAIR }, { "color", COLOR }, + { "colorGradient", COLOR }, + { "gradientType", STRING }, { "logoScale", FLOAT }, { "logoRotation", FLOAT }, { "logoRotationOrigin", NORMALIZED_PAIR }, @@ -177,6 +182,7 @@ std::map> The { "fontSize", FLOAT }, { "separatorColor", COLOR }, { "selectorColor", COLOR }, + { "selectorGradientColor", COLOR }, { "selectedColor", COLOR }, { "color", COLOR } } }, @@ -303,7 +309,8 @@ void ThemeData::loadFile(std::string system, std::map mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); pugi::xml_document doc; - pugi::xml_parse_result res = doc.load_file(path.c_str()); + pugi::xml_parse_result res = doc.load_file(path.c_str()); + if(!res) throw error << "XML parsing error: \n " << res.description(); @@ -469,6 +476,7 @@ void ThemeData::parseIncludes(const pugi::xml_node& root) pugi::xml_document includeDoc; pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); + if(!result) throw error << "Error parsing file: \n " << result.description(); @@ -1086,6 +1094,8 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData& theme) Text.selectedColor = elem->get("selectedColor"); if (elem->has("selectorColor")) Text.selectorColor = elem->get("selectorColor"); + if (elem->has("selectorGradientColor")) + Text.selectorGradientColor = elem->get("selectorGradientColor"); } elem = theme.getElement("menu", "menubutton", "menuButton"); @@ -1171,6 +1181,7 @@ void ThemeData::crawlIncludes(const pugi::xml_node& root, std::map ThemeData::getThemeSubSets(const std::string& if (!Utils::FileSystem::isDirectory(paths[i])) continue; - auto dirs = Utils::FileSystem::getDirInfo(paths[i] + "/" + theme, false); + auto dirs = Utils::FileSystem::getDirInfo(paths[i] + "/" + theme); for (auto it = dirs.cbegin(); it != dirs.cend(); ++it) { if (!it->directory || it->hidden) @@ -1218,7 +1229,8 @@ std::map ThemeData::getThemeSubSets(const std::string& dequepath.push_back(path); pugi::xml_document doc; - doc.load_file(path.c_str()); + doc.load_file(path.c_str()); + pugi::xml_node root = doc.child("theme"); crawlIncludes(root, sets, dequepath); findRegion(doc, sets); @@ -1231,7 +1243,8 @@ std::map ThemeData::getThemeSubSets(const std::string& dequepath.push_back(path); pugi::xml_document doc; - doc.load_file(path.c_str()); + doc.load_file(path.c_str()); + pugi::xml_node root = doc.child("theme"); crawlIncludes(root, sets, dequepath); findRegion(doc, sets); diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index eeed1a54c4..0e6b87c646 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -88,8 +88,9 @@ struct ThemeSet struct MenuElement { unsigned int color; unsigned int selectedColor; - unsigned int selectorColor; + unsigned int selectorColor; unsigned int separatorColor; + unsigned int selectorGradientColor; std::string path; std::string fadePath; std::shared_ptr font; @@ -126,11 +127,11 @@ class ThemeData public: ThemeMenu(ThemeData& theme); - MenuElement Background{ 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, ":/frame.png", ":/scroll_gradient.png", nullptr }; - MenuElement Title{ 0x555555FF, 0x555555FF, 0x555555FF, 0xFFFFFFFF, "", "", nullptr }; - MenuElement Text{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, "", "", nullptr }; - MenuElement TextSmall{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, "", "", nullptr }; - MenuElement Footer{ 0xC6C6C6FF, 0xC6C6C6FF, 0xC6C6C6FF, 0xFFFFFFFF, "", "", nullptr }; + MenuElement Background{ 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0, ":/frame.png", ":/scroll_gradient.png", nullptr }; + MenuElement Title{ 0x555555FF, 0x555555FF, 0x555555FF, 0xFFFFFFFF, 0, "", "", nullptr }; + MenuElement Text{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0, "", "", nullptr }; + MenuElement TextSmall{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0, "", "", nullptr }; + MenuElement Footer{ 0xC6C6C6FF, 0xC6C6C6FF, 0xC6C6C6FF, 0xFFFFFFFF, 0, "", "", nullptr }; MenuIconElement MenuIcons { "","","","","","","","","" }; IconElement Icons { ":/button.png", ":/button_filled.png", ":/on.svg", ":/off.svg", ":/option_arrow.svg", ":/arrow.svg", ":/slider_knob.svg" }; }; diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 2bcca44e05..0a12f302fa 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -319,6 +319,9 @@ void Window::endRenderLoadingScreen() void Window::renderLoadingScreen(std::string text, float percent) { + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, true); + Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); @@ -334,15 +337,13 @@ void Window::renderLoadingScreen(std::string text, float percent) float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); Renderer::drawRect(x, y, w, h, 0x252525FF); - Renderer::drawRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x656565FF); // 0xFFFFFFFF + // Renderer::drawGradientRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x858585FF, 0x555555FF); // 0xFFFFFFFF + Renderer::drawGradientRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x006C9EFF, 0x003E5CFF); // 0xFFFFFFFF } ImageComponent splash(this, true); splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); - if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, false, true); - if (mSplash != NULL) splash.setImage(mSplash); else @@ -381,7 +382,7 @@ void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) #if defined(_WIN32) if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, false, true); + mSplash = TextureResource::get(":/splash.svg", false, true, true); #endif if (mSplash != NULL) diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index 4befbde724..fc9925e581 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -164,6 +164,7 @@ void ComponentList::render(const Transform4x4f& parentTrans) auto menuTheme = ThemeData::getMenuTheme(); unsigned int selectorColor = menuTheme->Text.selectorColor; + unsigned int selectorGradientColor = menuTheme->Text.selectorGradientColor; unsigned int selectedColor = menuTheme->Text.selectedColor; unsigned int bgColor = menuTheme->Background.color; unsigned int separatorColor = menuTheme->Text.separatorColor; @@ -214,9 +215,17 @@ void ComponentList::render(const Transform4x4f& parentTrans) const float selectedRowHeight = getRowHeight(mEntries.at(mCursor).data); if ((selectorColor != bgColor) && ((selectorColor & 0xFF) != 0x00)) { - //Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, GL_ONE_MINUS_DST_COLOR, GL_ZERO); - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, GL_ZERO, GL_ONE_MINUS_SRC_COLOR); - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, GL_ONE, GL_ONE); + + if (selectorGradientColor != 0) + { + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, GL_ZERO, GL_ONE_MINUS_SRC_COLOR); + Renderer::drawGradientRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, selectorGradientColor, false, GL_ONE, GL_ONE); + } + else + { + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, GL_ZERO, GL_ONE_MINUS_SRC_COLOR); + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, GL_ONE, GL_ONE); + } } // Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0xFFFFFFFF, GL_ONE_MINUS_DST_COLOR, GL_ZERO); diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index fdcd8ed52d..85d1ce8fe5 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -43,8 +43,6 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mSelectedProperties.mBackgroundCenterColor = 0xFFFFFFFF; mSelectedProperties.mBackgroundEdgeColor = 0xFFFFFFFF; - - mDefaultProperties.mLabelSize = Vector2f(1.0, 0.30); mDefaultProperties.mLabelColor = 0xFFFFFFFF; mDefaultProperties.mLabelBackColor = 0; @@ -63,6 +61,9 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mSelectedProperties.mFontPath = ""; mSelectedProperties.mFontSize = 0; + mDefaultProperties.mMirror = Vector2f(0, 0); + mSelectedProperties.mMirror = Vector2f(0, 0); + mImage = std::make_shared(mWindow); mImage->setOrigin(0.5f, 0.5f); mImage->setAllowFading(false); @@ -150,6 +151,7 @@ void GridTileComponent::resize() mImage->setOrigin(0.5f, 0.5f); mImage->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); mImage->setColorShift(currentProperties.mImageColor); + mImage->setMirroring(currentProperties.mMirror); if (currentProperties.mImageSizeMode == "minSize") mImage->setMinSize(imageWidth, imageHeight); @@ -343,8 +345,17 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mDefaultProperties.mSelectionMode = elem->get("selectionMode"); mSelectedProperties.mSelectionMode = mDefaultProperties.mSelectionMode; } + + if (elem && elem->has("reflexion")) + { + mDefaultProperties.mMirror = elem->get("reflexion"); + mSelectedProperties.mMirror = mDefaultProperties.mMirror; + } } +// mDefaultProperties.mMirror = Vector2f(0, 0); +// mSelectedProperties.mMirror = Vector2f(0, 0); + // Apply theme to the selected gridtile // NOTE that some of the default gridtile properties influence on the selected gridtile properties // See THEMES.md for more informations @@ -382,6 +393,9 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem && elem->has("backgroundEdgeColor")) mSelectedProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); + if (elem && elem->has("reflexion")) + mSelectedProperties.mMirror = elem->get("reflexion"); + elem = theme->getElement(view, "gridtile", "text"); if (elem != NULL) { @@ -712,6 +726,13 @@ const GridTileProperties& GridTileComponent::getCurrentProperties() if (mDefaultProperties.mLabelGlowSize != mSelectedProperties.mLabelGlowSize) mMixedProperties.mLabelGlowSize = mDefaultProperties.mLabelGlowSize * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mLabelGlowSize * mSelectedZoomPercent; + + if (mDefaultProperties.mMirror != mSelectedProperties.mMirror) + { + float x = mDefaultProperties.mMirror.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mMirror.x() * mSelectedZoomPercent; + float y = mDefaultProperties.mMirror.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mMirror.y() * mSelectedZoomPercent; + mMixedProperties.mMirror = Vector2f(x, y); + } // Avoid to multiply font sizes in mem + it create strange sizings // if (mDefaultProperties.mFontSize != mSelectedProperties.mFontSize) diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index 0576b7ac8f..7903af9ff1 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -31,6 +31,8 @@ struct GridTileProperties std::string mFontPath; unsigned int mFontSize; + Vector2f mMirror; + /* . / main / fonts / Dosis - Bold.ttf< / fontPath> 0.025< / fontSize>*/ diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 7888b2f051..57456cdb38 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -24,9 +24,9 @@ Vector2f ImageComponent::getSize() const ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : GuiComponent(window), mTargetIsMax(false), mTargetIsMin(false), mFlipX(false), mFlipY(false), mTargetSize(0, 0), mColorShift(0xFFFFFFFF), mForceLoad(forceLoad), mDynamic(dynamic), mFadeOpacity(0), mFading(false), mRotateByTargetSize(false), mVisible(true), - mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f) + mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f), mMirror(0.0f, 0.0f) { - updateColors(); + } ImageComponent::~ImageComponent() @@ -262,21 +262,16 @@ void ImageComponent::setFlipY(bool flip) void ImageComponent::setColorShift(unsigned int color) { - if (mColorShift == color) - return; - mColorShift = color; // Grab the opacity from the color shift because we may need to apply it if // fading textures in - mOpacity = color & 0xff; - updateColors(); + mOpacity = color & 0xff; } void ImageComponent::setOpacity(unsigned char opacity) { mOpacity = opacity; - mColorShift = (mColorShift >> 8 << 8) | mOpacity; - updateColors(); + mColorShift = (mColorShift >> 8 << 8) | mOpacity; } void ImageComponent::updateVertices() @@ -290,6 +285,12 @@ void ImageComponent::updateVertices() Vector2f topLeft(size * mTopLeftCrop); Vector2f bottomRight(size * mBottomRightCrop); + mVertices[0].pos = Vector2f(topLeft.x(), topLeft.y()); + mVertices[1].pos = Vector2f(topLeft.x(), bottomRight.y()); + mVertices[2].pos = Vector2f(bottomRight.x(), bottomRight.y()); + mVertices[3].pos = Vector2f(bottomRight.x(), topLeft.y()); + + /* mVertices[0].pos = Vector2f(topLeft.x(), topLeft.y()); mVertices[1].pos = Vector2f(topLeft.x(), bottomRight.y()); @@ -298,6 +299,7 @@ void ImageComponent::updateVertices() mVertices[3].pos = Vector2f(bottomRight.x(), topLeft.y()); mVertices[4].pos = Vector2f(topLeft.x(), bottomRight.y()); mVertices[5].pos = Vector2f(bottomRight.x(), bottomRight.y()); + */ float px, py; if (mTexture->isTiled()) @@ -311,6 +313,13 @@ void ImageComponent::updateVertices() py = 1; } + mVertices[0].tex = Vector2f(mTopLeftCrop.x(), py - mTopLeftCrop.y()); + mVertices[1].tex = Vector2f(mTopLeftCrop.x(), 1 - mBottomRightCrop.y()); + + mVertices[2].tex = Vector2f(px * mBottomRightCrop.x(), 1 - mBottomRightCrop.y()); + mVertices[3].tex = Vector2f(px * mBottomRightCrop.x(), py - mTopLeftCrop.y()); + + /* mVertices[0].tex = Vector2f(mTopLeftCrop.x(), py - mTopLeftCrop.y()); mVertices[1].tex = Vector2f(mTopLeftCrop.x(), 1 - mBottomRightCrop.y()); mVertices[2].tex = Vector2f(px * mBottomRightCrop.x(), py - mTopLeftCrop.y()); @@ -318,25 +327,21 @@ void ImageComponent::updateVertices() mVertices[3].tex = Vector2f(px * mBottomRightCrop.x(), py - mTopLeftCrop.y()); mVertices[4].tex = Vector2f(mTopLeftCrop.x(), 1 - mBottomRightCrop.y()); mVertices[5].tex = Vector2f(px * mBottomRightCrop.x(), 1 - mBottomRightCrop.y()); + */ if (mFlipX) { - for(int i = 0; i < 6; i++) + for(int i = 0; i < 4; i++) mVertices[i].tex[0] = px - mVertices[i].tex[0]; } if (mFlipY) { - for(int i = 0; i < 6; i++) + for(int i = 0; i < 4; i++) mVertices[i].tex[1] = py - mVertices[i].tex[1]; } } -void ImageComponent::updateColors() -{ - Renderer::buildGLColorArray(mColors, mColorShift, 6); -} - void ImageComponent::render(const Transform4x4f& parentTrans) { if (!mVisible) @@ -350,7 +355,7 @@ void ImageComponent::render(const Transform4x4f& parentTrans) Renderer::setMatrix(trans); - if (mTexture && mOpacity > 0) + if (mTexture && (mColorShift & 0xff)) { if (Settings::getInstance()->getBool("DebugImage")) { @@ -367,16 +372,11 @@ void ImageComponent::render(const Transform4x4f& parentTrans) // when it finally loads fadeIn(mTexture->bind()); - GLfloat color[16] = { - 0.5,0,0,0, - 0,1,0,0, - 0,0,1,0, - 0,0,0,1 }; - glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + /* glEnableClientState(GL_VERTEX_ARRAY); glEnableClientState(GL_TEXTURE_COORD_ARRAY); glEnableClientState(GL_COLOR_ARRAY); @@ -384,12 +384,65 @@ void ImageComponent::render(const Transform4x4f& parentTrans) glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].pos); glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].tex); glColorPointer(4, GL_UNSIGNED_BYTE, 0, mColors); - glDrawArrays(GL_TRIANGLES, 0, 6); - + glDisableClientState(GL_VERTEX_ARRAY); glDisableClientState(GL_TEXTURE_COORD_ARRAY); glDisableClientState(GL_COLOR_ARRAY); + */ + + glBegin(GL_QUADS); + + GLfloat red = ((mColorShift & 0xff000000) >> 24) / 255.0; + GLfloat green = ((mColorShift & 0x00ff0000) >> 16) / 255.0; + GLfloat blue = ((mColorShift & 0x0000ff00) >> 8) / 255.0; + GLfloat alpha = ((mColorShift & 0x000000ff)) / 255.0; + + glColor4f(red, green, blue, alpha); + glTexCoord2f(mVertices[0].tex.x(), mVertices[0].tex.y()); + glVertex2f(mVertices[0].pos.x(), mVertices[0].pos.y()); + + glColor4f(red, green, blue, alpha); + glTexCoord2f(mVertices[1].tex.x(), mVertices[1].tex.y()); + glVertex2f(mVertices[1].pos.x(), mVertices[1].pos.y()); + + glColor4f(red, green, blue, alpha); + glTexCoord2f(mVertices[2].tex.x(), mVertices[2].tex.y()); + glVertex2f(mVertices[2].pos.x(), mVertices[2].pos.y()); + + glColor4f(red, green, blue, alpha); + glTexCoord2f(mVertices[3].tex.x(), mVertices[3].tex.y()); + glVertex2f(mVertices[3].pos.x(), mVertices[3].pos.y()); + + glEnd(); + + if (mMirror.x() != 0 || mMirror.y() != 0) + { + glBegin(GL_QUADS); + + int h = mVertices[1].pos.y() - mVertices[0].pos.y(); + + GLfloat alpha2 = alpha * mMirror.y(); + alpha *= mMirror.x(); + + glColor4f(red, green, blue, alpha); + glTexCoord2f(mVertices[0].tex.x(), mVertices[1].tex.y()); + glVertex2f(mVertices[0].pos.x(), mVertices[0].pos.y() + h); + + glColor4f(red, green, blue, alpha2); + glTexCoord2f(mVertices[1].tex.x(), mVertices[0].tex.y()); + glVertex2f(mVertices[1].pos.x(), mVertices[1].pos.y() + h); + + glColor4f(red, green, blue, alpha2); + glTexCoord2f(mVertices[2].tex.x(), mVertices[3].tex.y()); + glVertex2f(mVertices[2].pos.x(), mVertices[2].pos.y() + h); + + glColor4f(red, green, blue, alpha); + glTexCoord2f(mVertices[3].tex.x(), mVertices[2].tex.y()); + glVertex2f(mVertices[3].pos.x(), mVertices[3].pos.y() + h); + + glEnd(); + } glDisable(GL_TEXTURE_2D); glDisable(GL_BLEND); @@ -420,8 +473,7 @@ void ImageComponent::fadeIn(bool textureLoaded) mFadeOpacity = 0; mFading = true; // Set the colours to be translucent - mColorShift = (mColorShift >> 8 << 8) | 0; - updateColors(); + mColorShift = (mColorShift >> 8 << 8) | 0; } } else if (mFading) @@ -442,8 +494,7 @@ void ImageComponent::fadeIn(bool textureLoaded) } // Apply the combination of the target opacity and current fade float newOpacity = (float)mOpacity * ((float)mFadeOpacity / 255.0f); - mColorShift = (mColorShift >> 8 << 8) | (unsigned char)newOpacity; - updateColors(); + mColorShift = (mColorShift >> 8 << 8) | (unsigned char)newOpacity; } } } @@ -481,6 +532,7 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s setMinSize(elem->get("minSize") * scale); } + // position + size also implies origin if((properties & ORIGIN || (properties & POSITION && properties & ThemeFlags::SIZE)) && elem->has("origin")) setOrigin(elem->get("origin")); @@ -502,6 +554,9 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s if(properties & COLOR && elem->has("color")) setColorShift(elem->get("color")); + if (properties & COLOR && elem->has("reflexion")) + mMirror = elem->get("reflexion"); + if(properties & ThemeFlags::ROTATION) { if(elem->has("rotation")) setRotationDegrees(elem->get("rotation")); diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 6233725460..0037091c1a 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -81,6 +81,7 @@ class ImageComponent : public GuiComponent void render(const Transform4x4f& parentTrans) override; void setAllowFading(bool fade) { mAllowFading = fade; }; + void setMirroring(Vector2f mirror) { mMirror = mirror; }; virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; @@ -98,12 +99,12 @@ class ImageComponent : public GuiComponent { Vector2f pos; Vector2f tex; - } mVertices[6]; + } mVertices[4]; - GLubyte mColors[6*4]; +// GLubyte mColors[6*4]; void updateVertices(); - void updateColors(); +// void updateColors(); void fadeIn(bool textureLoaded); unsigned int mColorShift; @@ -122,6 +123,8 @@ class ImageComponent : public GuiComponent Vector2f mTopLeftCrop; Vector2f mBottomRightCrop; + + Vector2f mMirror; }; #endif // ES_CORE_COMPONENTS_IMAGE_COMPONENT_H diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index 8b7612dd9f..4acee3056b 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -14,7 +14,7 @@ TextComponent::TextComponent(Window* window) : GuiComponent(window), TextComponent::TextComponent(Window* window, const std::string& text, const std::shared_ptr& font, unsigned int color, Alignment align, Vector3f pos, Vector2f size, unsigned int bgcolor) : GuiComponent(window), - mFont(NULL), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), + mFont(NULL), mUppercase(false), mColor(1), mAutoCalcExtent(true, true), mHorizontalAlignment(align), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), mRenderBackground(false), mGlowColor(0), mGlowSize(2) { @@ -358,9 +358,6 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, const st if(properties & LINE_SPACING && elem->has("lineSpacing")) setLineSpacing(elem->get("lineSpacing")); - if (properties & COLOR && elem->has("color")) - setColor(elem->get("color")); - if (properties & COLOR && elem->has("glowColor")) mGlowColor = elem->get("glowColor"); diff --git a/es-core/src/platform.h b/es-core/src/platform.h index efc74417f6..f677d1cbaa 100644 --- a/es-core/src/platform.h +++ b/es-core/src/platform.h @@ -31,10 +31,14 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo int quitES(const std::string& filename); void touch(const std::string& filename); +#if defined(WIN32) +#include +#include +#include "Log.h" +#endif #if !defined(TRACE) -#if defined(WIN32) && defined(_DEBUG) - #include +#if defined(WIN32) && defined(_DEBUG) #include #define TRACE( s ) \ @@ -48,4 +52,40 @@ void touch(const std::string& filename); #endif #endif +class StopWatch +{ +public: + StopWatch(std::string name) + { +#if defined(WIN32) + mName = name; + mTicks = ::GetTickCount(); +#endif + } + + ~StopWatch() + { +#if defined(WIN32) + int now = ::GetTickCount(); + + mTicks = now - mTicks; + + LOG(LogInfo) << mName << " " << mTicks << " ms" << " on CPU " << GetCurrentProcessorNumber(); + TRACE(mName << " " << mTicks << " ms" << " on CPU " << GetCurrentProcessorNumber()); +#endif + } + +private: +#if defined(WIN32) + DWORD GetCurrentProcessorNumber() + { + int CPUInfo[4]; + __cpuid(CPUInfo, 1); + if ((CPUInfo[3] & (1 << 9)) == 0) return -1; // no APIC on chip + return (unsigned)CPUInfo[1] >> 24; + } +#endif + int mTicks; + std::string mName; +}; #endif // ES_CORE_PLATFORM_H diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 064a8955a9..67db642338 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -6,11 +6,13 @@ #include "Settings.h" #include #include +#include "platform.h" #if defined(_WIN32) // because windows... #include #include +#include #define getcwd _getcwd #define mkdir(x,y) _mkdir(x) #define snprintf _snprintf @@ -27,6 +29,17 @@ namespace Utils { namespace FileSystem { +#if defined(_WIN32) + std::mutex mFileMutex; +#endif + + pugi::xml_parse_result load_xml(pugi::xml_document& doc, const char* path) + { +#if defined(_WIN32) + std::unique_lock lock(mFileMutex); +#endif + return doc.load_file(path); + } #if defined(_WIN32) static std::string convertFromWideString(const std::wstring wstring) @@ -54,7 +67,9 @@ namespace Utils return (first.path.length() < second.path.length()); } - fileList getDirInfo(const std::string& _path, const bool _recursive) + + + fileList getDirInfo(const std::string& _path/*, const bool _recursive*/) { std::string path = getGenericPath(_path); fileList contentList; @@ -63,6 +78,8 @@ namespace Utils if (isDirectory(path)) { #if defined(_WIN32) + std::unique_lock lock(mFileMutex); + WIN32_FIND_DATAW findData; std::string wildcard = path + "/*"; HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); @@ -73,26 +90,17 @@ namespace Utils do { std::string name = convertFromWideString(findData.cFileName); - - // ignore "." and ".." - if ((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - - FileInfo fi; - fi.path = fullName; - fi.hidden = (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN; - fi.directory = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY; - contentList.push_back(fi); - - if (_recursive && fi.directory) - { - fileList fl = getDirInfo(fullName, true); - for (auto it = fl.cbegin(); it != fl.cend(); ++it) - contentList.push_back(*it); - } - } - } while (FindNextFileW(hFind, &findData)); + + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY && name == "." || name == "..") + continue; + + FileInfo fi; + fi.path = path + "/" + name; + fi.hidden = (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN; + fi.directory = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY; + contentList.push_back(fi); + } + while (FindNextFileW(hFind, &findData)); FindClose(hFind); } @@ -118,13 +126,6 @@ namespace Utils fi.readOnly = Utils::FileSystem::isHidden(fullName); fi.directory = isDirectory(fullName); contentList.push_back(fi); - - if (_recursive && fi.directory) - { - fileList fl = getDirInfo(fullName, true); - for (auto it = fl.cbegin(); it != fl.cend(); ++it) - contentList.push_back(*it); - } } } @@ -151,6 +152,11 @@ namespace Utils if(isDirectory(path)) { #if defined(_WIN32) + std::unique_lock* pLock = nullptr; + + if (!_recursive) + pLock = new std::unique_lock(mFileMutex); + WIN32_FIND_DATAW findData; std::string wildcard = path + "/*"; HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); @@ -163,23 +169,26 @@ namespace Utils std::string name = convertFromWideString(findData.cFileName); // ignore "." and ".." - if((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY && name == "." || name == "..") + continue; - if (!includeHidden && (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN) - continue; + std::string fullName(getGenericPath(path + "/" + name)); + + if (!includeHidden && (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN) + continue; - contentList.push_back(fullName); + contentList.push_back(fullName); - if(_recursive && (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) - contentList.merge(getDirContent(fullName, true, includeHidden)); - } + if(_recursive && (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) + contentList.merge(getDirContent(fullName, true, includeHidden)); } while(FindNextFileW(hFind, &findData)); FindClose(hFind); } + + if (pLock != nullptr) + delete pLock; #else // _WIN32 DIR* dir = opendir(path.c_str()); @@ -246,8 +255,19 @@ namespace Utils } // getPathList + std::string mCustomHomePath = ""; + + void setHomePath(std::string path) + { + mCustomHomePath = path; + } + std::string getHomePath() { + if (!mCustomHomePath.empty()) + return mCustomHomePath; + + static std::string path; // only construct the homepath once @@ -260,17 +280,9 @@ namespace Utils path = getExePath(); return path; } - + // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows char* envHome = getenv("HOME"); - - envHome = "H:/[Emulz]/EmulationStation/"; - -#ifdef _DEBUG - if (Utils::FileSystem::exists("H:/[Emulz]/EmulationStation/emulationstation.exe")) - envHome = "H:/[Emulz]/EmulationStation/"; -#endif - if (envHome) path = getGenericPath(envHome); @@ -693,13 +705,18 @@ namespace Utils } // createDirectory bool exists(const std::string& _path) - { + { +#ifdef WIN32 + DWORD dwAttr = GetFileAttributes(_path.c_str()); + if (0xFFFFFFFF == dwAttr) + return false; +#else std::string path = getGenericPath(_path); struct stat64 info; // check if stat64 succeeded return (stat64(path.c_str(), &info) == 0); - +#endif } // exists size_t getFileSize(const std::string& _path) @@ -742,6 +759,14 @@ namespace Utils bool isDirectory(const std::string& _path) { +#ifdef WIN32 + DWORD dwAttr = GetFileAttributes(_path.c_str()); + if (0xFFFFFFFF == dwAttr) + return false; + + return (dwAttr & FILE_ATTRIBUTE_DIRECTORY); +#else + std::string path = getGenericPath(_path); struct stat info; @@ -751,7 +776,7 @@ namespace Utils // check for S_IFDIR attribute return (S_ISDIR(info.st_mode)); - +#endif } // isDirectory bool isSymlink(const std::string& _path) diff --git a/es-core/src/utils/FileSystemUtil.h b/es-core/src/utils/FileSystemUtil.h index 3eb611cd0e..d572a0f526 100644 --- a/es-core/src/utils/FileSystemUtil.h +++ b/es-core/src/utils/FileSystemUtil.h @@ -4,6 +4,7 @@ #include #include +#include namespace Utils { @@ -20,7 +21,7 @@ namespace Utils typedef std::list stringList; typedef std::list fileList; - fileList getDirInfo(const std::string& _path, const bool _recursive = false); + fileList getDirInfo(const std::string& _path/*, const bool _recursive = false*/); stringList getDirContent (const std::string& _path, const bool _recursive = false, const bool includeHidden = true); stringList getPathList (const std::string& _path); std::string getHomePath (); @@ -50,6 +51,13 @@ namespace Utils bool isSymlink (const std::string& _path); bool isHidden (const std::string& _path); + void setHomePath (std::string path); + + + pugi::xml_parse_result load_xml(pugi::xml_document& doc, const char* path); + + + } // FileSystem:: diff --git a/es-core/src/utils/ThreadPool.cpp b/es-core/src/utils/ThreadPool.cpp new file mode 100644 index 0000000000..90a21d041b --- /dev/null +++ b/es-core/src/utils/ThreadPool.cpp @@ -0,0 +1,106 @@ +#include "ThreadPool.h" + +#if WIN32 +#include +#endif + +namespace Utils +{ + size_t ThreadPool::getProcessorCount() + { + //unsigned num_cpus = std::thread::hardware_concurrency(); + +#ifdef WIN32 + SYSTEM_INFO sysinfo; + GetSystemInfo(&sysinfo); + return sysinfo.dwNumberOfProcessors; +#else + return sysconf(_SC_NPROCESSORS_ONLN); +#endif + } + + ThreadPool::ThreadPool() : mRunning(true), mWaiting(false), mNumWork(0) + { + size_t num_threads = getProcessorCount() - 1; + + auto doWork = [&](size_t id) + { +#if WIN32 + auto mask = (static_cast(1) << id); + SetThreadAffinityMask(GetCurrentThread(), mask); +#endif + + while (mRunning) + { + _mutex.lock(); + if (!mWorkQueue.empty()) + { + auto work = mWorkQueue.front(); + mWorkQueue.pop(); + _mutex.unlock(); + + try + { + work(); + } + catch (...) {} + + mNumWork--; + } + else + { + _mutex.unlock(); + + // Extra code : Exit finished threads + if (mWaiting) + return; + + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + } + }; + + mThreads.reserve(num_threads); + + for (size_t i = 0; i < num_threads; i++) + mThreads.push_back(std::thread(doWork, i)); + } + + ThreadPool::~ThreadPool() + { + mRunning = false; + + for (std::thread& t : mThreads) + if (t.joinable()) + t.join(); + } + + void ThreadPool::queueWorkItem(work_function work) + { + _mutex.lock(); + mWorkQueue.push(work); + mNumWork++; + _mutex.unlock(); + } + + void ThreadPool::wait() + { + mWaiting = true; + while (mNumWork.load() > 0) + std::this_thread::yield(); + } + + void ThreadPool::wait(work_function work, int delay) + { + mWaiting = true; + + while (mNumWork.load() > 0) + { + work(); + + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(delay)); + } + } +} \ No newline at end of file diff --git a/es-core/src/utils/ThreadPool.h b/es-core/src/utils/ThreadPool.h new file mode 100644 index 0000000000..bff00d7bed --- /dev/null +++ b/es-core/src/utils/ThreadPool.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include +#include + +namespace Utils +{ + class ThreadPool + { + public: + static size_t getProcessorCount(); + + typedef std::function work_function; + + ThreadPool(); + ~ThreadPool(); + + void queueWorkItem(work_function work); + void wait(); + void wait(work_function work, int delay = 50); + + private: + bool mRunning; + bool mWaiting; + std::queue mWorkQueue; + std::atomic mNumWork; + std::mutex _mutex; + std::vector mThreads; + + }; +} \ No newline at end of file diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index ccd347f69a..b23bac8ec2 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -140,6 +140,10 @@ msgstr "STYLE D'AFFICHAGE" msgid "GAME LAUNCH TRANSITION" msgstr "TRANSITION AU LANCEMENT D'UN JEU" +#: +msgid "THREADED LOADING" +msgstr "Chargement multitâche" + #: msgid "RANDOM" msgstr "ALÉATOIRE" From eeccf4e7faa9df27a288ddd006a8fdd421219d24 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 29 Jun 2019 11:23:39 +0200 Subject: [PATCH 046/336] Performance fix when loading custom collections. --- es-app/src/CollectionSystemManager.cpp | 27 ++++++++++++------- es-app/src/CollectionSystemManager.h | 3 ++- es-app/src/FileData.cpp | 13 +++++++++ es-app/src/FileData.h | 2 ++ .../src/guis/GuiSlideshowScreensaverOptions.h | 1 + .../src/views/gamelist/GridGameListView.cpp | 2 +- .../views/gamelist/ISimpleGameListView.cpp | 4 +-- 7 files changed, 39 insertions(+), 13 deletions(-) diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 295f530a08..9d2bec80ea 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -438,9 +438,8 @@ void CollectionSystemManager::setEditMode(std::string collectionName) CollectionSystemData* sysData = &(mCustomCollectionSystemsData.at(mEditingCollection)); if (!sysData->isPopulated) - { populateCustomCollection(sysData); - } + // if it's bundled, this needs to be the bundle system mEditingCollectionSystemData = sysData; @@ -477,9 +476,8 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) SystemData* sysData = mEditingCollectionSystemData->system; mEditingCollectionSystemData->needsSave = true; if (!mEditingCollectionSystemData->isPopulated) - { populateCustomCollection(mEditingCollectionSystemData); - } + std::string key = file->getFullPath(); FolderData* rootFolder = sysData->getRootFolder(); @@ -758,7 +756,7 @@ void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysDa } // populates a Custom Collection System -void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sysData) +void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sysData, std::unordered_map* pMap) { SystemData* newSys = sysData->system; sysData->isPopulated = true; @@ -779,13 +777,21 @@ void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sys FolderData* folder = getAllGamesCollection()->getRootFolder(); + std::unordered_map map; + + if (pMap == nullptr) + { + folder->createChildrenByFilenameMap(map); + pMap = ↦ + } + // iterate list of files in config file for(std::string gameKey; getline(input, gameKey); ) { - FileData* file = folder->FindByPath(gameKey); - if (file != nullptr) + std::unordered_map::const_iterator it = pMap->find(gameKey); + if (it != pMap->cend()) { - CollectionFileData* newGame = new CollectionFileData(file, newSys); + CollectionFileData* newGame = new CollectionFileData(it->second, newSys); rootFolder->addChild(newGame); newSys->addToIndex(newGame); } @@ -830,6 +836,9 @@ void CollectionSystemManager::removeCollectionsFromDisplayedSystems() void CollectionSystemManager::addEnabledCollectionsToDisplayedSystems(std::map* colSystemData) { + std::unordered_map map; + getAllGamesCollection()->getRootFolder()->createChildrenByFilenameMap(map); + // add auto enabled ones for(std::map::iterator it = colSystemData->begin() ; it != colSystemData->end() ; it++ ) { @@ -840,7 +849,7 @@ void CollectionSystemManager::addEnabledCollectionsToDisplayedSystems(std::mapsecond.decl.isCustom) { - populateCustomCollection(&(it->second)); + populateCustomCollection(&(it->second), &map); } else { diff --git a/es-app/src/CollectionSystemManager.h b/es-app/src/CollectionSystemManager.h index a973c0db39..31ada3b25d 100644 --- a/es-app/src/CollectionSystemManager.h +++ b/es-app/src/CollectionSystemManager.h @@ -5,6 +5,7 @@ #include #include #include +#include class FileData; class FolderData; @@ -93,7 +94,7 @@ class CollectionSystemManager SystemData* getAllGamesCollection(); SystemData* createNewCollectionEntry(std::string name, CollectionSystemDecl sysDecl, bool index = true); void populateAutoCollection(CollectionSystemData* sysData); - void populateCustomCollection(CollectionSystemData* sysData); + void populateCustomCollection(CollectionSystemData* sysData, std::unordered_map* pMap = nullptr); void removeCollectionsFromDisplayedSystems(); void addEnabledCollectionsToDisplayedSystems(std::map* colSystemData); diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 8b215830a9..a7a3616055 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -519,4 +519,17 @@ FileData* FolderData::FindByPath(const std::string& path) } return nullptr; +} + +void FolderData::createChildrenByFilenameMap(std::unordered_map& map) +{ + std::vector children = getChildren(); + + for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + if ((*it)->getType() == FOLDER) + ((FolderData*)(*it))->createChildrenByFilenameMap(map); + else + map[(*it)->getKey()] = (*it); + } } \ No newline at end of file diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 5293da3c4b..cd19a086ca 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -146,6 +146,8 @@ class FolderData : public FileData void addChild(FileData* file); // Error if mType != FOLDER void removeChild(FileData* file); //Error if mType != FOLDER + void createChildrenByFilenameMap(std::unordered_map& map); + private: std::vector mChildren; }; diff --git a/es-app/src/guis/GuiSlideshowScreensaverOptions.h b/es-app/src/guis/GuiSlideshowScreensaverOptions.h index 4232a69bad..ec46ee4f0c 100644 --- a/es-app/src/guis/GuiSlideshowScreensaverOptions.h +++ b/es-app/src/guis/GuiSlideshowScreensaverOptions.h @@ -11,6 +11,7 @@ class GuiSlideshowScreensaverOptions : public GuiScreensaverOptions virtual ~GuiSlideshowScreensaverOptions(); private: + void addEditableTextComponent(ComponentListRow row, const std::string label, std::shared_ptr ed, std::string value); void addWithLabel(ComponentListRow row, const std::string label, std::shared_ptr component); }; diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 6f94251d85..3bc7844c36 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -153,7 +153,7 @@ FileData* GridGameListView::getCursor() void GridGameListView::setCursor(FileData* file) { - if(!mGrid.setCursor(file)) + if(!mGrid.setCursor(file) && file->getParent() != nullptr) { populateList(file->getParent()->getChildrenListToDisplay()); mGrid.setCursor(file); diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index d20cde7d40..be6772824c 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -103,8 +103,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) } else { - // it's a folder - if(folder->getChildren().size() > 0) + // it's a folder ? ( or PLACEHOLDER... ) + if(folder != nullptr && folder->getChildren().size() > 0) { mCursorStack.push(cursor); populateList(folder->getChildrenListToDisplay()); From 473440e6ef39ed470983dc3afe17d7b986e3f017 Mon Sep 17 00:00:00 2001 From: Anubis-13 <51127283+Anubis-13@users.noreply.github.com> Date: Sat, 29 Jun 2019 15:37:48 +0200 Subject: [PATCH 047/336] Create emulationstation2.po -ES Translation to Spanish of ES --- resources/es/emulationstation2.po | 1942 +++++++++++++++++++++++++++++ 1 file changed, 1942 insertions(+) create mode 100644 resources/es/emulationstation2.po diff --git a/resources/es/emulationstation2.po b/resources/es/emulationstation2.po new file mode 100644 index 0000000000..7d9bda7847 --- /dev/null +++ b/resources/es/emulationstation2.po @@ -0,0 +1,1942 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: recalbox-emulationstation\n" +"Language: es\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: +msgid "Loading..." +msgstr "Cargando..." + +#: +msgid "Preloading UI" +msgstr "Precargando interfaz" + +#: +msgid "GRID SIZE" +msgstr "TAMAÑO DE CUADRÃCULA" + +#: +msgid "DEFAULT GRID SIZE" +msgstr "TAMAÑO POR DEFECTO CUADRÃCULA" + +#: +msgid "UI MODE" +msgstr "MODO INTERFAZ" + +#: +msgid "PRELOAD UI" +msgstr "PRECARGA INTERFAZ" + +#: +msgid "VSYNC" +msgstr "SINCRONIZACIÓN VERTICAL" + +#: +msgid "DISPLAY FAVORITES FIRST IN GAMELIST" +msgstr "MOSTRAR JUEGOS FAVORITOS PRIMERO" + +#: +msgid "OPTIMIZE IMAGES VRAM USE" +msgstr "OPTIMIZAR USO VRMAM CON IMÃGENES" + +#: +msgid "Full" +msgstr "CompletO" + +#: +msgid "Kiosk" +msgstr "Kiosco" + +#: +msgid "Kid" +msgstr "NIÑO" + +#: +msgstr "GAME COLLECTION SETTINGS" +msgstr "LISTAS PERSONALIZADAS" + +#: +msgstr "CANCEL" +msgstr "CANCELAR" + +#: +msgid "automatic" +msgstr "automático" + +#: +msgid "basic" +msgstr "básico" + +#: +msgid "detailed" +msgstr "detallado" + +#: +msgid "grid" +msgstr "cuadrícula" + +#: +msgid "fade" +msgstr "desvanecerse" + +#: +msgid "instant" +msgstr "instantáneo" + +#: +msgid "slide" +msgstr "animado" + +#: +msgid "gridex" +msgstr "cuadrícula detallada" + +#: +msgid "ENABLE NAVIGATION SOUNDS" +msgstr "ACTIVAR SONIDOS DE NAVEGACIÓN" + +#: +msgid "ENABLE VIDEO AUDIO" +msgstr "ACTIVAR AUDIO DE VIDEO" + +#: +msgid "APPLY FILTER" +msgstr "FILTRAR LOS JUEGOS" + +#: +msgid "AUTOMATIC GAME COLLECTIONS" +msgstr "COLECCIONES AUTOMÃTICA" + +#: +msgid "SELECT COLLECTIONS" +msgstr "SELECCIONAR COLECCIONES" + +#: +msgid "CUSTOM GAME COLLECTIONS" +msgstr "COLECCIONES PERSONALIZADAS" + +#: +msgid "CREATE NEW CUSTOM COLLECTION FROM THEME" +msgstr "CREAR UNA COLECCIÓN DESDE SISTEMAS" + +#: +msgid "GAME COLLECTION SETTINGS" +msgstr "OPCIONES LISTAS DE JUEGO" + +#: +msgid "SELECT THEME FOLDER" +msgstr "SELECCIONAR CARPETA" + +#: +msgid "CREATE NEW CUSTOM COLLECTION" +msgstr "CREAR UNA COLECCIÓN PERSONALIZADA" + +#: +msgid "New Collection Name" +msgstr "nombre nueva colección" + +#: +msgid "Loading..." +msgstr "Cargando ..." + +#: +msgid "GROUP UNTHEMED CUSTOM COLLECTIONS" +msgstr "AGRUPAR COLECCIONES SIN SISTEMA" + +#: +msgid "SORT CUSTOM COLLECTIONS AND SYSTEMS" +msgstr "ORGANIZACIÓN DE COLECCIONES Y SISTEMAS" + +#: +msgid "FINISH EDITING COLLECTION" +msgstr "FINALIZAR COLECCIÓN" + +#: +msgid "PARSE GAMESLISTS ONLY" +msgstr "SOLO LISTA XML DE JUEGOS" + +#: +msgid "POWER SAVER MODES" +msgstr "MODO AHORRO DE ENERGÃA" + +#: +msgid "SAVE METADATA ON EXIT" +msgstr "GUARDAR METADATOS AL SALIR" + +#: +msgid "SHOW HIDDEN FILES" +msgstr "MOSTRAR ARCHIVOS OCULTOS" + +#: +msgid "VRAM LIMIT" +msgstr "LIMITE MAX VRAM" + +#: +msgid "RESET ALL FILTERS" +msgstr "RESTABLECER TODOS LOS FILTROS" + +#: +msgid "GAMELIST VIEW STYLE" +msgstr "MODO VISTA, LISTA DE JUEGOS" + +#: +msgid "GAME LAUNCH TRANSITION" +msgstr "TRANSICIÓN AL LANZAR JUEGO" + +#: +msgid "RANDOM" +msgstr "ALEATORIO" + +#: +msgid "Rating" +msgstr "Calificación" + +#: +msgid "Released" +msgstr "Publicado" + +#: +msgid "Developer" +msgstr "Desarrollado" + +#: +msgid "Publisher" +msgstr "Editor" + +#: +msgid "Genre" +msgstr "Género" + +#: +msgid "Players" +msgstr "Jugadores" + +#: +msgid "NO GAMES FOUND - SKIP" +msgstr "NO SE ENCUENTRAN JUEGOS - SALTAR " + +#: +msgid "RETRY" +msgstr "REINTENTAR" + +#: +msgid "SKIP" +msgstr "SALTAR" + +#: +msgid "SEARCH FOR" +msgstr "BUSCAR POR:" + +#: +msgid "SEARCH" +msgstr "BUSCAR" + +#: +msgid "SCRAPING IN PROGRESS" +msgstr "RASPADO EN CURSO" + +#: +msgid "SYSTEM" +msgstr "SISTEMA" + +#: +msgid "subtitle text" +msgstr "texto del subtítulo" + +#: +msgid "INPUT" +msgstr "ENTRADA" + +#: +msgid "search" +msgstr "búsqueda" + +#: +msgid "STOP" +msgstr "DETENER" + +#: +msgid "stop (progress saved)" +msgstr "detener (progreso guardado)" + +#: +msgid "GAME %i OF %i" +msgstr "JUEGO %i DE %i" + +#: +msgid "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" +"" +msgstr "¡NO PODEMOS ENCONTRAR SISTEMAS!\n" +""COMPRUEBE QUE SUS CAMINOS SON CORRECTOS EN EL ARCHIVO DE CONFIGURACIÓN DE SISTEMAS, Y SU DIRECTORIO DE JUEGOS TIENE AL MENOS UN JUEGO CON LA EXTENSIÓN CORRECTA.\n" +"\n" +"" + +#: +msgid "%i i ¡JUEGO RASPADO EXITOSAMENTE!" +msgid_plural "%i ¡JUEGOS EXITOSAMENTE RASPADOS!" +msgstr[0] "%i ¡JUEGO RASPADO!" +msgstr[1] "%i ¡JUEGOS RASPADOS!" + +#: +msgid "%i i ¡JUEGO SALTADO!" +msgid_plural "%i GAMES SKIPPED." +msgstr[0] "%i ¡JUEGO SALTADO!" +msgstr[1] "%i ¡JUEGOS SALTADOS!" + +#: +msgid "OK" +msgstr "OK" + +#: +msgid "EDIT METADATA" +msgstr "EDITAR LES METADATOS" + +#: +msgid "SCRAPE" +msgstr "RASPAR MEDIA" + +#: +msgid "SAVE" +msgstr "GUARDAR" + +#: +msgid "THIS WILL DELETE A FILE!\n" +"ARE YOU SURE?" +msgstr "¡ESTO BORRARà UN ARCHIVO! \n" +"¿ESTÃS SEGURO?" + +#: +msgid "YES" +msgstr "SI" + +#: +msgid "NO" +msgstr "NO" + +#: +msgid "DELETE" +msgstr "BORRAR" + +#: +msgid "SAVE CHANGES?" +msgstr "¿GUARDAR CAMBIOS?" + +#: +msgid "BACK" +msgstr "VOLVER" + +#: +msgid "CLOSE" +msgstr "CERRAR" + +#: +msgid "MAIN MENU" +msgstr "MENÚ PRINCIPAL" + +#: +msgid "KODI MEDIA CENTER" +msgstr "KODI MEDIA CENTER" + +#: +msgid "SYSTEM SETTINGS" +msgstr "OPCIONES DE SISTEMA" + +#: +msgid "VERSION" +msgstr "VERSIÓN" + +#: +msgid "DISK USAGE" +msgstr "ESPACIO EN DISCO" + +#: +msgid "STORAGE DEVICE" +msgstr "DISPOSITIVO DE ALMACENAMIENTO" + +#: +msgid "LANGUAGE" +msgstr "IDIOMA" + +#: +msgid "OVERCLOCK" +msgstr "OVERCLOCK" + +#: +msgid "EXTREM (1100Mhz)" +msgstr "EXTREMO (1100Mhz)" + +#: +msgid "TURBO (1000Mhz)" +msgstr "TURBO (1000Mhz)" + +#: +msgid "HIGH (950Mhz)" +msgstr "ALTO (950Mhz)" + +#: +msgid "NONE (700Mhz)" +msgstr "NINGUNO (900Mhz)" + +#: +msgid "TURBO (1050Mhz)+" +msgstr "TURBO (1050Mhz)+" + +#: +msgid "HIGH (1050Mhz)" +msgstr "ALTO (1050Mhz)" + +#: +msgid "NONE (900Mhz)" +msgstr "NINGUNO (900Mhz)" + +#: +msgid "NONE (1200Mhz)" +msgstr "NINGUNO (1200Mhz)" + +#: +msgid "NONE" +msgstr "NINGUNO" + +#. NEW SETTINGS ORGANIZATION +#: +msgid "UPDATES" +msgstr "ACTUALIZACIONES" + +#: +msgid "AUTO UPDATES" +msgstr "ACTUALIZACIONES AUTOMÃTICAS" + +#: +msgid "START UPDATE" +msgstr "INICIAR ACTUALIZACIÓN" + +#: +msgid "KODI SETTINGS" +msgstr "OPCIONES KODI" + +#: +msgid "ENABLE KODI" +msgstr "HABILITAR KODI" + +#: +msgid "KODI AT START" +msgstr "AUTO INICIAR KODI" + +#: +msgid "START KODI WITH X" +msgstr "COMENZAR KODI CON X" + +#: +msgid "SECURITY" +msgstr "SEGURIDAD" + +#: +msgid "ENFORCE SECURITY" +msgstr "REFORCAR LA SEGURIDAD" + +#: +msgid "ROOT PASSWORD" +msgstr "CONTRASEÑA DE ROOT" + +#: +msgid "THE SYSTEM WILL NOW REBOOT" +msgstr "EL SISTEMA AHORA REINICIARÃ" + +#: +msgid "GAMES SETTINGS" +msgstr "OPCIONES DE JUEGOS" + +#: +msgid "GAME RATIO" +msgstr "FORMATO DE JUEGO" + +#: +msgid "SMOOTH GAMES" +msgstr "JUEGOS SUAVES" + +#: +msgid "REWIND" +msgstr "RREBOBINAR" + +#: +msgid "AUTO SAVE/LOAD" +msgstr "AUTO GUARDAR/CARGAR" + +#: +msgid "SHADERS SET" +msgstr "SET DE SOMBRAS" + +#: +msgid "SCANLINES" +msgstr "SCANLINES" + +#: +msgid "RETRO" +msgstr "RETRO" + +#: +msgid "RETROACHIEVEMENTS SETTINGS" +msgstr "AJUSTES DE LOGROS" + +#: +msgid "RETROACHIEVEMENTS" +msgstr "LOGROS" + +#: +msgid "HARDCORE MODE" +msgstr "MODO HARDCORE" + +#: +msgid "USERNAME" +msgstr "NOMBRE DE USUARIO" + +#: +msgid "PASSWORD" +msgstr "CONTRASEÑA" + +#: +msgid "ADVANCED" +msgstr "AVANZADO" + +#: +msgid "REALLY UPDATE GAMES LISTS ?" +msgstr "¿SEGURO QUIERE ACTUALIZAR LAS LISTAS DE JUEGOS" + +#: +msgid "UPDATE GAMES LISTS" +msgstr "ACTUALIZAR LISTAS DE JUEGOS" + +#: +msgid "CONTROLLERS SETTINGS" +msgstr "AJUSTE CONTROLADORES" + +#: +msgid "UI SETTINGS" +msgstr "OPCIONES DE INTERFAZ" + +#: +msgid "OVERSCAN" +msgstr "OVERSCAN" + +#: +msgid "SCREENSAVER SETTINGS" +msgstr "AJUSTES SALVAPANTALLA" + +#: +msgid "SCREENSAVER AFTER" +msgstr "SALVAPANTALLA DESPUÉS DE:" + +#: +msgid "TRANSITION STYLE" +msgstr "ESTILO DE TRANSICIÓN" + +#: +msgid "SCREENSAVER BEHAVIOR" +msgstr "COMPORTAMIENTO DE PANTALLA" + +#: +msgid "SHOW FRAMERATE" +msgstr "MOSTRAR RATIO DE FRAMES" + +#: +msgid "ON-SCREEN HELP" +msgstr "AYUDA EN PANTALLA" + +#: +msgid "HIDE WHEN RUNNING GAME" +msgstr "OCULTAR AL LANZAR JUEGO" + +#: +msgid "QUICK SYSTEM SELECT" +msgstr "SELECCIÓN RAPIDA DE SISTEMA" + +#: +msgid "THEME SET" +msgstr "SISTEMA ESTABLECIDO" + +#: +msgid "SOUND SETTINGS" +msgstr "OPCIONES DE SONIDO" + +#: +msgid "SYSTEM VOLUME" +msgstr "VOLUMEN DEL SISTEMA" + +#: +msgid "FRONTEND MUSIC" +msgstr "MÚSICA DE FONDO" + +#: +msgid "OUTPUT DEVICE" +msgstr "DISPOSITIVO DE SALIDA" + +#: +msgid "HDMI" +msgstr "HDMI" + +#: +msgid "JACK" +msgstr "JACK" + +#: +msgid "AUTO" +msgstr "AUTO" + +#: +msgid "NETWORK SETTINGS" +msgstr "OPCIONES DE RED" + +#: +msgid "CONNECTED" +msgstr "CONECTAR" + +#: +msgid "NOT CONNECTED" +msgstr "NO CONECTADO" + +#: +msgid "STATUS" +msgstr "ESTADO" + +#: +msgid "IP ADDRESS" +msgstr "DIRECCIÓN IP" + +#: +msgid "HOSTNAME" +msgstr "NOMBRE HOST" + +#: +msgid "ENABLE WIFI" +msgstr "ACTIVAR WIFI" + +#: +msgid "WIFI SSID" +msgstr "SSID WIFI" + +#: +msgid "WIFI KEY" +msgstr "CLAVE WIFI" + +#: +msgid "WIFI ENABLED" +msgstr "WIFI HABILITADO" + +#: +msgid "WIFI CONFIGURATION ERROR" +msgstr "ERROR DE CONFIGURACIÓN WIFI" + +#: +msgid "SCRAPER" +msgstr "RASPADOR MEDIA" + +#: +msgid "SCRAPE FROM" +msgstr "RASPAR DESDE:" + +#: +msgid "SCRAPE RATINGS" +msgstr "CALIFICACIÓN DE RASPADO" + +#: +msgid "SCRAPE NOW" +msgstr "RASPAR AHORA"" + +#: +msgid "QUIT" +msgstr "SALIR" + +#: +msgid "REALLY RESTART?" +msgstr "¿SEGURO DE REINICIAR?" + +#: +msgid "RESTART SYSTEM" +msgstr "REINICIAR SISTEMA" + +#: +msgid "REALLY SHUTDOWN?" +msgstr "¿SEGURO DE APAGAR?" + +#: +msgid "SHUTDOWN SYSTEM" +msgstr "SISTEMA DE APAGADO" + +#: +msgid "Emulator" +msgstr "Emulador" + +#: +msgid "Core" +msgstr "Núcleo" + +#: +msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." +msgstr "USTED VA A CONFIGURAR UN CONTROLADOR, SI TIENE ÚNICAMENTE UN JOYSTICK O UNA CRUCETA DE CONTROL, CONFIGURE SÓLO LAS INSTRUCCIONES QUE TENGA EN SU CONTROLADOR O MANDO. SI NO TIENE UN BOTÓN ESPECIAL PARA HOTKEY, USE SELECT E IGNORE LOS BOTONES QUE NO HAYA EN SU CONTROL. NO PUEDE USAR OTRA VEZ UN BOTÓN YA CONFIGURADO. LOS NOMBRES DE LOS BOTONES ESTÃN BASADOS EN EL MANDO DE SUPER NINTENDO." + +#. GUIMENU +#: +msgid "CONFIGURE A CONTROLLER" +msgstr "CONFIGURAR UN CONTROLADOR" + +#. Bluetooth +#: +msgid "CONTROLLER PAIRED" +msgstr "CONTROLADOR EMPAREJADO" + +#: +msgid "UNABLE TO PAIR CONTROLLER" +msgstr "NO SE PUEDE EMPAREJAR EL CONTROLADOR + +#: +msgid "AN ERROR OCCURED" +msgstr "OCURRIÓ UN ERROR" + +#: +msgid "NO CONTROLLERS FOUND" +msgstr "NO ENCUENTRA CONTROLADOR" + +#: +msgid "PAIR A BLUETOOTH CONTROLLER" +msgstr "EMPAREJAR CONTROLADOR BLUETOOTH" + +#: +msgid "CONTROLLERS LINKS HAVE BEEN DELETED." +msgstr "EL EMPAREJAMIENTO DE CONTROLADORES HA SIDO BORRADO." + +#: +msgid "FORGET BLUETOOTH CONTROLLERS" +msgstr "OLVIDAR CONTROLADORES BLUETOOTH" + +#: +msgid "INPUT P%i" +msgstr "ENTRADA %i" + +#: +msgid "CHOOSE" +msgstr "ELEGIR" + +#: +msgid "SELECT" +msgstr "SELECCIONARR" + +#: +msgid "OPTIONS" +msgstr "OPCIONES" + +#: +msgid "JUMP TO LETTER" +msgstr "IR A LA LETRA O NÚMERO" + +#: +msgid "SORT GAMES BY" +msgstr "ORDENAR JUEGOS POR:" + +#. FAVORITES +#: +msgid "FAVORITES ONLY" +msgstr "SOLO FAVORITOS" + +#: +msgid "EDIT THIS GAME'S METADATA" +msgstr "EDITAR METADATOS DE ESTE JUEGO" + +#: +msgid "SCRAPE THESE GAMES" +msgstr "RASPAR ESTOS JUEGOS" + +#: +msgid "All Games" +msgstr "Todos los juegos" + +#. MISSING SCRAPPER TRANSLATIONS +#: +msgid "Only missing image" +msgstr "Sólo falta imagen" + +#: +msgid "FILTER" +msgstr "FILTRO" + +#: +msgid "SCRAPE THESE SYSTEMS" +msgstr "RASPAR ESTOS SISTEMAS" + +#: +msgid "SYSTEMS" +msgstr "SISTEMAS" + +#: +msgid "USER DECIDES ON CONFLICTS" +msgstr "ELIGIR SI HAY CONFLICTO" + +#: +msgid "START" +msgstr "INICIAR" + +#: +msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" +"CONTINUE ANYWAY?" +msgstr "ADVERTENCIA: ALGUNOS DE SUS SISTEMAS SELECCIONADOS NO TIENEN UN CONJUNTO DE SISTEMA.!LOS RESULTADOS PUEDEN SER ERRONEOS!\n" +"¿CONTINUAR IGUALMENTE?" + +#: +msgid "NO GAMES FIT THAT CRITERIA." +msgstr "SIN JUEGOS CON ESOS CRITERIOS." + +#: +msgid "REALLY UPDATE?" +msgstr "¿ACTUALIZAR EL SISTEMA?" + +#: +msgid "NETWORK CONNECTION NEEDED" +msgstr "SE NECESITA CONEXIÓN DE RED" + +#: +msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" +msgstr "ACTUALIZACIÓN DESCARGADA, EL SISTEMA AHORA REINICIARÃ" + +#: +msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" +msgstr "FALLÓ ACTUALIZAR, EL SISTEMA AHORA REINICIARÃ" + +#: +msgid "NO UPDATE AVAILABLE" +msgstr "NO HAY ACTUALIZACIÓN DISPONIBLE" + +#: +msgid "enter emulator" +msgstr "entrar en el emulador" + +#: +msgid "enter core" +msgstr "entrar en el núcleo" + +#: +msgid "Ratio" +msgstr "Ratio" + +#: +msgid "enter ratio" +msgstr "ingresar ratio" + +#: +msgid "Name" +msgstr "Nombre" + +#: +msgid "enter game name" +msgstr "entrer le nom du jeu" + +#: +msgid "Description" +msgstr "Descripción" + +#: +msgid "enter description" +msgstr "introducir descripción" + +#: +msgid "Image" +msgstr "Imagen" + +#: +msgid "enter path to image" +msgstr "ingresar ruta de la imagen" + +#: +msgid "Thumbnail" +msgstr "Miniatura" + +#: +msgid "enter path to thumbnail" +msgstr "ingresar ruta de la miniatura" + +#: +msgid "enter rating" +msgstr "ingresar calificación" + +#: +msgid "Release date" +msgstr "Fecha de lanzamiento" + +#: +msgid "enter release date" +msgstr "ingresar fecha de lanzamiento" + +#: +msgid "enter game developer" +msgstr "ingresar desarrollador de juegos" + +#: +msgid "enter game publisher" +msgstr "ingresar el editor del juego" + +#: +msgid "enter game genre" +msgstr "ingresar al género de juego" + +#: +msgid "enter number of players" +msgstr "ingresar el número de jugadores" + +#: +msgid "Favorite" +msgstr "Favorito" + +#: +msgid "añadir a favorito" +msgstr "entrer le favori" + +#: +msgid "Region" +msgstr "Región" + +#: +msgid "enter region" +msgstr "ingresar región" + +#: +msgid "Romtype" +msgstr "Tipo de rom" + +#: +msgid "enter romtype" +msgstr "ingresar tipo de rom" + +#: +msgid "Hidden" +msgstr "Oculto" + +#: +msgid "HIDDEN" +msgstr "OCULTO" + +#: +msgid "NAME" +msgstr "NOMBRE" + +#: +msgid "EMULATOR" +msgstr "EMULADORR" + +#: +msgid "FAVORITE" +msgstr "FAVORITO" + +#: +msgid "set hidden" +msgstr "conjunto oculto" + +#: +msgid "Play count" +msgstr "Contador de juego" + +#: +msgid "enter number of times played" +msgstr "ingresar el número de veces jugado" + +#: +msgid "Last played" +msgstr "Último jugado" + +#: +msgid "enter last played date" +msgstr "ingresar la última fecha de juego" + +#: +msgid "GAMES AVAILABLE" +msgstr "JUEGOS DISPONIBLES" + +#: +msgid "%i FAVORITE" +msgid_plural "%i FAVORITES" +msgstr[0] "%i FAVORITO" +msgstr[1] "%i FAVORITOS" + +#: +msgid "SCROLL" +msgstr "SCROLLER" + +#: +msgid "LAUNCH" +msgstr "LANZADOR" + +#: +msgid "Times played" +msgstr "tiempo jugado" + +#: +msgid "MENU" +msgstr "MENÚ" + +#: +msgid "FILENAME, ASCENDING" +msgstr "NOMBRE ARCHIVO, ASCENDENTE" + +#: +msgid "FILENAME, DESCENDING" +msgstr "NOMBRE ARCHIVO, DESCENDENTE" + +#: +msgid "RATING, ASCENDING" +msgstr "CALIFICACIÓN, ASCENDENTE" + +#: +msgid "RATING, DESCENDING" +msgstr "CALIFICACIÓN, DESCENDENTE" + +#: +msgid "TIMES PLAYED, ASCENDING" +msgstr "TIEMPO JUGADO, ASCENDENTE" + +#: +msgid "TIMES PLAYED, DESCENDING" +msgstr "TIEMPO JUGADO, DESCENDENTE" + +#: +msgid "LAST PLAYED, ASCENDING" +msgstr "ÚLTIMO JUEGO, ASCENDENTE" + +#: +msgid "LAST PLAYED, DESCENDING" +msgstr "ÚLTIMO JUEGO, DESCENDENTE" + +#: +msgid "WORKING..." +msgstr "EN CURSO ..." + +#: +msgid "CHANGE" +msgstr "CAMBIAR" + +#: +msgid "never" +msgstr "nunca" + +#: +msgid "just now" +msgstr "ahora" + +#: +msgid "%i sec ago" +msgid_plural "%i secs ago" +msgstr[0] "hace %i s" +msgstr[1] "hace %i s" + +#: +msgid "%i min ago" +msgid_plural "%i mins ago" +msgstr[0] "hace %i min" +msgstr[1] "hace %i min" + +#: +msgid "%i hour ago" +msgid_plural "%i hours ago" +msgstr[0] "hace %i h" +msgstr[1] "hace %i h" + +#: +msgid "%i day ago" +msgid_plural "%i days ago" +msgstr[0] "hace %i día" +msgstr[1] "hace %i días" + +#: +msgid "unknown" +msgstr "desconocido" + +#: +msgid "SELECT ALL" +msgstr "SELECCIONAR TODO" + +#: +msgid "SELECT NONE" +msgstr "NINGUNA SELECCIÓN" + +#: +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i SELECCIONADO" +msgstr[1] "%i SELECCIONADOS" + +#: +msgid "UP" +msgstr "ARRIBA" + +#: +msgid "DOWN" +msgstr "ABAJO" + +#: +msgid "LEFT" +msgstr "IZQUIERDA" + +#: +msgid "RIGHT" +msgstr "DERECHA" + +#: +msgid "JOYSTICK 1 UP" +msgstr "JOYSTICK 1 ARRIBA" + +#: +msgid "JOYSTICK 1 LEFT" +msgstr "JOYSTICK 1 IZQUIERDA" + +#: +msgid "JOYSTICK 2 UP" +msgstr "JOYSTICK 2 ARRIBA" + +#: +msgid "JOYSTICK 2 LEFT" +msgstr "JOYSTICK 2 IZQUIERDA" + +#: +msgid "PAGE UP" +msgstr "L1 (PÃGINA SIGUIENTE)" + +#: +msgid "PAGE DOWN" +msgstr "R1 (PÃGINA ANTERIOR)" + +#: +msgid "HOTKEY" +msgstr "HOTKEY" + +#: +msgid "CONFIGURING" +msgstr "CONFIGURACIÓN" + +#: +msgid "KEYBOARD" +msgstr "TECLADOR" + +#: +msgid "GAMEPAD %i" +msgstr "JOYSTICK %i" + +#. Config controllers missing translation +#: +msgid "PRESS ANYTHING" +msgstr "PRESIONE CUALQUIER BOTÓN" + +#: +msgid "ALREADY TAKEN" +msgstr "YA ESTà EN USO" + +#: +msgid "DISCARD CHANGES" +msgstr "DESECHAR LOS CAMBIOS" + +#: +msgid "WELCOME" +msgstr "BIENVENIDO" + +#: +msgid "CONFIGURE INPUT" +msgstr "CONFIGURAR CONTROLADOR" + +#: +msgid "%i GAMEPAD DETECTED" +msgid_plural "%i GAMEPADS DETECTED" +msgstr[0] "%i CONTROLADOR DETECTADO" +msgstr[1] "%i CONTROLADORES DETECTADOS" + +#: +msgid "NO GAMEPADS DETECTED" +msgstr "NINGÚN CONTROLADOR DETECTADO" + +#: +msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." +msgstr "MANTENGA UN BOTÓN PULSADO PARA CONFIGURAR CONTROL" + +#: +msgid "PRESS F4 TO QUIT AT ANY TIME." +msgstr "PULSE F4 PARA SALIR EN CUALQUIER MOMENTO" + +#: +msgid "PRESS ESC OR THE HOTKEY TO CANCEL." +msgstr "PULSE ESC O BOTÓN HOTKEY PARA CANCELAR" + +#: +msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" +msgstr "¿ QUIERES INICIAR KODI MEDIA CENTER ?" + +#: +msgid "LOADING..." +msgstr "CARGANDO ..." + +#: +msgid "PLEASE WAIT..." +msgstr "POR FAVOR ESPERE ..." + +#: +msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" +msgstr "¿ REALMENTE QUIERE APAGAR SIN GUARDAR METADATOS ?" + +#: +msgid "FAST SHUTDOWN SYSTEM" +msgstr "SISTEMA DE APAGADO RÃPIDO" + +#: +msgid "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." +msgstr "¡NO PODEMOS ENCONTRAR SISTEMAS!\n" +"COMPRUEBE DIRECTORIO SON CORRECTOS EN EL ARCHIVO DE CONFIGURACIÓN DE SISTEMAS, Y SU DIRECTORIO DE JUEGOS TIENE MÃNIMO UN JUEGO CON EXTENSIÓN CORRECTA.\n" +"VISITA EMULATIONSTATION.ORG PARA MÃS INFORMACIÓN." + +#: +msgid "ON SCREEN KEYBOARD" +msgstr "TECLADO DE PANTALLA" + +#: +msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" +msgstr "PULSE SHIFT PARA MAYÚSCULAS" + +#: +msgid "SPACE" +msgstr "ESPACIO" + +#: +msgid "DELETE A CHAR" +msgstr "BORRAR UN CARACTER" + +#: +msgid "SHIFT" +msgstr "MAYÚSCULAS" + +#: +msgid "STOP EDITING" +msgstr "DEJAR DE EDITAR" + +#: +msgid "MOVE CURSOR" +msgstr "MOVER EL CURSOR" + +#: +msgid "EDIT" +msgstr "EDITAR" + +#: +msgid "ACCEPT RESULT" +msgstr "ACEPTAR EL RESULTADO" + +#: +msgid "FILENAME" +msgstr "ARCHIVO" + +#: +msgid "RATING" +msgstr "VOTOS" + +#: +msgid "TIEMPO JUGADO" +msgstr "PARTIES JOUÉES" + +#: +msgid "LAST PLAYED" +msgstr "ÚLTIMO JUGADO" + +#: +msgid "NUMBER OF PLAYERS" +msgstr "NÚMERO DE JUGADORES" + +#: +msgid "DEVELOPER" +msgstr "DESARROLLADORA" + +#: +msgid "GENRE" +msgstr "GÉNERO" + +#: +msgid "SHOW HIDDEN" +msgstr "MOSTRAR OCULTADO" + +#: +msgid "EXTREM (1400Mhz)" +msgstr "EXTREMO (1400MHz)" + +#: +msgid "TURBO (1350Mhz)" +msgstr "TURBO (1350MHz)" + +#: +msgid "HIGH (1300Mhz)" +msgstr "ALTO (1300MHz)" + +#: +msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" +"IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." +msgstr "LOS CAMBIOS A TURBO Y EXTREMO PUEDEN CAUSAR INESTABILIDAD DEL SISTEMA POR SOBRECARGA, ASà QUE UTILIZALOS BAJO SU PROPIO RIESGO.\n" +"SI CONTINUAS, EL SISTEMA REINICIARà AHORA." + +#: +msgid "%i GAME HIDDEN" +msgid_plural "%i GAMES HIDDEN" +msgstr[0] "%i JUEGO OCULTO" +msgstr[1] "%i JUEGOS OCULTOS" + +#: +msgid "Start kodi media player." +msgstr "Iniciar el reproductor multimedia kodi" + +#: +msgid "Select the language for your recalbox, select an external drive to store your games and configurations, check your current version and the free space on your drive" +msgstr "Seleccione el idioma para su ES, seleccione una unidad externa para almacenar sus juegos y configuraciones, verifique su versión actual y el espacio libre en su unidad" + +#: +msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." +msgstr "Configurar la pantalla de juegos, la proporción, los filtros (sombreados), guardar automáticamente y cargar la cuenta de retroachievement." + +#: +msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." +msgstr "La proporción del juego es la proporción entre el ancho de la imagen y la altura de la imagen. Use AUTO para que el emulador elija la proporción del juego original, que le dará la mejor experiencia de retrogaming." + +#: +msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." +msgstr "Suavizar la imagen del juego. Esta opción hace que la imagen sea más suave, usando un filtro bilineal." + +#: +msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." +msgstr "Esta opción le permite rebobinar el juego si un monstruo lo mata, o si comete algún otro error. Use el comando HOTKEY + IZQUIERDA dentro del juego para rebobinar." + +#: +msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." +msgstr "Guardar automáticamente el estado al salir de un juego, y cargar automáticamente el último estado guardado al iniciar un juego." + +#: +msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." +msgstr "La escala de enteros es escalar por un factor de un número entero, como 2x, 3x, 4x, etc. Esta opción escala la imagen a la escala de enteros más grande por debajo de la resolución establecida. Por ejemplo, si configura la resolución de pantalla completa "Para 1920x1080 y habilitar el escalado de enteros, solo escalará una imagen de 320x240 hasta 1280x960, y dejará bordes negros alrededor. Esto es para mantener una proporción de píxeles de 1: 1 con la imagen de origen original, para que los píxeles no se dupliquen de manera desigual." + +#: +msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." +msgstr "Los sombreadores son como filtros para la representación del juego. Puede seleccionar un conjunto de sombreadores aquí,hay una colección de sombreadores para seleccionar en cada sistema. También puede cambiar el sombreador dentro del juego con HOTKEY + L2 o HOTKEY + R2" + +#: +msgid "Enable or disable RetroAchievements in games." +msgstr "Habilitar o deshabilitar RetroAchievements en juegos." + +#: +msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." +msgstr "El modo Hardcore desactiva * todas las funciones * savestate y rebobinar dentro del emulador: no podrá guardar ni recargar en ningún momento. Deberá completar el juego y obtener los logros por primera vez, al igual que en la consola original. ¡En recompensa por esto, obtendrás el logro estándar y el logro incondicional, en efecto obteniendo el doble de puntos! ¡Un juego normal que vale 400 puntos vale ahora 800 si lo completas en hardcore!\n" +"Por ejemplo: si completas el juego por 400 puntos, entonces tienes la oportunidad de ganar otros 400 en el hardcore." + +#: +msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" +msgstr "¡El sitio web retroachievements.org propone desafíos / logros / trofeos en plataformas como NES, SNES, GB, GBC, GBA, Genesis / Megadrive, TurboGrafx16 / PCEngine y más! ¡Crea tu cuenta en retroachievements.org y comienza tu búsqueda de logros! " + +#: +msgid "Add and configure up to 5 controllers." +msgstr "Agregar y configurar hasta 5 controladores." + +#: +msgid "Start the screensaver after N minutes." +msgstr "Iniciar el protector de pantalla después de N minutos." + +#: +msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." +msgstr "Establece el comportamiento del protector de pantalla. DIM reducirá la luz de la pantalla, y NEGRO volverá la pantalla a negro." + +#: +msgid "Shows a help at the bottom of the screen which displays commands you can use." +msgstr "Muestra una ayuda en la parte inferior de la pantalla que muestra los comandos que puede usar." + +#: +msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." +msgstr "Cuando está habilitado, puede cambiar entre sistemas mientras navega por una lista de juegos presionando IZQUIERDA o DERECHA." + +#: +msgid "Updates the gamelists, if you added games since the last boot." +msgstr "Actualiza las listas de juego, si agregó juegos desde el último arranque." + +#: +msgid "Set the volume of the sound output for the frontend and the games." +msgstr "Establecer el volumen de la salida de sonido para la interfaz y los juegos." + +#: +msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." +msgstr "Obtenga información e imágenes para sus juegos. El raspador descarga metadatos e imágenes para sus juegos desde diferentes servidores mejorando la experiencia del usuario en EmulationStation por completo." + +#: +msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." +msgstr "Seleccione un servidor para raspar. Se recomienda el servidor SCREENSCRAPER y se basa en www.screenscraper.fr y raspa los datos del juego en su idioma, si están disponibles." + +#: +msgid "Begin the scrape process with the configuration shown below." +msgstr "Comience el proceso de raspado con la configuración que se muestra a continuación." + +#: +msgid "Scrape and display game ratings." +msgstr "Raspar y mostrar clasificaciones de juegos." + +#: +msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." +msgstr "Configuración avanzada. Asegúrese de que realmente sabe lo que está haciendo, antes de cambiar cualquier valor en este menú." + +#: +msgid "Overclock your board to increase the performance.\n" +"Overclock settings are tested and validated by the community. Keep in mind that overclocking your board can void your warranty." +msgstr "Overclockee su tablero para aumentar el rendimiento.\ n" +"La comunidad ha probado y validado las configuraciones de overclock.\ n" +"Tenga en cuenta que el overclocking de su placa puede anular su garantía." + +#: +msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" +"This option highly speeds up boot time, but new games will not be detected." +msgstr "Solo muestra los juegos contenidos en el archivo gamelist.xml (ubicado en sus directorios roms).\n" +"Esta opción acelera mucho el tiempo de arranque, pero no se detectarán nuevos juegos." + +#: +msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." +msgstr "Esta opción le permite configurar el sistema seleccionado en modo fijo.\n" +"Con esta opción activada, el usuario no puede acceder a otros sistemas." + +#: +msgid "Always display the basic gamelist view, even if you have scraped your games." +msgstr "Mostrar siempre la vista básica de la lista de juegos, incluso si ha raspado sus juegos." + +#: +msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" +msgstr "Habilitar o deshabilitar Kodi, personalizar el inicio de Kodi, habilitar el botón X para iniciar Kodi." + +#: +msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." +msgstr "Habilitar o deshabilitar Kodi. Si Kodi está deshabilitado, no podrá iniciarlo con el botón X, o iniciarlo automáticamente en el arranque. La entrada del menú también se eliminará." + +#: +msgid "Use the X button to start Kodi." +msgstr "Usa el botón X para iniciar Kodi." + +#: +msgid "Automatically start into Kodi on boot." +msgstr "Iniciar automáticamente Kodi en el arranque." + +#: +msgid "Show the framerate in EmulationStation and in game." +msgstr "Mostrar tasa de cuadros en EmulationStation en el juego." + +#: +msgid "Select which emulator to use when you start a game for this system." +msgstr "Seleccione emulador que inicie juego para este sistema." + +#: +msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." +msgstr "Seleccione qué núcleo usar para el emulador seleccionado. Por ejemplo, el emulador LIBRETRO tiene muchos núcleos para ejecutar juegos de Super Nintendo. El núcleo predeterminado que elija aquí también se puede anular en la configuración específica del juego." + +#: +msgid "USE COMPOSED VISUALS" +msgstr "UTILIZAR VISUALES COMPUESTOS" + +#: +msgid "CHECK UPDATES" +msgstr "REVISAR ACTUALIZACIONES" + +#: +msgid "UPDATE TYPE" +msgstr "TIPO DE ACTUALIZACIÓN" + +#: +msgid "INTEGER SCALE (PIXEL PERFECT)" +msgstr "ESCALA DE ENTEROS (PIXEL PERFECTO)" + +#: +msgid "ADVANCED SETTINGS" +msgstr "AJUSTES AVANZADOS" + +#: +msgid "BOOT SETTINGS" +msgstr "PARÃMETROS DE INICIO" + +#: +msgid "GAMELIST ONLY" +msgstr "SÓLO LISTA JUEGO" + +#: +msgid "START ON SYSTEM" +msgstr "COMENZAR EN EL SISTEMA" + +#: +msgid "BOOT ON GAMELIST" +msgstr "EMPEZAR EN LA LISTA DE JUEGOS" + +#: +msgid "HIDE SYSTEM VIEW" +msgstr "OCULTAR VISTA DE SISTEMAS" + +#: +msgid "EMULATOR ADVANCED CONFIGURATION" +msgstr "CONFIGURACIÓN AVANZADA EMULADOR" + +#: +msgid "ADVANCED EMULATOR CONFIGURATION" +msgstr "CONFIGURACIÓN AVANZADA EMULADOR" + +#: +msgid "HELP" +msgstr "AYUDA" + +#: +msgid "THE SYSTEM IS UP TO DATE" +msgstr "EL SISTEMA ESTA ACTUALIZADO" + +#: +msgid "FORCE BASIC GAMELIST VIEW" +msgstr "FORZAR LA VISTA BÃSICA DE JUEGO " + +#: +msgid "DOWNLOADED" +msgstr "DESCARGADO" + +#: +msgid "UPDATE VERSION:" +msgstr "VERSIÓN ACTUALIZADA:" + +#: +msgid "UPDATE CHANGELOG:" +msgstr "ACTUALIZAR CAMBIO:" + +#: +msgid "MORE DETAILS" +msgstr "MÃS DETALLES" + +#: +msgid "CAROUSEL TRANSITIONS" +msgstr "TRANSICIONES DE CARRUSEL" + +#: +msgid "ENABLE FILTERS" +msgstr "ACTIVAR FILTROS" + +#: +msgid "THEME CONFIGURATION" +msgstr "CONFIGURACIÓN DE TEMA" + +#: +msgid "THEME COLORSET" +msgstr "COLORSET TEMA" + +#: +msgid "THEME ICONSET" +msgstr "ICONSET TEMA" + +#: +msgid "THEME MENU" +msgstr "MENÚ TEMA" + +#: +msgid "THEME SYSTEMVIEW" +msgstr "TEMA DE SISTEMA" + +#: +msgid "THEME GAMELISTVIEW" +msgstr "TEMA DE LISTA DE JUEGOS" + +#: +msgid "THEME REGION" +msgstr "REGIÓN DE TEMA" + +#: +msgid "THIS THEME HAS NO OPTION" +msgstr "ESTE TEMA NO TIENE OPCIÓNES" + +#: +msgid "MANUAL INPUT" +msgstr "ENTRADA MANUAL" + +#: +msgid "AN ERROR OCCURED - DOWNLOADED" +msgstr "HA OCURRIDO UN ERROR - DESCARGADO" + +#: +msgid "START KODI" +msgstr "COMENZAR KODI" + +#: +msgid "Muestra la actual versión de actualización disponible." +msgstr "Montre les versions disponibles." + +#: +msgid "Shows the current available update changelog." +msgstr "Montre le changelog de la mise à jour disponible." + +#: +msgid "Configurar un controlador asociado. Su controlador debe estar asociado / enchufado antes." +msgstr "Configurer une manette appairée. Votre manette doit être appairée / branchée auparavant." + +#: +msgid "Choose if carousel will be animated or not during transitions" +msgstr "Elija si el carrusel será animado o no durante las transiciones." + +#: +msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" +msgstr "Seleccione el tipo de transición que se producirá cuando inicie un juego. INSTANTANEO no hará nada, DESVANECER se desaparecerá paulatinammente y la ANIMADO hará zoom en la cubierta del juego (o nombre si no hay información de raspado)." + +#: +msgid "Select exisiting colorset options for this theme." +msgstr "Seleccione las opciones de conjuntos de colores existentes para este tema" + +#: +msgid "Select exisiting iconset options for this theme." +msgstr "Seleccione las opciones de conjuntos de iconos existentes para este tema" + +#: +msgid "Select exisiting menu style options for this theme." +msgstr "Seleccione las opciones de estilo de menú existentes para este tema" + +#: +msgid "Select exisiting system view options for this theme." +msgstr "Seleccione las opciones de vista del sistema existentes para este tema" + +#: +msgid "Select exisiting gamelist view options for this theme." +msgstr "Seleccione las opciones de vista de lista de juegos existentes para este tema" + +#: +msgid "Configure theme options if available." +msgstr "Configurar opciones de tema si están disponibles" + +#: +msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" +msgstr "Seleccionar región de logotipos, imágenes para sistemas que son diferentes para algunos países. Por ejemplo, Megadrive en EU / Genesis en EE. UU" + +#: +msgid "Type the name of your SSID if it is hidden or not listed" +msgstr "Escriba el nombre de su SSID si está oculto o no aparece en la lista" + +#: +msgid "Select a letter and the listing will go directly on the first game starting with this letter." +msgstr "Seleccione una letra y el listado irá directamente en el primer juego que comience con esta letra." + +#: +msgid "Select the way the game list is sortered (alphabetically, by notation...)." +msgstr "Seleccione la forma en que se clasifica la lista de juegos (alfabéticamente, por notación ...)." + +#: +msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." +msgstr "Cambia entre ver o no solo los juegos favoritos. Para agregar un juego en la lista de favoritos, selecciona el juego y cambia su estado usando 'Y'." + +#: +msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." +msgstr "Cambia entre ver o no los juegos ocultos. Para ocultar un juego, edita sus datos y selecciona" Ocultar." + +#: +msgid "This option display a menu which allows to change game data and many others options." +msgstr "Esta opción muestra un menú que permite cambiar los datos del juego y muchas otras opciones." + +#: +msgid "AVAILABLE UPDATE" +msgstr "ACTUALIZACION DISPONIBLE" + +#: +msgid "UPDATE CHANGELOG" +msgstr "ACTUALIZAR CAMBIOs" + +#: +msgid "CLOCK IN MENU" +msgstr "RELOJ EN EL MENÚ" + +#: +msgid "Now playing" +msgstr "Juego en curso" + +#: +msgid "DEFAULT (%1%)" +msgstr "POR DEFECTO (%1%)" + +#: +msgid "INPUT REQUIRED" +msgstr "SE REQUIERE ENTRADA" + +#: +msgid "(skipped)" +msgstr "(saltado)" + +#: +msgid "UP/DOWN TO SKIP" +msgstr "ARRIBA / ABAJO PARA SALTAR" + +#: +msgid "A TO UNSET" +msgstr "PARA CANCELAR" + +#: +msgid "DOWN TO SKIP AND KEEP [%1%]" +msgstr "ABAJO PARA SALTAR Y MANTENER [%1%]" + +#: +msgid "UP/DOWN TO SKIP AND KEEP [%1%]" +msgstr "ARRIBA / ABAJO PARA SALTAR Y MANTENER [%1%]" + +#: +msgid "Set duration of help popups, 0 means no popup." +msgstr "Establecer duración de ventanas emergentes de ayuda, 0 significa que no hay ventanas emergentes." + +#: +msgid "HELP POPUP DURATION" +msgstr "DURACIÓN VENTANA EMERGENTE DE AYUDA" + +#: +msgid "Set duration of music popups, 0 means no popup." +msgstr "Establecer duración de ventanas emergentes de música, 0 significa que no hay ventanas emergentes." + +#: +msgid "MUSIC POPUP DURATION" +msgstr "DURACIÓN VENTANA EMERGENTE DE MÚSICA" + +#: +msgid "POPUP SETTINGS" +msgstr "AJUSTES DE VENTANA EMERGENTES" + +#: +msgid "POPUP POSITION" +msgstr "POSICIÓN DE VENTANA EMERGENTES" + +#: +msgid "Select the position of popups on screen." +msgstr "Seleccione la posición de las ventanas emergentes en la pantalla" + +#: +msgid "Set position and duration of popups." +msgstr "Establecer posición y duración de las ventanas emergentes" + +#: +msgid "TOP/RIGHT" +msgstr "ARRIBA/DERECHA" + +#: +msgid "INFERIOR/DERECHA" +msgstr "BAS/DROITE" + +#: +msgid "BOTTOM/LEFT" +msgstr "ABAJO/IZQUIERDA" + +#: +msgid "TOP/LEFT" +msgstr "ARRIBA/IZQUIERDA" + +#: +msgid "SHOW FOLDERS CONTENT" +msgstr "MOSTRAR CONTENIDO DE CARPETAS" + +#: +msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." +msgstr "Cambia entre ver la estructura de las carpetas y ver todos los juegos en un nivel superior." + +#: +msgid "NETPLAY" +msgstr "JUEGO EN RED" + +#: +msgid "NETPLAY SETTINGS" +msgstr "OPCIONES DE JUEGO EN RED" + +#: +msgid "NETPLAY LOBBY" +msgstr "LOBBY DE JUEGO EN RED" + +#: +msgid "Enable or disable Netplay in games." +msgstr "Habilitar o deshabilitar juego en red en juegos." + +#: +msgid "PORT" +msgstr "PUERTO" + +#: +msgid "NICKNAME" +msgstr "APODO" + +#: +msgid "RELAY SERVER" +msgstr "SERVIDOR DE RETRANSMISIÓN" + +#: +msgid "Enable or disable connections throught relay servers." +msgstr "Habilitar o deshabilitar conexiones a través de servidores de retransmisión." + +#: +msgid "KODI/NETPLAY" +msgstr "KODI/JUEGO EN RED" + +#: +msgid "NO GAMES OR NO CONNECTION" +msgstr "NO HAY JUEGOS O SIN CONEXIÓN" + +#: +msgid "HASH NOW" +msgstr "MANTENER HASH" + +#: +msgid "HASH THESE SYSTEMS" +msgstr "CREAR HASH DE ESTOS SISTEMAS" + +#: +msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." +msgstr Agrega hash de roms en tus listas de juego para tener resultados más precisos en juego en red." + +#: +msgid "HASH ROMS" +msgstr "HASH DE ROMS" + +#: +msgid "Only missing hashs" +msgstr "Sólo faltan hashs" + +#: +msgid "Username" +msgstr "Nombre de usuario" + +#: +msgid "Country" +msgstr "País" + +#: +msgid "Latency" +msgstr "Latencia" + +#: +msgid "Host arch." +msgstr "Arco del host" + +#: +msgid "Core ver." +msgstr "Ver. de núcleo" + +#: +msgid "RA ver." +msgstr "Ver. de RA" + +#: +msgid "Can join" +msgstr "Jugable" + +#: +msgid "Rom and core match" +msgstr "Coincidencia de rom y núcleo" + +#: +msgid "Rom found" +msgstr "Se encontró rom" + +#: +msgid "No rom match" +msgstr "No hay coincidencia de rom" + +#: +msgid "Match" +msgstr "Coincide" + +#: +msgid "No Match" +msgstr "No hay coincidencia." + +#: +msgid "Rom file" +msgstr "Archivo rom" + +#: +msgid "Rom hash" +msgstr "Hash de la rom" + +#: +msgid "THIS COULD TAKE A WHILE, CONFIRM?" +msgstr "ESTO PODRÃA TOMAR UN TIEMPO, ¿CONFIRMAR?" + +#: +msgid "bueno" +msgstr "bon" + +#: +msgid "bad" +msgstr "malO" + +#: +msgid "medium" +msgstr "medio" + +#: +msgid "NETPLAY POPUP DURATION" +msgstr "DURACIÓN DE LA VENTANA EMER. JUEGO EN RED" + +#: +msgid "Set duration of netplay popups, 0 means no popup." +msgstr "Establecer duración de las ventanas emergentes de netplay, 0 significa que no hay ventanas emergentes." + +#: +msgid "Player" +msgstr "Jugador" + +#: +msgid "Game" +msgstr "Juego" + +#: +msgid "¡Juega en línea en juegos que se ejecutan en Retroarch como NES, SNES, FBA, Genesis / Megadrive y más!" +msgstr "Jouez en ligne à des jeux tournant sous Retroarch comme NES, SNES, FBA, Megadrive et bien plus!" + +#: +msgid "Rom, hash and core match" +msgstr "Rom, hash y coincidencia de núcleo" + +#: +msgid "No core match" +msgstr "No hay coincidencia de núcleo" + +#: +msgid "Add a clock in the main menu." +msgstr "Agregar un reloj en el menú principal" + +#: +msgid "UPGRADING" +msgstr "ACTUALIZANDO" + +#: +msgid "PREPARING" +msgstr "PREPARANDO" + +#: +msgid "Starting UI" +msgstr "Inicialización interfaz" + +#: +msgid "VERIFYING" +msgstr "VERIFICANDO" + +#: +msgid "EMPTY LIST" +msgstr "LISTA VACÃA" + + +#: Retroarch ratio +msgid "Auto" +msgstr "Automático" + +msgid "Square pixel" +msgstr "Píxel cuadrado" + +msgid "Square pixel" +msgstr "Pixel carré" + +msgid "Retroarch Config" +msgstr "Retroarch Configuración'" + +msgid "Retroarch Custom" +msgstr "Retroarch Customización'" + +msgid "Core provided" +msgstr "Núcleo proporcionado" + +msgid "Do not set" +msgstr "No establecer" From 36d0ad1864e06183bfbb86fc50d784491dca3c18 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 29 Jun 2019 16:22:21 +0200 Subject: [PATCH 048/336] bad location of es emulationstation2.po --- resources/{ => locale}/es/emulationstation2.po | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename resources/{ => locale}/es/emulationstation2.po (100%) diff --git a/resources/es/emulationstation2.po b/resources/locale/es/emulationstation2.po similarity index 100% rename from resources/es/emulationstation2.po rename to resources/locale/es/emulationstation2.po From 495d9251ac66179e656e7edbe4e9377bf1edb741 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 29 Jun 2019 17:02:31 +0200 Subject: [PATCH 049/336] BugFixes : The image background is not visibl eafter being unloaded even if loaded. Don't use global grid size if a different custom grid is used in a system. --- es-app/src/views/ViewController.cpp | 4 + es-core/src/Window.cpp | 3 - es-core/src/components/ImageComponent.cpp | 129 +++++++++++----------- 3 files changed, 70 insertions(+), 66 deletions(-) diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 598b2a9476..451ea55a07 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -309,6 +309,10 @@ std::shared_ptr ViewController::getGameListView(SystemData* syste std::string customThemeName; Vector2f gridSizeOverride = Vector2f::parseString(Settings::getInstance()->getString("DefaultGridSize")); + + if (viewPreference != "automatic" && !system->getSystemViewMode().empty() && system->getTheme()->hasView(system->getSystemViewMode()) && system->getSystemViewMode() != viewPreference) + gridSizeOverride = Vector2f(0, 0); + Vector2f bySystemGridOverride = system->getGridSizeOverride(); //Vector2f(0,0); if (bySystemGridOverride != Vector2f(0, 0)) gridSizeOverride = bySystemGridOverride; diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 0a12f302fa..65eefc1316 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -114,10 +114,7 @@ void Window::deinit(bool deinitRenderer) ResourceManager::getInstance()->unloadAll(); if (deinitRenderer) - { - // ResourceManager::getInstance()->unloadAll(); Renderer::deinit(); - } } void Window::textInput(const char* text) diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 57456cdb38..6e86f9a855 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -355,7 +355,7 @@ void ImageComponent::render(const Transform4x4f& parentTrans) Renderer::setMatrix(trans); - if (mTexture && (mColorShift & 0xff)) + if (mTexture) { if (Settings::getInstance()->getBool("DebugImage")) { @@ -372,80 +372,83 @@ void ImageComponent::render(const Transform4x4f& parentTrans) // when it finally loads fadeIn(mTexture->bind()); - glEnable(GL_TEXTURE_2D); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - /* - glEnableClientState(GL_VERTEX_ARRAY); - glEnableClientState(GL_TEXTURE_COORD_ARRAY); - glEnableClientState(GL_COLOR_ARRAY); - - glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].pos); - glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].tex); - glColorPointer(4, GL_UNSIGNED_BYTE, 0, mColors); - glDrawArrays(GL_TRIANGLES, 0, 6); - - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_TEXTURE_COORD_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); - */ - - glBegin(GL_QUADS); - - GLfloat red = ((mColorShift & 0xff000000) >> 24) / 255.0; - GLfloat green = ((mColorShift & 0x00ff0000) >> 16) / 255.0; - GLfloat blue = ((mColorShift & 0x0000ff00) >> 8) / 255.0; - GLfloat alpha = ((mColorShift & 0x000000ff)) / 255.0; - - glColor4f(red, green, blue, alpha); - glTexCoord2f(mVertices[0].tex.x(), mVertices[0].tex.y()); - glVertex2f(mVertices[0].pos.x(), mVertices[0].pos.y()); - - glColor4f(red, green, blue, alpha); - glTexCoord2f(mVertices[1].tex.x(), mVertices[1].tex.y()); - glVertex2f(mVertices[1].pos.x(), mVertices[1].pos.y()); - - glColor4f(red, green, blue, alpha); - glTexCoord2f(mVertices[2].tex.x(), mVertices[2].tex.y()); - glVertex2f(mVertices[2].pos.x(), mVertices[2].pos.y()); - - glColor4f(red, green, blue, alpha); - glTexCoord2f(mVertices[3].tex.x(), mVertices[3].tex.y()); - glVertex2f(mVertices[3].pos.x(), mVertices[3].pos.y()); - - glEnd(); - - if (mMirror.x() != 0 || mMirror.y() != 0) + if (mColorShift & 0xff) { - glBegin(GL_QUADS); + glEnable(GL_TEXTURE_2D); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + /* + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glEnableClientState(GL_COLOR_ARRAY); + + glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].pos); + glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].tex); + glColorPointer(4, GL_UNSIGNED_BYTE, 0, mColors); + glDrawArrays(GL_TRIANGLES, 0, 6); - int h = mVertices[1].pos.y() - mVertices[0].pos.y(); + glDisableClientState(GL_VERTEX_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + glDisableClientState(GL_COLOR_ARRAY); + */ + + glBegin(GL_QUADS); - GLfloat alpha2 = alpha * mMirror.y(); - alpha *= mMirror.x(); + GLfloat red = ((mColorShift & 0xff000000) >> 24) / 255.0; + GLfloat green = ((mColorShift & 0x00ff0000) >> 16) / 255.0; + GLfloat blue = ((mColorShift & 0x0000ff00) >> 8) / 255.0; + GLfloat alpha = ((mColorShift & 0x000000ff)) / 255.0; glColor4f(red, green, blue, alpha); - glTexCoord2f(mVertices[0].tex.x(), mVertices[1].tex.y()); - glVertex2f(mVertices[0].pos.x(), mVertices[0].pos.y() + h); + glTexCoord2f(mVertices[0].tex.x(), mVertices[0].tex.y()); + glVertex2f(mVertices[0].pos.x(), mVertices[0].pos.y()); - glColor4f(red, green, blue, alpha2); - glTexCoord2f(mVertices[1].tex.x(), mVertices[0].tex.y()); - glVertex2f(mVertices[1].pos.x(), mVertices[1].pos.y() + h); + glColor4f(red, green, blue, alpha); + glTexCoord2f(mVertices[1].tex.x(), mVertices[1].tex.y()); + glVertex2f(mVertices[1].pos.x(), mVertices[1].pos.y()); - glColor4f(red, green, blue, alpha2); - glTexCoord2f(mVertices[2].tex.x(), mVertices[3].tex.y()); - glVertex2f(mVertices[2].pos.x(), mVertices[2].pos.y() + h); + glColor4f(red, green, blue, alpha); + glTexCoord2f(mVertices[2].tex.x(), mVertices[2].tex.y()); + glVertex2f(mVertices[2].pos.x(), mVertices[2].pos.y()); glColor4f(red, green, blue, alpha); - glTexCoord2f(mVertices[3].tex.x(), mVertices[2].tex.y()); - glVertex2f(mVertices[3].pos.x(), mVertices[3].pos.y() + h); + glTexCoord2f(mVertices[3].tex.x(), mVertices[3].tex.y()); + glVertex2f(mVertices[3].pos.x(), mVertices[3].pos.y()); glEnd(); - } - glDisable(GL_TEXTURE_2D); - glDisable(GL_BLEND); + if (mMirror.x() != 0 || mMirror.y() != 0) + { + glBegin(GL_QUADS); + + int h = mVertices[1].pos.y() - mVertices[0].pos.y(); + + GLfloat alpha2 = alpha * mMirror.y(); + alpha *= mMirror.x(); + + glColor4f(red, green, blue, alpha); + glTexCoord2f(mVertices[0].tex.x(), mVertices[1].tex.y()); + glVertex2f(mVertices[0].pos.x(), mVertices[0].pos.y() + h); + + glColor4f(red, green, blue, alpha2); + glTexCoord2f(mVertices[1].tex.x(), mVertices[0].tex.y()); + glVertex2f(mVertices[1].pos.x(), mVertices[1].pos.y() + h); + + glColor4f(red, green, blue, alpha2); + glTexCoord2f(mVertices[2].tex.x(), mVertices[3].tex.y()); + glVertex2f(mVertices[2].pos.x(), mVertices[2].pos.y() + h); + + glColor4f(red, green, blue, alpha); + glTexCoord2f(mVertices[3].tex.x(), mVertices[2].tex.y()); + glVertex2f(mVertices[3].pos.x(), mVertices[3].pos.y() + h); + + glEnd(); + } + + glDisable(GL_TEXTURE_2D); + glDisable(GL_BLEND); + } } else { From 8798b200d09fea756b9cf63ec87cd4f91bff5f97 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 1 Jul 2019 00:34:19 +0200 Subject: [PATCH 050/336] SystemScreenSaver : Launch/exit game does not return in good place ( wrong display ) VideoVlcComponent : Make subtitles work... --- es-app/src/SystemScreenSaver.cpp | 7 +++-- .../views/gamelist/DetailedGameListView.cpp | 2 +- .../src/views/gamelist/GridGameListView.cpp | 2 +- .../src/views/gamelist/VideoGameListView.cpp | 4 +-- es-core/src/Window.cpp | 3 +- es-core/src/components/GridTileComponent.cpp | 2 +- es-core/src/components/VideoComponent.cpp | 2 +- es-core/src/components/VideoVlcComponent.cpp | 29 +++++++++++++++---- es-core/src/components/VideoVlcComponent.h | 2 ++ es-core/src/utils/FileSystemUtil.cpp | 28 ++++++++++++++++++ es-core/src/utils/FileSystemUtil.h | 2 +- 11 files changed, 66 insertions(+), 17 deletions(-) diff --git a/es-app/src/SystemScreenSaver.cpp b/es-app/src/SystemScreenSaver.cpp index b3e8e87fb2..7983acaaa4 100644 --- a/es-app/src/SystemScreenSaver.cpp +++ b/es-app/src/SystemScreenSaver.cpp @@ -464,12 +464,13 @@ void SystemScreenSaver::launchGame() if (mCurrentGame != NULL) { // launching Game - ViewController::get()->goToGameList(mCurrentGame->getSystem()); IGameListView* view = ViewController::get()->getGameListView(mCurrentGame->getSystem()).get(); view->setCursor(mCurrentGame); + if (Settings::getInstance()->getBool("ScreenSaverControls")) - { view->launch(mCurrentGame); - } + else + ViewController::get()->goToGameList(mCurrentGame->getSystem()); + } } diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 7029fc3e6b..1032b90a8b 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -123,7 +123,7 @@ void DetailedGameListView::createVideo() mVideo = new VideoPlayerComponent(window, ""); else #endif - mVideo = new VideoVlcComponent(mWindow, getTitlePath()); + mVideo = new VideoVlcComponent(mWindow, ""); mVideo->setOrigin(0.5f, 0.5f); mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 3bc7844c36..5270e27395 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -114,7 +114,7 @@ void GridGameListView::createVideo() mVideo = new VideoPlayerComponent(window, ""); else #endif - mVideo = new VideoVlcComponent(mWindow, getTitlePath()); + mVideo = new VideoVlcComponent(mWindow,""); mVideo->setOrigin(0.5f, 0.5f); mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index ef9973f36b..b758cabfee 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -33,9 +33,9 @@ VideoGameListView::VideoGameListView(Window* window, FolderData* root) : if (Settings::getInstance()->getBool("VideoOmxPlayer")) mVideo = new VideoPlayerComponent(window, ""); else - mVideo = new VideoVlcComponent(window, getTitlePath()); + mVideo = new VideoVlcComponent(window, ""); #else - mVideo = new VideoVlcComponent(window, getTitlePath()); + mVideo = new VideoVlcComponent(window, ""); #endif mList.setPosition(mSize.x() * (0.50f + padding), mList.getPosition().y()); diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 65eefc1316..e3cdacedbf 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -334,8 +334,7 @@ void Window::renderLoadingScreen(std::string text, float percent) float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); Renderer::drawRect(x, y, w, h, 0x252525FF); - // Renderer::drawGradientRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x858585FF, 0x555555FF); // 0xFFFFFFFF - Renderer::drawGradientRect(x + 1, y + 1, (w*percent) - 2, h - 2, 0x006C9EFF, 0x003E5CFF); // 0xFFFFFFFF + Renderer::drawGradientRect(x, y, (w*percent), h, 0x006C9EFF, 0x003E5CFF); // 0xFFFFFFFF } ImageComponent splash(this, true); diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 85d1ce8fe5..bde4561a2b 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -78,7 +78,7 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mVideo = new VideoPlayerComponent(window, ""); else #endif - mVideo = new VideoVlcComponent(window, getTitlePath()); + mVideo = new VideoVlcComponent(window, ""); // video mVideo->setOrigin(0.5f, 0.5f); diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 07780738fa..3ea29a2c51 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -17,7 +17,7 @@ std::string getTitlePath() { std::string getTitleFolder() { std::string home = Utils::FileSystem::getHomePath(); - return home + "/.emulationstation/tmp/"; + return Utils::FileSystem::getGenericPath(home + "/.emulationstation/tmp/"); } void writeSubtitle(const char* gameName, const char* systemName, bool always) diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index c15b676fce..4475f3dd0d 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -2,6 +2,7 @@ #include "resources/TextureResource.h" #include "utils/StringUtil.h" +#include "utils/FileSystemUtil.h" #include "PowerSaver.h" #include "Renderer.h" #include "Settings.h" @@ -43,7 +44,8 @@ static void display(void* data, void* id) VideoVlcComponent::VideoVlcComponent(Window* window, std::string subtitles) : VideoComponent(window), - mMediaPlayer(nullptr) + mMediaPlayer(nullptr), + mSubtitlePath(subtitles) { memset(&mContext, 0, sizeof(mContext)); @@ -260,20 +262,20 @@ void VideoVlcComponent::setupVLC(std::string subtitles) if (!mVLC) { const char** args; - const char* newargs[] = { "--quiet", "--sub-file", subtitles.c_str() }; + // const char* newargs[] = { "--quiet", "--sub-file", subtitles.c_str() }; const char* singleargs[] = { "--quiet" }; int argslen = 0; - + /* if (!subtitles.empty()) { argslen = sizeof(newargs) / sizeof(newargs[0]); args = newargs; } else - { + {*/ argslen = sizeof(singleargs) / sizeof(singleargs[0]); args = singleargs; - } + // } mVLC = libvlc_new(argslen, args); } } @@ -313,6 +315,15 @@ void VideoVlcComponent::startVideo() // Set the video that we are going to be playing so we don't attempt to restart it mPlayingVideoPath = mVideoPath; + + if (!mSubtitlePath.empty()) + { + auto ext = Utils::FileSystem::getExtension(path); + auto srt = Utils::String::replace(path, ext, ".srt"); + Utils::FileSystem::copyFile(mSubtitlePath, srt); + mSubtitleTmpFile = srt; + } + // Open the media mMedia = libvlc_media_new_path(mVLC, path.c_str()); if (mMedia) @@ -365,6 +376,8 @@ void VideoVlcComponent::startVideo() libvlc_audio_set_mute(mMediaPlayer, 1); } + auto cnt = libvlc_video_get_spu_count(mMediaPlayer); + libvlc_media_player_play(mMediaPlayer); libvlc_video_set_callbacks(mMediaPlayer, lock, unlock, display, (void*)&mContext); libvlc_video_set_format(mMediaPlayer, "RGBA", (int)mVideoWidth, (int)mVideoHeight, (int)mVideoWidth * 4); @@ -380,6 +393,12 @@ void VideoVlcComponent::startVideo() void VideoVlcComponent::stopVideo() { + if (!mSubtitleTmpFile.empty()) + { + Utils::FileSystem::removeFile(mSubtitleTmpFile); + mSubtitlePath = ""; + } + mIsPlaying = false; mStartDelayed = false; // Release the media player so it stops calling back to us diff --git a/es-core/src/components/VideoVlcComponent.h b/es-core/src/components/VideoVlcComponent.h index 4a1a27bcba..6246ae6b9a 100644 --- a/es-core/src/components/VideoVlcComponent.h +++ b/es-core/src/components/VideoVlcComponent.h @@ -68,6 +68,8 @@ class VideoVlcComponent : public VideoComponent libvlc_media_player_t* mMediaPlayer; VideoContext mContext; std::shared_ptr mTexture; + std::string mSubtitlePath; + std::string mSubtitleTmpFile; }; #endif // ES_CORE_COMPONENTS_VIDEO_VLC_COMPONENT_H diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 67db642338..b53e055745 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -680,6 +680,34 @@ namespace Utils } // removeFile + bool copyFile(const std::string& src, const std::string& dst) + { + std::string path = getGenericPath(src); + std::string pathD = getGenericPath(dst); + + // don't remove if it doesn't exists + if (!exists(path)) + return true; + + char buf[BUFSIZ]; + size_t size; + + FILE* source = fopen(path.c_str(), "rb"); + FILE* dest = fopen(pathD.c_str(), "wb"); + + // clean and more secure + // feof(FILE* stream) returns non-zero if the end of file indicator for stream is set + + while (size = fread(buf, 1, BUFSIZ, source)) { + fwrite(buf, 1, size, dest); + } + + fclose(source); + fclose(dest); + + return true; + } // removeFile + bool createDirectory(const std::string& _path) { std::string path = getGenericPath(_path); diff --git a/es-core/src/utils/FileSystemUtil.h b/es-core/src/utils/FileSystemUtil.h index d572a0f526..eb81148c1f 100644 --- a/es-core/src/utils/FileSystemUtil.h +++ b/es-core/src/utils/FileSystemUtil.h @@ -52,7 +52,7 @@ namespace Utils bool isHidden (const std::string& _path); void setHomePath (std::string path); - + bool copyFile (const std::string& src, const std::string& dst); pugi::xml_parse_result load_xml(pugi::xml_document& doc, const char* path); From 32bb380b4dade91d6d283cb781ba64fc9154d17a Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 1 Jul 2019 09:15:45 +0200 Subject: [PATCH 051/336] Video Screensaver : Bugfix --- es-core/src/components/VideoComponent.cpp | 3 +-- es-core/src/utils/FileSystemUtil.cpp | 24 ++++++++++++++--------- es-core/src/utils/FileSystemUtil.h | 2 +- 3 files changed, 17 insertions(+), 12 deletions(-) diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 3ea29a2c51..9c62a97948 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -16,8 +16,7 @@ std::string getTitlePath() { } std::string getTitleFolder() { - std::string home = Utils::FileSystem::getHomePath(); - return Utils::FileSystem::getGenericPath(home + "/.emulationstation/tmp/"); + return Utils::FileSystem::getGenericPath(Utils::FileSystem::getHomePath()) + "/.emulationstation/tmp/"; } void writeSubtitle(const char* gameName, const char* systemName, bool always) diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index b53e055745..ab91af34d9 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -680,7 +680,7 @@ namespace Utils } // removeFile - bool copyFile(const std::string& src, const std::string& dst) + bool copyFile(const std::string src, const std::string dst) { std::string path = getGenericPath(src); std::string pathD = getGenericPath(dst); @@ -689,21 +689,25 @@ namespace Utils if (!exists(path)) return true; - char buf[BUFSIZ]; + char buf[512]; size_t size; FILE* source = fopen(path.c_str(), "rb"); - FILE* dest = fopen(pathD.c_str(), "wb"); - - // clean and more secure - // feof(FILE* stream) returns non-zero if the end of file indicator for stream is set + if (source == nullptr) + return false; - while (size = fread(buf, 1, BUFSIZ, source)) { - fwrite(buf, 1, size, dest); + FILE* dest = fopen(pathD.c_str(), "wb"); + if (dest == nullptr) + { + fclose(source); + return false; } + while (size = fread(buf, 1, 512, source)) + fwrite(buf, 1, size, dest); + + fclose(dest); fclose(source); - fclose(dest); return true; } // removeFile @@ -738,6 +742,8 @@ namespace Utils DWORD dwAttr = GetFileAttributes(_path.c_str()); if (0xFFFFFFFF == dwAttr) return false; + + return true; #else std::string path = getGenericPath(_path); struct stat64 info; diff --git a/es-core/src/utils/FileSystemUtil.h b/es-core/src/utils/FileSystemUtil.h index eb81148c1f..f7f6f89585 100644 --- a/es-core/src/utils/FileSystemUtil.h +++ b/es-core/src/utils/FileSystemUtil.h @@ -52,7 +52,7 @@ namespace Utils bool isHidden (const std::string& _path); void setHomePath (std::string path); - bool copyFile (const std::string& src, const std::string& dst); + bool copyFile (const std::string src, const std::string dst); pugi::xml_parse_result load_xml(pugi::xml_document& doc, const char* path); From 78444c019b005a8d1fa35e448276f287b98f7e1c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 1 Jul 2019 09:23:44 +0200 Subject: [PATCH 052/336] Video Screensaver : fix deletion of temporary vlc subtitle file --- es-core/src/components/VideoVlcComponent.cpp | 82 +++++++++----------- 1 file changed, 38 insertions(+), 44 deletions(-) diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 4475f3dd0d..6ad49b6d49 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -233,51 +233,42 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) void VideoVlcComponent::setupContext() { - if (!mContext.valid) - { - // Create an RGBA surface to render the video into - mContext.surface = SDL_CreateRGBSurface(SDL_SWSURFACE, (int)mVideoWidth, (int)mVideoHeight, 32, 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff); - mContext.mutex = SDL_CreateMutex(); - mContext.component = this; - mContext.valid = true; - resize(); - } + if (mContext.valid) + return; + + // Create an RGBA surface to render the video into + mContext.surface = SDL_CreateRGBSurface(SDL_SWSURFACE, (int)mVideoWidth, (int)mVideoHeight, 32, 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff); + mContext.mutex = SDL_CreateMutex(); + mContext.component = this; + mContext.valid = true; + resize(); } void VideoVlcComponent::freeContext() { - if (mContext.valid) - { - SDL_FreeSurface(mContext.surface); - SDL_DestroyMutex(mContext.mutex); - - mContext.component = NULL; - mContext.valid = false; - } + if (!mContext.valid) + return; + + SDL_FreeSurface(mContext.surface); + SDL_DestroyMutex(mContext.mutex); + + mContext.component = NULL; + mContext.valid = false; } void VideoVlcComponent::setupVLC(std::string subtitles) { // If VLC hasn't been initialised yet then do it now - if (!mVLC) - { - const char** args; - // const char* newargs[] = { "--quiet", "--sub-file", subtitles.c_str() }; - const char* singleargs[] = { "--quiet" }; - int argslen = 0; - /* - if (!subtitles.empty()) - { - argslen = sizeof(newargs) / sizeof(newargs[0]); - args = newargs; - } - else - {*/ - argslen = sizeof(singleargs) / sizeof(singleargs[0]); - args = singleargs; - // } - mVLC = libvlc_new(argslen, args); - } + if (mVLC != nullptr) + return; + + const char** args; + const char* singleargs[] = { "--quiet" }; + + int argslen = sizeof(singleargs) / sizeof(singleargs[0]); + args = singleargs; + + mVLC = libvlc_new(argslen, args); } void VideoVlcComponent::handleLooping() @@ -314,13 +305,16 @@ void VideoVlcComponent::startVideo() { // Set the video that we are going to be playing so we don't attempt to restart it mPlayingVideoPath = mVideoPath; - - + if (!mSubtitlePath.empty()) { + if (!mSubtitleTmpFile.empty()) + Utils::FileSystem::removeFile(mSubtitleTmpFile); + auto ext = Utils::FileSystem::getExtension(path); auto srt = Utils::String::replace(path, ext, ".srt"); Utils::FileSystem::copyFile(mSubtitlePath, srt); + mSubtitleTmpFile = srt; } @@ -393,12 +387,6 @@ void VideoVlcComponent::startVideo() void VideoVlcComponent::stopVideo() { - if (!mSubtitleTmpFile.empty()) - { - Utils::FileSystem::removeFile(mSubtitleTmpFile); - mSubtitlePath = ""; - } - mIsPlaying = false; mStartDelayed = false; // Release the media player so it stops calling back to us @@ -411,4 +399,10 @@ void VideoVlcComponent::stopVideo() freeContext(); PowerSaver::resume(); } + + if (!mSubtitleTmpFile.empty()) + { + Utils::FileSystem::removeFile(mSubtitleTmpFile); + mSubtitleTmpFile = ""; + } } From 0a3c8c01c0f0c5379b6a3c6b2fe6964d7667cea1 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 1 Jul 2019 09:44:18 +0200 Subject: [PATCH 053/336] Menu theming : finish unfinished job On Screen keyboard : finish unfinished job --- es-app/src/components/ScraperSearchComponent.cpp | 9 ++------- es-app/src/guis/GuiCollectionSystemsOptions.cpp | 8 ++++---- es-app/src/guis/GuiMenu.cpp | 8 ++++---- es-app/src/guis/GuiMetaDataEd.cpp | 1 - es-app/src/guis/GuiScraperMulti.cpp | 6 +++--- es-app/src/guis/GuiScreensaverOptions.cpp | 6 +++--- es-app/src/guis/GuiSlideshowScreensaverOptions.cpp | 14 +++++++------- es-app/src/guis/GuiVideoScreensaverOptions.cpp | 6 +++--- es-core/src/components/BusyComponent.cpp | 7 ++++++- es-core/src/components/DateTimeEditComponent.cpp | 5 +++++ 10 files changed, 37 insertions(+), 33 deletions(-) diff --git a/es-app/src/components/ScraperSearchComponent.cpp b/es-app/src/components/ScraperSearchComponent.cpp index 788620b6db..92b3e184d6 100644 --- a/es-app/src/components/ScraperSearchComponent.cpp +++ b/es-app/src/components/ScraperSearchComponent.cpp @@ -27,7 +27,7 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) mGrid.setEntry(std::make_shared(mWindow), Vector2i(0, 0), false, false, Vector2i(1, 3), GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM); // selected result name - mResultName = std::make_shared(mWindow, "Result name", Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + mResultName = std::make_shared(mWindow, "Result name", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); // selected result thumbnail mResultThumbnail = std::make_shared(mWindow); @@ -35,7 +35,7 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) // selected result desc + container mDescContainer = std::make_shared(mWindow); - mResultDesc = std::make_shared(mWindow, "Result desc", Font::get(FONT_SIZE_SMALL), 0x777777FF); + mResultDesc = std::make_shared(mWindow, "Result desc", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); mDescContainer->addChild(mResultDesc.get()); mDescContainer->setAutoScroll(true); @@ -461,11 +461,6 @@ void ScraperSearchComponent::openInputScreen(ScraperSearchParams& params) // 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")); - /* - mWindow->pushGui(new GuiTextEditPopup(mWindow, _T("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, _T("SEARCH")));*/ } std::vector ScraperSearchComponent::getHelpPrompts() diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index 7d5ea965d8..81a8d436a8 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -3,7 +3,7 @@ #include "components/OptionListComponent.h" #include "components/SwitchComponent.h" #include "guis/GuiSettings.h" -#include "guis/GuiTextEditPopup.h" +#include "guis/GuiTextEditPopupKeyboard.h" #include "utils/StringUtil.h" #include "views/ViewController.h" #include "CollectionSystemManager.h" @@ -43,7 +43,7 @@ void GuiCollectionSystemsOptions::initializeMenu() }; row.makeAcceptInputHandler(createCollectionCall); - auto themeFolder = std::make_shared(mWindow, Utils::String::toUpper(name), Font::get(FONT_SIZE_SMALL), 0x777777FF); + auto themeFolder = std::make_shared(mWindow, Utils::String::toUpper(name), ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); row.addElement(themeFolder, true); s->addRow(row); } @@ -64,7 +64,7 @@ void GuiCollectionSystemsOptions::initializeMenu() createCollection(name); }; row.makeAcceptInputHandler([this, createCustomCollection] { - mWindow->pushGui(new GuiTextEditPopup(mWindow, _T("New Collection Name"), "", createCustomCollection, false)); + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, _T("New Collection Name"), "", createCustomCollection, false)); }); mMenu.addRow(row); @@ -84,7 +84,7 @@ void GuiCollectionSystemsOptions::initializeMenu() if(CollectionSystemManager::get()->isEditing()) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("FINISH EDITING COLLECTION")+" '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "'", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(mWindow, _T("FINISH EDITING COLLECTION")+" '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "'", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiCollectionSystemsOptions::exitEditMode, this)); mMenu.addRow(row); } diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index b33ff3dbe9..c5085779bc 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -925,7 +925,7 @@ void GuiMenu::openQuitMenu() LOG(LogWarning) << "Restart terminated with non-zero result!"; }, "NO", nullptr)); }); - row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); if(Settings::getInstance()->getBool("ShowExit")) @@ -938,7 +938,7 @@ void GuiMenu::openQuitMenu() quitES(""); }, "NO", nullptr)); }); - row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); } } @@ -952,7 +952,7 @@ void GuiMenu::openQuitMenu() LOG(LogWarning) << "Restart terminated with non-zero result!"; }, "NO", nullptr)); }); - row.addElement(std::make_shared(window, "RESTART SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(window, "RESTART SYSTEM", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); row.elements.clear(); @@ -965,7 +965,7 @@ void GuiMenu::openQuitMenu() LOG(LogWarning) << "Shutdown terminated with non-zero result!"; }, "NO", nullptr)); }); - row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", Font::get(FONT_SIZE_MEDIUM), 0x777777FF), true); + row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); s->updatePosition(); diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 7fcd00cc69..dff1075856 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -220,7 +220,6 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector row.makeAcceptInputHandler([this, title, ed, updateVal, multiLine] { mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, title, ed->getValue(), updateVal, multiLine)); - // mWindow->pushGui(new GuiTextEditPopup(mWindow, title, ed->getValue(), updateVal, multiLine)); }); break; } diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 210cac7ae6..391f276bd8 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -34,13 +34,13 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queue(mWindow, "SCRAPING IN PROGRESS", Font::get(FONT_SIZE_LARGE), 0x555555FF, ALIGN_CENTER); + mTitle = std::make_shared(mWindow, "SCRAPING IN PROGRESS", ThemeData::getMenuTheme()->Title.font, ThemeData::getMenuTheme()->Title.color, ALIGN_CENTER); mGrid.setEntry(mTitle, Vector2i(0, 0), false, true); - mSystem = std::make_shared(mWindow, _T("SYSTEM"), Font::get(FONT_SIZE_MEDIUM), 0x777777FF, ALIGN_CENTER); + mSystem = std::make_shared(mWindow, _T("SYSTEM"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color, ALIGN_CENTER); mGrid.setEntry(mSystem, Vector2i(0, 1), false, true); - mSubtitle = std::make_shared(mWindow, "subtitle text", Font::get(FONT_SIZE_SMALL), 0x888888FF, ALIGN_CENTER); + mSubtitle = std::make_shared(mWindow, "subtitle text", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color, ALIGN_CENTER); mGrid.setEntry(mSubtitle, Vector2i(0, 2), false, true); mSearchComp = std::make_shared(mWindow, diff --git a/es-app/src/guis/GuiScreensaverOptions.cpp b/es-app/src/guis/GuiScreensaverOptions.cpp index d308b11109..8d7df10d8f 100644 --- a/es-app/src/guis/GuiScreensaverOptions.cpp +++ b/es-app/src/guis/GuiScreensaverOptions.cpp @@ -1,6 +1,6 @@ #include "guis/GuiScreensaverOptions.h" -#include "guis/GuiTextEditPopup.h" +#include "guis/GuiTextEditPopupKeyboard.h" #include "views/ViewController.h" #include "Settings.h" #include "SystemData.h" @@ -73,7 +73,7 @@ void GuiScreensaverOptions::addEditableTextComponent(ComponentListRow row, const { row.elements.clear(); - auto lbl = std::make_shared(mWindow, Utils::String::toUpper(label), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + auto lbl = std::make_shared(mWindow, Utils::String::toUpper(label), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); row.addElement(lbl, true); // label row.addElement(ed, true); @@ -89,7 +89,7 @@ void GuiScreensaverOptions::addEditableTextComponent(ComponentListRow row, const auto updateVal = [ed](const std::string& newVal) { ed->setValue(newVal); }; // ok callback (apply new value to ed) row.makeAcceptInputHandler([this, label, ed, updateVal] { - mWindow->pushGui(new GuiTextEditPopup(mWindow, label, ed->getValue(), updateVal, false)); + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, label, ed->getValue(), updateVal, false)); }); assert(ed); addRow(row); diff --git a/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp b/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp index 5b4a5a4889..53285498b8 100644 --- a/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp +++ b/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp @@ -2,7 +2,7 @@ #include "components/SliderComponent.h" #include "components/SwitchComponent.h" -#include "guis/GuiTextEditPopup.h" +#include "guis/GuiTextEditPopupKeyboard.h" #include "utils/StringUtil.h" #include "Settings.h" #include "Window.h" @@ -30,7 +30,7 @@ GuiSlideshowScreensaverOptions::GuiSlideshowScreensaverOptions(Window* window, c }); // background audio file - auto sss_bg_audio_file = std::make_shared(mWindow, "", Font::get(FONT_SIZE_SMALL), 0x777777FF); + auto sss_bg_audio_file = std::make_shared(mWindow, "", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); addEditableTextComponent(row, "BACKGROUND AUDIO", sss_bg_audio_file, Settings::getInstance()->getString("SlideshowScreenSaverBackgroundAudioFile")); addSaveFunc([sss_bg_audio_file] { Settings::getInstance()->setString("SlideshowScreenSaverBackgroundAudioFile", sss_bg_audio_file->getValue()); @@ -43,7 +43,7 @@ GuiSlideshowScreensaverOptions::GuiSlideshowScreensaverOptions(Window* window, c addSaveFunc([sss_custom_source] { Settings::getInstance()->setBool("SlideshowScreenSaverCustomImageSource", sss_custom_source->getState()); }); // custom image directory - auto sss_image_dir = std::make_shared(mWindow, "", Font::get(FONT_SIZE_SMALL), 0x777777FF); + auto sss_image_dir = std::make_shared(mWindow, "", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); addEditableTextComponent(row, "CUSTOM IMAGE DIR", sss_image_dir, Settings::getInstance()->getString("SlideshowScreenSaverImageDir")); addSaveFunc([sss_image_dir] { Settings::getInstance()->setString("SlideshowScreenSaverImageDir", sss_image_dir->getValue()); @@ -58,7 +58,7 @@ GuiSlideshowScreensaverOptions::GuiSlideshowScreensaverOptions(Window* window, c }); // custom image filter - auto sss_image_filter = std::make_shared(mWindow, "", Font::get(FONT_SIZE_SMALL), 0x777777FF); + auto sss_image_filter = std::make_shared(mWindow, "", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); addEditableTextComponent(row, "CUSTOM IMAGE FILTER", sss_image_filter, Settings::getInstance()->getString("SlideshowScreenSaverImageFilter")); addSaveFunc([sss_image_filter] { Settings::getInstance()->setString("SlideshowScreenSaverImageFilter", sss_image_filter->getValue()); @@ -73,7 +73,7 @@ void GuiSlideshowScreensaverOptions::addWithLabel(ComponentListRow row, const st { row.elements.clear(); - auto lbl = std::make_shared(mWindow, Utils::String::toUpper(label), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + auto lbl = std::make_shared(mWindow, Utils::String::toUpper(label), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); row.addElement(lbl, true); // label row.addElement(component, false, true); @@ -85,7 +85,7 @@ void GuiSlideshowScreensaverOptions::addEditableTextComponent(ComponentListRow r { row.elements.clear(); - auto lbl = std::make_shared(mWindow, Utils::String::toUpper(label), Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + auto lbl = std::make_shared(mWindow, Utils::String::toUpper(label), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); row.addElement(lbl, true); // label row.addElement(ed, true); @@ -101,7 +101,7 @@ void GuiSlideshowScreensaverOptions::addEditableTextComponent(ComponentListRow r auto updateVal = [ed](const std::string& newVal) { ed->setValue(newVal); }; // ok callback (apply new value to ed) row.makeAcceptInputHandler([this, label, ed, updateVal] { - mWindow->pushGui(new GuiTextEditPopup(mWindow, label, ed->getValue(), updateVal, false)); + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, label, ed->getValue(), updateVal, false)); }); assert(ed); diff --git a/es-app/src/guis/GuiVideoScreensaverOptions.cpp b/es-app/src/guis/GuiVideoScreensaverOptions.cpp index b491c2ff1f..3eb54a64a3 100644 --- a/es-app/src/guis/GuiVideoScreensaverOptions.cpp +++ b/es-app/src/guis/GuiVideoScreensaverOptions.cpp @@ -64,20 +64,20 @@ GuiVideoScreensaverOptions::GuiVideoScreensaverOptions(Window* window, const cha }); // Define subtitle font - auto ss_omx_font_file = std::make_shared(mWindow, "", Font::get(FONT_SIZE_SMALL), 0x777777FF); + auto ss_omx_font_file = std::make_shared(mWindow, "", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); addEditableTextComponent(row, "PATH TO FONT FILE", ss_omx_font_file, Settings::getInstance()->getString("SubtitleFont")); addSaveFunc([ss_omx_font_file] { Settings::getInstance()->setString("SubtitleFont", ss_omx_font_file->getValue()); }); // Define subtitle italic font - auto ss_omx_italic_font_file = std::make_shared(mWindow, "", Font::get(FONT_SIZE_SMALL), 0x777777FF); + auto ss_omx_italic_font_file = std::make_shared(mWindow, "", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); addEditableTextComponent(row, "PATH TO ITALIC FONT FILE", ss_omx_italic_font_file, Settings::getInstance()->getString("SubtitleItalicFont")); addSaveFunc([ss_omx_italic_font_file] { Settings::getInstance()->setString("SubtitleItalicFont", ss_omx_italic_font_file->getValue()); }); #endif - + #ifndef _RPI_ auto captions_compatibility = std::make_shared(mWindow); captions_compatibility->setState(Settings::getInstance()->getBool("CaptionsCompatibility")); diff --git a/es-core/src/components/BusyComponent.cpp b/es-core/src/components/BusyComponent.cpp index 17f36df08d..b26e66b137 100644 --- a/es-core/src/components/BusyComponent.cpp +++ b/es-core/src/components/BusyComponent.cpp @@ -17,9 +17,14 @@ const AnimationDef BUSY_ANIMATION_DEF = { BUSY_ANIMATION_FRAMES, 4, true }; BusyComponent::BusyComponent(Window* window) : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(5, 3)) { + auto theme = ThemeData::getMenuTheme(); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setCenterColor(theme->Background.color); + mBackground.setEdgeColor(theme->Background.color); + mAnimation = std::make_shared(mWindow); mAnimation->load(&BUSY_ANIMATION_DEF); - mText = std::make_shared(mWindow, "WORKING...", Font::get(FONT_SIZE_MEDIUM), 0x777777FF); + mText = std::make_shared(mWindow, "WORKING...", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); // col 0 = animation, col 1 = spacer, col 2 = text mGrid.setEntry(mAnimation, Vector2i(1, 1), false, true); diff --git a/es-core/src/components/DateTimeEditComponent.cpp b/es-core/src/components/DateTimeEditComponent.cpp index 6676641af5..608c4f9c0d 100644 --- a/es-core/src/components/DateTimeEditComponent.cpp +++ b/es-core/src/components/DateTimeEditComponent.cpp @@ -8,6 +8,11 @@ DateTimeEditComponent::DateTimeEditComponent(Window* window, DisplayMode dispMod mEditing(false), mEditIndex(0), mDisplayMode(dispMode), mRelativeUpdateAccumulator(0), mColor(0x777777FF), mFont(Font::get(FONT_SIZE_SMALL, FONT_PATH_LIGHT)), mUppercase(false), mAutoSize(true) { + auto menuTheme = ThemeData::getMenuTheme(); + + mFont = menuTheme->TextSmall.font; + mColor = menuTheme->Text.color; + updateTextCache(); } From 5aeeba1ebc13bd4338eb55553f0c75751534b336 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 1 Jul 2019 16:20:46 +0200 Subject: [PATCH 054/336] - Locale : refactoring + using a map (faster than a vector) - VideoVlc : Simplified rendering using GL_QUADS instead of GL_TRIANGLES - Threaded loading : occasionnal crash. --- es-app/src/SystemData.cpp | 26 +--- es-app/src/guis/GuiGamelistFilter.cpp | 5 +- es-app/src/guis/GuiGamelistOptions.cpp | 6 +- es-app/src/guis/GuiMenu.cpp | 10 +- es-app/src/guis/GuiMetaDataEd.cpp | 2 +- es-app/src/main.cpp | 2 +- es-app/src/views/SystemView.cpp | 2 +- .../views/gamelist/DetailedGameListView.cpp | 2 +- es-core/CMakeLists.txt | 2 + es-core/src/EsLocale.cpp | 88 +++++++++++++ es-core/src/EsLocale.h | 33 +++++ es-core/src/GuiComponent.cpp | 97 -------------- es-core/src/GuiComponent.h | 59 +-------- es-core/src/components/VideoComponent.cpp | 6 +- es-core/src/components/VideoVlcComponent.cpp | 120 +++++++----------- es-core/src/guis/GuiTextEditPopupKeyboard.cpp | 8 +- es-core/src/utils/FileSystemUtil.cpp | 18 +-- 17 files changed, 196 insertions(+), 290 deletions(-) create mode 100644 es-core/src/EsLocale.cpp create mode 100644 es-core/src/EsLocale.h diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 91d8894a20..8311b98c3d 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -411,10 +411,6 @@ bool SystemData::loadConfig(Window* window) int currentSystem = 0; -#ifdef WIN32 - unsigned int Ticks = GetTickCount(); -#endif - typedef SystemData* SystemDataPtr; ThreadPool* pThreadPool = NULL; @@ -465,18 +461,11 @@ bool SystemData::loadConfig(Window* window) { if (window != NULL) { - int cnt = 0; - - pThreadPool->wait([window, &processedSystem, systemCount, systemsNames, &cnt] + pThreadPool->wait([window, &processedSystem, systemCount, &systemsNames] { - int px = processedSystem; - if (px == cnt) - return; - - cnt = px; - - auto name = px < 0 || px > systemsNames.size() ? "" : systemsNames.at(px); - window->renderLoadingScreen(name, (float)px / (float)(systemCount + 1)); + int px = processedSystem - 1; + if (px >= 0 && px < systemsNames.size()) + window->renderLoadingScreen(systemsNames.at(px), (float)px / (float)(systemCount + 1)); }, 10); } else @@ -488,7 +477,7 @@ bool SystemData::loadConfig(Window* window) if (pSystem != nullptr) sSystemVector.push_back(pSystem); } - + delete[] systems; delete pThreadPool; @@ -505,11 +494,6 @@ bool SystemData::loadConfig(Window* window) CollectionSystemManager::get()->loadCollectionSystems(); } -#ifdef WIN32 - Ticks = GetTickCount() - Ticks; -// ::MessageBox(0, std::to_string(Ticks).c_str(), NULL, NULL); -#endif - return true; } diff --git a/es-app/src/guis/GuiGamelistFilter.cpp b/es-app/src/guis/GuiGamelistFilter.cpp index 3160b4bc97..95598630f9 100644 --- a/es-app/src/guis/GuiGamelistFilter.cpp +++ b/es-app/src/guis/GuiGamelistFilter.cpp @@ -61,10 +61,9 @@ void GuiGamelistFilter::addFiltersToMenu() FilterIndexType type = (*it).type; // type of filter std::map* allKeys = (*it).allIndexKeys; // all possible filters for this type - std::string menuLabel = _L((*it).menuLabel); // text to show in menu + std::string menuLabel = _T((*it).menuLabel); // text to show in menu std::shared_ptr< OptionListComponent > optionList; - - + // add filters (with first one selected) ComponentListRow row; diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 030a8c6844..b1be6e8962 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -129,7 +129,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) { const FolderData::SortType& sort = FileSorts::SortTypes.at(i); - mListSort->add(_L(Utils::String::toUpper(sort.description)), &sort, i == 0); // TODO - actually make the sort type persistent + mListSort->add(_T(Utils::String::toUpper(sort.description)), &sort, i == 0); // TODO - actually make the sort type persistent } mMenu.addWithLabel(_T("SORT GAMES BY"), mListSort); @@ -153,7 +153,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool if (sel) found = true; - mViewMode->add(_L(*it), *it, sel); + mViewMode->add(_T(*it), *it, sel); } if (!found) @@ -178,7 +178,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool if (sel) found = true; - mGridSize->add(_L(*it), *it, sel); + mGridSize->add(_T(*it), *it, sel); } if (!found) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index c5085779bc..b18a6fa879 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -492,7 +492,7 @@ void GuiMenu::openUISettings() viewPreference = "automatic"; for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_L(*it), *it, viewPreference == *it); + gamelist_style->add(_T(*it), *it, viewPreference == *it); s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); s->addSaveFunc([gamelist_style, viewPreference] { @@ -520,7 +520,7 @@ void GuiMenu::openUISettings() if (sel) found = true; - mGridSize->add(_L(*it), *it, sel); + mGridSize->add(_T(*it), *it, sel); } if (!found) @@ -555,7 +555,7 @@ void GuiMenu::openUISettings() auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, _T("UI MODE"), false); std::vector UImodes = UIModeController::getInstance()->getUIModes(); for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) - UImodeSelection->add(_L(*it), *it, Settings::getInstance()->getString("UIMode") == *it); + UImodeSelection->add(_T(*it), *it, Settings::getInstance()->getString("UIMode") == *it); s->addWithLabel(_T("UI MODE"), UImodeSelection); Window* window = mWindow; s->addSaveFunc([UImodeSelection, window] @@ -627,7 +627,7 @@ void GuiMenu::openUISettings() transitions.push_back("slide"); transitions.push_back("instant"); for (auto it = transitions.cbegin(); it != transitions.cend(); it++) - transition_style->add(_L(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); + transition_style->add(_T(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); s->addWithLabel(_T("TRANSITION STYLE"), transition_style); s->addSaveFunc([transition_style] { @@ -649,7 +649,7 @@ void GuiMenu::openUISettings() gameTransitions.push_back("slide"); gameTransitions.push_back("instant"); for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) - transitionOfGames_style->add(_L(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); + transitionOfGames_style->add(_T(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); s->addSaveFunc([transitionOfGames_style] { diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index dff1075856..4d9e90e102 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -151,7 +151,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector continue; } - auto lbl = std::make_shared(mWindow, _L(Utils::String::toUpper(iter->displayName)), theme->Text.font, theme->Text.color); + auto lbl = std::make_shared(mWindow, _T(Utils::String::toUpper(iter->displayName)), theme->Text.font, theme->Text.color); row.addElement(lbl, true); // label switch (iter->type) diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index b842413256..5071c55657 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -251,7 +251,7 @@ bool loadSystemConfigFile(Window* window, const char** errorString) { *errorString = NULL; - if(!SystemData::loadConfig(window)) + if (!SystemData::loadConfig(window)) { LOG(LogError) << "Error while parsing systems configuration file!"; diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 71737653f1..656e9b2def 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -66,7 +66,7 @@ void SystemView::populate() { // no logo in theme; use text TextComponent* text = new TextComponent(mWindow, - (*it)->getName(), + (*it)->getFullName(), Font::get(FONT_SIZE_LARGE), 0x000000FF, ALIGN_CENTER); diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 1032b90a8b..22f35b2a44 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -295,7 +295,7 @@ std::string DetailedGameListView::getMetadata(FileData* file, std::string name) { std::string ret = file->metadata.get(name); if (ret == "unknown") - return _L(ret); + return _T(ret); return ret; } diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index ae52f7ad45..30704dc50b 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -19,6 +19,7 @@ set(CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/Sound.h ${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeData.h ${CMAKE_CURRENT_SOURCE_DIR}/src/Window.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/EsLocale.h # Animations ${CMAKE_CURRENT_SOURCE_DIR}/src/animations/Animation.h @@ -99,6 +100,7 @@ set(CORE_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/Sound.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeData.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/Window.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/EsLocale.cpp # Animations ${CMAKE_CURRENT_SOURCE_DIR}/src/animations/AnimationController.cpp diff --git a/es-core/src/EsLocale.cpp b/es-core/src/EsLocale.cpp new file mode 100644 index 0000000000..61af250f0f --- /dev/null +++ b/es-core/src/EsLocale.cpp @@ -0,0 +1,88 @@ +#include "EsLocale.h" +#include "resources/ResourceManager.h" +#include "Settings.h" +#include "utils/FileSystemUtil.h" + +#include + +std::map EsLocale::mItems; +std::string EsLocale::mCurrentLanguage = "en"; +bool EsLocale::mCurrentLanguageLoaded = true; // By default, 'en' is considered loaded + +void EsLocale::setLanguage(const std::string lang) +{ + mCurrentLanguage = lang; + mCurrentLanguageLoaded = false; +} + +const std::string EsLocale::getText(const std::string text) +{ + checkLocalisationLoaded(); + + auto item = mItems.find(text); + if (item != mItems.cend()) + return item->second; + + return text; +} + +void EsLocale::checkLocalisationLoaded() +{ + if (mCurrentLanguageLoaded) + { + if (Settings::getInstance()->getString("Language") == mCurrentLanguage) + return; + + mCurrentLanguage = Settings::getInstance()->getString("Language"); + } + + mCurrentLanguageLoaded = true; + + mItems.clear(); + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/" + mCurrentLanguage + "/emulationstation2.po"); + if (!Utils::FileSystem::exists(xmlpath)) + return; + + std::string msgid; + std::string msgstr; + std::string line; + + std::ifstream file(xmlpath); + while (std::getline(file, line)) + { + if (line.length() > 0 && line[0] == '#') + { + if (!msgid.empty() && !msgstr.empty()) + mItems[msgid] = msgstr; + + msgid = ""; + msgstr = ""; + } + else if (line.find("msgid") == 0) + { + auto start = line.find("\""); + if (start != std::string::npos) + { + auto end = line.find("\"", start + 1); + if (end != std::string::npos) + msgid = line.substr(start + 1, end - start - 1); + } + } + else if (line.find("msgstr") == 0) + { + auto start = line.find("\""); + if (start != std::string::npos) + { + auto end = line.find("\"", start + 1); + if (end != std::string::npos) + msgstr = line.substr(start + 1, end - start - 1); + } + } + } + + if (!msgid.empty() && !msgstr.empty()) + mItems[msgid] = msgstr; +} + + diff --git a/es-core/src/EsLocale.h b/es-core/src/EsLocale.h new file mode 100644 index 0000000000..b9aadd4eea --- /dev/null +++ b/es-core/src/EsLocale.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include "utils/StringUtil.h" + +class EsLocale +{ +public: + static const std::string getText(const std::string text); + + static void setLanguage(const std::string lang); + static const std::string getLanguage() { return mCurrentLanguage; } + +private: + static void checkLocalisationLoaded(); + static std::map mItems; + static std::string mCurrentLanguage; + static bool mCurrentLanguageLoaded; +}; + +#if defined(_WIN32) + #define UNICODE_CHARTYPE wchar_t* + #define UNICODE_CHARS(x) L ## x + #define UNICODE_STRING(x) Utils::String::convertFromWideString(L ## x) +#else + + #define UNICODE_CHARTYPE char* + #define UNICODE_CHARS(x) x + #define UNICODE_STRING(x) x +#endif // _WIN32 + +#define _T(x) EsLocale::getText(x) diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index b8946fb7c1..9fd81fe3e7 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -8,103 +8,6 @@ #include "Window.h" #include - -#if defined(_WIN32) -#define _conv(x) GuiTextTool::convertFromWideString(L ## x) -#else -#define _conv(x) x -#endif - -#include -#include -#include "resources\ResourceManager.h" - -std::vector GuiTextTool::mItems; -std::string GuiTextTool::mCurrentLanguage = "en"; -bool GuiTextTool::mCurrentLanguageLoaded = false; - -void GuiTextTool::setLanguage(std::string lang) -{ - mCurrentLanguage = lang; - mCurrentLanguageLoaded = false; -} - -void GuiTextTool::ensureLocalisation() -{ - if (mCurrentLanguageLoaded) - { - if (Settings::getInstance()->getString("Language") == mCurrentLanguage) - return; - - mCurrentLanguage = Settings::getInstance()->getString("Language"); - } - - mCurrentLanguageLoaded = true; - - for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) - delete (*it); - - mItems.clear(); - - std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/"+ mCurrentLanguage +"/emulationstation2.po"); - if (Utils::FileSystem::exists(xmlpath)) - { - LocalizationItem* currentItem = NULL; - - std::ifstream file(xmlpath); - std::string str; - while (std::getline(file, str)) - { - if (str.length() > 0 && str[0] == '#') - { - if (currentItem != NULL && currentItem->msgid.length() > 0 && currentItem->msgstr.length() > 0) - mItems.push_back(currentItem); - - currentItem = new LocalizationItem(); - } - - if (currentItem != NULL && str.find("msgid") == 0) - { - auto start = str.find("\""); - if (start != std::string::npos) - { - auto end = str.find("\"", start + 1); - if (end != std::string::npos) - currentItem->msgid = str.substr(start + 1, end - start - 1); - } - } - - if (currentItem != NULL && str.find("msgstr") == 0) - { - auto start = str.find("\""); - if (start != std::string::npos) - { - auto end = str.find("\"", start + 1); - if (end != std::string::npos) - currentItem->msgstr = str.substr(start + 1, end - start - 1); - } - } - } - - if (currentItem != NULL) - delete currentItem; - } -} - - -const std::string GuiTextTool::localize(const std::string text) -{ - ensureLocalisation(); - - for (std::vector::const_iterator it = mItems.cbegin(); it != mItems.cend(); ++it) - { - if (text == (*it)->msgid) - return (*it)->msgstr; - } - - return text; -} - bool GuiComponent::ALLOWANIMATIONS = true; GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255), diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 73623121db..c1e03e5ed5 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -7,13 +7,10 @@ #include "HelpPrompt.h" #include "HelpStyle.h" #include "InputConfig.h" +#include "EsLocale.h" #include #include -#if defined(_WIN32) -#include -#endif - class Animation; class AnimationController; class Font; @@ -181,58 +178,4 @@ class GuiComponent AnimationController* mAnimationMap[MAX_ANIMATIONS]; }; - - -class LocalizationItem -{ -public: - std::string msgid; - std::string msgstr; -}; - -class GuiTextTool -{ -public: -#if defined(_WIN32) - static const std::string convertFromWideString(const std::wstring wstring) - { - int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); - - std::string string; - string.resize(numBytes); - WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); - - return string; - } -#endif - static void ensureLocalisation(); - static const std::string localize(const std::string text); - - static void setLanguage(std::string lang); - static const std::string getLanguage() { return mCurrentLanguage; } - -private: - static std::vector mItems; - static std::string mCurrentLanguage; - static bool mCurrentLanguageLoaded; -}; - -#if defined(_WIN32) - -#define UNICODE_CHARTYPE wchar_t* -#define UNICODE_CHARS(x) L ## x -#define UNICODE_STRING(x) GuiTextTool::convertFromWideString(L ## x) - -#define _T(x) GuiTextTool::localize(GuiTextTool::convertFromWideString(L ## x)) -#define _L(x) GuiTextTool::localize(x) -#else - -#define UNICODE_CHARTYPE char* -#define UNICODE_CHARS(x) x -#define UNICODE_STRING(x) x - -#define _T(x) GuiTextTool::localize(x) -#define _L(x) GuiTextTool::localize(x) -#endif // _WIN32 - #endif // ES_CORE_GUI_COMPONENT_H diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 9c62a97948..a32012475b 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -30,13 +30,15 @@ void writeSubtitle(const char* gameName, const char* systemName, bool always) { fprintf(file, "1\n00:00:01,000 --> 00:00:08,000\n"); } + fprintf(file, "%s\n", gameName); - fprintf(file, "%s\n\n", systemName); + fprintf(file, "%s\n\n", systemName); + //fprintf(file, "%s\n\n", systemName); if (!always) { if (end > 12) { - fprintf(file, "2\n00:00:%d,000 --> 00:00:%d,000\n%s\n%s\n", end-4, end, gameName, systemName); + fprintf(file, "2\n00:00:%d,000 --> 00:00:%d,000\n%s\n%s\n", end-4, end, gameName, systemName); } } diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 6ad49b6d49..c589641a64 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -141,94 +141,60 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) { VideoComponent::render(parentTrans); - float x, y; + if (!mIsPlaying || !mContext.valid) + return; - Transform4x4f trans = parentTrans * getTransform(); + float t = mFadeIn; + if (mFadeIn < 1.0) + { + t = 1.0 - mFadeIn; + t -= 1; // cubic ease in + t = Math::lerp(0, 1, t*t*t + 1); + t = 1.0 - t; + } + if (t == 0.0) + return; + + Transform4x4f trans = parentTrans * getTransform(); Renderer::setMatrix(trans); - if (mIsPlaying && mContext.valid) - { - float tex_offs_x = 0.0f; - float tex_offs_y = 0.0f; - float x2; - float y2; - - x = 0.0; - y = 0.0; - x2 = mSize.x(); - y2 = mSize.y(); - - // Define a structure to contain the data for each vertex - struct Vertex - { - Vector2f pos; - Vector2f tex; - Vector4f colour; - } vertices[6]; - - // We need two triangles to cover the rectangular area - vertices[0].pos[0] = x; vertices[0].pos[1] = y; - vertices[1].pos[0] = x; vertices[1].pos[1] = y2; - vertices[2].pos[0] = x2; vertices[2].pos[1] = y; - - vertices[3].pos[0] = x2; vertices[3].pos[1] = y; - vertices[4].pos[0] = x; vertices[4].pos[1] = y2; - vertices[5].pos[0] = x2; vertices[5].pos[1] = y2; - - // Texture coordinates - vertices[0].tex[0] = -tex_offs_x; vertices[0].tex[1] = -tex_offs_y; - vertices[1].tex[0] = -tex_offs_x; vertices[1].tex[1] = 1.0f + tex_offs_y; - vertices[2].tex[0] = 1.0f + tex_offs_x; vertices[2].tex[1] = -tex_offs_y; - - vertices[3].tex[0] = 1.0f + tex_offs_x; vertices[3].tex[1] = -tex_offs_y; - vertices[4].tex[0] = -tex_offs_x; vertices[4].tex[1] = 1.0f + tex_offs_y; - vertices[5].tex[0] = 1.0f + tex_offs_x; vertices[5].tex[1] = 1.0f + tex_offs_y; - - float t = mFadeIn; - if (mFadeIn < 1.0) - { - t = 1.0 - mFadeIn; - t -= 1; // cubic ease in - t = Math::lerp(0, 1, t*t*t + 1); - t = 1.0 - t; - } + // red text< / font> + //phrase< / font> - // Colours - use this to fade the video in and out - for (int i = 0; i < (4 * 6); ++i) - { - if ((i%4) == 3) - vertices[i / 4].colour[i % 4] = t; - else - vertices[i / 4].colour[i % 4] = 1.0f; - } - - glEnable(GL_TEXTURE_2D); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + float x2 = mSize.x(); + float y2 = mSize.y(); + + glEnable(GL_TEXTURE_2D); + glEnable(GL_BLEND); + glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); + + // Build a texture for the video frame + mTexture->initFromPixels((unsigned char*)mContext.surface->pixels, mContext.surface->w, mContext.surface->h); + mTexture->bind(); + + glBegin(GL_QUADS); - // Build a texture for the video frame - mTexture->initFromPixels((unsigned char*)mContext.surface->pixels, mContext.surface->w, mContext.surface->h); - mTexture->bind(); + glColor4f(1.0f, 1.0f, 1.0f, t); + glTexCoord2f(0, 0); + glVertex2f(0, 0); - // Render it - glEnableClientState(GL_COLOR_ARRAY); - glEnableClientState(GL_VERTEX_ARRAY); - glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glColor4f(1.0f, 1.0f, 1.0f, t); + glTexCoord2f(0, 1.0f); + glVertex2f(0, y2); - glColorPointer(4, GL_FLOAT, sizeof(Vertex), &vertices[0].colour); - glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &vertices[0].pos); - glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &vertices[0].tex); + glColor4f(1.0f, 1.0f, 1.0f, t); + glTexCoord2f(1.0f, 1.0f); + glVertex2f(x2, y2); - glDrawArrays(GL_TRIANGLES, 0, 6); + glColor4f(1.0f, 1.0f, 1.0f, t); + glTexCoord2f(1.0f, 0); + glVertex2f(x2, 0); - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_TEXTURE_COORD_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); + glEnd(); - glDisable(GL_TEXTURE_2D); - glDisable(GL_BLEND); - } + glDisable(GL_TEXTURE_2D); + glDisable(GL_BLEND); } void VideoVlcComponent::setupContext() diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp index c4a3b26f45..e61c2796c7 100644 --- a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp @@ -74,7 +74,7 @@ GuiTextEditPopupKeyboard::GuiTextEditPopupKeyboard(Window* window, const std::st { std::vector> &layout = kbUs; - if (GuiTextTool::getLanguage() == "fr") + if (EsLocale::getLanguage() == "fr") layout = kbFrench; for (unsigned int i = 0; i < layout.size() / 2; i++) @@ -84,10 +84,10 @@ GuiTextEditPopupKeyboard::GuiTextEditPopupKeyboard(Window* window, const std::st { #ifdef WIN32 std::wstring toConvert = layout[2 * i][j]; - std::string atj = GuiTextTool::convertFromWideString(toConvert); + std::string atj = Utils::String::convertFromWideString(toConvert); toConvert = layout[2 * i + 1][j]; - std::string atjs = GuiTextTool::convertFromWideString(toConvert); + std::string atjs = Utils::String::convertFromWideString(toConvert); #else std::string atj = layout[2 * i][j]; std::string atjs = layout[2 * i + 1][j]; @@ -114,7 +114,7 @@ GuiTextEditPopupKeyboard::GuiTextEditPopupKeyboard(Window* window, const std::st mGrid.setEntry(mKeyboardGrid, Vector2i(0, 2), true, false); // Accept/Cancel buttons - buttons.push_back(std::make_shared(mWindow, _L(acceptBtnText), _L(acceptBtnText), [this, okCallback] { okCallback(mText->getValue()); delete this; })); + buttons.push_back(std::make_shared(mWindow, _T(acceptBtnText), _T(acceptBtnText), [this, okCallback] { okCallback(mText->getValue()); delete this; })); buttons.push_back(std::make_shared(mWindow, _T("SPACE"), _T("SPACE"), [this] { mText->startEditing(); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index ab91af34d9..c88beb2287 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -41,20 +41,6 @@ namespace Utils return doc.load_file(path); } -#if defined(_WIN32) - static std::string convertFromWideString(const std::wstring wstring) - { - int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); - std::string string; - - string.resize(numBytes); - WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); - - return std::string(string); - - } // convertFromWideString -#endif // _WIN32 - bool compareFileInfo(const FileInfo& first, const FileInfo& second) { unsigned int i = 0; @@ -89,7 +75,7 @@ namespace Utils // loop over all files in the directory do { - std::string name = convertFromWideString(findData.cFileName); + std::string name = Utils::String::convertFromWideString(findData.cFileName); if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY && name == "." || name == "..") continue; @@ -166,7 +152,7 @@ namespace Utils // loop over all files in the directory do { - std::string name = convertFromWideString(findData.cFileName); + std::string name = Utils::String::convertFromWideString(findData.cFileName); // ignore "." and ".." if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY && name == "." || name == "..") From 946687f54f8ef38d6fa5ed900a115fbac230e6d2 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 1 Jul 2019 16:41:00 +0200 Subject: [PATCH 055/336] Locale : better macros namings + fix favorite icon --- .../src/views/gamelist/BasicGameListView.cpp | 4 +- .../src/views/gamelist/GridGameListView.cpp | 4 +- es-core/src/EsLocale.h | 13 +++--- es-core/src/guis/GuiTextEditPopupKeyboard.cpp | 40 +++++++++---------- es-core/src/resources/TextureData.cpp | 3 ++ 5 files changed, 35 insertions(+), 29 deletions(-) diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index b139273d91..0db4903798 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -81,7 +81,7 @@ void BasicGameListView::populateList(const std::vector& files) continue; if (showFavoriteIcon) - mList.add(_T("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + mList.add(_U("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); else mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); } @@ -96,7 +96,7 @@ void BasicGameListView::populateList(const std::vector& files) if (showFavoriteIcon) { - mList.add(_T("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + mList.add(_U("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); continue; } } diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 5270e27395..350eb7fd56 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -221,7 +221,7 @@ void GridGameListView::populateList(const std::vector& files) continue; if (showFavoriteIcon) - mGrid.add(_T("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + mGrid.add(_U("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); else mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); } @@ -236,7 +236,7 @@ void GridGameListView::populateList(const std::vector& files) if (showFavoriteIcon) { - mGrid.add(_T("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + mGrid.add(_U("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); continue; } } diff --git a/es-core/src/EsLocale.h b/es-core/src/EsLocale.h index b9aadd4eea..74f85d309b 100644 --- a/es-core/src/EsLocale.h +++ b/es-core/src/EsLocale.h @@ -21,13 +21,16 @@ class EsLocale #if defined(_WIN32) #define UNICODE_CHARTYPE wchar_t* - #define UNICODE_CHARS(x) L ## x - #define UNICODE_STRING(x) Utils::String::convertFromWideString(L ## x) + #define _L(x) L ## x + #define _U(x) Utils::String::convertFromWideString(L ## x) + + #define _T(x) EsLocale::getText(x) #else #define UNICODE_CHARTYPE char* - #define UNICODE_CHARS(x) x - #define UNICODE_STRING(x) x + #define _L(x) x + #define _U(x) x + + #define _T(x) EsLocale::getText(x) #endif // _WIN32 -#define _T(x) EsLocale::getText(x) diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp index e61c2796c7..c5c936f21e 100644 --- a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp @@ -4,39 +4,39 @@ #include "utils/StringUtil.h" std::vector> kbFrench { - { UNICODE_CHARS("&"), UNICODE_CHARS("é"), UNICODE_CHARS("\""), UNICODE_CHARS("'"), UNICODE_CHARS("("), UNICODE_CHARS("#"), UNICODE_CHARS("è"), UNICODE_CHARS("!"), UNICODE_CHARS("ç"), UNICODE_CHARS("à"), UNICODE_CHARS(")"), UNICODE_CHARS("-") }, - { UNICODE_CHARS("1"), UNICODE_CHARS("2"), UNICODE_CHARS("3"), UNICODE_CHARS("4"), UNICODE_CHARS("5"), UNICODE_CHARS("6"), UNICODE_CHARS("7"), UNICODE_CHARS("8"), UNICODE_CHARS("9"), UNICODE_CHARS("0"), UNICODE_CHARS("@"), UNICODE_CHARS("_") }, + { _L("&"), _L("é"), _L("\""), _L("'"), _L("("), _L("#"), _L("è"), _L("!"), _L("ç"), _L("à"), _L(")"), _L("-") }, + { _L("1"), _L("2"), _L("3"), _L("4"), _L("5"), _L("6"), _L("7"), _L("8"), _L("9"), _L("0"), _L("@"), _L("_") }, /* - { UNICODE_CHARS("à"), UNICODE_CHARS("ä"), UNICODE_CHARS("ë"), UNICODE_CHARS("ì"), UNICODE_CHARS("ï"), UNICODE_CHARS("ò"), UNICODE_CHARS("ö"), UNICODE_CHARS("ü"), UNICODE_CHARS("\\"), UNICODE_CHARS("|"), UNICODE_CHARS("§"), UNICODE_CHARS("°") }, - { UNICODE_CHARS("á"), UNICODE_CHARS("â"), UNICODE_CHARS("ê"), UNICODE_CHARS("í"), UNICODE_CHARS("î"), UNICODE_CHARS("ó"), UNICODE_CHARS("ô"), UNICODE_CHARS("ú", "û"), UNICODE_CHARS("ñ"), UNICODE_CHARS("¡"), UNICODE_CHARS("¿") }, + { _L("à"), _L("ä"), _L("ë"), _L("ì"), _L("ï"), _L("ò"), _L("ö"), _L("ü"), _L("\\"), _L("|"), _L("§"), _L("°") }, + { _L("á"), _L("â"), _L("ê"), _L("í"), _L("î"), _L("ó"), _L("ô"), _L("ú", "û"), _L("ñ"), _L("¡"), _L("¿") }, */ - { UNICODE_CHARS("a"), UNICODE_CHARS("z"), UNICODE_CHARS("e"), UNICODE_CHARS("r"), UNICODE_CHARS("t"), UNICODE_CHARS("y"), UNICODE_CHARS("u"), UNICODE_CHARS("i"), UNICODE_CHARS("o"), UNICODE_CHARS("p"), UNICODE_CHARS("^"), UNICODE_CHARS("$") }, - { UNICODE_CHARS("A"), UNICODE_CHARS("Z"), UNICODE_CHARS("E"), UNICODE_CHARS("R"), UNICODE_CHARS("T"), UNICODE_CHARS("Y"), UNICODE_CHARS("U"), UNICODE_CHARS("I"), UNICODE_CHARS("O"), UNICODE_CHARS("P"), UNICODE_CHARS("¨"), UNICODE_CHARS("*") }, + { _L("a"), _L("z"), _L("e"), _L("r"), _L("t"), _L("y"), _L("u"), _L("i"), _L("o"), _L("p"), _L("^"), _L("$") }, + { _L("A"), _L("Z"), _L("E"), _L("R"), _L("T"), _L("Y"), _L("U"), _L("I"), _L("O"), _L("P"), _L("¨"), _L("*") }, - { UNICODE_CHARS("q"), UNICODE_CHARS("s"), UNICODE_CHARS("d"), UNICODE_CHARS("f"), UNICODE_CHARS("g"), UNICODE_CHARS("h"), UNICODE_CHARS("j"), UNICODE_CHARS("k"), UNICODE_CHARS("l"), UNICODE_CHARS("m"), UNICODE_CHARS("ù"), UNICODE_CHARS("`") }, - { UNICODE_CHARS("Q"), UNICODE_CHARS("S"), UNICODE_CHARS("D"), UNICODE_CHARS("F"), UNICODE_CHARS("G"), UNICODE_CHARS("H"), UNICODE_CHARS("J"), UNICODE_CHARS("K"), UNICODE_CHARS("L"), UNICODE_CHARS("M"), UNICODE_CHARS("%"), UNICODE_CHARS("£") }, + { _L("q"), _L("s"), _L("d"), _L("f"), _L("g"), _L("h"), _L("j"), _L("k"), _L("l"), _L("m"), _L("ù"), _L("`") }, + { _L("Q"), _L("S"), _L("D"), _L("F"), _L("G"), _L("H"), _L("J"), _L("K"), _L("L"), _L("M"), _L("%"), _L("£") }, //SHIFT key at position 0 - { UNICODE_CHARS("SHIFT"), UNICODE_CHARS("<"), UNICODE_CHARS("w"), UNICODE_CHARS("x"), UNICODE_CHARS("c"), UNICODE_CHARS("v"), UNICODE_CHARS("b"), UNICODE_CHARS("n"), UNICODE_CHARS(","), UNICODE_CHARS(";"), UNICODE_CHARS(":"), UNICODE_CHARS("=") }, - { UNICODE_CHARS("SHIFT"), UNICODE_CHARS(">"), UNICODE_CHARS("W"), UNICODE_CHARS("X"), UNICODE_CHARS("C"), UNICODE_CHARS("V"), UNICODE_CHARS("B"), UNICODE_CHARS("N"), UNICODE_CHARS("?"), UNICODE_CHARS("."), UNICODE_CHARS("/"), UNICODE_CHARS("+") } + { _L("SHIFT"), _L("<"), _L("w"), _L("x"), _L("c"), _L("v"), _L("b"), _L("n"), _L(","), _L(";"), _L(":"), _L("=") }, + { _L("SHIFT"), _L(">"), _L("W"), _L("X"), _L("C"), _L("V"), _L("B"), _L("N"), _L("?"), _L("."), _L("/"), _L("+") } }; std::vector> kbUs { - { UNICODE_CHARS("1"), UNICODE_CHARS("2"), UNICODE_CHARS("3"), UNICODE_CHARS("4"), UNICODE_CHARS("5"), UNICODE_CHARS("6"), UNICODE_CHARS("7"), UNICODE_CHARS("8"), UNICODE_CHARS("9"), UNICODE_CHARS("0"), UNICODE_CHARS("_"), UNICODE_CHARS("+") }, - { UNICODE_CHARS("!"), UNICODE_CHARS("@"), UNICODE_CHARS("#"), UNICODE_CHARS("$"), UNICODE_CHARS("%"), UNICODE_CHARS("^"), UNICODE_CHARS("&"), UNICODE_CHARS("*"), UNICODE_CHARS("("), UNICODE_CHARS(")"), UNICODE_CHARS("-"), UNICODE_CHARS("=") }, + { _L("1"), _L("2"), _L("3"), _L("4"), _L("5"), _L("6"), _L("7"), _L("8"), _L("9"), _L("0"), _L("_"), _L("+") }, + { _L("!"), _L("@"), _L("#"), _L("$"), _L("%"), _L("^"), _L("&"), _L("*"), _L("("), _L(")"), _L("-"), _L("=") }, /* - { UNICODE_CHARS("à"), UNICODE_CHARS("ä"), UNICODE_CHARS("è"), UNICODE_CHARS("ë"), UNICODE_CHARS("ì"), UNICODE_CHARS("ï"), UNICODE_CHARS("ò"), UNICODE_CHARS("ö"), UNICODE_CHARS("ù"), UNICODE_CHARS("ü"), UNICODE_CHARS("¨"), UNICODE_CHARS("¿") }, - { UNICODE_CHARS("á"), UNICODE_CHARS("â"), UNICODE_CHARS("é"), UNICODE_CHARS("ê"), UNICODE_CHARS("í"), UNICODE_CHARS("î"), UNICODE_CHARS("ó"), UNICODE_CHARS("ô"), UNICODE_CHARS("ú"), UNICODE_CHARS("û"), UNICODE_CHARS("ñ"), UNICODE_CHARS("¡") }, + { _L("à"), _L("ä"), _L("è"), _L("ë"), _L("ì"), _L("ï"), _L("ò"), _L("ö"), _L("ù"), _L("ü"), _L("¨"), _L("¿") }, + { _L("á"), _L("â"), _L("é"), _L("ê"), _L("í"), _L("î"), _L("ó"), _L("ô"), _L("ú"), _L("û"), _L("ñ"), _L("¡") }, */ - { UNICODE_CHARS("q"), UNICODE_CHARS("w"), UNICODE_CHARS("e"), UNICODE_CHARS("r"), UNICODE_CHARS("t"), UNICODE_CHARS("y"), UNICODE_CHARS("u"), UNICODE_CHARS("i"), UNICODE_CHARS("o"), UNICODE_CHARS("p"), UNICODE_CHARS("{"), UNICODE_CHARS("}") }, - { UNICODE_CHARS("Q"), UNICODE_CHARS("W"), UNICODE_CHARS("E"), UNICODE_CHARS("R"), UNICODE_CHARS("T"), UNICODE_CHARS("Y"), UNICODE_CHARS("U"), UNICODE_CHARS("I"), UNICODE_CHARS("O"), UNICODE_CHARS("P"), UNICODE_CHARS("["), UNICODE_CHARS("]") }, + { _L("q"), _L("w"), _L("e"), _L("r"), _L("t"), _L("y"), _L("u"), _L("i"), _L("o"), _L("p"), _L("{"), _L("}") }, + { _L("Q"), _L("W"), _L("E"), _L("R"), _L("T"), _L("Y"), _L("U"), _L("I"), _L("O"), _L("P"), _L("["), _L("]") }, - { UNICODE_CHARS("a"), UNICODE_CHARS("s"), UNICODE_CHARS("d"), UNICODE_CHARS("f"), UNICODE_CHARS("g"), UNICODE_CHARS("h"), UNICODE_CHARS("j"), UNICODE_CHARS("k"), UNICODE_CHARS("l"), UNICODE_CHARS(";"), UNICODE_CHARS("\""), UNICODE_CHARS("|") }, - { UNICODE_CHARS("A"), UNICODE_CHARS("S"), UNICODE_CHARS("D"), UNICODE_CHARS("F"), UNICODE_CHARS("G"), UNICODE_CHARS("H"), UNICODE_CHARS("J"), UNICODE_CHARS("K"), UNICODE_CHARS("L"), UNICODE_CHARS(":"), UNICODE_CHARS("'"), UNICODE_CHARS("\\") }, + { _L("a"), _L("s"), _L("d"), _L("f"), _L("g"), _L("h"), _L("j"), _L("k"), _L("l"), _L(";"), _L("\""), _L("|") }, + { _L("A"), _L("S"), _L("D"), _L("F"), _L("G"), _L("H"), _L("J"), _L("K"), _L("L"), _L(":"), _L("'"), _L("\\") }, - { UNICODE_CHARS("SHIFT"), UNICODE_CHARS("~"), UNICODE_CHARS("z"), UNICODE_CHARS("x"), UNICODE_CHARS("c"), UNICODE_CHARS("v"), UNICODE_CHARS("b"), UNICODE_CHARS("n"), UNICODE_CHARS("m"), UNICODE_CHARS(","), UNICODE_CHARS("."), UNICODE_CHARS("?") }, - { UNICODE_CHARS("SHIFT"), UNICODE_CHARS("`"), UNICODE_CHARS("Z"), UNICODE_CHARS("X"), UNICODE_CHARS("C"), UNICODE_CHARS("V"), UNICODE_CHARS("B"), UNICODE_CHARS("N"), UNICODE_CHARS("M"), UNICODE_CHARS("<"), UNICODE_CHARS(">"), UNICODE_CHARS("/") }, + { _L("SHIFT"), _L("~"), _L("z"), _L("x"), _L("c"), _L("v"), _L("b"), _L("n"), _L("m"), _L(","), _L("."), _L("?") }, + { _L("SHIFT"), _L("`"), _L("Z"), _L("X"), _L("C"), _L("V"), _L("B"), _L("N"), _L("M"), _L("<"), _L(">"), _L("/") }, }; GuiTextEditPopupKeyboard::GuiTextEditPopupKeyboard(Window* window, const std::string& title, const std::string& initValue, diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 36d3a945d8..69f35cfa6c 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -56,6 +56,9 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length return false; } + if (svgImage->width == 0 || svgImage->height == 0) + return false; + // We want to rasterise this texture at a specific resolution. If the source size // variables are set then use them otherwise set them from the parsed file if ((mSourceWidth == 0.0f) && (mSourceHeight == 0.0f)) From 4e69793398a965b9177c44639b53c7750aba9997 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 1 Jul 2019 18:00:05 +0200 Subject: [PATCH 056/336] Image VRAM optimization : take care if the image is scaled minSize --- .../views/gamelist/DetailedGameListView.cpp | 3 +- es-core/src/ImageIO.cpp | 10 ++++-- es-core/src/ImageIO.h | 2 +- es-core/src/components/GridTileComponent.cpp | 4 +-- es-core/src/components/ImageComponent.cpp | 5 ++- es-core/src/components/ImageComponent.h | 5 +-- es-core/src/components/SwitchComponent.cpp | 5 ++- es-core/src/components/VideoComponent.cpp | 2 +- es-core/src/components/VideoComponent.h | 2 +- es-core/src/resources/TextureData.cpp | 6 ++-- es-core/src/resources/TextureData.h | 7 ++-- es-core/src/resources/TextureResource.cpp | 8 ++--- es-core/src/resources/TextureResource.h | 36 +++++++++++++++++-- 13 files changed, 69 insertions(+), 26 deletions(-) diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 22f35b2a44..3b4a6486c7 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -325,7 +325,8 @@ void DetailedGameListView::updateInfoPanel() mVideo->setImage(imagePath, false, mVideo->getSize()); } - + + //mSelectedProperties.mImageSizeMode == "minSize")); if (mImage != nullptr) mImage->setImage(imagePath, false, mImage->getSize()); diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index 085b7ed9d0..e56d22e975 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -130,7 +130,7 @@ Vector2f ImageIO::adjustExternPictureSizef(Vector2f imageSize, Vector2f maxSize) return Vector2f(cxDIB, cyDIB); } -unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight, Vector2i& baseSize, Vector2i& packedSize) +unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight, bool externZoom, Vector2i& baseSize, Vector2i& packedSize) { baseSize = Vector2i(0, 0); packedSize = Vector2i(0, 0); @@ -171,9 +171,15 @@ unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const if (maxWidth > 0 && maxHeight > 0 && (width > maxWidth || height > maxHeight)) { Vector2i sz = adjustPictureSize(Vector2i(width, height), Vector2i(maxWidth, maxHeight)); + if (externZoom) + { + Vector2f szf = adjustExternPictureSizef(Vector2f(width, height), Vector2f(maxWidth, maxHeight)); + sz = Vector2i(szf.x(), szf.y()); // adjustPictureSize(Vector2i(width, height), Vector2i(maxWidth, maxHeight)); + } + if (sz.x() != width || sz.y() != height) { - FIBITMAP* imageRescaled = FreeImage_Rescale(fiBitmap, sz.x(), sz.y(), FILTER_BOX); + FIBITMAP* imageRescaled = FreeImage_Rescale(fiBitmap, sz.x(), sz.y(), FILTER_BICUBIC); FreeImage_Unload(fiBitmap); fiBitmap = imageRescaled; diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index 820c60824f..e1ba731cf1 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -12,7 +12,7 @@ class ImageIO { public: static std::vector loadFromMemoryRGBA32(const unsigned char * data, const size_t size, size_t & width, size_t & height); - static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight, Vector2i& baseSize, Vector2i& packedSize); + static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight, bool externZoom, Vector2i& baseSize, Vector2i& packedSize); static void flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height); static Vector2i adjustPictureSize(Vector2i imageSize, Vector2i maxSize); diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index bde4561a2b..6c02bef664 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -503,9 +503,9 @@ void GridTileComponent::setImage(const std::string& path) mCurrentPath = path; if (mSelectedProperties.mSize.x() > mSize.x()) - mImage->setImage(path, false, mSelectedProperties.mSize); + mImage->setImage(path, false, MaxSizeInfo(mSelectedProperties.mSize, mSelectedProperties.mImageSizeMode == "minSize")); else - mImage->setImage(path, false, mSize); + mImage->setImage(path, false, MaxSizeInfo(mSize, mSelectedProperties.mImageSizeMode == "minSize")); resize(); } diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 6e86f9a855..ae726dba91 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -138,8 +138,11 @@ void ImageComponent::setDefaultImage(std::string path) mDefaultPath = path; } -void ImageComponent::setImage(std::string path, bool tile, Vector2f maxSize) +void ImageComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) { + if (!maxSize.isExternalZoomKnown()) + maxSize = MaxSizeInfo(maxSize.x(), maxSize.y(), mTargetIsMin); + if (path.empty() || !ResourceManager::getInstance()->fileExists(path)) { if (mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 0037091c1a..7ef18bce28 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -5,9 +5,10 @@ #include "math/Vector2i.h" #include "GuiComponent.h" #include "platform.h" +#include "resources/TextureResource.h" #include GLHEADER -class TextureResource; +//class TextureResource; class ImageComponent : public GuiComponent { @@ -18,7 +19,7 @@ class ImageComponent : public GuiComponent void setDefaultImage(std::string path); //Loads the image at the given filepath. Will tile if tile is true (retrieves texture as tiling, creates vertices accordingly). - void setImage(std::string path, bool tile = false, Vector2f maxSize = Vector2f(0 ,0)); + void setImage(std::string path, bool tile = false, MaxSizeInfo maxSize = MaxSizeInfo()); //Loads an image from memory. void setImage(const char* image, size_t length, bool tile = false); //Use an already existing texture. diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index 3bc1f89e56..e1b1d7b9b2 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -7,10 +7,9 @@ SwitchComponent::SwitchComponent(Window* window, bool state) : GuiComponent(wind { auto menuTheme = ThemeData::getMenuTheme(); - float height = Font::get(FONT_SIZE_MEDIUM)->getLetterHeight(); - Vector2f maxSize = Vector2f(height, height); + float height = Font::get(FONT_SIZE_MEDIUM)->getLetterHeight(); - mImage.setImage(ThemeData::getMenuTheme()->Icons.off, false, maxSize); + mImage.setImage(ThemeData::getMenuTheme()->Icons.off, false, MaxSizeInfo(height, height)); mImage.setResize(0, height); mImage.setColorShift(menuTheme->Text.color); diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index a32012475b..e7abae8beb 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -128,7 +128,7 @@ bool VideoComponent::setVideo(std::string path) return false; } -void VideoComponent::setImage(std::string path, bool tile, Vector2f maxSize) +void VideoComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) { // Check that the image has changed if (path == mStaticImagePath) diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index 87675eaafd..51f9c49055 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -30,7 +30,7 @@ class VideoComponent : public GuiComponent // Loads the video at the given filepath bool setVideo(std::string path); // Loads a static image that is displayed if the video cannot be played - void setImage(std::string path, bool tile = false, Vector2f maxSize = Vector2f(0, 0)); + void setImage(std::string path, bool tile = false, MaxSizeInfo maxSize = MaxSizeInfo()); // Configures the component to show the default video void setDefaultVideo(); diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 69f35cfa6c..72d90bec8c 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -16,7 +16,7 @@ bool TextureData::OPTIMIZEVRAM = false; TextureData::TextureData(bool tile) : mTile(tile), mTextureID(0), mDataRGBA(nullptr), mScalable(false), - mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f), mMaxSize(Vector2f(0,0)), mPackedSize(Vector2i(0,0)), mBaseSize(Vector2i(0, 0)) + mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f), mMaxSize(MaxSizeInfo()), mPackedSize(Vector2i(0,0)), mBaseSize(Vector2i(0, 0)) { } @@ -132,7 +132,7 @@ bool TextureData::isRequiredTextureSizeOk() if (mBaseSize == Vector2i(0, 0)) return true; - if (mMaxSize == Vector2f(0, 0)) + if (mMaxSize.empty()) return true; if ((int) mMaxSize.x() <= mPackedSize.x() || (int) mMaxSize.y() <= mPackedSize.y()) @@ -155,7 +155,7 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng return true; } - unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height, OPTIMIZEVRAM ? mMaxSize.x() : 0, mMaxSize.y(), mBaseSize, mPackedSize); + unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height, OPTIMIZEVRAM ? mMaxSize.x() : 0, mMaxSize.y(), mMaxSize.externalZoom(), mBaseSize, mPackedSize); if (imageRGBA == NULL) { LOG(LogError) << "Could not initialize texture from memory, invalid data! (file path: " << mPath << ", data ptr: " << (size_t)fileData << ", reported size: " << length << ")"; diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index a2aaee863e..dcbcf44afd 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -9,8 +9,9 @@ #include "math/Vector2f.h" #include "math/Vector2i.h" +#include "resources/TextureResource.h" -class TextureResource; +// class TextureResource; class TextureData { @@ -44,7 +45,7 @@ class TextureData // Release the texture from conventional RAM void releaseRAM(); - void setMaxSize(Vector2f maxSize) + void setMaxSize(MaxSizeInfo maxSize) { if (mMaxSize.x() < maxSize.x() || mMaxSize.y() < maxSize.y()) mMaxSize = maxSize; @@ -79,7 +80,7 @@ class TextureData Vector2i mPackedSize; Vector2i mBaseSize; - Vector2f mMaxSize; + MaxSizeInfo mMaxSize; }; #endif // ES_CORE_RESOURCES_TEXTURE_DATA_H diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index bf64eba249..33fc2c4feb 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -10,7 +10,7 @@ std::map< TextureResource::TextureKeyType, std::shared_ptr> Tex std::set TextureResource::sAllTextures; -TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic, Vector2f maxSize) : mTextureData(nullptr), mForceLoad(false) +TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic, MaxSizeInfo maxSize) : mTextureData(nullptr), mForceLoad(false) { // Create a texture data object for this texture if (!path.empty()) @@ -118,7 +118,7 @@ void TextureResource::resetCache() sTextureMap.clear(); } -std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic, Vector2f maxSize) +std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic, MaxSizeInfo maxSize) { std::shared_ptr& rm = ResourceManager::getInstance(); @@ -138,7 +138,7 @@ std::shared_ptr TextureResource::get(const std::string& path, b { std::shared_ptr rc = foundTexture->second.lock(); - if (maxSize != Vector2f(0, 0) && TextureData::OPTIMIZEVRAM) + if (!maxSize.empty() && TextureData::OPTIMIZEVRAM) { auto dt = sTextureDataManager.get(rc.get()); if (dt != nullptr) @@ -156,7 +156,7 @@ std::shared_ptr TextureResource::get(const std::string& path, b { std::shared_ptr rc = permanentTexture->second; - if (maxSize != Vector2f(0, 0) && TextureData::OPTIMIZEVRAM) + if (!maxSize.empty() && TextureData::OPTIMIZEVRAM) { auto dt = sTextureDataManager.get(rc.get()); if (dt != nullptr) diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index 4e7b29b7fa..272ad270c0 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -11,12 +11,44 @@ class TextureData; +class MaxSizeInfo +{ +public: + MaxSizeInfo() : mSize(Vector2f(0, 0)), mExternalZoom(false) {} + + MaxSizeInfo(float x, float y) : mSize(Vector2f(x, y)), mExternalZoom(false), mExternalZoomKnown(false) { } + MaxSizeInfo(Vector2f size) : mSize(size), mExternalZoom(false), mExternalZoomKnown(false) { } + + MaxSizeInfo(float x, float y, bool externalZoom) : mSize(Vector2f(x, y)), mExternalZoom(externalZoom), mExternalZoomKnown(true){ } + MaxSizeInfo(Vector2f size, bool externalZoom) : mSize(size), mExternalZoom(externalZoom), mExternalZoomKnown(true) { } + + bool empty() { return mSize.x() == 0 && mSize.y() == 0; } + + float x() { return mSize.x(); } + float y() { return mSize.y(); } + + bool externalZoom() + { + return mExternalZoom; + } + + bool isExternalZoomKnown() + { + return mExternalZoomKnown; + } + +private: + Vector2f mSize; + bool mExternalZoom; + bool mExternalZoomKnown; +}; + // An OpenGL texture. // Automatically recreates the texture with renderer deinit/reinit. class TextureResource : public IReloadable { public: - static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true, Vector2f maxSize = Vector2f(0, 0)); + static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true, MaxSizeInfo maxSize = MaxSizeInfo()); void initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height); virtual void initFromMemory(const char* file, size_t length); @@ -41,7 +73,7 @@ class TextureResource : public IReloadable virtual void reload(); protected: - TextureResource(const std::string& path, bool tile, bool dynamic, Vector2f maxSize); + TextureResource(const std::string& path, bool tile, bool dynamic, MaxSizeInfo maxSize); private: // mTextureData is used for textures that are not loaded from a file - these ones From 5d049dd202d806976db35471ca796fab8f25cb7c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 1 Jul 2019 21:21:04 +0200 Subject: [PATCH 057/336] Bad management of folders containing only one game --- es-app/src/Gamelist.cpp | 4 ++++ es-app/src/SystemData.cpp | 21 +++++++++++++++------ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 03740ee722..a481d72e82 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -22,6 +22,10 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType return NULL; } + auto pGame = fileMap.find(path); + if (pGame != fileMap.end()) + return pGame->second; + Utils::FileSystem::stringList pathList = Utils::FileSystem::getPathList(relative); auto path_it = pathList.begin(); FolderData* treeNode = root; diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 8311b98c3d..bf619af4e3 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -184,15 +184,24 @@ void SystemData::populateFolder(FolderData* folder, std::unordered_mapgetChildren().size() == 0) delete newFolder; - else if (newFolder->findUniqueGameForFolder() != NULL) - delete newFolder; else { - const std::string& key = newFolder->getPath(); - if (fileMap.find(key) == fileMap.end()) + FileData* childGame = newFolder->findUniqueGameForFolder(); + if (childGame != NULL) + { + FileData* newGame = new FileData(GAME, childGame->getPath(), this); + folder->addChild(newGame); + fileMap[fileInfo.path] = newGame; + delete newFolder; + } + else { - folder->addChild(newFolder); - fileMap[key] = newFolder; + const std::string& key = newFolder->getPath(); + if (fileMap.find(key) == fileMap.end()) + { + folder->addChild(newFolder); + fileMap[key] = newFolder; + } } } } From e9df315b5c080a866435bdb11c8296fdcc5b7960 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 1 Jul 2019 21:32:28 +0200 Subject: [PATCH 058/336] Bad management of sub-folders containing only one game --- es-app/src/SystemData.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index bf619af4e3..84068fe468 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -190,7 +190,7 @@ void SystemData::populateFolder(FolderData* folder, std::unordered_mapgetPath(), this); - folder->addChild(newGame); + mRootFolder->addChild(newGame); fileMap[fileInfo.path] = newGame; delete newFolder; } From f9cdf661b2b4cc89f99b5766c4d37d34411ad56c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 2 Jul 2019 16:02:35 +0200 Subject: [PATCH 059/336] Hide when launch ON : bugfix Menu theming : finiahs buttons & grid unfinished work. Folders containing only 1 game : better way to do that. --- es-app/src/CollectionSystemManager.cpp | 7 --- es-app/src/FileData.h | 3 ++ es-app/src/Gamelist.cpp | 53 ++++++++++++++++++---- es-app/src/Gamelist.h | 1 + es-app/src/MetaData.cpp | 15 +++++- es-app/src/SystemData.cpp | 23 +++------- es-app/src/SystemData.h | 8 ++-- es-app/src/guis/GuiMenu.cpp | 6 +-- es-app/src/main.cpp | 6 ++- es-core/src/components/ButtonComponent.cpp | 13 +++++- es-core/src/components/ButtonComponent.h | 4 ++ es-core/src/components/ComponentGrid.cpp | 5 +- es-core/src/components/ComponentGrid.h | 2 +- 13 files changed, 100 insertions(+), 46 deletions(-) diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 9d2bec80ea..721834bb32 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -458,13 +458,6 @@ void CollectionSystemManager::exitEditMode() // adds or removes a game from a specific collection bool CollectionSystemManager::toggleGameInCollection(FileData* file) { - /* - if (file->getType() == FOLDER) - { - if (file->findUniqueGameForFolder() == NULL) - return false; - } - */ if (file->getType() == GAME) { GuiInfoPopup* s; diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index cd19a086ca..45e9f10ee0 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -118,6 +118,9 @@ class FolderData : public FileData ~FolderData() { + for (int i = mChildren.size() - 1; i >= 0; i--) + delete mChildren.at(i); + mChildren.clear(); } diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index a481d72e82..7cec7df63d 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -49,6 +49,13 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType { if (item == nullptr) { + // Skip if the extension in the gamelist is unknown + if (!system->getSystemEnvData()->isValidExtension(Utils::FileSystem::getExtension(path))) + { + LOG(LogWarning) << "gameList: file extension is not known by systemlist"; + return NULL; + } + // Add final game item = new FileData(GAME, path, system); if (!item->isArcadeAsset()) @@ -61,9 +68,8 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType return item; } } - - /* - if(!found) + + if (item == nullptr) { // 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 @@ -73,19 +79,43 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType return NULL; } - // create missing folder - //FCA TODO -> 1 seul jeu dans le folder ? - //FileData* folder = new FileData(FOLDER, Utils::FileSystem::getStem(treeNode->getPath()) + "/" + *path_it, system->getSystemEnvData(), system); - //treeNode->addChild(folder); - //treeNode = folder; + FolderData* folder = new FolderData(Utils::FileSystem::getStem(treeNode->getPath()) + "/" + *path_it, system); + treeNode->addChild(folder); + treeNode = folder; } - */ + path_it++; } return NULL; } +void refactorGameFolders(SystemData* system) +{ + FolderData* root = system->getRootFolder(); + + auto childs = root->getChildren(); + for (int i = childs.size() - 1; i >= 0; i--) + { + FileData* item = childs.at(i); + if (item->getType() == FOLDER) + { + FolderData* folder = (FolderData*)item; + FileData* uniqueGame = folder->findUniqueGameForFolder(); + if (uniqueGame != nullptr) + { + childs.erase(childs.begin() + i); + + FileData* newFile = new FileData(GAME, uniqueGame->getPath(), system); + newFile->metadata = uniqueGame->metadata; + root->addChild(newFile); + + delete folder; + } + } + } +} + void parseGamelist(SystemData* system, std::unordered_map& fileMap) { std::string xmlpath = system->getGamelistPath(false); @@ -132,7 +162,7 @@ void parseGamelist(SystemData* system, std::unordered_mapmetadata.get("name").empty()) file->metadata.set("name", defaultName); + if (Utils::FileSystem::isHidden(path)) + file->metadata.set("hidden", "true"); + file->metadata.resetChangedFlag(); } } diff --git a/es-app/src/Gamelist.h b/es-app/src/Gamelist.h index efc402b792..4623a0a737 100644 --- a/es-app/src/Gamelist.h +++ b/es-app/src/Gamelist.h @@ -12,5 +12,6 @@ void parseGamelist(SystemData* system, std::unordered_mapsecond == value) + return; + + mMap[id] = value; + } mWasChanged = true; } diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index bf619af4e3..03a361643a 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -45,6 +45,8 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys if (!Settings::getInstance()->getBool("IgnoreGamelist")) parseGamelist(this, fileMap); + refactorGameFolders(this); + //StopWatch ws("sort " + mName); mRootFolder->sort(FileSorts::SortTypes.at(0)); @@ -157,7 +159,7 @@ void SystemData::populateFolder(FolderData* folder, std::unordered_mapmSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) + if (mEnvData->isValidExtension(extension)) //std::find(mEnvData->mSearchExtensions.cbegin(), mEnvData->mSearchExtensions.cend(), extension) != mEnvData->mSearchExtensions.cend()) { if (fileMap.find(fileInfo.path) == fileMap.end()) { @@ -186,22 +188,11 @@ void SystemData::populateFolder(FolderData* folder, std::unordered_mapfindUniqueGameForFolder(); - if (childGame != NULL) - { - FileData* newGame = new FileData(GAME, childGame->getPath(), this); - folder->addChild(newGame); - fileMap[fileInfo.path] = newGame; - delete newFolder; - } - else + const std::string& key = newFolder->getPath(); + if (fileMap.find(key) == fileMap.end()) { - const std::string& key = newFolder->getPath(); - if (fileMap.find(key) == fileMap.end()) - { - folder->addChild(newFolder); - fileMap[key] = newFolder; - } + folder->addChild(newFolder); + fileMap[key] = newFolder; } } } diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 51a8bbed68..c83ac19142 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -32,11 +32,13 @@ struct SystemEnvironmentData std::vector mSearchExtensions; std::string mLaunchCommand; std::vector mPlatformIds; - -// std::string mDefaultCore; - std::vector mEmulators; + bool isValidExtension(const std::string extension) + { + return std::find(mSearchExtensions.cbegin(), mSearchExtensions.cend(), extension) != mSearchExtensions.cend(); + } + std::vector getCores(std::string emulatorName) { std::vector list; diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index b18a6fa879..9c16cb907b 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -892,13 +892,13 @@ void GuiMenu::openOtherSettings() void GuiMenu::openConfigInput() { Window* window = mWindow; - window->pushGui(new GuiDetectDevice(window, false, nullptr)); - /* +// window->pushGui(new GuiDetectDevice(window, false, nullptr)); + window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), [window] { window->pushGui(new GuiDetectDevice(window, false, nullptr)); }, _T("NO"), nullptr) - );*/ + ); } diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 5071c55657..d7570e8c68 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -504,7 +504,11 @@ int main(int argc, char* argv[]) #ifdef WIN32 if (processDuration < timeLimit) - Sleep(timeLimit - processDuration); + { + int timeToWait = timeLimit - processDuration; + if (timeToWait > 0 && timeToWait < 100) + Sleep(timeToWait); + } int swapStart = SDL_GetTicks(); #endif diff --git a/es-core/src/components/ButtonComponent.cpp b/es-core/src/components/ButtonComponent.cpp index eb4e64294a..f93d1690e1 100644 --- a/es-core/src/components/ButtonComponent.cpp +++ b/es-core/src/components/ButtonComponent.cpp @@ -17,6 +17,7 @@ ButtonComponent::ButtonComponent(Window* window, const std::string& text, const mTextColorUnfocused = menuTheme->Text.color; mTextColorFocused = menuTheme->Text.selectedColor; mColor = menuTheme->Text.color; + mColorFocused = menuTheme->Text.selectorColor; setPressedFunc(func); setText(text, helpText, upperCase); @@ -94,8 +95,8 @@ void ButtonComponent::updateImage() return; } - mBox.setCenterColor(mColor); - mBox.setEdgeColor(mColor); + mBox.setCenterColor(getCurBackColor()); + mBox.setEdgeColor(getCurBackColor()); mBox.setImagePath(mFocused ? ThemeData::getMenuTheme()->Icons.button_filled : ThemeData::getMenuTheme()->Icons.button); //mBox.setImagePath(mFocused ? ":/button_filled.png" : ":/button.png"); } @@ -130,6 +131,14 @@ unsigned int ButtonComponent::getCurTextColor() const return mTextColorFocused; } +unsigned int ButtonComponent::getCurBackColor() const +{ + if (!mFocused) + return mColor; + else + return mColorFocused; +} + std::vector ButtonComponent::getHelpPrompts() { std::vector prompts; diff --git a/es-core/src/components/ButtonComponent.h b/es-core/src/components/ButtonComponent.h index 0825aed5e7..4da6e9f80a 100644 --- a/es-core/src/components/ButtonComponent.h +++ b/es-core/src/components/ButtonComponent.h @@ -45,13 +45,17 @@ class ButtonComponent : public GuiComponent unsigned int mModdedColor; unsigned int getCurTextColor() const; + unsigned int getCurBackColor() const; + void updateImage(); std::string mText; std::string mHelpText; std::unique_ptr mTextCache; NinePatchComponent mBox; + unsigned int mColor; + unsigned int mColorFocused; }; #endif // ES_CORE_COMPONENTS_BUTTON_COMPONENT_H diff --git a/es-core/src/components/ComponentGrid.cpp b/es-core/src/components/ComponentGrid.cpp index 795e59b8c9..0836cc7f80 100644 --- a/es-core/src/components/ComponentGrid.cpp +++ b/es-core/src/components/ComponentGrid.cpp @@ -2,15 +2,16 @@ #include "Renderer.h" #include "Settings.h" +#include "ThemeData.h" using namespace GridFlags; -ComponentGrid::ComponentGrid(Window* window, const Vector2i& gridDimensions, unsigned int separatorColor) : GuiComponent(window), +ComponentGrid::ComponentGrid(Window* window, const Vector2i& gridDimensions) : GuiComponent(window), mGridSize(gridDimensions), mCursor(0, 0) { assert(gridDimensions.x() > 0 && gridDimensions.y() > 0); - mSeparatorColor = separatorColor; + mSeparatorColor = ThemeData::getMenuTheme()->Text.separatorColor; mCells.reserve(gridDimensions.x() * gridDimensions.y()); mColWidths = new float[gridDimensions.x()]; diff --git a/es-core/src/components/ComponentGrid.h b/es-core/src/components/ComponentGrid.h index 3ff8ec09e3..d6fd49b253 100644 --- a/es-core/src/components/ComponentGrid.h +++ b/es-core/src/components/ComponentGrid.h @@ -29,7 +29,7 @@ namespace GridFlags class ComponentGrid : public GuiComponent { public: - ComponentGrid(Window* window, const Vector2i& gridDimensions, unsigned int separatorColor = 0xC6C7C6FF); + ComponentGrid(Window* window, const Vector2i& gridDimensions/*, unsigned int separatorColor = 0xC6C7C6FF*/); virtual ~ComponentGrid(); void setSeparatorColor(unsigned int separatorColor) { mSeparatorColor = separatorColor; updateSeparators(); } From f0136b3144c00b1ec48b5f88009180c9f0aaff50 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 3 Jul 2019 10:34:09 +0200 Subject: [PATCH 060/336] Texture reloading : bugfix --- es-core/src/resources/TextureResource.cpp | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index 33fc2c4feb..1bceee7aee 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -6,7 +6,7 @@ TextureDataManager TextureResource::sTextureDataManager; std::map< TextureResource::TextureKeyType, std::weak_ptr> TextureResource::sTextureMap; -std::map< TextureResource::TextureKeyType, std::shared_ptr> TextureResource::sPermanentTextureMap; // FCAWEAK +std::map< TextureResource::TextureKeyType, std::shared_ptr> TextureResource::sPermanentTextureMap; std::set TextureResource::sAllTextures; @@ -134,7 +134,7 @@ std::shared_ptr TextureResource::get(const std::string& path, b auto foundTexture = sTextureMap.find(key); if(foundTexture != sTextureMap.cend()) { - if (!foundTexture->second.expired()) // FCAWEAK + if (!foundTexture->second.expired()) { std::shared_ptr rc = foundTexture->second.lock(); @@ -172,18 +172,17 @@ std::shared_ptr TextureResource::get(const std::string& path, b std::shared_ptr data = sTextureDataManager.get(tex.get()); // is it an SVG? - // if(key.first.substr(key.first.size() - 4, std::string::npos) != ".svg") // FCATMP + // if (key.first.substr(key.first.size() - 4, std::string::npos) != ".svg") // FCATMP { // Probably not. Add it to our map. We don't add SVGs because 2 svgs might be rasterized at different sizes // FCA useless -> If the svg is too small, it will be reloaded bigger with setSourceSize... if (canonicalPath.length() > 0 && canonicalPath[0] == ':') - sPermanentTextureMap[key] = std::shared_ptr(tex); // FCAWEAK + sPermanentTextureMap[key] = std::shared_ptr(tex); else - sTextureMap[key] = std::shared_ptr(tex); // FCAWEAK + sTextureMap[key] = std::shared_ptr(tex); } - - + // Add it to the reloadable list, exclusion for splash.svg manually managed #ifdef WIN32 if (path != ":/splash.svg") @@ -211,6 +210,7 @@ void TextureResource::rasterizeAt(size_t width, size_t height) data = mTextureData; else data = sTextureDataManager.get(this); + mSourceSize = Vector2f((float)width, (float)height); data->setSourceSize((float)width, (float)height); @@ -278,6 +278,4 @@ void TextureResource::reload() // For manually loaded textures we have to reload them here if (mTextureData && !mTextureData->isLoaded()) mTextureData->load(); - else if (mTextureData == nullptr) - sTextureDataManager.get(this); } From 2badc832802ec868fd4368c8e9b990d3a2e78b56 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 3 Jul 2019 18:09:56 +0200 Subject: [PATCH 061/336] TextureDataManager : bugfixes --- es-app/src/guis/GuiMenu.cpp | 9 +++- es-app/src/views/SystemView.cpp | 4 +- es-app/src/views/ViewController.cpp | 14 +++--- es-app/src/views/ViewController.h | 2 +- es-app/src/views/gamelist/IGameListView.cpp | 8 ++-- es-core/src/Renderer_draw_gl.cpp | 11 ++--- es-core/src/Renderer_init_sdlgl.cpp | 15 ++++-- es-core/src/Window.cpp | 15 +++--- es-core/src/components/ImageComponent.cpp | 10 ++-- es-core/src/resources/Font.cpp | 8 +++- es-core/src/resources/Font.h | 2 +- es-core/src/resources/ResourceManager.cpp | 34 ++++++++++---- es-core/src/resources/ResourceManager.h | 11 ++++- es-core/src/resources/TextureData.cpp | 2 +- es-core/src/resources/TextureDataManager.cpp | 35 +++++++------- es-core/src/resources/TextureDataManager.h | 4 +- es-core/src/resources/TextureResource.cpp | 48 +++++++++++--------- es-core/src/resources/TextureResource.h | 4 +- 18 files changed, 138 insertions(+), 98 deletions(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 9c16cb907b..0f9e108e9e 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -689,7 +689,14 @@ void GuiMenu::openUISettings() auto hideSystemView = std::make_shared(mWindow); hideSystemView->setState(Settings::getInstance()->getBool("HideSystemView")); s->addWithLabel(_T("HIDE SYSTEM VIEW"), hideSystemView); - s->addSaveFunc([hideSystemView] { Settings::getInstance()->setBool("HideSystemView", hideSystemView->getState()); }); + s->addSaveFunc([hideSystemView] + { + bool hideSysView = Settings::getInstance()->getBool("HideSystemView"); + Settings::getInstance()->setBool("HideSystemView", hideSystemView->getState()); + + if (!hideSysView && hideSystemView->getState()) + ViewController::get()->goToStart(true); + }); #if defined(_WIN32) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 656e9b2def..2ed5ef9bd0 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -357,11 +357,11 @@ void SystemView::render(const Transform4x4f& parentTrans) return; // nothing to render Transform4x4f trans = getTransform() * parentTrans; - + Vector2f clipPos(trans.translation().x(), trans.translation().y()); if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) return; - + auto systemInfoZIndex = mSystemInfo.getZIndex(); auto minMax = std::minmax(mCarousel.zIndex, systemInfoZIndex); diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 451ea55a07..31c3650088 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -56,7 +56,7 @@ void ViewController::goToStart(bool forceImmediate) if ((*it)->getName() == requestedSystem) { if (hideSystemView || startOnGamelist) - goToGameList(*it); + goToGameList(*it, forceImmediate); else goToSystemView(*it, forceImmediate); @@ -69,7 +69,7 @@ void ViewController::goToStart(bool forceImmediate) } if (hideSystemView || startOnGamelist) - goToGameList(SystemData::sSystemVector.at(0)); + goToGameList(SystemData::sSystemVector.at(0), forceImmediate); else goToSystemView(SystemData::sSystemVector.at(0), forceImmediate); } @@ -126,7 +126,7 @@ void ViewController::goToPrevGameList() goToGameList(system->getPrev()); } -void ViewController::goToGameList(SystemData* system) +void ViewController::goToGameList(SystemData* system, bool forceImmediate) { if(mState.viewing == SYSTEM_SELECT) { @@ -149,7 +149,7 @@ void ViewController::goToGameList(SystemData* system) if (mCurrentView) mCurrentView->onShow(); - playViewTransition(false); + playViewTransition(forceImmediate); } void ViewController::playViewTransition(bool forceImmediate) @@ -494,8 +494,7 @@ void ViewController::render(const Transform4x4f& parentTrans) Vector3f sysEnd = getSystemListView()->getPosition() + Vector3f(getSystemListView()->getSize().x(), getSystemListView()->getSize().y(), 0); // draw systemview -// if ((sysStart.x() == viewStart.x() && sysStart.y() < viewStart.y() && sysEnd.x() > viewEnd.x() && sysEnd.y() > viewEnd.y()) || -// (sysStart.x() < viewEnd.x() && sysStart.y() < viewEnd.y() && sysEnd.x() > viewStart.x() && sysEnd.y() > viewStart.y())) + if (!Settings::getInstance()->getBool("HideSystemView")) getSystemListView()->render(trans); // draw gamelists @@ -505,8 +504,7 @@ void ViewController::render(const Transform4x4f& parentTrans) Vector3f guiStart = it->second->getPosition(); Vector3f guiEnd = it->second->getPosition() + Vector3f(it->second->getSize().x(), it->second->getSize().y(), 0); -// if ((guiStart.x() == viewStart.x() && guiStart.y() < viewStart.y() && guiEnd.x() > viewEnd.x() && guiEnd.y() > viewEnd.y()) || -// (guiStart.x() < viewEnd.x() && guiStart.y() < viewEnd.y() && guiEnd.x() > viewStart.x() && guiEnd.y() > viewStart.y())) + if (guiEnd.x() >= viewStart.x() && guiEnd.y() >= viewStart.y() && guiStart.x() <= viewEnd.x() && guiStart.y() <= viewEnd.y()) it->second->render(trans); } diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h index 27900588f9..392b8ba0f0 100644 --- a/es-app/src/views/ViewController.h +++ b/es-app/src/views/ViewController.h @@ -33,7 +33,7 @@ class ViewController : public GuiComponent // Navigation. void goToNextGameList(); void goToPrevGameList(); - void goToGameList(SystemData* system); + void goToGameList(SystemData* system, bool forceImmediate = false); void goToSystemView(SystemData* system, bool forceImmediate = false); void goToStart(bool forceImmediate = false); void ReloadAndGoToStart(); diff --git a/es-app/src/views/gamelist/IGameListView.cpp b/es-app/src/views/gamelist/IGameListView.cpp index f2335dbb95..15ebbc7beb 100644 --- a/es-app/src/views/gamelist/IGameListView.cpp +++ b/es-app/src/views/gamelist/IGameListView.cpp @@ -49,16 +49,16 @@ void IGameListView::render(const Transform4x4f& parentTrans) { Transform4x4f trans = parentTrans * getTransform(); - Vector2f clipPos(trans.translation().x(), trans.translation().y()); - if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) - return; - float scaleX = trans.r0().x(); float scaleY = trans.r1().y(); Vector2i pos((int)Math::round(trans.translation()[0]), (int)Math::round(trans.translation()[1])); Vector2i size((int)Math::round(mSize.x() * scaleX), (int)Math::round(mSize.y() * scaleY)); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), size.x(), size.y())) + return; + Renderer::pushClipRect(pos, size); renderChildren(trans); Renderer::popClipRect(); diff --git a/es-core/src/Renderer_draw_gl.cpp b/es-core/src/Renderer_draw_gl.cpp index 5fbd0cefb3..30b1a7ee09 100644 --- a/es-core/src/Renderer_draw_gl.cpp +++ b/es-core/src/Renderer_draw_gl.cpp @@ -116,7 +116,7 @@ namespace Renderer { { return (value >= min) && (value <= max); } - + bool rectOverlap(ClipRect A, ClipRect B) { bool xOverlap = valueInRange(A.x, B.x, B.x + B.w) || @@ -132,17 +132,17 @@ namespace Renderer { { ClipRect screen = ClipRect(0, 0, Renderer::getWindowWidth(), Renderer::getWindowHeight()); ClipRect box = ClipRect(x, y, w, h); - + /* if (w > 0 && x + w <= 0) return false; if (h > 0 && y + h <= 0) return false; - + if (x == screen.w || y == screen.h) return false; - - if (!rectOverlap(screen, box)) + */ + if (!rectOverlap(box, screen)) return false; if (clipStack.empty()) @@ -152,7 +152,6 @@ namespace Renderer { return rectOverlap(screen, box); } - void drawRect(float x, float y, float w, float h, unsigned int color, GLenum blend_sfactor, GLenum blend_dfactor) { drawRect((int)Math::round(x), (int)Math::round(y), (int)Math::round(w), (int)Math::round(h), color, blend_sfactor, blend_dfactor); diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index c51fddf94a..9f01010c69 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -33,6 +33,8 @@ namespace Renderer SDL_Window* sdlWindow = NULL; SDL_GLContext sdlContext = NULL; + Vector2i sdlWindowPosition = Vector2i(SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED); + bool createSurface() { LOG(LogInfo) << "Creating surface..."; @@ -72,8 +74,8 @@ namespace Renderer screenRotate = Settings::getInstance()->getInt("ScreenRotate") ? Settings::getInstance()->getInt("ScreenRotate") : 0; sdlWindow = SDL_CreateWindow("EmulationStation", - SDL_WINDOWPOS_UNDEFINED, - SDL_WINDOWPOS_UNDEFINED, + sdlWindowPosition.x(), + sdlWindowPosition.y(), windowWidth, windowHeight, SDL_WINDOW_OPENGL | (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN)) ); @@ -147,7 +149,14 @@ namespace Renderer } void destroySurface() - { + { + if (Settings::getInstance()->getBool("Windowed")) + { + int x; int y; + SDL_GetWindowPosition(sdlWindow, &x, &y); + sdlWindowPosition = Vector2i(x, y); // Save position to restore it later + } + SDL_GL_DeleteContext(sdlContext); sdlContext = NULL; diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index e3cdacedbf..63cf190626 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -109,6 +109,8 @@ void Window::deinit(bool deinitRenderer) (*i)->onHide(); } + TRACE("-- deinit"); + InputManager::getInstance()->deinit(); TextureResource::resetCache(); ResourceManager::getInstance()->unloadAll(); @@ -317,7 +319,7 @@ void Window::endRenderLoadingScreen() void Window::renderLoadingScreen(std::string text, float percent) { if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); + mSplash = TextureResource::get(":/splash.svg", false, true, false, false); Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); @@ -369,6 +371,9 @@ void Window::renderLoadingScreen(std::string text, float percent) void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) { + if (mSplash == NULL) + mSplash = TextureResource::get(":/splash.svg", false, true, false, false); + Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); @@ -376,16 +381,8 @@ void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) ImageComponent splash(this, true); splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); -#if defined(_WIN32) - if (mSplash == NULL) - mSplash = TextureResource::get(":/splash.svg", false, true, true); -#endif - if (mSplash != NULL) - { - mSplash->reload(); // Ensure splash is loaded splash.setImage(mSplash); - } else splash.setImage(":/splash.svg"); diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index ae726dba91..e3d15f6229 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -148,10 +148,10 @@ void ImageComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) if (mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) mTexture.reset(); else - mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic, maxSize); + mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic, true, maxSize); } else - mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, maxSize); + mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, true, maxSize); resize(); } @@ -351,11 +351,11 @@ void ImageComponent::render(const Transform4x4f& parentTrans) return; Transform4x4f trans = parentTrans * getTransform(); - + Vector2f clipPos(trans.translation().x(), trans.translation().y()); if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) return; - + Renderer::setMatrix(trans); if (mTexture) @@ -375,7 +375,7 @@ void ImageComponent::render(const Transform4x4f& parentTrans) // when it finally loads fadeIn(mTexture->bind()); - if (mColorShift & 0xff) + // if (mColorShift & 0xff) { glEnable(GL_TEXTURE_2D); glEnable(GL_BLEND); diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index 216c2e3d92..42d9c6529a 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -107,12 +107,16 @@ void Font::reload() mLoaded = true; } -void Font::unload() +bool Font::unload() { if (mLoaded) + { unloadTextures(); + mLoaded = false; + return true; + } - mLoaded = false; + return false; } std::shared_ptr Font::get(int size, const std::string& path) diff --git a/es-core/src/resources/Font.h b/es-core/src/resources/Font.h index 2813ab1597..63d06d6e6c 100644 --- a/es-core/src/resources/Font.h +++ b/es-core/src/resources/Font.h @@ -53,7 +53,7 @@ class Font : public IReloadable float getHeight(float lineSpacing = 1.5f) const; float getLetterHeight(); - void unload() override; + bool unload() override; void reload() override; int getSize() const; diff --git a/es-core/src/resources/ResourceManager.cpp b/es-core/src/resources/ResourceManager.cpp index f06603a6b5..1693bb4ac2 100644 --- a/es-core/src/resources/ResourceManager.cpp +++ b/es-core/src/resources/ResourceManager.cpp @@ -93,18 +93,22 @@ bool ResourceManager::fileExists(const std::string& path) const return Utils::FileSystem::exists(path); } +#include "resources/TextureResource.h" + void ResourceManager::unloadAll() { auto iter = mReloadables.cbegin(); while(iter != mReloadables.cend()) - { - if (!iter->expired()) + { + std::shared_ptr info = *iter; + + if (!info->data.expired()) { - iter->lock()->unload(); + info->reload = info->data.lock()->unload(); iter++; - }else{ - iter = mReloadables.erase(iter); } + else + iter = mReloadables.erase(iter); } } @@ -113,17 +117,27 @@ void ResourceManager::reloadAll() auto iter = mReloadables.cbegin(); while(iter != mReloadables.cend()) { - if(!iter->expired()) + std::shared_ptr info = *iter; + + if (!info->data.expired()) { - iter->lock()->reload(); + if (info->reload) + { + info->data.lock()->reload(); + info->reload = false; + } + iter++; - }else{ - iter = mReloadables.erase(iter); } + else + iter = mReloadables.erase(iter); } } void ResourceManager::addReloadable(std::weak_ptr reloadable) { - mReloadables.push_back(reloadable); + std::shared_ptr info = std::make_shared(); + info->data = reloadable; + info->reload = false; + mReloadables.push_back(info); } diff --git a/es-core/src/resources/ResourceManager.h b/es-core/src/resources/ResourceManager.h index f66ed8c7fa..34f097f009 100644 --- a/es-core/src/resources/ResourceManager.h +++ b/es-core/src/resources/ResourceManager.h @@ -20,7 +20,7 @@ class ResourceManager; class IReloadable { public: - virtual void unload() = 0; + virtual bool unload() = 0; virtual void reload() = 0; }; @@ -45,7 +45,14 @@ class ResourceManager ResourceData loadFile(const std::string& path, size_t size) const; - std::list< std::weak_ptr > mReloadables; + class ReloadableInfo + { + public: + std::weak_ptr data; + bool reload; + }; + + std::list> mReloadables; // std::weak_ptr }; #endif // ES_CORE_RESOURCES_RESOURCE_MANAGER_H diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 72d90bec8c..bc8d42a4bc 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -207,7 +207,7 @@ bool TextureData::load() if (!mPath.empty()) { #ifdef WIN32 - //TRACE("TextureData::load(" << mPath << ", " << mTextureID << ")") + // TRACE("TextureData::load(" << mPath << ", " << mTextureID << ")") #endif std::shared_ptr& rm = ResourceManager::getInstance(); diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index 94306277a6..e3b70497f1 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -48,7 +48,7 @@ void TextureDataManager::remove(const TextureResource* key) } } -std::shared_ptr TextureDataManager::get(const TextureResource* key) +std::shared_ptr TextureDataManager::get(const TextureResource* key, bool enableLoading) { // If it's in the cache then we want to remove it from it's current location and // move it to the top @@ -65,7 +65,7 @@ std::shared_ptr TextureDataManager::get(const TextureResource* key) mTextureLookup[key] = mTextures.cbegin(); // Make sure it's loaded or queued for loading - if (!tex->isLoaded()) // FCATMP + if (enableLoading && !tex->isLoaded()) // FCATMP load(tex); } return tex; @@ -122,17 +122,11 @@ void TextureDataManager::load(std::shared_ptr tex, bool block) size_t size = TextureResource::getTotalMemUsage(); size_t max_texture = (size_t)Settings::getInstance()->getInt("MaxVRAM") * 1024 * 1024; - int cleanedMemory = 0; - int lastTime = SDL_GetTicks(); - for (auto it = mTextures.crbegin(); it != mTextures.crend(); ++it) { if (size < max_texture) break; - if (!(*it)->isLoaded()) // FCA added to avoid calling TextureResource::getTotalMemUsage() if texture is not loaded - continue; - (*it)->releaseVRAM(); (*it)->releaseRAM(); @@ -140,24 +134,14 @@ void TextureDataManager::load(std::shared_ptr tex, bool block) // any VRAM yet but it will be. Remove it from the loader queue mLoader->remove(*it); size = TextureResource::getTotalMemUsage(); - - cleanedMemory++; } -#ifdef WIN32 - if (cleanedMemory > 0) - { - lastTime = SDL_GetTicks() - lastTime; - TRACE("cleanedMemory : " << cleanedMemory << " items in " << lastTime << " ms") - } -#endif - if (!block) { mLoader->load(tex); } else - { + { mLoader->remove(tex); tex->load(); } @@ -260,3 +244,16 @@ size_t TextureLoader::getQueueSize() } return mem; } + +void TextureLoader::clearQueue() +{ + // Just abort any waiting texture + mTextureDataQ.clear(); + mTextureDataLookup.clear(); +} + +void TextureDataManager::clearQueue() +{ + if (mLoader != nullptr) + mLoader->clearQueue(); +} \ No newline at end of file diff --git a/es-core/src/resources/TextureDataManager.h b/es-core/src/resources/TextureDataManager.h index c7b3a029d8..1e2b0e6ff4 100644 --- a/es-core/src/resources/TextureDataManager.h +++ b/es-core/src/resources/TextureDataManager.h @@ -20,6 +20,7 @@ class TextureLoader void load(std::shared_ptr textureData); void remove(std::shared_ptr textureData); + void clearQueue(); size_t getQueueSize(); @@ -63,7 +64,7 @@ class TextureDataManager // will be deleted when the other thread has finished with it void remove(const TextureResource* key); - std::shared_ptr get(const TextureResource* key); + std::shared_ptr get(const TextureResource* key, bool enableLoading = true); bool bind(const TextureResource* key); // Get the total size of all textures managed by this object, loaded and unloaded in bytes @@ -76,6 +77,7 @@ class TextureDataManager // Load a texture, freeing resources as necessary to make space void load(std::shared_ptr tex, bool block = false); + void clearQueue(); private: std::list > mTextures; diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index 1bceee7aee..d32b2632e9 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -95,7 +95,8 @@ bool TextureResource::isTiled() const { if (mTextureData != nullptr) return mTextureData->tiled(); - std::shared_ptr data = sTextureDataManager.get(this); + + std::shared_ptr data = sTextureDataManager.get(this, false); return data->tiled(); } @@ -114,11 +115,12 @@ bool TextureResource::bind() void TextureResource::resetCache() { + sTextureDataManager.clearQueue(); sPermanentTextureMap.clear(); sTextureMap.clear(); } -std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic, MaxSizeInfo maxSize) +std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic, bool asReloadable, MaxSizeInfo maxSize) { std::shared_ptr& rm = ResourceManager::getInstance(); @@ -169,26 +171,20 @@ std::shared_ptr TextureResource::get(const std::string& path, b // need to create it std::shared_ptr tex; tex = std::shared_ptr(new TextureResource(key.first, tile, dynamic, maxSize)); - std::shared_ptr data = sTextureDataManager.get(tex.get()); - - // is it an SVG? - // if (key.first.substr(key.first.size() - 4, std::string::npos) != ".svg") // FCATMP + std::shared_ptr data = sTextureDataManager.get(tex.get(), !forceLoad); + + if (asReloadable) // // is it an SVG // if (key.first.substr(key.first.size() - 4, std::string::npos) != ".svg") // FCATMP { // Probably not. Add it to our map. We don't add SVGs because 2 svgs might be rasterized at different sizes - // FCA useless -> If the svg is too small, it will be reloaded bigger with setSourceSize... - - if (canonicalPath.length() > 0 && canonicalPath[0] == ':') + // FCA useless -> If the svg is too small, it will be reloaded bigger with setSourceSize... + /* if (canonicalPath.length() > 0 && canonicalPath[0] == ':') sPermanentTextureMap[key] = std::shared_ptr(tex); - else + else*/ sTextureMap[key] = std::shared_ptr(tex); + + rm->addReloadable(tex); } - // Add it to the reloadable list, exclusion for splash.svg manually managed -#ifdef WIN32 - if (path != ":/splash.svg") -#endif - rm->addReloadable(tex); - if (data != nullptr) data->setMaxSize(maxSize); @@ -196,7 +192,8 @@ std::shared_ptr TextureResource::get(const std::string& path, b if (forceLoad) { tex->mForceLoad = forceLoad; - data->load(); + if (data != nullptr && !data->isLoaded()) + data->load(); } return tex; @@ -259,17 +256,24 @@ size_t TextureResource::getTotalTextureSize() return total; } -void TextureResource::unload() +bool TextureResource::unload() { // Release the texture's resources std::shared_ptr data; if (mTextureData == nullptr) - data = sTextureDataManager.get(this); + data = sTextureDataManager.get(this, false); else data = mTextureData; - data->releaseVRAM(); - data->releaseRAM(); + if (data != nullptr && data->isLoaded()) + { + data->releaseVRAM(); + data->releaseRAM(); + + return true; + } + + return false; } void TextureResource::reload() @@ -278,4 +282,6 @@ void TextureResource::reload() // For manually loaded textures we have to reload them here if (mTextureData && !mTextureData->isLoaded()) mTextureData->load(); +// else if (mTextureData == nullptr) +// sTextureDataManager.get(this); } diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index 272ad270c0..5b16199f65 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -48,7 +48,7 @@ class MaxSizeInfo class TextureResource : public IReloadable { public: - static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true, MaxSizeInfo maxSize = MaxSizeInfo()); + static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true, bool asReloadable = true, MaxSizeInfo maxSize = MaxSizeInfo()); void initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height); virtual void initFromMemory(const char* file, size_t length); @@ -69,7 +69,7 @@ class TextureResource : public IReloadable static void resetCache(); public: - virtual void unload(); + virtual bool unload(); virtual void reload(); protected: From 04c6192e7bf33d5d482945527a4c8b0597623a89 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 4 Jul 2019 01:15:19 +0200 Subject: [PATCH 062/336] Texture manager : optimization & bugfixes - Reloading is now exactly as it was before unloading. SystemView : Stop using logos as non dynamic when they are PNG or JPGs -> Themes generally uses big logo sizes. Making them non dynamic allows them to be unloaded when room is needed. Image Vram optimizations : bugfixes --- es-app/src/views/SystemView.cpp | 5 ++-- es-core/src/ImageIO.cpp | 18 +++++++------- es-core/src/ImageIO.h | 2 +- es-core/src/Renderer_draw_gl.cpp | 4 +-- es-core/src/components/GridTileComponent.cpp | 4 +-- es-core/src/components/ImageComponent.cpp | 2 +- es-core/src/components/SwitchComponent.cpp | 3 +-- es-core/src/resources/TextureData.cpp | 26 ++++++++++++++++---- es-core/src/resources/TextureData.h | 6 +---- es-core/src/resources/TextureDataManager.cpp | 3 +++ es-core/src/resources/TextureResource.cpp | 18 ++++++++------ es-core/src/resources/TextureResource.h | 4 +++ 12 files changed, 59 insertions(+), 36 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 2ed5ef9bd0..11fc167373 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -53,8 +53,9 @@ void SystemView::populate() if ((!path.empty() && ResourceManager::getInstance()->fileExists(path)) || (!defaultPath.empty() && ResourceManager::getInstance()->fileExists(defaultPath))) - { - ImageComponent* logo = new ImageComponent(mWindow, false, false); + { + // Remove dynamic flags for png & jpg files : themes can contain oversized images that can't be unloaded by the TextureResource manager + ImageComponent* logo = new ImageComponent(mWindow, false, Utils::String::toLower(Utils::FileSystem::getExtension(path)) != ".svg"); logo->setMaxSize(mCarousel.logoSize * mCarousel.logoScale); logo->applyTheme(theme, "system", "logo", ThemeFlags::PATH | ThemeFlags::SIZE | ThemeFlags::COLOR); logo->setRotateByTargetSize(true); diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index e56d22e975..8c0bf4f7cd 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -86,8 +86,14 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d //public static Rectangle GetPictureRect(Size imageSize, Rectangle rcPhoto, bool outerZooming = false, bool sourceRect = false) -Vector2i ImageIO::adjustPictureSize(Vector2i imageSize, Vector2i maxSize) +Vector2i ImageIO::adjustPictureSize(Vector2i imageSize, Vector2i maxSize, bool externSize) { + if (externSize) + { + Vector2f szf = adjustExternPictureSizef(Vector2f(imageSize.x(), imageSize.y()), Vector2f(maxSize.x(), maxSize.y())); + return Vector2i(szf.x(), szf.y()); + } + int cxDIB = imageSize.x(); int cyDIB = imageSize.y(); int iMaxX = maxSize.x(); @@ -170,16 +176,10 @@ unsigned char* ImageIO::loadFromMemoryRGBA32Ex(const unsigned char * data, const if (maxWidth > 0 && maxHeight > 0 && (width > maxWidth || height > maxHeight)) { - Vector2i sz = adjustPictureSize(Vector2i(width, height), Vector2i(maxWidth, maxHeight)); - if (externZoom) - { - Vector2f szf = adjustExternPictureSizef(Vector2f(width, height), Vector2f(maxWidth, maxHeight)); - sz = Vector2i(szf.x(), szf.y()); // adjustPictureSize(Vector2i(width, height), Vector2i(maxWidth, maxHeight)); - } - + Vector2i sz = adjustPictureSize(Vector2i(width, height), Vector2i(maxWidth, maxHeight), externZoom); if (sz.x() != width || sz.y() != height) { - FIBITMAP* imageRescaled = FreeImage_Rescale(fiBitmap, sz.x(), sz.y(), FILTER_BICUBIC); + FIBITMAP* imageRescaled = FreeImage_Rescale(fiBitmap, sz.x(), sz.y(), FILTER_BOX); FreeImage_Unload(fiBitmap); fiBitmap = imageRescaled; diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index e1ba731cf1..9bb79022cc 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -15,7 +15,7 @@ class ImageIO static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight, bool externZoom, Vector2i& baseSize, Vector2i& packedSize); static void flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height); - static Vector2i adjustPictureSize(Vector2i imageSize, Vector2i maxSize); + static Vector2i adjustPictureSize(Vector2i imageSize, Vector2i maxSize, bool externSize = false); static Vector2f adjustExternPictureSizef(Vector2f imageSize, Vector2f maxSize); }; diff --git a/es-core/src/Renderer_draw_gl.cpp b/es-core/src/Renderer_draw_gl.cpp index 30b1a7ee09..4e28b73005 100644 --- a/es-core/src/Renderer_draw_gl.cpp +++ b/es-core/src/Renderer_draw_gl.cpp @@ -132,7 +132,7 @@ namespace Renderer { { ClipRect screen = ClipRect(0, 0, Renderer::getWindowWidth(), Renderer::getWindowHeight()); ClipRect box = ClipRect(x, y, w, h); - /* + if (w > 0 && x + w <= 0) return false; @@ -141,7 +141,7 @@ namespace Renderer { if (x == screen.w || y == screen.h) return false; - */ + if (!rectOverlap(box, screen)) return false; diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 6c02bef664..e28b019ac9 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -503,9 +503,9 @@ void GridTileComponent::setImage(const std::string& path) mCurrentPath = path; if (mSelectedProperties.mSize.x() > mSize.x()) - mImage->setImage(path, false, MaxSizeInfo(mSelectedProperties.mSize, mSelectedProperties.mImageSizeMode == "minSize")); + mImage->setImage(path, false, MaxSizeInfo(mSelectedProperties.mSize, mSelectedProperties.mImageSizeMode != "maxSize")); else - mImage->setImage(path, false, MaxSizeInfo(mSize, mSelectedProperties.mImageSizeMode == "minSize")); + mImage->setImage(path, false, MaxSizeInfo(mSize, mSelectedProperties.mImageSizeMode != "maxSize")); resize(); } diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index e3d15f6229..437866918b 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -141,7 +141,7 @@ void ImageComponent::setDefaultImage(std::string path) void ImageComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) { if (!maxSize.isExternalZoomKnown()) - maxSize = MaxSizeInfo(maxSize.x(), maxSize.y(), mTargetIsMin); + maxSize = MaxSizeInfo(maxSize.x(), maxSize.y(), !mTargetIsMax); if (path.empty() || !ResourceManager::getInstance()->fileExists(path)) { diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index e1b1d7b9b2..88a8f29e3c 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -9,7 +9,7 @@ SwitchComponent::SwitchComponent(Window* window, bool state) : GuiComponent(wind float height = Font::get(FONT_SIZE_MEDIUM)->getLetterHeight(); - mImage.setImage(ThemeData::getMenuTheme()->Icons.off, false, MaxSizeInfo(height, height)); + mImage.setImage(ThemeData::getMenuTheme()->Icons.off); mImage.setResize(0, height); mImage.setColorShift(menuTheme->Text.color); @@ -78,7 +78,6 @@ void SwitchComponent::onStateChanged() { auto theme = ThemeData::getMenuTheme(); mImage.setImage(mState ? theme->Icons.on : theme->Icons.off); - //mImage.setImage(mState ? ":/on.svg" : ":/off.svg"); } std::vector SwitchComponent::getHelpPrompts() diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index bc8d42a4bc..282124b635 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -88,18 +88,16 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length mBaseSize = Vector2i(mWidth, mHeight); - if (mMaxSize.x() > 0 && mMaxSize.y() > 0 && mHeight < mMaxSize.y() && mWidth < mMaxSize.y()) // FCATMP + if (mMaxSize.x() > 0 && mMaxSize.y() > 0 && mHeight < mMaxSize.y() && mWidth < mMaxSize.x()) // FCATMP { - Vector2i sz = ImageIO::adjustPictureSize(Vector2i(mWidth, mHeight), Vector2i(mMaxSize.x(), mMaxSize.y())); - mWidth = sz.x(); + Vector2i sz = ImageIO::adjustPictureSize(Vector2i(mWidth, mHeight), Vector2i(mMaxSize.x(), mMaxSize.y()), mMaxSize.externalZoom()); mHeight = sz.y(); mWidth = (int)((mHeight * svgImage->width) / svgImage->height); } if (OPTIMIZEVRAM && mMaxSize.x() > 0 && mMaxSize.y() > 0 && (mWidth > mMaxSize.x() || mHeight > mMaxSize.y())) { - Vector2i sz = ImageIO::adjustPictureSize(Vector2i(mWidth, mHeight), Vector2i(mMaxSize.x(), mMaxSize.y())); - mWidth = sz.x(); + Vector2i sz = ImageIO::adjustPictureSize(Vector2i(mWidth, mHeight), Vector2i(mMaxSize.x(), mMaxSize.y()), mMaxSize.externalZoom()); mHeight = sz.y(); mWidth = (mHeight * svgImage->width) / svgImage->height; @@ -169,6 +167,24 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng return initFromRGBAEx(imageRGBA, width, height); } + +void TextureData::setMaxSize(MaxSizeInfo maxSize) +{ + if (mSourceWidth == 0 || mSourceHeight == 0) + mMaxSize = maxSize; + else + { + Vector2i value = ImageIO::adjustPictureSize(Vector2i(mSourceWidth, mSourceHeight), Vector2i(mMaxSize.x(), mMaxSize.y()), mMaxSize.externalZoom()); + Vector2i newVal = ImageIO::adjustPictureSize(Vector2i(mSourceWidth, mSourceHeight), Vector2i(maxSize.x(), maxSize.y()), mMaxSize.externalZoom()); + + if (newVal.x() > value.x() || newVal.y() > value.y()) + mMaxSize = maxSize; + + //if (mMaxSize.x() < maxSize.x() || mMaxSize.y() < maxSize.y()) + } +}; + + bool TextureData::initFromRGBA(const unsigned char* dataRGBA, size_t width, size_t height) { // If already initialised then don't read again diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index dcbcf44afd..3a9078ecf5 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -45,11 +45,7 @@ class TextureData // Release the texture from conventional RAM void releaseRAM(); - void setMaxSize(MaxSizeInfo maxSize) - { - if (mMaxSize.x() < maxSize.x() || mMaxSize.y() < maxSize.y()) - mMaxSize = maxSize; - }; + void setMaxSize(MaxSizeInfo maxSize); // Get the amount of VRAM currenty used by this texture size_t getVRAMUsage(); diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index e3b70497f1..16e9e43075 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -127,6 +127,9 @@ void TextureDataManager::load(std::shared_ptr tex, bool block) if (size < max_texture) break; + if ((*it) == tex) + continue; + (*it)->releaseVRAM(); (*it)->releaseRAM(); diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index d32b2632e9..946437995f 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -12,6 +12,10 @@ std::set TextureResource::sAllTextures; TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic, MaxSizeInfo maxSize) : mTextureData(nullptr), mForceLoad(false) { +#if _DEBUG + mPath = path; +#endif + // Create a texture data object for this texture if (!path.empty()) { @@ -46,7 +50,9 @@ TextureResource::TextureResource(const std::string& path, bool tile, bool dynami // Create a texture managed by this class because it cannot be dynamically loaded and unloaded mTextureData = std::shared_ptr(new TextureData(tile)); } - sAllTextures.insert(this); + + if (sAllTextures.find(this) == sAllTextures.end()) + sAllTextures.insert(this); } TextureResource::~TextureResource() @@ -116,8 +122,6 @@ bool TextureResource::bind() void TextureResource::resetCache() { sTextureDataManager.clearQueue(); - sPermanentTextureMap.clear(); - sTextureMap.clear(); } std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic, bool asReloadable, MaxSizeInfo maxSize) @@ -177,9 +181,9 @@ std::shared_ptr TextureResource::get(const std::string& path, b { // Probably not. Add it to our map. We don't add SVGs because 2 svgs might be rasterized at different sizes // FCA useless -> If the svg is too small, it will be reloaded bigger with setSourceSize... - /* if (canonicalPath.length() > 0 && canonicalPath[0] == ':') + if (canonicalPath.length() > 0 && canonicalPath[0] == ':') sPermanentTextureMap[key] = std::shared_ptr(tex); - else*/ + else sTextureMap[key] = std::shared_ptr(tex); rm->addReloadable(tex); @@ -282,6 +286,6 @@ void TextureResource::reload() // For manually loaded textures we have to reload them here if (mTextureData && !mTextureData->isLoaded()) mTextureData->load(); -// else if (mTextureData == nullptr) -// sTextureDataManager.get(this); + else if (mTextureData == nullptr) + sTextureDataManager.get(this); } diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index 5b16199f65..4d9a5a6a5e 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -91,6 +91,10 @@ class TextureResource : public IReloadable static std::map< TextureKeyType, std::weak_ptr > sTextureMap; // map of textures, used to prevent duplicate textures static std::map< TextureKeyType, std::shared_ptr > sPermanentTextureMap; // map of textures, used to prevent duplicate textures // FCAWEAK static std::set sAllTextures; // Set of all textures, used for memory management + +#if _DEBUG + std::string mPath; +#endif }; #endif // ES_CORE_RESOURCES_TEXTURE_RESOURCE_H From 7ad3808290893f557f2488a6135437b626a6a8f1 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 4 Jul 2019 17:34:11 +0200 Subject: [PATCH 063/336] Theming helper : Press F5 to reload the theme + ctrl I / ctrl t / ctrl g for debugging now possible without --debug command line. Monitor : Support for -- monitor N command line, to run ES on a specific monitor ( starting at index 0 ) - a workaround for retroarch is in place to run on the same monitor. Ninepatch : Support add item as extra + suopport for color/cornerSize, centerColor & edgeColor in the ninepatch element. Text theming : Support for padding ( 4 points ) --- es-app/src/components/TextListComponent.h | 13 +++++ es-app/src/main.cpp | 14 ++++- es-app/src/views/SystemView.cpp | 6 +-- es-app/src/views/ViewController.cpp | 8 +++ es-core/src/Renderer_init_sdlgl.cpp | 33 ++++++++++++ es-core/src/Settings.cpp | 5 +- es-core/src/ThemeData.cpp | 11 ++++ es-core/src/Window.cpp | 12 ++--- es-core/src/components/ImageGridComponent.h | 28 +++++++--- es-core/src/components/NinePatchComponent.cpp | 22 +++++++- es-core/src/components/TextComponent.cpp | 28 ++++++---- es-core/src/components/TextComponent.h | 2 + es-core/src/components/VideoComponent.cpp | 7 +++ es-core/src/platform.cpp | 54 ++++++++++++++++++- es-core/src/resources/TextureData.cpp | 1 + es-core/src/utils/FileSystemUtil.cpp | 18 +++---- es-core/src/utils/FileSystemUtil.h | 6 +-- resources/locale/fr/emulationstation2.po | 4 ++ 18 files changed, 224 insertions(+), 48 deletions(-) diff --git a/es-app/src/components/TextListComponent.h b/es-app/src/components/TextListComponent.h index 282ac14f2f..ae39788b71 100644 --- a/es-app/src/components/TextListComponent.h +++ b/es-app/src/components/TextListComponent.h @@ -7,6 +7,7 @@ #include "utils/StringUtil.h" #include "Log.h" #include "Sound.h" +#include "Settings.h" #include class TextCache; @@ -140,6 +141,18 @@ void TextListComponent::render(const Transform4x4f& parentTrans) if(size() == 0) return; + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + + if (Settings::getInstance()->getBool("DebugGrid")) + { + Renderer::setMatrix(trans); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0xFF000033); + Renderer::setMatrix(parentTrans); + } + + const float entrySize = Math::max(font->getHeight(1.0), (float)font->getSize()) * mLineSpacing; int startEntry = 0; diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index d7570e8c68..b925c5eb02 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -52,7 +52,19 @@ bool parseArgs(int argc, char* argv[]) for(int i = 1; i < argc; i++) { - if (strcmp(argv[i], "--resolution") == 0) + if (strcmp(argv[i], "--monitor") == 0) + { + if (i >= argc - 1) + { + std::cerr << "Invalid monitor supplied."; + return false; + } + + int monitorId = atoi(argv[i + 1]); + i++; // skip the argument value + Settings::getInstance()->setInt("MonitorID", monitorId); + } + else if (strcmp(argv[i], "--resolution") == 0) { if(i >= argc - 2) { diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 11fc167373..0388183f15 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -395,10 +395,10 @@ std::vector SystemView::getHelpPrompts() prompts.push_back(HelpPrompt("left/right", _T("CHOOSE"))); prompts.push_back(HelpPrompt("a", _T("SELECT"))); - //prompts.push_back(HelpPrompt("x", "random")); + prompts.push_back(HelpPrompt("x", _T("RANDOM"))); -// if (!UIModeController::getInstance()->isUIModeKid() && Settings::getInstance()->getBool("ScreenSaverControls")) - // prompts.push_back(HelpPrompt("select", "launch screensaver")); + if (!UIModeController::getInstance()->isUIModeKid() && Settings::getInstance()->getBool("ScreenSaverControls")) + prompts.push_back(HelpPrompt("select", _T("LAUNCH SCREENSAVER"))); return prompts; } diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 31c3650088..f0595ba2e4 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -447,6 +447,14 @@ bool ViewController::input(InputConfig* config, Input input) if (mLockInput) return true; + if (config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_F5) + { + mWindow->renderLoadingScreen("Reloading..."); + ViewController::get()->reloadAll(); + mWindow->endRenderLoadingScreen(); + return true; + } + // open menu if(!UIModeController::getInstance()->isUIModeKid() && config->isMappedTo("start", input) && input.value != 0) { diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index 9f01010c69..3827ac969e 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -62,6 +62,8 @@ namespace Renderer SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1); #endif + + SDL_DisplayMode dispMode; SDL_GetDesktopDisplayMode(0, &dispMode); @@ -73,6 +75,37 @@ namespace Renderer screenOffsetY = Settings::getInstance()->getInt("ScreenOffsetY") ? Settings::getInstance()->getInt("ScreenOffsetY") : 0; screenRotate = Settings::getInstance()->getInt("ScreenRotate") ? Settings::getInstance()->getInt("ScreenRotate") : 0; + int monitorId = Settings::getInstance()->getInt("MonitorID"); + if (monitorId >=0 && sdlWindowPosition == Vector2i(SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED)) + { + int displays = SDL_GetNumVideoDisplays(); + if (displays >= monitorId) + { + SDL_Rect rc; + SDL_GetDisplayBounds(monitorId, &rc); + + sdlWindowPosition = Vector2i(rc.x, rc.y); + + if (Settings::getInstance()->getBool("Windowed") && (Settings::getInstance()->getInt("WindowWidth") || Settings::getInstance()->getInt("ScreenWidth"))) + { + if (windowWidth != rc.w || windowHeight != rc.h) + { + sdlWindowPosition = Vector2i( + rc.x + (rc.w - windowWidth) / 2, + rc.y + (rc.h - windowHeight) / 2 + ); + } + } + else + { + windowWidth = rc.w; + windowHeight = rc.h; + screenWidth = rc.w; + screenHeight = rc.h; + } + } + } + sdlWindow = SDL_CreateWindow("EmulationStation", sdlWindowPosition.x(), sdlWindowPosition.y(), diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 71089da1d9..7f6174ea2e 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -30,13 +30,14 @@ std::vector settings_dont_save { #endif { "FullscreenBorderless" }, { "Windowed" }, - { "WindowWidth" }, + { "WindowWidth" }, { "WindowHeight" }, { "ScreenWidth" }, { "ScreenHeight" }, { "ScreenOffsetX" }, { "ScreenOffsetY" }, { "ScreenRotate" }, + { "MonitorID" }, { "ExePath" }, { "HomePath" } }; @@ -191,7 +192,7 @@ void Settings::setDefaults() mIntMap["ScreenOffsetX"] = 0; mIntMap["ScreenOffsetY"] = 0; mIntMap["ScreenRotate"] = 0; - + mIntMap["MonitorID"] = -1; mStringMap["ExePath"] = ""; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 5c9b876524..894b00b6fb 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -2,6 +2,7 @@ #include "components/ImageComponent.h" #include "components/TextComponent.h" +#include "components/NinePatchComponent.h" #include "utils/FileSystemUtil.h" #include "Log.h" #include "platform.h" @@ -79,6 +80,8 @@ std::map> The { "glowColor", COLOR }, { "glowSize", FLOAT }, + { "padding", NORMALIZED_RECT }, + { "zIndex", FLOAT } } }, { "textlist", { { "pos", NORMALIZED_PAIR }, @@ -110,6 +113,12 @@ std::map> The { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, { "path", PATH }, + + { "color", COLOR }, + { "cornerSize", NORMALIZED_PAIR }, + { "centerColor", COLOR }, + { "edgeColor", COLOR }, + { "zIndex", FLOAT } } }, { "datetime", { { "pos", NORMALIZED_PAIR }, @@ -952,6 +961,8 @@ std::vector ThemeData::makeExtras(const std::shared_ptrinit(); - - //if (initRenderer) - ResourceManager::getInstance()->reloadAll(); + ResourceManager::getInstance()->reloadAll(); //keep a reference to the default fonts, so they don't keep getting destroyed/recreated if(mDefaultFonts.empty()) @@ -109,8 +107,6 @@ void Window::deinit(bool deinitRenderer) (*i)->onHide(); } - TRACE("-- deinit"); - InputManager::getInstance()->deinit(); TextureResource::resetCache(); ResourceManager::getInstance()->unloadAll(); @@ -167,17 +163,17 @@ void Window::input(InputConfig* config, Input input) if (cancelScreenSaver()) return; - if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_g && SDL_GetModState() & KMOD_LCTRL/* && Settings::getInstance()->getBool("Debug")*/) { // toggle debug grid with Ctrl-G Settings::getInstance()->setBool("DebugGrid", !Settings::getInstance()->getBool("DebugGrid")); } - else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_t && SDL_GetModState() & KMOD_LCTRL/* && Settings::getInstance()->getBool("Debug")*/) { // toggle TextComponent debug view with Ctrl-T Settings::getInstance()->setBool("DebugText", !Settings::getInstance()->getBool("DebugText")); } - else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && SDL_GetModState() & KMOD_LCTRL && Settings::getInstance()->getBool("Debug")) + else if(config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_i && SDL_GetModState() & KMOD_LCTRL/* && Settings::getInstance()->getBool("Debug")*/) { // toggle TextComponent debug view with Ctrl-I Settings::getInstance()->setBool("DebugImage", !Settings::getInstance()->getBool("DebugImage")); diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index af4cf4b516..e5b3d821a5 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -141,7 +141,7 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList::render(const Transform4x4f& parentTrans) if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) return; - float offsetX = isVertical() ? 0 : mCamera * mCameraDirection * (mTileSize.x() + mMargin.x()); - float offsetY = isVertical() ? mCamera * mCameraDirection * (mTileSize.y() + mMargin.y()) : 0; - - if (Settings::getInstance()->getBool("DebugImage")) + if (Settings::getInstance()->getBool("DebugGrid")) { Renderer::setMatrix(trans); - Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0xFF000055); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0xFF000033); Renderer::setMatrix(parentTrans); } + float offsetX = isVertical() ? 0 : mCamera * mCameraDirection * (mTileSize.x() + mMargin.x()); + float offsetY = isVertical() ? mCamera * mCameraDirection * (mTileSize.y() + mMargin.y()) : 0; + if (mEntriesDirty) { updateTiles(); @@ -287,6 +287,20 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) Vector2i pos((int)Math::round(trans.translation()[0]), (int)Math::round(trans.translation()[1])); Vector2i size((int)Math::round(mSize.x() * scaleX), (int)Math::round(mSize.y() * scaleY)); + if (Settings::getInstance()->getBool("DebugGrid")) + { + for (auto it = mTiles.begin(); it != mTiles.end(); it++) + { + std::shared_ptr tile = (*it); + + auto tt = tile->getTransform() * trans; + Renderer::setMatrix(tt); + Renderer::drawRect(0.0, 0.0, tile->getSize().x(), tile->getSize().y(), 0x00FF0033); + } + + Renderer::setMatrix(parentTrans); + } + Renderer::pushClipRect(pos, size); if (mCamera != 0) @@ -295,6 +309,8 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) (*it)->setPosition((*it)->getPosition().x() + offsetX, (*it)->getPosition().y() + offsetY); } + + // Render the selected image background on bottom of the others if needed std::shared_ptr selectedTile = NULL; for(auto it = mTiles.begin(); it != mTiles.end(); it++) diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index 911f85a676..910032d919 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -213,9 +213,27 @@ void NinePatchComponent::applyTheme(const std::shared_ptr& theme, con using namespace ThemeFlags; const ThemeData::ThemeElement* elem = theme->getElement(view, element, "ninepatch"); - if(!elem) + if (!elem) return; - if(properties & PATH && elem->has("path")) + if (properties & PATH && elem->has("path")) setImagePath(elem->get("path")); + + if (properties & COLOR) + { + if (elem->has("color")) + { + setCenterColor(elem->get("color")); + setEdgeColor(elem->get("color")); + } + + if (elem->has("centerColor")) + setCenterColor(elem->get("centerColor")); + + if (elem->has("edgeColor")) + setEdgeColor(elem->get("edgeColor")); + } + + if (elem->has("cornerSize")) + setCornerSize(elem->get("cornerSize")); } diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index 4acee3056b..bf06947c12 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -8,7 +8,7 @@ TextComponent::TextComponent(Window* window) : GuiComponent(window), mFont(Font::get(FONT_SIZE_MEDIUM)), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), mHorizontalAlignment(ALIGN_LEFT), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), - mRenderBackground(false), mGlowColor(0), mGlowSize(2) + mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)) { } @@ -16,7 +16,7 @@ TextComponent::TextComponent(Window* window, const std::string& text, const std: Vector3f pos, Vector2f size, unsigned int bgcolor) : GuiComponent(window), mFont(NULL), mUppercase(false), mColor(1), mAutoCalcExtent(true, true), mHorizontalAlignment(align), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), - mRenderBackground(false), mGlowColor(0), mGlowSize(2) + mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)) { setFont(font); setColor(color); @@ -149,7 +149,8 @@ void TextComponent::render(const Transform4x4f& parentTrans) yOff = (int) (getSize().y() - textSize.y()) / 2.0f; break; } - Vector3f off(0, yOff, 0); + + Vector3f off(mPadding.x(), mPadding.y() + yOff, 0); if(Settings::getInstance()->getBool("DebugText")) { @@ -164,7 +165,7 @@ void TextComponent::render(const Transform4x4f& parentTrans) { auto func = [this, off, yOff, parentTrans](float x, float y) { - Vector3f off = Vector3f(x, yOff + y, 0); + Vector3f off = Vector3f(mPadding.x() + x, mPadding.y() + yOff + y, 0); Transform4x4f trans = parentTrans * getTransform(); trans.translate(off); @@ -247,10 +248,13 @@ void TextComponent::onTextChanged() return; } + int sx = mSize.x() - mPadding.x() - mPadding.z(); + int sy = mSize.y() - mPadding.y() - mPadding.w(); + std::string text = mUppercase ? Utils::String::toUpper(mText) : mText; std::shared_ptr f = mFont; - const bool isMultiline = (mSize.y() == 0 || mSize.y() > f->getHeight()*1.2f); + const bool isMultiline = (mSize.y() == 0 || sy > f->getHeight()*1.2f); bool addAbbrev = false; if(!isMultiline) @@ -261,13 +265,13 @@ void TextComponent::onTextChanged() } Vector2f size = f->sizeText(text); - if(!isMultiline && mSize.x() && text.size() && (size.x() > mSize.x() || addAbbrev)) + if(!isMultiline && sx && text.size() && (size.x() > sx || addAbbrev)) { // abbreviate text const std::string abbrev = "..."; Vector2f abbrevSize = f->sizeText(abbrev); - while(text.size() && size.x() + abbrevSize.x() > mSize.x()) + while(text.size() && size.x() + abbrevSize.x() > sx) { size_t newSize = Utils::String::prevCursor(text, text.size()); text.erase(newSize, text.size() - newSize); @@ -276,9 +280,9 @@ void TextComponent::onTextChanged() text.append(abbrev); - mTextCache = std::shared_ptr(f->buildTextCache(text, Vector2f(0, 0), (mColor >> 8 << 8) | mOpacity, mSize.x(), mHorizontalAlignment, mLineSpacing)); + mTextCache = std::shared_ptr(f->buildTextCache(text, Vector2f(0, 0), (mColor >> 8 << 8) | mOpacity, sx, mHorizontalAlignment, mLineSpacing)); }else{ - mTextCache = std::shared_ptr(f->buildTextCache(f->wrapText(text, mSize.x()), Vector2f(0, 0), (mColor >> 8 << 8) | mOpacity, mSize.x(), mHorizontalAlignment, mLineSpacing)); + mTextCache = std::shared_ptr(f->buildTextCache(f->wrapText(text, sx), Vector2f(0, 0), (mColor >> 8 << 8) | mOpacity, sx, mHorizontalAlignment, mLineSpacing)); } } @@ -349,6 +353,12 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, const st LOG(LogError) << "Unknown text alignment string: " << str; } + if (properties & ALIGNMENT && elem->has("padding")) + { + Vector2f scale = getParent() ? getParent()->getSize() : Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + mPadding = elem->get("padding") * Vector4f(scale.x(), scale.y(), scale.x(), scale.y()); + } + if(properties & TEXT && elem->has("text")) setText(elem->get("text")); diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index ff03a45274..257c8e1b44 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -4,6 +4,7 @@ #include "resources/Font.h" #include "GuiComponent.h" +#include "math/Vector4f.h" class ThemeData; @@ -75,6 +76,7 @@ class TextComponent : public GuiComponent unsigned int mGlowColor; unsigned int mGlowSize; + Vector4f mPadding; }; #endif // ES_CORE_COMPONENTS_TEXT_COMPONENT_H diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index e7abae8beb..5927d85fd3 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -160,6 +160,13 @@ void VideoComponent::render(const Transform4x4f& parentTrans) Renderer::setMatrix(trans); + if (Settings::getInstance()->getBool("DebugImage")) + { + Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; + Renderer::drawRect(targetSizePos.x(), targetSizePos.y(), mTargetSize.x(), mTargetSize.y(), 0xFF000033); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); + } + // Handle the case where the video is delayed handleStartDelay(); diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index bf07b39759..6031976567 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -12,6 +12,7 @@ #include "Window.h" #include "GuiComponent.h" +#include "utils/FileSystemUtil.h" int runShutdownCommand() { @@ -82,6 +83,23 @@ void split_cmd(const std::string& cmd, } } +#ifdef WIN32 +int _monitorEnumIndex = 0; +HMONITOR _monitorEnumHandle = 0; + +BOOL CALLBACK MonitorEnumProc(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMonitor, LPARAM dwData) +{ + if (_monitorEnumIndex == dwData) + { + _monitorEnumHandle = hMonitor; + return FALSE; + } + + _monitorEnumIndex++; + return TRUE; // continue enumerating +} +#endif + int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window) { #ifdef WIN32 @@ -105,10 +123,36 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo lpExecInfo.fMask = SEE_MASK_DOENVSUBST | SEE_MASK_NOCLOSEPROCESS; lpExecInfo.hwnd = NULL; lpExecInfo.lpVerb = "open"; // to open program - lpExecInfo.lpParameters = args.c_str(); // file name as an argument + lpExecInfo.lpDirectory = NULL; lpExecInfo.nShow = SW_SHOW; // show command prompt with normal window size lpExecInfo.hInstApp = (HINSTANCE)SE_ERR_DDEFAIL; //WINSHELLAPI BOOL WINAPI result; + + std::string extraConfigFile; + + int monitorId = Settings::getInstance()->getInt("MonitorID"); + if (monitorId > 0) + { + _monitorEnumIndex = 0; + _monitorEnumHandle = 0; + EnumDisplayMonitors(NULL, NULL, MonitorEnumProc, monitorId); + if (_monitorEnumHandle != 0) + { + // Special processing for retroarch -> Set monitor index in the extra config file ( and disable save_on_exit ) + if (Utils::String::toLower(exe).find("retroarch.") != std::string::npos) + { + std::string video_monitor_index = "video_monitor_index = \""+ std::to_string(monitorId+1) +"\"\r\nconfig_save_on_exit = \"false\"\r\n"; + extraConfigFile = Utils::FileSystem::getGenericPath(Utils::FileSystem::getHomePath() + "/retroarch.custom.cfg"); + Utils::FileSystem::writeAllText(extraConfigFile, video_monitor_index); + args = args + " --appendconfig \""+ extraConfigFile +"\""; + } + + lpExecInfo.fMask |= SEE_MASK_HMONITOR; + lpExecInfo.hIcon = _monitorEnumHandle; + } + } + + lpExecInfo.lpParameters = args.c_str(); // file name as an argument ShellExecuteEx(&lpExecInfo); if (lpExecInfo.hProcess != NULL) @@ -129,11 +173,17 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo window->renderGameLoadingScreen(); } } - + + if (Utils::FileSystem::exists(extraConfigFile)) + Utils::FileSystem::removeFile(extraConfigFile); + CloseHandle(lpExecInfo.hProcess); return 0; } + if (Utils::FileSystem::exists(extraConfigFile)) + Utils::FileSystem::removeFile(extraConfigFile); + return 1; //return _wsystem(wchar_str.c_str()); #else diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 282124b635..aaab5b9e8c 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -344,6 +344,7 @@ void TextureData::setSourceSize(float width, float height) { mSourceWidth = width; mSourceHeight = height; + releaseVRAM(); releaseRAM(); } diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index c88beb2287..6b073ac881 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -24,6 +24,7 @@ #include #include #endif // _WIN32 +#include namespace Utils { @@ -32,15 +33,6 @@ namespace Utils #if defined(_WIN32) std::mutex mFileMutex; #endif - - pugi::xml_parse_result load_xml(pugi::xml_document& doc, const char* path) - { -#if defined(_WIN32) - std::unique_lock lock(mFileMutex); -#endif - return doc.load_file(path); - } - bool compareFileInfo(const FileInfo& first, const FileInfo& second) { unsigned int i = 0; @@ -53,7 +45,13 @@ namespace Utils return (first.path.length() < second.path.length()); } - + void writeAllText(const std::string fileName, const std::string text) + { + std::fstream fs; + fs.open(fileName.c_str(), std::fstream::out); + fs << text; + fs.close(); + } fileList getDirInfo(const std::string& _path/*, const bool _recursive*/) { diff --git a/es-core/src/utils/FileSystemUtil.h b/es-core/src/utils/FileSystemUtil.h index f7f6f89585..c23cbb1141 100644 --- a/es-core/src/utils/FileSystemUtil.h +++ b/es-core/src/utils/FileSystemUtil.h @@ -4,7 +4,6 @@ #include #include -#include namespace Utils { @@ -53,11 +52,8 @@ namespace Utils void setHomePath (std::string path); bool copyFile (const std::string src, const std::string dst); - - pugi::xml_parse_result load_xml(pugi::xml_document& doc, const char* path); - - + void writeAllText (const std::string fileName, const std::string text); } // FileSystem:: diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index 7716e2792d..6ccf3f3ccc 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -529,6 +529,10 @@ msgstr "OPTIONS DE L'INTERFACE" msgid "OVERSCAN" msgstr "OVERSCAN" +#: +msgid "LAUNCH SCREENSAVER" +msgstr "ÉCRAN DE VEILLE" + #: msgid "SCREENSAVER SETTINGS" msgstr "ÉCRAN DE VEILLE" From 433ec9ae0b1569d30a379f48f1b5b937987a5946 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 5 Jul 2019 16:13:53 +0200 Subject: [PATCH 064/336] Images : true async loading -> faster boot, grid has less lags.... Optimize images Vram : applies now only to scrapped images, not themes - set by default. --- es-app/src/views/ViewController.cpp | 6 +- .../views/gamelist/DetailedGameListView.cpp | 12 +-- .../src/views/gamelist/GridGameListView.cpp | 2 +- .../src/views/gamelist/VideoGameListView.cpp | 8 +- es-core/src/ImageIO.cpp | 90 ++++++++++++++++++- es-core/src/ImageIO.h | 2 + es-core/src/Renderer_init_sdlgl.cpp | 2 +- es-core/src/Settings.cpp | 2 +- es-core/src/Window.cpp | 10 +-- es-core/src/Window.h | 2 +- es-core/src/components/GridTileComponent.cpp | 10 ++- es-core/src/components/GridTileComponent.h | 2 + es-core/src/components/ImageComponent.cpp | 8 +- es-core/src/components/ImageComponent.h | 7 +- es-core/src/components/ImageGridComponent.h | 21 ++++- es-core/src/components/TextComponent.cpp | 5 +- es-core/src/components/VideoComponent.cpp | 2 +- es-core/src/resources/TextureData.cpp | 10 ++- es-core/src/resources/TextureData.h | 2 + es-core/src/resources/TextureDataManager.cpp | 74 +++++++++++---- es-core/src/resources/TextureDataManager.h | 19 ++-- es-core/src/resources/TextureResource.cpp | 46 ++++++++-- es-core/src/resources/TextureResource.h | 6 +- 23 files changed, 278 insertions(+), 70 deletions(-) diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index f0595ba2e4..e974aeb541 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -76,8 +76,6 @@ void ViewController::goToStart(bool forceImmediate) void ViewController::ReloadAndGoToStart() { - mWindow->renderLoadingScreen(_T("Loading...")); - ViewController::get()->reloadAll(); ViewController::get()->goToStart(false); } @@ -449,7 +447,9 @@ bool ViewController::input(InputConfig* config, Input input) if (config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_F5) { - mWindow->renderLoadingScreen("Reloading..."); + mWindow->render(); + mWindow->renderLoadingScreen(_T("Loading..."), -1, 180); + ViewController::get()->reloadAll(); mWindow->endRenderLoadingScreen(); return true; diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 3b4a6486c7..734f684a37 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -101,7 +101,7 @@ void DetailedGameListView::createImage() const float padding = 0.01f; // Image - mImage = new ImageComponent(mWindow); + mImage = new ImageComponent(mWindow, true); 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); @@ -331,7 +331,7 @@ void DetailedGameListView::updateInfoPanel() mImage->setImage(imagePath, false, mImage->getSize()); if (mMarquee != nullptr) - mMarquee->setImage(file->getMarqueePath()); + mMarquee->setImage(file->getMarqueePath(), false, mMarquee->getSize()); mDescription.setText(getMetadata(file, "desc")); mDescContainer.reset(); @@ -392,11 +392,11 @@ void DetailedGameListView::launch(FileData* game) { Vector3f target(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0); - if (mImage != nullptr && mImage->hasImage()) - target = Vector3f(mImage->getCenter().x(), mImage->getCenter().y(), 0); - else if (mVideo != nullptr) + if (mVideo != nullptr) target = Vector3f(mVideo->getCenter().x(), mVideo->getCenter().y(), 0); - + else if (mImage != nullptr && mImage->hasImage()) + target = Vector3f(mImage->getCenter().x(), mImage->getCenter().y(), 0); + ViewController::get()->launch(game, target); } diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 350eb7fd56..2fc049dd54 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -18,7 +18,7 @@ GridGameListView::GridGameListView(Window* window, FolderData* root, const std:: ISimpleGameListView(window, root), mGrid(window), mDescContainer(window), mDescription(window), - mImage(window), + mImage(window, true), mVideo(nullptr), mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index b758cabfee..6f84d8d71a 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -15,7 +15,7 @@ VideoGameListView::VideoGameListView(Window* window, FolderData* root) : BasicGameListView(window, root), mDescContainer(window), mDescription(window), mMarquee(window), - mImage(window), + mImage(window, true), mVideo(nullptr), mVideoPlaying(false), @@ -252,9 +252,9 @@ void VideoGameListView::updateInfoPanel() } mVideoPlaying = true; - mVideo->setImage(file->getThumbnailPath()); - mMarquee.setImage(file->getMarqueePath()); - mImage.setImage(file->getImagePath()); + mVideo->setImage(file->getThumbnailPath(), false, mVideo->getSize()); + mMarquee.setImage(file->getMarqueePath(), false, mMarquee.getSize()); + mImage.setImage(file->getImagePath(), false, mImage.getSize()); mDescription.setText(file->metadata.get("desc")); mDescContainer.reset(); diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index 8c0bf4f7cd..f185cba50e 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -4,6 +4,92 @@ #include #include +#include +#include +#include "math/Vector2i.h" +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" + +bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) +{ + auto ext = Utils::String::toLower(Utils::FileSystem::getExtension(fn)); + if (ext != ".jpg" && ext != ".png" && ext != ".jpeg" && ext != ".gif") + return false; + + FILE *f = fopen(fn, "rb"); + if (f == 0) + return false; + + fseek(f, 0, SEEK_END); + long len = ftell(f); + fseek(f, 0, SEEK_SET); + + if (len < 24) { + fclose(f); + return false; + } + + // Strategy: + // reading GIF dimensions requires the first 10 bytes of the file + // reading PNG dimensions requires the first 24 bytes of the file + // reading JPEG dimensions requires scanning through jpeg chunks + // In all formats, the file is at least 24 bytes big, so we'll read that always + unsigned char buf[24]; fread(buf, 1, 24, f); + + + // For JPEGs, we need to read the first 12 bytes of each chunk. + // We'll read those 12 bytes at buf+2...buf+14, i.e. overwriting the existing buf. + + if ((buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF && buf[3] == 0xE0 && buf[6] == 'J' && buf[7] == 'F' && buf[8] == 'I' && buf[9] == 'F') || + (buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF && buf[3] == 0xE1 && buf[6] == 'E' && buf[7] == 'x' && buf[8] == 'i' && buf[9] == 'f')) + { + long pos = 2; + while (buf[2] == 0xFF) + { + if (buf[3] == 0xC0 || buf[3] == 0xC1 || buf[3] == 0xC2 || buf[3] == 0xC3 || buf[3] == 0xC9 || buf[3] == 0xCA || buf[3] == 0xCB) + break; + + pos += 2 + (buf[4] << 8) + buf[5]; + if (pos + 12 > len) break; + fseek(f, pos, SEEK_SET); + fread(buf + 2, 1, 12, f); + } + } + + fclose(f); + + // JPEG: (first two bytes of buf are first two bytes of the jpeg file; rest of buf is the DCT frame + if (buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF) + { + *y = (buf[7] << 8) + buf[8]; + *x = (buf[9] << 8) + buf[10]; + + if (*x > 5000) // security ? + return false; + + return true; + } + + // GIF: first three bytes say "GIF", next three give version number. Then dimensions + if (buf[0] == 'G' && buf[1] == 'I' && buf[2] == 'F') + { + *x = buf[6] + (buf[7] << 8); + *y = buf[8] + (buf[9] << 8); + return true; + } + + // PNG: the first frame is by definition an IHDR frame, which gives dimensions + if (buf[0] == 0x89 && buf[1] == 'P' && buf[2] == 'N' && buf[3] == 'G' && buf[4] == 0x0D && buf[5] == 0x0A && buf[6] == 0x1A && buf[7] == 0x0A && buf[12] == 'I' && buf[13] == 'H' && buf[14] == 'D' && buf[15] == 'R') + { + *x = (buf[16] << 24) + (buf[17] << 16) + (buf[18] << 8) + (buf[19] << 0); + *y = (buf[20] << 24) + (buf[21] << 16) + (buf[22] << 8) + (buf[23] << 0); + + return true; + } + + return false; +} + std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * data, const size_t size, size_t & width, size_t & height) { std::vector rawData; @@ -82,10 +168,6 @@ std::vector ImageIO::loadFromMemoryRGBA32(const unsigned char * d return rawData; } -#include "math/Vector2i.h" - -//public static Rectangle GetPictureRect(Size imageSize, Rectangle rcPhoto, bool outerZooming = false, bool sourceRect = false) - Vector2i ImageIO::adjustPictureSize(Vector2i imageSize, Vector2i maxSize, bool externSize) { if (externSize) diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index 9bb79022cc..a599bd5236 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -14,6 +14,8 @@ class ImageIO static std::vector loadFromMemoryRGBA32(const unsigned char * data, const size_t size, size_t & width, size_t & height); static unsigned char* loadFromMemoryRGBA32Ex(const unsigned char * data, const size_t size, size_t & width, size_t & height, int maxWidth, int maxHeight, bool externZoom, Vector2i& baseSize, Vector2i& packedSize); + static bool getImageSize(const char *fn, unsigned int *x, unsigned int *y); + static void flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height); static Vector2i adjustPictureSize(Vector2i imageSize, Vector2i maxSize, bool externSize = false); static Vector2f adjustExternPictureSizef(Vector2f imageSize, Vector2f maxSize); diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index 3827ac969e..e89b73bb25 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -79,7 +79,7 @@ namespace Renderer if (monitorId >=0 && sdlWindowPosition == Vector2i(SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED)) { int displays = SDL_GetNumVideoDisplays(); - if (displays >= monitorId) + if (displays > monitorId) { SDL_Rect rc; SDL_GetDisplayBounds(monitorId, &rc); diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 7f6174ea2e..5a794648bb 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -85,7 +85,7 @@ void Settings::setDefaults() mBoolMap["QuickSystemSelect"] = true; mBoolMap["MoveCarousel"] = true; mBoolMap["SaveGamelistsOnExit"] = true; - mBoolMap["OptimizeVRAM"] = false; + mBoolMap["OptimizeVRAM"] = true; mBoolMap["ThreadedLoading"] = true; mBoolMap["Debug"] = false; diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 31ae35f130..946d9ac023 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -312,14 +312,14 @@ void Window::endRenderLoadingScreen() mSplash = NULL; } -void Window::renderLoadingScreen(std::string text, float percent) +void Window::renderLoadingScreen(std::string text, float percent, unsigned char opacity) { if (mSplash == NULL) mSplash = TextureResource::get(":/splash.svg", false, true, false, false); Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); - Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); + Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | opacity); if (percent >= 0) { @@ -331,8 +331,8 @@ void Window::renderLoadingScreen(std::string text, float percent) float x = Renderer::getScreenWidth() / 2 - w / 2; float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); - Renderer::drawRect(x, y, w, h, 0x252525FF); - Renderer::drawGradientRect(x, y, (w*percent), h, 0x006C9EFF, 0x003E5CFF); // 0xFFFFFFFF + Renderer::drawRect(x, y, w, h, 0x25252500 | opacity); + Renderer::drawGradientRect(x, y, (w*percent), h, 0x006C9E00 | opacity, 0x003E5C00 | opacity); // 0xFFFFFFFF } ImageComponent splash(this, true); @@ -347,7 +347,7 @@ void Window::renderLoadingScreen(std::string text, float percent) splash.render(trans); auto& font = mDefaultFonts.at(1); - TextCache* cache = font->buildTextCache(text, 0, 0, 0x656565FF); + TextCache* cache = font->buildTextCache(text, 0, 0, 0x65656500 | opacity); float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); float y = Math::round(Renderer::getScreenHeight() * 0.78f); // 35 diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 42a2498106..be3f548cd8 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -67,7 +67,7 @@ class Window void setAllowSleep(bool sleep); void endRenderLoadingScreen(); - void renderLoadingScreen(std::string text, float percent = -1); + void renderLoadingScreen(std::string text, float percent = -1, unsigned char opacity = 255); void renderGameLoadingScreen(float opacity=1, bool swapBuffers=true); void renderHelpPromptsEarly(); // used to render HelpPrompts before a fade diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index e28b019ac9..5741b94934 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -66,7 +66,7 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mImage = std::make_shared(mWindow); mImage->setOrigin(0.5f, 0.5f); - mImage->setAllowFading(false); + // mImage->setAllowFading(false); addChild(&mBackground); addChild(&(*mImage)); @@ -104,6 +104,14 @@ GridTileComponent::~GridTileComponent() mVideo = nullptr; } +std::shared_ptr GridTileComponent::getTexture() +{ + if (mImage != nullptr) + return mImage->getTexture(); + + return nullptr; +}; + void GridTileComponent::resize() { const GridTileProperties& currentProperties = getCurrentProperties(); diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index 7903af9ff1..8b4138aae6 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -77,6 +77,8 @@ class GridTileComponent : public GuiComponent virtual void onHide(); virtual void update(int deltaTime); + std::shared_ptr getTexture(); + private: void resize(); const GridTileProperties& getCurrentProperties(); diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 437866918b..44b327d28c 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -24,7 +24,7 @@ Vector2f ImageComponent::getSize() const ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : GuiComponent(window), mTargetIsMax(false), mTargetIsMin(false), mFlipX(false), mFlipY(false), mTargetSize(0, 0), mColorShift(0xFFFFFFFF), mForceLoad(forceLoad), mDynamic(dynamic), mFadeOpacity(0), mFading(false), mRotateByTargetSize(false), mVisible(true), - mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f), mMirror(0.0f, 0.0f) + mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f), mMirror(0.0f, 0.0f), mAllowAsync(false) { } @@ -140,7 +140,7 @@ void ImageComponent::setDefaultImage(std::string path) void ImageComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) { - if (!maxSize.isExternalZoomKnown()) + if (!maxSize.isExternalZoomKnown() && !maxSize.empty()) maxSize = MaxSizeInfo(maxSize.x(), maxSize.y(), !mTargetIsMax); if (path.empty() || !ResourceManager::getInstance()->fileExists(path)) @@ -482,7 +482,7 @@ void ImageComponent::fadeIn(bool textureLoaded) mColorShift = (mColorShift >> 8 << 8) | 0; } } - else if (mFading) + else if (mFading && textureLoaded) { // The texture is loaded and we need to fade it in. The fade is based on the frame rate // and is 1/4 second if running at 60 frames per second although the actual value is not @@ -553,7 +553,7 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s if (Utils::FileSystem::exists(path)) { bool tile = (elem->has("tile") && elem->get("tile")); - setImage(path, tile, Vector2f(mTargetSize.x(), mTargetSize.y())); + setImage(path, tile/*, Vector2f(mTargetSize.x(), mTargetSize.y())*/); } } diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 7ef18bce28..b98f2dadda 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -81,12 +81,16 @@ class ImageComponent : public GuiComponent void render(const Transform4x4f& parentTrans) override; + void setAllowAsync(bool async) { mAllowAsync = async; }; void setAllowFading(bool fade) { mAllowFading = fade; }; void setMirroring(Vector2f mirror) { mMirror = mirror; }; virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; virtual std::vector getHelpPrompts() override; + + std::shared_ptr getTexture() { return mTexture; }; + private: Vector2f mTargetSize; @@ -114,13 +118,14 @@ class ImageComponent : public GuiComponent std::shared_ptr mTexture; unsigned char mFadeOpacity; + bool mFading; bool mForceLoad; bool mDynamic; bool mRotateByTargetSize; bool mVisible; - bool mAllowFading; + bool mAllowAsync; Vector2f mTopLeftCrop; Vector2f mBottomRightCrop; diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index e5b3d821a5..e8d2934a98 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -521,11 +521,16 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) float endPos = 1; if (isAnimationPlaying(2)) - { - startPos = -(mCamera*2.0 / 3.0); - if (startPos < -1) - startPos = -1; + { + /* + startPos = -(mCamera * 0.75); + if (startPos > 1) + startPos = 1; + else if (startPos < -1) + startPos = -1; + */ + startPos = 0; cancelAnimation(2); updateTiles(direction, false, !GuiComponent::ALLOWANIMATIONS); } @@ -646,6 +651,14 @@ void ImageGridComponent::updateTiles(bool ascending, bool allowAnimation, boo return; } + // Temporary store previous texture so they can't be unloaded + std::vector> previousTextures; + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + std::shared_ptr tile = mTiles.at(ti); + previousTextures.push_back(tile->getTexture()); + } + if (!ascending) { int i = (int)mTiles.size() - 1; diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index bf06947c12..16a7635b9d 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -173,7 +173,10 @@ void TextComponent::render(const Transform4x4f& parentTrans) Renderer::setMatrix(trans); - mTextCache->setColor(mGlowColor); + unsigned char o = (unsigned char)((float)(mGlowColor & 0x000000FF) / 255.f * (float)(mColor & 0x000000FF)); + unsigned int color = (mGlowColor & 0xFFFFFF00) | (unsigned char)o; + + mTextCache->setColor(color); mFont->renderTextCache(mTextCache.get()); mTextCache->setColor(mColor); }; diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 5927d85fd3..fc73bf215f 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -54,7 +54,7 @@ void VideoComponent::setScreensaverMode(bool isScreensaver) VideoComponent::VideoComponent(Window* window) : GuiComponent(window), - mStaticImage(window), + mStaticImage(window, true), mVideoHeight(0), mVideoWidth(0), mStartDelayed(false), diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index aaab5b9e8c..b14b15dc0b 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -223,7 +223,7 @@ bool TextureData::load() if (!mPath.empty()) { #ifdef WIN32 - // TRACE("TextureData::load(" << mPath << ", " << mTextureID << ")") + TRACE("TextureData::load(" << mPath << ", " << (mMaxSize.empty() ? "" : "(hasMaxSize)") << ")") #endif std::shared_ptr& rm = ResourceManager::getInstance(); @@ -335,6 +335,14 @@ float TextureData::sourceHeight() return mSourceHeight; } +void TextureData::setTemporarySize(float width, float height) +{ + mWidth = width; + mHeight = height; + mSourceWidth = width; + mSourceHeight = height; +} + void TextureData::setSourceSize(float width, float height) { if (mScalable) diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 3a9078ecf5..70b56f1e79 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -56,6 +56,8 @@ class TextureData float sourceHeight(); void setSourceSize(float width, float height); + void setTemporarySize(float width, float height); + bool tiled() { return mTile; } bool isRequiredTextureSizeOk(); diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index 16e9e43075..0f1452179f 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -18,7 +18,8 @@ TextureDataManager::TextureDataManager() data[i*4+3] = 0; } mBlank->initFromRGBA(data, 5, 5); - mLoader = new TextureLoader; + + mLoader = new TextureLoader(this); } TextureDataManager::~TextureDataManager() @@ -26,17 +27,40 @@ TextureDataManager::~TextureDataManager() delete mLoader; } +void TextureDataManager::onTextureLoaded(std::shared_ptr tex) +{ + std::unique_lock lock(mMutex); + + for (auto it = mTextureLookup.cbegin(); it != mTextureLookup.cend(); it++) + { + std::shared_ptr texture = *(*it).second; + if (texture == tex) + { + const TextureResource* pResource = it->first; + ((TextureResource*)pResource)->onTextureLoaded(tex); + } + } +} + + std::shared_ptr TextureDataManager::add(const TextureResource* key, bool tiled) { remove(key); std::shared_ptr data(new TextureData(tiled)); mTextures.push_front(data); - mTextureLookup[key] = mTextures.cbegin(); + + { + std::unique_lock lock(mMutex); + mTextureLookup[key] = mTextures.cbegin(); + } + return data; } void TextureDataManager::remove(const TextureResource* key) { + std::unique_lock lock(mMutex); + // Find the entry in the list auto it = mTextureLookup.find(key); if (it != mTextureLookup.cend()) @@ -50,6 +74,8 @@ void TextureDataManager::remove(const TextureResource* key) std::shared_ptr TextureDataManager::get(const TextureResource* key, bool enableLoading) { + std::unique_lock lock(mMutex); + // If it's in the cache then we want to remove it from it's current location and // move it to the top std::shared_ptr tex; @@ -103,6 +129,7 @@ size_t TextureDataManager::getQueueSize() return mLoader->getQueueSize(); } + void TextureDataManager::load(std::shared_ptr tex, bool block) { // See if it's already loaded @@ -150,9 +177,16 @@ void TextureDataManager::load(std::shared_ptr tex, bool block) } } -TextureLoader::TextureLoader() : mExit(false) +TextureLoader::TextureLoader(TextureDataManager* mgr) : mExit(false) { - mThread = new std::thread(&TextureLoader::threadProc, this); + mManager = mgr; + + int num_threads = std::thread::hardware_concurrency() / 2; + if (num_threads == 0) + num_threads = 1; + + for (size_t i = 0; i < num_threads; i++) + mThreads.push_back(std::thread(&TextureLoader::threadProc, this)); } TextureLoader::~TextureLoader() @@ -163,9 +197,10 @@ TextureLoader::~TextureLoader() // Exit the thread mExit = true; - mEvent.notify_one(); - mThread->join(); - delete mThread; + mEvent.notify_all(); + + for (std::thread& t : mThreads) + t.join(); } void TextureLoader::threadProc() @@ -182,23 +217,23 @@ void TextureLoader::threadProc() textureData = mTextureDataQ.front(); mTextureDataQ.pop_front(); mTextureDataLookup.erase(mTextureDataLookup.find(textureData.get())); + + mProcessingTextureDataQ.push_back(textureData); } } - // Queue has been released here but we might have a texture to process - while (textureData) + + if (textureData) { - textureData->load(); + textureData->load(); + mManager->onTextureLoaded(textureData); - // See if there is another item in the queue - textureData = nullptr; - std::unique_lock lock(mMutex); - if (!mTextureDataQ.empty()) { - textureData = mTextureDataQ.front(); - mTextureDataQ.pop_front(); - mTextureDataLookup.erase(mTextureDataLookup.find(textureData.get())); + std::unique_lock lock(mMutex); + mProcessingTextureDataQ.remove(textureData); } } + + std::this_thread::yield(); } } @@ -208,6 +243,11 @@ void TextureLoader::load(std::shared_ptr textureData) if (!textureData->isLoaded()) { std::unique_lock lock(mMutex); + + // If is is currently loading, don't add again + if (std::find(mProcessingTextureDataQ.begin(), mProcessingTextureDataQ.end(), textureData) != mProcessingTextureDataQ.cend()) + return; + // Remove it from the queue if it is already there auto td = mTextureDataLookup.find(textureData.get()); if (td != mTextureDataLookup.cend()) diff --git a/es-core/src/resources/TextureDataManager.h b/es-core/src/resources/TextureDataManager.h index 1e2b0e6ff4..565694a3f7 100644 --- a/es-core/src/resources/TextureDataManager.h +++ b/es-core/src/resources/TextureDataManager.h @@ -7,15 +7,17 @@ #include #include #include +#include #include class TextureData; class TextureResource; +class TextureDataManager; class TextureLoader { public: - TextureLoader(); + TextureLoader(TextureDataManager* mgr); ~TextureLoader(); void load(std::shared_ptr textureData); @@ -24,17 +26,20 @@ class TextureLoader size_t getQueueSize(); -private: - void processQueue(); +private: void threadProc(); - std::list > mTextureDataQ; + std::list> mProcessingTextureDataQ; + + std::list> mTextureDataQ; std::map >::const_iterator > mTextureDataLookup; - std::thread* mThread; + std::vector mThreads; std::mutex mMutex; std::condition_variable mEvent; bool mExit; + + TextureDataManager* mManager; }; // @@ -78,7 +83,11 @@ class TextureDataManager void load(std::shared_ptr tex, bool block = false); void clearQueue(); + + void onTextureLoaded(std::shared_ptr tex); + private: + std::mutex mMutex; std::list > mTextures; std::map >::const_iterator > mTextureLookup; diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index 946437995f..16a70a313c 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -2,6 +2,8 @@ #include "utils/FileSystemUtil.h" #include "resources/TextureData.h" +#include "ImageIO.h" +#include "Settings.h" TextureDataManager TextureResource::sTextureDataManager; @@ -10,7 +12,7 @@ std::map< TextureResource::TextureKeyType, std::shared_ptr> Tex std::set TextureResource::sAllTextures; -TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic, MaxSizeInfo maxSize) : mTextureData(nullptr), mForceLoad(false) +TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic, bool allowAsync, MaxSizeInfo maxSize) : mTextureData(nullptr), mForceLoad(false) { #if _DEBUG mPath = path; @@ -28,8 +30,32 @@ TextureResource::TextureResource(const std::string& path, bool tile, bool dynami data->setMaxSize(maxSize); data->initFromPath(path); + bool async = false; + + std::shared_ptr& rm = ResourceManager::getInstance(); + auto fullpath = rm->getResourcePath(path); + + unsigned int width, height; + + if (allowAsync && Settings::getInstance()->getBool("ThreadedLoading") && ImageIO::getImageSize(fullpath.c_str(), &width, &height)) + { + data->setTemporarySize(width, height); + async = true; + } + // Force the texture manager to load it using a blocking load - sTextureDataManager.load(data, true); // + sTextureDataManager.load(data, !async); // + + if (async) + { + mSize = Vector2i(width, height); + mSourceSize = Vector2f(width, height); + } + else + { + mSize = Vector2i((int)data->width(), (int)data->height()); + mSourceSize = Vector2f(data->sourceWidth(), data->sourceHeight()); + } } else { @@ -40,10 +66,10 @@ TextureResource::TextureResource(const std::string& path, bool tile, bool dynami data->initFromPath(path); // Load it so we can read the width/height data->load(); - } - mSize = Vector2i((int)data->width(), (int)data->height()); - mSourceSize = Vector2f(data->sourceWidth(), data->sourceHeight()); + mSize = Vector2i((int)data->width(), (int)data->height()); + mSourceSize = Vector2f(data->sourceWidth(), data->sourceHeight()); + } } else { @@ -68,6 +94,12 @@ TextureResource::~TextureResource() } } +void TextureResource::onTextureLoaded(std::shared_ptr tex) +{ + mSize = Vector2i((int)tex->width(), (int)tex->height()); + mSourceSize = Vector2f(tex->sourceWidth(), tex->sourceHeight()); +} + void TextureResource::initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height) { // This is only valid if we have a local texture data object @@ -131,7 +163,7 @@ std::shared_ptr TextureResource::get(const std::string& path, b const std::string canonicalPath = Utils::FileSystem::getCanonicalPath(path); if(canonicalPath.empty()) { - std::shared_ptr tex(new TextureResource("", tile, false, maxSize)); + std::shared_ptr tex(new TextureResource("", tile, dynamic, !forceLoad, maxSize)); rm->addReloadable(tex); //make sure we get properly deinitialized even though we do nothing on reinitialization return tex; } @@ -174,7 +206,7 @@ std::shared_ptr TextureResource::get(const std::string& path, b // need to create it std::shared_ptr tex; - tex = std::shared_ptr(new TextureResource(key.first, tile, dynamic, maxSize)); + tex = std::shared_ptr(new TextureResource(key.first, tile, dynamic, !forceLoad, maxSize)); std::shared_ptr data = sTextureDataManager.get(tex.get(), !forceLoad); if (asReloadable) // // is it an SVG // if (key.first.substr(key.first.size() - 4, std::string::npos) != ".svg") // FCATMP diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index 4d9a5a6a5e..5d8e950693 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -47,6 +47,9 @@ class MaxSizeInfo // Automatically recreates the texture with renderer deinit/reinit. class TextureResource : public IReloadable { +protected: + TextureResource(const std::string& path, bool tile, bool dynamic, bool allowAsync, MaxSizeInfo maxSize); + public: static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true, bool asReloadable = true, MaxSizeInfo maxSize = MaxSizeInfo()); void initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height); @@ -72,8 +75,7 @@ class TextureResource : public IReloadable virtual bool unload(); virtual void reload(); -protected: - TextureResource(const std::string& path, bool tile, bool dynamic, MaxSizeInfo maxSize); + void onTextureLoaded(std::shared_ptr tex); private: // mTextureData is used for textures that are not loaded from a file - these ones From 16e7b48052b58d4ea9d876205f344ef3b66004a9 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 5 Jul 2019 17:59:40 +0200 Subject: [PATCH 065/336] Image VRAM optimization : bad sizing info for image contained in video components. --- es-app/src/views/gamelist/DetailedGameListView.cpp | 2 +- es-app/src/views/gamelist/GridGameListView.cpp | 2 +- es-app/src/views/gamelist/VideoGameListView.cpp | 2 +- es-core/src/components/VideoComponent.h | 10 ++++++++++ 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 734f684a37..787fc80d78 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -323,7 +323,7 @@ void DetailedGameListView::updateInfoPanel() if (!mVideo->setVideo(file->getVideoPath())) mVideo->setDefaultVideo(); - mVideo->setImage(imagePath, false, mVideo->getSize()); + mVideo->setImage(imagePath, false, mVideo->getTargetSize()); } //mSelectedProperties.mImageSizeMode == "minSize")); diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 2fc049dd54..281d189eb9 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -422,7 +422,7 @@ void GridGameListView::updateInfoPanel() if (file->getImagePath().empty()) { if (mVideo != nullptr) - mVideo->setImage(file->getThumbnailPath(), false, mVideo->getSize()); + mVideo->setImage(file->getThumbnailPath(), false, mVideo->getTargetSize()); mImage.setImage(file->getThumbnailPath(), false, mImage.getSize()); } diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index 6f84d8d71a..17d4f34eaf 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -252,7 +252,7 @@ void VideoGameListView::updateInfoPanel() } mVideoPlaying = true; - mVideo->setImage(file->getThumbnailPath(), false, mVideo->getSize()); + mVideo->setImage(file->getThumbnailPath(), false, mVideo->getTargetSize()); mMarquee.setImage(file->getMarqueePath(), false, mMarquee.getSize()); mImage.setImage(file->getImagePath(), false, mImage.getSize()); diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index 51f9c49055..28036492da 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -83,6 +83,14 @@ class VideoComponent : public GuiComponent void onVideoStarted(); + Vector2f getTargetSize() + { + if (mTargetSize == Vector2f(0, 0)) + return mSize; + + return mTargetSize; + }; + private: // Start the video Immediately virtual void startVideo() = 0; @@ -100,6 +108,8 @@ class VideoComponent : public GuiComponent // Manage the playing state of the component void manageState(); + + protected: unsigned mVideoWidth; From 4cab3a3ec49f05211235c6b8e4051350879de90b Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 6 Jul 2019 00:52:40 +0200 Subject: [PATCH 066/336] Corrections to allow building on Linux --- es-app/src/SystemData.cpp | 2 +- es-core/src/ImageIO.cpp | 5 ++++ es-core/src/ThemeData.cpp | 27 +++++++++++--------- es-core/src/ThemeData.h | 4 +-- es-core/src/components/ImageGridComponent.h | 9 ++++--- es-core/src/resources/TextureDataManager.cpp | 5 ++-- es-core/src/utils/FileSystemUtil.cpp | 2 +- es-core/src/utils/ThreadPool.cpp | 15 +---------- es-core/src/utils/ThreadPool.h | 3 +-- 9 files changed, 34 insertions(+), 38 deletions(-) diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 03a361643a..aaede17815 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -416,7 +416,7 @@ bool SystemData::loadConfig(Window* window) ThreadPool* pThreadPool = NULL; SystemDataPtr* systems = NULL; - if (ThreadPool::getProcessorCount() > 2 && Settings::getInstance()->getBool("ThreadedLoading")) + if (std::thread::hardware_concurrency() > 2 && Settings::getInstance()->getBool("ThreadedLoading")) { pThreadPool = new ThreadPool(); diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index f185cba50e..b499093ea4 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -184,8 +184,13 @@ Vector2i ImageIO::adjustPictureSize(Vector2i imageSize, Vector2i maxSize, bool e double xCoef = (double)iMaxX / (double)cxDIB; double yCoef = (double)iMaxY / (double)cyDIB; +#if WIN32 cyDIB = (int)((double)cyDIB * std::fmax(xCoef, yCoef)); cxDIB = (int)((double)cxDIB * std::fmax(xCoef, yCoef)); +#else + cyDIB = (int)((double)cyDIB * std::max(xCoef, yCoef)); + cxDIB = (int)((double)cxDIB * std::max(xCoef, yCoef)); +#endif if (cxDIB > iMaxX) { diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 894b00b6fb..3cdfefc0e0 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -351,9 +351,12 @@ void ThemeData::loadFile(std::string system, std::map const std::shared_ptr& ThemeData::getMenuTheme() { if (mMenuTheme == nullptr && mCurrentTheme != nullptr) - mMenuTheme = std::shared_ptr(new ThemeMenu(*mCurrentTheme)); + mMenuTheme = std::shared_ptr(new ThemeMenu(mCurrentTheme)); else if (mMenuTheme == nullptr) - return std::shared_ptr(new ThemeMenu(ThemeData())); + { + auto emptyData = ThemeData(); + return std::shared_ptr(new ThemeMenu(&emptyData)); + } return mMenuTheme; } @@ -1036,14 +1039,14 @@ std::string ThemeData::getThemeFromCurrentSet(const std::string& system) return set->second.getThemePath(system); } -ThemeData::ThemeMenu::ThemeMenu(ThemeData& theme) +ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) { Title.font = Font::get(FONT_SIZE_LARGE); Footer.font = Font::get(FONT_SIZE_SMALL); Text.font = Font::get(FONT_SIZE_MEDIUM); TextSmall.font = Font::get(FONT_SIZE_SMALL); - auto elem = theme.getElement("menu", "menubg", "menuBackground"); + auto elem = theme->getElement("menu", "menubg", "menuBackground"); if (elem) { if (elem->has("path") && ResourceManager::getInstance()->fileExists(elem->get("path"))) @@ -1056,7 +1059,7 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData& theme) Background.color = elem->get("color"); } - elem = theme.getElement("menu", "menutitle", "menuText"); + elem = theme->getElement("menu", "menutitle", "menuText"); if (elem) { if (elem->has("fontPath") || elem->has("fontSize")) @@ -1065,7 +1068,7 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData& theme) Title.color = elem->get("color"); } - elem = theme.getElement("menu", "menufooter", "menuText"); + elem = theme->getElement("menu", "menufooter", "menuText"); if (elem) { @@ -1075,7 +1078,7 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData& theme) Footer.color = elem->get("color"); } - elem = theme.getElement("menu", "menutextsmall", "menuTextSmall"); + elem = theme->getElement("menu", "menutextsmall", "menuTextSmall"); if (elem) { @@ -1090,7 +1093,7 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData& theme) Text.selectedColor = elem->get("selectorColor"); } - elem = theme.getElement("menu", "menutext", "menuText"); + elem = theme->getElement("menu", "menutext", "menuText"); if (elem) { @@ -1109,7 +1112,7 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData& theme) Text.selectorGradientColor = elem->get("selectorGradientColor"); } - elem = theme.getElement("menu", "menubutton", "menuButton"); + elem = theme->getElement("menu", "menubutton", "menuButton"); if (elem) { @@ -1119,7 +1122,7 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData& theme) Icons.button_filled = elem->get("filledPath"); } - elem = theme.getElement("menu", "menuswitch", "menuSwitch"); + elem = theme->getElement("menu", "menuswitch", "menuSwitch"); if (elem) { @@ -1129,12 +1132,12 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData& theme) Icons.off = elem->get("pathOff"); } - elem = theme.getElement("menu", "menuslider", "menuSlider"); + elem = theme->getElement("menu", "menuslider", "menuSlider"); if (elem && elem->has("path") && ResourceManager::getInstance()->fileExists(elem->get("path"))) Icons.knob = elem->get("path"); - elem = theme.getElement("menu", "menuicons", "menuIcons"); + elem = theme->getElement("menu", "menuicons", "menuIcons"); if (elem) { diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 0e6b87c646..4d0b2992da 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -125,7 +125,7 @@ class ThemeData class ThemeMenu { public: - ThemeMenu(ThemeData& theme); + ThemeMenu(ThemeData* theme); MenuElement Background{ 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0, ":/frame.png", ":/scroll_gradient.png", nullptr }; MenuElement Title{ 0x555555FF, 0x555555FF, 0x555555FF, 0xFFFFFFFF, 0, "", "", nullptr }; @@ -231,7 +231,7 @@ class ThemeData static const std::shared_ptr& getMenuTheme(); static std::map sortThemeSubSets(const std::map& subsetmap, const std::string& subset); - static std::map ThemeData::getThemeSubSets(const std::string& theme); + static std::map getThemeSubSets(const std::string& theme); private: static void crawlIncludes(const pugi::xml_node& root, std::map& sets, std::deque& dequepath); diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index e8d2934a98..7c4f5d2565 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -520,7 +520,7 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) float startPos = 0; float endPos = 1; - if (isAnimationPlaying(2)) + if (((GuiComponent*)this)->isAnimationPlaying(2)) { /* startPos = -(mCamera * 0.75); @@ -531,7 +531,7 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) startPos = -1; */ startPos = 0; - cancelAnimation(2); + ((GuiComponent*)this)->cancelAnimation(2); updateTiles(direction, false, !GuiComponent::ALLOWANIMATIONS); } @@ -623,7 +623,7 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) mCamera = t; }; - setAnimation(new LambdaAnimation(func, 250), 0, [this, direction] { + ((GuiComponent*)this)->setAnimation(new LambdaAnimation(func, 250), 0, [this, direction] { mCamera = 0; updateTiles(direction, false); }, false, 2); @@ -755,7 +755,8 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA if (idx < 0 || idx >= mTiles.size()) idx = 0; - tile->setSelected(true, allowAnimation, &mTiles.at(idx)->getBackgroundPosition()); + Vector3f pos = mTiles.at(idx)->getBackgroundPosition(); + tile->setSelected(true, allowAnimation, &pos); } else tile->setSelected(imgPos == mCursor, allowAnimation); diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index 0f1452179f..1d7cea9519 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -245,8 +245,9 @@ void TextureLoader::load(std::shared_ptr textureData) std::unique_lock lock(mMutex); // If is is currently loading, don't add again - if (std::find(mProcessingTextureDataQ.begin(), mProcessingTextureDataQ.end(), textureData) != mProcessingTextureDataQ.cend()) - return; + for (auto it = mProcessingTextureDataQ.begin(); it != mProcessingTextureDataQ.end(); it++) + if ((*it) == textureData) + return; // Remove it from the queue if it is already there auto td = mTextureDataLookup.find(textureData.get()); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 6b073ac881..7ec97a8b6f 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -107,7 +107,7 @@ namespace Utils FileInfo fi; fi.path = fullName; - fi.readOnly = Utils::FileSystem::isHidden(fullName); + fi.hidden = Utils::FileSystem::isHidden(fullName); fi.directory = isDirectory(fullName); contentList.push_back(fi); } diff --git a/es-core/src/utils/ThreadPool.cpp b/es-core/src/utils/ThreadPool.cpp index 90a21d041b..3108cb62ab 100644 --- a/es-core/src/utils/ThreadPool.cpp +++ b/es-core/src/utils/ThreadPool.cpp @@ -6,22 +6,9 @@ namespace Utils { - size_t ThreadPool::getProcessorCount() - { - //unsigned num_cpus = std::thread::hardware_concurrency(); - -#ifdef WIN32 - SYSTEM_INFO sysinfo; - GetSystemInfo(&sysinfo); - return sysinfo.dwNumberOfProcessors; -#else - return sysconf(_SC_NPROCESSORS_ONLN); -#endif - } - ThreadPool::ThreadPool() : mRunning(true), mWaiting(false), mNumWork(0) { - size_t num_threads = getProcessorCount() - 1; + size_t num_threads = std::thread::hardware_concurrency() - 1; auto doWork = [&](size_t id) { diff --git a/es-core/src/utils/ThreadPool.h b/es-core/src/utils/ThreadPool.h index bff00d7bed..34eb844696 100644 --- a/es-core/src/utils/ThreadPool.h +++ b/es-core/src/utils/ThreadPool.h @@ -4,14 +4,13 @@ #include #include #include +#include namespace Utils { class ThreadPool { public: - static size_t getProcessorCount(); - typedef std::function work_function; ThreadPool(); From 4b78fab459f41eed64285b46284b0ce6614b4721 Mon Sep 17 00:00:00 2001 From: Anubis-13 <51127283+Anubis-13@users.noreply.github.com> Date: Sat, 6 Jul 2019 10:15:29 +0200 Subject: [PATCH 067/336] Update New lines added to the file in French, now for Spanish. Some more correction --- resources/locale/es/emulationstation2.po | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/resources/locale/es/emulationstation2.po b/resources/locale/es/emulationstation2.po index 7d9bda7847..a52f8f2b11 100644 --- a/resources/locale/es/emulationstation2.po +++ b/resources/locale/es/emulationstation2.po @@ -14,7 +14,7 @@ msgstr "Cargando..." #: msgid "Preloading UI" -msgstr "Precargando interfaz" +msgstr "Iniciando..." #: msgid "GRID SIZE" @@ -30,7 +30,7 @@ msgstr "MODO INTERFAZ" #: msgid "PRELOAD UI" -msgstr "PRECARGA INTERFAZ" +msgstr "PRECARGA UI" #: msgid "VSYNC" @@ -525,6 +525,10 @@ msgstr "OPCIONES DE INTERFAZ" msgid "OVERSCAN" msgstr "OVERSCAN" +#: +msgid "LAUNCH SCREENSAVER" +msgstr "SALVAPANTALLA" + #: msgid "SCREENSAVER SETTINGS" msgstr "AJUSTES SALVAPANTALLA" @@ -558,8 +562,8 @@ msgid "QUICK SYSTEM SELECT" msgstr "SELECCIÓN RAPIDA DE SISTEMA" #: -msgid "THEME SET" -msgstr "SISTEMA ESTABLECIDO" +msgid "THEME" +msgstr "SELECCIÓN DE TEMAS" #: msgid "SOUND SETTINGS" From b93b409b4a46c03ed5d366433a09044ec03fadfe Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 6 Jul 2019 12:12:05 +0200 Subject: [PATCH 068/336] Try to reactivate Window after game launch Store/Restore Wnd position only when --windowed & --resolution args specified --- es-app/src/Gamelist.cpp | 3 +++ es-app/src/main.cpp | 7 +++++-- es-core/src/Renderer.h | 1 + es-core/src/Renderer_init_sdlgl.cpp | 27 +++++++++++++++++++++++++-- es-core/src/Window.cpp | 2 ++ 5 files changed, 36 insertions(+), 4 deletions(-) diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 7cec7df63d..afce45b7fe 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -318,12 +318,15 @@ void updateGamelist(SystemData* system) { std::string savFile = xmlWritePath + ".old"; + // remove previous gamelist.xml.old file if (Utils::FileSystem::exists(savFile)) Utils::FileSystem::removeFile(savFile); + // rename gamelist.xml to gamelist.xml.old if (Utils::FileSystem::exists(xmlWritePath)) std::rename(xmlWritePath.c_str(), savFile.c_str()); + // rename gamelist.tmp.xml to gamelist.xml std::rename(tmpFile.c_str(), xmlWritePath.c_str()); } else diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index b925c5eb02..cfcc961fde 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -298,9 +298,12 @@ int main(int argc, char* argv[]) { srand((unsigned int)time(NULL)); - //std::locale::global(std::locale("C")); +#if WIN32 std::locale::global(std::locale("en-US")); - +#else + std::locale::global(std::locale("C")); +#endif + if(!parseArgs(argc, argv)) return 0; diff --git a/es-core/src/Renderer.h b/es-core/src/Renderer.h index bbe34b9cc0..0108ff9f87 100644 --- a/es-core/src/Renderer.h +++ b/es-core/src/Renderer.h @@ -17,6 +17,7 @@ namespace Renderer { bool init(); void deinit(); + void activateWindow(); unsigned int getWindowWidth(); unsigned int getWindowHeight(); diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index e89b73bb25..25a930989c 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -6,6 +6,10 @@ #include "Settings.h" #include +#if WIN32 +#include +#endif + #ifdef USE_OPENGL_ES #define glOrtho glOrthof #endif @@ -35,6 +39,8 @@ namespace Renderer Vector2i sdlWindowPosition = Vector2i(SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED); + + bool createSurface() { LOG(LogInfo) << "Creating surface..."; @@ -76,7 +82,7 @@ namespace Renderer screenRotate = Settings::getInstance()->getInt("ScreenRotate") ? Settings::getInstance()->getInt("ScreenRotate") : 0; int monitorId = Settings::getInstance()->getInt("MonitorID"); - if (monitorId >=0 && sdlWindowPosition == Vector2i(SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED)) + if (monitorId >= 0 && sdlWindowPosition == Vector2i(SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED)) { int displays = SDL_GetNumVideoDisplays(); if (displays > monitorId) @@ -183,7 +189,7 @@ namespace Renderer void destroySurface() { - if (Settings::getInstance()->getBool("Windowed")) + if (Settings::getInstance()->getBool("Windowed") && Settings::getInstance()->getInt("WindowWidth") && Settings::getInstance()->getInt("WindowHeight")) { int x; int y; SDL_GetWindowPosition(sdlWindow, &x, &y); @@ -202,6 +208,23 @@ namespace Renderer SDL_Quit(); } + + void activateWindow() + { +#if WIN32 + if (sdlWindow == NULL) + return; + + SDL_SysWMinfo wmInfo; + SDL_VERSION(&wmInfo.version); + SDL_GetWindowWMInfo(sdlWindow, &wmInfo); + HWND hwnd = wmInfo.info.win.window; + + ::SetForegroundWindow(hwnd); + ::SetActiveWindow(hwnd); +#endif + } + bool init() { if(!createSurface()) diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 946d9ac023..b473171eaf 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -96,6 +96,8 @@ bool Window::init(bool initRenderer) if (peekGui()) peekGui()->updateHelpPrompts(); + Renderer::activateWindow(); + return true; } From d419acb8237e0e813f76b6f1fd6d5da4000e5f78 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 6 Jul 2019 18:48:58 +0200 Subject: [PATCH 069/336] Linux : fix - now compilable & runnable under linux --- es-core/src/utils/FileSystemUtil.cpp | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 7ec97a8b6f..e84a3d5180 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -257,13 +257,23 @@ namespace Utils // only construct the homepath once if (!path.length()) { - // verify if .emulationstation/es_systems.cfg is under exe's path to make app portable - std::string portableDir = getExePath() + "/.emulationstation/es_systems.cfg"; - if (Utils::FileSystem::exists(portableDir)) +#if defined(_WIN32) + char buffer[MAX_PATH]; + DWORD size = MAX_PATH; + DWORD result = GetModuleFileNameA(NULL, buffer, size); + if (result) { - path = getExePath(); - return path; + // verify if .emulationstation/es_systems.cfg is under exe's path to make app portable + + std::string ret = buffer; + std::string portableDir = getGenericPath(getParent(ret)) + "/.emulationstation/es_systems.cfg"; + if (Utils::FileSystem::exists(portableDir)) + { + path = getExePath(); + return path; + } } +#endif // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows char* envHome = getenv("HOME"); From 699c9951e377bd67a43f7bb97fd8a1e86ebbd6dc Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 6 Jul 2019 18:52:30 +0200 Subject: [PATCH 070/336] Linux : set FullscreenBorderless to false ( true applies only on win32 ) --- es-core/src/Settings.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 5a794648bb..4d02e63594 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -66,7 +66,11 @@ void Settings::setDefaults() mBoolMap["ShowHiddenFiles"] = false; mBoolMap["DrawFramerate"] = false; mBoolMap["ShowExit"] = true; +#if WIN32 mBoolMap["FullscreenBorderless"] = true; +#else + mBoolMap["FullscreenBorderless"] = false; +#endif mBoolMap["Windowed"] = false; mBoolMap["SplashScreen"] = true; mBoolMap["SplashScreenProgress"] = true; From 2b94f0dd6c7cd950ee26eb6c8dc1578e9f1fdbee Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 7 Jul 2019 01:29:06 +0200 Subject: [PATCH 071/336] Linux : forgot include Settings.h in gridview --- es-core/src/components/ImageGridComponent.h | 1 + 1 file changed, 1 insertion(+) diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 7c4f5d2565..09374e01b9 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -6,6 +6,7 @@ #include "components/IList.h" #include "resources/TextureResource.h" #include "GridTileComponent.h" +#include "Settings.h" #define EXTRAITEMS 2 From 2f55d91eb04060b4b19d4d4247dc1410ceee301a Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 7 Jul 2019 01:31:16 +0200 Subject: [PATCH 072/336] Linux : forgot some includes --- es-core/src/components/ImageGridComponent.h | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 09374e01b9..432d02a6ee 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -6,6 +6,7 @@ #include "components/IList.h" #include "resources/TextureResource.h" #include "GridTileComponent.h" +#include "animations/LambdaAnimation.h" #include "Settings.h" #define EXTRAITEMS 2 @@ -632,7 +633,7 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) template -void ImageGridComponent::updateTiles(bool ascending, bool allowAnimation, bool updateSelectedState = true) +void ImageGridComponent::updateTiles(bool ascending, bool allowAnimation, bool updateSelectedState) { if (!mTiles.size()) return; From 2308bca1e32268f48da5b5215949edecbcfe7f5c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 7 Jul 2019 02:35:31 +0200 Subject: [PATCH 073/336] MultiThreaded image loader : queue incompletly handled sometimes --- es-core/src/resources/TextureDataManager.cpp | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index 1d7cea9519..e5a2d66213 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -211,7 +211,7 @@ void TextureLoader::threadProc() { // Wait for an event to say there is something in the queue std::unique_lock lock(mMutex); - mEvent.wait(lock); + //mEvent.wait(lock); if (!mTextureDataQ.empty()) { textureData = mTextureDataQ.front(); @@ -231,9 +231,15 @@ void TextureLoader::threadProc() std::unique_lock lock(mMutex); mProcessingTextureDataQ.remove(textureData); } - } - std::this_thread::yield(); + std::this_thread::yield(); + } + else + { + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + } } @@ -260,7 +266,7 @@ void TextureLoader::load(std::shared_ptr textureData) // Put it on the start of the queue as we want the newly requested textures to load first mTextureDataQ.push_front(textureData); mTextureDataLookup[textureData.get()] = mTextureDataQ.cbegin(); - mEvent.notify_one(); + mEvent.notify_all(); } } From 2d51b508c3a1f0c219e07eb8e8b5df0abc9f8c2d Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 7 Jul 2019 10:28:45 +0200 Subject: [PATCH 074/336] HIDE WHEN LAUNCH - OFF : Show game image & game name when launching a game. --- es-app/src/views/ViewController.cpp | 2 ++ es-core/src/Window.cpp | 34 +++++++++++++++++++---- es-core/src/Window.h | 3 ++ es-core/src/resources/ResourceManager.cpp | 31 +++++++++++++++++++-- es-core/src/resources/ResourceManager.h | 2 ++ 5 files changed, 64 insertions(+), 8 deletions(-) diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index e974aeb541..94ef20387c 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -235,6 +235,8 @@ void ViewController::launch(FileData* game, Vector3f center) mWindow->stopInfoPopup(); // make sure we disable any existing info popup mLockInput = true; + mWindow->loadCustomImageLoadingScreen(game->getImagePath(), game->getName()); + std::string transition_style = Settings::getInstance()->getString("GameTransitionStyle"); if(transition_style == "fade") { diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index b473171eaf..deed1dc503 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -18,7 +18,7 @@ Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCoun { mHelp = new HelpComponent(this); mBackgroundOverlay = new ImageComponent(this); - mSplash = NULL; + mSplash = NULL; } Window::~Window() @@ -308,10 +308,8 @@ void Window::setAllowSleep(bool sleep) void Window::endRenderLoadingScreen() { - if (mSplash != NULL) - mSplash->unload(); - mSplash = NULL; + mCustomSplash = ""; } void Window::renderLoadingScreen(std::string text, float percent, unsigned char opacity) @@ -367,6 +365,24 @@ void Window::renderLoadingScreen(std::string text, float percent, unsigned char #endif } +void Window::loadCustomImageLoadingScreen(std::string imagePath, std::string customText) +{ + if (!Utils::FileSystem::exists(imagePath)) + return; + + if (Settings::getInstance()->getBool("HideWindow")) + return; + + if (mSplash != NULL) + endRenderLoadingScreen(); + + mSplash = TextureResource::get(imagePath, false, true, false, false); + mCustomSplash = customText; + + std::shared_ptr& rm = ResourceManager::getInstance(); + rm->removeReloadable(mSplash); +} + void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) { if (mSplash == NULL) @@ -377,6 +393,7 @@ void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); ImageComponent splash(this, true); + splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); if (mSplash != NULL) @@ -385,13 +402,18 @@ void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) splash.setImage(":/splash.svg"); splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); - splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); + + if (!mCustomSplash.empty()) + splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 210)); + else + splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); + splash.render(trans); auto& font = mDefaultFonts.at(1); font->reload(); // Ensure font is loaded - TextCache* cache = font->buildTextCache(_T("Loading..."), 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); + TextCache* cache = font->buildTextCache(mCustomSplash.empty() ? _T("Loading...") : mCustomSplash, 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); float y = Math::round(Renderer::getScreenHeight() * 0.835f); diff --git a/es-core/src/Window.h b/es-core/src/Window.h index be3f548cd8..9fc7e8aca6 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -70,6 +70,8 @@ class Window void renderLoadingScreen(std::string text, float percent = -1, unsigned char opacity = 255); void renderGameLoadingScreen(float opacity=1, bool swapBuffers=true); + void loadCustomImageLoadingScreen(std::string imagePath, std::string customText); + void renderHelpPromptsEarly(); // used to render HelpPrompts before a fade void setHelpPrompts(const std::vector& prompts, const HelpStyle& style); @@ -95,6 +97,7 @@ class Window bool mRenderScreenSaver; std::shared_ptr mSplash; + std::string mCustomSplash; std::vector mGuiStack; diff --git a/es-core/src/resources/ResourceManager.cpp b/es-core/src/resources/ResourceManager.cpp index 1693bb4ac2..c18bcd14ea 100644 --- a/es-core/src/resources/ResourceManager.cpp +++ b/es-core/src/resources/ResourceManager.cpp @@ -103,8 +103,12 @@ void ResourceManager::unloadAll() std::shared_ptr info = *iter; if (!info->data.expired()) - { - info->reload = info->data.lock()->unload(); + { + if (!info->locked) + info->reload = info->data.lock()->unload(); + else + info->locked = false; + iter++; } else @@ -139,5 +143,28 @@ void ResourceManager::addReloadable(std::weak_ptr reloadable) std::shared_ptr info = std::make_shared(); info->data = reloadable; info->reload = false; + info->locked = false; mReloadables.push_back(info); } + +void ResourceManager::removeReloadable(std::weak_ptr reloadable) +{ + auto iter = mReloadables.cbegin(); + while (iter != mReloadables.cend()) + { + std::shared_ptr info = *iter; + + if (!info->data.expired()) + { + if (info->data.lock() == reloadable.lock()) + { + info->locked = true; + break; + } + + iter++; + } + else + iter = mReloadables.erase(iter); + } +} \ No newline at end of file diff --git a/es-core/src/resources/ResourceManager.h b/es-core/src/resources/ResourceManager.h index 34f097f009..df68c6c958 100644 --- a/es-core/src/resources/ResourceManager.h +++ b/es-core/src/resources/ResourceManager.h @@ -30,6 +30,7 @@ class ResourceManager static std::shared_ptr& getInstance(); void addReloadable(std::weak_ptr reloadable); + void removeReloadable(std::weak_ptr reloadable); void unloadAll(); void reloadAll(); @@ -50,6 +51,7 @@ class ResourceManager public: std::weak_ptr data; bool reload; + bool locked; }; std::list> mReloadables; // std::weak_ptr From bb56e6b039eaf6e3b8ddda554b65a51aff0c99d1 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 8 Jul 2019 17:41:25 +0200 Subject: [PATCH 075/336] Sort order : Save/Restore sort order by system. Ninepatch : Opacity now works Theming "imagegrid.animateSelection" : allow disabling background movment. Images : Resize images bigger than screen to save VRAM. --- es-app/src/SystemData.cpp | 9 +++ es-app/src/SystemData.h | 5 ++ es-app/src/guis/GuiGamelistOptions.cpp | 15 +++- es-app/src/guis/GuiGamelistOptions.h | 2 +- es-app/src/views/ViewController.cpp | 6 +- .../src/views/gamelist/GridGameListView.cpp | 15 +++- es-core/src/ThemeData.cpp | 2 + es-core/src/components/GridTileComponent.cpp | 13 ++++ es-core/src/components/ImageGridComponent.h | 40 ++++++++-- es-core/src/components/NinePatchComponent.cpp | 18 +++++ es-core/src/components/NinePatchComponent.h | 2 + es-core/src/resources/TextureData.cpp | 12 ++- es-core/src/resources/TextureDataManager.cpp | 73 +++++++++++++++---- 13 files changed, 184 insertions(+), 28 deletions(-) diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index aaede17815..fe7f98c84e 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -23,6 +23,7 @@ std::vector SystemData::sSystemVector; SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) { + mSortId = 0; mGridSizeOverride = Vector2f(0, 0); mViewModeChanged = false; mFilterIndex = nullptr;// new FileFilterIndex(); @@ -62,6 +63,8 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys auto gridSizeOverride = Vector2f::parseString(Settings::getInstance()->getString(getName() + ".gridSize")); setSystemViewMode(defaultView, gridSizeOverride, false); + mSortId = Settings::getInstance()->getInt(getName() + ".sort"), + setIsGameSystemStatus(); loadTheme(); } @@ -729,3 +732,9 @@ void SystemData::loadTheme() mTheme = std::make_shared(); // reset to empty } } + +void SystemData::setSortId(const unsigned int sortId) +{ + mSortId = sortId; + Settings::getInstance()->setInt(getName() + ".sort", mSortId); +} \ No newline at end of file diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index c83ac19142..b80c8d2586 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -157,6 +157,9 @@ class SystemData if (mFilterIndex != nullptr) mFilterIndex->setUIModeFilters(); } + unsigned int getSortId() const { return mSortId; }; + void setSortId(const unsigned int sortId = 0); + private: static SystemData* loadSystem(pugi::xml_node system); @@ -172,6 +175,8 @@ class SystemData Vector2f mGridSizeOverride; bool mViewModeChanged; + unsigned int mSortId; + void populateFolder(FolderData* folder, std::unordered_map& fileMap); void indexAllGameFilters(const FolderData* folder); void setIsGameSystemStatus(); diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index b1be6e8962..b63004a42e 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -125,11 +125,16 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool mMenu.addRow(row); // sort list by + unsigned int currentSortId = mSystem->getSortId(); + if (currentSortId > FileSorts::SortTypes.size()) { + currentSortId = 0; + } + mListSort = std::make_shared(mWindow, _T("SORT GAMES BY"), false); for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) { const FolderData::SortType& sort = FileSorts::SortTypes.at(i); - mListSort->add(_T(Utils::String::toUpper(sort.description)), &sort, i == 0); // TODO - actually make the sort type persistent + mListSort->add(_T(Utils::String::toUpper(sort.description)), i, i == currentSortId); } mMenu.addWithLabel(_T("SORT GAMES BY"), mListSort); @@ -286,9 +291,13 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool GuiGamelistOptions::~GuiGamelistOptions() { // apply sort - if (!fromPlaceholder) { + if (!fromPlaceholder && mListSort->getSelected() != mSystem->getSortId()) + { + mSystem->setSortId(mListSort->getSelected()); + FolderData* root = mSystem->getRootFolder(); - root->sort(*mListSort->getSelected()); // will also recursively sort children + const FolderData::SortType& sort = FileSorts::SortTypes.at(mListSort->getSelected()); + root->sort(sort); // notify that the root folder was sorted getGamelist()->onFileChanged(root, FILE_SORTED); diff --git a/es-app/src/guis/GuiGamelistOptions.h b/es-app/src/guis/GuiGamelistOptions.h index 5d04361156..f12d160f78 100644 --- a/es-app/src/guis/GuiGamelistOptions.h +++ b/es-app/src/guis/GuiGamelistOptions.h @@ -35,7 +35,7 @@ class GuiGamelistOptions : public GuiComponent typedef OptionListComponent LetterList; std::shared_ptr mJumpToLetterList; - typedef OptionListComponent SortList; + typedef OptionListComponent SortList; std::shared_ptr mListSort; std::shared_ptr> mViewMode; diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 94ef20387c..58ac4f6387 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -542,9 +542,6 @@ void ViewController::preload() bool splash = Settings::getInstance()->getBool("SplashScreen") && Settings::getInstance()->getBool("SplashScreenProgress"); if (splash) mWindow->renderLoadingScreen(_T("Preloading UI"), (float)i / (float)max); - - // First load the system list - getSystemListView(); for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { @@ -557,6 +554,9 @@ void ViewController::preload() (*it)->resetFilters(); getGameListView(*it); } + + // First load the system list + getSystemListView(); } void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 281d189eb9..68f2b504e0 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -496,7 +496,20 @@ void GridGameListView::addPlaceholder() void GridGameListView::launch(FileData* game) { - ViewController::get()->launch(game); + Vector3f target(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f, 0); + + if (mVideoVisible && mVideo) + target = Vector3f(mVideo->getCenter().x(), mVideo->getCenter().y(), 0); + else if (mImageVisible) + target = Vector3f(mImage.getCenter().x(), mImage.getCenter().y(), 0); + else + { + auto tile = mGrid.getSelectedTile(); + if (tile != nullptr) + target = Vector3f(tile->getCenter().x(), tile->getCenter().y(), 0); + } + + ViewController::get()->launch(game, target); } void GridGameListView::remove(FileData *game, bool deleteFile) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 3cdfefc0e0..315887ebb4 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -43,6 +43,8 @@ std::map> The { "autoLayout", NORMALIZED_PAIR }, { "autoLayoutSelectedZoom", FLOAT }, + { "animateSelection", BOOLEAN }, + { "imageSource", STRING }, // image, thumbnail, marquee { "zIndex", FLOAT }, { "gameImage", PATH }, diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 5741b94934..205298a528 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -212,6 +212,14 @@ void GridTileComponent::resize() { if (currentProperties.mImageSizeMode == "minSize") bkSize = Vector2f(size.x(), size.y() - bottomPadding + topPadding); + else if (mAnimPosition == Vector3f(0, 0, 0)) + { + bkposition = Vector3f( + mImage->getPosition().x() - mImage->getSize().x() / 2 - mSelectedProperties.mPadding.x(), + mImage->getPosition().y() - mImage->getSize().y() / 2 - mSelectedProperties.mPadding.y(), 0); + + bkSize = Vector2f(mImage->getSize().x() + 2 * mSelectedProperties.mPadding.x(), mImage->getSize().y() + 2 * mSelectedProperties.mPadding.y()); + } else { bkposition = Vector3f( @@ -239,6 +247,11 @@ void GridTileComponent::resize() mBackground.setCenterColor(currentProperties.mBackgroundCenterColor); mBackground.setEdgeColor(currentProperties.mBackgroundEdgeColor); mBackground.setImagePath(currentProperties.mBackgroundImage); + + if (mSelected && mAnimPosition == Vector3f(0, 0, 0) && mSelectedZoomPercent != 1.0) + mBackground.setOpacity(mSelectedZoomPercent * 255); + else + mBackground.setOpacity(255); } void GridTileComponent::update(int deltaTime) diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 432d02a6ee..4eb0fb0eb9 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -74,6 +74,8 @@ class ImageGridComponent : public IList void setGridSizeOverride(Vector2f size); + std::shared_ptr getSelectedTile(); + protected: virtual void onCursorChanged(const CursorState& state) override; @@ -93,6 +95,7 @@ class ImageGridComponent : public IList // TILES bool mLastRowPartial; + bool mAnimateSelection; Vector2f mAutoLayout; float mAutoLayoutZoom; Vector4f mPadding; @@ -141,6 +144,7 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList::setGridSizeOverride(Vector2f size) mGridSizeOverride = size; } +template +std::shared_ptr ImageGridComponent::getSelectedTile() +{ + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + std::shared_ptr tile = mTiles.at(ti); + if (tile->isSelected()) + return tile; + } + + return nullptr; +} + template void ImageGridComponent::onHide() { @@ -311,8 +328,6 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) (*it)->setPosition((*it)->getPosition().x() + offsetX, (*it)->getPosition().y() + offsetY); } - - // Render the selected image background on bottom of the others if needed std::shared_ptr selectedTile = NULL; for(auto it = mTiles.begin(); it != mTiles.end(); it++) @@ -321,7 +336,8 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) if (tile->isSelected()) { selectedTile = tile; - if (tile->shouldSplitRendering()) + + if (mAnimateSelection && tile->shouldSplitRendering()) tile->renderBackground(trans); break; @@ -338,7 +354,7 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) // Render the selected image content on top of the others if (selectedTile != NULL) { - if (selectedTile->shouldSplitRendering()) + if (mAnimateSelection && selectedTile->shouldSplitRendering()) selectedTile->renderContent(trans); else selectedTile->render(trans); @@ -380,6 +396,9 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("autoLayout")) mAutoLayout = elem->get("autoLayout"); + if (elem->has("animateSelection")) + mAnimateSelection = elem->get("animateSelection"); + if (elem->has("autoLayoutSelectedZoom")) mAutoLayoutZoom = elem->get("autoLayoutSelectedZoom"); @@ -568,7 +587,15 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) } if (newTile != nullptr) - newTile->setSelected(true, true, oldPos == Vector3f(0, 0) ? nullptr : &oldPos, true); + { + if (!mAnimateSelection) + { + oldPos = Vector3f(0, 0); + newTile->setSelected(true, true, &oldPos, true); + } + else + newTile->setSelected(true, true, oldPos == Vector3f(0, 0) ? nullptr : &oldPos, true); + } } int firstVisibleCol = mStartPosition / dimOpposite; @@ -758,6 +785,9 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA idx = 0; Vector3f pos = mTiles.at(idx)->getBackgroundPosition(); + if (!mAnimateSelection) + pos = Vector3f(0, 0, 0); + tile->setSelected(true, allowAnimation, &pos); } else diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index 910032d919..7443dd93ec 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -30,8 +30,26 @@ void NinePatchComponent::updateColors() if (mColors == NULL) return; + unsigned int oe = mEdgeColor; + unsigned int oc = mCenterColor; + + mEdgeColor = (mEdgeColor >> 8 << 8) | (unsigned char)(((mEdgeColor & 0xff) / 255.0f * (mOpacity / 255.0f)) * 255.0f); + mCenterColor = (mCenterColor >> 8 << 8) | (unsigned char)(((mCenterColor & 0xff) / 255.0f * (mOpacity / 255.0f)) * 255.0f); + Renderer::buildGLColorArray(mColors, mEdgeColor, 6 * 9); Renderer::buildGLColorArray(&mColors[4 * 6 * 4], mCenterColor, 6); + + mEdgeColor = oe; + mCenterColor = oc; +} + +void NinePatchComponent::setOpacity(unsigned char opacity) +{ + if (mOpacity == opacity) + return; + + mOpacity = opacity; + updateColors(); } void NinePatchComponent::buildVertices() diff --git a/es-core/src/components/NinePatchComponent.h b/es-core/src/components/NinePatchComponent.h index b197e9adfd..11ad7dea11 100644 --- a/es-core/src/components/NinePatchComponent.h +++ b/es-core/src/components/NinePatchComponent.h @@ -44,6 +44,8 @@ class NinePatchComponent : public GuiComponent void setCornerSize(int sizeX, int sizeY); inline void setCornerSize(const Vector2f& size) { setCornerSize(size.x(), size.y()); } + virtual void setOpacity(unsigned char opacity); + private: void buildVertices(); void updateColors(); diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index b14b15dc0b..fa664dc882 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -10,6 +10,7 @@ #include #include #include +#include "Renderer.h" #define DPI 96 @@ -153,7 +154,16 @@ bool TextureData::initImageFromMemory(const unsigned char* fileData, size_t leng return true; } - unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height, OPTIMIZEVRAM ? mMaxSize.x() : 0, mMaxSize.y(), mMaxSize.externalZoom(), mBaseSize, mPackedSize); + + auto x = OPTIMIZEVRAM ? mMaxSize.x() : Renderer::getScreenWidth(); + if (x > Renderer::getScreenWidth()) + x = Renderer::getScreenWidth(); + + auto y = OPTIMIZEVRAM ? mMaxSize.y() : Renderer::getScreenHeight(); + if (y > Renderer::getScreenHeight()) + y = Renderer::getScreenHeight(); + + unsigned char* imageRGBA = ImageIO::loadFromMemoryRGBA32Ex((const unsigned char*)(fileData), length, width, height, x, y, mMaxSize.externalZoom(), mBaseSize, mPackedSize); if (imageRGBA == NULL) { LOG(LogError) << "Could not initialize texture from memory, invalid data! (file path: " << mPath << ", data ptr: " << (size_t)fileData << ", reported size: " << length << ")"; diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index e5a2d66213..99c61c17d3 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -3,7 +3,8 @@ #include "resources/TextureData.h" #include "resources/TextureResource.h" #include "Settings.h" - +#include "utils/StringUtil.h" +#include "utils/FileSystemUtil.h" #include TextureDataManager::TextureDataManager() @@ -129,6 +130,38 @@ size_t TextureDataManager::getQueueSize() return mLoader->getQueueSize(); } +bool compareTextures(const std::shared_ptr& first, const std::shared_ptr& second) +{ + bool isResource = first->mPath.rfind(":/") == 0; + bool secondIsResource = second->mPath.rfind(":/") == 0; + if (isResource && !secondIsResource) + return true; + /* + if (secondIsResource) + return false; + + bool firstisTheme = first->mPath.rfind("/themes/") != std::string::npos; + bool secondIsTheme = second->mPath.rfind("/themes/") != std::string::npos; + + + if (firstisTheme && secondIsTheme) + { + auto firstExt = Utils::String::toLower(Utils::FileSystem::getExtension(first->mPath)); + if (firstExt == ".svg") + { + auto secondExt = Utils::String::toLower(Utils::FileSystem::getExtension(second->mPath)); + if (secondExt != ".svg") + return true; + + return false; + } + } + + if (firstisTheme && !secondIsTheme) + return true; + */ + return false; +} void TextureDataManager::load(std::shared_ptr tex, bool block) { @@ -149,22 +182,34 @@ void TextureDataManager::load(std::shared_ptr tex, bool block) size_t size = TextureResource::getTotalMemUsage(); size_t max_texture = (size_t)Settings::getInstance()->getInt("MaxVRAM") * 1024 * 1024; - for (auto it = mTextures.crbegin(); it != mTextures.crend(); ++it) + if (size >= max_texture) { - if (size < max_texture) - break; + std::list> orderedTextures(mTextures); + orderedTextures.sort(compareTextures); + + for (auto it = orderedTextures.crbegin(); it != orderedTextures.crend(); ++it) + { + if (size < max_texture) + break; - if ((*it) == tex) - continue; + if ((*it) == tex) + continue; - (*it)->releaseVRAM(); - (*it)->releaseRAM(); - // It may be already in the loader queue. In this case it wouldn't have been using - // any VRAM yet but it will be. Remove it from the loader queue - mLoader->remove(*it); - size = TextureResource::getTotalMemUsage(); - } +#if _DEBUG + if ((*it)->isLoaded()) + TRACE("Unloading " << (*it)->mPath); +#endif + + (*it)->releaseVRAM(); + (*it)->releaseRAM(); + + // It may be already in the loader queue. In this case it wouldn't have been using + // any VRAM yet but it will be. Remove it from the loader queue + mLoader->remove(*it); + size = TextureResource::getTotalMemUsage(); + } + } if (!block) { @@ -237,7 +282,7 @@ void TextureLoader::threadProc() else { std::this_thread::yield(); - std::this_thread::sleep_for(std::chrono::milliseconds(20)); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); } } From 486c185fa2b76cc08e27c43561cf12f1e1e85b8a Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 10 Jul 2019 17:25:01 +0200 Subject: [PATCH 076/336] Custom collections : make stored paths relative to home (if they are). Gamelist : don't try to save gamelist when a file without metadata is found ( otherwise it saves everytime ) & trying to secure saving. TextureResources : Avoid crashing if a ImageComponent has negative size ( possible in grid ) SystemView : Realtime position/size calculations of carousel... Not used yet, but I have a project for it.... --- es-app/src/CollectionSystemManager.cpp | 11 +- es-app/src/Gamelist.cpp | 63 ++++++---- es-app/src/SystemData.cpp | 2 +- es-app/src/views/SystemView.cpp | 118 +++++++++++++------ es-app/src/views/SystemView.h | 9 +- es-app/src/views/ViewController.cpp | 12 ++ es-core/src/components/ImageComponent.cpp | 2 +- es-core/src/components/VideoVlcComponent.cpp | 2 +- es-core/src/resources/TextureResource.cpp | 5 +- es-core/src/resources/TextureResource.h | 2 +- es-core/src/utils/FileSystemUtil.cpp | 2 +- 11 files changed, 157 insertions(+), 71 deletions(-) diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 721834bb32..b066e54648 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -103,15 +103,21 @@ void CollectionSystemManager::saveCustomCollection(SystemData* sys) auto games = sys->getRootFolder()->getChildren(); bool found = mCustomCollectionSystemsData.find(name) != mCustomCollectionSystemsData.cend(); - if (found) { + if (found) + { CollectionSystemData sysData = mCustomCollectionSystemsData.at(name); if (sysData.needsSave) { + auto home = Utils::FileSystem::getHomePath(); + std::ofstream configFile; configFile.open(getCustomCollectionConfigPath(name)); for(auto iter = games.cbegin(); iter != games.cend(); ++iter) { std::string path = (*iter)->getKey(); + + path = Utils::FileSystem::createRelativePath(path, "portnawak", true); + configFile << path << std::endl; } configFile.close(); @@ -781,6 +787,8 @@ void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sys // iterate list of files in config file for(std::string gameKey; getline(input, gameKey); ) { + gameKey = Utils::FileSystem::resolveRelativePath(gameKey, "portnawak", true); + std::unordered_map::const_iterator it = pMap->find(gameKey); if (it != pMap->cend()) { @@ -793,6 +801,7 @@ void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sys LOG(LogInfo) << "Couldn't find game referenced at '" << gameKey << "' for system config '" << path << "'"; } } + rootFolder->sort(getSortTypeFromString(sysDecl.defaultSort)); updateCollectionFolderMetadata(newSys); } diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index afce45b7fe..25e8d622c4 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -9,10 +9,15 @@ #include "SystemData.h" #include +#ifdef WIN32 +#include +#endif + FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType type, std::unordered_map& fileMap) { // first, verify that path is within the system's root folder FolderData* root = system->getRootFolder(); + bool contains = false; std::string relative = Utils::FileSystem::removeCommonPath(path, root->getPath(), contains); @@ -35,6 +40,8 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType { std::string key = Utils::FileSystem::combine(treeNode->getPath(), *path_it); FileData* item = (fileMap.find(key) != fileMap.end()) ? fileMap[key] : nullptr; + if (item != nullptr) + return item; // this is the end if(path_it == --pathList.end()) @@ -47,22 +54,19 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType if (type == GAME) // Final file { - if (item == nullptr) + // Skip if the extension in the gamelist is unknown + if (!system->getSystemEnvData()->isValidExtension(Utils::FileSystem::getExtension(path))) + { + LOG(LogWarning) << "gameList: file extension is not known by systemlist"; + return NULL; + } + + // Add final game + item = new FileData(GAME, path, system); + if (!item->isArcadeAsset()) { - // Skip if the extension in the gamelist is unknown - if (!system->getSystemEnvData()->isValidExtension(Utils::FileSystem::getExtension(path))) - { - LOG(LogWarning) << "gameList: file extension is not known by systemlist"; - return NULL; - } - - // Add final game - item = new FileData(GAME, path, system); - if (!item->isArcadeAsset()) - { - fileMap[key] = item; - treeNode->addChild(item); - } + fileMap[key] = item; + treeNode->addChild(item); } return item; @@ -93,12 +97,14 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType void refactorGameFolders(SystemData* system) { FolderData* root = system->getRootFolder(); + if (root == nullptr) + return; auto childs = root->getChildren(); for (int i = childs.size() - 1; i >= 0; i--) { FileData* item = childs.at(i); - if (item->getType() == FOLDER) + if (item != nullptr && item->getType() == FOLDER) { FolderData* folder = (FolderData*)item; FileData* uniqueGame = folder->findUniqueGameForFolder(); @@ -186,7 +192,7 @@ void parseGamelist(SystemData* system, std::unordered_mapgetPath(), system->getStartPath(), false).c_str()); + return false; } + + //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(Utils::FileSystem::createRelativePath(file->getPath(), system->getStartPath(), false).c_str()); + return true; } void updateGamelist(SystemData* system) @@ -290,8 +297,8 @@ void updateGamelist(SystemData* system) } // it was either removed or never existed to begin with; either way, we can add it now - addFileDataNode(root, *fit, tag, system); - ++numUpdated; + if (addFileDataNode(root, *fit, tag, system)) + ++numUpdated; // Only if really added } //now write the file @@ -313,8 +320,14 @@ void updateGamelist(SystemData* system) } else if (Utils::FileSystem::exists(tmpFile)) { + doc.reset(); + +#ifdef WIN32 + ::Sleep(50); // Introduce a small sleep +#endif + // Secure XML writing - if ((int) Utils::FileSystem::getFileSize(tmpFile) > 0) + if (Utils::FileSystem::getFileSize(tmpFile) != 0) { std::string savFile = xmlWritePath + ".old"; diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index fe7f98c84e..77631aa6da 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -49,7 +49,7 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys refactorGameFolders(this); //StopWatch ws("sort " + mName); - mRootFolder->sort(FileSorts::SortTypes.at(0)); + // mRootFolder->sort(FileSorts::SortTypes.at(0)); //indexAllGameFilters(mRootFolder); } diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 0388183f15..ab18930aca 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -43,7 +43,8 @@ void SystemView::populate() Entry e; e.name = (*it)->getName(); e.object = *it; - + e.data.logoIsImage = false; + // make logo const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); if (logoElem) @@ -56,10 +57,11 @@ void SystemView::populate() { // Remove dynamic flags for png & jpg files : themes can contain oversized images that can't be unloaded by the TextureResource manager ImageComponent* logo = new ImageComponent(mWindow, false, Utils::String::toLower(Utils::FileSystem::getExtension(path)) != ".svg"); - logo->setMaxSize(mCarousel.logoSize * mCarousel.logoScale); + logo->setMaxSize(carouselLogoSize() * mCarousel.logoScale); logo->applyTheme(theme, "system", "logo", ThemeFlags::PATH | ThemeFlags::SIZE | ThemeFlags::COLOR); logo->setRotateByTargetSize(true); e.data.logo = std::shared_ptr(logo); + e.data.logoIsImage = true; } } @@ -71,7 +73,7 @@ void SystemView::populate() Font::get(FONT_SIZE_LARGE), 0x000000FF, ALIGN_CENTER); - text->setSize(mCarousel.logoSize * mCarousel.logoScale); + text->setSize(carouselLogoSize() * mCarousel.logoScale); text->applyTheme((*it)->getTheme(), "system", "logoText", ThemeFlags::FONT_PATH | ThemeFlags::FONT_SIZE | ThemeFlags::COLOR | ThemeFlags::FORCE_UPPERCASE | ThemeFlags::LINE_SPACING | ThemeFlags::TEXT); e.data.logo = std::shared_ptr(text); @@ -105,7 +107,7 @@ void SystemView::populate() } - Vector2f denormalized = mCarousel.logoSize * e.data.logo->getOrigin(); + Vector2f denormalized = carouselLogoSize() * e.data.logo->getOrigin(); e.data.logo->setPosition(denormalized.x(), denormalized.y(), 0.0); // delete any existing extras for (auto extra : e.data.backgroundExtras) @@ -356,6 +358,26 @@ void SystemView::render(const Transform4x4f& parentTrans) { if(size() == 0) return; // nothing to render + + if (mSize.x() != Renderer::getScreenWidth() || mSize.x() != Renderer::getScreenHeight()) + { + for (int i = 0; i < mEntries.size(); i++) + { + if (mEntries[i].data.logo) + { + Vector2f denormalized = carouselLogoSize() * mEntries[i].data.logo->getOrigin(); + mEntries[i].data.logo->setPosition(denormalized.x(), denormalized.y(), 0.0); + + if (mEntries[i].data.logoIsImage) + { + ImageComponent* img = (ImageComponent*)mEntries[i].data.logo.get(); + img->setMaxSize(carouselLogoSize() * mCarousel.logoScale); + } + else + mEntries[i].data.logo->setSize(carouselLogoSize() * mCarousel.logoScale); + } + } + } Transform4x4f trans = getTransform() * parentTrans; @@ -443,18 +465,18 @@ void SystemView::renderCarousel(const Transform4x4f& trans) { // background box behind logos Transform4x4f carouselTrans = trans; - carouselTrans.translate(Vector3f(mCarousel.pos.x(), mCarousel.pos.y(), 0.0)); - carouselTrans.translate(Vector3f(mCarousel.origin.x() * mCarousel.size.x() * -1, mCarousel.origin.y() * mCarousel.size.y() * -1, 0.0f)); + carouselTrans.translate(Vector3f(carouselPos().x(), carouselPos().y(), 0.0)); + carouselTrans.translate(Vector3f(mCarousel.origin.x() * carouselSize().x() * -1, mCarousel.origin.y() * carouselSize().y() * -1, 0.0f)); Vector2f clipPos(carouselTrans.translation().x(), carouselTrans.translation().y()); - Renderer::pushClipRect(Vector2i((int)clipPos.x(), (int)clipPos.y()), Vector2i((int)mCarousel.size.x(), (int)mCarousel.size.y())); + Renderer::pushClipRect(Vector2i((int)clipPos.x(), (int)clipPos.y()), Vector2i((int)carouselSize().x(), (int)carouselSize().y())); Renderer::setMatrix(carouselTrans); if (mCarousel.colorGradient != 0) - Renderer::drawGradientRect(0.0, 0.0, mCarousel.size.x(), mCarousel.size.y(), mCarousel.color, mCarousel.colorGradient, mCarousel.colorGradientHorz); + Renderer::drawGradientRect(0.0, 0.0, carouselSize().x(), carouselSize().y(), mCarousel.color, mCarousel.colorGradient, mCarousel.colorGradientHorz); else - Renderer::drawRect(0.0, 0.0, mCarousel.size.x(), mCarousel.size.y(), mCarousel.color); + Renderer::drawRect(0.0, 0.0, carouselSize().x(), carouselSize().y(), mCarousel.color); // draw logos Vector2f logoSpacing(0.0, 0.0); // NB: logoSpacing will include the size of the logo itself as well! @@ -464,53 +486,53 @@ void SystemView::renderCarousel(const Transform4x4f& trans) switch (mCarousel.type) { case VERTICAL_WHEEL: - yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2.f - (mCamOffset * logoSpacing[1]); + yOff = (carouselSize().y() - carouselLogoSize().y()) / 2.f - (mCamOffset * logoSpacing[1]); if (mCarousel.logoAlignment == ALIGN_LEFT) - xOff = mCarousel.logoSize.x() / 10.f; + xOff = carouselLogoSize().x() / 10.f; else if (mCarousel.logoAlignment == ALIGN_RIGHT) - xOff = mCarousel.size.x() - (mCarousel.logoSize.x() * 1.1f); + xOff = carouselSize().x() - (carouselLogoSize().x() * 1.1f); else - xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2.f; + xOff = (carouselSize().x() - carouselLogoSize().x()) / 2.f; break; case VERTICAL: - logoSpacing[1] = ((mCarousel.size.y() - (mCarousel.logoSize.y() * mCarousel.maxLogoCount)) / (mCarousel.maxLogoCount)) + mCarousel.logoSize.y(); - yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2.f - (mCamOffset * logoSpacing[1]); + logoSpacing[1] = ((carouselSize().y() - (carouselLogoSize().y() * mCarousel.maxLogoCount)) / (mCarousel.maxLogoCount)) + carouselLogoSize().y(); + yOff = (carouselSize().y() - carouselLogoSize().y()) / 2.f - (mCamOffset * logoSpacing[1]); if (mCarousel.logoAlignment == ALIGN_LEFT) - xOff = mCarousel.logoSize.x() / 10.f; + xOff = carouselLogoSize().x() / 10.f; else if (mCarousel.logoAlignment == ALIGN_RIGHT) - xOff = mCarousel.size.x() - (mCarousel.logoSize.x() * 1.1f); + xOff = carouselSize().x() - (carouselLogoSize().x() * 1.1f); else - xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2; + xOff = (carouselSize().x() - carouselLogoSize().x()) / 2; break; case HORIZONTAL_WHEEL: - xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2 - (mCamOffset * logoSpacing[1]); + xOff = (carouselSize().x() - carouselLogoSize().x()) / 2 - (mCamOffset * logoSpacing[1]); if (mCarousel.logoAlignment == ALIGN_TOP) - yOff = mCarousel.logoSize.y() / 10; + yOff = carouselLogoSize().y() / 10; else if (mCarousel.logoAlignment == ALIGN_BOTTOM) - yOff = mCarousel.size.y() - (mCarousel.logoSize.y() * 1.1f); + yOff = carouselSize().y() - (carouselLogoSize().y() * 1.1f); else - yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2; + yOff = (carouselSize().y() - carouselLogoSize().y()) / 2; break; case HORIZONTAL: default: - logoSpacing[0] = ((mCarousel.size.x() - (mCarousel.logoSize.x() * mCarousel.maxLogoCount)) / (mCarousel.maxLogoCount)) + mCarousel.logoSize.x(); - xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2.f - (mCamOffset * logoSpacing[0]); + logoSpacing[0] = ((carouselSize().x() - (carouselLogoSize().x() * mCarousel.maxLogoCount)) / (mCarousel.maxLogoCount)) + carouselLogoSize().x(); + xOff = (carouselSize().x() - carouselLogoSize().x()) / 2.f - (mCamOffset * logoSpacing[0]); if (mCarousel.logoAlignment == ALIGN_TOP) - yOff = mCarousel.logoSize.y() / 10.f; + yOff = carouselLogoSize().y() / 10.f; else if (mCarousel.logoAlignment == ALIGN_BOTTOM) - yOff = mCarousel.size.y() - (mCarousel.logoSize.y() * 1.1f); + yOff = carouselSize().y() - (carouselLogoSize().y() * 1.1f); else - yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2.f; + yOff = (carouselSize().y() - carouselLogoSize().y()) / 2.f; break; } if (mCarousel.logoPos.x() >= 0) - xOff = mCarousel.logoPos.x() - (mCarousel.type == HORIZONTAL ? (mCamOffset * logoSpacing[0]) : 0); + xOff = carouselLogoPos().x() - (mCarousel.type == HORIZONTAL ? (mCamOffset * logoSpacing[0]) : 0); if (mCarousel.logoPos.y() >= 0) - yOff = mCarousel.logoPos.y() - (mCarousel.type == VERTICAL ? (mCamOffset * logoSpacing[1]) : 0); + yOff = carouselLogoPos().y() - (mCarousel.type == VERTICAL ? (mCamOffset * logoSpacing[1]) : 0); int center = (int)(mCamOffset); int logoCount = Math::min(mCarousel.maxLogoCount, (int)mEntries.size()); @@ -621,10 +643,10 @@ void SystemView::getDefaultElements(void) // Carousel mCarousel.type = HORIZONTAL; mCarousel.logoAlignment = ALIGN_CENTER; - mCarousel.size.x() = mSize.x(); - mCarousel.size.y() = 0.2325f * mSize.y(); + mCarousel.size.x() = 1; // mSize.x(); + mCarousel.size.y() = 0.2325f; // *mSize.y(); mCarousel.pos.x() = 0.0f; - mCarousel.pos.y() = 0.5f * (mSize.y() - mCarousel.size.y()); + mCarousel.pos.y() = 0.5f - mCarousel.size.y(); // *(mSize.y() - mCarousel.size.y()); mCarousel.origin.x() = 0.0f; mCarousel.origin.y() = 0.0f; mCarousel.color = 0xFFFFFFD8; @@ -634,8 +656,8 @@ void SystemView::getDefaultElements(void) mCarousel.logoRotation = 7.5; mCarousel.logoRotationOrigin.x() = -5; mCarousel.logoRotationOrigin.y() = 0.5; - mCarousel.logoSize.x() = 0.25f * mSize.x(); - mCarousel.logoSize.y() = 0.155f * mSize.y(); + mCarousel.logoSize.x() = 0.25f; // *mSize.x(); + mCarousel.logoSize.y() = 0.155f; // *mSize.y(); mCarousel.logoPos = Vector2f(-1, -1); mCarousel.maxLogoCount = 3; mCarousel.zIndex = 40; @@ -651,6 +673,26 @@ void SystemView::getDefaultElements(void) mSystemInfo.setDefaultZIndex(50); } +Vector2f SystemView::carouselSize() +{ + return mCarousel.size * mSize; +} + +Vector2f SystemView::carouselLogoPos() +{ + return mCarousel.logoPos * mSize; +} + +Vector2f SystemView::carouselPos() +{ + return mCarousel.pos * mSize; +} + +Vector2f SystemView::carouselLogoSize() +{ + return mCarousel.logoSize * mSize; +} + void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) { if (elem->has("type")) @@ -665,9 +707,9 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) mCarousel.type = HORIZONTAL; } if (elem->has("size")) - mCarousel.size = elem->get("size") * mSize; + mCarousel.size = elem->get("size"); // *mSize; if (elem->has("pos")) - mCarousel.pos = elem->get("pos") * mSize; + mCarousel.pos = elem->get("pos"); // *mSize; if (elem->has("origin")) mCarousel.origin = elem->get("origin"); if (elem->has("color")) @@ -681,9 +723,9 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) if (elem->has("logoScale")) mCarousel.logoScale = elem->get("logoScale"); if (elem->has("logoSize")) - mCarousel.logoSize = elem->get("logoSize") * mSize; + mCarousel.logoSize = elem->get("logoSize"); // *mSize; if (elem->has("logoPos")) - mCarousel.logoPos = elem->get("logoPos") * mSize; + mCarousel.logoPos = elem->get("logoPos"); // *mSize; if (elem->has("maxLogoCount")) mCarousel.maxLogoCount = (int)Math::round(elem->get("maxLogoCount")); if (elem->has("zIndex")) diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index 2e4a585896..80dd6d7ee3 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -20,8 +20,9 @@ enum CarouselType : unsigned int }; struct SystemViewData -{ +{ std::shared_ptr logo; + bool logoIsImage; std::vector backgroundExtras; }; @@ -67,6 +68,12 @@ class SystemView : public IList void onCursorChanged(const CursorState& state) override; private: + Vector2f carouselLogoPos(); + Vector2f carouselLogoSize(); + Vector2f carouselPos(); + Vector2f carouselSize(); + + void populate(); void getViewElements(const std::shared_ptr& theme); void getDefaultElements(void); diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 58ac4f6387..04219d43d0 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -505,7 +505,19 @@ void ViewController::render(const Transform4x4f& parentTrans) // draw systemview if (!Settings::getInstance()->getBool("HideSystemView")) + { + /* + //getSystemListView()->setSize(Vector2f(getSystemListView()->getSize().x(), getSystemListView()->getSize().y())); + getSystemListView()->setSize(Vector2f(getSystemListView()->getSize().x(), 400)); + + Transform4x4f ts = Transform4x4f::Identity(); + Transform4x4f transInverse; + transInverse.invert(getSystemListView()->getTransform()); + + getSystemListView()->render(transInverse); + */ getSystemListView()->render(trans); + } // draw gamelists for(auto it = mGameListViews.cbegin(); it != mGameListViews.cend(); it++) diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 44b327d28c..e29941d408 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -123,7 +123,7 @@ void ImageComponent::resize() mSize[0] = Math::round(mSize.x()); mSize[1] = Math::round(mSize.y()); - mTexture->rasterizeAt((size_t)mSize.x(), (size_t)mSize.y()); + mTexture->rasterizeAt(mSize.x(), mSize.y()); onSizeChanged(); } diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index c589641a64..8fe3faf2c8 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -132,7 +132,7 @@ void VideoVlcComponent::resize() } // mSize.y() should already be rounded - mTexture->rasterizeAt((size_t)Math::round(mSize.x()), (size_t)Math::round(mSize.y())); + mTexture->rasterizeAt(Math::round(mSize.x()), Math::round(mSize.y())); onSizeChanged(); } diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index 16a70a313c..eac70d58ff 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -236,8 +236,11 @@ std::shared_ptr TextureResource::get(const std::string& path, b } // For scalable source images in textures we want to set the resolution to rasterize at -void TextureResource::rasterizeAt(size_t width, size_t height) +void TextureResource::rasterizeAt(float width, float height) { + if (width < 0) width = -width; + if (height < 0) height = -height; + std::shared_ptr data; if (mTextureData != nullptr) data = mTextureData; diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index 5d8e950693..4582af1598 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -56,7 +56,7 @@ class TextureResource : public IReloadable virtual void initFromMemory(const char* file, size_t length); // For scalable source images in textures we want to set the resolution to rasterize at - void rasterizeAt(size_t width, size_t height); + void rasterizeAt(float width, float height); Vector2f getSourceImageSize() const; virtual ~TextureResource(); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index e84a3d5180..01d6522294 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -568,7 +568,7 @@ namespace Utils // replace '~' with homePath if(_allowHome && (path[0] == '~') && (path[1] == '/')) - return (getHomePath() + &(path[1])); + return (getGenericPath(getHomePath()) + &(path[1])); // nothing to resolve return path; From ae9f528c9945b58c9511eff51d783752efe01f91 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 10 Jul 2019 19:23:03 +0200 Subject: [PATCH 077/336] Bugfix : some created then unloaded systems can cause crashes at startup --- es-app/src/SystemData.cpp | 3 +++ es-core/src/ThemeData.h | 2 ++ 2 files changed, 5 insertions(+) diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 77631aa6da..2191573022 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -497,6 +497,9 @@ bool SystemData::loadConfig(Window* window) CollectionSystemManager::get()->loadCollectionSystems(); } + if (SystemData::sSystemVector.size() > 0) + ThemeData::setDefaultTheme(SystemData::sSystemVector.at(0)->getTheme().get()); + return true; } diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 4d0b2992da..cd7c900cb8 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -233,6 +233,8 @@ class ThemeData static std::map sortThemeSubSets(const std::map& subsetmap, const std::string& subset); static std::map getThemeSubSets(const std::string& theme); + static void setDefaultTheme(ThemeData* theme) { mCurrentTheme = theme; }; + private: static void crawlIncludes(const pugi::xml_node& root, std::map& sets, std::deque& dequepath); static void findRegion(const pugi::xml_document& doc, std::map& sets); From db12675aa6f8c195fec0588601a5273db0f6fc25 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 10 Jul 2019 22:31:01 +0200 Subject: [PATCH 078/336] Gamelist : Log xml saves & file renamings. Folders : don't parse downloaded_image & downloaded_videos folders for games. --- es-app/src/Gamelist.cpp | 8 ++++++-- es-app/src/SystemData.cpp | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 25e8d622c4..89d85b5975 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -333,14 +333,18 @@ void updateGamelist(SystemData* system) // remove previous gamelist.xml.old file if (Utils::FileSystem::exists(savFile)) - Utils::FileSystem::removeFile(savFile); + Utils::FileSystem::removeFile(savFile); // rename gamelist.xml to gamelist.xml.old if (Utils::FileSystem::exists(xmlWritePath)) std::rename(xmlWritePath.c_str(), savFile.c_str()); + else + LOG(LogError) << "Unable to rename \"" << xmlWritePath << "to " << savFile << "\"!"; // rename gamelist.tmp.xml to gamelist.xml - std::rename(tmpFile.c_str(), xmlWritePath.c_str()); + if (std::rename(tmpFile.c_str(), xmlWritePath.c_str()) != 0) + LOG(LogError) << "Unable to rename \"" << tmpFile << "to " << xmlWritePath << "\"!"; + } else Utils::FileSystem::removeFile(tmpFile); diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 2191573022..a3d6633f7e 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -181,7 +181,7 @@ void SystemData::populateFolder(FolderData* folder, std::unordered_map Date: Wed, 10 Jul 2019 22:53:35 +0200 Subject: [PATCH 079/336] Gamelist parsing not loading uppercase file extensions. --- es-app/src/Gamelist.cpp | 2 +- es-app/src/SystemData.cpp | 13 ++----------- 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 89d85b5975..b5cbebc0f3 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -55,7 +55,7 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType if (type == GAME) // Final file { // Skip if the extension in the gamelist is unknown - if (!system->getSystemEnvData()->isValidExtension(Utils::FileSystem::getExtension(path))) + if (!system->getSystemEnvData()->isValidExtension(Utils::String::toLower(Utils::FileSystem::getExtension(path)))) { LOG(LogWarning) << "gameList: file extension is not known by systemlist"; return NULL; diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index a3d6633f7e..014ac0d0d7 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -111,12 +111,6 @@ void SystemData::setIsGameSystemStatus() mIsGameSystem = (mName != "retropie"); } -char _easytolower(char in) { - if (in <= 'Z' && in >= 'A') - return in - ('Z' - 'z'); - return in; -} - void SystemData::populateFolder(FolderData* folder, std::unordered_map& fileMap) { const std::string& folderPath = folder->getPath(); @@ -155,8 +149,7 @@ void SystemData::populateFolder(FolderData* folder, std::unordered_map Date: Fri, 12 Jul 2019 00:01:39 +0200 Subject: [PATCH 080/336] Gamelist : small bugfix on sorts --- es-app/src/SystemData.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 014ac0d0d7..80e236dd06 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -49,7 +49,7 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys refactorGameFolders(this); //StopWatch ws("sort " + mName); - // mRootFolder->sort(FileSorts::SortTypes.at(0)); + mRootFolder->sort(FileSorts::SortTypes.at(0)); //indexAllGameFilters(mRootFolder); } From 9f58add94823dada7dd1d37432a5e022a28f7da0 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 12 Jul 2019 11:28:08 +0200 Subject: [PATCH 081/336] ScreenScraper : take current language as default. SystemView : fix default carousel & systeminfo position due to refactoring Scraper : Enable windows proxy if existing Scraper : finished theming & locale. --- .../src/components/ScraperSearchComponent.cpp | 28 ++++--- es-app/src/guis/GuiGameScraper.cpp | 2 +- es-app/src/scrapers/ScreenScraper.cpp | 17 +++-- es-app/src/scrapers/ScreenScraper.h | 2 +- es-app/src/views/SystemView.cpp | 23 +++++- es-app/src/views/SystemView.h | 2 + es-core/src/HttpReq.cpp | 73 +++++++++++++++++-- es-core/src/components/BusyComponent.cpp | 2 +- es-core/src/guis/GuiMsgBox.cpp | 5 +- 9 files changed, 120 insertions(+), 34 deletions(-) diff --git a/es-app/src/components/ScraperSearchComponent.cpp b/es-app/src/components/ScraperSearchComponent.cpp index 92b3e184d6..9e27c596cf 100644 --- a/es-app/src/components/ScraperSearchComponent.cpp +++ b/es-app/src/components/ScraperSearchComponent.cpp @@ -19,6 +19,12 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) mGrid(window, Vector2i(4, 3)), mBusyAnim(window), mSearchType(type) { + auto theme = ThemeData::getMenuTheme(); + + auto font = theme->TextSmall.font; // Font::get(FONT_SIZE_SMALL); // this gets replaced in onSizeChanged() so its just a placeholder + const unsigned int mdColor = theme->Text.color; // 0x777777FF; + const unsigned int mdLblColor = theme->Text.color; // 0x666666FF; + addChild(&mGrid); mBlockAccept = false; @@ -27,7 +33,7 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) mGrid.setEntry(std::make_shared(mWindow), Vector2i(0, 0), false, false, Vector2i(1, 3), GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM); // selected result name - mResultName = std::make_shared(mWindow, "Result name", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); + mResultName = std::make_shared(mWindow, "Result name", ThemeData::getMenuTheme()->Text.font, mdColor); // selected result thumbnail mResultThumbnail = std::make_shared(mWindow); @@ -35,14 +41,12 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) // selected result desc + container mDescContainer = std::make_shared(mWindow); - mResultDesc = std::make_shared(mWindow, "Result desc", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); + mResultDesc = std::make_shared(mWindow, "Result desc", font, mdColor); 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(mWindow); mMD_ReleaseDate = std::make_shared(mWindow); mMD_ReleaseDate->setColor(mdColor); @@ -51,12 +55,12 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) mMD_Genre = std::make_shared(mWindow, "", font, mdColor); mMD_Players = std::make_shared(mWindow, "", font, mdColor); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, "RATING:", font, mdLblColor), mMD_Rating, false)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, "RELEASED:", font, mdLblColor), mMD_ReleaseDate)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, "DEVELOPER:", font, mdLblColor), mMD_Developer)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, "PUBLISHER:", font, mdLblColor), mMD_Publisher)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, "GENRE:", font, mdLblColor), mMD_Genre)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, "PLAYERS:", font, mdLblColor), mMD_Players)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _T("Rating") + ":", font, mdLblColor), mMD_Rating, false)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _T("Released") + ":", font, mdLblColor), mMD_ReleaseDate)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _T("Developer") + ":", font, mdLblColor), mMD_Developer)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _T("Publisher") + ":", font, mdLblColor), mMD_Publisher)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _T("Genre") + ":", font, mdLblColor), mMD_Genre)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _T("Players") + ":", font, mdLblColor), mMD_Players)); mMD_Grid = std::make_shared(mWindow, Vector2i(2, (int)mMD_Pairs.size()*2 - 1)); unsigned int i = 0; diff --git a/es-app/src/guis/GuiGameScraper.cpp b/es-app/src/guis/GuiGameScraper.cpp index c9c63d943a..3e39e14e86 100644 --- a/es-app/src/guis/GuiGameScraper.cpp +++ b/es-app/src/guis/GuiGameScraper.cpp @@ -43,7 +43,7 @@ GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std:: // buttons std::vector< std::shared_ptr > buttons; - buttons.push_back(std::make_shared(mWindow, "INPUT", "search", [&] { + buttons.push_back(std::make_shared(mWindow, _T("INPUT"), _T("search"), [&] { mSearch->openInputScreen(mSearchParams); mGrid.resetCursor(); })); diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index 31f32ea408..c7e62f9248 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -7,6 +7,7 @@ #include "PlatformId.h" #include "Settings.h" #include "SystemData.h" +#include "EsLocale.h" #include #include @@ -158,8 +159,10 @@ void ScreenScraperRequest::process(const std::unique_ptr& req, std::vec { assert(req->status() == HttpReq::REQ_SUCCESS); - pugi::xml_document doc; - pugi::xml_parse_result parseResult = doc.load(req->getContent().c_str()); + std::string content = req->getContent(); + + pugi::xml_document doc; + pugi::xml_parse_result parseResult = doc.load(content.c_str()); if (!parseResult) { @@ -187,10 +190,13 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve { ScraperSearchResult result; ScreenScraperRequest::ScreenScraperConfig ssConfig; + + std::string region = Utils::String::toLower(ssConfig.region); + std::string language = Utils::String::toLower(EsLocale::getLanguage()); + if (language != "EN") + region = "eu"; - std::string region = Utils::String::toLower(ssConfig.region).c_str(); - std::string language = Utils::String::toLower(ssConfig.language).c_str(); - + // ssConfig.language // Name fallback: US, WOR(LD). ( Xpath: Data/jeu[0]/noms/nom[*] ). result.mdl.set("name", find_child_by_attribute_list(game.child("noms"), "nom", "region", { region, "wor", "us" , "ss", "eu", "jp" }).text().get()); @@ -254,6 +260,7 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve // We need to do this because any child of 'medias' has the form // // and we need to find the right media for the region. + pugi::xpath_node_set results = media_list.select_nodes((static_cast("media[@type='") + ssConfig.media_name + "']").c_str()); if (results.size()) diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index 24063744e2..cf4134e09e 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -51,7 +51,7 @@ class ScreenScraperRequest : public ScraperHttpRequest // Which Language to use when selecting the textual information // Applies to: description, genre - std::string language = "EN"; + //std::string language = "EN"; ScreenScraperConfig() {}; } configuration; diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index ab18930aca..439732bdc2 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -26,6 +26,8 @@ SystemView::SystemView(Window* window) : IList(wind populate(); } + + void SystemView::populate() { mEntries.clear(); @@ -356,9 +358,9 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) void SystemView::render(const Transform4x4f& parentTrans) { - if(size() == 0) + if (size() == 0) return; // nothing to render - + if (mSize.x() != Renderer::getScreenWidth() || mSize.x() != Renderer::getScreenHeight()) { for (int i = 0; i < mEntries.size(); i++) @@ -646,7 +648,7 @@ void SystemView::getDefaultElements(void) mCarousel.size.x() = 1; // mSize.x(); mCarousel.size.y() = 0.2325f; // *mSize.y(); mCarousel.pos.x() = 0.0f; - mCarousel.pos.y() = 0.5f - mCarousel.size.y(); // *(mSize.y() - mCarousel.size.y()); + mCarousel.pos.y() = 0.5f * (1 - 0.2325f);//- mCarousel.size.y(); // *(mSize.y() - mCarousel.size.y()); mCarousel.origin.x() = 0.0f; mCarousel.origin.y() = 0.0f; mCarousel.color = 0xFFFFFFD8; @@ -664,7 +666,7 @@ void SystemView::getDefaultElements(void) // System Info Bar mSystemInfo.setSize(mSize.x(), mSystemInfo.getFont()->getLetterHeight()*2.2f); - mSystemInfo.setPosition(0, (mCarousel.pos.y() + mCarousel.size.y() - 0.2f)); + mSystemInfo.setPosition(0, (carouselPos().y() + carouselSize().y() - 0.2f)); mSystemInfo.setBackgroundColor(0xDDDDDDD8); mSystemInfo.setRenderBackground(true); mSystemInfo.setFont(Font::get((int)(0.035f * mSize.y()), Font::getDefaultPath())); @@ -673,6 +675,19 @@ void SystemView::getDefaultElements(void) mSystemInfo.setDefaultZIndex(50); } +void SystemView::onSizeChanged() +{ + mSystemInfo.setPosition(0, (carouselPos().y() + carouselSize().y() - 0.2f)); + + if (SystemData::sSystemVector.size() == 0) + return; + + const std::shared_ptr& theme = SystemData::sSystemVector.at(0)->getTheme(); + const ThemeData::ThemeElement* sysInfoElem = theme->getElement("system", "systemInfo", "text"); + if (sysInfoElem) + mSystemInfo.applyTheme(theme, "system", "systemInfo", ThemeFlags::POSITION); +} + Vector2f SystemView::carouselSize() { return mCarousel.size * mSize; diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index 80dd6d7ee3..675292fdc9 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -63,6 +63,7 @@ class SystemView : public IList std::vector getHelpPrompts() override; virtual HelpStyle getHelpStyle() override; + void onSizeChanged() override; protected: void onCursorChanged(const CursorState& state) override; @@ -73,6 +74,7 @@ class SystemView : public IList Vector2f carouselPos(); Vector2f carouselSize(); + void populate(); void getViewElements(const std::shared_ptr& theme); diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index ce307a43a6..9a4329b282 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -37,6 +37,48 @@ bool HttpReq::isUrl(const std::string& str) (str.find("http://") != std::string::npos || str.find("https://") != std::string::npos || str.find("www.") != std::string::npos)); } +#ifdef WIN32 +LONG _regGetDWORD(HKEY hKey, const std::string &strPath, const std::string &strValueName) +{ + HKEY hSubKey; + LONG nRet = ::RegOpenKeyEx(hKey, strPath.c_str(), 0L, KEY_QUERY_VALUE, &hSubKey); + if (nRet == ERROR_SUCCESS) + { + DWORD dwBufferSize(sizeof(DWORD)); + DWORD nResult(0); + + nRet = ::RegQueryValueExA(hSubKey, strValueName.c_str(), 0, NULL, reinterpret_cast(&nResult), &dwBufferSize); + ::RegCloseKey(hSubKey); + + if (nRet == ERROR_SUCCESS) + return nResult; + } + + return 0; +} + +std::string _regGetString(HKEY hKey, const std::string &strPath, const std::string &strValueName) +{ + std::string ret; + + HKEY hSubKey; + LONG nRet = ::RegOpenKeyEx(hKey, strPath.c_str(), 0L, KEY_QUERY_VALUE, &hSubKey); + if (nRet == ERROR_SUCCESS) + { + char szBuffer[1024]; + DWORD dwBufferSize = sizeof(szBuffer); + + nRet = ::RegQueryValueExA(hSubKey, strValueName.c_str(), 0, NULL, (LPBYTE)szBuffer, &dwBufferSize); + ::RegCloseKey(hSubKey); + + if (nRet == ERROR_SUCCESS) + ret = szBuffer; + } + + return ret; +} +#endif + HttpReq::HttpReq(const std::string& url) : mStatus(REQ_IN_PROGRESS), mHandle(NULL) { @@ -102,16 +144,35 @@ HttpReq::HttpReq(const std::string& url) onError(curl_easy_strerror(err)); return; } - /* + #ifdef WIN32 -#ifdef _DEBUG + if (_regGetDWORD(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", "ProxyEnable")) + { + auto proxyServer = _regGetString(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", "ProxyServer"); + if (!proxyServer.empty()) + { + std::string protocol = (url.find("https:/") == 0 ? "https=" : "http="); + + size_t pxs = proxyServer.find(protocol); + if (pxs != std::string::npos) + { + size_t pxe = proxyServer.find(";", pxs); + if (pxe == std::string::npos) + pxe = proxyServer.size() - 1; + proxyServer = proxyServer.substr(pxs + protocol.size(), pxe - pxs - protocol.size()); + } + + if (!proxyServer.empty()) + { CURLcode ret; - curl_easy_setopt(mHandle, CURLOPT_PROXY, "127.0.0.1:8081"); - curl_easy_setopt(mHandle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); -#endif + curl_easy_setopt(mHandle, CURLOPT_PROXY, proxyServer.c_str()); + curl_easy_setopt(mHandle, CURLOPT_PROXYTYPE, CURLPROXY_HTTP); + } + } + } #endif - */ + //add the handle to our multi CURLMcode merr = curl_multi_add_handle(s_multi_handle, mHandle); if(merr != CURLM_OK) diff --git a/es-core/src/components/BusyComponent.cpp b/es-core/src/components/BusyComponent.cpp index b26e66b137..37cf4f9f44 100644 --- a/es-core/src/components/BusyComponent.cpp +++ b/es-core/src/components/BusyComponent.cpp @@ -24,7 +24,7 @@ BusyComponent::BusyComponent(Window* window) : GuiComponent(window), mAnimation = std::make_shared(mWindow); mAnimation->load(&BUSY_ANIMATION_DEF); - mText = std::make_shared(mWindow, "WORKING...", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); + mText = std::make_shared(mWindow, _T("WORKING..."), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); // col 0 = animation, col 1 = spacer, col 2 = text mGrid.setEntry(mAnimation, Vector2i(1, 1), false, true); diff --git a/es-core/src/guis/GuiMsgBox.cpp b/es-core/src/guis/GuiMsgBox.cpp index 843a844a48..daf98e7131 100644 --- a/es-core/src/guis/GuiMsgBox.cpp +++ b/es-core/src/guis/GuiMsgBox.cpp @@ -37,7 +37,7 @@ GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, }else{ for(auto it = mButtons.cbegin(); it != mButtons.cend(); it++) { - if(Utils::String::toUpper((*it)->getText()) == "OK" || Utils::String::toUpper((*it)->getText()) == "NO") + if(Utils::String::toUpper((*it)->getText()) == _T("OK") || Utils::String::toUpper((*it)->getText()) == _T("NO")) { mAcceleratorFunc = (*it)->getPressedFunc(); break; @@ -80,10 +80,7 @@ bool GuiMsgBox::input(InputConfig* config, Input input) } if(mAcceleratorFunc && config->isMappedTo("b", input) && input.value != 0) - { mAcceleratorFunc(); - return true; - } return GuiComponent::input(config, input); } From cb5290390015ecf4a8ccca55d9c7d9b1f727eb46 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 12 Jul 2019 17:10:40 +0200 Subject: [PATCH 082/336] Scraping : Added options for different sources for image & thumbnail Scraping : Added options for scraping videos Metadata Editor : View all metadatas again. --- es-app/src/SystemData.cpp | 16 +- .../src/components/ScraperSearchComponent.cpp | 2 +- es-app/src/guis/GuiMenu.cpp | 45 ++++++ es-app/src/guis/GuiMetaDataEd.cpp | 20 ++- es-app/src/scrapers/Scraper.cpp | 66 ++++++-- es-app/src/scrapers/Scraper.h | 1 + es-app/src/scrapers/ScreenScraper.cpp | 141 +++++++++++++----- es-app/src/scrapers/ScreenScraper.h | 8 +- es-core/src/Settings.cpp | 5 + 9 files changed, 243 insertions(+), 61 deletions(-) diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 80e236dd06..9e4d71660b 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -603,16 +603,20 @@ SystemData* SystemData::getPrev() const std::string SystemData::getGamelistPath(bool forWrite) const { - std::string filePath; + std::string fileRomPath = mRootFolder->getPath() + "/gamelist.xml"; + if(Utils::FileSystem::exists(fileRomPath)) + return fileRomPath; - filePath = mRootFolder->getPath() + "/gamelist.xml"; - if(Utils::FileSystem::exists(filePath)) - return filePath; + std::string filePath = Utils::FileSystem::getHomePath() + "/.emulationstation/gamelists/" + mName + "/gamelist.xml"; + + // Default to system rom folder + if (forWrite && !Utils::FileSystem::exists(filePath) && Utils::FileSystem::isDirectory(mRootFolder->getPath())) + return fileRomPath; - filePath = Utils::FileSystem::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 Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(filePath)); - if(forWrite || Utils::FileSystem::exists(filePath)) + + if (forWrite || Utils::FileSystem::exists(filePath)) return filePath; return "/etc/emulationstation/gamelists/" + mName + "/gamelist.xml"; diff --git a/es-app/src/components/ScraperSearchComponent.cpp b/es-app/src/components/ScraperSearchComponent.cpp index 9e27c596cf..235cd74ac6 100644 --- a/es-app/src/components/ScraperSearchComponent.cpp +++ b/es-app/src/components/ScraperSearchComponent.cpp @@ -377,7 +377,7 @@ void ScraperSearchComponent::returnResult(ScraperSearchResult result) mBlockAccept = true; // resolve metadata image before returning - if(!result.imageUrl.empty()) + if(!result.imageUrl.empty() || !result.thumbnailUrl.empty()) { mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch); return; diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 0f9e108e9e..7c6e1b2a94 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -105,6 +105,51 @@ void GuiMenu::openScraperSettings() s->addWithLabel(_T("SCRAPE RATINGS"), scrape_ratings); s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); }); + + + + + + + std::string imageSourceName = Settings::getInstance()->getString("ScrapperImageSrc"); + auto imageSource = std::make_shared< OptionListComponent >(mWindow, _T("PREFERED IMAGE SOURCE"), false); + imageSource->add(_T("NONE"), "", imageSourceName.empty()); + imageSource->add(_T("SCREENSHOT"), "ss", imageSourceName == "ss"); + imageSource->add(_T("BOX 2D"), "box-2D", imageSourceName == "box-2D"); + imageSource->add(_T("BOX 3D"), "box-3D", imageSourceName == "box-3D"); + imageSource->add(_T("MIX"), "mixrbv1", imageSourceName == "mixrbv1"); + imageSource->add(_T("WHEEL"), "wheel", imageSourceName == "wheel"); + s->addWithLabel("PREFERED IMAGE SOURCE", imageSource); + + s->addSaveFunc([imageSource] { + if (Settings::getInstance()->getString("ScrapperImageSrc") != imageSource->getSelected()) + Settings::getInstance()->setString("ScrapperImageSrc", imageSource->getSelected()); + }); + + + std::string thumbSourceName = Settings::getInstance()->getString("ScrapperThumbSrc"); + auto thumbSource = std::make_shared< OptionListComponent >(mWindow, _T("PREFERED THUMBNAIL SOURCE"), false); + thumbSource->add(_T("NONE"), "", thumbSourceName.empty()); + thumbSource->add(_T("SCREENSHOT"), "ss", thumbSourceName == "ss"); + thumbSource->add(_T("BOX 2D"), "box-2D", thumbSourceName == "box-2D"); + thumbSource->add(_T("BOX 3D"), "box-3D", thumbSourceName == "box-3D"); + thumbSource->add(_T("MIX"), "mixrbv1", thumbSourceName == "mixrbv1"); + thumbSource->add(_T("WHEEL"), "wheel", thumbSourceName == "wheel"); + s->addWithLabel("PREFERED THUMBNAIL SOURCE", thumbSource); + + s->addSaveFunc([thumbSource] { + if (Settings::getInstance()->getString("ScrapperThumbSrc") != thumbSource->getSelected()) + Settings::getInstance()->setString("ScrapperThumbSrc", thumbSource->getSelected()); + }); + + + // scrape video + auto scrape_video = std::make_shared(mWindow); + scrape_video->setState(Settings::getInstance()->getBool("ScrapeVideos")); + s->addWithLabel(_T("SCRAPE VIDEOS"), scrape_video); + s->addSaveFunc([scrape_video] { Settings::getInstance()->setBool("ScrapeVideos", scrape_video->getState()); }); + + // scrape now ComponentListRow row; auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 4d9e90e102..37b9cbde00 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -53,6 +53,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector mList = std::make_shared(mWindow); mGrid.setEntry(mList, Vector2i(0, 1), true, true); + SystemData* system = file->getSystem(); auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); @@ -66,14 +67,14 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector // don't add statistics if(iter->isStatistic) continue; - + /* #if defined(_WIN32) if (iter->displayName == "sortname" || iter->displayName == "image" || iter->displayName == "video" || iter->displayName == "marquee" || iter->displayName == "thumbnail" || iter->displayName == "kidgame" || iter->displayName == "description" || iter->displayName == "release date" || iter->displayName == "genre" || iter->displayName == "publisher" || iter->displayName == "developer" || iter->displayName == "players") continue; #endif - +*/ // create ed and add it (and any related components) to mMenu // ed's value will be set below ComponentListRow row; @@ -252,6 +253,21 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector mButtons = makeButtonGrid(mWindow, buttons); mGrid.setEntry(mButtons, Vector2i(0, 2), true, false); + mGrid.setUnhandledInputCallback([this](InputConfig* config, Input input) -> bool { + if (config->isMappedLike("down", input)) { + mGrid.setCursorTo(mList); + mList->setCursorIndex(0); + return true; + } + if (config->isMappedLike("up", input)) { + mList->setCursorIndex(mList->size() - 1); + mGrid.moveCursor(Vector2i(0, 1)); + return true; + } + return false; + }); + + // resize + center float width = (float)Math::min(Renderer::getScreenHeight(), (int)(Renderer::getScreenWidth() * 0.90f)); setSize(width, Renderer::getScreenHeight() * 0.82f); diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index 76d3fc9634..d943886b07 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -143,7 +143,6 @@ MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const Scrape { if(!result.imageUrl.empty()) { - std::string ext; // If we have a file extension returned by the scraper, then use it. @@ -158,13 +157,38 @@ MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const Scrape ext = result.imageUrl.substr(dot, std::string::npos); } - std::string imgPath = getSaveAsPath(search, "image", ext); + if (!result.imageUrl.empty()) + { + std::string imgPath = getSaveAsPath(search, "image", ext); + + mFuncs.push_back(ResolvePair(downloadImageAsync(result.imageUrl, imgPath), [this, imgPath] + { + mResult.mdl.set("image", imgPath); + mResult.imageUrl = ""; + })); + } - mFuncs.push_back(ResolvePair(downloadImageAsync(result.imageUrl, imgPath), [this, imgPath] + if (!result.thumbnailUrl.empty() && result.thumbnailUrl.find(result.imageUrl) != 0) { - mResult.mdl.set("image", imgPath); - mResult.imageUrl = ""; - })); + std::string thumbPath = getSaveAsPath(search, "thumb", ext); + + mFuncs.push_back(ResolvePair(downloadImageAsync(result.thumbnailUrl, thumbPath), [this, thumbPath] + { + mResult.mdl.set("thumbnail", thumbPath); + mResult.thumbnailUrl = ""; + })); + } + + if (!result.videoUrl.empty()) + { + std::string videoPath = getSaveAsPath(search, "video", ".mp4"); + + mFuncs.push_back(ResolvePair(downloadImageAsync(result.videoUrl, videoPath), [this, videoPath] + { + mResult.mdl.set("video", videoPath); + mResult.videoUrl = ""; + })); + } } } @@ -233,12 +257,13 @@ void ImageDownloadHandle::update() setError("Failed to save image. Disk full?"); return; } - - // resize it - if(!resizeImage(mSavePath, mMaxWidth, mMaxHeight)) + + // resize downloaded image + if (Utils::FileSystem::getExtension(mSavePath) != ".mp4") { - setError("Error saving resized image. Out of memory? Disk full?"); - return; + resizeImage(mSavePath, mMaxWidth, mMaxHeight); + //setError("Error saving resized image. Out of memory? Disk full?"); + //return; } setStatus(ASYNC_DONE); @@ -304,10 +329,24 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight) std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix, const std::string& extension) { - const std::string subdirectory = params.system->getName(); + std::string subFolder = "downloaded_images"; + if (suffix == "video") + subFolder = "downloaded_videos"; + const std::string name = Utils::FileSystem::getStem(params.game->getPath()) + "-" + suffix; - std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/downloaded_images/"; + const std::string basePath = Utils::FileSystem::getParent(params.system->getGamelistPath(false)); + if (basePath.find("/.emulationstation/") == std::string::npos) + { + const std::string pth = Utils::FileSystem::getGenericPath(params.system->getRootFolder()->getPath() + "/"+ subFolder +"/"); + if (!Utils::FileSystem::exists(pth)) + Utils::FileSystem::createDirectory(pth); + + return pth + "/" + name + extension; + } + + const std::string subdirectory = params.system->getName(); + std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/"+ subFolder +"/"; if(!Utils::FileSystem::exists(path)) Utils::FileSystem::createDirectory(path); @@ -317,7 +356,6 @@ std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& if(!Utils::FileSystem::exists(path)) Utils::FileSystem::createDirectory(path); - path += name + extension; return path; } diff --git a/es-app/src/scrapers/Scraper.h b/es-app/src/scrapers/Scraper.h index c6dd6a2613..7d59cab5b3 100644 --- a/es-app/src/scrapers/Scraper.h +++ b/es-app/src/scrapers/Scraper.h @@ -31,6 +31,7 @@ struct ScraperSearchResult MetaDataList mdl; std::string imageUrl; std::string thumbnailUrl; + std::string videoUrl; // Needed to pre-set the image type std::string imageType; diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index c7e62f9248..0178431b12 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -180,6 +180,68 @@ void ScreenScraperRequest::process(const std::unique_ptr& req, std::vec } +pugi::xml_node ScreenScraperRequest::findMedia(pugi::xml_node media_list, std::vector mediaNames, std::string region) +{ + for (std::string media : mediaNames) + { + pugi::xml_node art = findMedia(media_list, media, region); + if (art) + return art; + } + + return pugi::xml_node(NULL); +} + +pugi::xml_node ScreenScraperRequest::findMedia(pugi::xml_node media_list, std::string mediaName, std::string region) +{ + pugi::xml_node art = pugi::xml_node(NULL); + + // Do an XPath query for media[type='$media_type'], then filter by region + // We need to do this because any child of 'medias' has the form + // + // and we need to find the right media for the region. + + pugi::xpath_node_set results = media_list.select_nodes((static_cast("media[@type='") + mediaName + "']").c_str()); + + if (!results.size()) + return art; + + // Region fallback: WOR(LD), US, CUS(TOM?), JP, EU + for (auto _region : std::vector{ region, "wor", "us", "cus", "jp", "eu", "" }) + { + if (art) + break; + + for (auto node : results) + { + if (node.node().attribute("region").value() == _region) + { + art = node.node(); + break; + } + } + } + + return art; +} + +std::vector ScreenScraperRequest::getRipList(std::string imageSource) +{ + std::vector ripList; + + if (imageSource == "ss") + ripList = { "ss", "mixrbv1", "mixrbv2", "box-2D", "box-3D" }; + else if (imageSource == "mixrbv1" || imageSource == "mixrbv2" || imageSource == "mixrbv") + ripList = { "mixrbv1", "mixrbv2", "ss", "box-3D", "box-2D" }; + else if (imageSource == "box-2D") + ripList = { "box-2D", "box-3D" }; + else if (imageSource == "box-3D") + ripList = { "box-3D", "box-2D" }; + else if (imageSource == "wheel") + ripList = { "wheel", "screenmarqueesmall", "screenmarquee" }; + + return ripList; +} void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::vector& out_results) { @@ -254,51 +316,60 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve if (media_list) { - pugi::xml_node art = pugi::xml_node(NULL); - - // Do an XPath query for media[type='$media_type'], then filter by region - // We need to do this because any child of 'medias' has the form - // - // and we need to find the right media for the region. + std::vector ripList = getRipList(Settings::getInstance()->getString("ScrapperImageSrc")); + if (!ripList.empty()) + { + pugi::xml_node art = findMedia(media_list, ripList, region); + if (art) + { + // Sending a 'softname' containing space will make the image URLs returned by the API also contain the space. + // Escape any spaces in the URL here + result.imageUrl = Utils::String::replace(art.text().get(), " ", "%20"); - pugi::xpath_node_set results = media_list.select_nodes((static_cast("media[@type='") + ssConfig.media_name + "']").c_str()); + // Get the media type returned by ScreenScraper + std::string media_type = art.attribute("format").value(); + if (!media_type.empty()) + result.imageType = "." + media_type; - if (results.size()) + // Ask for the same image, but with a smaller size, for the thumbnail displayed during scraping + result.thumbnailUrl = result.imageUrl + "&maxheight=250"; + } + else + LOG(LogDebug) << "Failed to find media XML node for image"; + } + + if (!Settings::getInstance()->getString("ScrapperThumbSrc").empty() && + Settings::getInstance()->getString("ScrapperThumbSrc") != Settings::getInstance()->getString("ScrapperImageSrc")) { - // Region fallback: WOR(LD), US, CUS(TOM?), JP, EU - for (auto _region : std::vector{ region, "wor", "us", "cus", "jp", "eu" }) + ripList = getRipList(Settings::getInstance()->getString("ScrapperThumbSrc")); + if (!ripList.empty()) { + pugi::xml_node art = findMedia(media_list, ripList, region); if (art) - break; - - for (auto node : results) { - if (node.node().attribute("region").value() == _region) - { - art = node.node(); - break; - } + // Sending a 'softname' containing space will make the image URLs returned by the API also contain the space. + // Escape any spaces in the URL here + // Ask for the same image, but with a smaller size, for the thumbnail displayed during scraping + result.thumbnailUrl = Utils::String::replace(art.text().get(), " ", "%20"); } + else + LOG(LogDebug) << "Failed to find media XML node for thumbnail"; } - } // results + } - if (art) + if (Settings::getInstance()->getBool("ScrapeVideos")) { - // Sending a 'softname' containing space will make the image URLs returned by the API also contain the space. - // Escape any spaces in the URL here - result.imageUrl = Utils::String::replace(art.text().get(), " ", "%20"); - - // Get the media type returned by ScreenScraper - std::string media_type = art.attribute("format").value(); - if (!media_type.empty()) - result.imageType = "." + media_type; - - // Ask for the same image, but with a smaller size, for the thumbnail displayed during scraping - result.thumbnailUrl = result.imageUrl + "&maxheight=250"; - }else{ - LOG(LogDebug) << "Failed to find media XML node with name=" << ssConfig.media_name; + pugi::xml_node art = findMedia(media_list, "video", region); + if (art) + { + // Sending a 'softname' containing space will make the image URLs returned by the API also contain the space. + // Escape any spaces in the URL here + // Ask for the same image, but with a smaller size, for the thumbnail displayed during scraping + result.videoUrl = Utils::String::replace(art.text().get(), " ", "%20"); + } + else + LOG(LogDebug) << "Failed to find media XML node for video"; } - } out_results.push_back(result); @@ -335,8 +406,6 @@ void ScreenScraperRequest::processList(const pugi::xml_document& xmldoc, std::ve game = game.next_sibling("jeu"); } - - } std::string ScreenScraperRequest::ScreenScraperConfig::getGameSearchUrl(const std::string gameName) const diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index cf4134e09e..05ec0ecf95 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -43,7 +43,8 @@ class ScreenScraperRequest : public ScraperHttpRequest Note that no all games contain values for these, so we default to "box-2D" since it's the most common. **/ - std::string media_name = "box-2D"; + //std::string media_image = "ss"; + //std::string media_thumnail = "box-2D"; // Which Region to use when selecting the artwork // Applies to: artwork, name of the game, date of release @@ -65,7 +66,10 @@ class ScreenScraperRequest : public ScraperHttpRequest std::queue< std::unique_ptr >* mRequestQueue; - +private: + std::vector getRipList(std::string imageSource); + pugi::xml_node findMedia(pugi::xml_node media_list, std::vector mediaNames, std::string region); + pugi::xml_node findMedia(pugi::xml_node media_list, std::string mediaName, std::string region); }; diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 4d02e63594..79579f167c 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -92,6 +92,7 @@ void Settings::setDefaults() mBoolMap["OptimizeVRAM"] = true; mBoolMap["ThreadedLoading"] = true; + mBoolMap["Debug"] = false; mBoolMap["DebugGrid"] = false; mBoolMap["DebugText"] = false; @@ -200,6 +201,10 @@ void Settings::setDefaults() mStringMap["ExePath"] = ""; + mStringMap["ScrapperImageSrc"] = "box-2D"; + mStringMap["ScrapperThumbSrc"] = ""; + mBoolMap["ScrapeVideos"] = false; + mDefaultBoolMap = mBoolMap; mDefaultIntMap = mIntMap; mDefaultFloatMap = mFloatMap; From 16f4f2fd3b8a21c3208c488baad42446360641ac Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 15 Jul 2019 09:19:00 +0200 Subject: [PATCH 083/336] Fix & adaptions on RetroPie#571 --- es-app/src/FileData.cpp | 7 ++++--- es-app/src/FileData.h | 4 ++-- es-app/src/guis/GuiCollectionSystemsOptions.cpp | 6 +++--- resources/locale/fr/emulationstation2.po | 4 ++++ 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 767015e7f1..65ef13b388 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -113,7 +113,7 @@ const bool FileData::getKidGame() return metadata.get("kidgame") != "false"; } -const std::string& FileData::getName() +const std::string FileData::getName() { return metadata.getName(); } @@ -322,7 +322,7 @@ void CollectionFileData::refreshMetadata() mDirty = true; } -const std::string& CollectionFileData::getName() +const std::string CollectionFileData::getName() { if (mDirty) { mCollectionFileName = Utils::String::removeParenthesis(mSourceFileData->metadata.get("name")); @@ -332,7 +332,8 @@ const std::string& CollectionFileData::getName() if (Settings::getInstance()->getBool("CollectionShowSystemInfo")) return mCollectionFileName; - return mSourceFileData->metadata.get("name"); + + return Utils::String::removeParenthesis(mSourceFileData->metadata.get("name")); } // returns Sort Type based on a string description diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 45e9f10ee0..8d36a459d7 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -34,7 +34,7 @@ class FileData FileData(FileType type, const std::string& path, SystemData* system); virtual ~FileData(); - virtual const std::string& getName(); + virtual const std::string getName(); inline FileType getType() const { return mType; } @@ -92,7 +92,7 @@ class CollectionFileData : public FileData public: CollectionFileData(FileData* file, SystemData* system); ~CollectionFileData(); - const std::string& getName(); + const std::string getName(); void refreshMetadata(); FileData* getSourceFileData(); std::string getKey(); diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index 61a8bbb3c3..5e7e6c0b50 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -83,7 +83,7 @@ void GuiCollectionSystemsOptions::initializeMenu() toggleSystemNameInCollections = std::make_shared(mWindow); toggleSystemNameInCollections->setState(Settings::getInstance()->getBool("CollectionShowSystemInfo")); - mMenu.addWithLabel("SHOW SYSTEM NAME IN COLLECTIONS", toggleSystemNameInCollections); + mMenu.addWithLabel(_T("SHOW SYSTEM NAME IN COLLECTIONS"), toggleSystemNameInCollections); if(CollectionSystemManager::get()->isEditing()) { @@ -183,14 +183,14 @@ void GuiCollectionSystemsOptions::applySettings() bool prevSort = Settings::getInstance()->getBool("SortAllSystems"); bool outBundle = bundleCustomCollections->getState(); bool prevBundle = Settings::getInstance()->getBool("UseCustomCollectionsSystem"); + bool prevShow = Settings::getInstance()->getBool("CollectionShowSystemInfo"); bool outShow = toggleSystemNameInCollections->getState(); - bool needUpdateSettings = prevAuto != outAuto || prevCustom != outCustom || outSort != prevSort || outBundle != prevBundle || prevShow != outShow ; bool outFavoritesFirst = favoritesFirstSwitch->getState(); bool prevFavoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); - bool needUpdateSettings = prevAuto != outAuto || prevCustom != outCustom || outSort != prevSort || outBundle != prevBundle || outFavoritesFirst != prevFavoritesFirst; + bool needUpdateSettings = prevAuto != outAuto || prevCustom != outCustom || outSort != prevSort || outBundle != prevBundle || outFavoritesFirst != prevFavoritesFirst || prevShow != outShow; if (needUpdateSettings) { updateSettings(outAuto, outCustom); diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index 6ccf3f3ccc..d984d698c1 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -40,6 +40,10 @@ msgstr "SYNCHRONISATION VERTICALE" msgid "DISPLAY FAVORITES FIRST IN GAMELIST" msgstr "AFFICHER LES FAVORIS EN PREMIER" +#: +msgid "SHOW SYSTEM NAME IN COLLECTIONS" +msgstr "AFFICHER LE NOM DU SYSTÈME DANS LES COLLECTIONS" + #: msgid "OPTIMIZE IMAGES VRAM USE" msgstr "OPTIMISER LE BESOIN MÉMOIRE DES IMAGES" From 65e112f980d574530c356547d85948c921f09e5e Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 15 Jul 2019 17:26:15 +0200 Subject: [PATCH 084/336] Resources loader : don't perform async load for resources --- es-core/src/components/ImageGridComponent.h | 56 +++++++-------------- es-core/src/resources/TextureResource.cpp | 4 ++ 2 files changed, 21 insertions(+), 39 deletions(-) diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 4eb0fb0eb9..b56d3186b0 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -82,7 +82,7 @@ class ImageGridComponent : public IList private: // TILES void buildTiles(); - void updateTiles(bool ascending = true, bool allowAnimation = true, bool updateSelectedState = true); + void updateTiles(bool allowAnimation = true, bool updateSelectedState = true); void updateTileAtPos(int tilePos, int imgPos, bool allowAnimation = true, bool updateSelectedState = true); void calcGridDimension(); @@ -505,8 +505,6 @@ void ImageGridComponent::onSizeChanged() template void ImageGridComponent::onCursorChanged(const CursorState& state) { - - if (mLastCursor == mCursor) { if (state == CURSOR_STOPPED && mCursorChangedCallback) @@ -553,7 +551,7 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) */ startPos = 0; ((GuiComponent*)this)->cancelAnimation(2); - updateTiles(direction, false, !GuiComponent::ALLOWANIMATIONS); + updateTiles(false, !GuiComponent::ALLOWANIMATIONS); } if (GuiComponent::ALLOWANIMATIONS) @@ -627,7 +625,7 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) if (lastCursor < 0 || !GuiComponent::ALLOWANIMATIONS) { - updateTiles(direction, lastCursor >= 0 && GuiComponent::ALLOWANIMATIONS); + updateTiles(lastCursor >= 0 && GuiComponent::ALLOWANIMATIONS); if (mCursorChangedCallback) mCursorChangedCallback(state); @@ -654,13 +652,13 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) ((GuiComponent*)this)->setAnimation(new LambdaAnimation(func, 250), 0, [this, direction] { mCamera = 0; - updateTiles(direction, false); + updateTiles(false); }, false, 2); } template -void ImageGridComponent::updateTiles(bool ascending, bool allowAnimation, bool updateSelectedState) +void ImageGridComponent::updateTiles(bool allowAnimation, bool updateSelectedState) { if (!mTiles.size()) return; @@ -671,7 +669,7 @@ void ImageGridComponent::updateTiles(bool ascending, bool allowAnimation, boo for (int ti = 0; ti < (int)mTiles.size(); ti++) { std::shared_ptr tile = mTiles.at(ti); - + tile->setSelected(false, allowAnimation); tile->setLabel(""); tile->setImage(mDefaultGameTexture); @@ -688,48 +686,28 @@ void ImageGridComponent::updateTiles(bool ascending, bool allowAnimation, boo previousTextures.push_back(tile->getTexture()); } - if (!ascending) - { - int i = (int)mTiles.size() - 1; - int end = -1; - int img = mStartPosition + (int)mTiles.size() - 1; - - if (isVertical()) - img -= EXTRAITEMS * mGridDimension.x(); - else - img -= EXTRAITEMS * mGridDimension.y(); + int i = 0; + int end = (int)mTiles.size(); + int img = mStartPosition; - while (i != end) - { - updateTileAtPos(i, img, allowAnimation, updateSelectedState); - i--; img--; - } - } + if (isVertical()) + img -= EXTRAITEMS * mGridDimension.x(); else - { - int i = 0; - int end = (int)mTiles.size(); - int img = mStartPosition; + img -= EXTRAITEMS * mGridDimension.y(); - if (isVertical()) - img -= EXTRAITEMS * mGridDimension.x(); - else - img -= EXTRAITEMS * mGridDimension.y(); - - while (i != end) - { - updateTileAtPos(i, img, allowAnimation, updateSelectedState); - i++; img++; - } + while (i != end) + { + updateTileAtPos(i, img, allowAnimation, updateSelectedState); + i++; img++; } + if (updateSelectedState) mLastCursor = mCursor; mEntriesDirty = false; } - template void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowAnimation, bool updateSelectedState) { diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index eac70d58ff..dbaa2361a5 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -168,6 +168,10 @@ std::shared_ptr TextureResource::get(const std::string& path, b return tex; } + // internal resources should not be dynamic + if (canonicalPath.length() > 0 && canonicalPath[0] == ':') + dynamic = false; + TextureKeyType key(canonicalPath, tile); auto foundTexture = sTextureMap.find(key); if(foundTexture != sTextureMap.cend()) From 07b5a9010737057a33120e6939f116a07bd8c1bd Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 17 Jul 2019 16:37:16 +0200 Subject: [PATCH 085/336] Game launch : resolved image sizing problems for landscape images. GridView : use scrollLoop + centerSelection theme definitions. Scraper : small bugfix --- es-app/src/scrapers/Scraper.cpp | 6 +- es-app/src/views/SystemView.cpp | 50 ++++++-- es-app/src/views/ViewController.cpp | 53 ++++++--- es-core/src/Settings.cpp | 7 ++ es-core/src/ThemeData.cpp | 4 +- es-core/src/Window.cpp | 10 +- es-core/src/components/ImageGridComponent.h | 124 +++++++++++++------- 7 files changed, 179 insertions(+), 75 deletions(-) diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index d943886b07..c781781c44 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -164,7 +164,11 @@ MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const Scrape mFuncs.push_back(ResolvePair(downloadImageAsync(result.imageUrl, imgPath), [this, imgPath] { mResult.mdl.set("image", imgPath); - mResult.imageUrl = ""; + + if (mResult.thumbnailUrl.find(mResult.imageUrl) == 0) + mResult.thumbnailUrl = ""; + + mResult.imageUrl = ""; })); } diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 439732bdc2..69a53b4a4a 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -390,7 +390,9 @@ void SystemView::render(const Transform4x4f& parentTrans) auto systemInfoZIndex = mSystemInfo.getZIndex(); auto minMax = std::minmax(mCarousel.zIndex, systemInfoZIndex); - renderExtras(trans, INT16_MIN, minMax.first); + if (!Settings::getInstance()->getBool("FixedCarousel")) + renderExtras(trans, INT16_MIN, minMax.first); + renderFade(trans); if (mCarousel.zIndex > mSystemInfo.getZIndex()) { @@ -407,7 +409,8 @@ void SystemView::render(const Transform4x4f& parentTrans) renderInfoBar(trans); } - renderExtras(trans, minMax.second, INT16_MAX); + if (!Settings::getInstance()->getBool("FixedCarousel")) + renderExtras(trans, minMax.second, INT16_MAX); } std::vector SystemView::getHelpPrompts() @@ -467,7 +470,12 @@ void SystemView::renderCarousel(const Transform4x4f& trans) { // background box behind logos Transform4x4f carouselTrans = trans; - carouselTrans.translate(Vector3f(carouselPos().x(), carouselPos().y(), 0.0)); + + if (Settings::getInstance()->getBool("FixedCarousel")) + carouselTrans.translate(Vector3f(mPosition.x() + carouselPos().x(), mPosition.y() + carouselPos().y(), 0.0)); + else + carouselTrans.translate(Vector3f(carouselPos().x(), carouselPos().y(), 0.0)); + carouselTrans.translate(Vector3f(mCarousel.origin.x() * carouselSize().x() * -1, mCarousel.origin.y() * carouselSize().y() * -1, 0.0f)); Vector2f clipPos(carouselTrans.translation().x(), carouselTrans.translation().y()); @@ -595,7 +603,10 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp // Adding texture loading buffers depending on scrolling speed and status int bufferIndex = getScrollingVelocity() + 1; - Renderer::pushClipRect(Vector2i::Zero(), Vector2i((int)mSize.x(), (int)mSize.y())); + if (Settings::getInstance()->getBool("FixedCarousel")) + Renderer::pushClipRect(Vector2i((int)mPosition.x(), (int)mPosition.y()), Vector2i((int)mSize.x(), (int)mSize.y())); + else + Renderer::pushClipRect(Vector2i(0, 0), Vector2i((int)mSize.x(), (int)mSize.y())); for (int i = extrasCenter + logoBuffersLeft[bufferIndex]; i <= extrasCenter + logoBuffersRight[bufferIndex]; i++) { @@ -609,10 +620,24 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp if (mShowing || index == mCursor) { Transform4x4f extrasTrans = trans; - if (mCarousel.type == HORIZONTAL || mCarousel.type == HORIZONTAL_WHEEL) - extrasTrans.translate(Vector3f((i - mExtrasCamOffset) * mSize.x(), 0, 0)); + + if (Settings::getInstance()->getBool("FixedCarousel")) + { + if (mCarousel.type == HORIZONTAL || mCarousel.type == HORIZONTAL_WHEEL) + extrasTrans.translate(Vector3f(mPosition.x() + (i - mExtrasCamOffset) * mSize.x(), mPosition.y(), 0)); + else + extrasTrans.translate(Vector3f(mPosition.x(), mPosition.y() + (i - mExtrasCamOffset) * mSize.y(), 0)); + + if (mSize.x() != (float)Renderer::getScreenWidth() || mSize.y() != (float)Renderer::getScreenHeight()) + extrasTrans.scale(Vector3f(mSize.x() / (float)Renderer::getScreenWidth(), mSize.y() / (float)Renderer::getScreenHeight(), 0.0)); + } else - extrasTrans.translate(Vector3f(0, (i - mExtrasCamOffset) * mSize.y(), 0)); + { + if (mCarousel.type == HORIZONTAL || mCarousel.type == HORIZONTAL_WHEEL) + extrasTrans.translate(Vector3f((i - mExtrasCamOffset) * mSize.x(), 0, 0)); + else + extrasTrans.translate(Vector3f(0,(i - mExtrasCamOffset) * mSize.y(), 0)); + } Renderer::pushClipRect(Vector2i((int)extrasTrans.translation()[0], (int)extrasTrans.translation()[1]), Vector2i((int)mSize.x(), (int)mSize.y())); @@ -635,7 +660,7 @@ void SystemView::renderFade(const Transform4x4f& trans) if (mExtrasFadeOpacity) { Renderer::setMatrix(trans); - Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000000 | (unsigned char)(mExtrasFadeOpacity * 255)); + Renderer::drawRect(mPosition.x(), mPosition.y(), mSize.x(), mSize.y(), 0x00000000 | (unsigned char)(mExtrasFadeOpacity * 255)); } } @@ -690,6 +715,9 @@ void SystemView::onSizeChanged() Vector2f SystemView::carouselSize() { + if (Settings::getInstance()->getBool("FixedCarousel")) + return mSize; + return mCarousel.size * mSize; } @@ -700,11 +728,17 @@ Vector2f SystemView::carouselLogoPos() Vector2f SystemView::carouselPos() { + if (Settings::getInstance()->getBool("FixedCarousel")) + return Vector2f(0, 0); + return mCarousel.pos * mSize; } Vector2f SystemView::carouselLogoSize() { + if (Settings::getInstance()->getBool("FixedCarousel")) + return mCarousel.logoSize / mCarousel.size * mSize; + return mCarousel.logoSize * mSize; } diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 04219d43d0..23cc3cd903 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -113,7 +113,13 @@ void ViewController::goToNextGameList() assert(mState.viewing == GAME_LIST); SystemData* system = getState().getSystem(); assert(system); - goToGameList(system->getNext()); + + system = system->getNext(); + goToGameList(system); + + // FixedCarousel + if (Settings::getInstance()->getBool("FixedCarousel")) + getSystemListView()->goToSystem(system, true); } void ViewController::goToPrevGameList() @@ -121,7 +127,13 @@ void ViewController::goToPrevGameList() assert(mState.viewing == GAME_LIST); SystemData* system = getState().getSystem(); assert(system); - goToGameList(system->getPrev()); + + system = system->getPrev(); + goToGameList(system); + + // FixedCarousel + if (Settings::getInstance()->getBool("FixedCarousel")) + getSystemListView()->goToSystem(system, true); } void ViewController::goToGameList(SystemData* system, bool forceImmediate) @@ -447,6 +459,7 @@ bool ViewController::input(InputConfig* config, Input input) if (mLockInput) return true; + if (config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_F5) { mWindow->render(); @@ -480,6 +493,10 @@ void ViewController::update(int deltaTime) { if(mCurrentView) { + // FixedCarousel + if (Settings::getInstance()->getBool("FixedCarousel")) + getSystemListView()->update(deltaTime); + mCurrentView->update(deltaTime); } @@ -504,20 +521,8 @@ void ViewController::render(const Transform4x4f& parentTrans) Vector3f sysEnd = getSystemListView()->getPosition() + Vector3f(getSystemListView()->getSize().x(), getSystemListView()->getSize().y(), 0); // draw systemview - if (!Settings::getInstance()->getBool("HideSystemView")) - { - /* - //getSystemListView()->setSize(Vector2f(getSystemListView()->getSize().x(), getSystemListView()->getSize().y())); - getSystemListView()->setSize(Vector2f(getSystemListView()->getSize().x(), 400)); - - Transform4x4f ts = Transform4x4f::Identity(); - Transform4x4f transInverse; - transInverse.invert(getSystemListView()->getTransform()); - - getSystemListView()->render(transInverse); - */ + if (!Settings::getInstance()->getBool("HideSystemView") && !Settings::getInstance()->getBool("FixedCarousel")) getSystemListView()->render(trans); - } // draw gamelists for(auto it = mGameListViews.cbegin(); it != mGameListViews.cend(); it++) @@ -530,6 +535,24 @@ void ViewController::render(const Transform4x4f& parentTrans) it->second->render(trans); } + + // FixedCarousel + if (Settings::getInstance()->getBool("FixedCarousel")) + { + getSystemListView()->setPosition(Vector3f(0, 0, 0)); + getSystemListView()->setSize(Vector2f(Renderer::getScreenWidth(), Renderer::getScreenHeight() * 0.1)); + + Transform4x4f ts = Transform4x4f::Identity(); + Transform4x4f transInverse; + transInverse.invert(getSystemListView()->getTransform()); + + getSystemListView()->render(transInverse); + } + + + + + if(mWindow->peekGui() == this) mWindow->renderHelpPromptsEarly(); diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 6a14585202..0ef2fc97ab 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -39,6 +39,7 @@ std::vector settings_dont_save { { "ScreenRotate" }, { "MonitorID" }, { "ExePath" }, + { "FixedCarousel" }, { "HomePath" } }; @@ -61,6 +62,12 @@ void Settings::setDefaults() mBoolMap.clear(); mIntMap.clear(); +#if _DEBUG + mBoolMap["FixedCarousel"] = false; +#else + mBoolMap["FixedCarousel"] = false; +#endif + mBoolMap["BackgroundJoystickInput"] = false; mBoolMap["ParseGamelistOnly"] = false; mBoolMap["ShowHiddenFiles"] = false; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 315887ebb4..7925858ef2 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -50,7 +50,9 @@ std::map> The { "gameImage", PATH }, { "folderImage", PATH }, { "showVideoAtDelay", FLOAT }, - { "scrollDirection", STRING } } }, + { "scrollDirection", STRING }, + { "centerSelection", BOOLEAN }, + { "scrollLoop", BOOLEAN } } }, { "gridtile", { { "size", NORMALIZED_PAIR }, { "padding", NORMALIZED_PAIR }, diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index deed1dc503..3b6759f6fb 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -391,17 +391,17 @@ void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) Transform4x4f trans = Transform4x4f::Identity(); Renderer::setMatrix(trans); Renderer::drawRect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | (unsigned char)(opacity * 255)); - - ImageComponent splash(this, true); - splash.setResize(Renderer::getScreenWidth() * 0.4f, 0.0f); + ImageComponent splash(this, true); if (mSplash != NULL) splash.setImage(mSplash); else splash.setImage(":/splash.svg"); - splash.setPosition((Renderer::getScreenWidth() - splash.getSize().x()) / 2, (Renderer::getScreenHeight() - splash.getSize().y()) / 2 * 0.7f); + splash.setOrigin(0.5, 0.5); + splash.setPosition(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() * 0.835f / 2.0f); + splash.setMaxSize(Renderer::getScreenWidth() * 0.60f, Renderer::getScreenHeight() * 0.60f); if (!mCustomSplash.empty()) splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 210)); @@ -409,7 +409,7 @@ void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) splash.setColorShift(0xFFFFFF00 | (unsigned char)(opacity * 255)); splash.render(trans); - + auto& font = mDefaultFonts.at(1); font->reload(); // Ensure font is loaded diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index b56d3186b0..1444dbf06d 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -117,6 +117,9 @@ class ImageGridComponent : public IList float mCameraDirection; // MISCELLANEOUS + bool mCenterSelection; + bool mScrollLoop; + ScrollDirection mScrollDirection; ImageSource mImageSource; @@ -124,15 +127,15 @@ class ImageGridComponent : public IList }; template -ImageGridComponent::ImageGridComponent(Window* window) : IList(window) +ImageGridComponent::ImageGridComponent(Window* window) : IList(window, LIST_SCROLL_STYLE_QUICK, LIST_ALWAYS_LOOP) { Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); mCamera = 0.0; mCameraDirection = 1.0; - mGridSizeOverride = Vector2f(0, 0); - mAutoLayout = Vector2f(0, 0); + mGridSizeOverride = Vector2f::Zero(); + mAutoLayout = Vector2f::Zero(); mAutoLayoutZoom = 1.0; mVideoDelay = 750; @@ -147,10 +150,13 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList void ImageGridComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = getTransform() * parentTrans; + Transform4x4f tileTrans = trans; Vector2f clipPos(trans.translation().x(), trans.translation().y()); if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) @@ -293,6 +300,8 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) float offsetX = isVertical() ? 0 : mCamera * mCameraDirection * (mTileSize.x() + mMargin.x()); float offsetY = isVertical() ? mCamera * mCameraDirection * (mTileSize.y() + mMargin.y()) : 0; + tileTrans.translate(Vector3f(offsetX, offsetY, 0.0)); + if (mEntriesDirty) { updateTiles(); @@ -321,13 +330,7 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) } Renderer::pushClipRect(pos, size); - - if (mCamera != 0) - { - for (auto it = mTiles.begin(); it != mTiles.end(); it++) - (*it)->setPosition((*it)->getPosition().x() + offsetX, (*it)->getPosition().y() + offsetY); - } - + // Render the selected image background on bottom of the others if needed std::shared_ptr selectedTile = NULL; for(auto it = mTiles.begin(); it != mTiles.end(); it++) @@ -338,7 +341,7 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) selectedTile = tile; if (mAnimateSelection && tile->shouldSplitRendering()) - tile->renderBackground(trans); + tile->renderBackground(tileTrans); break; } @@ -348,28 +351,22 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) { std::shared_ptr tile = (*it); if (!tile->isSelected()) - tile->render(trans); + tile->render(tileTrans); } // Render the selected image content on top of the others if (selectedTile != NULL) { if (mAnimateSelection && selectedTile->shouldSplitRendering()) - selectedTile->renderContent(trans); + selectedTile->renderContent(tileTrans); else - selectedTile->render(trans); + selectedTile->render(tileTrans); } Renderer::popClipRect(); listRenderTitleOverlay(trans); - if (mCamera != 0) - { - for (auto it = mTiles.begin(); it != mTiles.end(); it++) - (*it)->setPosition((*it)->getPosition().x() - offsetX, (*it)->getPosition().y() - offsetY); - } - GuiComponent::renderChildren(trans); } @@ -419,13 +416,25 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, { auto direction = elem->get("scrollDirection"); if (direction == "horizontal") + { + mCenterSelection = false; mScrollDirection = SCROLL_HORIZONTALLY; + } else if (direction == "horizontalCenter") + { + mCenterSelection = true; mScrollDirection = SCROLL_HORIZONTALLY_CENTER; + } else if (direction == "verticalCenter") + { + mCenterSelection = true; mScrollDirection = SCROLL_VERTICALLY_CENTER; - else + } + else + { + mCenterSelection = false; mScrollDirection = SCROLL_VERTICALLY; + } } if (elem->has("showVideoAtDelay")) @@ -436,6 +445,19 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, else mAllowVideo = false; + if (elem->has("centerSelection")) + mCenterSelection = (elem->get("centerSelection")); + + if (mCenterSelection && elem->has("scrollLoop")) + mScrollLoop = (elem->get("scrollLoop")); + else + mScrollLoop = false; + /* + if (mScrollLoop) + mLoopType = LIST_ALWAYS_LOOP; + else + mLoopType = LIST_PAUSE_AT_END; + */ if (elem->has("gameImage")) { std::string path = elem->get("gameImage"); @@ -510,17 +532,18 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) if (state == CURSOR_STOPPED && mCursorChangedCallback) mCursorChangedCallback(state); - TRACE("skip ImageGridComponent::onCursorChanged " << state); - return; } - TRACE("ImageGridComponent::onCursorChanged " << state); - - bool centerSel = (mScrollDirection == SCROLL_HORIZONTALLY_CENTER || mScrollDirection == SCROLL_VERTICALLY_CENTER); - + if (mScrollLoop && mEntries.size() < mGridDimension.y() * mGridDimension.x()) + mScrollLoop = false; + bool direction = mCursor >= mLastCursor; + int diff = direction ? mCursor - mLastCursor : mLastCursor - mCursor; + if (mScrollLoop && diff == mEntries.size() - 1) + direction = !direction; + int oldStart = mStartPosition; float dimScrollable = isVertical() ? mGridDimension.y() - 2 * EXTRAITEMS : mGridDimension.x() - 2 * EXTRAITEMS; @@ -541,14 +564,6 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) if (((GuiComponent*)this)->isAnimationPlaying(2)) { - /* - startPos = -(mCamera * 0.75); - - if (startPos > 1) - startPos = 1; - else if (startPos < -1) - startPos = -1; - */ startPos = 0; ((GuiComponent*)this)->cancelAnimation(2); updateTiles(false, !GuiComponent::ALLOWANIMATIONS); @@ -564,6 +579,15 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) oldTile = mTiles[oldIdx]; int newIdx = mCursor - mStartPosition + (dimOpposite * EXTRAITEMS); + + if (mScrollLoop) + { + if (newIdx < 0) + newIdx += mEntries.size(); + else if (newIdx >= mTiles.size()) + newIdx -= mEntries.size(); + } + if (newIdx >= 0 && newIdx < mTiles.size()) newTile = mTiles[newIdx]; @@ -576,7 +600,7 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) } } - Vector3f oldPos = Vector3f(0, 0); + Vector3f oldPos = Vector3f::Zero(); if (oldTile != nullptr && oldTile != newTile) { @@ -598,9 +622,9 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) int firstVisibleCol = mStartPosition / dimOpposite; - if ((col < centralCol || (col == 0 && col == centralCol)) && !centerSel) + if ((col < centralCol || (col == 0 && col == centralCol)) && !mCenterSelection) mStartPosition = 0; - else if ((col - centralCol) > lastScroll && !centerSel) + else if ((col - centralCol) > lastScroll && !mCenterSelection && !mScrollLoop) mStartPosition = lastScroll * dimOpposite; else if (maxCentralCol != centralCol && col == firstVisibleCol + maxCentralCol || col == firstVisibleCol + centralCol) { @@ -625,7 +649,7 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) if (lastCursor < 0 || !GuiComponent::ALLOWANIMATIONS) { - updateTiles(lastCursor >= 0 && GuiComponent::ALLOWANIMATIONS); + updateTiles((lastCursor >= 0 || mScrollLoop) && GuiComponent::ALLOWANIMATIONS); if (mCursorChangedCallback) mCursorChangedCallback(state); @@ -690,10 +714,7 @@ void ImageGridComponent::updateTiles(bool allowAnimation, bool updateSelected int end = (int)mTiles.size(); int img = mStartPosition; - if (isVertical()) - img -= EXTRAITEMS * mGridDimension.x(); - else - img -= EXTRAITEMS * mGridDimension.y(); + img -= EXTRAITEMS * (isVertical() ? mGridDimension.x() : mGridDimension.y()); while (i != end) { @@ -701,7 +722,6 @@ void ImageGridComponent::updateTiles(bool allowAnimation, bool updateSelected i++; img++; } - if (updateSelectedState) mLastCursor = mCursor; @@ -713,6 +733,14 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA { std::shared_ptr tile = mTiles.at(tilePos); + if (mScrollLoop) + { + if (imgPos < 0) + imgPos += mEntries.size(); + else if (imgPos >= size()) + imgPos -= mEntries.size(); + } + // If we have more tiles than we have to display images on screen, hide them if(imgPos < 0 || imgPos >= size() || tilePos < 0 || tilePos >= (int) mTiles.size()) // Same for tiles out of the buffer { @@ -787,6 +815,12 @@ void ImageGridComponent::buildTiles() calcGridDimension(); + if (mCenterSelection) + { + int dimScrollable = (isVertical() ? mGridDimension.y() : mGridDimension.x()) - 2 * EXTRAITEMS; + mStartPosition -= (int)Math::floorf(dimScrollable / 2.0f); + } + Vector2f tileDistance = mTileSize + mMargin; Vector2f tileSize = mTileSize; From defd62720740ec86009dcab4bbf2f56b4778731d Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 18 Jul 2019 15:34:35 +0200 Subject: [PATCH 086/336] ScrollLoop : Fixes -> if item count < cols / if item count mod rows != 0 --- es-core/src/components/ImageGridComponent.h | 69 ++++++++++++--------- 1 file changed, 39 insertions(+), 30 deletions(-) diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 1444dbf06d..b0f6dc3a08 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -14,9 +14,7 @@ enum ScrollDirection { SCROLL_VERTICALLY, - SCROLL_HORIZONTALLY, - SCROLL_VERTICALLY_CENTER, - SCROLL_HORIZONTALLY_CENTER, + SCROLL_HORIZONTALLY }; enum ImageSource @@ -85,8 +83,8 @@ class ImageGridComponent : public IList void updateTiles(bool allowAnimation = true, bool updateSelectedState = true); void updateTileAtPos(int tilePos, int imgPos, bool allowAnimation = true, bool updateSelectedState = true); void calcGridDimension(); - - bool isVertical() { return mScrollDirection == SCROLL_VERTICALLY || mScrollDirection == SCROLL_VERTICALLY_CENTER; }; + + bool isVertical() { return mScrollDirection == SCROLL_VERTICALLY; }; bool mEntriesDirty; int mLastCursor; @@ -110,6 +108,7 @@ class ImageGridComponent : public IList std::string mName; int mStartPosition; + bool mAllowVideo; float mVideoDelay; @@ -127,7 +126,7 @@ class ImageGridComponent : public IList }; template -ImageGridComponent::ImageGridComponent(Window* window) : IList(window, LIST_SCROLL_STYLE_QUICK, LIST_ALWAYS_LOOP) +ImageGridComponent::ImageGridComponent(Window* window) : IList(window) { Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); @@ -141,7 +140,7 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList::applyTheme(const std::shared_ptr& theme, else if (direction == "horizontalCenter") { mCenterSelection = true; - mScrollDirection = SCROLL_HORIZONTALLY_CENTER; + mScrollDirection = SCROLL_HORIZONTALLY; } else if (direction == "verticalCenter") { mCenterSelection = true; - mScrollDirection = SCROLL_VERTICALLY_CENTER; + mScrollDirection = SCROLL_VERTICALLY; } else { @@ -452,12 +451,7 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, mScrollLoop = (elem->get("scrollLoop")); else mScrollLoop = false; - /* - if (mScrollLoop) - mLoopType = LIST_ALWAYS_LOOP; - else - mLoopType = LIST_PAUSE_AT_END; - */ + if (elem->has("gameImage")) { std::string path = elem->get("gameImage"); @@ -535,8 +529,6 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) return; } - if (mScrollLoop && mEntries.size() < mGridDimension.y() * mGridDimension.x()) - mScrollLoop = false; bool direction = mCursor >= mLastCursor; @@ -580,13 +572,13 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) int newIdx = mCursor - mStartPosition + (dimOpposite * EXTRAITEMS); - if (mScrollLoop) + if (mScrollLoop && mScrollTier == 0 && diff == mEntries.size() - 1) { - if (newIdx < 0) + if (direction) newIdx += mEntries.size(); - else if (newIdx >= mTiles.size()) - newIdx -= mEntries.size(); - } + else + newIdx -= mEntries.size(); + } if (newIdx >= 0 && newIdx < mTiles.size()) newTile = mTiles[newIdx]; @@ -639,7 +631,7 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) mStartPosition = (col - maxCentralCol) * dimOpposite; else mStartPosition = (col - centralCol) * dimOpposite; - } + } auto lastCursor = mLastCursor; mLastCursor = mCursor; @@ -733,12 +725,29 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA { std::shared_ptr tile = mTiles.at(tilePos); - if (mScrollLoop) + bool loopedIndex = false; + + if (mScrollLoop && mEntries.size() > 0) { - if (imgPos < 0) - imgPos += mEntries.size(); - else if (imgPos >= size()) - imgPos -= mEntries.size(); + int dimOpposite = isVertical() ? mGridDimension.x() : mGridDimension.y(); + + int min = dimOpposite == 1 ? 0 : (mEntries.size() % dimOpposite) - dimOpposite; + int max = mEntries.size() - min; + + if (imgPos < min) + { + loopedIndex = true; + + while (imgPos < min) + imgPos += max; + } + else if (imgPos >= max) + { + loopedIndex = true; + + while (imgPos >= max) + imgPos -= max; + } } // If we have more tiles than we have to display images on screen, hide them @@ -782,7 +791,7 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA if (updateSelectedState) { - if (imgPos == mCursor && mCursor != mLastCursor) + if (!loopedIndex && imgPos == mCursor && mCursor != mLastCursor) { int dif = mCursor - tilePos; int idx = mLastCursor - dif; @@ -797,7 +806,7 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA tile->setSelected(true, allowAnimation, &pos); } else - tile->setSelected(imgPos == mCursor, allowAnimation); + tile->setSelected(!loopedIndex && imgPos == mCursor, allowAnimation); } } } From 867fef8f2454f0af234c3e8a942f5fa15693bf25 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 20 Jul 2019 21:40:32 +0200 Subject: [PATCH 087/336] TextureResource : bugfix ( for linux ) NinePatchComponent : avoid unload/reload/unload/reload.... --- es-core/src/components/NinePatchComponent.cpp | 8 +++++++- es-core/src/resources/TextureResource.cpp | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index 7443dd93ec..b49ae2ad7d 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -60,7 +60,12 @@ void NinePatchComponent::buildVertices() if(mColors != NULL) delete[] mColors; - mTexture = TextureResource::get(mPath); + if (mPath.empty()) + return; + + auto tr = TextureResource::get(mPath); + if (tr != mTexture) + mTexture = tr; if(mTexture->getSize() == Vector2i::Zero()) { @@ -203,6 +208,7 @@ void NinePatchComponent::setImagePath(const std::string& path) return; mPath = path; + mTexture = nullptr; buildVertices(); } diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index dbaa2361a5..2535f85979 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -220,7 +220,7 @@ std::shared_ptr TextureResource::get(const std::string& path, b if (canonicalPath.length() > 0 && canonicalPath[0] == ':') sPermanentTextureMap[key] = std::shared_ptr(tex); else - sTextureMap[key] = std::shared_ptr(tex); + sTextureMap[key] = std::weak_ptr(tex); rm->addReloadable(tex); } From cad26cda2dfe7b4435e5c5a13a0f3ffba0e7ee78 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 20 Jul 2019 23:59:36 +0200 Subject: [PATCH 088/336] TextureResource : removing permanent texture map as it is a problem on linux --- es-core/src/resources/TextureResource.cpp | 25 +---------------------- 1 file changed, 1 insertion(+), 24 deletions(-) diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index 2535f85979..db3b96d9d9 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -8,8 +8,6 @@ TextureDataManager TextureResource::sTextureDataManager; std::map< TextureResource::TextureKeyType, std::weak_ptr> TextureResource::sTextureMap; -std::map< TextureResource::TextureKeyType, std::shared_ptr> TextureResource::sPermanentTextureMap; - std::set TextureResource::sAllTextures; TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic, bool allowAsync, MaxSizeInfo maxSize) : mTextureData(nullptr), mForceLoad(false) @@ -193,21 +191,6 @@ std::shared_ptr TextureResource::get(const std::string& path, b sTextureMap.erase(foundTexture); } - auto permanentTexture = sPermanentTextureMap.find(key); - if (permanentTexture != sPermanentTextureMap.cend()) - { - std::shared_ptr rc = permanentTexture->second; - - if (!maxSize.empty() && TextureData::OPTIMIZEVRAM) - { - auto dt = sTextureDataManager.get(rc.get()); - if (dt != nullptr) - dt->setMaxSize(maxSize); - } - - return rc; - } - // need to create it std::shared_ptr tex; tex = std::shared_ptr(new TextureResource(key.first, tile, dynamic, !forceLoad, maxSize)); @@ -215,13 +198,7 @@ std::shared_ptr TextureResource::get(const std::string& path, b if (asReloadable) // // is it an SVG // if (key.first.substr(key.first.size() - 4, std::string::npos) != ".svg") // FCATMP { - // Probably not. Add it to our map. We don't add SVGs because 2 svgs might be rasterized at different sizes - // FCA useless -> If the svg is too small, it will be reloaded bigger with setSourceSize... - if (canonicalPath.length() > 0 && canonicalPath[0] == ':') - sPermanentTextureMap[key] = std::shared_ptr(tex); - else - sTextureMap[key] = std::weak_ptr(tex); - + sTextureMap[key] = std::weak_ptr(tex); rm->addReloadable(tex); } From 75ea8954bbbac5016112f7452e90302c934a4640 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 21 Jul 2019 10:37:33 +0200 Subject: [PATCH 089/336] Add sdl2_mixer to dependencies & build --- CMake/Packages/FindSDL2MIXER.cmake | 49 ++++++++++++++++++++++++++++++ CMakeLists.txt | 3 ++ 2 files changed, 52 insertions(+) create mode 100644 CMake/Packages/FindSDL2MIXER.cmake diff --git a/CMake/Packages/FindSDL2MIXER.cmake b/CMake/Packages/FindSDL2MIXER.cmake new file mode 100644 index 0000000000..ce18011829 --- /dev/null +++ b/CMake/Packages/FindSDL2MIXER.cmake @@ -0,0 +1,49 @@ +# +# this module look for SDL2_Mixer (http://www.libsdl.org) support +# it will define the following values +# +# SDLMIXER_INCLUDE_DIR = where SDL_mixer.h can be found +# SDLMIXER_LIBRARY = the library to link against SDL2_mixer +# SDLMIXER_FOUND = set to 1 if SDL2_mixer is found +# + +IF(SDL2_Mixer_INCLUDE_DIRS) + + FIND_PATH(SDLMIXER_INCLUDE_DIR SDL2/SDL_mixer.h ${SDL2_Mixer_INCLUDE_DIRS}) + FIND_LIBRARY(SDLMIXER_LIBRARY SDL2_mixer ${SDL2_Mixer_LIBRARY_DIRS}) + +ELSE(SDL2_Mixer_INCLUDE_DIRS) + + SET(TRIAL_LIBRARY_PATHS + $ENV{SDL2_MIXER_HOME}/lib + /usr/lib + /usr/local/lib + /sw/lib + ) + SET(TRIAL_INCLUDE_PATHS + $ENV{SDL2_MIXER_HOME}/include/SDL2 + /usr/include/SDL2 + /usr/local/include/SDL2 + /sw/include/SDL2 + ) + + FIND_LIBRARY(SDLMIXER_LIBRARY SDL2_mixer ${TRIAL_LIBRARY_PATHS}) + FIND_PATH(SDLMIXER_INCLUDE_DIR SDL_mixer.h ${TRIAL_INCLUDE_PATHS}) + +ENDIF(SDL2_Mixer_INCLUDE_DIRS) + +IF(SDLMIXER_INCLUDE_DIR AND SDLMIXER_LIBRARY) + SET(SDLMIXER_FOUND 1 CACHE BOOL "Found SDL2_Mixer library") +ELSE(SDLMIXER_INCLUDE_DIR AND SDLMIXER_LIBRARY) + SET(SDLMIXER_FOUND 0 CACHE BOOL "Not fount SDL2_Mixer library") +ENDIF(SDLMIXER_INCLUDE_DIR AND SDLMIXER_LIBRARY) + +MARK_AS_ADVANCED( + SDLMIXER_INCLUDE_DIR + SDLMIXER_LIBRARY + SDLMIXER_FOUND +) + +INCLUDE(FindPackageHandleStandardArgs) + +FIND_PACKAGE_HANDLE_STANDARD_ARGS(SDLMIXER REQUIRED_VARS SDLMIXER_INCLUDE_DIR SDLMIXER_LIBRARY) diff --git a/CMakeLists.txt b/CMakeLists.txt index ef2d778a22..12882f085b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -55,6 +55,7 @@ endif() find_package(Freetype REQUIRED) find_package(FreeImage REQUIRED) find_package(SDL2 REQUIRED) +find_package(SDL2MIXER REQUIRED) find_package(CURL REQUIRED) find_package(VLC REQUIRED) find_package(RapidJSON REQUIRED) @@ -119,6 +120,7 @@ set(COMMON_INCLUDE_DIRS ${FREETYPE_INCLUDE_DIRS} ${FreeImage_INCLUDE_DIRS} ${SDL2_INCLUDE_DIR} + ${SDLMIXER_INCLUDE_DIR} ${CURL_INCLUDE_DIR} ${VLC_INCLUDE_DIR} ${RAPIDJSON_INCLUDE_DIRS} @@ -180,6 +182,7 @@ set(COMMON_LIBRARIES ${FREETYPE_LIBRARIES} ${FreeImage_LIBRARIES} ${SDL2_LIBRARY} + ${SDLMIXER_LIBRARY} ${CURL_LIBRARIES} ${VLC_LIBRARIES} pugixml From f683ae3fd97f68815fd4b06e4c456f29ee708893 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 22 Jul 2019 00:49:02 +0200 Subject: [PATCH 090/336] Support for musics --- es-app/src/FileData.cpp | 5 + es-app/src/guis/GuiMenu.cpp | 22 +- es-app/src/main.cpp | 4 + es-app/src/views/SystemView.cpp | 10 +- es-app/src/views/SystemView.h | 5 +- es-app/src/views/ViewController.cpp | 7 +- es-core/src/AudioManager.cpp | 252 +++++++++++++++-------- es-core/src/AudioManager.h | 37 +++- es-core/src/Settings.cpp | 24 ++- es-core/src/Sound.cpp | 121 ++--------- es-core/src/Sound.h | 15 +- es-core/src/ThemeData.h | 5 +- resources/locale/fr/emulationstation2.po | 4 + 13 files changed, 297 insertions(+), 214 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 65ef13b388..168c3497e7 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -259,6 +259,7 @@ void FileData::launchGame(Window* window) window->init(hideWindow); VolumeControl::getInstance()->init(); + AudioManager::getInstance()->setSystemName(mSystem->getTheme()->getSystemThemeFolder()); // system-specific music -> automatic Init window->normalizeNextUpdate(); //update number of times the game has been launched @@ -273,6 +274,10 @@ void FileData::launchGame(Window* window) gameToUpdate->metadata.set("lastplayed", Utils::Time::DateTime(Utils::Time::now())); CollectionSystemManager::get()->refreshCollectionSystems(gameToUpdate); } + + // music + if (Settings::getInstance()->getBool("audio.bgmusic")) + AudioManager::getInstance()->playRandomMusic(); } CollectionFileData::CollectionFileData(FileData* file, SystemData* system) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 7c6e1b2a94..da4696ce75 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -18,7 +18,7 @@ #include "VolumeControl.h" #include #include - +#include "AudioManager.h" #include "resources/TextureData.h" #include "animations/LambdaAnimation.h" #include "GuiGamelistOptions.h" // grid sizes @@ -231,7 +231,27 @@ void GuiMenu::openSoundSettings() VolumeControl::getInstance()->init(); }); #endif + // disable sounds + auto music_enabled = std::make_shared(mWindow); + music_enabled->setState(Settings::getInstance()->getBool("audio.bgmusic")); + s->addWithLabel(_T("FRONTEND MUSIC"), music_enabled); + s->addSaveFunc([music_enabled] { + Settings::getInstance()->setBool("audio.bgmusic", music_enabled->getState()); + if (music_enabled->getState()) + AudioManager::getInstance()->playRandomMusic(); + else + AudioManager::getInstance()->stopMusic(); + }); + + // music per system + auto music_per_system = std::make_shared(mWindow); + music_per_system->setState(Settings::getInstance()->getBool("audio.persystem")); + s->addWithLabel(_T("ONLY PLAY SYSTEM-SPECIFIC MUSIC FOLDER"), music_per_system); + s->addSaveFunc([music_per_system] { + Settings::getInstance()->setBool("audio.persystem", music_per_system->getState()); + }); + // disable sounds auto sounds_enabled = std::make_shared(mWindow); sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index cfcc961fde..2cf5495fc2 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -27,6 +27,7 @@ #include "resources/TextureData.h" #include +#include "AudioManager.h" bool scrape_cmdline = false; @@ -441,6 +442,9 @@ int main(int argc, char* argv[]) window.endRenderLoadingScreen(); + if (Settings::getInstance()->getBool("audio.bgmusic")) + AudioManager::getInstance()->playRandomMusic(); + #ifdef WIN32 DWORD displayFrequency = 60; diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 69a53b4a4a..3487d441a2 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -9,6 +9,7 @@ #include "Settings.h" #include "SystemData.h" #include "Window.h" +#include "AudioManager.h" // buffer values for scrolling velocity (left, stopped, right) const int logoBuffersLeft[] = { -5, -2, -1 }; @@ -21,13 +22,12 @@ SystemView::SystemView(Window* window) : IList(wind mCamOffset = 0; mExtrasCamOffset = 0; mExtrasFadeOpacity = 0.0f; + mLastSystem = nullptr; setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); populate(); } - - void SystemView::populate() { mEntries.clear(); @@ -226,6 +226,11 @@ void SystemView::update(int deltaTime) void SystemView::onCursorChanged(const CursorState& /*state*/) { + if (mLastSystem != getSelected()) { + mLastSystem = getSelected(); + AudioManager::getInstance()->themeChanged(getSelected()->getTheme()); + } + // update help style updateHelpPrompts(); @@ -254,6 +259,7 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) bool goFast = transition_style == "instant"; const float infoStartOpacity = mSystemInfo.getOpacity() / 255.f; + Animation* infoFadeOut = new LambdaAnimation( [infoStartOpacity, this] (float t) { diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index 675292fdc9..d6bae81a7f 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -87,8 +87,9 @@ class SystemView : public IList void renderFade(const Transform4x4f& trans); - SystemViewCarousel mCarousel; - TextComponent mSystemInfo; + SystemViewCarousel mCarousel; + TextComponent mSystemInfo; + SystemData* mLastSystem; // unit is list index float mCamOffset; diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 23cc3cd903..54a40dde85 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -16,6 +16,7 @@ #include "Settings.h" #include "SystemData.h" #include "Window.h" +#include "AudioManager.h" ViewController* ViewController::sInstance = NULL; @@ -115,7 +116,9 @@ void ViewController::goToNextGameList() assert(system); system = system->getNext(); - goToGameList(system); + goToGameList(system); + + AudioManager::getInstance()->themeChanged(system->getTheme()); // FixedCarousel if (Settings::getInstance()->getBool("FixedCarousel")) @@ -131,6 +134,8 @@ void ViewController::goToPrevGameList() system = system->getPrev(); goToGameList(system); + AudioManager::getInstance()->themeChanged(system->getTheme()); + // FixedCarousel if (Settings::getInstance()->getBool("FixedCarousel")) getSystemListView()->goToSystem(system, true); diff --git a/es-core/src/AudioManager.cpp b/es-core/src/AudioManager.cpp index 1d0f8ac495..786d8658d1 100644 --- a/es-core/src/AudioManager.cpp +++ b/es-core/src/AudioManager.cpp @@ -4,111 +4,74 @@ #include "Settings.h" #include "Sound.h" #include +#include +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" std::vector> AudioManager::sSoundVector; -SDL_AudioSpec AudioManager::sAudioFormat; std::shared_ptr AudioManager::sInstance; - - -void AudioManager::mixAudio(void* /*unused*/, Uint8 *stream, int len) -{ - bool stillPlaying = false; - - //initialize the buffer to "silence" - SDL_memset(stream, 0, len); - - //iterate through all our samples - std::vector>::const_iterator soundIt = sSoundVector.cbegin(); - while (soundIt != sSoundVector.cend()) - { - std::shared_ptr sound = *soundIt; - if(sound->isPlaying()) - { - //calculate rest length of current sample - Uint32 restLength = (sound->getLength() - sound->getPosition()); - if (restLength > (Uint32)len) { - //if stream length is smaller than smaple lenght, clip it - restLength = len; - } - //mix sample into stream - SDL_MixAudio(stream, &(sound->getData()[sound->getPosition()]), restLength, SDL_MIX_MAXVOLUME); - if (sound->getPosition() + restLength < sound->getLength()) - { - //sample hasn't ended yet - stillPlaying = true; - } - //set new sound position. if this is at or beyond the end of the sample, it will stop automatically - sound->setPosition(sound->getPosition() + restLength); - } - //advance to next sound - ++soundIt; - } - - //we have processed all samples. check if some will still be playing - if (!stillPlaying) { - //no. pause audio till a Sound::play() wakes us up - SDL_PauseAudio(1); - } -} +Mix_Music* AudioManager::mCurrentMusic = NULL; AudioManager::AudioManager() -{ +{ init(); } AudioManager::~AudioManager() { - deinit(); + //stop all playback + stop(); + stopMusic(); + + // Stop playing all Sounds & reload them + for (unsigned int i = 0; i < sSoundVector.size(); i++) + sSoundVector[i]->deinit(); + + Mix_HookMusicFinished(nullptr); + Mix_HaltMusic(); + + //completely tear down SDL audio. else SDL hogs audio resources and emulators might fail to start... + SDL_CloseAudio(); + SDL_QuitSubSystem(SDL_INIT_AUDIO); } std::shared_ptr & AudioManager::getInstance() { //check if an AudioManager instance is already created, if not create one - if (sInstance == nullptr && Settings::getInstance()->getBool("EnableSounds")) { + // && Settings::getInstance()->getBool("EnableSounds") + if (sInstance == nullptr) sInstance = std::shared_ptr(new AudioManager); - } + return sInstance; } void AudioManager::init() { + mRunningFromPlaylist = false; + if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) { LOG(LogError) << "Error initializing SDL audio!\n" << SDL_GetError(); return; } - //stop playing all Sounds - for(unsigned int i = 0; i < sSoundVector.size(); i++) + // Stop playing all Sounds & reload them + for (unsigned int i = 0; i < sSoundVector.size(); i++) { - if(sSoundVector.at(i)->isPlaying()) - { - sSoundVector[i]->stop(); - } + sSoundVector[i]->stop(); + sSoundVector[i]->init(); } - //Set up format and callback. Play 16-bit stereo audio at 44.1Khz - sAudioFormat.freq = 44100; - sAudioFormat.format = AUDIO_S16; - sAudioFormat.channels = 2; - sAudioFormat.samples = 4096; - sAudioFormat.callback = mixAudio; - sAudioFormat.userdata = NULL; - //Open the audio device and pause - if (SDL_OpenAudio(&sAudioFormat, NULL) < 0) { - LOG(LogError) << "AudioManager Error - Unable to open SDL audio: " << SDL_GetError() << std::endl; - } + if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 4096) < 0) + LOG(LogError) << "MUSIC Error - Unable to open SDLMixer audio: " << SDL_GetError() << std::endl; + else + LOG(LogInfo) << "SDL AUDIO Initialized"; } void AudioManager::deinit() { - //stop all playback - stop(); - //completely tear down SDL audio. else SDL hogs audio resources and emulators might fail to start... - SDL_CloseAudio(); - SDL_QuitSubSystem(SDL_INIT_AUDIO); - sInstance = NULL; + sInstance = NULL; } void AudioManager::registerSound(std::shared_ptr & sound) @@ -120,9 +83,9 @@ void AudioManager::registerSound(std::shared_ptr & sound) void AudioManager::unregisterSound(std::shared_ptr & sound) { getInstance(); - for(unsigned int i = 0; i < sSoundVector.size(); i++) + for (unsigned int i = 0; i < sSoundVector.size(); i++) { - if(sSoundVector.at(i) == sound) + if (sSoundVector.at(i) == sound) { sSoundVector[i]->stop(); sSoundVector.erase(sSoundVector.cbegin() + i); @@ -135,21 +98,148 @@ void AudioManager::unregisterSound(std::shared_ptr & sound) void AudioManager::play() { getInstance(); - - //unpause audio, the mixer will figure out if samples need to be played... - SDL_PauseAudio(0); } void AudioManager::stop() { - //stop playing all Sounds - for(unsigned int i = 0; i < sSoundVector.size(); i++) + // Stop playing all Sounds + for (unsigned int i = 0; i < sSoundVector.size(); i++) + if (sSoundVector.at(i)->isPlaying()) + sSoundVector[i]->stop(); +} + +void AudioManager::findMusic(const std::string &path, std::vector& all_matching_files) +{ + if (!Utils::FileSystem::isDirectory(path)) + return; + + bool anySystem = !Settings::getInstance()->getBool("audio.persystem"); + + auto dirContent = Utils::FileSystem::getDirContent(path); + for (auto it = dirContent.cbegin(); it != dirContent.cend(); ++it) { - if(sSoundVector.at(i)->isPlaying()) + if (Utils::FileSystem::isDirectory(*it)) { - sSoundVector[i]->stop(); + if (*it == "." || *it == "..") + continue; + + if (anySystem || mSystemName == Utils::FileSystem::getFileName(*it)) + findMusic(*it, all_matching_files); } + else + { + std::string extension = Utils::String::toLower(Utils::FileSystem::getExtension(*it)); + if (extension == ".mp3" || extension == ".ogg") + all_matching_files.push_back(*it); + } + } +} + +void AudioManager::playRandomMusic(bool continueIfPlaying) +{ + std::vector musics; + + // check in Theme music directory + if (!mCurrentThemeMusicDirectory.empty()) + findMusic(mCurrentThemeMusicDirectory, musics); + + // check in User music directory + if (musics.empty() && !Settings::getInstance()->getString("UserMusicDirectory").empty()) + findMusic(Settings::getInstance()->getString("MusicDirectory"), musics); + + // check in System music directory + if (musics.empty() && !Settings::getInstance()->getString("MusicDirectory").empty()) + findMusic(Settings::getInstance()->getString("MusicDirectory"), musics); + + // check in .emulationstation/music directory + if (musics.empty()) + findMusic(Utils::FileSystem::getHomePath() + "/.emulationstation/music", musics); + + if (musics.empty()) + return; + +#if defined(WIN32) + srand(time(NULL) % getpid()); +#else + srand(time(NULL) % getpid() + getppid()); +#endif + + int randomIndex = rand() % musics.size(); + + // continue playing ? + if (mCurrentMusic != NULL && continueIfPlaying) + return; + + playMusic(musics.at(randomIndex)); + mRunningFromPlaylist = true; + Mix_HookMusicFinished(AudioManager::onMusicFinished); +} + +void AudioManager::playMusic(std::string path) +{ + // free the previous music + stopMusic(); + + // load a new music + mCurrentMusic = Mix_LoadMUS(path.c_str()); + if (mCurrentMusic == NULL) + { + LOG(LogError) << Mix_GetError() << " for " << path; + return; + } + + if (Mix_FadeInMusic(mCurrentMusic, 1, 1000) == -1) + { + stopMusic(); + return; + } + + Mix_HookMusicFinished(AudioManager::onMusicFinished); +} + +void AudioManager::onMusicFinished() +{ + AudioManager::getInstance()->playRandomMusic(false); +} + +void AudioManager::stopMusic() +{ + if (mCurrentMusic == NULL) + return; + + Mix_HookMusicFinished(nullptr); + Mix_FreeMusic(mCurrentMusic); + Mix_HaltMusic(); + + mCurrentMusic = NULL; +} + +void AudioManager::themeChanged(const std::shared_ptr& theme) +{ + mCurrentThemeMusicDirectory = ""; + + if (Settings::getInstance()->getBool("audio.bgmusic")) + { + const ThemeData::ThemeElement* elem = theme->getElement("system", "directory", "sound"); + if (elem && elem->has("path")) + mCurrentThemeMusicDirectory = elem->get("path"); + + std::string bgSound; + + elem = theme->getElement("system", "bgsound", "sound"); + if (elem && elem->has("path") && Utils::FileSystem::exists(elem->get("path"))) + bgSound = elem->get("path"); + + // Found a music for the system + if (!bgSound.empty()) + { + mRunningFromPlaylist = false; + playMusic(bgSound); + return; + } + + mSystemName = theme->getSystemThemeFolder(); + if (!mRunningFromPlaylist || Settings::getInstance()->getBool("audio.persystem")) + playRandomMusic(false); } - //pause audio - SDL_PauseAudio(1); } diff --git a/es-core/src/AudioManager.h b/es-core/src/AudioManager.h index e27ce1dc2e..5814aabff4 100644 --- a/es-core/src/AudioManager.h +++ b/es-core/src/AudioManager.h @@ -5,18 +5,14 @@ #include #include #include +#include "SDL_mixer.h" +#include "ThemeData.h" +#include class Sound; class AudioManager -{ - static SDL_AudioSpec sAudioFormat; - static std::vector> sSoundVector; - static std::shared_ptr sInstance; - - static void mixAudio(void *unused, Uint8 *stream, int len); - - AudioManager(); +{ public: static std::shared_ptr & getInstance(); @@ -30,7 +26,32 @@ class AudioManager void play(); void stop(); + void playRandomMusic(bool continueIfPlaying = true); + void stopMusic(); + void themeChanged(const std::shared_ptr& theme); + + void setSystemName(std::string name) { + mSystemName = name; + } + virtual ~AudioManager(); + +private: + AudioManager(); + + static std::vector> sSoundVector; + static std::shared_ptr sInstance; + static Mix_Music* mCurrentMusic; + + static void onMusicFinished(); + + void findMusic(const std::string &path, std::vector& all_matching_files); + void playMusic(std::string path); + + std::string mSystemName; + std::string mCurrentThemeMusicDirectory; + bool mRunningFromPlaylist; + }; #endif // ES_CORE_AUDIO_MANAGER_H diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 0ef2fc97ab..e2a104d20a 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -30,7 +30,7 @@ std::vector settings_dont_save { #endif { "FullscreenBorderless" }, { "Windowed" }, - { "WindowWidth" }, + { "WindowWidth" }, { "WindowHeight" }, { "ScreenWidth" }, { "ScreenHeight" }, @@ -39,8 +39,10 @@ std::vector settings_dont_save { { "ScreenRotate" }, { "MonitorID" }, { "ExePath" }, - { "FixedCarousel" }, - { "HomePath" } + { "FixedCarousel" }, + { "HomePath" }, + { "MusicDirectory"}, + { "UserMusicDirectory" } }; Settings::Settings() @@ -212,6 +214,22 @@ void Settings::setDefaults() mStringMap["ScrapperImageSrc"] = "box-2D"; mStringMap["ScrapperThumbSrc"] = ""; mBoolMap["ScrapeVideos"] = false; + + mBoolMap["audio.bgmusic"] = true; + mBoolMap["audio.persystem"] = false; + +#ifdef BATOCERA + mStringMap["MusicDirectory"] = "/usr/share/batocera/music"; + mStringMap["UserMusicDirectory"] = "/userdata/music"; +#else +#ifdef RECALBOX + mStringMap["MusicDirectory"] = "/recalbox/share/music/"; + mStringMap["UserMusicDirectory"] = "/recalbox/share/music/"; +#else + mStringMap["MusicDirectory"] = ""; + mStringMap["UserMusicDirectory"] = ""; +#endif +#endif mDefaultBoolMap = mBoolMap; mDefaultIntMap = mIntMap; diff --git a/es-core/src/Sound.cpp b/es-core/src/Sound.cpp index af0b63f903..60f6a233ca 100644 --- a/es-core/src/Sound.cpp +++ b/es-core/src/Sound.cpp @@ -1,5 +1,4 @@ #include "Sound.h" - #include "AudioManager.h" #include "Log.h" #include "Settings.h" @@ -33,7 +32,7 @@ std::shared_ptr Sound::getFromTheme(const std::shared_ptr& the return get(elem->get("path")); } -Sound::Sound(const std::string & path) : mSampleData(NULL), mSamplePos(0), mSampleLength(0), playing(false) +Sound::Sound(const std::string & path) : mSampleData(NULL), mPlaying(false) { loadFile(path); } @@ -51,131 +50,47 @@ void Sound::loadFile(const std::string & path) void Sound::init() { - if(mSampleData != NULL) + if (mSampleData != nullptr) deinit(); - if(mPath.empty()) + if (mPath.empty()) + return; + + if (!Settings::getInstance()->getBool("EnableSounds")) return; //load wav file via SDL - SDL_AudioSpec wave; - Uint8 * data = NULL; - Uint32 dlen = 0; - if (SDL_LoadWAV(mPath.c_str(), &wave, &data, &dlen) == NULL) { + mSampleData = Mix_LoadWAV(mPath.c_str()); + if (mSampleData == nullptr) + { LOG(LogError) << "Error loading sound \"" << mPath << "\"!\n" << " " << SDL_GetError(); return; } - //build conversion buffer - SDL_AudioCVT cvt; - SDL_BuildAudioCVT(&cvt, wave.format, wave.channels, wave.freq, AUDIO_S16, 2, 44100); - //copy data to conversion buffer - cvt.len = dlen; - cvt.buf = new Uint8[cvt.len * cvt.len_mult]; - memcpy(cvt.buf, data, dlen); - //convert buffer to stereo, 16bit, 44.1kHz - if (SDL_ConvertAudio(&cvt) < 0) { - LOG(LogError) << "Error converting sound \"" << mPath << "\" to 44.1kHz, 16bit, stereo format!\n" << " " << SDL_GetError(); - delete[] cvt.buf; - } - else { - //worked. set up member data - SDL_LockAudio(); - mSampleData = cvt.buf; - mSampleLength = cvt.len_cvt; - mSamplePos = 0; - mSampleFormat.channels = 2; - mSampleFormat.freq = 44100; - mSampleFormat.format = AUDIO_S16; - SDL_UnlockAudio(); - } - //free wav data now - SDL_FreeWAV(data); } void Sound::deinit() { - playing = false; + mPlaying = false; - if(mSampleData != NULL) - { - SDL_LockAudio(); - delete[] mSampleData; - mSampleData = NULL; - mSampleLength = 0; - mSamplePos = 0; - SDL_UnlockAudio(); - } + if (mSampleData != nullptr) + Mix_FreeChunk(mSampleData); } void Sound::play() { - if(mSampleData == NULL) + if (mSampleData == nullptr) return; - if(!Settings::getInstance()->getBool("EnableSounds")) - return; - - AudioManager::getInstance(); - - SDL_LockAudio(); - if (playing) - { - //replay from start. rewind the sample to the beginning - mSamplePos = 0; - - } - else - { - //flag our sample as playing - playing = true; - } - SDL_UnlockAudio(); - //tell the AudioManager to start playing samples - AudioManager::getInstance()->play(); + mPlaying = true; + Mix_PlayChannel(-1, mSampleData, 0); } bool Sound::isPlaying() const { - return playing; + return mPlaying; } void Sound::stop() { - //flag our sample as playing and rewind its position - SDL_LockAudio(); - playing = false; - mSamplePos = 0; - SDL_UnlockAudio(); -} - -const Uint8 * Sound::getData() const -{ - return mSampleData; -} - -Uint32 Sound::getPosition() const -{ - return mSamplePos; -} - -void Sound::setPosition(Uint32 newPosition) -{ - mSamplePos = newPosition; - if (mSamplePos >= mSampleLength) { - //got to or beyond the end of the sample. stop playing - playing = false; - mSamplePos = 0; - } -} - -Uint32 Sound::getLength() const -{ - return mSampleLength; -} - -Uint32 Sound::getLengthMS() const -{ - //44100 samples per second, 2 channels (stereo) - //I have no idea why the *0.75 is necessary, but otherwise it's inaccurate - return (Uint32)((mSampleLength / 44100.0f / 2.0f * 0.75f) * 1000); -} + mPlaying = false; +} \ No newline at end of file diff --git a/es-core/src/Sound.h b/es-core/src/Sound.h index d54688d8d8..0d77db5d52 100644 --- a/es-core/src/Sound.h +++ b/es-core/src/Sound.h @@ -2,20 +2,17 @@ #ifndef ES_CORE_SOUND_H #define ES_CORE_SOUND_H -#include "SDL_audio.h" #include #include +#include "SDL_mixer.h" class ThemeData; class Sound { std::string mPath; - SDL_AudioSpec mSampleFormat; - Uint8 * mSampleData; - Uint32 mSamplePos; - Uint32 mSampleLength; - bool playing; + Mix_Chunk* mSampleData; + bool mPlaying; public: static std::shared_ptr get(const std::string& path); @@ -32,12 +29,6 @@ class Sound bool isPlaying() const; void stop(); - const Uint8 * getData() const; - Uint32 getPosition() const; - void setPosition(Uint32 newPosition); - Uint32 getLength() const; - Uint32 getLengthMS() const; - private: Sound(const std::string & path = ""); static std::map< std::string, std::shared_ptr > sMap; diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index cd7c900cb8..7273027962 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -234,6 +234,9 @@ class ThemeData static std::map getThemeSubSets(const std::string& theme); static void setDefaultTheme(ThemeData* theme) { mCurrentTheme = theme; }; + std::string getSystemThemeFolder() { + return mSystemThemeFolder; + } private: static void crawlIncludes(const pugi::xml_node& root, std::map& sets, std::deque& dequepath); @@ -262,7 +265,7 @@ class ThemeData std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); std::string resolvePlaceholders(const char* in); - + std::map mViews; std::string mColorset; diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index d984d698c1..c9754dff82 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -585,6 +585,10 @@ msgstr "VOLUME DU SYSTÈME" msgid "FRONTEND MUSIC" msgstr "MUSIQUE DE FOND" +#: +msgid "ONLY PLAY SYSTEM-SPECIFIC MUSIC FOLDER" +msgstr "JOUER UNIQUEMENT LES MUSIQUES DES SYSTÈMES" + #: msgid "OUTPUT DEVICE" msgstr "SORTIE AUDIO" From d814537b06e70ff200faef2fa0fdb7a16eda0818 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 22 Jul 2019 12:17:19 +0200 Subject: [PATCH 091/336] Theming : renaming selectorColorGradient to selectorColorEnd & colorGradient to colorEnd to be compatible with future retropie changes --- es-app/src/components/TextListComponent.h | 4 ++-- es-app/src/views/SystemView.cpp | 4 ++-- es-core/src/ThemeData.cpp | 10 +++++----- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/es-app/src/components/TextListComponent.h b/es-app/src/components/TextListComponent.h index ae39788b71..6ddcaa5a29 100644 --- a/es-app/src/components/TextListComponent.h +++ b/es-app/src/components/TextListComponent.h @@ -390,8 +390,8 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, c setColor(0, elem->get("primaryColor")); if(elem->has("secondaryColor")) setColor(1, elem->get("secondaryColor")); - if (elem->has("selectorGradientColor")) - setSelectorGradientColor(elem->get("selectorGradientColor")); + if (elem->has("selectorColorEnd")) + setSelectorGradientColor(elem->get("selectorColorEnd")); } setFont(Font::getFromTheme(elem, properties, mFont)); diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 3487d441a2..8edfa91578 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -770,8 +770,8 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) if (elem->has("color")) mCarousel.color = elem->get("color"); - if (elem->has("colorGradient")) - mCarousel.colorGradient = elem->get("colorGradient"); + if (elem->has("colorEnd")) + mCarousel.colorGradient = elem->get("colorEnd"); mCarousel.colorGradientHorz = (elem->has("gradientType") && !(elem->get("type").compare("horizontal"))); diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 7925858ef2..0948ef865d 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -94,7 +94,7 @@ std::map> The { "selectorHeight", FLOAT }, { "selectorOffsetY", FLOAT }, { "selectorColor", COLOR }, - { "selectorGradientColor", COLOR }, + { "selectorColorEnd", COLOR }, { "selectorImagePath", PATH }, { "selectorImageTile", BOOLEAN }, { "selectedColor", COLOR }, @@ -179,7 +179,7 @@ std::map> The { "pos", NORMALIZED_PAIR }, { "origin", NORMALIZED_PAIR }, { "color", COLOR }, - { "colorGradient", COLOR }, + { "colorEnd", COLOR }, { "gradientType", STRING }, { "logoScale", FLOAT }, { "logoRotation", FLOAT }, @@ -195,7 +195,7 @@ std::map> The { "fontSize", FLOAT }, { "separatorColor", COLOR }, { "selectorColor", COLOR }, - { "selectorGradientColor", COLOR }, + { "selectorColorEnd", COLOR }, { "selectedColor", COLOR }, { "color", COLOR } } }, @@ -1112,8 +1112,8 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) Text.selectedColor = elem->get("selectedColor"); if (elem->has("selectorColor")) Text.selectorColor = elem->get("selectorColor"); - if (elem->has("selectorGradientColor")) - Text.selectorGradientColor = elem->get("selectorGradientColor"); + if (elem->has("selectorColorEnd")) + Text.selectorGradientColor = elem->get("selectorColorEnd"); } elem = theme->getElement("menu", "menubutton", "menuButton"); From 8c224184ca01de3f4114962676866286833ae60b Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 24 Jul 2019 11:27:14 +0200 Subject: [PATCH 092/336] Add debian depends on libsdl2-mixer-2.0-0 --- es-app/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 4ff4fdcce9..69df29f9cd 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -158,8 +158,8 @@ SET(CPACK_RESOURCE_FILE README "${CMAKE_CURRENT_SOURCE_DIR}/README.md") SET(CPACK_DEBIAN_PACKAGE_MAINTAINER "Alec Lofquist ") SET(CPACK_DEBIAN_PACKAGE_SECTION "misc") SET(CPACK_DEBIAN_PACKAGE_PRIORITY "extra") -SET(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libsdl2-2.0-0, libfreeimage3, libfreetype6, libcurl3, libasound2") -SET(CPACK_DEBIAN_PACKAGE_BUILDS_DEPENDS "debhelper (>= 8.0.0), cmake, g++ (>= 4.8), libsdl2-dev, libfreeimage-dev, libfreetype6-dev, libcurl4-openssl-dev, libasound2-dev, libgl1-mesa-dev, rapidjson-dev") +SET(CPACK_DEBIAN_PACKAGE_DEPENDS "libc6, libsdl2-2.0-0, libsdl2-mixer-2.0-0, libfreeimage3, libfreetype6, libcurl3, libasound2") +SET(CPACK_DEBIAN_PACKAGE_BUILDS_DEPENDS "debhelper (>= 8.0.0), cmake, g++ (>= 4.8), libsdl2-dev, libsdl2-mixer-dev, libfreeimage-dev, libfreetype6-dev, libcurl4-openssl-dev, libasound2-dev, libgl1-mesa-dev, rapidjson-dev") SET(CPACK_PACKAGE_VENDOR "emulationstation.org") SET(CPACK_PACKAGE_VERSION "2.0.0~rc1") From d22a21e1d146e923b01b61dd9a63697afc4ab1e4 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Fri, 2 Aug 2019 10:19:25 +0200 Subject: [PATCH 093/336] removed harcoded french message --- es-app/src/guis/GuiMetaDataEd.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 37b9cbde00..f4ce75689e 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -370,9 +370,9 @@ void GuiMetaDataEd::close(bool closeAllWindows) { // changes were made, ask if the user wants to save them mWindow->pushGui(new GuiMsgBox(mWindow, - "ENREGISTRER LES MODIFICATIONS ?", - "OUI", [this, closeFunc] { save(); closeFunc(); }, - "NON", closeFunc + _T("SAVE CHANGES ?"), + _T("YES"), [this, closeFunc] { save(); closeFunc(); }, + _T("NO"), closeFunc )); }else{ closeFunc(); From 80163db58f64cce7d39d38ff8c5b5a8b3d995f65 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Fri, 2 Aug 2019 10:31:05 +0200 Subject: [PATCH 094/336] Window : with hide when launch option off, try to activate windows before InputManager::init --- es-core/src/Window.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 3b6759f6fb..5083f52db3 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -72,12 +72,16 @@ GuiComponent* Window::peekGui() bool Window::init(bool initRenderer) { + LOG(LogInfo) << "Window::init"; + if (initRenderer && !Renderer::init()) { LOG(LogError) << "Renderer failed to initialize!"; return false; } - + else + Renderer::activateWindow(); + InputManager::getInstance()->init(); ResourceManager::getInstance()->reloadAll(); @@ -95,9 +99,7 @@ bool Window::init(bool initRenderer) // update our help because font sizes probably changed if (peekGui()) peekGui()->updateHelpPrompts(); - - Renderer::activateWindow(); - + return true; } From 664a878aa6013c44552ef9dd6f351ad441d69b3d Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Fri, 2 Aug 2019 11:24:37 +0200 Subject: [PATCH 095/336] Try to use SDL_RaiseWindow to gain input focus --- es-core/src/Renderer_init_sdlgl.cpp | 597 ++++++++++++++-------------- 1 file changed, 300 insertions(+), 297 deletions(-) diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index 25a930989c..8e02b886fe 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -1,302 +1,305 @@ -#include "Renderer.h" - -#include "resources/ResourceManager.h" -#include "ImageIO.h" -#include "Log.h" -#include "Settings.h" -#include - -#if WIN32 -#include -#endif - -#ifdef USE_OPENGL_ES - #define glOrtho glOrthof -#endif - -namespace Renderer -{ - static bool initialCursorState; - - unsigned int windowWidth = 0; - unsigned int windowHeight = 0; - unsigned int screenWidth = 0; - unsigned int screenHeight = 0; - unsigned int screenOffsetX = 0; - unsigned int screenOffsetY = 0; - unsigned int screenRotate = 0; - - unsigned int getWindowWidth() { return windowWidth; } - unsigned int getWindowHeight() { return windowHeight; } - unsigned int getScreenWidth() { return screenWidth; } - unsigned int getScreenHeight() { return screenHeight; } - unsigned int getScreenOffsetX() { return screenOffsetX; } - unsigned int getScreenOffsetY() { return screenOffsetY; } - unsigned int getScreenRotate() { return screenRotate; } - - SDL_Window* sdlWindow = NULL; - SDL_GLContext sdlContext = NULL; - - Vector2i sdlWindowPosition = Vector2i(SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED); - - - - bool createSurface() - { - LOG(LogInfo) << "Creating surface..."; - - if (SDL_Init(SDL_INIT_VIDEO) != 0) - { - LOG(LogError) << "Error initializing SDL!\n " << SDL_GetError(); - return false; - } - - //hide mouse cursor early - initialCursorState = SDL_ShowCursor(0) == 1; - - SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); - SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16); - SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); - - // multisample anti-aliasing - //SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); - //SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 2); - -#ifdef USE_OPENGL_ES - SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1); -#endif - - - - SDL_DisplayMode dispMode; - SDL_GetDesktopDisplayMode(0, &dispMode); - - windowWidth = Settings::getInstance()->getInt("WindowWidth") ? Settings::getInstance()->getInt("WindowWidth") : dispMode.w; - windowHeight = Settings::getInstance()->getInt("WindowHeight") ? Settings::getInstance()->getInt("WindowHeight") : dispMode.h; - screenWidth = Settings::getInstance()->getInt("ScreenWidth") ? Settings::getInstance()->getInt("ScreenWidth") : windowWidth; - screenHeight = Settings::getInstance()->getInt("ScreenHeight") ? Settings::getInstance()->getInt("ScreenHeight") : windowHeight; - screenOffsetX = Settings::getInstance()->getInt("ScreenOffsetX") ? Settings::getInstance()->getInt("ScreenOffsetX") : 0; - screenOffsetY = Settings::getInstance()->getInt("ScreenOffsetY") ? Settings::getInstance()->getInt("ScreenOffsetY") : 0; - screenRotate = Settings::getInstance()->getInt("ScreenRotate") ? Settings::getInstance()->getInt("ScreenRotate") : 0; - - int monitorId = Settings::getInstance()->getInt("MonitorID"); - if (monitorId >= 0 && sdlWindowPosition == Vector2i(SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED)) - { - int displays = SDL_GetNumVideoDisplays(); - if (displays > monitorId) - { - SDL_Rect rc; - SDL_GetDisplayBounds(monitorId, &rc); - - sdlWindowPosition = Vector2i(rc.x, rc.y); - - if (Settings::getInstance()->getBool("Windowed") && (Settings::getInstance()->getInt("WindowWidth") || Settings::getInstance()->getInt("ScreenWidth"))) - { - if (windowWidth != rc.w || windowHeight != rc.h) - { - sdlWindowPosition = Vector2i( - rc.x + (rc.w - windowWidth) / 2, - rc.y + (rc.h - windowHeight) / 2 - ); - } - } - else - { - windowWidth = rc.w; - windowHeight = rc.h; - screenWidth = rc.w; - screenHeight = rc.h; - } - } - } - - sdlWindow = SDL_CreateWindow("EmulationStation", - sdlWindowPosition.x(), - sdlWindowPosition.y(), - windowWidth, windowHeight, - SDL_WINDOW_OPENGL | (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN)) - ); - - if (sdlWindow == NULL) - { - LOG(LogError) << "Error creating SDL window!\n\t" << SDL_GetError(); - return false; - } - - LOG(LogInfo) << "Created window successfully."; - - //support screen rotation - if((screenRotate == 1) || (screenRotate == 3)) - { - int temp; - temp = windowWidth; - windowWidth = windowHeight; - windowHeight = temp; - temp = screenWidth; - screenWidth = screenHeight; - screenHeight = temp; - temp = screenOffsetX; - screenOffsetX = screenOffsetY; - screenOffsetY = temp; - } - - //set an icon for the window - size_t width = 0; - size_t height = 0; - ResourceData resData = ResourceManager::getInstance()->getFileData(":/window_icon_256.png"); - std::vector rawData = ImageIO::loadFromMemoryRGBA32(resData.ptr.get(), resData.length, width, height); - if (!rawData.empty()) - { - ImageIO::flipPixelsVert(rawData.data(), width, height); - - //SDL interprets each pixel as a 32-bit number, so our masks must depend on the endianness (byte order) of the machine - #if SDL_BYTEORDER == SDL_BIG_ENDIAN - Uint32 rmask = 0xff000000; Uint32 gmask = 0x00ff0000; Uint32 bmask = 0x0000ff00; Uint32 amask = 0x000000ff; - #else - Uint32 rmask = 0x000000ff; Uint32 gmask = 0x0000ff00; Uint32 bmask = 0x00ff0000; Uint32 amask = 0xff000000; - #endif - //try creating SDL surface from logo data - SDL_Surface * logoSurface = SDL_CreateRGBSurfaceFrom((void *)rawData.data(), (int)width, (int)height, 32, (int)(width * 4), rmask, gmask, bmask, amask); - if (logoSurface != NULL) - { - SDL_SetWindowIcon(sdlWindow, logoSurface); - SDL_FreeSurface(logoSurface); - } - } - - sdlContext = SDL_GL_CreateContext(sdlWindow); - - // vsync - if (Settings::getInstance()->getBool("VSync")) - { - // SDL_GL_SetSwapInterval(0) for immediate updates (no vsync, default), - // 1 for updates synchronized with the vertical retrace, - // or -1 for late swap tearing. - // SDL_GL_SetSwapInterval returns 0 on success, -1 on error. - // if vsync is requested, try normal vsync; if that doesn't work, try late swap tearing - // if that doesn't work, report an error - - if (SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) - LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; - } - else - SDL_GL_SetSwapInterval(0); - - return true; - } - - void destroySurface() - { - if (Settings::getInstance()->getBool("Windowed") && Settings::getInstance()->getInt("WindowWidth") && Settings::getInstance()->getInt("WindowHeight")) - { - int x; int y; - SDL_GetWindowPosition(sdlWindow, &x, &y); - sdlWindowPosition = Vector2i(x, y); // Save position to restore it later - } - - SDL_GL_DeleteContext(sdlContext); - sdlContext = NULL; - - SDL_DestroyWindow(sdlWindow); - sdlWindow = NULL; - - //show mouse cursor - SDL_ShowCursor(initialCursorState); - - SDL_Quit(); - } - - - void activateWindow() - { -#if WIN32 - if (sdlWindow == NULL) - return; - +#include "Renderer.h" + +#include "resources/ResourceManager.h" +#include "ImageIO.h" +#include "Log.h" +#include "Settings.h" +#include + +#if WIN32 +#include +#endif + +#ifdef USE_OPENGL_ES + #define glOrtho glOrthof +#endif + +namespace Renderer +{ + static bool initialCursorState; + + unsigned int windowWidth = 0; + unsigned int windowHeight = 0; + unsigned int screenWidth = 0; + unsigned int screenHeight = 0; + unsigned int screenOffsetX = 0; + unsigned int screenOffsetY = 0; + unsigned int screenRotate = 0; + + unsigned int getWindowWidth() { return windowWidth; } + unsigned int getWindowHeight() { return windowHeight; } + unsigned int getScreenWidth() { return screenWidth; } + unsigned int getScreenHeight() { return screenHeight; } + unsigned int getScreenOffsetX() { return screenOffsetX; } + unsigned int getScreenOffsetY() { return screenOffsetY; } + unsigned int getScreenRotate() { return screenRotate; } + + SDL_Window* sdlWindow = NULL; + SDL_GLContext sdlContext = NULL; + + Vector2i sdlWindowPosition = Vector2i(SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED); + + + + bool createSurface() + { + LOG(LogInfo) << "Creating surface..."; + + if (SDL_Init(SDL_INIT_VIDEO) != 0) + { + LOG(LogError) << "Error initializing SDL!\n " << SDL_GetError(); + return false; + } + + //hide mouse cursor early + initialCursorState = SDL_ShowCursor(0) == 1; + + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); + SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 16); + SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1); + + // multisample anti-aliasing + //SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, 1); + //SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 2); + +#ifdef USE_OPENGL_ES + SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1); +#endif + + + + SDL_DisplayMode dispMode; + SDL_GetDesktopDisplayMode(0, &dispMode); + + windowWidth = Settings::getInstance()->getInt("WindowWidth") ? Settings::getInstance()->getInt("WindowWidth") : dispMode.w; + windowHeight = Settings::getInstance()->getInt("WindowHeight") ? Settings::getInstance()->getInt("WindowHeight") : dispMode.h; + screenWidth = Settings::getInstance()->getInt("ScreenWidth") ? Settings::getInstance()->getInt("ScreenWidth") : windowWidth; + screenHeight = Settings::getInstance()->getInt("ScreenHeight") ? Settings::getInstance()->getInt("ScreenHeight") : windowHeight; + screenOffsetX = Settings::getInstance()->getInt("ScreenOffsetX") ? Settings::getInstance()->getInt("ScreenOffsetX") : 0; + screenOffsetY = Settings::getInstance()->getInt("ScreenOffsetY") ? Settings::getInstance()->getInt("ScreenOffsetY") : 0; + screenRotate = Settings::getInstance()->getInt("ScreenRotate") ? Settings::getInstance()->getInt("ScreenRotate") : 0; + + int monitorId = Settings::getInstance()->getInt("MonitorID"); + if (monitorId >= 0 && sdlWindowPosition == Vector2i(SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED)) + { + int displays = SDL_GetNumVideoDisplays(); + if (displays > monitorId) + { + SDL_Rect rc; + SDL_GetDisplayBounds(monitorId, &rc); + + sdlWindowPosition = Vector2i(rc.x, rc.y); + + if (Settings::getInstance()->getBool("Windowed") && (Settings::getInstance()->getInt("WindowWidth") || Settings::getInstance()->getInt("ScreenWidth"))) + { + if (windowWidth != rc.w || windowHeight != rc.h) + { + sdlWindowPosition = Vector2i( + rc.x + (rc.w - windowWidth) / 2, + rc.y + (rc.h - windowHeight) / 2 + ); + } + } + else + { + windowWidth = rc.w; + windowHeight = rc.h; + screenWidth = rc.w; + screenHeight = rc.h; + } + } + } + + sdlWindow = SDL_CreateWindow("EmulationStation", + sdlWindowPosition.x(), + sdlWindowPosition.y(), + windowWidth, windowHeight, + SDL_WINDOW_OPENGL | (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN)) + ); + + if (sdlWindow == NULL) + { + LOG(LogError) << "Error creating SDL window!\n\t" << SDL_GetError(); + return false; + } + + LOG(LogInfo) << "Created window successfully."; + + //support screen rotation + if((screenRotate == 1) || (screenRotate == 3)) + { + int temp; + temp = windowWidth; + windowWidth = windowHeight; + windowHeight = temp; + temp = screenWidth; + screenWidth = screenHeight; + screenHeight = temp; + temp = screenOffsetX; + screenOffsetX = screenOffsetY; + screenOffsetY = temp; + } + + //set an icon for the window + size_t width = 0; + size_t height = 0; + ResourceData resData = ResourceManager::getInstance()->getFileData(":/window_icon_256.png"); + std::vector rawData = ImageIO::loadFromMemoryRGBA32(resData.ptr.get(), resData.length, width, height); + if (!rawData.empty()) + { + ImageIO::flipPixelsVert(rawData.data(), width, height); + + //SDL interprets each pixel as a 32-bit number, so our masks must depend on the endianness (byte order) of the machine + #if SDL_BYTEORDER == SDL_BIG_ENDIAN + Uint32 rmask = 0xff000000; Uint32 gmask = 0x00ff0000; Uint32 bmask = 0x0000ff00; Uint32 amask = 0x000000ff; + #else + Uint32 rmask = 0x000000ff; Uint32 gmask = 0x0000ff00; Uint32 bmask = 0x00ff0000; Uint32 amask = 0xff000000; + #endif + //try creating SDL surface from logo data + SDL_Surface * logoSurface = SDL_CreateRGBSurfaceFrom((void *)rawData.data(), (int)width, (int)height, 32, (int)(width * 4), rmask, gmask, bmask, amask); + if (logoSurface != NULL) + { + SDL_SetWindowIcon(sdlWindow, logoSurface); + SDL_FreeSurface(logoSurface); + } + } + + sdlContext = SDL_GL_CreateContext(sdlWindow); + + // vsync + if (Settings::getInstance()->getBool("VSync")) + { + // SDL_GL_SetSwapInterval(0) for immediate updates (no vsync, default), + // 1 for updates synchronized with the vertical retrace, + // or -1 for late swap tearing. + // SDL_GL_SetSwapInterval returns 0 on success, -1 on error. + // if vsync is requested, try normal vsync; if that doesn't work, try late swap tearing + // if that doesn't work, report an error + + if (SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) + LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; + } + else + SDL_GL_SetSwapInterval(0); + + return true; + } + + void destroySurface() + { + if (Settings::getInstance()->getBool("Windowed") && Settings::getInstance()->getInt("WindowWidth") && Settings::getInstance()->getInt("WindowHeight")) + { + int x; int y; + SDL_GetWindowPosition(sdlWindow, &x, &y); + sdlWindowPosition = Vector2i(x, y); // Save position to restore it later + } + + SDL_GL_DeleteContext(sdlContext); + sdlContext = NULL; + + SDL_DestroyWindow(sdlWindow); + sdlWindow = NULL; + + //show mouse cursor + SDL_ShowCursor(initialCursorState); + + SDL_Quit(); + } + + + void activateWindow() + { + SDL_RaiseWindow(sdlWindow); + SDL_SetWindowInputFocus(sdlWindow); + /* +#if WIN32 + if (sdlWindow == NULL) + return; + SDL_SysWMinfo wmInfo; SDL_VERSION(&wmInfo.version); SDL_GetWindowWMInfo(sdlWindow, &wmInfo); HWND hwnd = wmInfo.info.win.window; - ::SetForegroundWindow(hwnd); - ::SetActiveWindow(hwnd); -#endif - } - - bool init() - { - if(!createSurface()) - return false; - - //gotta flip y since y=0 is at the bottom - switch(screenRotate) - { - case 0: - { - glViewport(screenOffsetX, windowHeight - screenHeight - screenOffsetY, screenWidth, screenHeight); - glMatrixMode(GL_PROJECTION); - glOrtho(0, screenWidth, screenHeight, 0, -1.0, 1.0); - } - break; - - case 1: - { - glViewport(screenOffsetY, windowWidth - screenWidth - screenOffsetX, screenHeight, screenWidth); - glMatrixMode(GL_PROJECTION); - glOrtho(0, screenHeight, screenWidth, 0, -1.0, 1.0); - glRotatef(90, 0, 0, 1); - glTranslatef(0, screenHeight * -1.0f, 0); - } - break; - - case 2: - { - glViewport(screenOffsetX, windowHeight - screenHeight - screenOffsetY, screenWidth, screenHeight); - glMatrixMode(GL_PROJECTION); - glOrtho(0, screenWidth, screenHeight, 0, -1.0, 1.0); - glRotatef(180, 0, 0, 1); - glTranslatef(screenWidth * -1.0f, screenHeight * -1.0f, 0); - } - break; - - case 3: - { - glViewport(screenOffsetY, windowWidth - screenWidth - screenOffsetX, screenHeight, screenWidth); - glMatrixMode(GL_PROJECTION); - glOrtho(0, screenHeight, screenWidth, 0, -1.0, 1.0); - glRotatef(270, 0, 0, 1); - glTranslatef(screenWidth * -1.0f, 0, 0); - } - break; - } - - glMatrixMode(GL_MODELVIEW); - glClearColor(0.0f, 0.0f, 0.0f, 0.0f); - - return true; - } - - void deinit() - { - destroySurface(); - } - - void swapBuffers() - { -#ifdef WIN32 - glFlush(); - glFinish(); - Sleep(0); -#endif - - SDL_GL_SwapWindow(sdlWindow); - -#ifdef WIN32 - Sleep(0); -#endif - - glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); - } -}; + ::SetForegroundWindow(hwnd); + ::SetActiveWindow(hwnd); +#endif*/ + } + + bool init() + { + if(!createSurface()) + return false; + + //gotta flip y since y=0 is at the bottom + switch(screenRotate) + { + case 0: + { + glViewport(screenOffsetX, windowHeight - screenHeight - screenOffsetY, screenWidth, screenHeight); + glMatrixMode(GL_PROJECTION); + glOrtho(0, screenWidth, screenHeight, 0, -1.0, 1.0); + } + break; + + case 1: + { + glViewport(screenOffsetY, windowWidth - screenWidth - screenOffsetX, screenHeight, screenWidth); + glMatrixMode(GL_PROJECTION); + glOrtho(0, screenHeight, screenWidth, 0, -1.0, 1.0); + glRotatef(90, 0, 0, 1); + glTranslatef(0, screenHeight * -1.0f, 0); + } + break; + + case 2: + { + glViewport(screenOffsetX, windowHeight - screenHeight - screenOffsetY, screenWidth, screenHeight); + glMatrixMode(GL_PROJECTION); + glOrtho(0, screenWidth, screenHeight, 0, -1.0, 1.0); + glRotatef(180, 0, 0, 1); + glTranslatef(screenWidth * -1.0f, screenHeight * -1.0f, 0); + } + break; + + case 3: + { + glViewport(screenOffsetY, windowWidth - screenWidth - screenOffsetX, screenHeight, screenWidth); + glMatrixMode(GL_PROJECTION); + glOrtho(0, screenHeight, screenWidth, 0, -1.0, 1.0); + glRotatef(270, 0, 0, 1); + glTranslatef(screenWidth * -1.0f, 0, 0); + } + break; + } + + glMatrixMode(GL_MODELVIEW); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); + + return true; + } + + void deinit() + { + destroySurface(); + } + + void swapBuffers() + { +#ifdef WIN32 + glFlush(); + glFinish(); + Sleep(0); +#endif + + SDL_GL_SwapWindow(sdlWindow); + +#ifdef WIN32 + Sleep(0); +#endif + + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); + } +}; From 7959a878490f0c242b69ee81239e24bb07eca0e2 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Fri, 2 Aug 2019 12:59:08 +0200 Subject: [PATCH 096/336] FileData : try to DeinitRenderer before Init again --- es-app/src/FileData.cpp | 1068 ++++++++++++++++++++------------------- 1 file changed, 536 insertions(+), 532 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 168c3497e7..925909f65d 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -1,332 +1,336 @@ -#include "FileData.h" - -#include "utils/FileSystemUtil.h" -#include "utils/StringUtil.h" -#include "utils/TimeUtil.h" -#include "AudioManager.h" -#include "CollectionSystemManager.h" -#include "FileFilterIndex.h" -#include "FileSorts.h" -#include "Log.h" -#include "MameNames.h" -#include "platform.h" -#include "Scripting.h" -#include "SystemData.h" -#include "VolumeControl.h" -#include "Window.h" -#include "views/UIModeController.h" -#include - -FileData::FileData(FileType type, const std::string& path, SystemData* system) - : mType(type), mSystem(system), mParent(NULL), metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor! -{ - mPath = Utils::FileSystem::createRelativePath(path, getSystemEnvData()->mStartPath, false); - -// TRACE("FileData : " << mPath); - - // metadata needs at least a name field (since that's what getName() will return) - if (metadata.get("name").empty()) - metadata.set("name", getDisplayName()); -} - -const std::string FileData::getPath() const -{ - if (mPath.empty()) - return getSystemEnvData()->mStartPath; - - return Utils::FileSystem::resolveRelativePath(mPath, getSystemEnvData()->mStartPath, true); -} - -inline SystemEnvironmentData* FileData::getSystemEnvData() const -{ - return mSystem->getSystemEnvData(); -} - -std::string FileData::getSystemName() const -{ - return mSystem->getName(); -} - -FileData::~FileData() -{ - if(mParent) - mParent->removeChild(this); - - if(mType == GAME) - mSystem->removeFromIndex(this); -} - -std::string FileData::getDisplayName() const -{ - std::string stem = Utils::FileSystem::getStem(getPath()); - if(mSystem && mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO)) - stem = MameNames::getInstance()->getRealName(stem); - - return stem; -} - -std::string FileData::getCleanName() const -{ - return Utils::String::removeParenthesis(this->getDisplayName()); -} - -const std::string FileData::getThumbnailPath() const -{ - std::string thumbnail = metadata.get("thumbnail"); - - // no thumbnail, try image - if(thumbnail.empty()) - { - thumbnail = metadata.get("image"); - - // no image, try to use local image - if(thumbnail.empty() && Settings::getInstance()->getBool("LocalArt")) - { - const char* extList[2] = { ".png", ".jpg" }; - for(int i = 0; i < 2; i++) - { - if(thumbnail.empty()) - { - std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; - if(Utils::FileSystem::exists(path)) - thumbnail = path; - } - } - } - } - - return thumbnail; -} - -const bool FileData::getFavorite() -{ - return metadata.get("favorite") == "true"; -} - -const bool FileData::getHidden() -{ - return metadata.get("hidden") == "true"; -} - -const bool FileData::getKidGame() -{ - return metadata.get("kidgame") != "false"; -} - -const std::string FileData::getName() -{ - return metadata.getName(); -} - -const std::string FileData::getCore() const -{ - return metadata.get("core"); -} - -const std::string FileData::getEmulator() const -{ - return metadata.get("emulator"); -} - -const std::string FileData::getVideoPath() const -{ - std::string video = metadata.get("video"); - - // no video, try to use local video - if(video.empty() && Settings::getInstance()->getBool("LocalArt")) - { - std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-video.mp4"; - if (Utils::FileSystem::exists(path)) - video = path; - } - - return video; -} - -const std::string FileData::getMarqueePath() const -{ - std::string marquee = metadata.get("marquee"); - - // no marquee, try to use local marquee - if (marquee.empty() && Settings::getInstance()->getBool("LocalArt")) - { - const char* extList[2] = { ".png", ".jpg" }; - for(int i = 0; i < 2; i++) - { - if(marquee.empty()) - { - std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-marquee" + extList[i]; - if(Utils::FileSystem::exists(path)) - marquee = path; - } - } - } - - return marquee; -} - -const std::string FileData::getImagePath() const -{ - std::string image = metadata.get("image"); - - // no image, try to use local image - if(image.empty()) - { - const char* extList[2] = { ".png", ".jpg" }; - for(int i = 0; i < 2; i++) - { - if(image.empty()) - { - std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; - if(Utils::FileSystem::exists(path)) - image = path; - } - } - } - - return image; -} - -std::string FileData::getKey() { - return getFileName(); -} - -const bool FileData::isArcadeAsset() -{ - if (mSystem && (mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO))) - { - const std::string stem = Utils::FileSystem::getStem(getPath()); - return MameNames::getInstance()->isBios(stem) || MameNames::getInstance()->isDevice(stem); - } - - return false; -} - -FileData* FileData::getSourceFileData() -{ - return this; -} - -void FileData::launchGame(Window* window) -{ - LOG(LogInfo) << "Attempting to launch game..."; - - AudioManager::getInstance()->deinit(); - VolumeControl::getInstance()->deinit(); - - bool hideWindow = Settings::getInstance()->getBool("HideWindow"); - window->deinit(hideWindow); - - std::string command = getSystemEnvData()->mLaunchCommand; - - const std::string rom = Utils::FileSystem::getEscapedPath(getPath()); - const std::string basename = Utils::FileSystem::getStem(getPath()); - const std::string rom_raw = Utils::FileSystem::getPreferredPath(getPath()); - - std::string emulator = getEmulator(); - if (emulator.length() == 0) - emulator = getSystemEnvData()->getDefaultEmulator(); - - std::string core = getCore(); - if (core.length() == 0) - core = getSystemEnvData()->getDefaultCore(emulator); - - std::string customCommandLine = getSystemEnvData()->getEmulatorCommandLine(emulator); - if (customCommandLine.length() > 0) - command = customCommandLine; - - command = Utils::String::replace(command, "%EMULATOR%", emulator); - command = Utils::String::replace(command, "%CORE%", core); - - command = Utils::String::replace(command, "%ROM%", rom); - command = Utils::String::replace(command, "%BASENAME%", basename); - command = Utils::String::replace(command, "%ROM_RAW%", rom_raw); - command = Utils::String::replace(command, "%SYSTEM%", getSystemName()); - command = Utils::String::replace(command, "%HOME%", Utils::FileSystem::getHomePath()); - - Scripting::fireEvent("game-start", rom, basename); - - LOG(LogInfo) << " " << command; - - int exitCode = runSystemCommand(command, getDisplayName(), hideWindow ? NULL : window); - if (exitCode != 0) - { - LOG(LogWarning) << "...launch terminated with nonzero exit code " << exitCode << "!"; - } - - Scripting::fireEvent("game-end"); - - window->init(hideWindow); - - VolumeControl::getInstance()->init(); - AudioManager::getInstance()->setSystemName(mSystem->getTheme()->getSystemThemeFolder()); // system-specific music -> automatic Init - window->normalizeNextUpdate(); - - //update number of times the game has been launched - if (exitCode == 0) - { - FileData* gameToUpdate = getSourceFileData(); - - int timesPlayed = gameToUpdate->metadata.getInt("playcount") + 1; - gameToUpdate->metadata.set("playcount", std::to_string(static_cast(timesPlayed))); - - //update last played time - gameToUpdate->metadata.set("lastplayed", Utils::Time::DateTime(Utils::Time::now())); - CollectionSystemManager::get()->refreshCollectionSystems(gameToUpdate); - } - - // music - if (Settings::getInstance()->getBool("audio.bgmusic")) - AudioManager::getInstance()->playRandomMusic(); -} - -CollectionFileData::CollectionFileData(FileData* file, SystemData* system) - : FileData(file->getSourceFileData()->getType(), "", system) -{ - mSourceFileData = file->getSourceFileData(); - mParent = NULL; - metadata = mSourceFileData->metadata; - mDirty = true; -} - -SystemEnvironmentData* CollectionFileData::getSystemEnvData() const -{ - return mSourceFileData->getSystemEnvData(); -} - -const std::string CollectionFileData::getPath() const -{ - return mSourceFileData->getPath(); -} - -std::string CollectionFileData::getSystemName() const -{ - return mSourceFileData->getSystem()->getName(); -} - -CollectionFileData::~CollectionFileData() -{ - // need to remove collection file data at the collection object destructor - if(mParent) - mParent->removeChild(this); - mParent = NULL; -} - -std::string CollectionFileData::getKey() { - return getFullPath(); -} - -FileData* CollectionFileData::getSourceFileData() -{ - return mSourceFileData; -} - -void CollectionFileData::refreshMetadata() -{ - metadata = mSourceFileData->metadata; - mDirty = true; -} - +#include "FileData.h" + +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" +#include "utils/TimeUtil.h" +#include "AudioManager.h" +#include "CollectionSystemManager.h" +#include "FileFilterIndex.h" +#include "FileSorts.h" +#include "Log.h" +#include "MameNames.h" +#include "platform.h" +#include "Scripting.h" +#include "SystemData.h" +#include "VolumeControl.h" +#include "Window.h" +#include "views/UIModeController.h" +#include +#include "Renderer.h" + +FileData::FileData(FileType type, const std::string& path, SystemData* system) + : mType(type), mSystem(system), mParent(NULL), metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor! +{ + mPath = Utils::FileSystem::createRelativePath(path, getSystemEnvData()->mStartPath, false); + +// TRACE("FileData : " << mPath); + + // metadata needs at least a name field (since that's what getName() will return) + if (metadata.get("name").empty()) + metadata.set("name", getDisplayName()); +} + +const std::string FileData::getPath() const +{ + if (mPath.empty()) + return getSystemEnvData()->mStartPath; + + return Utils::FileSystem::resolveRelativePath(mPath, getSystemEnvData()->mStartPath, true); +} + +inline SystemEnvironmentData* FileData::getSystemEnvData() const +{ + return mSystem->getSystemEnvData(); +} + +std::string FileData::getSystemName() const +{ + return mSystem->getName(); +} + +FileData::~FileData() +{ + if(mParent) + mParent->removeChild(this); + + if(mType == GAME) + mSystem->removeFromIndex(this); +} + +std::string FileData::getDisplayName() const +{ + std::string stem = Utils::FileSystem::getStem(getPath()); + if(mSystem && mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO)) + stem = MameNames::getInstance()->getRealName(stem); + + return stem; +} + +std::string FileData::getCleanName() const +{ + return Utils::String::removeParenthesis(this->getDisplayName()); +} + +const std::string FileData::getThumbnailPath() const +{ + std::string thumbnail = metadata.get("thumbnail"); + + // no thumbnail, try image + if(thumbnail.empty()) + { + thumbnail = metadata.get("image"); + + // no image, try to use local image + if(thumbnail.empty() && Settings::getInstance()->getBool("LocalArt")) + { + const char* extList[2] = { ".png", ".jpg" }; + for(int i = 0; i < 2; i++) + { + if(thumbnail.empty()) + { + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; + if(Utils::FileSystem::exists(path)) + thumbnail = path; + } + } + } + } + + return thumbnail; +} + +const bool FileData::getFavorite() +{ + return metadata.get("favorite") == "true"; +} + +const bool FileData::getHidden() +{ + return metadata.get("hidden") == "true"; +} + +const bool FileData::getKidGame() +{ + return metadata.get("kidgame") != "false"; +} + +const std::string FileData::getName() +{ + return metadata.getName(); +} + +const std::string FileData::getCore() const +{ + return metadata.get("core"); +} + +const std::string FileData::getEmulator() const +{ + return metadata.get("emulator"); +} + +const std::string FileData::getVideoPath() const +{ + std::string video = metadata.get("video"); + + // no video, try to use local video + if(video.empty() && Settings::getInstance()->getBool("LocalArt")) + { + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-video.mp4"; + if (Utils::FileSystem::exists(path)) + video = path; + } + + return video; +} + +const std::string FileData::getMarqueePath() const +{ + std::string marquee = metadata.get("marquee"); + + // no marquee, try to use local marquee + if (marquee.empty() && Settings::getInstance()->getBool("LocalArt")) + { + const char* extList[2] = { ".png", ".jpg" }; + for(int i = 0; i < 2; i++) + { + if(marquee.empty()) + { + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-marquee" + extList[i]; + if(Utils::FileSystem::exists(path)) + marquee = path; + } + } + } + + return marquee; +} + +const std::string FileData::getImagePath() const +{ + std::string image = metadata.get("image"); + + // no image, try to use local image + if(image.empty()) + { + const char* extList[2] = { ".png", ".jpg" }; + for(int i = 0; i < 2; i++) + { + if(image.empty()) + { + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; + if(Utils::FileSystem::exists(path)) + image = path; + } + } + } + + return image; +} + +std::string FileData::getKey() { + return getFileName(); +} + +const bool FileData::isArcadeAsset() +{ + if (mSystem && (mSystem->hasPlatformId(PlatformIds::ARCADE) || mSystem->hasPlatformId(PlatformIds::NEOGEO))) + { + const std::string stem = Utils::FileSystem::getStem(getPath()); + return MameNames::getInstance()->isBios(stem) || MameNames::getInstance()->isDevice(stem); + } + + return false; +} + +FileData* FileData::getSourceFileData() +{ + return this; +} + +void FileData::launchGame(Window* window) +{ + LOG(LogInfo) << "Attempting to launch game..."; + + AudioManager::getInstance()->deinit(); + VolumeControl::getInstance()->deinit(); + + bool hideWindow = Settings::getInstance()->getBool("HideWindow"); + window->deinit(hideWindow); + + std::string command = getSystemEnvData()->mLaunchCommand; + + const std::string rom = Utils::FileSystem::getEscapedPath(getPath()); + const std::string basename = Utils::FileSystem::getStem(getPath()); + const std::string rom_raw = Utils::FileSystem::getPreferredPath(getPath()); + + std::string emulator = getEmulator(); + if (emulator.length() == 0) + emulator = getSystemEnvData()->getDefaultEmulator(); + + std::string core = getCore(); + if (core.length() == 0) + core = getSystemEnvData()->getDefaultCore(emulator); + + std::string customCommandLine = getSystemEnvData()->getEmulatorCommandLine(emulator); + if (customCommandLine.length() > 0) + command = customCommandLine; + + command = Utils::String::replace(command, "%EMULATOR%", emulator); + command = Utils::String::replace(command, "%CORE%", core); + + command = Utils::String::replace(command, "%ROM%", rom); + command = Utils::String::replace(command, "%BASENAME%", basename); + command = Utils::String::replace(command, "%ROM_RAW%", rom_raw); + command = Utils::String::replace(command, "%SYSTEM%", getSystemName()); + command = Utils::String::replace(command, "%HOME%", Utils::FileSystem::getHomePath()); + + Scripting::fireEvent("game-start", rom, basename); + + LOG(LogInfo) << " " << command; + + int exitCode = runSystemCommand(command, getDisplayName(), hideWindow ? NULL : window); + if (exitCode != 0) + { + LOG(LogWarning) << "...launch terminated with nonzero exit code " << exitCode << "!"; + } + + Scripting::fireEvent("game-end"); + + if (!hideWindow) + Renderer::deinit(); + + window->init(true); + + VolumeControl::getInstance()->init(); + AudioManager::getInstance()->setSystemName(mSystem->getTheme()->getSystemThemeFolder()); // system-specific music -> automatic Init + window->normalizeNextUpdate(); + + //update number of times the game has been launched + if (exitCode == 0) + { + FileData* gameToUpdate = getSourceFileData(); + + int timesPlayed = gameToUpdate->metadata.getInt("playcount") + 1; + gameToUpdate->metadata.set("playcount", std::to_string(static_cast(timesPlayed))); + + //update last played time + gameToUpdate->metadata.set("lastplayed", Utils::Time::DateTime(Utils::Time::now())); + CollectionSystemManager::get()->refreshCollectionSystems(gameToUpdate); + } + + // music + if (Settings::getInstance()->getBool("audio.bgmusic")) + AudioManager::getInstance()->playRandomMusic(); +} + +CollectionFileData::CollectionFileData(FileData* file, SystemData* system) + : FileData(file->getSourceFileData()->getType(), "", system) +{ + mSourceFileData = file->getSourceFileData(); + mParent = NULL; + metadata = mSourceFileData->metadata; + mDirty = true; +} + +SystemEnvironmentData* CollectionFileData::getSystemEnvData() const +{ + return mSourceFileData->getSystemEnvData(); +} + +const std::string CollectionFileData::getPath() const +{ + return mSourceFileData->getPath(); +} + +std::string CollectionFileData::getSystemName() const +{ + return mSourceFileData->getSystem()->getName(); +} + +CollectionFileData::~CollectionFileData() +{ + // need to remove collection file data at the collection object destructor + if(mParent) + mParent->removeChild(this); + mParent = NULL; +} + +std::string CollectionFileData::getKey() { + return getFullPath(); +} + +FileData* CollectionFileData::getSourceFileData() +{ + return mSourceFileData; +} + +void CollectionFileData::refreshMetadata() +{ + metadata = mSourceFileData->metadata; + mDirty = true; +} + const std::string CollectionFileData::getName() { if (mDirty) { @@ -339,206 +343,206 @@ const std::string CollectionFileData::getName() return mCollectionFileName; return Utils::String::removeParenthesis(mSourceFileData->metadata.get("name")); -} - -// returns Sort Type based on a string description -FolderData::SortType getSortTypeFromString(std::string desc) { - std::vector SortTypes = FileSorts::SortTypes; - // find it - for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) - { - const FolderData::SortType& sort = FileSorts::SortTypes.at(i); - if(sort.description == desc) - { - return sort; - } - } - // if not found default to name, ascending - return FileSorts::SortTypes.at(0); -} - -FileData* FolderData::findUniqueGameForFolder() -{ - std::vector children = getChildren(); - - if (children.size() == 1 && children.at(0)->getType() == GAME) - return children.at(0); - - for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) - { - if ((*it)->getType() == GAME) - return NULL; - - FolderData* folder = (FolderData*)(*it); - FileData* ret = folder->findUniqueGameForFolder(); - if (ret != NULL) - return ret; - } - - return NULL; -} - -const std::vector FolderData::getChildrenListToDisplay() -{ - std::vector ret; - - bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); - bool filterKidGame = false; - - if (!Settings::getInstance()->getBool("ForceDisableFilters")) - { - if (UIModeController::getInstance()->isUIModeKiosk()) - showHiddenFiles = false; - - if (UIModeController::getInstance()->isUIModeKid()) - filterKidGame = true; - } - - auto sys = CollectionSystemManager::get()->getSystemToView(mSystem); - - FileFilterIndex* idx = sys->getIndex(); - if (idx != nullptr && idx->isFiltered()) - { - for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if (idx->showFile((*it))) { - ret.push_back(*it); - } - } - } - else - { - bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); - if (showHiddenFiles) - return mChildren; - - for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if (!showHiddenFiles && (*it)->getHidden()) - continue; - - if (filterKidGame && !(*it)->getKidGame()) - continue; - - ret.push_back(*it); - } - } - - return ret; -} - -std::vector FolderData::getFilesRecursive(unsigned int typeMask, bool displayedOnly) const -{ - std::vector out; - - FileFilterIndex* idx = mSystem->getIndex(); - - for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if ((*it)->getType() & typeMask) - { - if (!displayedOnly || idx == nullptr || !idx->isFiltered() || idx->showFile(*it)) - out.push_back(*it); - } - - if ((*it)->getType() != FOLDER) - continue; - - FolderData* folder = (FolderData*)(*it); - if (folder->getChildren().size() > 0) - { - std::vector subchildren = folder->getFilesRecursive(typeMask, displayedOnly); - out.insert(out.cend(), subchildren.cbegin(), subchildren.cend()); - } - } - - return out; -} - -void FolderData::addChild(FileData* file) -{ - assert(mType == FOLDER); - assert(file->getParent() == NULL); - - const std::string key = file->getKey(); - - mChildren.push_back(file); - file->setParent(this); -} - -void FolderData::removeChild(FileData* file) -{ - assert(mType == FOLDER); - assert(file->getParent() == this); - - for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if (*it == file) - { - file->setParent(NULL); - mChildren.erase(it); - return; - } - } - - // File somehow wasn't in our children. - assert(false); - -} - -void FolderData::sort(ComparisonFunction& comparator, bool ascending) -{ - std::stable_sort(mChildren.begin(), mChildren.end(), comparator); - - for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if ((*it)->getType() != FOLDER) - continue; - - FolderData* folder = (FolderData*)(*it); - - if (folder->getChildren().size() > 0) - folder->sort(comparator, ascending); - } - - if (!ascending) - std::reverse(mChildren.begin(), mChildren.end()); -} - -void FolderData::sort(const SortType& type) -{ - sort(*type.comparisonFunction, type.ascending); -} - -FileData* FolderData::FindByPath(const std::string& path) -{ - std::vector children = getChildren(); - - for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) - { - if ((*it)->getPath() == path) - return (*it); - - if ((*it)->getType() != FOLDER) - continue; - - auto item = ((FolderData*)(*it))->FindByPath(path); - if (item != nullptr) - return item; - } - - return nullptr; -} - -void FolderData::createChildrenByFilenameMap(std::unordered_map& map) -{ - std::vector children = getChildren(); - - for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) - { - if ((*it)->getType() == FOLDER) - ((FolderData*)(*it))->createChildrenByFilenameMap(map); - else - map[(*it)->getKey()] = (*it); - } -} \ No newline at end of file +} + +// returns Sort Type based on a string description +FolderData::SortType getSortTypeFromString(std::string desc) { + std::vector SortTypes = FileSorts::SortTypes; + // find it + for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) + { + const FolderData::SortType& sort = FileSorts::SortTypes.at(i); + if(sort.description == desc) + { + return sort; + } + } + // if not found default to name, ascending + return FileSorts::SortTypes.at(0); +} + +FileData* FolderData::findUniqueGameForFolder() +{ + std::vector children = getChildren(); + + if (children.size() == 1 && children.at(0)->getType() == GAME) + return children.at(0); + + for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + if ((*it)->getType() == GAME) + return NULL; + + FolderData* folder = (FolderData*)(*it); + FileData* ret = folder->findUniqueGameForFolder(); + if (ret != NULL) + return ret; + } + + return NULL; +} + +const std::vector FolderData::getChildrenListToDisplay() +{ + std::vector ret; + + bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + bool filterKidGame = false; + + if (!Settings::getInstance()->getBool("ForceDisableFilters")) + { + if (UIModeController::getInstance()->isUIModeKiosk()) + showHiddenFiles = false; + + if (UIModeController::getInstance()->isUIModeKid()) + filterKidGame = true; + } + + auto sys = CollectionSystemManager::get()->getSystemToView(mSystem); + + FileFilterIndex* idx = sys->getIndex(); + if (idx != nullptr && idx->isFiltered()) + { + for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + if (idx->showFile((*it))) { + ret.push_back(*it); + } + } + } + else + { + bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); + if (showHiddenFiles) + return mChildren; + + for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + if (!showHiddenFiles && (*it)->getHidden()) + continue; + + if (filterKidGame && !(*it)->getKidGame()) + continue; + + ret.push_back(*it); + } + } + + return ret; +} + +std::vector FolderData::getFilesRecursive(unsigned int typeMask, bool displayedOnly) const +{ + std::vector out; + + FileFilterIndex* idx = mSystem->getIndex(); + + for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + if ((*it)->getType() & typeMask) + { + if (!displayedOnly || idx == nullptr || !idx->isFiltered() || idx->showFile(*it)) + out.push_back(*it); + } + + if ((*it)->getType() != FOLDER) + continue; + + FolderData* folder = (FolderData*)(*it); + if (folder->getChildren().size() > 0) + { + std::vector subchildren = folder->getFilesRecursive(typeMask, displayedOnly); + out.insert(out.cend(), subchildren.cbegin(), subchildren.cend()); + } + } + + return out; +} + +void FolderData::addChild(FileData* file) +{ + assert(mType == FOLDER); + assert(file->getParent() == NULL); + + const std::string key = file->getKey(); + + mChildren.push_back(file); + file->setParent(this); +} + +void FolderData::removeChild(FileData* file) +{ + assert(mType == FOLDER); + assert(file->getParent() == this); + + for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + if (*it == file) + { + file->setParent(NULL); + mChildren.erase(it); + return; + } + } + + // File somehow wasn't in our children. + assert(false); + +} + +void FolderData::sort(ComparisonFunction& comparator, bool ascending) +{ + std::stable_sort(mChildren.begin(), mChildren.end(), comparator); + + for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + if ((*it)->getType() != FOLDER) + continue; + + FolderData* folder = (FolderData*)(*it); + + if (folder->getChildren().size() > 0) + folder->sort(comparator, ascending); + } + + if (!ascending) + std::reverse(mChildren.begin(), mChildren.end()); +} + +void FolderData::sort(const SortType& type) +{ + sort(*type.comparisonFunction, type.ascending); +} + +FileData* FolderData::FindByPath(const std::string& path) +{ + std::vector children = getChildren(); + + for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + if ((*it)->getPath() == path) + return (*it); + + if ((*it)->getType() != FOLDER) + continue; + + auto item = ((FolderData*)(*it))->FindByPath(path); + if (item != nullptr) + return item; + } + + return nullptr; +} + +void FolderData::createChildrenByFilenameMap(std::unordered_map& map) +{ + std::vector children = getChildren(); + + for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + if ((*it)->getType() == FOLDER) + ((FolderData*)(*it))->createChildrenByFilenameMap(map); + else + map[(*it)->getKey()] = (*it); + } +} From 06e7032dc7b488fc11518b0bcb1bec5d2f028568 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Fri, 2 Aug 2019 13:37:39 +0200 Subject: [PATCH 097/336] Cancel previous change --- es-app/src/FileData.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 925909f65d..cade4b4925 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -16,7 +16,6 @@ #include "Window.h" #include "views/UIModeController.h" #include -#include "Renderer.h" FileData::FileData(FileType type, const std::string& path, SystemData* system) : mType(type), mSystem(system), mParent(NULL), metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor! @@ -257,10 +256,7 @@ void FileData::launchGame(Window* window) Scripting::fireEvent("game-end"); - if (!hideWindow) - Renderer::deinit(); - - window->init(true); + window->init(hideWindow); VolumeControl::getInstance()->init(); AudioManager::getInstance()->setSystemName(mSystem->getTheme()->getSystemThemeFolder()); // system-specific music -> automatic Init From f2791554ff63e17c62cd7ebe41246400500b49e2 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Fri, 2 Aug 2019 15:16:42 +0200 Subject: [PATCH 098/336] Try another method to reactivate sdl window --- es-core/src/Renderer_init_sdlgl.cpp | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index 8e02b886fe..836c4fd569 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -213,7 +213,7 @@ namespace Renderer { SDL_RaiseWindow(sdlWindow); SDL_SetWindowInputFocus(sdlWindow); - /* + #if WIN32 if (sdlWindow == NULL) return; @@ -221,11 +221,19 @@ namespace Renderer SDL_SysWMinfo wmInfo; SDL_VERSION(&wmInfo.version); SDL_GetWindowWMInfo(sdlWindow, &wmInfo); - HWND hwnd = wmInfo.info.win.window; - - ::SetForegroundWindow(hwnd); - ::SetActiveWindow(hwnd); -#endif*/ + HWND hWnd = wmInfo.info.win.window; + + HWND hCurWnd = ::GetForegroundWindow(); + DWORD dwMyID = ::GetCurrentThreadId(); + DWORD dwCurID = ::GetWindowThreadProcessId(hCurWnd, NULL); + ::AttachThreadInput(dwCurID, dwMyID, TRUE); + ::SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); + ::SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); + ::SetForegroundWindow(hWnd); + ::AttachThreadInput(dwCurID, dwMyID, FALSE); + ::SetFocus(hWnd); + ::SetActiveWindow(hWnd); +#endif } bool init() From fd8ec523693bb0389ecd93cd3d3518b391f95e89 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Wed, 7 Aug 2019 15:56:43 +0200 Subject: [PATCH 099/336] activateWindow is causing problems --- es-core/src/Renderer_init_sdlgl.cpp | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index 836c4fd569..34889d013f 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -211,8 +211,8 @@ namespace Renderer void activateWindow() { - SDL_RaiseWindow(sdlWindow); - SDL_SetWindowInputFocus(sdlWindow); + // SDL_RaiseWindow(sdlWindow); + // SDL_SetWindowInputFocus(sdlWindow); #if WIN32 if (sdlWindow == NULL) @@ -223,15 +223,15 @@ namespace Renderer SDL_GetWindowWMInfo(sdlWindow, &wmInfo); HWND hWnd = wmInfo.info.win.window; - HWND hCurWnd = ::GetForegroundWindow(); - DWORD dwMyID = ::GetCurrentThreadId(); - DWORD dwCurID = ::GetWindowThreadProcessId(hCurWnd, NULL); - ::AttachThreadInput(dwCurID, dwMyID, TRUE); - ::SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); - ::SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); + // HWND hCurWnd = ::GetForegroundWindow(); + // DWORD dwMyID = ::GetCurrentThreadId(); + // DWORD dwCurID = ::GetWindowThreadProcessId(hCurWnd, NULL); + // ::AttachThreadInput(dwCurID, dwMyID, TRUE); + // ::SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); + // ::SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); ::SetForegroundWindow(hWnd); - ::AttachThreadInput(dwCurID, dwMyID, FALSE); - ::SetFocus(hWnd); + // ::AttachThreadInput(dwCurID, dwMyID, FALSE); + // ::SetFocus(hWnd); ::SetActiveWindow(hWnd); #endif } From 75feb8f9c5b14e16502ab98de1ebe1b22ad24159 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 13 Aug 2019 18:18:28 +0200 Subject: [PATCH 100/336] Video view : problem with image sizing when image is not described in theme. Collection : adding localisation on several items. --- es-app/src/CollectionSystemManager.cpp | 14 ++--- es-app/src/guis/GuiGamelistOptions.cpp | 4 +- .../src/views/gamelist/VideoGameListView.cpp | 63 ++++++++++++++----- es-app/src/views/gamelist/VideoGameListView.h | 3 +- es-core/src/Renderer_init_sdlgl.cpp | 31 ++++++--- es-core/src/Window.cpp | 2 +- es-core/src/resources/TextureResource.cpp | 9 +++ es-core/src/resources/TextureResource.h | 2 +- resources/locale/fr/emulationstation2.po | 32 ++++++++++ 9 files changed, 121 insertions(+), 39 deletions(-) diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index af5351ed74..c806c5a391 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -590,12 +590,12 @@ void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) { FolderData* rootFolder = sys->getRootFolder(); - std::string desc = "This collection is empty."; + std::string desc = _T("This collection is empty."); std::string rating = "0"; std::string players = "1"; std::string releasedate = "N/A"; - std::string developer = "None"; - std::string genre = "None"; + std::string developer = _T("None"); + std::string genre = _T("None"); std::string video = ""; std::string thumbnail = ""; std::string image = ""; @@ -620,8 +620,8 @@ void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) rating = (new_rating > rating ? (new_rating != "" ? new_rating : rating) : rating); players = (new_players > players ? (new_players != "" ? new_players : players) : players); releasedate = (new_releasedate < releasedate ? (new_releasedate != "" ? new_releasedate : releasedate) : releasedate); - developer = (developer == "None" ? new_developer : (new_developer != developer ? "Various" : new_developer)); - genre = (genre == "None" ? new_genre : (new_genre != genre ? "Various" : new_genre)); + developer = (developer == _T("None") ? new_developer : (new_developer != developer ? _T("Various") : new_developer)); + genre = (genre == _T("None") ? new_genre : (new_genre != genre ? _T("Various") : new_genre)); switch(games_counter) { @@ -632,11 +632,11 @@ void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) games_list += "'" + file->getName() + "'"; break; case 4: - games_list += " among other titles."; + games_list += " " + _T("among other titles."); } } - desc = "This collection contains " + std::to_string(games_counter) + " games, including " + games_list; + desc = _T("This collection contains") + " " + std::to_string(games_counter) + " " + _T("games, including") + " " + games_list; FileData* randomGame = sys->getRandomGame(); if (randomGame != nullptr) diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index b63004a42e..77b5e23040 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -234,7 +234,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool CollectionSystemManager::get()->getCustomCollectionsBundle()->getName() == system->getName())) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "ADD/REMOVE GAMES TO THIS GAME COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(mWindow, _T("ADD/REMOVE GAMES TO THIS GAME COLLECTION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::startEditMode, this)); mMenu.addRow(row); } @@ -242,7 +242,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool if(UIModeController::getInstance()->isUIModeFull() && CollectionSystemManager::get()->isEditing()) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, "FINISH EDITING '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' COLLECTION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(mWindow, _T("FINISH EDITING")+" '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' "+_T("COLLECTION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::exitEditMode, this)); mMenu.addRow(row); } diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index 17d4f34eaf..c77b4b79fc 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -15,7 +15,7 @@ VideoGameListView::VideoGameListView(Window* window, FolderData* root) : BasicGameListView(window, root), mDescContainer(window), mDescription(window), mMarquee(window), - mImage(window, true), + mImage(nullptr), mVideo(nullptr), mVideoPlaying(false), @@ -50,14 +50,6 @@ VideoGameListView::VideoGameListView(Window* window, FolderData* root) : mMarquee.setDefaultZIndex(35); addChild(&mMarquee); - // Image - mImage.setOrigin(0.5f, 0.5f); - // Default to off the screen - mImage.setPosition(2.0f, 2.0f); - mImage.setMaxSize(1.0f, 1.0f); - mImage.setDefaultZIndex(30); - addChild(&mImage); - // video mVideo->setOrigin(0.5f, 0.5f); mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); @@ -115,7 +107,27 @@ VideoGameListView::VideoGameListView(Window* window, FolderData* root) : VideoGameListView::~VideoGameListView() { - delete mVideo; + if (mImage != nullptr) + delete mImage; + + if (mVideo != nullptr) + delete mVideo; +} + +void VideoGameListView::createImage() +{ + if (mImage != nullptr) + return; + + const float padding = 0.01f; + + // Image + mImage = new ImageComponent(mWindow, true); + mImage->setOrigin(0.5f, 0.5f); + mImage->setPosition(2.0f, 2.0f); + mImage->setMaxSize(100.0f, 100.0f); + mImage->setDefaultZIndex(30); + addChild(mImage); } void VideoGameListView::onThemeChanged(const std::shared_ptr& theme) @@ -124,7 +136,19 @@ void VideoGameListView::onThemeChanged(const std::shared_ptr& theme) using namespace ThemeFlags; mMarquee.applyTheme(theme, getName(), "md_marquee", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); - mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + + if (theme->getElement(getName(), "md_image", "image")) + { + createImage(); + mImage->applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + } + else if (mImage != nullptr) + { + removeChild(mImage); + delete mImage; + mImage = nullptr; + } + mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION); mName.applyTheme(theme, getName(), "md_name", ALL); @@ -254,7 +278,9 @@ void VideoGameListView::updateInfoPanel() mVideo->setImage(file->getThumbnailPath(), false, mVideo->getTargetSize()); mMarquee.setImage(file->getMarqueePath(), false, mMarquee.getSize()); - mImage.setImage(file->getImagePath(), false, mImage.getSize()); + + if (mImage != nullptr) + mImage->setImage(file->getImagePath(), false, mImage->getSize()); mDescription.setText(file->metadata.get("desc")); mDescContainer.reset(); @@ -280,7 +306,10 @@ void VideoGameListView::updateInfoPanel() comps.push_back(&mMarquee); comps.push_back(mVideo); comps.push_back(&mDescription); - comps.push_back(&mImage); + + if (mImage != nullptr) + comps.push_back(mImage); + comps.push_back(&mName); std::vector labels = getMDLabels(); comps.insert(comps.cend(), labels.cbegin(), labels.cend()); @@ -317,11 +346,11 @@ void VideoGameListView::launch(FileData* game) { target = Vector3f(mMarquee.getCenter().x(), mMarquee.getCenter().y(), 0); } - else if(mImage.hasImage() && - (mImage.getPosition().x() < screenWidth && mImage.getPosition().x() > 2.0f && - mImage.getPosition().y() < screenHeight && mImage.getPosition().y() > 2.0f)) + else if(mImage != nullptr && mImage->hasImage() && + (mImage->getPosition().x() < screenWidth && mImage->getPosition().x() > 2.0f && + mImage->getPosition().y() < screenHeight && mImage->getPosition().y() > 2.0f)) { - target = Vector3f(mImage.getCenter().x(), mImage.getCenter().y(), 0); + target = Vector3f(mImage->getCenter().x(), mImage->getCenter().y(), 0); } else if(mHeaderImage.hasImage() && (mHeaderImage.getPosition().x() < screenWidth && mHeaderImage.getPosition().x() > 0.0f && diff --git a/es-app/src/views/gamelist/VideoGameListView.h b/es-app/src/views/gamelist/VideoGameListView.h index 585663a38b..c7bf438d5f 100644 --- a/es-app/src/views/gamelist/VideoGameListView.h +++ b/es-app/src/views/gamelist/VideoGameListView.h @@ -34,13 +34,14 @@ class VideoGameListView : public BasicGameListView private: void updateInfoPanel(); + void createImage(); void initMDLabels(); void initMDValues(); ImageComponent mMarquee; VideoComponent* mVideo; - ImageComponent mImage; + ImageComponent* mImage; TextComponent mLblRating, mLblReleaseDate, mLblDeveloper, mLblPublisher, mLblGenre, mLblPlayers, mLblLastPlayed, mLblPlayCount; diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index 25a930989c..b3e0cb6867 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -209,20 +209,31 @@ namespace Renderer } - void activateWindow() - { -#if WIN32 - if (sdlWindow == NULL) - return; - + void activateWindow() + { + SDL_RaiseWindow(sdlWindow); + SDL_SetWindowInputFocus(sdlWindow); + /* +#if WIN32 + if (sdlWindow == NULL) + return; + SDL_SysWMinfo wmInfo; SDL_VERSION(&wmInfo.version); SDL_GetWindowWMInfo(sdlWindow, &wmInfo); - HWND hwnd = wmInfo.info.win.window; + HWND hWnd = wmInfo.info.win.window; - ::SetForegroundWindow(hwnd); - ::SetActiveWindow(hwnd); -#endif + // HWND hCurWnd = ::GetForegroundWindow(); + // DWORD dwMyID = ::GetCurrentThreadId(); + // DWORD dwCurID = ::GetWindowThreadProcessId(hCurWnd, NULL); + // ::AttachThreadInput(dwCurID, dwMyID, TRUE); + // ::SetWindowPos(hWnd, HWND_TOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); + // ::SetWindowPos(hWnd, HWND_NOTOPMOST, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE); + ::SetForegroundWindow(hWnd); + // ::AttachThreadInput(dwCurID, dwMyID, FALSE); + // ::SetFocus(hWnd); + ::SetActiveWindow(hWnd); +#endif*/ } bool init() diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 3b6759f6fb..914a3c4978 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -376,7 +376,7 @@ void Window::loadCustomImageLoadingScreen(std::string imagePath, std::string cus if (mSplash != NULL) endRenderLoadingScreen(); - mSplash = TextureResource::get(imagePath, false, true, false, false); + mSplash = TextureResource::get(imagePath, false, true, false, false, MaxSizeInfo(Renderer::getScreenWidth() * 0.60f, Renderer::getScreenHeight() * 0.60f)); mCustomSplash = customText; std::shared_ptr& rm = ResourceManager::getInstance(); diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index db3b96d9d9..9595478a42 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -182,7 +182,16 @@ std::shared_ptr TextureResource::get(const std::string& path, b { auto dt = sTextureDataManager.get(rc.get()); if (dt != nullptr) + { dt->setMaxSize(maxSize); + + if (dt->isLoaded() && !dt->isRequiredTextureSizeOk()) + { + dt->releaseVRAM(); + dt->releaseRAM(); + dt->load(); + } + } } return rc; diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index 4582af1598..7ef8decec2 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -22,7 +22,7 @@ class MaxSizeInfo MaxSizeInfo(float x, float y, bool externalZoom) : mSize(Vector2f(x, y)), mExternalZoom(externalZoom), mExternalZoomKnown(true){ } MaxSizeInfo(Vector2f size, bool externalZoom) : mSize(size), mExternalZoom(externalZoom), mExternalZoomKnown(true) { } - bool empty() { return mSize.x() == 0 && mSize.y() == 0; } + bool empty() { return mSize.x() >= 1 && mSize.y() <= 1; } float x() { return mSize.x(); } float y() { return mSize.y(); } diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index c9754dff82..7b040971e5 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -8,6 +8,30 @@ msgstr "" "Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +#: +msgid "This collection is empty" +msgstr "Cette collection est vide" + +#: +msgid "None" +msgstr "Aucun" + +#: +msgid "Various" +msgstr "Multiple" + +#: +msgid "among other titles." +msgstr "d'autres jeux" + +#: +msgid "This collection contains" +msgstr "Cette collection contient" + +#: +msgid "games, including" +msgstr "jeux, incluant " + #: msgid "Loading..." msgstr "Chargement en cours" @@ -16,6 +40,14 @@ msgstr "Chargement en cours" msgid "Preloading UI" msgstr "Chargement de l'interface" +#: +msgid "ADD/REMOVE GAMES TO THIS GAME COLLECTION" +msgstr "Ajouter/Retirer des jeux à cette collection" + +#: +msgid "FINISH EDITING" +msgstr "Terminer l'édition" + #: msgid "GRID SIZE" msgstr "DIMENSIONS DE LA GRILLE" From 9616c69e6a8da36a6a4e5aaf79231b1dc52b59ca Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 13 Aug 2019 18:21:17 +0200 Subject: [PATCH 101/336] Merge intermediate changes --- es-core/src/Renderer_init_sdlgl.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-core/src/Renderer_init_sdlgl.cpp b/es-core/src/Renderer_init_sdlgl.cpp index 69cf23f2fc..f1f1d425ae 100644 --- a/es-core/src/Renderer_init_sdlgl.cpp +++ b/es-core/src/Renderer_init_sdlgl.cpp @@ -225,7 +225,7 @@ namespace Renderer ::SetForegroundWindow(hwnd); ::SetActiveWindow(hwnd); -#endif +#endif*/ } bool init() From 270dbe98901e2d5eec43b10c97355ad638f3da7c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 13 Aug 2019 18:30:09 +0200 Subject: [PATCH 102/336] InputManager : don't deinit on Windows if Hide when launch game is off to avoid joystick redetection --- es-core/src/Window.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 9502d0710e..45e8fabb8f 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -74,15 +74,19 @@ bool Window::init(bool initRenderer) { LOG(LogInfo) << "Window::init"; - if (initRenderer && !Renderer::init()) + if (initRenderer) { - LOG(LogError) << "Renderer failed to initialize!"; - return false; + if (!Renderer::init()) + { + LOG(LogError) << "Renderer failed to initialize!"; + return false; + } + + InputManager::getInstance()->init(); } else Renderer::activateWindow(); - - InputManager::getInstance()->init(); + ResourceManager::getInstance()->reloadAll(); //keep a reference to the default fonts, so they don't keep getting destroyed/recreated @@ -111,7 +115,9 @@ void Window::deinit(bool deinitRenderer) (*i)->onHide(); } - InputManager::getInstance()->deinit(); + if (deinitRenderer) + InputManager::getInstance()->deinit(); + TextureResource::resetCache(); ResourceManager::getInstance()->unloadAll(); From 81d68f7bf2e3c7e44fe10d10624076d4d91bab19 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 13 Aug 2019 19:46:18 +0200 Subject: [PATCH 103/336] Exit menu : option for full exit menu + corrected shutdown actions on windows --- es-app/src/guis/GuiMenu.cpp | 57 +++++++++++++----------- es-app/src/main.cpp | 37 ++++++++++++++- es-core/src/Settings.cpp | 8 +++- es-core/src/platform.cpp | 10 ++++- es-core/src/platform.h | 9 +++- resources/locale/fr/emulationstation2.po | 11 +++++ 6 files changed, 101 insertions(+), 31 deletions(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index da4696ce75..66e7760b14 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -46,11 +46,7 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN addEntry(_T("ADVANCED SETTINGS"), theme->Text.color, true, [this] { openOtherSettings(); }, theme->MenuIcons.advanced); } -#ifdef WIN32 - addEntry(_T("QUIT"), theme->Text.color, false, [this] {openQuitMenu(); }, theme->MenuIcons.quit); -#else - addEntry(_T("QUIT"), theme->Text.color, true, [this] {openQuitMenu(); }, theme->MenuIcons.quit); -#endif + addEntry(_T("QUIT"), theme->Text.color, !Settings::getInstance()->getBool("ShowOnlyExit"), [this] {openQuitMenu(); }, theme->MenuIcons.quit); addChild(&mMenu); addVersionInfo(); @@ -956,6 +952,14 @@ void GuiMenu::openOtherSettings() s->addWithLabel(_T("THREADED LOADING"), threadedLoading); s->addSaveFunc([threadedLoading] { Settings::getInstance()->setBool("ThreadedLoading", threadedLoading->getState()); }); +#ifndef _RPI_ + // full exit + auto fullExitMenu = std::make_shared(mWindow); + fullExitMenu->setState(!Settings::getInstance()->getBool("ShowOnlyExit")); + s->addWithLabel(_T("COMPLETE QUIT MENU"), fullExitMenu); + s->addSaveFunc([fullExitMenu] { Settings::getInstance()->setBool("ShowOnlyExit", !fullExitMenu->getState()); }); +#endif + s->updatePosition(); mWindow->pushGui(s); @@ -976,11 +980,12 @@ void GuiMenu::openConfigInput() void GuiMenu::openQuitMenu() { -#ifdef WIN32 - Scripting::fireEvent("quit"); - quitES(""); - return; -#endif + if (Settings::getInstance()->getBool("ShowOnlyExit")) + { + Scripting::fireEvent("quit"); + quitES(); + return; + } auto s = new GuiSettings(mWindow, _T("QUIT")); @@ -990,54 +995,54 @@ void GuiMenu::openQuitMenu() if (UIModeController::getInstance()->isUIModeFull()) { row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + window->pushGui(new GuiMsgBox(window, _T("REALLY RESTART?"), _T("YES"), [] { Scripting::fireEvent("quit"); - if(quitES("/tmp/es-restart") != 0) + if(quitES(SDL_MSG_RESTART) != 0) LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); + }, _T("NO"), nullptr)); }); - row.addElement(std::make_shared(window, "RESTART EMULATIONSTATION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(window, _T("RESTART EMULATIONSTATION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); if(Settings::getInstance()->getBool("ShowExit")) { row.elements.clear(); row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY QUIT?", "YES", + window->pushGui(new GuiMsgBox(window, _T("REALLY QUIT?"), _T("YES"), [] { Scripting::fireEvent("quit"); - quitES(""); - }, "NO", nullptr)); + quitES(); + }, _T("NO"), nullptr)); }); - row.addElement(std::make_shared(window, "QUIT EMULATIONSTATION", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(window, _T("QUIT EMULATIONSTATION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); } } row.elements.clear(); row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY RESTART?", "YES", + window->pushGui(new GuiMsgBox(window, _T("REALLY RESTART?"), _T("YES"), [] { Scripting::fireEvent("quit", "reboot"); Scripting::fireEvent("reboot"); - if (quitES("/tmp/es-sysrestart") != 0) + if (quitES(SDL_MSG_REBOOT) != 0) LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, "NO", nullptr)); + }, _T("NO"), nullptr)); }); - row.addElement(std::make_shared(window, "RESTART SYSTEM", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(window, _T("RESTART SYSTEM"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); row.elements.clear(); row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, "REALLY SHUTDOWN?", "YES", + window->pushGui(new GuiMsgBox(window, _T("REALLY SHUTDOWN?"), _T("YES"), [] { Scripting::fireEvent("quit", "shutdown"); Scripting::fireEvent("shutdown"); - if (quitES("/tmp/es-shutdown") != 0) + if (quitES(SDL_MSG_SHUTDOWN) != 0) LOG(LogWarning) << "Shutdown terminated with non-zero result!"; - }, "NO", nullptr)); + }, _T("NO"), nullptr)); }); - row.addElement(std::make_shared(window, "SHUTDOWN SYSTEM", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(window, _T("SHUTDOWN SYSTEM"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); s->updatePosition(); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 2cf5495fc2..3a6829b8a2 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -463,6 +463,7 @@ int main(int argc, char* argv[]) int lastTime = SDL_GetTicks(); int ps_time = SDL_GetTicks(); + int exitMode = 0; bool running = true; @@ -479,8 +480,24 @@ int main(int argc, char* argv[]) { InputManager::getInstance()->parseEvent(event, &window); - if (event.type == SDL_QUIT) + switch (event.type) + { + case SDL_QUIT: + running = false; + break; + case SDL_QUIT | SDL_MSG_REBOOT: + running = false; + exitMode = SDL_MSG_REBOOT; + break; + case SDL_QUIT | SDL_MSG_SHUTDOWN: running = false; + exitMode = SDL_MSG_SHUTDOWN; + break; + case SDL_QUIT | SDL_MSG_RESTART: + running = false; + exitMode = SDL_MSG_RESTART; + break; + } } while(SDL_PollEvent(&event)); @@ -558,6 +575,24 @@ int main(int argc, char* argv[]) FreeImage_DeInitialise(); #endif + if (exitMode == SDL_MSG_RESTART) + { + LOG(LogInfo) << "Restarting EmulationStation"; + touch("/tmp/es-restart"); + } + else if (exitMode == SDL_MSG_REBOOT) + { + LOG(LogInfo) << "Rebooting system"; + touch("/tmp/es-sysrestart"); + runRestartCommand(); + } + else if (exitMode == SDL_MSG_SHUTDOWN) + { + LOG(LogInfo) << "Shutting system down"; + touch("/tmp/es-shutdown"); + runShutdownCommand(); + } + LOG(LogInfo) << "EmulationStation cleanly shutting down."; return 0; diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index e2a104d20a..8d665a5879 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -75,11 +75,16 @@ void Settings::setDefaults() mBoolMap["ShowHiddenFiles"] = false; mBoolMap["DrawFramerate"] = false; mBoolMap["ShowExit"] = true; + + #if WIN32 + mBoolMap["ShowOnlyExit"] = true; mBoolMap["FullscreenBorderless"] = true; #else + mBoolMap["ShowOnlyExit"] = false; mBoolMap["FullscreenBorderless"] = false; #endif + mBoolMap["Windowed"] = false; mBoolMap["SplashScreen"] = true; mBoolMap["SplashScreenProgress"] = true; @@ -99,8 +104,7 @@ void Settings::setDefaults() mBoolMap["MoveCarousel"] = true; mBoolMap["SaveGamelistsOnExit"] = true; mBoolMap["OptimizeVRAM"] = true; - mBoolMap["ThreadedLoading"] = true; - + mBoolMap["ThreadedLoading"] = true; mBoolMap["Debug"] = false; mBoolMap["DebugGrid"] = false; diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index 6031976567..661b919898 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -191,6 +191,14 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo #endif } +int quitES(int mode) +{ + SDL_Event *quit = new SDL_Event(); + quit->type = SDL_QUIT | mode; + SDL_PushEvent(quit); + return 0; +} +/* int quitES(const std::string& filename) { if (!filename.empty()) @@ -200,7 +208,7 @@ int quitES(const std::string& filename) SDL_PushEvent(quit); return 0; } - +*/ void touch(const std::string& filename) { #ifdef WIN32 diff --git a/es-core/src/platform.h b/es-core/src/platform.h index f677d1cbaa..05dd3cf4e6 100644 --- a/es-core/src/platform.h +++ b/es-core/src/platform.h @@ -25,10 +25,17 @@ class Window; +#define SDL_MSG_RESTART 0X8000 +#define SDL_MSG_SHUTDOWN 0X4000 +#define SDL_MSG_REBOOT 0x2000 + int runShutdownCommand(); // shut down the system (returns 0 if successful) int runRestartCommand(); // restart the system (returns 0 if successful) int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window); // run a utf-8 encoded in the shell (requires wstring conversion on Windows) -int quitES(const std::string& filename); + +int quitES(int mode = 0); + +//int quitES(const std::string& filename); void touch(const std::string& filename); #if defined(WIN32) diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index 7b040971e5..edf08da119 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -701,6 +701,14 @@ msgstr "SCRAPPER MAINTENANT" msgid "QUIT" msgstr "QUITTER" +#: +msgid "QUIT EMULATIONSTATION" +msgstr "QUITTER EMULATIONSTATION" + +#: +msgid "RESTART EMULATIONSTATION" +msgstr "REDEMARRER EMULATIONSTATION" + #: msgid "REALLY RESTART?" msgstr "REDÉMARRER ?" @@ -1966,6 +1974,9 @@ msgstr "Verification" msgid "EMPTY LIST" msgstr "Liste vide" +#: +msgid "COMPLETE QUIT MENU" +msgstr "AFFICHER TOUTES LES COMMANDES DU MENU QUITTER" #: Retroarch ratio msgid "Auto" From a5d4e068263dd79e2fa251a88960a3ab610eb3c3 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 16 Aug 2019 01:09:23 +0200 Subject: [PATCH 104/336] Localisation : Support for plurals + using _() macro to unify calls --- .gitignore | 3 +- CMakeLists.txt | 19 +- es-app/src/CollectionSystemManager.cpp | 14 +- es-app/src/EmulationStation.h | 8 +- es-app/src/SystemData.cpp | 4 +- .../src/components/ScraperSearchComponent.cpp | 18 +- .../src/guis/GuiCollectionSystemsOptions.cpp | 32 +-- es-app/src/guis/GuiGameScraper.cpp | 4 +- es-app/src/guis/GuiGamelistFilter.cpp | 8 +- es-app/src/guis/GuiGamelistOptions.cpp | 34 +-- es-app/src/guis/GuiMenu.cpp | 259 +++++++++--------- es-app/src/guis/GuiMetaDataEd.cpp | 38 +-- es-app/src/guis/GuiScraperMulti.cpp | 2 +- es-app/src/guis/GuiScraperStart.cpp | 30 +- es-app/src/guis/GuiScreensaverOptions.cpp | 6 +- es-app/src/guis/GuiSettings.cpp | 25 +- es-app/src/guis/GuiSettings.h | 13 +- es-app/src/main.cpp | 6 +- es-app/src/views/SystemView.cpp | 18 +- es-app/src/views/ViewController.cpp | 6 +- .../src/views/gamelist/BasicGameListView.cpp | 12 +- .../views/gamelist/DetailedGameListView.cpp | 18 +- .../src/views/gamelist/GridGameListView.cpp | 12 +- es-core/src/EsLocale.cpp | 150 ++++++++-- es-core/src/EsLocale.h | 17 +- es-core/src/Window.cpp | 2 +- es-core/src/components/BusyComponent.cpp | 2 +- es-core/src/components/ComponentGrid.cpp | 6 +- es-core/src/components/ComponentList.cpp | 2 +- es-core/src/components/DateTimeComponent.cpp | 4 +- .../src/components/DateTimeEditComponent.cpp | 4 +- es-core/src/components/OptionListComponent.h | 8 +- es-core/src/components/SliderComponent.cpp | 2 +- es-core/src/guis/GuiMsgBox.cpp | 2 +- es-core/src/guis/GuiTextEditPopup.cpp | 4 +- es-core/src/guis/GuiTextEditPopupKeyboard.cpp | 18 +- es-core/src/utils/StringUtil.cpp | 4 +- 37 files changed, 494 insertions(+), 320 deletions(-) diff --git a/.gitignore b/.gitignore index 19593e251e..ec05c29b64 100644 --- a/.gitignore +++ b/.gitignore @@ -2,7 +2,8 @@ *.slo *.lo *.o - +*.mo + # Compiled Dynamic libraries *.so diff --git a/CMakeLists.txt b/CMakeLists.txt index e2e2d69847..89f4c990b2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,6 +62,11 @@ find_package(VLC REQUIRED) find_package(RapidJSON REQUIRED) find_package(libCEC) +# i18n +# if(MSVC) +# find_package(Intl REQUIRED) +# endif() + #add ALSA for Linux if(${CMAKE_SYSTEM_NAME} MATCHES "Linux") find_package(ALSA REQUIRED) @@ -129,6 +134,12 @@ set(COMMON_INCLUDE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/es-core/src ) +# if(MSVC) +# LIST(APPEND COMMON_INCLUDE_DIRS +# ${Intl_INCLUDE_DIRS} +# ) +# endif() + #add libCEC_INCLUDE_DIR if(DEFINED libCEC_FOUND) LIST(APPEND COMMON_INCLUDE_DIRS @@ -181,7 +192,7 @@ endif() set(COMMON_LIBRARIES ${FREETYPE_LIBRARIES} - ${FreeImage_LIBRARIES} + ${FreeImage_LIBRARIES} ${SDL2_LIBRARY} ${SDLMIXER_LIBRARY} ${CURL_LIBRARIES} @@ -190,6 +201,12 @@ set(COMMON_LIBRARIES nanosvg ) +# if(MSVC) +# LIST(APPEND COMMON_LIBRARIES +# ${Intl_LIBRARIES} +# ) +# endif() + #add libCEC_LIBRARIES if(DEFINED libCEC_FOUND) if(DEFINED BCMHOST) diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index c806c5a391..75b4291a77 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -590,12 +590,12 @@ void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) { FolderData* rootFolder = sys->getRootFolder(); - std::string desc = _T("This collection is empty."); + std::string desc = _("This collection is empty."); std::string rating = "0"; std::string players = "1"; std::string releasedate = "N/A"; - std::string developer = _T("None"); - std::string genre = _T("None"); + std::string developer = _("None"); + std::string genre = _("None"); std::string video = ""; std::string thumbnail = ""; std::string image = ""; @@ -620,8 +620,8 @@ void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) rating = (new_rating > rating ? (new_rating != "" ? new_rating : rating) : rating); players = (new_players > players ? (new_players != "" ? new_players : players) : players); releasedate = (new_releasedate < releasedate ? (new_releasedate != "" ? new_releasedate : releasedate) : releasedate); - developer = (developer == _T("None") ? new_developer : (new_developer != developer ? _T("Various") : new_developer)); - genre = (genre == _T("None") ? new_genre : (new_genre != genre ? _T("Various") : new_genre)); + developer = (developer == _("None") ? new_developer : (new_developer != developer ? _("Various") : new_developer)); + genre = (genre == _("None") ? new_genre : (new_genre != genre ? _("Various") : new_genre)); switch(games_counter) { @@ -632,11 +632,11 @@ void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) games_list += "'" + file->getName() + "'"; break; case 4: - games_list += " " + _T("among other titles."); + games_list += " " + _("among other titles."); } } - desc = _T("This collection contains") + " " + std::to_string(games_counter) + " " + _T("games, including") + " " + games_list; + desc = _("This collection contains") + " " + std::to_string(games_counter) + " " + _("games, including") + " " + games_list; FileData* randomGame = sys->getRandomGame(); if (randomGame != nullptr) diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index 5bb5922f1c..71b3850a9e 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -7,14 +7,14 @@ #define PROGRAM_VERSION_MAJOR 2 #define PROGRAM_VERSION_MINOR 10 #define PROGRAM_VERSION_MAINTENANCE 0 -#define PROGRAM_VERSION_BUILD 5 +#define PROGRAM_VERSION_BUILD 6 -#define PROGRAM_VERSION_STRING "2.10.0.5 FCAMOD" -#define SCRAPER_VERSION_STRING "2.10.0.5" +#define PROGRAM_VERSION_STRING "2.10.0.6 FCAMOD" +#define SCRAPER_VERSION_STRING "2.10.0.6" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ -#define RESOURCE_VERSION_STRING "2,10,0,5\0" +#define RESOURCE_VERSION_STRING "2,10,0,6\0" #define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE,PROGRAM_VERSION_BUILD #endif // ES_APP_EMULATION_STATION_H diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 9e4d71660b..ac21243a3f 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -476,14 +476,14 @@ bool SystemData::loadConfig(Window* window) delete pThreadPool; if (window != NULL) - window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + window->renderLoadingScreen(_("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); CollectionSystemManager::get()->updateSystemsList(); } else { if (window != NULL) - window->renderLoadingScreen(_T("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + window->renderLoadingScreen(_("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); CollectionSystemManager::get()->loadCollectionSystems(); } diff --git a/es-app/src/components/ScraperSearchComponent.cpp b/es-app/src/components/ScraperSearchComponent.cpp index 235cd74ac6..7ae149115d 100644 --- a/es-app/src/components/ScraperSearchComponent.cpp +++ b/es-app/src/components/ScraperSearchComponent.cpp @@ -55,12 +55,12 @@ ScraperSearchComponent::ScraperSearchComponent(Window* window, SearchType type) mMD_Genre = std::make_shared(mWindow, "", font, mdColor); mMD_Players = std::make_shared(mWindow, "", font, mdColor); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _T("Rating") + ":", font, mdLblColor), mMD_Rating, false)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _T("Released") + ":", font, mdLblColor), mMD_ReleaseDate)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _T("Developer") + ":", font, mdLblColor), mMD_Developer)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _T("Publisher") + ":", font, mdLblColor), mMD_Publisher)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _T("Genre") + ":", font, mdLblColor), mMD_Genre)); - mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _T("Players") + ":", font, mdLblColor), mMD_Players)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _("Rating") + ":", font, mdLblColor), mMD_Rating, false)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _("Released") + ":", font, mdLblColor), mMD_ReleaseDate)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _("Developer") + ":", font, mdLblColor), mMD_Developer)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _("Publisher") + ":", font, mdLblColor), mMD_Publisher)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _("Genre") + ":", font, mdLblColor), mMD_Genre)); + mMD_Pairs.push_back(MetaDataPair(std::make_shared(mWindow, _("Players") + ":", font, mdLblColor), mMD_Players)); mMD_Grid = std::make_shared(mWindow, Vector2i(2, (int)mMD_Pairs.size()*2 - 1)); unsigned int i = 0; @@ -240,12 +240,12 @@ void ScraperSearchComponent::onSearchDone(const std::vector if (!isValidConfiguredScraper()) { mWindow->pushGui(new GuiMsgBox(mWindow, Utils::String::toUpper("Configured scraper is no longer available.\nPlease change the scraping source in the settings."), - _T("FINISH"), mSkipCallback)); + _("FINISH"), mSkipCallback)); } else { ComponentListRow row; - row.addElement(std::make_shared(mWindow, _T("NO GAMES FOUND - SKIP"), font, color), true); + row.addElement(std::make_shared(mWindow, _("NO GAMES FOUND - SKIP"), font, color), true); if(mSkipCallback) row.makeAcceptInputHandler(mSkipCallback); @@ -471,7 +471,7 @@ std::vector ScraperSearchComponent::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); if(getSelectedIndex() != -1) - prompts.push_back(HelpPrompt("a", _T("accept result"))); + prompts.push_back(HelpPrompt("a", _("accept result"))); return prompts; } diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index 5e7e6c0b50..5f3036d366 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -9,7 +9,7 @@ #include "CollectionSystemManager.h" #include "Window.h" -GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) : GuiComponent(window), mMenu(window, _T("GAME COLLECTION SETTINGS")) +GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) : GuiComponent(window), mMenu(window, _("GAME COLLECTION SETTINGS")) { initializeMenu(); } @@ -27,9 +27,9 @@ void GuiCollectionSystemsOptions::initializeMenu() std::vector unusedFolders = CollectionSystemManager::get()->getUnusedSystemsFromTheme(); if (unusedFolders.size() > 0) { - addEntry(_T("CREATE NEW CUSTOM COLLECTION FROM THEME"), ThemeData::getMenuTheme()->Text.color, true, + addEntry(_("CREATE NEW CUSTOM COLLECTION FROM THEME"), ThemeData::getMenuTheme()->Text.color, true, [this, unusedFolders] { - auto s = new GuiSettings(mWindow, _T("SELECT THEME FOLDER")); + auto s = new GuiSettings(mWindow, _("SELECT THEME FOLDER")); std::shared_ptr< OptionListComponent > folderThemes = std::make_shared< OptionListComponent >(mWindow, "SELECT THEME FOLDER", true); // add Custom Systems @@ -54,7 +54,7 @@ void GuiCollectionSystemsOptions::initializeMenu() } ComponentListRow row; - row.addElement(std::make_shared(mWindow, _T("CREATE NEW CUSTOM COLLECTION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(mWindow, _("CREATE NEW CUSTOM COLLECTION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); auto createCustomCollection = [this](const std::string& newVal) { std::string name = newVal; // we need to store the first Gui and remove it, as it'll be deleted by the actual Gui @@ -64,36 +64,36 @@ void GuiCollectionSystemsOptions::initializeMenu() createCollection(name); }; row.makeAcceptInputHandler([this, createCustomCollection] { - mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, _T("New Collection Name"), "", createCustomCollection, false)); + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, _("New Collection Name"), "", createCustomCollection, false)); }); mMenu.addRow(row); bundleCustomCollections = std::make_shared(mWindow); bundleCustomCollections->setState(Settings::getInstance()->getBool("UseCustomCollectionsSystem")); - mMenu.addWithLabel(_T("GROUP UNTHEMED CUSTOM COLLECTIONS"), bundleCustomCollections); + mMenu.addWithLabel(_("GROUP UNTHEMED CUSTOM COLLECTIONS"), bundleCustomCollections); sortAllSystemsSwitch = std::make_shared(mWindow); sortAllSystemsSwitch->setState(Settings::getInstance()->getBool("SortAllSystems")); - mMenu.addWithLabel(_T("SORT CUSTOM COLLECTIONS AND SYSTEMS"), sortAllSystemsSwitch); + mMenu.addWithLabel(_("SORT CUSTOM COLLECTIONS AND SYSTEMS"), sortAllSystemsSwitch); favoritesFirstSwitch = std::make_shared(mWindow); favoritesFirstSwitch->setState(Settings::getInstance()->getBool("FavoritesFirst")); - mMenu.addWithLabel(_T("DISPLAY FAVORITES FIRST IN GAMELIST"), favoritesFirstSwitch); + mMenu.addWithLabel(_("DISPLAY FAVORITES FIRST IN GAMELIST"), favoritesFirstSwitch); toggleSystemNameInCollections = std::make_shared(mWindow); toggleSystemNameInCollections->setState(Settings::getInstance()->getBool("CollectionShowSystemInfo")); - mMenu.addWithLabel(_T("SHOW SYSTEM NAME IN COLLECTIONS"), toggleSystemNameInCollections); + mMenu.addWithLabel(_("SHOW SYSTEM NAME IN COLLECTIONS"), toggleSystemNameInCollections); if(CollectionSystemManager::get()->isEditing()) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("FINISH EDITING COLLECTION")+" '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "'", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(mWindow, _("FINISH EDITING COLLECTION")+" '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "'", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiCollectionSystemsOptions::exitEditMode, this)); mMenu.addRow(row); } - mMenu.addButton(_T("BACK"), _T("BACK"), std::bind(&GuiCollectionSystemsOptions::applySettings, this)); + mMenu.addButton(_("BACK"), _("BACK"), std::bind(&GuiCollectionSystemsOptions::applySettings, this)); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); } @@ -151,25 +151,25 @@ void GuiCollectionSystemsOptions::addSystemsToMenu() std::map autoSystems = CollectionSystemManager::get()->getAutoCollectionSystems(); - autoOptionList = std::make_shared< OptionListComponent >(mWindow, _T("SELECT COLLECTIONS"), true); + autoOptionList = std::make_shared< OptionListComponent >(mWindow, _("SELECT COLLECTIONS"), true); // add Auto Systems for(std::map::const_iterator it = autoSystems.cbegin() ; it != autoSystems.cend() ; it++ ) { autoOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); } - mMenu.addWithLabel(_T("AUTOMATIC GAME COLLECTIONS"), autoOptionList); + mMenu.addWithLabel(_("AUTOMATIC GAME COLLECTIONS"), autoOptionList); std::map customSystems = CollectionSystemManager::get()->getCustomCollectionSystems(); - customOptionList = std::make_shared< OptionListComponent >(mWindow, _T("SELECT COLLECTIONS"), true); + customOptionList = std::make_shared< OptionListComponent >(mWindow, _("SELECT COLLECTIONS"), true); // add Custom Systems for(std::map::const_iterator it = customSystems.cbegin() ; it != customSystems.cend() ; it++ ) { customOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); } - mMenu.addWithLabel(_T("CUSTOM GAME COLLECTIONS"), customOptionList); + mMenu.addWithLabel(_("CUSTOM GAME COLLECTIONS"), customOptionList); } void GuiCollectionSystemsOptions::applySettings() @@ -233,6 +233,6 @@ bool GuiCollectionSystemsOptions::input(InputConfig* config, Input input) std::vector GuiCollectionSystemsOptions::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("b", _("BACK"))); return prompts; } diff --git a/es-app/src/guis/GuiGameScraper.cpp b/es-app/src/guis/GuiGameScraper.cpp index 3e39e14e86..2ba3b0e5ef 100644 --- a/es-app/src/guis/GuiGameScraper.cpp +++ b/es-app/src/guis/GuiGameScraper.cpp @@ -43,11 +43,11 @@ GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std:: // buttons std::vector< std::shared_ptr > buttons; - buttons.push_back(std::make_shared(mWindow, _T("INPUT"), _T("search"), [&] { + buttons.push_back(std::make_shared(mWindow, _("INPUT"), _("search"), [&] { mSearch->openInputScreen(mSearchParams); mGrid.resetCursor(); })); - buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); + buttons.push_back(std::make_shared(mWindow, _("CANCEL"), _("CANCEL"), [&] { delete this; })); mButtonGrid = makeButtonGrid(mWindow, buttons); mGrid.setEntry(mButtonGrid, Vector2i(0, 6), true, false); diff --git a/es-app/src/guis/GuiGamelistFilter.cpp b/es-app/src/guis/GuiGamelistFilter.cpp index 95598630f9..ec2aad4347 100644 --- a/es-app/src/guis/GuiGamelistFilter.cpp +++ b/es-app/src/guis/GuiGamelistFilter.cpp @@ -21,14 +21,14 @@ void GuiGamelistFilter::initializeMenu() // show filtered menu row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("RESET ALL FILTERS"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(mWindow, _("RESET ALL FILTERS"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistFilter::resetAllFilters, this)); mMenu.addRow(row); row.elements.clear(); addFiltersToMenu(); - mMenu.addButton(_T("BACK"), _T("BACK"), std::bind(&GuiGamelistFilter::applyFilters, this)); + mMenu.addButton(_("BACK"), _("BACK"), std::bind(&GuiGamelistFilter::applyFilters, this)); mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); } @@ -61,7 +61,7 @@ void GuiGamelistFilter::addFiltersToMenu() FilterIndexType type = (*it).type; // type of filter std::map* allKeys = (*it).allIndexKeys; // all possible filters for this type - std::string menuLabel = _T((*it).menuLabel); // text to show in menu + std::string menuLabel = _((*it).menuLabel); // text to show in menu std::shared_ptr< OptionListComponent > optionList; // add filters (with first one selected) @@ -111,6 +111,6 @@ bool GuiGamelistFilter::input(InputConfig* config, Input input) std::vector GuiGamelistFilter::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("b", _("BACK"))); return prompts; } diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 77b5e23040..f52eb0ac2b 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -91,7 +91,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool if(curChar < startChar || curChar > endChar) curChar = startChar; - mJumpToLetterList = std::make_shared(mWindow, _T("JUMP TO LETTER"), false); + mJumpToLetterList = std::make_shared(mWindow, _("JUMP TO LETTER"), false); for (char c = startChar; c <= endChar; c++) { // check if c is a valid first letter in current list @@ -107,7 +107,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool } } - row.addElement(std::make_shared(mWindow, _T("JUMP TO LETTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(mWindow, _("JUMP TO LETTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(mJumpToLetterList, false); row.input_handler = [&](InputConfig* config, Input input) { @@ -130,18 +130,18 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool currentSortId = 0; } - mListSort = std::make_shared(mWindow, _T("SORT GAMES BY"), false); + mListSort = std::make_shared(mWindow, _("SORT GAMES BY"), false); for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) { const FolderData::SortType& sort = FileSorts::SortTypes.at(i); - mListSort->add(_T(Utils::String::toUpper(sort.description)), i, i == currentSortId); + mListSort->add(_(Utils::String::toUpper(sort.description)), i, i == currentSortId); } - mMenu.addWithLabel(_T("SORT GAMES BY"), mListSort); + mMenu.addWithLabel(_("SORT GAMES BY"), mListSort); } // GameList view style - mViewMode = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); + mViewMode = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); std::vector styles; styles.push_back("automatic"); @@ -158,13 +158,13 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool if (sel) found = true; - mViewMode->add(_T(*it), *it, sel); + mViewMode->add(_(*it), *it, sel); } if (!found) mViewMode->selectFirstItem(); - mMenu.addWithLabel(_T("GAMELIST VIEW STYLE"), mViewMode); + mMenu.addWithLabel(_("GAMELIST VIEW STYLE"), mViewMode); @@ -174,7 +174,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool auto gridOverride = system->getGridSizeOverride(); auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); - mGridSize = std::make_shared>(mWindow, _T("GRID SIZE"), false); + mGridSize = std::make_shared>(mWindow, _("GRID SIZE"), false); found = false; for (auto it = gridSizes.cbegin(); it != gridSizes.cend(); it++) @@ -183,20 +183,20 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool if (sel) found = true; - mGridSize->add(_T(*it), *it, sel); + mGridSize->add(_(*it), *it, sel); } if (!found) mGridSize->selectFirstItem(); - mMenu.addWithLabel(_T("GRID SIZE"), mGridSize); + mMenu.addWithLabel(_("GRID SIZE"), mGridSize); } // show filtered menu if(!Settings::getInstance()->getBool("ForceDisableFilters")) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("APPLY FILTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(mWindow, _("APPLY FILTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openGamelistFilter, this)); mMenu.addRow(row); @@ -208,7 +208,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool // maximum vram auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel(_T("VRAM LIMIT"), max_vram); + s->addWithLabel(_("VRAM LIMIT"), max_vram); s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); */ @@ -234,7 +234,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool CollectionSystemManager::get()->getCustomCollectionsBundle()->getName() == system->getName())) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("ADD/REMOVE GAMES TO THIS GAME COLLECTION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(mWindow, _("ADD/REMOVE GAMES TO THIS GAME COLLECTION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::startEditMode, this)); mMenu.addRow(row); } @@ -242,7 +242,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool if(UIModeController::getInstance()->isUIModeFull() && CollectionSystemManager::get()->isEditing()) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("FINISH EDITING")+" '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' "+_T("COLLECTION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(mWindow, _("FINISH EDITING")+" '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "' "+_("COLLECTION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::exitEditMode, this)); mMenu.addRow(row); } @@ -250,7 +250,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool if (UIModeController::getInstance()->isUIModeFull() && !fromPlaceholder && !(mSystem->isCollection() && file->getType() == FOLDER)) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, _T("EDIT THIS GAME'S METADATA"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(mWindow, _("EDIT THIS GAME'S METADATA"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGamelistOptions::openMetaDataEd, this)); mMenu.addRow(row); @@ -450,7 +450,7 @@ HelpStyle GuiGamelistOptions::getHelpStyle() std::vector GuiGamelistOptions::getHelpPrompts() { auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("CLOSE"))); + prompts.push_back(HelpPrompt("b", _("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 66e7760b14..a3ac6a201f 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -23,7 +23,7 @@ #include "animations/LambdaAnimation.h" #include "GuiGamelistOptions.h" // grid sizes -GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN MENU")), mVersion(window) +GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _("MAIN MENU")), mVersion(window) { auto theme = ThemeData::getMenuTheme(); @@ -31,22 +31,22 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN if (isFullUI) { - addEntry(_T("UI SETTINGS"), theme->Text.color, true, [this] { openUISettings(); }, theme->MenuIcons.ui); - addEntry(_T("CONFIGURE INPUT"), theme->Text.color, true, [this] { openConfigInput(); }, theme->MenuIcons.controllers); + addEntry(_("UI SETTINGS"), theme->Text.color, true, [this] { openUISettings(); }, theme->MenuIcons.ui); + addEntry(_("CONFIGURE INPUT"), theme->Text.color, true, [this] { openConfigInput(); }, theme->MenuIcons.controllers); } - addEntry(_T("SOUND SETTINGS"), theme->Text.color, true, [this] { openSoundSettings(); }, theme->MenuIcons.sound); + addEntry(_("SOUND SETTINGS"), theme->Text.color, true, [this] { openSoundSettings(); }, theme->MenuIcons.sound); if (isFullUI) - addEntry(_T("SCRAPER"), theme->Text.color, true, [this] { openScraperSettings(); }, theme->MenuIcons.scraper); + addEntry(_("SCRAPER"), theme->Text.color, true, [this] { openScraperSettings(); }, theme->MenuIcons.scraper); if (isFullUI) { - addEntry(_T("GAME COLLECTION SETTINGS"), theme->Text.color, true, [this] { openCollectionSystemSettings(); }, theme->MenuIcons.games); - addEntry(_T("ADVANCED SETTINGS"), theme->Text.color, true, [this] { openOtherSettings(); }, theme->MenuIcons.advanced); + addEntry(_("GAME COLLECTION SETTINGS"), theme->Text.color, true, [this] { openCollectionSystemSettings(); }, theme->MenuIcons.games); + addEntry(_("ADVANCED SETTINGS"), theme->Text.color, true, [this] { openOtherSettings(); }, theme->MenuIcons.advanced); } - addEntry(_T("QUIT"), theme->Text.color, !Settings::getInstance()->getBool("ShowOnlyExit"), [this] {openQuitMenu(); }, theme->MenuIcons.quit); + addEntry(_("QUIT"), theme->Text.color, !Settings::getInstance()->getBool("ShowOnlyExit"), [this] {openQuitMenu(); }, theme->MenuIcons.quit); addChild(&mMenu); addVersionInfo(); @@ -82,23 +82,23 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _T("MAIN void GuiMenu::openScraperSettings() { - auto s = new GuiSettings(mWindow, _T("SCRAPER")); + auto s = new GuiSettings(mWindow, _("SCRAPER")); // scrape from - auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, _T("SCRAPE FROM"), false); + auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, _("SCRAPE FROM"), false); std::vector scrapers = getScraperList(); // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. for(auto it = scrapers.cbegin(); it != scrapers.cend(); it++) scraper_list->add(*it, *it, *it == Settings::getInstance()->getString("Scraper")); - s->addWithLabel(_T("SCRAPE FROM"), scraper_list); + 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(mWindow); scrape_ratings->setState(Settings::getInstance()->getBool("ScrapeRatings")); - s->addWithLabel(_T("SCRAPE RATINGS"), scrape_ratings); + s->addWithLabel(_("SCRAPE RATINGS"), scrape_ratings); s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); }); @@ -108,13 +108,13 @@ void GuiMenu::openScraperSettings() std::string imageSourceName = Settings::getInstance()->getString("ScrapperImageSrc"); - auto imageSource = std::make_shared< OptionListComponent >(mWindow, _T("PREFERED IMAGE SOURCE"), false); - imageSource->add(_T("NONE"), "", imageSourceName.empty()); - imageSource->add(_T("SCREENSHOT"), "ss", imageSourceName == "ss"); - imageSource->add(_T("BOX 2D"), "box-2D", imageSourceName == "box-2D"); - imageSource->add(_T("BOX 3D"), "box-3D", imageSourceName == "box-3D"); - imageSource->add(_T("MIX"), "mixrbv1", imageSourceName == "mixrbv1"); - imageSource->add(_T("WHEEL"), "wheel", imageSourceName == "wheel"); + auto imageSource = std::make_shared< OptionListComponent >(mWindow, _("PREFERED IMAGE SOURCE"), false); + imageSource->add(_("NONE"), "", imageSourceName.empty()); + imageSource->add(_("SCREENSHOT"), "ss", imageSourceName == "ss"); + imageSource->add(_("BOX 2D"), "box-2D", imageSourceName == "box-2D"); + imageSource->add(_("BOX 3D"), "box-3D", imageSourceName == "box-3D"); + imageSource->add(_("MIX"), "mixrbv1", imageSourceName == "mixrbv1"); + imageSource->add(_("WHEEL"), "wheel", imageSourceName == "wheel"); s->addWithLabel("PREFERED IMAGE SOURCE", imageSource); s->addSaveFunc([imageSource] { @@ -124,13 +124,13 @@ void GuiMenu::openScraperSettings() std::string thumbSourceName = Settings::getInstance()->getString("ScrapperThumbSrc"); - auto thumbSource = std::make_shared< OptionListComponent >(mWindow, _T("PREFERED THUMBNAIL SOURCE"), false); - thumbSource->add(_T("NONE"), "", thumbSourceName.empty()); - thumbSource->add(_T("SCREENSHOT"), "ss", thumbSourceName == "ss"); - thumbSource->add(_T("BOX 2D"), "box-2D", thumbSourceName == "box-2D"); - thumbSource->add(_T("BOX 3D"), "box-3D", thumbSourceName == "box-3D"); - thumbSource->add(_T("MIX"), "mixrbv1", thumbSourceName == "mixrbv1"); - thumbSource->add(_T("WHEEL"), "wheel", thumbSourceName == "wheel"); + auto thumbSource = std::make_shared< OptionListComponent >(mWindow, _("PREFERED THUMBNAIL SOURCE"), false); + thumbSource->add(_("NONE"), "", thumbSourceName.empty()); + thumbSource->add(_("SCREENSHOT"), "ss", thumbSourceName == "ss"); + thumbSource->add(_("BOX 2D"), "box-2D", thumbSourceName == "box-2D"); + thumbSource->add(_("BOX 3D"), "box-3D", thumbSourceName == "box-3D"); + thumbSource->add(_("MIX"), "mixrbv1", thumbSourceName == "mixrbv1"); + thumbSource->add(_("WHEEL"), "wheel", thumbSourceName == "wheel"); s->addWithLabel("PREFERED THUMBNAIL SOURCE", thumbSource); s->addSaveFunc([thumbSource] { @@ -142,7 +142,7 @@ void GuiMenu::openScraperSettings() // scrape video auto scrape_video = std::make_shared(mWindow); scrape_video->setState(Settings::getInstance()->getBool("ScrapeVideos")); - s->addWithLabel(_T("SCRAPE VIDEOS"), scrape_video); + s->addWithLabel(_("SCRAPE VIDEOS"), scrape_video); s->addSaveFunc([scrape_video] { Settings::getInstance()->setBool("ScrapeVideos", scrape_video->getState()); }); @@ -153,7 +153,7 @@ void GuiMenu::openScraperSettings() openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; row.makeAcceptInputHandler(openAndSave); - auto scrape_now = std::make_shared(mWindow, _T("SCRAPE NOW"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); + auto scrape_now = std::make_shared(mWindow, _("SCRAPE NOW"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); auto bracket = makeArrow(mWindow); row.addElement(scrape_now, true); row.addElement(bracket, false); @@ -165,12 +165,12 @@ void GuiMenu::openScraperSettings() void GuiMenu::openSoundSettings() { - auto s = new GuiSettings(mWindow, _T("SOUND SETTINGS")); + auto s = new GuiSettings(mWindow, _("SOUND SETTINGS")); // volume auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); volume->setValue((float)VolumeControl::getInstance()->getVolume()); - s->addWithLabel(_T("SYSTEM VOLUME"), volume); + s->addWithLabel(_("SYSTEM VOLUME"), volume); s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); if (UIModeController::getInstance()->isUIModeFull()) @@ -230,7 +230,7 @@ void GuiMenu::openSoundSettings() // disable sounds auto music_enabled = std::make_shared(mWindow); music_enabled->setState(Settings::getInstance()->getBool("audio.bgmusic")); - s->addWithLabel(_T("FRONTEND MUSIC"), music_enabled); + s->addWithLabel(_("FRONTEND MUSIC"), music_enabled); s->addSaveFunc([music_enabled] { Settings::getInstance()->setBool("audio.bgmusic", music_enabled->getState()); if (music_enabled->getState()) @@ -243,7 +243,7 @@ void GuiMenu::openSoundSettings() // music per system auto music_per_system = std::make_shared(mWindow); music_per_system->setState(Settings::getInstance()->getBool("audio.persystem")); - s->addWithLabel(_T("ONLY PLAY SYSTEM-SPECIFIC MUSIC FOLDER"), music_per_system); + s->addWithLabel(_("ONLY PLAY SYSTEM-SPECIFIC MUSIC FOLDER"), music_per_system); s->addSaveFunc([music_per_system] { Settings::getInstance()->setBool("audio.persystem", music_per_system->getState()); }); @@ -251,7 +251,7 @@ void GuiMenu::openSoundSettings() // disable sounds auto sounds_enabled = std::make_shared(mWindow); sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); - s->addWithLabel(_T("ENABLE NAVIGATION SOUNDS"), sounds_enabled); + s->addWithLabel(_("ENABLE NAVIGATION SOUNDS"), sounds_enabled); s->addSaveFunc([sounds_enabled] { if (sounds_enabled->getState() && !Settings::getInstance()->getBool("EnableSounds") @@ -265,7 +265,7 @@ void GuiMenu::openSoundSettings() auto video_audio = std::make_shared(mWindow); video_audio->setState(Settings::getInstance()->getBool("VideoAudio")); - s->addWithLabel(_T("ENABLE VIDEO AUDIO"), video_audio); + s->addWithLabel(_("ENABLE VIDEO AUDIO"), video_audio); s->addSaveFunc([video_audio] { Settings::getInstance()->setBool("VideoAudio", video_audio->getState()); }); #ifdef _RPI_ @@ -299,11 +299,14 @@ void GuiMenu::openSoundSettings() } void GuiMenu::openUISettings() -{ - auto s = new GuiSettings(mWindow, _T("UI SETTINGS")); - +{ auto pthis = this; + Window* window = mWindow; + auto s = new GuiSettings(mWindow, _("UI SETTINGS")); + s->setBeforeCloseFunc([window, pthis] { delete pthis; window->pushGui(new GuiMenu(window)); }); + s->enableBeforeCloseFunc(false); + // theme set auto theme = ThemeData::getMenuTheme(); auto themeSets = ThemeData::getThemeSets(); @@ -318,10 +321,9 @@ void GuiMenu::openUISettings() auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) theme_set->add(it->first, it->first, it == selectedSet); - s->addWithLabel(_T("THEME"), theme_set); + s->addWithLabel(_("THEME"), theme_set); - Window* window = mWindow; - s->addSaveFunc([window, theme_set, pthis] + s->addSaveFunc([s, theme_set] { std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); if (oldTheme != theme_set->getSelected()) @@ -367,15 +369,12 @@ void GuiMenu::openUISettings() else Settings::getInstance()->setString("ThemeGamelistView", themeGamelistViewSets.begin()->first); - - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); CollectionSystemManager::get()->updateSystemsList(); ViewController::get()->goToStart(); ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation - delete pthis; - window->pushGui(new GuiMenu(window)); + s->enableBeforeCloseFunc(true); } }); @@ -383,8 +382,8 @@ void GuiMenu::openUISettings() { // theme config - std::function openGui = [this, theme_set, window, pthis] { - auto themeconfig = new GuiSettings(mWindow, _T("THEME CONFIGURATION").c_str()); + std::function openGui = [this, theme_set, s, window] { + auto themeconfig = new GuiSettings(mWindow, _("THEME CONFIGURATION").c_str()); auto SelectedTheme = theme_set->getSelected(); @@ -401,39 +400,39 @@ void GuiMenu::openUISettings() auto selectedColorSet = themeColorSets.find(Settings::getInstance()->getString("ThemeColorSet")); if (selectedColorSet == themeColorSets.end()) selectedColorSet = themeColorSets.begin(); - auto theme_colorset = std::make_shared >(mWindow, _T("THEME COLORSET"), false); + auto theme_colorset = std::make_shared >(mWindow, _("THEME COLORSET"), false); for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) theme_colorset->add(it->first, it->first, it == selectedColorSet); if (!themeColorSets.empty()) - themeconfig->addWithLabel(_T("THEME COLORSET"), theme_colorset); + themeconfig->addWithLabel(_("THEME COLORSET"), theme_colorset); // iconset auto selectedIconSet = themeIconSets.find(Settings::getInstance()->getString("ThemeIconSet")); if (selectedIconSet == themeIconSets.end()) selectedIconSet = themeIconSets.begin(); - auto theme_iconset = std::make_shared >(mWindow, _T("THEME ICONSET"), false); + auto theme_iconset = std::make_shared >(mWindow, _("THEME ICONSET"), false); for (auto it = themeIconSets.begin(); it != themeIconSets.end(); it++) theme_iconset->add(it->first, it->first, it == selectedIconSet); if (!themeIconSets.empty()) - themeconfig->addWithLabel(_T("THEME ICONSET"), theme_iconset); + themeconfig->addWithLabel(_("THEME ICONSET"), theme_iconset); // menu auto selectedMenu = themeMenus.find(Settings::getInstance()->getString("ThemeMenu")); if (selectedMenu == themeMenus.end()) selectedMenu = themeMenus.begin(); - auto theme_menu = std::make_shared >(mWindow, _T("THEME MENU"), false); + auto theme_menu = std::make_shared >(mWindow, _("THEME MENU"), false); for (auto it = themeMenus.begin(); it != themeMenus.end(); it++) theme_menu->add(it->first, it->first, it == selectedMenu); if (!themeMenus.empty()) - themeconfig->addWithLabel(_T("THEME MENU"), theme_menu); + themeconfig->addWithLabel(_("THEME MENU"), theme_menu); // systemview @@ -441,13 +440,13 @@ void GuiMenu::openUISettings() if (selectedSystemviewSet == themeSystemviewSets.end()) selectedSystemviewSet = themeSystemviewSets.begin(); - auto theme_systemview = std::make_shared >(mWindow, _T("THEME SYSTEMVIEW"), false); + auto theme_systemview = std::make_shared >(mWindow, _("THEME SYSTEMVIEW"), false); for (auto it = themeSystemviewSets.begin(); it != themeSystemviewSets.end(); it++) theme_systemview->add(it->first, it->first, it == selectedSystemviewSet); if (!themeSystemviewSets.empty()) - themeconfig->addWithLabel(_T("THEME SYSTEMVIEW"), theme_systemview); + themeconfig->addWithLabel(_("THEME SYSTEMVIEW"), theme_systemview); // gamelistview @@ -455,28 +454,28 @@ void GuiMenu::openUISettings() if (selectedGamelistViewSet == themeGamelistViewSets.end()) selectedGamelistViewSet = themeGamelistViewSets.begin(); - auto theme_gamelistview = std::make_shared >(mWindow, _T("THEME GAMELISTVIEW"), false); + auto theme_gamelistview = std::make_shared >(mWindow, _("THEME GAMELISTVIEW"), false); for (auto it = themeGamelistViewSets.begin(); it != themeGamelistViewSets.end(); it++) theme_gamelistview->add(it->first, it->first, it == selectedGamelistViewSet); if (!themeGamelistViewSets.empty()) - themeconfig->addWithLabel(_T("THEME GAMELISTVIEW"), theme_gamelistview); + themeconfig->addWithLabel(_("THEME GAMELISTVIEW"), theme_gamelistview); // themeregion auto selectedRegion = themeRegions.find(Settings::getInstance()->getString("ThemeRegionName")); if (selectedRegion == themeRegions.end()) selectedRegion = themeRegions.begin(); - auto theme_region = std::make_shared >(mWindow, _T("THEME GAMELISTVIEW"), false); + auto theme_region = std::make_shared >(mWindow, _("THEME GAMELISTVIEW"), false); for (auto it = themeRegions.begin(); it != themeRegions.end(); it++) theme_region->add(it->first, it->first, it == selectedRegion); if (!themeRegions.empty()) - themeconfig->addWithLabel(_T("THEME REGION"), theme_region); + themeconfig->addWithLabel(_("THEME REGION"), theme_region); - themeconfig->addSaveFunc([this, window, theme_set, theme_colorset, theme_iconset, theme_menu, theme_systemview, theme_gamelistview, theme_region, pthis] { + themeconfig->addSaveFunc([this, s, theme_set, theme_colorset, theme_iconset, theme_menu, theme_systemview, theme_gamelistview, theme_region] { bool needReload = false; if (Settings::getInstance()->getString("ThemeColorSet") != theme_colorset->getSelected() && !theme_colorset->getSelected().empty()) needReload = true; @@ -508,9 +507,7 @@ void GuiMenu::openUISettings() ViewController::get()->goToStart(); ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation - delete pthis; - window->pushGui(new GuiMenu(window)); - //ReloadAll(); + s->enableBeforeCloseFunc(true); } }); if (!themeRegions.empty() || !themeGamelistViewSets.empty() || !themeSystemviewSets.empty() || !themeIconSets.empty() || !themeMenus.empty() || !themeColorSets.empty()) @@ -519,17 +516,17 @@ void GuiMenu::openUISettings() mWindow->pushGui(themeconfig); } else - mWindow->pushGui(new GuiMsgBox(window, _T("THIS THEME HAS NO OPTION"), _T("OK"))); + mWindow->pushGui(new GuiMsgBox(window, _("THIS THEME HAS NO OPTION"), _("OK"))); }; - s->addSubMenu(_T("THEME CONFIGURATION"), openGui); + s->addSubMenu(_("THEME CONFIGURATION"), openGui); } } // GameList view style if (system != NULL && !system->getTheme()->hasSubsets()) { - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _T("GAMELIST VIEW STYLE"), false); + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); std::vector styles; styles.push_back("automatic"); @@ -553,9 +550,9 @@ void GuiMenu::openUISettings() viewPreference = "automatic"; for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_T(*it), *it, viewPreference == *it); + gamelist_style->add(_(*it), *it, viewPreference == *it); - s->addWithLabel(_T("GAMELIST VIEW STYLE"), gamelist_style); + s->addWithLabel(_("GAMELIST VIEW STYLE"), gamelist_style); s->addSaveFunc([gamelist_style, viewPreference] { bool needReload = false; if (viewPreference != gamelist_style->getSelected()) @@ -572,7 +569,7 @@ void GuiMenu::openUISettings() Vector2f gridOverride = Vector2f::parseString(Settings::getInstance()->getString("DefaultGridSize")); auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); - auto mGridSize = std::make_shared>(mWindow, _T("DEFAULT GRID SIZE"), false); + auto mGridSize = std::make_shared>(mWindow, _("DEFAULT GRID SIZE"), false); bool found = false; for (auto it = GuiGamelistOptions::gridSizes.cbegin(); it != GuiGamelistOptions::gridSizes.cend(); it++) @@ -581,13 +578,13 @@ void GuiMenu::openUISettings() if (sel) found = true; - mGridSize->add(_T(*it), *it, sel); + mGridSize->add(_(*it), *it, sel); } if (!found) mGridSize->selectFirstItem(); - s->addWithLabel(_T("DEFAULT GRID SIZE"), mGridSize); + s->addWithLabel(_("DEFAULT GRID SIZE"), mGridSize); s->addSaveFunc([mGridSize] { std::string str = mGridSize->getSelected(); @@ -613,12 +610,12 @@ void GuiMenu::openUISettings() //#ifndef WIN32 //UI mode - auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, _T("UI MODE"), false); + auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, _("UI MODE"), false); std::vector UImodes = UIModeController::getInstance()->getUIModes(); for (auto it = UImodes.cbegin(); it != UImodes.cend(); it++) - UImodeSelection->add(_T(*it), *it, Settings::getInstance()->getString("UIMode") == *it); - s->addWithLabel(_T("UI MODE"), UImodeSelection); - Window* window = mWindow; + UImodeSelection->add(_(*it), *it, Settings::getInstance()->getString("UIMode") == *it); + s->addWithLabel(_("UI MODE"), UImodeSelection); + s->addSaveFunc([UImodeSelection, window] { std::string selectedMode = UImodeSelection->getSelected(); @@ -669,28 +666,34 @@ void GuiMenu::openUISettings() if (langues.size() > 1) { - auto language = std::make_shared< OptionListComponent >(mWindow, _T("LANGUAGE"), false); + auto language = std::make_shared< OptionListComponent >(mWindow, _("LANGUAGE"), false); for (auto it = langues.cbegin(); it != langues.cend(); it++) language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); - s->addWithLabel(_T("LANGUAGE"), language); - s->addSaveFunc([language] { - Settings::getInstance()->setString("Language", language->getSelected()); + s->addWithLabel(_("LANGUAGE"), language); + s->addSaveFunc([language, window, pthis] { + + if (language->getSelected() != Settings::getInstance()->getString("Language")) + { + Settings::getInstance()->setString("Language", language->getSelected()); + delete pthis; + window->pushGui(new GuiMenu(window)); + } }); } } // transition style - auto transition_style = std::make_shared< OptionListComponent >(mWindow, _T("TRANSITION STYLE"), false); + auto transition_style = std::make_shared< OptionListComponent >(mWindow, _("TRANSITION STYLE"), false); std::vector transitions; transitions.push_back("fade"); transitions.push_back("slide"); transitions.push_back("instant"); for (auto it = transitions.cbegin(); it != transitions.cend(); it++) - transition_style->add(_T(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); + transition_style->add(_(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); - s->addWithLabel(_T("TRANSITION STYLE"), transition_style); + s->addWithLabel(_("TRANSITION STYLE"), transition_style); s->addSaveFunc([transition_style] { if (Settings::getInstance()->getString("TransitionStyle") == "instant" && transition_style->getSelected() != "instant" @@ -704,15 +707,15 @@ void GuiMenu::openUISettings() }); - auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _T("GAME LAUNCH TRANSITION"), false); + auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _("GAME LAUNCH TRANSITION"), false); std::vector gameTransitions; gameTransitions.push_back("fade"); gameTransitions.push_back("slide"); gameTransitions.push_back("instant"); for (auto it = gameTransitions.cbegin(); it != gameTransitions.cend(); it++) - transitionOfGames_style->add(_T(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); + transitionOfGames_style->add(_(*it), *it, Settings::getInstance()->getString("GameTransitionStyle") == *it); - s->addWithLabel(_T("GAME LAUNCH TRANSITION"), transitionOfGames_style); + s->addWithLabel(_("GAME LAUNCH TRANSITION"), transitionOfGames_style); s->addSaveFunc([transitionOfGames_style] { if (Settings::getInstance()->getString("GameTransitionStyle") == "instant" && transitionOfGames_style->getSelected() != "instant" @@ -726,8 +729,8 @@ void GuiMenu::openUISettings() // Optionally start in selected system - auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _T("START ON SYSTEM"), false); - systemfocus_list->add(_T("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); + auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _("START ON SYSTEM"), false); + systemfocus_list->add(_("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { if ("retropie" != (*it)->getName()) @@ -735,7 +738,7 @@ void GuiMenu::openUISettings() systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); } } - s->addWithLabel(_T("START ON SYSTEM"), systemfocus_list); + s->addWithLabel(_("START ON SYSTEM"), systemfocus_list); s->addSaveFunc([systemfocus_list] { Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); }); @@ -743,13 +746,13 @@ void GuiMenu::openUISettings() // Open gamelist at start auto bootOnGamelist = std::make_shared(mWindow); bootOnGamelist->setState(Settings::getInstance()->getBool("StartupOnGameList")); - s->addWithLabel(_T("BOOT ON GAMELIST"), bootOnGamelist); + s->addWithLabel(_("BOOT ON GAMELIST"), bootOnGamelist); s->addSaveFunc([bootOnGamelist] { Settings::getInstance()->setBool("StartupOnGameList", bootOnGamelist->getState()); }); // Hide system view auto hideSystemView = std::make_shared(mWindow); hideSystemView->setState(Settings::getInstance()->getBool("HideSystemView")); - s->addWithLabel(_T("HIDE SYSTEM VIEW"), hideSystemView); + s->addWithLabel(_("HIDE SYSTEM VIEW"), hideSystemView); s->addSaveFunc([hideSystemView] { bool hideSysView = Settings::getInstance()->getBool("HideSystemView"); @@ -764,20 +767,20 @@ void GuiMenu::openUISettings() // quick system select (left/right in game list view) auto hideWindowScreen = std::make_shared(mWindow); hideWindowScreen->setState(Settings::getInstance()->getBool("HideWindow")); - s->addWithLabel(_T("HIDE WHEN RUNNING GAME"), hideWindowScreen); + s->addWithLabel(_("HIDE WHEN RUNNING GAME"), hideWindowScreen); s->addSaveFunc([hideWindowScreen] { Settings::getInstance()->setBool("HideWindow", hideWindowScreen->getState()); }); #endif // quick system select (left/right in game list view) auto quick_sys_select = std::make_shared(mWindow); quick_sys_select->setState(Settings::getInstance()->getBool("QuickSystemSelect")); - s->addWithLabel(_T("QUICK SYSTEM SELECT"), quick_sys_select); + s->addWithLabel(_("QUICK SYSTEM SELECT"), quick_sys_select); s->addSaveFunc([quick_sys_select] { Settings::getInstance()->setBool("QuickSystemSelect", quick_sys_select->getState()); }); // carousel transition option auto move_carousel = std::make_shared(mWindow); move_carousel->setState(Settings::getInstance()->getBool("MoveCarousel")); - s->addWithLabel(_T("CAROUSEL TRANSITIONS"), move_carousel); + s->addWithLabel(_("CAROUSEL TRANSITIONS"), move_carousel); s->addSaveFunc([move_carousel] { if (move_carousel->getState() && !Settings::getInstance()->getBool("MoveCarousel") @@ -792,13 +795,13 @@ void GuiMenu::openUISettings() // show help auto show_help = std::make_shared(mWindow); show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); - s->addWithLabel(_T("ON-SCREEN HELP"), show_help); + s->addWithLabel(_("ON-SCREEN HELP"), show_help); s->addSaveFunc([show_help] { Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState()); }); // enable filters (ForceDisableFilters) auto enable_filter = std::make_shared(mWindow); enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); - s->addWithLabel(_T("ENABLE FILTERS"), enable_filter); + s->addWithLabel(_("ENABLE FILTERS"), enable_filter); s->addSaveFunc([enable_filter] { bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); @@ -809,7 +812,7 @@ void GuiMenu::openUISettings() // screensaver ComponentListRow screensaver_row; screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, _T("SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); + screensaver_row.addElement(std::make_shared(mWindow, _("SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); screensaver_row.addElement(makeArrow(mWindow), false); screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); s->addRow(screensaver_row); @@ -821,16 +824,16 @@ void GuiMenu::openUISettings() void GuiMenu::openOtherSettings() { - auto s = new GuiSettings(mWindow, _T("ADVANCED SETTINGS")); + auto s = new GuiSettings(mWindow, _("ADVANCED SETTINGS")); // maximum vram auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel(_T("VRAM LIMIT"), max_vram); + s->addWithLabel(_("VRAM LIMIT"), max_vram); s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); // power saver - auto power_saver = std::make_shared< OptionListComponent >(mWindow, _T("POWER SAVER MODES"), false); + auto power_saver = std::make_shared< OptionListComponent >(mWindow, _("POWER SAVER MODES"), false); std::vector modes; modes.push_back("disabled"); modes.push_back("default"); @@ -839,7 +842,7 @@ void GuiMenu::openOtherSettings() for (auto it = modes.cbegin(); it != modes.cend(); it++) power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); - s->addWithLabel(_T("POWER SAVER MODES"), power_saver); + s->addWithLabel(_("POWER SAVER MODES"), power_saver); s->addSaveFunc([this, power_saver] { if (Settings::getInstance()->getString("PowerSaverMode") != "instant" && power_saver->getSelected() == "instant") { Settings::getInstance()->setString("TransitionStyle", "instant"); @@ -857,25 +860,25 @@ void GuiMenu::openOtherSettings() // gamelists auto save_gamelists = std::make_shared(mWindow); save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); - s->addWithLabel(_T("SAVE METADATA ON EXIT"), save_gamelists); + s->addWithLabel(_("SAVE METADATA ON EXIT"), save_gamelists); s->addSaveFunc([save_gamelists] { Settings::getInstance()->setBool("SaveGamelistsOnExit", save_gamelists->getState()); }); auto parse_gamelists = std::make_shared(mWindow); parse_gamelists->setState(Settings::getInstance()->getBool("ParseGamelistOnly")); - s->addWithLabel(_T("PARSE GAMESLISTS ONLY"), parse_gamelists); + s->addWithLabel(_("PARSE GAMESLISTS ONLY"), parse_gamelists); s->addSaveFunc([parse_gamelists] { Settings::getInstance()->setBool("ParseGamelistOnly", parse_gamelists->getState()); }); #ifndef WIN32 auto local_art = std::make_shared(mWindow); local_art->setState(Settings::getInstance()->getBool("LocalArt")); - s->addWithLabel(_T("SEARCH FOR LOCAL ART"), local_art); + s->addWithLabel(_("SEARCH FOR LOCAL ART"), local_art); s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); #endif // hidden files auto hidden_files = std::make_shared(mWindow); hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); - s->addWithLabel(_T("SHOW HIDDEN FILES"), hidden_files); + s->addWithLabel(_("SHOW HIDDEN FILES"), hidden_files); s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); #ifdef _RPI_ @@ -901,13 +904,13 @@ void GuiMenu::openOtherSettings() // preload UI auto preloadUI = std::make_shared(mWindow); preloadUI->setState(Settings::getInstance()->getBool("PreloadUI")); - s->addWithLabel(_T("PRELOAD UI"), preloadUI); + s->addWithLabel(_("PRELOAD UI"), preloadUI); s->addSaveFunc([preloadUI] { Settings::getInstance()->setBool("PreloadUI", preloadUI->getState()); }); // optimizeVram auto optimizeVram = std::make_shared(mWindow); optimizeVram->setState(Settings::getInstance()->getBool("OptimizeVRAM")); - s->addWithLabel(_T("OPTIMIZE IMAGES VRAM USE"), optimizeVram); + s->addWithLabel(_("OPTIMIZE IMAGES VRAM USE"), optimizeVram); s->addSaveFunc([optimizeVram] { TextureData::OPTIMIZEVRAM = optimizeVram->getState(); @@ -918,7 +921,7 @@ void GuiMenu::openOtherSettings() // vsync auto vsync = std::make_shared(mWindow); vsync->setState(Settings::getInstance()->getBool("VSync")); - s->addWithLabel(_T("VSYNC"), vsync); + s->addWithLabel(_("VSYNC"), vsync); s->addSaveFunc([vsync] { Settings::getInstance()->setBool("VSync", vsync->getState()); @@ -943,20 +946,20 @@ void GuiMenu::openOtherSettings() // framerate auto framerate = std::make_shared(mWindow); framerate->setState(Settings::getInstance()->getBool("DrawFramerate")); - s->addWithLabel(_T("SHOW FRAMERATE"), framerate); + s->addWithLabel(_("SHOW FRAMERATE"), framerate); s->addSaveFunc([framerate] { Settings::getInstance()->setBool("DrawFramerate", framerate->getState()); }); // threaded loading auto threadedLoading = std::make_shared(mWindow); threadedLoading->setState(Settings::getInstance()->getBool("ThreadedLoading")); - s->addWithLabel(_T("THREADED LOADING"), threadedLoading); + s->addWithLabel(_("THREADED LOADING"), threadedLoading); s->addSaveFunc([threadedLoading] { Settings::getInstance()->setBool("ThreadedLoading", threadedLoading->getState()); }); #ifndef _RPI_ // full exit auto fullExitMenu = std::make_shared(mWindow); fullExitMenu->setState(!Settings::getInstance()->getBool("ShowOnlyExit")); - s->addWithLabel(_T("COMPLETE QUIT MENU"), fullExitMenu); + s->addWithLabel(_("COMPLETE QUIT MENU"), fullExitMenu); s->addSaveFunc([fullExitMenu] { Settings::getInstance()->setBool("ShowOnlyExit", !fullExitMenu->getState()); }); #endif @@ -970,10 +973,10 @@ void GuiMenu::openConfigInput() Window* window = mWindow; // window->pushGui(new GuiDetectDevice(window, false, nullptr)); - window->pushGui(new GuiMsgBox(window, _T("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _T("YES"), + window->pushGui(new GuiMsgBox(window, _("ARE YOU SURE YOU WANT TO CONFIGURE INPUT?"), _("YES"), [window] { window->pushGui(new GuiDetectDevice(window, false, nullptr)); - }, _T("NO"), nullptr) + }, _("NO"), nullptr) ); } @@ -987,7 +990,7 @@ void GuiMenu::openQuitMenu() return; } - auto s = new GuiSettings(mWindow, _T("QUIT")); + auto s = new GuiSettings(mWindow, _("QUIT")); Window* window = mWindow; @@ -995,54 +998,54 @@ void GuiMenu::openQuitMenu() if (UIModeController::getInstance()->isUIModeFull()) { row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, _T("REALLY RESTART?"), _T("YES"), + window->pushGui(new GuiMsgBox(window, _("REALLY RESTART?"), _("YES"), [] { Scripting::fireEvent("quit"); if(quitES(SDL_MSG_RESTART) != 0) LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, _T("NO"), nullptr)); + }, _("NO"), nullptr)); }); - row.addElement(std::make_shared(window, _T("RESTART EMULATIONSTATION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(window, _("RESTART EMULATIONSTATION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); if(Settings::getInstance()->getBool("ShowExit")) { row.elements.clear(); row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, _T("REALLY QUIT?"), _T("YES"), + window->pushGui(new GuiMsgBox(window, _("REALLY QUIT?"), _("YES"), [] { Scripting::fireEvent("quit"); quitES(); - }, _T("NO"), nullptr)); + }, _("NO"), nullptr)); }); - row.addElement(std::make_shared(window, _T("QUIT EMULATIONSTATION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(window, _("QUIT EMULATIONSTATION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); } } row.elements.clear(); row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, _T("REALLY RESTART?"), _T("YES"), + window->pushGui(new GuiMsgBox(window, _("REALLY RESTART?"), _("YES"), [] { Scripting::fireEvent("quit", "reboot"); Scripting::fireEvent("reboot"); if (quitES(SDL_MSG_REBOOT) != 0) LOG(LogWarning) << "Restart terminated with non-zero result!"; - }, _T("NO"), nullptr)); + }, _("NO"), nullptr)); }); - row.addElement(std::make_shared(window, _T("RESTART SYSTEM"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(window, _("RESTART SYSTEM"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); row.elements.clear(); row.makeAcceptInputHandler([window] { - window->pushGui(new GuiMsgBox(window, _T("REALLY SHUTDOWN?"), _T("YES"), + window->pushGui(new GuiMsgBox(window, _("REALLY SHUTDOWN?"), _("YES"), [] { Scripting::fireEvent("quit", "shutdown"); Scripting::fireEvent("shutdown"); if (quitES(SDL_MSG_SHUTDOWN) != 0) LOG(LogWarning) << "Shutdown terminated with non-zero result!"; - }, _T("NO"), nullptr)); + }, _("NO"), nullptr)); }); - row.addElement(std::make_shared(window, _T("SHUTDOWN SYSTEM"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(window, _("SHUTDOWN SYSTEM"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); s->updatePosition(); @@ -1066,7 +1069,7 @@ void GuiMenu::addVersionInfo() } void GuiMenu::openScreensaverOptions() { - mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _T("SCREENSAVER SETTINGS"))); + mWindow->pushGui(new GuiGeneralScreensaverOptions(mWindow, _("SCREENSAVER SETTINGS"))); } void GuiMenu::openCollectionSystemSettings() { @@ -1139,8 +1142,8 @@ HelpStyle GuiMenu::getHelpStyle() std::vector GuiMenu::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); - prompts.push_back(HelpPrompt("a", _T("SELECT"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + prompts.push_back(HelpPrompt("up/down", _("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _("SELECT"))); + prompts.push_back(HelpPrompt("start", _("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index f4ce75689e..e847912595 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -41,7 +41,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector mHeaderGrid = std::make_shared(mWindow, Vector2i(1, 5)); - mTitle = std::make_shared(mWindow, _T("EDIT METADATA"), theme->Title.font, theme->Title.color, ALIGN_CENTER); + mTitle = std::make_shared(mWindow, _("EDIT METADATA"), theme->Title.font, theme->Title.color, ALIGN_CENTER); mSubtitle = std::make_shared(mWindow, Utils::String::toUpper(Utils::FileSystem::getFileName(scraperParams.game->getPath())), theme->TextSmall.font, theme->TextSmall.color, ALIGN_CENTER); @@ -56,8 +56,8 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector SystemData* system = file->getSystem(); - auto emul_choice = std::make_shared>(mWindow, _T("EMULATOR"), false); - auto core_choice = std::make_shared>(mWindow, _T("CORE"), false); + auto emul_choice = std::make_shared>(mWindow, _("EMULATOR"), false); + auto core_choice = std::make_shared>(mWindow, _("CORE"), false); // populate list for(auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) @@ -85,14 +85,14 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector std::string currentEmul = file->getEmulator(); if (defaultEmul.length() == 0) - emul_choice->add(_T("DEFAULT"), "", true); + emul_choice->add(_("DEFAULT"), "", true); else - emul_choice->add(_T("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); + emul_choice->add(_("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); for (auto core : file->getSystemEnvData()->mEmulators) emul_choice->add(core.mName, core.mName, core.mName == currentEmul); - row.addElement(std::make_shared(mWindow, _T("EMULATOR"), theme->Text.font, theme->Text.color), true); + row.addElement(std::make_shared(mWindow, _("EMULATOR"), theme->Text.font, theme->Text.color), true); row.addElement(emul_choice, false); mList->addRow(row); @@ -109,9 +109,9 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector core_choice->clear(); if (defaultCore.length() == 0) - core_choice->add(_T("DEFAULT"), "", false); + core_choice->add(_("DEFAULT"), "", false); else - core_choice->add(_T("DEFAULT")+" ("+ defaultCore+")", "", false); + core_choice->add(_("DEFAULT")+" ("+ defaultCore+")", "", false); std::vector cores = system->getSystemEnvData()->getCores(emulatorName); @@ -136,7 +136,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector if (iter->displayName == "core") { - // core_choice->add(_T("DEFAULT"), "", true); + // core_choice->add(_("DEFAULT"), "", true); core_choice->setTag(iter->key); row.addElement(std::make_shared(mWindow, "CORE", theme->Text.font, theme->Text.color), true); @@ -152,7 +152,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector continue; } - auto lbl = std::make_shared(mWindow, _T(Utils::String::toUpper(iter->displayName)), theme->Text.font, theme->Text.color); + auto lbl = std::make_shared(mWindow, _(Utils::String::toUpper(iter->displayName)), theme->Text.font, theme->Text.color); row.addElement(lbl, true); // label switch (iter->type) @@ -238,16 +238,16 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector std::vector< std::shared_ptr > buttons; if (!scraperParams.system->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) - buttons.push_back(std::make_shared(mWindow, _T("SCRAPE"), _T("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); + buttons.push_back(std::make_shared(mWindow, _("SCRAPE"), _("SCRAPE"), std::bind(&GuiMetaDataEd::fetch, this))); - buttons.push_back(std::make_shared(mWindow, _T("SAVE"), _T("SAVE"), [&] { save(); delete this; })); - buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("CANCEL"), [&] { delete this; })); + buttons.push_back(std::make_shared(mWindow, _("SAVE"), _("SAVE"), [&] { save(); delete this; })); + buttons.push_back(std::make_shared(mWindow, _("CANCEL"), _("CANCEL"), [&] { delete this; })); if(mDeleteFunc) { auto deleteFileAndSelf = [&] { mDeleteFunc(); delete this; }; auto deleteBtnFunc = [this, deleteFileAndSelf] { mWindow->pushGui(new GuiMsgBox(mWindow, "THIS WILL DELETE THE ACTUAL GAME FILE(S)!\nARE YOU SURE?", "YES", deleteFileAndSelf, "NO", nullptr)); }; - buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE"), deleteBtnFunc)); + buttons.push_back(std::make_shared(mWindow, _("DELETE"), _("DELETE"), deleteBtnFunc)); } mButtons = makeButtonGrid(mWindow, buttons); @@ -370,9 +370,9 @@ void GuiMetaDataEd::close(bool closeAllWindows) { // changes were made, ask if the user wants to save them mWindow->pushGui(new GuiMsgBox(mWindow, - _T("SAVE CHANGES ?"), - _T("YES"), [this, closeFunc] { save(); closeFunc(); }, - _T("NO"), closeFunc + _("SAVE CHANGES ?"), + _("YES"), [this, closeFunc] { save(); closeFunc(); }, + _("NO"), closeFunc )); }else{ closeFunc(); @@ -397,7 +397,7 @@ bool GuiMetaDataEd::input(InputConfig* config, Input input) std::vector GuiMetaDataEd::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + prompts.push_back(HelpPrompt("b", _("BACK"))); + prompts.push_back(HelpPrompt("start", _("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 391f276bd8..4078736885 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -37,7 +37,7 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queue(mWindow, "SCRAPING IN PROGRESS", ThemeData::getMenuTheme()->Title.font, ThemeData::getMenuTheme()->Title.color, ALIGN_CENTER); mGrid.setEntry(mTitle, Vector2i(0, 0), false, true); - mSystem = std::make_shared(mWindow, _T("SYSTEM"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color, ALIGN_CENTER); + mSystem = std::make_shared(mWindow, _("SYSTEM"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color, ALIGN_CENTER); mGrid.setEntry(mSystem, Vector2i(0, 1), false, true); mSubtitle = std::make_shared(mWindow, "subtitle text", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color, ALIGN_CENTER); diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index 6762fea19a..b51afd7dd1 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -9,33 +9,33 @@ #include "SystemData.h" GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), - mMenu(window, _T("SCRAPE NOW")) + mMenu(window, _("SCRAPE NOW")) { addChild(&mMenu); // add filters (with first one selected) - mFilters = std::make_shared< OptionListComponent >(mWindow, _T("SCRAPE THESE GAMES"), false); - mFilters->add(_T("All Games"), + mFilters = std::make_shared< OptionListComponent >(mWindow, _("SCRAPE THESE GAMES"), false); + mFilters->add(_("All Games"), [](SystemData*, FileData*) -> bool { return true; }, false); - mFilters->add(_T("Only missing image"), + mFilters->add(_("Only missing image"), [](SystemData*, FileData* g) -> bool { return g->metadata.get("image").empty(); }, true); - mMenu.addWithLabel(_T("FILTER"), mFilters); + mMenu.addWithLabel(_("FILTER"), mFilters); //add systems (all with a platformid specified selected) - mSystems = std::make_shared< OptionListComponent >(mWindow, _T("SCRAPE THESE SYSTEMS"), true); + mSystems = std::make_shared< OptionListComponent >(mWindow, _("SCRAPE THESE SYSTEMS"), true); for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { if(!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) mSystems->add((*it)->getFullName(), *it, !(*it)->getPlatformIds().empty()); } - mMenu.addWithLabel(_T("SYSTEMS"), mSystems); + mMenu.addWithLabel(_("SYSTEMS"), mSystems); mApproveResults = std::make_shared(mWindow); mApproveResults->setState(true); - mMenu.addWithLabel(_T("USER DECIDES ON CONFLICTS"), mApproveResults); + mMenu.addWithLabel(_("USER DECIDES ON CONFLICTS"), mApproveResults); - mMenu.addButton(_T("START"), _T("START"), std::bind(&GuiScraperStart::pressedStart, this)); - mMenu.addButton(_T("BACK"), _T("BACK"), [&] { delete this; }); + 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); } @@ -49,8 +49,8 @@ void GuiScraperStart::pressedStart() { mWindow->pushGui(new GuiMsgBox(mWindow, Utils::String::toUpper("Warning: some of your selected systems do not have a platform set. Results may be even more inaccurate than usual!\nContinue anyway?"), - _T("YES"), std::bind(&GuiScraperStart::start, this), - _T("NO"), nullptr)); + _("YES"), std::bind(&GuiScraperStart::start, this), + _("NO"), nullptr)); return; } } @@ -64,7 +64,7 @@ void GuiScraperStart::start() if(searches.empty()) { - mWindow->pushGui(new GuiMsgBox(mWindow, _T("NO GAMES FIT THAT CRITERIA."))); + mWindow->pushGui(new GuiMsgBox(mWindow, _("NO GAMES FIT THAT CRITERIA."))); } else { @@ -123,7 +123,7 @@ bool GuiScraperStart::input(InputConfig* config, Input input) std::vector GuiScraperStart::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + prompts.push_back(HelpPrompt("b", _("BACK"))); + prompts.push_back(HelpPrompt("start", _("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiScreensaverOptions.cpp b/es-app/src/guis/GuiScreensaverOptions.cpp index 8d7df10d8f..460241cffe 100644 --- a/es-app/src/guis/GuiScreensaverOptions.cpp +++ b/es-app/src/guis/GuiScreensaverOptions.cpp @@ -10,7 +10,7 @@ GuiScreensaverOptions::GuiScreensaverOptions(Window* window, std::string title) { addChild(&mMenu); - mMenu.addButton(_T("BACK"), _T("BACK"), [this] { delete this; }); + mMenu.addButton(_("BACK"), _("BACK"), [this] { delete this; }); setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); @@ -63,8 +63,8 @@ std::vector GuiScreensaverOptions::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + prompts.push_back(HelpPrompt("b", _("BACK"))); + prompts.push_back(HelpPrompt("start", _("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiSettings.cpp b/es-app/src/guis/GuiSettings.cpp index 0ffc6f65af..f7c77827f2 100644 --- a/es-app/src/guis/GuiSettings.cpp +++ b/es-app/src/guis/GuiSettings.cpp @@ -8,13 +8,13 @@ GuiSettings::GuiSettings(Window* window, std::string title) : GuiComponent(window), mMenu(window, title) { addChild(&mMenu); - mMenu.addButton(_T("BACK"), _T("BACK"), [this] { delete this; }); + mMenu.addButton(_("BACK"), _("BACK"), [this] { saveAndClose(); }); updatePosition(); } GuiSettings::~GuiSettings() { - save(); + } void GuiSettings::updatePosition() @@ -25,20 +25,30 @@ void GuiSettings::updatePosition() void GuiSettings::save() { - if(!mSaveFuncs.size()) + if (!mSaveFuncs.size()) return; - for(auto it = mSaveFuncs.cbegin(); it != mSaveFuncs.cend(); it++) + for (auto it = mSaveFuncs.cbegin(); it != mSaveFuncs.cend(); it++) (*it)(); Settings::getInstance()->saveFile(); } +void GuiSettings::saveAndClose() +{ + save(); + + if (mBeforeCloseFunc != nullptr && mEnableBeforeCloseFunc) + mBeforeCloseFunc(); + + delete this; +} + bool GuiSettings::input(InputConfig* config, Input input) { if(config->isMappedTo("b", input) && input.value != 0) { - delete this; + saveAndClose(); return true; } @@ -48,6 +58,7 @@ bool GuiSettings::input(InputConfig* config, Input input) Window* window = mWindow; while(window->peekGui() && window->peekGui() != ViewController::get()) delete window->peekGui(); + return true; } @@ -65,8 +76,8 @@ std::vector GuiSettings::getHelpPrompts() { std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); - prompts.push_back(HelpPrompt("start", _T("CLOSE"))); + prompts.push_back(HelpPrompt("b", _("BACK"))); + prompts.push_back(HelpPrompt("start", _("CLOSE"))); return prompts; } diff --git a/es-app/src/guis/GuiSettings.h b/es-app/src/guis/GuiSettings.h index c9793156a5..205451dbee 100644 --- a/es-app/src/guis/GuiSettings.h +++ b/es-app/src/guis/GuiSettings.h @@ -10,10 +10,8 @@ class GuiSettings : public GuiComponent public: GuiSettings(Window* window, std::string title); virtual ~GuiSettings(); // just calls save(); - - + void updatePosition(); - void save(); inline void addRow(const ComponentListRow& row) { mMenu.addRow(row); }; inline void addWithLabel(const std::string& label, const std::shared_ptr& comp) { mMenu.addWithLabel(label, comp); }; inline void addSaveFunc(const std::function& func) { mSaveFuncs.push_back(func); }; @@ -34,9 +32,18 @@ class GuiSettings : public GuiComponent std::vector getHelpPrompts() override; HelpStyle getHelpStyle() override; + inline void setBeforeCloseFunc(const std::function& func) { mBeforeCloseFunc = func; mEnableBeforeCloseFunc = (mBeforeCloseFunc != nullptr); }; + void enableBeforeCloseFunc(bool use) { mEnableBeforeCloseFunc = use; } + + void save(); + private: + void saveAndClose(); + MenuComponent mMenu; std::vector< std::function > mSaveFuncs; + std::function mBeforeCloseFunc; + bool mEnableBeforeCloseFunc; }; #endif // ES_APP_GUIS_GUI_SETTINGS_H diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 3a6829b8a2..b66a530f32 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -385,7 +385,7 @@ int main(int argc, char* argv[]) LOG(LogInfo) << " ARB_texture_non_power_of_two: " << (glExts.find("ARB_texture_non_power_of_two") != std::string::npos ? "ok" : "MISSING"); if (splashScreen) - window.renderLoadingScreen(_T("Loading...")); + window.renderLoadingScreen(_("Loading...")); } const char* errorMsg = NULL; @@ -405,7 +405,7 @@ int main(int argc, char* argv[]) // 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, - _T("QUIT"), [] { + _("QUIT"), [] { SDL_Event* quit = new SDL_Event(); quit->type = SDL_QUIT; SDL_PushEvent(quit); @@ -426,7 +426,7 @@ int main(int argc, char* argv[]) ViewController::get()->preload(); if (splashScreen && splashScreenProgress) - window.renderLoadingScreen(_T("Starting UI")); + window.renderLoadingScreen(_("Starting UI")); //choose which GUI to open depending on if an input configuration already exists if (errorMsg == NULL) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 8edfa91578..dc60d62761 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -275,8 +275,12 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) if (!getSelected()->isGameSystem()) ss << "CONFIGURATION"; else - ss << gameCount << " " << _T("GAMES AVAILABLE"); - + { + char strbuf[256]; + snprintf(strbuf, 256, EsLocale::nGetText("%i GAME AVAILABLE", "%i GAMES AVAILABLE", gameCount).c_str(), gameCount); + ss << strbuf; + } + mSystemInfo.setText(ss.str()); }, false, 1); @@ -423,15 +427,15 @@ std::vector SystemView::getHelpPrompts() { std::vector prompts; if (mCarousel.type == VERTICAL || mCarousel.type == VERTICAL_WHEEL) - prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("up/down", _("CHOOSE"))); else - prompts.push_back(HelpPrompt("left/right", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("left/right", _("CHOOSE"))); - prompts.push_back(HelpPrompt("a", _T("SELECT"))); - prompts.push_back(HelpPrompt("x", _T("RANDOM"))); + prompts.push_back(HelpPrompt("a", _("SELECT"))); + prompts.push_back(HelpPrompt("x", _("RANDOM"))); if (!UIModeController::getInstance()->isUIModeKid() && Settings::getInstance()->getBool("ScreenSaverControls")) - prompts.push_back(HelpPrompt("select", _T("LAUNCH SCREENSAVER"))); + prompts.push_back(HelpPrompt("select", _("LAUNCH SCREENSAVER"))); return prompts; } diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 54a40dde85..bdc0da3501 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -468,7 +468,7 @@ bool ViewController::input(InputConfig* config, Input input) if (config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_F5) { mWindow->render(); - mWindow->renderLoadingScreen(_T("Loading..."), -1, 180); + mWindow->renderLoadingScreen(_("Loading..."), -1, 180); ViewController::get()->reloadAll(); mWindow->endRenderLoadingScreen(); @@ -581,14 +581,14 @@ void ViewController::preload() bool splash = Settings::getInstance()->getBool("SplashScreen") && Settings::getInstance()->getBool("SplashScreenProgress"); if (splash) - mWindow->renderLoadingScreen(_T("Preloading UI"), (float)i / (float)max); + mWindow->renderLoadingScreen(_("Preloading UI"), (float)i / (float)max); for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { if (splash) { i++; - mWindow->renderLoadingScreen(_T("Preloading UI"), (float) i / (float)max); + mWindow->renderLoadingScreen(_("Preloading UI"), (float) i / (float)max); } (*it)->resetFilters(); diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 0db4903798..c1af84d24f 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -204,17 +204,17 @@ std::vector BasicGameListView::getHelpPrompts() std::vector prompts; if(Settings::getInstance()->getBool("QuickSystemSelect")) - prompts.push_back(HelpPrompt("left/right", _T("SYSTEM"))); + prompts.push_back(HelpPrompt("left/right", _("SYSTEM"))); - prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); - prompts.push_back(HelpPrompt("a", _T("LAUNCH"))); - prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("up/down", _("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _("LAUNCH"))); + prompts.push_back(HelpPrompt("b", _("BACK"))); if(!UIModeController::getInstance()->isUIModeKid()) - prompts.push_back(HelpPrompt("select", _T("options"))); + prompts.push_back(HelpPrompt("select", _("options"))); if(mRoot->getSystem()->isGameSystem()) - prompts.push_back(HelpPrompt("x", _T("RANDOM"))); + prompts.push_back(HelpPrompt("x", _("RANDOM"))); if(mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) { diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 787fc80d78..b3917c75f5 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -34,29 +34,29 @@ DetailedGameListView::DetailedGameListView(Window* window, FolderData* root) : createImage(); // metadata labels + values - mLblRating.setText(_T("Rating") + ": "); + mLblRating.setText(_("Rating") + ": "); addChild(&mLblRating); addChild(&mRating); - mLblReleaseDate.setText(_T("Released") + ": "); + mLblReleaseDate.setText(_("Released") + ": "); addChild(&mLblReleaseDate); addChild(&mReleaseDate); - mLblDeveloper.setText(_T("Developer") + ": "); + mLblDeveloper.setText(_("Developer") + ": "); addChild(&mLblDeveloper); addChild(&mDeveloper); - mLblPublisher.setText(_T("Publisher")+": "); + mLblPublisher.setText(_("Publisher")+": "); addChild(&mLblPublisher); addChild(&mPublisher); - mLblGenre.setText(_T("Genre") + ": "); + mLblGenre.setText(_("Genre") + ": "); addChild(&mLblGenre); addChild(&mGenre); - mLblPlayers.setText(_T("Players") + ": "); + mLblPlayers.setText(_("Players") + ": "); addChild(&mLblPlayers); addChild(&mPlayers); - mLblLastPlayed.setText(_T("Last played") + ": "); + mLblLastPlayed.setText(_("Last played") + ": "); addChild(&mLblLastPlayed); mLastPlayed.setDisplayRelative(true); addChild(&mLastPlayed); - mLblPlayCount.setText(_T("Times played")); + mLblPlayCount.setText(_("Times played")); addChild(&mLblPlayCount); addChild(&mPlayCount); @@ -295,7 +295,7 @@ std::string DetailedGameListView::getMetadata(FileData* file, std::string name) { std::string ret = file->metadata.get(name); if (ret == "unknown") - return _T(ret); + return _(ret); return ret; } diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 68f2b504e0..bbf70b2627 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -588,18 +588,18 @@ std::vector GridGameListView::getHelpPrompts() std::vector prompts; if (Settings::getInstance()->getBool("QuickSystemSelect")) - prompts.push_back(HelpPrompt("lr", _T("SYSTEM"))); - prompts.push_back(HelpPrompt("up/down/left/right", _T("CHOOSE"))); - prompts.push_back(HelpPrompt("a", _T("LAUNCH"))); + prompts.push_back(HelpPrompt("lr", _("SYSTEM"))); + prompts.push_back(HelpPrompt("up/down/left/right", _("CHOOSE"))); + prompts.push_back(HelpPrompt("a", _("LAUNCH"))); if (!Settings::getInstance()->getBool("HideSystemView")) - prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("b", _("BACK"))); if (!UIModeController::getInstance()->isUIModeKid()) - prompts.push_back(HelpPrompt("select", _T("OPTIONS"))); + prompts.push_back(HelpPrompt("select", _("OPTIONS"))); if (mRoot->getSystem()->isGameSystem()) - prompts.push_back(HelpPrompt("x", _T("RANDOM"))); + prompts.push_back(HelpPrompt("x", _("RANDOM"))); if (mRoot->getSystem()->isGameSystem() && !UIModeController::getInstance()->isUIModeKid()) { diff --git a/es-core/src/EsLocale.cpp b/es-core/src/EsLocale.cpp index 61af250f0f..3fd6f03151 100644 --- a/es-core/src/EsLocale.cpp +++ b/es-core/src/EsLocale.cpp @@ -9,11 +9,33 @@ std::map EsLocale::mItems; std::string EsLocale::mCurrentLanguage = "en"; bool EsLocale::mCurrentLanguageLoaded = true; // By default, 'en' is considered loaded -void EsLocale::setLanguage(const std::string lang) -{ - mCurrentLanguage = lang; - mCurrentLanguageLoaded = false; -} +// List of all possible plural forms here +// https://github.com/translate/l10n-guide/blob/master/docs/l10n/pluralforms.rst +// Test rules without spaces & without parenthesis - there are 19 distinct rules + +PluralRule rules[] = { + { "en", "n!=1", [](int n) { return n != 1 ? 1 : 0; } }, + { "fr", "n>1", [](int n) { return n>1 ? 1 : 0; } }, + { "jp", "0", [](int n) { return 0; } }, + { "ru", "n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&n%100<10||n%100>=20?1:2", [](int n) { return n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2; } }, + { "ar", "n==0?0:n==1?1:n==2?2:n%100>=3&&n%100<=10?3:n%100>=11?4:5", [](int n) { return n == 0 ? 0 : n == 1 ? 1 : n == 2 ? 2 : n % 100 >= 3 && n % 100 <= 10 ? 3 : n % 100 >= 11 ? 4 : 5; } }, + { "pl", "n==1?0:n%10>=2&&n%10<=4&&n%100<10||n%100>=20?1:2", [](int n) { return n == 1 ? 0 : n % 10 >= 2 && n % 10 <= 4 && (n % 100<10 || n % 100 >= 20) ? 1 : 2; } }, + { "ga", "n==1?0:n==2?1:n>2&&n<7?2:n>6&&n<11?3:4", [](int n) { return n == 1 ? 0 : n == 2 ? 1 : (n>2 && n<7) ? 2 : (n>6 && n<11) ? 3 : 4; } }, + { "gd", "n==1||n==11?0:n==2||n==12?1:n>2&&n<20?2:3", [](int n) { return (n == 1 || n == 11) ? 0 : (n == 2 || n == 12) ? 1 : (n>2 && n<20) ? 2 : 3; } }, + { "mk", "n==1||n%10==1?0:1", [](int n) { return n == 1 || n % 10 == 1 ? 0 : 1; } }, + { "is", "n%10!=1||n%100==11", [](int n) { return (n % 10 != 1 || n % 100 == 11) ? 1 : 0; } }, + { "lv", "n%10==1&&n%100!=11?0:n!=0?1:2", [](int n) { return n % 10 == 1 && n % 100 != 11 ? 0 : n != 0 ? 1 : 2; } }, + { "lt", "n%10==1&&n%100!=11?0:n%10>=2&&n%100<10||n%100>=20?1:2", [](int n) { return n % 10 == 1 && n % 100 != 11 ? 0 : n % 10 >= 2 && (n % 100<10 || n % 100 >= 20) ? 1 : 2; } }, + { "mn", "n==0?0:n==1?1:2", [](int n) { return n == 0 ? 0 : n == 1 ? 1 : 2; } }, + { "ro", "n==1?0:n==0||n%100>0&&n%100<20?1:2", [](int n) { return n == 1 ? 0 : (n == 0 || (n % 100>0 && n % 100<20)) ? 1 : 2; } }, + { "cs", "n==1?0:n>=2&&n<=4?1:2", [](int n) { return (n == 1) ? 0 : (n >= 2 && n <= 4) ? 1 : 2; } }, + { "sl", "n%100==1?0:n%100==2?1:n%100==3||n%100==4?2:3", [](int n) { return n % 100 == 1 ? 0 : n % 100 == 2 ? 1 : n % 100 == 3 || n % 100 == 4 ? 2 : 3; } }, + { "mt", "n==1?0:n==0||n%100>1&&n%100<11?1:n%100>10&&n%100<20?2:3", [](int n) { return n == 1 ? 0 : n == 0 || (n % 100>1 && n % 100<11) ? 1 : (n % 100>10 && n % 100<20) ? 2 : 3; } }, + { "cy", "n==1?0:n==2?1:n!=8&&n!=11?2:3", [](int n) { return (n == 1) ? 0 : (n == 2) ? 1 : (n != 8 && n != 11) ? 2 : 3; } }, + { "kw", "n==1?0:n==2?1:n==3?2:3", [](int n) { return (n == 1) ? 0 : (n == 2) ? 1 : (n == 3) ? 2 : 3; } } +}; + +PluralRule EsLocale::mPluralRule = rules[0]; const std::string EsLocale::getText(const std::string text) { @@ -26,6 +48,33 @@ const std::string EsLocale::getText(const std::string text) return text; } +const std::string EsLocale::nGetText(const std::string msgid, const std::string msgid_plural, int n) +{ + if (mCurrentLanguage.empty() || mCurrentLanguage == "en") // English default + return n != 1 ? msgid_plural : msgid; + + if (mPluralRule.rule.empty()) + return n != 1 ? getText(msgid_plural) : getText(msgid); + + checkLocalisationLoaded(); + + int pluralId = mPluralRule.evaluate(n); + if (pluralId == 0) + return getText(msgid); + + auto item = mItems.find(std::to_string(pluralId) + "@" + msgid_plural); + if (item != mItems.cend()) + return item->second; + + item = mItems.find(msgid_plural); + if (item != mItems.cend()) + return item->second; + + return msgid_plural; +} + +const std::vector pluralRules(rules, rules + sizeof(rules) / sizeof(rules[0])); + void EsLocale::checkLocalisationLoaded() { if (mCurrentLanguageLoaded) @@ -37,6 +86,7 @@ void EsLocale::checkLocalisationLoaded() } mCurrentLanguageLoaded = true; + mPluralRule = rules[0]; mItems.clear(); @@ -45,22 +95,68 @@ void EsLocale::checkLocalisationLoaded() return; std::string msgid; - std::string msgstr; + std::string msgid_plural; + std::string line; std::ifstream file(xmlpath); while (std::getline(file, line)) { - if (line.length() > 0 && line[0] == '#') + if (line.find("\"Plural-Forms:") == 0) { - if (!msgid.empty() && !msgstr.empty()) - mItems[msgid] = msgstr; - - msgid = ""; - msgstr = ""; + auto start = line.find("plural="); + if (start != std::string::npos) + { + std::string plural; + + auto end = line.find(";", start + 1); + if (end == std::string::npos) + { + plural = line.substr(start + 7, line.size() - start - 7 - 1); + + std::getline(file, line); + end = line.find(";", start + 1); + if (end != std::string::npos) + plural += line.substr(1, end - 1); + } + else + plural = line.substr(start + 7, end - start - 7); + + plural = Utils::String::replace(plural, " ", ""); + + if (Utils::String::endsWith(plural, ";")) + plural = plural.substr(0, plural.size() - 1); + + // if (Utils::String::startsWith(plural, "(") && Utils::String::endsWith(plural, ")")) + // plural = plural.substr(1, plural.size() - 2); + plural = Utils::String::replace(plural, "(", ""); + plural = Utils::String::replace(plural, ")", ""); + + for (auto iter = pluralRules.cbegin(); iter != pluralRules.cend(); iter++) + { + if (plural == iter->rule) + { + mPluralRule = *iter; + break; + } + } + } } + else if (line.find("msgid_plural") == 0) + { + auto start = line.find("\""); + if (start != std::string::npos && !msgid.empty()) + { + auto end = line.find("\"", start + 1); + if (end != std::string::npos) + msgid_plural = line.substr(start + 1, end - start - 1); + } + } else if (line.find("msgid") == 0) { + msgid = ""; + msgid_plural = ""; + auto start = line.find("\""); if (start != std::string::npos) { @@ -71,18 +167,40 @@ void EsLocale::checkLocalisationLoaded() } else if (line.find("msgstr") == 0) { + std::string idx; + + if (!msgid_plural.empty()) + { + auto idxStart = line.find("["); + if (idxStart != std::string::npos) + { + auto idxEnd = line.find("]", idxStart + 1); + if (idxEnd != std::string::npos) + idx = line.substr(idxStart + 1, idxEnd - idxStart - 1); + } + } + auto start = line.find("\""); if (start != std::string::npos) { auto end = line.find("\"", start + 1); if (end != std::string::npos) - msgstr = line.substr(start + 1, end - start - 1); + { + std::string msgstr = line.substr(start + 1, end - start - 1); + if (!msgid.empty() && !msgstr.empty()) + mItems[msgid] = msgstr; + + if (!msgid_plural.empty() && !msgstr.empty()) + { + if (!idx.empty() && idx != "0") + mItems[idx + "@" + msgid_plural] = msgstr; + else + mItems[msgid_plural] = msgstr; + } + } } } } - - if (!msgid.empty() && !msgstr.empty()) - mItems[msgid] = msgstr; } diff --git a/es-core/src/EsLocale.h b/es-core/src/EsLocale.h index 74f85d309b..dc25c496bd 100644 --- a/es-core/src/EsLocale.h +++ b/es-core/src/EsLocale.h @@ -2,14 +2,22 @@ #include #include +#include #include "utils/StringUtil.h" +struct PluralRule +{ + std::string key; + std::string rule; + std::function evaluate; +}; + class EsLocale { public: static const std::string getText(const std::string text); + static const std::string nGetText(const std::string msgid, const std::string msgid_plural, int n); - static void setLanguage(const std::string lang); static const std::string getLanguage() { return mCurrentLanguage; } private: @@ -17,20 +25,23 @@ class EsLocale static std::map mItems; static std::string mCurrentLanguage; static bool mCurrentLanguageLoaded; + + static PluralRule mPluralRule; }; + #if defined(_WIN32) #define UNICODE_CHARTYPE wchar_t* #define _L(x) L ## x #define _U(x) Utils::String::convertFromWideString(L ## x) - #define _T(x) EsLocale::getText(x) + #define _(x) EsLocale::getText(x) #else #define UNICODE_CHARTYPE char* #define _L(x) x #define _U(x) x - #define _T(x) EsLocale::getText(x) + #define _(x) EsLocale::getText(x) #endif // _WIN32 diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 45e8fabb8f..5c6cdd248b 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -421,7 +421,7 @@ void Window::renderGameLoadingScreen(float opacity, bool swapBuffers) auto& font = mDefaultFonts.at(1); font->reload(); // Ensure font is loaded - TextCache* cache = font->buildTextCache(mCustomSplash.empty() ? _T("Loading...") : mCustomSplash, 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); + TextCache* cache = font->buildTextCache(mCustomSplash.empty() ? _("Loading...") : mCustomSplash, 0, 0, 0x65656500 | (unsigned char)(opacity * 255)); float x = Math::round((Renderer::getScreenWidth() - cache->metrics.size.x()) / 2.0f); float y = Math::round(Renderer::getScreenHeight() * 0.835f); diff --git a/es-core/src/components/BusyComponent.cpp b/es-core/src/components/BusyComponent.cpp index 37cf4f9f44..fd8c2ee7e0 100644 --- a/es-core/src/components/BusyComponent.cpp +++ b/es-core/src/components/BusyComponent.cpp @@ -24,7 +24,7 @@ BusyComponent::BusyComponent(Window* window) : GuiComponent(window), mAnimation = std::make_shared(mWindow); mAnimation->load(&BUSY_ANIMATION_DEF); - mText = std::make_shared(mWindow, _T("WORKING..."), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); + mText = std::make_shared(mWindow, _("WORKING..."), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); // col 0 = animation, col 1 = spacer, col 2 = text mGrid.setEntry(mAnimation, Vector2i(1, 1), false, true); diff --git a/es-core/src/components/ComponentGrid.cpp b/es-core/src/components/ComponentGrid.cpp index 0836cc7f80..9ad364962c 100644 --- a/es-core/src/components/ComponentGrid.cpp +++ b/es-core/src/components/ComponentGrid.cpp @@ -453,11 +453,11 @@ std::vector ComponentGrid::getHelpPrompts() } if(canScrollHoriz && canScrollVert) - prompts.push_back(HelpPrompt("up/down/left/right", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("up/down/left/right", _("CHOOSE"))); else if(canScrollHoriz) - prompts.push_back(HelpPrompt("left/right", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("left/right", _("CHOOSE"))); else if(canScrollVert) - prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("up/down", _("CHOOSE"))); return prompts; } diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index fc9925e581..7efd02702d 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -358,7 +358,7 @@ std::vector ComponentList::getHelpPrompts() } if(addMovePrompt) - prompts.push_back(HelpPrompt("up/down", _T("CHOOSE"))); + prompts.push_back(HelpPrompt("up/down", _("CHOOSE"))); } return prompts; diff --git a/es-core/src/components/DateTimeComponent.cpp b/es-core/src/components/DateTimeComponent.cpp index 5fcfd2ba73..c32d90e84b 100644 --- a/es-core/src/components/DateTimeComponent.cpp +++ b/es-core/src/components/DateTimeComponent.cpp @@ -51,7 +51,7 @@ std::string DateTimeComponent::getDisplayString() const if (mDisplayRelative) { //relative time if(mTime.getTime() == 0) - return _T("never"); + return _("never"); Utils::Time::DateTime now(Utils::Time::now()); Utils::Time::Duration dur(now.getTime() - mTime.getTime()); @@ -71,7 +71,7 @@ std::string DateTimeComponent::getDisplayString() const } if(mTime.getTime() == 0) - return _T("unknown"); + return _("unknown"); return Utils::Time::timeToString(mTime.getTime(), mFormat); } diff --git a/es-core/src/components/DateTimeEditComponent.cpp b/es-core/src/components/DateTimeEditComponent.cpp index 608c4f9c0d..1728d3b172 100644 --- a/es-core/src/components/DateTimeEditComponent.cpp +++ b/es-core/src/components/DateTimeEditComponent.cpp @@ -212,7 +212,7 @@ std::string DateTimeEditComponent::getDisplayString(DisplayMode mode) const { //relative time if(mTime.getTime() == 0) - return _T("never"); + return _("never"); Utils::Time::DateTime now(Utils::Time::now()); Utils::Time::Duration dur(now.getTime() - mTime.getTime()); @@ -234,7 +234,7 @@ std::string DateTimeEditComponent::getDisplayString(DisplayMode mode) const } if(mTime.getTime() == 0) - return _T("unknown"); + return _("unknown"); return Utils::Time::timeToString(mTime, fmt); } diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index b52720343b..df46455c77 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -90,11 +90,11 @@ class OptionListComponent : public GuiComponent mMenu.addRow(row, (!mParent->mMultiSelect && it->selected)); } - mMenu.addButton(_T("BACK"), _T("accept"), [this] { delete this; }); + mMenu.addButton(_("BACK"), _("accept"), [this] { delete this; }); if(mParent->mMultiSelect) { - mMenu.addButton(_T("SELECT ALL"), _T("SELECT ALL"), [this, checkboxes] { + mMenu.addButton(_("SELECT ALL"), _("SELECT ALL"), [this, checkboxes] { for(unsigned int i = 0; i < mParent->mEntries.size(); i++) { mParent->mEntries.at(i).selected = true; @@ -103,7 +103,7 @@ class OptionListComponent : public GuiComponent mParent->onSelectedChanged(); }); - mMenu.addButton(_T("SELECT NONE"), _T("SELECT NONE"), [this, checkboxes] { + mMenu.addButton(_("SELECT NONE"), _("SELECT NONE"), [this, checkboxes] { for(unsigned int i = 0; i < mParent->mEntries.size(); i++) { mParent->mEntries.at(i).selected = false; @@ -134,7 +134,7 @@ class OptionListComponent : public GuiComponent std::vector getHelpPrompts() override { auto prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("b", _("BACK"))); return prompts; } }; diff --git a/es-core/src/components/SliderComponent.cpp b/es-core/src/components/SliderComponent.cpp index 5139795cee..f7f31c9048 100644 --- a/es-core/src/components/SliderComponent.cpp +++ b/es-core/src/components/SliderComponent.cpp @@ -149,6 +149,6 @@ void SliderComponent::onValueChanged() std::vector SliderComponent::getHelpPrompts() { std::vector prompts; - prompts.push_back(HelpPrompt("left/right", _T("CHANGE"))); + prompts.push_back(HelpPrompt("left/right", _("CHANGE"))); return prompts; } diff --git a/es-core/src/guis/GuiMsgBox.cpp b/es-core/src/guis/GuiMsgBox.cpp index daf98e7131..301bb986a9 100644 --- a/es-core/src/guis/GuiMsgBox.cpp +++ b/es-core/src/guis/GuiMsgBox.cpp @@ -37,7 +37,7 @@ GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, }else{ for(auto it = mButtons.cbegin(); it != mButtons.cend(); it++) { - if(Utils::String::toUpper((*it)->getText()) == _T("OK") || Utils::String::toUpper((*it)->getText()) == _T("NO")) + if(Utils::String::toUpper((*it)->getText()) == _("OK") || Utils::String::toUpper((*it)->getText()) == _("NO")) { mAcceleratorFunc = (*it)->getPressedFunc(); break; diff --git a/es-core/src/guis/GuiTextEditPopup.cpp b/es-core/src/guis/GuiTextEditPopup.cpp index 1244ff0871..c0e1ca8b37 100644 --- a/es-core/src/guis/GuiTextEditPopup.cpp +++ b/es-core/src/guis/GuiTextEditPopup.cpp @@ -26,7 +26,7 @@ GuiTextEditPopup::GuiTextEditPopup(Window* window, const std::string& title, con std::vector< std::shared_ptr > buttons; buttons.push_back(std::make_shared(mWindow, acceptBtnText, acceptBtnText, [this, okCallback] { okCallback(mText->getValue()); delete this; })); - buttons.push_back(std::make_shared(mWindow, "CANCEL", "discard changes", [this] { delete this; })); + buttons.push_back(std::make_shared(mWindow, _("CANCEL"), _("discard changes"), [this] { delete this; })); mButtonGrid = makeButtonGrid(mWindow, buttons); @@ -73,6 +73,6 @@ bool GuiTextEditPopup::input(InputConfig* config, Input input) std::vector GuiTextEditPopup::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _T("BACK"))); + prompts.push_back(HelpPrompt("b", _("BACK"))); return prompts; } diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp index c5c936f21e..fa2875c4aa 100644 --- a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp @@ -97,7 +97,7 @@ GuiTextEditPopupKeyboard::GuiTextEditPopupKeyboard(Window* window, const std::st if (atj == "SHIFT") { // Special case for shift key - mShiftButton = std::make_shared(mWindow, "SHIFT", _T("SHIFTS FOR UPPER,LOWER, AND SPECIAL"), [this] { + mShiftButton = std::make_shared(mWindow, "SHIFT", _("SHIFTS FOR UPPER,LOWER, AND SPECIAL"), [this] { shiftKeys(); }); buttons.push_back(mShiftButton); @@ -114,21 +114,21 @@ GuiTextEditPopupKeyboard::GuiTextEditPopupKeyboard(Window* window, const std::st mGrid.setEntry(mKeyboardGrid, Vector2i(0, 2), true, false); // Accept/Cancel buttons - buttons.push_back(std::make_shared(mWindow, _T(acceptBtnText), _T(acceptBtnText), [this, okCallback] { okCallback(mText->getValue()); delete this; })); + buttons.push_back(std::make_shared(mWindow, _(acceptBtnText), _(acceptBtnText), [this, okCallback] { okCallback(mText->getValue()); delete this; })); - buttons.push_back(std::make_shared(mWindow, _T("SPACE"), _T("SPACE"), [this] { + buttons.push_back(std::make_shared(mWindow, _("SPACE"), _("SPACE"), [this] { mText->startEditing(); mText->textInput(" "); mText->stopEditing(); })); - buttons.push_back(std::make_shared(mWindow, _T("DELETE"), _T("DELETE A CHAR"), [this] { + buttons.push_back(std::make_shared(mWindow, _("DELETE"), _("DELETE A CHAR"), [this] { mText->startEditing(); mText->textInput("\b"); mText->stopEditing(); })); - buttons.push_back(std::make_shared(mWindow, _T("CANCEL"), _T("discard changes"), [this] { delete this; })); + buttons.push_back(std::make_shared(mWindow, _("CANCEL"), _("discard changes"), [this] { delete this; })); mButtons = makeButtonGrid(mWindow, buttons); mGrid.setEntry(mButtons, Vector2i(0, 3), true, false); @@ -257,10 +257,10 @@ void GuiTextEditPopupKeyboard::shiftKeys() std::vector GuiTextEditPopupKeyboard::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("x", _T("SHIFT"))); - prompts.push_back(HelpPrompt("b", _T("BACK"))); - prompts.push_back(HelpPrompt("r", _T("SPACE"))); - prompts.push_back(HelpPrompt("l", _T("DELETE"))); + prompts.push_back(HelpPrompt("x", _("SHIFT"))); + prompts.push_back(HelpPrompt("b", _("BACK"))); + prompts.push_back(HelpPrompt("r", _("SPACE"))); + prompts.push_back(HelpPrompt("l", _("DELETE"))); return prompts; } diff --git a/es-core/src/utils/StringUtil.cpp b/es-core/src/utils/StringUtil.cpp index 6d78501c6a..40cf71a9b1 100644 --- a/es-core/src/utils/StringUtil.cpp +++ b/es-core/src/utils/StringUtil.cpp @@ -244,7 +244,9 @@ namespace Utils bool endsWith(const std::string& _string, const std::string& _end) { - return (_string.find(_end) == (_string.size() - _end.size())); + if (_end.size() > _string.size()) return false; + return std::equal(_end.rbegin(), _end.rend(), _string.rbegin()); + //return (_string.find(_end, 0, ) == (_string.size() - _end.size())); } // endsWith From 3d7d687a8a17b937d628815d2be27fc160e823dd Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 19 Aug 2019 13:49:48 +0200 Subject: [PATCH 105/336] Windows : When launching a game, expand environment variables before invoking shellexecute --- es-core/src/platform.cpp | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index 661b919898..1b33468cc6 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -43,7 +43,7 @@ std::string trim(const std::string& str) return str.substr(first, (last - first + 1)); } -void split_cmd(const std::string& cmd, +void split_cmd(std::string cmd, std::string* executable, std::string* parameters) { @@ -51,7 +51,6 @@ void split_cmd(const std::string& cmd, size_t exec_end; c = trim(c); - //boost::trim_all(c); if (c[0] == '\"') { @@ -108,14 +107,22 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo // on Windows we use _wsystem to support non-ASCII paths // which requires converting from utf8 to a wstring - typedef std::codecvt_utf8 convert_type; - std::wstring_convert converter; - std::wstring wchar_str = converter.from_bytes(cmd_utf8); - + //typedef std::codecvt_utf8 convert_type; + //std::wstring_convert converter; + //std::wstring wchar_str = converter.from_bytes(cmd_utf8); + std::string command = cmd_utf8; + + #define BUFFER_SIZE 8192 + + TCHAR szEnvPath[BUFFER_SIZE]; + DWORD dwLen = ExpandEnvironmentStringsA(command.c_str(), szEnvPath, BUFFER_SIZE); + if (dwLen > 0 && dwLen < BUFFER_SIZE) + command = std::string(szEnvPath); + std::string exe; std::string args; - split_cmd(cmd_utf8, &exe, &args); + split_cmd(command, &exe, &args); SHELLEXECUTEINFO lpExecInfo; lpExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); From b3a868d28ec371bccff2d7b11394a2813b788491 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 19 Aug 2019 15:07:27 +0200 Subject: [PATCH 106/336] Shutdown & restart actions : Refactored quitES / processQuitMode to avoid using SDL event mecanism --- es-app/src/guis/GuiMenu.cpp | 9 +++-- es-app/src/main.cpp | 36 ++------------------ es-core/src/platform.cpp | 42 ++++++++++++++++-------- es-core/src/platform.h | 19 +++++------ resources/locale/fr/emulationstation2.po | 8 +++-- 5 files changed, 52 insertions(+), 62 deletions(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index a3ac6a201f..a16a8b3210 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -997,16 +997,19 @@ void GuiMenu::openQuitMenu() ComponentListRow row; if (UIModeController::getInstance()->isUIModeFull()) { +#ifndef WIN32 + // Restart does not work on Windows row.makeAcceptInputHandler([window] { window->pushGui(new GuiMsgBox(window, _("REALLY RESTART?"), _("YES"), [] { Scripting::fireEvent("quit"); - if(quitES(SDL_MSG_RESTART) != 0) + if(quitES(QuitMode::RESTART) != 0) LOG(LogWarning) << "Restart terminated with non-zero result!"; }, _("NO"), nullptr)); }); row.addElement(std::make_shared(window, _("RESTART EMULATIONSTATION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); s->addRow(row); +#endif if(Settings::getInstance()->getBool("ShowExit")) { @@ -1028,7 +1031,7 @@ void GuiMenu::openQuitMenu() [] { Scripting::fireEvent("quit", "reboot"); Scripting::fireEvent("reboot"); - if (quitES(SDL_MSG_REBOOT) != 0) + if (quitES(QuitMode::REBOOT) != 0) LOG(LogWarning) << "Restart terminated with non-zero result!"; }, _("NO"), nullptr)); }); @@ -1041,7 +1044,7 @@ void GuiMenu::openQuitMenu() [] { Scripting::fireEvent("quit", "shutdown"); Scripting::fireEvent("shutdown"); - if (quitES(SDL_MSG_SHUTDOWN) != 0) + if (quitES(QuitMode::SHUTDOWN) != 0) LOG(LogWarning) << "Shutdown terminated with non-zero result!"; }, _("NO"), nullptr)); }); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index b66a530f32..c0fcb7197d 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -480,24 +480,8 @@ int main(int argc, char* argv[]) { InputManager::getInstance()->parseEvent(event, &window); - switch (event.type) - { - case SDL_QUIT: - running = false; - break; - case SDL_QUIT | SDL_MSG_REBOOT: - running = false; - exitMode = SDL_MSG_REBOOT; - break; - case SDL_QUIT | SDL_MSG_SHUTDOWN: + if (event.type == SDL_QUIT) running = false; - exitMode = SDL_MSG_SHUTDOWN; - break; - case SDL_QUIT | SDL_MSG_RESTART: - running = false; - exitMode = SDL_MSG_RESTART; - break; - } } while(SDL_PollEvent(&event)); @@ -575,23 +559,7 @@ int main(int argc, char* argv[]) FreeImage_DeInitialise(); #endif - if (exitMode == SDL_MSG_RESTART) - { - LOG(LogInfo) << "Restarting EmulationStation"; - touch("/tmp/es-restart"); - } - else if (exitMode == SDL_MSG_REBOOT) - { - LOG(LogInfo) << "Rebooting system"; - touch("/tmp/es-sysrestart"); - runRestartCommand(); - } - else if (exitMode == SDL_MSG_SHUTDOWN) - { - LOG(LogInfo) << "Shutting system down"; - touch("/tmp/es-shutdown"); - runShutdownCommand(); - } + processQuitMode(); LOG(LogInfo) << "EmulationStation cleanly shutting down."; diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index 1b33468cc6..a511bdf1d3 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -10,6 +10,7 @@ #include #include "Window.h" +#include "Log.h" #include "GuiComponent.h" #include "utils/FileSystemUtil.h" @@ -198,24 +199,18 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo #endif } -int quitES(int mode) +QuitMode quitMode = QuitMode::QUIT; + +int quitES(QuitMode mode) { + quitMode = mode; + SDL_Event *quit = new SDL_Event(); - quit->type = SDL_QUIT | mode; + quit->type = SDL_QUIT; SDL_PushEvent(quit); return 0; } -/* -int quitES(const std::string& filename) -{ - if (!filename.empty()) - touch(filename); - SDL_Event* quit = new SDL_Event(); - quit->type = SDL_QUIT; - SDL_PushEvent(quit); - return 0; -} -*/ + void touch(const std::string& filename) { #ifdef WIN32 @@ -227,4 +222,25 @@ void touch(const std::string& filename) if (fd >= 0) close(fd); #endif +} + +void processQuitMode() +{ + switch (quitMode) + { + case QuitMode::RESTART: + LOG(LogInfo) << "Restarting EmulationStation"; + touch("/tmp/es-restart"); + break; + case QuitMode::REBOOT: + LOG(LogInfo) << "Rebooting system"; + touch("/tmp/es-sysrestart"); + runRestartCommand(); + break; + case QuitMode::SHUTDOWN: + LOG(LogInfo) << "Shutting system down"; + touch("/tmp/es-shutdown"); + runShutdownCommand(); + break; + } } \ No newline at end of file diff --git a/es-core/src/platform.h b/es-core/src/platform.h index 05dd3cf4e6..23fd7e5b94 100644 --- a/es-core/src/platform.h +++ b/es-core/src/platform.h @@ -25,18 +25,17 @@ class Window; -#define SDL_MSG_RESTART 0X8000 -#define SDL_MSG_SHUTDOWN 0X4000 -#define SDL_MSG_REBOOT 0x2000 +enum QuitMode +{ + QUIT = 0, + RESTART = 1, + SHUTDOWN = 2, + REBOOT = 3 +}; -int runShutdownCommand(); // shut down the system (returns 0 if successful) -int runRestartCommand(); // restart the system (returns 0 if successful) int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window); // run a utf-8 encoded in the shell (requires wstring conversion on Windows) - -int quitES(int mode = 0); - -//int quitES(const std::string& filename); -void touch(const std::string& filename); +int quitES(QuitMode mode = QuitMode::QUIT); +void processQuitMode(); #if defined(WIN32) #include diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index edf08da119..86d3b77ea9 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -709,9 +709,13 @@ msgstr "QUITTER EMULATIONSTATION" msgid "RESTART EMULATIONSTATION" msgstr "REDEMARRER EMULATIONSTATION" +#: +msgid "REALLY QUIT?" +msgstr "ETES-VOUS SUR DE VOULOIR QUITTER ?" + #: msgid "REALLY RESTART?" -msgstr "REDÉMARRER ?" +msgstr "ETES-VOUS SUR DE VOULOIR REDÉMARRER ?" #: msgid "RESTART SYSTEM" @@ -719,7 +723,7 @@ msgstr "REDÉMARRER" #: msgid "REALLY SHUTDOWN?" -msgstr "ÉTEINDRE ?" +msgstr "ETES-VOUS SUR DE VOULOIR ÉTEINDRE ?" #: msgid "SHUTDOWN SYSTEM" From 2e7b99374fea5df57609e22df7da552834d04b71 Mon Sep 17 00:00:00 2001 From: Tsolete <46019970+Tsolete@users.noreply.github.com> Date: Wed, 21 Aug 2019 22:08:03 +0200 Subject: [PATCH 107/336] Update emulationstation2.po Full translation in Spanish. --- resources/locale/es/emulationstation2.po | 298 +++++++++++------------ 1 file changed, 149 insertions(+), 149 deletions(-) diff --git a/resources/locale/es/emulationstation2.po b/resources/locale/es/emulationstation2.po index a52f8f2b11..0d3cdbcc9b 100644 --- a/resources/locale/es/emulationstation2.po +++ b/resources/locale/es/emulationstation2.po @@ -34,7 +34,7 @@ msgstr "PRECARGA UI" #: msgid "VSYNC" -msgstr "SINCRONIZACIÓN VERTICAL" +msgstr "VSYNC" #: msgid "DISPLAY FAVORITES FIRST IN GAMELIST" @@ -46,7 +46,7 @@ msgstr "OPTIMIZAR USO VRMAM CON IMÃGENES" #: msgid "Full" -msgstr "CompletO" +msgstr "Completo" #: msgid "Kiosk" @@ -54,11 +54,11 @@ msgstr "Kiosco" #: msgid "Kid" -msgstr "NIÑO" +msgstr "Niño" #: msgstr "GAME COLLECTION SETTINGS" -msgstr "LISTAS PERSONALIZADAS" +msgstr "OPCIONES LISTAS PERSONALIZADAS" #: msgstr "CANCEL" @@ -102,15 +102,15 @@ msgstr "ACTIVAR SONIDOS DE NAVEGACIÓN" #: msgid "ENABLE VIDEO AUDIO" -msgstr "ACTIVAR AUDIO DE VIDEO" +msgstr "ACTIVAR AUDIO DE VÃDEO" #: msgid "APPLY FILTER" -msgstr "FILTRAR LOS JUEGOS" +msgstr "APLICAR FILTRO" #: msgid "AUTOMATIC GAME COLLECTIONS" -msgstr "COLECCIONES AUTOMÃTICA" +msgstr "COLECCIONES DE JUEGOS AUTOMÃTICAS" #: msgid "SELECT COLLECTIONS" @@ -138,7 +138,7 @@ msgstr "CREAR UNA COLECCIÓN PERSONALIZADA" #: msgid "New Collection Name" -msgstr "nombre nueva colección" +msgstr "Nombre nueva colección" #: msgid "Loading..." @@ -150,11 +150,11 @@ msgstr "AGRUPAR COLECCIONES SIN SISTEMA" #: msgid "SORT CUSTOM COLLECTIONS AND SYSTEMS" -msgstr "ORGANIZACIÓN DE COLECCIONES Y SISTEMAS" +msgstr "ORGANIZAR COLECCIONES Y SISTEMAS PERSONALIZADOS" #: msgid "FINISH EDITING COLLECTION" -msgstr "FINALIZAR COLECCIÓN" +msgstr "FINALIZAR EDICIÓN" #: msgid "PARSE GAMESLISTS ONLY" @@ -162,7 +162,7 @@ msgstr "SOLO LISTA XML DE JUEGOS" #: msgid "POWER SAVER MODES" -msgstr "MODO AHORRO DE ENERGÃA" +msgstr "MODOS AHORRO DE ENERGÃA" #: msgid "SAVE METADATA ON EXIT" @@ -202,7 +202,7 @@ msgstr "Publicado" #: msgid "Developer" -msgstr "Desarrollado" +msgstr "Desarrollador" #: msgid "Publisher" @@ -218,7 +218,7 @@ msgstr "Jugadores" #: msgid "NO GAMES FOUND - SKIP" -msgstr "NO SE ENCUENTRAN JUEGOS - SALTAR " +msgstr "NO SE ENCUENTRAN JUEGOS - SALTAR" #: msgid "RETRY" @@ -238,7 +238,7 @@ msgstr "BUSCAR" #: msgid "SCRAPING IN PROGRESS" -msgstr "RASPADO EN CURSO" +msgstr "ESCANEADO EN CURSO" #: msgid "SYSTEM" @@ -273,16 +273,16 @@ msgid "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" "" -msgstr "¡NO PODEMOS ENCONTRAR SISTEMAS!\n" -""COMPRUEBE QUE SUS CAMINOS SON CORRECTOS EN EL ARCHIVO DE CONFIGURACIÓN DE SISTEMAS, Y SU DIRECTORIO DE JUEGOS TIENE AL MENOS UN JUEGO CON LA EXTENSIÓN CORRECTA.\n" +msgstr "¡NO HEMOS ENCONTRADO NINGÚN SISTEMA!\n" +""COMPRUEBE QUE SUS RUTAS SON CORRECTAS EN EL ARCHIVO DE CONFIGURACIÓN DE SISTEMAS, Y SU DIRECTORIO DE JUEGOS TIENE AL MENOS UN JUEGO CON LA EXTENSIÓN CORRECTA.\n" "\n" "" #: -msgid "%i i ¡JUEGO RASPADO EXITOSAMENTE!" -msgid_plural "%i ¡JUEGOS EXITOSAMENTE RASPADOS!" -msgstr[0] "%i ¡JUEGO RASPADO!" -msgstr[1] "%i ¡JUEGOS RASPADOS!" +msgid "%i i ¡JUEGO ESCANEADO EXITOSAMENTE!" +msgid_plural "%i ¡JUEGOS EXITOSAMENTE ESCANEADOS!" +msgstr[0] "%i ¡JUEGO ESCANEADO!" +msgstr[1] "%i ¡JUEGOS ESCANEADOS!" #: msgid "%i i ¡JUEGO SALTADO!" @@ -296,11 +296,11 @@ msgstr "OK" #: msgid "EDIT METADATA" -msgstr "EDITAR LES METADATOS" +msgstr "EDITAR LOS METADATOS" #: msgid "SCRAPE" -msgstr "RASPAR MEDIA" +msgstr "ESCANEAR" #: msgid "SAVE" @@ -439,7 +439,7 @@ msgstr "SEGURIDAD" #: msgid "ENFORCE SECURITY" -msgstr "REFORCAR LA SEGURIDAD" +msgstr "REFORZAR LA SEGURIDAD" #: msgid "ROOT PASSWORD" @@ -447,7 +447,7 @@ msgstr "CONTRASEÑA DE ROOT" #: msgid "THE SYSTEM WILL NOW REBOOT" -msgstr "EL SISTEMA AHORA REINICIARÃ" +msgstr "EL SISTEMA SE REINICIARÃ" #: msgid "GAMES SETTINGS" @@ -471,7 +471,7 @@ msgstr "AUTO GUARDAR/CARGAR" #: msgid "SHADERS SET" -msgstr "SET DE SOMBRAS" +msgstr "CONJUNTO DE SHADERS" #: msgid "SCANLINES" @@ -483,11 +483,11 @@ msgstr "RETRO" #: msgid "RETROACHIEVEMENTS SETTINGS" -msgstr "AJUSTES DE LOGROS" +msgstr "AJUSTES DE RETROACHEIVMENTS" #: msgid "RETROACHIEVEMENTS" -msgstr "LOGROS" +msgstr "RETROACHIEVEMENTS" #: msgid "HARDCORE MODE" @@ -507,7 +507,7 @@ msgstr "AVANZADO" #: msgid "REALLY UPDATE GAMES LISTS ?" -msgstr "¿SEGURO QUIERE ACTUALIZAR LAS LISTAS DE JUEGOS" +msgstr "¿SEGURO QUE QUIERE ACTUALIZAR LAS LISTAS DE JUEGOS?" #: msgid "UPDATE GAMES LISTS" @@ -515,7 +515,7 @@ msgstr "ACTUALIZAR LISTAS DE JUEGOS" #: msgid "CONTROLLERS SETTINGS" -msgstr "AJUSTE CONTROLADORES" +msgstr "AJUSTE MANDOS" #: msgid "UI SETTINGS" @@ -527,15 +527,15 @@ msgstr "OVERSCAN" #: msgid "LAUNCH SCREENSAVER" -msgstr "SALVAPANTALLA" +msgstr "SALVAPANTALLAS" #: msgid "SCREENSAVER SETTINGS" -msgstr "AJUSTES SALVAPANTALLA" +msgstr "AJUSTES SALVAPANTALLAS" #: msgid "SCREENSAVER AFTER" -msgstr "SALVAPANTALLA DESPUÉS DE:" +msgstr "SALVAPANTALLAS DESPUÉS DE:" #: msgid "TRANSITION STYLE" @@ -543,11 +543,11 @@ msgstr "ESTILO DE TRANSICIÓN" #: msgid "SCREENSAVER BEHAVIOR" -msgstr "COMPORTAMIENTO DE PANTALLA" +msgstr "COMPORTAMIENTO DE SALVAPANTALLAS" #: msgid "SHOW FRAMERATE" -msgstr "MOSTRAR RATIO DE FRAMES" +msgstr "MOSTRAR TASA DE IMÃGENES" #: msgid "ON-SCREEN HELP" @@ -563,7 +563,7 @@ msgstr "SELECCIÓN RAPIDA DE SISTEMA" #: msgid "THEME" -msgstr "SELECCIÓN DE TEMAS" +msgstr "TEMAS" #: msgid "SOUND SETTINGS" @@ -599,7 +599,7 @@ msgstr "OPCIONES DE RED" #: msgid "CONNECTED" -msgstr "CONECTAR" +msgstr "CONECTADO" #: msgid "NOT CONNECTED" @@ -619,7 +619,7 @@ msgstr "NOMBRE HOST" #: msgid "ENABLE WIFI" -msgstr "ACTIVAR WIFI" +msgstr "HABILITAR WIFI" #: msgid "WIFI SSID" @@ -639,19 +639,19 @@ msgstr "ERROR DE CONFIGURACIÓN WIFI" #: msgid "SCRAPER" -msgstr "RASPADOR MEDIA" +msgstr "ESCANEADOR" #: msgid "SCRAPE FROM" -msgstr "RASPAR DESDE:" +msgstr "ESCANEAR DESDE:" #: msgid "SCRAPE RATINGS" -msgstr "CALIFICACIÓN DE RASPADO" +msgstr "ESCANEAR CALIFICACIONES" #: msgid "SCRAPE NOW" -msgstr "RASPAR AHORA"" +msgstr "ESCANEAR AHORA" #: msgid "QUIT" @@ -671,7 +671,7 @@ msgstr "¿SEGURO DE APAGAR?" #: msgid "SHUTDOWN SYSTEM" -msgstr "SISTEMA DE APAGADO" +msgstr "SAPAGAR EL SISTEMA" #: msgid "Emulator" @@ -683,21 +683,21 @@ msgstr "Núcleo" #: msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." -msgstr "USTED VA A CONFIGURAR UN CONTROLADOR, SI TIENE ÚNICAMENTE UN JOYSTICK O UNA CRUCETA DE CONTROL, CONFIGURE SÓLO LAS INSTRUCCIONES QUE TENGA EN SU CONTROLADOR O MANDO. SI NO TIENE UN BOTÓN ESPECIAL PARA HOTKEY, USE SELECT E IGNORE LOS BOTONES QUE NO HAYA EN SU CONTROL. NO PUEDE USAR OTRA VEZ UN BOTÓN YA CONFIGURADO. LOS NOMBRES DE LOS BOTONES ESTÃN BASADOS EN EL MANDO DE SUPER NINTENDO." +msgstr "USTED VA A CONFIGURAR UN MANDO, SI TIENE SÓLO UN JOYSTICK, CONFIGURE LAS TECLAS DE DIRECCIÓN Y SÃLTESE LA CONFIGURACIÓN DEL JOYSTICK MANTENIENDO APRETADO UN BOTÓN. SI NO TIENE UN BOTÓN ESPECIAL PARA HOTKEY, USE SELECT E IGNORE LOS BOTONES QUE NO HAYA EN SU MANDO. NO PUEDE USAR OTRA VEZ UN BOTÓN YA CONFIGURADO MANTENIENDO APRETADO UN BOTÓN. LOS NOMBRES DE LOS BOTONES ESTÃN BASADOS EN EL MANDO DE SUPER NINTENDO." #. GUIMENU #: msgid "CONFIGURE A CONTROLLER" -msgstr "CONFIGURAR UN CONTROLADOR" +msgstr "CONFIGURAR UN MANDO" #. Bluetooth #: msgid "CONTROLLER PAIRED" -msgstr "CONTROLADOR EMPAREJADO" +msgstr "MANDO EMPAREJADO" #: msgid "UNABLE TO PAIR CONTROLLER" -msgstr "NO SE PUEDE EMPAREJAR EL CONTROLADOR +msgstr "NO SE PUEDE EMPAREJAR EL MANDO" #: msgid "AN ERROR OCCURED" @@ -705,19 +705,19 @@ msgstr "OCURRIÓ UN ERROR" #: msgid "NO CONTROLLERS FOUND" -msgstr "NO ENCUENTRA CONTROLADOR" +msgstr "NO SE ENCUENTRA MANDO" #: msgid "PAIR A BLUETOOTH CONTROLLER" -msgstr "EMPAREJAR CONTROLADOR BLUETOOTH" +msgstr "EMPAREJAR MANDO BLUETOOTH" #: msgid "CONTROLLERS LINKS HAVE BEEN DELETED." -msgstr "EL EMPAREJAMIENTO DE CONTROLADORES HA SIDO BORRADO." +msgstr "SE HA BORRADO EL EMPAREJAMIENTO DEL MANDO." #: msgid "FORGET BLUETOOTH CONTROLLERS" -msgstr "OLVIDAR CONTROLADORES BLUETOOTH" +msgstr "OLVIDAR MANDOS BLUETOOTH" #: msgid "INPUT P%i" @@ -729,7 +729,7 @@ msgstr "ELEGIR" #: msgid "SELECT" -msgstr "SELECCIONARR" +msgstr "SELECCIONAR" #: msgid "OPTIONS" @@ -754,7 +754,7 @@ msgstr "EDITAR METADATOS DE ESTE JUEGO" #: msgid "SCRAPE THESE GAMES" -msgstr "RASPAR ESTOS JUEGOS" +msgstr "ESCANEAR ESTOS JUEGOS" #: msgid "All Games" @@ -763,7 +763,7 @@ msgstr "Todos los juegos" #. MISSING SCRAPPER TRANSLATIONS #: msgid "Only missing image" -msgstr "Sólo falta imagen" +msgstr "Sólo imagenes faltantes" #: msgid "FILTER" @@ -771,7 +771,7 @@ msgstr "FILTRO" #: msgid "SCRAPE THESE SYSTEMS" -msgstr "RASPAR ESTOS SISTEMAS" +msgstr "ESCANEAR ESTOS SISTEMAS" #: msgid "SYSTEMS" @@ -788,7 +788,7 @@ msgstr "INICIAR" #: msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" "CONTINUE ANYWAY?" -msgstr "ADVERTENCIA: ALGUNOS DE SUS SISTEMAS SELECCIONADOS NO TIENEN UN CONJUNTO DE SISTEMA.!LOS RESULTADOS PUEDEN SER ERRONEOS!\n" +msgstr "ADVERTENCIA: ALGUNOS DE SUS SISTEMAS SELECCIONADOS NO TIENEN UNA PLATAFORMA.!LOS RESULTADOS PUEDEN SER ERRONEOS!\n" "¿CONTINUAR IGUALMENTE?" #: @@ -805,11 +805,11 @@ msgstr "SE NECESITA CONEXIÓN DE RED" #: msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" -msgstr "ACTUALIZACIÓN DESCARGADA, EL SISTEMA AHORA REINICIARÃ" +msgstr "ACTUALIZACIÓN DESCARGADA, EL SISTEMA SE REINICIARÃ" #: msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" -msgstr "FALLÓ ACTUALIZAR, EL SISTEMA AHORA REINICIARÃ" +msgstr "FALLO ACTUALIZAR, EL SISTEMA SE REINICIARÃ" #: msgid "NO UPDATE AVAILABLE" @@ -817,11 +817,11 @@ msgstr "NO HAY ACTUALIZACIÓN DISPONIBLE" #: msgid "enter emulator" -msgstr "entrar en el emulador" +msgstr "introducir emulador" #: msgid "enter core" -msgstr "entrar en el núcleo" +msgstr "introducir núcleo" #: msgid "Ratio" @@ -829,7 +829,7 @@ msgstr "Ratio" #: msgid "enter ratio" -msgstr "ingresar ratio" +msgstr "introducir ratio" #: msgid "Name" @@ -837,7 +837,7 @@ msgstr "Nombre" #: msgid "enter game name" -msgstr "entrer le nom du jeu" +msgstr "introducir nombre de juego" #: msgid "Description" @@ -853,7 +853,7 @@ msgstr "Imagen" #: msgid "enter path to image" -msgstr "ingresar ruta de la imagen" +msgstr "introducir ruta de la imagen" #: msgid "Thumbnail" @@ -861,11 +861,11 @@ msgstr "Miniatura" #: msgid "enter path to thumbnail" -msgstr "ingresar ruta de la miniatura" +msgstr "introducir ruta de la miniatura" #: msgid "enter rating" -msgstr "ingresar calificación" +msgstr "introducir calificación" #: msgid "Release date" @@ -873,31 +873,31 @@ msgstr "Fecha de lanzamiento" #: msgid "enter release date" -msgstr "ingresar fecha de lanzamiento" +msgstr "introducir fecha de lanzamiento" #: msgid "enter game developer" -msgstr "ingresar desarrollador de juegos" +msgstr "introducir desarrollador del juego" #: msgid "enter game publisher" -msgstr "ingresar el editor del juego" +msgstr "introducir el editor del juego" #: msgid "enter game genre" -msgstr "ingresar al género de juego" +msgstr "introducir el género de juego" #: msgid "enter number of players" -msgstr "ingresar el número de jugadores" +msgstr "introducir el número de jugadores" #: msgid "Favorite" msgstr "Favorito" #: -msgid "añadir a favorito" -msgstr "entrer le favori" +msgid "entrer le favori" +msgstr "añadir a favoritos" #: msgid "Region" @@ -905,7 +905,7 @@ msgstr "Región" #: msgid "enter region" -msgstr "ingresar región" +msgstr "introducir región" #: msgid "Romtype" @@ -913,7 +913,7 @@ msgstr "Tipo de rom" #: msgid "enter romtype" -msgstr "ingresar tipo de rom" +msgstr "introducir tipo de rom" #: msgid "Hidden" @@ -929,7 +929,7 @@ msgstr "NOMBRE" #: msgid "EMULATOR" -msgstr "EMULADORR" +msgstr "EMULADOR" #: msgid "FAVORITE" @@ -937,23 +937,23 @@ msgstr "FAVORITO" #: msgid "set hidden" -msgstr "conjunto oculto" +msgstr "marcar como oculto" #: msgid "Play count" -msgstr "Contador de juego" +msgstr "veces jugado" #: msgid "enter number of times played" -msgstr "ingresar el número de veces jugado" +msgstr "introducir el número de veces jugado" #: msgid "Last played" -msgstr "Último jugado" +msgstr "Última vez jugado" #: msgid "enter last played date" -msgstr "ingresar la última fecha de juego" +msgstr "introducir la última vez que jugó al juego" #: msgid "GAMES AVAILABLE" @@ -967,15 +967,15 @@ msgstr[1] "%i FAVORITOS" #: msgid "SCROLL" -msgstr "SCROLLER" +msgstr "SCROLL" #: msgid "LAUNCH" -msgstr "LANZADOR" +msgstr "LANZAR" #: msgid "Times played" -msgstr "tiempo jugado" +msgstr "Veces jugado" #: msgid "MENU" @@ -999,23 +999,23 @@ msgstr "CALIFICACIÓN, DESCENDENTE" #: msgid "TIMES PLAYED, ASCENDING" -msgstr "TIEMPO JUGADO, ASCENDENTE" +msgstr "VECES JUGADO, ASCENDENTE" #: msgid "TIMES PLAYED, DESCENDING" -msgstr "TIEMPO JUGADO, DESCENDENTE" +msgstr "VECES JUGADO, DESCENDENTE" #: msgid "LAST PLAYED, ASCENDING" -msgstr "ÚLTIMO JUEGO, ASCENDENTE" +msgstr "ÚLTIMA VEZ JUGADO, ASCENDENTE" #: msgid "LAST PLAYED, DESCENDING" -msgstr "ÚLTIMO JUEGO, DESCENDENTE" +msgstr "ÚLTIMA VEZ JUGADO, DESCENDENTE" #: msgid "WORKING..." -msgstr "EN CURSO ..." +msgstr "EN CURSO..." #: msgid "CHANGE" @@ -1032,8 +1032,8 @@ msgstr "ahora" #: msgid "%i sec ago" msgid_plural "%i secs ago" -msgstr[0] "hace %i s" -msgstr[1] "hace %i s" +msgstr[0] "hace %i segundos" +msgstr[1] "hace %i segundos" #: msgid "%i min ago" @@ -1044,8 +1044,8 @@ msgstr[1] "hace %i min" #: msgid "%i hour ago" msgid_plural "%i hours ago" -msgstr[0] "hace %i h" -msgstr[1] "hace %i h" +msgstr[0] "hace %i horas" +msgstr[1] "hace %i horas" #: msgid "%i day ago" @@ -1063,7 +1063,7 @@ msgstr "SELECCIONAR TODO" #: msgid "SELECT NONE" -msgstr "NINGUNA SELECCIÓN" +msgstr "SELECCIONAR NINGUNO" #: msgid "%i SELECTED" @@ -1093,7 +1093,7 @@ msgstr "JOYSTICK 1 ARRIBA" #: msgid "JOYSTICK 1 LEFT" -msgstr "JOYSTICK 1 IZQUIERDA" +msgstr "JOYSTICK 1 IZQUIERDA" #: msgid "JOYSTICK 2 UP" @@ -1121,11 +1121,11 @@ msgstr "CONFIGURACIÓN" #: msgid "KEYBOARD" -msgstr "TECLADOR" +msgstr "TECLADO" #: msgid "GAMEPAD %i" -msgstr "JOYSTICK %i" +msgstr "MANDO %i" #. Config controllers missing translation #: @@ -1146,21 +1146,21 @@ msgstr "BIENVENIDO" #: msgid "CONFIGURE INPUT" -msgstr "CONFIGURAR CONTROLADOR" +msgstr "CONFIGURAR MANDO" #: msgid "%i GAMEPAD DETECTED" msgid_plural "%i GAMEPADS DETECTED" -msgstr[0] "%i CONTROLADOR DETECTADO" -msgstr[1] "%i CONTROLADORES DETECTADOS" +msgstr[0] "%i MANDO DETECTADO" +msgstr[1] "%i MANDOS DETECTADOS" #: msgid "NO GAMEPADS DETECTED" -msgstr "NINGÚN CONTROLADOR DETECTADO" +msgstr "NINGÚN MANDO DETECTADO" #: msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." -msgstr "MANTENGA UN BOTÓN PULSADO PARA CONFIGURAR CONTROL" +msgstr "MANTENGA UN BOTÓN PULSADO PARA CONFIGURAR EL MANDO" #: msgid "PRESS F4 TO QUIT AT ANY TIME." @@ -1172,19 +1172,19 @@ msgstr "PULSE ESC O BOTÓN HOTKEY PARA CANCELAR" #: msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" -msgstr "¿ QUIERES INICIAR KODI MEDIA CENTER ?" +msgstr "¿QUIERES INICIAR KODI MEDIA CENTER?" #: msgid "LOADING..." -msgstr "CARGANDO ..." +msgstr "CARGANDO..." #: msgid "PLEASE WAIT..." -msgstr "POR FAVOR ESPERE ..." +msgstr "POR FAVOR ESPERE..." #: msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" -msgstr "¿ REALMENTE QUIERE APAGAR SIN GUARDAR METADATOS ?" +msgstr "¿REALMENTE QUIERE APAGAR SIN GUARDAR LOS METADATOS?" #: msgid "FAST SHUTDOWN SYSTEM" @@ -1196,7 +1196,7 @@ msgid "WE CAN'T FIND ANY SYSTEMS!\n" "\n" "VISIT EMULATIONSTATION.ORG FOR MORE INFORMATION." msgstr "¡NO PODEMOS ENCONTRAR SISTEMAS!\n" -"COMPRUEBE DIRECTORIO SON CORRECTOS EN EL ARCHIVO DE CONFIGURACIÓN DE SISTEMAS, Y SU DIRECTORIO DE JUEGOS TIENE MÃNIMO UN JUEGO CON EXTENSIÓN CORRECTA.\n" +"COMPRUEBE QUE LA RUTA ES CORRECTA EN EL ARCHIVO DE CONFIGURACIÓN DE SISTEMAS, Y SU DIRECTORIO DE JUEGOS TIENE MÃNIMO UN JUEGO CON EXTENSIÓN CORRECTA.\n" "VISITA EMULATIONSTATION.ORG PARA MÃS INFORMACIÓN." #: @@ -1237,19 +1237,19 @@ msgstr "ACEPTAR EL RESULTADO" #: msgid "FILENAME" -msgstr "ARCHIVO" +msgstr "NOMBRE DE ARCHIVO" #: msgid "RATING" msgstr "VOTOS" #: -msgid "TIEMPO JUGADO" -msgstr "PARTIES JOUÉES" +msgid "PARTIES JOUÉES" +msgstr "VECES JUGADO" #: msgid "LAST PLAYED" -msgstr "ÚLTIMO JUGADO" +msgstr "ÚLTIMA VEZ JUGADO" #: msgid "NUMBER OF PLAYERS" @@ -1257,7 +1257,7 @@ msgstr "NÚMERO DE JUGADORES" #: msgid "DEVELOPER" -msgstr "DESARROLLADORA" +msgstr "DESARROLLADOR" #: msgid "GENRE" @@ -1265,7 +1265,7 @@ msgstr "GÉNERO" #: msgid "SHOW HIDDEN" -msgstr "MOSTRAR OCULTADO" +msgstr "MOSTRAR OCULTO" #: msgid "EXTREM (1400Mhz)" @@ -1282,7 +1282,7 @@ msgstr "ALTO (1300MHz)" #: msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" "IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." -msgstr "LOS CAMBIOS A TURBO Y EXTREMO PUEDEN CAUSAR INESTABILIDAD DEL SISTEMA POR SOBRECARGA, ASà QUE UTILIZALOS BAJO SU PROPIO RIESGO.\n" +msgstr "TURBO Y EXTREMO PUEDEN CAUSAR INESTABILIDAD DEL SISTEMA POR SOBRECARGA, ASà QUE UTILIZALOS BAJO SU PROPIO RIESGO.\n" "SI CONTINUAS, EL SISTEMA REINICIARà AHORA." #: @@ -1293,7 +1293,7 @@ msgstr[1] "%i JUEGOS OCULTOS" #: msgid "Start kodi media player." -msgstr "Iniciar el reproductor multimedia kodi" +msgstr "Iniciar Kodi media player" #: msgid "Select the language for your recalbox, select an external drive to store your games and configurations, check your current version and the free space on your drive" @@ -1301,7 +1301,7 @@ msgstr "Seleccione el idioma para su ES, seleccione una unidad externa para alma #: msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." -msgstr "Configurar la pantalla de juegos, la proporción, los filtros (sombreados), guardar automáticamente y cargar la cuenta de retroachievement." +msgstr "Configurar la pantalla de juegos, la proporción, los filtros (shaders), guardar automáticamente y cargar la cuenta de retroachievement." #: msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." @@ -1321,11 +1321,11 @@ msgstr "Guardar automáticamente el estado al salir de un juego, y cargar autom #: msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." -msgstr "La escala de enteros es escalar por un factor de un número entero, como 2x, 3x, 4x, etc. Esta opción escala la imagen a la escala de enteros más grande por debajo de la resolución establecida. Por ejemplo, si configura la resolución de pantalla completa "Para 1920x1080 y habilitar el escalado de enteros, solo escalará una imagen de 320x240 hasta 1280x960, y dejará bordes negros alrededor. Esto es para mantener una proporción de píxeles de 1: 1 con la imagen de origen original, para que los píxeles no se dupliquen de manera desigual." +msgstr "La escala de enteros es escalar por un factor de un número entero, como 2x, 3x, 4x, etc. Esta opción escala la imagen a la escala de enteros más grande por debajo de la resolución establecida. Por ejemplo, si configura la resolución de pantalla completa "Para 1920x1080 y habilitar el escalado de enteros, solo escalará una imagen de 320x240 hasta 1280x960, y dejará bordes negros alrededor. Esto es para mantener una proporción de píxeles de 1:1 con la imagen de origen original, para que los píxeles no se dupliquen de manera desigual." #: msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." -msgstr "Los sombreadores son como filtros para la representación del juego. Puede seleccionar un conjunto de sombreadores aquí,hay una colección de sombreadores para seleccionar en cada sistema. También puede cambiar el sombreador dentro del juego con HOTKEY + L2 o HOTKEY + R2" +msgstr "Los shaders son como filtros para la renderización del juego. Puede seleccionar un conjunto de shaders aquí, hay una colección de shaders para seleccionar en cada sistema. También puede cambiar el shader dentro del juego con HOTKEY + L2 o HOTKEY + R2" #: msgid "Enable or disable RetroAchievements in games." @@ -1333,7 +1333,7 @@ msgstr "Habilitar o deshabilitar RetroAchievements en juegos." #: msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." -msgstr "El modo Hardcore desactiva * todas las funciones * savestate y rebobinar dentro del emulador: no podrá guardar ni recargar en ningún momento. Deberá completar el juego y obtener los logros por primera vez, al igual que en la consola original. ¡En recompensa por esto, obtendrás el logro estándar y el logro incondicional, en efecto obteniendo el doble de puntos! ¡Un juego normal que vale 400 puntos vale ahora 800 si lo completas en hardcore!\n" +msgstr "El modo Hardcore desactiva *todas las funciones* de savestate y rebobinar dentro del emulador: no podrá guardar ni recargar en ningún momento. Deberá completar el juego y obtener los logros por primera vez, al igual que en la consola original. ¡En recompensa por esto, obtendrás el logro estándar y el logro hardcore, en efecto obteniendo el doble de puntos! ¡Un juego normal que vale 400 puntos vale ahora 800 si lo completas en hardcore!\n" "Por ejemplo: si completas el juego por 400 puntos, entonces tienes la oportunidad de ganar otros 400 en el hardcore." #: @@ -1342,15 +1342,15 @@ msgstr "¡El sitio web retroachievements.org propone desafíos / logros / trofeo #: msgid "Add and configure up to 5 controllers." -msgstr "Agregar y configurar hasta 5 controladores." +msgstr "Agregar y configurar hasta 5 mandos." #: msgid "Start the screensaver after N minutes." -msgstr "Iniciar el protector de pantalla después de N minutos." +msgstr "Iniciar el salvapantallas después de N minutos." #: msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." -msgstr "Establece el comportamiento del protector de pantalla. DIM reducirá la luz de la pantalla, y NEGRO volverá la pantalla a negro." +msgstr "Establece el comportamiento del salvapantallas. DIM reducirá la luz de la pantalla, y NEGRO volverá la pantalla a negro." #: msgid "Shows a help at the bottom of the screen which displays commands you can use." @@ -1362,7 +1362,7 @@ msgstr "Cuando está habilitado, puede cambiar entre sistemas mientras navega po #: msgid "Updates the gamelists, if you added games since the last boot." -msgstr "Actualiza las listas de juego, si agregó juegos desde el último arranque." +msgstr "Actualiza las listas de juegos, si agregó juegos desde el último arranque." #: msgid "Set the volume of the sound output for the frontend and the games." @@ -1370,19 +1370,19 @@ msgstr "Establecer el volumen de la salida de sonido para la interfaz y los jueg #: msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." -msgstr "Obtenga información e imágenes para sus juegos. El raspador descarga metadatos e imágenes para sus juegos desde diferentes servidores mejorando la experiencia del usuario en EmulationStation por completo." +msgstr "Obtenga información e imágenes para sus juegos. El escaneadorr descarga metadatos e imágenes para sus juegos desde diferentes servidores mejorando la experiencia del usuario en EmulationStation por completo." #: msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." -msgstr "Seleccione un servidor para raspar. Se recomienda el servidor SCREENSCRAPER y se basa en www.screenscraper.fr y raspa los datos del juego en su idioma, si están disponibles." +msgstr "Seleccione un servidor para escanear. Se recomienda el servidor SCREENSCRAPER y se basa en www.screenscraper.fr y escanea los datos del juego en su idioma, si están disponibles." #: msgid "Begin the scrape process with the configuration shown below." -msgstr "Comience el proceso de raspado con la configuración que se muestra a continuación." +msgstr "Comience el proceso de escaneado con la configuración que se muestra a continuación." #: msgid "Scrape and display game ratings." -msgstr "Raspar y mostrar clasificaciones de juegos." +msgstr "Escanear y mostrar clasificaciones de juegos." #: msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." @@ -1391,7 +1391,7 @@ msgstr "Configuración avanzada. Asegúrese de que realmente sabe lo que está h #: msgid "Overclock your board to increase the performance.\n" "Overclock settings are tested and validated by the community. Keep in mind that overclocking your board can void your warranty." -msgstr "Overclockee su tablero para aumentar el rendimiento.\ n" +msgstr "Overclockee su placa para aumentar el rendimiento.\ n" "La comunidad ha probado y validado las configuraciones de overclock.\ n" "Tenga en cuenta que el overclocking de su placa puede anular su garantía." @@ -1408,7 +1408,7 @@ msgstr "Esta opción le permite configurar el sistema seleccionado en modo fijo. #: msgid "Always display the basic gamelist view, even if you have scraped your games." -msgstr "Mostrar siempre la vista básica de la lista de juegos, incluso si ha raspado sus juegos." +msgstr "Mostrar siempre la vista básica de la lista de juegos, incluso si ha escaneado sus juegos." #: msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" @@ -1428,15 +1428,15 @@ msgstr "Iniciar automáticamente Kodi en el arranque." #: msgid "Show the framerate in EmulationStation and in game." -msgstr "Mostrar tasa de cuadros en EmulationStation en el juego." +msgstr "Mostrar tasa de imágenes en EmulationStation en el juego." #: msgid "Select which emulator to use when you start a game for this system." -msgstr "Seleccione emulador que inicie juego para este sistema." +msgstr "Seleccione el emulador que inicie juego para este sistema." #: msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." -msgstr "Seleccione qué núcleo usar para el emulador seleccionado. Por ejemplo, el emulador LIBRETRO tiene muchos núcleos para ejecutar juegos de Super Nintendo. El núcleo predeterminado que elija aquí también se puede anular en la configuración específica del juego." +msgstr "Seleccione qué núcleo usará para el emulador seleccionado. Por ejemplo, el emulador LIBRETRO tiene muchos núcleos para ejecutar juegos de Super Nintendo. El núcleo predeterminado que elija aquí también se puede anular en la configuración específica del juego." #: msgid "USE COMPOSED VISUALS" @@ -1496,7 +1496,7 @@ msgstr "EL SISTEMA ESTA ACTUALIZADO" #: msgid "FORCE BASIC GAMELIST VIEW" -msgstr "FORZAR LA VISTA BÃSICA DE JUEGO " +msgstr "FORZAR LA VISTA BÃSICA DE JUEGO" #: msgid "DOWNLOADED" @@ -1508,7 +1508,7 @@ msgstr "VERSIÓN ACTUALIZADA:" #: msgid "UPDATE CHANGELOG:" -msgstr "ACTUALIZAR CAMBIO:" +msgstr "ACTUALIZAR REGISTRO DE CAMBIOS:" #: msgid "MORE DETAILS" @@ -1552,7 +1552,7 @@ msgstr "REGIÓN DE TEMA" #: msgid "THIS THEME HAS NO OPTION" -msgstr "ESTE TEMA NO TIENE OPCIÓNES" +msgstr "ESTE TEMA NO TIENE OPCIONES" #: msgid "MANUAL INPUT" @@ -1567,16 +1567,16 @@ msgid "START KODI" msgstr "COMENZAR KODI" #: -msgid "Muestra la actual versión de actualización disponible." -msgstr "Montre les versions disponibles." +msgid "Montre les versions disponibles." +msgstr "Muestra la actual versión de actualización disponible." #: msgid "Shows the current available update changelog." -msgstr "Montre le changelog de la mise à jour disponible." +msgstr "Muestra el registro de cambios actual." #: -msgid "Configurar un controlador asociado. Su controlador debe estar asociado / enchufado antes." -msgstr "Configurer une manette appairée. Votre manette doit être appairée / branchée auparavant." +msgid "Configurer une manette appairée. Votre manette doit être appairée / branchée auparavant." +msgstr "Configurar un mando asociado. Su mando debe estar asociado / enchufado previamente." #: msgid "Choose if carousel will be animated or not during transitions" @@ -1584,7 +1584,7 @@ msgstr "Elija si el carrusel será animado o no durante las transiciones." #: msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" -msgstr "Seleccione el tipo de transición que se producirá cuando inicie un juego. INSTANTANEO no hará nada, DESVANECER se desaparecerá paulatinammente y la ANIMADO hará zoom en la cubierta del juego (o nombre si no hay información de raspado)." +msgstr "Seleccione el tipo de transición que se producirá cuando inicie un juego. INSTANTANEO no hará nada, DESVANECER se desvanecerá paulatinammente y ANIMADO hará zoom en la cubierta del juego (o nombre si no hay información de escaneado)." #: msgid "Select exisiting colorset options for this theme." @@ -1612,7 +1612,7 @@ msgstr "Configurar opciones de tema si están disponibles" #: msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" -msgstr "Seleccionar región de logotipos, imágenes para sistemas que son diferentes para algunos países. Por ejemplo, Megadrive en EU / Genesis en EE. UU" +msgstr "Seleccionar la región de logotipos y las imágenes para sistemas que son diferentes para algunos países. Por ejemplo, Megadrive en EU / Genesis en EE.UU." #: msgid "Type the name of your SSID if it is hidden or not listed" @@ -1628,11 +1628,11 @@ msgstr "Seleccione la forma en que se clasifica la lista de juegos (alfabéticam #: msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." -msgstr "Cambia entre ver o no solo los juegos favoritos. Para agregar un juego en la lista de favoritos, selecciona el juego y cambia su estado usando 'Y'." +msgstr "Cambia entre ver o no sólo los juegos favoritos. Para agregar un juego en la lista de favoritos, selecciona el juego y cambia su estado usando 'Y'." #: msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." -msgstr "Cambia entre ver o no los juegos ocultos. Para ocultar un juego, edita sus datos y selecciona" Ocultar." +msgstr "Cambia entre ver o no los juegos ocultos. Para ocultar un juego, edita sus datos y selecciona 'Ocultar'." #: msgid "This option display a menu which allows to change game data and many others options." @@ -1640,11 +1640,11 @@ msgstr "Esta opción muestra un menú que permite cambiar los datos del juego y #: msgid "AVAILABLE UPDATE" -msgstr "ACTUALIZACION DISPONIBLE" +msgstr "ACTUALIZACIÓN DISPONIBLE" #: msgid "UPDATE CHANGELOG" -msgstr "ACTUALIZAR CAMBIOs" +msgstr "ACTUALIZAR REGISTRO DE CAMBIOS" #: msgid "CLOCK IN MENU" @@ -1652,7 +1652,7 @@ msgstr "RELOJ EN EL MENÚ" #: msgid "Now playing" -msgstr "Juego en curso" +msgstr "Jugando" #: msgid "DEFAULT (%1%)" @@ -1672,7 +1672,7 @@ msgstr "ARRIBA / ABAJO PARA SALTAR" #: msgid "A TO UNSET" -msgstr "PARA CANCELAR" +msgstr "A PARA CANCELAR" #: msgid "DOWN TO SKIP AND KEEP [%1%]" @@ -1719,8 +1719,8 @@ msgid "TOP/RIGHT" msgstr "ARRIBA/DERECHA" #: -msgid "INFERIOR/DERECHA" -msgstr "BAS/DROITE" +msgid "BAS/DROITE" +msgstr "INFERIOR/DERECHA" #: msgid "BOTTOM/LEFT" @@ -1752,7 +1752,7 @@ msgstr "LOBBY DE JUEGO EN RED" #: msgid "Enable or disable Netplay in games." -msgstr "Habilitar o deshabilitar juego en red en juegos." +msgstr "Habilitar o deshabilitar juego en red." #: msgid "PORT" From 73dfa75ea80539b433ea0d2b3ee97328765ce0d8 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 21 Aug 2019 22:41:38 +0200 Subject: [PATCH 108/336] Localisation : add missing strings + BR --- es-app/src/FileData.cpp | 2 + es-app/src/scrapers/Scraper.cpp | 4 +- es-core/src/guis/GuiDetectDevice.cpp | 17 +- es-core/src/platform.cpp | 9 +- resources/locale/br/emulationstation2.po | 1944 ++++++++++++++++++++++ resources/locale/fr/emulationstation2.po | 12 + 6 files changed, 1979 insertions(+), 9 deletions(-) create mode 100644 resources/locale/br/emulationstation2.po diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index cade4b4925..d7079a1b4f 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -235,6 +235,8 @@ void FileData::launchGame(Window* window) if (customCommandLine.length() > 0) command = customCommandLine; + + command = Utils::String::replace(command, "%EMULATOR%", emulator); command = Utils::String::replace(command, "%CORE%", core); diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index c781781c44..f592314fbf 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -10,8 +10,8 @@ #include const std::map scraper_request_funcs { - { "TheGamesDB", &thegamesdb_generate_json_scraper_requests }, - { "ScreenScraper", &screenscraper_generate_scraper_requests } + { "ScreenScraper", &screenscraper_generate_scraper_requests }, + { "TheGamesDB", &thegamesdb_generate_json_scraper_requests } }; std::unique_ptr startScraperSearch(const ScraperSearchParams& params) diff --git a/es-core/src/guis/GuiDetectDevice.cpp b/es-core/src/guis/GuiDetectDevice.cpp index cd92f1664e..6b9fa1ed74 100644 --- a/es-core/src/guis/GuiDetectDevice.cpp +++ b/es-core/src/guis/GuiDetectDevice.cpp @@ -28,7 +28,7 @@ GuiDetectDevice::GuiDetectDevice(Window* window, bool firstRun, const std::funct addChild(&mGrid); // title - mTitle = std::make_shared(mWindow, firstRun ? "WELCOME" : "CONFIGURE INPUT", + mTitle = std::make_shared(mWindow, firstRun ? _("WELCOME") : _("CONFIGURE INPUT"), ThemeData::getMenuTheme()->Title.font, ThemeData::getMenuTheme()->Title.color, ALIGN_CENTER); mGrid.setEntry(mTitle, Vector2i(0, 0), false, true, Vector2i(1, 1), GridFlags::BORDER_BOTTOM); @@ -37,18 +37,23 @@ GuiDetectDevice::GuiDetectDevice(Window* window, bool firstRun, const std::funct std::stringstream deviceInfo; int numDevices = InputManager::getInstance()->getNumJoysticks(); - if(numDevices > 0) - deviceInfo << numDevices << " GAMEPAD" << (numDevices > 1 ? "S" : "") << " DETECTED"; + if (numDevices > 0) + { + char strbuf[256]; + snprintf(strbuf, 256, EsLocale::nGetText("%i GAMEPAD DETECTED", "%i GAMEPADS DETECTED", numDevices).c_str(), numDevices); // batocera + deviceInfo << strbuf; + } else - deviceInfo << "NO GAMEPADS DETECTED"; + deviceInfo << _("NO GAMEPADS DETECTED"); + mDeviceInfo = std::make_shared(mWindow, deviceInfo.str(), ThemeData::getMenuTheme()->TextSmall.font, 0x999999FF, ALIGN_CENTER); mGrid.setEntry(mDeviceInfo, Vector2i(0, 1), false, true); // message - mMsg1 = std::make_shared(mWindow, "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT.", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color, ALIGN_CENTER); + mMsg1 = std::make_shared(mWindow, _("HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT."), ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color, ALIGN_CENTER); mGrid.setEntry(mMsg1, Vector2i(0, 2), false, true); - const char* msg2str = firstRun ? "PRESS F4 TO QUIT AT ANY TIME." : "PRESS ESC TO CANCEL."; + std::string msg2str = firstRun ? _("PRESS F4 TO QUIT AT ANY TIME.") : _("PRESS ESC TO CANCEL."); mMsg2 = std::make_shared(mWindow, msg2str, ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color, ALIGN_CENTER); mGrid.setEntry(mMsg2, Vector2i(0, 3), false, true); diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index 661b919898..aea0dd1062 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -112,10 +112,17 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo std::wstring_convert converter; std::wstring wchar_str = converter.from_bytes(cmd_utf8); + std::string command = cmd_utf8; + + char expandedString[MAX_PATH]; + DWORD rc = ExpandEnvironmentStringsA(cmd_utf8.c_str(), expandedString, MAX_PATH - 1); + if (0 != rc && MAX_PATH - 1 >= rc) + command = expandedString; + std::string exe; std::string args; - split_cmd(cmd_utf8, &exe, &args); + split_cmd(command, &exe, &args); SHELLEXECUTEINFO lpExecInfo; lpExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); diff --git a/resources/locale/br/emulationstation2.po b/resources/locale/br/emulationstation2.po new file mode 100644 index 0000000000..7224b8a534 --- /dev/null +++ b/resources/locale/br/emulationstation2.po @@ -0,0 +1,1944 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: recalbox-emulationstation\n" +"Language: BR Traduzido por JTulho_LTDA\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +#: +msgid "Loading..." +msgstr "Carregando..." + +#: +msgid "Preloading UI" +msgstr "Iniciando..." + +#: +msgid "GRID SIZE" +msgstr "TAMANHO DA GRADE" + +#: +msgid "DEFAULT GRID SIZE" +msgstr "TAMANHO DE GRADE PADRÃO" + +#: +msgid "UI MODE" +msgstr "MODO DE INTERFACE" + +#: +msgid "PRELOAD UI" +msgstr "Pré-carga UI" + +#: +msgid "VSYNC" +msgstr "SINCRONIZAÇÃO VERTICAL" + +#: +msgid "DISPLAY FAVORITES FIRST IN GAMELIST" +msgstr "EXIBIR FAVORITOS PRIMEIRO EM GAMELIST" + +#: +msgid "OPTIMIZE IMAGES VRAM USE" +msgstr "OTIMIZE O USO DO VRMAM COM IMAGENS" + +#: +msgid "Full" +msgstr "Completo" + +#: +msgid "Kiosk" +msgstr "Quiosque" + +#: +msgid "Kid" +msgstr "Criança" + +#: +msgstr "GAME COLLECTION SETTINGS" +msgstr "LISTAS PERSONALIZADAS" + +#: +msgstr "CANCEL" +msgstr "CANCELAR" + +#: +msgid "automatic" +msgstr "automático" + +#: +msgid "basic" +msgstr "básico" + +#: +msgid "detailed" +msgstr "detalhado" + +#: +msgid "grid" +msgstr "grade" + +#: +msgid "fade" +msgstr "desaparecer" + +#: +msgid "instant" +msgstr "instantáneo" + +#: +msgid "slide" +msgstr "animado" + +#: +msgid "gridex" +msgstr "grade detalhada" + +#: +msgid "ENABLE NAVIGATION SOUNDS" +msgstr "ATIVAR SOM DE NAVEGAÇÃO" + +#: +msgid "ENABLE VIDEO AUDIO" +msgstr "ATIVAR AUDIO DE VIDEO" + +#: +msgid "APPLY FILTER" +msgstr "APLICAR FILTRO" + +#: +msgid "AUTOMATIC GAME COLLECTIONS" +msgstr "COLEÇÕES DE JOGO AUTOMÃTICAS" + +#: +msgid "SELECT COLLECTIONS" +msgstr "SELECIONAR COLEÇÕES" + +#: +msgid "CUSTOM GAME COLLECTIONS" +msgstr "COLEÇÕES PERSONALIZADAS" + +#: +msgid "CREATE NEW CUSTOM COLLECTION FROM THEME" +msgstr "CRIAR UMA COLEÇÃO DE SISTEMAS" + +#: +msgid "GAME COLLECTION SETTINGS" +msgstr "CONFIGURAR COLEÇÃO DE JOGOS" + +#: +msgid "SELECT THEME FOLDER" +msgstr "SELECIONAR PASTA" + +#: +msgid "CREATE NEW CUSTOM COLLECTION" +msgstr "CRIAR NOVA COLEÇÃO PERSONALIZADA" + +#: +msgid "New Collection Name" +msgstr "novo nome da coleção" + +#: +msgid "Loading..." +msgstr "Carregando..." + +#: +msgid "GROUP UNTHEMED CUSTOM COLLECTIONS" +msgstr "COLEÇÕES DE GRUPO SEM SISTEMA" + +#: +msgid "SORT CUSTOM COLLECTIONS AND SYSTEMS" +msgstr "ORGANIZAÇÃO DE COLEÇÕES E SISTEMAS" + +#: +msgid "FINISH EDITING COLLECTION" +msgstr "FINALIZAR COLEÇÃO" + +#: +msgid "PARSE GAMESLISTS ONLY" +msgstr "APENAS LISTA DE JOGOS XML" + +#: +msgid "POWER SAVER MODES" +msgstr "MODO DE POUPANÇA DE ENERGIA" + +#: +msgid "SAVE METADATA ON EXIT" +msgstr "SALVAR METADATA NA SAÃDA" + +#: +msgid "SHOW HIDDEN FILES" +msgstr "MOSTRAR ARQUIVOS OCULTOS" + +#: +msgid "VRAM LIMIT" +msgstr "LIMITE DE VRAM" + +#: +msgid "RESET ALL FILTERS" +msgstr "REINICIAR TODOS OS FILTROS" + +#: +msgid "GAMELIST VIEW STYLE" +msgstr "MODO DE VISUALIZAÇÃO, LISTA DE JOGO" + +#: +msgid "GAME LAUNCH TRANSITION" +msgstr "TRANSIÇÃO PARA LANÇAR O JOGO" + +#: +msgid "RANDOM" +msgstr "ALEATÓRIO" + +#: +msgid "Rating" +msgstr "Avaliação" + +#: +msgid "Released" +msgstr "Publicado" + +#: +msgid "Developer" +msgstr "Desenvolvido" + +#: +msgid "Publisher" +msgstr "Editor" + +#: +msgid "Genre" +msgstr "Género" + +#: +msgid "Players" +msgstr "Jogadores" + +#: +msgid "NO GAMES FOUND - SKIP" +msgstr "NENHUM JOGO ENCONTRADO - PULAR" + +#: +msgid "RETRY" +msgstr "REPETIR" + +#: +msgid "SKIP" +msgstr "PULAR" + +#: +msgid "SEARCH FOR" +msgstr "BUSCAR POR:" + +#: +msgid "SEARCH" +msgstr "BUSCAR" + +#: +msgid "SCRAPING IN PROGRESS" +msgstr "SCRAPING EM PROGRESSO" + +#: +msgid "SYSTEM" +msgstr "SISTEMA" + +#: +msgid "subtitle text" +msgstr "texto de legenda" + +#: +msgid "INPUT" +msgstr "ENTRADA" + +#: +msgid "search" +msgstr "BUSCAR" + +#: +msgid "STOP" +msgstr "PARAR" + +#: +msgid "stop (progress saved)" +msgstr "parar (progresso salvo)" + +#: +msgid "GAME %i OF %i" +msgstr "JOGO %i DE %i" + +#: +msgid "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" +"" +msgstr "¡NÃO PODEMOS ENCONTRAR SISTEMAS! \ N " +"" VERIFIQUE SE SUAS ESTRADAS ESTÃO CORRETAS NO ARQUIVO DE CONFIGURAÇÃO DO SISTEMA E SEU DIRETÓRIO DE JOGOS TEM PELO MENOS UM JOGO COM A EXTENSÃO CORRETA.\n" +"\n" +"" + +#: +msgid "%i i ¡SCRAPER DE JOGO COMPLETO!" +msgid_plural "%i ¡SCRAPERS DE JOGOS COMPLETOS!" +msgstr[0] "%i ¡SCRAPER DO JOGO!" +msgstr[1] "%i ¡SCRAPERS DOS JOGOS!" + +#: +msgid "%i i ¡JOGO PULADO!" +msgid_plural "%i JOGOS PULADOS." +msgstr[0] "%i ¡JOGO SALTADO!" +msgstr[1] "%i ¡JOGOS SALTADOS!" + +#: +msgid "OK" +msgstr "OK" + +#: +msgid "EDIT METADATA" +msgstr "EDITAR METADATA" + +#: +msgid "SCRAPE" +msgstr "SCRAPER DE MEDIA" + +#: +msgid "SAVE" +msgstr "SALVAR" + +#: +msgid "THIS WILL DELETE A FILE!\n" +"ARE YOU SURE?" +msgstr "¡ISTO APAGARà UM ARQUIVO! \ n " +"TEM CERTEZA?" + +#: +msgid "YES" +msgstr "SIM" + +#: +msgid "NO" +msgstr "NÃO" + +#: +msgid "DELETE" +msgstr "APAGAR" + +#: +msgid "SAVE CHANGES?" +msgstr "¿SALVAR MUDANÇAS?" + +#: +msgid "BACK" +msgstr "VOLTAR" + +#: +msgid "CLOSE" +msgstr "FECHAR" + +#: +msgid "MAIN MENU" +msgstr "MENU PRINCIPAL" + +#: +msgid "KODI MEDIA CENTER" +msgstr "KODI MEDIA CENTER" + +#: +msgid "SYSTEM SETTINGS" +msgstr "OPÇÕES DE SISTEMA" + +#: +msgid "VERSION" +msgstr "VERSÃO" + +#: +msgid "DISK USAGE" +msgstr "ESPAÇO EM DISCO" + +#: +msgid "STORAGE DEVICE" +msgstr "DISPOSITIVO DE ARMAZENAMENTO" + +#: +msgid "LANGUAGE" +msgstr "IDIOMA" + +#: +msgid "OVERCLOCK" +msgstr "OVERCLOCK" + +#: +msgid "EXTREM (1100Mhz)" +msgstr "EXTREMO (1100Mhz)" + +#: +msgid "TURBO (1000Mhz)" +msgstr "TURBO (1000Mhz)" + +#: +msgid "HIGH (950Mhz)" +msgstr "ALTO (950Mhz)" + +#: +msgid "NONE (700Mhz)" +msgstr "NENHUM (900Mhz)" + +#: +msgid "TURBO (1050Mhz)+" +msgstr "TURBO (1050Mhz)+" + +#: +msgid "HIGH (1050Mhz)" +msgstr "ALTO (1050Mhz)" + +#: +msgid "NONE (900Mhz)" +msgstr "NENHUM (900Mhz)" + +#: +msgid "NONE (1200Mhz)" +msgstr "NENHUM (1200Mhz)" + +#: +msgid "NONE" +msgstr "NENHUM" + +#. NEW SETTINGS ORGANIZATION +#: +msgid "UPDATES" +msgstr "ATUALIZAÇÕES" + +#: +msgid "AUTO UPDATES" +msgstr "ATUALIZAÇÕES AUTOMÃTICAS" + +#: +msgid "START UPDATE" +msgstr "INICIAR ATUALIZAÇÕES" + +#: +msgid "KODI SETTINGS" +msgstr "OPÇÕES DO KODI" + +#: +msgid "ENABLE KODI" +msgstr "HABILITAR KODI" + +#: +msgid "KODI AT START" +msgstr "AUTO INICIAR KODI" + +#: +msgid "START KODI WITH X" +msgstr "INICIAR KODI COM X" + +#: +msgid "SECURITY" +msgstr "SEGURANÇA" + +#: +msgid "ENFORCE SECURITY" +msgstr "FORTALECER A SEGURANÇA" + +#: +msgid "ROOT PASSWORD" +msgstr "SENHA ROOT" + +#: +msgid "THE SYSTEM WILL NOW REBOOT" +msgstr "O SISTEMA REINICIARà AGORA" + +#: +msgid "GAMES SETTINGS" +msgstr "OPÇÕES DE JOGOS" + +#: +msgid "GAME RATIO" +msgstr "FORMATO DE JOGO" + +#: +msgid "SMOOTH GAMES" +msgstr "JOGOS SUAVES" + +#: +msgid "REWIND" +msgstr "REBOBINAR" + +#: +msgid "AUTO SAVE/LOAD" +msgstr "AUTO SALVAR/CARREGAR" + +#: +msgid "SHADERS SET" +msgstr "CONJUNTO DE SOMBRAS" + +#: +msgid "SCANLINES" +msgstr "SCANLINES" + +#: +msgid "RETRO" +msgstr "RETRO" + +#: +msgid "RETROACHIEVEMENTS SETTINGS" +msgstr "AJUSTAR RECOMPENSAS" + +#: +msgid "RETROACHIEVEMENTS" +msgstr "RECOMPENSAS" + +#: +msgid "HARDCORE MODE" +msgstr "MODO HARDCORE" + +#: +msgid "USERNAME" +msgstr "NOME DE USUARIO" + +#: +msgid "PASSWORD" +msgstr "SENHA" + +#: +msgid "ADVANCED" +msgstr "AVANÇADO" + +#: +msgid "REALLY UPDATE GAMES LISTS ?" +msgstr "¿REALMENTE QUER ATUALIZAR A LISTA DE JOGOS?" + +#: +msgid "UPDATE GAMES LISTS" +msgstr "ATUALIZAR LISTA DE JOGOS" + +#: +msgid "CONTROLLERS SETTINGS" +msgstr "CONFIGURAR CONTROLES" + +#: +msgid "UI SETTINGS" +msgstr "CONFIGURAR INTERFACE" + +#: +msgid "OVERSCAN" +msgstr "OVERSCAN" + +#: +msgid "LAUNCH SCREENSAVER" +msgstr "INICIAR SCREENSAVER" + +#: +msgid "SCREENSAVER SETTINGS" +msgstr "AJUSTAR SCREENSAVER" + +#: +msgid "SCREENSAVER AFTER" +msgstr "SCREENSAVER DEPOIS DE:" + +#: +msgid "TRANSITION STYLE" +msgstr "ESTILO DE TRANSIÇÃO" + +#: +msgid "SCREENSAVER BEHAVIOR" +msgstr "COMPORTAMENTO DE TELA" + +#: +msgid "SHOW FRAMERATE" +msgstr "MOSTRAR TAXA DE QUADROS" + +#: +msgid "ON-SCREEN HELP" +msgstr "AJUDA NA TELA" + +#: +msgid "HIDE WHEN RUNNING GAME" +msgstr "ESCONDER AO EXECUTAR O JOGO" + +#: +msgid "QUICK SYSTEM SELECT" +msgstr "SELEÇÃO RAPIDA DE SISTEMA" + +#: +msgid "THEME" +msgstr "SELEÇÃO DE TEMAS" + +#: +msgid "SOUND SETTINGS" +msgstr "CONFIGURAR AUDIO" + +#: +msgid "SYSTEM VOLUME" +msgstr "VOLUME DO SISTEMA" + +#: +msgid "FRONTEND MUSIC" +msgstr "MÚSICA DE FUNDO" + +#: +msgid "OUTPUT DEVICE" +msgstr "DISPOSITIVO DE SAIDA" + +#: +msgid "HDMI" +msgstr "HDMI" + +#: +msgid "JACK" +msgstr "JACK" + +#: +msgid "AUTO" +msgstr "AUTO" + +#: +msgid "NETWORK SETTINGS" +msgstr "OPÇÕES DE RED" + +#: +msgid "CONNECTED" +msgstr "CONECTADO" + +#: +msgid "NOT CONNECTED" +msgstr "NÃO CONECTADO" + +#: +msgid "STATUS" +msgstr "ESTADO" + +#: +msgid "IP ADDRESS" +msgstr "ENDEREÇO ​​DE IP" + +#: +msgid "HOSTNAME" +msgstr "NOME DO HOST" + +#: +msgid "ENABLE WIFI" +msgstr "ATIVAR WIFI" + +#: +msgid "WIFI SSID" +msgstr "SSID WIFI" + +#: +msgid "WIFI KEY" +msgstr "CHAVE DO WIFI" + +#: +msgid "WIFI ENABLED" +msgstr "WIFI HABILITADO" + +#: +msgid "WIFI CONFIGURATION ERROR" +msgstr "ERRO DE CONFIGURAÇÃO DO WIFI" + +#: +msgid "SCRAPER" +msgstr "SCRAPER DE JOGOS" + +#: +msgid "SCRAPE FROM" +msgstr "SCRAPE DESDE:" + +#: +msgid "SCRAPE RATINGS" +msgstr "CLASSIFICAÇÕES DE SCRAPER" + +#: +msgid "SCRAPE NOW" +msgstr "SCRAPE AGORA"" + +#: +msgid "QUIT" +msgstr "SAIR" + +#: +msgid "REALLY RESTART?" +msgstr "¿QUER REALMENTE REINICIAR?" + +#: +msgid "RESTART SYSTEM" +msgstr "REINICIAR SISTEMA" + +#: +msgid "REALLY SHUTDOWN?" +msgstr "¿QUER REALMENTE DESLIGAR?" + +#: +msgid "SHUTDOWN SYSTEM" +msgstr "DESLIGANDO SISTEMA" + +#: +msgid "Emulator" +msgstr "Emulador" + +#: +msgid "Core" +msgstr "Núcleo" + +#: +msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." +msgstr "CONFIGURAR UM CONTROLE, SE VOCÊ TIVER SÓ UM JOYSTICK OU OUTRO CONTROLADOR, CONFIGURE APENAS OS BOTÕES QUE VOCÊ TEM NO SEU CONTROLADOR OU CONTROLE. SE VOCÊ NÃO TIVER UMA CHAVE ESPECIAL PARA O HOTKEY, USE O SELECT E IGNORE OS BOTÕES QUE NÃO ESTÃO NO SEU CONTROLE. VOCÊ NÃO PODE USAR UM BOTÃO CONFIGURADO NOVAMENTE. O NOME DOS BOTÕES SÃO BASEADOS NO CONTROLE DE SUPER NINTENDO." + +#. GUIMENU +#: +msgid "CONFIGURE A CONTROLLER" +msgstr "CONFIGURAR UM CONTROLE" + +#. Bluetooth +#: +msgid "CONTROLLER PAIRED" +msgstr "CONTROLE PAREADO" + +#: +msgid "UNABLE TO PAIR CONTROLLER" +msgstr "NÃO FOI POSSIVEL PAREAR ESSE CONTROLE" + +#: +msgid "AN ERROR OCCURED" +msgstr "OCORREU UM ERRO" + +#: +msgid "NO CONTROLLERS FOUND" +msgstr "CONTROLE NÃO ENCONTRADO" + +#: +msgid "PAIR A BLUETOOTH CONTROLLER" +msgstr "PAREAR CONTROLE BLUETOOTH" + +#: +msgid "CONTROLLERS LINKS HAVE BEEN DELETED." +msgstr "CONTROLES PAREADOS APAGADOS." + +#: +msgid "FORGET BLUETOOTH CONTROLLERS" +msgstr "ESQUECER CONTROLES BLUETOOTH" + +#: +msgid "INPUT P%i" +msgstr "ENTRADA %i" + +#: +msgid "CHOOSE" +msgstr "ESCOLHER" + +#: +msgid "SELECT" +msgstr "SELECIONAR" + +#: +msgid "OPTIONS" +msgstr "OPÇÕES" + +#: +msgid "JUMP TO LETTER" +msgstr "SALTAR PARA LETRA O NÚMERO" + +#: +msgid "SORT GAMES BY" +msgstr "ORDENAR JOGOS POR:" + +#. FAVORITES +#: +msgid "FAVORITES ONLY" +msgstr "SOMENTE FAVORITOS" + +#: +msgid "EDIT THIS GAME'S METADATA" +msgstr "EDITAR METADATOS PARA ESTE JOGO" + +#: +msgid "SCRAPE THESE GAMES" +msgstr "SCRAPER PARA ESTES JOGOS" + +#: +msgid "All Games" +msgstr "TODOS JOGOS" + +#. MISSING SCRAPPER TRANSLATIONS +#: +msgid "Only missing image" +msgstr "SOMENTE FALTANDO IMAGEM" + +#: +msgid "FILTER" +msgstr "FILTRO" + +#: +msgid "SCRAPE THESE SYSTEMS" +msgstr "SCRAPER PARA ESTES SISTEMAS" + +#: +msgid "SYSTEMS" +msgstr "SISTEMAS" + +#: +msgid "USER DECIDES ON CONFLICTS" +msgstr "ESCOLHER EM CONFLITOS" + +#: +msgid "START" +msgstr "INICIAR" + +#: +msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" +"CONTINUE ANYWAY?" +msgstr "AVISO: ALGUNS DOS SEUS SISTEMAS SELECIONADOS NÃO TÊM UM CONJUNTO DE PLATAFORMA. OS RESULTADOS PODEM SER AINDA MAIS IMPRECISOS QUE O NORMAL! \ N " +"CONTINUE DE QUALQUER MANEIRA?" + +#: +msgid "NO GAMES FIT THAT CRITERIA." +msgstr "NENHUM JOGO COM ESTES CRITÉRIOS." + +#: +msgid "REALLY UPDATE?" +msgstr "¿ATUALIZAR SISTEMA?" + +#: +msgid "NETWORK CONNECTION NEEDED" +msgstr "CONEXÃO DE REDE NECESSÃRIA" + +#: +msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" +msgstr "ATUALIZAÇÃO COMPLETA, O SISTEMA REINICIARà AGORA" + +#: +msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" +msgstr "ATUALIZAÇÃO FALHOU, O SISTEMA REINICIARà AGORA" + +#: +msgid "NO UPDATE AVAILABLE" +msgstr "NENHUMA ATUALIZAÇÃO DISPONIVEL" + +#: +msgid "enter emulator" +msgstr "Entrar no emulador" + +#: +msgid "enter core" +msgstr "Entrar no núcleo" + +#: +msgid "Ratio" +msgstr "Ratio" + +#: +msgid "enter ratio" +msgstr "Entrar ratio" + +#: +msgid "Name" +msgstr "Nome" + +#: +msgid "enter game name" +msgstr "digite o nome do jogo" + +#: +msgid "Description" +msgstr "Descrição" + +#: +msgid "enter description" +msgstr "insira a descrição" + +#: +msgid "Image" +msgstr "Imagem" + +#: +msgid "enter path to image" +msgstr "insira o caminho para a imagem" + +#: +msgid "Thumbnail" +msgstr "Miniatura" + +#: +msgid "enter path to thumbnail" +msgstr "insira o caminho para a miniatura" + +#: +msgid "enter rating" +msgstr "insira a classificação" + +#: +msgid "Release date" +msgstr "Data de lançamento" + +#: +msgid "enter release date" +msgstr "insira a data de lançamento" + +#: +msgid "enter game developer" +msgstr "entrar em desenvolvedor de jogos" + +#: +msgid "enter game publisher" +msgstr "entrar na editora de jogos" + +#: +msgid "enter game genre" +msgstr "entrar no gênero de jogo" + +#: +msgid "enter number of players" +msgstr "insira o número de jogadores" + +#: +msgid "Favorite" +msgstr "Favorito" + +#: +msgid "añadir a favorito" +msgstr "adicionar aos favoritos" + +#: +msgid "Region" +msgstr "Região" + +#: +msgid "enter region" +msgstr "entrar na região" + +#: +msgid "Romtype" +msgstr "Tipo de rom" + +#: +msgid "enter romtype" +msgstr "entrar em tipo de rom" + +#: +msgid "Hidden" +msgstr "Oculto" + +#: +msgid "HIDDEN" +msgstr "OCULTO" + +#: +msgid "NAME" +msgstr "NOME" + +#: +msgid "EMULATOR" +msgstr "EMULADOR" + +#: +msgid "FAVORITE" +msgstr "FAVORITO" + +#: +msgid "set hidden" +msgstr "conjunto oculto" + +#: +msgid "Play count" +msgstr "Contagem de jogos" + +#: +msgid "enter number of times played" +msgstr "insira o número de vezes que jogou" + +#: +msgid "Last played" +msgstr "Último jogado" + +#: +msgid "enter last played date" +msgstr "insira a última data reproduzida" + +#: +msgid "GAMES AVAILABLE" +msgstr "JOGOS DISPONIVEIS" + +#: +msgid "%i FAVORITE" +msgid_plural "%i FAVORITES" +msgstr[0] "%i FAVORITO" +msgstr[1] "%i FAVORITOS" + +#: +msgid "SCROLL" +msgstr "ROLAGEM" + +#: +msgid "LAUNCH" +msgstr "INICIAR" + +#: +msgid "Times played" +msgstr "Tempo jogado" + +#: +msgid "MENU" +msgstr "MENU" + +#: +msgid "FILENAME, ASCENDING" +msgstr "NOME DO ARQUIVO, ASCENDENTE" + +#: +msgid "FILENAME, DESCENDING" +msgstr "NOME DO ARQUIVO, DESCENDENTE" + +#: +msgid "RATING, ASCENDING" +msgstr "CLASSIFICAÇÃO, ASCENDENTE" + +#: +msgid "RATING, DESCENDING" +msgstr "CLASSIFICAÇÃO, DESCENDENTE" + +#: +msgid "TIMES PLAYED, ASCENDING" +msgstr "TEMPO JOGADO, ASCENDENTE" + +#: +msgid "TIMES PLAYED, DESCENDING" +msgstr "TEMPO JOGADO, DESCENDENTE" + +#: +msgid "LAST PLAYED, ASCENDING" +msgstr "ÚLTIMO JOGO, ASCENDENTE" + +#: +msgid "LAST PLAYED, DESCENDING" +msgstr "ÚLTIMO JOGO, DESCENDENTE" + +#: +msgid "WORKING..." +msgstr "TRABALHANDO..." + +#: +msgid "CHANGE" +msgstr "MUDAR" + +#: +msgid "never" +msgstr "nunca" + +#: +msgid "just now" +msgstr "agora" + +#: +msgid "%i sec ago" +msgid_plural "%i secs ago" +msgstr[0] "hace %i s" +msgstr[1] "hace %i s" + +#: +msgid "%i min ago" +msgid_plural "%i mins ago" +msgstr[0] "hace %i min" +msgstr[1] "hace %i min" + +#: +msgid "%i hour ago" +msgid_plural "%i hours ago" +msgstr[0] "hace %i h" +msgstr[1] "hace %i h" + +#: +msgid "%i day ago" +msgid_plural "%i days ago" +msgstr[0] "hace %i día" +msgstr[1] "hace %i días" + +#: +msgid "unknown" +msgstr "desconhecido" + +#: +msgid "SELECT ALL" +msgstr "SELECIONAR TUDO" + +#: +msgid "SELECT NONE" +msgstr "NENHUM SELECIONADO" + +#: +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i SELECIONADO" +msgstr[1] "%i SELECIONADOS" + +#: +msgid "UP" +msgstr "CIMA" + +#: +msgid "DOWN" +msgstr "BAIXO" + +#: +msgid "LEFT" +msgstr "ESQUERDA" + +#: +msgid "RIGHT" +msgstr "DIREITA" + +#: +msgid "JOYSTICK 1 UP" +msgstr "JOYSTICK 1 CIMA" + +#: +msgid "JOYSTICK 1 LEFT" +msgstr "JOYSTICK 1 ESQUERDA" + +#: +msgid "JOYSTICK 2 UP" +msgstr "JOYSTICK 2 CIMA" + +#: +msgid "JOYSTICK 2 LEFT" +msgstr "JOYSTICK 2 ESQUERDA" + +#: +msgid "PAGE UP" +msgstr "L1 (PÃGINA SEGUINTE)" + +#: +msgid "PAGE DOWN" +msgstr "R1 (PÃGINA ANTERIOR)" + +#: +msgid "HOTKEY" +msgstr "HOTKEY" + +#: +msgid "CONFIGURING" +msgstr "CONFIGURAÇÃO" + +#: +msgid "KEYBOARD" +msgstr "TECLADO" + +#: +msgid "GAMEPAD %i" +msgstr "JOYSTICK %i" + +#. Config controllers missing translation +#: +msgid "PRESS ANYTHING" +msgstr "PRESSIONE QUALQUER BOTÃO" + +#: +msgid "ALREADY TAKEN" +msgstr "Jà ESTà EM USO" + +#: +msgid "DISCARD CHANGES" +msgstr "DESCARTAR MUDANÇAS" + +#: +msgid "WELCOME" +msgstr "BEM VINDO" + +#: +msgid "CONFIGURE INPUT" +msgstr "CONFIGURAR CONTROLES" + +#: +msgid "%i GAMEPAD DETECTED" +msgid_plural "%i GAMEPADS DETECTED" +msgstr[0] "%i CONTROLE DETECTADO" +msgstr[1] "%i CONTROLES DETECTADOS" + +#: +msgid "NO GAMEPADS DETECTED" +msgstr "NENHUM CONTROLE DETECTADO" + +#: +msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." +msgstr "SEGURE UM BOTÃO NO SEU DISPOSITIVO PARA CONFIGURAR." + +#: +msgid "PRESS F4 TO QUIT AT ANY TIME." +msgstr "PRESSIONE F4 PARA SAIR A QUALQUER MOMENTO." + +#: +msgid "PRESS ESC OR THE HOTKEY TO CANCEL." +msgstr "PRESSIONE ESC OU O HOTKEY PARA CANCELAR." + +#: +msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" +msgstr "QUER INICIAR O KODI MEDIA CENTER ?" + +#: +msgid "LOADING..." +msgstr "CARREGANDO ..." + +#: +msgid "PLEASE WAIT..." +msgstr "POR FAVOR ESPERE ..." + +#: +msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" +msgstr "¿ VOCÊ REALMENTE QUER DESLIGAR SEM SALVAR MATADATA ?" + +#: +msgid "FAST SHUTDOWN SYSTEM" +msgstr "DESLIGAMENTE RÃPIDO DO SISTEMA" + +#: +msgid "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." +msgstr "¡NENHUM SISTEMA ENCONTRADO!\n" +"VERIFIQUE SE OS SEUS CAMINHOS ESTÃO CORRETOS NO ARQUIVO DE CONFIGURAÇÃO DOS SISTEMAS E SEU DIRETÓRIO DE JOGOS TEM PELO MENOS UM JOGO COM A EXTENSÃO CORRETA.\n" +"SE INSCREVE NO MEU CANAL DO YOUTUBE PARA FORTALECER ( JTulho*LTDA )." + +#: +msgid "ON SCREEN KEYBOARD" +msgstr "TECLADO NA TELA" + +#: +msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" +msgstr "PRESSIONE SHIFT PARA MAIÚSCULAS" + +#: +msgid "SPACE" +msgstr "ESPAÇO" + +#: +msgid "DELETE A CHAR" +msgstr "APAGAR UM CARACTER" + +#: +msgid "SHIFT" +msgstr "MAIÚSCULAS" + +#: +msgid "STOP EDITING" +msgstr "PARE A EDIÇÃO" + +#: +msgid "MOVE CURSOR" +msgstr "MOVER O CURSOR" + +#: +msgid "EDIT" +msgstr "EDITAR" + +#: +msgid "ACCEPT RESULT" +msgstr "ACEITAR RESULTADO" + +#: +msgid "FILENAME" +msgstr "NOME DO ARQUIVO" + +#: +msgid "RATING" +msgstr "AVALIAÇÃO" + +#: +msgid "TIEMPO JUGADO" +msgstr "TEMPO JOGADO" + +#: +msgid "LAST PLAYED" +msgstr "ÚLTIMO JOGADO" + +#: +msgid "NUMBER OF PLAYERS" +msgstr "NÚMERO DE JOGADORES" + +#: +msgid "DEVELOPER" +msgstr "DESENVOLVEDORA" + +#: +msgid "GENRE" +msgstr "GÉNERO" + +#: +msgid "SHOW HIDDEN" +msgstr "MOSTRAR OCULTADO" + +#: +msgid "EXTREM (1400Mhz)" +msgstr "EXTREMO (1400MHz)" + +#: +msgid "TURBO (1350Mhz)" +msgstr "TURBO (1350MHz)" + +#: +msgid "HIGH (1300Mhz)" +msgstr "ALTO (1300MHz)" + +#: +msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" +"IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." +msgstr "TURBO E EXTREMO, DEFINIÇÃO DE OVERCLOCK PODEM PROVOCAR INCERTEZAS DO SISTEMA, PORTANTO UTILIZE-OS POR SUA CONTA E RISCO. \ N " +"SE VOCÊ CONTINUAR, O SISTEMA REINICIARà AGORA." + +#: +msgid "%i GAME HIDDEN" +msgid_plural "%i GAMES HIDDEN" +msgstr[0] "%i JOGO OCULTO" +msgstr[1] "%i JOGOS OCULTOS" + +#: +msgid "Start kodi media player." +msgstr "Iniciar KODI" + +#: +msgid "Select the language for your recalbox, select an external drive to store your games and configurations, check your current version and the free space on your drive" +msgstr "Selecione o idioma para o seu recalbox, selecione uma unidade externa para armazenar seus jogos e configurações, verifique sua versão atual e o espaço livre em sua unidade" + +#: +msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." +msgstr "Configurar exibição de jogos, taxa, filtros (shaders), auto salvar e carregar e conta retroachievement." + +#: +msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." +msgstr "A razão do jogo é a razão entre a largura da imagem e a altura da imagem. Use AUTO para permitir que o emulador escolha a taxa de jogo original, que lhe dará a melhor experiência de retrogaming." + +#: +msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." +msgstr "Suavilize a imagem do jogo. Esta opção torna a imagem mais suave, usando filtragem bilinear." + +#: +msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." +msgstr "Esta opção permite-lhe rebobinar o jogo se for morto por um monstro ou se cometer outro erro. Use o comando HOTKEY + LEFT no jogo para retroceder." + +#: +msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." +msgstr "Auto salvar o estado quando você sair de um jogo e carregar automaticamente o último estado salvo quando você iniciar um jogo." + +#: +msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." +msgstr "A escala inteira é dimensionada por um fator de um número inteiro, como 2x, 3x, 4x, etc. Essa opção dimensiona a imagem até a maior escala inteira abaixo da resolução definida. Por exemplo, se você definir a resolução de tela cheia como 1920x1080 e ativar o dimensionamento de números inteiros, ela só dimensionará uma imagem de 320x240 até 1280x960 e deixará as bordas pretas ao redor. Isso é para manter uma proporção de pixel 1: 1 com a imagem original, para que os pixels não sejam duplicados de forma desigual." + +#: +msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." +msgstr "Shaders são como filtros para a renderização do jogo. Você pode selecionar um shader set aqui, que é uma coleção de shaders selecionados para cada sistema. Você também pode alterar o shader dentro do jogo com HOTKEY + L2 ou HOTKEY + R2." + +#: +msgid "Enable or disable RetroAchievements in games." +msgstr "Habilitar ou desabilitar RetroAchievements em jogos." + +#: +msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." +msgstr "O modo Hardcore desativa as funções * all * savestate e rewind dentro do emulador: você não poderá salvar e recarregar a qualquer momento. Você terá que completar o jogo e obter as conquistas pela primeira vez, assim como no console original. Em recompensa por isso, você vai ganhar tanto a conquista padrão quanto a hardcore, ganhando o dobro de pontos! Um jogo regular no valor de 400 pontos, agora vale 800 se você o completar no hardcore! Por exemplo: se você completar o jogo por 400 pontos, você terá a oportunidade de ganhar outros 400 no hardcore." + +#: +msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" +msgstr "O website retroachievements.org propõe desafios / conquistas / troféus em plataformas como NES, SNES, GB, GBC, GBA, Gênesis / Megadrive, TurboGrafx16 / PCEngine e mais! Crie sua conta no retroachievements.org e comece sua busca por conquistas!" + +#: +msgid "Add and configure up to 5 controllers." +msgstr "Adicione e configure até 5 controles." + +#: +msgid "Start the screensaver after N minutes." +msgstr "Inicie o protetor de tela após N minutos." + +#: +msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." +msgstr "Definir o comportamento do protetor de tela. O DIM reduzirá a luz da tela e o preto tornará a tela preta." + +#: +msgid "Shows a help at the bottom of the screen which displays commands you can use." +msgstr "Mostra uma ajuda na parte inferior da tela que exibe os comandos que você pode usar." + +#: +msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." +msgstr "Quando ativado, você pode alternar entre sistemas enquanto navega em um gamelista pressionando ESQUERDA ou DIREITA." + +#: +msgid "Updates the gamelists, if you added games since the last boot." +msgstr "Atualiza os gamelistas, se você adicionou jogos desde a última inicialização." + +#: +msgid "Set the volume of the sound output for the frontend and the games." +msgstr "Defina o volume da saída de som para o frontend e os jogos." + +#: +msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." +msgstr "Obtenha informações e visuais para seus jogos. O raspador faz o download de metadados e imagens para seus jogos de diferentes servidores e aprimora a experiência do usuário no EmulationStation completamente." + +#: +msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." +msgstr "Selecione um servidor para raspar. O servidor SCREENSCRAPER é recomendado e é baseado em www.screenscraper.fr e raspa os dados do jogo em seu idioma, se disponível." + +#: +msgid "Begin the scrape process with the configuration shown below." +msgstr "Comece o processo de raspar com a configuração mostrada abaixo." + +#: +msgid "Scrape and display game ratings." +msgstr "Raspe e exiba classificações de jogos." + +#: +msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." +msgstr "Configurações avançadas. Por favor, certifique-se de que você realmente sabe o que está fazendo, antes de alterar qualquer valor neste menu." + +#: +msgid "Overclock your board to increase the performance.\n" +"Overclock settings are tested and validated by the community. Keep in mind that overclocking your board can void your warranty." +msgstr "Overclock sua placa para aumentar o desempenho.\n" +"As configurações de overclock são testadas e validadas pela comunidade. Lembre-se de que o overclock da sua placa pode anular sua garantia." + +#: +msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" +"This option highly speeds up boot time, but new games will not be detected." +msgstr "Apenas mostre jogos contidos no arquivo gamelist.xml (localizado em seus diretórios de roms).\n" +"Esta opção acelera bastante o tempo de inicialização, mas novos jogos não serão detectados." + +#: +msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." +msgstr "Esta opção permite que você configure o sistema selecionado no modo fixo.\n" +"Com esta opção ativada, o usuário não pode acessar outros sistemas." + +#: +msgid "Always display the basic gamelist view, even if you have scraped your games." +msgstr "Sempre exiba a visão básica do gamelist, mesmo que você tenha raspado seus jogos." + +#: +msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" +msgstr "Ativar ou desativar o Kodi, personalizar a inicialização do Kodi, ativar o botão X para iniciar o Kodi." + +#: +msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." +msgstr "Ativar ou desativar o Kodi. Se o kodi estiver desativado, você não poderá iniciá-lo com o botão X ou iniciá-lo automaticamente na inicialização. A entrada do menu também será removida." + +#: +msgid "Use the X button to start Kodi." +msgstr "Use o botão X para iniciar o Kodi." + +#: +msgid "Automatically start into Kodi on boot." +msgstr "Iniciar automaticamente no Kodi na inicialização." + +#: +msgid "Show the framerate in EmulationStation and in game." +msgstr "Mostrar o taxa de quadros no EmulationStation e no jogo." + +#: +msgid "Select which emulator to use when you start a game for this system." +msgstr "Selecione qual emulador usar quando você iniciar um jogo para este sistema." + +#: +msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." +msgstr "Selecione qual núcleo usar para o emulador selecionado. Por exemplo, o emulador LIBRETRO possui muitos núcleos para executar os jogos do Super Nintendo. O núcleo padrão que você escolhe aqui também pode ser substituído nas configurações específicas do jogo." + +#: +msgid "USE COMPOSED VISUALS" +msgstr "USE VISUAIS COMPOSTOS" + +#: +msgid "CHECK UPDATES" +msgstr "VERIFICAR ATUALIZAÇÕES" + +#: +msgid "UPDATE TYPE" +msgstr "TIPO DE ACTUALIZAÇÃO" + +#: +msgid "INTEGER SCALE (PIXEL PERFECT)" +msgstr "TELA INTEIRA (PIXEL PERFEITO)" + +#: +msgid "ADVANCED SETTINGS" +msgstr "CONFIGURAÇÕES AVANÇADAS" + +#: +msgid "BOOT SETTINGS" +msgstr "PARÃMETROS DE INICIO" + +#: +msgid "GAMELIST ONLY" +msgstr "SOMENTE LISTA JOGO" + +#: +msgid "START ON SYSTEM" +msgstr "INICIAR EM SISTEMA" + +#: +msgid "BOOT ON GAMELIST" +msgstr "COMECE NA LISTA DE JOGOS" + +#: +msgid "HIDE SYSTEM VIEW" +msgstr "OCULTAR VISUALIZAÇÃO DO SISTEMA" + +#: +msgid "EMULATOR ADVANCED CONFIGURATION" +msgstr "CONFIGURAÇÃO AVANÇADA DO EMULADOR" + +#: +msgid "ADVANCED EMULATOR CONFIGURATION" +msgstr "CONFIGURAÇÃO AVANÇADA DO EMULADOR" + +#: +msgid "HELP" +msgstr "AJUDA" + +#: +msgid "THE SYSTEM IS UP TO DATE" +msgstr "O SISTEMA ESTà ATUALIZADO" + +#: +msgid "FORCE BASIC GAMELIST VIEW" +msgstr "FORCE A VISTA DO JOGO BÃSICO " + +#: +msgid "DOWNLOADED" +msgstr "BAIXANDO" + +#: +msgid "UPDATE VERSION:" +msgstr "VERSÃO ATUALIZADA:" + +#: +msgid "UPDATE CHANGELOG:" +msgstr "ATUALIZAR MUDANÇA:" + +#: +msgid "MORE DETAILS" +msgstr "MAIS DETALHES" + +#: +msgid "CAROUSEL TRANSITIONS" +msgstr "TRANSIÇÕES DE CARROSSEL" + +#: +msgid "ENABLE FILTERS" +msgstr "ATIVAR FILTROS" + +#: +msgid "THEME CONFIGURATION" +msgstr "CONFIGURAÇÕES DE TEMA" + +#: +msgid "THEME COLORSET" +msgstr "COLORSET TEMA" + +#: +msgid "THEME ICONSET" +msgstr "ICONSET TEMA" + +#: +msgid "THEME MENU" +msgstr "MENÚ TEMA" + +#: +msgid "THEME SYSTEMVIEW" +msgstr "TEMA DE SISTEMA" + +#: +msgid "THEME GAMELISTVIEW" +msgstr "TEMA DE LISTA DE JOGOS" + +#: +msgid "THEME REGION" +msgstr "REGIÃO DO TEMA" + +#: +msgid "THIS THEME HAS NO OPTION" +msgstr "ESTE TEMA NÃO TEM OPÇÕES" + +#: +msgid "MANUAL INPUT" +msgstr "ENTRADA MANUAL" + +#: +msgid "AN ERROR OCCURED - DOWNLOADED" +msgstr "OCORREU UM ERRO - BAIXADO" + +#: +msgid "START KODI" +msgstr "INICIAR KODI" + +#: +msgid "Muestra la actual versión de actualización disponible." +msgstr "Mostra a versão de atualização atual disponível." + +#: +msgid "Shows the current available update changelog." +msgstr "Mostra o atual log de alterações de atualização disponível." + +#: +msgid "Configurar un controlador asociado. Su controlador debe estar asociado / enchufado antes." +msgstr "Configure um controlador associado. Seu controlador deve estar associado / conectado antes." + +#: +msgid "Choose if carousel will be animated or not during transitions" +msgstr "Escolha se o carrossel será animado ou não durante as transições." + +#: +msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" +msgstr "Selecione o tipo de transição que ocorre quando você inicia um jogo. INSTANT não fará nada, o FADE desaparecerá no escuro e o SLIDE ampliará a capa do jogo (ou o nome se não houver informações de scraper)." + +#: +msgid "Select exisiting colorset options for this theme." +msgstr "Selecione as opções de cores existentes para este tema." + +#: +msgid "Select exisiting iconset options for this theme." +msgstr "Selecione as opções existentes do conjunto de ícones para este tema." + +#: +msgid "Select exisiting menu style options for this theme." +msgstr "Selecione as opções de estilo de menu existentes para este tema." + +#: +msgid "Select exisiting system view options for this theme." +msgstr "Selecione as opções existentes de visualização do sistema para este tema." + +#: +msgid "Select exisiting gamelist view options for this theme." +msgstr "Selecione as opções de exibição existentes da lista de jogos para este tema." + +#: +msgid "Configure theme options if available." +msgstr "Configurar opções de tema, se disponíveis." + +#: +msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" +msgstr "Selecione Região de logotipos, imagens para sistema diferentes para alguns países. Por exemplo, E.g. Megadrive em EU / Genesis em US" + +#: +msgid "Type the name of your SSID if it is hidden or not listed" +msgstr "Digite o nome do seu SSID se ele estiver oculto ou não listado" + +#: +msgid "Select a letter and the listing will go directly on the first game starting with this letter." +msgstr "Selecione uma carta e a listagem irá diretamente no primeiro jogo começando com esta carta." + +#: +msgid "Select the way the game list is sortered (alphabetically, by notation...)." +msgstr "Selecione o modo como a lista de jogos é filtrada (alfabeticamente, por notação ...)." + +#: +msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." +msgstr "Alterne entre ver ou não apenas os jogos favoritos. Para adicionar um jogo na lista de favoritos, selecione o jogo e alterne seu estado usando 'Y'." + +#: +msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." +msgstr "Alterne entre ver ou não os jogos escondidos. Para ocultar um jogo, edite seus dados e selecione "Ocultar"." + +#: +msgid "This option display a menu which allows to change game data and many others options." +msgstr "Esta opção exibe um menu que permite alterar dados do jogo e muitas outras opções." + +#: +msgid "AVAILABLE UPDATE" +msgstr "ATUALIZAÇÃO DISPONÃVEL" + +#: +msgid "UPDATE CHANGELOG" +msgstr "ATUALIZAR CAMBIOS" + +#: +msgid "CLOCK IN MENU" +msgstr "ASSISTIR NO MENU" + +#: +msgid "Now playing" +msgstr "Jogando agora" + +#: +msgid "DEFAULT (%1%)" +msgstr "PADRÃO (%1%)" + +#: +msgid "INPUT REQUIRED" +msgstr "ENTRADA NECESSÃRIA" + +#: +msgid "(skipped)" +msgstr "(saltado)" + +#: +msgid "UP/DOWN TO SKIP" +msgstr "CIMA / BAIXO PARA SALTAR" + +#: +msgid "A TO UNSET" +msgstr "A PARA CANCELAR" + +#: +msgid "DOWN TO SKIP AND KEEP [%1%]" +msgstr "BAIXO PARA SALTAR E MANTER [%1%]" + +#: +msgid "UP/DOWN TO SKIP AND KEEP [%1%]" +msgstr "CIMA /BAIXO PARA SALTAR E MANTER [%1%]" + +#: +msgid "Set duration of help popups, 0 means no popup." +msgstr "Definir a duração dos janelas emergentes de ajuda, 0 significa que não há popup." + +#: +msgid "HELP POPUP DURATION" +msgstr "AJUDA DA DURAÇÃO DO POPUP" + +#: +msgid "Set duration of music popups, 0 means no popup." +msgstr "Definir duração de popups de música, 0 significa que não há popup." + +#: +msgid "MUSIC POPUP DURATION" +msgstr "DURAÇÃO POPUP MÚSICA" + +#: +msgid "POPUP SETTINGS" +msgstr "CONFIGURAR POPUP" + +#: +msgid "POPUP POSITION" +msgstr "POSIÇÃO DE POPUP" + +#: +msgid "Select the position of popups on screen." +msgstr "Selecione a posição dos popups na tela." + +#: +msgid "Set position and duration of popups." +msgstr "Definir a posição e duração dos popups." + +#: +msgid "TOP/RIGHT" +msgstr "CIMA/DIREITA" + +#: +msgid "INFERIOR/DERECHA" +msgstr "BAIXO/DIREITA" + +#: +msgid "BOTTOM/LEFT" +msgstr "BAIXO/ESQUERDA" + +#: +msgid "TOP/LEFT" +msgstr "CIMA/ESQUERDA" + +#: +msgid "SHOW FOLDERS CONTENT" +msgstr "MOSTRAR O CONTEÚDO DA PASTA" + +#: +msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." +msgstr "Alterne entre ver a estrutura das pastas e ver todos os jogos em um nível superior nivelado." + +#: +msgid "NETPLAY" +msgstr "JOGO EM REDE" + +#: +msgid "NETPLAY SETTINGS" +msgstr "OPÇÕES DE JOGO EM REDE" + +#: +msgid "NETPLAY LOBBY" +msgstr "LOBBY DE JOGO EM REDE" + +#: +msgid "Enable or disable Netplay in games." +msgstr "Ativar ou desativar jogo em rede em jogos." + +#: +msgid "PORT" +msgstr "PORTA" + +#: +msgid "NICKNAME" +msgstr "APELIDO" + +#: +msgid "RELAY SERVER" +msgstr "SERVIDOR DE RETRANSMISSÃO" + +#: +msgid "Enable or disable connections throught relay servers." +msgstr "Ativar ou desativar conexões através de servidores de retransmissão." + +#: +msgid "KODI/NETPLAY" +msgstr "KODI/JUEGO EM REDE" + +#: +msgid "NO GAMES OR NO CONNECTION" +msgstr "NENHUM JOGO OU NENHUMA CONEXÃO" + +#: +msgid "HASH NOW" +msgstr "MANTENHA HASH" + +#: +msgid "HASH THESE SYSTEMS" +msgstr "CRIE HASH DESTES SISTEMAS" + +#: +msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." +msgstr "Adicione um hash de roms em seus gamelistas para ter resultados mais precisos em jogos em rede." + +#: +msgid "HASH ROMS" +msgstr "HASH DE ROMS" + +#: +msgid "Only missing hashs" +msgstr "Apenas faltando hashs" + +#: +msgid "Username" +msgstr "Nome de usuario" + +#: +msgid "Country" +msgstr "País" + +#: +msgid "Latency" +msgstr "Latencia" + +#: +msgid "Host arch." +msgstr "Arco do host" + +#: +msgid "Core ver." +msgstr "Ver. de núcleo" + +#: +msgid "RA ver." +msgstr "Ver. de RA" + +#: +msgid "Can join" +msgstr "Pode juntar" + +#: +msgid "Rom and core match" +msgstr "Match de rom e núcleo" + +#: +msgid "Rom found" +msgstr "Rom encontrado" + +#: +msgid "No rom match" +msgstr "Não á match de rom" + +#: +msgid "Match" +msgstr "Match" + +#: +msgid "No Match" +msgstr "Não á Match" + +#: +msgid "Rom file" +msgstr "Arquivo rom" + +#: +msgid "Rom hash" +msgstr "Hash da rom" + +#: +msgid "THIS COULD TAKE A WHILE, CONFIRM?" +msgstr "ISSO PODE DEMORAR UM POUCO, CONFIRMAR?" + +#: +msgid "bueno" +msgstr "bom" + +#: +msgid "bad" +msgstr "ruim" + +#: +msgid "medium" +msgstr "médio" + +#: +msgid "NETPLAY POPUP DURATION" +msgstr "DURAÇÃO DO POPUP NO JOGO EM REDE" + +#: +msgid "Set duration of netplay popups, 0 means no popup." +msgstr "Definir a duração dos pop-ups de netplay, 0 significa que não há pop-ups." + +#: +msgid "Player" +msgstr "Jogador" + +#: +msgid "Game" +msgstr "Jogo" + +#: +msgid "¡Juega en línea en juegos que se ejecutan en Retroarch como NES, SNES, FBA, Genesis / Megadrive y más!" +msgstr "Jogue online em jogos que rodam em Retroarch como NES, SNES, FBA, Genesis / Megadrive e muito mais!" + +#: +msgid "Rom, hash and core match" +msgstr "Rom, hash e match de núcleo" + +#: +msgid "No core match" +msgstr "Não á match de núcleo" + +#: +msgid "Add a clock in the main menu." +msgstr "Adicione um relógio no menu principal." + +#: +msgid "UPGRADING" +msgstr "ATUALIZANDO" + +#: +msgid "PREPARING" +msgstr "PREPARANDO" + +#: +msgid "Starting UI" +msgstr "Iniciando interface" + +#: +msgid "VERIFYING" +msgstr "VERIFICANDO" + +#: +msgid "EMPTY LIST" +msgstr "LISTA VAZIA" + + +#: Retroarch ratio +msgid "Auto" +msgstr "Automático" + +msgid "Square pixel" +msgstr "Píxel Quadrado" + +msgid "Square pixel" +msgstr "Pixel quadrado" + +msgid "Retroarch Config" +msgstr "Configuração do Retroarch'" + +msgid "Retroarch Custom" +msgstr "Customização do Retroarch'" + +msgid "Core provided" +msgstr "Núcleo proporcionado" + +msgid "Do not set" +msgstr "Não definir" diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index edf08da119..ea008c93c5 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -1011,12 +1011,24 @@ msgstr "entrer la date de la dernière partie" msgid "GAMES AVAILABLE" msgstr "JEUX DISPONIBLES" +#: +msgid "%i GAME AVAILABLE" +msgid_plural "%i GAMES AVAILABLE" +msgstr[0] "%i JEU DISPONIBLE" +msgstr[1] "%i JEUX DISPONIBLES" + #: msgid "%i FAVORITE" msgid_plural "%i FAVORITES" msgstr[0] "%i FAVORI" msgstr[1] "%i FAVORIS" +#: +msgid "%i GAME SUCCESSFULLY SCRAPED!" +msgid_plural "%i GAMES SUCCESSFULLY SCRAPED!" +msgstr[0] "%i JEU SCRAPPÉ !" +msgstr[1] "%i JEUX SCRAPPÉS !" + #: msgid "SCROLL" msgstr "SCROLLER" From 65ca5b3549a41b9f0b8b2dd5682c34c811ea8727 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 23 Aug 2019 17:14:18 +0200 Subject: [PATCH 109/336] Fixes in AudioManager & music management --- es-app/src/FileData.cpp | 2 +- es-core/src/AudioManager.cpp | 48 +++++++++++++++++++++--------------- es-core/src/AudioManager.h | 4 ++- 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index cade4b4925..ba74709478 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -259,7 +259,7 @@ void FileData::launchGame(Window* window) window->init(hideWindow); VolumeControl::getInstance()->init(); - AudioManager::getInstance()->setSystemName(mSystem->getTheme()->getSystemThemeFolder()); // system-specific music -> automatic Init + AudioManager::getInstance()->init(); window->normalizeNextUpdate(); //update number of times the game has been launched diff --git a/es-core/src/AudioManager.cpp b/es-core/src/AudioManager.cpp index 786d8658d1..63397a4264 100644 --- a/es-core/src/AudioManager.cpp +++ b/es-core/src/AudioManager.cpp @@ -10,35 +10,19 @@ std::vector> AudioManager::sSoundVector; std::shared_ptr AudioManager::sInstance; -Mix_Music* AudioManager::mCurrentMusic = NULL; -AudioManager::AudioManager() +AudioManager::AudioManager() : mCurrentMusic(NULL), mInitialized(false) { init(); } AudioManager::~AudioManager() { - //stop all playback - stop(); - stopMusic(); - - // Stop playing all Sounds & reload them - for (unsigned int i = 0; i < sSoundVector.size(); i++) - sSoundVector[i]->deinit(); - - Mix_HookMusicFinished(nullptr); - Mix_HaltMusic(); - - //completely tear down SDL audio. else SDL hogs audio resources and emulators might fail to start... - SDL_CloseAudio(); - SDL_QuitSubSystem(SDL_INIT_AUDIO); + deinit(); } std::shared_ptr & AudioManager::getInstance() { - //check if an AudioManager instance is already created, if not create one - // && Settings::getInstance()->getBool("EnableSounds") if (sInstance == nullptr) sInstance = std::shared_ptr(new AudioManager); @@ -47,6 +31,9 @@ std::shared_ptr & AudioManager::getInstance() void AudioManager::init() { + if (mInitialized) + return; + mRunningFromPlaylist = false; if (SDL_InitSubSystem(SDL_INIT_AUDIO) != 0) @@ -66,12 +53,33 @@ void AudioManager::init() if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 4096) < 0) LOG(LogError) << "MUSIC Error - Unable to open SDLMixer audio: " << SDL_GetError() << std::endl; else + { + mInitialized = true; LOG(LogInfo) << "SDL AUDIO Initialized"; + } } void AudioManager::deinit() { - sInstance = NULL; + if (!mInitialized) + return; + + mInitialized = false; + + //stop all playback + stop(); + stopMusic(); + + // Stop playing all Sounds & reload them + for (unsigned int i = 0; i < sSoundVector.size(); i++) + sSoundVector[i]->deinit(); + + Mix_HookMusicFinished(nullptr); + Mix_HaltMusic(); + + //completely tear down SDL audio. else SDL hogs audio resources and emulators might fail to start... + Mix_CloseAudio(); + SDL_QuitSubSystem(SDL_INIT_AUDIO); } void AudioManager::registerSound(std::shared_ptr & sound) @@ -208,8 +216,8 @@ void AudioManager::stopMusic() return; Mix_HookMusicFinished(nullptr); - Mix_FreeMusic(mCurrentMusic); Mix_HaltMusic(); + Mix_FreeMusic(mCurrentMusic); mCurrentMusic = NULL; } diff --git a/es-core/src/AudioManager.h b/es-core/src/AudioManager.h index 5814aabff4..d0acfb79de 100644 --- a/es-core/src/AudioManager.h +++ b/es-core/src/AudioManager.h @@ -41,7 +41,7 @@ class AudioManager static std::vector> sSoundVector; static std::shared_ptr sInstance; - static Mix_Music* mCurrentMusic; + static void onMusicFinished(); @@ -51,7 +51,9 @@ class AudioManager std::string mSystemName; std::string mCurrentThemeMusicDirectory; bool mRunningFromPlaylist; + bool mInitialized; + Mix_Music* mCurrentMusic; }; #endif // ES_CORE_AUDIO_MANAGER_H From 40cbcf199ed9fdad67a1aa6839e9b42a7e1f419c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 24 Aug 2019 00:16:29 +0200 Subject: [PATCH 110/336] Music : display music name in a popup --- es-app/src/guis/GuiMenu.cpp | 9 ++++++++- es-app/src/main.cpp | 24 ++++++++++++++++++++++++ es-core/src/AudioManager.cpp | 1 + es-core/src/AudioManager.h | 13 +++++++++++++ es-core/src/Settings.cpp | 14 ++------------ es-core/src/Window.cpp | 3 +-- es-core/src/Window.h | 2 +- 7 files changed, 50 insertions(+), 16 deletions(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index a16a8b3210..2e555ed36c 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -239,7 +239,14 @@ void GuiMenu::openSoundSettings() AudioManager::getInstance()->stopMusic(); }); - + //display music titles + auto display_titles = std::make_shared(mWindow); + display_titles->setState(Settings::getInstance()->getBool("MusicTitles")); + s->addWithLabel(_("DISPLAY SONG TITLES"), display_titles); + s->addSaveFunc([display_titles] { + Settings::getInstance()->setBool("MusicTitles", display_titles->getState()); + }); + // music per system auto music_per_system = std::make_shared(mWindow); music_per_system->setState(Settings::getInstance()->getBool("audio.persystem")); diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index c0fcb7197d..88ab25cb0d 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -295,6 +295,28 @@ void onExit() Log::close(); } +#include "AudioManager.h" +#include "guis/GuiInfoPopup.h" + +static std::vector mMessages; + +void processAudioTitles(Window* window) +{ + if (Settings::getInstance()->getBool("MusicTitles")) + { + std::string songName = AudioManager::getInstance()->popSongName(); + if (!songName.empty()) + mMessages.push_back(_("Now playing: ") + songName); + } + + if (!mMessages.empty()) + { + std::string message = mMessages.back(); + mMessages.pop_back(); + window->setInfoPopup(new GuiInfoPopup(window, message, 4000)); + } +} + int main(int argc, char* argv[]) { srand((unsigned int)time(NULL)); @@ -515,6 +537,8 @@ int main(int argc, char* argv[]) if (deltaTime < 0) deltaTime = 1000; + processAudioTitles(&window); + window.update(deltaTime); window.render(); diff --git a/es-core/src/AudioManager.cpp b/es-core/src/AudioManager.cpp index 63397a4264..3a6f0912f5 100644 --- a/es-core/src/AudioManager.cpp +++ b/es-core/src/AudioManager.cpp @@ -203,6 +203,7 @@ void AudioManager::playMusic(std::string path) } Mix_HookMusicFinished(AudioManager::onMusicFinished); + mCurrentSong = Utils::FileSystem::getStem(path); } void AudioManager::onMusicFinished() diff --git a/es-core/src/AudioManager.h b/es-core/src/AudioManager.h index d0acfb79de..269ad847d3 100644 --- a/es-core/src/AudioManager.h +++ b/es-core/src/AudioManager.h @@ -34,6 +34,18 @@ class AudioManager mSystemName = name; } + std::string popSongName() + { + if (!mCurrentSong.empty()) + { + std::string ret = mCurrentSong; + mCurrentSong = ""; + return ret; + } + + return ""; + } + virtual ~AudioManager(); private: @@ -48,6 +60,7 @@ class AudioManager void findMusic(const std::string &path, std::vector& all_matching_files); void playMusic(std::string path); + std::string mCurrentSong; std::string mSystemName; std::string mCurrentThemeMusicDirectory; bool mRunningFromPlaylist; diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 8d665a5879..5b3b63ce95 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -74,8 +74,7 @@ void Settings::setDefaults() mBoolMap["ParseGamelistOnly"] = false; mBoolMap["ShowHiddenFiles"] = false; mBoolMap["DrawFramerate"] = false; - mBoolMap["ShowExit"] = true; - + mBoolMap["ShowExit"] = true; #if WIN32 mBoolMap["ShowOnlyExit"] = true; @@ -105,6 +104,7 @@ void Settings::setDefaults() mBoolMap["SaveGamelistsOnExit"] = true; mBoolMap["OptimizeVRAM"] = true; mBoolMap["ThreadedLoading"] = true; + mBoolMap["MusicTitles"] = true; mBoolMap["Debug"] = false; mBoolMap["DebugGrid"] = false; @@ -222,18 +222,8 @@ void Settings::setDefaults() mBoolMap["audio.bgmusic"] = true; mBoolMap["audio.persystem"] = false; -#ifdef BATOCERA - mStringMap["MusicDirectory"] = "/usr/share/batocera/music"; - mStringMap["UserMusicDirectory"] = "/userdata/music"; -#else -#ifdef RECALBOX - mStringMap["MusicDirectory"] = "/recalbox/share/music/"; - mStringMap["UserMusicDirectory"] = "/recalbox/share/music/"; -#else mStringMap["MusicDirectory"] = ""; mStringMap["UserMusicDirectory"] = ""; -#endif -#endif mDefaultBoolMap = mBoolMap; mDefaultIntMap = mIntMap; diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 5c6cdd248b..609c053d67 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -10,7 +10,6 @@ #include "Scripting.h" #include #include - #include Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), @@ -198,7 +197,7 @@ void Window::input(InputConfig* config, Input input) } void Window::update(int deltaTime) -{ +{ if(mNormalizeNextUpdate) { mNormalizeNextUpdate = false; diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 9fc7e8aca6..6d8dd2c866 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -99,7 +99,7 @@ class Window std::shared_ptr mSplash; std::string mCustomSplash; - std::vector mGuiStack; + std::vector mGuiStack; std::vector< std::shared_ptr > mDefaultFonts; From 2ed50a8ea25b6fdca3483475fed133fcc423644a Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 24 Aug 2019 02:37:06 +0200 Subject: [PATCH 111/336] GuiInfoPopup : Use Menu themings for colors & frame + display at top/right --- es-app/src/guis/GuiInfoPopup.cpp | 22 ++++++++++++++++++---- es-app/src/guis/GuiInfoPopup.h | 1 + es-app/src/main.cpp | 2 +- 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/es-app/src/guis/GuiInfoPopup.cpp b/es-app/src/guis/GuiInfoPopup.cpp index a7fff0951f..4c72798218 100644 --- a/es-app/src/guis/GuiInfoPopup.cpp +++ b/es-app/src/guis/GuiInfoPopup.cpp @@ -9,6 +9,9 @@ GuiInfoPopup::GuiInfoPopup(Window* window, std::string message, int duration) : GuiComponent(window), mMessage(message), mDuration(duration), running(true) { + auto theme = ThemeData::getMenuTheme(); + mBackColor = theme->Background.color; + mFrame = new NinePatchComponent(window); float maxWidth = Renderer::getScreenWidth() * 0.9f; float maxHeight = Renderer::getScreenHeight() * 0.2f; @@ -16,7 +19,7 @@ GuiInfoPopup::GuiInfoPopup(Window* window, std::string message, int duration) : std::shared_ptr s = std::make_shared(mWindow, "", Font::get(FONT_SIZE_MINI), - 0x444444FF, + theme->Text.color, //0x444444FF, ALIGN_CENTER); // we do this to force the text container to resize and return an actual expected popup size @@ -43,9 +46,16 @@ GuiInfoPopup::GuiInfoPopup(Window* window, std::string message, int duration) : float posX = Renderer::getScreenWidth()*0.5f - mSize.x()*0.5f; float posY = Renderer::getScreenHeight() * 0.02f; + // FCA TopRight + posX = Renderer::getScreenWidth()*0.98f - mSize.x()*0.98f; + posY = Renderer::getScreenHeight() * 0.02f; + setPosition(posX, posY, 0); + + mFrame->setImagePath(theme->Background.path); + mFrame->setCenterColor(mBackColor); + mFrame->setEdgeColor(mBackColor); - mFrame->setImagePath(":/frame.png"); mFrame->fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); addChild(mFrame); @@ -108,10 +118,14 @@ bool GuiInfoPopup::updateState() { alpha = ((-(curTime - mStartTime - mDuration)*255)/500); } + + if (alpha > mBackColor & 0xff) + alpha = mBackColor & 0xff; + mGrid->setOpacity((unsigned char)alpha); // apply fade in effect to popup frame - mFrame->setEdgeColor(0xFFFFFF00 | (unsigned char)(alpha)); - mFrame->setCenterColor(0xFFFFFF00 | (unsigned char)(alpha)); + mFrame->setEdgeColor((mBackColor & 0xffffff00) | (unsigned char)(alpha)); + mFrame->setCenterColor((mBackColor & 0xffffff00) | (unsigned char)(alpha)); return true; } \ No newline at end of file diff --git a/es-app/src/guis/GuiInfoPopup.h b/es-app/src/guis/GuiInfoPopup.h index 8d4d3cfd17..fdd2e9ade7 100644 --- a/es-app/src/guis/GuiInfoPopup.h +++ b/es-app/src/guis/GuiInfoPopup.h @@ -24,6 +24,7 @@ class GuiInfoPopup : public GuiComponent, public Window::InfoPopup ComponentGrid* mGrid; NinePatchComponent* mFrame; bool running; + unsigned int mBackColor; }; #endif // ES_APP_GUIS_GUI_INFO_POPUP_H diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 88ab25cb0d..263bbe1064 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -306,7 +306,7 @@ void processAudioTitles(Window* window) { std::string songName = AudioManager::getInstance()->popSongName(); if (!songName.empty()) - mMessages.push_back(_("Now playing: ") + songName); + mMessages.push_back(/*_("Now playing: ") +*/songName); } if (!mMessages.empty()) From 59bc0f21582bd31db63bbdc343d9e5e4676da97f Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 26 Aug 2019 11:44:48 +0200 Subject: [PATCH 112/336] Incomplete commit (why?) --- es-app/src/Gamelist.cpp | 7 +- es-app/src/MetaData.cpp | 20 --- es-app/src/guis/GuiMenu.cpp | 1 + es-app/src/main.cpp | 24 ++- .../src/views/gamelist/GridGameListView.cpp | 2 - es-core/src/GuiComponent.cpp | 2 +- es-core/src/components/ButtonComponent.cpp | 1 - es-core/src/components/ComponentGrid.cpp | 47 ++---- es-core/src/components/ComponentGrid.h | 12 +- es-core/src/components/ComponentList.cpp | 10 +- es-core/src/components/ImageComponent.cpp | 149 ++++++++---------- es-core/src/components/ImageComponent.h | 12 +- es-core/src/components/NinePatchComponent.cpp | 130 +++++---------- es-core/src/components/SliderComponent.cpp | 2 - es-core/src/components/TextComponent.cpp | 1 - es-core/src/components/VideoVlcComponent.cpp | 22 ++- es-core/src/guis/GuiDetectDevice.cpp | 1 - es-core/src/renderers/Renderer.cpp | 90 +++++++++++ es-core/src/renderers/Renderer.h | 6 + es-core/src/resources/Font.cpp | 105 ++++-------- es-core/src/resources/TextureData.cpp | 26 +-- es-core/src/resources/TextureData.h | 4 +- es-core/src/resources/TextureDataManager.cpp | 6 - es-core/src/utils/FileSystemUtil.cpp | 141 +++++++---------- es-core/src/utils/FileSystemUtil.h | 27 ++-- 25 files changed, 367 insertions(+), 481 deletions(-) diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 2dfee03e15..5bf58c9368 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -270,6 +270,8 @@ void updateGamelist(SystemData* system) if (!(*fit)->metadata.wasChanged()) continue; + bool removed = false; + // 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)) @@ -286,7 +288,8 @@ void updateGamelist(SystemData* system) if(nodePath == gamePath) { // found it - root.remove_child(fileNode); + removed = true; + root.remove_child(fileNode); break; } } @@ -294,6 +297,8 @@ void updateGamelist(SystemData* system) // it was either removed or never existed to begin with; either way, we can add it now if (addFileDataNode(root, *fit, tag, system)) ++numUpdated; // Only if really added + else if (removed) + ++numUpdated; // Only if really removed } //now write the file diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index 6fdd0b0c7a..c0b8237ac4 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -254,26 +254,6 @@ float MetaDataList::getFloat(const std::string& key) const return (float)atof(get(key).c_str()); } -bool MetaDataList::isDefault() -{ - if (!mName.empty()) - return false; - - const std::vector& mdd = getMDD(); - - for (auto iter = mdd.cbegin(); iter != mdd.cend(); iter++) - { - auto it = mMap.find(iter->id); - if (it == mMap.end()) - continue; - - if (it->second != iter->defaultValue) - return false; - } - - return true; -} - bool MetaDataList::wasChanged() const { return mWasChanged; diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 2e555ed36c..68968f6caf 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -22,6 +22,7 @@ #include "resources/TextureData.h" #include "animations/LambdaAnimation.h" #include "GuiGamelistOptions.h" // grid sizes +#include "platform.h" GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _("MAIN MENU")), mVersion(window) { diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 263bbe1064..863ab740ef 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -33,24 +33,24 @@ bool scrape_cmdline = false; bool parseArgs(int argc, char* argv[]) { + Utils::FileSystem::setExePath(argv[0]); + + // We need to process --home before any call to Settings::getInstance(), because settings are loaded from homepath for (int i = 1; i < argc; i++) { if (strcmp(argv[i], "--home") == 0) { - if (i == argc - 1) - continue; - - std::string arg = argv[i + 1]; - if (arg.find("-") == 0) - continue; + if (i >= argc - 1) + { + std::cerr << "Invalid home path supplied."; + return false; + } Utils::FileSystem::setHomePath(argv[i + 1]); - i++; // skip vsync value + break; } } - - Settings::getInstance()->setString("ExePath", argv[0]); - + for(int i = 1; i < argc; i++) { if (strcmp(argv[i], "--monitor") == 0) @@ -402,10 +402,6 @@ int main(int argc, char* argv[]) return 1; } - std::string glExts = (const char*)glGetString(GL_EXTENSIONS); - LOG(LogInfo) << "Checking available OpenGL extensions..."; - LOG(LogInfo) << " ARB_texture_non_power_of_two: " << (glExts.find("ARB_texture_non_power_of_two") != std::string::npos ? "ok" : "MISSING"); - if (splashScreen) window.renderLoadingScreen(_("Loading...")); } diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index bbf70b2627..63d64cdb00 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -393,8 +393,6 @@ void GridGameListView::updateInfoPanel() { FileData* file = (mGrid.size() == 0 || mGrid.isScrolling()) ? NULL : mGrid.getSelected(); - TRACE("GridGameListView::updateInfoPanel " << (file ? "null" : "ok")); - bool fadingOut; if (file == NULL) { diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index 9fd81fe3e7..dc78dbe02c 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -3,7 +3,7 @@ #include "animations/Animation.h" #include "animations/AnimationController.h" #include "Log.h" -#include "Renderer.h" +#include "renderers/Renderer.h" #include "ThemeData.h" #include "Window.h" #include diff --git a/es-core/src/components/ButtonComponent.cpp b/es-core/src/components/ButtonComponent.cpp index f93d1690e1..db1601666a 100644 --- a/es-core/src/components/ButtonComponent.cpp +++ b/es-core/src/components/ButtonComponent.cpp @@ -2,7 +2,6 @@ #include "resources/Font.h" #include "utils/StringUtil.h" -#include "Renderer.h" ButtonComponent::ButtonComponent(Window* window, const std::string& text, const std::string& helpText, const std::function& func, bool upperCase) : GuiComponent(window), mBox(window, ThemeData::getMenuTheme()->Icons.button), diff --git a/es-core/src/components/ComponentGrid.cpp b/es-core/src/components/ComponentGrid.cpp index 9ad364962c..df038257af 100644 --- a/es-core/src/components/ComponentGrid.cpp +++ b/es-core/src/components/ComponentGrid.cpp @@ -1,6 +1,4 @@ #include "components/ComponentGrid.h" - -#include "Renderer.h" #include "Settings.h" #include "ThemeData.h" @@ -155,6 +153,8 @@ void ComponentGrid::updateSeparators() bool drawAll = Settings::getInstance()->getBool("DebugGrid"); + const unsigned int color = Renderer::convertColor(mSeparatorColor); + Vector2f pos; Vector2f size; for(auto it = mCells.cbegin(); it != mCells.cend(); it++) @@ -174,30 +174,27 @@ void ComponentGrid::updateSeparators() for(int y = it->pos.y(); y < it->pos.y() + it->dim.y(); y++) size[1] += getRowHeight(y); - if(it->border & BORDER_TOP || drawAll) + if (it->border & BORDER_TOP || drawAll) { - mLines.push_back(Vert(pos.x(), pos.y())); - mLines.push_back(Vert(pos.x() + size.x(), pos.y())); + mLines.push_back({ { pos.x(), pos.y() }, { 0.0f, 0.0f }, color }); + mLines.push_back({ { pos.x() + size.x(), pos.y() }, { 0.0f, 0.0f }, color }); } - if(it->border & BORDER_BOTTOM || drawAll) + if (it->border & BORDER_BOTTOM || drawAll) { - mLines.push_back(Vert(pos.x(), pos.y() + size.y())); - mLines.push_back(Vert(pos.x() + size.x(), mLines.back().y)); + mLines.push_back({ { pos.x(), pos.y() + size.y() }, { 0.0f, 0.0f }, color }); + mLines.push_back({ { pos.x() + size.x(), mLines.back().pos.y() }, { 0.0f, 0.0f }, color }); } - if(it->border & BORDER_LEFT || drawAll) + if (it->border & BORDER_LEFT || drawAll) { - mLines.push_back(Vert(pos.x(), pos.y())); - mLines.push_back(Vert(pos.x(), pos.y() + size.y())); + mLines.push_back({ { pos.x(), pos.y() }, { 0.0f, 0.0f }, color }); + mLines.push_back({ { pos.x(), pos.y() + size.y() }, { 0.0f, 0.0f }, color }); } - if(it->border & BORDER_RIGHT || drawAll) + if (it->border & BORDER_RIGHT || drawAll) { - mLines.push_back(Vert(pos.x() + size.x(), pos.y())); - mLines.push_back(Vert(mLines.back().x, pos.y() + size.y())); + mLines.push_back({ { pos.x() + size.x(), pos.y() }, { 0.0f, 0.0f }, color }); + mLines.push_back({ { mLines.back().pos.x(), pos.y() + size.y() }, { 0.0f, 0.0f }, color }); } } - - mLineColors.reserve(mLines.size()); - Renderer::buildGLColorArray((GLubyte*)mLineColors.data(), mSeparatorColor, (unsigned int)mLines.size()); } void ComponentGrid::onSizeChanged() @@ -373,20 +370,8 @@ void ComponentGrid::render(const Transform4x4f& parentTrans) if(mLines.size()) { Renderer::setMatrix(trans); - - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - glEnableClientState(GL_VERTEX_ARRAY); - glEnableClientState(GL_COLOR_ARRAY); - - glVertexPointer(2, GL_FLOAT, 0, &mLines[0].x); - glColorPointer(4, GL_UNSIGNED_BYTE, 0, mLineColors.data()); - - glDrawArrays(GL_LINES, 0, (GLsizei)mLines.size()); - - glDisable(GL_BLEND); - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); + Renderer::bindTexture(0); + Renderer::drawLines(&mLines[0], mLines.size()); } } diff --git a/es-core/src/components/ComponentGrid.h b/es-core/src/components/ComponentGrid.h index d6fd49b253..699b9d55ab 100644 --- a/es-core/src/components/ComponentGrid.h +++ b/es-core/src/components/ComponentGrid.h @@ -3,6 +3,7 @@ #define ES_CORE_COMPONENTS_COMPONENT_GRID_H #include "math/Vector2i.h" +#include "renderers/Renderer.h" #include "GuiComponent.h" namespace GridFlags @@ -100,15 +101,8 @@ class ComponentGrid : public GuiComponent float* mRowHeights; float* mColWidths; - struct Vert - { - Vert(float xi = 0, float yi = 0) : x(xi), y(yi) {}; - float x; - float y; - }; - - std::vector mLines; - std::vector mLineColors; + std::vector mLines; + //std::vector mLineColors; unsigned int mSeparatorColor; // Update position & size diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index 7efd02702d..43b54361da 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -1,7 +1,5 @@ #include "components/ComponentList.h" -#include "Renderer.h" - #define TOTAL_HORIZONTAL_PADDING_PX 20 ComponentList::ComponentList(Window* window) : IList(window, LIST_SCROLL_STYLE_SLOW, LIST_NEVER_LOOP) @@ -218,13 +216,13 @@ void ComponentList::render(const Transform4x4f& parentTrans) if (selectorGradientColor != 0) { - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, GL_ZERO, GL_ONE_MINUS_SRC_COLOR); - Renderer::drawGradientRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, selectorGradientColor, false, GL_ONE, GL_ONE); + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, Renderer::Blend::ZERO, Renderer::Blend::ONE_MINUS_SRC_COLOR); + Renderer::drawGradientRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, selectorGradientColor, false, Renderer::Blend::ONE, Renderer::Blend::ONE); } else { - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, GL_ZERO, GL_ONE_MINUS_SRC_COLOR); - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, GL_ONE, GL_ONE); + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, Renderer::Blend::ZERO, Renderer::Blend::ONE_MINUS_SRC_COLOR); + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, Renderer::Blend::ONE, Renderer::Blend::ONE); } } diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index e29941d408..085c2883b0 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -2,7 +2,7 @@ #include "resources/TextureResource.h" #include "Log.h" -#include "Renderer.h" +#include "renderers/Renderer.h" #include "Settings.h" #include "ThemeData.h" @@ -278,6 +278,37 @@ void ImageComponent::setOpacity(unsigned char opacity) } void ImageComponent::updateVertices() +{ + if (!mTexture || !mTexture->isInitialized()) + return; + + // we go through this mess to make sure everything is properly rounded + // if we just round vertices at the end, edge cases occur near sizes of 0.5 + const Vector2f size = { Math::round(mSize.x()), Math::round(mSize.y()) }; + const Vector2f topLeft = { size * mTopLeftCrop }; + const Vector2f bottomRight = { size * mBottomRightCrop }; + const float px = mTexture->isTiled() ? mSize.x() / getTextureSize().x() : 1.0f; + const float py = mTexture->isTiled() ? mSize.y() / getTextureSize().y() : 1.0f; + const unsigned int color = Renderer::convertColor(mColorShift); + + mVertices[0] = { { topLeft.x(), topLeft.y() }, { mTopLeftCrop.x(), py - mTopLeftCrop.y() }, color }; + mVertices[1] = { { topLeft.x(), bottomRight.y() }, { mTopLeftCrop.x(), 1.0f - mBottomRightCrop.y() }, color }; + mVertices[2] = { { bottomRight.x(), topLeft.y() }, { mBottomRightCrop.x() * px, py - mTopLeftCrop.y() }, color }; + mVertices[3] = { { bottomRight.x(), bottomRight.y() }, { mBottomRightCrop.x() * px, 1.0f - mBottomRightCrop.y() }, color }; + + if (mFlipX) + { + for (int i = 0; i < 4; i++) + mVertices[i].tex[0] = px - mVertices[i].tex[0]; + } + if (mFlipY) + { + for (int i = 0; i < 4; i++) + mVertices[i].tex[1] = py - mVertices[i].tex[1]; + } +} +/* +void ImageComponent::updateVerticesFca() { if (!mTexture || !mTexture->isInitialized()) return; @@ -292,17 +323,6 @@ void ImageComponent::updateVertices() mVertices[1].pos = Vector2f(topLeft.x(), bottomRight.y()); mVertices[2].pos = Vector2f(bottomRight.x(), bottomRight.y()); mVertices[3].pos = Vector2f(bottomRight.x(), topLeft.y()); - - /* - mVertices[0].pos = Vector2f(topLeft.x(), topLeft.y()); - mVertices[1].pos = Vector2f(topLeft.x(), bottomRight.y()); - - mVertices[2].pos = Vector2f(bottomRight.x(), topLeft.y()); - - mVertices[3].pos = Vector2f(bottomRight.x(), topLeft.y()); - mVertices[4].pos = Vector2f(topLeft.x(), bottomRight.y()); - mVertices[5].pos = Vector2f(bottomRight.x(), bottomRight.y()); - */ float px, py; if (mTexture->isTiled()) @@ -322,15 +342,6 @@ void ImageComponent::updateVertices() mVertices[2].tex = Vector2f(px * mBottomRightCrop.x(), 1 - mBottomRightCrop.y()); mVertices[3].tex = Vector2f(px * mBottomRightCrop.x(), py - mTopLeftCrop.y()); - /* - mVertices[0].tex = Vector2f(mTopLeftCrop.x(), py - mTopLeftCrop.y()); - mVertices[1].tex = Vector2f(mTopLeftCrop.x(), 1 - mBottomRightCrop.y()); - mVertices[2].tex = Vector2f(px * mBottomRightCrop.x(), py - mTopLeftCrop.y()); - - mVertices[3].tex = Vector2f(px * mBottomRightCrop.x(), py - mTopLeftCrop.y()); - mVertices[4].tex = Vector2f(mTopLeftCrop.x(), 1 - mBottomRightCrop.y()); - mVertices[5].tex = Vector2f(px * mBottomRightCrop.x(), 1 - mBottomRightCrop.y()); - */ if (mFlipX) { @@ -344,7 +355,7 @@ void ImageComponent::updateVertices() mVertices[i].tex[1] = py - mVertices[i].tex[1]; } } - +*/ void ImageComponent::render(const Transform4x4f& parentTrans) { if (!mVisible) @@ -375,82 +386,48 @@ void ImageComponent::render(const Transform4x4f& parentTrans) // when it finally loads fadeIn(mTexture->bind()); - // if (mColorShift & 0xff) - { - glEnable(GL_TEXTURE_2D); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - /* - glEnableClientState(GL_VERTEX_ARRAY); - glEnableClientState(GL_TEXTURE_COORD_ARRAY); - glEnableClientState(GL_COLOR_ARRAY); - - glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].pos); - glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].tex); - glColorPointer(4, GL_UNSIGNED_BYTE, 0, mColors); - glDrawArrays(GL_TRIANGLES, 0, 6); - - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_TEXTURE_COORD_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); - */ - - glBegin(GL_QUADS); + const unsigned int color = Renderer::convertColor(mColorShift); - GLfloat red = ((mColorShift & 0xff000000) >> 24) / 255.0; - GLfloat green = ((mColorShift & 0x00ff0000) >> 16) / 255.0; - GLfloat blue = ((mColorShift & 0x0000ff00) >> 8) / 255.0; - GLfloat alpha = ((mColorShift & 0x000000ff)) / 255.0; + for (int i = 0; i < 4; ++i) + mVertices[i].col = color; - glColor4f(red, green, blue, alpha); - glTexCoord2f(mVertices[0].tex.x(), mVertices[0].tex.y()); - glVertex2f(mVertices[0].pos.x(), mVertices[0].pos.y()); + Renderer::drawTriangleStrips(&mVertices[0], 4); - glColor4f(red, green, blue, alpha); - glTexCoord2f(mVertices[1].tex.x(), mVertices[1].tex.y()); - glVertex2f(mVertices[1].pos.x(), mVertices[1].pos.y()); - - glColor4f(red, green, blue, alpha); - glTexCoord2f(mVertices[2].tex.x(), mVertices[2].tex.y()); - glVertex2f(mVertices[2].pos.x(), mVertices[2].pos.y()); - - glColor4f(red, green, blue, alpha); - glTexCoord2f(mVertices[3].tex.x(), mVertices[3].tex.y()); - glVertex2f(mVertices[3].pos.x(), mVertices[3].pos.y()); - - glEnd(); + if (mMirror.x() != 0 || mMirror.y() != 0) + { + float alpha = ((mColorShift & 0x000000ff)) / 255.0; + float alpha2 = alpha * mMirror.y(); - if (mMirror.x() != 0 || mMirror.y() != 0) - { - glBegin(GL_QUADS); + alpha *= mMirror.x(); - int h = mVertices[1].pos.y() - mVertices[0].pos.y(); + const unsigned int colorT = Renderer::convertColor((mColorShift & 0xffffff00) + (unsigned char) (255.0*alpha)); + const unsigned int colorB = Renderer::convertColor((mColorShift & 0xffffff00) + (unsigned char) (255.0*alpha2)); - GLfloat alpha2 = alpha * mMirror.y(); - alpha *= mMirror.x(); + int h = mVertices[1].pos.y() - mVertices[0].pos.y(); - glColor4f(red, green, blue, alpha); - glTexCoord2f(mVertices[0].tex.x(), mVertices[1].tex.y()); - glVertex2f(mVertices[0].pos.x(), mVertices[0].pos.y() + h); + Renderer::Vertex mirrorVertices[4]; - glColor4f(red, green, blue, alpha2); - glTexCoord2f(mVertices[1].tex.x(), mVertices[0].tex.y()); - glVertex2f(mVertices[1].pos.x(), mVertices[1].pos.y() + h); + mirrorVertices[0] = { + { mVertices[0].pos.x(), mVertices[0].pos.y() + h }, + { mVertices[0].tex.x(), mVertices[1].tex.y() }, + colorT }; - glColor4f(red, green, blue, alpha2); - glTexCoord2f(mVertices[2].tex.x(), mVertices[3].tex.y()); - glVertex2f(mVertices[2].pos.x(), mVertices[2].pos.y() + h); + mirrorVertices[1] = { + { mVertices[1].pos.x(), mVertices[1].pos.y() + h }, + { mVertices[1].tex.x(), mVertices[0].tex.y() }, + colorB }; - glColor4f(red, green, blue, alpha); - glTexCoord2f(mVertices[3].tex.x(), mVertices[2].tex.y()); - glVertex2f(mVertices[3].pos.x(), mVertices[3].pos.y() + h); + mirrorVertices[2] = { + { mVertices[2].pos.x(), mVertices[2].pos.y() + h }, + { mVertices[2].tex.x(), mVertices[3].tex.y() }, + colorT }; - glEnd(); - } + mirrorVertices[3] = { + { mVertices[3].pos.x(), mVertices[3].pos.y() + h }, + { mVertices[3].tex.x(), mVertices[2].tex.y() }, + colorB }; - glDisable(GL_TEXTURE_2D); - glDisable(GL_BLEND); + Renderer::drawTriangleStrips(&mirrorVertices[0], 4); } } else diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index b98f2dadda..a36b60bd9f 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -2,13 +2,10 @@ #ifndef ES_CORE_COMPONENTS_IMAGE_COMPONENT_H #define ES_CORE_COMPONENTS_IMAGE_COMPONENT_H +#include "renderers/Renderer.h" #include "math/Vector2i.h" #include "GuiComponent.h" -#include "platform.h" #include "resources/TextureResource.h" -#include GLHEADER - -//class TextureResource; class ImageComponent : public GuiComponent { @@ -100,12 +97,7 @@ class ImageComponent : public GuiComponent // Used internally whenever the resizing parameters or texture change. void resize(); - struct Vertex - { - Vector2f pos; - Vector2f tex; - } mVertices[4]; - + Renderer::Vertex mVertices[4]; // GLubyte mColors[6*4]; void updateVertices(); diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index b49ae2ad7d..e168565e4d 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -2,14 +2,13 @@ #include "resources/TextureResource.h" #include "Log.h" -#include "Renderer.h" #include "ThemeData.h" NinePatchComponent::NinePatchComponent(Window* window, const std::string& path, unsigned int edgeColor, unsigned int centerColor) : GuiComponent(window), mCornerSize(16, 16), mEdgeColor(edgeColor), mCenterColor(centerColor), mPath(path), - mVertices(NULL), mColors(NULL) + mVertices(NULL) { mPreviousSize = Vector2f(0, 0); if(!mPath.empty()) @@ -20,27 +19,18 @@ NinePatchComponent::~NinePatchComponent() { if (mVertices != NULL) delete[] mVertices; - - if (mColors != NULL) - delete[] mColors; } void NinePatchComponent::updateColors() { - if (mColors == NULL) - return; - - unsigned int oe = mEdgeColor; - unsigned int oc = mCenterColor; + const unsigned int edgeColor = Renderer::convertColor(mEdgeColor); + const unsigned int centerColor = Renderer::convertColor(mCenterColor); - mEdgeColor = (mEdgeColor >> 8 << 8) | (unsigned char)(((mEdgeColor & 0xff) / 255.0f * (mOpacity / 255.0f)) * 255.0f); - mCenterColor = (mCenterColor >> 8 << 8) | (unsigned char)(((mCenterColor & 0xff) / 255.0f * (mOpacity / 255.0f)) * 255.0f); + for (int i = 0; i < 6 * 9; ++i) + mVertices[i].col = edgeColor; - Renderer::buildGLColorArray(mColors, mEdgeColor, 6 * 9); - Renderer::buildGLColorArray(&mColors[4 * 6 * 4], mCenterColor, 6); - - mEdgeColor = oe; - mCenterColor = oc; + for (int i = 6 * 4; i < 6; ++i) + mVertices[(6 * 4) + i].col = mCenterColor; } void NinePatchComponent::setOpacity(unsigned char opacity) @@ -54,115 +44,75 @@ void NinePatchComponent::setOpacity(unsigned char opacity) void NinePatchComponent::buildVertices() { - if(mVertices != NULL) + if (mVertices != NULL) delete[] mVertices; - if(mColors != NULL) - delete[] mColors; - - if (mPath.empty()) - return; - - auto tr = TextureResource::get(mPath); - if (tr != mTexture) - mTexture = tr; + mTexture = TextureResource::get(mPath); - if(mTexture->getSize() == Vector2i::Zero()) + if (mTexture->getSize() == Vector2i::Zero()) { mVertices = NULL; - mColors = NULL; LOG(LogWarning) << "NinePatchComponent missing texture!"; return; } - mVertices = new Vertex[6 * 9]; - mColors = new GLubyte[6 * 9 * 4]; - updateColors(); + mVertices = new Renderer::Vertex[6 * 9]; const Vector2f texSize = Vector2f((float)mTexture->getSize().x(), (float)mTexture->getSize().y()); - float imgSizeX[3] = {mCornerSize.x(), mSize.x() - mCornerSize.x() * 2, mCornerSize.x()}; - float imgSizeY[3] = {mCornerSize.y(), mSize.y() - mCornerSize.y() * 2, mCornerSize.y()}; - float imgPosX[3] = {0, imgSizeX[0], imgSizeX[0] + imgSizeX[1]}; - float imgPosY[3] = {0, imgSizeY[0], imgSizeY[0] + imgSizeY[1]}; + const float imgSizeX[3] = { mCornerSize.x(), mSize.x() - mCornerSize.x() * 2, mCornerSize.x() }; + const float imgSizeY[3] = { mCornerSize.y(), mSize.y() - mCornerSize.y() * 2, mCornerSize.y() }; + const float imgPosX[3] = { 0, imgSizeX[0], imgSizeX[0] + imgSizeX[1] }; + const float imgPosY[3] = { 0, imgSizeY[0], imgSizeY[0] + imgSizeY[1] }; //the "1 +" in posY and "-" in sizeY is to deal with texture coordinates having a bottom left corner origin vs. verticies having a top left origin - float texSizeX[3] = {mCornerSize.x() / texSize.x(), (texSize.x() - mCornerSize.x() * 2) / texSize.x(), mCornerSize.x() / texSize.x()}; - float texSizeY[3] = {-mCornerSize.y() / texSize.y(), -(texSize.y() - mCornerSize.y() * 2) / texSize.y(), -mCornerSize.y() / texSize.y()}; - float texPosX[3] = {0, texSizeX[0], texSizeX[0] + texSizeX[1]}; - float texPosY[3] = {1, 1 + texSizeY[0], 1 + texSizeY[0] + texSizeY[1]}; + const float texSizeX[3] = { mCornerSize.x() / texSize.x(), (texSize.x() - mCornerSize.x() * 2) / texSize.x(), mCornerSize.x() / texSize.x() }; + const float texSizeY[3] = { -mCornerSize.y() / texSize.y(), -(texSize.y() - mCornerSize.y() * 2) / texSize.y(), -mCornerSize.y() / texSize.y() }; + const float texPosX[3] = { 0, texSizeX[0], texSizeX[0] + texSizeX[1] }; + const float texPosY[3] = { 1, 1 + texSizeY[0], 1 + texSizeY[0] + texSizeY[1] }; int v = 0; - for(int slice = 0; slice < 9; slice++) + for (int slice = 0; slice < 9; slice++) { - int sliceX = slice % 3; - int sliceY = slice / 3; - - Vector2f imgPos = Vector2f(imgPosX[sliceX], imgPosY[sliceY]); - Vector2f imgSize = Vector2f(imgSizeX[sliceX], imgSizeY[sliceY]); - - mVertices[v + 0].pos = imgPos; - mVertices[v + 1].pos = imgPos + Vector2f(0, imgSize.y()); - mVertices[v + 2].pos = imgPos + Vector2f(imgSize.x(), 0); - mVertices[v + 3].pos = mVertices[v + 2].pos; - mVertices[v + 4].pos = mVertices[v + 1].pos; - mVertices[v + 5].pos = imgPos + imgSize; - - Vector2f texPos = Vector2f(texPosX[sliceX], texPosY[sliceY]); - Vector2f texSize = Vector2f(texSizeX[sliceX], texSizeY[sliceY]); - - mVertices[v + 0].tex = texPos; - mVertices[v + 1].tex = texPos + Vector2f(0, texSize.y()); - mVertices[v + 2].tex = texPos + Vector2f(texSize.x(), 0); - mVertices[v + 3].tex = mVertices[v + 2].tex; - mVertices[v + 4].tex = mVertices[v + 1].tex; - mVertices[v + 5].tex = texPos + texSize; + const int sliceX = slice % 3; + const int sliceY = slice / 3; + const Vector2f imgPos = Vector2f(imgPosX[sliceX], imgPosY[sliceY]); + const Vector2f imgSize = Vector2f(imgSizeX[sliceX], imgSizeY[sliceY]); + const Vector2f texPos = Vector2f(texPosX[sliceX], texPosY[sliceY]); + const Vector2f texSize = Vector2f(texSizeX[sliceX], texSizeY[sliceY]); + + mVertices[v + 1] = { { imgPos.x() , imgPos.y() }, { texPos.x(), texPos.y() }, 0 }; + mVertices[v + 2] = { { imgPos.x() , imgPos.y() + imgSize.y() }, { texPos.x(), texPos.y() + texSize.y() }, 0 }; + mVertices[v + 3] = { { imgPos.x() + imgSize.x(), imgPos.y() }, { texPos.x() + texSize.x(), texPos.y() }, 0 }; + mVertices[v + 4] = { { imgPos.x() + imgSize.x(), imgPos.y() + imgSize.y() }, { texPos.x() + texSize.x(), texPos.y() + texSize.y() }, 0 }; + + // make duplicates of first and last vertex so this can be rendered as a triangle strip + mVertices[v + 0] = mVertices[v + 1]; + mVertices[v + 5] = mVertices[v + 4]; v += 6; } // round vertices - for(int i = 0; i < 6*9; i++) + for (int i = 0; i < 6 * 9; i++) { mVertices[i].pos.round(); } + + updateColors(); } void NinePatchComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = parentTrans * getTransform(); trans.round(); - - Vector2f clipPos(trans.translation().x(), trans.translation().y()); - if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) - return; - if(mTexture && mVertices != NULL) + if (mTexture && mVertices != NULL) { Renderer::setMatrix(trans); mTexture->bind(); - - glEnable(GL_TEXTURE_2D); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - glEnableClientState(GL_VERTEX_ARRAY); - glEnableClientState(GL_TEXTURE_COORD_ARRAY); - glEnableClientState(GL_COLOR_ARRAY); - - glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].pos); - glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &mVertices[0].tex); - glColorPointer(4, GL_UNSIGNED_BYTE, 0, mColors); - - glDrawArrays(GL_TRIANGLES, 0, 6 * 9); - - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_TEXTURE_COORD_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); - - glDisable(GL_TEXTURE_2D); - glDisable(GL_BLEND); + Renderer::drawTriangleStrips(&mVertices[0], 6 * 9); } renderChildren(trans); diff --git a/es-core/src/components/SliderComponent.cpp b/es-core/src/components/SliderComponent.cpp index f7f31c9048..b5b3961f1f 100644 --- a/es-core/src/components/SliderComponent.cpp +++ b/es-core/src/components/SliderComponent.cpp @@ -1,8 +1,6 @@ #include "components/SliderComponent.h" #include "resources/Font.h" -#include "Renderer.h" - #define MOVE_REPEAT_DELAY 500 #define MOVE_REPEAT_RATE 40 diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index 16a7635b9d..cc76bd0761 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -2,7 +2,6 @@ #include "utils/StringUtil.h" #include "Log.h" -#include "Renderer.h" #include "Settings.h" TextComponent::TextComponent(Window* window) : GuiComponent(window), diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 8fe3faf2c8..c42ba48a58 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -1,10 +1,10 @@ #include "components/VideoVlcComponent.h" +#include "renderers/Renderer.h" #include "resources/TextureResource.h" #include "utils/StringUtil.h" #include "utils/FileSystemUtil.h" #include "PowerSaver.h" -#include "Renderer.h" #include "Settings.h" #include #include @@ -159,6 +159,24 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); Renderer::setMatrix(trans); + + const unsigned int fadeIn = t; //(unsigned int)(Math::clamp(0.0f, mFadeIn, 1.0f) * 255.0f); + const unsigned int color = Renderer::convertColor((fadeIn << 24) | (fadeIn << 16) | (fadeIn << 8) | 255); + Renderer::Vertex vertices[4]; + + vertices[0] = { { 0.0f , 0.0f }, { 0.0f, 0.0f }, color }; + vertices[1] = { { 0.0f , mSize.y() }, { 0.0f, 1.0f }, color }; + vertices[2] = { { mSize.x(), 0.0f }, { 1.0f, 0.0f }, color }; + vertices[3] = { { mSize.x(), mSize.y() }, { 1.0f, 1.0f }, color }; + + // Build a texture for the video frame + mTexture->initFromPixels((unsigned char*)mContext.surface->pixels, mContext.surface->w, mContext.surface->h); + mTexture->bind(); + + // Render it + Renderer::drawTriangleStrips(&vertices[0], 4); + + /* // red text< / font> //phrase< / font> @@ -194,7 +212,7 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) glEnd(); glDisable(GL_TEXTURE_2D); - glDisable(GL_BLEND); + glDisable(GL_BLEND); */ } void VideoVlcComponent::setupContext() diff --git a/es-core/src/guis/GuiDetectDevice.cpp b/es-core/src/guis/GuiDetectDevice.cpp index 6b9fa1ed74..1e82281bcc 100644 --- a/es-core/src/guis/GuiDetectDevice.cpp +++ b/es-core/src/guis/GuiDetectDevice.cpp @@ -6,7 +6,6 @@ #include "utils/StringUtil.h" #include "InputManager.h" #include "PowerSaver.h" -#include "Renderer.h" #include "Window.h" #define HOLD_TIME 1000 diff --git a/es-core/src/renderers/Renderer.cpp b/es-core/src/renderers/Renderer.cpp index 65b5fedf9d..dbd884e774 100644 --- a/es-core/src/renderers/Renderer.cpp +++ b/es-core/src/renderers/Renderer.cpp @@ -13,6 +13,8 @@ namespace Renderer { static std::stack clipStack; + static std::stack nativeClipStack; + static SDL_Window* sdlWindow = nullptr; static int windowWidth = 0; static int windowHeight = 0; @@ -112,6 +114,12 @@ namespace Renderer } // destroyWindow + void activateWindow() + { + SDL_RaiseWindow(sdlWindow); + SDL_SetWindowInputFocus(sdlWindow); + } + bool init() { if(!createWindow()) @@ -216,6 +224,7 @@ namespace Renderer if(box.h < 0) box.h = 0; clipStack.push(box); + nativeClipStack.push(Rect(_pos.x(), _pos.y(), _size.x(), _size.y())); setScissor(box); @@ -230,12 +239,55 @@ namespace Renderer } clipStack.pop(); + nativeClipStack.pop(); if(clipStack.empty()) setScissor(Rect(0, 0, 0, 0)); else setScissor(clipStack.top()); } // popClipRect + bool isClippingEnabled() { return !clipStack.empty(); } + + bool valueInRange(int value, int min, int max) + { + return (value >= min) && (value <= max); + } + + bool rectOverlap(Rect &A, Rect &B) + { + bool xOverlap = valueInRange(A.x, B.x, B.x + B.w) || + valueInRange(B.x, A.x, A.x + A.w); + + bool yOverlap = valueInRange(A.y, B.y, B.y + B.h) || + valueInRange(B.y, A.y, A.y + A.h); + + return xOverlap && yOverlap; + } + + bool isVisibleOnScreen(float x, float y, float w, float h) + { + Rect screen = Rect(0, 0, Renderer::getWindowWidth(), Renderer::getWindowHeight()); + Rect box = Rect(x, y, w, h); + + if (w > 0 && x + w <= 0) + return false; + + if (h > 0 && y + h <= 0) + return false; + + if (x == screen.w || y == screen.h) + return false; + + if (!rectOverlap(box, screen)) + return false; + + if (clipStack.empty()) + return true; + + screen = nativeClipStack.top(); + return rectOverlap(screen, box); + } + void drawRect(const float _x, const float _y, const float _w, const float _h, const unsigned int _color, const Blend::Factor _srcBlendFactor, const Blend::Factor _dstBlendFactor) { drawRect((int)Math::round(_x), (int)Math::round(_y), (int)Math::round(_w), (int)Math::round(_h), _color, _srcBlendFactor, _dstBlendFactor); @@ -257,6 +309,44 @@ namespace Renderer } // drawRect + void drawGradientRect(int _x, int _y, int _w, int _h, unsigned int _color, unsigned int _colorBottom, bool _horz, const Blend::Factor _srcBlendFactor, const Blend::Factor _dstBlendFactor) + { + const unsigned int color = convertColor(_color); + const unsigned int colorBottom = convertColor(_colorBottom); + + Vertex vertices[4]; + + vertices[0] = { { (float)(_x), (float)(_y) }, { 0.0f, 0.0f }, _horz ? colorBottom : color }; + vertices[1] = { { (float)(_x), (float)(_y + _h) }, { 0.0f, 0.0f }, colorBottom }; + vertices[2] = { { (float)(_x + _w), (float)(_y) }, { 0.0f, 0.0f }, color }; + vertices[3] = { { (float)(_x + _w), (float)(_y + _h) }, { 0.0f, 0.0f }, _horz ? color : colorBottom }; + + bindTexture(0); + drawTriangleStrips(vertices, 4, _srcBlendFactor, _dstBlendFactor); + /* + + glEnable(GL_BLEND); + glBlendFunc(blend_sfactor, blend_dfactor); + + glBegin(GL_QUADS); + + glColor4f(MAKEQUAD(horz ? colorBottom : color)); + glVertex2f(x, y); + + glColor4f(MAKEQUAD(color)); + glVertex2f(x + w, y); + + glColor4f(MAKEQUAD(horz ? color : colorBottom)); + glVertex2f(x + w, y + h); + + glColor4f(MAKEQUAD(colorBottom)); + glVertex2f(x, y + h); + + glEnd(); + + glDisable(GL_BLEND);*/ + } + SDL_Window* getSDLWindow() { return sdlWindow; } int getWindowWidth() { return windowWidth; } int getWindowHeight() { return windowHeight; } diff --git a/es-core/src/renderers/Renderer.h b/es-core/src/renderers/Renderer.h index f542385757..fc692f90e8 100644 --- a/es-core/src/renderers/Renderer.h +++ b/es-core/src/renderers/Renderer.h @@ -97,6 +97,12 @@ namespace Renderer void setSwapInterval (); void swapBuffers (); + // FCA methods + bool isClippingEnabled(); + bool isVisibleOnScreen(float x, float y, float w, float h); + void activateWindow(); + void drawGradientRect(int _x, int _y, int _w, int _h, unsigned int _color, unsigned int _colorBottom, bool _horz = false, const Blend::Factor _srcBlendFactor = Blend::SRC_ALPHA, const Blend::Factor _dstBlendFactor = Blend::ONE_MINUS_SRC_ALPHA); + } // Renderer:: #endif // ES_CORE_RENDERER_RENDERER_H diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index 42d9c6529a..80fc1bb50b 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -1,9 +1,13 @@ #include "resources/Font.h" +#include "renderers/Renderer.h" #include "utils/FileSystemUtil.h" #include "utils/StringUtil.h" #include "Log.h" -#include "Renderer.h" + +#ifdef WIN32 +#include +#endif FT_Library Font::sLibrary = NULL; @@ -191,27 +195,14 @@ bool Font::FontTexture::findEmpty(const Vector2i& size, Vector2i& cursor_out) void Font::FontTexture::initTexture() { assert(textureId == 0); - - glGenTextures(1, &textureId); - glBindTexture(GL_TEXTURE_2D, textureId); - - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); - - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST); - - glPixelStorei(GL_PACK_ALIGNMENT, 1); - glPixelStorei(GL_UNPACK_ALIGNMENT, 1); - - glTexImage2D(GL_TEXTURE_2D, 0, GL_ALPHA, textureSize.x(), textureSize.y(), 0, GL_ALPHA, GL_UNSIGNED_BYTE, NULL); + textureId = Renderer::createTexture(Renderer::Texture::ALPHA, false, false, textureSize.x(), textureSize.y(), nullptr); } void Font::FontTexture::deinitTexture() { if(textureId != 0) { - glDeleteTextures(1, &textureId); + Renderer::destroyTexture(textureId); textureId = 0; } } @@ -388,9 +379,7 @@ Font::Glyph* Font::getGlyph(unsigned int id) pGlyph->bearing = Vector2f((float)g->metrics.horiBearingX / 64.0f, (float)g->metrics.horiBearingY / 64.0f); // upload glyph bitmap to texture - glBindTexture(GL_TEXTURE_2D, tex->textureId); - glTexSubImage2D(GL_TEXTURE_2D, 0, cursor.x(), cursor.y(), glyphSize.x(), glyphSize.y(), GL_ALPHA, GL_UNSIGNED_BYTE, g->bitmap.buffer); - glBindTexture(GL_TEXTURE_2D, 0); + Renderer::updateTexture(tex->textureId, Renderer::Texture::ALPHA, cursor.x(), cursor.y(), glyphSize.x(), glyphSize.y(), g->bitmap.buffer); // update max glyph height if (id != 61446 && glyphSize.y() > mMaxGlyphHeight) @@ -427,11 +416,8 @@ void Font::rebuildTextures() Vector2i glyphSize((int)(it->second->texSize.x() * tex->textureSize.x()), (int)(it->second->texSize.y() * tex->textureSize.y())); // upload to texture - glBindTexture(GL_TEXTURE_2D, tex->textureId); - glTexSubImage2D(GL_TEXTURE_2D, 0, cursor.x(), cursor.y(), glyphSize.x(), glyphSize.y(), GL_ALPHA, GL_UNSIGNED_BYTE, glyphSlot->bitmap.buffer); + Renderer::updateTexture(tex->textureId, Renderer::Texture::ALPHA, cursor.x(), cursor.y(), glyphSize.x(), glyphSize.y(), glyphSlot->bitmap.buffer); } - - glBindTexture(GL_TEXTURE_2D, 0); } void Font::renderTextCache(TextCache* cache) @@ -448,27 +434,8 @@ void Font::renderTextCache(TextCache* cache) auto vertexList = *it; - glBindTexture(GL_TEXTURE_2D, *it->textureIdPtr); - glEnable(GL_TEXTURE_2D); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - glEnableClientState(GL_VERTEX_ARRAY); - glEnableClientState(GL_TEXTURE_COORD_ARRAY); - glEnableClientState(GL_COLOR_ARRAY); - - glVertexPointer(2, GL_FLOAT, sizeof(TextCache::Vertex), &it->verts[0].pos); - glTexCoordPointer(2, GL_FLOAT, sizeof(TextCache::Vertex), &it->verts[0].tex); - glColorPointer(4, GL_UNSIGNED_BYTE, 0, it->colors.data()); - - glDrawArrays(GL_TRIANGLES, 0, (GLsizei)(it->verts.size())); - - glDisableClientState(GL_VERTEX_ARRAY); - glDisableClientState(GL_TEXTURE_COORD_ARRAY); - glDisableClientState(GL_COLOR_ARRAY); - - glDisable(GL_TEXTURE_2D); - glDisable(GL_BLEND); + Renderer::bindTexture(*it->textureIdPtr); + Renderer::drawTriangleStrips(&it->verts[0], it->verts.size()); } } @@ -640,7 +607,7 @@ TextCache* Font::buildTextCache(const std::string& text, Vector2f offset, unsign float y = offset[1] + (yBot + yTop)/2.0f; // vertices by texture - std::map< FontTexture*, std::vector > vertMap; + std::map< FontTexture*, std::vector > vertMap; size_t cursor = 0; while(cursor < text.length()) @@ -663,37 +630,23 @@ TextCache* Font::buildTextCache(const std::string& text, Vector2f offset, unsign if(glyph == NULL) continue; - std::vector& verts = vertMap[glyph->texture]; + std::vector& verts = vertMap[glyph->texture]; size_t oldVertSize = verts.size(); verts.resize(oldVertSize + 6); - TextCache::Vertex* tri = verts.data() + oldVertSize; - - const float glyphStartX = x + glyph->bearing.x(); + Renderer::Vertex* vertices = verts.data() + oldVertSize; - const Vector2i& textureSize = glyph->texture->textureSize; + const float glyphStartX = x + glyph->bearing.x(); + const Vector2i& textureSize = glyph->texture->textureSize; + const unsigned int convertedColor = Renderer::convertColor(color); - // triangle 1 - // round to fix some weird "cut off" text bugs - tri[0].pos = Vector2f(Math::round(glyphStartX), Math::round(y + (glyph->texSize.y() * textureSize.y() - glyph->bearing.y()))); - tri[1].pos = Vector2f(Math::round(glyphStartX + glyph->texSize.x() * textureSize.x()), Math::round(y - glyph->bearing.y())); - tri[2].pos = Vector2f(tri[0].pos.x(), tri[1].pos.y()); + vertices[1] = { { Math::round(glyphStartX), Math::round(y - glyph->bearing.y()) }, { glyph->texPos.x(), glyph->texPos.y() }, convertedColor }; + vertices[2] = { { Math::round(glyphStartX), Math::round(y - glyph->bearing.y() + (glyph->texSize.y() * textureSize.y())) }, { glyph->texPos.x(), glyph->texPos.y() + glyph->texSize.y() }, convertedColor }; + vertices[3] = { { Math::round(glyphStartX + glyph->texSize.x() * textureSize.x()), Math::round(y - glyph->bearing.y()) }, { glyph->texPos.x() + glyph->texSize.x(), glyph->texPos.y() }, convertedColor }; + vertices[4] = { { Math::round(glyphStartX + glyph->texSize.x() * textureSize.x()), Math::round(y - glyph->bearing.y() + (glyph->texSize.y() * textureSize.y())) }, { glyph->texPos.x() + glyph->texSize.x(), glyph->texPos.y() + glyph->texSize.y() }, convertedColor }; - //tri[0].tex = Vector2f(0, 0); - //tri[0].tex = Vector2f(1, 1); - //tri[0].tex = Vector2f(0, 1); - - tri[0].tex = Vector2f(glyph->texPos.x(), glyph->texPos.y() + glyph->texSize.y()); - tri[1].tex = Vector2f(glyph->texPos.x() + glyph->texSize.x(), glyph->texPos.y()); - tri[2].tex = Vector2f(tri[0].tex.x(), tri[1].tex.y()); - - // triangle 2 - tri[3].pos = tri[0].pos; - tri[4].pos = tri[1].pos; - tri[5].pos = Vector2f(tri[1].pos.x(), tri[0].pos.y()); - - tri[3].tex = tri[0].tex; - tri[4].tex = tri[1].tex; - tri[5].tex = Vector2f(tri[1].tex.x(), tri[0].tex.y()); + // make duplicates of first and last vertex so this can be rendered as a triangle strip + vertices[0] = vertices[1]; + vertices[5] = vertices[4]; // advance x += glyph->advance.x(); @@ -712,9 +665,6 @@ TextCache* Font::buildTextCache(const std::string& text, Vector2f offset, unsign vertList.textureIdPtr = &it->first->textureId; vertList.verts = it->second; - - vertList.colors.resize(4 * it->second.size()); - Renderer::buildGLColorArray(vertList.colors.data(), color, (unsigned int)(it->second.size())); } clearFaceCache(); @@ -729,8 +679,11 @@ TextCache* Font::buildTextCache(const std::string& text, float offsetX, float of void TextCache::setColor(unsigned int color) { - for(auto it = vertexLists.cbegin(); it != vertexLists.cend(); it++) - Renderer::buildGLColorArray((GLubyte*)(it->colors.data()), color, (unsigned int)(it->verts.size())); + const unsigned int convertedColor = Renderer::convertColor(color); + + for (auto it = vertexLists.begin(); it != vertexLists.end(); it++) + for (auto it2 = it->verts.begin(); it2 != it->verts.end(); it2++) + it2->col = convertedColor; } std::shared_ptr Font::getFromTheme(const ThemeData::ThemeElement* elem, unsigned int properties, const std::shared_ptr& orig) diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index fa664dc882..9e85bf3a32 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -1,16 +1,14 @@ #include "resources/TextureData.h" #include "math/Misc.h" +#include "renderers/Renderer.h" #include "resources/ResourceManager.h" #include "ImageIO.h" #include "Log.h" -#include "platform.h" -#include GLHEADER #include #include #include #include -#include "Renderer.h" #define DPI 96 @@ -232,10 +230,6 @@ bool TextureData::load() // Need to load. See if there is a file if (!mPath.empty()) { -#ifdef WIN32 - TRACE("TextureData::load(" << mPath << ", " << (mMaxSize.empty() ? "" : "(hasMaxSize)") << ")") -#endif - std::shared_ptr& rm = ResourceManager::getInstance(); const ResourceData& data = rm->getFileData(mPath); @@ -269,7 +263,7 @@ bool TextureData::uploadAndBind() std::unique_lock lock(mMutex); if (mTextureID != 0) { - glBindTexture(GL_TEXTURE_2D, mTextureID); + Renderer::bindTexture(mTextureID); } else { @@ -282,19 +276,7 @@ bool TextureData::uploadAndBind() if ((mWidth == 0) || (mHeight == 0) || (mDataRGBA == nullptr)) return false; - glGetError(); - //now for the openGL texture stuff - glGenTextures(1, &mTextureID); - glBindTexture(GL_TEXTURE_2D, mTextureID); - - glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, (GLsizei)mWidth, (GLsizei)mHeight, 0, GL_RGBA, GL_UNSIGNED_BYTE, mDataRGBA); - - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); - - const GLint wrapMode = mTile ? GL_REPEAT : GL_CLAMP_TO_EDGE; - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode); - glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode); + mTextureID = Renderer::createTexture(Renderer::Texture::RGBA, true, mTile, mWidth, mHeight, mDataRGBA); } return true; @@ -305,7 +287,7 @@ void TextureData::releaseVRAM() std::unique_lock lock(mMutex); if (mTextureID != 0) { - glDeleteTextures(1, &mTextureID); + Renderer::destroyTexture(mTextureID); mTextureID = 0; } } diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 70b56f1e79..7261205bea 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -2,8 +2,6 @@ #ifndef ES_CORE_RESOURCES_TEXTURE_DATA_H #define ES_CORE_RESOURCES_TEXTURE_DATA_H -#include "platform.h" -#include GLHEADER #include #include @@ -63,7 +61,7 @@ class TextureData bool isRequiredTextureSizeOk(); std::string mPath; - GLuint mTextureID; + unsigned int mTextureID; private: std::mutex mMutex; diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index 99c61c17d3..5bb5a86482 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -195,12 +195,6 @@ void TextureDataManager::load(std::shared_ptr tex, bool block) if ((*it) == tex) continue; - -#if _DEBUG - if ((*it)->isLoaded()) - TRACE("Unloading " << (*it)->mPath); -#endif - (*it)->releaseVRAM(); (*it)->releaseRAM(); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 01d6522294..e0d81b7728 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -30,9 +30,13 @@ namespace Utils { namespace FileSystem { + static std::string homePath; + static std::string exePath; + #if defined(_WIN32) - std::mutex mFileMutex; + static std::mutex mFileMutex; #endif + bool compareFileInfo(const FileInfo& first, const FileInfo& second) { unsigned int i = 0; @@ -239,66 +243,47 @@ namespace Utils } // getPathList - std::string mCustomHomePath = ""; - - void setHomePath(std::string path) + void setHomePath(const std::string& _path) { - mCustomHomePath = path; + homePath = getGenericPath(_path); } - std::string getHomePath() - { - if (!mCustomHomePath.empty()) - return mCustomHomePath; - - - static std::string path; - - // only construct the homepath once - if (!path.length()) - { -#if defined(_WIN32) - char buffer[MAX_PATH]; - DWORD size = MAX_PATH; - DWORD result = GetModuleFileNameA(NULL, buffer, size); - if (result) - { - // verify if .emulationstation/es_systems.cfg is under exe's path to make app portable - - std::string ret = buffer; - std::string portableDir = getGenericPath(getParent(ret)) + "/.emulationstation/es_systems.cfg"; - if (Utils::FileSystem::exists(portableDir)) - { - path = getExePath(); - return path; - } - } -#endif - - // this should give us something like "/home/YOUR_USERNAME" on Linux and "C:/Users/YOUR_USERNAME/" on Windows - char* envHome = getenv("HOME"); - if (envHome) - path = getGenericPath(envHome); - -#if defined(_WIN32) - // but does not seem to work for Windows XP or Vista, so try something else - if(!path.length()) - { - char* envHomeDrive = getenv("HOMEDRIVE"); - char* envHomePath = getenv("HOMEPATH"); - if(envHomeDrive && envHomePath) - path = getGenericPath(std::string(envHomeDrive) + "/" + envHomePath); - } -#endif // _WIN32 - - // no homepath found, fall back to current working directory - if(!path.length()) - path = getCWDPath(); - } - - // return constructed homepath - return path; - + std::string getHomePath() + { + // only construct the homepath once + if (homePath.length()) + return homePath; + + // check if "getExePath()/.emulationstation/es_systems.cfg" exists + if (Utils::FileSystem::exists(getExePath() + "/.emulationstation/es_systems.cfg")) + homePath = getExePath(); + + // check for HOME environment variable + if (!homePath.length()) + { + char* envHome = getenv("HOME"); + if (envHome) + homePath = getGenericPath(envHome); + } + +#if defined(_WIN32) + // on Windows we need to check HOMEDRIVE and HOMEPATH + if (!homePath.length()) + { + char* envHomeDrive = getenv("HOMEDRIVE"); + char* envHomePath = getenv("HOMEPATH"); + if (envHomeDrive && envHomePath) + homePath = getGenericPath(std::string(envHomeDrive) + "/" + envHomePath); + } +#endif // _WIN32 + + // no homepath found, fall back to current working directory + if (!homePath.length()) + homePath = getCWDPath(); + + // return constructed homepath + return homePath; + } // getHomePath std::string getCWDPath() @@ -307,32 +292,20 @@ namespace Utils return (getcwd(temp, 512) ? getGenericPath(temp) : ""); } // getCWDPath - std::string getExePath() - { - static std::string path; - - // only construct the exepath once - if (!path.length()) - { -#if defined(_WIN32) - char buffer[MAX_PATH]; - DWORD size = MAX_PATH; - DWORD result = GetModuleFileNameA(NULL, buffer, size); - if (result) - { - std::string ret = buffer; - path = getGenericPath(getParent(ret)); - return path; - } -#endif - path = getCanonicalPath(Settings::getInstance()->getString("ExePath")); - if (isRegularFile(path)) - path = getParent(path); - } - - // return constructed exepath - return path; - + void setExePath(const std::string& _path) + { + exePath = getCanonicalPath(_path); + + if (isRegularFile(exePath)) + exePath = getParent(exePath); + + } // setExePath + + std::string getExePath() + { + // return constructed exepath + return exePath; + } // getExePath std::string getPreferredPath(const std::string& _path) diff --git a/es-core/src/utils/FileSystemUtil.h b/es-core/src/utils/FileSystemUtil.h index c5d65e1d28..87fb1ac94b 100644 --- a/es-core/src/utils/FileSystemUtil.h +++ b/es-core/src/utils/FileSystemUtil.h @@ -9,18 +9,8 @@ namespace Utils { namespace FileSystem { - struct FileInfo - { - public: - std::string path; - bool hidden; - bool directory; - }; - typedef std::list stringList; - typedef std::list fileList; - fileList getDirInfo(const std::string& _path/*, const bool _recursive = false*/); stringList getDirContent (const std::string& _path, const bool _recursive = false, const bool includeHidden = true); stringList getPathList (const std::string& _path); void setHomePath (const std::string& _path); @@ -51,11 +41,22 @@ namespace Utils bool isDirectory (const std::string& _path); bool isSymlink (const std::string& _path); bool isHidden (const std::string& _path); - - void setHomePath (std::string path); - bool copyFile (const std::string src, const std::string dst); + // FCA + struct FileInfo + { + public: + std::string path; + bool hidden; + bool directory; + }; + + typedef std::list fileList; + + fileList getDirInfo(const std::string& _path/*, const bool _recursive = false*/); + void writeAllText (const std::string fileName, const std::string text); + bool copyFile(const std::string src, const std::string dst); } // FileSystem:: From f4df6671ec3cbe498916a0e8d20c29b939014d0b Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 26 Aug 2019 13:51:11 +0200 Subject: [PATCH 113/336] Fix build & missing functions in new renderer class (related to previous merge) --- es-app/src/guis/GuiMenu.cpp | 17 +-------- es-core/src/platform.cpp | 22 ++--------- es-core/src/platform.h | 20 ++-------- es-core/src/renderers/Renderer.cpp | 46 ++++++++++++++++++++++- es-core/src/renderers/Renderer_GL21.cpp | 11 ++++++ es-core/src/renderers/Renderer_GLES10.cpp | 11 ++++++ 6 files changed, 76 insertions(+), 51 deletions(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 68968f6caf..3af9a7e428 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -23,6 +23,7 @@ #include "animations/LambdaAnimation.h" #include "GuiGamelistOptions.h" // grid sizes #include "platform.h" +#include "renderers/Renderer.h" // setSwapInterval() GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _("MAIN MENU")), mVersion(window) { @@ -933,21 +934,7 @@ void GuiMenu::openOtherSettings() s->addSaveFunc([vsync] { Settings::getInstance()->setBool("VSync", vsync->getState()); - - // vsync - if (Settings::getInstance()->getBool("VSync")) - { - // SDL_GL_SetSwapInterval(0) for immediate updates (no vsync, default), - // 1 for updates synchronized with the vertical retrace, - // or -1 for late swap tearing. - // SDL_GL_SetSwapInterval returns 0 on success, -1 on error. - // if vsync is requested, try normal vsync; if that doesn't work, try late swap tearing - // if that doesn't work, report an error - if (SDL_GL_SetSwapInterval(1) != 0 && SDL_GL_SetSwapInterval(-1) != 0) - LOG(LogWarning) << "Tried to enable vsync, but failed! (" << SDL_GetError() << ")"; - } - else - SDL_GL_SetSwapInterval(0); + Renderer::setSwapInterval(); }); #endif diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index a511bdf1d3..79d101bc3d 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -14,6 +14,7 @@ #include "GuiComponent.h" #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" int runShutdownCommand() { @@ -33,26 +34,11 @@ int runRestartCommand() #endif } -std::string trim(const std::string& str) +void splitCommand(std::string cmd, std::string* executable, std::string* parameters) { - size_t first = str.find_first_not_of(' '); - if (std::string::npos == first) - { - return str; - } - size_t last = str.find_last_not_of(' '); - return str.substr(first, (last - first + 1)); -} - -void split_cmd(std::string cmd, - std::string* executable, - std::string* parameters) -{ - std::string c(cmd); + std::string c = Utils::String::trim(cmd); size_t exec_end; - c = trim(c); - if (c[0] == '\"') { exec_end = c.find_first_of('\"', 1); @@ -123,7 +109,7 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo std::string exe; std::string args; - split_cmd(command, &exe, &args); + splitCommand(command, &exe, &args); SHELLEXECUTEINFO lpExecInfo; lpExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); diff --git a/es-core/src/platform.h b/es-core/src/platform.h index 23fd7e5b94..82f090fc95 100644 --- a/es-core/src/platform.h +++ b/es-core/src/platform.h @@ -4,23 +4,9 @@ #include -//the Makefile defines one of these: -//#define USE_OPENGL_ES -//#define USE_OPENGL_DESKTOP - -#ifdef USE_OPENGL_ES - #define GLHEADER -#endif - -#ifdef USE_OPENGL_DESKTOP - //why the hell this naming inconsistency exists is well beyond me - #ifdef WIN32 - #define sleep Sleep - #endif - - #define GL_GLEXT_PROTOTYPES - - #define GLHEADER +//why the hell this naming inconsistency exists is well beyond me +#ifdef WIN32 + #define sleep Sleep #endif class Window; diff --git a/es-core/src/renderers/Renderer.cpp b/es-core/src/renderers/Renderer.cpp index dbd884e774..c2365bb94a 100644 --- a/es-core/src/renderers/Renderer.cpp +++ b/es-core/src/renderers/Renderer.cpp @@ -25,6 +25,8 @@ namespace Renderer static int screenRotate = 0; static bool initialCursorState = 1; + static Vector2i sdlWindowPosition = Vector2i(SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED); + static void setIcon() { size_t width = 0; @@ -81,11 +83,46 @@ namespace Renderer screenOffsetY = Settings::getInstance()->getInt("ScreenOffsetY") ? Settings::getInstance()->getInt("ScreenOffsetY") : 0; screenRotate = Settings::getInstance()->getInt("ScreenRotate") ? Settings::getInstance()->getInt("ScreenRotate") : 0; + int monitorId = Settings::getInstance()->getInt("MonitorID"); + if (monitorId >= 0 && sdlWindowPosition == Vector2i(SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED)) + { + int displays = SDL_GetNumVideoDisplays(); + if (displays > monitorId) + { + SDL_Rect rc; + SDL_GetDisplayBounds(monitorId, &rc); + + sdlWindowPosition = Vector2i(rc.x, rc.y); + + if (Settings::getInstance()->getBool("Windowed") && (Settings::getInstance()->getInt("WindowWidth") || Settings::getInstance()->getInt("ScreenWidth"))) + { + if (windowWidth != rc.w || windowHeight != rc.h) + { + sdlWindowPosition = Vector2i( + rc.x + (rc.w - windowWidth) / 2, + rc.y + (rc.h - windowHeight) / 2 + ); + } + } + else + { + windowWidth = rc.w; + windowHeight = rc.h; + screenWidth = rc.w; + screenHeight = rc.h; + } + } + } + setupWindow(); unsigned int windowFlags = (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN)) | getWindowFlags(); - if((sdlWindow = SDL_CreateWindow("EmulationStation", SDL_WINDOWPOS_UNDEFINED, SDL_WINDOWPOS_UNDEFINED, windowWidth, windowHeight, windowFlags)) == nullptr) + if((sdlWindow = SDL_CreateWindow("EmulationStation", + sdlWindowPosition.x(), + sdlWindowPosition.y(), + windowWidth, windowHeight, + windowFlags)) == nullptr) { LOG(LogError) << "Error creating SDL window!\n\t" << SDL_GetError(); return false; @@ -103,6 +140,13 @@ namespace Renderer static void destroyWindow() { + if (Settings::getInstance()->getBool("Windowed") && Settings::getInstance()->getInt("WindowWidth") && Settings::getInstance()->getInt("WindowHeight")) + { + int x; int y; + SDL_GetWindowPosition(sdlWindow, &x, &y); + sdlWindowPosition = Vector2i(x, y); // Save position to restore it later + } + destroyContext(); SDL_DestroyWindow(sdlWindow); diff --git a/es-core/src/renderers/Renderer_GL21.cpp b/es-core/src/renderers/Renderer_GL21.cpp index f3b4247ec5..81368257d0 100644 --- a/es-core/src/renderers/Renderer_GL21.cpp +++ b/es-core/src/renderers/Renderer_GL21.cpp @@ -241,7 +241,18 @@ namespace Renderer void swapBuffers() { +#ifdef WIN32 + glFlush(); + glFinish(); + Sleep(0); +#endif + SDL_GL_SwapWindow(getSDLWindow()); + +#ifdef WIN32 + Sleep(0); +#endif + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } // swapBuffers diff --git a/es-core/src/renderers/Renderer_GLES10.cpp b/es-core/src/renderers/Renderer_GLES10.cpp index 3a99b45d1f..c07a802dcf 100644 --- a/es-core/src/renderers/Renderer_GLES10.cpp +++ b/es-core/src/renderers/Renderer_GLES10.cpp @@ -241,7 +241,18 @@ namespace Renderer void swapBuffers() { +#ifdef WIN32 + glFlush(); + glFinish(); + Sleep(0); +#endif + SDL_GL_SwapWindow(getSDLWindow()); + +#ifdef WIN32 + Sleep(0); +#endif + glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); } // swapBuffers From afe50bae1b0d792a9d00029fdaa0e9562d5be234 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 27 Aug 2019 11:06:01 +0200 Subject: [PATCH 114/336] Videos blacked out since including tomaz refactorings --- es-core/src/components/VideoVlcComponent.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index c42ba48a58..57d7114915 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -160,8 +160,8 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) Renderer::setMatrix(trans); - const unsigned int fadeIn = t; //(unsigned int)(Math::clamp(0.0f, mFadeIn, 1.0f) * 255.0f); - const unsigned int color = Renderer::convertColor((fadeIn << 24) | (fadeIn << 16) | (fadeIn << 8) | 255); + const unsigned int fadeIn = t * 255.0f; + const unsigned int color = Renderer::convertColor(0xFFFFFF00 | fadeIn); Renderer::Vertex vertices[4]; vertices[0] = { { 0.0f , 0.0f }, { 0.0f, 0.0f }, color }; From bca47ed0817bd835dfbb1e62d1694f8661c8e2ed Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 27 Aug 2019 11:39:55 +0200 Subject: [PATCH 115/336] VideoVlcComponent : optimisation, if streamed image size is already the same, don't free/reallocate memory (which is slow), just copy bytes --- es-core/src/resources/TextureData.h | 4 ++++ es-core/src/resources/TextureResource.cpp | 10 +++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 7261205bea..9c35d0eaf6 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -63,6 +63,10 @@ class TextureData std::string mPath; unsigned int mTextureID; + unsigned char* getDataRGBA() { + return mDataRGBA; + } + private: std::mutex mMutex; bool mTile; diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index 9595478a42..4a9f14cad2 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -101,8 +101,16 @@ void TextureResource::onTextureLoaded(std::shared_ptr tex) void TextureResource::initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height) { // This is only valid if we have a local texture data object - assert(mTextureData != nullptr); + assert(mTextureData != nullptr); mTextureData->releaseVRAM(); + + // FCA optimisation, if streamed image size is already the same, don't free/reallocate memory (which is slow), just copy bytes + if (mTextureData->getDataRGBA() != nullptr && mSize.x() == width && mSize.y() == height) + { + memcpy(mTextureData->getDataRGBA(), dataRGBA, width * height * 4); + return; + } + mTextureData->releaseRAM(); mTextureData->initFromRGBA(dataRGBA, width, height); // Cache the image dimensions From 3ae86908e9f5d0fda6ed080548e7f4c50df09834 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 27 Aug 2019 14:34:40 +0200 Subject: [PATCH 116/336] GridView : Disable clipping in the containers bounds, for some themes --- es-core/src/components/ImageGridComponent.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index b0f6dc3a08..9a2046765f 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -328,7 +328,7 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) Renderer::setMatrix(parentTrans); } - Renderer::pushClipRect(pos, size); + // Renderer::pushClipRect(pos, size); // Render the selected image background on bottom of the others if needed std::shared_ptr selectedTile = NULL; @@ -362,7 +362,7 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) selectedTile->render(tileTrans); } - Renderer::popClipRect(); + // Renderer::popClipRect(); listRenderTitleOverlay(trans); From fa930cf0abc374e03824c78c30c968c3031ffeb6 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 27 Aug 2019 14:54:24 +0200 Subject: [PATCH 117/336] GridView : Avoid splitted rendering on old themes not defining autolayout --- es-core/src/components/ImageGridComponent.h | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 9a2046765f..de3fd24509 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -328,7 +328,7 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) Renderer::setMatrix(parentTrans); } - // Renderer::pushClipRect(pos, size); + bool splittedRendering = (mAnimateSelection && mAutoLayout.x() != 0); // Render the selected image background on bottom of the others if needed std::shared_ptr selectedTile = NULL; @@ -339,13 +339,15 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) { selectedTile = tile; - if (mAnimateSelection && tile->shouldSplitRendering()) + if (splittedRendering && tile->shouldSplitRendering()) tile->renderBackground(tileTrans); break; } } + Renderer::pushClipRect(pos, size); + for (auto it = mTiles.begin(); it != mTiles.end(); it++) { std::shared_ptr tile = (*it); @@ -353,17 +355,17 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) tile->render(tileTrans); } + Renderer::popClipRect(); + // Render the selected image content on top of the others if (selectedTile != NULL) { - if (mAnimateSelection && selectedTile->shouldSplitRendering()) + if (splittedRendering && selectedTile->shouldSplitRendering()) selectedTile->renderContent(tileTrans); else selectedTile->render(tileTrans); } - // Renderer::popClipRect(); - listRenderTitleOverlay(trans); GuiComponent::renderChildren(trans); From d0362dd869c5219950551c397f6bda8adee04b3c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO <51082152+fabricecaruso@users.noreply.github.com> Date: Tue, 27 Aug 2019 15:08:20 +0200 Subject: [PATCH 118/336] Update THEMES.md --- THEMES.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/THEMES.md b/THEMES.md index 4cb4551fd9..90bf1f5fed 100644 --- a/THEMES.md +++ b/THEMES.md @@ -735,7 +735,7 @@ Can be created as an extra. - Where on the component `pos` refers to. For example, an origin of `0.5 0.5` and a `pos` of `0.5 0.5` would place the component exactly in the middle of the screen. If the "POSITION" and "SIZE" attributes are themable, "ORIGIN" is implied. * `selectorColor` - type: COLOR. - Color of the "selector bar." -* `selectorGradientColor` - type: NORMALIZED_PAIR. +* `selectorColorEnd` - type: NORMALIZED_PAIR. - NEW : Bottom color for the gradient of the "selector bar." * `selectorImagePath` - type: PATH. - Path to image to render in place of "selector bar." @@ -856,7 +856,7 @@ EmulationStation borrows the concept of "nine patches" from Android (or "9-Slice * `color` - type: COLOR. - Controls the color of the carousel background. - Default is FFFFFFD8 -* `colorGradient` - type: COLOR. +* `colorEnd` - type: COLOR. - NEW : Color for the end of gradient * `gradientType` - type: STRING. - NEW : Sets the gradient direction. Accepted values are "horizontal" and "vertical". @@ -898,6 +898,8 @@ EmulationStation borrows the concept of "nine patches" from Android (or "9-Slice - Default is FFFFFFFF. Color of text for selected menu entry. * `selectorColor` - type: COLOR. - Default is 878787FF. Color of the selector bar. +* `selectorColorEnd` - type: NORMALIZED_PAIR. + - NEW : Bottom color for the gradient of the "selector bar." The help system is a special element that displays a context-sensitive list of actions the user can take at any time. You should try and keep the position constant throughout every screen. Keep in mind the "default" settings (including position) are used whenever the user opens a menu. From 9525def9c437d48cb790e40fdd7a3cf367836d4c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 27 Aug 2019 16:20:41 +0200 Subject: [PATCH 119/336] Image VRAM use : Fixed maxsize information --- es-app/src/views/gamelist/DetailedGameListView.cpp | 9 +++++---- es-app/src/views/gamelist/GridGameListView.cpp | 8 ++++---- es-app/src/views/gamelist/VideoGameListView.cpp | 6 +++--- es-core/src/components/ImageComponent.cpp | 4 ++-- es-core/src/components/ImageComponent.h | 8 ++++++++ es-core/src/components/VideoComponent.h | 8 ++++---- 6 files changed, 26 insertions(+), 17 deletions(-) diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index b3917c75f5..06816c2908 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -300,6 +300,8 @@ std::string DetailedGameListView::getMetadata(FileData* file, std::string name) return ret; } +#include "platform.h" + void DetailedGameListView::updateInfoPanel() { FileData* file = (mList.size() == 0 || mList.isScrolling()) ? NULL : mList.getSelected(); @@ -323,15 +325,14 @@ void DetailedGameListView::updateInfoPanel() if (!mVideo->setVideo(file->getVideoPath())) mVideo->setDefaultVideo(); - mVideo->setImage(imagePath, false, mVideo->getTargetSize()); + mVideo->setImage(imagePath, false, mVideo->getMaxSizeInfo()); } - //mSelectedProperties.mImageSizeMode == "minSize")); if (mImage != nullptr) - mImage->setImage(imagePath, false, mImage->getSize()); + mImage->setImage(imagePath, false, mImage->getMaxSizeInfo()); if (mMarquee != nullptr) - mMarquee->setImage(file->getMarqueePath(), false, mMarquee->getSize()); + mMarquee->setImage(file->getMarqueePath(), false, mMarquee->getMaxSizeInfo()); mDescription.setText(getMetadata(file, "desc")); mDescContainer.reset(); diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 63d64cdb00..6579a71d08 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -420,16 +420,16 @@ void GridGameListView::updateInfoPanel() if (file->getImagePath().empty()) { if (mVideo != nullptr) - mVideo->setImage(file->getThumbnailPath(), false, mVideo->getTargetSize()); + mVideo->setImage(file->getThumbnailPath(), false, mVideo->getMaxSizeInfo()); - mImage.setImage(file->getThumbnailPath(), false, mImage.getSize()); + mImage.setImage(file->getThumbnailPath(), false, mImage.getMaxSizeInfo()); } else { if (mVideo != nullptr) - mVideo->setImage(file->getImagePath(), false, mVideo->getSize()); + mVideo->setImage(file->getImagePath(), false, mVideo->getMaxSizeInfo()); - mImage.setImage(file->getImagePath(), false, mImage.getSize()); + mImage.setImage(file->getImagePath(), false, mImage.getMaxSizeInfo()); } } else diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index c77b4b79fc..522cb5d281 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -276,11 +276,11 @@ void VideoGameListView::updateInfoPanel() } mVideoPlaying = true; - mVideo->setImage(file->getThumbnailPath(), false, mVideo->getTargetSize()); - mMarquee.setImage(file->getMarqueePath(), false, mMarquee.getSize()); + mVideo->setImage(file->getThumbnailPath(), false, mVideo->getMaxSizeInfo()); + mMarquee.setImage(file->getMarqueePath(), false, mMarquee.getMaxSizeInfo()); if (mImage != nullptr) - mImage->setImage(file->getImagePath(), false, mImage->getSize()); + mImage->setImage(file->getImagePath(), false, mImage->getMaxSizeInfo()); mDescription.setText(file->metadata.get("desc")); mDescContainer.reset(); diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 085c2883b0..517f5a5342 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -140,8 +140,8 @@ void ImageComponent::setDefaultImage(std::string path) void ImageComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) { - if (!maxSize.isExternalZoomKnown() && !maxSize.empty()) - maxSize = MaxSizeInfo(maxSize.x(), maxSize.y(), !mTargetIsMax); +// if (!maxSize.isExternalZoomKnown() && !maxSize.empty()) +// maxSize = MaxSizeInfo(maxSize.x(), maxSize.y(), !mTargetIsMax); if (path.empty() || !ResourceManager::getInstance()->fileExists(path)) { diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index a36b60bd9f..6ce28c51c6 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -88,6 +88,14 @@ class ImageComponent : public GuiComponent std::shared_ptr getTexture() { return mTexture; }; + const MaxSizeInfo getMaxSizeInfo() + { + if (mTargetSize == Vector2f(0, 0)) + return MaxSizeInfo(mSize, mTargetIsMax); + + return MaxSizeInfo(mTargetSize, mTargetIsMax); + }; + private: Vector2f mTargetSize; diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index 28036492da..ed40d32d39 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -83,12 +83,12 @@ class VideoComponent : public GuiComponent void onVideoStarted(); - Vector2f getTargetSize() - { + const MaxSizeInfo getMaxSizeInfo() + { if (mTargetSize == Vector2f(0, 0)) - return mSize; + return MaxSizeInfo(mSize, mTargetIsMax); - return mTargetSize; + return MaxSizeInfo(mTargetSize, mTargetIsMax); }; private: From 4274b4f1847f9476529080bcd81c5230ba941e2d Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 28 Aug 2019 09:18:50 +0200 Subject: [PATCH 120/336] Renderer : set clear color to black --- es-core/src/renderers/Renderer_GL21.cpp | 2 +- es-core/src/renderers/Renderer_GLES10.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/es-core/src/renderers/Renderer_GL21.cpp b/es-core/src/renderers/Renderer_GL21.cpp index 81368257d0..2068982e8b 100644 --- a/es-core/src/renderers/Renderer_GL21.cpp +++ b/es-core/src/renderers/Renderer_GL21.cpp @@ -77,7 +77,7 @@ namespace Renderer sdlContext = SDL_GL_CreateContext(getSDLWindow()); SDL_GL_MakeCurrent(getSDLWindow(), sdlContext); - glClearColor(0.5f, 0.5f, 0.5f, 0.0f); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); std::string glExts = (const char*)glGetString(GL_EXTENSIONS); LOG(LogInfo) << "Checking available OpenGL extensions..."; diff --git a/es-core/src/renderers/Renderer_GLES10.cpp b/es-core/src/renderers/Renderer_GLES10.cpp index c07a802dcf..26cf73a6ba 100644 --- a/es-core/src/renderers/Renderer_GLES10.cpp +++ b/es-core/src/renderers/Renderer_GLES10.cpp @@ -77,7 +77,7 @@ namespace Renderer sdlContext = SDL_GL_CreateContext(getSDLWindow()); SDL_GL_MakeCurrent(getSDLWindow(), sdlContext); - glClearColor(0.5f, 0.5f, 0.5f, 0.0f); + glClearColor(0.0f, 0.0f, 0.0f, 0.0f); std::string glExts = (const char*)glGetString(GL_EXTENSIONS); LOG(LogInfo) << "Checking available OpenGL extensions..."; From 49058293fe91a8ec2228bc1fb558ce624c5748d5 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 28 Aug 2019 10:10:48 +0200 Subject: [PATCH 121/336] Info : Add build time info on main menu --- es-app/src/EmulationStation.h | 8 +++---- es-app/src/guis/GuiMenu.cpp | 43 +++++++++++++++++++++++++++++++++-- 2 files changed, 45 insertions(+), 6 deletions(-) diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index 71b3850a9e..39119ca251 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -7,14 +7,14 @@ #define PROGRAM_VERSION_MAJOR 2 #define PROGRAM_VERSION_MINOR 10 #define PROGRAM_VERSION_MAINTENANCE 0 -#define PROGRAM_VERSION_BUILD 6 +#define PROGRAM_VERSION_BUILD 7 -#define PROGRAM_VERSION_STRING "2.10.0.6 FCAMOD" -#define SCRAPER_VERSION_STRING "2.10.0.6" +#define PROGRAM_VERSION_STRING "2.10.0.7 FCAMOD" +#define SCRAPER_VERSION_STRING "2.10.0.7" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ -#define RESOURCE_VERSION_STRING "2,10,0,6\0" +#define RESOURCE_VERSION_STRING "2,10,0,7\0" #define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE,PROGRAM_VERSION_BUILD #endif // ES_APP_EMULATION_STATION_H diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 3af9a7e428..07e001076b 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -1050,9 +1050,48 @@ void GuiMenu::openQuitMenu() mWindow->pushGui(s); } +std::string getBuildTime() +{ + std::string datestr = __DATE__; + std::string timestr = __TIME__; + + std::istringstream iss_date(datestr); + std::string str_month; + int day; + int year; + iss_date >> str_month >> day >> year; + + int month; + if (str_month == "Jan") month = 1; + else if (str_month == "Feb") month = 2; + else if (str_month == "Mar") month = 3; + else if (str_month == "Apr") month = 4; + else if (str_month == "May") month = 5; + else if (str_month == "Jun") month = 6; + else if (str_month == "Jul") month = 7; + else if (str_month == "Aug") month = 8; + else if (str_month == "Sep") month = 9; + else if (str_month == "Oct") month = 10; + else if (str_month == "Nov") month = 11; + else if (str_month == "Dec") month = 12; + else exit(-1); + + for (std::string::size_type pos = timestr.find(':'); pos != std::string::npos; pos = timestr.find(':', pos)) + timestr[pos] = ' '; + + std::istringstream iss_time(timestr); + int hour, min, sec; + iss_time >> hour >> min >> sec; + + char buffer[100]; + sprintf_s(buffer, "%4d%.2d%.2d%.2d%.2d%.2d\n", year, month, day, hour, min, sec); + return buffer; +} + void GuiMenu::addVersionInfo() { - std::string buildDate = (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); + std::string buildDate = getBuildTime(); + // (Settings::getInstance()->getBool("Debug") ? std::string( " (" + Utils::String::toUpper(PROGRAM_BUILT_STRING) + ")") : ("")); auto theme = ThemeData::getMenuTheme(); // mVersion.setFont(Font::get(FONT_SIZE_SMALL)); @@ -1061,7 +1100,7 @@ void GuiMenu::addVersionInfo() mVersion.setFont(theme->Footer.font); mVersion.setColor(theme->Footer.color); - mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + buildDate); + mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + " BUILD " + buildDate); mVersion.setHorizontalAlignment(ALIGN_CENTER); addChild(&mVersion); } From 1a11ad6e92fce53f3f91444e1e00ff7d17314a22 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 28 Aug 2019 11:40:29 +0200 Subject: [PATCH 122/336] SystemView : Allow video element as extra --- es-app/src/views/SystemView.cpp | 65 ++++++++++++++++++++++++++++++++- es-app/src/views/SystemView.h | 9 ++++- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index cf85bed575..c9bc8fe3ec 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -22,6 +22,9 @@ SystemView::SystemView(Window* window) : IList(wind mExtrasCamOffset = 0; mExtrasFadeOpacity = 0.0f; mLastSystem = nullptr; + mScreensaverActive = false; + mDisable = false; + mShowing = false; setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); populate(); @@ -220,6 +223,7 @@ bool SystemView::input(InputConfig* config, Input input) void SystemView::update(int deltaTime) { listUpdate(deltaTime); + updateExtras(deltaTime); GuiComponent::update(deltaTime); } @@ -360,9 +364,15 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) this->mExtrasCamOffset = endPos; }, move_carousel ? 500 : 1); } + + for (int i = 0; i < mEntries.size(); i++) + if (i != mCursor) + activateExtras(i, false); - - setAnimation(anim, 0, nullptr, false, 0); + setAnimation(anim, 0, [this] + { + activateExtras(mCursor); + }, false, 0); } void SystemView::render(const Transform4x4f& parentTrans) @@ -810,9 +820,60 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) void SystemView::onShow() { mShowing = true; + activateExtras(mCursor); } void SystemView::onHide() { mShowing = false; + activateExtras(mCursor); +} + +void SystemView::onScreenSaverActivate() +{ + mScreensaverActive = true; + activateExtras(mCursor); +} + +void SystemView::onScreenSaverDeactivate() +{ + mScreensaverActive = false; + activateExtras(mCursor); +} + +void SystemView::topWindow(bool isTop) +{ + mDisable = !isTop; + activateExtras(mCursor); +} + +void SystemView::activateExtras(int cursor, bool activate) +{ + if (cursor < 0 || cursor >= mEntries.size()) + return; + + bool show = activate && mShowing && !mScreensaverActive && !mDisable; + + SystemViewData data = mEntries.at(cursor).data; + for (unsigned int j = 0; j < data.backgroundExtras.size(); j++) + { + GuiComponent *extra = data.backgroundExtras[j]; + if (show && activate) + extra->onShow(); + else + extra->onHide(); + } } + +void SystemView::updateExtras(int deltaTime) +{ + if (!mShowing) + return; + + for (int i = 0; i < mEntries.size(); i++) + { + SystemViewData data = mEntries.at(i).data; + for (unsigned int j = 0; j < data.backgroundExtras.size(); j++) + data.backgroundExtras[j]->update(deltaTime); + } +} \ No newline at end of file diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index d6bae81a7f..5cde505b05 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -74,7 +74,12 @@ class SystemView : public IList Vector2f carouselPos(); Vector2f carouselSize(); - + void activateExtras(int cursor, bool activate = true); + void updateExtras(int deltaTime); + + virtual void onScreenSaverActivate() override; + virtual void onScreenSaverDeactivate() override; + virtual void topWindow(bool isTop) override; void populate(); void getViewElements(const std::shared_ptr& theme); @@ -98,6 +103,8 @@ class SystemView : public IList bool mViewNeedsReload; bool mShowing; + bool mDisable; + bool mScreensaverActive; }; #endif // ES_APP_VIEWS_SYSTEM_VIEW_H From 9f288d011b4b429baa0861958ee8ee5a40eae26a Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 28 Aug 2019 14:27:48 +0200 Subject: [PATCH 123/336] Video : Add support for minsize. --- es-core/src/ThemeData.cpp | 4 +- es-core/src/components/GridTileComponent.cpp | 4 +- es-core/src/components/ImageComponent.cpp | 2 + es-core/src/components/VideoComponent.cpp | 2 + es-core/src/components/VideoComponent.h | 6 +- .../src/components/VideoPlayerComponent.cpp | 11 ++ es-core/src/components/VideoPlayerComponent.h | 1 + es-core/src/components/VideoVlcComponent.cpp | 104 +++++++++++------- es-core/src/components/VideoVlcComponent.h | 3 + 9 files changed, 96 insertions(+), 41 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 0948ef865d..2e48a22066 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -164,6 +164,7 @@ std::map> The { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, { "maxSize", NORMALIZED_PAIR }, + { "minSize", NORMALIZED_PAIR }, { "origin", NORMALIZED_PAIR }, { "rotation", FLOAT }, { "rotationOrigin", NORMALIZED_PAIR }, @@ -972,11 +973,12 @@ std::vector ThemeData::makeExtras(const std::shared_ptrgetBool("VideoOmxPlayer")) comp = new VideoPlayerComponent(window, ""); else -#endif +#endif*/ comp = new VideoVlcComponent(window, ""); } diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 6f3b7e204b..2fa4cd3bbe 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -70,13 +70,13 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa addChild(&mBackground); addChild(&(*mImage)); addChild(&mLabel); - + /* // Create the correct type of video window #ifdef _RPI_ if (Settings::getInstance()->getBool("VideoOmxPlayer")) mVideo = new VideoPlayerComponent(window, ""); else -#endif +#endif*/ mVideo = new VideoVlcComponent(window, ""); // video diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 517f5a5342..e270c7f41f 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -429,6 +429,8 @@ void ImageComponent::render(const Transform4x4f& parentTrans) Renderer::drawTriangleStrips(&mirrorVertices[0], 4); } + + Renderer::bindTexture(0); } else { diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 383ab1e275..6e2a2c68ef 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -217,6 +217,8 @@ void VideoComponent::applyTheme(const std::shared_ptr& theme, const s setResize(elem->get("size") * scale); else if(elem->has("maxSize")) setMaxSize(elem->get("maxSize") * scale); + else if (elem->has("minSize")) + setMinSize(elem->get("minSize") * scale); } // position + size also implies origin diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index ed40d32d39..4f4ee71cbc 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -72,6 +72,9 @@ class VideoComponent : public GuiComponent virtual void setMaxSize(float width, float height) = 0; inline void setMaxSize(const Vector2f& size) { setMaxSize(size.x(), size.y()); } + virtual void setMinSize(float width, float height) = 0; + inline void setMinSize(const Vector2f& size) { setMinSize(size.x(), size.y()); } + Vector2f getVideoSize() { return Vector2f(mVideoWidth, mVideoHeight); } bool isPlaying() { return mIsPlaying; @@ -81,7 +84,7 @@ class VideoComponent : public GuiComponent return mIsWaitingForVideoToStart; } - void onVideoStarted(); + virtual void onVideoStarted(); const MaxSizeInfo getMaxSizeInfo() { @@ -130,6 +133,7 @@ class VideoComponent : public GuiComponent bool mScreensaverActive; bool mScreensaverMode; bool mTargetIsMax; + bool mTargetIsMin; bool mIsWaitingForVideoToStart; diff --git a/es-core/src/components/VideoPlayerComponent.cpp b/es-core/src/components/VideoPlayerComponent.cpp index 652317b171..7daf19ec2c 100644 --- a/es-core/src/components/VideoPlayerComponent.cpp +++ b/es-core/src/components/VideoPlayerComponent.cpp @@ -54,6 +54,17 @@ void VideoPlayerComponent::setMaxSize(float width, float height) onSizeChanged(); } +void VideoPlayerComponent::setMinSize(float width, float height) +{ + setSize(width, height); + mTargetSize = Vector2f(width, height); + mTargetIsMax = false; + mStaticImage.setMinSize(width, height); + onSizeChanged(); + + // TODO add cropping with --crop 100,100,300,300 +} + void VideoPlayerComponent::startVideo() { if (!mIsPlaying) diff --git a/es-core/src/components/VideoPlayerComponent.h b/es-core/src/components/VideoPlayerComponent.h index a68c337a31..6ac417524e 100644 --- a/es-core/src/components/VideoPlayerComponent.h +++ b/es-core/src/components/VideoPlayerComponent.h @@ -25,6 +25,7 @@ class VideoPlayerComponent : public VideoComponent // Can be set before or after a video is loaded. // Never breaks the aspect ratio. setMaxSize() and setResize() are mutually exclusive. void setMaxSize(float width, float height); + void setMinSize(float width, float height); private: // Start the video Immediately diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 57d7114915..4ce3ea3483 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -13,6 +13,8 @@ #include #endif +#include "ImageIO.h" + libvlc_instance_t* VideoVlcComponent::mVLC = NULL; // VLC prepares to render a video frame. @@ -63,20 +65,46 @@ VideoVlcComponent::~VideoVlcComponent() void VideoVlcComponent::setResize(float width, float height) { + if (mSize.x() != 0 && mSize.y() != 0 && !mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) + return; + mTargetSize = Vector2f(width, height); mTargetIsMax = false; + mTargetIsMin = false; mStaticImage.setResize(width, height); resize(); } void VideoVlcComponent::setMaxSize(float width, float height) { + if (mSize.x() != 0 && mSize.y() != 0 && mTargetIsMax && !mTargetIsMin && mTargetSize.x() == width && mTargetSize.y() == height) + return; + mTargetSize = Vector2f(width, height); mTargetIsMax = true; + mTargetIsMin = false; mStaticImage.setMaxSize(width, height); resize(); } +void VideoVlcComponent::setMinSize(float width, float height) +{ + if (mSize.x() != 0 && mSize.y() != 0 && mTargetIsMin && !mTargetIsMax && mTargetSize.x() == width && mTargetSize.y() == height) + return; + + mTargetSize = Vector2f(width, height); + mTargetIsMax = false; + mTargetIsMin = true; + mStaticImage.setMinSize(width, height); + resize(); +} + +void VideoVlcComponent::onVideoStarted() +{ + VideoComponent::onVideoStarted(); + resize(); +} + void VideoVlcComponent::resize() { if(!mTexture) @@ -113,7 +141,33 @@ void VideoVlcComponent::resize() mSize[1] = Math::round(mSize[1]); mSize[0] = (mSize[1] / textureSize.y()) * textureSize.x(); - }else{ + } + else if (mTargetIsMin) + { + /*mSize = textureSize; + + Vector2f resizeScale((mTargetSize.x() / mSize.x()), (mTargetSize.y() / mSize.y())); + + if (resizeScale.x() > resizeScale.y()) + { + mSize[0] *= resizeScale.x(); + mSize[1] *= resizeScale.x(); + + // float cropPercent = (mSize.y() - mTargetSize.y()) / (mSize.y() * 2); + // crop(0, cropPercent, 0, cropPercent); + } + else { + mSize[0] *= resizeScale.y(); + mSize[1] *= resizeScale.y(); + + // float cropPercent = (mSize.x() - mTargetSize.x()) / (mSize.x() * 2); + // crop(cropPercent, 0, cropPercent, 0); + } + */ + mSize = ImageIO::adjustExternPictureSizef(textureSize, mTargetSize); + } + else + { // if both components are set, we just stretch // if no components are set, we don't resize at all mSize = mTargetSize == Vector2f::Zero() ? textureSize : mTargetSize; @@ -173,46 +227,22 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) mTexture->initFromPixels((unsigned char*)mContext.surface->pixels, mContext.surface->w, mContext.surface->h); mTexture->bind(); + if (mTargetIsMin) + { + Vector2f targetPos = (mTargetSize - mSize) * mOrigin * -1; + + Vector2i pos(trans.translation().x() + (int)targetPos.x(), trans.translation().y() + (int)targetPos.y()); + Vector2i size((int)Math::round(mTargetSize.x()), (int)Math::round(mTargetSize.y())); + Renderer::pushClipRect(pos, size); + } + // Render it Renderer::drawTriangleStrips(&vertices[0], 4); - /* - // red text< / font> - //phrase< / font> - - float x2 = mSize.x(); - float y2 = mSize.y(); - - glEnable(GL_TEXTURE_2D); - glEnable(GL_BLEND); - glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA); - - // Build a texture for the video frame - mTexture->initFromPixels((unsigned char*)mContext.surface->pixels, mContext.surface->w, mContext.surface->h); - mTexture->bind(); - - glBegin(GL_QUADS); - - glColor4f(1.0f, 1.0f, 1.0f, t); - glTexCoord2f(0, 0); - glVertex2f(0, 0); - - glColor4f(1.0f, 1.0f, 1.0f, t); - glTexCoord2f(0, 1.0f); - glVertex2f(0, y2); - - glColor4f(1.0f, 1.0f, 1.0f, t); - glTexCoord2f(1.0f, 1.0f); - glVertex2f(x2, y2); - - glColor4f(1.0f, 1.0f, 1.0f, t); - glTexCoord2f(1.0f, 0); - glVertex2f(x2, 0); - - glEnd(); + if (mTargetIsMin) + Renderer::popClipRect(); - glDisable(GL_TEXTURE_2D); - glDisable(GL_BLEND); */ + Renderer::bindTexture(0); } void VideoVlcComponent::setupContext() diff --git a/es-core/src/components/VideoVlcComponent.h b/es-core/src/components/VideoVlcComponent.h index 6246ae6b9a..81ae70e679 100644 --- a/es-core/src/components/VideoVlcComponent.h +++ b/es-core/src/components/VideoVlcComponent.h @@ -47,6 +47,7 @@ class VideoVlcComponent : public VideoComponent // Can be set before or after a video is loaded. // Never breaks the aspect ratio. setMaxSize() and setResize() are mutually exclusive. void setMaxSize(float width, float height); + void setMinSize(float width, float height); private: // Calculates the correct mSize from our resizing information (set by setResize/setMaxSize). @@ -59,6 +60,8 @@ class VideoVlcComponent : public VideoComponent // Handle looping the video. Must be called periodically virtual void handleLooping(); + virtual void onVideoStarted(); + void setupContext(); void freeContext(); From 284014cb08696eeccb37b3023338d3508c2a27bc Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 28 Aug 2019 15:54:28 +0200 Subject: [PATCH 124/336] SystemView videos : better load/unload timings during transitions. --- es-app/src/views/SystemView.cpp | 17 +++++++++++++++-- es-app/src/views/SystemView.h | 2 ++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index c9bc8fe3ec..e365660e2e 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -25,6 +25,7 @@ SystemView::SystemView(Window* window) : IList(wind mScreensaverActive = false; mDisable = false; mShowing = false; + mLastCursor = 0; setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); populate(); @@ -304,6 +305,13 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) if(endPos == mCamOffset && endPos == mExtrasCamOffset) return; + + if (mLastCursor == mCursor) + return; + + int oldCursor = mLastCursor; + mLastCursor = mCursor; + Animation* anim; bool move_carousel = Settings::getInstance()->getBool("MoveCarousel"); if(transition_style == "fade") @@ -366,12 +374,17 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) } for (int i = 0; i < mEntries.size(); i++) - if (i != mCursor) + if (i != oldCursor && i != mCursor) activateExtras(i, false); + activateExtras(mCursor); + setAnimation(anim, 0, [this] { - activateExtras(mCursor); + for (int i = 0; i < mEntries.size(); i++) + if (i != mCursor) + activateExtras(i, false); + }, false, 0); } diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index 5cde505b05..c793ad886a 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -105,6 +105,8 @@ class SystemView : public IList bool mShowing; bool mDisable; bool mScreensaverActive; + + int mLastCursor; }; #endif // ES_APP_VIEWS_SYSTEM_VIEW_H From 2ac609421a27124c7b7f306b723874a62a14c0a6 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 28 Aug 2019 21:48:24 +0200 Subject: [PATCH 125/336] Menu icons : make theme elements dynamic - they can now be extented simply in GuiMenu --- es-app/src/guis/GuiMenu.cpp | 63 ++++++++++-------------- es-app/src/guis/GuiMenu.h | 5 +- es-app/src/guis/GuiSettings.cpp | 1 + es-app/src/guis/GuiSettings.h | 3 +- es-core/src/ThemeData.cpp | 53 ++++++++------------ es-core/src/ThemeData.h | 31 ++++++------ es-core/src/components/MenuComponent.cpp | 60 +++++++++++++++++++++- es-core/src/components/MenuComponent.h | 3 +- 8 files changed, 133 insertions(+), 86 deletions(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 07e001076b..d8b8ce5b1e 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -33,22 +33,22 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _("MAIN M if (isFullUI) { - addEntry(_("UI SETTINGS"), theme->Text.color, true, [this] { openUISettings(); }, theme->MenuIcons.ui); - addEntry(_("CONFIGURE INPUT"), theme->Text.color, true, [this] { openConfigInput(); }, theme->MenuIcons.controllers); + addEntry(_("UI SETTINGS"), true, [this] { openUISettings(); }, "iconUI"); + addEntry(_("CONFIGURE INPUT"), true, [this] { openConfigInput(); }, "iconControllers"); } - addEntry(_("SOUND SETTINGS"), theme->Text.color, true, [this] { openSoundSettings(); }, theme->MenuIcons.sound); + addEntry(_("SOUND SETTINGS"), true, [this] { openSoundSettings(); }, "iconSound"); if (isFullUI) - addEntry(_("SCRAPER"), theme->Text.color, true, [this] { openScraperSettings(); }, theme->MenuIcons.scraper); + addEntry(_("SCRAPER"), true, [this] { openScraperSettings(); }, "iconScraper"); if (isFullUI) { - addEntry(_("GAME COLLECTION SETTINGS"), theme->Text.color, true, [this] { openCollectionSystemSettings(); }, theme->MenuIcons.games); - addEntry(_("ADVANCED SETTINGS"), theme->Text.color, true, [this] { openOtherSettings(); }, theme->MenuIcons.advanced); + addEntry(_("GAME COLLECTION SETTINGS"), true, [this] { openCollectionSystemSettings(); }, "iconGames"); + addEntry(_("ADVANCED SETTINGS"), true, [this] { openOtherSettings(); }, "iconAdvanced"); } - addEntry(_("QUIT"), theme->Text.color, !Settings::getInstance()->getBool("ShowOnlyExit"), [this] {openQuitMenu(); }, theme->MenuIcons.quit); + addEntry(_("QUIT"), !Settings::getInstance()->getBool("ShowOnlyExit"), [this] {openQuitMenu(); }, "iconQuit"); addChild(&mMenu); addVersionInfo(); @@ -103,12 +103,7 @@ void GuiMenu::openScraperSettings() s->addWithLabel(_("SCRAPE RATINGS"), scrape_ratings); s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); }); - - - - - - + // image source std::string imageSourceName = Settings::getInstance()->getString("ScrapperImageSrc"); auto imageSource = std::make_shared< OptionListComponent >(mWindow, _("PREFERED IMAGE SOURCE"), false); imageSource->add(_("NONE"), "", imageSourceName.empty()); @@ -140,27 +135,19 @@ void GuiMenu::openScraperSettings() Settings::getInstance()->setString("ScrapperThumbSrc", thumbSource->getSelected()); }); - // scrape video auto scrape_video = std::make_shared(mWindow); scrape_video->setState(Settings::getInstance()->getBool("ScrapeVideos")); s->addWithLabel(_("SCRAPE VIDEOS"), scrape_video); s->addSaveFunc([scrape_video] { Settings::getInstance()->setBool("ScrapeVideos", scrape_video->getState()); }); - // scrape now ComponentListRow row; auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; std::function openAndSave = openScrapeNow; openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; - row.makeAcceptInputHandler(openAndSave); - - auto scrape_now = std::make_shared(mWindow, _("SCRAPE NOW"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); - auto bracket = makeArrow(mWindow); - row.addElement(scrape_now, true); - row.addElement(bracket, false); - s->addRow(row); - + s->addEntry(_("SCRAPE NOW"), true, openAndSave, "iconScraper"); + s->updatePosition(); mWindow->pushGui(s); } @@ -1119,28 +1106,32 @@ void GuiMenu::onSizeChanged() mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); } -void GuiMenu::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func, const std::string iconName) +void GuiMenu::addEntry(std::string name, bool add_arrow, const std::function& func, const std::string iconName) { auto theme = ThemeData::getMenuTheme(); std::shared_ptr font = theme->Text.font; - color = theme->Text.color; + unsigned int color = theme->Text.color; // populate the list ComponentListRow row; if (!iconName.empty()) { - // icon - auto icon = std::make_shared(mWindow); - icon->setImage(iconName); - icon->setColorShift(theme->Text.color); - icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); - row.addElement(icon, false); - - // spacer between icon and text - auto spacer = std::make_shared(mWindow); - spacer->setSize(10, 0); - row.addElement(spacer, false); + std::string iconPath = theme->getMenuIcon(iconName); + if (!iconPath.empty()) + { + // icon + auto icon = std::make_shared(mWindow); + icon->setImage(iconPath); + icon->setColorShift(theme->Text.color); + icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); + row.addElement(icon, false); + + // spacer between icon and text + auto spacer = std::make_shared(mWindow); + spacer->setSize(10, 0); + row.addElement(spacer, false); + } } row.addElement(std::make_shared(mWindow, name, font, color), true); diff --git a/es-app/src/guis/GuiMenu.h b/es-app/src/guis/GuiMenu.h index 99c7ef5445..b70a871412 100644 --- a/es-app/src/guis/GuiMenu.h +++ b/es-app/src/guis/GuiMenu.h @@ -5,6 +5,8 @@ #include "components/MenuComponent.h" #include "GuiComponent.h" +class GuiSettings; + class GuiMenu : public GuiComponent { public: @@ -16,7 +18,8 @@ class GuiMenu : public GuiComponent HelpStyle getHelpStyle() override; private: - void addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func, const std::string iconName); + void addEntry(std::string name, bool add_arrow, const std::function& func, const std::string iconName = ""); + void addVersionInfo(); void openCollectionSystemSettings(); void openConfigInput(); diff --git a/es-app/src/guis/GuiSettings.cpp b/es-app/src/guis/GuiSettings.cpp index 2ce6eb5433..66193fc6ab 100644 --- a/es-app/src/guis/GuiSettings.cpp +++ b/es-app/src/guis/GuiSettings.cpp @@ -81,3 +81,4 @@ std::vector GuiSettings::getHelpPrompts() return prompts; } + diff --git a/es-app/src/guis/GuiSettings.h b/es-app/src/guis/GuiSettings.h index 205451dbee..0f20a530b3 100644 --- a/es-app/src/guis/GuiSettings.h +++ b/es-app/src/guis/GuiSettings.h @@ -13,8 +13,9 @@ class GuiSettings : public GuiComponent void updatePosition(); inline void addRow(const ComponentListRow& row) { mMenu.addRow(row); }; - inline void addWithLabel(const std::string& label, const std::shared_ptr& comp) { mMenu.addWithLabel(label, comp); }; + inline void addWithLabel(const std::string& label, const std::shared_ptr& comp, const std::string iconName = "") { mMenu.addWithLabel(label, comp, iconName); }; inline void addSaveFunc(const std::function& func) { mSaveFuncs.push_back(func); }; + inline void addEntry(const std::string name, bool add_arrow, const std::function& func, const std::string iconName = "") { mMenu.addEntry(name, add_arrow, func, iconName); }; inline void addSubMenu(const std::string& label, const std::function& func) { ComponentListRow row; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 2e48a22066..cc01dc9f89 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -768,21 +768,31 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapsecond; if (!overwrite && element.properties.find(node.name()) != element.properties.cend()) continue; std::string str = resolveSystemVariable(mSystemThemeFolder, resolvePlaceholders(node.text().as_string())); - switch(typeIt->second) + switch(type) { case NORMALIZED_RECT: { @@ -1144,35 +1154,14 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) Icons.knob = elem->get("path"); elem = theme->getElement("menu", "menuicons", "menuIcons"); - if (elem) { - if (elem->has("iconSystem") && ResourceManager::getInstance()->fileExists(elem->get("iconSystem"))) - MenuIcons.system = elem->get("iconSystem"); - - if (elem->has("iconUpdates") && ResourceManager::getInstance()->fileExists(elem->get("iconUpdates"))) - MenuIcons.updates = elem->get("iconUpdates"); - - if (elem->has("iconGames") && ResourceManager::getInstance()->fileExists(elem->get("iconGames"))) - MenuIcons.games = elem->get("iconGames"); - - if (elem->has("iconControllers") && ResourceManager::getInstance()->fileExists(elem->get("iconControllers"))) - MenuIcons.controllers = elem->get("iconControllers"); - - if (elem->has("iconUI") && ResourceManager::getInstance()->fileExists(elem->get("iconUI"))) - MenuIcons.ui = elem->get("iconUI"); - - if (elem->has("iconSound") && ResourceManager::getInstance()->fileExists(elem->get("iconSound"))) - MenuIcons.sound = elem->get("iconSound"); - - if (elem->has("iconScraper") && ResourceManager::getInstance()->fileExists(elem->get("iconScraper"))) - MenuIcons.scraper = elem->get("iconScraper"); - - if (elem->has("iconAdvanced") && ResourceManager::getInstance()->fileExists(elem->get("iconAdvanced"))) - MenuIcons.advanced = elem->get("iconAdvanced"); - - if (elem->has("iconQuit") && ResourceManager::getInstance()->fileExists(elem->get("iconQuit"))) - MenuIcons.quit = elem->get("iconQuit"); + for (auto prop : elem->properties) + { + std::string path = prop.second.s; + if (!path.empty() && ResourceManager::getInstance()->fileExists(path)) + setMenuIcon(prop.first, path); + } } } diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index e98b011a2c..6add262aa1 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -106,19 +106,6 @@ struct IconElement { std::string knob; }; -struct MenuIconElement { - - std::string system; - std::string updates; - std::string games; - std::string controllers; - std::string ui; - std::string sound; - std::string scraper; - std::string advanced; - std::string quit; -}; - class ThemeData { public: @@ -132,8 +119,24 @@ class ThemeData MenuElement Text{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0, "", "", nullptr }; MenuElement TextSmall{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0, "", "", nullptr }; MenuElement Footer{ 0xC6C6C6FF, 0xC6C6C6FF, 0xC6C6C6FF, 0xFFFFFFFF, 0, "", "", nullptr }; - MenuIconElement MenuIcons { "","","","","","","","","" }; IconElement Icons { ":/button.png", ":/button_filled.png", ":/on.svg", ":/off.svg", ":/option_arrow.svg", ":/arrow.svg", ":/slider_knob.svg" }; + + std::string getMenuIcon(const std::string name) + { + auto it = mMenuIcons.find(name); + if (it != mMenuIcons.cend()) + return it->second; + + return ""; + } + + void setMenuIcon(const std::string name, const std::string path) + { + mMenuIcons[name] = path; + } + + private: + std::map mMenuIcons; }; diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 82753fb796..0a594778bc 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -55,16 +55,74 @@ MenuComponent::MenuComponent(Window* window, std::string title, const std::share mGrid.resetCursor(); } -void MenuComponent::addWithLabel(const std::string& label, const std::shared_ptr& comp, bool setCursorHere, bool invert_when_selected) +void MenuComponent::addWithLabel(const std::string& label, const std::shared_ptr& comp, const std::string iconName, bool setCursorHere, bool invert_when_selected) { auto theme = ThemeData::getMenuTheme(); ComponentListRow row; + + if (!iconName.empty()) + { + std::string iconPath = theme->getMenuIcon(iconName); + if (!iconPath.empty()) + { + // icon + auto icon = std::make_shared(mWindow); + icon->setImage(iconPath); + icon->setColorShift(theme->Text.color); + icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); + row.addElement(icon, false); + + // spacer between icon and text + auto spacer = std::make_shared(mWindow); + spacer->setSize(10, 0); + row.addElement(spacer, false); + } + } + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(label), theme->Text.font, theme->Text.color), true); row.addElement(comp, false, invert_when_selected); addRow(row, setCursorHere); } +void MenuComponent::addEntry(const std::string name, bool add_arrow, const std::function& func, const std::string iconName, bool setCursorHere, bool invert_when_selected) +{ + auto theme = ThemeData::getMenuTheme(); + std::shared_ptr font = theme->Text.font; + unsigned int color = theme->Text.color; + + // populate the list + ComponentListRow row; + + if (!iconName.empty()) + { + std::string iconPath = theme->getMenuIcon(iconName); + if (!iconPath.empty()) + { + // icon + auto icon = std::make_shared(mWindow); + icon->setImage(iconPath); + icon->setColorShift(theme->Text.color); + icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); + row.addElement(icon, false); + + // spacer between icon and text + auto spacer = std::make_shared(mWindow); + spacer->setSize(10, 0); + row.addElement(spacer, false); + } + } + + row.addElement(std::make_shared(mWindow, name, font, color), true, invert_when_selected); + + if (add_arrow) + row.addElement(makeArrow(mWindow), false); + + row.makeAcceptInputHandler(func); + + addRow(row, setCursorHere); +} + void MenuComponent::setTitle(const char* title, const std::shared_ptr& font) { mTitle->setText(Utils::String::toUpper(title)); diff --git a/es-core/src/components/MenuComponent.h b/es-core/src/components/MenuComponent.h index 71926ce348..ce6bf212f2 100644 --- a/es-core/src/components/MenuComponent.h +++ b/es-core/src/components/MenuComponent.h @@ -26,7 +26,8 @@ class MenuComponent : public GuiComponent inline void addRow(const ComponentListRow& row, bool setCursorHere = false) { mList->addRow(row, setCursorHere); updateSize(); } - void addWithLabel(const std::string& label, const std::shared_ptr& comp, bool setCursorHere = false, bool invert_when_selected = true); + void addWithLabel(const std::string& label, const std::shared_ptr& comp, const std::string iconName = "", bool setCursorHere = false, bool invert_when_selected = true); + void addEntry(const std::string name, bool add_arrow, const std::function& func, const std::string iconName="", bool setCursorHere = false, bool invert_when_selected = true); void addButton(const std::string& label, const std::string& helpText, const std::function& callback); From 7820c1e9599a843a0f2e8de0f5ec9b0693b19feb Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 29 Aug 2019 10:37:11 +0200 Subject: [PATCH 126/336] Sound : Fix a crash when returning from a game when Navigation Sounds are enabled and the theme define sounds. --- es-core/src/AudioManager.cpp | 49 ++++++++++++++++++++---------------- es-core/src/Sound.cpp | 14 ++++++----- es-core/src/ThemeData.cpp | 8 +----- es-core/src/ThemeData.h | 5 ---- 4 files changed, 36 insertions(+), 40 deletions(-) diff --git a/es-core/src/AudioManager.cpp b/es-core/src/AudioManager.cpp index 3a6f0912f5..ffcf1e306e 100644 --- a/es-core/src/AudioManager.cpp +++ b/es-core/src/AudioManager.cpp @@ -145,6 +145,9 @@ void AudioManager::findMusic(const std::string &path, std::vector& void AudioManager::playRandomMusic(bool continueIfPlaying) { + if (!mInitialized) + return; + std::vector musics; // check in Theme music directory @@ -180,14 +183,16 @@ void AudioManager::playRandomMusic(bool continueIfPlaying) playMusic(musics.at(randomIndex)); mRunningFromPlaylist = true; - Mix_HookMusicFinished(AudioManager::onMusicFinished); } void AudioManager::playMusic(std::string path) { + if (!mInitialized) + return; + // free the previous music stopMusic(); - + // load a new music mCurrentMusic = Mix_LoadMUS(path.c_str()); if (mCurrentMusic == NULL) @@ -227,28 +232,28 @@ void AudioManager::themeChanged(const std::shared_ptr& theme) { mCurrentThemeMusicDirectory = ""; - if (Settings::getInstance()->getBool("audio.bgmusic")) - { - const ThemeData::ThemeElement* elem = theme->getElement("system", "directory", "sound"); - if (elem && elem->has("path")) - mCurrentThemeMusicDirectory = elem->get("path"); - - std::string bgSound; + if (!Settings::getInstance()->getBool("audio.bgmusic")) + return; + + const ThemeData::ThemeElement* elem = theme->getElement("system", "directory", "sound"); + if (elem && elem->has("path")) + mCurrentThemeMusicDirectory = elem->get("path"); - elem = theme->getElement("system", "bgsound", "sound"); - if (elem && elem->has("path") && Utils::FileSystem::exists(elem->get("path"))) - bgSound = elem->get("path"); + std::string bgSound; - // Found a music for the system - if (!bgSound.empty()) - { - mRunningFromPlaylist = false; - playMusic(bgSound); - return; - } + elem = theme->getElement("system", "bgsound", "sound"); + if (elem && elem->has("path") && Utils::FileSystem::exists(elem->get("path"))) + bgSound = elem->get("path"); - mSystemName = theme->getSystemThemeFolder(); - if (!mRunningFromPlaylist || Settings::getInstance()->getBool("audio.persystem")) - playRandomMusic(false); + // Found a music for the system + if (!bgSound.empty()) + { + mRunningFromPlaylist = false; + playMusic(bgSound); + return; } + + mSystemName = theme->getSystemThemeFolder(); + if (!mRunningFromPlaylist || Settings::getInstance()->getBool("audio.persystem")) + playRandomMusic(false); } diff --git a/es-core/src/Sound.cpp b/es-core/src/Sound.cpp index 60f6a233ca..a8495868a6 100644 --- a/es-core/src/Sound.cpp +++ b/es-core/src/Sound.cpp @@ -23,7 +23,7 @@ std::shared_ptr Sound::getFromTheme(const std::shared_ptr& the LOG(LogInfo) << " req sound [" << view << "." << element << "]"; const ThemeData::ThemeElement* elem = theme->getElement(view, element, "sound"); - if(!elem || !elem->has("path")) + if(elem == nullptr || !elem->has("path")) { LOG(LogInfo) << " (missing)"; return get(""); @@ -50,10 +50,9 @@ void Sound::loadFile(const std::string & path) void Sound::init() { - if (mSampleData != nullptr) - deinit(); + deinit(); - if (mPath.empty()) + if (mPath.empty() || !Utils::FileSystem::exists(mPath)) return; if (!Settings::getInstance()->getBool("EnableSounds")) @@ -72,8 +71,11 @@ void Sound::deinit() { mPlaying = false; - if (mSampleData != nullptr) - Mix_FreeChunk(mSampleData); + if (mSampleData == nullptr) + return; + + Mix_FreeChunk(mSampleData); + mSampleData = nullptr; } void Sound::play() diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index cc01dc9f89..ee3ef2b63c 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -1085,7 +1085,6 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) } elem = theme->getElement("menu", "menufooter", "menuText"); - if (elem) { if (elem->has("fontPath") || elem->has("fontSize")) @@ -1095,7 +1094,6 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) } elem = theme->getElement("menu", "menutextsmall", "menuTextSmall"); - if (elem) { if (elem->has("fontPath") || elem->has("fontSize")) @@ -1110,7 +1108,6 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) } elem = theme->getElement("menu", "menutext", "menuText"); - if (elem) { if (elem->has("fontPath") || elem->has("fontSize")) @@ -1129,7 +1126,6 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) } elem = theme->getElement("menu", "menubutton", "menuButton"); - if (elem) { if (elem->has("path")) @@ -1139,7 +1135,6 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) } elem = theme->getElement("menu", "menuswitch", "menuSwitch"); - if (elem) { if (elem->has("pathOn") && ResourceManager::getInstance()->fileExists(elem->get("pathOn"))) @@ -1149,7 +1144,6 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) } elem = theme->getElement("menu", "menuslider", "menuSlider"); - if (elem && elem->has("path") && ResourceManager::getInstance()->fileExists(elem->get("path"))) Icons.knob = elem->get("path"); @@ -1160,7 +1154,7 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) { std::string path = prop.second.s; if (!path.empty() && ResourceManager::getInstance()->fileExists(path)) - setMenuIcon(prop.first, path); + mMenuIcons[prop.first] = path; } } } diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 6add262aa1..2b0f52bf72 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -130,11 +130,6 @@ class ThemeData return ""; } - void setMenuIcon(const std::string name, const std::string path) - { - mMenuIcons[name] = path; - } - private: std::map mMenuIcons; }; From 667ba87973ce42a7173a0875b6dd75b49fcc9e4e Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 29 Aug 2019 11:08:50 +0200 Subject: [PATCH 127/336] Sounds : fixed way to restore sounds after AudioManager deinited/reinited --- es-core/src/AudioManager.cpp | 19 ++++++++++++------- es-core/src/AudioManager.h | 1 + es-core/src/Sound.cpp | 6 ++++++ 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/es-core/src/AudioManager.cpp b/es-core/src/AudioManager.cpp index ffcf1e306e..234f33f6e3 100644 --- a/es-core/src/AudioManager.cpp +++ b/es-core/src/AudioManager.cpp @@ -29,6 +29,14 @@ std::shared_ptr & AudioManager::getInstance() return sInstance; } +bool AudioManager::isInitialized() +{ + if (sInstance == nullptr) + return false; + + return sInstance->mInitialized; +} + void AudioManager::init() { if (mInitialized) @@ -42,13 +50,6 @@ void AudioManager::init() return; } - // Stop playing all Sounds & reload them - for (unsigned int i = 0; i < sSoundVector.size(); i++) - { - sSoundVector[i]->stop(); - sSoundVector[i]->init(); - } - //Open the audio device and pause if (Mix_OpenAudio(44100, MIX_DEFAULT_FORMAT, 2, 4096) < 0) LOG(LogError) << "MUSIC Error - Unable to open SDLMixer audio: " << SDL_GetError() << std::endl; @@ -56,6 +57,10 @@ void AudioManager::init() { mInitialized = true; LOG(LogInfo) << "SDL AUDIO Initialized"; + + // Reload sounds + for (unsigned int i = 0; i < sSoundVector.size(); i++) + sSoundVector[i]->init(); } } diff --git a/es-core/src/AudioManager.h b/es-core/src/AudioManager.h index 269ad847d3..b3d5ff7728 100644 --- a/es-core/src/AudioManager.h +++ b/es-core/src/AudioManager.h @@ -16,6 +16,7 @@ class AudioManager public: static std::shared_ptr & getInstance(); + static bool isInitialized(); void init(); void deinit(); diff --git a/es-core/src/Sound.cpp b/es-core/src/Sound.cpp index a8495868a6..59eb101bcd 100644 --- a/es-core/src/Sound.cpp +++ b/es-core/src/Sound.cpp @@ -52,6 +52,9 @@ void Sound::init() { deinit(); + if (!AudioManager::isInitialized()) + return; + if (mPath.empty() || !Utils::FileSystem::exists(mPath)) return; @@ -83,6 +86,9 @@ void Sound::play() if (mSampleData == nullptr) return; + if (!AudioManager::isInitialized()) + return; + mPlaying = true; Mix_PlayChannel(-1, mSampleData, 0); } From fe1a969207dcb4e0004fa0b22daa6b920dd7c12c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 29 Aug 2019 17:16:59 +0200 Subject: [PATCH 128/336] SystemView videos : better management. Videos : When a video is stopped because of opening the menu (but video file still bound), continue to display the last renderer frame. --- es-app/src/views/SystemView.cpp | 56 +++++++++++++------- es-app/src/views/SystemView.h | 7 ++- es-core/src/components/ImageComponent.cpp | 50 +---------------- es-core/src/components/VideoVlcComponent.cpp | 23 ++++++-- es-core/src/resources/TextureResource.cpp | 3 ++ 5 files changed, 65 insertions(+), 74 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index e365660e2e..4eb1ec3fb7 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -31,9 +31,27 @@ SystemView::SystemView(Window* window) : IList(wind populate(); } -void SystemView::populate() +SystemView::~SystemView() { + clearEntries(); +} + +void SystemView::clearEntries() +{ + for (int i = 0; i < mEntries.size(); i++) + { + for (auto extra : mEntries[i].data.backgroundExtras) + delete extra; + + mEntries[i].data.backgroundExtras.clear(); + } + mEntries.clear(); +} + +void SystemView::populate() +{ + clearEntries(); for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { @@ -224,7 +242,7 @@ bool SystemView::input(InputConfig* config, Input input) void SystemView::update(int deltaTime) { listUpdate(deltaTime); - updateExtras(deltaTime); + updateExtras([this, deltaTime](GuiComponent* p) { p->update(deltaTime); }); GuiComponent::update(deltaTime); } @@ -839,25 +857,38 @@ void SystemView::onShow() void SystemView::onHide() { mShowing = false; - activateExtras(mCursor); + updateExtras([this](GuiComponent* p) { p->onHide(); }); } void SystemView::onScreenSaverActivate() { mScreensaverActive = true; - activateExtras(mCursor); + updateExtras([this](GuiComponent* p) { p->onScreenSaverActivate(); }); } void SystemView::onScreenSaverDeactivate() { mScreensaverActive = false; - activateExtras(mCursor); + updateExtras([this](GuiComponent* p) { p->onScreenSaverDeactivate(); }); } void SystemView::topWindow(bool isTop) { mDisable = !isTop; - activateExtras(mCursor); + updateExtras([this, isTop](GuiComponent* p) { p->topWindow(isTop); }); +} + +void SystemView::updateExtras(const std::function& func) +{ + for (int i = 0; i < mEntries.size(); i++) + { + SystemViewData data = mEntries.at(i).data; + for (unsigned int j = 0; j < data.backgroundExtras.size(); j++) + { + GuiComponent* extra = data.backgroundExtras[j]; + func(extra); + } + } } void SystemView::activateExtras(int cursor, bool activate) @@ -877,16 +908,3 @@ void SystemView::activateExtras(int cursor, bool activate) extra->onHide(); } } - -void SystemView::updateExtras(int deltaTime) -{ - if (!mShowing) - return; - - for (int i = 0; i < mEntries.size(); i++) - { - SystemViewData data = mEntries.at(i).data; - for (unsigned int j = 0; j < data.backgroundExtras.size(); j++) - data.backgroundExtras[j]->update(deltaTime); - } -} \ No newline at end of file diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index c793ad886a..dc724061ba 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -7,6 +7,7 @@ #include "resources/Font.h" #include "GuiComponent.h" #include +#include class AnimatedImageComponent; class SystemData; @@ -49,6 +50,7 @@ class SystemView : public IList { public: SystemView(Window* window); + ~SystemView(); virtual void onShow() override; virtual void onHide() override; @@ -74,8 +76,9 @@ class SystemView : public IList Vector2f carouselPos(); Vector2f carouselSize(); - void activateExtras(int cursor, bool activate = true); - void updateExtras(int deltaTime); + void activateExtras(int cursor, bool activate = true); + void updateExtras(const std::function& func); + void clearEntries(); virtual void onScreenSaverActivate() override; virtual void onScreenSaverDeactivate() override; diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index e270c7f41f..0fe406f349 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -279,7 +279,7 @@ void ImageComponent::setOpacity(unsigned char opacity) void ImageComponent::updateVertices() { - if (!mTexture || !mTexture->isInitialized()) + if (!mTexture) return; // we go through this mess to make sure everything is properly rounded @@ -307,55 +307,7 @@ void ImageComponent::updateVertices() mVertices[i].tex[1] = py - mVertices[i].tex[1]; } } -/* -void ImageComponent::updateVerticesFca() -{ - if (!mTexture || !mTexture->isInitialized()) - return; - - // we go through this mess to make sure everything is properly rounded - // if we just round vertices at the end, edge cases occur near sizes of 0.5 - Vector2f size(Math::round(mSize.x()), Math::round(mSize.y())); - Vector2f topLeft(size * mTopLeftCrop); - Vector2f bottomRight(size * mBottomRightCrop); - - mVertices[0].pos = Vector2f(topLeft.x(), topLeft.y()); - mVertices[1].pos = Vector2f(topLeft.x(), bottomRight.y()); - mVertices[2].pos = Vector2f(bottomRight.x(), bottomRight.y()); - mVertices[3].pos = Vector2f(bottomRight.x(), topLeft.y()); - - float px, py; - if (mTexture->isTiled()) - { - px = mSize.x() / getTextureSize().x(); - py = mSize.y() / getTextureSize().y(); - } - else - { - px = 1; - py = 1; - } - mVertices[0].tex = Vector2f(mTopLeftCrop.x(), py - mTopLeftCrop.y()); - mVertices[1].tex = Vector2f(mTopLeftCrop.x(), 1 - mBottomRightCrop.y()); - - mVertices[2].tex = Vector2f(px * mBottomRightCrop.x(), 1 - mBottomRightCrop.y()); - mVertices[3].tex = Vector2f(px * mBottomRightCrop.x(), py - mTopLeftCrop.y()); - - - if (mFlipX) - { - for(int i = 0; i < 4; i++) - mVertices[i].tex[0] = px - mVertices[i].tex[0]; - } - - if (mFlipY) - { - for(int i = 0; i < 4; i++) - mVertices[i].tex[1] = py - mVertices[i].tex[1]; - } -} -*/ void ImageComponent::render(const Transform4x4f& parentTrans) { if (!mVisible) diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 4ce3ea3483..a914a75576 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -195,8 +195,17 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) { VideoComponent::render(parentTrans); + bool initFromPixels = true; + if (!mIsPlaying || !mContext.valid) - return; + { + // If video is still attached to the path & texture is initialized, we suppose it had just been stopped (onhide, ondisable, screensaver...) + // still render the last frame + if (mTexture != nullptr && !mVideoPath.empty() && mPlayingVideoPath == mVideoPath && mTexture->isInitialized()) + initFromPixels = false; + else + return; + } float t = mFadeIn; if (mFadeIn < 1.0) @@ -211,9 +220,17 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) return; Transform4x4f trans = parentTrans * getTransform(); - Renderer::setMatrix(trans); + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; + Renderer::setMatrix(trans); + + // Build a texture for the video frame + if (initFromPixels) + mTexture->initFromPixels((unsigned char*)mContext.surface->pixels, mContext.surface->w, mContext.surface->h); + const unsigned int fadeIn = t * 255.0f; const unsigned int color = Renderer::convertColor(0xFFFFFF00 | fadeIn); Renderer::Vertex vertices[4]; @@ -223,8 +240,6 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) vertices[2] = { { mSize.x(), 0.0f }, { 1.0f, 0.0f }, color }; vertices[3] = { { mSize.x(), mSize.y() }, { 1.0f, 1.0f }, color }; - // Build a texture for the video frame - mTexture->initFromPixels((unsigned char*)mContext.surface->pixels, mContext.surface->w, mContext.surface->h); mTexture->bind(); if (mTargetIsMin) diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index 4a9f14cad2..e13a636e8e 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -260,6 +260,9 @@ Vector2f TextureResource::getSourceImageSize() const bool TextureResource::isInitialized() const { + if (mTextureData != nullptr) + return mTextureData->isLoaded(); + return true; } From 2421638421fbbc15c56a90fc497b888b04e1b20e Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 30 Aug 2019 17:48:39 +0200 Subject: [PATCH 129/336] themable help images --- es-app/src/guis/GuiMenu.cpp | 37 ++++---------------- es-core/src/HelpStyle.cpp | 33 +++++++++++++++++ es-core/src/HelpStyle.h | 2 ++ es-core/src/Window.cpp | 3 +- es-core/src/components/HelpComponent.cpp | 7 +++- es-core/src/components/IList.h | 6 +++- es-core/src/components/ImageComponent.cpp | 2 +- es-core/src/components/OptionListComponent.h | 2 +- 8 files changed, 57 insertions(+), 35 deletions(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index d8b8ce5b1e..30261ce0b4 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -335,36 +335,13 @@ void GuiMenu::openUISettings() auto themeRegions = ThemeData::sortThemeSubSets(themeSubSets, "region"); // theme changed without setting options, forcing options to avoid crash/blank theme - if (themeRegions.empty()) - Settings::getInstance()->setString("ThemeRegionName", ""); - else - Settings::getInstance()->setString("ThemeRegionName", themeRegions.begin()->first); - - if (themeColorSets.empty()) - Settings::getInstance()->setString("ThemeColorSet", ""); - else - Settings::getInstance()->setString("ThemeColorSet", themeColorSets.begin()->first); - - if (themeIconSets.empty()) - Settings::getInstance()->setString("ThemeIconSet", ""); - else - Settings::getInstance()->setString("ThemeIconSet", themeIconSets.begin()->first); - - if (themeMenus.empty()) - Settings::getInstance()->setString("ThemeMenu", ""); - else - Settings::getInstance()->setString("ThemeMenu", themeMenus.begin()->first); - - if (themeSystemviewSets.empty()) - Settings::getInstance()->setString("ThemeSystemView", ""); - else - Settings::getInstance()->setString("ThemeSystemView", themeSystemviewSets.begin()->first); - - if (themeGamelistViewSets.empty()) - Settings::getInstance()->setString("ThemeGamelistView", ""); - else - Settings::getInstance()->setString("ThemeGamelistView", themeGamelistViewSets.begin()->first); - + Settings::getInstance()->setString("ThemeRegionName", themeRegions.empty() ? "" : themeRegions.begin()->first); + Settings::getInstance()->setString("ThemeColorSet", themeColorSets.empty() ? "" : themeColorSets.begin()->first); + Settings::getInstance()->setString("ThemeIconSet", themeIconSets.empty() ? "" : themeIconSets.begin()->first); + Settings::getInstance()->setString("ThemeMenu", themeMenus.empty() ? "" : themeMenus.begin()->first); + Settings::getInstance()->setString("ThemeSystemView", themeSystemviewSets.empty() ? "" : themeSystemviewSets.begin()->first); + Settings::getInstance()->setString("ThemeGamelistView", themeGamelistViewSets.empty() ? "" : themeGamelistViewSets.begin()->first); + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); CollectionSystemManager::get()->updateSystemsList(); ViewController::get()->goToStart(); diff --git a/es-core/src/HelpStyle.cpp b/es-core/src/HelpStyle.cpp index 3d3d4d6195..937ea22174 100644 --- a/es-core/src/HelpStyle.cpp +++ b/es-core/src/HelpStyle.cpp @@ -35,4 +35,37 @@ void HelpStyle::applyTheme(const std::shared_ptr& theme, const std::s if(elem->has("fontPath") || elem->has("fontSize")) font = Font::getFromTheme(elem, ThemeFlags::ALL, font); + + if (elem->has("iconUpDown")) + iconMap["up/down"] = elem->get("iconUpDown"); + + if (elem->has("iconLeftRight")) + iconMap["left/right"] = elem->get("iconLeftRight"); + + if (elem->has("iconUpDownLeftRight")) + iconMap["up/down/left/right"] = elem->get("iconUpDownLeftRight"); + + if (elem->has("iconA")) + iconMap["a"] = elem->get("iconA"); + + if (elem->has("iconB")) + iconMap["b"] = elem->get("iconB"); + + if (elem->has("iconX")) + iconMap["x"] = elem->get("iconX"); + + if (elem->has("iconY")) + iconMap["y"] = elem->get("iconY"); + + if (elem->has("iconL")) + iconMap["l"] = elem->get("iconL"); + + if (elem->has("iconR")) + iconMap["r"] = elem->get("iconR"); + + if (elem->has("iconStart")) + iconMap["start"] = elem->get("iconStart"); + + if (elem->has("iconSelect")) + iconMap["select"] = elem->get("iconSelect"); } diff --git a/es-core/src/HelpStyle.h b/es-core/src/HelpStyle.h index 8682a8d864..d1cab3fa5d 100644 --- a/es-core/src/HelpStyle.h +++ b/es-core/src/HelpStyle.h @@ -5,6 +5,7 @@ #include "math/Vector2f.h" #include #include +#include class Font; class ThemeData; @@ -16,6 +17,7 @@ struct HelpStyle unsigned int iconColor; unsigned int textColor; std::shared_ptr font; + std::map iconMap; HelpStyle(); // default values void applyTheme(const std::shared_ptr& theme, const std::string& view); diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 4afe6ef7dd..20fced3710 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -15,7 +15,8 @@ Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCoun mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) { mHelp = new HelpComponent(this); - mBackgroundOverlay = new ImageComponent(this); + mBackgroundOverlay = new ImageComponent(this); + mSplash = NULL; } diff --git a/es-core/src/components/HelpComponent.cpp b/es-core/src/components/HelpComponent.cpp index 1d7ef55291..6c2e26c060 100644 --- a/es-core/src/components/HelpComponent.cpp +++ b/es-core/src/components/HelpComponent.cpp @@ -72,7 +72,12 @@ void HelpComponent::updateGrid() for(auto it = mPrompts.cbegin(); it != mPrompts.cend(); it++) { auto icon = std::make_shared(mWindow); - icon->setImage(getIconTexture(it->first.c_str())); + + if (mStyle.iconMap.find(it->first) != mStyle.iconMap.end() && Utils::FileSystem::exists(mStyle.iconMap[it->first])) + icon->setImage(mStyle.iconMap[it->first]); + else + icon->setImage(getIconTexture(it->first.c_str())); + icon->setColorShift(mStyle.iconColor); icon->setResize(0, height); icons.push_back(icon); diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index bacb70c65f..fa836402fe 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -5,6 +5,7 @@ #include "components/ImageComponent.h" #include "resources/Font.h" #include "PowerSaver.h" +#include "ThemeData.h" enum CursorState { @@ -89,7 +90,7 @@ class IList : public GuiComponent mTitleOverlayOpacity = 0x00; mTitleOverlayColor = 0xFFFFFF00; mGradient.setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - mGradient.setImage(":/scroll_gradient.png"); + mGradient.setImage(ThemeData::getMenuTheme()->Background.fadePath); // ":/scroll_gradient.png"); mTitleOverlayFont = Font::get(FONT_SIZE_LARGE); } @@ -138,6 +139,9 @@ class IList : public GuiComponent void setCursorIndex(int index) { + if (mCursor == index) + return; + if (index >= 0 && index < (int)mEntries.size()) { mCursor = index; diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 0fe406f349..dfadba2b91 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -510,6 +510,6 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s std::vector ImageComponent::getHelpPrompts() { std::vector ret; - ret.push_back(HelpPrompt("a", "SELECTIONNER")); + ret.push_back(HelpPrompt("a", _("SELECT"))); return ret; } diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index df46455c77..c7523e22d2 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -166,7 +166,7 @@ class OptionListComponent : public GuiComponent addChild(&mLeftArrow); mRightArrow.setImage(ThemeData::getMenuTheme()->Icons.option_arrow); // ":/option_arrow.svg"); - mLeftArrow.setColorShift(theme->Text.color); + mRightArrow.setColorShift(theme->Text.color); addChild(&mRightArrow); } From 95ff441221e319639de1e167d182458ca662f241 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 31 Aug 2019 16:31:10 +0200 Subject: [PATCH 130/336] Fixes for last merge with Retropie changes ( gradients ) --- es-app/src/components/TextListComponent.h | 2 - es-app/src/views/SystemView.cpp | 4 +- es-core/src/ThemeData.cpp | 6 +++ es-core/src/ThemeData.h | 11 +++-- es-core/src/Window.cpp | 2 +- es-core/src/components/ComponentList.cpp | 22 ++++----- es-core/src/components/ImageComponent.cpp | 15 +++--- es-core/src/renderers/Renderer.cpp | 56 +++++------------------ es-core/src/renderers/Renderer.h | 6 +-- 9 files changed, 46 insertions(+), 78 deletions(-) diff --git a/es-app/src/components/TextListComponent.h b/es-app/src/components/TextListComponent.h index 6936d3b5dc..365ae44c0f 100644 --- a/es-app/src/components/TextListComponent.h +++ b/es-app/src/components/TextListComponent.h @@ -397,8 +397,6 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, c setColor(0, elem->get("primaryColor")); if(elem->has("secondaryColor")) setColor(1, elem->get("secondaryColor")); - if (elem->has("selectorColorEnd")) - setSelectorGradientColor(elem->get("selectorColorEnd")); } setFont(Font::getFromTheme(elem, properties, mFont)); diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 9bc2d4ffc3..22519d3d74 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -534,8 +534,8 @@ void SystemView::renderCarousel(const Transform4x4f& trans) Renderer::setMatrix(carouselTrans); Renderer::drawRect(0.0, 0.0, mCarousel.size.x(), mCarousel.size.y(), mCarousel.color, mCarousel.colorEnd, mCarousel.colorGradientHorizontal); - if (mCarousel.colorGradient != 0) - Renderer::drawGradientRect(0.0, 0.0, carouselSize().x(), carouselSize().y(), mCarousel.color, mCarousel.colorGradient, mCarousel.colorGradientHorz); + if (mCarousel.colorEnd != 0) + Renderer::drawRect(0.0, 0.0, carouselSize().x(), carouselSize().y(), mCarousel.color, mCarousel.colorEnd, mCarousel.colorGradientHorizontal); else Renderer::drawRect(0.0, 0.0, carouselSize().x(), carouselSize().y(), mCarousel.color); diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index e8c3beaaa3..8e509bc652 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -207,6 +207,7 @@ std::map> The { "separatorColor", COLOR }, { "selectorColor", COLOR }, { "selectorColorEnd", COLOR }, + { "selectorGradientType", STRING }, { "selectedColor", COLOR }, { "color", COLOR } } }, @@ -1130,9 +1131,14 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) if (elem->has("selectedColor")) Text.selectedColor = elem->get("selectedColor"); if (elem->has("selectorColor")) + { Text.selectorColor = elem->get("selectorColor"); + Text.selectorGradientColor = Text.selectorColor; + } if (elem->has("selectorColorEnd")) Text.selectorGradientColor = elem->get("selectorColorEnd"); + if (elem->has("selectorGradientType")) + Text.selectorGradientType = !(elem->get("selectorGradientType").compare("horizontal")); } elem = theme->getElement("menu", "menubutton", "menuButton"); diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 20d541a814..89d0bcb5e6 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -92,6 +92,7 @@ struct MenuElement { unsigned int selectorColor; unsigned int separatorColor; unsigned int selectorGradientColor; + bool selectorGradientType; std::string path; std::string fadePath; std::shared_ptr font; @@ -115,11 +116,11 @@ class ThemeData public: ThemeMenu(ThemeData* theme); - MenuElement Background{ 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0, ":/frame.png", ":/scroll_gradient.png", nullptr }; - MenuElement Title{ 0x555555FF, 0x555555FF, 0x555555FF, 0xFFFFFFFF, 0, "", "", nullptr }; - MenuElement Text{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0, "", "", nullptr }; - MenuElement TextSmall{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0, "", "", nullptr }; - MenuElement Footer{ 0xC6C6C6FF, 0xC6C6C6FF, 0xC6C6C6FF, 0xFFFFFFFF, 0, "", "", nullptr }; + MenuElement Background{ 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, true, ":/frame.png", ":/scroll_gradient.png", nullptr }; + MenuElement Title{ 0x555555FF, 0x555555FF, 0x555555FF, 0xFFFFFFFF, 0x555555FF, true, "", "", nullptr }; + MenuElement Text{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0x878787FF, true, "", "", nullptr }; + MenuElement TextSmall{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0x878787FF, true, "", "", nullptr }; + MenuElement Footer{ 0xC6C6C6FF, 0xC6C6C6FF, 0xC6C6C6FF, 0xFFFFFFFF, 0xC6C6C6FF, true, "", "", nullptr }; IconElement Icons { ":/button.png", ":/button_filled.png", ":/on.svg", ":/off.svg", ":/option_arrow.svg", ":/arrow.svg", ":/slider_knob.svg" }; std::string getMenuIcon(const std::string name) diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 20fced3710..e3e18e642c 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -339,7 +339,7 @@ void Window::renderLoadingScreen(std::string text, float percent, unsigned char float y = Renderer::getScreenHeight() - (Renderer::getScreenHeight() * 3 * baseHeight); Renderer::drawRect(x, y, w, h, 0x25252500 | opacity); - Renderer::drawGradientRect(x, y, (w*percent), h, 0x006C9E00 | opacity, 0x003E5C00 | opacity); // 0xFFFFFFFF + Renderer::drawRect(x, y, (w*percent), h, 0x006C9E00 | opacity, 0x003E5C00 | opacity, true); // 0xFFFFFFFF } ImageComponent splash(this, true); diff --git a/es-core/src/components/ComponentList.cpp b/es-core/src/components/ComponentList.cpp index 43b54361da..0880cec057 100644 --- a/es-core/src/components/ComponentList.cpp +++ b/es-core/src/components/ComponentList.cpp @@ -167,6 +167,7 @@ void ComponentList::render(const Transform4x4f& parentTrans) unsigned int bgColor = menuTheme->Background.color; unsigned int separatorColor = menuTheme->Text.separatorColor; unsigned int textColor = menuTheme->Text.color; + bool selectorGradientHorz = menuTheme->Text.selectorGradientType; Transform4x4f trans = parentTrans * getTransform(); trans.round(); @@ -211,21 +212,18 @@ void ComponentList::render(const Transform4x4f& parentTrans) // (1 - dst) + 0x77 const float selectedRowHeight = getRowHeight(mEntries.at(mCursor).data); - + /* + if ((selectorColor != bgColor) && ((selectorColor & 0xFF) != 0x00)) { + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, Renderer::Blend::ZERO, Renderer::Blend::ONE_MINUS_SRC_COLOR); + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, selectorGradientColor, selectorGradientHorz, Renderer::Blend::ONE, Renderer::Blend::ONE); + } + */ if ((selectorColor != bgColor) && ((selectorColor & 0xFF) != 0x00)) { - if (selectorGradientColor != 0) - { - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, Renderer::Blend::ZERO, Renderer::Blend::ONE_MINUS_SRC_COLOR); - Renderer::drawGradientRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, selectorGradientColor, false, Renderer::Blend::ONE, Renderer::Blend::ONE); - } - else - { - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, Renderer::Blend::ZERO, Renderer::Blend::ONE_MINUS_SRC_COLOR); - Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, Renderer::Blend::ONE, Renderer::Blend::ONE); - } + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, bgColor, Renderer::Blend::ZERO, Renderer::Blend::ONE_MINUS_SRC_COLOR); + Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, selectorColor, selectorGradientColor, selectorGradientHorz, Renderer::Blend::ONE, Renderer::Blend::ONE); } - + // Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0xFFFFFFFF, GL_ONE_MINUS_DST_COLOR, GL_ZERO); // Renderer::drawRect(0.0f, mSelectorBarOffset, mSize.x(), selectedRowHeight, 0x777777FF, GL_ONE, GL_ONE); diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index fc14105d86..9a1a789c39 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -22,7 +22,7 @@ Vector2f ImageComponent::getSize() const } ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : GuiComponent(window), - mTargetIsMax(false), mTargetIsMin(false), mFlipX(false), mFlipY(false), mTargetSize(0, 0), mColorShift(0xFFFFFFFF), + mTargetIsMax(false), mTargetIsMin(false), mFlipX(false), mFlipY(false), mTargetSize(0, 0), mColorShift(0xFFFFFFFF), mColorShiftEnd(0xFFFFFFFF), mForceLoad(forceLoad), mDynamic(dynamic), mFadeOpacity(0), mFading(false), mRotateByTargetSize(false), mVisible(true), mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f), mMirror(0.0f, 0.0f), mAllowAsync(false) { @@ -266,6 +266,7 @@ void ImageComponent::setFlipY(bool flip) void ImageComponent::setColorShift(unsigned int color) { mColorShift = color; + mColorShiftEnd = color; // Grab the opacity from the color shift because we may need to apply it if // fading textures in mOpacity = color & 0xff; @@ -274,16 +275,11 @@ void ImageComponent::setColorShift(unsigned int color) void ImageComponent::setColorShiftEnd(unsigned int color) { mColorShiftEnd = color; - // Grab the opacity from the color shift because we may need to apply it if - // fading textures in - mOpacity = color & 0xff; - updateColors(); } void ImageComponent::setColorGradientHorizontal(bool horizontal) { mColorGradientHorizontal = horizontal; - updateColors(); } void ImageComponent::setOpacity(unsigned char opacity) @@ -354,9 +350,12 @@ void ImageComponent::render(const Transform4x4f& parentTrans) fadeIn(mTexture->bind()); const unsigned int color = Renderer::convertColor(mColorShift); + const unsigned int colorEnd = Renderer::convertColor(mColorShiftEnd); - for (int i = 0; i < 4; ++i) - mVertices[i].col = color; + mVertices[0].col = color; + mVertices[1].col = mColorGradientHorizontal ? colorEnd : color; + mVertices[2].col = mColorGradientHorizontal ? color : colorEnd; + mVertices[3].col = colorEnd; Renderer::drawTriangleStrips(&mVertices[0], 4); diff --git a/es-core/src/renderers/Renderer.cpp b/es-core/src/renderers/Renderer.cpp index 6a6e280bc7..96d27b879e 100644 --- a/es-core/src/renderers/Renderer.cpp +++ b/es-core/src/renderers/Renderer.cpp @@ -334,64 +334,30 @@ namespace Renderer void drawRect(const float _x, const float _y, const float _w, const float _h, const unsigned int _color, const Blend::Factor _srcBlendFactor, const Blend::Factor _dstBlendFactor) { - drawRect((int)Math::round(_x), (int)Math::round(_y), (int)Math::round(_w), (int)Math::round(_h), _color, _colorEnd, horizontalGradient, _srcBlendFactor, _dstBlendFactor); - + drawRect(_x, _y, _w, _h, _color, _color, true, _srcBlendFactor, _dstBlendFactor); } // drawRect - void drawRect(const int _x, const int _y, const int _w, const int _h, const unsigned int _color, const unsigned int _colorEnd, bool horizontalGradient, const Blend::Factor _srcBlendFactor, const Blend::Factor _dstBlendFactor) + void drawRect(const float _x, const float _y, const float _w, const float _h, const unsigned int _color, const unsigned int _colorEnd, bool horizontalGradient, const Blend::Factor _srcBlendFactor, const Blend::Factor _dstBlendFactor) { + int x = Math::round(_x); + int y = Math::round(_y); + int w = Math::round(_w); + int h = Math::round(_h); + const unsigned int color = convertColor(_color); const unsigned int colorEnd = convertColor(_colorEnd); Vertex vertices[4]; - vertices[0] = { { (float)(_x ), (float)(_y ) }, { 0.0f, 0.0f }, color }; - vertices[1] = { { (float)(_x ), (float)(_y + _h) }, { 0.0f, 0.0f }, horizontalGradient ? colorEnd : color }; - vertices[2] = { { (float)(_x + _w), (float)(_y ) }, { 0.0f, 0.0f }, horizontalGradient ? color : colorEnd }; - vertices[3] = { { (float)(_x + _w), (float)(_y + _h) }, { 0.0f, 0.0f }, colorEnd }; + vertices[0] = { { (float)(x ), (float)(y ) }, { 0.0f, 0.0f }, color }; + vertices[1] = { { (float)(x ), (float)(y + h) }, { 0.0f, 0.0f }, horizontalGradient ? colorEnd : color }; + vertices[2] = { { (float)(x + w), (float)(y ) }, { 0.0f, 0.0f }, horizontalGradient ? color : colorEnd }; + vertices[3] = { { (float)(x + w), (float)(y + h) }, { 0.0f, 0.0f }, colorEnd }; bindTexture(0); drawTriangleStrips(vertices, 4, _srcBlendFactor, _dstBlendFactor); } // drawRect - void drawGradientRect(int _x, int _y, int _w, int _h, unsigned int _color, unsigned int _colorBottom, bool _horz, const Blend::Factor _srcBlendFactor, const Blend::Factor _dstBlendFactor) - { - const unsigned int color = convertColor(_color); - const unsigned int colorBottom = convertColor(_colorBottom); - - Vertex vertices[4]; - - vertices[0] = { { (float)(_x), (float)(_y) }, { 0.0f, 0.0f }, _horz ? colorBottom : color }; - vertices[1] = { { (float)(_x), (float)(_y + _h) }, { 0.0f, 0.0f }, colorBottom }; - vertices[2] = { { (float)(_x + _w), (float)(_y) }, { 0.0f, 0.0f }, color }; - vertices[3] = { { (float)(_x + _w), (float)(_y + _h) }, { 0.0f, 0.0f }, _horz ? color : colorBottom }; - - bindTexture(0); - drawTriangleStrips(vertices, 4, _srcBlendFactor, _dstBlendFactor); - /* - - glEnable(GL_BLEND); - glBlendFunc(blend_sfactor, blend_dfactor); - - glBegin(GL_QUADS); - - glColor4f(MAKEQUAD(horz ? colorBottom : color)); - glVertex2f(x, y); - - glColor4f(MAKEQUAD(color)); - glVertex2f(x + w, y); - - glColor4f(MAKEQUAD(horz ? color : colorBottom)); - glVertex2f(x + w, y + h); - - glColor4f(MAKEQUAD(colorBottom)); - glVertex2f(x, y + h); - - glEnd(); - - glDisable(GL_BLEND);*/ - } - SDL_Window* getSDLWindow() { return sdlWindow; } int getWindowWidth() { return windowWidth; } int getWindowHeight() { return windowHeight; } diff --git a/es-core/src/renderers/Renderer.h b/es-core/src/renderers/Renderer.h index da2f983f6e..ae60a2a8dd 100644 --- a/es-core/src/renderers/Renderer.h +++ b/es-core/src/renderers/Renderer.h @@ -66,8 +66,8 @@ namespace Renderer void deinit (); void pushClipRect (const Vector2i& _pos, const Vector2i& _size); void popClipRect (); - void drawRect (const float _x, const float _y, const float _w, const float _h, const unsigned int _color, const unsigned int _colorEnd, bool horizontalGradient = false, const Blend::Factor _srcBlendFactor = Blend::SRC_ALPHA, const Blend::Factor _dstBlendFactor = Blend::ONE_MINUS_SRC_ALPHA); - void drawRect (const int _x, const int _y, const int _w, const int _h, const unsigned int _color, const unsigned int _colorEnd, bool horizontalGradient = false, const Blend::Factor _srcBlendFactor = Blend::SRC_ALPHA, const Blend::Factor _dstBlendFactor = Blend::ONE_MINUS_SRC_ALPHA); + void drawRect (const float _x, const float _y, const float _w, const float _h, const unsigned int _color, const Blend::Factor _srcBlendFactor = Blend::SRC_ALPHA, const Blend::Factor _dstBlendFactor = Blend::ONE_MINUS_SRC_ALPHA); + void drawRect (const float _x, const float _y, const float _w, const float _h, const unsigned int _color, const unsigned int _colorEnd, bool horizontalGradient = false, const Blend::Factor _srcBlendFactor = Blend::SRC_ALPHA, const Blend::Factor _dstBlendFactor = Blend::ONE_MINUS_SRC_ALPHA); SDL_Window* getSDLWindow (); int getWindowWidth (); @@ -101,7 +101,7 @@ namespace Renderer bool isClippingEnabled(); bool isVisibleOnScreen(float x, float y, float w, float h); void activateWindow(); - void drawGradientRect(int _x, int _y, int _w, int _h, unsigned int _color, unsigned int _colorBottom, bool _horz = false, const Blend::Factor _srcBlendFactor = Blend::SRC_ALPHA, const Blend::Factor _dstBlendFactor = Blend::ONE_MINUS_SRC_ALPHA); +// void drawGradientRect(int _x, int _y, int _w, int _h, unsigned int _color, unsigned int _colorBottom, bool _horz = false, const Blend::Factor _srcBlendFactor = Blend::SRC_ALPHA, const Blend::Factor _dstBlendFactor = Blend::ONE_MINUS_SRC_ALPHA); } // Renderer:: From bd63607d32f58bb2d2d00407ab43a32096585bd0 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 31 Aug 2019 17:46:55 +0200 Subject: [PATCH 131/336] Minor fixes --- es-app/src/FileData.cpp | 2 ++ es-app/src/views/SystemView.cpp | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index b590343cb7..14ad47c594 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -27,6 +27,8 @@ FileData::FileData(FileType type, const std::string& path, SystemData* system) // metadata needs at least a name field (since that's what getName() will return) if (metadata.get("name").empty()) metadata.set("name", getDisplayName()); + + metadata.resetChangedFlag(); } const std::string FileData::getPath() const diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 22519d3d74..f28af90600 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -712,7 +712,7 @@ void SystemView::renderFade(const Transform4x4f& trans) { unsigned int fadeColor = 0x00000000 | (unsigned char)(mExtrasFadeOpacity * 255); Renderer::setMatrix(trans); - Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), fadeColor, fadeColor); + Renderer::drawRect(mPosition.x(), mPosition.y(), mSize.x(), mSize.y(), fadeColor); } } From da4edf2f9c18ded1ce209d686c0fb85aa5f56ad5 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 31 Aug 2019 19:16:52 +0200 Subject: [PATCH 132/336] ImageComponent : Fix "maths" on alpha channel defined in themes --- es-core/src/components/ImageComponent.cpp | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 9a1a789c39..99296cb006 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -269,7 +269,7 @@ void ImageComponent::setColorShift(unsigned int color) mColorShiftEnd = color; // Grab the opacity from the color shift because we may need to apply it if // fading textures in - mOpacity = color & 0xff; + //mOpacity = color & 0xff; } void ImageComponent::setColorShiftEnd(unsigned int color) @@ -285,7 +285,8 @@ void ImageComponent::setColorGradientHorizontal(bool horizontal) void ImageComponent::setOpacity(unsigned char opacity) { mOpacity = opacity; - mColorShift = (mColorShift >> 8 << 8) | mOpacity; + //mColorShift = (mColorShift >> 8 << 8) | mOpacity; + //mColorShiftEnd = (mColorShiftEnd >> 8 << 8) | mOpacity; } void ImageComponent::updateVertices() @@ -349,8 +350,10 @@ void ImageComponent::render(const Transform4x4f& parentTrans) // when it finally loads fadeIn(mTexture->bind()); - const unsigned int color = Renderer::convertColor(mColorShift); - const unsigned int colorEnd = Renderer::convertColor(mColorShiftEnd); + float opacity = (mOpacity * (mFading ? mFadeOpacity / 255.0 : 1.0)) / 255.0; + + const unsigned int color = Renderer::convertColor(mColorShift & 0xFFFFFF00 | (unsigned char) ((mColorShift & 0xFF) * opacity)); + const unsigned int colorEnd = Renderer::convertColor(mColorShiftEnd & 0xFFFFFF00 | (unsigned char)((mColorShiftEnd & 0xFF) * opacity)); mVertices[0].col = color; mVertices[1].col = mColorGradientHorizontal ? colorEnd : color; @@ -424,7 +427,8 @@ void ImageComponent::fadeIn(bool textureLoaded) mFadeOpacity = 0; mFading = true; // Set the colours to be translucent - mColorShift = (mColorShift >> 8 << 8) | 0; + // mColorShift = (mColorShift >> 8 << 8) | 0; + // mColorShiftEnd = (mColorShiftEnd >> 8 << 8) | 0; } } else if (mFading && textureLoaded) @@ -444,8 +448,9 @@ void ImageComponent::fadeIn(bool textureLoaded) mFadeOpacity = (unsigned char)opacity; } // Apply the combination of the target opacity and current fade - float newOpacity = (float)mOpacity * ((float)mFadeOpacity / 255.0f); - mColorShift = (mColorShift >> 8 << 8) | (unsigned char)newOpacity; + // float newOpacity = (float)mOpacity * ((float)mFadeOpacity / 255.0f); + // mColorShift = (mColorShift >> 8 << 8) | (unsigned char)newOpacity; + // mColorShiftEnd = (mColorShiftEnd >> 8 << 8) | (unsigned char)newOpacity; } } } @@ -505,10 +510,7 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s if(properties & COLOR) { if(elem->has("color")) - { setColorShift(elem->get("color")); - setColorShiftEnd(elem->get("color")); - } if (elem->has("colorEnd")) setColorShiftEnd(elem->get("colorEnd")); From 8e2904e79c06c691cc922a6cd3ad11c4b0270320 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 1 Sep 2019 11:13:57 +0200 Subject: [PATCH 133/336] Fix VideoGrid --- es-core/src/components/VideoComponent.cpp | 1 + es-core/src/components/VideoVlcComponent.cpp | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index d2af97cba3..0dfd87e33b 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -63,6 +63,7 @@ VideoComponent::VideoComponent(Window* window) : mDisable(false), mScreensaverMode(false), mTargetIsMax(false), + mTargetIsMin(false), mTargetSize(0, 0) { mFadeIn = 0.0f; diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index ecbfa93fa9..cd2fa93b95 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -244,18 +244,18 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) vertices[2] = { { mSize.x(), 0.0f }, { 1.0f, 0.0f }, color }; vertices[3] = { { mSize.x(), mSize.y() }, { 1.0f, 1.0f }, color }; - // round vertices - for(int i = 0; i < 4; ++i) - vertices[i].pos.round(); + // round vertices + for(int i = 0; i < 4; ++i) + vertices[i].pos.round(); - mTexture->bind(); + mTexture->bind(); if (mTargetIsMin) { Vector2f targetPos = (mTargetSize - mSize) * mOrigin * -1; - + Vector2i pos(trans.translation().x() + (int)targetPos.x(), trans.translation().y() + (int)targetPos.y()); - Vector2i size((int)Math::round(mTargetSize.x()), (int)Math::round(mTargetSize.y())); + Vector2i size((int)mTargetSize.round().x(), (int)mTargetSize.round().y()); Renderer::pushClipRect(pos, size); } From 708d7f8f995f6c8c94f1ad52714dd85da3839df2 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 1 Sep 2019 13:25:53 +0200 Subject: [PATCH 134/336] Fix menu padding for small or 4:3 resolutions --- es-core/src/components/MenuComponent.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 0a594778bc..b8819b19fd 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -2,8 +2,8 @@ #include "components/ButtonComponent.h" -#define BUTTON_GRID_VERT_PADDING 32 -#define BUTTON_GRID_HORIZ_PADDING 10 +#define BUTTON_GRID_VERT_PADDING (Renderer::getScreenHeight()*0.0296296) //32 +#define BUTTON_GRID_HORIZ_PADDING (Renderer::getScreenWidth()*0.0052083333) //10 #define TITLE_HEIGHT (mTitle->getFont()->getLetterHeight() + TITLE_VERT_PADDING) From d41b92f018ada74b25b1fff63b14221b65a77646 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 1 Sep 2019 21:37:42 +0200 Subject: [PATCH 135/336] SystemView : Add support for static background using in --- es-app/src/views/SystemView.cpp | 36 +++++++++++++++++++++++++++++---- es-app/src/views/SystemView.h | 1 + 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 8c5bee7477..2625546080 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -26,6 +26,7 @@ SystemView::SystemView(Window* window) : IList(wind mDisable = false; mShowing = false; mLastCursor = 0; + mStaticBackground = nullptr; setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); populate(); @@ -33,6 +34,12 @@ SystemView::SystemView(Window* window) : IList(wind SystemView::~SystemView() { + if (mStaticBackground != nullptr) + { + delete mStaticBackground; + mStaticBackground = nullptr; + } + clearEntries(); } @@ -440,8 +447,10 @@ void SystemView::render(const Transform4x4f& parentTrans) auto systemInfoZIndex = mSystemInfo.getZIndex(); auto minMax = std::minmax(mCarousel.zIndex, systemInfoZIndex); - if (!Settings::getInstance()->getBool("FixedCarousel")) - renderExtras(trans, INT16_MIN, minMax.first); + renderExtras(trans, INT16_MIN, minMax.first); + + if (mStaticBackground != nullptr) + mStaticBackground->render(trans); renderFade(trans); @@ -459,8 +468,7 @@ void SystemView::render(const Transform4x4f& parentTrans) renderInfoBar(trans); } - if (!Settings::getInstance()->getBool("FixedCarousel")) - renderExtras(trans, minMax.second, INT16_MAX); + renderExtras(trans, minMax.second, INT16_MAX); } std::vector SystemView::getHelpPrompts() @@ -512,6 +520,20 @@ void SystemView::getViewElements(const std::shared_ptr& theme) if (sysInfoElem) mSystemInfo.applyTheme(theme, "system", "systemInfo", ThemeFlags::ALL); + const ThemeData::ThemeElement* fixedBackgroundElem = theme->getElement("system", "fixedBackground", "image"); + if (fixedBackgroundElem) + { + if (mStaticBackground == nullptr) + mStaticBackground = new ImageComponent(mWindow, false); + + mStaticBackground->applyTheme(theme, "system", "staticBackground", ThemeFlags::ALL); + } + else if (mStaticBackground != nullptr) + { + delete mStaticBackground; + mStaticBackground = nullptr; + } + mViewNeedsReload = false; } @@ -750,6 +772,12 @@ void SystemView::getDefaultElements(void) mSystemInfo.setColor(0x000000FF); mSystemInfo.setZIndex(50); mSystemInfo.setDefaultZIndex(50); + + if (mStaticBackground != nullptr) + { + delete mStaticBackground; + mStaticBackground = nullptr; + } } void SystemView::onSizeChanged() diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index 011482b5e1..cbc165df3c 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -98,6 +98,7 @@ class SystemView : public IList SystemViewCarousel mCarousel; TextComponent mSystemInfo; SystemData* mLastSystem; + ImageComponent* mStaticBackground; // unit is list index float mCamOffset; From d1228bf3c78d360f63c59705e8a07b637fda0cda Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 2 Sep 2019 00:55:23 +0200 Subject: [PATCH 136/336] NinePatchComponent : Fix crash if updateColors is called without vertices created --- es-core/src/components/NinePatchComponent.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index 759f8bee68..003aacbbad 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -23,6 +23,9 @@ NinePatchComponent::~NinePatchComponent() void NinePatchComponent::updateColors() { + if (mVertices == nullptr) + return; + const unsigned int edgeColor = Renderer::convertColor(mEdgeColor); const unsigned int centerColor = Renderer::convertColor(mCenterColor); From bd642884f128808be0fed1e214a002ff37746e09 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 2 Sep 2019 20:51:53 +0200 Subject: [PATCH 137/336] GLES10 : can't build because of missing #include of Transform4x4f --- es-core/src/renderers/Renderer_GLES10.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/es-core/src/renderers/Renderer_GLES10.cpp b/es-core/src/renderers/Renderer_GLES10.cpp index 1346e6c117..ae9d670740 100644 --- a/es-core/src/renderers/Renderer_GLES10.cpp +++ b/es-core/src/renderers/Renderer_GLES10.cpp @@ -3,6 +3,7 @@ #include "renderers/Renderer.h" #include "Log.h" #include "Settings.h" +#include "math/Transform4x4f.h" #include #include From 79b2b08cb9b3aabd367634403d7ddf75c8daa602 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 3 Sep 2019 00:26:10 +0200 Subject: [PATCH 138/336] Linux build : ref to missing --- es-core/src/AudioManager.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/es-core/src/AudioManager.cpp b/es-core/src/AudioManager.cpp index 234f33f6e3..79710e82dd 100644 --- a/es-core/src/AudioManager.cpp +++ b/es-core/src/AudioManager.cpp @@ -8,6 +8,12 @@ #include "utils/FileSystemUtil.h" #include "utils/StringUtil.h" +#ifdef WIN32 +#include +#else +#include +#endif + std::vector> AudioManager::sSoundVector; std::shared_ptr AudioManager::sInstance; From ff78949c81a4b352c2e1aff246645955a32be027 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 3 Sep 2019 16:59:35 +0200 Subject: [PATCH 139/336] Misc fixes --- es-app/src/guis/GuiGamelistOptions.cpp | 29 +------ es-app/src/guis/GuiMenu.cpp | 55 ++++++------ es-app/src/views/ViewController.cpp | 21 +++-- es-core/src/GuiComponent.cpp | 73 +++++++++++++++- es-core/src/GuiComponent.h | 20 ++++- es-core/src/Settings.cpp | 31 +++++-- es-core/src/Settings.h | 2 + es-core/src/ThemeData.h | 2 + es-core/src/components/IList.h | 5 ++ es-core/src/components/MenuComponent.h | 3 +- es-core/src/components/NinePatchComponent.cpp | 6 +- es-core/src/resources/Font.cpp | 3 +- es-core/src/utils/StringUtil.cpp | 85 ++++++++++--------- 13 files changed, 212 insertions(+), 123 deletions(-) diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index f52eb0ac2b..76d74385c9 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -258,34 +258,7 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool // center the menu setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - - float x0 = (mSize.x() - mMenu.getSize().x()) / 2; - - float y1 = Renderer::getScreenHeight(); - float y2 = (mSize.y() - mMenu.getSize().y()) / 2; - - if (Settings::getInstance()->getString("PowerSaverMode") == "instant" || Settings::getInstance()->getString("TransitionStyle") == "instant") - setPosition(x0, y2); - else - { - setPosition(x0, y1); - - auto fadeFunc = [this, x0, y1, y2](float t) { - - t -= 1; // cubic ease out - float pct = Math::lerp(0, 1, t*t*t + 1); - - float y = y1 * (1 - pct) + y2 * pct; - setPosition(x0, y); - }; - - setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, x0, y2] - { - setPosition(x0, y2); - }); - - setPosition(x0, y2); - } + mMenu.animateTo(Vector2f((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2)); } GuiGamelistOptions::~GuiGamelistOptions() diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 30261ce0b4..af98b4381c 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -54,32 +54,15 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _("MAIN M addVersionInfo(); setSize(mMenu.getSize()); - - float y1 = Renderer::getScreenHeight(); - float y2 = (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2; - - if (Settings::getInstance()->getString("PowerSaverMode") == "instant" || Settings::getInstance()->getString("TransitionStyle") == "instant") - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); - else - { - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y1); - - auto fadeFunc = [this, y1, y2](float t) { - - t -= 1; // cubic ease out - float pct = Math::lerp(0, 1, t*t*t + 1); - - float y = y1 * (1 - pct) + y2 * pct; - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y); - }; - - setAnimation(new LambdaAnimation(fadeFunc, 350), 0, [this, fadeFunc, y2] - { - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); - }); - - setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, y2); - } + /* + setScale(Vector3f( + Renderer::getScreenWidth() / mMenu.getSize().x(), + Renderer::getScreenHeight() / mMenu.getSize().y(), + 1.0));*/ + + animateTo( + Vector2f((Renderer::getScreenWidth() - mSize.x()) / 2, Renderer::getScreenHeight() * 0.9), + Vector2f((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2)); } void GuiMenu::openScraperSettings() @@ -1064,8 +1047,10 @@ void GuiMenu::addVersionInfo() mVersion.setFont(theme->Footer.font); mVersion.setColor(theme->Footer.color); + mVersion.setLineSpacing(0); mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + " BUILD " + buildDate); - mVersion.setHorizontalAlignment(ALIGN_CENTER); + mVersion.setHorizontalAlignment(ALIGN_CENTER); + mVersion.setVerticalAlignment(ALIGN_CENTER); addChild(&mVersion); } @@ -1079,8 +1064,10 @@ void GuiMenu::openCollectionSystemSettings() { void GuiMenu::onSizeChanged() { - mVersion.setSize(mSize.x(), 0); - mVersion.setPosition(0, mSize.y() - mVersion.getSize().y()); + float h = mMenu.getButtonGridHeight(); + + mVersion.setSize(mSize.x(), h); + mVersion.setPosition(0, mSize.y() - h); // mVersion.getSize().y() } void GuiMenu::addEntry(std::string name, bool add_arrow, const std::function& func, const std::string iconName) @@ -1140,7 +1127,15 @@ bool GuiMenu::input(InputConfig* config, Input input) HelpStyle GuiMenu::getHelpStyle() { HelpStyle style = HelpStyle(); - style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); + + if (ThemeData::getDefaultTheme() != nullptr) + { + std::shared_ptr theme = std::shared_ptr(ThemeData::getDefaultTheme(), [](ThemeData*) {}); + style.applyTheme(theme, "system"); + } + else + style.applyTheme(ViewController::get()->getState().getSystem()->getTheme(), "system"); + return style; } diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 445468de23..6fd9af0bbc 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -639,6 +639,17 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) void ViewController::reloadAll() { + SystemData* system = nullptr; + + if (mState.viewing == SYSTEM_SELECT) + { + int idx = mSystemListView->getCursorIndex(); + if (idx >= 0 && idx < SystemData::sSystemVector.size()) + system = SystemData::sSystemVector[mSystemListView->getCursorIndex()]; + else + system = mState.getSystem(); + } + // clear all gamelistviews std::map cursorMap; @@ -671,15 +682,15 @@ void ViewController::reloadAll() if(mState.viewing == GAME_LIST) { mCurrentView = getGameListView(mState.getSystem()); - }else if(mState.viewing == SYSTEM_SELECT) - { - SystemData* system = mState.getSystem(); + } + else if(mState.viewing == SYSTEM_SELECT && system != nullptr) + { goToSystemView(SystemData::sSystemVector.front(), false); mSystemListView->goToSystem(system, false); mCurrentView = mSystemListView; - }else{ - goToSystemView(SystemData::sSystemVector.front(), false); } + else + goToSystemView(SystemData::sSystemVector.front(), false); updateHelpPrompts(); } diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index 971433c203..8e5d69b2b3 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -2,6 +2,7 @@ #include "animations/Animation.h" #include "animations/AnimationController.h" +#include "animations/LambdaAnimation.h" #include "Log.h" #include "renderers/Renderer.h" #include "ThemeData.h" @@ -132,12 +133,12 @@ void GuiComponent::setRotation(float rotation) mRotation = rotation; } -float GuiComponent::getScale() const +Vector3f GuiComponent::getScale() const { return mScale; } -void GuiComponent::setScale(float scale) +void GuiComponent::setScale(Vector3f scale) { mScale = scale; } @@ -480,7 +481,15 @@ void GuiComponent::updateHelpPrompts() HelpStyle GuiComponent::getHelpStyle() { - return HelpStyle(); + HelpStyle style = HelpStyle(); + + if (ThemeData::getDefaultTheme() != nullptr) + { + std::shared_ptr theme = std::shared_ptr(ThemeData::getDefaultTheme(), [](ThemeData*) {}); + style.applyTheme(theme, "system"); + } + + return style; } bool GuiComponent::isProcessing() const @@ -516,4 +525,62 @@ void GuiComponent::topWindow(bool isTop) { for(unsigned int i = 0; i < getChildCount(); i++) getChild(i)->topWindow(isTop); +} + +void GuiComponent::animateTo(Vector2f from, Vector2f to, unsigned int flags, int delay) +{ + if ((flags & AnimateFlags::POSITION) == 0) + from = to; + + Vector3f scale = mScale; + + float x1 = from.x(); + float x2 = to.x(); + float y1 = from.y(); + float y2 = to.y(); + + if (Settings::getInstance()->getString("PowerSaverMode") == "instant" || Settings::getInstance()->getString("TransitionStyle") == "instant") + setPosition(x2, y2); + else + { + setPosition(x1, y1); + + if ((flags & AnimateFlags::OPACITY) == AnimateFlags::OPACITY) + setOpacity(0); + + if ((flags & AnimateFlags::SCALE) == AnimateFlags::SCALE) + mScale = Vector3f(0, 0, 1); + + auto fadeFunc = [this, x1, x2, y1, y2, flags, scale](float t) { + + t -= 1; // cubic ease out + float pct = Math::lerp(0, 1, t*t*t + 1); + + if ((flags & AnimateFlags::OPACITY) == AnimateFlags::OPACITY) + setOpacity(pct*255.0); + + if ((flags & AnimateFlags::SCALE) == AnimateFlags::SCALE) + mScale = Vector3f(pct * scale.x(), pct * scale.y(), pct * scale.z()); + + float x = (x1 + mSize.x() / 2 - (mSize.x() / 2 * mScale.x())) * (1 - pct) + (x2 + mSize.x() / 2 - (mSize.x() / 2 * mScale.x())) * pct; + float y = (y1 + mSize.x() / 2 - (mSize.y() / 2 * mScale.y())) * (1 - pct) + (y2 + mSize.y() / 2 - (mSize.y() / 2 * mScale.y())) * pct; + + if (mScale.x() != 0.0f) + setPosition(x, y); + }; + + setAnimation(new LambdaAnimation(fadeFunc, delay), 0, [this, fadeFunc, x2, y2, flags, scale] + { + if ((flags & AnimateFlags::SCALE) == AnimateFlags::SCALE) + mScale = scale; + + if ((flags & AnimateFlags::OPACITY) == AnimateFlags::OPACITY) + setOpacity(255); + + float x = x2 + mSize.x() / 2 - (mSize.x() / 2 * mScale.x()); + float y = y2 + mSize.y() / 2 - (mSize.y() / 2 * mScale.y()); + + setPosition(x, y); + }); + } } \ No newline at end of file diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index b0d5b2f74e..070755c942 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -18,6 +18,17 @@ class InputConfig; class ThemeData; class Window; +namespace AnimateFlags +{ + enum Flags : unsigned int + { + POSITION = 1, + SCALE = 2, + OPACITY = 4, + ALL = 0xFFFFFFFF + }; +} + class GuiComponent { public: @@ -72,8 +83,8 @@ class GuiComponent void setRotation(float rotation); inline void setRotationDegrees(float rotation) { setRotation((float)ES_DEG_TO_RAD(rotation)); } - float getScale() const; - void setScale(float scale); + Vector3f getScale() const; + void setScale(Vector3f scale); float getZIndex() const; void setZIndex(float zIndex); @@ -147,6 +158,9 @@ class GuiComponent // Returns true if the component is busy doing background processing (e.g. HTTP downloads) bool isProcessing() const; + void animateTo(Vector2f from, Vector2f to, unsigned int flags = 0xFFFFFFFF, int delay = 350); + void animateTo(Vector2f from, unsigned int flags = AnimateFlags::OPACITY | AnimateFlags::SCALE, int delay = 350) { animateTo(from, from, flags, delay); } + protected: void renderChildren(const Transform4x4f& transform) const; void updateSelf(int deltaTime); // updates animations @@ -166,7 +180,7 @@ class GuiComponent Vector2f mSize; float mRotation = 0.0; - float mScale = 1.0; + Vector3f mScale = Vector3f(1.0, 1.0, 1.0); float mDefaultZIndex = 0; float mZIndex = 0; diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 5b3b63ce95..0bb4245e73 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -47,6 +47,7 @@ std::vector settings_dont_save { Settings::Settings() { + mHasConfigRoot = false; setDefaults(); loadFile(); } @@ -232,7 +233,7 @@ void Settings::setDefaults() } template -void saveMap(pugi::xml_document& doc, std::map& map, const char* type, std::map& defaultMap) +void saveMap(pugi::xml_node& doc, std::map& map, const char* type, std::map& defaultMap) { for(auto iter = map.cbegin(); iter != map.cend(); iter++) { @@ -256,10 +257,14 @@ void Settings::saveFile() const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_settings.cfg"; pugi::xml_document doc; + pugi::xml_node root = doc; - saveMap(doc, mBoolMap, "bool", mDefaultBoolMap); - saveMap(doc, mIntMap, "int", mDefaultIntMap); - saveMap(doc, mFloatMap, "float", mDefaultFloatMap); + if (mHasConfigRoot) + root = doc.append_child("config"); // batocera, root element + + saveMap(root, mBoolMap, "bool", mDefaultBoolMap); + saveMap(root, mIntMap, "int", mDefaultIntMap); + saveMap(root, mFloatMap, "float", mDefaultFloatMap); //saveMap(doc, mStringMap, "string"); for(auto iter = mStringMap.cbegin(); iter != mStringMap.cend(); iter++) @@ -301,13 +306,23 @@ void Settings::loadFile() return; } - for(pugi::xml_node node = doc.child("bool"); node; node = node.next_sibling("bool")) + pugi::xml_node root = doc; + + // Batocera has a root element, learn reading them + pugi::xml_node config = doc.child("config"); + if (config) + { + mHasConfigRoot = true; + root = config; + } + + for(pugi::xml_node node = root.child("bool"); node; node = node.next_sibling("bool")) setBool(node.attribute("name").as_string(), node.attribute("value").as_bool()); - for(pugi::xml_node node = doc.child("int"); node; node = node.next_sibling("int")) + for(pugi::xml_node node = root.child("int"); node; node = node.next_sibling("int")) setInt(node.attribute("name").as_string(), node.attribute("value").as_int()); - for(pugi::xml_node node = doc.child("float"); node; node = node.next_sibling("float")) + for(pugi::xml_node node = root.child("float"); node; node = node.next_sibling("float")) setFloat(node.attribute("name").as_string(), node.attribute("value").as_float()); - for(pugi::xml_node node = doc.child("string"); node; node = node.next_sibling("string")) + for(pugi::xml_node node = root.child("string"); node; node = node.next_sibling("string")) setString(node.attribute("name").as_string(), node.attribute("value").as_string()); } diff --git a/es-core/src/Settings.h b/es-core/src/Settings.h index 8050879d55..8eead71d4b 100644 --- a/es-core/src/Settings.h +++ b/es-core/src/Settings.h @@ -42,6 +42,8 @@ class Settings std::map mDefaultIntMap; std::map mDefaultFloatMap; std::map mDefaultStringMap; + + bool mHasConfigRoot; }; #endif // ES_CORE_SETTINGS_H diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 89d0bcb5e6..473ebce2a2 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -234,6 +234,8 @@ class ThemeData static std::map getThemeSubSets(const std::string& theme); static void setDefaultTheme(ThemeData* theme) { mCurrentTheme = theme; }; + static ThemeData* getDefaultTheme() { return mCurrentTheme; }; + std::string getSystemThemeFolder() { return mSystemThemeFolder; } diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index fa836402fe..408f88a044 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -165,6 +165,11 @@ class IList : public GuiComponent return false; } + int getCursorIndex() + { + return mCursor; + } + // entry management void add(const Entry& e) { diff --git a/es-core/src/components/MenuComponent.h b/es-core/src/components/MenuComponent.h index ce6bf212f2..1498c5c743 100644 --- a/es-core/src/components/MenuComponent.h +++ b/es-core/src/components/MenuComponent.h @@ -38,10 +38,11 @@ class MenuComponent : public GuiComponent virtual std::vector getHelpPrompts() override; + float getButtonGridHeight() const; + private: void updateSize(); void updateGrid(); - float getButtonGridHeight() const; NinePatchComponent mBackground; ComponentGrid mGrid; diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index 003aacbbad..f6ec14bf83 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -26,8 +26,10 @@ void NinePatchComponent::updateColors() if (mVertices == nullptr) return; - const unsigned int edgeColor = Renderer::convertColor(mEdgeColor); - const unsigned int centerColor = Renderer::convertColor(mCenterColor); + float opacity = mOpacity / 255.0; + + const unsigned int edgeColor = Renderer::convertColor(mEdgeColor & 0xFFFFFF00 | (unsigned char)((mEdgeColor & 0xFF) * opacity)); + const unsigned int centerColor = Renderer::convertColor(mCenterColor & 0xFFFFFF00 | (unsigned char)((mCenterColor & 0xFF) * opacity)); for (int i = 0; i < 6 * 9; ++i) mVertices[i].col = edgeColor; diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index ec558263a8..1d11663e95 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -327,7 +327,8 @@ Font::Glyph* Font::getGlyph(unsigned int id) { if (id < 255) { - // FCA : optimisation : array is always really fastest than a map + // FCA ptimisation : array is always really fastest than a map + // When computing long descriptions, it can come here size*2 times per frame Glyph* fastCache = mGlyphCacheArray[id]; if (fastCache != NULL) return fastCache; diff --git a/es-core/src/utils/StringUtil.cpp b/es-core/src/utils/StringUtil.cpp index 40cf71a9b1..069692ee5a 100644 --- a/es-core/src/utils/StringUtil.cpp +++ b/es-core/src/utils/StringUtil.cpp @@ -11,48 +11,6 @@ namespace Utils { namespace String { -#if defined(_WIN32) - const std::string convertFromWideString(const std::wstring wstring) - { - int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); - - std::string string; - string.resize(numBytes); - WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); - - return string; - } - - const std::wstring convertToWideString(const std::string string) - { - int numBytes = MultiByteToWideChar(CP_UTF8, 0, string.c_str(), (int)string.length(), nullptr, 0); - - std::wstring wstring; - wstring.resize(numBytes); - MultiByteToWideChar(CP_UTF8, 0, string.c_str(), (int)string.length(), (WCHAR*)wstring.c_str(), numBytes); - - return wstring; - } -#endif - std::vector split(const std::string& s, char seperator) - { - std::vector output; - - std::string::size_type prev_pos = 0, pos = 0; - while ((pos = s.find(seperator, pos)) != std::string::npos) - { - std::string substring(s.substr(prev_pos, pos - prev_pos)); - - output.push_back(substring); - - prev_pos = ++pos; - } - - output.push_back(s.substr(prev_pos, pos - prev_pos)); // Last word - - return output; - } - unsigned int chars2Unicode(const std::string& _string, size_t& _cursor) { const char& c = _string[_cursor]; @@ -351,6 +309,49 @@ namespace Utils } // scramble + std::vector split(const std::string& s, char seperator) + { + std::vector output; + + std::string::size_type prev_pos = 0, pos = 0; + while ((pos = s.find(seperator, pos)) != std::string::npos) + { + std::string substring(s.substr(prev_pos, pos - prev_pos)); + + output.push_back(substring); + + prev_pos = ++pos; + } + + output.push_back(s.substr(prev_pos, pos - prev_pos)); // Last word + + return output; + } + +#if defined(_WIN32) + const std::string convertFromWideString(const std::wstring wstring) + { + int numBytes = WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), nullptr, 0, nullptr, nullptr); + + std::string string; + string.resize(numBytes); + WideCharToMultiByte(CP_UTF8, 0, wstring.c_str(), (int)wstring.length(), (char*)string.c_str(), numBytes, nullptr, nullptr); + + return string; + } + + const std::wstring convertToWideString(const std::string string) + { + int numBytes = MultiByteToWideChar(CP_UTF8, 0, string.c_str(), (int)string.length(), nullptr, 0); + + std::wstring wstring; + wstring.resize(numBytes); + MultiByteToWideChar(CP_UTF8, 0, string.c_str(), (int)string.length(), (WCHAR*)wstring.c_str(), numBytes); + + return wstring; + } +#endif + } // String:: } // Utils:: From b719ca1b9947072fce76acf3023e834b4cfca9ae Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 4 Sep 2019 16:09:15 +0200 Subject: [PATCH 140/336] Theming : Support incomplete subsets & allow a mix of subsets & defaultview Support for small resolutions (GPICase) --- es-app/src/guis/GuiMenu.cpp | 202 +++++++++++++++------- es-core/src/Settings.cpp | 2 +- es-core/src/ThemeData.cpp | 4 + es-core/src/ThemeData.h | 2 + es-core/src/Window.cpp | 7 +- es-core/src/components/MenuComponent.cpp | 7 + es-core/src/components/VideoComponent.cpp | 8 +- es-core/src/renderers/Renderer.cpp | 1 + es-core/src/renderers/Renderer.h | 2 + es-core/src/resources/Font.cpp | 7 + es-core/src/resources/Font.h | 2 +- 11 files changed, 171 insertions(+), 73 deletions(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index af98b4381c..dbe306716a 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -58,8 +58,8 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _("MAIN M setScale(Vector3f( Renderer::getScreenWidth() / mMenu.getSize().x(), Renderer::getScreenHeight() / mMenu.getSize().y(), - 1.0));*/ - + 1.0)); + */ animateTo( Vector2f((Renderer::getScreenWidth() - mSize.x()) / 2, Renderer::getScreenHeight() * 0.9), Vector2f((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2)); @@ -302,7 +302,7 @@ void GuiMenu::openUISettings() theme_set->add(it->first, it->first, it == selectedSet); s->addWithLabel(_("THEME"), theme_set); - s->addSaveFunc([s, theme_set] + s->addSaveFunc([s, theme_set, window] { std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); if (oldTheme != theme_set->getSelected()) @@ -325,11 +325,15 @@ void GuiMenu::openUISettings() Settings::getInstance()->setString("ThemeSystemView", themeSystemviewSets.empty() ? "" : themeSystemviewSets.begin()->first); Settings::getInstance()->setString("ThemeGamelistView", themeGamelistViewSets.empty() ? "" : themeGamelistViewSets.begin()->first); + window->renderLoadingScreen(_("Loading...")); + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); CollectionSystemManager::get()->updateSystemsList(); ViewController::get()->goToStart(); ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + window->endRenderLoadingScreen(); + s->enableBeforeCloseFunc(true); } }); @@ -351,118 +355,182 @@ void GuiMenu::openUISettings() auto themeGamelistViewSets = ThemeData::sortThemeSubSets(themeSubSets, "gamelistview"); auto themeRegions = ThemeData::sortThemeSubSets(themeSubSets, "region"); + // colorset + std::shared_ptr> theme_colorset = nullptr; + if (themeColorSets.size() > 0) + { + auto selectedColorSet = themeColorSets.find(Settings::getInstance()->getString("ThemeColorSet")); + if (selectedColorSet == themeColorSets.end()) + selectedColorSet = themeColorSets.begin(); - auto selectedColorSet = themeColorSets.find(Settings::getInstance()->getString("ThemeColorSet")); - if (selectedColorSet == themeColorSets.end()) - selectedColorSet = themeColorSets.begin(); - auto theme_colorset = std::make_shared >(mWindow, _("THEME COLORSET"), false); + theme_colorset = std::make_shared >(mWindow, _("THEME COLORSET"), false); - for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) - theme_colorset->add(it->first, it->first, it == selectedColorSet); + for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) + theme_colorset->add(it->first, it->first, it == selectedColorSet); - if (!themeColorSets.empty()) - themeconfig->addWithLabel(_("THEME COLORSET"), theme_colorset); + if (!themeColorSets.empty()) + themeconfig->addWithLabel(_("THEME COLORSET"), theme_colorset); + } // iconset + std::shared_ptr> theme_iconset = nullptr; + if (themeIconSets.size() > 0) + { + auto selectedIconSet = themeIconSets.find(Settings::getInstance()->getString("ThemeIconSet")); + if (selectedIconSet == themeIconSets.end()) + selectedIconSet = themeIconSets.begin(); - auto selectedIconSet = themeIconSets.find(Settings::getInstance()->getString("ThemeIconSet")); - if (selectedIconSet == themeIconSets.end()) - selectedIconSet = themeIconSets.begin(); - auto theme_iconset = std::make_shared >(mWindow, _("THEME ICONSET"), false); + theme_iconset = std::make_shared >(mWindow, _("THEME ICONSET"), false); - for (auto it = themeIconSets.begin(); it != themeIconSets.end(); it++) - theme_iconset->add(it->first, it->first, it == selectedIconSet); + for (auto it = themeIconSets.begin(); it != themeIconSets.end(); it++) + theme_iconset->add(it->first, it->first, it == selectedIconSet); - if (!themeIconSets.empty()) - themeconfig->addWithLabel(_("THEME ICONSET"), theme_iconset); + if (!themeIconSets.empty()) + themeconfig->addWithLabel(_("THEME ICONSET"), theme_iconset); + } // menu + std::shared_ptr> theme_menu = nullptr; + if (themeMenus.size() > 0) + { + auto selectedMenu = themeMenus.find(Settings::getInstance()->getString("ThemeMenu")); + if (selectedMenu == themeMenus.end()) + selectedMenu = themeMenus.begin(); - auto selectedMenu = themeMenus.find(Settings::getInstance()->getString("ThemeMenu")); - if (selectedMenu == themeMenus.end()) - selectedMenu = themeMenus.begin(); - auto theme_menu = std::make_shared >(mWindow, _("THEME MENU"), false); + theme_menu = std::make_shared >(mWindow, _("THEME MENU"), false); - for (auto it = themeMenus.begin(); it != themeMenus.end(); it++) - theme_menu->add(it->first, it->first, it == selectedMenu); + for (auto it = themeMenus.begin(); it != themeMenus.end(); it++) + theme_menu->add(it->first, it->first, it == selectedMenu); - if (!themeMenus.empty()) - themeconfig->addWithLabel(_("THEME MENU"), theme_menu); + if (!themeMenus.empty()) + themeconfig->addWithLabel(_("THEME MENU"), theme_menu); + } // systemview - auto selectedSystemviewSet = themeSystemviewSets.find(Settings::getInstance()->getString("ThemeSystemView")); - if (selectedSystemviewSet == themeSystemviewSets.end()) - selectedSystemviewSet = themeSystemviewSets.begin(); + std::shared_ptr> theme_systemview = nullptr; + if (themeSystemviewSets.size() > 0) + { + auto selectedSystemviewSet = themeSystemviewSets.find(Settings::getInstance()->getString("ThemeSystemView")); + if (selectedSystemviewSet == themeSystemviewSets.end()) + selectedSystemviewSet = themeSystemviewSets.begin(); - auto theme_systemview = std::make_shared >(mWindow, _("THEME SYSTEMVIEW"), false); + theme_systemview = std::make_shared >(mWindow, _("THEME SYSTEMVIEW"), false); - for (auto it = themeSystemviewSets.begin(); it != themeSystemviewSets.end(); it++) - theme_systemview->add(it->first, it->first, it == selectedSystemviewSet); + for (auto it = themeSystemviewSets.begin(); it != themeSystemviewSets.end(); it++) + theme_systemview->add(it->first, it->first, it == selectedSystemviewSet); - if (!themeSystemviewSets.empty()) - themeconfig->addWithLabel(_("THEME SYSTEMVIEW"), theme_systemview); + if (!themeSystemviewSets.empty()) + themeconfig->addWithLabel(_("THEME SYSTEMVIEW"), theme_systemview); + } // gamelistview - auto selectedGamelistViewSet = themeGamelistViewSets.find(Settings::getInstance()->getString("ThemeGamelistView")); - if (selectedGamelistViewSet == themeGamelistViewSets.end()) - selectedGamelistViewSet = themeGamelistViewSets.begin(); + std::shared_ptr> theme_gamelistview = nullptr; + if (themeGamelistViewSets.size() > 0) + { + auto selectedGamelistViewSet = themeGamelistViewSets.find(Settings::getInstance()->getString("ThemeGamelistView")); + if (selectedGamelistViewSet == themeGamelistViewSets.end()) + selectedGamelistViewSet = themeGamelistViewSets.begin(); - auto theme_gamelistview = std::make_shared >(mWindow, _("THEME GAMELISTVIEW"), false); + theme_gamelistview = std::make_shared >(mWindow, _("THEME GAMELISTVIEW"), false); - for (auto it = themeGamelistViewSets.begin(); it != themeGamelistViewSets.end(); it++) - theme_gamelistview->add(it->first, it->first, it == selectedGamelistViewSet); + for (auto it = themeGamelistViewSets.begin(); it != themeGamelistViewSets.end(); it++) + theme_gamelistview->add(it->first, it->first, it == selectedGamelistViewSet); - if (!themeGamelistViewSets.empty()) - themeconfig->addWithLabel(_("THEME GAMELISTVIEW"), theme_gamelistview); + if (!themeGamelistViewSets.empty()) + themeconfig->addWithLabel(_("THEME GAMELISTVIEW"), theme_gamelistview); + } // themeregion - auto selectedRegion = themeRegions.find(Settings::getInstance()->getString("ThemeRegionName")); - if (selectedRegion == themeRegions.end()) - selectedRegion = themeRegions.begin(); + std::shared_ptr> theme_region = nullptr; + if (themeRegions.size() > 0) + { + auto selectedRegion = themeRegions.find(Settings::getInstance()->getString("ThemeRegionName")); + if (selectedRegion == themeRegions.end()) + selectedRegion = themeRegions.begin(); + + theme_region = std::make_shared >(mWindow, _("THEME GAMELISTVIEW"), false); - auto theme_region = std::make_shared >(mWindow, _("THEME GAMELISTVIEW"), false); + for (auto it = themeRegions.begin(); it != themeRegions.end(); it++) + theme_region->add(it->first, it->first, it == selectedRegion); - for (auto it = themeRegions.begin(); it != themeRegions.end(); it++) - theme_region->add(it->first, it->first, it == selectedRegion); + if (!themeRegions.empty()) + themeconfig->addWithLabel(_("THEME REGION"), theme_region); + } - if (!themeRegions.empty()) - themeconfig->addWithLabel(_("THEME REGION"), theme_region); + // gamelist_style + std::shared_ptr> gamelist_style = nullptr; + // if (theme_gamelistview == nullptr) + { + gamelist_style = std::make_shared< OptionListComponent >(mWindow, _("FORCE GAMELIST VIEW STYLE"), false); + + std::vector styles; + styles.push_back("automatic"); + + auto system = ViewController::get()->getState().getSystem(); + if (system != NULL) + { + auto mViews = system->getTheme()->getViewsOfTheme(); + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + styles.push_back(*it); + } + else + { + styles.push_back("basic"); + styles.push_back("detailed"); + } + + auto viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + if (!system->getTheme()->hasView(viewPreference)) + viewPreference = "automatic"; + + for (auto it = styles.cbegin(); it != styles.cend(); it++) + gamelist_style->add(_(*it), *it, viewPreference == *it); + + themeconfig->addWithLabel(_("FORCE GAMELIST VIEW STYLE"), gamelist_style); + } - themeconfig->addSaveFunc([this, s, theme_set, theme_colorset, theme_iconset, theme_menu, theme_systemview, theme_gamelistview, theme_region] { + themeconfig->addSaveFunc([this, s, theme_set, theme_colorset, theme_iconset, theme_menu, theme_systemview, theme_gamelistview, theme_region, gamelist_style] { bool needReload = false; - if (Settings::getInstance()->getString("ThemeColorSet") != theme_colorset->getSelected() && !theme_colorset->getSelected().empty()) + if (theme_colorset != nullptr && Settings::getInstance()->getString("ThemeColorSet") != theme_colorset->getSelected() && !theme_colorset->getSelected().empty()) + needReload = true; + if (theme_iconset != nullptr && Settings::getInstance()->getString("ThemeIconSet") != theme_iconset->getSelected() && !theme_iconset->getSelected().empty()) needReload = true; - if (Settings::getInstance()->getString("ThemeIconSet") != theme_iconset->getSelected() && !theme_iconset->getSelected().empty()) + if (theme_menu != nullptr && Settings::getInstance()->getString("ThemeMenu") != theme_menu->getSelected() && !theme_menu->getSelected().empty()) needReload = true; - if (Settings::getInstance()->getString("ThemeMenu") != theme_menu->getSelected() && !theme_menu->getSelected().empty()) + if (theme_systemview != nullptr && Settings::getInstance()->getString("ThemeSystemView") != theme_systemview->getSelected() && !theme_systemview->getSelected().empty()) needReload = true; - if (Settings::getInstance()->getString("ThemeSystemView") != theme_systemview->getSelected() && !theme_systemview->getSelected().empty()) + if (theme_gamelistview != nullptr && Settings::getInstance()->getString("ThemeGamelistView") != theme_gamelistview->getSelected() && !theme_gamelistview->getSelected().empty()) needReload = true; - if (Settings::getInstance()->getString("ThemeGamelistView") != theme_gamelistview->getSelected() && !theme_gamelistview->getSelected().empty()) + if (theme_region != nullptr && Settings::getInstance()->getString("ThemeRegionName") != theme_region->getSelected() && !theme_region->getSelected().empty()) needReload = true; - if (Settings::getInstance()->getString("ThemeRegionName") != theme_region->getSelected() && !theme_region->getSelected().empty()) + if (gamelist_style != nullptr && Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected() && !gamelist_style->getSelected().empty()) needReload = true; if (needReload) { - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - Settings::getInstance()->setString("ThemeColorSet", theme_colorset->getSelected()); - Settings::getInstance()->setString("ThemeIconSet", theme_iconset->getSelected()); - Settings::getInstance()->setString("ThemeMenu", theme_menu->getSelected()); - Settings::getInstance()->setString("ThemeSystemView", theme_systemview->getSelected()); - Settings::getInstance()->setString("ThemeGamelistView", theme_gamelistview->getSelected()); - Settings::getInstance()->setString("ThemeRegionName", theme_region->getSelected()); - //Settings::getInstance()->setBool("ThemeChanged", true); + Settings::getInstance()->setString("ThemeSet", theme_set == nullptr ? "" : theme_set->getSelected()); + Settings::getInstance()->setString("ThemeColorSet", theme_colorset == nullptr ? "" : theme_colorset->getSelected()); + Settings::getInstance()->setString("ThemeIconSet", theme_iconset == nullptr ? "" : theme_iconset->getSelected()); + Settings::getInstance()->setString("ThemeMenu", theme_menu == nullptr ? "" : theme_menu->getSelected()); + Settings::getInstance()->setString("ThemeSystemView", theme_systemview == nullptr ? "" : theme_systemview->getSelected()); + Settings::getInstance()->setString("ThemeGamelistView", theme_gamelistview == nullptr ? "" : theme_gamelistview->getSelected()); + Settings::getInstance()->setString("ThemeRegionName", theme_region == nullptr ? "" : theme_region->getSelected()); + Settings::getInstance()->setString("GamelistViewStyle", gamelist_style == nullptr ? "" : gamelist_style->getSelected()); + + mWindow->renderLoadingScreen(_("Loading...")); + //reload theme std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); CollectionSystemManager::get()->updateSystemsList(); ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + ViewController::get()->reloadAll(); + mWindow->endRenderLoadingScreen(); + s->enableBeforeCloseFunc(true); } }); diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 0bb4245e73..4358116541 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -280,7 +280,7 @@ void Settings::saveFile() if (def != mDefaultStringMap.cend() && def->second == iter->second) continue; - pugi::xml_node node = doc.append_child("string"); + pugi::xml_node node = root.append_child("string"); node.append_attribute("name").set_value(iter->first.c_str()); node.append_attribute("value").set_value(iter->second.c_str()); } diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 8e509bc652..4f3e21eec1 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -304,6 +304,7 @@ std::string ThemeData::resolvePlaceholders(const char* in) ThemeData::ThemeData() { mHasSubsets = false; + mHasGamelistSubsets = false; mColorset = Settings::getInstance()->getString("ThemeColorSet"); mIconset = Settings::getInstance()->getString("ThemeIconSet"); @@ -423,6 +424,7 @@ bool ThemeData::parseSubset(const pugi::xml_node& node) } if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) { + mHasGamelistSubsets = true; parse = true; return parse; } @@ -1073,6 +1075,8 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) Text.font = Font::get(FONT_SIZE_MEDIUM); TextSmall.font = Font::get(FONT_SIZE_SMALL); + + auto elem = theme->getElement("menu", "menubg", "menuBackground"); if (elem) { diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 473ebce2a2..f1b163bad3 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -228,6 +228,7 @@ class ThemeData std::vector getViewsOfTheme(); bool hasSubsets() { return mHasSubsets; } + bool hasGameListSubsets() { return mHasGamelistSubsets; } static const std::shared_ptr& getMenuTheme(); static std::map sortThemeSubSets(const std::map& subsetmap, const std::string& subset); @@ -281,6 +282,7 @@ class ThemeData std::map mVariables; bool mHasSubsets; + bool mHasGamelistSubsets; static std::shared_ptr mMenuTheme; static ThemeData* mCurrentTheme; diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 3503fd6d5c..ee33fd10c7 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -263,8 +263,11 @@ void Window::render() } } - if(!mRenderedHelpPrompts) - mHelp->render(transform); + + // GPI skip + if (!Renderer::isSmallScreen()) + if(!mRenderedHelpPrompts) + mHelp->render(transform); if(Settings::getInstance()->getBool("DrawFramerate") && mFrameDataText) { diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index b8819b19fd..7142380003 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -138,6 +138,13 @@ float MenuComponent::getButtonGridHeight() const void MenuComponent::updateSize() { + // GPI + if (Renderer::isSmallScreen()) + { + setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + return; + } + const float maxHeight = Renderer::getScreenHeight() * 0.75f; float height = TITLE_HEIGHT + mList->getTotalRowHeight() + getButtonGridHeight() + 2; if(height > maxHeight) diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 0dfd87e33b..64ba9b70f5 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -259,7 +259,12 @@ void VideoComponent::applyTheme(const std::shared_ptr& theme, const s setVisible(true); if (elem->has("path")) - mVideoPath = elem->get("path"); + { + if (Utils::FileSystem::exists(elem->get("path"))) + mVideoPath = elem->get("path"); + else + mVideoPath = mConfig.defaultVideoPath; + } } std::vector VideoComponent::getHelpPrompts() @@ -269,7 +274,6 @@ std::vector VideoComponent::getHelpPrompts() return ret; } - void VideoComponent::handleLooping() { } diff --git a/es-core/src/renderers/Renderer.cpp b/es-core/src/renderers/Renderer.cpp index 965a7c08d5..391a98accc 100644 --- a/es-core/src/renderers/Renderer.cpp +++ b/es-core/src/renderers/Renderer.cpp @@ -367,4 +367,5 @@ namespace Renderer int getScreenOffsetY() { return screenOffsetY; } int getScreenRotate() { return screenRotate; } + bool isSmallScreen() { return screenWidth < 400 && screenHeight < 400; }; } // Renderer:: diff --git a/es-core/src/renderers/Renderer.h b/es-core/src/renderers/Renderer.h index ae60a2a8dd..05c01387af 100644 --- a/es-core/src/renderers/Renderer.h +++ b/es-core/src/renderers/Renderer.h @@ -103,6 +103,8 @@ namespace Renderer void activateWindow(); // void drawGradientRect(int _x, int _y, int _w, int _h, unsigned int _color, unsigned int _colorBottom, bool _horz = false, const Blend::Factor _srcBlendFactor = Blend::SRC_ALPHA, const Blend::Factor _dstBlendFactor = Blend::ONE_MINUS_SRC_ALPHA); + // GPI Case + bool isSmallScreen(); } // Renderer:: #endif // ES_CORE_RENDERER_RENDERER_H diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index 1d11663e95..790945d8a7 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -74,8 +74,15 @@ size_t Font::getTotalMemUsage() Font::Font(int size, const std::string& path) : mSize(size), mPath(path) { + mSize = size; + + // GPI + if (Renderer::isSmallScreen()) + mSize = size * 1.5; + assert(mSize > 0); + mLoaded = true; mMaxGlyphHeight = 0; diff --git a/es-core/src/resources/Font.h b/es-core/src/resources/Font.h index 2e0595a71e..59253afb2e 100644 --- a/es-core/src/resources/Font.h +++ b/es-core/src/resources/Font.h @@ -130,7 +130,7 @@ class Font : public IReloadable int mMaxGlyphHeight; - const int mSize; + int mSize; const std::string mPath; float getNewlineStartOffset(const std::string& text, const unsigned int& charStart, const float& xLen, const Alignment& alignment); From 732aa37fa4a00dba955e68898e7354974240eb06 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 4 Sep 2019 17:37:03 +0200 Subject: [PATCH 141/336] smallscreen help fix --- es-core/src/Window.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index ee33fd10c7..bc1ce6a673 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -265,7 +265,7 @@ void Window::render() // GPI skip - if (!Renderer::isSmallScreen()) + if (mGuiStack.size() < 2 || !Renderer::isSmallScreen()) if(!mRenderedHelpPrompts) mHelp->render(transform); From dd26e39012c32d21baa4689b26abfd45982bd280 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 6 Sep 2019 12:04:46 +0200 Subject: [PATCH 142/336] Small fixes --- es-app/src/SystemData.cpp | 6 +++--- es-core/src/components/ImageComponent.cpp | 2 +- es-core/src/resources/TextureResource.h | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index ac21243a3f..ce85653801 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -313,10 +313,10 @@ SystemData* SystemData::loadSystem(pugi::xml_node system) } // 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) + if (platformId != PlatformIds::PLATFORM_UNKNOWN) platformIds.push_back(platformId); + else if (str != NULL && str[0] != '\0' && platformId == PlatformIds::PLATFORM_UNKNOWN) + LOG(LogWarning) << " Unknown platform for system \"" << name << "\" (platform \"" << str << "\" from list \"" << platformList << "\")"; } // theme folder diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 0fd44c1c3a..78c83608fe 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -26,7 +26,7 @@ ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : G mForceLoad(forceLoad), mDynamic(dynamic), mFadeOpacity(0), mFading(false), mRotateByTargetSize(false), mVisible(true), mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f), mMirror(0.0f, 0.0f), mAllowAsync(false) { - + mAllowFading = true; } ImageComponent::~ImageComponent() diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index 7ef8decec2..2d023f9d7b 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -22,7 +22,7 @@ class MaxSizeInfo MaxSizeInfo(float x, float y, bool externalZoom) : mSize(Vector2f(x, y)), mExternalZoom(externalZoom), mExternalZoomKnown(true){ } MaxSizeInfo(Vector2f size, bool externalZoom) : mSize(size), mExternalZoom(externalZoom), mExternalZoomKnown(true) { } - bool empty() { return mSize.x() >= 1 && mSize.y() <= 1; } + bool empty() { return mSize.x() <= 1 && mSize.y() <= 1; } float x() { return mSize.x(); } float y() { return mSize.y(); } From dac49a83e26d1de53a0279654d0a90ef1cd37447 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 6 Sep 2019 19:42:04 +0200 Subject: [PATCH 143/336] TextureResource : fix --- .../views/gamelist/DetailedGameListView.cpp | 2 +- es-core/src/components/ImageComponent.cpp | 40 ++++++++++++++----- es-core/src/components/ImageComponent.h | 3 ++ es-core/src/components/VideoComponent.cpp | 4 +- es-core/src/components/VideoVlcComponent.cpp | 2 +- es-core/src/resources/TextureResource.cpp | 8 +++- es-core/src/resources/TextureResource.h | 2 +- 7 files changed, 45 insertions(+), 16 deletions(-) diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 91e205266e..cabeec5003 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -101,7 +101,7 @@ void DetailedGameListView::createImage() const float padding = 0.01f; // Image - mImage = new ImageComponent(mWindow, true); + mImage = new ImageComponent(mWindow); 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); diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 78c83608fe..df40be7925 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -26,6 +26,7 @@ ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : G mForceLoad(forceLoad), mDynamic(dynamic), mFadeOpacity(0), mFading(false), mRotateByTargetSize(false), mVisible(true), mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f), mMirror(0.0f, 0.0f), mAllowAsync(false) { + mLoadingTexture = nullptr; mAllowFading = true; } @@ -140,8 +141,11 @@ void ImageComponent::setDefaultImage(std::string path) void ImageComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) { -// if (!maxSize.isExternalZoomKnown() && !maxSize.empty()) -// maxSize = MaxSizeInfo(maxSize.x(), maxSize.y(), !mTargetIsMax); + if (mPath == path) + return; + + mPath = path; + mLoadingTexture = nullptr; if (path.empty() || !ResourceManager::getInstance()->fileExists(path)) { @@ -149,10 +153,19 @@ void ImageComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) mTexture.reset(); else mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic, true, maxSize); - } - else - mTexture = TextureResource::get(path, tile, mForceLoad, mDynamic, true, maxSize); - + } + else + { + std::shared_ptr texture = TextureResource::get(path, tile, mForceLoad, mDynamic, true, maxSize); + + // TODO probleme de proportions si l'on fait ca, à investiguer... + if (!mForceLoad && mDynamic && !mAllowFading && texture != nullptr && !texture->isLoaded()) + mLoadingTexture = texture; + else + mTexture = texture; + } + + if (mLoadingTexture == nullptr); resize(); } @@ -329,6 +342,13 @@ void ImageComponent::render(const Transform4x4f& parentTrans) if (!mVisible) return; + if (mLoadingTexture != nullptr && mLoadingTexture->isLoaded()) + { + mTexture = mLoadingTexture; + mLoadingTexture = nullptr; + resize(); + } + Transform4x4f trans = parentTrans * getTransform(); Vector2f clipPos(trans.translation().x(), trans.translation().y()); @@ -346,8 +366,8 @@ void ImageComponent::render(const Transform4x4f& parentTrans) Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); } - if (mTexture->isInitialized()) - { +// if (mTexture->isInitialized()) +// { // actually draw the image // The bind() function returns false if the texture is not currently loaded. A blank // texture is bound in this case but we want to handle a fade so it doesn't just 'jump' in @@ -404,12 +424,12 @@ void ImageComponent::render(const Transform4x4f& parentTrans) } Renderer::bindTexture(0); - } + /* } else { LOG(LogError) << "Image texture is not initialized!"; mTexture.reset(); - } + }*/ } GuiComponent::renderChildren(trans); diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index c0ad920f92..4351b2c3db 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -135,6 +135,9 @@ class ImageComponent : public GuiComponent Vector2f mBottomRightCrop; Vector2f mMirror; + + std::string mPath; + std::shared_ptr mLoadingTexture; }; #endif // ES_CORE_COMPONENTS_IMAGE_COMPONENT_H diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 64ba9b70f5..8eaea978c6 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -53,7 +53,7 @@ void VideoComponent::setScreensaverMode(bool isScreensaver) VideoComponent::VideoComponent(Window* window) : GuiComponent(window), - mStaticImage(window, true), + mStaticImage(window), mVideoHeight(0), mVideoWidth(0), mStartDelayed(false), @@ -69,6 +69,8 @@ VideoComponent::VideoComponent(Window* window) : mFadeIn = 0.0f; mIsWaitingForVideoToStart = false; + mStaticImage.setAllowFading(false); + // Setup the default configuration mConfig.showSnapshotDelay = false; mConfig.showSnapshotNoVideo = false; diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index cd2fa93b95..1a130502e3 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -204,7 +204,7 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) { // If video is still attached to the path & texture is initialized, we suppose it had just been stopped (onhide, ondisable, screensaver...) // still render the last frame - if (mTexture != nullptr && !mVideoPath.empty() && mPlayingVideoPath == mVideoPath && mTexture->isInitialized()) + if (mTexture != nullptr && !mVideoPath.empty() && mPlayingVideoPath == mVideoPath && mTexture->isLoaded()) initFromPixels = false; else return; diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index e13a636e8e..bb8cab47b6 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -245,7 +245,7 @@ void TextureResource::rasterizeAt(float width, float height) else data = sTextureDataManager.get(this); - mSourceSize = Vector2f((float)width, (float)height); + // mSourceSize = Vector2f((float)width, (float)height); data->setSourceSize((float)width, (float)height); if (mForceLoad || (mTextureData != nullptr)) @@ -258,11 +258,15 @@ Vector2f TextureResource::getSourceImageSize() const return mSourceSize; } -bool TextureResource::isInitialized() const +bool TextureResource::isLoaded() const { if (mTextureData != nullptr) return mTextureData->isLoaded(); + auto data = sTextureDataManager.get(this, false); + if (data != nullptr) + return data->isLoaded(); + return true; } diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index 2d023f9d7b..188a868083 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -61,7 +61,7 @@ class TextureResource : public IReloadable virtual ~TextureResource(); - bool isInitialized() const; + bool isLoaded() const; bool isTiled() const; const Vector2i getSize() const; From 2abe5c95fa96b90249c4a17f547e339279e5db95 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 6 Sep 2019 23:02:42 +0200 Subject: [PATCH 144/336] Fix compilation (memcpy) under Ubuntu --- es-core/src/resources/TextureResource.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index bb8cab47b6..0f305e06c2 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -4,6 +4,7 @@ #include "resources/TextureData.h" #include "ImageIO.h" #include "Settings.h" +#include TextureDataManager TextureResource::sTextureDataManager; From a9e2f5a8712deff641b40f94b90370b0ef8aa985 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 10 Sep 2019 16:05:56 +0200 Subject: [PATCH 145/336] Subset corrections & support for tinyscreens + fixed grid clipping --- es-app/src/guis/GuiMenu.cpp | 24 ++-- es-core/src/ThemeData.cpp | 152 ++++++++------------ es-core/src/ThemeData.h | 16 +-- es-core/src/components/ImageGridComponent.h | 10 +- es-core/src/guis/GuiTextEditPopupKeyboard.h | 26 +--- es-core/src/utils/FileSystemUtil.cpp | 2 +- 6 files changed, 89 insertions(+), 141 deletions(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index dbe306716a..b3c4f91cb3 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -310,12 +310,12 @@ void GuiMenu::openUISettings() Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); auto themeSubSets = ThemeData::getThemeSubSets(theme_set->getSelected()); - auto themeColorSets = ThemeData::sortThemeSubSets(themeSubSets, "colorset"); - auto themeIconSets = ThemeData::sortThemeSubSets(themeSubSets, "iconset"); - auto themeMenus = ThemeData::sortThemeSubSets(themeSubSets, "menu"); - auto themeSystemviewSets = ThemeData::sortThemeSubSets(themeSubSets, "systemview"); - auto themeGamelistViewSets = ThemeData::sortThemeSubSets(themeSubSets, "gamelistview"); - auto themeRegions = ThemeData::sortThemeSubSets(themeSubSets, "region"); + auto themeColorSets = ThemeData::getSubSet(themeSubSets, "colorset"); + auto themeIconSets = ThemeData::getSubSet(themeSubSets, "iconset"); + auto themeMenus = ThemeData::getSubSet(themeSubSets, "menu"); + auto themeSystemviewSets = ThemeData::getSubSet(themeSubSets, "systemview"); + auto themeGamelistViewSets = ThemeData::getSubSet(themeSubSets, "gamelistview"); + auto themeRegions = ThemeData::getSubSet(themeSubSets, "region"); // theme changed without setting options, forcing options to avoid crash/blank theme Settings::getInstance()->setString("ThemeRegionName", themeRegions.empty() ? "" : themeRegions.begin()->first); @@ -348,12 +348,12 @@ void GuiMenu::openUISettings() auto SelectedTheme = theme_set->getSelected(); auto themeSubSets = ThemeData::getThemeSubSets(SelectedTheme); - auto themeColorSets = ThemeData::sortThemeSubSets(themeSubSets, "colorset"); - auto themeIconSets = ThemeData::sortThemeSubSets(themeSubSets, "iconset"); - auto themeMenus = ThemeData::sortThemeSubSets(themeSubSets, "menu"); - auto themeSystemviewSets = ThemeData::sortThemeSubSets(themeSubSets, "systemview"); - auto themeGamelistViewSets = ThemeData::sortThemeSubSets(themeSubSets, "gamelistview"); - auto themeRegions = ThemeData::sortThemeSubSets(themeSubSets, "region"); + auto themeColorSets = ThemeData::getSubSet(themeSubSets, "colorset"); + auto themeIconSets = ThemeData::getSubSet(themeSubSets, "iconset"); + auto themeMenus = ThemeData::getSubSet(themeSubSets, "menu"); + auto themeSystemviewSets = ThemeData::getSubSet(themeSubSets, "systemview"); + auto themeGamelistViewSets = ThemeData::getSubSet(themeSubSets, "gamelistview"); + auto themeRegions = ThemeData::getSubSet(themeSubSets, "region"); // colorset diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 4f3e21eec1..4c94969afb 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -304,7 +304,6 @@ std::string ThemeData::resolvePlaceholders(const char* in) ThemeData::ThemeData() { mHasSubsets = false; - mHasGamelistSubsets = false; mColorset = Settings::getInstance()->getString("ThemeColorSet"); mIconset = Settings::getInstance()->getString("ThemeIconSet"); @@ -390,95 +389,52 @@ std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolde return result; } -bool ThemeData::parseSubset(const pugi::xml_node& node) +bool ThemeData::isFirstSubset(const pugi::xml_node& node) { - bool parse = true; + const std::string subsetToFind = node.attribute("subset").as_string(); + const std::string name = node.attribute("name").as_string(); - if (node.attribute("subset")) - { - mHasSubsets = true; + pugi::xml_node root = node.parent(); - parse = false; + for (pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) + { const std::string subsetAttr = node.attribute("subset").as_string(); - const std::string nameAttr = node.attribute("name").as_string(); + if (subsetAttr.empty() || subsetAttr != subsetToFind) + continue; - if (subsetAttr == "colorset" && nameAttr == mColorset) - { - parse = true; - return parse; - } - if (subsetAttr == "iconset" && nameAttr == mIconset) - { - parse = true; - return parse; - } - if (subsetAttr == "menu" && nameAttr == mMenu) - { - parse = true; - return parse; - } - if (subsetAttr == "systemview" && nameAttr == mSystemview) - { - parse = true; - return parse; - } - if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) - { - mHasGamelistSubsets = true; - parse = true; - return parse; - } - /* - if (subsetAttr == "menu" || subsetAttr == "iconset" || subsetAttr == "gamelistview") - { - if (nameAttr.rfind("1-") != std::string::npos) - { - parse = true; - return parse; - } - } - else if (subsetAttr == "systemview") - { - if (nameAttr.rfind("8-") != std::string::npos) - { - parse = true; - return parse; - } - } - else if (nameAttr.rfind("2-") != std::string::npos) - { - parse = true; - return parse; - }*/ - /* - if (subsetAttr == "colorset" && nameAttr == mColorset) - { - parse = true; - return parse; - } - if (subsetAttr == "iconset" && nameAttr == mIconset) - { - parse = true; - return parse; - } - if (subsetAttr == "menu" && nameAttr == mMenu) - { - parse = true; - return parse; - } - if (subsetAttr == "systemview" && nameAttr == mSystemview) - { - parse = true; - return parse; - } - if (subsetAttr == "gamelistview" && nameAttr == mGamelistview) - { - parse = true; - return parse; - }*/ + const std::string nameAttr = node.attribute("name").as_string(); + return (nameAttr == name); } - return parse; + return false; +} + +bool ThemeData::parseSubset(const pugi::xml_node& node) +{ + if (!node.attribute("subset")) + return true; + + mHasSubsets = true; + + const std::string subsetAttr = node.attribute("subset").as_string(); + const std::string nameAttr = node.attribute("name").as_string(); + + if (subsetAttr == "colorset" && (nameAttr == mColorset || (mColorset.empty() && isFirstSubset(node)))) + return true; + + if (subsetAttr == "iconset" && (nameAttr == mIconset || (mIconset.empty() && isFirstSubset(node)))) + return true; + + if (subsetAttr == "menu" && (nameAttr == mMenu || (mMenu.empty() && isFirstSubset(node)))) + return true; + + if (subsetAttr == "systemview" && (nameAttr == mSystemview || (mSystemview.empty() && isFirstSubset(node)))) + return true; + + if (subsetAttr == "gamelistview" && (nameAttr == mGamelistview || (mGamelistview.empty() && isFirstSubset(node)))) + return true; + + return false; } void ThemeData::parseIncludes(const pugi::xml_node& root) @@ -576,6 +532,17 @@ void ThemeData::parseViews(const pugi::xml_node& root) if (!node.attribute("name")) continue; + if (node.attribute("tinyScreen")) + { + const std::string tinyScreenAttr = node.attribute("tinyScreen").as_string(); + + if (!Renderer::isSmallScreen() && tinyScreenAttr == "true") + continue; + + if (Renderer::isSmallScreen() && tinyScreenAttr == "false") + continue; + } + const char* delim = " \t\r\n,"; const std::string nameAttr = node.attribute("name").as_string(); size_t prevOff = nameAttr.find_first_not_of(delim, 0); @@ -1179,9 +1146,9 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) } } -std::map ThemeData::sortThemeSubSets(const std::map& subsetmap, const std::string& subset) +std::unordered_map ThemeData::getSubSet(const std::unordered_map& subsetmap, const std::string& subset) { - std::map sortedsets; + std::unordered_map sortedsets; for (const auto& it : subsetmap) { @@ -1192,18 +1159,21 @@ std::map ThemeData::sortThemeSubSets(const std::map& sets, std::deque& dequepath) +void ThemeData::crawlIncludes(const pugi::xml_node& root, std::unordered_map& sets, std::deque& dequepath) { for (pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) { - sets[node.attribute("name").as_string()] = node.attribute("subset").as_string(); + std::string name = node.attribute("name").as_string(); + std::string subset = node.attribute("subset").as_string(); + if (!subset.empty()) + sets.insert(std::pair(name, subset)); const char* relPath = node.text().get(); std::string path = Utils::FileSystem::resolveRelativePath(relPath, dequepath.back(), true); dequepath.push_back(path); pugi::xml_document includeDoc; - /*pugi::xml_parse_result result =*/ includeDoc.load_file(path.c_str()); + includeDoc.load_file(path.c_str()); pugi::xml_node root = includeDoc.child("theme"); crawlIncludes(root, sets, dequepath); @@ -1212,7 +1182,7 @@ void ThemeData::crawlIncludes(const pugi::xml_node& root, std::map& sets) +void ThemeData::findRegion(const pugi::xml_document& doc, std::unordered_map& sets) { pugi::xpath_node_set regionattr = doc.select_nodes("//@region"); for (auto xpath_node : regionattr) @@ -1222,9 +1192,9 @@ void ThemeData::findRegion(const pugi::xml_document& doc, std::map ThemeData::getThemeSubSets(const std::string& theme) +std::unordered_map ThemeData::getThemeSubSets(const std::string& theme) { - std::map sets; + std::unordered_map sets; std::deque dequepath; diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index f1b163bad3..6f36393c62 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -228,11 +229,10 @@ class ThemeData std::vector getViewsOfTheme(); bool hasSubsets() { return mHasSubsets; } - bool hasGameListSubsets() { return mHasGamelistSubsets; } static const std::shared_ptr& getMenuTheme(); - static std::map sortThemeSubSets(const std::map& subsetmap, const std::string& subset); - static std::map getThemeSubSets(const std::string& theme); + static std::unordered_map getSubSet(const std::unordered_map& subsetmap, const std::string& subset); + static std::unordered_map getThemeSubSets(const std::string& theme); static void setDefaultTheme(ThemeData* theme) { mCurrentTheme = theme; }; static ThemeData* getDefaultTheme() { return mCurrentTheme; }; @@ -242,8 +242,8 @@ class ThemeData } private: - static void crawlIncludes(const pugi::xml_node& root, std::map& sets, std::deque& dequepath); - static void findRegion(const pugi::xml_document& doc, std::map& sets); + static void crawlIncludes(const pugi::xml_node& root, std::unordered_map& sets, std::deque& dequepath); + static void findRegion(const pugi::xml_document& doc, std::unordered_map& sets); static std::map< std::string, std::map > sElementMap; static std::vector sSupportedFeatures; @@ -263,7 +263,8 @@ class ThemeData void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element, bool overwrite=true); bool parseRegion(const pugi::xml_node& node); bool parseSubset(const pugi::xml_node& node); - + bool isFirstSubset(const pugi::xml_node& node); + void parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& view, std::string baseClass); std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); @@ -281,8 +282,7 @@ class ThemeData std::map mVariables; - bool mHasSubsets; - bool mHasGamelistSubsets; + bool mHasSubsets; static std::shared_ptr mMenuTheme; static ThemeData* mCurrentTheme; diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index de3fd24509..5f53b8aba7 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -330,6 +330,8 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) bool splittedRendering = (mAnimateSelection && mAutoLayout.x() != 0); + Renderer::pushClipRect(pos, size); + // Render the selected image background on bottom of the others if needed std::shared_ptr selectedTile = NULL; for(auto it = mTiles.begin(); it != mTiles.end(); it++) @@ -345,9 +347,7 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) break; } } - - Renderer::pushClipRect(pos, size); - + for (auto it = mTiles.begin(); it != mTiles.end(); it++) { std::shared_ptr tile = (*it); @@ -355,8 +355,6 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) tile->render(tileTrans); } - Renderer::popClipRect(); - // Render the selected image content on top of the others if (selectedTile != NULL) { @@ -366,6 +364,8 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) selectedTile->render(tileTrans); } + Renderer::popClipRect(); + listRenderTitleOverlay(trans); GuiComponent::renderChildren(trans); diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.h b/es-core/src/guis/GuiTextEditPopupKeyboard.h index 62e9813e3d..cf3c13d55e 100644 --- a/es-core/src/guis/GuiTextEditPopupKeyboard.h +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.h @@ -35,15 +35,8 @@ class GuiTextEditPopupKeyboard : public GuiComponent NinePatchComponent mBackground; ComponentGrid mGrid; - // Vectors for button rows - + // Vectors for button rows std::vector< std::shared_ptr > buttons; - /* - std::vector< std::shared_ptr > kButtons; - std::vector< std::shared_ptr > hButtons; - std::vector< std::shared_ptr > bButtons; - std::vector< std::shared_ptr > digitButtons; - */ std::vector keyboardButtons; std::shared_ptr mShiftButton; @@ -51,23 +44,8 @@ class GuiTextEditPopupKeyboard : public GuiComponent std::shared_ptr mTitle; std::shared_ptr mText; std::shared_ptr mKeyboardGrid; - std::shared_ptr mButtonGrid; - std::shared_ptr mNewGrid; std::shared_ptr mButtons; - // Define keyboard key rows. - const char* numRow[10] = { "1","2","3","4","5","6","7","8","9","0" }; - const char* numRowUp[10] = { "!", "@", "#", "$", "%", "^", "&", "*", "(", ")" }; - const char* topRow[10] = { "q","w","e","r","t","y","u","i","o","p" }; - const char* topRowUp[10] = { "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P" }; // Just so I don't have to deal with toupper - const char* homeRow[10] = { "a","s","d","f","g","h","j","k","l",";" }; - const char* homeRowUp[10] = { "A", "S", "D", "F", "G", "H", "J", "K", "L", ":" }; - const char* bottomRow[9] = { "z","x","c","v","b","n","m",",","." }; // Shift is handled in the constructor - const char* bottomRowUp[9] = { "Z", "X", "C", "V", "B", "N", "M", "<", ">" }; - - int mxIndex = 0; // Stores the X index and makes every grid the same. - bool mMultiLine; - bool mShift = false; - bool mShiftChange = false; + bool mShift = false; }; diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index e0d81b7728..123383a700 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -587,7 +587,7 @@ namespace Utils std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); // check if path contains common - if(path.find(common) == 0) + if(path.find(common) == 0 && path != common) { _contains = true; return path.substr(common.length() + 1); From 28873954f81902c71e063a8dfeb9a5fe6d3a8605 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 11 Sep 2019 14:27:38 +0200 Subject: [PATCH 146/336] Subset : small fixes & parse includes/view/customView in the order they are declared --- es-app/src/guis/GuiMenu.cpp | 40 ++- es-core/src/Settings.cpp | 2 +- es-core/src/ThemeData.cpp | 395 ++++++++++++++++--------- es-core/src/ThemeData.h | 33 ++- es-core/src/Window.cpp | 4 + es-core/src/components/HelpComponent.h | 2 + 6 files changed, 298 insertions(+), 178 deletions(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index b3c4f91cb3..53eafa1651 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -318,12 +318,12 @@ void GuiMenu::openUISettings() auto themeRegions = ThemeData::getSubSet(themeSubSets, "region"); // theme changed without setting options, forcing options to avoid crash/blank theme - Settings::getInstance()->setString("ThemeRegionName", themeRegions.empty() ? "" : themeRegions.begin()->first); - Settings::getInstance()->setString("ThemeColorSet", themeColorSets.empty() ? "" : themeColorSets.begin()->first); - Settings::getInstance()->setString("ThemeIconSet", themeIconSets.empty() ? "" : themeIconSets.begin()->first); - Settings::getInstance()->setString("ThemeMenu", themeMenus.empty() ? "" : themeMenus.begin()->first); - Settings::getInstance()->setString("ThemeSystemView", themeSystemviewSets.empty() ? "" : themeSystemviewSets.begin()->first); - Settings::getInstance()->setString("ThemeGamelistView", themeGamelistViewSets.empty() ? "" : themeGamelistViewSets.begin()->first); + Settings::getInstance()->setString("ThemeRegionName", themeRegions.empty() ? "" : themeRegions[0]); + Settings::getInstance()->setString("ThemeColorSet", themeColorSets.empty() ? "" : themeColorSets[0]); + Settings::getInstance()->setString("ThemeIconSet", themeIconSets.empty() ? "" : themeIconSets[0]); + Settings::getInstance()->setString("ThemeMenu", themeMenus.empty() ? "" : themeMenus[0]); + Settings::getInstance()->setString("ThemeSystemView", themeSystemviewSets.empty() ? "" : themeSystemviewSets[0]); + Settings::getInstance()->setString("ThemeGamelistView", themeGamelistViewSets.empty() ? "" : themeGamelistViewSets[0]); window->renderLoadingScreen(_("Loading...")); @@ -360,14 +360,14 @@ void GuiMenu::openUISettings() std::shared_ptr> theme_colorset = nullptr; if (themeColorSets.size() > 0) { - auto selectedColorSet = themeColorSets.find(Settings::getInstance()->getString("ThemeColorSet")); + auto selectedColorSet = std::find(themeColorSets.cbegin(), themeColorSets.cend(), Settings::getInstance()->getString("ThemeColorSet")); if (selectedColorSet == themeColorSets.end()) selectedColorSet = themeColorSets.begin(); theme_colorset = std::make_shared >(mWindow, _("THEME COLORSET"), false); for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) - theme_colorset->add(it->first, it->first, it == selectedColorSet); + theme_colorset->add(*it, *it, it == selectedColorSet); if (!themeColorSets.empty()) themeconfig->addWithLabel(_("THEME COLORSET"), theme_colorset); @@ -377,14 +377,14 @@ void GuiMenu::openUISettings() std::shared_ptr> theme_iconset = nullptr; if (themeIconSets.size() > 0) { - auto selectedIconSet = themeIconSets.find(Settings::getInstance()->getString("ThemeIconSet")); + auto selectedIconSet = std::find(themeIconSets.cbegin(), themeIconSets.cend(), Settings::getInstance()->getString("ThemeIconSet")); if (selectedIconSet == themeIconSets.end()) selectedIconSet = themeIconSets.begin(); theme_iconset = std::make_shared >(mWindow, _("THEME ICONSET"), false); for (auto it = themeIconSets.begin(); it != themeIconSets.end(); it++) - theme_iconset->add(it->first, it->first, it == selectedIconSet); + theme_iconset->add(*it, *it, it == selectedIconSet); if (!themeIconSets.empty()) themeconfig->addWithLabel(_("THEME ICONSET"), theme_iconset); @@ -394,50 +394,48 @@ void GuiMenu::openUISettings() std::shared_ptr> theme_menu = nullptr; if (themeMenus.size() > 0) { - auto selectedMenu = themeMenus.find(Settings::getInstance()->getString("ThemeMenu")); + auto selectedMenu = std::find(themeMenus.cbegin(), themeMenus.cend(), Settings::getInstance()->getString("ThemeMenu")); if (selectedMenu == themeMenus.end()) selectedMenu = themeMenus.begin(); theme_menu = std::make_shared >(mWindow, _("THEME MENU"), false); for (auto it = themeMenus.begin(); it != themeMenus.end(); it++) - theme_menu->add(it->first, it->first, it == selectedMenu); + theme_menu->add(*it, *it, it == selectedMenu); if (!themeMenus.empty()) themeconfig->addWithLabel(_("THEME MENU"), theme_menu); } // systemview - std::shared_ptr> theme_systemview = nullptr; if (themeSystemviewSets.size() > 0) { - auto selectedSystemviewSet = themeSystemviewSets.find(Settings::getInstance()->getString("ThemeSystemView")); + auto selectedSystemviewSet = std::find(themeSystemviewSets.cbegin(), themeSystemviewSets.cend(), Settings::getInstance()->getString("ThemeSystemView")); if (selectedSystemviewSet == themeSystemviewSets.end()) selectedSystemviewSet = themeSystemviewSets.begin(); theme_systemview = std::make_shared >(mWindow, _("THEME SYSTEMVIEW"), false); for (auto it = themeSystemviewSets.begin(); it != themeSystemviewSets.end(); it++) - theme_systemview->add(it->first, it->first, it == selectedSystemviewSet); + theme_systemview->add(*it, *it, it == selectedSystemviewSet); if (!themeSystemviewSets.empty()) themeconfig->addWithLabel(_("THEME SYSTEMVIEW"), theme_systemview); } // gamelistview - std::shared_ptr> theme_gamelistview = nullptr; if (themeGamelistViewSets.size() > 0) { - auto selectedGamelistViewSet = themeGamelistViewSets.find(Settings::getInstance()->getString("ThemeGamelistView")); + auto selectedGamelistViewSet = std::find(themeGamelistViewSets.cbegin(), themeGamelistViewSets.cend(), Settings::getInstance()->getString("ThemeGamelistView")); if (selectedGamelistViewSet == themeGamelistViewSets.end()) selectedGamelistViewSet = themeGamelistViewSets.begin(); theme_gamelistview = std::make_shared >(mWindow, _("THEME GAMELISTVIEW"), false); for (auto it = themeGamelistViewSets.begin(); it != themeGamelistViewSets.end(); it++) - theme_gamelistview->add(it->first, it->first, it == selectedGamelistViewSet); + theme_gamelistview->add(*it, *it, it == selectedGamelistViewSet); if (!themeGamelistViewSets.empty()) themeconfig->addWithLabel(_("THEME GAMELISTVIEW"), theme_gamelistview); @@ -447,14 +445,14 @@ void GuiMenu::openUISettings() std::shared_ptr> theme_region = nullptr; if (themeRegions.size() > 0) { - auto selectedRegion = themeRegions.find(Settings::getInstance()->getString("ThemeRegionName")); + auto selectedRegion = std::find(themeRegions.cbegin(), themeRegions.cend(), Settings::getInstance()->getString("ThemeRegionName")); if (selectedRegion == themeRegions.end()) selectedRegion = themeRegions.begin(); - theme_region = std::make_shared >(mWindow, _("THEME GAMELISTVIEW"), false); + theme_region = std::make_shared >(mWindow, _("THEME REGION"), false); for (auto it = themeRegions.begin(); it != themeRegions.end(); it++) - theme_region->add(it->first, it->first, it == selectedRegion); + theme_region->add(*it, *it, it == selectedRegion); if (!themeRegions.empty()) themeconfig->addWithLabel(_("THEME REGION"), theme_region); diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 4358116541..73f3126a50 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -88,7 +88,7 @@ void Settings::setDefaults() mBoolMap["Windowed"] = false; mBoolMap["SplashScreen"] = true; mBoolMap["SplashScreenProgress"] = true; - mBoolMap["PreloadUI"] = true; + mBoolMap["PreloadUI"] = false; mBoolMap["StartupOnGameList"] = false; mBoolMap["HideSystemView"] = false; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 4c94969afb..200ade2ec2 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -355,10 +355,7 @@ void ThemeData::loadFile(std::string system, std::map mDefaultView = root.attribute("defaultView").as_string(); parseVariables(root); - parseIncludes(root); - parseViews(root); - parseCustomViews(root); - parseFeatures(root); + parseTheme(root); mMenuTheme = nullptr; mCurrentTheme = this; @@ -437,69 +434,6 @@ bool ThemeData::parseSubset(const pugi::xml_node& node) return false; } -void ThemeData::parseIncludes(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) - { - if (!parseSubset(node)) - continue; - - std::string relPath = resolvePlaceholders(node.text().as_string()); - std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); - path = resolveSystemVariable(mSystemThemeFolder, path); - - if (!ResourceManager::getInstance()->fileExists(path)) - { - //throw error << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; - LOG(LogWarning) << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; - continue; - } - - mPaths.push_back(path); - - pugi::xml_document includeDoc; - pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); - - if(!result) - throw error << "Error parsing file: \n " << result.description(); - - pugi::xml_node theme = includeDoc.child("theme"); - if(!theme) - throw error << "Missing tag!"; - - parseVariables(theme); - parseIncludes(theme); - parseViews(theme); - parseCustomViews(theme); - parseFeatures(theme); - - mPaths.pop_back(); - } -} - -void ThemeData::parseFeatures(const pugi::xml_node& root) -{ - ThemeException error; - error.setFiles(mPaths); - - for(pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) - { - if(!node.attribute("supported")) - throw error << "Feature missing \"supported\" attribute!"; - - const std::string supportedAttr = node.attribute("supported").as_string(); - - if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) - { - parseViews(node); - parseCustomViews(node); - } - } -} - void ThemeData::parseVariables(const pugi::xml_node& root) { ThemeException error; @@ -516,18 +450,20 @@ void ThemeData::parseVariables(const pugi::xml_node& root) std::string val = it->text().as_string(); if (!val.empty()) + { + mVariables.erase(key); mVariables.insert(std::pair(key, val)); + } } } - void ThemeData::parseViews(const pugi::xml_node& root) { ThemeException error; error.setFiles(mPaths); // parse views - for (pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) + for(pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) { if (!node.attribute("name")) continue; @@ -549,19 +485,19 @@ void ThemeData::parseViews(const pugi::xml_node& root) size_t off = nameAttr.find_first_of(delim, prevOff); std::string viewKey; - while (off != std::string::npos || prevOff != std::string::npos) + while(off != std::string::npos || prevOff != std::string::npos) { viewKey = nameAttr.substr(prevOff, off - prevOff); prevOff = nameAttr.find_first_not_of(delim, off); off = nameAttr.find_first_of(delim, prevOff); if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) - { + { ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; parseView(node, view); for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) - { + { if (it->second.isCustomView && it->second.baseType == viewKey) { ThemeView& customView = (ThemeView&)it->second; @@ -625,45 +561,96 @@ void ThemeData::parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& } } -void ThemeData::parseCustomViews(const pugi::xml_node& root) +std::vector ThemeData::getViewsOfTheme() { - ThemeException error; - error.setFiles(mPaths); - - // parse views - for (pugi::xml_node node = root.child("customView"); node; node = node.next_sibling("customView")) + std::vector ret; + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) { - if (!node.attribute("name")) + if (it->first == "menu" || it->first == "system") continue; - std::string viewKey = node.attribute("name").as_string(); - - ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; - view.isCustomView = true; - - std::string inherits = node.attribute("inherits").as_string(); - if (!inherits.empty()) + ret.push_back(it->first); + } + + return ret; +} + +void ThemeData::parseViewElement(const pugi::xml_node& node) +{ + if (!node.attribute("name")) + { + LOG(LogWarning) << "View missing \"name\" attribute!"; + return; + } + + if (node.attribute("tinyScreen")) + { + const std::string tinyScreenAttr = node.attribute("tinyScreen").as_string(); + + if (!Renderer::isSmallScreen() && tinyScreenAttr == "true") + return; + + if (Renderer::isSmallScreen() && tinyScreenAttr == "false") + return; + } + + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + std::string viewKey; + while (off != std::string::npos || prevOff != std::string::npos) + { + viewKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + + if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) { - view.baseType = inherits; - parseCustomViewBaseClass(root, view, inherits); + ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; + parseView(node, view); + + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + { + if (it->second.isCustomView && it->second.baseType == viewKey) + { + ThemeView& customView = (ThemeView&)it->second; + parseView(node, customView); + } + } } - - parseView(node, view); } } -std::vector ThemeData::getViewsOfTheme() +void ThemeData::parseCustomView(const pugi::xml_node& node, const pugi::xml_node& root) { - std::vector ret; - for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + if (!node.attribute("name")) + return; + + if (node.attribute("tinyScreen")) { - if (it->first == "menu" || it->first == "system") - continue; + const std::string tinyScreenAttr = node.attribute("tinyScreen").as_string(); - ret.push_back(it->first); + if (!Renderer::isSmallScreen() && tinyScreenAttr == "true") + return; + + if (Renderer::isSmallScreen() && tinyScreenAttr == "false") + return; } - return ret; + std::string viewKey = node.attribute("name").as_string(); + + ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; + view.isCustomView = true; + + std::string inherits = node.attribute("inherits").as_string(); + if (!inherits.empty()) + { + view.baseType = inherits; + parseCustomViewBaseClass(root, view, inherits); + } + + parseView(node, view); } void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view, bool overwriteElements) @@ -1146,27 +1133,85 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) } } -std::unordered_map ThemeData::getSubSet(const std::unordered_map& subsetmap, const std::string& subset) +std::vector ThemeData::getThemeSubSets(const std::string& theme) { - std::unordered_map sortedsets; + std::vector sets; - for (const auto& it : subsetmap) + std::deque dequepath; + + static const size_t pathCount = 2; + std::string paths[pathCount] = + { + "/etc/emulationstation/themes", + Utils::FileSystem::getHomePath() + "/.emulationstation/themes" + }; + + for (size_t i = 0; i < pathCount; i++) { - if (it.second == subset) - sortedsets[it.first] = it.first; + if (!Utils::FileSystem::isDirectory(paths[i])) + continue; + + auto dirs = Utils::FileSystem::getDirInfo(paths[i] + "/" + theme); + for (auto it = dirs.cbegin(); it != dirs.cend(); ++it) + { + if (!it->directory || it->hidden) + continue; + + std::string path = it->path + "/theme.xml"; + if (!Utils::FileSystem::exists(path)) + continue; + + dequepath.push_back(path); + pugi::xml_document doc; + doc.load_file(path.c_str()); + + pugi::xml_node root = doc.child("theme"); + crawlIncludes(root, sets, dequepath); + findRegion(doc, sets); + dequepath.pop_back(); + } + + std::string path = paths[i] + "/" + theme + "/theme.xml"; + if (!Utils::FileSystem::exists(path)) + continue; + + dequepath.push_back(path); + pugi::xml_document doc; + doc.load_file(path.c_str()); + + pugi::xml_node root = doc.child("theme"); + crawlIncludes(root, sets, dequepath); + findRegion(doc, sets); + dequepath.pop_back(); } - return sortedsets; + + return sets; } +std::vector ThemeData::getSubSet(const std::vector& subsets, const std::string& subset) +{ + std::vector ret; + + for (const auto& it : subsets) + { + if (it.subset == subset) + ret.push_back(it.name); + } -void ThemeData::crawlIncludes(const pugi::xml_node& root, std::unordered_map& sets, std::deque& dequepath) + return ret; +} + +void ThemeData::crawlIncludes(const pugi::xml_node& root, std::vector& sets, std::deque& dequepath) { for (pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) { std::string name = node.attribute("name").as_string(); std::string subset = node.attribute("subset").as_string(); if (!subset.empty()) - sets.insert(std::pair(name, subset)); + { + sets.push_back(Subset(subset, name)); + } + // sets.insert(std::pair(name, subset)); const char* relPath = node.text().get(); std::string path = Utils::FileSystem::resolveRelativePath(relPath, dequepath.back(), true); @@ -1182,67 +1227,121 @@ void ThemeData::crawlIncludes(const pugi::xml_node& root, std::unordered_map& sets) +void ThemeData::findRegion(const pugi::xml_document& doc, std::vector& sets) { pugi::xpath_node_set regionattr = doc.select_nodes("//@region"); for (auto xpath_node : regionattr) { - if (xpath_node.attribute() != nullptr) - sets[xpath_node.attribute().value()] = "region"; + if (xpath_node.attribute() == nullptr) + continue; + + std::string elemKey = xpath_node.attribute().value(); + if (elemKey.empty()) + continue; + + for (auto sb : sets) + if (sb.subset == "region" && sb.name == elemKey) + return; + + sets.push_back(Subset("region", elemKey)); } } -std::unordered_map ThemeData::getThemeSubSets(const std::string& theme) +void ThemeData::parseTheme(const pugi::xml_node& root) { - std::unordered_map sets; + if (root.attribute("defaultView")) + mDefaultView = root.attribute("defaultView").as_string(); - std::deque dequepath; + for (pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + { + std::string name = node.name(); + + if (name == "include") + parseInclude(node); + else if (name == "view") + parseViewElement(node); + else if (name == "customView") + parseCustomView(node, root); + } - static const size_t pathCount = 2; - std::string paths[pathCount] = + // Unfortunately, recalbox does not do things in order, features have to be loaded after + for (pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) + parseFeature(node); +} + +void ThemeData::parseInclude(const pugi::xml_node& node) +{ + if (!parseSubset(node)) + return; + + if (node.attribute("tinyScreen")) { - "/etc/emulationstation/themes", - Utils::FileSystem::getHomePath() + "/.emulationstation/themes" - }; + const std::string tinyScreenAttr = node.attribute("tinyScreen").as_string(); - for (size_t i = 0; i < pathCount; i++) + if (!Renderer::isSmallScreen() && tinyScreenAttr == "true") + return; + + if (Renderer::isSmallScreen() && tinyScreenAttr == "false") + return; + } + + std::string relPath = resolvePlaceholders(node.text().as_string()); + std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); + path = resolveSystemVariable(mSystemThemeFolder, path); + + if (!ResourceManager::getInstance()->fileExists(path)) { - if (!Utils::FileSystem::isDirectory(paths[i])) - continue; + LOG(LogWarning) << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + return; + } - auto dirs = Utils::FileSystem::getDirInfo(paths[i] + "/" + theme); - for (auto it = dirs.cbegin(); it != dirs.cend(); ++it) - { - if (!it->directory || it->hidden) - continue; - - std::string path = it->path + "/theme.xml"; - if (!Utils::FileSystem::exists(path)) - continue; + mPaths.push_back(path); - dequepath.push_back(path); - pugi::xml_document doc; - doc.load_file(path.c_str()); + pugi::xml_document includeDoc; + pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); + if (!result) + { + LOG(LogWarning) << "Error parsing file: \n " << result.description() << " from included file \"" << relPath << "\":\n "; + return; + } - pugi::xml_node root = doc.child("theme"); - crawlIncludes(root, sets, dequepath); - findRegion(doc, sets); - dequepath.pop_back(); - } + pugi::xml_node theme = includeDoc.child("theme"); + if (!theme) + { + LOG(LogWarning) << "Missing tag!" << " from included file \"" << relPath << "\":\n "; + return; + } - std::string path = paths[i] + "/" + theme + "/theme.xml"; - if (!Utils::FileSystem::exists(path)) - continue; + parseVariables(theme); + parseTheme(theme); - dequepath.push_back(path); - pugi::xml_document doc; - doc.load_file(path.c_str()); + mPaths.pop_back(); +} - pugi::xml_node root = doc.child("theme"); - crawlIncludes(root, sets, dequepath); - findRegion(doc, sets); - dequepath.pop_back(); +void ThemeData::parseFeature(const pugi::xml_node& node) +{ + if (!node.attribute("supported")) + { + LOG(LogWarning) << "Feature missing \"supported\" attribute!"; + return; } - - return sets; + + const std::string supportedAttr = node.attribute("supported").as_string(); + + if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) + parseViews(node); +} + +void ThemeData::parseVariable(const pugi::xml_node& node) +{ + std::string key = node.name(); + if (key.empty()) + return; + + std::string val = node.text().as_string(); + if (val.empty()) + return; + + mVariables.erase(key); + mVariables.insert(std::pair(key, val)); } diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 6f36393c62..1e319fe874 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -87,6 +87,18 @@ struct ThemeSet inline std::string getThemePath(const std::string& system) const { return path + "/" + system + "/theme.xml"; } }; +struct Subset +{ + Subset(const std::string set, const std::string nm) + { + subset = set; + name = nm; + } + + std::string subset; + std::string name; +}; + struct MenuElement { unsigned int color; unsigned int selectedColor; @@ -231,8 +243,9 @@ class ThemeData bool hasSubsets() { return mHasSubsets; } static const std::shared_ptr& getMenuTheme(); - static std::unordered_map getSubSet(const std::unordered_map& subsetmap, const std::string& subset); - static std::unordered_map getThemeSubSets(const std::string& theme); + + static std::vector getThemeSubSets(const std::string& theme); + static std::vector getSubSet(const std::vector& subsets, const std::string& subset); static void setDefaultTheme(ThemeData* theme) { mCurrentTheme = theme; }; static ThemeData* getDefaultTheme() { return mCurrentTheme; }; @@ -242,8 +255,8 @@ class ThemeData } private: - static void crawlIncludes(const pugi::xml_node& root, std::unordered_map& sets, std::deque& dequepath); - static void findRegion(const pugi::xml_document& doc, std::unordered_map& sets); + static void findRegion(const pugi::xml_document& doc, std::vector& sets); + static void crawlIncludes(const pugi::xml_node& root, std::vector& sets, std::deque& dequepath); static std::map< std::string, std::map > sElementMap; static std::vector sSupportedFeatures; @@ -254,13 +267,17 @@ class ThemeData std::string mDefaultView; - void parseFeatures(const pugi::xml_node& themeRoot); - void parseIncludes(const pugi::xml_node& themeRoot); + void parseTheme(const pugi::xml_node& root); + + void parseFeature(const pugi::xml_node& node); + void parseInclude(const pugi::xml_node& node); + void parseVariable(const pugi::xml_node& node); void parseVariables(const pugi::xml_node& root); void parseViews(const pugi::xml_node& themeRoot); - void parseCustomViews(const pugi::xml_node& root); + void parseCustomView(const pugi::xml_node& node, const pugi::xml_node& root); + void parseViewElement(const pugi::xml_node& node); void parseView(const pugi::xml_node& viewNode, ThemeView& view, bool overwriteElements = true); - void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element, bool overwrite=true); + void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element, bool overwrite = true); bool parseRegion(const pugi::xml_node& node); bool parseSubset(const pugi::xml_node& node); bool isFirstSubset(const pugi::xml_node& node); diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index bc1ce6a673..90d147ed64 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -445,6 +445,10 @@ void Window::renderHelpPromptsEarly() void Window::setHelpPrompts(const std::vector& prompts, const HelpStyle& style) { + // Keep a temporary reference to the previous grid. + // It avoids unloading/reloading images if they are the same, and avoids flickerings + auto oldGrid = mHelp->getGrid(); + mHelp->clearPrompts(); mHelp->setStyle(style); diff --git a/es-core/src/components/HelpComponent.h b/es-core/src/components/HelpComponent.h index e9c5d2e07d..bdebf42612 100644 --- a/es-core/src/components/HelpComponent.h +++ b/es-core/src/components/HelpComponent.h @@ -22,6 +22,8 @@ class HelpComponent : public GuiComponent void setStyle(const HelpStyle& style); + std::shared_ptr getGrid() { return mGrid; }; + private: std::shared_ptr getIconTexture(const char* name); std::map< std::string, std::shared_ptr > mIconCache; From 99499bb5650b842e786ebdbdc4f05032de0d1838 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 11 Sep 2019 14:35:47 +0200 Subject: [PATCH 147/336] Fix crash when theme folder don't contains theme.xml file --- es-app/src/SystemData.cpp | 1 + es-app/src/views/ViewController.cpp | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index ce85653801..afd8e129c2 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -363,6 +363,7 @@ SystemData* SystemData::loadSystem(pugi::xml_node system) bool SystemData::loadConfig(Window* window) { deleteSystems(); + ThemeData::setDefaultTheme(nullptr); std::string path = getConfigPath(false); diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 6fd9af0bbc..996883eeac 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -601,6 +601,9 @@ void ViewController::preload() void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) { + if (reloadTheme) + ThemeData::setDefaultTheme(nullptr); + for(auto it = mGameListViews.cbegin(); it != mGameListViews.cend(); it++) { if(it->second.get() == view) @@ -632,6 +635,10 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) break; } } + + if (SystemData::sSystemVector.size() > 0 && reloadTheme) + ThemeData::setDefaultTheme(SystemData::sSystemVector.at(0)->getTheme().get()); + // Redisplay the current view if (mCurrentView) mCurrentView->onShow(); @@ -639,6 +646,8 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) void ViewController::reloadAll() { + ThemeData::setDefaultTheme(nullptr); + SystemData* system = nullptr; if (mState.viewing == SYSTEM_SELECT) @@ -674,6 +683,9 @@ void ViewController::reloadAll() getGameListView(it->first)->setCursor(it->second); } + if (SystemData::sSystemVector.size() > 0) + ThemeData::setDefaultTheme(SystemData::sSystemVector.at(0)->getTheme().get()); + // Rebuild SystemListView mSystemListView.reset(); getSystemListView(); From 3f3c3dcc45f84697f8fd81cbb676b5b61224e399 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 11 Sep 2019 14:58:18 +0200 Subject: [PATCH 148/336] ThemeData/ImageComponent : minSize attribute was not supported --- es-core/src/ThemeData.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 200ade2ec2..86df2c5102 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -24,6 +24,7 @@ std::map> The { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, { "maxSize", NORMALIZED_PAIR }, + { "minSize", NORMALIZED_PAIR }, { "origin", NORMALIZED_PAIR }, { "rotation", FLOAT }, { "rotationOrigin", NORMALIZED_PAIR }, From bdb1b1fd5af9fea936ac88e8a73198230ea6686a Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 28 Sep 2019 17:27:31 +0200 Subject: [PATCH 149/336] Enhancements (vlc video player/texture manager) --- es-core/src/ThemeData.cpp | 70 ++- es-core/src/components/VideoVlcComponent.cpp | 451 +++++++++++-------- es-core/src/components/VideoVlcComponent.h | 44 +- es-core/src/math/Vector2f.h | 2 + es-core/src/renderers/Renderer_GL21.cpp | 10 +- es-core/src/renderers/Renderer_GLES10.cpp | 10 +- es-core/src/resources/TextureData.cpp | 47 +- es-core/src/resources/TextureData.h | 3 + es-core/src/resources/TextureDataManager.cpp | 218 ++++----- es-core/src/resources/TextureDataManager.h | 5 +- es-core/src/resources/TextureResource.cpp | 9 + es-core/src/resources/TextureResource.h | 1 + 12 files changed, 542 insertions(+), 328 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 86df2c5102..318bfcea7a 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -181,6 +181,7 @@ std::map> The { "default", PATH }, { "path", PATH }, { "delay", FLOAT }, + { "effect", STRING }, { "visible", BOOLEAN }, { "zIndex", FLOAT }, { "showSnapshotNoVideo", BOOLEAN }, @@ -469,17 +470,6 @@ void ThemeData::parseViews(const pugi::xml_node& root) if (!node.attribute("name")) continue; - if (node.attribute("tinyScreen")) - { - const std::string tinyScreenAttr = node.attribute("tinyScreen").as_string(); - - if (!Renderer::isSmallScreen() && tinyScreenAttr == "true") - continue; - - if (Renderer::isSmallScreen() && tinyScreenAttr == "false") - continue; - } - const char* delim = " \t\r\n,"; const std::string nameAttr = node.attribute("name").as_string(); size_t prevOff = nameAttr.find_first_not_of(delim, 0); @@ -584,17 +574,6 @@ void ThemeData::parseViewElement(const pugi::xml_node& node) return; } - if (node.attribute("tinyScreen")) - { - const std::string tinyScreenAttr = node.attribute("tinyScreen").as_string(); - - if (!Renderer::isSmallScreen() && tinyScreenAttr == "true") - return; - - if (Renderer::isSmallScreen() && tinyScreenAttr == "false") - return; - } - const char* delim = " \t\r\n,"; const std::string nameAttr = node.attribute("name").as_string(); size_t prevOff = nameAttr.find_first_not_of(delim, 0); @@ -639,6 +618,18 @@ void ThemeData::parseCustomView(const pugi::xml_node& node, const pugi::xml_node return; } + if (node.attribute("ifHelpPrompts")) + { + const std::string helpVisibleAttr = node.attribute("ifHelpPrompts").as_string(); + bool help = Settings::getInstance()->getBool("ShowHelpPrompts"); + + if (!help && helpVisibleAttr == "true") + return; + + if (help && helpVisibleAttr == "false") + return; + } + std::string viewKey = node.attribute("name").as_string(); ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; @@ -659,6 +650,29 @@ void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view, bool over ThemeException error; error.setFiles(mPaths); + if (root.attribute("tinyScreen")) + { + const std::string tinyScreenAttr = root.attribute("tinyScreen").as_string(); + + if (!Renderer::isSmallScreen() && tinyScreenAttr == "true") + return; + + if (Renderer::isSmallScreen() && tinyScreenAttr == "false") + return; + } + + if (root.attribute("ifHelpPrompts")) + { + const std::string helpVisibleAttr = root.attribute("ifHelpPrompts").as_string(); + bool help = Settings::getInstance()->getBool("ShowHelpPrompts"); + + if (!help && helpVisibleAttr == "true") + return; + + if (help && helpVisibleAttr == "false") + return; + } + for (pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) { if (!node.attribute("name")) @@ -1286,6 +1300,18 @@ void ThemeData::parseInclude(const pugi::xml_node& node) return; } + if (node.attribute("ifHelpPrompts")) + { + const std::string helpVisibleAttr = node.attribute("ifHelpPrompts").as_string(); + bool help = Settings::getInstance()->getBool("ShowHelpPrompts"); + + if (!help && helpVisibleAttr == "true") + return; + + if (help && helpVisibleAttr == "false") + return; + } + std::string relPath = resolvePlaceholders(node.text().as_string()); std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); path = resolveSystemVariable(mSystemThemeFolder, path); diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 1a130502e3..49b161c903 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -1,13 +1,14 @@ #include "components/VideoVlcComponent.h" -#include "renderers/Renderer.h" +#include "renderers/Renderer.h" #include "resources/TextureResource.h" #include "utils/StringUtil.h" -#include "utils/FileSystemUtil.h" #include "PowerSaver.h" #include "Settings.h" #include #include +#include +#include "ThemeData.h" #ifdef WIN32 #include @@ -15,26 +16,37 @@ #include "ImageIO.h" +#define MATHPI 3.141592653589793238462643383279502884L + libvlc_instance_t* VideoVlcComponent::mVLC = NULL; // VLC prepares to render a video frame. -static void *lock(void *data, void **p_pixels) { +static void *lock(void *data, void **p_pixels) +{ struct VideoContext *c = (struct VideoContext *)data; - SDL_LockMutex(c->mutex); - SDL_LockSurface(c->surface); - *p_pixels = c->surface->pixels; + + int frame = (c->surfaceId ^ 1); + + c->mutexes[frame].lock(); + c->hasFrame[frame] = false; + *p_pixels = c->surfaces[frame]; return NULL; // Picture identifier, not needed here. } // VLC just rendered a video frame. -static void unlock(void *data, void* /*id*/, void *const* /*p_pixels*/) { +static void unlock(void *data, void* /*id*/, void *const* /*p_pixels*/) +{ struct VideoContext *c = (struct VideoContext *)data; - SDL_UnlockSurface(c->surface); - SDL_UnlockMutex(c->mutex); + + int frame = (c->surfaceId ^ 1); + + c->surfaceId = frame; + c->hasFrame[frame] = true; + c->mutexes[frame].unlock(); } // VLC wants to display a video frame. -static void display(void* data, void* id) +static void display(void* data, void* id) { if (data == NULL) return; @@ -46,13 +58,12 @@ static void display(void* data, void* id) VideoVlcComponent::VideoVlcComponent(Window* window, std::string subtitles) : VideoComponent(window), - mMediaPlayer(nullptr), - mSubtitlePath(subtitles) + mMediaPlayer(nullptr), + mMedia(nullptr) { - memset(&mContext, 0, sizeof(mContext)); - // Get an empty texture for rendering the video - mTexture = TextureResource::get(""); + mTexture = nullptr;// TextureResource::get(""); + mEffect = VideoVlcFlags::VideoVlcEffect::BUMP; // Make sure VLC has been initialised setupVLC(subtitles); @@ -107,86 +118,67 @@ void VideoVlcComponent::onVideoStarted() void VideoVlcComponent::resize() { - if(!mTexture) + if (!mTexture) return; const Vector2f textureSize((float)mVideoWidth, (float)mVideoHeight); - if(textureSize == Vector2f::Zero()) + if (textureSize == Vector2f::Zero()) return; - // SVG rasterization is determined by height (see SVGResource.cpp), and rasterization is done in terms of pixels - // if rounding is off enough in the rasterization step (for images with extreme aspect ratios), it can cause cutoff when the aspect ratio breaks - // so, we always make sure the resultant height is an integer to make sure cutoff doesn't happen, and scale width from that - // (you'll see this scattered throughout the function) - // this is probably not the best way, so if you're familiar with this problem and have a better solution, please make a pull request! - - if(mTargetIsMax) - { + // SVG rasterization is determined by height (see SVGResource.cpp), and rasterization is done in terms of pixels + // if rounding is off enough in the rasterization step (for images with extreme aspect ratios), it can cause cutoff when the aspect ratio breaks + // so, we always make sure the resultant height is an integer to make sure cutoff doesn't happen, and scale width from that + // (you'll see this scattered throughout the function) + // this is probably not the best way, so if you're familiar with this problem and have a better solution, please make a pull request! - mSize = textureSize; + if (mTargetIsMax) + { - Vector2f resizeScale((mTargetSize.x() / mSize.x()), (mTargetSize.y() / mSize.y())); + mSize = textureSize; - if(resizeScale.x() < resizeScale.y()) - { - mSize[0] *= resizeScale.x(); - mSize[1] *= resizeScale.x(); - }else{ - mSize[0] *= resizeScale.y(); - mSize[1] *= resizeScale.y(); - } + Vector2f resizeScale((mTargetSize.x() / mSize.x()), (mTargetSize.y() / mSize.y())); - // for SVG rasterization, always calculate width from rounded height (see comment above) - mSize[1] = Math::round(mSize[1]); - mSize[0] = (mSize[1] / textureSize.y()) * textureSize.x(); - - } - else if (mTargetIsMin) + if (resizeScale.x() < resizeScale.y()) { - /*mSize = textureSize; - - Vector2f resizeScale((mTargetSize.x() / mSize.x()), (mTargetSize.y() / mSize.y())); - - if (resizeScale.x() > resizeScale.y()) - { - mSize[0] *= resizeScale.x(); - mSize[1] *= resizeScale.x(); + mSize[0] *= resizeScale.x(); + mSize[1] *= resizeScale.x(); + } + else { + mSize[0] *= resizeScale.y(); + mSize[1] *= resizeScale.y(); + } - // float cropPercent = (mSize.y() - mTargetSize.y()) / (mSize.y() * 2); - // crop(0, cropPercent, 0, cropPercent); - } - else { - mSize[0] *= resizeScale.y(); - mSize[1] *= resizeScale.y(); + // for SVG rasterization, always calculate width from rounded height (see comment above) + mSize[1] = Math::round(mSize[1]); + mSize[0] = (mSize[1] / textureSize.y()) * textureSize.x(); - // float cropPercent = (mSize.x() - mTargetSize.x()) / (mSize.x() * 2); - // crop(cropPercent, 0, cropPercent, 0); - } - */ - mSize = ImageIO::adjustExternPictureSizef(textureSize, mTargetSize); + } + else if (mTargetIsMin) + { + mSize = ImageIO::adjustExternPictureSizef(textureSize, mTargetSize); + } + else { + // if both components are set, we just stretch + // if no components are set, we don't resize at all + mSize = mTargetSize == Vector2f::Zero() ? textureSize : mTargetSize; + + // if only one component is set, we resize in a way that maintains aspect ratio + // for SVG rasterization, we always calculate width from rounded height (see comment above) + if (!mTargetSize.x() && mTargetSize.y()) + { + mSize[1] = Math::round(mTargetSize.y()); + mSize[0] = (mSize.y() / textureSize.y()) * textureSize.x(); } - else + else if (mTargetSize.x() && !mTargetSize.y()) { - // if both components are set, we just stretch - // if no components are set, we don't resize at all - mSize = mTargetSize == Vector2f::Zero() ? textureSize : mTargetSize; - - // if only one component is set, we resize in a way that maintains aspect ratio - // for SVG rasterization, we always calculate width from rounded height (see comment above) - if(!mTargetSize.x() && mTargetSize.y()) - { - mSize[1] = Math::round(mTargetSize.y()); - mSize[0] = (mSize.y() / textureSize.y()) * textureSize.x(); - }else if(mTargetSize.x() && !mTargetSize.y()) - { - mSize[1] = Math::round((mTargetSize.x() / textureSize.x()) * textureSize.y()); - mSize[0] = (mSize.y() / textureSize.y()) * textureSize.x(); - } + mSize[1] = Math::round((mTargetSize.x() / textureSize.x()) * textureSize.y()); + mSize[0] = (mSize.y() / textureSize.y()) * textureSize.x(); } + } // mSize.y() should already be rounded - mTexture->rasterizeAt(Math::round(mSize.x()), Math::round(mSize.y())); + mTexture->rasterizeAt((size_t)Math::round(mSize.x()), (size_t)Math::round(mSize.y())); onSizeChanged(); } @@ -201,7 +193,7 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) bool initFromPixels = true; if (!mIsPlaying || !mContext.valid) - { + { // If video is still attached to the path & texture is initialized, we suppose it had just been stopped (onhide, ondisable, screensaver...) // still render the last frame if (mTexture != nullptr && !mVideoPath.empty() && mPlayingVideoPath == mVideoPath && mTexture->isLoaded()) @@ -221,11 +213,10 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) if (t == 0.0) return; - + Transform4x4f trans = parentTrans * getTransform(); - Vector2f clipPos(trans.translation().x(), trans.translation().y()); - if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + if (!Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) return; GuiComponent::renderChildren(trans); @@ -233,39 +224,79 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) // Build a texture for the video frame if (initFromPixels) - mTexture->initFromPixels((unsigned char*)mContext.surface->pixels, mContext.surface->w, mContext.surface->h); - + { + int frame = mContext.surfaceId; + if (mContext.hasFrame[frame]) + { + if (mTexture == nullptr) + { + mTexture = TextureResource::get(""); + resize(); + } + + mContext.mutexes[frame].lock(); + mTexture->initFromExternalPixels(mContext.surfaces[frame], mVideoWidth, mVideoHeight); + mContext.hasFrame[frame] = false; + mContext.mutexes[frame].unlock(); + } + } + + if (mTexture == nullptr) + return; + const unsigned int fadeIn = t * 255.0f; const unsigned int color = Renderer::convertColor(0xFFFFFF00 | fadeIn); Renderer::Vertex vertices[4]; - vertices[0] = { { 0.0f , 0.0f }, { 0.0f, 0.0f }, color }; - vertices[1] = { { 0.0f , mSize.y() }, { 0.0f, 1.0f }, color }; - vertices[2] = { { mSize.x(), 0.0f }, { 1.0f, 0.0f }, color }; - vertices[3] = { { mSize.x(), mSize.y() }, { 1.0f, 1.0f }, color }; + if (mEffect == VideoVlcFlags::VideoVlcEffect::BUMP && mFadeIn > 0.0 && mFadeIn < 1.0 && mConfig.startDelay > 0) + { + // Bump Effect + float bump = sin((MATHPI / 2.0) * mFadeIn) + sin(MATHPI * mFadeIn) / 2.0; + + float w = mSize.x() * bump; + float h = mSize.y() * bump; + float centerX = mSize.x() / 2.0f; + float centerY = mSize.y() / 2.0f; + + Vector2f topLeft(Math::round(centerX - w / 2.0f), Math::round(centerY - h / 2.0f)); + Vector2f bottomRight(Math::round(centerX + w / 2.0f), Math::round(centerY + h / 2.0f)); + + vertices[0] = { { topLeft.x() , topLeft.y() },{ 0.0f, 0.0f }, color }; + vertices[1] = { { topLeft.x() , bottomRight.y() },{ 0.0f, 1.0f }, color }; + vertices[2] = { { bottomRight.x() , topLeft.y() },{ 1.0f, 0.0f }, color }; + vertices[3] = { { bottomRight.x() , bottomRight.y() },{ 1.0f, 1.0f }, color }; + } + else + { + vertices[0] = { { 0.0f , 0.0f },{ 0.0f, 0.0f }, color }; + vertices[1] = { { 0.0f , mSize.y() },{ 0.0f, 1.0f }, color }; + vertices[2] = { { mSize.x(), 0.0f },{ 1.0f, 0.0f }, color }; + vertices[3] = { { mSize.x(), mSize.y() },{ 1.0f, 1.0f }, color }; + } // round vertices - for(int i = 0; i < 4; ++i) + for (int i = 0; i < 4; ++i) vertices[i].pos.round(); - - mTexture->bind(); - if (mTargetIsMin) + if (mTexture->bind()) { - Vector2f targetPos = (mTargetSize - mSize) * mOrigin * -1; - - Vector2i pos(trans.translation().x() + (int)targetPos.x(), trans.translation().y() + (int)targetPos.y()); - Vector2i size((int)mTargetSize.round().x(), (int)mTargetSize.round().y()); - Renderer::pushClipRect(pos, size); - } - - // Render it - Renderer::drawTriangleStrips(&vertices[0], 4); + if (mTargetIsMin) + { + Vector2f targetPos = (mTargetSize - mSize) * mOrigin * -1; + + Vector2i pos(trans.translation().x() + (int)targetPos.x(), trans.translation().y() + (int)targetPos.y()); + Vector2i size((int)mTargetSize.round().x(), (int)mTargetSize.round().y()); + Renderer::pushClipRect(pos, size); + } - if (mTargetIsMin) - Renderer::popClipRect(); + // Render it + Renderer::drawTriangleStrips(&vertices[0], 4); - Renderer::bindTexture(0); + if (mTargetIsMin) + Renderer::popClipRect(); + + Renderer::bindTexture(0); + } } void VideoVlcComponent::setupContext() @@ -274,8 +305,10 @@ void VideoVlcComponent::setupContext() return; // Create an RGBA surface to render the video into - mContext.surface = SDL_CreateRGBSurface(SDL_SWSURFACE, (int)mVideoWidth, (int)mVideoHeight, 32, 0xff000000, 0x00ff0000, 0x0000ff00, 0x000000ff); - mContext.mutex = SDL_CreateMutex(); + mContext.surfaces[0] = new unsigned char[mVideoWidth * mVideoHeight * 4]; + mContext.surfaces[1] = new unsigned char[mVideoWidth * mVideoHeight * 4]; + mContext.hasFrame[0] = false; + mContext.hasFrame[1] = false; mContext.component = this; mContext.valid = true; resize(); @@ -286,26 +319,44 @@ void VideoVlcComponent::freeContext() if (!mContext.valid) return; - SDL_FreeSurface(mContext.surface); - SDL_DestroyMutex(mContext.mutex); + if (!mDisable) + { + // Release texture memory -> except if mDisable by topWindow ( ex: menu was poped ) + mTexture = nullptr; + } + delete[] mContext.surfaces[0]; + delete[] mContext.surfaces[1]; + mContext.surfaces[0] = nullptr; + mContext.surfaces[1] = nullptr; + mContext.hasFrame[0] = false; + mContext.hasFrame[1] = false; mContext.component = NULL; mContext.valid = false; } void VideoVlcComponent::setupVLC(std::string subtitles) { - // If VLC hasn't been initialised yet then do it now if (mVLC != nullptr) return; - const char** args; - const char* singleargs[] = { "--quiet" }; + std::vector cmdline; + cmdline.push_back("--quiet"); + cmdline.push_back("--no-video-title-show"); + + if (!subtitles.empty()) + { + cmdline.push_back("--sub-file"); + cmdline.push_back(subtitles); + } + const char* *theArgs = new const char*[10]; + + for (int i = 0; i < cmdline.size(); i++) + theArgs[i] = cmdline[i].c_str(); - int argslen = sizeof(singleargs) / sizeof(singleargs[0]); - args = singleargs; + mVLC = libvlc_new(cmdline.size(), theArgs); - mVLC = libvlc_new(argslen, args); + delete[] theArgs; } void VideoVlcComponent::handleLooping() @@ -320,7 +371,9 @@ void VideoVlcComponent::handleLooping() libvlc_audio_set_mute(mMediaPlayer, 1); } //libvlc_media_player_set_position(mMediaPlayer, 0.0f); - libvlc_media_player_set_media(mMediaPlayer, mMedia); + if (mMedia) + libvlc_media_player_set_media(mMediaPlayer, mMedia); + libvlc_media_player_play(mMediaPlayer); } } @@ -328,95 +381,93 @@ void VideoVlcComponent::handleLooping() void VideoVlcComponent::startVideo() { - if (!mIsPlaying) { - mVideoWidth = 0; - mVideoHeight = 0; + if (mIsPlaying) + return; + + mVideoWidth = 0; + mVideoHeight = 0; #ifdef WIN32 - std::string path(Utils::String::replace(mVideoPath, "/", "\\")); + std::string path(Utils::String::replace(mVideoPath, "/", "\\")); #else - std::string path(mVideoPath); + std::string path(mVideoPath); #endif - // Make sure we have a video path - if (mVLC && (path.size() > 0)) + // Make sure we have a video path + if (mVLC && (path.size() > 0)) + { + // Set the video that we are going to be playing so we don't attempt to restart it + mPlayingVideoPath = mVideoPath; + + // Open the media + mMedia = libvlc_media_new_path(mVLC, path.c_str()); + if (mMedia) { - // Set the video that we are going to be playing so we don't attempt to restart it - mPlayingVideoPath = mVideoPath; - - if (!mSubtitlePath.empty()) + unsigned track_count; + // Get the media metadata so we can find the aspect ratio + libvlc_media_parse(mMedia); + libvlc_media_track_t** tracks; + track_count = libvlc_media_tracks_get(mMedia, &tracks); + for (unsigned track = 0; track < track_count; ++track) { - if (!mSubtitleTmpFile.empty()) - Utils::FileSystem::removeFile(mSubtitleTmpFile); - - auto ext = Utils::FileSystem::getExtension(path); - auto srt = Utils::String::replace(path, ext, ".srt"); - Utils::FileSystem::copyFile(mSubtitlePath, srt); - - mSubtitleTmpFile = srt; + if (tracks[track]->i_type == libvlc_track_video) + { + mVideoWidth = tracks[track]->video->i_width; + mVideoHeight = tracks[track]->video->i_height; + break; + } } - - // Open the media - mMedia = libvlc_media_new_path(mVLC, path.c_str()); - if (mMedia) + libvlc_media_tracks_release(tracks, track_count); + + // Make sure we found a valid video track + if ((mVideoWidth > 0) && (mVideoHeight > 0)) { - unsigned track_count; - // Get the media metadata so we can find the aspect ratio - libvlc_media_parse(mMedia); - libvlc_media_track_t** tracks; - track_count = libvlc_media_tracks_get(mMedia, &tracks); - for (unsigned track = 0; track < track_count; ++track) +#ifndef _RPI_ + if (mScreensaverMode) { - if (tracks[track]->i_type == libvlc_track_video) - { - mVideoWidth = tracks[track]->video->i_width; - mVideoHeight = tracks[track]->video->i_height; - break; + if (!Settings::getInstance()->getBool("CaptionsCompatibility")) { + + Vector2f resizeScale((Renderer::getScreenWidth() / (float)mVideoWidth), (Renderer::getScreenHeight() / (float)mVideoHeight)); + + if (resizeScale.x() < resizeScale.y()) + { + mVideoWidth = (unsigned int)(mVideoWidth * resizeScale.x()); + mVideoHeight = (unsigned int)(mVideoHeight * resizeScale.x()); + } + else { + mVideoWidth = (unsigned int)(mVideoWidth * resizeScale.y()); + mVideoHeight = (unsigned int)(mVideoHeight * resizeScale.y()); + } } } - libvlc_media_tracks_release(tracks, track_count); +#endif - // Make sure we found a valid video track - if ((mVideoWidth > 0) && (mVideoHeight > 0)) + if (Settings::getInstance()->getBool("OptimizeVideo") && !mTargetSize.empty()) { -#ifndef _RPI_ - if (mScreensaverMode) + // If video is bigger than display, ask VLC for a smaller image + auto sz = ImageIO::adjustPictureSize(Vector2i(mVideoWidth, mVideoHeight), Vector2i(mTargetSize.x(), mTargetSize.y()), mTargetIsMin); + if (sz.x() < mVideoWidth || sz.y() < mVideoHeight) { - if(!Settings::getInstance()->getBool("CaptionsCompatibility")) { - - Vector2f resizeScale((Renderer::getScreenWidth() / (float)mVideoWidth), (Renderer::getScreenHeight() / (float)mVideoHeight)); - - if(resizeScale.x() < resizeScale.y()) - { - mVideoWidth = (unsigned int) (mVideoWidth * resizeScale.x()); - mVideoHeight = (unsigned int) (mVideoHeight * resizeScale.x()); - }else{ - mVideoWidth = (unsigned int) (mVideoWidth * resizeScale.y()); - mVideoHeight = (unsigned int) (mVideoHeight * resizeScale.y()); - } - } + mVideoWidth = sz.x(); + mVideoHeight = sz.y(); } -#endif - PowerSaver::pause(); - setupContext(); + } - // Setup the media player - mMediaPlayer = libvlc_media_player_new_from_media(mMedia); + PowerSaver::pause(); + setupContext(); - if (!Settings::getInstance()->getBool("VideoAudio")) - { - libvlc_audio_set_mute(mMediaPlayer, 1); - } + // Setup the media player + mMediaPlayer = libvlc_media_player_new_from_media(mMedia); - auto cnt = libvlc_video_get_spu_count(mMediaPlayer); - - libvlc_media_player_play(mMediaPlayer); - libvlc_video_set_callbacks(mMediaPlayer, lock, unlock, display, (void*)&mContext); - libvlc_video_set_format(mMediaPlayer, "RGBA", (int)mVideoWidth, (int)mVideoHeight, (int)mVideoWidth * 4); - - // Update the playing state - //mIsPlaying = true; - //mFadeIn = 0.0f; - } + if (!Settings::getInstance()->getBool("VideoAudio")) + libvlc_audio_set_mute(mMediaPlayer, 1); + + libvlc_media_player_play(mMediaPlayer); + libvlc_video_set_callbacks(mMediaPlayer, lock, unlock, display, (void*)&mContext); + libvlc_video_set_format(mMediaPlayer, "RGBA", (int)mVideoWidth, (int)mVideoHeight, (int)mVideoWidth * 4); + + // Update the playing state -> Useless now set by display() & onVideoStarted + //mIsPlaying = true; + //mFadeIn = 0.0f; } } } @@ -426,20 +477,38 @@ void VideoVlcComponent::stopVideo() { mIsPlaying = false; mStartDelayed = false; + // Release the media player so it stops calling back to us if (mMediaPlayer) { libvlc_media_player_stop(mMediaPlayer); libvlc_media_player_release(mMediaPlayer); - libvlc_media_release(mMedia); mMediaPlayer = NULL; - freeContext(); - PowerSaver::resume(); } - if (!mSubtitleTmpFile.empty()) + // Release the media + if (mMedia) { - Utils::FileSystem::removeFile(mSubtitleTmpFile); - mSubtitleTmpFile = ""; + libvlc_media_release(mMedia); + mMedia = NULL; } + + freeContext(); + PowerSaver::resume(); } + +void VideoVlcComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) +{ + VideoComponent::applyTheme(theme, view, element, properties); + + using namespace ThemeFlags; + + const ThemeData::ThemeElement* elem = theme->getElement(view, element, "video"); + if (elem && elem->has("effect")) + { + if (!(elem->get("effect").compare("bump"))) + mEffect = VideoVlcFlags::VideoVlcEffect::BUMP; + else + mEffect = VideoVlcFlags::VideoVlcEffect::NONE; + } +} \ No newline at end of file diff --git a/es-core/src/components/VideoVlcComponent.h b/es-core/src/components/VideoVlcComponent.h index 81ae70e679..bd697e11f8 100644 --- a/es-core/src/components/VideoVlcComponent.h +++ b/es-core/src/components/VideoVlcComponent.h @@ -3,20 +3,44 @@ #define ES_CORE_COMPONENTS_VIDEO_VLC_COMPONENT_H #include "VideoComponent.h" +#include -struct SDL_mutex; -struct SDL_Surface; struct libvlc_instance_t; struct libvlc_media_t; struct libvlc_media_player_t; -struct VideoContext { - SDL_Surface* surface; - SDL_mutex* mutex; +struct VideoContext +{ + VideoContext() + { + surfaces[0] = nullptr; + surfaces[1] = nullptr; + component = nullptr; + valid = false; + hasFrame[0] = false; + hasFrame[1] = false; + surfaceId = 0; + } + + int surfaceId; + unsigned char* surfaces[2]; + std::mutex mutexes[2]; + bool hasFrame[2]; + VideoComponent* component; bool valid; }; + +namespace VideoVlcFlags +{ + enum VideoVlcEffect + { + NONE, + BUMP + }; +} + class VideoVlcComponent : public VideoComponent { // Structure that groups together the configuration of the video component @@ -31,12 +55,11 @@ class VideoVlcComponent : public VideoComponent public: static void setupVLC(std::string subtitles); - VideoVlcComponent(Window* window, std::string subtitles); + VideoVlcComponent(Window* window, std::string subtitles = ""); virtual ~VideoVlcComponent(); void render(const Transform4x4f& parentTrans) override; - // Resize the video to fit this size. If one axis is zero, scale that axis to maintain aspect ratio. // If both are non-zero, potentially break the aspect ratio. If both are zero, no resizing. // Can be set before or after a video is loaded. @@ -49,6 +72,8 @@ class VideoVlcComponent : public VideoComponent void setMaxSize(float width, float height); void setMinSize(float width, float height); + virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); + private: // Calculates the correct mSize from our resizing information (set by setResize/setMaxSize). // Used internally whenever the resizing parameters or texture change. @@ -65,14 +90,19 @@ class VideoVlcComponent : public VideoComponent void setupContext(); void freeContext(); + void setEffect(VideoVlcFlags::VideoVlcEffect effect) { mEffect = effect; } + private: static libvlc_instance_t* mVLC; libvlc_media_t* mMedia; libvlc_media_player_t* mMediaPlayer; VideoContext mContext; std::shared_ptr mTexture; + std::string mSubtitlePath; std::string mSubtitleTmpFile; + + VideoVlcFlags::VideoVlcEffect mEffect; }; #endif // ES_CORE_COMPONENTS_VIDEO_VLC_COMPONENT_H diff --git a/es-core/src/math/Vector2f.h b/es-core/src/math/Vector2f.h index 93e109d799..dab7eb0372 100644 --- a/es-core/src/math/Vector2f.h +++ b/es-core/src/math/Vector2f.h @@ -61,6 +61,8 @@ class Vector2f static const Vector2f UnitX() { return { 1, 0 }; } static const Vector2f UnitY() { return { 0, 1 }; } + inline bool empty() { return mX == 0.0 && mY == 0.0; } + private: float mX; diff --git a/es-core/src/renderers/Renderer_GL21.cpp b/es-core/src/renderers/Renderer_GL21.cpp index eb6d96a0d6..5cf1cca5c1 100644 --- a/es-core/src/renderers/Renderer_GL21.cpp +++ b/es-core/src/renderers/Renderer_GL21.cpp @@ -125,7 +125,15 @@ namespace Renderer void updateTexture(const unsigned int _texture, const Texture::Type _type, const unsigned int _x, const unsigned _y, const unsigned int _width, const unsigned int _height, void* _data) { bindTexture(_texture); - glTexSubImage2D(GL_TEXTURE_2D, 0, _x, _y, _width, _height, convertTextureType(_type), GL_UNSIGNED_BYTE, _data); + + if (_x == -1 && _y == -1) + { + const GLenum type = convertTextureType(_type); + glTexImage2D(GL_TEXTURE_2D, 0, type, _width, _height, 0, type, GL_UNSIGNED_BYTE, _data); + } + else + glTexSubImage2D(GL_TEXTURE_2D, 0, _x, _y, _width, _height, convertTextureType(_type), GL_UNSIGNED_BYTE, _data); + bindTexture(0); } // updateTexture diff --git a/es-core/src/renderers/Renderer_GLES10.cpp b/es-core/src/renderers/Renderer_GLES10.cpp index ae9d670740..9a02683d26 100644 --- a/es-core/src/renderers/Renderer_GLES10.cpp +++ b/es-core/src/renderers/Renderer_GLES10.cpp @@ -125,7 +125,15 @@ namespace Renderer void updateTexture(const unsigned int _texture, const Texture::Type _type, const unsigned int _x, const unsigned _y, const unsigned int _width, const unsigned int _height, void* _data) { bindTexture(_texture); - glTexSubImage2D(GL_TEXTURE_2D, 0, _x, _y, _width, _height, convertTextureType(_type), GL_UNSIGNED_BYTE, _data); + + if (_x == -1 && _y == -1) + { + const GLenum type = convertTextureType(_type); + glTexImage2D(GL_TEXTURE_2D, 0, type, _width, _height, 0, type, GL_UNSIGNED_BYTE, _data); + } + else + glTexSubImage2D(GL_TEXTURE_2D, 0, _x, _y, _width, _height, convertTextureType(_type), GL_UNSIGNED_BYTE, _data);* + bindTexture(0); } // updateTexture diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 9e85bf3a32..58853724c9 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -17,7 +17,7 @@ bool TextureData::OPTIMIZEVRAM = false; TextureData::TextureData(bool tile) : mTile(tile), mTextureID(0), mDataRGBA(nullptr), mScalable(false), mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f), mMaxSize(MaxSizeInfo()), mPackedSize(Vector2i(0,0)), mBaseSize(Vector2i(0, 0)) { - + mIsExternalDataRGBA = false; } TextureData::~TextureData() @@ -197,6 +197,13 @@ bool TextureData::initFromRGBA(const unsigned char* dataRGBA, size_t width, size { // If already initialised then don't read again std::unique_lock lock(mMutex); + + if (mIsExternalDataRGBA) + { + mIsExternalDataRGBA = false; + mDataRGBA = nullptr; + } + if (mDataRGBA) return true; @@ -212,6 +219,13 @@ bool TextureData::initFromRGBAEx(unsigned char* dataRGBA, size_t width, size_t h { // If already initialised then don't read again std::unique_lock lock(mMutex); + + if (mIsExternalDataRGBA) + { + mIsExternalDataRGBA = false; + mDataRGBA = nullptr; + } + if (mDataRGBA) return true; @@ -223,6 +237,25 @@ bool TextureData::initFromRGBAEx(unsigned char* dataRGBA, size_t width, size_t h return true; } +bool TextureData::initFromExternalRGBA(unsigned char* dataRGBA, size_t width, size_t height) +{ + // If already initialised then don't read again + std::unique_lock lock(mMutex); + + if (!mIsExternalDataRGBA && mDataRGBA != nullptr) + delete[] mDataRGBA; + + mIsExternalDataRGBA = true; + mDataRGBA = dataRGBA; + mWidth = width; + mHeight = height; + + if (mTextureID != 0) + Renderer::updateTexture(mTextureID, Renderer::Texture::RGBA, -1, -1, mWidth, mHeight, mDataRGBA); + + return true; +} + bool TextureData::load() { bool retval = false; @@ -277,6 +310,13 @@ bool TextureData::uploadAndBind() return false; mTextureID = Renderer::createTexture(Renderer::Texture::RGBA, true, mTile, mWidth, mHeight, mDataRGBA); + if (mTextureID) + { + if (mDataRGBA != nullptr && !mIsExternalDataRGBA) + delete[] mDataRGBA; + + mDataRGBA = nullptr; + } } return true; @@ -295,7 +335,10 @@ void TextureData::releaseVRAM() void TextureData::releaseRAM() { std::unique_lock lock(mMutex); - delete[] mDataRGBA; + + if (mDataRGBA != nullptr && !mIsExternalDataRGBA) + delete[] mDataRGBA; + mDataRGBA = 0; } diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 9c35d0eaf6..7477c9bbe4 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -27,6 +27,7 @@ class TextureData bool initImageFromMemory(const unsigned char* fileData, size_t length); bool initFromRGBA(const unsigned char* dataRGBA, size_t width, size_t height); bool initFromRGBAEx(unsigned char* dataRGBA, size_t width, size_t height); + bool initFromExternalRGBA(unsigned char* dataRGBA, size_t width, size_t height); // Read the data into memory if necessary bool load(); @@ -81,6 +82,8 @@ class TextureData Vector2i mPackedSize; Vector2i mBaseSize; MaxSizeInfo mMaxSize; + + bool mIsExternalDataRGBA; }; #endif // ES_CORE_RESOURCES_TEXTURE_DATA_H diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index 5bb5a86482..48d0c2b831 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -13,10 +13,10 @@ TextureDataManager::TextureDataManager() mBlank = std::shared_ptr(new TextureData(false)); for (int i = 0; i < (5 * 5); ++i) { - data[i*4] = (i % 2) * 255; - data[i*4+1] = (i % 2) * 255; - data[i*4+2] = (i % 2) * 255; - data[i*4+3] = 0; + data[i * 4] = 0; // (i % 2) * 255; + data[i * 4 + 1] = 0; // (i % 2) * 255; + data[i * 4 + 2] = 0; // (i % 2) * 255; + data[i * 4 + 3] = 0; } mBlank->initFromRGBA(data, 5, 5); @@ -46,15 +46,22 @@ void TextureDataManager::onTextureLoaded(std::shared_ptr tex) std::shared_ptr TextureDataManager::add(const TextureResource* key, bool tiled) { - remove(key); - std::shared_ptr data(new TextureData(tiled)); - mTextures.push_front(data); + std::unique_lock lock(mMutex); + // Find the entry in the list + auto it = mTextureLookup.find(key); + if (it != mTextureLookup.cend()) { - std::unique_lock lock(mMutex); - mTextureLookup[key] = mTextures.cbegin(); + // Remove the list entry + mTextures.erase((*it).second); + // And the lookup + mTextureLookup.erase(it); } + std::shared_ptr data = std::make_shared(tiled); + mTextures.push_front(data); + mTextureLookup[key] = mTextures.cbegin(); + return data; } @@ -73,6 +80,15 @@ void TextureDataManager::remove(const TextureResource* key) } } +void TextureDataManager::cancelAsync(const TextureResource* key) +{ + std::unique_lock lock(mMutex); + + auto it = mTextureLookup.find(key); + if (it != mTextureLookup.cend()) + mLoader->remove(*(*it).second); +} + std::shared_ptr TextureDataManager::get(const TextureResource* key, bool enableLoading) { std::unique_lock lock(mMutex); @@ -84,16 +100,23 @@ std::shared_ptr TextureDataManager::get(const TextureResource* key, if (it != mTextureLookup.cend()) { tex = *(*it).second; - // Remove the list entry - mTextures.erase((*it).second); - // Put it at the top - mTextures.push_front(tex); - // Store it back in the lookup - mTextureLookup[key] = mTextures.cbegin(); + + if (mTextures.cbegin() != (*it).second) + { + // Remove the list entry + mTextures.erase((*it).second); + // Put it at the top + mTextures.push_front(tex); + // Store it back in the lookup + mTextureLookup[key] = mTextures.cbegin(); + } // Make sure it's loaded or queued for loading if (enableLoading && !tex->isLoaded()) // FCATMP + { + lock.unlock(); load(tex); + } } return tex; } @@ -111,17 +134,23 @@ bool TextureDataManager::bind(const TextureResource* key) size_t TextureDataManager::getTotalSize() { + std::unique_lock lock(mMutex); + size_t total = 0; for (auto tex : mTextures) total += tex->width() * tex->height() * 4; + return total; } size_t TextureDataManager::getCommittedSize() { + std::unique_lock lock(mMutex); + size_t total = 0; for (auto tex : mTextures) total += tex->getVRAMUsage(); + return total; } @@ -136,30 +165,7 @@ bool compareTextures(const std::shared_ptr& first, const std::share bool secondIsResource = second->mPath.rfind(":/") == 0; if (isResource && !secondIsResource) return true; - /* - if (secondIsResource) - return false; - - bool firstisTheme = first->mPath.rfind("/themes/") != std::string::npos; - bool secondIsTheme = second->mPath.rfind("/themes/") != std::string::npos; - - - if (firstisTheme && secondIsTheme) - { - auto firstExt = Utils::String::toLower(Utils::FileSystem::getExtension(first->mPath)); - if (firstExt == ".svg") - { - auto secondExt = Utils::String::toLower(Utils::FileSystem::getExtension(second->mPath)); - if (secondExt != ".svg") - return true; - return false; - } - } - - if (firstisTheme && !secondIsTheme) - return true; - */ return false; } @@ -184,6 +190,8 @@ void TextureDataManager::load(std::shared_ptr tex, bool block) if (size >= max_texture) { + std::unique_lock lock(mMutex); + std::list> orderedTextures(mTextures); orderedTextures.sort(compareTextures); @@ -195,13 +203,27 @@ void TextureDataManager::load(std::shared_ptr tex, bool block) if ((*it) == tex) continue; - (*it)->releaseVRAM(); - (*it)->releaseRAM(); + bool changed = false; + + if ((*it)->isLoaded()) + { + (*it)->releaseVRAM(); + (*it)->releaseRAM(); + + changed = true; + } // It may be already in the loader queue. In this case it wouldn't have been using // any VRAM yet but it will be. Remove it from the loader queue - mLoader->remove(*it); - size = TextureResource::getTotalMemUsage(); + if (mLoader->remove(*it)) + changed = true; + + if (changed) + { + lock.unlock(); + size = TextureResource::getTotalMemUsage(); + lock.lock(); + } } } @@ -231,8 +253,7 @@ TextureLoader::TextureLoader(TextureDataManager* mgr) : mExit(false) TextureLoader::~TextureLoader() { // Just abort any waiting texture - mTextureDataQ.clear(); - mTextureDataLookup.clear(); + clearQueue(); // Exit the thread mExit = true; @@ -244,101 +265,94 @@ TextureLoader::~TextureLoader() void TextureLoader::threadProc() { - while (!mExit) + while (true) { - std::shared_ptr textureData; - { - // Wait for an event to say there is something in the queue - std::unique_lock lock(mMutex); - //mEvent.wait(lock); - if (!mTextureDataQ.empty()) - { - textureData = mTextureDataQ.front(); - mTextureDataQ.pop_front(); - mTextureDataLookup.erase(mTextureDataLookup.find(textureData.get())); + // Wait for an event to say there is something in the queue + std::unique_lock lock(mLoaderLock); + mEvent.wait(lock, [this]() { return mExit || !mTextureDataQ.empty(); }); - mProcessingTextureDataQ.push_back(textureData); - } - } + if (mExit) + break; - if (textureData) + if (!mTextureDataQ.empty()) { - textureData->load(); - mManager->onTextureLoaded(textureData); + std::shared_ptr textureData = mTextureDataQ.front(); + mTextureDataQ.pop_front(); + + mProcessingTextureDataQ.push_back(textureData); + + lock.unlock(); + if (textureData && !textureData->isLoaded()) { - std::unique_lock lock(mMutex); + textureData->load(); + mManager->onTextureLoaded(textureData); + + lock.lock(); mProcessingTextureDataQ.remove(textureData); - } - std::this_thread::yield(); - } - else - { - std::this_thread::yield(); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); + std::this_thread::yield(); + } } - } } void TextureLoader::load(std::shared_ptr textureData) { + std::unique_lock lock(mLoaderLock); + // Make sure it's not already loaded - if (!textureData->isLoaded()) - { - std::unique_lock lock(mMutex); - - // If is is currently loading, don't add again - for (auto it = mProcessingTextureDataQ.begin(); it != mProcessingTextureDataQ.end(); it++) - if ((*it) == textureData) - return; - - // Remove it from the queue if it is already there - auto td = mTextureDataLookup.find(textureData.get()); - if (td != mTextureDataLookup.cend()) - { - mTextureDataQ.erase((*td).second); - mTextureDataLookup.erase(td); - } + if (textureData->isLoaded()) + return; - // Put it on the start of the queue as we want the newly requested textures to load first - mTextureDataQ.push_front(textureData); - mTextureDataLookup[textureData.get()] = mTextureDataQ.cbegin(); - mEvent.notify_all(); - } + // If is is currently loading, don't add again + if (std::find(mProcessingTextureDataQ.cbegin(), mProcessingTextureDataQ.cend(), textureData) != mProcessingTextureDataQ.cend()) + return; + + // Remove it from the queue if it is already there + auto tx = std::find(mTextureDataQ.cbegin(), mTextureDataQ.cend(), textureData); + if (tx != mTextureDataQ.cend()) + mTextureDataQ.erase(tx); + + // Put it on the start of the queue as we want the newly requested textures to load first + mTextureDataQ.push_front(textureData); + mEvent.notify_one(); } -void TextureLoader::remove(std::shared_ptr textureData) +bool TextureLoader::remove(std::shared_ptr textureData) { // Just remove it from the queue so we don't attempt to load it - std::unique_lock lock(mMutex); - auto td = mTextureDataLookup.find(textureData.get()); - if (td != mTextureDataLookup.cend()) + std::unique_lock lock(mLoaderLock); + + auto tx = std::find(mTextureDataQ.cbegin(), mTextureDataQ.cend(), textureData); + if (tx != mTextureDataQ.cend()) { - mTextureDataQ.erase((*td).second); - mTextureDataLookup.erase(td); + mTextureDataQ.erase(tx); + return true; } + + return false; } size_t TextureLoader::getQueueSize() { + std::unique_lock lock(mLoaderLock); + // Gets the amount of video memory that will be used once all textures in // the queue are loaded size_t mem = 0; - std::unique_lock lock(mMutex); - for (auto tex : mTextureDataQ) - { + for (auto tex : mTextureDataQ) mem += tex->width() * tex->height() * 4; - } + return mem; } void TextureLoader::clearQueue() { + std::unique_lock lock(mLoaderLock); + // Just abort any waiting texture mTextureDataQ.clear(); - mTextureDataLookup.clear(); } void TextureDataManager::clearQueue() diff --git a/es-core/src/resources/TextureDataManager.h b/es-core/src/resources/TextureDataManager.h index 565694a3f7..ed54139279 100644 --- a/es-core/src/resources/TextureDataManager.h +++ b/es-core/src/resources/TextureDataManager.h @@ -21,7 +21,7 @@ class TextureLoader ~TextureLoader(); void load(std::shared_ptr textureData); - void remove(std::shared_ptr textureData); + bool remove(std::shared_ptr textureData); void clearQueue(); size_t getQueueSize(); @@ -35,7 +35,7 @@ class TextureLoader std::map >::const_iterator > mTextureDataLookup; std::vector mThreads; - std::mutex mMutex; + std::mutex mLoaderLock; std::condition_variable mEvent; bool mExit; @@ -68,6 +68,7 @@ class TextureDataManager // be referenced by a smart point so we only need to remove it from our array and it // will be deleted when the other thread has finished with it void remove(const TextureResource* key); + void cancelAsync(const TextureResource* key); std::shared_ptr get(const TextureResource* key, bool enableLoading = true); bool bind(const TextureResource* key); diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index 0f305e06c2..297232a847 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -99,6 +99,15 @@ void TextureResource::onTextureLoaded(std::shared_ptr tex) mSourceSize = Vector2f(tex->sourceWidth(), tex->sourceHeight()); } +void TextureResource::initFromExternalPixels(unsigned char* dataRGBA, size_t width, size_t height) +{ + mTextureData->initFromExternalRGBA(dataRGBA, width, height); + + // Cache the image dimensions + mSize = Vector2i((int)width, (int)height); + mSourceSize = Vector2f(mTextureData->sourceWidth(), mTextureData->sourceHeight()); +} + void TextureResource::initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height) { // This is only valid if we have a local texture data object diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index 188a868083..fdb062a4ea 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -53,6 +53,7 @@ class TextureResource : public IReloadable public: static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true, bool asReloadable = true, MaxSizeInfo maxSize = MaxSizeInfo()); void initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height); + void initFromExternalPixels(unsigned char* dataRGBA, size_t width, size_t height); virtual void initFromMemory(const char* file, size_t length); // For scalable source images in textures we want to set the resolution to rasterize at From d3cbfb19099b88115e0ab3e2d2d0807f5144362c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 2 Oct 2019 17:30:29 +0200 Subject: [PATCH 150/336] v2.10.1.1 : Major update with many changes. --- es-app/CMakeLists.txt | 8 +- es-app/src/CollectionSystemManager.cpp | 63 +- es-app/src/CollectionSystemManager.h | 2 +- es-app/src/EmulationStation.h | 10 +- es-app/src/FileData.cpp | 68 +- es-app/src/FileData.h | 4 +- es-app/src/FileFilterIndex.cpp | 36 +- es-app/src/FileFilterIndex.h | 13 +- es-app/src/FileSorts.cpp | 5 +- es-app/src/MetaData.cpp | 45 +- es-app/src/MetaData.h | 14 + es-app/src/SystemData.cpp | 35 +- es-app/src/SystemData.h | 8 +- es-app/src/VolumeControl.cpp | 30 +- es-app/src/VolumeControl.h | 30 +- es-app/src/components/RatingComponent.cpp | 16 +- .../src/components/ScraperSearchComponent.cpp | 8 +- es-app/src/components/TextListComponent.h | 2 +- .../src/guis/GuiCollectionSystemsOptions.cpp | 57 +- es-app/src/guis/GuiCollectionSystemsOptions.h | 3 +- es-app/src/guis/GuiGamelistOptions.cpp | 238 +++-- es-app/src/guis/GuiGamelistOptions.h | 8 +- es-app/src/guis/GuiMenu.cpp | 810 ++++++++++-------- es-app/src/guis/GuiMenu.h | 6 +- es-app/src/guis/GuiScraperMulti.cpp | 2 +- es-app/src/guis/GuiScraperStart.cpp | 70 +- es-app/src/guis/GuiScraperStart.h | 1 + es-app/src/guis/GuiSettings.cpp | 60 +- es-app/src/guis/GuiSettings.h | 54 +- es-app/src/main.cpp | 18 +- .../scrapers/GamesDBJSONScraperResources.cpp | 2 +- es-app/src/scrapers/Scraper.cpp | 240 ++++-- es-app/src/scrapers/Scraper.h | 63 +- es-app/src/scrapers/ScreenScraper.cpp | 68 +- es-app/src/scrapers/ScreenScraper.h | 9 +- es-app/src/scrapers/ThreadedScraper.cpp | 170 ++++ es-app/src/scrapers/ThreadedScraper.h | 48 ++ es-app/src/views/SystemView.cpp | 20 +- es-app/src/views/SystemView.h | 1 + es-app/src/views/ViewController.cpp | 17 +- es-app/src/views/ViewController.h | 5 +- .../src/views/gamelist/BasicGameListView.cpp | 5 + es-app/src/views/gamelist/BasicGameListView.h | 2 + .../views/gamelist/DetailedGameListView.cpp | 18 +- .../src/views/gamelist/GridGameListView.cpp | 9 +- es-app/src/views/gamelist/GridGameListView.h | 1 + es-app/src/views/gamelist/IGameListView.h | 2 + .../views/gamelist/ISimpleGameListView.cpp | 18 + .../src/views/gamelist/ISimpleGameListView.h | 4 + .../src/views/gamelist/VideoGameListView.cpp | 6 +- es-core/CMakeLists.txt | 6 +- es-core/src/AsyncHandle.h | 2 + es-core/src/AudioManager.cpp | 34 +- es-core/src/AudioManager.h | 2 +- es-core/src/HttpReq.cpp | 147 +++- es-core/src/HttpReq.h | 17 +- es-core/src/ImageIO.cpp | 46 +- es-core/src/Log.cpp | 111 ++- es-core/src/Log.h | 18 +- es-core/src/Settings.cpp | 37 +- es-core/src/Settings.h | 11 +- es-core/src/Sound.cpp | 3 + es-core/src/ThemeData.cpp | 204 ++--- es-core/src/ThemeData.h | 17 +- es-core/src/Window.cpp | 110 ++- es-core/src/Window.h | 21 +- .../components/AsyncNotificationComponent.cpp | 130 +++ .../components/AsyncNotificationComponent.h | 34 + es-core/src/components/GridTileComponent.cpp | 343 ++++---- es-core/src/components/GridTileComponent.h | 21 +- es-core/src/components/IList.h | 9 + es-core/src/components/ImageComponent.cpp | 203 +++-- es-core/src/components/ImageComponent.h | 11 + es-core/src/components/ImageGridComponent.h | 5 +- es-core/src/components/MenuComponent.cpp | 7 + es-core/src/components/NinePatchComponent.cpp | 68 +- es-core/src/components/NinePatchComponent.h | 6 +- es-core/src/components/OptionListComponent.h | 109 ++- es-core/src/components/SliderComponent.cpp | 3 + es-core/src/components/SliderComponent.h | 3 + es-core/src/components/TextComponent.cpp | 199 ++--- es-core/src/components/TextComponent.h | 17 +- es-core/src/components/TextEditComponent.cpp | 152 ++-- es-core/src/components/TextEditComponent.h | 7 +- es-core/src/components/VideoComponent.cpp | 4 +- {es-app => es-core}/src/guis/GuiInfoPopup.cpp | 0 {es-app => es-core}/src/guis/GuiInfoPopup.h | 0 es-core/src/guis/GuiMsgBox.cpp | 113 ++- es-core/src/guis/GuiMsgBox.h | 31 +- es-core/src/guis/GuiTextEditPopupKeyboard.cpp | 306 ++++--- es-core/src/guis/GuiTextEditPopupKeyboard.h | 19 +- es-core/src/renderers/Renderer.cpp | 20 + es-core/src/renderers/Renderer.h | 2 + es-core/src/resources/TextureResource.cpp | 6 + es-core/src/resources/TextureResource.h | 2 + resources/alert.svg | 47 + resources/glyphs.ttf | Bin 1904 -> 122092 bytes resources/info.svg | 51 ++ resources/question.svg | 50 ++ resources/search.svg | 8 + resources/warning.svg | 49 ++ 101 files changed, 3560 insertions(+), 1693 deletions(-) create mode 100644 es-app/src/scrapers/ThreadedScraper.cpp create mode 100644 es-app/src/scrapers/ThreadedScraper.h create mode 100644 es-core/src/components/AsyncNotificationComponent.cpp create mode 100644 es-core/src/components/AsyncNotificationComponent.h rename {es-app => es-core}/src/guis/GuiInfoPopup.cpp (100%) rename {es-app => es-core}/src/guis/GuiInfoPopup.h (100%) create mode 100644 resources/alert.svg create mode 100644 resources/info.svg create mode 100644 resources/question.svg create mode 100644 resources/search.svg create mode 100644 resources/warning.svg diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 69df29f9cd..0873517c2e 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -34,14 +34,14 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.h # Scrapers ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.h ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBJSONScraper.h ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBJSONScraperResources.h ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ScreenScraper.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ThreadedScraper.h # Views ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/BasicGameListView.h @@ -92,14 +92,14 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp - ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp # Scrapers ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBJSONScraper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBJSONScraperResources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ScreenScraper.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ThreadedScraper.cpp # Views ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/BasicGameListView.cpp diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 3bdc5c7ade..cad9819230 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -177,8 +177,12 @@ void CollectionSystemManager::updateSystemsList() { // remove all Collection Systems removeCollectionsFromDisplayedSystems(); + + std::unordered_map map; + getAllGamesCollection()->getRootFolder()->createChildrenByFilenameMap(map); + // add custom enabled ones - addEnabledCollectionsToDisplayedSystems(&mCustomCollectionSystemsData); + addEnabledCollectionsToDisplayedSystems(&mCustomCollectionSystemsData, &map); if (Settings::getInstance()->getBool("SortAllSystems")) { @@ -209,7 +213,7 @@ void CollectionSystemManager::updateSystemsList() } // add auto enabled ones - addEnabledCollectionsToDisplayedSystems(&mAutoCollectionSystemsData); + addEnabledCollectionsToDisplayedSystems(&mAutoCollectionSystemsData, &map); /* // create views for collections, before reload for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) @@ -292,6 +296,9 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS ViewController::get()->getGameListView(curSys)->onFileChanged(newGame, FILE_METADATA_CHANGED); } } + + curSys->updateDisplayedGameCount(); + rootFolder->sort(getSortTypeFromString(mCollectionSystemDeclsIndex[name].defaultSort)); if (name == "recent") { @@ -305,11 +312,17 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS void CollectionSystemManager::trimCollectionCount(FolderData* rootFolder, int limit) { - SystemData* curSys = rootFolder->getSystem(); - while ((int)rootFolder->getChildrenListToDisplay().size() > limit) - { - CollectionFileData* gameToRemove = (CollectionFileData*)rootFolder->getChildrenListToDisplay().back(); - ViewController::get()->getGameListView(curSys).get()->remove(gameToRemove, false); + SystemData* curSys = rootFolder->getSystem(); + std::shared_ptr listView = ViewController::get()->getGameListView(curSys, false); + + auto& childs = rootFolder->getChildren(); + while ((int)childs.size() > limit) + { + CollectionFileData* gameToRemove = (CollectionFileData*)childs.back(); + if (listView == nullptr) + delete gameToRemove; + else + listView.get()->remove(gameToRemove, false); } } @@ -449,14 +462,16 @@ void CollectionSystemManager::setEditMode(std::string collectionName) // if it's bundled, this needs to be the bundle system mEditingCollectionSystemData = sysData; - GuiInfoPopup* s = new GuiInfoPopup(mWindow, "Editing the '" + Utils::String::toUpper(collectionName) + "' Collection. Add/remove games with Y.", 10000); - mWindow->setInfoPopup(s); + char strbuf[512]; + snprintf(strbuf, 512, _("Editing the '%s' Collection. Add/remove games with Y.").c_str(), Utils::String::toUpper(collectionName).c_str()); + mWindow->displayNotificationMessage(strbuf, 10000); } void CollectionSystemManager::exitEditMode() { - GuiInfoPopup* s = new GuiInfoPopup(mWindow, "Finished editing the '" + mEditingCollection + "' Collection.", 4000); - mWindow->setInfoPopup(s); + char strbuf[512]; + snprintf(strbuf, 512, _("Finished editing the '%s' Collection.").c_str(), mEditingCollection.c_str()); + mWindow->displayNotificationMessage(strbuf, 10000); mIsEditingCustom = false; mEditingCollection = "Favorites"; } @@ -540,15 +555,16 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(file, FILE_METADATA_CHANGED); } } - if (adding) - { - s = new GuiInfoPopup(mWindow, "Added '" + Utils::String::removeParenthesis(name) + "' to '" + Utils::String::toUpper(sysName) + "'", 4000); - } - else - { - s = new GuiInfoPopup(mWindow, "Removed '" + Utils::String::removeParenthesis(name) + "' from '" + Utils::String::toUpper(sysName) + "'", 4000); - } - mWindow->setInfoPopup(s); + + char trstring[512]; + + if (adding) + snprintf(trstring, 512, _("Added '%s' to '%s'").c_str(), Utils::String::removeParenthesis(name).c_str(), Utils::String::toUpper(sysName).c_str()); // batocera + else + snprintf(trstring, 512, _("Removed '%s' from '%s'").c_str(), Utils::String::removeParenthesis(name).c_str(), Utils::String::toUpper(sysName).c_str()); // batocera + + mWindow->displayNotificationMessage(trstring, 4000); + return true; } return false; @@ -836,11 +852,8 @@ void CollectionSystemManager::removeCollectionsFromDisplayedSystems() ViewController::get()->removeGameListView(mCustomCollectionsBundle); } -void CollectionSystemManager::addEnabledCollectionsToDisplayedSystems(std::map* colSystemData) +void CollectionSystemManager::addEnabledCollectionsToDisplayedSystems(std::map* colSystemData, std::unordered_map* pMap) { - std::unordered_map map; - getAllGamesCollection()->getRootFolder()->createChildrenByFilenameMap(map); - // add auto enabled ones for(std::map::iterator it = colSystemData->begin() ; it != colSystemData->end() ; it++ ) { @@ -851,7 +864,7 @@ void CollectionSystemManager::addEnabledCollectionsToDisplayedSystems(std::mapsecond.decl.isCustom) { - populateCustomCollection(&(it->second), &map); + populateCustomCollection(&(it->second), pMap); } else { diff --git a/es-app/src/CollectionSystemManager.h b/es-app/src/CollectionSystemManager.h index 31ada3b25d..69c48fce62 100644 --- a/es-app/src/CollectionSystemManager.h +++ b/es-app/src/CollectionSystemManager.h @@ -97,7 +97,7 @@ class CollectionSystemManager void populateCustomCollection(CollectionSystemData* sysData, std::unordered_map* pMap = nullptr); void removeCollectionsFromDisplayedSystems(); - void addEnabledCollectionsToDisplayedSystems(std::map* colSystemData); + void addEnabledCollectionsToDisplayedSystems(std::map* colSystemData, std::unordered_map* pMap); std::vector getSystemsFromConfig(); std::vector getSystemsFromTheme(); diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index 39119ca251..4581cf9238 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -6,15 +6,15 @@ // Do this version number update as the very last commit for the new release version. #define PROGRAM_VERSION_MAJOR 2 #define PROGRAM_VERSION_MINOR 10 -#define PROGRAM_VERSION_MAINTENANCE 0 -#define PROGRAM_VERSION_BUILD 7 +#define PROGRAM_VERSION_MAINTENANCE 1 +#define PROGRAM_VERSION_BUILD 1 -#define PROGRAM_VERSION_STRING "2.10.0.7 FCAMOD" -#define SCRAPER_VERSION_STRING "2.10.0.7" +#define PROGRAM_VERSION_STRING "2.10.1.1 FCAMOD" +#define SCRAPER_VERSION_STRING "2.10.1.1" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ -#define RESOURCE_VERSION_STRING "2,10,0,7\0" +#define RESOURCE_VERSION_STRING "2,10,1,1\0" #define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE,PROGRAM_VERSION_BUILD #endif // ES_APP_EMULATION_STATION_H diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 14ad47c594..5f887ec819 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -386,6 +386,7 @@ const std::vector FolderData::getChildrenListToDisplay() { std::vector ret; + bool flatFolders = Settings::getInstance()->getBool("FlatFolders"); bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); bool filterKidGame = false; @@ -400,42 +401,59 @@ const std::vector FolderData::getChildrenListToDisplay() auto sys = CollectionSystemManager::get()->getSystemToView(mSystem); - FileFilterIndex* idx = sys->getIndex(); - if (idx != nullptr && idx->isFiltered()) + FileFilterIndex* idx = sys->getIndex(false); + if (idx != nullptr && !idx->isFiltered()) + idx = nullptr; + + std::vector* items = &mChildren; + + std::vector flatGameList; + if (flatFolders) { - for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if (idx->showFile((*it))) { - ret.push_back(*it); - } - } + flatGameList = getFlatGameList(false, sys); + items = &flatGameList; } - else - { - bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); - if (showHiddenFiles) - return mChildren; - - for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if (!showHiddenFiles && (*it)->getHidden()) - continue; - if (filterKidGame && !(*it)->getKidGame()) - continue; + for (auto it = items->cbegin(); it != items->cend(); it++) + { + if (idx != nullptr && !idx->showFile((*it))) + continue; - ret.push_back(*it); - } + if (!showHiddenFiles && (*it)->getHidden()) + continue; + + if (filterKidGame && !(*it)->getKidGame()) + continue; + + ret.push_back(*it); } return ret; } -std::vector FolderData::getFilesRecursive(unsigned int typeMask, bool displayedOnly) const +std::vector FolderData::getFlatGameList(bool displayedOnly, SystemData* system) const +{ + std::vector ret = getFilesRecursive(GAME, displayedOnly, system); + + unsigned int currentSortId = system->getSortId(); + if (currentSortId < 0 || currentSortId >FileSorts::SortTypes.size()) + currentSortId = 0; + + auto sort = FileSorts::SortTypes.at(currentSortId); + + std::stable_sort(ret.begin(), ret.end(), sort.comparisonFunction); + + if (!sort.ascending) + std::reverse(ret.begin(), ret.end()); + + return ret; +} + +std::vector FolderData::getFilesRecursive(unsigned int typeMask, bool displayedOnly, SystemData* system) const { std::vector out; - FileFilterIndex* idx = mSystem->getIndex(); + FileFilterIndex* idx = (system != nullptr ? system : mSystem)->getIndex(false); for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) { @@ -451,7 +469,7 @@ std::vector FolderData::getFilesRecursive(unsigned int typeMask, bool FolderData* folder = (FolderData*)(*it); if (folder->getChildren().size() > 0) { - std::vector subchildren = folder->getFilesRecursive(typeMask, displayedOnly); + std::vector subchildren = folder->getFilesRecursive(typeMask, displayedOnly, system); out.insert(out.cend(), subchildren.cbegin(), subchildren.cend()); } } diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 8d36a459d7..1eef06fdbd 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -144,7 +144,7 @@ class FolderData : public FileData inline const std::vector& getChildren() const { return mChildren; } const std::vector getChildrenListToDisplay(); - std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false) const; + std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false, SystemData* system = nullptr) const; void addChild(FileData* file); // Error if mType != FOLDER void removeChild(FileData* file); //Error if mType != FOLDER @@ -152,6 +152,8 @@ class FolderData : public FileData void createChildrenByFilenameMap(std::unordered_map& map); private: + std::vector FolderData::getFlatGameList(bool displayedOnly, SystemData* system) const; + std::vector mChildren; }; diff --git a/es-app/src/FileFilterIndex.cpp b/es-app/src/FileFilterIndex.cpp index 67ef44ceb9..dfa27cafa7 100644 --- a/es-app/src/FileFilterIndex.cpp +++ b/es-app/src/FileFilterIndex.cpp @@ -21,7 +21,7 @@ FileFilterIndex::FileFilterIndex() { PUBDEV_FILTER, &pubDevIndexAllKeys, &filterByPubDev, &pubDevIndexFilteredKeys, "developer", true, "publisher", "PUBLISHER / DEVELOPER" }, { RATINGS_FILTER, &ratingsIndexAllKeys, &filterByRatings, &ratingsIndexFilteredKeys, "rating", false, "", "RATING" }, { KIDGAME_FILTER, &kidGameIndexAllKeys, &filterByKidGame, &kidGameIndexFilteredKeys, "kidgame", false, "", "KIDGAME" }, - { HIDDEN_FILTER, &hiddenIndexAllKeys, &filterByHidden, &hiddenIndexFilteredKeys, "hidden", false, "", "HIDDEN" } + // { HIDDEN_FILTER, &hiddenIndexAllKeys, &filterByHidden, &hiddenIndexFilteredKeys, "hidden", false, "", "HIDDEN" } }; filterDataDecl = std::vector(filterDecls, filterDecls + sizeof(filterDecls) / sizeof(filterDecls[0])); @@ -51,7 +51,7 @@ void FileFilterIndex::importIndex(FileFilterIndex* indexToImport) { &pubDevIndexAllKeys, &(indexToImport->pubDevIndexAllKeys) }, { &ratingsIndexAllKeys, &(indexToImport->ratingsIndexAllKeys) }, { &favoritesIndexAllKeys, &(indexToImport->favoritesIndexAllKeys) }, - { &hiddenIndexAllKeys, &(indexToImport->hiddenIndexAllKeys) }, + // { &hiddenIndexAllKeys, &(indexToImport->hiddenIndexAllKeys) }, { &kidGameIndexAllKeys, &(indexToImport->kidGameIndexAllKeys) }, }; @@ -81,7 +81,7 @@ void FileFilterIndex::resetIndex() clearIndex(pubDevIndexAllKeys); clearIndex(ratingsIndexAllKeys); clearIndex(favoritesIndexAllKeys); - clearIndex(hiddenIndexAllKeys); + // clearIndex(hiddenIndexAllKeys); clearIndex(kidGameIndexAllKeys); } @@ -186,7 +186,7 @@ void FileFilterIndex::addToIndex(FileData* game) managePubDevEntryInIndex(game); manageRatingsEntryInIndex(game); manageFavoritesEntryInIndex(game); - manageHiddenEntryInIndex(game); + //manageHiddenEntryInIndex(game); manageKidGameEntryInIndex(game); } @@ -197,7 +197,7 @@ void FileFilterIndex::removeFromIndex(FileData* game) managePubDevEntryInIndex(game, true); manageRatingsEntryInIndex(game, true); manageFavoritesEntryInIndex(game, true); - manageHiddenEntryInIndex(game, true); + //manageHiddenEntryInIndex(game, true); manageKidGameEntryInIndex(game, true); } @@ -281,14 +281,19 @@ void FileFilterIndex::debugPrintIndexes() for (auto x: favoritesIndexAllKeys) { LOG(LogInfo) << "Favorites Index: " << x.first << ": " << x.second; } - for (auto x : hiddenIndexAllKeys) { - LOG(LogInfo) << "Hidden Index: " << x.first << ": " << x.second; - } +// for (auto x : hiddenIndexAllKeys) { +// LOG(LogInfo) << "Hidden Index: " << x.first << ": " << x.second; +// } for (auto x : kidGameIndexAllKeys) { LOG(LogInfo) << "KidGames Index: " << x.first << ": " << x.second; } } +void FileFilterIndex::setTextFilter(const std::string text) +{ + mTextFilter = Utils::String::toUpper(text); +} + bool FileFilterIndex::showFile(FileData* game) { // this shouldn't happen, but just in case let's get it out of the way @@ -304,15 +309,16 @@ bool FileFilterIndex::showFile(FileData* game) for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it ) { if (showFile(*it)) - { return true; - } } return false; } bool keepGoing = false; + if (!mTextFilter.empty() && Utils::String::toUpper(game->getName()).find(mTextFilter) != std::string::npos) + keepGoing = true; + for (std::vector::const_iterator it = filterDataDecl.cbegin(); it != filterDataDecl.cend(); ++it ) { FilterDataDecl filterData = (*it); if(*(filterData.filteredByRef)) @@ -347,10 +353,10 @@ bool FileFilterIndex::showFile(FileData* game) bool FileFilterIndex::isKeyBeingFilteredBy(std::string key, FilterIndexType type) { - const FilterIndexType filterTypes[7] = { FAVORITES_FILTER, GENRE_FILTER, PLAYER_FILTER, PUBDEV_FILTER, RATINGS_FILTER,HIDDEN_FILTER, KIDGAME_FILTER }; - std::vector filterKeysList[7] = { favoritesIndexFilteredKeys, genreIndexFilteredKeys, playersIndexFilteredKeys, pubDevIndexFilteredKeys, ratingsIndexFilteredKeys, hiddenIndexFilteredKeys, kidGameIndexFilteredKeys }; + const FilterIndexType filterTypes[6] = { FAVORITES_FILTER, GENRE_FILTER, PLAYER_FILTER, PUBDEV_FILTER, RATINGS_FILTER, KIDGAME_FILTER }; // ,HIDDEN_FILTER + std::vector filterKeysList[6] = { favoritesIndexFilteredKeys, genreIndexFilteredKeys, playersIndexFilteredKeys, pubDevIndexFilteredKeys, ratingsIndexFilteredKeys, kidGameIndexFilteredKeys }; // hiddenIndexFilteredKeys, - for (int i = 0; i < 7; i++) + for (int i = 0; i < 6; i++) { if (filterTypes[i] == type) { @@ -472,7 +478,7 @@ void FileFilterIndex::manageFavoritesEntryInIndex(FileData* game, bool remove) manageIndexEntry(&favoritesIndexAllKeys, key, remove); } - +/* void FileFilterIndex::manageHiddenEntryInIndex(FileData* game, bool remove) { // flag for including unknowns @@ -485,7 +491,7 @@ void FileFilterIndex::manageHiddenEntryInIndex(FileData* game, bool remove) manageIndexEntry(&hiddenIndexAllKeys, key, remove); } - +*/ void FileFilterIndex::manageKidGameEntryInIndex(FileData* game, bool remove) { // flag for including unknowns diff --git a/es-app/src/FileFilterIndex.h b/es-app/src/FileFilterIndex.h index 93b9c8d9ff..132a0a533b 100644 --- a/es-app/src/FileFilterIndex.h +++ b/es-app/src/FileFilterIndex.h @@ -42,7 +42,7 @@ class FileFilterIndex void clearAllFilters(); void debugPrintIndexes(); bool showFile(FileData* game); - bool isFiltered() { return (filterByGenre || filterByPlayers || filterByPubDev || filterByRatings || filterByFavorites || filterByHidden || filterByKidGame); }; + bool isFiltered() { return (!mTextFilter.empty() || filterByGenre || filterByPlayers || filterByPubDev || filterByRatings || filterByFavorites || filterByHidden || filterByKidGame); }; bool isKeyBeingFilteredBy(std::string key, FilterIndexType type); std::vector& getFilterDataDecls(); @@ -51,6 +51,9 @@ class FileFilterIndex void resetFilters(); void setUIModeFilters(); + void setTextFilter(const std::string text); + inline const std::string getTextFilter() { return mTextFilter; } + private: std::vector filterDataDecl; std::string getIndexableKey(FileData* game, FilterIndexType type, bool getSecondary); @@ -60,7 +63,7 @@ class FileFilterIndex void managePubDevEntryInIndex(FileData* game, bool remove = false); void manageRatingsEntryInIndex(FileData* game, bool remove = false); void manageFavoritesEntryInIndex(FileData* game, bool remove = false); - void manageHiddenEntryInIndex(FileData* game, bool remove = false); + //void manageHiddenEntryInIndex(FileData* game, bool remove = false); void manageKidGameEntryInIndex(FileData* game, bool remove = false); void manageIndexEntry(std::map* index, std::string key, bool remove); @@ -80,7 +83,7 @@ class FileFilterIndex std::map pubDevIndexAllKeys; std::map ratingsIndexAllKeys; std::map favoritesIndexAllKeys; - std::map hiddenIndexAllKeys; + //std::map hiddenIndexAllKeys; std::map kidGameIndexAllKeys; std::vector genreIndexFilteredKeys; @@ -88,11 +91,11 @@ class FileFilterIndex std::vector pubDevIndexFilteredKeys; std::vector ratingsIndexFilteredKeys; std::vector favoritesIndexFilteredKeys; - std::vector hiddenIndexFilteredKeys; + //std::vector hiddenIndexFilteredKeys; std::vector kidGameIndexFilteredKeys; FileData* mRootFolder; - + std::string mTextFilter; }; #endif // ES_APP_FILE_FILTER_INDEX_H diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index 25c3c6a66f..012ecb63e2 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -41,8 +41,9 @@ namespace FileSorts //returns if file1 should come before file2 bool compareName(const FileData* file1, const FileData* file2) { - std::string name1 = file1->metadata.getName(); - return name1.compare(file2->metadata.getName()) < 0; + std::string name1 = Utils::String::toUpper(file1->metadata.getName()); + std::string name2 = Utils::String::toUpper(file2->metadata.getName()); + return name1.compare(name2) < 0; } bool compareRating(const FileData* file1, const FileData* file2) diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index c0b8237ac4..10831f6b6d 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -5,14 +5,15 @@ #include "Log.h" #include #include "SystemData.h" +#include "Settings.h" MetaDataDecl gameDecls[] = { // key, type, default, statistic, name in GuiMetaDataEd, prompt in GuiMetaDataEd { 0, "name", MD_STRING, "", false, "name", "enter game name"}, // { 1, "sortname", MD_STRING, "", false, "sortname", "enter game sort name"}, { 2, "desc", MD_MULTILINE_STRING, "", false, "description", "enter description"}, - { 3, "emulator", MD_PLIST, "", false, "emulator", "emulator" }, - { 4, "core", MD_PLIST, "", false, "core", "core" }, + { 3, "emulator", MD_PLIST, "", false, "emulator", "emulator" }, + { 4, "core", MD_PLIST, "", false, "core", "core" }, { 5, "image", MD_PATH, "", false, "image", "enter path to image"}, { 6, "video", MD_PATH , "", false, "video", "enter path to video"}, { 7, "marquee", MD_PATH, "", false, "marquee", "enter path to marquee"}, @@ -263,3 +264,43 @@ void MetaDataList::resetChangedFlag() { mWasChanged = false; } + +void MetaDataList::importScrappedMetadata(const MetaDataList& source) +{ + int type = MetaDataImportType::Types::ALL; + + if (Settings::getInstance()->getString("Scraper") == "ScreenScraper") + { + if (Settings::getInstance()->getString("ScrapperImageSrc").empty()) + type &= ~MetaDataImportType::Types::IMAGE; + + if (!Settings::getInstance()->getString("ScrapperThumbSrc").empty()) + type &= ~MetaDataImportType::Types::THUMB; + + if (!Settings::getInstance()->getBool("ScrapeVideos")) + type &= ~MetaDataImportType::Types::VIDEO; + + if (!Settings::getInstance()->getBool("ScrapeMarquee")) + type &= ~MetaDataImportType::Types::MARQUEE; + } + + for (auto mdd : getMDD()) + { + if (mdd.key == "favorite" || mdd.key == "playcount" || mdd.key == "lastplayed") + continue; + + if (mdd.key == "image" && (type & MetaDataImportType::Types::IMAGE) != MetaDataImportType::Types::IMAGE) + continue; + + if (mdd.key == "thumbnail" && (type & MetaDataImportType::Types::THUMB) != MetaDataImportType::Types::THUMB) + continue; + + if (mdd.key == "marquee" && (type & MetaDataImportType::Types::MARQUEE) != MetaDataImportType::Types::MARQUEE) + continue; + + if (mdd.key == "video" && (type & MetaDataImportType::Types::VIDEO) != MetaDataImportType::Types::VIDEO) + continue; + + set(mdd.key, source.get(mdd.key)); + } +} \ No newline at end of file diff --git a/es-app/src/MetaData.h b/es-app/src/MetaData.h index a4dde15bec..95844d04e3 100644 --- a/es-app/src/MetaData.h +++ b/es-app/src/MetaData.h @@ -26,6 +26,18 @@ enum MetaDataType MD_PLIST }; +namespace MetaDataImportType +{ + enum Types : int + { + IMAGE = 1, + THUMB = 2, + VIDEO = 4, + MARQUEE = 8, + ALL = IMAGE | THUMB | VIDEO | MARQUEE + }; +} + struct MetaDataDecl { unsigned char id; @@ -67,6 +79,8 @@ class MetaDataList inline const std::vector& getMDD() const { return getMDDByType(getType()); } const std::string& getName() const; + void importScrappedMetadata(const MetaDataList& source); + private: std::string mName; unsigned char mType; diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index afd8e129c2..c0c3bc1cd1 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -23,7 +23,9 @@ std::vector SystemData::sSystemVector; SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) { - mSortId = 0; + mGameCount = -1; + mSortId = Settings::getInstance()->getInt(getName() + ".sort"), + mGridSizeOverride = Vector2f(0, 0); mViewModeChanged = false; mFilterIndex = nullptr;// new FileFilterIndex(); @@ -48,8 +50,10 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys refactorGameFolders(this); - //StopWatch ws("sort " + mName); - mRootFolder->sort(FileSorts::SortTypes.at(0)); + if (mSortId >= 0 && mSortId < FileSorts::SortTypes.size()) + mRootFolder->sort(FileSorts::SortTypes.at(mSortId)); + else + mRootFolder->sort(FileSorts::SortTypes.at(0)); //indexAllGameFilters(mRootFolder); } @@ -63,8 +67,6 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys auto gridSizeOverride = Vector2f::parseString(Settings::getInstance()->getString(getName() + ".gridSize")); setSystemViewMode(defaultView, gridSizeOverride, false); - mSortId = Settings::getInstance()->getInt(getName() + ".sort"), - setIsGameSystemStatus(); loadTheme(); } @@ -700,9 +702,17 @@ FileData* SystemData::getRandomGame() return list.at(target); } -unsigned int SystemData::getDisplayedGameCount() const +int SystemData::getDisplayedGameCount() +{ + if (mGameCount < 0) + mGameCount = mRootFolder->getFilesRecursive(GAME, true).size(); + + return mGameCount; +} + +void SystemData::updateDisplayedGameCount() { - return (unsigned int)mRootFolder->getFilesRecursive(GAME, true).size(); + mGameCount =-1; } void SystemData::loadTheme() @@ -736,4 +746,13 @@ void SystemData::setSortId(const unsigned int sortId) { mSortId = sortId; Settings::getInstance()->setInt(getName() + ".sort", mSortId); -} \ No newline at end of file +} + +void SystemData::deleteIndex() +{ + if (mFilterIndex != nullptr) + { + delete mFilterIndex; + mFilterIndex = nullptr; + } +} diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index b80c8d2586..e0853465f9 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -110,7 +110,9 @@ class SystemData std::string getThemePath() const; unsigned int getGameCount() const; - unsigned int getDisplayedGameCount() const; + + int getDisplayedGameCount(); + void updateDisplayedGameCount(); static void deleteSystems(); static bool loadConfig(Window* window); //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. @@ -152,11 +154,12 @@ class SystemData if (mFilterIndex != nullptr) mFilterIndex->resetIndex(); }; - void setUIModeFilters() { if (mFilterIndex != nullptr) mFilterIndex->setUIModeFilters(); } + void deleteIndex(); + unsigned int getSortId() const { return mSortId; }; void setSortId(const unsigned int sortId = 0); @@ -184,6 +187,7 @@ class SystemData FileFilterIndex* mFilterIndex; FolderData* mRootFolder; + int mGameCount; }; #endif // ES_APP_SYSTEM_DATA_H diff --git a/es-app/src/VolumeControl.cpp b/es-app/src/VolumeControl.cpp index 9153523ed8..703c9916b5 100644 --- a/es-app/src/VolumeControl.cpp +++ b/es-app/src/VolumeControl.cpp @@ -8,12 +8,12 @@ #endif #if defined(__linux__) - #if defined(_RPI_) || defined(_VERO4K_) - const char * VolumeControl::mixerName = "PCM"; - #else - const char * VolumeControl::mixerName = "Master"; - #endif - const char * VolumeControl::mixerCard = "default"; +#if defined(_RPI_) || defined(_VERO4K_) +const char * VolumeControl::mixerName = "PCM"; +#else +const char * VolumeControl::mixerName = "Master"; +#endif +const char * VolumeControl::mixerCard = "default"; #endif std::weak_ptr VolumeControl::sInstance; @@ -22,7 +22,7 @@ std::weak_ptr VolumeControl::sInstance; VolumeControl::VolumeControl() : originalVolume(0), internalVolume(0) #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) , mixerIndex(0), mixerHandle(nullptr), mixerElem(nullptr), mixerSelemId(nullptr) #elif defined(WIN32) || defined(_WIN32) @@ -35,10 +35,10 @@ VolumeControl::VolumeControl() originalVolume = getVolume(); } -VolumeControl::VolumeControl(const VolumeControl & right): +VolumeControl::VolumeControl(const VolumeControl & right) : originalVolume(0), internalVolume(0) #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) , mixerIndex(0), mixerHandle(nullptr), mixerElem(nullptr), mixerSelemId(nullptr) #elif defined(WIN32) || defined(_WIN32) @@ -81,7 +81,7 @@ void VolumeControl::init() { //initialize audio mixer interface #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) //try to open mixer device if (mixerHandle == nullptr) @@ -152,10 +152,10 @@ void VolumeControl::init() } #elif defined(WIN32) || defined(_WIN32) //get windows version information - OSVERSIONINFOEXA osVer = {sizeof(OSVERSIONINFO)}; + OSVERSIONINFOEXA osVer = { sizeof(OSVERSIONINFO) }; ::GetVersionExA(reinterpret_cast(&osVer)); //check windows version - if(osVer.dwMajorVersion < 6) + if (osVer.dwMajorVersion < 6) { //Windows older than Vista. use mixer API. open default mixer if (mixerHandle == nullptr) @@ -229,7 +229,7 @@ void VolumeControl::deinit() { //deinitialize audio mixer interface #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) if (mixerHandle != nullptr) { snd_mixer_detach(mixerHandle, mixerCard); @@ -256,7 +256,7 @@ int VolumeControl::getVolume() const int volume = 0; #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) if (mixerElem != nullptr) { @@ -350,7 +350,7 @@ void VolumeControl::setVolume(int volume) //store values in internal variables internalVolume = volume; #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) if (mixerElem != nullptr) { diff --git a/es-app/src/VolumeControl.h b/es-app/src/VolumeControl.h index a2e420e7e2..e4135f59f2 100644 --- a/es-app/src/VolumeControl.h +++ b/es-app/src/VolumeControl.h @@ -5,15 +5,15 @@ #include #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#error TODO: Not implemented for MacOS yet!!! #elif defined(__linux__) - #include - #include - #include +#include +#include +#include #elif defined(WIN32) || defined(_WIN32) - #include - #include - #include +#include +#include +#include #endif /*! @@ -22,14 +22,14 @@ Singleton pattern. Call getInstance() to get an object. class VolumeControl { #if defined (__APPLE__) - #error TODO: Not implemented for MacOS yet!!! +#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; + 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; @@ -43,7 +43,7 @@ class VolumeControl VolumeControl(); VolumeControl(const VolumeControl & right); - VolumeControl & operator=(const VolumeControl & right); + VolumeControl & operator=(const VolumeControl & right); public: static std::shared_ptr & getInstance(); diff --git a/es-app/src/components/RatingComponent.cpp b/es-app/src/components/RatingComponent.cpp index 0b0c0bbacc..d7048cb334 100644 --- a/es-app/src/components/RatingComponent.cpp +++ b/es-app/src/components/RatingComponent.cpp @@ -106,7 +106,7 @@ void RatingComponent::updateColors() void RatingComponent::render(const Transform4x4f& parentTrans) { - if (!isVisible()) + if (!isVisible() || mFilledTexture == nullptr || mUnfilledTexture == nullptr) return; Transform4x4f trans = parentTrans * getTransform(); @@ -118,11 +118,17 @@ void RatingComponent::render(const Transform4x4f& parentTrans) Renderer::setMatrix(trans); - mFilledTexture->bind(); - Renderer::drawTriangleStrips(&mVertices[0], 4); + if (mFilledTexture->bind()) + { + Renderer::drawTriangleStrips(&mVertices[0], 4); + Renderer::bindTexture(0); + } - mUnfilledTexture->bind(); - Renderer::drawTriangleStrips(&mVertices[4], 4); + if (mUnfilledTexture->bind()) + { + Renderer::drawTriangleStrips(&mVertices[4], 4); + Renderer::bindTexture(0); + } renderChildren(trans); } diff --git a/es-app/src/components/ScraperSearchComponent.cpp b/es-app/src/components/ScraperSearchComponent.cpp index 56b2ed7212..362a0208f6 100644 --- a/es-app/src/components/ScraperSearchComponent.cpp +++ b/es-app/src/components/ScraperSearchComponent.cpp @@ -283,10 +283,10 @@ void ScraperSearchComponent::onSearchDone(const std::vector void ScraperSearchComponent::onSearchError(const std::string& error) { LOG(LogInfo) << "ScraperSearchComponent search error: " << error; - mWindow->pushGui(new GuiMsgBox(mWindow, Utils::String::toUpper(error), - "RETRY", std::bind(&ScraperSearchComponent::search, this, mLastSearch), - "SKIP", mSkipCallback, - "CANCEL", mCancelCallback)); + mWindow->pushGui(new GuiMsgBox(mWindow, _("AN ERROR HAS OCCURED") + " :\n" + Utils::String::toUpper(error), + _("RETRY"), std::bind(&ScraperSearchComponent::search, this, mLastSearch), // batocera + _("SKIP"), mSkipCallback, // batocera + _("CANCEL"), mCancelCallback, ICON_ERROR)); // batocera } int ScraperSearchComponent::getSelectedIndex() diff --git a/es-app/src/components/TextListComponent.h b/es-app/src/components/TextListComponent.h index 304ac6fafe..b034b8ffc7 100644 --- a/es-app/src/components/TextListComponent.h +++ b/es-app/src/components/TextListComponent.h @@ -390,7 +390,7 @@ void TextListComponent::applyTheme(const std::shared_ptr& theme, c if (elem->has("selectorColorEnd")) setSelectorColorEnd(elem->get("selectorColorEnd")); if (elem->has("selectorGradientType")) - setSelectorColorGradientHorizontal(!(elem->get("selectorGradientType").compare("horizontal"))); + setSelectorColorGradientHorizontal(elem->get("selectorGradientType").compare("horizontal")); if(elem->has("selectedColor")) setSelectedColor(elem->get("selectedColor")); if(elem->has("primaryColor")) diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index 5f3036d366..c1e17fdbe0 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -76,11 +76,7 @@ void GuiCollectionSystemsOptions::initializeMenu() sortAllSystemsSwitch = std::make_shared(mWindow); sortAllSystemsSwitch->setState(Settings::getInstance()->getBool("SortAllSystems")); mMenu.addWithLabel(_("SORT CUSTOM COLLECTIONS AND SYSTEMS"), sortAllSystemsSwitch); - - favoritesFirstSwitch = std::make_shared(mWindow); - favoritesFirstSwitch->setState(Settings::getInstance()->getBool("FavoritesFirst")); - mMenu.addWithLabel(_("DISPLAY FAVORITES FIRST IN GAMELIST"), favoritesFirstSwitch); - + toggleSystemNameInCollections = std::make_shared(mWindow); toggleSystemNameInCollections->setState(Settings::getInstance()->getBool("CollectionShowSystemInfo")); mMenu.addWithLabel(_("SHOW SYSTEM NAME IN COLLECTIONS"), toggleSystemNameInCollections); @@ -174,45 +170,28 @@ void GuiCollectionSystemsOptions::addSystemsToMenu() void GuiCollectionSystemsOptions::applySettings() { - std::string outAuto = Utils::String::vectorToCommaString(autoOptionList->getSelectedObjects()); - std::string prevAuto = Settings::getInstance()->getString("CollectionSystemsAuto"); - std::string outCustom = Utils::String::vectorToCommaString(customOptionList->getSelectedObjects()); - std::string prevCustom = Settings::getInstance()->getString("CollectionSystemsCustom"); - - bool outSort = sortAllSystemsSwitch->getState(); - bool prevSort = Settings::getInstance()->getBool("SortAllSystems"); - bool outBundle = bundleCustomCollections->getState(); - bool prevBundle = Settings::getInstance()->getBool("UseCustomCollectionsSystem"); - - bool prevShow = Settings::getInstance()->getBool("CollectionShowSystemInfo"); - bool outShow = toggleSystemNameInCollections->getState(); - - bool outFavoritesFirst = favoritesFirstSwitch->getState(); - bool prevFavoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); - - bool needUpdateSettings = prevAuto != outAuto || prevCustom != outCustom || outSort != prevSort || outBundle != prevBundle || outFavoritesFirst != prevFavoritesFirst || prevShow != outShow; - if (needUpdateSettings) - { - updateSettings(outAuto, outCustom); - } - + std::string newAutoSettings = Utils::String::vectorToCommaString(autoOptionList->getSelectedObjects()); + std::string newCustomSettings = Utils::String::vectorToCommaString(customOptionList->getSelectedObjects()); + updateSettings(newAutoSettings, newCustomSettings); delete this; } void GuiCollectionSystemsOptions::updateSettings(std::string newAutoSettings, std::string newCustomSettings) { - Settings::getInstance()->setString("CollectionSystemsAuto", newAutoSettings); - Settings::getInstance()->setString("CollectionSystemsCustom", newCustomSettings); - Settings::getInstance()->setBool("SortAllSystems", sortAllSystemsSwitch->getState()); - Settings::getInstance()->setBool("UseCustomCollectionsSystem", bundleCustomCollections->getState()); - Settings::getInstance()->setBool("CollectionShowSystemInfo", toggleSystemNameInCollections->getState()); - Settings::getInstance()->setBool("FavoritesFirst", favoritesFirstSwitch->getState()); - - Settings::getInstance()->saveFile(); - CollectionSystemManager::get()->loadEnabledListFromSettings(); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); + bool dirty = Settings::getInstance()->setString("CollectionSystemsAuto", newAutoSettings); + dirty |= Settings::getInstance()->setString("CollectionSystemsCustom", newCustomSettings); + dirty |= Settings::getInstance()->setBool("SortAllSystems", sortAllSystemsSwitch->getState()); + dirty |= Settings::getInstance()->setBool("UseCustomCollectionsSystem", bundleCustomCollections->getState()); + dirty |= Settings::getInstance()->setBool("CollectionShowSystemInfo", toggleSystemNameInCollections->getState()); + + if (dirty) + { + Settings::getInstance()->saveFile(); + CollectionSystemManager::get()->loadEnabledListFromSettings(); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(); + } } bool GuiCollectionSystemsOptions::input(InputConfig* config, Input input) diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.h b/es-app/src/guis/GuiCollectionSystemsOptions.h index 4757834bd1..6f789ce4fe 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.h +++ b/es-app/src/guis/GuiCollectionSystemsOptions.h @@ -29,8 +29,7 @@ class GuiCollectionSystemsOptions : public GuiComponent std::shared_ptr< OptionListComponent > autoOptionList; std::shared_ptr< OptionListComponent > customOptionList; - - std::shared_ptr favoritesFirstSwitch; + std::shared_ptr sortAllSystemsSwitch; std::shared_ptr bundleCustomCollections; std::shared_ptr toggleSystemNameInCollections; diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 76d74385c9..254327f1d0 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -10,8 +10,12 @@ #include "FileSorts.h" #include "GuiMetaDataEd.h" #include "SystemData.h" - +#include "components/SwitchComponent.h" #include "animations/LambdaAnimation.h" +#include "guis/GuiTextEditPopup.h" +#include "guis/GuiTextEditPopupKeyboard.h" +#include "guis/GuiMsgBox.h" +#include "scrapers/ThreadedScraper.h" std::vector GuiGamelistOptions::gridSizes { "automatic", @@ -68,78 +72,73 @@ std::vector GuiGamelistOptions::gridSizes { }; GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool showGridFeatures) : GuiComponent(window), - mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false) + mSystem(system), mMenu(window, "OPTIONS"), fromPlaceholder(false), mFiltersChanged(false), mReloadAll(false) { - mGridSize = NULL; + auto theme = ThemeData::getMenuTheme(); + + mGridSize = nullptr; addChild(&mMenu); + if (!Settings::getInstance()->getBool("ForceDisableFilters")) + addTextFilterToMenu(); + // check it's not a placeholder folder - if it is, only show "Filter Options" FileData* file = getGamelist()->getCursor(); fromPlaceholder = file->isPlaceHolder(); ComponentListRow row; - if (!fromPlaceholder) { + if (!fromPlaceholder) + { // jump to letter row.elements.clear(); - // define supported character range - // this range includes all numbers, capital letters, and most reasonable symbols - char startChar = '!'; - char endChar = '_'; + std::vector letters = getGamelist()->getEntriesLetters(); + if (!letters.empty()) + { + mJumpToLetterList = std::make_shared(mWindow, _("JUMP TO..."), false); // batocera + + char curChar = (char)toupper(getGamelist()->getCursor()->getName()[0]); - char curChar = (char)toupper(getGamelist()->getCursor()->getName()[0]); - if(curChar < startChar || curChar > endChar) - curChar = startChar; + if (std::find(letters.begin(), letters.end(), std::string(1, curChar)) == letters.end()) + curChar = letters.at(0)[0]; - mJumpToLetterList = std::make_shared(mWindow, _("JUMP TO LETTER"), false); - for (char c = startChar; c <= endChar; c++) - { - // check if c is a valid first letter in current list - const std::vector& files = getGamelist()->getCursor()->getParent()->getChildrenListToDisplay(); - for (auto file : files) + for (auto letter : letters) + mJumpToLetterList->add(letter, letter[0], letter[0] == curChar); + + row.addElement(std::make_shared(mWindow, _("JUMP TO..."), theme->Text.font, theme->Text.color), true); // batocera + row.addElement(mJumpToLetterList, false); + row.input_handler = [&](InputConfig* config, Input input) { - char candidate = (char)toupper(file->getName()[0]); - if (c == candidate) + if (config->isMappedTo("a", input) && input.value) { - mJumpToLetterList->add(std::string(1, c), c, c == curChar); - break; + jumpToLetter(); + return true; } - } - } - - row.addElement(std::make_shared(mWindow, _("JUMP TO LETTER"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); - row.addElement(mJumpToLetterList, false); - row.input_handler = [&](InputConfig* config, Input input) - { - if(config->isMappedTo("a", input) && input.value) - { - jumpToLetter(); - return true; - } - else if(mJumpToLetterList->input(config, input)) - { - return true; - } - return false; - }; - mMenu.addRow(row); - - // sort list by - unsigned int currentSortId = mSystem->getSortId(); - if (currentSortId > FileSorts::SortTypes.size()) { - currentSortId = 0; + else if (mJumpToLetterList->input(config, input)) + { + return true; + } + return false; + }; + mMenu.addRow(row); } + } - mListSort = std::make_shared(mWindow, _("SORT GAMES BY"), false); - for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) - { - const FolderData::SortType& sort = FileSorts::SortTypes.at(i); - mListSort->add(_(Utils::String::toUpper(sort.description)), i, i == currentSortId); - } + // sort list by + unsigned int currentSortId = mSystem->getSortId(); + if (currentSortId > FileSorts::SortTypes.size()) { + currentSortId = 0; + } - mMenu.addWithLabel(_("SORT GAMES BY"), mListSort); + mListSort = std::make_shared(mWindow, _("SORT GAMES BY"), false); + for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) + { + const FolderData::SortType& sort = FileSorts::SortTypes.at(i); + mListSort->add(_(Utils::String::toUpper(sort.description)), i, i == currentSortId); } + mMenu.addWithLabel(_("SORT GAMES BY"), mListSort); + // GameList view style mViewMode = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); std::vector styles; @@ -202,29 +201,35 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool mMenu.addRow(row); } - - - /* - // maximum vram - auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); - max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel(_("VRAM LIMIT"), max_vram); - s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); - */ - - - - - - - - - - - - - - + // Show favorites first in gamelists + auto favoritesFirstSwitch = std::make_shared(mWindow); + favoritesFirstSwitch->setState(Settings::getInstance()->getBool("FavoritesFirst")); + mMenu.addWithLabel(_("SHOW FAVORITES ON TOP"), favoritesFirstSwitch); + addSaveFunc([favoritesFirstSwitch, this] + { + if (Settings::getInstance()->setBool("FavoritesFirst", favoritesFirstSwitch->getState())) + mReloadAll = true; + }); + + // hidden files + auto hidden_files = std::make_shared(mWindow); + hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); + mMenu.addWithLabel(_("SHOW HIDDEN FILES"), hidden_files); + addSaveFunc([hidden_files, this] + { + if (Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState())) + mReloadAll = true; + }); + + // Flat folders + auto flatFolders = std::make_shared(mWindow); + flatFolders->setState(!Settings::getInstance()->getBool("FlatFolders")); + mMenu.addWithLabel(_("SHOW FOLDERS"), flatFolders); // batocera + addSaveFunc([flatFolders, this] + { + if (Settings::getInstance()->setBool("FlatFolders", !flatFolders->getState())) + mReloadAll = true; + }); std::map customCollections = CollectionSystemManager::get()->getCustomCollectionSystems(); @@ -263,6 +268,12 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool GuiGamelistOptions::~GuiGamelistOptions() { + if (mSystem == nullptr) + return; + + for (auto it = mSaveFuncs.cbegin(); it != mSaveFuncs.cend(); it++) + (*it)(); + // apply sort if (!fromPlaceholder && mListSort->getSelected() != mSystem->getSortId()) { @@ -293,11 +304,17 @@ GuiGamelistOptions::~GuiGamelistOptions() } bool viewModeChanged = mSystem->setSystemViewMode(mViewMode->getSelected(), gridSizeOverride); - if (viewModeChanged || mFiltersChanged) - { - if (viewModeChanged) - Settings::getInstance()->saveFile(); + Settings::getInstance()->saveFile(); + + if (mReloadAll) + { + mWindow->renderLoadingScreen(_("Loading...")); + ViewController::get()->reloadAll(mWindow); + mWindow->endRenderLoadingScreen(); + } + else if (mFiltersChanged || viewModeChanged) + { // only reload full view if we came from a placeholder // as we need to re-display the remaining elements for whatever new // game is selected @@ -305,6 +322,63 @@ GuiGamelistOptions::~GuiGamelistOptions() } } +void GuiGamelistOptions::addTextFilterToMenu() +{ + auto theme = ThemeData::getMenuTheme(); + std::shared_ptr font = theme->Text.font; + unsigned int color = theme->Text.color; + + ComponentListRow row; + + auto lbl = std::make_shared(mWindow, _("FILTER GAMES BY TEXT"), font, color); + row.addElement(lbl, true); // label + + std::string searchText; + + auto idx = mSystem->getIndex(false); + if (idx != nullptr) + searchText = idx->getTextFilter(); + + mTextFilter = std::make_shared(mWindow, searchText, font, color, ALIGN_RIGHT); + row.addElement(mTextFilter, true); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); + row.addElement(spacer, false); + + auto bracket = std::make_shared(mWindow); + + auto searchIcon = theme->getMenuIcon("searchIcon"); + bracket->setImage(searchIcon.empty() ? ":/search.svg" : searchIcon); + + bracket->setResize(Vector2f(0, lbl->getFont()->getLetterHeight())); + row.addElement(bracket, false); + + auto updateVal = [this](const std::string& newVal) + { + mTextFilter->setValue(Utils::String::toUpper(newVal)); + + auto index = mSystem->getIndex(!newVal.empty()); + if (index != nullptr) + { + mFiltersChanged = true; + + index->setTextFilter(newVal); + if (!index->isFiltered()) + mSystem->deleteIndex(); + + delete this; + } + }; + + row.makeAcceptInputHandler([this, updateVal] + { + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, _("FILTER GAMES BY TEXT"), mTextFilter->getValue(), updateVal, false)); + }); + + mMenu.addRow(row); +} + void GuiGamelistOptions::openGamelistFilter() { mFiltersChanged = true; @@ -342,6 +416,12 @@ void GuiGamelistOptions::exitEditMode() void GuiGamelistOptions::openMetaDataEd() { + if (ThreadedScraper::isRunning()) + { + mWindow->pushGui(new GuiMsgBox(mWindow, _("THIS FUNCTION IS DISABLED WHEN SCRAPING IS RUNNING"))); + return; + } + // open metadata editor // get the FileData that hosts the original metadata FileData* file = getGamelist()->getCursor()->getSourceFileData(); diff --git a/es-app/src/guis/GuiGamelistOptions.h b/es-app/src/guis/GuiGamelistOptions.h index f12d160f78..379ecc1dfc 100644 --- a/es-app/src/guis/GuiGamelistOptions.h +++ b/es-app/src/guis/GuiGamelistOptions.h @@ -24,6 +24,9 @@ class GuiGamelistOptions : public GuiComponent static std::vector gridSizes; private: + inline void addSaveFunc(const std::function& func) { mSaveFuncs.push_back(func); }; + + void addTextFilterToMenu(); void openGamelistFilter(); void openMetaDataEd(); void startEditMode(); @@ -38,14 +41,17 @@ class GuiGamelistOptions : public GuiComponent typedef OptionListComponent SortList; std::shared_ptr mListSort; + std::shared_ptr mTextFilter; std::shared_ptr> mViewMode; - std::shared_ptr> mGridSize; SystemData* mSystem; IGameListView* getGamelist(); bool fromPlaceholder; bool mFiltersChanged; + + std::vector< std::function > mSaveFuncs; + bool mReloadAll; }; #endif // ES_APP_GUIS_GUI_GAME_LIST_OPTIONS_H diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 53eafa1651..18661a37e9 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -24,8 +24,10 @@ #include "GuiGamelistOptions.h" // grid sizes #include "platform.h" #include "renderers/Renderer.h" // setSwapInterval() +#include "guis/GuiTextEditPopupKeyboard.h" +#include "scrapers/ThreadedScraper.h" -GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _("MAIN MENU")), mVersion(window) +GuiMenu::GuiMenu(Window* window, bool animate) : GuiComponent(window), mMenu(window, _("MAIN MENU")), mVersion(window) { auto theme = ThemeData::getMenuTheme(); @@ -39,12 +41,10 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _("MAIN M addEntry(_("SOUND SETTINGS"), true, [this] { openSoundSettings(); }, "iconSound"); - if (isFullUI) - addEntry(_("SCRAPER"), true, [this] { openScraperSettings(); }, "iconScraper"); - if (isFullUI) { addEntry(_("GAME COLLECTION SETTINGS"), true, [this] { openCollectionSystemSettings(); }, "iconGames"); + addEntry(_("SCRAPER"), true, [this] { openScraperSettings(); }, "iconScraper"); addEntry(_("ADVANCED SETTINGS"), true, [this] { openOtherSettings(); }, "iconAdvanced"); } @@ -54,84 +54,134 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, _("MAIN M addVersionInfo(); setSize(mMenu.getSize()); - /* - setScale(Vector3f( - Renderer::getScreenWidth() / mMenu.getSize().x(), - Renderer::getScreenHeight() / mMenu.getSize().y(), - 1.0)); - */ - animateTo( - Vector2f((Renderer::getScreenWidth() - mSize.x()) / 2, Renderer::getScreenHeight() * 0.9), - Vector2f((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2)); + + if (animate) + animateTo( + Vector2f((Renderer::getScreenWidth() - mSize.x()) / 2, Renderer::getScreenHeight() * 0.9), + Vector2f((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2)); + else + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); } void GuiMenu::openScraperSettings() { auto s = new GuiSettings(mWindow, _("SCRAPER")); + std::string scraper = Settings::getInstance()->getString("Scraper"); + // scrape from auto scraper_list = std::make_shared< OptionListComponent< std::string > >(mWindow, _("SCRAPE FROM"), false); std::vector scrapers = getScraperList(); // Select either the first entry of the one read from the settings, just in case the scraper from settings has vanished. - for(auto it = scrapers.cbegin(); it != scrapers.cend(); it++) - scraper_list->add(*it, *it, *it == Settings::getInstance()->getString("Scraper")); + for (auto it = scrapers.cbegin(); it != scrapers.cend(); it++) + scraper_list->add(*it, *it, *it == 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(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()); }); - - // image source - std::string imageSourceName = Settings::getInstance()->getString("ScrapperImageSrc"); - auto imageSource = std::make_shared< OptionListComponent >(mWindow, _("PREFERED IMAGE SOURCE"), false); - imageSource->add(_("NONE"), "", imageSourceName.empty()); - imageSource->add(_("SCREENSHOT"), "ss", imageSourceName == "ss"); - imageSource->add(_("BOX 2D"), "box-2D", imageSourceName == "box-2D"); - imageSource->add(_("BOX 3D"), "box-3D", imageSourceName == "box-3D"); - imageSource->add(_("MIX"), "mixrbv1", imageSourceName == "mixrbv1"); - imageSource->add(_("WHEEL"), "wheel", imageSourceName == "wheel"); - s->addWithLabel("PREFERED IMAGE SOURCE", imageSource); - - s->addSaveFunc([imageSource] { - if (Settings::getInstance()->getString("ScrapperImageSrc") != imageSource->getSelected()) - Settings::getInstance()->setString("ScrapperImageSrc", imageSource->getSelected()); - }); + if (scraper == "ScreenScraper") + { + // image source + std::string imageSourceName = Settings::getInstance()->getString("ScrapperImageSrc"); + auto imageSource = std::make_shared< OptionListComponent >(mWindow, _("PREFERED IMAGE SOURCE"), false); + imageSource->add(_("NONE"), "", imageSourceName.empty()); + imageSource->add(_("SCREENSHOT"), "ss", imageSourceName == "ss"); + imageSource->add(_("TITLE SCREENSHOT"), "sstitle", imageSourceName == "sstitle"); + imageSource->add(_("BOX 2D"), "box-2D", imageSourceName == "box-2D"); + imageSource->add(_("BOX 3D"), "box-3D", imageSourceName == "box-3D"); + imageSource->add(_("MIX"), "mixrbv1", imageSourceName == "mixrbv1"); + imageSource->add(_("WHEEL"), "wheel", imageSourceName == "wheel"); + s->addWithLabel(_("PREFERED IMAGE SOURCE"), imageSource); + + s->addSaveFunc([imageSource] { + if (Settings::getInstance()->getString("ScrapperImageSrc") != imageSource->getSelected()) + Settings::getInstance()->setString("ScrapperImageSrc", imageSource->getSelected()); + }); - std::string thumbSourceName = Settings::getInstance()->getString("ScrapperThumbSrc"); - auto thumbSource = std::make_shared< OptionListComponent >(mWindow, _("PREFERED THUMBNAIL SOURCE"), false); - thumbSource->add(_("NONE"), "", thumbSourceName.empty()); - thumbSource->add(_("SCREENSHOT"), "ss", thumbSourceName == "ss"); - thumbSource->add(_("BOX 2D"), "box-2D", thumbSourceName == "box-2D"); - thumbSource->add(_("BOX 3D"), "box-3D", thumbSourceName == "box-3D"); - thumbSource->add(_("MIX"), "mixrbv1", thumbSourceName == "mixrbv1"); - thumbSource->add(_("WHEEL"), "wheel", thumbSourceName == "wheel"); - s->addWithLabel("PREFERED THUMBNAIL SOURCE", thumbSource); - s->addSaveFunc([thumbSource] { - if (Settings::getInstance()->getString("ScrapperThumbSrc") != thumbSource->getSelected()) - Settings::getInstance()->setString("ScrapperThumbSrc", thumbSource->getSelected()); - }); + std::string thumbSourceName = Settings::getInstance()->getString("ScrapperThumbSrc"); + auto thumbSource = std::make_shared< OptionListComponent >(mWindow, _("PREFERED THUMBNAIL SOURCE"), false); + thumbSource->add(_("NONE"), "", thumbSourceName.empty()); + thumbSource->add(_("SCREENSHOT"), "ss", thumbSourceName == "ss"); + thumbSource->add(_("TITLE SCREENSHOT"), "sstitle", thumbSourceName == "sstitle"); + thumbSource->add(_("BOX 2D"), "box-2D", thumbSourceName == "box-2D"); + thumbSource->add(_("BOX 3D"), "box-3D", thumbSourceName == "box-3D"); + thumbSource->add(_("MIX"), "mixrbv1", thumbSourceName == "mixrbv1"); + thumbSource->add(_("WHEEL"), "wheel", thumbSourceName == "wheel"); + s->addWithLabel(_("PREFERED THUMBNAIL SOURCE"), thumbSource); + + s->addSaveFunc([thumbSource] { + if (Settings::getInstance()->getString("ScrapperThumbSrc") != thumbSource->getSelected()) + Settings::getInstance()->setString("ScrapperThumbSrc", thumbSource->getSelected()); + }); - // scrape video - auto scrape_video = std::make_shared(mWindow); - scrape_video->setState(Settings::getInstance()->getBool("ScrapeVideos")); - s->addWithLabel(_("SCRAPE VIDEOS"), scrape_video); - s->addSaveFunc([scrape_video] { Settings::getInstance()->setBool("ScrapeVideos", scrape_video->getState()); }); + // scrape ratings + auto scrape_ratings = std::make_shared(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 marquee + auto scrape_marquee = std::make_shared(mWindow); + scrape_marquee->setState(Settings::getInstance()->getBool("ScrapeMarquee")); + s->addWithLabel(_("SCRAPE MARQUEE"), scrape_marquee); + s->addSaveFunc([scrape_marquee] { Settings::getInstance()->setBool("ScrapeMarquee", scrape_marquee->getState()); }); + + // scrape video + auto scrape_video = std::make_shared(mWindow); + scrape_video->setState(Settings::getInstance()->getBool("ScrapeVideos")); + s->addWithLabel(_("SCRAPE VIDEOS"), scrape_video); + s->addSaveFunc([scrape_video] { Settings::getInstance()->setBool("ScrapeVideos", scrape_video->getState()); }); + + // Account + createInputTextRow(s, _("USERNAME"), "ScreenScraperUser", false); + createInputTextRow(s, _("PASSWORD"), "ScreenScraperPass", true); + } + else + { + // scrape ratings + auto scrape_ratings = std::make_shared(mWindow); + scrape_ratings->setState(Settings::getInstance()->getBool("ScrapeRatings")); + s->addWithLabel(_("SCRAPE RATINGS"), scrape_ratings); // batocera + s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); }); + } // scrape now ComponentListRow row; - auto openScrapeNow = [this] { mWindow->pushGui(new GuiScraperStart(mWindow)); }; + auto openScrapeNow = [this] + { + if (ThreadedScraper::isRunning()) + { + Window* window = mWindow; + + mWindow->pushGui(new GuiMsgBox(mWindow, _("SCRAPING IS RUNNING. DO YOU WANT TO STOP IT ?"), _("YES"), [this, window] + { + ThreadedScraper::stop(); + }, _("NO"), nullptr)); + + return; + } + + mWindow->pushGui(new GuiScraperStart(mWindow)); + }; std::function openAndSave = openScrapeNow; openAndSave = [s, openAndSave] { s->save(); openAndSave(); }; s->addEntry(_("SCRAPE NOW"), true, openAndSave, "iconScraper"); - + s->updatePosition(); + + scraper_list->setSelectedChangedCallback([this, s, scraper, scraper_list](std::string value) + { + if (value != scraper && (scraper == "ScreenScraper" || value == "ScreenScraper")) + { + Settings::getInstance()->setString("Scraper", value); + delete s; + openScraperSettings(); + } + }); + mWindow->pushGui(s); } @@ -142,8 +192,9 @@ void GuiMenu::openSoundSettings() // volume auto volume = std::make_shared(mWindow, 0.f, 100.f, 1.f, "%"); volume->setValue((float)VolumeControl::getInstance()->getVolume()); + volume->setOnValueChanged([](const float &newVal) { VolumeControl::getInstance()->setVolume((int)Math::round(newVal)); }); s->addWithLabel(_("SYSTEM VOLUME"), volume); - s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); + //s->addSaveFunc([volume] { VolumeControl::getInstance()->setVolume((int)Math::round(volume->getValue())); }); if (UIModeController::getInstance()->isUIModeFull()) { @@ -227,6 +278,15 @@ void GuiMenu::openSoundSettings() Settings::getInstance()->setBool("audio.persystem", music_per_system->getState()); }); + // batocera - music per system + auto enableThemeMusics = std::make_shared(mWindow); + enableThemeMusics->setState(Settings::getInstance()->getBool("audio.thememusics")); + s->addWithLabel(_("PLAY THEME MUSICS"), enableThemeMusics); + s->addSaveFunc([enableThemeMusics] { + if (Settings::getInstance()->setBool("audio.thememusics", enableThemeMusics->getState())) + AudioManager::getInstance()->themeChanged(ViewController::get()->getState().getSystem()->getTheme(), true); + }); + // disable sounds auto sounds_enabled = std::make_shared(mWindow); sounds_enabled->setState(Settings::getInstance()->getBool("EnableSounds")); @@ -277,285 +337,143 @@ void GuiMenu::openSoundSettings() } -void GuiMenu::openUISettings() -{ - auto pthis = this; - Window* window = mWindow; - - auto s = new GuiSettings(mWindow, _("UI SETTINGS")); - s->setBeforeCloseFunc([window, pthis] { delete pthis; window->pushGui(new GuiMenu(window)); }); - s->enableBeforeCloseFunc(false); - - // theme set - auto theme = ThemeData::getMenuTheme(); - auto themeSets = ThemeData::getThemeSets(); - auto system = ViewController::get()->getState().getSystem(); - - if (!themeSets.empty()) +void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptr> theme_set) +{ + if (Settings::getInstance()->getString("ThemeSet") != theme_set->getSelected()) { - std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if (selectedSet == themeSets.cend()) - selectedSet = themeSets.cbegin(); - - auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); - for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) - theme_set->add(it->first, it->first, it == selectedSet); - s->addWithLabel(_("THEME"), theme_set); - - s->addSaveFunc([s, theme_set, window] - { - std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - if (oldTheme != theme_set->getSelected()) - { - Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); - - auto themeSubSets = ThemeData::getThemeSubSets(theme_set->getSelected()); - auto themeColorSets = ThemeData::getSubSet(themeSubSets, "colorset"); - auto themeIconSets = ThemeData::getSubSet(themeSubSets, "iconset"); - auto themeMenus = ThemeData::getSubSet(themeSubSets, "menu"); - auto themeSystemviewSets = ThemeData::getSubSet(themeSubSets, "systemview"); - auto themeGamelistViewSets = ThemeData::getSubSet(themeSubSets, "gamelistview"); - auto themeRegions = ThemeData::getSubSet(themeSubSets, "region"); - - // theme changed without setting options, forcing options to avoid crash/blank theme - Settings::getInstance()->setString("ThemeRegionName", themeRegions.empty() ? "" : themeRegions[0]); - Settings::getInstance()->setString("ThemeColorSet", themeColorSets.empty() ? "" : themeColorSets[0]); - Settings::getInstance()->setString("ThemeIconSet", themeIconSets.empty() ? "" : themeIconSets[0]); - Settings::getInstance()->setString("ThemeMenu", themeMenus.empty() ? "" : themeMenus[0]); - Settings::getInstance()->setString("ThemeSystemView", themeSystemviewSets.empty() ? "" : themeSystemviewSets[0]); - Settings::getInstance()->setString("ThemeGamelistView", themeGamelistViewSets.empty() ? "" : themeGamelistViewSets[0]); - - window->renderLoadingScreen(_("Loading...")); - - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); // TODO - replace this with some sort of signal-based implementation - - window->endRenderLoadingScreen(); - - s->enableBeforeCloseFunc(true); - } - }); - - if (system != NULL && system->getTheme()->hasSubsets()) - { - - // theme config - std::function openGui = [this, theme_set, s, window] { - auto themeconfig = new GuiSettings(mWindow, _("THEME CONFIGURATION").c_str()); + mWindow->pushGui(new GuiMsgBox(mWindow, _("YOU MUST APPLY THE THEME BEFORE EDIT CONFIGURATION"), _("OK"))); + return; + } - auto SelectedTheme = theme_set->getSelected(); + Window* window = mWindow; - auto themeSubSets = ThemeData::getThemeSubSets(SelectedTheme); - auto themeColorSets = ThemeData::getSubSet(themeSubSets, "colorset"); - auto themeIconSets = ThemeData::getSubSet(themeSubSets, "iconset"); - auto themeMenus = ThemeData::getSubSet(themeSubSets, "menu"); - auto themeSystemviewSets = ThemeData::getSubSet(themeSubSets, "systemview"); - auto themeGamelistViewSets = ThemeData::getSubSet(themeSubSets, "gamelistview"); - auto themeRegions = ThemeData::getSubSet(themeSubSets, "region"); + auto system = ViewController::get()->getState().getSystem(); + auto theme = system->getTheme(); + auto themeconfig = new GuiSettings(mWindow, _("THEME CONFIGURATION").c_str()); - // colorset - std::shared_ptr> theme_colorset = nullptr; - if (themeColorSets.size() > 0) - { - auto selectedColorSet = std::find(themeColorSets.cbegin(), themeColorSets.cend(), Settings::getInstance()->getString("ThemeColorSet")); - if (selectedColorSet == themeColorSets.end()) - selectedColorSet = themeColorSets.begin(); + auto themeSubSets = theme->getSubSets(); + auto themeColorSets = ThemeData::getSubSet(themeSubSets, "colorset"); + auto themeIconSets = ThemeData::getSubSet(themeSubSets, "iconset"); + auto themeMenus = ThemeData::getSubSet(themeSubSets, "menu"); + auto themeSystemviewSets = ThemeData::getSubSet(themeSubSets, "systemview"); + auto themeGamelistViewSets = ThemeData::getSubSet(themeSubSets, "gamelistview"); + auto themeRegions = ThemeData::getSubSet(themeSubSets, "region"); - theme_colorset = std::make_shared >(mWindow, _("THEME COLORSET"), false); + // colorset + std::shared_ptr> theme_colorset = nullptr; + if (themeColorSets.size() > 0) + { + auto selectedColorSet = std::find(themeColorSets.cbegin(), themeColorSets.cend(), Settings::getInstance()->getString("ThemeColorSet")); + if (selectedColorSet == themeColorSets.end()) + selectedColorSet = themeColorSets.begin(); - for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) - theme_colorset->add(*it, *it, it == selectedColorSet); + theme_colorset = std::make_shared >(mWindow, _("THEME COLORSET"), false); - if (!themeColorSets.empty()) - themeconfig->addWithLabel(_("THEME COLORSET"), theme_colorset); - } + for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) + theme_colorset->add(*it, *it, it == selectedColorSet); - // iconset - std::shared_ptr> theme_iconset = nullptr; - if (themeIconSets.size() > 0) - { - auto selectedIconSet = std::find(themeIconSets.cbegin(), themeIconSets.cend(), Settings::getInstance()->getString("ThemeIconSet")); - if (selectedIconSet == themeIconSets.end()) - selectedIconSet = themeIconSets.begin(); + if (!themeColorSets.empty()) + themeconfig->addWithLabel(_("THEME COLORSET"), theme_colorset); + } - theme_iconset = std::make_shared >(mWindow, _("THEME ICONSET"), false); + // iconset + std::shared_ptr> theme_iconset = nullptr; + if (themeIconSets.size() > 0) + { + auto selectedIconSet = std::find(themeIconSets.cbegin(), themeIconSets.cend(), Settings::getInstance()->getString("ThemeIconSet")); + if (selectedIconSet == themeIconSets.end()) + selectedIconSet = themeIconSets.begin(); - for (auto it = themeIconSets.begin(); it != themeIconSets.end(); it++) - theme_iconset->add(*it, *it, it == selectedIconSet); + theme_iconset = std::make_shared >(mWindow, _("THEME ICONSET"), false); - if (!themeIconSets.empty()) - themeconfig->addWithLabel(_("THEME ICONSET"), theme_iconset); - } + for (auto it = themeIconSets.begin(); it != themeIconSets.end(); it++) + theme_iconset->add(*it, *it, it == selectedIconSet); - // menu - std::shared_ptr> theme_menu = nullptr; - if (themeMenus.size() > 0) - { - auto selectedMenu = std::find(themeMenus.cbegin(), themeMenus.cend(), Settings::getInstance()->getString("ThemeMenu")); - if (selectedMenu == themeMenus.end()) - selectedMenu = themeMenus.begin(); + if (!themeIconSets.empty()) + themeconfig->addWithLabel(_("THEME ICONSET"), theme_iconset); + } - theme_menu = std::make_shared >(mWindow, _("THEME MENU"), false); + // menu + std::shared_ptr> theme_menu = nullptr; + if (themeMenus.size() > 0) + { + auto selectedMenu = std::find(themeMenus.cbegin(), themeMenus.cend(), Settings::getInstance()->getString("ThemeMenu")); + if (selectedMenu == themeMenus.end()) + selectedMenu = themeMenus.begin(); - for (auto it = themeMenus.begin(); it != themeMenus.end(); it++) - theme_menu->add(*it, *it, it == selectedMenu); + theme_menu = std::make_shared >(mWindow, _("THEME MENU"), false); - if (!themeMenus.empty()) - themeconfig->addWithLabel(_("THEME MENU"), theme_menu); - } + for (auto it = themeMenus.begin(); it != themeMenus.end(); it++) + theme_menu->add(*it, *it, it == selectedMenu); - // systemview - std::shared_ptr> theme_systemview = nullptr; - if (themeSystemviewSets.size() > 0) - { - auto selectedSystemviewSet = std::find(themeSystemviewSets.cbegin(), themeSystemviewSets.cend(), Settings::getInstance()->getString("ThemeSystemView")); - if (selectedSystemviewSet == themeSystemviewSets.end()) - selectedSystemviewSet = themeSystemviewSets.begin(); - - theme_systemview = std::make_shared >(mWindow, _("THEME SYSTEMVIEW"), false); + if (!themeMenus.empty()) + themeconfig->addWithLabel(_("THEME MENU"), theme_menu); + } - for (auto it = themeSystemviewSets.begin(); it != themeSystemviewSets.end(); it++) - theme_systemview->add(*it, *it, it == selectedSystemviewSet); + // systemview + std::shared_ptr> theme_systemview = nullptr; + if (themeSystemviewSets.size() > 0) + { + auto selectedSystemviewSet = std::find(themeSystemviewSets.cbegin(), themeSystemviewSets.cend(), Settings::getInstance()->getString("ThemeSystemView")); + if (selectedSystemviewSet == themeSystemviewSets.end()) + selectedSystemviewSet = themeSystemviewSets.begin(); - if (!themeSystemviewSets.empty()) - themeconfig->addWithLabel(_("THEME SYSTEMVIEW"), theme_systemview); - } + theme_systemview = std::make_shared >(mWindow, _("THEME SYSTEMVIEW"), false); - // gamelistview - std::shared_ptr> theme_gamelistview = nullptr; - if (themeGamelistViewSets.size() > 0) - { - auto selectedGamelistViewSet = std::find(themeGamelistViewSets.cbegin(), themeGamelistViewSets.cend(), Settings::getInstance()->getString("ThemeGamelistView")); - if (selectedGamelistViewSet == themeGamelistViewSets.end()) - selectedGamelistViewSet = themeGamelistViewSets.begin(); + for (auto it = themeSystemviewSets.begin(); it != themeSystemviewSets.end(); it++) + theme_systemview->add(*it, *it, it == selectedSystemviewSet); - theme_gamelistview = std::make_shared >(mWindow, _("THEME GAMELISTVIEW"), false); + if (!themeSystemviewSets.empty()) + themeconfig->addWithLabel(_("THEME SYSTEMVIEW"), theme_systemview); + } - for (auto it = themeGamelistViewSets.begin(); it != themeGamelistViewSets.end(); it++) - theme_gamelistview->add(*it, *it, it == selectedGamelistViewSet); + // gamelistview + std::shared_ptr> theme_gamelistview = nullptr; + if (themeGamelistViewSets.size() > 0) + { + auto selectedGamelistViewSet = std::find(themeGamelistViewSets.cbegin(), themeGamelistViewSets.cend(), Settings::getInstance()->getString("ThemeGamelistView")); + if (selectedGamelistViewSet == themeGamelistViewSets.end()) + selectedGamelistViewSet = themeGamelistViewSets.begin(); - if (!themeGamelistViewSets.empty()) - themeconfig->addWithLabel(_("THEME GAMELISTVIEW"), theme_gamelistview); - } + theme_gamelistview = std::make_shared >(mWindow, _("THEME GAMELISTVIEW"), false); - // themeregion - std::shared_ptr> theme_region = nullptr; - if (themeRegions.size() > 0) - { - auto selectedRegion = std::find(themeRegions.cbegin(), themeRegions.cend(), Settings::getInstance()->getString("ThemeRegionName")); - if (selectedRegion == themeRegions.end()) - selectedRegion = themeRegions.begin(); + for (auto it = themeGamelistViewSets.begin(); it != themeGamelistViewSets.end(); it++) + theme_gamelistview->add(*it, *it, it == selectedGamelistViewSet); - theme_region = std::make_shared >(mWindow, _("THEME REGION"), false); + if (!themeGamelistViewSets.empty()) + themeconfig->addWithLabel(_("THEME GAMELISTVIEW"), theme_gamelistview); + } - for (auto it = themeRegions.begin(); it != themeRegions.end(); it++) - theme_region->add(*it, *it, it == selectedRegion); + // themeregion + std::shared_ptr> theme_region = nullptr; + if (themeRegions.size() > 0) + { + auto selectedRegion = std::find(themeRegions.cbegin(), themeRegions.cend(), Settings::getInstance()->getString("ThemeRegionName")); + if (selectedRegion == themeRegions.end()) + selectedRegion = themeRegions.begin(); - if (!themeRegions.empty()) - themeconfig->addWithLabel(_("THEME REGION"), theme_region); - } + theme_region = std::make_shared >(mWindow, _("THEME REGION"), false); - // gamelist_style - std::shared_ptr> gamelist_style = nullptr; - // if (theme_gamelistview == nullptr) - { - gamelist_style = std::make_shared< OptionListComponent >(mWindow, _("FORCE GAMELIST VIEW STYLE"), false); - - std::vector styles; - styles.push_back("automatic"); - - auto system = ViewController::get()->getState().getSystem(); - if (system != NULL) - { - auto mViews = system->getTheme()->getViewsOfTheme(); - for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) - styles.push_back(*it); - } - else - { - styles.push_back("basic"); - styles.push_back("detailed"); - } - - auto viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); - if (!system->getTheme()->hasView(viewPreference)) - viewPreference = "automatic"; - - for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_(*it), *it, viewPreference == *it); - - themeconfig->addWithLabel(_("FORCE GAMELIST VIEW STYLE"), gamelist_style); - } - - themeconfig->addSaveFunc([this, s, theme_set, theme_colorset, theme_iconset, theme_menu, theme_systemview, theme_gamelistview, theme_region, gamelist_style] { - bool needReload = false; - if (theme_colorset != nullptr && Settings::getInstance()->getString("ThemeColorSet") != theme_colorset->getSelected() && !theme_colorset->getSelected().empty()) - needReload = true; - if (theme_iconset != nullptr && Settings::getInstance()->getString("ThemeIconSet") != theme_iconset->getSelected() && !theme_iconset->getSelected().empty()) - needReload = true; - if (theme_menu != nullptr && Settings::getInstance()->getString("ThemeMenu") != theme_menu->getSelected() && !theme_menu->getSelected().empty()) - needReload = true; - if (theme_systemview != nullptr && Settings::getInstance()->getString("ThemeSystemView") != theme_systemview->getSelected() && !theme_systemview->getSelected().empty()) - needReload = true; - if (theme_gamelistview != nullptr && Settings::getInstance()->getString("ThemeGamelistView") != theme_gamelistview->getSelected() && !theme_gamelistview->getSelected().empty()) - needReload = true; - if (theme_region != nullptr && Settings::getInstance()->getString("ThemeRegionName") != theme_region->getSelected() && !theme_region->getSelected().empty()) - needReload = true; - if (gamelist_style != nullptr && Settings::getInstance()->getString("GamelistViewStyle") != gamelist_style->getSelected() && !gamelist_style->getSelected().empty()) - needReload = true; - - if (needReload) { - - Settings::getInstance()->setString("ThemeSet", theme_set == nullptr ? "" : theme_set->getSelected()); - Settings::getInstance()->setString("ThemeColorSet", theme_colorset == nullptr ? "" : theme_colorset->getSelected()); - Settings::getInstance()->setString("ThemeIconSet", theme_iconset == nullptr ? "" : theme_iconset->getSelected()); - Settings::getInstance()->setString("ThemeMenu", theme_menu == nullptr ? "" : theme_menu->getSelected()); - Settings::getInstance()->setString("ThemeSystemView", theme_systemview == nullptr ? "" : theme_systemview->getSelected()); - Settings::getInstance()->setString("ThemeGamelistView", theme_gamelistview == nullptr ? "" : theme_gamelistview->getSelected()); - Settings::getInstance()->setString("ThemeRegionName", theme_region == nullptr ? "" : theme_region->getSelected()); - Settings::getInstance()->setString("GamelistViewStyle", gamelist_style == nullptr ? "" : gamelist_style->getSelected()); - - mWindow->renderLoadingScreen(_("Loading...")); - - //reload theme - std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->goToStart(); - ViewController::get()->reloadAll(); - - mWindow->endRenderLoadingScreen(); - - s->enableBeforeCloseFunc(true); - } - }); - if (!themeRegions.empty() || !themeGamelistViewSets.empty() || !themeSystemviewSets.empty() || !themeIconSets.empty() || !themeMenus.empty() || !themeColorSets.empty()) - { - themeconfig->updatePosition(); - mWindow->pushGui(themeconfig); - } - else - mWindow->pushGui(new GuiMsgBox(window, _("THIS THEME HAS NO OPTION"), _("OK"))); - }; + for (auto it = themeRegions.begin(); it != themeRegions.end(); it++) + theme_region->add(*it, *it, it == selectedRegion); - s->addSubMenu(_("THEME CONFIGURATION"), openGui); - } + if (!themeRegions.empty()) + themeconfig->addWithLabel(_("THEME REGION"), theme_region); } - // GameList view style - if (system != NULL && !system->getTheme()->hasSubsets()) + // gamelist_style + std::shared_ptr> gamelist_style = nullptr; + //if (theme_gamelistview == nullptr) { - auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); + gamelist_style = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); + std::vector styles; styles.push_back("automatic"); - auto system = ViewController::get()->getState().getSystem(); if (system != NULL) { - auto mViews = system->getTheme()->getViewsOfTheme(); + auto mViews = theme->getViewsOfTheme(); for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) styles.push_back(*it); } @@ -563,35 +481,26 @@ void GuiMenu::openUISettings() { styles.push_back("basic"); styles.push_back("detailed"); - styles.push_back("video"); - styles.push_back("grid"); } auto viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); - if (!system->getTheme()->hasView(viewPreference)) + if (!theme->hasView(viewPreference)) viewPreference = "automatic"; for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_(*it), *it, viewPreference == *it); - - s->addWithLabel(_("GAMELIST VIEW STYLE"), gamelist_style); - s->addSaveFunc([gamelist_style, viewPreference] { - bool needReload = false; - if (viewPreference != gamelist_style->getSelected()) - needReload = true; - Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected()); - if (needReload) - ViewController::get()->reloadAll(); - }); + gamelist_style->add(_(it->c_str()), *it, viewPreference == *it); + + themeconfig->addWithLabel(_("GAMELIST VIEW STYLE"), gamelist_style); } - + // Default grid size - if (system != NULL && system->getTheme()->hasView("grid")) + std::shared_ptr> mGridSize = nullptr; + if (system != NULL && theme->hasView("grid")) { Vector2f gridOverride = Vector2f::parseString(Settings::getInstance()->getString("DefaultGridSize")); auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); - auto mGridSize = std::make_shared>(mWindow, _("DEFAULT GRID SIZE"), false); + mGridSize = std::make_shared>(mWindow, _("DEFAULT GRID SIZE"), false); bool found = false; for (auto it = GuiGamelistOptions::gridSizes.cbegin(); it != GuiGamelistOptions::gridSizes.cend(); it++) @@ -600,14 +509,40 @@ void GuiMenu::openUISettings() if (sel) found = true; - mGridSize->add(_(*it), *it, sel); + mGridSize->add(_(it->c_str()), *it, sel); } if (!found) mGridSize->selectFirstItem(); - s->addWithLabel(_("DEFAULT GRID SIZE"), mGridSize); - s->addSaveFunc([mGridSize] + themeconfig->addWithLabel(_("DEFAULT GRID SIZE"), mGridSize); + } + + + themeconfig->addEntry(_("RESET GAMELIST CUSTOMISATIONS"), false, [s, themeconfig, window] + { + Settings::getInstance()->setString("GamelistViewStyle", ""); + Settings::getInstance()->setString("DefaultGridSize", ""); + + for (auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) + (*sysIt)->setSystemViewMode("automatic", Vector2f(0, 0)); + + themeconfig->setVariable("reloadAll", true); + themeconfig->close(); + }); + + themeconfig->addSaveFunc([this, themeconfig, theme_set, theme_colorset, theme_iconset, theme_menu, theme_systemview, theme_gamelistview, theme_region, gamelist_style, mGridSize, window] + { + bool reloadAll = Settings::getInstance()->setString("ThemeSet", theme_set == nullptr ? "" : theme_set->getSelected()); + reloadAll |= Settings::getInstance()->setString("ThemeColorSet", theme_colorset == nullptr ? "" : theme_colorset->getSelected()); + reloadAll |= Settings::getInstance()->setString("ThemeIconSet", theme_iconset == nullptr ? "" : theme_iconset->getSelected()); + reloadAll |= Settings::getInstance()->setString("ThemeMenu", theme_menu == nullptr ? "" : theme_menu->getSelected()); + reloadAll |= Settings::getInstance()->setString("ThemeSystemView", theme_systemview == nullptr ? "" : theme_systemview->getSelected()); + reloadAll |= Settings::getInstance()->setString("ThemeGamelistView", theme_gamelistview == nullptr ? "" : theme_gamelistview->getSelected()); + reloadAll |= Settings::getInstance()->setString("ThemeRegionName", theme_region == nullptr ? "" : theme_region->getSelected()); + reloadAll |= Settings::getInstance()->setString("GamelistViewStyle", gamelist_style == nullptr ? "" : gamelist_style->getSelected()); + + if (mGridSize != nullptr) { std::string str = mGridSize->getSelected(); std::string value = ""; @@ -621,13 +556,119 @@ void GuiMenu::openUISettings() Vector2f gridSizeOverride = Vector2f((float)atof(first.c_str()), (float)atof(second.c_str())); value = Utils::String::replace(Utils::String::replace(gridSizeOverride.toString(), ".000000", ""), "0 0", ""); } - - if (value != Settings::getInstance()->getString("DefaultGridSize")) + + reloadAll |= Settings::getInstance()->setString("DefaultGridSize", value); + } + else + reloadAll |= Settings::getInstance()->setString("DefaultGridSize", ""); + + if (reloadAll || themeconfig->getVariable("reloadAll")) + { + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->reloadAll(window); + window->endRenderLoadingScreen(); + + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + } + }); + + mWindow->pushGui(themeconfig); +} + +void GuiMenu::openUISettings() +{ + auto pthis = this; + Window* window = mWindow; + + auto s = new GuiSettings(mWindow, _("UI SETTINGS")); + + // theme set + auto theme = ThemeData::getMenuTheme(); + auto themeSets = ThemeData::getThemeSets(); + auto system = ViewController::get()->getState().getSystem(); + + if (!themeSets.empty()) + { + std::map::const_iterator selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if (selectedSet == themeSets.cend()) + selectedSet = themeSets.cbegin(); + + auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME", false); + for (auto it = themeSets.cbegin(); it != themeSets.cend(); it++) + theme_set->add(it->first, it->first, it == selectedSet); + + s->addWithLabel(_("THEME"), theme_set); + s->addSaveFunc([s, theme_set, window] + { + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + if (oldTheme != theme_set->getSelected()) { - Settings::getInstance()->setString("DefaultGridSize", value); - ViewController::get()->reloadAll(); + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); + + // theme changed without setting options, forcing options to avoid crash/blank theme + Settings::getInstance()->setString("ThemeRegionName", ""); + Settings::getInstance()->setString("ThemeColorSet", ""); + Settings::getInstance()->setString("ThemeIconSet", ""); + Settings::getInstance()->setString("ThemeMenu", ""); + Settings::getInstance()->setString("ThemeSystemView", ""); + Settings::getInstance()->setString("ThemeGamelistView", ""); + Settings::getInstance()->setString("GamelistViewStyle", ""); + Settings::getInstance()->setString("DefaultGridSize", ""); + + for (auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) + (*sysIt)->setSystemViewMode("automatic", Vector2f(0, 0)); + + s->setVariable("reloadCollections", true); + s->setVariable("reloadAll", true); + s->setVariable("reloadGuiMenu", true); + + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); } }); + + bool showThemeConfiguration = system->getTheme()->hasSubsets() || system->getTheme()->hasView("grid"); + if (showThemeConfiguration) + { + s->addSubMenu(_("THEME CONFIGURATION"), [this, s, theme_set]() { openThemeConfiguration(s, theme_set); }); + } + else // GameList view style only, acts like Retropie for simple themes + { + auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); + std::vector styles; + styles.push_back("automatic"); + + auto system = ViewController::get()->getState().getSystem(); + if (system != NULL) + { + auto mViews = system->getTheme()->getViewsOfTheme(); + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + styles.push_back(*it); + } + else + { + styles.push_back("basic"); + styles.push_back("detailed"); + styles.push_back("video"); + styles.push_back("grid"); + } + + auto viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + if (!system->getTheme()->hasView(viewPreference)) + viewPreference = "automatic"; + + for (auto it = styles.cbegin(); it != styles.cend(); it++) + gamelist_style->add(_(it->c_str()), *it, viewPreference == *it); + + s->addWithLabel(_("GAMELIST VIEW STYLE"), gamelist_style); + s->addSaveFunc([s, gamelist_style, window] { + if (Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected())) + { + s->setVariable("reloadAll", true); + s->setVariable("reloadGuiMenu", true); + } + }); + } } //#ifndef WIN32 @@ -694,13 +735,12 @@ void GuiMenu::openUISettings() language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); s->addWithLabel(_("LANGUAGE"), language); - s->addSaveFunc([language, window, pthis] { + s->addSaveFunc([language, window, pthis, s] { if (language->getSelected() != Settings::getInstance()->getString("Language")) { - Settings::getInstance()->setString("Language", language->getSelected()); - delete pthis; - window->pushGui(new GuiMenu(window)); + if (Settings::getInstance()->setString("Language", language->getSelected())) + s->setVariable("reloadGuiMenu", true); } }); } @@ -727,8 +767,7 @@ void GuiMenu::openUISettings() Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); GuiComponent::ALLOWANIMATIONS = Settings::getInstance()->getString("TransitionStyle") != "instant"; }); - - + auto transitionOfGames_style = std::make_shared< OptionListComponent >(mWindow, _("GAME LAUNCH TRANSITION"), false); std::vector gameTransitions; gameTransitions.push_back("fade"); @@ -818,16 +857,20 @@ void GuiMenu::openUISettings() auto show_help = std::make_shared(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()); }); + s->addSaveFunc([s, show_help] + { + if (Settings::getInstance()->setBool("ShowHelpPrompts", show_help->getState())) + s->setVariable("reloadAll", true); + }); // enable filters (ForceDisableFilters) auto enable_filter = std::make_shared(mWindow); enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); s->addWithLabel(_("ENABLE FILTERS"), enable_filter); - s->addSaveFunc([enable_filter] { + s->addSaveFunc([enable_filter, s] { bool filter_is_enabled = !Settings::getInstance()->getBool("ForceDisableFilters"); - Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState()); - if (enable_filter->getState() != filter_is_enabled) ViewController::get()->ReloadAndGoToStart(); + if (Settings::getInstance()->setBool("ForceDisableFilters", !enable_filter->getState())) + s->setVariable("reloadAll", true); }); @@ -839,6 +882,23 @@ void GuiMenu::openUISettings() screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); s->addRow(screensaver_row); + s->onFinalize([s, pthis, window] + { + if (s->getVariable("reloadCollections")) + CollectionSystemManager::get()->updateSystemsList(); + + if (s->getVariable("reloadAll")) + { + ViewController::get()->reloadAll(window); + window->endRenderLoadingScreen(); + } + + if (s->getVariable("reloadGuiMenu")) + { + delete pthis; + window->pushGui(new GuiMenu(window, false)); + } + }); s->updatePosition(); mWindow->pushGui(s); @@ -849,7 +909,7 @@ void GuiMenu::openOtherSettings() auto s = new GuiSettings(mWindow, _("ADVANCED SETTINGS")); // maximum vram - auto max_vram = std::make_shared(mWindow, 0.f, 2000.f, 10.f, "Mb"); + auto max_vram = std::make_shared(mWindow, 40.f, 1000.f, 10.f, "Mb"); max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); s->addWithLabel(_("VRAM LIMIT"), max_vram); s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); @@ -862,7 +922,7 @@ void GuiMenu::openOtherSettings() modes.push_back("enhanced"); modes.push_back("instant"); for (auto it = modes.cbegin(); it != modes.cend(); it++) - power_saver->add(*it, *it, Settings::getInstance()->getString("PowerSaverMode") == *it); + power_saver->add(_(it->c_str()), *it, Settings::getInstance()->getString("PowerSaverMode") == *it); s->addWithLabel(_("POWER SAVER MODES"), power_saver); s->addSaveFunc([this, power_saver] { @@ -897,12 +957,6 @@ void GuiMenu::openOtherSettings() s->addSaveFunc([local_art] { Settings::getInstance()->setBool("LocalArt", local_art->getState()); }); #endif - // hidden files - auto hidden_files = std::make_shared(mWindow); - hidden_files->setState(Settings::getInstance()->getBool("ShowHiddenFiles")); - s->addWithLabel(_("SHOW HIDDEN FILES"), hidden_files); - s->addSaveFunc([hidden_files] { Settings::getInstance()->setBool("ShowHiddenFiles", hidden_files->getState()); }); - #ifdef _RPI_ // Video Player - VideoOmxPlayer auto omx_player = std::make_shared(mWindow); @@ -971,6 +1025,32 @@ void GuiMenu::openOtherSettings() s->addSaveFunc([fullExitMenu] { Settings::getInstance()->setBool("ShowOnlyExit", !fullExitMenu->getState()); }); #endif + // log level + auto logLevel = std::make_shared< OptionListComponent >(mWindow, _("LOG LEVEL"), false); + std::vector levels; + levels.push_back("default"); + levels.push_back("disabled"); + levels.push_back("warning"); + levels.push_back("error"); + levels.push_back("debug"); + + auto level = Settings::getInstance()->getString("LogLevel"); + if (level.empty()) + level = "default"; + + for (auto it = levels.cbegin(); it != levels.cend(); it++) + logLevel->add(_(it->c_str()), *it, level == *it); + + s->addWithLabel(_("LOG LEVEL"), logLevel); + s->addSaveFunc([this, logLevel] + { + if (Settings::getInstance()->setString("LogLevel", logLevel->getSelected() == "default" ? "" : logLevel->getSelected())) + { + Log::setupReportingLevel(); + Log::init(); + } + }); + s->updatePosition(); mWindow->pushGui(s); @@ -1213,3 +1293,51 @@ std::vector GuiMenu::getHelpPrompts() prompts.push_back(HelpPrompt("start", _("CLOSE"))); return prompts; } + +void GuiMenu::createInputTextRow(GuiSettings *gui, std::string title, const char *settingsID, bool password) +{ + auto theme = ThemeData::getMenuTheme(); + std::shared_ptr font = theme->Text.font; + unsigned int color = theme->Text.color; + + // LABEL + Window *window = mWindow; + ComponentListRow row; + + auto lbl = std::make_shared(window, title, font, color); + row.addElement(lbl, true); // label + + std::shared_ptr ed; + + std::string value = Settings::getInstance()->getString(settingsID); + + ed = std::make_shared(window, ((password && value != "") ? "*********" : value), font, color, ALIGN_RIGHT); // Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT) + row.addElement(ed, true); + + auto spacer = std::make_shared(mWindow); + spacer->setSize(Renderer::getScreenWidth() * 0.005f, 0); + row.addElement(spacer, false); + + auto bracket = std::make_shared(mWindow); + bracket->setImage(theme->Icons.arrow); + bracket->setResize(Vector2f(0, lbl->getFont()->getLetterHeight())); + row.addElement(bracket, false); + + auto updateVal = [ed, settingsID, password](const std::string &newVal) { + if (!password) + ed->setValue(newVal); + else { + ed->setValue("*********"); + } + + Settings::getInstance()->setString(settingsID, newVal); + }; // ok callback (apply new value to ed) + + row.makeAcceptInputHandler([this, title, updateVal, settingsID] + { + std::string data = Settings::getInstance()->getString(settingsID); + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, title, data, updateVal, false)); + }); + + gui->addRow(row); +} \ No newline at end of file diff --git a/es-app/src/guis/GuiMenu.h b/es-app/src/guis/GuiMenu.h index b70a871412..956d10330f 100644 --- a/es-app/src/guis/GuiMenu.h +++ b/es-app/src/guis/GuiMenu.h @@ -3,6 +3,7 @@ #define ES_APP_GUIS_GUI_MENU_H #include "components/MenuComponent.h" +#include "components/OptionListComponent.h" #include "GuiComponent.h" class GuiSettings; @@ -10,7 +11,7 @@ class GuiSettings; class GuiMenu : public GuiComponent { public: - GuiMenu(Window* window); + GuiMenu(Window* window, bool animate = true); bool input(InputConfig* config, Input input) override; void onSizeChanged() override; @@ -29,6 +30,9 @@ class GuiMenu : public GuiComponent void openScreensaverOptions(); void openSoundSettings(); void openUISettings(); + void openThemeConfiguration(GuiSettings* s, std::shared_ptr> theme_set); + + void createInputTextRow(GuiSettings *gui, std::string title, const char *settingsID, bool password); MenuComponent mMenu; TextComponent mVersion; diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 4078736885..2fb0a5ab6f 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -118,7 +118,7 @@ void GuiScraperMulti::acceptResult(const ScraperSearchResult& result) { ScraperSearchParams& search = mSearchQueue.front(); - search.game->metadata = result.mdl; + search.game->metadata.importScrappedMetadata(result.mdl); updateGamelist(search.system); mSearchQueue.pop(); diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index b51afd7dd1..ae0cf39213 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -7,31 +7,66 @@ #include "views/ViewController.h" #include "FileData.h" #include "SystemData.h" +#include "scrapers/ThreadedScraper.h" GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), mMenu(window, _("SCRAPE NOW")) { + mOverwriteMedias = true; + addChild(&mMenu); // add filters (with first one selected) mFilters = std::make_shared< OptionListComponent >(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); + + mFilters->add(_("Only missing medias"), [this](SystemData*, FileData* g) -> bool + { + mOverwriteMedias = false; + + if (Settings::getInstance()->getString("Scraper") == "ScreenScraper") + { + if (!Settings::getInstance()->getString("ScrapperImageSrc").empty() && g->metadata.get("image").empty()) + return true; + + if (Settings::getInstance()->getString("ScrapperThumbSrc").empty() && g->metadata.get("thumbnail").empty()) + return true; + + if (Settings::getInstance()->getBool("ScrapeVideos") && g->metadata.get("video").empty()) + return true; + + if (Settings::getInstance()->getBool("ScrapeMarquee") && g->metadata.get("marquee").empty()) + return true; + + return false; + } + else + return g->metadata.get("image").empty(); + + }, true); + mMenu.addWithLabel(_("FILTER"), mFilters); + std::string currentSystem; + + if (ViewController::get()->getState().viewing == ViewController::GAME_LIST) + currentSystem = ViewController::get()->getState().getSystem()->getName(); + //add systems (all with a platformid specified selected) mSystems = std::make_shared< OptionListComponent >(mWindow, _("SCRAPE THESE SYSTEMS"), true); for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { - if(!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) - mSystems->add((*it)->getFullName(), *it, !(*it)->getPlatformIds().empty()); + if (!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) + mSystems->add((*it)->getFullName(), *it, + currentSystem.empty() ? + !(*it)->getPlatformIds().empty() : + (*it)->getName() == currentSystem && !(*it)->getPlatformIds().empty()); } mMenu.addWithLabel(_("SYSTEMS"), mSystems); mApproveResults = std::make_shared(mWindow); - mApproveResults->setState(true); + mApproveResults->setState(false); mMenu.addWithLabel(_("USER DECIDES ON CONFLICTS"), mApproveResults); mMenu.addButton(_("START"), _("START"), std::bind(&GuiScraperStart::pressedStart, this)); @@ -68,8 +103,26 @@ void GuiScraperStart::start() } else { - GuiScraperMulti* gsm = new GuiScraperMulti(mWindow, searches, mApproveResults->getState()); - mWindow->pushGui(gsm); + if (ThreadedScraper::isRunning()) + { + Window* window = mWindow; + + mWindow->pushGui(new GuiMsgBox(mWindow, _("SCRAPING IS RUNNING. DO YOU WANT TO STOP IT ?"), _("YES"), [this, window] + { + ThreadedScraper::stop(); + }, _("NO"), nullptr)); + + return; + } + + if (mApproveResults->getState()) + { + GuiScraperMulti* gsm = new GuiScraperMulti(mWindow, searches, mApproveResults->getState()); + mWindow->pushGui(gsm); + } + else + ThreadedScraper::start(mWindow, searches); + delete this; } } @@ -87,7 +140,8 @@ std::queue GuiScraperStart::getSearches(std::vector mApproveResults; MenuComponent mMenu; + bool mOverwriteMedias; }; #endif // ES_APP_GUIS_GUI_SCRAPER_START_H diff --git a/es-app/src/guis/GuiSettings.cpp b/es-app/src/guis/GuiSettings.cpp index 66193fc6ab..fc81a23580 100644 --- a/es-app/src/guis/GuiSettings.cpp +++ b/es-app/src/guis/GuiSettings.cpp @@ -4,17 +4,26 @@ #include "Settings.h" #include "SystemData.h" #include "Window.h" +#include "EsLocale.h" -GuiSettings::GuiSettings(Window* window, std::string title) : GuiComponent(window), mMenu(window, title) +GuiSettings::GuiSettings(Window* window, const std::string title) : GuiComponent(window), mMenu(window, title) { addChild(&mMenu); - mMenu.addButton(_("BACK"), _("BACK"), [this] { saveAndClose(); }); - updatePosition(); + + mCloseButton = "start"; + mMenu.addButton(_("BACK"), _("go back"), [this] { close(); }); + + setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + + if (Renderer::isSmallScreen()) + mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); + else + mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); } GuiSettings::~GuiSettings() { - + } void GuiSettings::updatePosition() @@ -23,6 +32,16 @@ void GuiSettings::updatePosition() mMenu.setPosition((mSize.x() - mMenu.getSize().x()) / 2, (mSize.y() - mMenu.getSize().y()) / 2); } +void GuiSettings::close() +{ + save(); + + if (mOnFinalizeFunc != nullptr) + mOnFinalizeFunc(); + + delete this; +} + void GuiSettings::save() { if (!mSaveFuncs.size()) @@ -34,31 +53,20 @@ void GuiSettings::save() Settings::getInstance()->saveFile(); } -void GuiSettings::saveAndClose() -{ - save(); - - if (mBeforeCloseFunc != nullptr && mEnableBeforeCloseFunc) - mBeforeCloseFunc(); - - delete this; -} - bool GuiSettings::input(InputConfig* config, Input input) { - if(config->isMappedTo("b", input) && input.value != 0) + if (config->isMappedTo("b", input) && input.value != 0) { - saveAndClose(); + close(); return true; } - if(config->isMappedTo("start", input) && input.value != 0) + if (config->isMappedTo(mCloseButton, input) && input.value != 0) { // close everything Window* window = mWindow; - while(window->peekGui() && window->peekGui() != ViewController::get()) + while (window->peekGui() && window->peekGui() != ViewController::get()) delete window->peekGui(); - return true; } @@ -77,8 +85,20 @@ std::vector GuiSettings::getHelpPrompts() std::vector prompts = mMenu.getHelpPrompts(); prompts.push_back(HelpPrompt("b", _("BACK"))); - prompts.push_back(HelpPrompt("start", _("CLOSE"))); + prompts.push_back(HelpPrompt(mCloseButton, _("CLOSE"))); return prompts; } +void GuiSettings::addSubMenu(const std::string& label, const std::function& func) +{ + ComponentListRow row; + row.makeAcceptInputHandler(func); + + auto theme = ThemeData::getMenuTheme(); + + auto entryMenu = std::make_shared(mWindow, label, theme->Text.font, theme->Text.color); + row.addElement(entryMenu, true); + row.addElement(makeArrow(mWindow), false); + mMenu.addRow(row); +}; \ No newline at end of file diff --git a/es-app/src/guis/GuiSettings.h b/es-app/src/guis/GuiSettings.h index 0f20a530b3..440063d797 100644 --- a/es-app/src/guis/GuiSettings.h +++ b/es-app/src/guis/GuiSettings.h @@ -8,43 +8,53 @@ class GuiSettings : public GuiComponent { public: - GuiSettings(Window* window, std::string title); + GuiSettings(Window* window, const std::string title); virtual ~GuiSettings(); // just calls save(); - - void updatePosition(); + + void close(); + void save(); + inline void addRow(const ComponentListRow& row) { mMenu.addRow(row); }; - inline void addWithLabel(const std::string& label, const std::shared_ptr& comp, const std::string iconName = "") { mMenu.addWithLabel(label, comp, iconName); }; + inline void addWithLabel(const std::string& label, const std::shared_ptr& comp) { mMenu.addWithLabel(label, comp); }; inline void addSaveFunc(const std::function& func) { mSaveFuncs.push_back(func); }; - inline void addEntry(const std::string name, bool add_arrow, const std::function& func, const std::string iconName = "") { mMenu.addEntry(name, add_arrow, func, iconName); }; + inline void addEntry(const std::string name, bool add_arrow = false, const std::function& func = nullptr, const std::string iconName = "") { mMenu.addEntry(name, add_arrow, func, iconName); }; - inline void addSubMenu(const std::string& label, const std::function& func) { - ComponentListRow row; - row.makeAcceptInputHandler(func); + void addSubMenu(const std::string& label, const std::function& func); - auto theme = ThemeData::getMenuTheme(); - - auto entryMenu = std::make_shared(mWindow, label, theme->Text.font, theme->Text.color); - row.addElement(entryMenu, true); - row.addElement(makeArrow(mWindow), false); - mMenu.addRow(row); - }; + inline void setSave(bool sav) { mDoSave = sav; }; // batocera bool input(InputConfig* config, Input input) override; std::vector getHelpPrompts() override; HelpStyle getHelpStyle() override; - inline void setBeforeCloseFunc(const std::function& func) { mBeforeCloseFunc = func; mEnableBeforeCloseFunc = (mBeforeCloseFunc != nullptr); }; - void enableBeforeCloseFunc(bool use) { mEnableBeforeCloseFunc = use; } + MenuComponent& getMenu() { return mMenu; } - void save(); + inline void onFinalize(const std::function& func) { mOnFinalizeFunc = func; }; -private: - void saveAndClose(); + bool getVariable(const std::string name) + { + if (mVariableMap.find(name) == mVariableMap.cend()) + return false; + + return mVariableMap[name]; + } + void setCloseButton(const std::string name) { mCloseButton = name; } + void setVariable(const std::string name, bool value) { mVariableMap[name] = value; } + void updatePosition(); + +protected: MenuComponent mMenu; + +private: + bool mDoSave = true; // batocera + std::vector< std::function > mSaveFuncs; - std::function mBeforeCloseFunc; - bool mEnableBeforeCloseFunc; + std::function mOnFinalizeFunc; + + std::map mVariableMap; + + std::string mCloseButton; }; #endif // ES_APP_GUIS_GUI_SETTINGS_H diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 863ab740ef..b0913c4bb7 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -295,25 +295,13 @@ void onExit() Log::close(); } -#include "AudioManager.h" -#include "guis/GuiInfoPopup.h" - -static std::vector mMessages; - void processAudioTitles(Window* window) { if (Settings::getInstance()->getBool("MusicTitles")) { std::string songName = AudioManager::getInstance()->popSongName(); if (!songName.empty()) - mMessages.push_back(/*_("Now playing: ") +*/songName); - } - - if (!mMessages.empty()) - { - std::string message = mMessages.back(); - mMessages.pop_back(); - window->setInfoPopup(new GuiInfoPopup(window, message, 4000)); + window->displayNotificationMessage(_U("\uF028 ") + songName); } } @@ -373,8 +361,8 @@ int main(int argc, char* argv[]) return 1; //start the logger - Log::init(); - Log::open(); + Log::setupReportingLevel(); + Log::init(); LOG(LogInfo) << "EmulationStation - v" << PROGRAM_VERSION_STRING << ", built " << PROGRAM_BUILT_STRING; //always close the log on exit diff --git a/es-app/src/scrapers/GamesDBJSONScraperResources.cpp b/es-app/src/scrapers/GamesDBJSONScraperResources.cpp index 7d59bef46b..809e8831f5 100644 --- a/es-app/src/scrapers/GamesDBJSONScraperResources.cpp +++ b/es-app/src/scrapers/GamesDBJSONScraperResources.cpp @@ -17,7 +17,7 @@ using namespace rapidjson; namespace { -constexpr char GamesDBAPIKey[] = "445fcbc3f32bb2474bc27016b99eb963d318ee3a608212c543b9a79de1041600"; + constexpr char GamesDBAPIKey[] = "445fcbc3f32bb2474bc27016b99eb963d318ee3a608212c543b9a79de1041600"; constexpr int MAX_WAIT_MS = 90000; diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index f592314fbf..fe64fe1c2d 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -8,10 +8,13 @@ #include "SystemData.h" #include #include +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" +// batocera const std::map scraper_request_funcs { { "ScreenScraper", &screenscraper_generate_scraper_requests }, - { "TheGamesDB", &thegamesdb_generate_json_scraper_requests } + { "TheGamesDB", &thegamesdb_generate_json_scraper_requests } }; std::unique_ptr startScraperSearch(const ScraperSearchParams& params) @@ -89,7 +92,7 @@ void ScraperSearchHandle::update() } // we finished without any errors! - if(mRequestQueue.empty()) + if(mRequestQueue.empty() && mStatus != ASYNC_ERROR) { setStatus(ASYNC_DONE); return; @@ -141,58 +144,113 @@ std::unique_ptr resolveMetaDataAssets(const ScraperSearchResult MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search) : mResult(result) { - if(!result.imageUrl.empty()) + mPercent = -1; + + std::string ext; + + // If we have a file extension returned by the scraper, then use it. + // Otherwise, try to guess it by the name of the URL, which point to an image. + if (!result.imageType.empty()) { - std::string ext; + ext = result.imageType; + } + else + { + size_t dot = result.imageUrl.find_last_of('.'); - // If we have a file extension returned by the scraper, then use it. - // Otherwise, try to guess it by the name of the URL, which point to an image. - if (!result.imageType.empty()) - { - ext = result.imageType; - }else{ - size_t dot = result.imageUrl.find_last_of('.'); + if (dot != std::string::npos) + ext = result.imageUrl.substr(dot, std::string::npos); + } - if (dot != std::string::npos) - ext = result.imageUrl.substr(dot, std::string::npos); - } + bool ss = Settings::getInstance()->getString("Scraper") == "ScreenScraper"; - if (!result.imageUrl.empty()) - { - std::string imgPath = getSaveAsPath(search, "image", ext); + auto tmp = Settings::getInstance()->getString("ScrapperImageSrc"); + auto md = search.game->metadata.get("image"); - mFuncs.push_back(ResolvePair(downloadImageAsync(result.imageUrl, imgPath), [this, imgPath] + if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperImageSrc").empty() && !search.game->metadata.get("image").empty()) + mResult.mdl.set("image", search.game->metadata.get("image")); + else if (!result.imageUrl.empty()) + { + std::string imgPath = getSaveAsPath(search, "image", ext); + + mFuncs.push_back(new ResolvePair( + [this, result, imgPath] + { + return downloadImageAsync(result.imageUrl, imgPath); + }, + [this, imgPath] { mResult.mdl.set("image", imgPath); if (mResult.thumbnailUrl.find(mResult.imageUrl) == 0) mResult.thumbnailUrl = ""; - mResult.imageUrl = ""; - })); - } + mResult.imageUrl = ""; + }, "image", result.mdl.getName())); + } - if (!result.thumbnailUrl.empty() && result.thumbnailUrl.find(result.imageUrl) != 0) - { - std::string thumbPath = getSaveAsPath(search, "thumb", ext); + if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperThumbSrc").empty() && !search.game->metadata.get("thumbnail").empty()) + mResult.mdl.set("thumbnail", search.game->metadata.get("thumbnail")); + else if (!result.thumbnailUrl.empty() && result.thumbnailUrl.find(result.imageUrl) != 0) + { + std::string thumbPath = getSaveAsPath(search, "thumb", ext); - mFuncs.push_back(ResolvePair(downloadImageAsync(result.thumbnailUrl, thumbPath), [this, thumbPath] + mFuncs.push_back(new ResolvePair( + [this, result, thumbPath] + { + return downloadImageAsync(result.thumbnailUrl, thumbPath); + }, + [this, thumbPath] { mResult.mdl.set("thumbnail", thumbPath); mResult.thumbnailUrl = ""; - })); - } + }, "thumbnail", result.mdl.getName())); + } - if (!result.videoUrl.empty()) - { - std::string videoPath = getSaveAsPath(search, "video", ".mp4"); + if (!search.overWriteMedias && Settings::getInstance()->getBool("ScrapeMarquee") && !search.game->metadata.get("marquee").empty()) + mResult.mdl.set("marquee", search.game->metadata.get("marquee")); + else if (!result.marqueeUrl.empty()) + { + std::string marqueePath = getSaveAsPath(search, "marquee", ext); + + mFuncs.push_back(new ResolvePair( + [this, result, marqueePath] + { + return downloadImageAsync(result.marqueeUrl, marqueePath); + }, + [this, marqueePath] + { + mResult.mdl.set("marquee", marqueePath); + mResult.marqueeUrl = ""; + }, "marquee", result.mdl.getName())); + } + + if (!search.overWriteMedias && Settings::getInstance()->getBool("ScrapeVideos") && !search.game->metadata.get("video").empty()) + mResult.mdl.set("video", search.game->metadata.get("video")); + else if (!result.videoUrl.empty()) + { + std::string videoPath = getSaveAsPath(search, "video", ".mp4"); - mFuncs.push_back(ResolvePair(downloadImageAsync(result.videoUrl, videoPath), [this, videoPath] + mFuncs.push_back(new ResolvePair( + [this, result, videoPath] + { + return downloadImageAsync(result.videoUrl, videoPath); + }, + [this, videoPath] { mResult.mdl.set("video", videoPath); mResult.videoUrl = ""; - })); - } + }, "video", result.mdl.getName())); + } + + auto it = mFuncs.cbegin(); + if (it == mFuncs.cend()) + setStatus(ASYNC_DONE); + else + { + mSource = (*it)->source; + mCurrentItem = (*it)->name; + (*it)->Run(); } } @@ -202,21 +260,40 @@ void MDResolveHandle::update() return; auto it = mFuncs.cbegin(); - while(it != mFuncs.cend()) + if (it == mFuncs.cend()) { - if(it->first->status() == ASYNC_ERROR) - { - setError(it->first->getStatusString()); - return; - }else if(it->first->status() == ASYNC_DONE) + setStatus(ASYNC_DONE); + return; + } + + ResolvePair* pPair = (*it); + + if (pPair->handle->status() == ASYNC_IN_PROGRESS) + mPercent = pPair->handle->getPercent(); + + if (pPair->handle->status() == ASYNC_ERROR) + { + setError(pPair->handle->getStatusString()); + for (auto fc : mFuncs) + delete fc; + + return; + } + else if (pPair->handle->status() == ASYNC_DONE) + { + pPair->onFinished(); + mFuncs.erase(it); + delete pPair; + + auto next = mFuncs.cbegin(); + if (next != mFuncs.cend()) { - it->second(); - it = mFuncs.erase(it); - continue; + mSource = (*next)->source; + mCurrentItem = (*next)->name; + (*next)->Run(); } - it++; } - + if(mFuncs.empty()) setStatus(ASYNC_DONE); } @@ -232,6 +309,14 @@ ImageDownloadHandle::ImageDownloadHandle(const std::string& url, const std::stri { } +int ImageDownloadHandle::getPercent() +{ + if (mReq->status() == HttpReq::REQ_IN_PROGRESS) + return mReq->getPercent(); + + return -1; +} + void ImageDownloadHandle::update() { if(mReq->status() == HttpReq::REQ_IN_PROGRESS) @@ -245,29 +330,25 @@ void ImageDownloadHandle::update() return; } - // download is done, save it to disk - std::ofstream stream(mSavePath, std::ios_base::out | std::ios_base::binary); - if(stream.bad()) + if (mStatus == ASYNC_IN_PROGRESS) { - 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; - } + if (!mReq->saveContent(mSavePath)) + { + setError("Failed to save image. Disk full?"); + return; + } - // resize downloaded image - if (Utils::FileSystem::getExtension(mSavePath) != ".mp4") - { - resizeImage(mSavePath, mMaxWidth, mMaxHeight); - //setError("Error saving resized image. Out of memory? Disk full?"); - //return; + // It's an image ? + std::string ext = Utils::String::toLower(Utils::FileSystem::getExtension(mSavePath)); + if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".bmp" || ext == ".gif") + { + // resize it + if (!resizeImage(mSavePath, mMaxWidth, mMaxHeight)) + { + setError("Error saving resized image. Out of memory? Disk full?"); + return; + } + } } setStatus(ASYNC_DONE); @@ -333,32 +414,23 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight) std::string getSaveAsPath(const ScraperSearchParams& params, const std::string& suffix, const std::string& extension) { - std::string subFolder = "downloaded_images"; - if (suffix == "video") - subFolder = "downloaded_videos"; - + const std::string subdirectory = params.system->getName(); const std::string name = Utils::FileSystem::getStem(params.game->getPath()) + "-" + suffix; - const std::string basePath = Utils::FileSystem::getParent(params.system->getGamelistPath(false)); - if (basePath.find("/.emulationstation/") == std::string::npos) - { - const std::string pth = Utils::FileSystem::getGenericPath(params.system->getRootFolder()->getPath() + "/"+ subFolder +"/"); - if (!Utils::FileSystem::exists(pth)) - Utils::FileSystem::createDirectory(pth); + std::string subFolder = "images"; + if (suffix == "video") + subFolder = "videos"; - return pth + "/" + name + extension; - } - - const std::string subdirectory = params.system->getName(); - std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/"+ subFolder +"/"; + std::string path = params.system->getRootFolder()->getPath() + "/" + subFolder + "/"; // batocera if(!Utils::FileSystem::exists(path)) Utils::FileSystem::createDirectory(path); - path += subdirectory + "/"; - - if(!Utils::FileSystem::exists(path)) - Utils::FileSystem::createDirectory(path); + // batocera + //path += subdirectory + "/"; + // + //if(!Utils::FileSystem::exists(path)) + // Utils::FileSystem::createDirectory(path); path += name + extension; return path; diff --git a/es-app/src/scrapers/Scraper.h b/es-app/src/scrapers/Scraper.h index 3b60130d68..08064d81c8 100644 --- a/es-app/src/scrapers/Scraper.h +++ b/es-app/src/scrapers/Scraper.h @@ -18,9 +18,12 @@ class SystemData; struct ScraperSearchParams { + ScraperSearchParams() { overWriteMedias = true; } + SystemData* system; FileData* game; + bool overWriteMedias; std::string nameOverride; }; @@ -32,6 +35,12 @@ struct ScraperSearchResult std::string imageUrl; std::string thumbnailUrl; std::string videoUrl; + std::string marqueeUrl; + + bool hadMedia() + { + return !imageUrl.empty() || !thumbnailUrl.empty() || !videoUrl.empty() || !marqueeUrl.empty(); + } // Needed to pre-set the image type std::string imageType; @@ -54,8 +63,8 @@ struct ScraperSearchResult // 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), +// 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. @@ -72,7 +81,7 @@ class ScraperRequest : public AsyncHandle // returns "true" once we're done virtual void update() = 0; - + protected: std::vector& mResults; }; @@ -130,13 +139,53 @@ class MDResolveHandle : public AsyncHandle MDResolveHandle(const ScraperSearchResult& result, const ScraperSearchParams& search); void update() override; - inline const ScraperSearchResult& getResult() const { assert(mStatus == ASYNC_DONE); return mResult; } + inline const ScraperSearchResult& getResult() const { return mResult; } // assert(mStatus == ASYNC_DONE); -> FCA : Why ??? + + std::string getCurrentItem() { + return mCurrentItem; + } + + std::string getCurrentSource() { + return mSource; + } + + int getPercent() { + return mPercent; + } private: ScraperSearchResult mResult; - typedef std::pair< std::unique_ptr, std::function > ResolvePair; - std::vector mFuncs; + class ResolvePair + { + public: + ResolvePair(std::function()> _invoker, std::function _function, std::string _name, std::string _source) + { + func = _invoker; + onFinished = _function; + name = _name; + source = _source; + } + + void Run() + { + handle = func(); + } + + std::function onFinished; + std::string name; + std::string source; + + std::unique_ptr handle; + + private: + std::function()> func; + }; + + std::vector mFuncs; + std::string mCurrentItem; + std::string mSource; + int mPercent; }; class ImageDownloadHandle : public AsyncHandle @@ -146,6 +195,8 @@ class ImageDownloadHandle : public AsyncHandle void update() override; + virtual int getPercent(); + private: std::unique_ptr mReq; std::string mSavePath; diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index fc203dd54b..67b5f3abbe 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -7,9 +7,9 @@ #include "PlatformId.h" #include "Settings.h" #include "SystemData.h" -#include "EsLocale.h" #include #include +#include "EsLocale.h" using namespace PlatformIds; @@ -126,6 +126,7 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, ScreenScraperRequest::ScreenScraperConfig ssConfig; + // FCA Fix for names override not working on Retropie if (params.nameOverride.length() == 0) { path = ssConfig.getGameSearchUrl(params.game->getFileName()); @@ -172,25 +173,23 @@ void ScreenScraperRequest::process(const std::unique_ptr& req, std::vec { assert(req->status() == HttpReq::REQ_SUCCESS); - std::string content = req->getContent(); + auto content = req->getContent(); - pugi::xml_document doc; + pugi::xml_document doc; pugi::xml_parse_result parseResult = doc.load(content.c_str()); if (!parseResult) { std::stringstream ss; ss << "ScreenScraperRequest - Error parsing XML." << std::endl << parseResult.description() << ""; - std::string err = ss.str(); - setError(err); - LOG(LogError) << err; + //setError(err); Don't consider it an error -> Request is a success. Simply : Game is not found + LOG(LogWarning) << err; return; } processGame(doc, results); - } pugi::xml_node ScreenScraperRequest::findMedia(pugi::xml_node media_list, std::vector mediaNames, std::string region) @@ -218,7 +217,7 @@ pugi::xml_node ScreenScraperRequest::findMedia(pugi::xml_node media_list, std::s if (!results.size()) return art; - + // Region fallback: WOR(LD), US, CUS(TOM?), JP, EU for (auto _region : std::vector{ region, "wor", "us", "cus", "jp", "eu", "" }) { @@ -243,7 +242,9 @@ std::vector ScreenScraperRequest::getRipList(std::string imageSourc std::vector ripList; if (imageSource == "ss") - ripList = { "ss", "mixrbv1", "mixrbv2", "box-2D", "box-3D" }; + ripList = { "ss", "sstitle", "mixrbv1", "mixrbv2", "box-2D", "box-3D" }; + else if (imageSource == "sstitle") + ripList = { "sstitle", "ss", "mixrbv1", "mixrbv2", "box-2D", "box-3D" }; else if (imageSource == "mixrbv1" || imageSource == "mixrbv2" || imageSource == "mixrbv") ripList = { "mixrbv1", "mixrbv2", "ss", "box-3D", "box-2D" }; else if (imageSource == "box-2D") @@ -265,13 +266,22 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve { ScraperSearchResult result; ScreenScraperRequest::ScreenScraperConfig ssConfig; - + std::string region = Utils::String::toLower(ssConfig.region); - std::string language = Utils::String::toLower(EsLocale::getLanguage()); - if (language != "EN") - region = "eu"; - // ssConfig.language + std::string language = Utils::String::toLower(EsLocale::getLanguage()); + if (language.empty()) + language = "en"; + else + { + auto shortNameDivider = language.find("_"); + if (shortNameDivider != std::string::npos) + { + region = Utils::String::toLower(language.substr(shortNameDivider + 1)); + language = Utils::String::toLower(language.substr(0, shortNameDivider)); + } + } + // Name fallback: US, WOR(LD). ( Xpath: Data/jeu[0]/noms/nom[*] ). result.mdl.set("name", find_child_by_attribute_list(game.child("noms"), "nom", "region", { region, "wor", "us" , "ss", "eu", "jp" }).text().get()); @@ -350,8 +360,8 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve else LOG(LogDebug) << "Failed to find media XML node for image"; } - - if (!Settings::getInstance()->getString("ScrapperThumbSrc").empty() && + + if (!Settings::getInstance()->getString("ScrapperThumbSrc").empty() && Settings::getInstance()->getString("ScrapperThumbSrc") != Settings::getInstance()->getString("ScrapperImageSrc")) { ripList = getRipList(Settings::getInstance()->getString("ScrapperThumbSrc")); @@ -360,8 +370,6 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve pugi::xml_node art = findMedia(media_list, ripList, region); if (art) { - // Sending a 'softname' containing space will make the image URLs returned by the API also contain the space. - // Escape any spaces in the URL here // Ask for the same image, but with a smaller size, for the thumbnail displayed during scraping result.thumbnailUrl = Utils::String::replace(art.text().get(), " ", "%20"); } @@ -370,19 +378,24 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve } } + if (Settings::getInstance()->getBool("ScrapeMarquee")) + { + pugi::xml_node art = findMedia(media_list, "marquee", region); + if (art) + result.marqueeUrl = Utils::String::replace(art.text().get(), " ", "%20"); + else + LOG(LogDebug) << "Failed to find media XML node for video"; + } + if (Settings::getInstance()->getBool("ScrapeVideos")) { pugi::xml_node art = findMedia(media_list, "video", region); if (art) - { - // Sending a 'softname' containing space will make the image URLs returned by the API also contain the space. - // Escape any spaces in the URL here - // Ask for the same image, but with a smaller size, for the thumbnail displayed during scraping result.videoUrl = Utils::String::replace(art.text().get(), " ", "%20"); - } else LOG(LogDebug) << "Failed to find media XML node for video"; } + } out_results.push_back(result); @@ -423,11 +436,18 @@ void ScreenScraperRequest::processList(const pugi::xml_document& xmldoc, std::ve std::string ScreenScraperRequest::ScreenScraperConfig::getGameSearchUrl(const std::string gameName) const { - return API_URL_BASE + std::string ret = API_URL_BASE + "/jeuInfos.php?devid=" + Utils::String::scramble(API_DEV_U, API_DEV_KEY) + "&devpassword=" + Utils::String::scramble(API_DEV_P, API_DEV_KEY) + "&softname=" + HttpReq::urlEncode(API_SOFT_NAME) + "&output=xml" + "&romnom=" + HttpReq::urlEncode(gameName); + std::string user = Settings::getInstance()->getString("ScreenScraperUser"); + std::string pass = Settings::getInstance()->getString("ScreenScraperPass"); + + if (!user.empty() && !pass.empty()) + ret = ret + "&ssid=" + HttpReq::urlEncode(user) + "&sspassword=" + HttpReq::urlEncode(pass); + + return ret; } diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index 7a8c9ce398..b0020b0e5e 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -28,7 +28,7 @@ class ScreenScraperRequest : public ScraperHttpRequest const std::string API_DEV_P = { 108, 28, 54, 55, 83, 43, 91, 44, 30, 22, 41, 12, 0, 108, 38, 29 }; const std::string API_DEV_KEY = { 54, 73, 115, 100, 101, 67, 111, 107, 79, 66, 68, 66, 67, 56, 118, 77, 54, 88, 101, 54 }; const std::string API_URL_BASE = "https://www.screenscraper.fr/api2"; - const std::string API_SOFT_NAME = "Emulationstation "+static_cast(SCRAPER_VERSION_STRING); + const std::string API_SOFT_NAME = "Emulationstation " + static_cast(PROGRAM_VERSION_STRING); /** Which type of image artwork we need. Possible values (not a comprehensive list): - ss: in-game screenshot @@ -43,11 +43,10 @@ class ScreenScraperRequest : public ScraperHttpRequest Note that no all games contain values for these, so we default to "box-2D" since it's the most common. **/ - //std::string media_image = "ss"; - //std::string media_thumnail = "box-2D"; + //std::string media_name = "box-2D"; // Which Region to use when selecting the artwork - // Applies to: artwork, name of the game, date of release + // Applies to: artwork, name of the game, date of release std::string region = "US"; // Which Language to use when selecting the textual information @@ -68,7 +67,7 @@ class ScreenScraperRequest : public ScraperHttpRequest private: std::vector getRipList(std::string imageSource); - pugi::xml_node findMedia(pugi::xml_node media_list, std::vector mediaNames, std::string region); + pugi::xml_node findMedia(pugi::xml_node media_list, std::vector mediaNames, std::string region); pugi::xml_node findMedia(pugi::xml_node media_list, std::string mediaName, std::string region); }; diff --git a/es-app/src/scrapers/ThreadedScraper.cpp b/es-app/src/scrapers/ThreadedScraper.cpp new file mode 100644 index 0000000000..a77c71998a --- /dev/null +++ b/es-app/src/scrapers/ThreadedScraper.cpp @@ -0,0 +1,170 @@ +#include "ThreadedScraper.h" +#include "Window.h" +#include "FileData.h" +#include "components/AsyncNotificationComponent.h" +#include "EsLocale.h" + +#define GUIICON _U("\uF03E ") + +ThreadedScraper* ThreadedScraper::mInstance = nullptr; +bool ThreadedScraper::mPaused = false; + +ThreadedScraper::ThreadedScraper(Window* window, const std::queue& searches) + : mSearchQueue(searches), mWindow(window) +{ + mExit = false; + mTotal = (int) mSearchQueue.size(); + + mWndNotification = new AsyncNotificationComponent(window); + + mWindow->registerNotificationComponent(mWndNotification); + search(mSearchQueue.front()); + mHandle = new std::thread(&ThreadedScraper::run, this); +} + +ThreadedScraper::~ThreadedScraper() +{ + mWindow->unRegisterNotificationComponent(mWndNotification); + delete mWndNotification; + + ThreadedScraper::mInstance = nullptr; +} + +std::string ThreadedScraper::formatGameName(FileData* game) +{ + return "["+game->getSystemName()+"] " + game->getName(); +} + +void ThreadedScraper::search(const ScraperSearchParams& params) +{ + mCurrentAction = ""; + mLastSearch = params; + mSearchHandle = startScraperSearch(params); + + std::string idx = std::to_string(mTotal + 1- mSearchQueue.size()) + "/" + std::to_string(mTotal); + + mWndNotification->updateTitle(GUIICON + _("SCRAPING") + "... " + idx); + mWndNotification->updateText(formatGameName(params.game), _("Searching")+"..."); + mWndNotification->updatePercent(-1); +} + +void ThreadedScraper::run() +{ + while (!mExit && !mSearchQueue.empty()) + { + if (mPaused) + { + while (!mExit && mPaused) + { + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(500)); + } + } + + if (mSearchHandle && mSearchHandle->status() != ASYNC_IN_PROGRESS) + { + auto status = mSearchHandle->status(); + auto results = mSearchHandle->getResults(); + auto statusString = mSearchHandle->getStatusString(); + + mSearchHandle.reset(); + + if (status == ASYNC_DONE) + { + if (results.size() > 0) + { + if (results[0].hadMedia()) + processMedias(results[0]); + else + acceptResult(results[0]); + } + } + else if (status == ASYNC_ERROR) + mErrors.push_back(statusString); + } + + if (mMDResolveHandle && mMDResolveHandle->status() != ASYNC_IN_PROGRESS) + { + auto status = mMDResolveHandle->status(); + auto result = mMDResolveHandle->getResult(); + auto statusString = mMDResolveHandle->getStatusString(); + + mCurrentAction = ""; + mMDResolveHandle.reset(); + + if (status == ASYNC_DONE) + acceptResult(result); + else if (status == ASYNC_ERROR) + mErrors.push_back(statusString); + } + + if (mMDResolveHandle && mMDResolveHandle->status() == ASYNC_IN_PROGRESS) + { + std::string action = mMDResolveHandle->getCurrentItem(); + if (action != mCurrentAction) + { + mCurrentAction = action; + mWndNotification->updateText(formatGameName(mLastSearch.game), "Downloading "+ mCurrentAction); + } + + mWndNotification->updatePercent(mMDResolveHandle->getPercent()); + } + + if (mSearchHandle == nullptr && mMDResolveHandle == nullptr) + { + mSearchQueue.pop(); + + if (!mSearchQueue.empty()) + search(mSearchQueue.front()); + } + else + { + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + } + } + + if (!mExit) + mWindow->displayNotificationMessage(GUIICON + _("SCRAPING FINISHED. REFRESH UPDATE GAMES LISTS TO APPLY CHANGES.")); + + delete this; + ThreadedScraper::mInstance = nullptr; +} + +void ThreadedScraper::processMedias(ScraperSearchResult result) +{ + ScraperSearchParams& search = mSearchQueue.front(); + + if (result.hadMedia()) + mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch); + + search.game->metadata.importScrappedMetadata(result.mdl); +} + +void ThreadedScraper::acceptResult(const ScraperSearchResult& result) +{ + ScraperSearchParams& search = mSearchQueue.front(); + search.game->metadata = result.mdl; +} + +void ThreadedScraper::start(Window* window, const std::queue& searches) +{ + if (ThreadedScraper::mInstance != nullptr) + return; + + ThreadedScraper::mInstance = new ThreadedScraper(window, searches); +} + +void ThreadedScraper::stop() +{ + auto thread = ThreadedScraper::mInstance; + if (thread == nullptr) + return; + + try + { + thread->mExit = true; + } + catch (...) {} +} + diff --git a/es-app/src/scrapers/ThreadedScraper.h b/es-app/src/scrapers/ThreadedScraper.h new file mode 100644 index 0000000000..56428f002a --- /dev/null +++ b/es-app/src/scrapers/ThreadedScraper.h @@ -0,0 +1,48 @@ +#pragma once + +#include +#include "Scraper.h" +#include "components/AsyncNotificationComponent.h" + +class ThreadedScraper +{ +public: + static void start(Window* window, const std::queue& searches); + static void stop(); + static bool isRunning() { return mInstance != nullptr; } + + static void pause() { mPaused = true; } + static void resume() { mPaused = false; } + +private: + ThreadedScraper(Window* window, const std::queue& searches); + ~ThreadedScraper(); + + Window* mWindow; + AsyncNotificationComponent* mWndNotification; + std::string mCurrentAction; + + std::vector mErrors; + + void run(); + + std::thread* mHandle; + std::queue mSearchQueue; + + ScraperSearchParams mLastSearch; + std::unique_ptr mSearchHandle; + std::unique_ptr mMDResolveHandle; + + void search(const ScraperSearchParams& params); + void processMedias(ScraperSearchResult result); + void acceptResult(const ScraperSearchResult& result); + + std::string formatGameName(FileData* game); + + int mTotal; + bool mExit; + + static bool mPaused; + static ThreadedScraper* mInstance; +}; + diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 2625546080..2f3a3ea61f 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -88,7 +88,7 @@ void SystemView::populate() // Remove dynamic flags for png & jpg files : themes can contain oversized images that can't be unloaded by the TextureResource manager ImageComponent* logo = new ImageComponent(mWindow, false, Utils::String::toLower(Utils::FileSystem::getExtension(path)) != ".svg"); logo->setMaxSize(carouselLogoSize() * mCarousel.logoScale); - logo->applyTheme(theme, "system", "logo", ThemeFlags::PATH | ThemeFlags::SIZE | ThemeFlags::COLOR); + logo->applyTheme(theme, "system", "logo", ThemeFlags::PATH | ThemeFlags::SIZE | ThemeFlags::COLOR | ThemeFlags::ALIGNMENT | ThemeFlags::VISIBLE); logo->setRotateByTargetSize(true); e.data.logo = std::shared_ptr(logo); e.data.logoIsImage = true; @@ -285,9 +285,11 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) cancelAnimation(2); std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); - bool goFast = transition_style == "instant"; - const float infoStartOpacity = mSystemInfo.getOpacity() / 255.f; + int systemInfoDelay = mCarousel.systemInfoDelay; + bool goFast = transition_style == "instant" || systemInfoDelay == 0; + + const float infoStartOpacity = mSystemInfo.getOpacity() / 255.f; Animation* infoFadeOut = new LambdaAnimation( [infoStartOpacity, this] (float t) @@ -322,7 +324,7 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) // ViewController::get()->getGameListView(mEntries.at(mCursor).object); // fake preload // wait 600ms to fade in - setAnimation(infoFadeIn, goFast ? 0 : 500, [this] { + setAnimation(infoFadeIn, systemInfoDelay, [this] { ViewController::get()->getGameListView(mEntries.at(mCursor).object); // fake preload }, false, 2); @@ -518,9 +520,12 @@ void SystemView::getViewElements(const std::shared_ptr& theme) const ThemeData::ThemeElement* sysInfoElem = theme->getElement("system", "systemInfo", "text"); if (sysInfoElem) + { mSystemInfo.applyTheme(theme, "system", "systemInfo", ThemeFlags::ALL); + mSystemInfo.setOpacity(0); + } - const ThemeData::ThemeElement* fixedBackgroundElem = theme->getElement("system", "fixedBackground", "image"); + const ThemeData::ThemeElement* fixedBackgroundElem = theme->getElement("system", "staticBackground", "image"); if (fixedBackgroundElem) { if (mStaticBackground == nullptr) @@ -762,6 +767,7 @@ void SystemView::getDefaultElements(void) mCarousel.logoPos = Vector2f(-1, -1); mCarousel.maxLogoCount = 3; mCarousel.zIndex = 40; + mCarousel.systemInfoDelay = 2000; // System Info Bar mSystemInfo.setSize(mSize.x(), mSystemInfo.getFont()->getLetterHeight()*2.2f); @@ -849,7 +855,7 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) if (elem->has("colorEnd")) mCarousel.colorEnd = elem->get("colorEnd"); if (elem->has("gradientType")) - mCarousel.colorGradientHorizontal = !(elem->get("gradientType").compare("horizontal")); + mCarousel.colorGradientHorizontal = elem->get("gradientType").compare("horizontal"); if (elem->has("logoScale")) mCarousel.logoScale = elem->get("logoScale"); if (elem->has("logoSize")) @@ -877,6 +883,8 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) else mCarousel.logoAlignment = ALIGN_CENTER; } + if (elem->has("systemInfoDelay")) + mCarousel.systemInfoDelay = elem->get("systemInfoDelay"); } void SystemView::onShow() diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index cbc165df3c..b70f5c3ebf 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -44,6 +44,7 @@ struct SystemViewCarousel Vector2f logoSize; Vector2f logoPos; float zIndex; + float systemInfoDelay; }; class SystemView : public IList diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 996883eeac..1a801d049a 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -298,14 +298,18 @@ void ViewController::removeGameListView(SystemData* system) } } -std::shared_ptr ViewController::getGameListView(SystemData* system) +std::shared_ptr ViewController::getGameListView(SystemData* system, bool loadIfnull) { //if we already made one, return that one auto exists = mGameListViews.find(system); if(exists != mGameListViews.cend()) return exists->second; + if (!loadIfnull) + return nullptr; + system->setUIModeFilters(); + system->updateDisplayedGameCount(); //if we didn't, make it, remember it, and return it std::shared_ptr view; @@ -623,6 +627,8 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) system->loadTheme(); system->setUIModeFilters(); + system->updateDisplayedGameCount(); + std::shared_ptr newView = getGameListView(system); // to counter having come from a placeholder @@ -644,7 +650,7 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) mCurrentView->onShow(); } -void ViewController::reloadAll() +void ViewController::reloadAll(Window* window) { ThemeData::setDefaultTheme(nullptr); @@ -673,6 +679,8 @@ void ViewController::reloadAll() cursorMap[(*it)] = NULL; } + float idx = 0; + // load themes, create gamelistviews and reset filters for(auto it = cursorMap.cbegin(); it != cursorMap.cend(); it++) { @@ -681,6 +689,11 @@ void ViewController::reloadAll() if (it->second != NULL) getGameListView(it->first)->setCursor(it->second); + + idx++; + + if (window) + window->renderLoadingScreen(_("Loading..."), (float)idx / (float)cursorMap.size()); } if (SystemData::sSystemVector.size() > 0) diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h index 2abbed762e..9061775983 100644 --- a/es-app/src/views/ViewController.h +++ b/es-app/src/views/ViewController.h @@ -10,6 +10,7 @@ class IGameListView; class SystemData; class SystemView; +class Window; // Used to smoothly transition the camera between multiple views (e.g. from system to system, from gamelist to gamelist). class ViewController : public GuiComponent @@ -28,7 +29,7 @@ class ViewController : public GuiComponent // 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. + void reloadAll(Window* window = nullptr); // Reload everything with a theme. Used when the "ThemeSet" setting changes. // Navigation. void goToNextGameList(); @@ -82,7 +83,7 @@ class ViewController : public GuiComponent virtual std::vector getHelpPrompts() override; virtual HelpStyle getHelpStyle() override; - std::shared_ptr getGameListView(SystemData* system); + std::shared_ptr getGameListView(SystemData* system, bool loadIfnull = true); std::shared_ptr getSystemListView(); void removeGameListView(SystemData* system); diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index c1af84d24f..755f269855 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -226,3 +226,8 @@ std::vector BasicGameListView::getHelpPrompts() } return prompts; } + +std::vector BasicGameListView::getFileDataEntries() +{ + return mList.getObjects(); +} diff --git a/es-app/src/views/gamelist/BasicGameListView.h b/es-app/src/views/gamelist/BasicGameListView.h index 1123f842fd..663aeeca68 100644 --- a/es-app/src/views/gamelist/BasicGameListView.h +++ b/es-app/src/views/gamelist/BasicGameListView.h @@ -33,6 +33,8 @@ class BasicGameListView : public ISimpleGameListView virtual void onShow() override; protected: + virtual std::vector getFileDataEntries() override; + virtual std::string getQuickSystemSelectRightButton() override; virtual std::string getQuickSystemSelectLeftButton() override; virtual void populateList(const std::vector& files) override; diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index cabeec5003..9caca5cc93 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -158,7 +158,7 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them if (theme->getElement(getName(), "md_video", "video")) { createVideo(); - mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION); + mVideo->applyTheme(theme, getName(), "md_video", ALL ^ (PATH)); } else if (mVideo != nullptr) { @@ -170,7 +170,7 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them if (mVideo == nullptr || theme->getElement(getName(), "md_image", "image")) { createImage(); - mImage->applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + mImage->applyTheme(theme, getName(), "md_image", ALL ^ (PATH)); } else if (mImage != nullptr) { @@ -182,7 +182,7 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them if (theme->getElement(getName(), "md_marquee", "image")) { createMarquee(); - mMarquee->applyTheme(theme, getName(), "md_marquee", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + mMarquee->applyTheme(theme, getName(), "md_marquee", ALL ^ (PATH)); } else if (mMarquee != nullptr) { @@ -384,7 +384,17 @@ void DetailedGameListView::updateInfoPanel() { comp->setOpacity((unsigned char)(Math::lerp(0.0f, 1.0f, t)*255)); }; - comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut); + + bool isFadeOut = fadingOut; + comp->setAnimation(new LambdaAnimation(func, 150), 0, [this, isFadeOut] + { + if (isFadeOut) + { + if (mVideo != nullptr) mVideo->setImage(""); + if (mImage != nullptr) mImage->setImage(""); + if (mMarquee != nullptr) mMarquee->setImage(""); + } + }, fadingOut); } } } diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 6579a71d08..ae333cd591 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -262,7 +262,7 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) if (theme->getElement(getName(), "md_image", "image")) { mImageVisible = true; - mImage.applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + mImage.applyTheme(theme, getName(), "md_image", ALL ^ (PATH)); } else mImageVisible = false; @@ -270,7 +270,7 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) if (theme->getElement(getName(), "md_video", "video")) { createVideo(); - mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION); + mVideo->applyTheme(theme, getName(), "md_video", ALL ^ (PATH)); } else if (mVideo != nullptr) { @@ -606,3 +606,8 @@ std::vector GridGameListView::getHelpPrompts() } return prompts; } + +std::vector GridGameListView::getFileDataEntries() +{ + return mGrid.getObjects(); +} diff --git a/es-app/src/views/gamelist/GridGameListView.h b/es-app/src/views/gamelist/GridGameListView.h index cf270692c8..f9c78065b3 100644 --- a/es-app/src/views/gamelist/GridGameListView.h +++ b/es-app/src/views/gamelist/GridGameListView.h @@ -40,6 +40,7 @@ class GridGameListView : public ISimpleGameListView virtual void onShow(); protected: + virtual std::vector getFileDataEntries() override; virtual std::string getQuickSystemSelectRightButton() override; virtual std::string getQuickSystemSelectLeftButton() override; virtual void populateList(const std::vector& files) override; diff --git a/es-app/src/views/gamelist/IGameListView.h b/es-app/src/views/gamelist/IGameListView.h index d9243d4628..3f6d69495f 100644 --- a/es-app/src/views/gamelist/IGameListView.h +++ b/es-app/src/views/gamelist/IGameListView.h @@ -44,6 +44,8 @@ class IGameListView : public GuiComponent virtual void setThemeName(std::string name); + virtual std::vector getEntriesLetters() = 0; + protected: std::string mCustomThemeName; diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index c6cab0c373..3f67112809 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -182,6 +182,24 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) return IGameListView::input(config, input); } +std::vector ISimpleGameListView::getEntriesLetters() +{ + std::set setOfLetters; + + for (auto file : getFileDataEntries()) + if (file->getType() == GAME) + setOfLetters.insert(std::string(1, toupper(file->getName()[0]))); + + std::vector letters; + + for (const auto letter : setOfLetters) + letters.push_back(letter); + + std::sort(letters.begin(), letters.end()); + return letters; +} + + diff --git a/es-app/src/views/gamelist/ISimpleGameListView.h b/es-app/src/views/gamelist/ISimpleGameListView.h index a5b1314cc7..133a447a47 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.h +++ b/es-app/src/views/gamelist/ISimpleGameListView.h @@ -27,7 +27,11 @@ class ISimpleGameListView : public IGameListView virtual bool input(InputConfig* config, Input input) override; virtual void launch(FileData* game) = 0; + virtual std::vector getEntriesLetters() override; + protected: + virtual std::vector getFileDataEntries() = 0; + virtual std::string getQuickSystemSelectRightButton() = 0; virtual std::string getQuickSystemSelectLeftButton() = 0; virtual void populateList(const std::vector& files) = 0; diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index d674e20633..b318bf7fb2 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -135,12 +135,12 @@ void VideoGameListView::onThemeChanged(const std::shared_ptr& theme) BasicGameListView::onThemeChanged(theme); using namespace ThemeFlags; - mMarquee.applyTheme(theme, getName(), "md_marquee", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + mMarquee.applyTheme(theme, getName(), "md_marquee", ALL ^ (PATH)); if (theme->getElement(getName(), "md_image", "image")) { createImage(); - mImage->applyTheme(theme, getName(), "md_image", POSITION | ThemeFlags::SIZE | Z_INDEX | ROTATION); + mImage->applyTheme(theme, getName(), "md_image", ALL ^ (PATH)); } else if (mImage != nullptr) { @@ -149,7 +149,7 @@ void VideoGameListView::onThemeChanged(const std::shared_ptr& theme) mImage = nullptr; } - mVideo->applyTheme(theme, getName(), "md_video", POSITION | ThemeFlags::SIZE | ThemeFlags::DELAY | Z_INDEX | ROTATION); + mVideo->applyTheme(theme, getName(), "md_video", ALL ^ (PATH)); mName.applyTheme(theme, getName(), "md_name", ALL); initMDLabels(); diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index c28d17882b..8aafb040ac 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -27,6 +27,7 @@ set(CORE_HEADERS # GuiComponents ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimatedImageComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncNotificationComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ButtonComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentGrid.h @@ -48,11 +49,12 @@ set(CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextEditComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoPlayerComponent.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoVlcComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoVlcComponent.h # Guis ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiDetectDevice.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInputConfig.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMsgBox.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopup.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopupKeyboard.h @@ -107,6 +109,7 @@ set(CORE_SOURCES # GuiComponents ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimatedImageComponent.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncNotificationComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/BusyComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ButtonComponent.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentGrid.cpp @@ -130,6 +133,7 @@ set(CORE_SOURCES # Guis ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiDetectDevice.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInputConfig.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiInfoPopup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiMsgBox.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopup.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiTextEditPopupKeyboard.cpp diff --git a/es-core/src/AsyncHandle.h b/es-core/src/AsyncHandle.h index 65ffb5317a..5368dae968 100644 --- a/es-core/src/AsyncHandle.h +++ b/es-core/src/AsyncHandle.h @@ -23,6 +23,8 @@ class AsyncHandle // Update and return the latest status. inline AsyncHandleStatus status() { update(); return mStatus; } + virtual int getPercent() { return -1; } + // User-friendly string of our current status. Will return error message if status() == SEARCH_ERROR. inline std::string getStatusString() { diff --git a/es-core/src/AudioManager.cpp b/es-core/src/AudioManager.cpp index 79710e82dd..7a28b56b3a 100644 --- a/es-core/src/AudioManager.cpp +++ b/es-core/src/AudioManager.cpp @@ -239,29 +239,37 @@ void AudioManager::stopMusic() mCurrentMusic = NULL; } -void AudioManager::themeChanged(const std::shared_ptr& theme) +void AudioManager::themeChanged(const std::shared_ptr& theme, bool force) { + if (!force && mSystemName == theme->getSystemThemeFolder()) + return; + + mSystemName = theme->getSystemThemeFolder(); mCurrentThemeMusicDirectory = ""; if (!Settings::getInstance()->getBool("audio.bgmusic")) return; const ThemeData::ThemeElement* elem = theme->getElement("system", "directory", "sound"); - if (elem && elem->has("path")) - mCurrentThemeMusicDirectory = elem->get("path"); - std::string bgSound; + if (Settings::getInstance()->getBool("audio.thememusics")) + { + if (elem && elem->has("path")) + mCurrentThemeMusicDirectory = elem->get("path"); - elem = theme->getElement("system", "bgsound", "sound"); - if (elem && elem->has("path") && Utils::FileSystem::exists(elem->get("path"))) - bgSound = elem->get("path"); + std::string bgSound; - // Found a music for the system - if (!bgSound.empty()) - { - mRunningFromPlaylist = false; - playMusic(bgSound); - return; + elem = theme->getElement("system", "bgsound", "sound"); + if (elem && elem->has("path") && Utils::FileSystem::exists(elem->get("path"))) + bgSound = elem->get("path"); + + // Found a music for the system + if (!bgSound.empty()) + { + mRunningFromPlaylist = false; + playMusic(bgSound); + return; + } } mSystemName = theme->getSystemThemeFolder(); diff --git a/es-core/src/AudioManager.h b/es-core/src/AudioManager.h index b3d5ff7728..147b9d42d1 100644 --- a/es-core/src/AudioManager.h +++ b/es-core/src/AudioManager.h @@ -29,7 +29,7 @@ class AudioManager void playRandomMusic(bool continueIfPlaying = true); void stopMusic(); - void themeChanged(const std::shared_ptr& theme); + void themeChanged(const std::shared_ptr& theme, bool force=false); void setSystemName(std::string name) { mSystemName = name; diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index 3a602b4379..50f60ac188 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -4,6 +4,17 @@ #include "Log.h" #include +#include + +#ifdef WIN32 +#include +#else +#include +#endif + +#include +static std::mutex mMutex; + CURLM* HttpReq::s_multi_handle = curl_multi_init(); std::map HttpReq::s_requests; @@ -33,7 +44,7 @@ std::string HttpReq::urlEncode(const std::string &s) bool HttpReq::isUrl(const std::string& str) { //the worst guess - return (!str.empty() && !Utils::FileSystem::exists(str) && + return (!str.empty() && !Utils::FileSystem::exists(str) && (str.find("http://") != std::string::npos || str.find("https://") != std::string::npos || str.find("www.") != std::string::npos)); } @@ -67,7 +78,7 @@ std::string _regGetString(HKEY hKey, const std::string &strPath, const std::stri { char szBuffer[1024]; DWORD dwBufferSize = sizeof(szBuffer); - + nRet = ::RegQueryValueExA(hSubKey, strValueName.c_str(), 0, NULL, (LPBYTE)szBuffer, &dwBufferSize); ::RegCloseKey(hSubKey); @@ -82,6 +93,7 @@ std::string _regGetString(HKEY hKey, const std::string &strPath, const std::stri HttpReq::HttpReq(const std::string& url) : mStatus(REQ_IN_PROGRESS), mHandle(NULL) { + mPercent = -1; mHandle = curl_easy_init(); if(mHandle == NULL) @@ -119,7 +131,7 @@ HttpReq::HttpReq(const std::string& url) } //set curl restrict redirect protocols - err = curl_easy_setopt(mHandle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); + err = curl_easy_setopt(mHandle, CURLOPT_REDIR_PROTOCOLS, CURLPROTO_HTTP | CURLPROTO_HTTPS); if(err != CURLE_OK) { mStatus = REQ_IO_ERROR; @@ -146,6 +158,7 @@ HttpReq::HttpReq(const std::string& url) } #ifdef WIN32 + // Setup system proxy on Windows if required if (_regGetDWORD(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", "ProxyEnable")) { auto proxyServer = _regGetString(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", "ProxyServer"); @@ -172,11 +185,43 @@ HttpReq::HttpReq(const std::string& url) } } #endif + + std::unique_lock lock(mMutex); + +#if defined(WIN32) + srand(time(NULL) % getpid()); + std::string TempPath; + char lpTempPathBuffer[MAX_PATH]; + if (GetTempPathA(MAX_PATH, lpTempPathBuffer)) + { + + TCHAR szTempFileName[MAX_PATH]; + + if (GetTempFileName(lpTempPathBuffer, TEXT("httpreq"), 0, szTempFileName)) + mStreamPath = std::string(szTempFileName); + else + { + do { mStreamPath = std::string(lpTempPathBuffer) + "httpreq" + std::to_string(rand() % 99999) + ".tmp"; } + while (Utils::FileSystem::exists(mStreamPath)); + } + } + +#else + srand(time(NULL) % getpid() + getppid()); + + do { mStreamPath = "/tmp/httpreq" + std::to_string(rand() % 99999) + ".tmp"; } + while (Utils::FileSystem::exists(mStreamPath)); +#endif + + mStream.open(mStreamPath, std::ios_base::out | std::ios_base::binary); //add the handle to our multi CURLMcode merr = curl_multi_add_handle(s_multi_handle, mHandle); if(merr != CURLM_OK) { + if (mStream.is_open()) + mStream.close(); + mStatus = REQ_IO_ERROR; onError(curl_multi_strerror(merr)); return; @@ -187,6 +232,16 @@ HttpReq::HttpReq(const std::string& url) HttpReq::~HttpReq() { + std::unique_lock lock(mMutex); + + if (mStream.is_open()) + { + mStream.flush(); + mStream.close(); + } + + Utils::FileSystem::removeFile(mStreamPath); + if(mHandle) { s_requests.erase(mHandle); @@ -202,12 +257,17 @@ HttpReq::~HttpReq() HttpReq::Status HttpReq::status() { + std::unique_lock lock(mMutex); + if(mStatus == REQ_IN_PROGRESS) { int handle_count; CURLMcode merr = curl_multi_perform(s_multi_handle, &handle_count); if(merr != CURLM_OK && merr != CURLM_CALL_MULTI_PERFORM) { + if (mStream.is_open()) + mStream.close(); + mStatus = REQ_IO_ERROR; onError(curl_multi_strerror(merr)); return mStatus; @@ -219,18 +279,25 @@ HttpReq::Status HttpReq::status() { if(msg->msg == CURLMSG_DONE) { - HttpReq* req = s_requests[msg->easy_handle]; - + HttpReq* req = s_requests[msg->easy_handle]; if(req == NULL) { LOG(LogError) << "Cannot find easy handle!"; continue; } + if (req->mStream.is_open()) + { + req->mStream.flush(); + req->mStream.close(); + } + if(msg->data.result == CURLE_OK) { req->mStatus = REQ_SUCCESS; - }else{ + } + else + { req->mStatus = REQ_IO_ERROR; req->onError(curl_easy_strerror(msg->data.result)); } @@ -241,10 +308,24 @@ HttpReq::Status HttpReq::status() return mStatus; } -std::string HttpReq::getContent() const +std::string HttpReq::getContent() { assert(mStatus == REQ_SUCCESS); - return mContent.str(); + + if (mStream.is_open()) + { + mStream.flush(); + mStream.close(); + } + + std::ifstream t(mStreamPath); + t.seekg(0, std::ios::end); + size_t size = t.tellg(); + std::string buffer(size, ' '); + t.seekg(0); + t.read(&buffer[0], size); + + return buffer; // mContent.str(); } void HttpReq::onError(const char* msg) @@ -262,14 +343,56 @@ std::string HttpReq::getErrorMsg() //return value is number of elements successfully read size_t HttpReq::write_content(void* buff, size_t size, size_t nmemb, void* req_ptr) { - std::stringstream& ss = ((HttpReq*)req_ptr)->mContent; + HttpReq* request = ((HttpReq*)req_ptr); + + std::ofstream& ss = request->mStream; ss.write((char*)buff, size * nmemb); + double cl; + if (!curl_easy_getinfo(request->mHandle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &cl)) + { + if (cl <= 0) + request->mPercent = -1; + else + { + double position = (double)ss.tellp(); + request->mPercent = (int) (position * 100.0 / cl); + } + } + return nmemb; } -//used as a curl callback -/*int HttpReq::update_progress(void* req_ptr, double dlTotal, double dlNow, double ulTotal, double ulNow) +bool HttpReq::saveContent(const std::string filename) { + assert(mStatus == REQ_SUCCESS); + + if (mStream.is_open()) + { + mStream.flush(); + mStream.close(); + } -}*/ + if (!Utils::FileSystem::exists(mStreamPath)) + return false; + + std::ifstream ifs(mStreamPath, std::ios_base::in | std::ios_base::binary); + if (ifs.bad()) + return false; + + if (Utils::FileSystem::exists(filename)) + Utils::FileSystem::removeFile(filename); + + std::ofstream ofs(filename, std::ios_base::out | std::ios_base::binary); + if (ofs.bad()) + return false; + + ofs << ifs.rdbuf(); + + ifs.close(); + ofs.close(); + if (ofs.bad()) + return false; + + return true; +} \ No newline at end of file diff --git a/es-core/src/HttpReq.h b/es-core/src/HttpReq.h index f6032a3c76..3681886d76 100644 --- a/es-core/src/HttpReq.h +++ b/es-core/src/HttpReq.h @@ -5,12 +5,13 @@ #include #include #include +#include /* Usage: * HttpReq myRequest("www.google.com", "/index.html"); * //for blocking behavior: while(myRequest.status() == HttpReq::REQ_IN_PROGRESS); * //for non-blocking behavior: check if(myRequest.status() != HttpReq::REQ_IN_PROGRESS) in some sort of update method - * + * * //once one of those completes, the request is ready * if(myRequest.status() != REQ_SUCCESS) * { @@ -44,11 +45,16 @@ class HttpReq std::string getErrorMsg(); - std::string getContent() const; // mStatus must be REQ_SUCCESS + std::string getContent(); // mStatus must be REQ_SUCCESS + + + bool saveContent(const std::string filename); static std::string urlEncode(const std::string &s); static bool isUrl(const std::string& s); + int getPercent() { return mPercent; } + private: static size_t write_content(void* buff, size_t size, size_t nmemb, void* req_ptr); //static int update_progress(void* req_ptr, double dlTotal, double dlNow, double ulTotal, double ulNow); @@ -65,8 +71,13 @@ class HttpReq Status mStatus; - std::stringstream mContent; + std::string mStreamPath; + std::ofstream mStream; + + //std::stringstream mContent; std::string mErrorMsg; + + int mPercent; }; #endif // ES_CORE_HTTP_REQ_H diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index b499093ea4..59686558ec 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -12,20 +12,19 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) { + LOG(LogDebug) << "ImageIO::loadImageSize " << fn; + auto ext = Utils::String::toLower(Utils::FileSystem::getExtension(fn)); if (ext != ".jpg" && ext != ".png" && ext != ".jpeg" && ext != ".gif") + { + LOG(LogWarning) << "ImageIO::loadImageSize\tUnknown file type"; return false; - - FILE *f = fopen(fn, "rb"); - if (f == 0) - return false; - - fseek(f, 0, SEEK_END); - long len = ftell(f); - fseek(f, 0, SEEK_SET); + } - if (len < 24) { - fclose(f); + FILE *f = fopen(fn, "rb"); + if (f == 0) + { + LOG(LogWarning) << "ImageIO::loadImageSize\tUnable to open file"; return false; } @@ -34,15 +33,19 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) // reading PNG dimensions requires the first 24 bytes of the file // reading JPEG dimensions requires scanning through jpeg chunks // In all formats, the file is at least 24 bytes big, so we'll read that always - unsigned char buf[24]; fread(buf, 1, 24, f); - + unsigned char buf[24]; + if (fread(buf, 1, 24, f) != 24) + return false; // For JPEGs, we need to read the first 12 bytes of each chunk. // We'll read those 12 bytes at buf+2...buf+14, i.e. overwriting the existing buf. + bool jfif = false; if ((buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF && buf[3] == 0xE0 && buf[6] == 'J' && buf[7] == 'F' && buf[8] == 'I' && buf[9] == 'F') || (buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF && buf[3] == 0xE1 && buf[6] == 'E' && buf[7] == 'x' && buf[8] == 'i' && buf[9] == 'f')) { + jfif = true; + long pos = 2; while (buf[2] == 0xFF) { @@ -50,20 +53,25 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) break; pos += 2 + (buf[4] << 8) + buf[5]; - if (pos + 12 > len) break; - fseek(f, pos, SEEK_SET); - fread(buf + 2, 1, 12, f); + + if (fseek(f, pos, SEEK_SET) != 0) + break; + + if (fread(buf + 2, 1, 12, f) != 12) + break; } } fclose(f); // JPEG: (first two bytes of buf are first two bytes of the jpeg file; rest of buf is the DCT frame - if (buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF) + if (jfif && buf[0] == 0xFF && buf[1] == 0xD8 && buf[2] == 0xFF) { *y = (buf[7] << 8) + buf[8]; *x = (buf[9] << 8) + buf[10]; + LOG(LogDebug) << "ImageIO::loadImageSize\tJPG size " << std::string(std::to_string(*x) + "x" + std::to_string(*y)).c_str(); + if (*x > 5000) // security ? return false; @@ -75,6 +83,9 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) { *x = buf[6] + (buf[7] << 8); *y = buf[8] + (buf[9] << 8); + + LOG(LogDebug) << "ImageIO::loadImageSize\tGIF size " << std::string(std::to_string(*x) + "x" + std::to_string(*y)).c_str(); + return true; } @@ -84,9 +95,12 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) *x = (buf[16] << 24) + (buf[17] << 16) + (buf[18] << 8) + (buf[19] << 0); *y = (buf[20] << 24) + (buf[21] << 16) + (buf[22] << 8) + (buf[23] << 0); + LOG(LogDebug) << "ImageIO::loadImageSize\tPNG size " << std::string(std::to_string(*x) + "x" + std::to_string(*y)).c_str(); + return true; } + LOG(LogWarning) << "ImageIO::loadImageSize\tUnable to extract size"; return false; } diff --git a/es-core/src/Log.cpp b/es-core/src/Log.cpp index 951286519e..a2634c171b 100644 --- a/es-core/src/Log.cpp +++ b/es-core/src/Log.cpp @@ -3,9 +3,18 @@ #include "utils/FileSystemUtil.h" #include "platform.h" #include +#include +#include "Settings.h" + +#if WIN32 +#include +#endif + +static std::mutex mLogLock; LogLevel Log::reportingLevel = LogInfo; -FILE* Log::file = NULL; //fopen(getLogPath().c_str(), "w"); +bool Log::dirty = false; +FILE* Log::file = NULL; LogLevel Log::getReportingLevel() { @@ -25,20 +34,44 @@ void Log::setReportingLevel(LogLevel level) void Log::init() { + std::unique_lock lock(mLogLock); + + if (file != NULL) + close(); + + if (Settings::getInstance()->getString("LogLevel") == "disabled") + { + remove(getLogPath().c_str()); + return; + } + remove((getLogPath() + ".bak").c_str()); + // rename previous log file rename(getLogPath().c_str(), (getLogPath() + ".bak").c_str()); - return; -} -void Log::open() -{ file = fopen(getLogPath().c_str(), "w"); + dirty = false; } std::ostringstream& Log::get(LogLevel level) { - os << "lvl" << level << ": \t"; + switch (level) + { + case LogError: + os << "ERROR\t"; + break; + case LogWarning: + os << "WARNING\t"; + break; + case LogDebug: + os << "DEBUG\t"; + break; + default: + os << "INFO\t"; + break; + } + messageLevel = level; return os; @@ -46,36 +79,68 @@ std::ostringstream& Log::get(LogLevel level) void Log::flush() { - fflush(getOutput()); + if (!dirty) + return; + + if (file != nullptr) + fflush(file); + + dirty = false; } void Log::close() { - fclose(file); + if (file != NULL) + { + fflush(file); + fclose(file); + } + + dirty = false; file = NULL; } -FILE* Log::getOutput() +Log::~Log() { - return file; + std::unique_lock lock(mLogLock); + + if (file != NULL) + { + os << std::endl; + fprintf(file, "%s", os.str().c_str()); + dirty = true; + } + + // If it's an error, also print to console + // print all messages if using --debug + if (messageLevel == LogError || reportingLevel >= LogDebug) + { +#if WIN32 + OutputDebugStringA(os.str().c_str()); +#else + fprintf(stderr, "%s", os.str().c_str()); +#endif + } } -Log::~Log() +void Log::setupReportingLevel() { - os << std::endl; + LogLevel lvl = LogInfo; - if(getOutput() == NULL) + if (Settings::getInstance()->getBool("Debug")) + lvl = LogDebug; + else { - // not open yet, print to stdout - std::cerr << "ERROR - tried to write to log file before it was open! The following won't be logged:\n"; - std::cerr << os.str(); - return; + auto level = Settings::getInstance()->getString("LogLevel"); + if (level == "debug") + lvl = LogDebug; + else if (level == "information") + lvl = LogInfo; + else if (level == "warning") + lvl = LogWarning; + else if (level == "error") + lvl = LogError; } - fprintf(getOutput(), "%s", os.str().c_str()); - - //if it's an error, also print to console - //print all messages if using --debug - if(messageLevel == LogError || reportingLevel >= LogDebug) - fprintf(stderr, "%s", os.str().c_str()); + setReportingLevel(lvl); } diff --git a/es-core/src/Log.h b/es-core/src/Log.h index 0b69a74feb..1c005cf46f 100644 --- a/es-core/src/Log.h +++ b/es-core/src/Log.h @@ -3,10 +3,13 @@ #define ES_CORE_LOG_H #include +#include -#define LOG(level) \ -if(level > Log::getReportingLevel()) ; \ -else Log().get(level) +#define LOG(level) if(!Log::Enabled() || level > Log::getReportingLevel()) ; else Log().get(level) + +#define TRYCATCH(m, x) try { x; } \ +catch (const std::exception& e) { LOG(LogError) << m << " Exception " << e.what(); Log::flush(); throw e; } \ +catch (...) { LOG(LogError) << m << " Unknown Exception occured"; Log::flush(); throw; } enum LogLevel { LogError, LogWarning, LogInfo, LogDebug }; @@ -19,19 +22,24 @@ class Log static LogLevel getReportingLevel(); static void setReportingLevel(LogLevel level); + static void setupReportingLevel(); static std::string getLogPath(); static void flush(); static void init(); - static void open(); static void close(); + + static inline bool Enabled() { return file != NULL; } + protected: std::ostringstream os; static FILE* file; + private: static LogLevel reportingLevel; - static FILE* getOutput(); + static bool dirty; + LogLevel messageLevel; }; diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 73f3126a50..268e225f1e 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -62,6 +62,8 @@ Settings* Settings::getInstance() void Settings::setDefaults() { + mWasChanged = false; + mBoolMap.clear(); mIntMap.clear(); @@ -94,6 +96,8 @@ void Settings::setDefaults() mStringMap["StartupSystem"] = ""; + mBoolMap["FlatFolders"] = false; + mBoolMap["VSync"] = true; mBoolMap["EnableSounds"] = true; mBoolMap["ShowHelpPrompts"] = true; @@ -135,8 +139,7 @@ void Settings::setDefaults() mStringMap["TransitionStyle"] = "slide"; mStringMap["Language"] = "en"; mStringMap["ThemeSet"] = ""; - mStringMap["ScreenSaverBehavior"] = "dim"; - mStringMap["Scraper"] = "TheGamesDB"; + mStringMap["ScreenSaverBehavior"] = "dim"; mStringMap["GamelistViewStyle"] = "automatic"; mStringMap["DefaultGridSize"] = ""; @@ -215,13 +218,16 @@ void Settings::setDefaults() mIntMap["MonitorID"] = -1; mStringMap["ExePath"] = ""; + mStringMap["Scraper"] = "ScreenScraper"; + mStringMap["ScrapperImageSrc"] = "ss"; + mStringMap["ScrapperThumbSrc"] = "box-2D"; - mStringMap["ScrapperImageSrc"] = "box-2D"; - mStringMap["ScrapperThumbSrc"] = ""; + mBoolMap["ScrapeMarquee"] = false; mBoolMap["ScrapeVideos"] = false; mBoolMap["audio.bgmusic"] = true; mBoolMap["audio.persystem"] = false; + mBoolMap["audio.thememusics"] = true; mStringMap["MusicDirectory"] = ""; mStringMap["UserMusicDirectory"] = ""; @@ -251,8 +257,13 @@ void saveMap(pugi::xml_node& doc, std::map& map, const char* type, std::ma } } -void Settings::saveFile() +bool Settings::saveFile() { + if (!mWasChanged) + return false; + + mWasChanged = false; + LOG(LogDebug) << "Settings::saveFile() : Saving Settings to file."; const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_settings.cfg"; @@ -289,6 +300,8 @@ void Settings::saveFile() Scripting::fireEvent("config-changed"); Scripting::fireEvent("settings-changed"); + + return true; } void Settings::loadFile() @@ -324,6 +337,8 @@ void Settings::loadFile() setFloat(node.attribute("name").as_string(), node.attribute("value").as_float()); for(pugi::xml_node node = root.child("string"); node; node = node.next_sibling("string")) setString(node.attribute("name").as_string(), node.attribute("value").as_string()); + + mWasChanged = false; } //Print a warning message if the setting we're trying to get doesn't already exist in the map, then return the value in the map. @@ -336,9 +351,17 @@ void Settings::loadFile() } \ return mapName[name]; \ } \ -void Settings::setMethodName(const std::string& name, type value) \ +bool Settings::setMethodName(const std::string& name, type value) \ { \ - mapName[name] = value; \ + if (mapName.count(name) == 0 || mapName[name] != value) { \ + mapName[name] = value; \ +\ + if (std::find(settings_dont_save.cbegin(), settings_dont_save.cend(), name) == settings_dont_save.cend()) \ + mWasChanged = true; \ +\ + return true; \ + } \ + return false; \ } SETTINGS_GETSET(bool, mBoolMap, getBool, setBool, false); diff --git a/es-core/src/Settings.h b/es-core/src/Settings.h index 8eead71d4b..5c2d28664a 100644 --- a/es-core/src/Settings.h +++ b/es-core/src/Settings.h @@ -11,7 +11,7 @@ class Settings static Settings* getInstance(); void loadFile(); - void saveFile(); + bool saveFile(); //You will get a warning if you try a get on a key that is not already present. bool getBool(const std::string& name); @@ -19,10 +19,10 @@ class Settings float getFloat(const std::string& name); const std::string& getString(const std::string& name); - void setBool(const std::string& name, bool value); - void setInt(const std::string& name, int value); - void setFloat(const std::string& name, float value); - void setString(const std::string& name, const std::string& value); + bool setBool(const std::string& name, bool value); + bool setInt(const std::string& name, int value); + bool setFloat(const std::string& name, float value); + bool setString(const std::string& name, const std::string& value); private: static Settings* sInstance; @@ -43,6 +43,7 @@ class Settings std::map mDefaultFloatMap; std::map mDefaultStringMap; + bool mWasChanged; bool mHasConfigRoot; }; diff --git a/es-core/src/Sound.cpp b/es-core/src/Sound.cpp index 59eb101bcd..6b621e194b 100644 --- a/es-core/src/Sound.cpp +++ b/es-core/src/Sound.cpp @@ -89,6 +89,9 @@ void Sound::play() if (!AudioManager::isInitialized()) return; + if (!Settings::getInstance()->getBool("EnableSounds")) + return; + mPlaying = true; Mix_PlayChannel(-1, mSampleData, 0); } diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 318bfcea7a..0aa7ce9d9d 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -36,6 +36,11 @@ std::map> The { "gradientType", STRING }, { "visible", BOOLEAN }, { "reflexion", NORMALIZED_PAIR }, + { "reflexionOnFrame", BOOLEAN }, + { "horizontalAlignment", STRING }, + { "verticalAlignment", STRING }, + { "flipX", BOOLEAN }, + { "flipY", BOOLEAN }, { "zIndex", FLOAT } } }, { "imagegrid", { { "pos", NORMALIZED_PAIR }, @@ -87,6 +92,7 @@ std::map> The { "glowColor", COLOR }, { "glowSize", FLOAT }, + { "glowOffset", NORMALIZED_PAIR }, { "padding", NORMALIZED_RECT }, @@ -169,7 +175,18 @@ std::map> The { "textColor", COLOR }, { "iconColor", COLOR }, { "fontPath", PATH }, - { "fontSize", FLOAT } } }, + { "fontSize", FLOAT }, + { "iconUpDown", PATH }, + { "iconLeftRight", PATH }, + { "iconUpDownLeftRight", PATH }, + { "iconA", PATH }, + { "iconB", PATH }, + { "iconX", PATH }, + { "iconY", PATH }, + { "iconL", PATH }, + { "iconR", PATH }, + { "iconStart", PATH }, + { "iconSelect", PATH } } }, { "video", { { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, @@ -201,6 +218,7 @@ std::map> The { "logoPos", NORMALIZED_PAIR }, { "logoAlignment", STRING }, { "maxLogoCount", FLOAT }, + { "systemInfoDelay", FLOAT }, { "zIndex", FLOAT } } }, { "menuText", { @@ -239,6 +257,10 @@ std::map> The { "pathOn", PATH }, { "pathOff", PATH } } }, + { "menuTextEdit",{ + { "active", PATH }, + { "inactive", PATH } } }, + { "menuSlider",{ { "path", PATH } } }, @@ -305,8 +327,6 @@ std::string ThemeData::resolvePlaceholders(const char* in) ThemeData::ThemeData() { - mHasSubsets = false; - mColorset = Settings::getInstance()->getString("ThemeColorSet"); mIconset = Settings::getInstance()->getString("ThemeIconSet"); mMenu = Settings::getInstance()->getString("ThemeMenu"); @@ -326,7 +346,7 @@ void ThemeData::loadFile(std::string system, std::map if(!Utils::FileSystem::exists(path)) throw error << "File does not exist!"; - mHasSubsets = false; + mSubsets.clear(); mVersion = 0; mViews.clear(); @@ -413,11 +433,23 @@ bool ThemeData::parseSubset(const pugi::xml_node& node) if (!node.attribute("subset")) return true; - mHasSubsets = true; - const std::string subsetAttr = node.attribute("subset").as_string(); const std::string nameAttr = node.attribute("name").as_string(); + if (!subsetAttr.empty()) + { + bool add = true; + + for (auto sb : mSubsets) { + if (sb.subset == subsetAttr && sb.name == nameAttr) { + add = false; break; + } + } + + if (add) + mSubsets.push_back(Subset(subsetAttr, nameAttr)); + } + if (subsetAttr == "colorset" && (nameAttr == mColorset || (mColorset.empty() && isFirstSubset(node)))) return true; @@ -714,31 +746,43 @@ void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view, bool over bool ThemeData::parseRegion(const pugi::xml_node& node) { - bool parse = true; + if (!node.attribute("region")) + return true; + + const std::string nameAttr = node.attribute("region").as_string(); - if (node.attribute("region")) + if (!nameAttr.empty()) { - std::string regionsetting = Settings::getInstance()->getString("ThemeRegionName"); + bool add = true; - parse = false; - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("region").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while (off != std::string::npos || prevOff != std::string::npos) - { - std::string elemKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - if (elemKey == regionsetting) - { - parse = true; - return parse; + for (auto sb : mSubsets) { + if (sb.subset == "region" && sb.name == nameAttr) { + add = false; break; } } + if (add) + mSubsets.push_back(Subset("region", nameAttr)); } - return parse; + + std::string regionsetting = Settings::getInstance()->getString("ThemeRegionName"); + if (regionsetting.empty()) + regionsetting = "eu"; + + const char* delim = " \t\r\n,"; + + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + if (elemKey == regionsetting) + return true; + } + + return false; } void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element, bool overwrite) @@ -1111,7 +1155,7 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) if (elem->has("selectorColorEnd")) Text.selectorGradientColor = elem->get("selectorColorEnd"); if (elem->has("selectorGradientType")) - Text.selectorGradientType = !(elem->get("selectorGradientType").compare("horizontal")); + Text.selectorGradientType = elem->get("selectorGradientType").compare("horizontal"); } elem = theme->getElement("menu", "menubutton", "menuButton"); @@ -1123,6 +1167,15 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) Icons.button_filled = elem->get("filledPath"); } + elem = theme->getElement("menu", "menutextedit", "menuTextEdit"); + if (elem) + { + if (elem->has("active") && ResourceManager::getInstance()->fileExists(elem->get("active"))) + Icons.textinput_ninepatch_active = elem->get("active"); + if (elem->has("inactive") && ResourceManager::getInstance()->fileExists(elem->get("inactive"))) + Icons.textinput_ninepatch = elem->get("inactive"); + } + elem = theme->getElement("menu", "menuswitch", "menuSwitch"); if (elem) { @@ -1148,61 +1201,6 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) } } -std::vector ThemeData::getThemeSubSets(const std::string& theme) -{ - std::vector sets; - - std::deque dequepath; - - static const size_t pathCount = 2; - std::string paths[pathCount] = - { - "/etc/emulationstation/themes", - Utils::FileSystem::getHomePath() + "/.emulationstation/themes" - }; - - for (size_t i = 0; i < pathCount; i++) - { - if (!Utils::FileSystem::isDirectory(paths[i])) - continue; - - auto dirs = Utils::FileSystem::getDirInfo(paths[i] + "/" + theme); - for (auto it = dirs.cbegin(); it != dirs.cend(); ++it) - { - if (!it->directory || it->hidden) - continue; - - std::string path = it->path + "/theme.xml"; - if (!Utils::FileSystem::exists(path)) - continue; - - dequepath.push_back(path); - pugi::xml_document doc; - doc.load_file(path.c_str()); - - pugi::xml_node root = doc.child("theme"); - crawlIncludes(root, sets, dequepath); - findRegion(doc, sets); - dequepath.pop_back(); - } - - std::string path = paths[i] + "/" + theme + "/theme.xml"; - if (!Utils::FileSystem::exists(path)) - continue; - - dequepath.push_back(path); - pugi::xml_document doc; - doc.load_file(path.c_str()); - - pugi::xml_node root = doc.child("theme"); - crawlIncludes(root, sets, dequepath); - findRegion(doc, sets); - dequepath.pop_back(); - } - - return sets; -} - std::vector ThemeData::getSubSet(const std::vector& subsets, const std::string& subset) { std::vector ret; @@ -1216,52 +1214,6 @@ std::vector ThemeData::getSubSet(const std::vector& subsets return ret; } -void ThemeData::crawlIncludes(const pugi::xml_node& root, std::vector& sets, std::deque& dequepath) -{ - for (pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) - { - std::string name = node.attribute("name").as_string(); - std::string subset = node.attribute("subset").as_string(); - if (!subset.empty()) - { - sets.push_back(Subset(subset, name)); - } - // sets.insert(std::pair(name, subset)); - - const char* relPath = node.text().get(); - std::string path = Utils::FileSystem::resolveRelativePath(relPath, dequepath.back(), true); - - dequepath.push_back(path); - pugi::xml_document includeDoc; - includeDoc.load_file(path.c_str()); - - pugi::xml_node root = includeDoc.child("theme"); - crawlIncludes(root, sets, dequepath); - findRegion(includeDoc, sets); - dequepath.pop_back(); - } -} - -void ThemeData::findRegion(const pugi::xml_document& doc, std::vector& sets) -{ - pugi::xpath_node_set regionattr = doc.select_nodes("//@region"); - for (auto xpath_node : regionattr) - { - if (xpath_node.attribute() == nullptr) - continue; - - std::string elemKey = xpath_node.attribute().value(); - if (elemKey.empty()) - continue; - - for (auto sb : sets) - if (sb.subset == "region" && sb.name == elemKey) - return; - - sets.push_back(Subset("region", elemKey)); - } -} - void ThemeData::parseTheme(const pugi::xml_node& root) { if (root.attribute("defaultView")) diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 1e319fe874..5ad50bf6a4 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -119,6 +119,8 @@ struct IconElement { std::string option_arrow; std::string arrow; std::string knob; + std::string textinput_ninepatch; + std::string textinput_ninepatch_active; }; class ThemeData @@ -134,7 +136,7 @@ class ThemeData MenuElement Text{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0x878787FF, true, "", "", nullptr }; MenuElement TextSmall{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0x878787FF, true, "", "", nullptr }; MenuElement Footer{ 0xC6C6C6FF, 0xC6C6C6FF, 0xC6C6C6FF, 0xFFFFFFFF, 0xC6C6C6FF, true, "", "", nullptr }; - IconElement Icons { ":/button.png", ":/button_filled.png", ":/on.svg", ":/off.svg", ":/option_arrow.svg", ":/arrow.svg", ":/slider_knob.svg" }; + IconElement Icons { ":/button.png", ":/button_filled.png", ":/on.svg", ":/off.svg", ":/option_arrow.svg", ":/arrow.svg", ":/slider_knob.svg", ":/textinput_ninepatch.png", ":/textinput_ninepatch_active.png" }; std::string getMenuIcon(const std::string name) { @@ -240,13 +242,12 @@ class ThemeData std::vector getViewsOfTheme(); - bool hasSubsets() { return mHasSubsets; } + bool hasSubsets() { return mSubsets.size() > 0; } + std::vector getSubSets() { return mSubsets; } + static std::vector getSubSet(const std::vector& subsets, const std::string& subset); static const std::shared_ptr& getMenuTheme(); - static std::vector getThemeSubSets(const std::string& theme); - static std::vector getSubSet(const std::vector& subsets, const std::string& subset); - static void setDefaultTheme(ThemeData* theme) { mCurrentTheme = theme; }; static ThemeData* getDefaultTheme() { return mCurrentTheme; }; @@ -297,12 +298,12 @@ class ThemeData std::string mSystemThemeFolder; - std::map mVariables; - - bool mHasSubsets; + std::map mVariables; static std::shared_ptr mMenuTheme; static ThemeData* mCurrentTheme; + + std::vector mSubsets; }; #endif // ES_CORE_THEME_DATA_H diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 90d147ed64..d769104159 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -10,6 +10,8 @@ #include #include #include +#include "guis/GuiInfoPopup.h" +#include "components/AsyncNotificationComponent.h" Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL) @@ -258,6 +260,13 @@ void Window::render() bottom->render(transform); if(bottom != top) { + if (top->getValue() == "GuiMsgBox" && mGuiStack.size() > 2) + { + auto& middle = mGuiStack.at(mGuiStack.size() - 2); + if (middle != bottom) + middle->render(transform); + } + mBackgroundOverlay->render(transform); top->render(transform); } @@ -284,9 +293,9 @@ void Window::render() renderScreenSaver(); if(!mRenderScreenSaver && mInfoPopup) - { mInfoPopup->render(transform); - } + + renderRegisteredNotificationComponents(transform); if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) { @@ -320,6 +329,9 @@ void Window::endRenderLoadingScreen() { mSplash = NULL; mCustomSplash = ""; + + // Window has not way to apply Theme -> As a workaround : endRenderLoadingScreen is always called when theme changes. + mBackgroundOverlay->setImage(ThemeData::getMenuTheme()->Background.fadePath); } void Window::renderLoadingScreen(std::string text, float percent, unsigned char opacity) @@ -568,3 +580,97 @@ void Window::renderScreenSaver() if (mScreenSaver) mScreenSaver->renderScreenSaver(); } + +static std::mutex mNotificationMessagesLock; + +void Window::displayNotificationMessage(std::string message, int duration) +{ + std::unique_lock lock(mNotificationMessagesLock); + + if (duration <= 0) + { + duration = Settings::getInstance()->getInt("audio.display_titles_time"); + if (duration <= 2 || duration > 120) + duration = 10; + + duration *= 1000; + } + + NotificationMessage msg; + msg.first = message; + msg.second = duration; + mNotificationMessages.push_back(msg); +} + +void Window::processNotificationMessages() +{ + std::unique_lock lock(mNotificationMessagesLock); + + if (mNotificationMessages.empty()) + return; + + NotificationMessage msg = mNotificationMessages.back(); + mNotificationMessages.pop_back(); + + LOG(LogDebug) << "Notification message :" << msg.first.c_str(); + + if (mInfoPopup) + delete mInfoPopup; + + mInfoPopup = new GuiInfoPopup(this, msg.first, msg.second); +} + +void Window::registerNotificationComponent(AsyncNotificationComponent* pc) +{ + std::unique_lock lock(mNotificationMessagesLock); + + if (std::find(mAsyncNotificationComponent.cbegin(), mAsyncNotificationComponent.cend(), pc) != mAsyncNotificationComponent.cend()) + return; + + mAsyncNotificationComponent.push_back(pc); +} + +void Window::unRegisterNotificationComponent(AsyncNotificationComponent* pc) +{ + std::unique_lock lock(mNotificationMessagesLock); + + auto it = std::find(mAsyncNotificationComponent.cbegin(), mAsyncNotificationComponent.cend(), pc); + if (it != mAsyncNotificationComponent.cend()) + mAsyncNotificationComponent.erase(it); +} + +void Window::renderRegisteredNotificationComponents(const Transform4x4f& trans) +{ + std::unique_lock lock(mNotificationMessagesLock); + +#define PADDING_H (Renderer::getScreenWidth()*0.01) + + float posY = Renderer::getScreenHeight() * 0.02f; + + for (auto child : mAsyncNotificationComponent) + { + float posX = Renderer::getScreenWidth()*0.99f - child->getSize().x(); + + child->setPosition(posX, posY, 0); + child->render(trans); + + posY += child->getSize().y() + PADDING_H; + } +} + +void Window::postToUiThread(const std::function& func) +{ + std::unique_lock lock(mNotificationMessagesLock); + + mFunctions.push_back(func); +} + +void Window::processPostedFunctions() +{ + std::unique_lock lock(mNotificationMessagesLock); + + for (auto func : mFunctions) + func(this); + + mFunctions.clear(); +} \ No newline at end of file diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 6d8dd2c866..a34ddbc9ea 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -7,6 +7,7 @@ #include "Settings.h" #include +#include class FileData; class Font; @@ -17,6 +18,7 @@ class InputConfig; class TextCache; class Transform4x4f; class TextureResource; +class AsyncNotificationComponent; struct HelpStyle; @@ -76,14 +78,31 @@ class Window void setHelpPrompts(const std::vector& prompts, const HelpStyle& style); void setScreenSaver(ScreenSaver* screenSaver) { mScreenSaver = screenSaver; } - void setInfoPopup(InfoPopup* infoPopup) { delete mInfoPopup; mInfoPopup = infoPopup; } +// void setInfoPopup(InfoPopup* infoPopup) { delete mInfoPopup; mInfoPopup = infoPopup; } inline void stopInfoPopup() { if (mInfoPopup) mInfoPopup->stop(); }; void startScreenSaver(); bool cancelScreenSaver(); void renderScreenSaver(); + void displayNotificationMessage(std::string message, int duration = -1); + + void registerNotificationComponent(AsyncNotificationComponent* pc); + void unRegisterNotificationComponent(AsyncNotificationComponent* pc); + + void postToUiThread(const std::function& func); + private: + void processPostedFunctions(); + + void renderRegisteredNotificationComponents(const Transform4x4f& trans); + std::vector mAsyncNotificationComponent; + std::vector> mFunctions; + + typedef std::pair NotificationMessage; + std::vector mNotificationMessages; + void processNotificationMessages(); + void onSleep(); void onWake(); diff --git a/es-core/src/components/AsyncNotificationComponent.cpp b/es-core/src/components/AsyncNotificationComponent.cpp new file mode 100644 index 0000000000..d16d630898 --- /dev/null +++ b/es-core/src/components/AsyncNotificationComponent.cpp @@ -0,0 +1,130 @@ +#include "AsyncNotificationComponent.h" +#include "ThemeData.h" +#include "PowerSaver.h" +#include "components/ComponentGrid.h" +#include "components/NinePatchComponent.h" +#include "components/TextComponent.h" +#include "EsLocale.h" + +#define PADDING_PX (Renderer::getScreenWidth()*0.01) + +AsyncNotificationComponent::AsyncNotificationComponent(Window* window, bool actionLine) + : GuiComponent(window) +{ + mPercent = -1; + + float width = Renderer::getScreenWidth() * 0.14f; + + auto theme = ThemeData::getMenuTheme(); + + mTitle = std::make_shared(mWindow, "", theme->TextSmall.font, theme->TextSmall.color, ALIGN_LEFT); + mGameName = std::make_shared(mWindow, "", theme->TextSmall.font, theme->Text.color, ALIGN_LEFT); + + if (actionLine) + mAction = std::make_shared(mWindow, "", theme->TextSmall.font, theme->Text.color, ALIGN_LEFT); + + Vector2f fullSize(width + 2 * PADDING_PX, 2 * PADDING_PX + mTitle->getSize().y() + mGameName->getSize().y() + (mAction == nullptr ? 0 : mAction->getSize().y())); + Vector2f gridSize(width, mTitle->getSize().y() + mGameName->getSize().y() + (mAction == nullptr ? 0 : mAction->getSize().y())); + + setSize(fullSize); + + mFrame = new NinePatchComponent(window); + mFrame->setImagePath(theme->Background.path); + mFrame->setCenterColor(theme->Background.color); + mFrame->setEdgeColor(theme->Background.color); + mFrame->fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); + addChild(mFrame); + + mGrid = new ComponentGrid(window, Vector2i(1, mAction == nullptr ? 2 : 3)); + mGrid->setPosition((fullSize.x() - gridSize.x()) / 2.0, (fullSize.y() - gridSize.y()) / 2.0); + mGrid->setSize(gridSize); + mGrid->setEntry(mTitle, Vector2i(0, 0), false, true); + mGrid->setEntry(mGameName, Vector2i(0, 1), false, true); + + if (mAction != nullptr) + mGrid->setEntry(mAction, Vector2i(0, 2), false, true); + + addChild(mGrid); + + float posX = Renderer::getScreenWidth()*0.5f - mSize.x()*0.5f; + float posY = Renderer::getScreenHeight() * 0.02f; + + // FCA TopRight + posX = Renderer::getScreenWidth()*0.99f - mSize.x(); + posY = Renderer::getScreenHeight() * 0.02f; + + setPosition(posX, posY, 0); + setOpacity(200); + + PowerSaver::pause(); +} + +AsyncNotificationComponent::~AsyncNotificationComponent() +{ + delete mFrame; + delete mGrid; + + PowerSaver::resume(); +} + +void AsyncNotificationComponent::updateText(const std::string text, const std::string action) +{ + std::unique_lock lock(mMutex); + + mGameName->setText(text); + + if (mAction != nullptr) + mAction->setText(action); +} + +void AsyncNotificationComponent::updatePercent(int percent) +{ + std::unique_lock lock(mMutex); + + mPercent = percent; +} + +void AsyncNotificationComponent::updateTitle(const std::string text) +{ + std::unique_lock lock(mMutex); + + mTitle->setText(text); +} + +void AsyncNotificationComponent::render(const Transform4x4f& parentTrans) +{ + std::unique_lock lock(mMutex); + + Transform4x4f trans = parentTrans * getTransform(); + + mFrame->render(trans); + + auto lastControl = mGameName; + if (mAction != nullptr) + lastControl = mAction; + + float x = mGrid->getPosition().x() + lastControl->getPosition().x(); + float y = mGrid->getPosition().y() + lastControl->getPosition().y(); + float w = lastControl->getSize().x(); + float h = lastControl->getSize().y(); + + h /= 10.0; + y += lastControl->getSize().y(); + + Renderer::setMatrix(trans); + + if (mPercent >= 0) + { + float percent = mPercent / 100.0; + if (percent < 0) + percent = 0; + if (percent > 100) + percent = 100; + + auto theme = ThemeData::getMenuTheme(); + auto color = (theme->Text.selectedColor & 0xFFFFFF00) | 0x40; + Renderer::drawRect(x, y, (w*percent), h, color); + } + + mGrid->render(trans); +} diff --git a/es-core/src/components/AsyncNotificationComponent.h b/es-core/src/components/AsyncNotificationComponent.h new file mode 100644 index 0000000000..9837b2e7ed --- /dev/null +++ b/es-core/src/components/AsyncNotificationComponent.h @@ -0,0 +1,34 @@ +#pragma once + +#include +#include "GuiComponent.h" + +class ComponentGrid; +class NinePatchComponent; +class TextComponent; +class Window; + +class AsyncNotificationComponent : public GuiComponent +{ +public: + AsyncNotificationComponent(Window* window, bool actionLine = true); + ~AsyncNotificationComponent(); + + void updateTitle(const std::string text); + void updateText(const std::string text, const std::string action = ""); + void updatePercent(int percent); + + void render(const Transform4x4f& parentTrans) override; + +private: + std::shared_ptr mTitle; + std::shared_ptr mGameName; + std::shared_ptr mAction; + + ComponentGrid* mGrid; + NinePatchComponent* mFrame; + + std::mutex mMutex; + + int mPercent; +}; \ No newline at end of file diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 2fa4cd3bbe..29f1c78419 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -13,24 +13,40 @@ #endif #include "components/VideoVlcComponent.h" #include "utils/FileSystemUtil.h" -#ifdef _RPI_ + #include "Settings.h" -#endif +#include "ImageGridComponent.h" #define VIDEODELAY 100 GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBackground(window), mLabel(window), mVideo(nullptr), mVideoPlaying(false), mShown(false) -{ +{ mSelectedZoomPercent = 1.0f; mAnimPosition = Vector3f(0, 0); + mVideo = nullptr; mLabelMerged = false; + resetProperties(); + + mImage = std::make_shared(mWindow); + mImage->setOrigin(0.5f, 0.5f); + + addChild(&mBackground); + addChild(&(*mImage)); + addChild(&mLabel); + + setSelected(false); + setVisible(true); +} + +void GridTileComponent::resetProperties() +{ mDefaultProperties.mSize = getDefaultTileSize(); mDefaultProperties.mPadding = Vector2f(16.0f, 16.0f); mDefaultProperties.mImageColor = 0xFFFFFFDD; // 0xAAAAAABB; mDefaultProperties.mBackgroundImage = ":/frame.png"; - mDefaultProperties.mBackgroundCornerSize = Vector2f(16 ,16); + mDefaultProperties.mBackgroundCornerSize = Vector2f(16, 16); mDefaultProperties.mBackgroundCenterColor = 0xAAAAEEFF; mDefaultProperties.mBackgroundEdgeColor = 0xAAAAEEFF; @@ -41,7 +57,7 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mSelectedProperties.mBackgroundCornerSize = mDefaultProperties.mBackgroundCornerSize; mSelectedProperties.mBackgroundCenterColor = 0xFFFFFFFF; mSelectedProperties.mBackgroundEdgeColor = 0xFFFFFFFF; - + mDefaultProperties.mLabelSize = Vector2f(1.0, 0.30); mDefaultProperties.mLabelColor = 0xFFFFFFFF; mDefaultProperties.mLabelBackColor = 0; @@ -62,31 +78,6 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mDefaultProperties.mMirror = Vector2f(0, 0); mSelectedProperties.mMirror = Vector2f(0, 0); - - mImage = std::make_shared(mWindow); - mImage->setOrigin(0.5f, 0.5f); - // mImage->setAllowFading(false); - - addChild(&mBackground); - addChild(&(*mImage)); - addChild(&mLabel); - /* - // Create the correct type of video window -#ifdef _RPI_ - if (Settings::getInstance()->getBool("VideoOmxPlayer")) - mVideo = new VideoPlayerComponent(window, ""); - else -#endif*/ - mVideo = new VideoVlcComponent(window, ""); - - // video - mVideo->setOrigin(0.5f, 0.5f); - mVideo->setStartDelay(VIDEODELAY); - mVideo->setDefaultZIndex(30); - addChild(mVideo); - - setSelected(false); - setVisible(true); } void GridTileComponent::forceSize(Vector2f size, float selectedZoom) @@ -103,59 +94,59 @@ GridTileComponent::~GridTileComponent() mVideo = nullptr; } -std::shared_ptr GridTileComponent::getTexture() -{ +std::shared_ptr GridTileComponent::getTexture() +{ if (mImage != nullptr) return mImage->getTexture(); - return nullptr; + return nullptr; }; void GridTileComponent::resize() { - const GridTileProperties& currentProperties = getCurrentProperties(); + auto currentProperties = getCurrentProperties(); Vector2f size = currentProperties.mSize; - setSize(size); + if (mSize != size) + setSize(size); - float height = (int) (size.y() * currentProperties.mLabelSize.y()); + float height = (int)(size.y() * currentProperties.mLabelSize.y()); float labelHeight = height; - mLabel.setColor(currentProperties.mLabelColor); - mLabel.setBackgroundColor(currentProperties.mLabelBackColor); - mLabel.setGlowColor(currentProperties.mLabelGlowColor); - mLabel.setGlowSize(currentProperties.mLabelGlowSize); + if (mLabelVisible) + { + mLabel.setColor(currentProperties.mLabelColor); + mLabel.setBackgroundColor(currentProperties.mLabelBackColor); + mLabel.setGlowColor(currentProperties.mLabelGlowColor); + mLabel.setGlowSize(currentProperties.mLabelGlowSize); - if (mDefaultProperties.mFontPath != mSelectedProperties.mFontPath || mDefaultProperties.mFontSize != mSelectedProperties.mFontSize) - mLabel.setFont(currentProperties.mFontPath, currentProperties.mFontSize); + if (mDefaultProperties.mFontPath != mSelectedProperties.mFontPath || mDefaultProperties.mFontSize != mSelectedProperties.mFontSize) + mLabel.setFont(currentProperties.mFontPath, currentProperties.mFontSize); - if (mLabelMerged) - { - mLabel.setPosition(currentProperties.mPadding.x(), mSize.y() - height - currentProperties.mPadding.y()); - mLabel.setSize(size.x() - 2 * currentProperties.mPadding.x(), height); - } - else - { - mLabel.setPosition(0, mSize.y() - height); - mLabel.setSize(size.x(), height); + if (mLabelMerged) + { + mLabel.setPosition(currentProperties.mPadding.x(), mSize.y() - height - currentProperties.mPadding.y()); + mLabel.setSize(size.x() - 2 * currentProperties.mPadding.x(), height); + } + else + { + mLabel.setPosition(0, mSize.y() - height); + mLabel.setSize(size.x(), height); + } } - if (!mLabelVisible || mLabelMerged) - height = 0; - - if (currentProperties.mLabelSize.x() == 0) + if (!mLabelVisible || mLabelMerged || currentProperties.mLabelSize.x() == 0) height = 0; float topPadding = currentProperties.mPadding.y(); float bottomPadding = std::max(topPadding, height); float paddingX = currentProperties.mPadding.x(); - + float imageWidth = size.x() - paddingX * 2.0; float imageHeight = size.y() - topPadding - bottomPadding; if (mImage != nullptr) { - mImage->setOrigin(0.5f, 0.5f); mImage->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); mImage->setColorShift(currentProperties.mImageColor); mImage->setMirroring(currentProperties.mMirror); @@ -167,21 +158,23 @@ void GridTileComponent::resize() else mImage->setMaxSize(imageWidth, imageHeight); - if (mLabelMerged) - { - mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, currentProperties.mPadding.y() + mImage->getSize().y() - labelHeight); - mLabel.setSize(mImage->getSize().x(), labelHeight); - } - else if (currentProperties.mPadding.x() == 0) + if (mLabelVisible) { - mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, mImage->getSize().y()); - mLabel.setSize(mImage->getSize().x(), labelHeight); + if (mLabelMerged) + { + mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, currentProperties.mPadding.y() + mImage->getSize().y() - labelHeight); + mLabel.setSize(mImage->getSize().x(), labelHeight); + } + else if (currentProperties.mPadding.x() == 0) + { + mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, mImage->getSize().y()); + mLabel.setSize(mImage->getSize().x(), labelHeight); + } } } if (mVideo != nullptr && mVideo->isPlaying()) { - mVideo->setOrigin(0.5f, 0.5f); mVideo->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); if (currentProperties.mImageSizeMode == "minSize") @@ -192,22 +185,17 @@ void GridTileComponent::resize() mVideo->setSize(ImageIO::adjustExternPictureSizef(vs, Vector2f(imageWidth, imageHeight))); } - else - if (currentProperties.mImageSizeMode == "size") - mVideo->setSize(imageWidth, size.y() - topPadding - bottomPadding ); else - mVideo->setMaxSize(imageWidth, size.y() - topPadding - bottomPadding); + if (currentProperties.mImageSizeMode == "size") + mVideo->setSize(imageWidth, size.y() - topPadding - bottomPadding); + else + mVideo->setMaxSize(imageWidth, size.y() - topPadding - bottomPadding); } Vector3f bkposition = Vector3f(0, 0); Vector2f bkSize = size; - - -// if (!mLabelMerged && currentProperties.mImageSizeMode == "minSize") -// bkSize = Vector2f(size.x(), size.y() - bottomPadding + topPadding); - - if (mImage != NULL && currentProperties.mSelectionMode == "image" && mImage->getSize() != Vector2f(0,0)) + if (mImage != NULL && currentProperties.mSelectionMode == "image" && mImage->getSize() != Vector2f(0, 0)) { if (currentProperties.mImageSizeMode == "minSize") bkSize = Vector2f(size.x(), size.y() - bottomPadding + topPadding); @@ -246,7 +234,7 @@ void GridTileComponent::resize() mBackground.setCenterColor(currentProperties.mBackgroundCenterColor); mBackground.setEdgeColor(currentProperties.mBackgroundEdgeColor); mBackground.setImagePath(currentProperties.mBackgroundImage); - + if (mSelected && mAnimPosition == Vector3f(0, 0, 0) && mSelectedZoomPercent != 1.0) mBackground.setOpacity(mSelectedZoomPercent * 255); else @@ -256,8 +244,8 @@ void GridTileComponent::resize() void GridTileComponent::update(int deltaTime) { GuiComponent::update(deltaTime); - - if ((mVideo != nullptr && mVideo->isPlaying())) + + if (mVideo != nullptr && mVideo->isPlaying()) resize(); } @@ -281,17 +269,19 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) return; - float padding = getCurrentProperties().mPadding.x(); - float topPadding = getCurrentProperties().mPadding.y(); + auto currentProperties = getCurrentProperties(); + + float padding = currentProperties.mPadding.x(); + float topPadding = currentProperties.mPadding.y(); float bottomPadding = topPadding; if (mLabelVisible && !mLabelMerged) - bottomPadding = std::max((int)topPadding, (int)(mSize.y() * getCurrentProperties().mLabelSize.y())); + bottomPadding = std::max((int)topPadding, (int)(mSize.y() * currentProperties.mLabelSize.y())); Vector2i pos((int)Math::round(trans.translation()[0] + padding), (int)Math::round(trans.translation()[1] + topPadding)); Vector2i size((int)Math::round(mSize.x() - 2 * padding), (int)Math::round(mSize.y() - topPadding - bottomPadding)); - - if (getCurrentProperties().mImageSizeMode == "minSize") + + if (currentProperties.mImageSizeMode == "minSize") Renderer::pushClipRect(pos, size); if (mImage != NULL) @@ -300,13 +290,13 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) if (mSelected && !mVideoPath.empty() && mVideo != nullptr) mVideo->render(trans); - if (!mLabelMerged && getCurrentProperties().mImageSizeMode == "minSize") + if (!mLabelMerged && currentProperties.mImageSizeMode == "minSize") Renderer::popClipRect(); - if (mLabelVisible && getCurrentProperties().mLabelSize.y()>0) + if (mLabelVisible && currentProperties.mLabelSize.y() > 0) mLabel.render(trans); - if (mLabelMerged && getCurrentProperties().mImageSizeMode == "minSize") + if (mLabelMerged && currentProperties.mImageSizeMode == "minSize") Renderer::popClipRect(); } @@ -319,14 +309,43 @@ void GridTileComponent::render(const Transform4x4f& parentTrans) renderContent(parentTrans); } +void GridTileComponent::createVideo() +{ + if (mVideo != nullptr) + return; + + mVideo = new VideoVlcComponent(mWindow, ""); + + // video + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setStartDelay(VIDEODELAY); + mVideo->setDefaultZIndex(30); + addChild(mVideo); +} + void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { + resetProperties(); + Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + const ThemeData::ThemeElement* grid = theme->getElement(view, "gamegrid", "imagegrid"); + if (grid && grid->has("showVideoAtDelay")) + { + createVideo(); + mVideo->applyTheme(theme, view, element, properties); + } + else if (mVideo != nullptr) + { + removeChild(mVideo); + delete mVideo; + mVideo = nullptr; + } + // Apply theme to the default gridtile const ThemeData::ThemeElement* elem = theme->getElement(view, "default", "gridtile"); if (elem) - { + { if (elem->has("size")) mDefaultProperties.mSize = elem->get("size") * screen; @@ -373,33 +392,33 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons } } -// mDefaultProperties.mMirror = Vector2f(0, 0); -// mSelectedProperties.mMirror = Vector2f(0, 0); + // mDefaultProperties.mMirror = Vector2f(0, 0); + // mSelectedProperties.mMirror = Vector2f(0, 0); - // Apply theme to the selected gridtile - // NOTE that some of the default gridtile properties influence on the selected gridtile properties - // See THEMES.md for more informations + // Apply theme to the selected gridtile + // NOTE that some of the default gridtile properties influence on the selected gridtile properties + // See THEMES.md for more informations elem = theme->getElement(view, "selected", "gridtile"); mSelectedProperties.mSize = elem && elem->has("size") ? - elem->get("size") * screen : - mDefaultProperties.mSize; - //getSelectedTileSize(); + elem->get("size") * screen : + mDefaultProperties.mSize; + //getSelectedTileSize(); mSelectedProperties.mPadding = elem && elem->has("padding") ? - elem->get("padding") : - mDefaultProperties.mPadding; + elem->get("padding") : + mDefaultProperties.mPadding; if (elem && elem->has("imageColor")) mSelectedProperties.mImageColor = elem->get("imageColor"); mSelectedProperties.mBackgroundImage = elem && elem->has("backgroundImage") ? - elem->get("backgroundImage") : - mDefaultProperties.mBackgroundImage; + elem->get("backgroundImage") : + mDefaultProperties.mBackgroundImage; mSelectedProperties.mBackgroundCornerSize = elem && elem->has("backgroundCornerSize") ? - elem->get("backgroundCornerSize") : - mDefaultProperties.mBackgroundCornerSize; + elem->get("backgroundCornerSize") : + mDefaultProperties.mBackgroundCornerSize; if (elem && elem->has("backgroundColor")) { @@ -431,18 +450,18 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem && elem->has("color")) { mDefaultProperties.mLabelColor = elem->get("color"); - mSelectedProperties.mLabelColor = mDefaultProperties.mLabelColor; + mSelectedProperties.mLabelColor = mDefaultProperties.mLabelColor; } if (elem && elem->has("backgroundColor")) { mDefaultProperties.mLabelBackColor = elem->get("backgroundColor"); - mSelectedProperties.mLabelBackColor = mDefaultProperties.mLabelBackColor; + mSelectedProperties.mLabelBackColor = mDefaultProperties.mLabelBackColor; } if (elem && elem->has("glowSize")) { - mDefaultProperties.mLabelGlowSize = (unsigned int) elem->get("glowSize"); + mDefaultProperties.mLabelGlowSize = (unsigned int)elem->get("glowSize"); mSelectedProperties.mLabelGlowSize = mDefaultProperties.mLabelGlowSize; } @@ -519,22 +538,22 @@ void GridTileComponent::setImage(const std::string& path) { if (mCurrentPath == path) return; - - mCurrentPath = path; + + mCurrentPath = path; if (mSelectedProperties.mSize.x() > mSize.x()) mImage->setImage(path, false, MaxSizeInfo(mSelectedProperties.mSize, mSelectedProperties.mImageSizeMode != "maxSize")); else mImage->setImage(path, false, MaxSizeInfo(mSize, mSelectedProperties.mImageSizeMode != "maxSize")); - resize(); + resize(); } -void GridTileComponent::reset() +void GridTileComponent::resetImages() { setLabel(""); - setVideo(""); setImage(""); + stopVideo(); } void GridTileComponent::setLabel(std::string name) @@ -557,9 +576,9 @@ void GridTileComponent::setVideo(const std::string& path, float defaultDelay) { if (defaultDelay >= 0.0) mVideo->setStartDelay(defaultDelay); - + if (mVideoPath.empty()) - mVideo->setVideo(""); + stopVideo(); } resize(); @@ -577,35 +596,53 @@ void GridTileComponent::onHide() mShown = false; } +void GridTileComponent::startVideo() +{ + if (mVideo != nullptr) + { + // Inform video component about size before staring in order to be able to use OptimizeVideo parameter + if (mSelectedProperties.mImageSizeMode == "minSize") + mVideo->setMinSize(mSelectedProperties.mSize); + else + mVideo->setResize(mSelectedProperties.mSize); + + mVideo->setVideo(mVideoPath); + } +} + +void GridTileComponent::stopVideo() +{ + if (mVideo != nullptr) + mVideo->setVideo(""); +} + void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f* pPosition, bool force) { - if (!mShown || !GuiComponent::ALLOWANIMATIONS) + if (!mShown || !ALLOWANIMATIONS) allowAnimation = false; if (mSelected == selected && !force) { - if (mSelected && mVideo != nullptr) - mVideo->setVideo(mVideoPath); + if (mSelected) + startVideo(); return; } - mSelected = selected; + mSelected = selected; - if (!mSelected && mVideo != nullptr) - mVideo->setVideo(""); + if (!mSelected) + stopVideo(); if (selected) { if (pPosition == NULL || !allowAnimation) - { + { cancelAnimation(3); - + this->setSelectedZoom(1); mAnimPosition = Vector3f(0, 0, 0); - - if (mVideo != NULL) - mVideo->setVideo(mVideoPath); + startVideo(); resize(); } @@ -628,10 +665,7 @@ void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f setAnimation(new LambdaAnimation(func, 250), 0, [this] { this->setSelectedZoom(1); mAnimPosition = Vector3f(0, 0, 0); - - if (mVideo != NULL) - mVideo->setVideo(mVideoPath); - + startVideo(); }, false, 3); } } @@ -641,18 +675,13 @@ void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f { cancelAnimation(3); this->setSelectedZoom(0); - - if (mVideo != NULL) - mVideo->setVideo(""); - + stopVideo(); resize(); } else { this->setSelectedZoom(1); - - if (mVideo != NULL) - mVideo->setVideo(""); + stopVideo(); auto func = [this](float t) { @@ -660,7 +689,7 @@ void GridTileComponent::setSelected(bool selected, bool allowAnimation, Vector3f float pct = Math::lerp(0, 1, t*t*t + 1); this->setSelectedZoom(1.0 - pct); }; - + cancelAnimation(3); setAnimation(new LambdaAnimation(func, 250), 0, [this] { this->setSelectedZoom(0); @@ -683,28 +712,10 @@ void GridTileComponent::setVisible(bool visible) mVisible = visible; } -unsigned int mixColors(unsigned int first, unsigned int second, float percent) +GridTileProperties GridTileComponent::getCurrentProperties() { - unsigned char alpha0 = (first >> 24) & 0xFF; - unsigned char blue0 = (first >> 16) & 0xFF; - unsigned char green0 = (first >> 8) & 0xFF; - unsigned char red0 = first & 0xFF; - - unsigned char alpha1 = (second >> 24) & 0xFF; - unsigned char blue1 = (second >> 16) & 0xFF; - unsigned char green1 = (second >> 8) & 0xFF; - unsigned char red1 = second & 0xFF; - - unsigned char alpha = (unsigned char)(alpha0 * (1.0 - percent) + alpha1 * percent); - unsigned char blue = (unsigned char)(blue0 * (1.0 - percent) + blue1 * percent); - unsigned char green = (unsigned char)(green0 * (1.0 - percent) + green1 * percent); - unsigned char red = (unsigned char)(red0 * (1.0 - percent) + red1 * percent); - - return (alpha << 24) | (blue << 16) | (green << 8) | red; -} + GridTileProperties mMixedProperties; -const GridTileProperties& GridTileComponent::getCurrentProperties() -{ if (mSelectedZoomPercent == 0.0f || mSelectedZoomPercent == 1.0f) return mSelected ? mSelectedProperties : mDefaultProperties; @@ -728,25 +739,25 @@ const GridTileProperties& GridTileComponent::getCurrentProperties() if (mDefaultProperties.mImageColor != mSelectedProperties.mImageColor) { - mMixedProperties.mImageColor = mixColors(mDefaultProperties.mImageColor, mSelectedProperties.mImageColor, mSelectedZoomPercent); + mMixedProperties.mImageColor = Renderer::mixColors(mDefaultProperties.mImageColor, mSelectedProperties.mImageColor, mSelectedZoomPercent); } if (mDefaultProperties.mLabelSize != mSelectedProperties.mLabelSize) - mMixedProperties.mLabelSize = Vector2f(mDefaultProperties.mLabelSize.x(), + mMixedProperties.mLabelSize = Vector2f(mDefaultProperties.mLabelSize.x(), mDefaultProperties.mLabelSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mLabelSize.y() * mSelectedZoomPercent); if (mDefaultProperties.mLabelColor != mSelectedProperties.mLabelColor) - mMixedProperties.mLabelColor = mixColors(mDefaultProperties.mLabelColor, mSelectedProperties.mLabelColor, mSelectedZoomPercent); + mMixedProperties.mLabelColor = Renderer::mixColors(mDefaultProperties.mLabelColor, mSelectedProperties.mLabelColor, mSelectedZoomPercent); if (mDefaultProperties.mLabelBackColor != mSelectedProperties.mLabelBackColor) - mMixedProperties.mLabelBackColor = mixColors(mDefaultProperties.mLabelBackColor, mSelectedProperties.mLabelBackColor, mSelectedZoomPercent); + mMixedProperties.mLabelBackColor = Renderer::mixColors(mDefaultProperties.mLabelBackColor, mSelectedProperties.mLabelBackColor, mSelectedZoomPercent); if (mDefaultProperties.mLabelGlowColor != mSelectedProperties.mLabelGlowColor) - mMixedProperties.mLabelGlowColor = mixColors(mDefaultProperties.mLabelGlowColor, mSelectedProperties.mLabelGlowColor, mSelectedZoomPercent); + mMixedProperties.mLabelGlowColor = Renderer::mixColors(mDefaultProperties.mLabelGlowColor, mSelectedProperties.mLabelGlowColor, mSelectedZoomPercent); if (mDefaultProperties.mLabelGlowSize != mSelectedProperties.mLabelGlowSize) mMixedProperties.mLabelGlowSize = mDefaultProperties.mLabelGlowSize * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mLabelGlowSize * mSelectedZoomPercent; - + if (mDefaultProperties.mMirror != mSelectedProperties.mMirror) { float x = mDefaultProperties.mMirror.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mMirror.x() * mSelectedZoomPercent; @@ -754,14 +765,14 @@ const GridTileProperties& GridTileComponent::getCurrentProperties() mMixedProperties.mMirror = Vector2f(x, y); } -// Avoid to multiply font sizes in mem + it create strange sizings -// if (mDefaultProperties.mFontSize != mSelectedProperties.mFontSize) - // mMixedProperties.mFontSize = mDefaultProperties.mFontSize * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mFontSize * mSelectedZoomPercent; + // Avoid to multiply font sizes in mem + it create strange sizings + // if (mDefaultProperties.mFontSize != mSelectedProperties.mFontSize) + // mMixedProperties.mFontSize = mDefaultProperties.mFontSize * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mFontSize * mSelectedZoomPercent; return mMixedProperties; } -Vector3f GridTileComponent::getBackgroundPosition() -{ +Vector3f GridTileComponent::getBackgroundPosition() +{ return Vector3f(mBackground.getPosition().x() + mPosition.x(), mBackground.getPosition().y() + mPosition.y(), 0); } \ No newline at end of file diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index 8b4138aae6..94dc635d0d 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -6,8 +6,8 @@ #include "ImageComponent.h" #include "TextComponent.h" - class VideoComponent; + struct GridTileProperties { Vector2f mSize; @@ -32,10 +32,6 @@ struct GridTileProperties unsigned int mFontSize; Vector2f mMirror; - - /* - . / main / fonts / Dosis - Bold.ttf< / fontPath> - 0.025< / fontSize>*/ }; class GridTileComponent : public GuiComponent @@ -54,14 +50,14 @@ class GridTileComponent : public GuiComponent Vector2f getSelectedTileSize() const; bool isSelected() const; - void reset(); + void resetImages(); void setLabel(std::string name); void setVideo(const std::string& path, float defaultDelay = -1.0); void setImage(const std::string& path); // void setImage(const std::shared_ptr& texture, std::string name); - void setSelected(bool selected, bool allowAnimation = true, Vector3f* pPosition = NULL, bool force=false); + void setSelected(bool selected, bool allowAnimation = true, Vector3f* pPosition = NULL, bool force = false); void setVisible(bool visible); void forceSize(Vector2f size, float selectedZoom = 1.0); @@ -80,12 +76,18 @@ class GridTileComponent : public GuiComponent std::shared_ptr getTexture(); private: + void resetProperties(); + void createVideo(); + + void startVideo(); + void stopVideo(); + void resize(); - const GridTileProperties& getCurrentProperties(); + GridTileProperties getCurrentProperties(); std::shared_ptr mImage; - TextComponent mLabel; + TextComponent mLabel; bool mLabelVisible; bool mLabelMerged; @@ -94,7 +96,6 @@ class GridTileComponent : public GuiComponent GridTileProperties mDefaultProperties; GridTileProperties mSelectedProperties; - GridTileProperties mMixedProperties; std::string mCurrentPath; std::string mVideoPath; diff --git a/es-core/src/components/IList.h b/es-core/src/components/IList.h index 408f88a044..6e27fff3d8 100644 --- a/es-core/src/components/IList.h +++ b/es-core/src/components/IList.h @@ -192,6 +192,15 @@ class IList : public GuiComponent inline int size() const { return (int)mEntries.size(); } + inline std::vector getObjects() + { + std::vector objects; + for (auto it = mEntries.begin(); it != mEntries.end(); it++) + objects.push_back((*it).object); + + return objects; + } + protected: void remove(typename std::vector::const_iterator& it) { diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index df40be7925..e8df835066 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -24,8 +24,12 @@ Vector2f ImageComponent::getSize() const ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : GuiComponent(window), mTargetIsMax(false), mTargetIsMin(false), mFlipX(false), mFlipY(false), mTargetSize(0, 0), mColorShift(0xFFFFFFFF), mColorShiftEnd(0xFFFFFFFF), mForceLoad(forceLoad), mDynamic(dynamic), mFadeOpacity(0), mFading(false), mRotateByTargetSize(false), mVisible(true), - mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f), mMirror(0.0f, 0.0f), mAllowAsync(false) + mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f), mMirror(0.0f, 0.0f), mAllowAsync(false), + mPadding(Vector4f(0, 0, 0, 0)) { + mHorizontalAlignment = ALIGN_CENTER; + mVerticalAlignment = ALIGN_CENTER; + mReflectOnBorders = false; mLoadingTexture = nullptr; mAllowFading = true; } @@ -145,7 +149,11 @@ void ImageComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) return; mPath = path; - mLoadingTexture = nullptr; + + // If the previous image is in the async queue, remove it + TextureResource::cancelAsync(mLoadingTexture); + TextureResource::cancelAsync(mTexture); + mLoadingTexture.reset(); if (path.empty() || !ResourceManager::getInstance()->fileExists(path)) { @@ -158,15 +166,14 @@ void ImageComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) { std::shared_ptr texture = TextureResource::get(path, tile, mForceLoad, mDynamic, true, maxSize); - // TODO probleme de proportions si l'on fait ca, à investiguer... if (!mForceLoad && mDynamic && !mAllowFading && texture != nullptr && !texture->isLoaded()) mLoadingTexture = texture; else mTexture = texture; } - if (mLoadingTexture == nullptr); - resize(); + if (mLoadingTexture == nullptr) + resize(); } void ImageComponent::setImage(const char* path, size_t length, bool tile) @@ -316,10 +323,17 @@ void ImageComponent::updateVertices() const unsigned int color = Renderer::convertColor(mColorShift); const unsigned int colorEnd = Renderer::convertColor(mColorShiftEnd); - mVertices[0] = { { topLeft.x(), topLeft.y() }, { mTopLeftCrop.x(), py - mTopLeftCrop.y() }, color }; - mVertices[1] = { { topLeft.x(), bottomRight.y() }, { mTopLeftCrop.x(), 1.0f - mBottomRightCrop.y() }, color }; - mVertices[2] = { { bottomRight.x(), topLeft.y() }, { mBottomRightCrop.x() * px, py - mTopLeftCrop.y() }, color }; - mVertices[3] = { { bottomRight.x(), bottomRight.y() }, { mBottomRightCrop.x() * px, 1.0f - mBottomRightCrop.y() }, color }; + mVertices[0] = { { topLeft.x() + mPadding.x(), topLeft.y() + mPadding.y() }, + { mTopLeftCrop.x(), py - mTopLeftCrop.y() }, color }; + + mVertices[1] = { { topLeft.x() + mPadding.x(), bottomRight.y() - mPadding.w() }, + { mTopLeftCrop.x(), 1.0f - mBottomRightCrop.y() }, mColorGradientHorizontal ? colorEnd : color }; + + mVertices[2] = { { bottomRight.x() - mPadding.z(), topLeft.y() + mPadding.y() }, + { mBottomRightCrop.x() * px, py - mTopLeftCrop.y() }, mColorGradientHorizontal ? color : colorEnd }; + + mVertices[3] = { { bottomRight.x() - mPadding.z(), bottomRight.y() - mPadding.w() }, + { mBottomRightCrop.x() * px, 1.0f - mBottomRightCrop.y() }, color }; // round vertices for(int i = 0; i < 4; ++i) @@ -345,7 +359,7 @@ void ImageComponent::render(const Transform4x4f& parentTrans) if (mLoadingTexture != nullptr && mLoadingTexture->isLoaded()) { mTexture = mLoadingTexture; - mLoadingTexture = nullptr; + mLoadingTexture.reset(); resize(); } @@ -357,79 +371,89 @@ void ImageComponent::render(const Transform4x4f& parentTrans) Renderer::setMatrix(trans); - if (mTexture) + if (mTexture && mOpacity > 0) { - if (Settings::getInstance()->getBool("DebugImage")) + Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; + + if (Settings::getInstance()->getBool("DebugImage")) { - Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; Renderer::drawRect(targetSizePos.x(), targetSizePos.y(), mTargetSize.x(), mTargetSize.y(), 0xFF000033); - Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), 0x00000033); + } + + if (!mTexture->bind()) + { + fadeIn(false); + return; } -// if (mTexture->isInitialized()) -// { - // actually draw the image - // The bind() function returns false if the texture is not currently loaded. A blank - // texture is bound in this case but we want to handle a fade so it doesn't just 'jump' in - // when it finally loads - fadeIn(mTexture->bind()); + if (mVerticalAlignment == ALIGN_TOP) + trans.translate(Vector3f(0, targetSizePos.y(), 0.0f)); + else if (mVerticalAlignment == ALIGN_BOTTOM) + trans.translate(Vector3f(targetSizePos.x(), targetSizePos.y() + mTargetSize.y() - mSize.y(), 0.0f)); - float opacity = (mOpacity * (mFading ? mFadeOpacity / 255.0 : 1.0)) / 255.0; - - const unsigned int color = Renderer::convertColor(mColorShift & 0xFFFFFF00 | (unsigned char) ((mColorShift & 0xFF) * opacity)); - const unsigned int colorEnd = Renderer::convertColor(mColorShiftEnd & 0xFFFFFF00 | (unsigned char)((mColorShiftEnd & 0xFF) * opacity)); + if (mHorizontalAlignment == ALIGN_LEFT) + trans.translate(Vector3f(targetSizePos.x(), 0, 0.0f)); + else if (mHorizontalAlignment == ALIGN_BOTTOM) + trans.translate(Vector3f(targetSizePos.x(), targetSizePos.y() + mTargetSize.y() - mSize.y(), 0.0f)); - mVertices[0].col = color; - mVertices[1].col = mColorGradientHorizontal ? colorEnd : color; - mVertices[2].col = mColorGradientHorizontal ? color : colorEnd; - mVertices[3].col = colorEnd; + Renderer::setMatrix(trans); - Renderer::drawTriangleStrips(&mVertices[0], 4); + fadeIn(true); - if (mMirror.x() != 0 || mMirror.y() != 0) - { - float alpha = ((mColorShift & 0x000000ff)) / 255.0; - float alpha2 = alpha * mMirror.y(); - alpha *= mMirror.x(); + float opacity = (mOpacity * (mFading ? mFadeOpacity / 255.0 : 1.0)) / 255.0; - const unsigned int colorT = Renderer::convertColor((mColorShift & 0xffffff00) + (unsigned char) (255.0*alpha)); - const unsigned int colorB = Renderer::convertColor((mColorShift & 0xffffff00) + (unsigned char) (255.0*alpha2)); + const unsigned int color = Renderer::convertColor(mColorShift & 0xFFFFFF00 | (unsigned char)((mColorShift & 0xFF) * opacity)); + const unsigned int colorEnd = Renderer::convertColor(mColorShiftEnd & 0xFFFFFF00 | (unsigned char)((mColorShiftEnd & 0xFF) * opacity)); - int h = mVertices[1].pos.y() - mVertices[0].pos.y(); + mVertices[0].col = color; + mVertices[1].col = mColorGradientHorizontal ? colorEnd : color; + mVertices[2].col = mColorGradientHorizontal ? color : colorEnd; + mVertices[3].col = colorEnd; - Renderer::Vertex mirrorVertices[4]; + Renderer::drawTriangleStrips(&mVertices[0], 4); - mirrorVertices[0] = { - { mVertices[0].pos.x(), mVertices[0].pos.y() + h }, - { mVertices[0].tex.x(), mVertices[1].tex.y() }, - colorT }; + if (mMirror.x() != 0 || mMirror.y() != 0) + { + float alpha = ((mColorShift & 0x000000ff)) / 255.0; + float alpha2 = alpha * mMirror.y(); - mirrorVertices[1] = { - { mVertices[1].pos.x(), mVertices[1].pos.y() + h }, - { mVertices[1].tex.x(), mVertices[0].tex.y() }, - colorB }; + alpha *= mMirror.x(); - mirrorVertices[2] = { - { mVertices[2].pos.x(), mVertices[2].pos.y() + h }, - { mVertices[2].tex.x(), mVertices[3].tex.y() }, - colorT }; + const unsigned int colorT = Renderer::convertColor((mColorShift & 0xffffff00) + (unsigned char)(255.0*alpha)); + const unsigned int colorB = Renderer::convertColor((mColorShift & 0xffffff00) + (unsigned char)(255.0*alpha2)); - mirrorVertices[3] = { - { mVertices[3].pos.x(), mVertices[3].pos.y() + h }, - { mVertices[3].tex.x(), mVertices[2].tex.y() }, - colorB }; + int h = mVertices[1].pos.y() - mVertices[0].pos.y(); + if (mReflectOnBorders) + h = mTargetSize.y(); - Renderer::drawTriangleStrips(&mirrorVertices[0], 4); - } + Renderer::Vertex mirrorVertices[4]; - Renderer::bindTexture(0); - /* } - else - { - LOG(LogError) << "Image texture is not initialized!"; - mTexture.reset(); - }*/ + mirrorVertices[0] = { + { mVertices[0].pos.x(), mVertices[0].pos.y() + h }, + { mVertices[0].tex.x(), mVertices[1].tex.y() }, + colorT }; + + mirrorVertices[1] = { + { mVertices[1].pos.x(), mVertices[1].pos.y() + h }, + { mVertices[1].tex.x(), mVertices[0].tex.y() }, + colorB }; + + mirrorVertices[2] = { + { mVertices[2].pos.x(), mVertices[2].pos.y() + h }, + { mVertices[2].tex.x(), mVertices[3].tex.y() }, + colorT }; + + mirrorVertices[3] = { + { mVertices[3].pos.x(), mVertices[3].pos.y() + h }, + { mVertices[3].tex.x(), mVertices[2].tex.y() }, + colorB }; + + Renderer::drawTriangleStrips(&mirrorVertices[0], 4); + } + + Renderer::bindTexture(0); } GuiComponent::renderChildren(trans); @@ -531,26 +555,63 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s } } - if(properties & COLOR) + if (properties & COLOR) { - if(elem->has("color")) + if (elem->has("color")) setColorShift(elem->get("color")); if (elem->has("colorEnd")) setColorShiftEnd(elem->get("colorEnd")); if (elem->has("gradientType")) - setColorGradientHorizontal(!(elem->get("gradientType").compare("horizontal"))); - } + setColorGradientHorizontal(elem->get("gradientType").compare("horizontal")); + + if (elem->has("reflexion")) + mMirror = elem->get("reflexion"); + else + mMirror = Vector2f::Zero(); - if (properties & COLOR && elem->has("reflexion")) - mMirror = elem->get("reflexion"); + if (elem->has("reflexionOnFrame")) + mReflectOnBorders = elem->get("reflexionOnFrame"); + else + mReflectOnBorders = false; + } - if(properties & ThemeFlags::ROTATION) { + if(properties & ThemeFlags::ROTATION) + { if(elem->has("rotation")) setRotationDegrees(elem->get("rotation")); + if(elem->has("rotationOrigin")) setRotationOrigin(elem->get("rotationOrigin")); + + if (elem->has("flipX")) + setFlipX(elem->get("flipX")); + + if (elem->has("flipY")) + setFlipY(elem->get("flipY")); + } + + if (properties & ALIGNMENT && elem->has("horizontalAlignment")) + { + std::string str = elem->get("horizontalAlignment"); + if (str == "left") + setHorizontalAlignment(ALIGN_LEFT); + else if (str == "right") + setHorizontalAlignment(ALIGN_RIGHT); + else + setHorizontalAlignment(ALIGN_CENTER); + } + + if (properties & ALIGNMENT && elem->has("verticalAlignment")) + { + std::string str = elem->get("verticalAlignment"); + if (str == "top") + setVerticalAlignment(ALIGN_TOP); + else if (str == "bottom") + setVerticalAlignment(ALIGN_BOTTOM); + else + setVerticalAlignment(ALIGN_CENTER); } if(properties & ThemeFlags::Z_INDEX && elem->has("zIndex")) diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 4351b2c3db..3b8c2daf3d 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -6,6 +6,7 @@ #include "math/Vector2i.h" #include "GuiComponent.h" #include "resources/TextureResource.h" +#include "resources/Font.h" class ImageComponent : public GuiComponent { @@ -98,6 +99,11 @@ class ImageComponent : public GuiComponent return MaxSizeInfo(mTargetSize, mTargetIsMax); }; + void setPadding(const Vector4f padding) { mPadding = padding; updateVertices(); } + + void setHorizontalAlignment(Alignment align) { mHorizontalAlignment = align; } + void setVerticalAlignment(Alignment align) { mVerticalAlignment = align; } + private: Vector2f mTargetSize; @@ -135,9 +141,14 @@ class ImageComponent : public GuiComponent Vector2f mBottomRightCrop; Vector2f mMirror; + bool mReflectOnBorders; std::string mPath; std::shared_ptr mLoadingTexture; + + Vector4f mPadding; + Alignment mHorizontalAlignment; + Alignment mVerticalAlignment; }; #endif // ES_CORE_COMPONENTS_IMAGE_COMPONENT_H diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 5f53b8aba7..46e20c9166 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -758,7 +758,7 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA if (updateSelectedState) tile->setSelected(false, allowAnimation); - tile->reset(); + tile->resetImages(); tile->setVisible(false); } else @@ -871,8 +871,7 @@ void ImageGridComponent::buildTiles() // X--; tile->setPosition(X * tileDistance.x() + startPosition.x(), Y * tileDistance.y() + startPosition.y()); - tile->setOrigin(0.5f, 0.5f); - tile->reset(); + tile->setOrigin(0.5f, 0.5f); if (mTheme) tile->applyTheme(mTheme, mName, "gridtile", ThemeFlags::ALL); diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index 7142380003..fc83efb2e9 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -23,6 +23,13 @@ MenuComponent::MenuComponent(Window* window, std::string title, const std::share mTitle = std::make_shared(mWindow); mTitle->setHorizontalAlignment(ALIGN_CENTER); mTitle->setColor(theme->Title.color); // 0x555555FF + + if (theme->Title.selectorColor != 0x555555FF) + { + mTitle->setBackgroundColor(theme->Title.selectorColor); + mTitle->setRenderBackground(true); + } + setTitle(title.c_str(), theme->Title.font); // titleFont mGrid.setEntry(mTitle, Vector2i(0, 0), false); diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index f6ec14bf83..69d30ca5a2 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -5,14 +5,21 @@ #include "ThemeData.h" NinePatchComponent::NinePatchComponent(Window* window, const std::string& path, unsigned int edgeColor, unsigned int centerColor) : GuiComponent(window), - mCornerSize(16, 16), - mEdgeColor(edgeColor), mCenterColor(centerColor), - mPath(path), - mVertices(NULL) +mCornerSize(16, 16), +mEdgeColor(edgeColor), mCenterColor(centerColor), +mVertices(NULL) { mPreviousSize = Vector2f(0, 0); - if(!mPath.empty()) - buildVertices(); + setImagePath(path); +} + +void NinePatchComponent::setOpacity(unsigned char opacity) +{ + if (mOpacity == opacity) + return; + + mOpacity = opacity; + updateColors(); } NinePatchComponent::~NinePatchComponent() @@ -35,25 +42,17 @@ void NinePatchComponent::updateColors() mVertices[i].col = edgeColor; for (int i = 6 * 4; i < 6; ++i) - mVertices[(6 * 4) + i].col = mCenterColor; + mVertices[(6 * 4) + i].col = centerColor; } -void NinePatchComponent::setOpacity(unsigned char opacity) +void NinePatchComponent::buildVertices() { - if (mOpacity == opacity) + if (mTexture == nullptr) return; - mOpacity = opacity; - updateColors(); -} - -void NinePatchComponent::buildVertices() -{ if (mVertices != NULL) delete[] mVertices; - mTexture = TextureResource::get(mPath); - if (mTexture->getSize() == Vector2i::Zero()) { mVertices = NULL; @@ -64,6 +63,8 @@ void NinePatchComponent::buildVertices() mVertices = new Renderer::Vertex[6 * 9]; const Vector2f texSize = Vector2f((float)mTexture->getSize().x(), (float)mTexture->getSize().y()); + if (texSize.x() <= 0 || texSize.y() <= 0) + return; const float imgSizeX[3] = { mCornerSize.x(), mSize.x() - mCornerSize.x() * 2, mCornerSize.x() }; const float imgSizeY[3] = { mCornerSize.y(), mSize.y() - mCornerSize.y() * 2, mCornerSize.y() }; @@ -71,10 +72,10 @@ void NinePatchComponent::buildVertices() const float imgPosY[3] = { 0, imgSizeY[0], imgSizeY[0] + imgSizeY[1] }; //the "1 +" in posY and "-" in sizeY is to deal with texture coordinates having a bottom left corner origin vs. verticies having a top left origin - const float texSizeX[3] = { mCornerSize.x() / texSize.x(), (texSize.x() - mCornerSize.x() * 2) / texSize.x(), mCornerSize.x() / texSize.x() }; + const float texSizeX[3] = { mCornerSize.x() / texSize.x(), (texSize.x() - mCornerSize.x() * 2) / texSize.x(), mCornerSize.x() / texSize.x() }; const float texSizeY[3] = { -mCornerSize.y() / texSize.y(), -(texSize.y() - mCornerSize.y() * 2) / texSize.y(), -mCornerSize.y() / texSize.y() }; - const float texPosX[3] = { 0, texSizeX[0], texSizeX[0] + texSizeX[1] }; - const float texPosY[3] = { 1, 1 + texSizeY[0], 1 + texSizeY[0] + texSizeY[1] }; + const float texPosX[3] = { 0, texSizeX[0], texSizeX[0] + texSizeX[1] }; + const float texPosY[3] = { 1, 1 + texSizeY[0], 1 + texSizeY[0] + texSizeY[1] }; int v = 0; for (int slice = 0; slice < 9; slice++) @@ -92,7 +93,7 @@ void NinePatchComponent::buildVertices() mVertices[v + 4] = { { imgPos.x() + imgSize.x(), imgPos.y() + imgSize.y() }, { texPos.x() + texSize.x(), texPos.y() + texSize.y() }, 0 }; // round vertices - for(int i = 1; i < 5; ++i) + for (int i = 1; i < 5; ++i) mVertices[v + i].pos.round(); // make duplicates of first and last vertex so this can be rendered as a triangle strip @@ -107,17 +108,26 @@ void NinePatchComponent::buildVertices() void NinePatchComponent::render(const Transform4x4f& parentTrans) { - if (!isVisible()) + if (!isVisible() || mTexture == nullptr || mVertices == nullptr) return; Transform4x4f trans = parentTrans * getTransform(); + if (!Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) + return; - if (mTexture && mVertices != NULL) + if (mCornerSize.x() == 0 && mCornerSize.y() == 0) { - Renderer::setMatrix(trans); + float opacity = mOpacity / 255.0; + const unsigned int edgeColor = mEdgeColor & 0xFFFFFF00 | (unsigned char)((mEdgeColor & 0xFF) * opacity); - mTexture->bind(); + Renderer::setMatrix(trans); + Renderer::drawRect(0.0, 0.0, mSize.x(), mSize.y(), edgeColor, edgeColor); + } + else if (mTexture->bind()) + { + Renderer::setMatrix(trans); Renderer::drawTriangleStrips(&mVertices[0], 6 * 9); + Renderer::bindTexture(0); } renderChildren(trans); @@ -154,7 +164,7 @@ void NinePatchComponent::fitTo(Vector2f size, Vector3f position, Vector2f paddin setSize(size + mCornerSize * 2); setPosition(position.x() + Math::lerp(-mCornerSize.x(), mCornerSize.x(), mOrigin.x()), - position.y() + Math::lerp(-mCornerSize.y(), mCornerSize.y(), mOrigin.y())); + position.y() + Math::lerp(-mCornerSize.y(), mCornerSize.y(), mOrigin.y())); } void NinePatchComponent::setImagePath(const std::string& path) @@ -163,7 +173,7 @@ void NinePatchComponent::setImagePath(const std::string& path) return; mPath = path; - mTexture = nullptr; + mTexture = TextureResource::get(mPath); buildVertices(); } @@ -197,7 +207,7 @@ void NinePatchComponent::applyTheme(const std::shared_ptr& theme, con if (properties & PATH && elem->has("path")) setImagePath(elem->get("path")); - + if (properties & COLOR) { if (elem->has("color")) @@ -205,7 +215,7 @@ void NinePatchComponent::applyTheme(const std::shared_ptr& theme, con setCenterColor(elem->get("color")); setEdgeColor(elem->get("color")); } - + if (elem->has("centerColor")) setCenterColor(elem->get("centerColor")); diff --git a/es-core/src/components/NinePatchComponent.h b/es-core/src/components/NinePatchComponent.h index ee09d94440..76c2dd2cb5 100644 --- a/es-core/src/components/NinePatchComponent.h +++ b/es-core/src/components/NinePatchComponent.h @@ -34,9 +34,6 @@ class NinePatchComponent : public GuiComponent void setEdgeColor(unsigned int edgeColor); // Apply a color shift to the "edge" parts of the ninepatch. void setCenterColor(unsigned int centerColor); // Apply a color shift to the "center" part of the ninepatch. - unsigned int getCenterColor() { return mCenterColor; }; - unsigned int getEdgeColor() { return mEdgeColor; }; - virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; const Vector2f& getCornerSize() const; @@ -53,10 +50,11 @@ class NinePatchComponent : public GuiComponent std::string mPath; Vector2f mCornerSize; - Vector2f mPreviousSize; unsigned int mEdgeColor; unsigned int mCenterColor; std::shared_ptr mTexture; + + Vector2f mPreviousSize; }; #endif // ES_CORE_COMPONENTS_NINE_PATCH_COMPONENT_H diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index c7523e22d2..966be8a916 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -36,7 +36,8 @@ class OptionListComponent : public GuiComponent OptionListComponent* mParent; public: - OptionListPopup(Window* window, OptionListComponent* parent, const std::string& title) : GuiComponent(window), + OptionListPopup(Window* window, OptionListComponent* parent, const std::string& title, + const std::function callback = nullptr) : GuiComponent(window), mMenu(window, title.c_str()), mParent(parent) { auto menuTheme = ThemeData::getMenuTheme(); @@ -51,39 +52,67 @@ class OptionListComponent : public GuiComponent for(auto it = mParent->mEntries.begin(); it != mParent->mEntries.end(); it++) { row.elements.clear(); - row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, color), true); OptionListData& e = *it; - if(mParent->mMultiSelect) + if (callback != nullptr) { - // add checkbox - auto checkbox = std::make_shared(mWindow); - checkbox->setImage(it->selected ? CHECKED_PATH : UNCHECKED_PATH); - checkbox->setResize(0, font->getLetterHeight()); - row.addElement(checkbox, false); - - // input handler - // update checkbox state & selected value - row.makeAcceptInputHandler([this, &e, checkbox] + callback(e.object, row); + + if (!mParent->mMultiSelect) { - e.selected = !e.selected; - checkbox->setImage(e.selected ? CHECKED_PATH : UNCHECKED_PATH); - mParent->onSelectedChanged(); - }); - - // for select all/none - checkboxes.push_back(checkbox.get()); - }else{ - // input handler for non-multiselect - // update selected value and close - row.makeAcceptInputHandler([this, &e] + row.makeAcceptInputHandler([this, &e] + { + e.selected = !e.selected; + mParent->onSelectedChanged(); + }); + } + else { - mParent->mEntries.at(mParent->getSelectedId()).selected = false; - e.selected = true; - mParent->onSelectedChanged(); - delete this; - }); + row.makeAcceptInputHandler([this, &e] + { + mParent->mEntries.at(mParent->getSelectedId()).selected = false; + e.selected = true; + mParent->onSelectedChanged(); + delete this; + }); + } + } + else + { + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, color), true); + + if (mParent->mMultiSelect) + { + // add checkbox + auto checkbox = std::make_shared(mWindow); + checkbox->setImage(it->selected ? CHECKED_PATH : UNCHECKED_PATH); + checkbox->setResize(0, font->getLetterHeight()); + row.addElement(checkbox, false); + + // input handler + // update checkbox state & selected value + row.makeAcceptInputHandler([this, &e, checkbox] + { + e.selected = !e.selected; + checkbox->setImage(e.selected ? CHECKED_PATH : UNCHECKED_PATH); + mParent->onSelectedChanged(); + }); + + // for select all/none + checkboxes.push_back(checkbox.get()); + } + else { + // input handler for non-multiselect + // update selected value and close + row.makeAcceptInputHandler([this, &e] + { + mParent->mEntries.at(mParent->getSelectedId()).selected = false; + e.selected = true; + mParent->onSelectedChanged(); + delete this; + }); + } } // also set cursor to this row if we're not multi-select and this row is selected @@ -145,9 +174,10 @@ class OptionListComponent : public GuiComponent { auto theme = ThemeData::getMenuTheme(); - // auto font = Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT); - mText.setFont(theme->Text.font); // font - mText.setColor(theme->Text.color); // 0x777777FF + mAddRowCallback = nullptr; + + mText.setFont(theme->Text.font); + mText.setColor(theme->Text.color); mText.setHorizontalAlignment(ALIGN_CENTER); addChild(&mText); @@ -314,8 +344,19 @@ class OptionListComponent : public GuiComponent mSelectedChangedCallback = callback; } + void setRowTemplate(std::function callback) + { + mAddRowCallback = callback; + } private: + std::function mAddRowCallback; + + void open() + { + mWindow->pushGui(new OptionListPopup(mWindow, this, mName, mAddRowCallback)); + } + unsigned int getSelectedId() { assert(mMultiSelect == false); @@ -329,12 +370,6 @@ class OptionListComponent : public GuiComponent return 0; } - void open() - { - mWindow->pushGui(new OptionListPopup(mWindow, this, mName)); - } - - void onSelectedChanged() { if(mMultiSelect) diff --git a/es-core/src/components/SliderComponent.cpp b/es-core/src/components/SliderComponent.cpp index 405ed5271d..dfd8fbf74e 100644 --- a/es-core/src/components/SliderComponent.cpp +++ b/es-core/src/components/SliderComponent.cpp @@ -141,6 +141,9 @@ void SliderComponent::onValueChanged() mKnob.setResize(0, mSize.y() * 0.7f); float lineLength = mSize.x() - mKnob.getSize().x() - (mValueCache ? mValueCache->metrics.size.x() + 4 : 0); mKnob.setPosition(((mValue + mMin) / mMax) * lineLength + mKnob.getSize().x()/2, mSize.y() / 2); + + if (mValueChanged) + mValueChanged(mValue); } std::vector SliderComponent::getHelpPrompts() diff --git a/es-core/src/components/SliderComponent.h b/es-core/src/components/SliderComponent.h index 1ab529208f..d66b00b395 100644 --- a/es-core/src/components/SliderComponent.h +++ b/es-core/src/components/SliderComponent.h @@ -27,6 +27,8 @@ class SliderComponent : public GuiComponent virtual std::vector getHelpPrompts() override; virtual void setColor(unsigned int color); + inline void setOnValueChanged(const std::function& callback) { mValueChanged = callback; } + private: void onValueChanged(); @@ -43,6 +45,7 @@ class SliderComponent : public GuiComponent std::shared_ptr mValueCache; unsigned int mColor; + std::function mValueChanged; }; #endif // ES_CORE_COMPONENTS_SLIDER_COMPONENT_H diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index 23379d6acf..504142ae14 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -4,18 +4,18 @@ #include "Log.h" #include "Settings.h" -TextComponent::TextComponent(Window* window) : GuiComponent(window), - mFont(Font::get(FONT_SIZE_MEDIUM)), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), - mHorizontalAlignment(ALIGN_LEFT), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), - mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)) +TextComponent::TextComponent(Window* window) : GuiComponent(window), +mFont(Font::get(FONT_SIZE_MEDIUM)), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), +mHorizontalAlignment(ALIGN_LEFT), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), +mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)), mGlowOffset(Vector2f(0, 0)) { } TextComponent::TextComponent(Window* window, const std::string& text, const std::shared_ptr& font, unsigned int color, Alignment align, - Vector3f pos, Vector2f size, unsigned int bgcolor) : GuiComponent(window), - mFont(NULL), mUppercase(false), mColor(1), mAutoCalcExtent(true, true), + Vector3f pos, Vector2f size, unsigned int bgcolor) : GuiComponent(window), + mFont(NULL), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), mHorizontalAlignment(align), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), - mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)) + mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)), mGlowOffset(Vector2f(0, 0)) { setFont(font); setColor(color); @@ -33,9 +33,6 @@ void TextComponent::onSizeChanged() void TextComponent::setFont(const std::shared_ptr& font) { - if (mFont == font) - return; - mFont = font; onTextChanged(); } @@ -61,18 +58,13 @@ void TextComponent::setColor(unsigned int color) return; mColor = color; - mColorOpacity = mColor & 0x000000FF; onColorChanged(); } // Set the color of the background box void TextComponent::setBackgroundColor(unsigned int color) { - if (mBgColor == color) - return; - mBgColor = color; - mBgColorOpacity = mBgColor & 0x000000FF; } void TextComponent::setRenderBackground(bool render) @@ -83,24 +75,11 @@ void TextComponent::setRenderBackground(bool render) // Scale the opacity void TextComponent::setOpacity(unsigned char opacity) { - // This method is mostly called to do fading in-out of the Text component element. - // Therefore, we assume here that opacity is a fractional value (expressed as an int 0-255), - // of the opacity originally set with setColor() or setBackgroundColor(). - - unsigned char o = (unsigned char)((float)opacity / 255.f * (float) mColorOpacity); - mColor = (mColor & 0xFFFFFF00) | (unsigned char) o; - - unsigned char bgo = (unsigned char)((float)opacity / 255.f * (float)mBgColorOpacity); - mBgColor = (mBgColor & 0xFFFFFF00) | (unsigned char)bgo; + if (opacity == mOpacity) + return; + mOpacity = opacity; onColorChanged(); - - GuiComponent::setOpacity(opacity); -} - -unsigned char TextComponent::getOpacity() const -{ - return mColor & 0x000000FF; } void TextComponent::setText(const std::string& text) @@ -115,46 +94,60 @@ void TextComponent::setUppercase(bool uppercase) onTextChanged(); } -void TextComponent::render(const Transform4x4f& parentTrans) +void TextComponent::renderSingleGlow(const Transform4x4f& parentTrans, float yOff, float x, float y) { - if (mColorOpacity == 0) - return; + Vector3f off = Vector3f(mPadding.x() + x + mGlowOffset.x(), mPadding.y() + yOff + y + mGlowOffset.y(), 0); + Transform4x4f trans = parentTrans * getTransform(); + + trans.translate(off); + trans.round(); + + Renderer::setMatrix(trans); + unsigned char alpha = (unsigned char)((mGlowColor & 0xFF) * (mOpacity / 255.0)); + unsigned int color = (mGlowColor & 0xFFFFFF00) | alpha; + + mTextCache->setColor(color); + mFont->renderTextCache(mTextCache.get()); +} + +void TextComponent::render(const Transform4x4f& parentTrans) +{ if (!isVisible()) return; Transform4x4f trans = parentTrans * getTransform(); - Vector2f clipPos(trans.translation().x(), trans.translation().y()); - if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + if (!Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) return; if (mRenderBackground) { Renderer::setMatrix(trans); - Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), mBgColor, mBgColor); + + auto bgColor = mBgColor & 0xFFFFFF00 | (unsigned char)((mBgColor & 0xFF) * (mOpacity / 255.0)); + Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), bgColor, bgColor); } - - if (mTextCache) + + if (mTextCache && mFont) { const Vector2f& textSize = mTextCache->metrics.size; float yOff = 0; - switch(mVerticalAlignment) + switch (mVerticalAlignment) { - case ALIGN_TOP: - yOff = 0; - break; - case ALIGN_BOTTOM: - yOff = (getSize().y() - textSize.y()); - break; - case ALIGN_CENTER: - yOff = (int) (getSize().y() - textSize.y()) / 2.0f; - break; + case ALIGN_TOP: + yOff = 0; + break; + case ALIGN_BOTTOM: + yOff = (getSize().y() - textSize.y()); + break; + case ALIGN_CENTER: + yOff = (getSize().y() - textSize.y()) / 2.0f; + break; } - Vector3f off(mPadding.x(), mPadding.y() + yOff, 0); - if(Settings::getInstance()->getBool("DebugText")) + if (Settings::getInstance()->getBool("DebugText")) { // draw the "textbox" area, what we are aligned within Renderer::setMatrix(trans); @@ -163,53 +156,33 @@ void TextComponent::render(const Transform4x4f& parentTrans) if ((mGlowColor & 0x000000FF) != 0 && mGlowSize > 0) { - auto draw = [this, off, yOff, parentTrans](int margin) - { - auto func = [this, off, yOff, parentTrans](float x, float y) - { - Vector3f off = Vector3f(mPadding.x() + x, mPadding.y() + yOff + y, 0); - Transform4x4f trans = parentTrans * getTransform(); - - trans.translate(off); - trans.round(); - - Renderer::setMatrix(trans); + int x = -mGlowSize; + int y = -mGlowSize; + renderSingleGlow(parentTrans, yOff, x, y); - unsigned char o = (unsigned char)((float)(mGlowColor & 0x000000FF) / 255.f * (float)(mColor & 0x000000FF)); - unsigned int color = (mGlowColor & 0xFFFFFF00) | (unsigned char)o; + for (int i = 0; i < 2 * mGlowSize; i++) + renderSingleGlow(parentTrans, yOff, ++x, y); - mTextCache->setColor(color); - mFont->renderTextCache(mTextCache.get()); - mTextCache->setColor(mColor); - }; + for (int i = 0; i < 2 * mGlowSize; i++) + renderSingleGlow(parentTrans, yOff, x, ++y); - int x = -margin; - int y = -margin; - func(x, y); + for (int i = 0; i < 2 * mGlowSize; i++) + renderSingleGlow(parentTrans, yOff, --x, y); - for (int i = 0; i < 2 * margin; i++) - func(++x, y); + for (int i = 0; i < 2 * mGlowSize; i++) + renderSingleGlow(parentTrans, yOff, x, --y); - for (int i = 0; i < 2 * margin; i++) - func(x, ++y); - - for (int i = 0; i < 2 * margin; i++) - func(--x, y); - - for (int i = 0; i < 2 * margin; i++) - func(x, --y); - }; - - draw(mGlowSize); + // Restore text color + onColorChanged(); } trans.translate(off); Renderer::setMatrix(trans); // draw the text area, where the text actually is going - if(Settings::getInstance()->getBool("DebugText")) + if (Settings::getInstance()->getBool("DebugText")) { - switch(mHorizontalAlignment) + switch (mHorizontalAlignment) { case ALIGN_LEFT: Renderer::drawRect(0.0f, 0.0f, mTextCache->metrics.size.x(), mTextCache->metrics.size.y(), 0x00000033, 0x00000033); @@ -224,18 +197,17 @@ void TextComponent::render(const Transform4x4f& parentTrans) } mFont->renderTextCache(mTextCache.get()); - - } } void TextComponent::calculateExtent() { - if(mAutoCalcExtent.x()) + if (mAutoCalcExtent.x()) { mSize = mFont->sizeText(mUppercase ? Utils::String::toUpper(mText) : mText, mLineSpacing); - }else{ - if(mAutoCalcExtent.y()) + } + else { + if (mAutoCalcExtent.y()) { mSize[1] = mFont->sizeWrappedText(mUppercase ? Utils::String::toUpper(mText) : mText, getSize().x(), mLineSpacing).y(); } @@ -246,7 +218,7 @@ void TextComponent::onTextChanged() { calculateExtent(); - if(!mFont || mText.empty()) + if (!mFont || mText.empty()) { mTextCache.reset(); return; @@ -261,21 +233,23 @@ void TextComponent::onTextChanged() const bool isMultiline = (mSize.y() == 0 || sy > f->getHeight()*1.2f); bool addAbbrev = false; - if(!isMultiline) + if (!isMultiline) { size_t newline = text.find('\n'); text = text.substr(0, newline); // single line of text - stop at the first newline since it'll mess everything up addAbbrev = newline != std::string::npos; } + auto color = mColor & 0xFFFFFF00 | (unsigned char)((mColor & 0xFF) * (mOpacity / 255.0)); + Vector2f size = f->sizeText(text); - if(!isMultiline && sx && text.size() && (size.x() > sx || addAbbrev)) + if (!isMultiline && sx && text.size() && (size.x() > sx || addAbbrev)) { // abbreviate text const std::string abbrev = "..."; Vector2f abbrevSize = f->sizeText(abbrev); - while(text.size() && size.x() + abbrevSize.x() > sx) + while (text.size() && size.x() + abbrevSize.x() > sx) { size_t newSize = Utils::String::prevCursor(text, text.size()); text.erase(newSize, text.size() - newSize); @@ -284,17 +258,19 @@ void TextComponent::onTextChanged() text.append(abbrev); - mTextCache = std::shared_ptr(f->buildTextCache(text, Vector2f(0, 0), (mColor >> 8 << 8) | mOpacity, sx, mHorizontalAlignment, mLineSpacing)); - }else{ - mTextCache = std::shared_ptr(f->buildTextCache(f->wrapText(text, sx), Vector2f(0, 0), (mColor >> 8 << 8) | mOpacity, sx, mHorizontalAlignment, mLineSpacing)); + mTextCache = std::shared_ptr(f->buildTextCache(text, Vector2f(0, 0), color, sx, mHorizontalAlignment, mLineSpacing)); + } + else { + mTextCache = std::shared_ptr(f->buildTextCache(f->wrapText(text, sx), Vector2f(0, 0), color, sx, mHorizontalAlignment, mLineSpacing)); } } void TextComponent::onColorChanged() { - if(mTextCache) + if (mTextCache) { - mTextCache->setColor(mColor); + auto color = mColor & 0xFFFFFF00 | (unsigned char)((mColor & 0xFF) * (mOpacity / 255.0)); + mTextCache->setColor(color); } } @@ -332,11 +308,11 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, const st using namespace ThemeFlags; const ThemeData::ThemeElement* elem = theme->getElement(view, element, "text"); - if(!elem) + if (!elem) return; if (properties & COLOR && elem->has("color")) - setColor(elem->get("color")); + setColor(elem->get("color")); setRenderBackground(false); if (properties & COLOR && elem->has("backgroundColor")) { @@ -344,14 +320,14 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, const st setRenderBackground(true); } - if(properties & ALIGNMENT && elem->has("alignment")) + if (properties & ALIGNMENT && elem->has("alignment")) { std::string str = elem->get("alignment"); - if(str == "left") + if (str == "left") setHorizontalAlignment(ALIGN_LEFT); - else if(str == "center") + else if (str == "center") setHorizontalAlignment(ALIGN_CENTER); - else if(str == "right") + else if (str == "right") setHorizontalAlignment(ALIGN_RIGHT); else LOG(LogError) << "Unknown text alignment string: " << str; @@ -363,20 +339,23 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, const st mPadding = elem->get("padding") * Vector4f(scale.x(), scale.y(), scale.x(), scale.y()); } - if(properties & TEXT && elem->has("text")) + if (properties & TEXT && elem->has("text")) setText(elem->get("text")); - if(properties & FORCE_UPPERCASE && elem->has("forceUppercase")) + if (properties & FORCE_UPPERCASE && elem->has("forceUppercase")) setUppercase(elem->get("forceUppercase")); - if(properties & LINE_SPACING && elem->has("lineSpacing")) + if (properties & LINE_SPACING && elem->has("lineSpacing")) setLineSpacing(elem->get("lineSpacing")); if (properties & COLOR && elem->has("glowColor")) mGlowColor = elem->get("glowColor"); if (properties & COLOR && elem->has("glowSize")) - mGlowSize = (int) elem->get("glowSize"); + mGlowSize = (int)elem->get("glowSize"); + + if (properties & COLOR && elem->has("glowOffset")) + mGlowOffset = elem->get("glowOffset"); setFont(Font::getFromTheme(elem, properties, mFont)); } diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index 257c8e1b44..b76750f901 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -4,7 +4,6 @@ #include "resources/Font.h" #include "GuiComponent.h" -#include "math/Vector4f.h" class ThemeData; @@ -21,12 +20,12 @@ class TextComponent : public GuiComponent Vector3f pos = Vector3f::Zero(), Vector2f size = Vector2f::Zero(), unsigned int bgcolor = 0x00000000); void setFont(const std::shared_ptr& font); + void setFont(std::string path, int size); void setUppercase(bool uppercase); void onSizeChanged() override; - - std::string getText() { return mText; } + const std::string getText() { return mText; } void setText(const std::string& text); - virtual void setColor(unsigned int color); + void setColor(unsigned int color); void setHorizontalAlignment(Alignment align); void setVerticalAlignment(Alignment align); void setLineSpacing(float spacing); @@ -38,7 +37,6 @@ class TextComponent : public GuiComponent std::string getValue() const override; void setValue(const std::string& value) override; - unsigned char getOpacity() const override; void setOpacity(unsigned char opacity) override; inline std::shared_ptr getFont() const { return mFont; } @@ -47,8 +45,8 @@ class TextComponent : public GuiComponent void setGlowColor(unsigned int color) { mGlowColor = color; }; void setGlowSize(unsigned int size) { mGlowSize = size; }; - - void setFont(std::string path, int size); + + void setPadding(const Vector4f padding) { mPadding = padding; } protected: virtual void onTextChanged(); @@ -58,13 +56,13 @@ class TextComponent : public GuiComponent private: void calculateExtent(); + void renderSingleGlow(const Transform4x4f& parentTrans, float yOff, float x, float y); void onColorChanged(); unsigned int mColor; unsigned int mBgColor; - unsigned char mColorOpacity; - unsigned char mBgColorOpacity; + bool mRenderBackground; bool mUppercase; @@ -76,6 +74,7 @@ class TextComponent : public GuiComponent unsigned int mGlowColor; unsigned int mGlowSize; + Vector2f mGlowOffset; Vector4f mPadding; }; diff --git a/es-core/src/components/TextEditComponent.cpp b/es-core/src/components/TextEditComponent.cpp index 6d98139bb2..38ecfa421d 100644 --- a/es-core/src/components/TextEditComponent.cpp +++ b/es-core/src/components/TextEditComponent.cpp @@ -2,6 +2,7 @@ #include "resources/Font.h" #include "utils/StringUtil.h" +#include "EsLocale.h" #define TEXT_PADDING_HORIZ 10 #define TEXT_PADDING_VERT 2 @@ -9,11 +10,18 @@ #define CURSOR_REPEAT_START_DELAY 500 #define CURSOR_REPEAT_SPEED 28 // lower is faster +#define BLINKTIME 1000 + TextEditComponent::TextEditComponent(Window* window) : GuiComponent(window), - mBox(window, ":/textinput_ninepatch.png"), mFocused(false), - mScrollOffset(0.0f, 0.0f), mCursor(0), mEditing(false), mFont(Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT)), - mCursorRepeatDir(0) +mBox(window, ":/textinput_ninepatch.png"), mFocused(false), +mScrollOffset(0.0f, 0.0f), mCursor(0), mEditing(false), mFont(Font::get(FONT_SIZE_MEDIUM, FONT_PATH_LIGHT)), +mCursorRepeatDir(0) { + mBlinkTime = 0; + + auto theme = ThemeData::getMenuTheme(); + mBox.setImagePath(ThemeData::getMenuTheme()->Icons.textinput_ninepatch); + addChild(&mBox); onFocusLost(); @@ -24,13 +32,13 @@ TextEditComponent::TextEditComponent(Window* window) : GuiComponent(window), void TextEditComponent::onFocusGained() { mFocused = true; - mBox.setImagePath(":/textinput_ninepatch_active.png"); + mBox.setImagePath(ThemeData::getMenuTheme()->Icons.textinput_ninepatch_active); } void TextEditComponent::onFocusLost() { mFocused = false; - mBox.setImagePath(":/textinput_ninepatch.png"); + mBox.setImagePath(ThemeData::getMenuTheme()->Icons.textinput_ninepatch); } void TextEditComponent::onSizeChanged() @@ -52,18 +60,19 @@ std::string TextEditComponent::getValue() const void TextEditComponent::textInput(const char* text) { - if(mEditing) + if (mEditing) { mCursorRepeatDir = 0; - if(text[0] == '\b') + if (text[0] == '\b') { - if(mCursor > 0) + if (mCursor > 0) { size_t newCursor = Utils::String::prevCursor(mText, mCursor); mText.erase(mText.begin() + newCursor, mText.begin() + mCursor); mCursor = (unsigned int)newCursor; } - }else{ + } + else { mText.insert(mCursor, text); mCursor += (unsigned int)strlen(text); } @@ -94,71 +103,76 @@ bool TextEditComponent::input(InputConfig* config, Input input) bool const cursor_right = (config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedLike("right", input)) || (config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_RIGHT); - if(input.value == 0) + if (input.value == 0) { - if(cursor_left || cursor_right) + if (cursor_left || cursor_right) mCursorRepeatDir = 0; return false; } - if((config->isMappedTo("a", input) || (config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_RETURN)) && mFocused && !mEditing) + if ((config->isMappedTo("a", input) || (config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_RETURN)) && mFocused && !mEditing) { startEditing(); return true; } - if(mEditing) + if (mEditing) { - if(config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_RETURN) + if (config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_RETURN) { - if(isMultiline()) + if (isMultiline()) { textInput("\n"); - }else{ + } + else { stopEditing(); } return true; } - if((config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_ESCAPE) || (config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedTo("b", input))) + if ((config->getDeviceId() == DEVICE_KEYBOARD && input.id == SDLK_ESCAPE) || (config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedTo("b", input))) { stopEditing(); return true; } - if(config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedLike("up", input)) + if (config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedLike("up", input)) { // TODO - }else if(config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedLike("down", input)) + } + else if (config->getDeviceId() != DEVICE_KEYBOARD && config->isMappedLike("down", input)) { // TODO - }else if(cursor_left || cursor_right) + } + else if (cursor_left || cursor_right) { + mBlinkTime = 0; mCursorRepeatDir = cursor_left ? -1 : 1; mCursorRepeatTimer = -(CURSOR_REPEAT_START_DELAY - CURSOR_REPEAT_SPEED); moveCursor(mCursorRepeatDir); - } else if(config->getDeviceId() == DEVICE_KEYBOARD) + } + else if (config->getDeviceId() == DEVICE_KEYBOARD) { - switch(input.id) + switch (input.id) { - case SDLK_HOME: - setCursor(0); - break; - - case SDLK_END: - setCursor(std::string::npos); - break; - - case SDLK_DELETE: - if(mCursor < mText.length()) - { - // Fake as Backspace one char to the right - moveCursor(1); - textInput("\b"); - } - break; + case SDLK_HOME: + setCursor(0); + break; + + case SDLK_END: + setCursor(std::string::npos); + break; + + case SDLK_DELETE: + if (mCursor < mText.length()) + { + // Fake as Backspace one char to the right + moveCursor(1); + textInput("\b"); + } + break; } } @@ -171,17 +185,21 @@ bool TextEditComponent::input(InputConfig* config, Input input) void TextEditComponent::update(int deltaTime) { + mBlinkTime += deltaTime; + if (mBlinkTime >= BLINKTIME) + mBlinkTime = 0; + updateCursorRepeat(deltaTime); GuiComponent::update(deltaTime); } void TextEditComponent::updateCursorRepeat(int deltaTime) { - if(mCursorRepeatDir == 0) + if (mCursorRepeatDir == 0) return; mCursorRepeatTimer += deltaTime; - while(mCursorRepeatTimer >= CURSOR_REPEAT_SPEED) + while (mCursorRepeatTimer >= CURSOR_REPEAT_SPEED) { moveCursor(mCursorRepeatDir); mCursorRepeatTimer -= CURSOR_REPEAT_SPEED; @@ -196,7 +214,7 @@ void TextEditComponent::moveCursor(int amt) void TextEditComponent::setCursor(size_t pos) { - if(pos == std::string::npos) + if (pos == std::string::npos) mCursor = (unsigned int)mText.length(); else mCursor = (int)pos; @@ -207,32 +225,35 @@ void TextEditComponent::setCursor(size_t pos) void TextEditComponent::onTextChanged() { std::string wrappedText = (isMultiline() ? mFont->wrapText(mText, getTextAreaSize().x()) : mText); - mTextCache = std::unique_ptr(mFont->buildTextCache(wrappedText, 0, 0, 0x77777700 | getOpacity())); + mTextCache = std::unique_ptr(mFont->buildTextCache(wrappedText, 0, 0, (ThemeData::getMenuTheme()->Text.color & 0xFFFFFF00) | getOpacity())); - if(mCursor > (int)mText.length()) + if (mCursor > (int)mText.length()) mCursor = (unsigned int)mText.length(); } void TextEditComponent::onCursorChanged() { - if(isMultiline()) + if (isMultiline()) { Vector2f textSize = mFont->getWrappedTextCursorOffset(mText, getTextAreaSize().x(), mCursor); - if(mScrollOffset.y() + getTextAreaSize().y() < textSize.y() + mFont->getHeight()) //need to scroll down? + if (mScrollOffset.y() + getTextAreaSize().y() < textSize.y() + mFont->getHeight()) //need to scroll down? { mScrollOffset[1] = textSize.y() - getTextAreaSize().y() + mFont->getHeight(); - }else if(mScrollOffset.y() > textSize.y()) //need to scroll up? + } + else if (mScrollOffset.y() > textSize.y()) //need to scroll up? { mScrollOffset[1] = textSize.y(); } - }else{ + } + else { Vector2f cursorPos = mFont->sizeText(mText.substr(0, mCursor)); - if(mScrollOffset.x() + getTextAreaSize().x() < cursorPos.x()) + if (mScrollOffset.x() + getTextAreaSize().x() < cursorPos.x()) { mScrollOffset[0] = cursorPos.x() - getTextAreaSize().x(); - }else if(mScrollOffset.x() > cursorPos.x()) + } + else if (mScrollOffset.x() > cursorPos.x()) { mScrollOffset[0] = cursorPos.x(); } @@ -242,6 +263,10 @@ void TextEditComponent::onCursorChanged() void TextEditComponent::render(const Transform4x4f& parentTrans) { Transform4x4f trans = getTransform() * parentTrans; + + if (!Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) + return; + renderChildren(trans); // text + cursor rendering @@ -256,7 +281,7 @@ void TextEditComponent::render(const Transform4x4f& parentTrans) trans.translate(Vector3f(-mScrollOffset.x(), -mScrollOffset.y(), 0)); Renderer::setMatrix(trans); - if(mTextCache) + if (mTextCache) { mFont->renderTextCache(mTextCache.get()); } @@ -265,19 +290,25 @@ void TextEditComponent::render(const Transform4x4f& parentTrans) Renderer::popClipRect(); // draw cursor - if(mEditing) + if (mEditing) { Vector2f cursorPos; - if(isMultiline()) + if (isMultiline()) { cursorPos = mFont->getWrappedTextCursorOffset(mText, getTextAreaSize().x(), mCursor); - }else{ + } + else + { cursorPos = mFont->sizeText(mText.substr(0, mCursor)); cursorPos[1] = 0; } - float cursorHeight = mFont->getHeight() * 0.8f; - Renderer::drawRect(cursorPos.x(), cursorPos.y() + (mFont->getHeight() - cursorHeight) / 2, 2.0f, cursorHeight, 0x000000FF, 0x000000FF); + if (mBlinkTime < BLINKTIME / 2) + { + float cursorHeight = mFont->getHeight() * 0.8f; + auto cursorColor = (ThemeData::getMenuTheme()->Text.color & 0xFFFFFF00) | getOpacity(); + Renderer::drawRect(cursorPos.x(), cursorPos.y() + (mFont->getHeight() - cursorHeight) / 2, 2.0f, cursorHeight, cursorColor, cursorColor); // 0x000000FF + } } } @@ -299,12 +330,13 @@ Vector2f TextEditComponent::getTextAreaSize() const std::vector TextEditComponent::getHelpPrompts() { std::vector prompts; - if(mEditing) + if (mEditing) { - prompts.push_back(HelpPrompt("up/down/left/right", "move cursor")); - prompts.push_back(HelpPrompt("b", "stop editing")); - }else{ - prompts.push_back(HelpPrompt("a", "edit")); + prompts.push_back(HelpPrompt("up/down/left/right", _("MOVE CURSOR"))); // batocera + prompts.push_back(HelpPrompt("b", _("STOP EDITING"))); + } + else { + prompts.push_back(HelpPrompt("a", _("EDIT"))); } return prompts; } diff --git a/es-core/src/components/TextEditComponent.h b/es-core/src/components/TextEditComponent.h index a1639e0fab..206c542cac 100644 --- a/es-core/src/components/TextEditComponent.h +++ b/es-core/src/components/TextEditComponent.h @@ -34,9 +34,8 @@ class TextEditComponent : public GuiComponent virtual std::vector getHelpPrompts() override; - void startEditing(); - void stopEditing(); - + void startEditing(); // batocera + void stopEditing(); // batocera private: void onTextChanged(); @@ -63,6 +62,8 @@ class TextEditComponent : public GuiComponent std::shared_ptr mFont; std::unique_ptr mTextCache; + + int mBlinkTime; }; #endif // ES_CORE_COMPONENTS_TEXT_EDIT_COMPONENT_H diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 8eaea978c6..d5178b4bca 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -53,7 +53,7 @@ void VideoComponent::setScreensaverMode(bool isScreensaver) VideoComponent::VideoComponent(Window* window) : GuiComponent(window), - mStaticImage(window), + mStaticImage(window, true), mVideoHeight(0), mVideoWidth(0), mStartDelayed(false), @@ -272,7 +272,7 @@ void VideoComponent::applyTheme(const std::shared_ptr& theme, const s std::vector VideoComponent::getHelpPrompts() { std::vector ret; - ret.push_back(HelpPrompt("a", "SELECTIONNER")); + ret.push_back(HelpPrompt("a", _("SELECT"))); return ret; } diff --git a/es-app/src/guis/GuiInfoPopup.cpp b/es-core/src/guis/GuiInfoPopup.cpp similarity index 100% rename from es-app/src/guis/GuiInfoPopup.cpp rename to es-core/src/guis/GuiInfoPopup.cpp diff --git a/es-app/src/guis/GuiInfoPopup.h b/es-core/src/guis/GuiInfoPopup.h similarity index 100% rename from es-app/src/guis/GuiInfoPopup.h rename to es-core/src/guis/GuiInfoPopup.h diff --git a/es-core/src/guis/GuiMsgBox.cpp b/es-core/src/guis/GuiMsgBox.cpp index 74e9d89382..382701f091 100644 --- a/es-core/src/guis/GuiMsgBox.cpp +++ b/es-core/src/guis/GuiMsgBox.cpp @@ -2,26 +2,84 @@ #include "components/ButtonComponent.h" #include "components/MenuComponent.h" +#include "components/ImageComponent.h" +#include "resources/ResourceManager.h" +#include "EsLocale.h" -#define HORIZONTAL_PADDING_PX 20 +#define HORIZONTAL_PADDING_PX (Renderer::getScreenWidth()*0.01) + +GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, const std::string& name1, const std::function& func1, GuiMsgBoxIcon icon) + : GuiMsgBox(window, text, name1, func1, "", nullptr, "", nullptr, icon) { } GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, const std::string& name1, const std::function& func1, const std::string& name2, const std::function& func2, - const std::string& name3, const std::function& func3) : GuiComponent(window), - mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 2)) + GuiMsgBoxIcon icon) + : GuiMsgBox(window, text, name1, func1, name2, func2, "", nullptr, icon) { } + +GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, + const std::string& name1, const std::function& func1, + const std::string& name2, const std::function& func2, + const std::string& name3, const std::function& func3, + GuiMsgBoxIcon icon) : GuiComponent(window), + mBackground(window, ":/frame.png"), mGrid(window, Vector2i(2, 2)) + { auto theme = ThemeData::getMenuTheme(); - mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setImagePath(theme->Background.path); mBackground.setCenterColor(theme->Background.color); mBackground.setEdgeColor(theme->Background.color); float width = Renderer::getScreenWidth() * 0.6f; // max width float minWidth = Renderer::getScreenWidth() * 0.3f; // minimum width + + mImage = nullptr; + + std::string imageFile; + + switch (icon) + { + case ICON_INFORMATION: + imageFile = ":/info.svg"; + break; + case ICON_QUESTION: + imageFile = ":/question.svg"; + break; + case ICON_WARNING: + imageFile = ":/warning.svg"; + break; + case ICON_ERROR: + imageFile = ":/alert.svg"; + break; + case ICON_AUTOMATIC: + + if (text.rfind("?") != std::string::npos || name1 == _("YES")) + imageFile = ":/question.svg"; + else if (name1 == _("OK")) + { + if (name2.empty()) + imageFile = ":/info.svg"; + else + imageFile = ":/question.svg"; + } + + break; + } + + if (!imageFile.empty() && ResourceManager::getInstance()->fileExists(imageFile) && !Renderer::isSmallScreen()) + { + mImage = std::make_shared(window); + mImage->setImage(imageFile); + mImage->setColorShift(theme->Text.color); + mImage->setMaxSize(theme->Text.font->getLetterHeight() * 2.0f, theme->Text.font->getLetterHeight() * 2.0f); + + mGrid.setEntry(mImage, Vector2i(0, 0), false, false); + } + mMsg = std::make_shared(mWindow, text, ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color, mImage == nullptr || Renderer::isSmallScreen() ? ALIGN_CENTER : ALIGN_LEFT); // CENTER + mMsg->setPadding(Vector4f(Renderer::getScreenWidth()*0.015f, 0, Renderer::getScreenWidth()*0.015f, 0)); - mMsg = std::make_shared(mWindow, text, ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color, ALIGN_CENTER); - mGrid.setEntry(mMsg, Vector2i(0, 0), false, false); + mGrid.setEntry(mMsg, Vector2i(mImage == nullptr ? 0 : 1, 0), false, false, Vector2i(mImage == nullptr ? 2 : 1, 1)); // create the buttons mButtons.push_back(std::make_shared(mWindow, name1, name1, std::bind(&GuiMsgBox::deleteMeAndCall, this, func1))); @@ -30,7 +88,7 @@ GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, if(!name3.empty()) mButtons.push_back(std::make_shared(mWindow, name3, name3, std::bind(&GuiMsgBox::deleteMeAndCall, this, func3))); - // set accelerator automatically (button to press when "b" is pressed) + // set accelerator automatically (button to press when BUTTON_BACK is pressed) if(mButtons.size() == 1) { mAcceleratorFunc = mButtons.front()->getPressedFunc(); @@ -47,20 +105,33 @@ GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, // put the buttons into a ComponentGrid mButtonGrid = makeButtonGrid(mWindow, mButtons); - mGrid.setEntry(mButtonGrid, Vector2i(0, 1), true, false, Vector2i(1, 1), GridFlags::BORDER_TOP); + mGrid.setEntry(mButtonGrid, Vector2i(0, 1), true, false, Vector2i(2, 1), GridFlags::BORDER_TOP); // decide final width if(mMsg->getSize().x() < width && mButtonGrid->getSize().x() < width) { // mMsg and buttons are narrower than width - width = Math::max(mButtonGrid->getSize().x(), mMsg->getSize().x()); + width = Math::max(mButtonGrid->getSize().x(), mMsg->getSize().x() + 3 * HORIZONTAL_PADDING_PX); + + if (mImage != nullptr) + width += mImage->getSize().x() + 2 * HORIZONTAL_PADDING_PX; + width = Math::max(width, minWidth); } - + // now that we know width, we can find height mMsg->setSize(width, 0); // mMsg->getSize.y() now returns the proper length - const float msgHeight = Math::max(Font::get(FONT_SIZE_LARGE)->getHeight(), mMsg->getSize().y()*1.225f); - setSize(width + HORIZONTAL_PADDING_PX*2, msgHeight + mButtonGrid->getSize().y()); + + float msgHeight = Math::max(Font::get(FONT_SIZE_LARGE)->getHeight(), mMsg->getSize().y()*1.225f); + + if (msgHeight + mButtonGrid->getSize().y() > Renderer::getScreenHeight()) + { + setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + if (mImage != nullptr) + mMsg->setSize(Renderer::getScreenWidth() - mImage->getSize().x() - 4* HORIZONTAL_PADDING_PX, 0); + } + else + setSize(width + HORIZONTAL_PADDING_PX*2, msgHeight + mButtonGrid->getSize().y()); // center for good measure setPosition((Renderer::getScreenWidth() - mSize.x()) / 2.0f, (Renderer::getScreenHeight() - mSize.y()) / 2.0f); @@ -72,15 +143,19 @@ GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, bool GuiMsgBox::input(InputConfig* config, Input input) { // special case for when GuiMsgBox comes up to report errors before anything has been configured - if(config->getDeviceId() == DEVICE_KEYBOARD && !config->isConfigured() && input.value && + if(config->getDeviceId() == DEVICE_KEYBOARD && !config->isConfigured() && input.value && (input.id == SDLK_RETURN || input.id == SDLK_ESCAPE || input.id == SDLK_SPACE)) { mAcceleratorFunc(); return true; } - if(mAcceleratorFunc && config->isMappedTo("b", input) && input.value != 0) + /* when it's not configured, allow to remove the message box too to allow the configdevice window a chance */ + if(mAcceleratorFunc && ((config->isMappedTo("b", input) && input.value != 0) || (config->isConfigured() == false && input.type == TYPE_BUTTON))) // batocera + { mAcceleratorFunc(); + return true; + } return GuiComponent::input(config, input); } @@ -88,9 +163,15 @@ bool GuiMsgBox::input(InputConfig* config, Input input) void GuiMsgBox::onSizeChanged() { mGrid.setSize(mSize); - mGrid.setRowHeightPerc(1, mButtonGrid->getSize().y() / mSize.y()); - // update messagebox size + if (mImage != nullptr) + { + auto width = mImage->getSize().x() + (Renderer::isSmallScreen() ? 5 : 2) * HORIZONTAL_PADDING_PX; + mGrid.setColWidthPerc(0, width / mSize.x(), true); + } + + mGrid.setRowHeightPerc(1, mButtonGrid->getSize().y() / mSize.y()); + mMsg->setSize(mSize.x() - HORIZONTAL_PADDING_PX*2, mGrid.getRowHeight(0)); mGrid.onSizeChanged(); diff --git a/es-core/src/guis/GuiMsgBox.h b/es-core/src/guis/GuiMsgBox.h index cd9883244b..a710002377 100644 --- a/es-core/src/guis/GuiMsgBox.h +++ b/es-core/src/guis/GuiMsgBox.h @@ -8,26 +8,49 @@ class ButtonComponent; class TextComponent; +class ImageComponent; + +enum GuiMsgBoxIcon +{ + ICON_AUTOMATIC, + ICON_INFORMATION, + ICON_QUESTION, + ICON_WARNING, + ICON_ERROR +}; class GuiMsgBox : public GuiComponent { public: + GuiMsgBox(Window* window, const std::string& text, + const std::string& name1, const std::function& func1, + const std::string& name2, const std::function& func2, + const std::string& name3, const std::function& func3, + GuiMsgBoxIcon icon = ICON_AUTOMATIC); + + + GuiMsgBox(Window* window, const std::string& text, + const std::string& name1, const std::function& func1, + const std::string& name2, const std::function& func2, + GuiMsgBoxIcon icon = ICON_AUTOMATIC); + GuiMsgBox(Window* window, const std::string& text, const std::string& name1 = "OK", const std::function& func1 = nullptr, - const std::string& name2 = "", const std::function& func2 = nullptr, - const std::string& name3 = "", const std::function& func3 = nullptr); + GuiMsgBoxIcon icon = ICON_AUTOMATIC); bool input(InputConfig* config, Input input) override; void onSizeChanged() override; std::vector getHelpPrompts() override; + std::string getValue() const override { return "GuiMsgBox"; } + private: void deleteMeAndCall(const std::function& func); NinePatchComponent mBackground; - ComponentGrid mGrid; - + ComponentGrid mGrid; + std::shared_ptr mImage; std::shared_ptr mMsg; std::vector< std::shared_ptr > mButtons; std::shared_ptr mButtonGrid; diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp index fa2875c4aa..f301d39f4d 100644 --- a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp @@ -1,184 +1,211 @@ #include "guis/GuiTextEditPopupKeyboard.h" #include "components/MenuComponent.h" -#include "Log.h" #include "utils/StringUtil.h" +#include "Log.h" +#include "EsLocale.h" -std::vector> kbFrench { - { _L("&"), _L("é"), _L("\""), _L("'"), _L("("), _L("#"), _L("è"), _L("!"), _L("ç"), _L("à"), _L(")"), _L("-") }, - { _L("1"), _L("2"), _L("3"), _L("4"), _L("5"), _L("6"), _L("7"), _L("8"), _L("9"), _L("0"), _L("@"), _L("_") }, - /* - { _L("à"), _L("ä"), _L("ë"), _L("ì"), _L("ï"), _L("ò"), _L("ö"), _L("ü"), _L("\\"), _L("|"), _L("§"), _L("°") }, - { _L("á"), _L("â"), _L("ê"), _L("í"), _L("î"), _L("ó"), _L("ô"), _L("ú", "û"), _L("ñ"), _L("¡"), _L("¿") }, - */ - { _L("a"), _L("z"), _L("e"), _L("r"), _L("t"), _L("y"), _L("u"), _L("i"), _L("o"), _L("p"), _L("^"), _L("$") }, - { _L("A"), _L("Z"), _L("E"), _L("R"), _L("T"), _L("Y"), _L("U"), _L("I"), _L("O"), _L("P"), _L("¨"), _L("*") }, - - { _L("q"), _L("s"), _L("d"), _L("f"), _L("g"), _L("h"), _L("j"), _L("k"), _L("l"), _L("m"), _L("ù"), _L("`") }, - { _L("Q"), _L("S"), _L("D"), _L("F"), _L("G"), _L("H"), _L("J"), _L("K"), _L("L"), _L("M"), _L("%"), _L("£") }, - - //SHIFT key at position 0 - { _L("SHIFT"), _L("<"), _L("w"), _L("x"), _L("c"), _L("v"), _L("b"), _L("n"), _L(","), _L(";"), _L(":"), _L("=") }, - { _L("SHIFT"), _L(">"), _L("W"), _L("X"), _L("C"), _L("V"), _L("B"), _L("N"), _L("?"), _L("."), _L("/"), _L("+") } -}; +std::vector> kbUs { -std::vector> kbUs { + { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "_", "+" }, + { "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", "-", "=" }, - { _L("1"), _L("2"), _L("3"), _L("4"), _L("5"), _L("6"), _L("7"), _L("8"), _L("9"), _L("0"), _L("_"), _L("+") }, - { _L("!"), _L("@"), _L("#"), _L("$"), _L("%"), _L("^"), _L("&"), _L("*"), _L("("), _L(")"), _L("-"), _L("=") }, -/* - { _L("à"), _L("ä"), _L("è"), _L("ë"), _L("ì"), _L("ï"), _L("ò"), _L("ö"), _L("ù"), _L("ü"), _L("¨"), _L("¿") }, - { _L("á"), _L("â"), _L("é"), _L("ê"), _L("í"), _L("î"), _L("ó"), _L("ô"), _L("ú"), _L("û"), _L("ñ"), _L("¡") }, - */ - { _L("q"), _L("w"), _L("e"), _L("r"), _L("t"), _L("y"), _L("u"), _L("i"), _L("o"), _L("p"), _L("{"), _L("}") }, - { _L("Q"), _L("W"), _L("E"), _L("R"), _L("T"), _L("Y"), _L("U"), _L("I"), _L("O"), _L("P"), _L("["), _L("]") }, + { "à", "ä", "è", "ë", "ì", "ï", "ò", "ö", "ù", "ü", "¨", "¿" }, + { "á", "â", "é", "ê", "í", "î", "ó", "ô", "ú", "û", "ñ", "¡" }, + + { "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "{", "}" }, + { "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "[", "]" }, + + { "a", "s", "d", "f", "g", "h", "j", "k", "l", ";", "\"", "|" }, + { "A", "S", "D", "F", "G", "H", "J", "K", "L", ":", "'", "\\" }, + + { "SHIFT", "~", "z", "x", "c", "v", "b", "n", "m", ",", ".", "?" }, + { "SHIFT", "`", "Z", "X", "C", "V", "B", "N", "M", "<", ">", "/" }, +}; - { _L("a"), _L("s"), _L("d"), _L("f"), _L("g"), _L("h"), _L("j"), _L("k"), _L("l"), _L(";"), _L("\""), _L("|") }, - { _L("A"), _L("S"), _L("D"), _L("F"), _L("G"), _L("H"), _L("J"), _L("K"), _L("L"), _L(":"), _L("'"), _L("\\") }, +std::vector> kbFr { + { "&", "é", "\"", "'", "(", "#", "è", "!", "ç", "à", ")", "-" }, + { "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "@", "_" }, + + { "à", "ä", "ë", "ì", "ï", "ò", "ö", "ü", "\\", "|", "§", "°" }, + { "á", "â", "ê", "í", "î", "ó", "ô", "ú", "û", "ñ", "¡", "¿" }, + + { "a", "z", "e", "r", "t", "y", "u", "i", "o", "p", "^", "$" }, + { "A", "Z", "E", "R", "T", "Y", "U", "I", "O", "P", "¨", "*" }, - { _L("SHIFT"), _L("~"), _L("z"), _L("x"), _L("c"), _L("v"), _L("b"), _L("n"), _L("m"), _L(","), _L("."), _L("?") }, - { _L("SHIFT"), _L("`"), _L("Z"), _L("X"), _L("C"), _L("V"), _L("B"), _L("N"), _L("M"), _L("<"), _L(">"), _L("/") }, + { "q", "s", "d", "f", "g", "h", "j", "k", "l", "m", "ù", "`" }, + { "Q", "S", "D", "F", "G", "H", "J", "K", "L", "M", "%", "£" }, + + { "SHIFT", "<", "w", "x", "c", "v", "b", "n", ",", ";", ":", "=" }, + { "SHIFT", ">", "W", "X", "C", "V", "B", "N", "?", ".", "/", "+" } }; -GuiTextEditPopupKeyboard::GuiTextEditPopupKeyboard(Window* window, const std::string& title, const std::string& initValue, - const std::function& okCallback, bool multiLine, const char* acceptBtnText) +GuiTextEditPopupKeyboard::GuiTextEditPopupKeyboard(Window* window, const std::string& title, const std::string& initValue, + const std::function& okCallback, bool multiLine, const std::string acceptBtnText) : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 7)), mMultiLine(multiLine) { auto theme = ThemeData::getMenuTheme(); - - mBackground.setImagePath(theme->Background.path); // ":/frame.png" + mBackground.setImagePath(theme->Background.path); mBackground.setCenterColor(theme->Background.color); mBackground.setEdgeColor(theme->Background.color); addChild(&mBackground); addChild(&mGrid); - mTitle = std::make_shared(mWindow, Utils::String::toUpper(title), ThemeData::getMenuTheme()->Title.font, ThemeData::getMenuTheme()->Title.color, ALIGN_CENTER); + mTitle = std::make_shared(mWindow, Utils::String::toUpper(title), theme->Title.font, theme->Title.color, ALIGN_CENTER); + + // Accept/Cancel/Delete/Space buttons + std::vector > buttons; + + buttons.push_back(std::make_shared(mWindow, acceptBtnText, acceptBtnText, [this, okCallback] { okCallback(mText->getValue()); delete this; })); + auto space = std::make_shared(mWindow, _("SPACE"), _("SPACE"), [this] { + mText->startEditing(); + mText->textInput(" "); + mText->stopEditing(); + }); + + if (Renderer::isSmallScreen()) + space->setSize(space->getSize().x(), space->getSize().y()); + else + space->setSize(space->getSize().x() * 3, space->getSize().y()); + + buttons.push_back(space); + buttons.push_back(std::make_shared(mWindow, _("DELETE"), _("DELETE A CHAR"), [this] { + mText->startEditing(); + mText->textInput("\b"); + mText->stopEditing(); + })); + + buttons.push_back(std::make_shared(mWindow, _("RESET"), _("RESET"), [this, okCallback] { okCallback(""); delete this; })); + + buttons.push_back(std::make_shared(mWindow, _("CANCEL"), _("DISCARD CHANGES"), [this] { delete this; })); + + // Add buttons + mButtons = makeButtonGrid(mWindow, buttons); + + mKeyboardGrid = std::make_shared(mWindow, Vector2i(12, 5)); mText = std::make_shared(mWindow); mText->setValue(initValue); - if(!multiLine) + if (!multiLine) mText->setCursor(initValue.size()); // Header mGrid.setEntry(mTitle, Vector2i(0, 0), false, true); // Text edit add - mGrid.setEntry(mText, Vector2i(0, 1), true, false, Vector2i(1, 1)); + mGrid.setEntry(mText, Vector2i(0, 1), true, false, Vector2i(1, 1), GridFlags::BORDER_TOP | GridFlags::BORDER_BOTTOM); std::vector< std::vector< std::shared_ptr > > buttonList; // Keyboard // Case for if multiline is enabled, then don't create the keyboard. if (!mMultiLine) - { - std::vector> &layout = kbUs; + { + std::vector>* layout = &kbUs; + + std::string language = EsLocale::getLanguage(); + if (!language.empty()) + { + auto shortNameDivider = language.find("_"); + if (shortNameDivider != std::string::npos) + language = Utils::String::toLower(language.substr(0, shortNameDivider)); + } - if (EsLocale::getLanguage() == "fr") - layout = kbFrench; + if (language == "fr") + layout = &kbFr; - for (unsigned int i = 0; i < layout.size() / 2; i++) - { + for (unsigned int i = 0; i < 5; i++) + { std::vector> buttons; - for (unsigned int j = 0; j < layout[2 * i].size(); j++) - { -#ifdef WIN32 - std::wstring toConvert = layout[2 * i][j]; - std::string atj = Utils::String::convertFromWideString(toConvert); - - toConvert = layout[2 * i + 1][j]; - std::string atjs = Utils::String::convertFromWideString(toConvert); -#else - std::string atj = layout[2 * i][j]; - std::string atjs = layout[2 * i + 1][j]; - -#endif - - if (atj == "SHIFT") + for (unsigned int j = 0; j < 12; j++) + { + std::string lower = (*layout)[2 * i][j]; + std::string upper = (*layout)[2 * i + 1][j]; + + std::shared_ptr button = nullptr; + + if (lower == "SHIFT") { // Special case for shift key - mShiftButton = std::make_shared(mWindow, "SHIFT", _("SHIFTS FOR UPPER,LOWER, AND SPECIAL"), [this] { + mShiftButton = std::make_shared(mWindow, _U("\uF176"), _("SHIFTS FOR UPPER,LOWER, AND SPECIAL"), [this] { shiftKeys(); - }); - buttons.push_back(mShiftButton); - } - else - buttons.push_back(makeButton(atj, atjs)); - } - buttonList.push_back(buttons); - } - } - - const float gridWidth = Renderer::getScreenWidth() * 0.85f; - mKeyboardGrid = makeMultiDimButtonGrid(mWindow, buttonList, gridWidth - 20); - mGrid.setEntry(mKeyboardGrid, Vector2i(0, 2), true, false); + }, false); - // Accept/Cancel buttons - buttons.push_back(std::make_shared(mWindow, _(acceptBtnText), _(acceptBtnText), [this, okCallback] { okCallback(mText->getValue()); delete this; })); - - buttons.push_back(std::make_shared(mWindow, _("SPACE"), _("SPACE"), [this] { - mText->startEditing(); - mText->textInput(" "); - mText->stopEditing(); - })); + button = mShiftButton; + } + else + button = makeButton(lower, upper); - buttons.push_back(std::make_shared(mWindow, _("DELETE"), _("DELETE A CHAR"), [this] { - mText->startEditing(); - mText->textInput("\b"); - mText->stopEditing(); - })); + buttons.push_back(button); - buttons.push_back(std::make_shared(mWindow, _("CANCEL"), _("discard changes"), [this] { delete this; })); + button->setSize(getButtonSize()); + mKeyboardGrid->setEntry(button, Vector2i(j, i), true, false); + buttonList.push_back(buttons); + } + } + // END KEYBOARD IF + } - mButtons = makeButtonGrid(mWindow, buttons); - mGrid.setEntry(mButtons, Vector2i(0, 3), true, false); + // Add keyboard keys + mGrid.setEntry(mKeyboardGrid, Vector2i(0, 2), true, true, Vector2i(2, 4)); + mGrid.setEntry(mButtons, Vector2i(0, 6), true, false); // Determine size from text size float textHeight = mText->getFont()->getHeight(); if (multiLine) textHeight *= 6; - mText->setSize(gridWidth - 40, textHeight); + mText->setSize(0, textHeight); // If multiline, set all diminsions back to default, else draw size for keyboard. - if (mMultiLine) + if (mMultiLine) { - setSize(Renderer::getScreenWidth() * 0.5f, mTitle->getFont()->getHeight() + textHeight + mKeyboardGrid->getSize().y() + 40); + if (Renderer::isSmallScreen()) + setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + else + setSize(Renderer::getScreenWidth() * 0.5f, mTitle->getFont()->getHeight() + textHeight + mKeyboardGrid->getSize().y() + 40); + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); } - else + else { - setSize(gridWidth, mTitle->getFont()->getHeight() + textHeight + 40 + mKeyboardGrid->getSize().y() + mButtons->getSize().y()); + if (Renderer::isSmallScreen()) + setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + else // Set size based on ScreenHieght * .08f by the amount of keyboard rows there are. + setSize(Renderer::getScreenWidth() * 0.95f, mTitle->getFont()->getHeight() + textHeight + 40 + (Renderer::getScreenHeight() * 0.085f) * 6); + setPosition((Renderer::getScreenWidth() - mSize.x()) / 2, (Renderer::getScreenHeight() - mSize.y()) / 2); } } + void GuiTextEditPopupKeyboard::onSizeChanged() { mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); mText->setSize(mSize.x() - 40, mText->getSize().y()); - float fullHeight = mTitle->getFont()->getHeight() + mText->getSize().y() + mKeyboardGrid->getSize().y() + mButtons->getSize().y(); - // update grid - mGrid.setRowHeightPerc(0, mTitle->getFont()->getHeight() / fullHeight); - mGrid.setRowHeightPerc(1, mText->getSize().y() / fullHeight); - mGrid.setRowHeightPerc(2, mKeyboardGrid->getSize().y() / fullHeight); - mGrid.setRowHeightPerc(3, mButtons->getSize().y() / fullHeight); + mGrid.setRowHeightPerc(0, mTitle->getFont()->getHeight() / mSize.y()); + mGrid.setRowHeightPerc(2, mKeyboardGrid->getSize().y() / mSize.y()); + mGrid.setRowHeightPerc(6, mButtons->getSize().y() / mSize.y()); mGrid.setSize(mSize); - mKeyboardGrid->onSizeChanged(); - /* - mBackground.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); - - mText->setSize(mSize.x() - 40, mText->getSize().y()); + // force the keyboard size and position here + // for an unknown reason, without setting that, the position is "sometimes" (1/2 on s905x for example) not displayed correctly + // as if a variable were not correctly initialized - // update grid - mGrid.setRowHeightPerc(0, mTitle->getFont()->getHeight() / mSize.y()); - mGrid.setRowHeightPerc(2, mKeyboardGrid->getSize().y() / mSize.y()); - - mGrid.setSize(mSize);*/ + if (Renderer::isSmallScreen()) // small screens // batocera + { + mKeyboardGrid->setSize(getButtonSize().x() * 12.0f, getButtonSize().y() * 5.0f); + mKeyboardGrid->setPosition(Renderer::getScreenWidth() * 0.05f / 2.00f, mTitle->getFont()->getHeight() + mText->getFont()->getHeight() + 15 + 6); + } + else + { + mKeyboardGrid->setSize(getButtonSize().x() * 12.2f, getButtonSize().y() * 5.2f); // Small margin between buttons + mKeyboardGrid->setPosition(Renderer::getScreenWidth() * 0.05f / 2.00f, mTitle->getFont()->getHeight() + mText->getFont()->getHeight() + 40 + 6); + } } bool GuiTextEditPopupKeyboard::input(InputConfig* config, Input input) @@ -194,20 +221,20 @@ bool GuiTextEditPopupKeyboard::input(InputConfig* config, Input input) } // For deleting a chara (Left Top Button) - if (config->isMappedTo("lefttop", input) && input.value) { + if (config->isMappedTo("PageUp", input) && input.value) { mText->startEditing(); mText->textInput("\b"); mText->stopEditing(); } // For Adding a space (Right Top Button) - if (config->isMappedTo("righttop", input) && input.value) { + if (config->isMappedTo("PageDown", input) && input.value) { mText->startEditing(); mText->textInput(" "); } - // For Shifting (X) - if (config->isMappedTo("x", input) && input.value) { + // For Shifting (Y) + if (config->isMappedTo("y", input) && input.value) { if (mShift) mShift = false; else mShift = true; shiftKeys(); @@ -217,25 +244,10 @@ bool GuiTextEditPopupKeyboard::input(InputConfig* config, Input input) return false; } - +/* void GuiTextEditPopupKeyboard::update(int deltatime) { -} - -std::shared_ptr GuiTextEditPopupKeyboard::makeButton(const std::string& key, const std::string& shiftedKey) -{ - std::shared_ptr button = std::make_shared(mWindow, key, key, [this, key, shiftedKey] { - mText->startEditing(); - if (mShift) - mText->textInput(shiftedKey.c_str()); - else - mText->textInput(key.c_str()); - mText->stopEditing(); - }, false); - KeyboardButton kb(button, key, shiftedKey); - keyboardButtons.push_back(kb); - return button; -} +}*/ // Shifts the keys when user hits the shift button. void GuiTextEditPopupKeyboard::shiftKeys() @@ -251,16 +263,46 @@ void GuiTextEditPopupKeyboard::shiftKeys() { const std::string& text = mShift ? kb.shiftedKey : kb.key; kb.button->setText(text, text, false); + kb.button->setSize(getButtonSize()); } } std::vector GuiTextEditPopupKeyboard::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); - prompts.push_back(HelpPrompt("x", _("SHIFT"))); + prompts.push_back(HelpPrompt("y", _("SHIFT"))); prompts.push_back(HelpPrompt("b", _("BACK"))); prompts.push_back(HelpPrompt("r", _("SPACE"))); prompts.push_back(HelpPrompt("l", _("DELETE"))); return prompts; } +std::shared_ptr GuiTextEditPopupKeyboard::makeButton(const std::string& key, const std::string& shiftedKey) +{ + std::shared_ptr button = std::make_shared(mWindow, key, key, [this, key, shiftedKey] + { + mText->startEditing(); + + if (mShift) + mText->textInput(shiftedKey.c_str()); + else + mText->textInput(key.c_str()); + + mText->stopEditing(); + }, false); + + KeyboardButton kb(button, key, shiftedKey); + keyboardButtons.push_back(kb); + return button; +} + +const Vector2f GuiTextEditPopupKeyboard::getButtonSize() +{ + if (Renderer::isSmallScreen()) + { + float height = (Renderer::getScreenHeight() - mText->getSize().y() - mTitle->getSize().y() - mButtons->getSize().y()) / 6.0; + return Vector2f((Renderer::getScreenWidth() * 0.95f) / 12.0f, height); + } + + return Vector2f((Renderer::getScreenWidth() * 0.89f) / 12.0f, mText->getFont()->getHeight() + 6.0f); +} \ No newline at end of file diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.h b/es-core/src/guis/GuiTextEditPopupKeyboard.h index cf3c13d55e..73f86cea57 100644 --- a/es-core/src/guis/GuiTextEditPopupKeyboard.h +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.h @@ -10,15 +10,14 @@ class GuiTextEditPopupKeyboard : public GuiComponent { public: GuiTextEditPopupKeyboard(Window* window, const std::string& title, const std::string& initValue, - const std::function& okCallback, bool multiLine, const char* acceptBtnText = "OK"); + const std::function& okCallback, bool multiLine, const std::string acceptBtnText = "OK"); bool input(InputConfig* config, Input input); - void update(int deltatime) override; + //void update(int deltatime) override; void onSizeChanged(); std::vector getHelpPrompts() override; private: - class KeyboardButton { public: @@ -27,25 +26,23 @@ class GuiTextEditPopupKeyboard : public GuiComponent const std::string shiftedKey; KeyboardButton(const std::shared_ptr b, const std::string& k, const std::string& sk) : button(b), key(k), shiftedKey(sk) {}; }; - + std::shared_ptr makeButton(const std::string& key, const std::string& shiftedKey); + std::vector keyboardButtons; + std::shared_ptr mShiftButton; + const Vector2f getButtonSize(); void shiftKeys(); NinePatchComponent mBackground; ComponentGrid mGrid; - // Vectors for button rows - std::vector< std::shared_ptr > buttons; - - std::vector keyboardButtons; - std::shared_ptr mShiftButton; - std::shared_ptr mTitle; std::shared_ptr mText; std::shared_ptr mKeyboardGrid; std::shared_ptr mButtons; - + bool mMultiLine; bool mShift = false; }; + diff --git a/es-core/src/renderers/Renderer.cpp b/es-core/src/renderers/Renderer.cpp index 391a98accc..72b6d4df05 100644 --- a/es-core/src/renderers/Renderer.cpp +++ b/es-core/src/renderers/Renderer.cpp @@ -368,4 +368,24 @@ namespace Renderer int getScreenRotate() { return screenRotate; } bool isSmallScreen() { return screenWidth < 400 && screenHeight < 400; }; + + unsigned int mixColors(unsigned int first, unsigned int second, float percent) + { + unsigned char alpha0 = (first >> 24) & 0xFF; + unsigned char blue0 = (first >> 16) & 0xFF; + unsigned char green0 = (first >> 8) & 0xFF; + unsigned char red0 = first & 0xFF; + + unsigned char alpha1 = (second >> 24) & 0xFF; + unsigned char blue1 = (second >> 16) & 0xFF; + unsigned char green1 = (second >> 8) & 0xFF; + unsigned char red1 = second & 0xFF; + + unsigned char alpha = (unsigned char)(alpha0 * (1.0 - percent) + alpha1 * percent); + unsigned char blue = (unsigned char)(blue0 * (1.0 - percent) + blue1 * percent); + unsigned char green = (unsigned char)(green0 * (1.0 - percent) + green1 * percent); + unsigned char red = (unsigned char)(red0 * (1.0 - percent) + red1 * percent); + + return (alpha << 24) | (blue << 16) | (green << 8) | red; + } } // Renderer:: diff --git a/es-core/src/renderers/Renderer.h b/es-core/src/renderers/Renderer.h index 05c01387af..88b43577cf 100644 --- a/es-core/src/renderers/Renderer.h +++ b/es-core/src/renderers/Renderer.h @@ -105,6 +105,8 @@ namespace Renderer // GPI Case bool isSmallScreen(); + + unsigned int mixColors(unsigned int first, unsigned int second, float percent); } // Renderer:: #endif // ES_CORE_RENDERER_RENDERER_H diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index 297232a847..f13fdd23af 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -172,6 +172,12 @@ void TextureResource::resetCache() sTextureDataManager.clearQueue(); } +void TextureResource::cancelAsync(std::shared_ptr texture) +{ + if (texture != nullptr) + sTextureDataManager.cancelAsync(texture.get()); +} + std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic, bool asReloadable, MaxSizeInfo maxSize) { std::shared_ptr& rm = ResourceManager::getInstance(); diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index fdb062a4ea..254f263fdd 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -52,6 +52,8 @@ class TextureResource : public IReloadable public: static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true, bool asReloadable = true, MaxSizeInfo maxSize = MaxSizeInfo()); + static void cancelAsync(std::shared_ptr texture); + void initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height); void initFromExternalPixels(unsigned char* dataRGBA, size_t width, size_t height); virtual void initFromMemory(const char* file, size_t length); diff --git a/resources/alert.svg b/resources/alert.svg new file mode 100644 index 0000000000..dd5fb44755 --- /dev/null +++ b/resources/alert.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/glyphs.ttf b/resources/glyphs.ttf index 238aacf77559719c876b83163fad7c471f284416..ed9372f8ea0fbaa04f42630a48887e4b38945345 100644 GIT binary patch literal 122092 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 +|+&Vdls@0SAya!5R?! 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

6YuX1v{q9=vI}{pN+P4FW!CI?#11< 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 z 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 literal 1904 zcmdT^O>7%Q6n^7%*0r4b0+y3NFe6q?#zuN}K}Qe-u)LqUk96oZgZyS2w&3)}0i zH$Rq;h*S!+;(&yNBIv1bKrKi}D5{VUltU$i$_XTt8=&5hAO|=AY0R7TSi&hPXJ#}r z-+SMi_h#RlSs(!5g%yxMdFtt?dOmntN6lwA&gQFH{Ri#NI)JXMIQ^yjIC57|-qe{^@U5=Di~NTuCv{Y~64EEgKZo*zdevH%$L#%gQ1(@fyoMac za;%~psA|h~a6uY%FQH6pT9x^pc;i*%H!-ZYZZxb%$8IenA4L8zNYVg#6M7Iod{6%h zkRaGQ&DR0A9xa;%$ZXIJSz;{)!xH*j5xfH&98nxL0BCLf!wu&;&53{zi8x#fwQ^Qb zk}{^GuB)8Kz-`~$2u?^h?(yNYHBS6q5r-RgHLj17ccf`>gC8KJ#uNU~zQ9mGQpbJ% z(6}#Nx?TJxeu?-mZT(vOk=#6V{sVGx>+apZ2o{N-Uuu0V-H={@LChAAWNKpn#D3W& zyM{+(S28s`GC=~i6xbJPGZQJ>Xexp0U^1?b4D;@|Jo(CzwcYc(=V#UqUhvT5)#OzV z>FYm!x%Z(FZ*T5;=H_6?<)?`&-aTi(Sn>KF@J!|&`<@czz>0fU>b>Oq?p>2F&#div zCYU=yF7)}2y*Bq#P!2yoUhMOYo%uYz>hB$r`h&zdI{NHK9?DBvjLzzhJf6UySz?zuzvuV~n%yhC3wGl2*&k`Viqc=E)+^ zZ(DHeK+GkMorr>MKPc^l0|FDgsYeBNAU+|m1fy_LU?=L!0{38@>jKL#0M`U|!908_ zup2zYBd`~SNI>9z*h?Par*XRR3<{YO7|%?W1jaLyivmlalD7oL^OIWw_rOl_t-vz+ z{28*K!6MEE)WCvMz@P*RP(iKP&CxKOHJN6y0$nOwIz8R2Ga8SmS#2?E)T~piv{2DZ zA%%q)R+ZS{^i*0+2_nbLM%k#*SVU2#pVZ8}PGj+d6wMAzb2oMf!=$lTWGbRW)Px$N z$wc^E+1lp$WQA!BMom^^CbbM|>5R@A^`==a=@xCUyv4&5(G_Qk=$cxlR$x`le35FF zWtPt^*d{f@D(6`va!gxfbWW=kn!07x)6pp3qb;$9QDu>`@vq8g1-I53EQ&viFpgPF z8pG6CI2TY}!s1(?BRUODEIvaNhX| + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/question.svg b/resources/question.svg new file mode 100644 index 0000000000..39a090afdf --- /dev/null +++ b/resources/question.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/resources/search.svg b/resources/search.svg new file mode 100644 index 0000000000..c712b43b07 --- /dev/null +++ b/resources/search.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/resources/warning.svg b/resources/warning.svg new file mode 100644 index 0000000000..9cf70aa4b0 --- /dev/null +++ b/resources/warning.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + From fbbd58ab61955b35dd1de2ac97dc6f76133953f1 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 6 Oct 2019 18:38:24 +0200 Subject: [PATCH 151/336] =?UTF-8?q?TextComponent=20:=20Support=20for=20ref?= =?UTF-8?q?lexion=20(=20=20using=20"reflexion"=20&=20"reflexionOnFrame"=20?= =?UTF-8?q?=C3=A9lements=20)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- es-core/src/ThemeData.cpp | 3 +- es-core/src/components/TextComponent.cpp | 61 ++++++++++++++++++++---- es-core/src/components/TextComponent.h | 6 ++- es-core/src/resources/Font.cpp | 54 ++++++++++++++++++++- es-core/src/resources/Font.h | 2 + 5 files changed, 112 insertions(+), 14 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 0aa7ce9d9d..c18b3444d8 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -89,7 +89,8 @@ std::map> The { "forceUppercase", BOOLEAN }, { "lineSpacing", FLOAT }, { "value", STRING }, - + { "reflexion", NORMALIZED_PAIR }, + { "reflexionOnFrame", BOOLEAN }, { "glowColor", COLOR }, { "glowSize", FLOAT }, { "glowOffset", NORMALIZED_PAIR }, diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index 504142ae14..027c7e8028 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -5,9 +5,10 @@ #include "Settings.h" TextComponent::TextComponent(Window* window) : GuiComponent(window), -mFont(Font::get(FONT_SIZE_MEDIUM)), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), -mHorizontalAlignment(ALIGN_LEFT), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), -mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)), mGlowOffset(Vector2f(0, 0)) + mFont(Font::get(FONT_SIZE_MEDIUM)), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), + mHorizontalAlignment(ALIGN_LEFT), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), + mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)), mGlowOffset(Vector2f(0, 0)) + mReflection(0.0f, 0.0f), mReflectOnBorders(false) { } @@ -15,7 +16,8 @@ TextComponent::TextComponent(Window* window, const std::string& text, const std: Vector3f pos, Vector2f size, unsigned int bgcolor) : GuiComponent(window), mFont(NULL), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), mHorizontalAlignment(align), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), - mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)), mGlowOffset(Vector2f(0, 0)) + mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)), mGlowOffset(Vector2f(0, 0)), + mReflection(0.0f, 0.0f), mReflectOnBorders(false) { setFont(font); setColor(color); @@ -197,6 +199,32 @@ void TextComponent::render(const Transform4x4f& parentTrans) } mFont->renderTextCache(mTextCache.get()); + + if (mReflection.x() != 0 || mReflection.y() != 0) + { + Transform4x4f mirror = trans; + mirror.translate(-off); + mirror.r1().y() = -mirror.r1().y(); + mirror.r3().y() = mirror.r3().y() + off.y() + textSize.y(); + + if (mReflectOnBorders) + mirror.r3().y() = mirror.r3().y() + mSize.y(); + else + mirror.r3().y() = mirror.r3().y() + textSize.y(); + + Renderer::setMatrix(mirror); + + float baseOpacity = mOpacity / 255.0; + float alpha = baseOpacity * ((mColor & 0x000000ff)) / 255.0; + float alpha2 = baseOpacity * alpha * mReflection.y(); + + alpha *= mReflection.x(); + + const unsigned int colorT = Renderer::convertColor((mColor & 0xffffff00) + (unsigned char)(255.0*alpha)); + const unsigned int colorB = Renderer::convertColor((mColor & 0xffffff00) + (unsigned char)(255.0*alpha2)); + + mFont->renderGradientTextCache(mTextCache.get(), colorB, colorT); + } } } @@ -348,14 +376,27 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, const st if (properties & LINE_SPACING && elem->has("lineSpacing")) setLineSpacing(elem->get("lineSpacing")); - if (properties & COLOR && elem->has("glowColor")) - mGlowColor = elem->get("glowColor"); + if (properties & COLOR) + { + if (elem->has("glowColor")) + mGlowColor = elem->get("glowColor"); + + if (elem->has("glowSize")) + mGlowSize = (int)elem->get("glowSize"); - if (properties & COLOR && elem->has("glowSize")) - mGlowSize = (int)elem->get("glowSize"); + if (elem->has("glowOffset")) + mGlowOffset = elem->get("glowOffset"); - if (properties & COLOR && elem->has("glowOffset")) - mGlowOffset = elem->get("glowOffset"); + if (elem->has("reflexion")) + mReflection = elem->get("reflexion"); + else + mReflection = Vector2f::Zero(); + + if (elem->has("reflexionOnFrame")) + mReflectOnBorders = elem->get("reflexionOnFrame"); + else + mReflectOnBorders = false; + } setFont(Font::getFromTheme(elem, properties, mFont)); } diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index b76750f901..2bb021e18d 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -75,7 +75,11 @@ class TextComponent : public GuiComponent unsigned int mGlowColor; unsigned int mGlowSize; Vector2f mGlowOffset; - Vector4f mPadding; + Vector4f mPadding; + + + Vector2f mReflection; + bool mReflectOnBorders; }; #endif // ES_CORE_COMPONENTS_TEXT_COMPONENT_H diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index 790945d8a7..ef0bfddbf8 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -440,13 +440,63 @@ void Font::renderTextCache(TextCache* cache) { assert(*it->textureIdPtr != 0); - auto vertexList = *it; - Renderer::bindTexture(*it->textureIdPtr); Renderer::drawTriangleStrips(&it->verts[0], it->verts.size()); } } +void Font::renderGradientTextCache(TextCache* cache, unsigned int colorTop, unsigned int colorBottom, bool horz) +{ + if (cache == NULL) + { + LOG(LogError) << "Attempted to draw NULL TextCache!"; + return; + } + + for (auto it = cache->vertexLists.cbegin(); it != cache->vertexLists.cend(); it++) + { + assert(*it->textureIdPtr != 0); + + std::vector vxs; + vxs.resize(it->verts.size()); + + for (int i = 0; i < it->verts.size(); i += 6) + { + vxs[i + 1] = it->verts[i + 1]; + vxs[i + 1].col = colorTop; + + vxs[i + 2] = it->verts[i + 2]; + vxs[i + 2].col = colorBottom; + + vxs[i + 3] = it->verts[i + 3]; + vxs[i + 3].col = colorTop; + + vxs[i + 4] = it->verts[i + 4]; + vxs[i + 4].col = colorBottom; + + // make duplicates of first and last vertex so this can be rendered as a triangle strip + vxs[i + 0] = vxs[i + 1]; + vxs[i + 5] = vxs[i + 4]; + + /* + vertices[1] = { { glyphStartX , y - glyph->bearing.y() }, { glyph->texPos.x(), glyph->texPos.y() }, convertedColor }; + vertices[2] = { { glyphStartX , y - glyph->bearing.y() + (glyph->texSize.y() * textureSize.y()) }, { glyph->texPos.x(), glyph->texPos.y() + glyph->texSize.y() }, convertedColor }; + vertices[3] = { { glyphStartX + glyph->texSize.x() * textureSize.x(), y - glyph->bearing.y() }, { glyph->texPos.x() + glyph->texSize.x(), glyph->texPos.y() }, convertedColor }; + vertices[4] = { { glyphStartX + glyph->texSize.x() * textureSize.x(), y - glyph->bearing.y() + (glyph->texSize.y() * textureSize.y()) }, { glyph->texPos.x() + glyph->texSize.x(), glyph->texPos.y() + glyph->texSize.y() }, convertedColor }; + + // make duplicates of first and last vertex so this can be rendered as a triangle strip + vertices[0] = vertices[1]; + vertices[5] = vertices[4]; + */ + } + + Renderer::bindTexture(*it->textureIdPtr); + Renderer::drawTriangleStrips(&vxs[0], vxs.size()); + Renderer::bindTexture(0); + } +} + + Vector2f Font::sizeText(std::string text, float lineSpacing) { float lineWidth = 0.0f; diff --git a/es-core/src/resources/Font.h b/es-core/src/resources/Font.h index 59253afb2e..0214094e54 100644 --- a/es-core/src/resources/Font.h +++ b/es-core/src/resources/Font.h @@ -44,7 +44,9 @@ class Font : public IReloadable Vector2f sizeText(std::string text, float lineSpacing = 1.5f); // Returns the expected size of a string when rendered. Extra spacing is applied to the Y axis. TextCache* buildTextCache(const std::string& text, float offsetX, float offsetY, unsigned int color); TextCache* buildTextCache(const std::string& text, Vector2f offset, unsigned int color, float xLen, Alignment alignment = ALIGN_LEFT, float lineSpacing = 1.5f); + void renderTextCache(TextCache* cache); + void renderGradientTextCache(TextCache* cache, unsigned int colorTop, unsigned int colorBottom, bool horz = false); std::string wrapText(std::string text, float xLen); // Inserts newlines into text to make it wrap properly. Vector2f sizeWrappedText(std::string text, float xLen, float lineSpacing = 1.5f); // Returns the expected size of a string after wrapping is applied. From cea70d157b4fbbddef0470d557ca837e2a090617 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 6 Oct 2019 18:41:01 +0200 Subject: [PATCH 152/336] Text reflexion : better calculations ( for multiline ) --- es-app/src/views/SystemView.cpp | 13 ++++++++++++- es-core/src/resources/Font.cpp | 34 ++++++++++++++++++--------------- 2 files changed, 31 insertions(+), 16 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 2f3a3ea61f..9ee31a440e 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -88,7 +88,18 @@ void SystemView::populate() // Remove dynamic flags for png & jpg files : themes can contain oversized images that can't be unloaded by the TextureResource manager ImageComponent* logo = new ImageComponent(mWindow, false, Utils::String::toLower(Utils::FileSystem::getExtension(path)) != ".svg"); logo->setMaxSize(carouselLogoSize() * mCarousel.logoScale); - logo->applyTheme(theme, "system", "logo", ThemeFlags::PATH | ThemeFlags::SIZE | ThemeFlags::COLOR | ThemeFlags::ALIGNMENT | ThemeFlags::VISIBLE); + + logo->applyTheme(theme, "system", "logo", ThemeFlags::COLOR | ThemeFlags::ALIGNMENT | ThemeFlags::VISIBLE); // ThemeFlags::PATH | + + // Process here to be enable to set max picture size + auto elem = theme->getElement("system", "logo", "image"); + if (elem && elem->has("path")) + { + auto path = elem->get("path"); + if (Utils::FileSystem::exists(path)) + logo->setImage(path, (elem->has("tile") && elem->get("tile")), MaxSizeInfo(mCarousel.logoSize * mCarousel.logoScale)); + } + logo->setRotateByTargetSize(true); e.data.logo = std::shared_ptr(logo); e.data.logoIsImage = true; diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index ef0bfddbf8..3a28d6101f 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -460,34 +460,38 @@ void Font::renderGradientTextCache(TextCache* cache, unsigned int colorTop, unsi std::vector vxs; vxs.resize(it->verts.size()); + float maxY = -1; + + for (int i = 0; i < it->verts.size(); i += 6) + if (maxY == -1 || maxY < it->verts[i + 2].pos.y()) + maxY = it->verts[i + 2].pos.y(); + for (int i = 0; i < it->verts.size(); i += 6) { + float topOffset = it->verts[i + 1].pos.y(); + float bottomOffset = it->verts[i + 2].pos.y(); + + float topPercent = (maxY == 0 ? 1.0 : topOffset / maxY); + float bottomPercent = (maxY == 0 ? 1.0 : bottomOffset / maxY); + + const unsigned int colorT = Renderer::mixColors(colorTop, colorBottom, topPercent); + const unsigned int colorB = Renderer::mixColors(colorTop, colorBottom, bottomPercent); + vxs[i + 1] = it->verts[i + 1]; - vxs[i + 1].col = colorTop; + vxs[i + 1].col = colorT; vxs[i + 2] = it->verts[i + 2]; - vxs[i + 2].col = colorBottom; + vxs[i + 2].col = colorB; vxs[i + 3] = it->verts[i + 3]; - vxs[i + 3].col = colorTop; + vxs[i + 3].col = colorT; vxs[i + 4] = it->verts[i + 4]; - vxs[i + 4].col = colorBottom; + vxs[i + 4].col = colorB; // make duplicates of first and last vertex so this can be rendered as a triangle strip vxs[i + 0] = vxs[i + 1]; vxs[i + 5] = vxs[i + 4]; - - /* - vertices[1] = { { glyphStartX , y - glyph->bearing.y() }, { glyph->texPos.x(), glyph->texPos.y() }, convertedColor }; - vertices[2] = { { glyphStartX , y - glyph->bearing.y() + (glyph->texSize.y() * textureSize.y()) }, { glyph->texPos.x(), glyph->texPos.y() + glyph->texSize.y() }, convertedColor }; - vertices[3] = { { glyphStartX + glyph->texSize.x() * textureSize.x(), y - glyph->bearing.y() }, { glyph->texPos.x() + glyph->texSize.x(), glyph->texPos.y() }, convertedColor }; - vertices[4] = { { glyphStartX + glyph->texSize.x() * textureSize.x(), y - glyph->bearing.y() + (glyph->texSize.y() * textureSize.y()) }, { glyph->texPos.x() + glyph->texSize.x(), glyph->texPos.y() + glyph->texSize.y() }, convertedColor }; - - // make duplicates of first and last vertex so this can be rendered as a triangle strip - vertices[0] = vertices[1]; - vertices[5] = vertices[4]; - */ } Renderer::bindTexture(*it->textureIdPtr); From a9a295a1f3f0ba7e50b0ae18a481acc504c5f8cf Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 6 Oct 2019 18:44:24 +0200 Subject: [PATCH 153/336] GridView : Support for marquee using + reviewed positionning when element exists. --- .../src/views/gamelist/GridGameListView.cpp | 10 +- es-core/src/components/GridTileComponent.cpp | 124 ++++++++++++++++-- es-core/src/components/GridTileComponent.h | 14 +- es-core/src/components/ImageGridComponent.h | 18 ++- 4 files changed, 143 insertions(+), 23 deletions(-) diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index ae333cd591..ed57f66ce8 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -221,9 +221,9 @@ void GridGameListView::populateList(const std::vector& files) continue; if (showFavoriteIcon) - mGrid.add(_U("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + mGrid.add(_U("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), *it); else - mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), *it); } } @@ -236,12 +236,12 @@ void GridGameListView::populateList(const std::vector& files) if (showFavoriteIcon) { - mGrid.add(_U("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + mGrid.add(_U("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), *it); continue; } } - mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), *it); + mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), *it); } } else @@ -489,7 +489,7 @@ void GridGameListView::addPlaceholder() { // empty grid - add a placeholder FileData* placeholder = new FileData(PLACEHOLDER, "", this->mRoot->getSystem()); - mGrid.add(placeholder->getName(), "", "", placeholder); + mGrid.add(placeholder->getName(), "", "", "", placeholder); } void GridGameListView::launch(FileData* game) diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 29f1c78419..f249490cb3 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -24,6 +24,7 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mSelectedZoomPercent = 1.0f; mAnimPosition = Vector3f(0, 0); mVideo = nullptr; + mMarquee = nullptr; mLabelMerged = false; @@ -58,6 +59,7 @@ void GridTileComponent::resetProperties() mSelectedProperties.mBackgroundCenterColor = 0xFFFFFFFF; mSelectedProperties.mBackgroundEdgeColor = 0xFFFFFFFF; + mDefaultProperties.mLabelPos = Vector2f(-1, -1); mDefaultProperties.mLabelSize = Vector2f(1.0, 0.30); mDefaultProperties.mLabelColor = 0xFFFFFFFF; mDefaultProperties.mLabelBackColor = 0; @@ -67,6 +69,7 @@ void GridTileComponent::resetProperties() mDefaultProperties.mFontPath = ""; mDefaultProperties.mFontSize = 0; + mSelectedProperties.mLabelPos = Vector2f(-1, -1); mSelectedProperties.mLabelSize = Vector2f(1.0, 0.30); mSelectedProperties.mLabelColor = 0xFFFFFFFF; mSelectedProperties.mLabelBackColor = 0; @@ -113,6 +116,7 @@ void GridTileComponent::resize() float height = (int)(size.y() * currentProperties.mLabelSize.y()); float labelHeight = height; + if (mLabelVisible) { mLabel.setColor(currentProperties.mLabelColor); @@ -123,15 +127,18 @@ void GridTileComponent::resize() if (mDefaultProperties.mFontPath != mSelectedProperties.mFontPath || mDefaultProperties.mFontSize != mSelectedProperties.mFontSize) mLabel.setFont(currentProperties.mFontPath, currentProperties.mFontSize); - if (mLabelMerged) - { - mLabel.setPosition(currentProperties.mPadding.x(), mSize.y() - height - currentProperties.mPadding.y()); - mLabel.setSize(size.x() - 2 * currentProperties.mPadding.x(), height); - } - else + if (currentProperties.mLabelPos.x() < 0) { - mLabel.setPosition(0, mSize.y() - height); - mLabel.setSize(size.x(), height); + if (mLabelMerged) + { + mLabel.setPosition(currentProperties.mPadding.x(), mSize.y() - height - currentProperties.mPadding.y()); + mLabel.setSize(size.x() - 2 * currentProperties.mPadding.x(), height); + } + else + { + mLabel.setPosition(0, mSize.y() - height); + mLabel.setSize(size.x(), height); + } } } @@ -145,6 +152,17 @@ void GridTileComponent::resize() float imageWidth = size.x() - paddingX * 2.0; float imageHeight = size.y() - topPadding - bottomPadding; + if (mMarquee != nullptr) + { + mMarquee->setPosition( + currentProperties.mMarqueePos.x() * size.x(), + currentProperties.mMarqueePos.y() * size.y()); + + mMarquee->setMaxSize( + currentProperties.mMarqueeSize.x() * size.x(), + currentProperties.mMarqueeSize.y() * size.y()); + } + if (mImage != nullptr) { mImage->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); @@ -158,7 +176,7 @@ void GridTileComponent::resize() else mImage->setMaxSize(imageWidth, imageHeight); - if (mLabelVisible) + if (mLabelVisible && currentProperties.mLabelPos.x() < 0) { if (mLabelMerged) { @@ -173,6 +191,17 @@ void GridTileComponent::resize() } } + if (currentProperties.mLabelPos.x() >= 0) + { + mLabel.setPosition( + currentProperties.mLabelPos.x() * size.x(), + currentProperties.mLabelPos.y() * size.y()); + + mLabel.setSize( + currentProperties.mLabelSize.x() * size.x(), + currentProperties.mLabelSize.y() * size.y()); + } + if (mVideo != nullptr && mVideo->isPlaying()) { mVideo->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); @@ -293,7 +322,9 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) if (!mLabelMerged && currentProperties.mImageSizeMode == "minSize") Renderer::popClipRect(); - if (mLabelVisible && currentProperties.mLabelSize.y() > 0) + if (mMarquee != nullptr && mMarquee->hasImage()) + mMarquee->render(trans); + else if (mLabelVisible && currentProperties.mLabelSize.y()>0) mLabel.render(trans); if (mLabelMerged && currentProperties.mImageSizeMode == "minSize") @@ -309,6 +340,17 @@ void GridTileComponent::render(const Transform4x4f& parentTrans) renderContent(parentTrans); } +void GridTileComponent::createMarquee() +{ + if (mMarquee != nullptr) + return; + + mMarquee = new ImageComponent(mWindow); + mMarquee->setOrigin(0.5f, 0.5f); + mMarquee->setDefaultZIndex(35); + addChild(mMarquee); +} + void GridTileComponent::createVideo() { if (mVideo != nullptr) @@ -391,9 +433,32 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mSelectedProperties.mMirror = mDefaultProperties.mMirror; } } + + elem = theme->getElement(view, "gridtile.marquee", "image"); + if (elem) + { + createMarquee(); + mMarquee->applyTheme(theme, view, "gridtile.marquee", ThemeFlags::ALL ^ (ThemeFlags::PATH)); - // mDefaultProperties.mMirror = Vector2f(0, 0); - // mSelectedProperties.mMirror = Vector2f(0, 0); + if (elem->has("pos")) + mDefaultProperties.mMarqueePos = elem->get("pos"); + else + mDefaultProperties.mMarqueeSize = Vector2f(0, 0); + + if (elem->has("pos")) + mDefaultProperties.mMarqueeSize = elem->get("size"); + else + mDefaultProperties.mMarqueeSize = Vector2f(1, 1); + + mSelectedProperties.mMarqueePos = mDefaultProperties.mMarqueePos; + mSelectedProperties.mMarqueeSize = mDefaultProperties.mMarqueeSize; + } + else if (mMarquee != nullptr) + { + removeChild(mMarquee); + delete mMarquee; + mMarquee = nullptr; + } // Apply theme to the selected gridtile // NOTE that some of the default gridtile properties influence on the selected gridtile properties @@ -440,11 +505,22 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons { float sh = (float)Renderer::getScreenHeight(); + if (elem && elem->has("pos")) + { + mDefaultProperties.mLabelPos = elem->get("pos"); + mSelectedProperties.mLabelPos = mDefaultProperties.mLabelPos; + mLabelMerged = true; + } + else + mLabelMerged = false; + if (elem && elem->has("size")) { mDefaultProperties.mLabelSize = elem->get("size"); mSelectedProperties.mLabelSize = mDefaultProperties.mLabelSize; - mLabelMerged = mDefaultProperties.mLabelSize.x() == 0; + + if (!mLabelMerged) + mLabelMerged = mDefaultProperties.mLabelSize.x() == 0; } if (elem && elem->has("color")) @@ -489,6 +565,9 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons elem = theme->getElement(view, "gridtile_selected", "text"); if (elem != NULL) { + if (elem && elem->has("pos") && mDefaultProperties.mLabelPos.x() >= 0) + mSelectedProperties.mLabelPos = elem->get("pos"); + if (elem && elem->has("size")) mSelectedProperties.mLabelSize = elem->get("size"); @@ -549,10 +628,29 @@ void GridTileComponent::setImage(const std::string& path) resize(); } +void GridTileComponent::setMarquee(const std::string& path) +{ + if (mMarquee == nullptr) + return; + + if (mCurrentMarquee == path) + return; + + mCurrentMarquee = path; + + if (mSelectedProperties.mSize.x() > mSize.x()) + mMarquee->setImage(path, false, MaxSizeInfo(mSelectedProperties.mSize)); + else + mMarquee->setImage(path, false, MaxSizeInfo(mSize)); + + resize(); +} + void GridTileComponent::resetImages() { setLabel(""); setImage(""); + setMarquee(""); stopVideo(); } diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index 94dc635d0d..124854b7b8 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -21,7 +21,9 @@ struct GridTileProperties std::string mImageSizeMode; std::string mSelectionMode; + Vector2f mLabelPos; Vector2f mLabelSize; + unsigned int mLabelColor; unsigned int mLabelBackColor; @@ -32,6 +34,10 @@ struct GridTileProperties unsigned int mFontSize; Vector2f mMirror; + + + Vector2f mMarqueePos; + Vector2f mMarqueeSize; }; class GridTileComponent : public GuiComponent @@ -56,7 +62,8 @@ class GridTileComponent : public GuiComponent void setVideo(const std::string& path, float defaultDelay = -1.0); void setImage(const std::string& path); - // void setImage(const std::shared_ptr& texture, std::string name); + void setMarquee(const std::string& path); + void setSelected(bool selected, bool allowAnimation = true, Vector3f* pPosition = NULL, bool force = false); void setVisible(bool visible); @@ -78,7 +85,7 @@ class GridTileComponent : public GuiComponent private: void resetProperties(); void createVideo(); - + void createMarquee(); void startVideo(); void stopVideo(); @@ -97,6 +104,7 @@ class GridTileComponent : public GuiComponent GridTileProperties mDefaultProperties; GridTileProperties mSelectedProperties; + std::string mCurrentMarquee; std::string mCurrentPath; std::string mVideoPath; @@ -109,6 +117,8 @@ class GridTileComponent : public GuiComponent Vector3f mAnimPosition; VideoComponent* mVideo; + ImageComponent* mMarquee; + bool mVideoPlaying; bool mShown; }; diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 46e20c9166..5ebc5b5f58 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -27,6 +27,7 @@ enum ImageSource struct ImageGridData { std::string texturePath; + std::string marqueePath; std::string videoPath; }; @@ -52,7 +53,7 @@ class ImageGridComponent : public IList ImageGridComponent(Window* window); - void add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const T& obj); + void add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, const T& obj); bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; @@ -160,13 +161,14 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList -void ImageGridComponent::add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const T& obj) +void ImageGridComponent::add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, const T& obj) { typename IList::Entry entry; entry.name = name; entry.object = obj; entry.data.texturePath = imagePath; entry.data.videoPath = videoPath; + entry.data.marqueePath = marqueePath; static_cast*>(this)->add(entry); mEntriesDirty = true; @@ -691,6 +693,7 @@ void ImageGridComponent::updateTiles(bool allowAnimation, bool updateSelected tile->setSelected(false, allowAnimation); tile->setLabel(""); tile->setImage(mDefaultGameTexture); + tile->setMarquee(""); tile->setVisible(false); } return; @@ -777,11 +780,20 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA else tile->setImage(mDefaultGameTexture); + // Marquee + std::string marqueePath = mEntries.at(imgPos).data.marqueePath; + + if (!marqueePath.empty() && ResourceManager::getInstance()->fileExists(marqueePath)) + tile->setMarquee(marqueePath); + else + tile->setMarquee(""); + + // Video if (mAllowVideo && imgPos == mCursor) { std::string videoPath = mEntries.at(imgPos).data.videoPath; - if (ResourceManager::getInstance()->fileExists(videoPath)) + if (!videoPath.empty() && ResourceManager::getInstance()->fileExists(videoPath)) tile->setVideo(videoPath, mVideoDelay); else if (mEntries.at(imgPos).object->getType() == 2) tile->setVideo(""); From 48f1d9e0b0947231b384aeed38045feb559fdeb3 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 6 Oct 2019 18:44:54 +0200 Subject: [PATCH 154/336] Scraper : Thumbnail preview don't work anymore ( related to httpreq ) --- es-core/src/HttpReq.cpp | 15 ++++++++------- es-core/src/HttpReq.h | 1 - 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index 50f60ac188..ccf0947780 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -317,15 +317,16 @@ std::string HttpReq::getContent() mStream.flush(); mStream.close(); } + + std::ifstream ifs(mStreamPath, std::ios_base::in | std::ios_base::binary); + if (ifs.bad()) + return ""; - std::ifstream t(mStreamPath); - t.seekg(0, std::ios::end); - size_t size = t.tellg(); - std::string buffer(size, ' '); - t.seekg(0); - t.read(&buffer[0], size); + std::stringstream ofs; + ofs << ifs.rdbuf(); + ifs.close(); - return buffer; // mContent.str(); + return ofs.str(); } void HttpReq::onError(const char* msg) diff --git a/es-core/src/HttpReq.h b/es-core/src/HttpReq.h index 3681886d76..1f08ce2a5b 100644 --- a/es-core/src/HttpReq.h +++ b/es-core/src/HttpReq.h @@ -74,7 +74,6 @@ class HttpReq std::string mStreamPath; std::ofstream mStream; - //std::stringstream mContent; std::string mErrorMsg; int mPercent; From c39a247f733d0b433dc51b91e4743249d65a3ad9 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 6 Oct 2019 18:45:16 +0200 Subject: [PATCH 155/336] Scraper : take wheel as preference when marquee is ON --- es-app/src/scrapers/ScreenScraper.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index 67b5f3abbe..5f3bd75398 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -253,6 +253,8 @@ std::vector ScreenScraperRequest::getRipList(std::string imageSourc ripList = { "box-3D", "box-2D" }; else if (imageSource == "wheel") ripList = { "wheel", "screenmarqueesmall", "screenmarquee" }; + else if (imageSource == "marquee") + ripList = { "wheel", "wheelsteel", "screenmarqueesmall", "screenmarquee" }; return ripList; } @@ -380,11 +382,15 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve if (Settings::getInstance()->getBool("ScrapeMarquee")) { - pugi::xml_node art = findMedia(media_list, "marquee", region); - if (art) - result.marqueeUrl = Utils::String::replace(art.text().get(), " ", "%20"); - else - LOG(LogDebug) << "Failed to find media XML node for video"; + ripList = getRipList("marquee"); + if (!ripList.empty()) + { + pugi::xml_node art = findMedia(media_list, ripList, region); + if (art) + result.marqueeUrl = Utils::String::replace(art.text().get(), " ", "%20"); + else + LOG(LogDebug) << "Failed to find media XML node for video"; + } } if (Settings::getInstance()->getBool("ScrapeVideos")) From 49fccae1955372ca0d35c34590112e3009083ede Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 6 Oct 2019 18:45:43 +0200 Subject: [PATCH 156/336] Screenscraper : check invalid responses when downloading medias --- es-app/src/scrapers/Scraper.cpp | 19 +++++++++---------- es-core/src/HttpReq.cpp | 18 +++++++++++++----- es-core/src/HttpReq.h | 2 +- 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index fe64fe1c2d..8d8f58bb80 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -332,23 +332,22 @@ void ImageDownloadHandle::update() if (mStatus == ASYNC_IN_PROGRESS) { - if (!mReq->saveContent(mSavePath)) + int ret = mReq->saveContent(mSavePath, true); + if (ret == 2) { - setError("Failed to save image. Disk full?"); + setError("Failed to save media : The server response is invalid"); + return; + } + else if (ret == 1) + { + setError("Failed to save image on disk. Disk full?"); return; } // It's an image ? std::string ext = Utils::String::toLower(Utils::FileSystem::getExtension(mSavePath)); if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".bmp" || ext == ".gif") - { - // resize it - if (!resizeImage(mSavePath, mMaxWidth, mMaxHeight)) - { - setError("Error saving resized image. Out of memory? Disk full?"); - return; - } - } + resizeImage(mSavePath, mMaxWidth, mMaxHeight); } setStatus(ASYNC_DONE); diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index ccf0947780..432d67b00f 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -1,6 +1,7 @@ #include "HttpReq.h" #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" #include "Log.h" #include @@ -364,7 +365,7 @@ size_t HttpReq::write_content(void* buff, size_t size, size_t nmemb, void* req_p return nmemb; } -bool HttpReq::saveContent(const std::string filename) +int HttpReq::saveContent(const std::string filename, bool checkMedia) { assert(mStatus == REQ_SUCCESS); @@ -377,23 +378,30 @@ bool HttpReq::saveContent(const std::string filename) if (!Utils::FileSystem::exists(mStreamPath)) return false; + if (checkMedia && Utils::FileSystem::getFileSize(mStreamPath) < 300) + { + auto data = Utils::String::toUpper(getContent()); + if (data.find("NOMEDIA") != std::string::npos || data.find("ERREUR") != std::string::npos || data.find("ERROR") != std::string::npos || data.find("PROBL") != std::string::npos) + return 2; + } + std::ifstream ifs(mStreamPath, std::ios_base::in | std::ios_base::binary); if (ifs.bad()) - return false; + return 1; if (Utils::FileSystem::exists(filename)) Utils::FileSystem::removeFile(filename); std::ofstream ofs(filename, std::ios_base::out | std::ios_base::binary); if (ofs.bad()) - return false; + return 1; ofs << ifs.rdbuf(); ifs.close(); ofs.close(); if (ofs.bad()) - return false; + return 1; - return true; + return 0; } \ No newline at end of file diff --git a/es-core/src/HttpReq.h b/es-core/src/HttpReq.h index 1f08ce2a5b..35843fd654 100644 --- a/es-core/src/HttpReq.h +++ b/es-core/src/HttpReq.h @@ -48,7 +48,7 @@ class HttpReq std::string getContent(); // mStatus must be REQ_SUCCESS - bool saveContent(const std::string filename); + int saveContent(const std::string filename, bool checkMedia = false); static std::string urlEncode(const std::string &s); static bool isUrl(const std::string& s); From 41844087d5928b7b1a68d6dbbc11efc5f5178772 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 6 Oct 2019 18:49:07 +0200 Subject: [PATCH 157/336] GridView : marquees are flickering / reloading when scrolling up. --- es-core/src/components/GridTileComponent.cpp | 13 +++++++---- es-core/src/components/GridTileComponent.h | 2 +- es-core/src/components/ImageGridComponent.h | 24 +++++++++++++++++--- 3 files changed, 30 insertions(+), 9 deletions(-) diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index f249490cb3..e332617951 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -97,13 +97,16 @@ GridTileComponent::~GridTileComponent() mVideo = nullptr; } -std::shared_ptr GridTileComponent::getTexture() -{ - if (mImage != nullptr) + +std::shared_ptr GridTileComponent::getTexture(bool marquee) +{ + if (marquee && mMarquee != nullptr) + return mMarquee->getTexture(); + else if (!marquee && mImage != nullptr) return mImage->getTexture(); - return nullptr; -}; + return nullptr; +} void GridTileComponent::resize() { diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index 124854b7b8..f4aecda525 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -80,7 +80,7 @@ class GridTileComponent : public GuiComponent virtual void onHide(); virtual void update(int deltaTime); - std::shared_ptr getTexture(); + std::shared_ptr getTexture(bool marquee = false); private: void resetProperties(); diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 5ebc5b5f58..823bd3d85e 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -699,12 +699,12 @@ void ImageGridComponent::updateTiles(bool allowAnimation, bool updateSelected return; } - // Temporary store previous texture so they can't be unloaded + // Temporary store previous texture so they can't be unloaded - avoids flickering & reloading std::vector> previousTextures; for (int ti = 0; ti < (int)mTiles.size(); ti++) { - std::shared_ptr tile = mTiles.at(ti); - previousTextures.push_back(tile->getTexture()); + previousTextures.push_back(mTiles.at(ti)->getTexture(true)); + previousTextures.push_back(mTiles.at(ti)->getTexture(false)); } int i = 0; @@ -718,6 +718,24 @@ void ImageGridComponent::updateTiles(bool allowAnimation, bool updateSelected updateTileAtPos(i, img, allowAnimation, updateSelectedState); i++; img++; } + + // Collect new textures + std::vector> newTextures; + for (int ti = 0; ti < (int)mTiles.size(); ti++) + { + newTextures.push_back(mTiles.at(ti)->getTexture(true)); + newTextures.push_back(mTiles.at(ti)->getTexture(false)); + } + + // Compare old texture with new textures -> Remove missing from async queue if existing + for (auto tex : previousTextures) + { + if (tex == nullptr) + continue; + + if (std::find(newTextures.cbegin(), newTextures.cend(), tex) == newTextures.cend()) + TextureResource::cancelAsync(tex); + } if (updateSelectedState) mLastCursor = mCursor; From 8b6c079da65e0c91360ef6f4846b1996b0015705 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 6 Oct 2019 18:57:27 +0200 Subject: [PATCH 158/336] ScreenScraper : Use jeuRecherche instead of jeuInfos when using manual Metadata/Scrape function -> jeuRecherche can return multiple results. --- es-app/src/EmulationStation.h | 8 +++---- es-app/src/guis/GuiMetaDataEd.cpp | 7 ++++++ es-app/src/scrapers/ScreenScraper.cpp | 29 ++++++++++++++++-------- es-app/src/scrapers/ScreenScraper.h | 2 +- es-core/src/components/TextComponent.cpp | 2 +- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index 4581cf9238..c131ea3f60 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -6,15 +6,15 @@ // Do this version number update as the very last commit for the new release version. #define PROGRAM_VERSION_MAJOR 2 #define PROGRAM_VERSION_MINOR 10 -#define PROGRAM_VERSION_MAINTENANCE 1 +#define PROGRAM_VERSION_MAINTENANCE 2 #define PROGRAM_VERSION_BUILD 1 -#define PROGRAM_VERSION_STRING "2.10.1.1 FCAMOD" -#define SCRAPER_VERSION_STRING "2.10.1.1" +#define PROGRAM_VERSION_STRING "2.10.2.1 FCAMOD" +#define SCRAPER_VERSION_STRING "2.10.2.1" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ -#define RESOURCE_VERSION_STRING "2,10,1,1\0" +#define RESOURCE_VERSION_STRING "2,10,2,1\0" #define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE,PROGRAM_VERSION_BUILD #endif // ES_APP_EMULATION_STATION_H diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index e847912595..b497207cd8 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -325,6 +325,8 @@ void GuiMetaDataEd::save() void GuiMetaDataEd::fetch() { + mScraperParams.nameOverride = mScraperParams.game->getName(); + GuiGameScraper* scr = new GuiGameScraper(mWindow, mScraperParams, std::bind(&GuiMetaDataEd::fetchDone, this, std::placeholders::_1)); mWindow->pushGui(scr); } @@ -335,6 +337,11 @@ void GuiMetaDataEd::fetchDone(const ScraperSearchResult& result) { auto val = mEditors.at(i)->getValue(); auto key = mEditors.at(i)->getTag(); + + // Don't override favorite & hidden values, as they are not statistics + if (key == "favorite" || key == "hidden") + continue; + mEditors.at(i)->setValue(result.mdl.get(key)); } } diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index 5f3bd75398..803a85d6ca 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -134,8 +134,8 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, } else { - path = ssConfig.getGameSearchUrl(params.nameOverride); - path += "&romtype=jeu"; + path = ssConfig.getGameSearchUrl(params.nameOverride, true); + //path += "&romtype=jeu"; } auto& platforms = params.system->getPlatformIds(); @@ -251,10 +251,8 @@ std::vector ScreenScraperRequest::getRipList(std::string imageSourc ripList = { "box-2D", "box-3D" }; else if (imageSource == "box-3D") ripList = { "box-3D", "box-2D" }; - else if (imageSource == "wheel") - ripList = { "wheel", "screenmarqueesmall", "screenmarquee" }; - else if (imageSource == "marquee") - ripList = { "wheel", "wheelsteel", "screenmarqueesmall", "screenmarquee" }; + else if (imageSource == "wheel" || imageSource == "marquee") + ripList = { "wheel", "wheel-hd", "wheel-steel", "wheel-carbon", "screenmarqueesmall", "screenmarquee" }; return ripList; } @@ -262,9 +260,10 @@ std::vector ScreenScraperRequest::getRipList(std::string imageSourc void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::vector& out_results) { pugi::xml_node data = xmldoc.child("Data"); - pugi::xml_node game = data.child("jeu"); + if (data.child("jeux")) + data = data.child("jeux"); - if (game) + for (pugi::xml_node game = data.child("jeu"); game; game = game.next_sibling("jeu")) { ScraperSearchResult result; ScreenScraperRequest::ScreenScraperConfig ssConfig; @@ -440,8 +439,10 @@ void ScreenScraperRequest::processList(const pugi::xml_document& xmldoc, std::ve } } -std::string ScreenScraperRequest::ScreenScraperConfig::getGameSearchUrl(const std::string gameName) const +std::string ScreenScraperRequest::ScreenScraperConfig::getGameSearchUrl(const std::string gameName, bool jeuRecherche) const { + + std::string ret = API_URL_BASE + "/jeuInfos.php?devid=" + Utils::String::scramble(API_DEV_U, API_DEV_KEY) + "&devpassword=" + Utils::String::scramble(API_DEV_P, API_DEV_KEY) @@ -449,6 +450,16 @@ std::string ScreenScraperRequest::ScreenScraperConfig::getGameSearchUrl(const st + "&output=xml" + "&romnom=" + HttpReq::urlEncode(gameName); + if (jeuRecherche) + { + ret = API_URL_BASE + + "/jeuRecherche.php?devid=" + Utils::String::scramble(API_DEV_U, API_DEV_KEY) + + "&devpassword=" + Utils::String::scramble(API_DEV_P, API_DEV_KEY) + + "&softname=" + HttpReq::urlEncode(API_SOFT_NAME) + + "&output=xml" + + "&recherche=" + HttpReq::urlEncode(gameName); + } + std::string user = Settings::getInstance()->getString("ScreenScraperUser"); std::string pass = Settings::getInstance()->getString("ScreenScraperPass"); diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index b0020b0e5e..ea038d4649 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -21,7 +21,7 @@ class ScreenScraperRequest : public ScraperHttpRequest // Settings for the scraper static const struct ScreenScraperConfig { - std::string getGameSearchUrl(const std::string gameName) const; + std::string getGameSearchUrl(const std::string gameName, bool jeuRecherche=false) const; // Access to the API const std::string API_DEV_U = { 91, 32, 7, 17 }; diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index 027c7e8028..ab3f120bcc 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -7,7 +7,7 @@ TextComponent::TextComponent(Window* window) : GuiComponent(window), mFont(Font::get(FONT_SIZE_MEDIUM)), mUppercase(false), mColor(0x000000FF), mAutoCalcExtent(true, true), mHorizontalAlignment(ALIGN_LEFT), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), - mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)), mGlowOffset(Vector2f(0, 0)) + mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)), mGlowOffset(Vector2f(0, 0)), mReflection(0.0f, 0.0f), mReflectOnBorders(false) { } From af21597faea5c23768b19faba089994dff3dbef6 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 9 Oct 2019 10:19:16 +0200 Subject: [PATCH 159/336] ScreenSaver : Display game text / system + fading transition + Restored old menu system ( more readable ) + allow "start" to run games. Fix ( onShow not called ) when returning back from games & kodi & game runned from screensaver . GridView : Add sound support imagegrid.scrollSound # Conflicts: # es-app/src/ApiSystem.cpp # es-app/src/FileData.cpp # es-app/src/SystemScreenSaver.cpp # es-app/src/guis/GuiGeneralScreensaverOptions.cpp # es-app/src/guis/GuiMenu.cpp # es-app/src/views/SystemView.cpp # es-core/src/Settings.cpp # es-core/src/Window.cpp --- es-app/src/SystemScreenSaver.cpp | 295 ++++++-- es-app/src/SystemScreenSaver.h | 37 +- .../src/guis/GuiGeneralScreensaverOptions.cpp | 37 +- .../guis/GuiSlideshowScreensaverOptions.cpp | 10 +- es-core/src/Settings.cpp | 714 +++++++++--------- es-core/src/ThemeData.cpp | 1 + es-core/src/Window.cpp | 16 +- es-core/src/Window.h | 1 + es-core/src/components/ImageGridComponent.h | 8 +- 9 files changed, 671 insertions(+), 448 deletions(-) diff --git a/es-app/src/SystemScreenSaver.cpp b/es-app/src/SystemScreenSaver.cpp index 503c714558..94eec14b85 100644 --- a/es-app/src/SystemScreenSaver.cpp +++ b/es-app/src/SystemScreenSaver.cpp @@ -13,9 +13,12 @@ #include "PowerSaver.h" #include "Sound.h" #include "SystemData.h" +#include "components/ImageComponent.h" +#include "components/TextComponent.h" #include #include -#define FADE_TIME 300 + +#define FADE_TIME 600 SystemScreenSaver::SystemScreenSaver(Window* window) : mVideoScreensaver(NULL), @@ -31,8 +34,9 @@ SystemScreenSaver::SystemScreenSaver(Window* window) : mSystemName(""), mGameName(""), mCurrentGame(NULL), - mStopBackgroundAudio(true) + mLoadingNext(false) { + mWindow->setScreenSaver(this); std::string path = getTitleFolder(); if(!Utils::FileSystem::exists(path)) @@ -46,14 +50,18 @@ SystemScreenSaver::~SystemScreenSaver() // Delete subtitle file, if existing remove(getTitlePath().c_str()); mCurrentGame = NULL; - delete mVideoScreensaver; - delete mImageScreensaver; + + if (mVideoScreensaver != nullptr) + { + delete mVideoScreensaver; + mVideoScreensaver = nullptr; + } } + bool SystemScreenSaver::allowSleep() { - //return false; - return ((mVideoScreensaver == NULL) && (mImageScreensaver == NULL)); + return (mVideoScreensaver == nullptr && mImageScreensaver == nullptr); } bool SystemScreenSaver::isScreenSaverActive() @@ -63,6 +71,8 @@ bool SystemScreenSaver::isScreenSaverActive() void SystemScreenSaver::startScreenSaver() { + stopScreenSaver(); + std::string screensaver_behavior = Settings::getInstance()->getString("ScreenSaverBehavior"); if (!mVideoScreensaver && (screensaver_behavior == "random video")) { @@ -119,11 +129,19 @@ void SystemScreenSaver::startScreenSaver() else if (screensaver_behavior == "slideshow") { // Configure to fade out the windows, Skip Fading if Instant mode - mState = PowerSaver::getMode() == PowerSaver::INSTANT - ? STATE_SCREENSAVER_ACTIVE - : STATE_FADE_OUT_WINDOW; + mState = PowerSaver::getMode() == PowerSaver::INSTANT + ? STATE_SCREENSAVER_ACTIVE + : STATE_FADE_OUT_WINDOW; + mVideoChangeTime = Settings::getInstance()->getInt("ScreenSaverSwapImageTimeout"); - mOpacity = 0.0f; + + if (mState == STATE_FADE_OUT_WINDOW) + { + mState = STATE_FADE_IN_VIDEO; + mOpacity = 1.0f; + } + else + mOpacity = 0.0f; // Load a random image std::string path = ""; @@ -134,30 +152,15 @@ void SystemScreenSaver::startScreenSaver() mCurrentGame = NULL; } else - { pickRandomGameListImage(path); - } - if (!mImageScreensaver) - { - mImageScreensaver = new ImageComponent(mWindow, false, false); - } + mImageScreensaver = std::make_shared(mWindow); mTimer = 0; mImageScreensaver->setImage(path); - mImageScreensaver->setOrigin(0.5f, 0.5f); - mImageScreensaver->setPosition(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f); - - if (Settings::getInstance()->getBool("SlideshowScreenSaverStretch")) - { - mImageScreensaver->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - } - else - { - mImageScreensaver->setMaxSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - } - + mImageScreensaver->setGame(mCurrentGame); + /* std::string bg_audio_file = Settings::getInstance()->getString("SlideshowScreenSaverBackgroundAudioFile"); if ((!mBackgroundAudio) && (bg_audio_file != "")) { @@ -169,7 +172,7 @@ void SystemScreenSaver::startScreenSaver() mBackgroundAudio->play(); } } - + */ PowerSaver::runningScreenSaver(true); mTimer = 0; return; @@ -181,22 +184,31 @@ void SystemScreenSaver::startScreenSaver() void SystemScreenSaver::stopScreenSaver() { - if ((mBackgroundAudio) && (mStopBackgroundAudio)) + if (mLoadingNext) + mFadingImageScreensaver = mImageScreensaver; + else + mFadingImageScreensaver = nullptr; + /* + if (mBackgroundAudio && !mLoadingNext) { mBackgroundAudio->stop(); mBackgroundAudio.reset(); + // if we were playing audio, we paused PS PowerSaver::resume(); } - + */ // so that we stop the background audio next time, unless we're restarting the screensaver - mStopBackgroundAudio = true; + mLoadingNext = false; - delete mVideoScreensaver; - mVideoScreensaver = NULL; - delete mImageScreensaver; - mImageScreensaver = NULL; + if (mVideoScreensaver != nullptr) + { + delete mVideoScreensaver; + mVideoScreensaver = nullptr; + } + mImageScreensaver = nullptr; + // we need this to loop through different videos mState = STATE_INACTIVE; PowerSaver::runningScreenSaver(false); @@ -204,6 +216,8 @@ void SystemScreenSaver::stopScreenSaver() void SystemScreenSaver::renderScreenSaver() { + Transform4x4f transform = Transform4x4f::Identity(); + std::string screensaver_behavior = Settings::getInstance()->getString("ScreenSaverBehavior"); if (mVideoScreensaver && screensaver_behavior == "random video") { @@ -213,37 +227,37 @@ void SystemScreenSaver::renderScreenSaver() // Only render the video if the state requires it if ((int)mState >= STATE_FADE_IN_VIDEO) - { - Transform4x4f transform = Transform4x4f::Identity(); mVideoScreensaver->render(transform); - } } else if (mImageScreensaver && screensaver_behavior == "slideshow") { // Render black background - Renderer::setMatrix(Transform4x4f::Identity()); - Renderer::drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF, 0x000000FF); + Renderer::setMatrix(transform); + Renderer::drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x000000FF); + + if (mFadingImageScreensaver != nullptr) + mFadingImageScreensaver->render(transform); // Only render the video if the state requires it if ((int)mState >= STATE_FADE_IN_VIDEO) - { + { if (mImageScreensaver->hasImage()) { - mImageScreensaver->setOpacity(255- (unsigned char) (mOpacity * 255)); - - Transform4x4f transform = Transform4x4f::Identity(); + unsigned int opacity = 255 - (unsigned char)(mOpacity * 255); + + Renderer::setMatrix(transform); + Renderer::drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), 0x00000000 | opacity); + mImageScreensaver->setOpacity(opacity); mImageScreensaver->render(transform); } } // Check if we need to restart the background audio + /* if ((mBackgroundAudio) && (Settings::getInstance()->getString("SlideshowScreenSaverBackgroundAudioFile") != "")) - { if (!mBackgroundAudio->isPlaying()) - { mBackgroundAudio->play(); - } - } + */ } else if (mState != STATE_INACTIVE) { @@ -347,7 +361,6 @@ void SystemScreenSaver::pickRandomVideo(std::string& path) if (mVideoCount > 0) { int video = (int)(((float)rand() / float(RAND_MAX)) * (float)mVideoCount); - pickGameListNode(video, "video", path); } } @@ -359,7 +372,6 @@ void SystemScreenSaver::pickRandomGameListImage(std::string& path) if (mImageCount > 0) { int image = (int)(((float)rand() / float(RAND_MAX)) * (float)mImageCount); - pickGameListNode(image, "image", path); } } @@ -416,7 +428,7 @@ void SystemScreenSaver::update(int deltaTime) mOpacity = 1.0f; // Update to the next state - mState = STATE_FADE_IN_VIDEO; + mState = STATE_FADE_IN_VIDEO; } } else if (mState == STATE_FADE_IN_VIDEO) @@ -427,6 +439,7 @@ void SystemScreenSaver::update(int deltaTime) mOpacity = 0.0f; // Update to the next state mState = STATE_SCREENSAVER_ACTIVE; + mFadingImageScreensaver = nullptr; } } else if (mState == STATE_SCREENSAVER_ACTIVE) @@ -434,9 +447,7 @@ void SystemScreenSaver::update(int deltaTime) // Update the timer that swaps the videos mTimer += deltaTime; if (mTimer > mVideoChangeTime) - { nextVideo(); - } } // If we have a loaded video then update it @@ -446,11 +457,13 @@ void SystemScreenSaver::update(int deltaTime) mImageScreensaver->update(deltaTime); } -void SystemScreenSaver::nextVideo() { - mStopBackgroundAudio = false; - stopScreenSaver(); +void SystemScreenSaver::nextVideo() +{ + mLoadingNext = true; startScreenSaver(); - mState = STATE_SCREENSAVER_ACTIVE; + + if (mFadingImageScreensaver == nullptr) + mState = STATE_SCREENSAVER_ACTIVE; } FileData* SystemScreenSaver::getCurrentGame() @@ -463,13 +476,171 @@ void SystemScreenSaver::launchGame() if (mCurrentGame != NULL) { // launching Game - IGameListView* view = ViewController::get()->getGameListView(mCurrentGame->getSystem()).get(); - view->setCursor(mCurrentGame); + auto view = ViewController::get()->getGameListView(mCurrentGame->getSystem(), false); + if (view != nullptr) + view->setCursor(mCurrentGame); if (Settings::getInstance()->getBool("ScreenSaverControls")) - view->launch(mCurrentGame); - else + mCurrentGame->launchGame(mWindow); + else ViewController::get()->goToGameList(mCurrentGame->getSystem()); + } +} + + +// ------------------------------------------------------------------------------------------------------------------------ +// IMAGE SCREEN SAVER CLASS +// ------------------------------------------------------------------------------------------------------------------------ + +ImageScreenSaver::ImageScreenSaver(Window* window) : GuiComponent(window) +{ + mImage = nullptr; + mMarquee = nullptr; + mLabelGame = nullptr; + mLabelSystem = nullptr; +} + +ImageScreenSaver::~ImageScreenSaver() +{ + if (mMarquee != nullptr) + delete mMarquee; + + if (mImage != nullptr) + delete mImage; + + if (mLabelGame != nullptr) + { + delete mLabelGame; + mLabelGame = nullptr; + } + + if (mLabelSystem != nullptr) + { + delete mLabelSystem; + mLabelSystem = nullptr; + } +} + +void ImageScreenSaver::setImage(const std::string path) +{ + if (mImage == nullptr) + { + mImage = new ImageComponent(mWindow, true); + mImage->setOrigin(0.5f, 0.5f); + mImage->setPosition(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f); + + if (Settings::getInstance()->getBool("SlideshowScreenSaverStretch")) + mImage->setMinSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + else + mImage->setMaxSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + } + + mImage->setImage(path); +} + +bool ImageScreenSaver::hasImage() +{ + return mImage != nullptr && mImage->hasImage(); +} + +void ImageScreenSaver::render(const Transform4x4f& transform) +{ + if (mImage) + { + mImage->setOpacity(mOpacity); + mImage->render(transform); + } + + if (mMarquee) + { + mMarquee->setOpacity(mOpacity); + mMarquee->render(transform); + } + else if (mLabelGame) + { + mLabelGame->setOpacity(mOpacity); + mLabelGame->render(transform); + } + if (mLabelSystem) + { + mLabelSystem->setOpacity(mOpacity); + mLabelSystem->render(transform); } } + +void ImageScreenSaver::update(int deltaTime) +{ + GuiComponent::update(deltaTime); +} + +void ImageScreenSaver::setOpacity(unsigned char opacity) +{ + mOpacity = opacity; +} + +void ImageScreenSaver::setGame(FileData* game) +{ + if (mLabelGame != nullptr) + { + delete mLabelGame; + mLabelGame = nullptr; + } + + if (mLabelSystem != nullptr) + { + delete mLabelSystem; + mLabelSystem = nullptr; + } + + if (mMarquee != nullptr) + { + delete mMarquee; + mMarquee = nullptr; + } + + if (game == nullptr) + return; + + if (!Settings::getInstance()->getBool("SlideshowScreenSaverGameName")) + return; + + /* + if (Utils::FileSystem::exists(game->getMarqueePath())) + { + mMarquee = new ImageComponent(mWindow, true); + mMarquee->setImage(game->getMarqueePath()); + mMarquee->setOrigin(0.5f, 0.5f); + mMarquee->setPosition(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() * 0.78f); + mMarquee->setMaxSize((float)Renderer::getScreenWidth() * 0.40f, (float)Renderer::getScreenHeight() * 0.20f); + } + */ + auto ph = ThemeData::getMenuTheme()->Text.font->getPath(); + auto sz = Renderer::getScreenHeight() / 16.f; + auto font = Font::get(sz, ph); + + int h = Renderer::getScreenHeight() / 4.0f; + int fh = font->getLetterHeight(); + + mLabelGame = new TextComponent(mWindow); + mLabelGame->setPosition(0, Renderer::getScreenHeight() - h - fh / 2); + mLabelGame->setSize(Renderer::getScreenWidth(), h - fh / 2); + mLabelGame->setHorizontalAlignment(ALIGN_CENTER); + mLabelGame->setVerticalAlignment(ALIGN_CENTER); + mLabelGame->setColor(0xFFFFFFFF); + mLabelGame->setGlowColor(0x00000040); + mLabelGame->setGlowSize(3); + mLabelGame->setFont(font); + mLabelGame->setText(game->getName()); + + mLabelSystem = new TextComponent(mWindow); + mLabelSystem->setPosition(0, Renderer::getScreenHeight() - h + fh / 2); + mLabelSystem->setSize(Renderer::getScreenWidth(), h + fh / 2); + mLabelSystem->setHorizontalAlignment(ALIGN_CENTER); + mLabelSystem->setVerticalAlignment(ALIGN_CENTER); + mLabelSystem->setColor(0xD0D0D0FF); + mLabelSystem->setGlowColor(0x00000060); + mLabelSystem->setGlowSize(2); + mLabelSystem->setFont(ph, sz * 0.66); + mLabelSystem->setText(game->getSystem()->getFullName()); +} \ No newline at end of file diff --git a/es-app/src/SystemScreenSaver.h b/es-app/src/SystemScreenSaver.h index a26f4407e9..f0089a5473 100644 --- a/es-app/src/SystemScreenSaver.h +++ b/es-app/src/SystemScreenSaver.h @@ -3,10 +3,34 @@ #define ES_APP_SYSTEM_SCREEN_SAVER_H #include "Window.h" +#include "GuiComponent.h" class ImageComponent; class Sound; class VideoComponent; +class TextComponent; + +class ImageScreenSaver : GuiComponent +{ +public: + ImageScreenSaver(Window* window); + ~ImageScreenSaver(); + + void setGame(FileData* mCurrentGame); + void setImage(const std::string path); + bool hasImage(); + + void render(const Transform4x4f& transform) override; + void update(int deltaTime) override; + + void setOpacity(unsigned char opacity) override; + +private: + ImageComponent* mImage; + ImageComponent* mMarquee; + TextComponent* mLabelGame; + TextComponent* mLabelSystem; +}; // Screensaver implementation for main window class SystemScreenSaver : public Window::ScreenSaver @@ -36,8 +60,6 @@ class SystemScreenSaver : public Window::ScreenSaver void pickRandomGameListImage(std::string& path); void pickRandomCustomImage(std::string& path); - void input(InputConfig* config, Input input); - enum STATE { STATE_INACTIVE, STATE_FADE_OUT_WINDOW, @@ -51,7 +73,10 @@ class SystemScreenSaver : public Window::ScreenSaver VideoComponent* mVideoScreensaver; bool mImagesCounted; unsigned long mImageCount; - ImageComponent* mImageScreensaver; + + std::shared_ptr mFadingImageScreensaver; + std::shared_ptr mImageScreensaver; + Window* mWindow; STATE mState; float mOpacity; @@ -60,8 +85,10 @@ class SystemScreenSaver : public Window::ScreenSaver std::string mGameName; std::string mSystemName; int mVideoChangeTime; - std::shared_ptr mBackgroundAudio; - bool mStopBackgroundAudio; + + //std::shared_ptr mBackgroundAudio; + bool mLoadingNext; + }; #endif // ES_APP_SYSTEM_SCREEN_SAVER_H diff --git a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp index d77d622eea..38ddc2fa99 100644 --- a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp +++ b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp @@ -11,36 +11,31 @@ GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, std::string title) : GuiScreensaverOptions(window, title) { // screensaver time - auto screensaver_time = std::make_shared(mWindow, 0.f, 30.f, 1.f, "m"); + auto screensaver_time = std::make_shared(mWindow, 0.f, 120.0f, 1.f, "m"); screensaver_time->setValue((float)(Settings::getInstance()->getInt("ScreenSaverTime") / (1000 * 60))); - addWithLabel("SCREENSAVER AFTER", screensaver_time); + addWithLabel(_("SCREENSAVER AFTER"), screensaver_time); addSaveFunc([screensaver_time] { Settings::getInstance()->setInt("ScreenSaverTime", (int)Math::round(screensaver_time->getValue()) * (1000 * 60)); PowerSaver::updateTimeouts(); }); - // Allow ScreenSaver Controls - ScreenSaverControls - auto ss_controls = std::make_shared(mWindow); - ss_controls->setState(Settings::getInstance()->getBool("ScreenSaverControls")); - addWithLabel("SCREENSAVER CONTROLS", ss_controls); - addSaveFunc([ss_controls] { Settings::getInstance()->setBool("ScreenSaverControls", ss_controls->getState()); }); - // screensaver behavior - auto screensaver_behavior = std::make_shared< OptionListComponent >(mWindow, "SCREENSAVER BEHAVIOR", false); + auto screensaver_behavior = std::make_shared< OptionListComponent >(mWindow, _("SCREENSAVER BEHAVIOR"), false); std::vector screensavers; screensavers.push_back("dim"); screensavers.push_back("black"); screensavers.push_back("random video"); screensavers.push_back("slideshow"); for(auto it = screensavers.cbegin(); it != screensavers.cend(); it++) - screensaver_behavior->add(*it, *it, Settings::getInstance()->getString("ScreenSaverBehavior") == *it); - addWithLabel("SCREENSAVER BEHAVIOR", screensaver_behavior); + screensaver_behavior->add(_(it->c_str()), *it, Settings::getInstance()->getString("ScreenSaverBehavior") == *it); + addWithLabel(_("SCREENSAVER BEHAVIOR"), screensaver_behavior); addSaveFunc([this, screensaver_behavior] { - if (Settings::getInstance()->getString("ScreenSaverBehavior") != "random video" && screensaver_behavior->getSelected() == "random video") { + if (Settings::getInstance()->getString("ScreenSaverBehavior") != "random video" + && screensaver_behavior->getSelected() == "random video") { // if before it wasn't risky but now there's a risk of problems, show warning mWindow->pushGui(new GuiMsgBox(mWindow, - "The \"Random Video\" screensaver shows videos from your gamelist.\n\nIf you do not have videos, or if in several consecutive attempts the games it selects don't have videos it will default to black.\n\nMore options in the \"UI Settings\" > \"Video Screensaver\" menu.", - "OK", [] { return; })); + _("THE \"RANDOM VIDEO\" SCREENSAVER SHOWS VIDEOS FROM YOUR GAMELIST.\nIF YOU DON'T HAVE VIDEOS, OR IF NONE OF THEM CAN BE PLAYED AFTER A FEW ATTEMPTS, IT WILL DEFAULT TO \"BLACK\".\nMORE OPTIONS IN THE \"UI SETTINGS\" -> \"RANDOM VIDEO SCREENSAVER SETTINGS\" MENU."), + _("OK"), [] { return; })); } Settings::getInstance()->setString("ScreenSaverBehavior", screensaver_behavior->getSelected()); PowerSaver::updateTimeouts(); @@ -50,16 +45,22 @@ GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, std:: // show filtered menu row.elements.clear(); - row.addElement(std::make_shared(mWindow, "VIDEO SCREENSAVER SETTINGS", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(mWindow, _("VIDEO SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGeneralScreensaverOptions::openVideoScreensaverOptions, this)); addRow(row); row.elements.clear(); - row.addElement(std::make_shared(mWindow, "SLIDESHOW SCREENSAVER SETTINGS", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); + row.addElement(std::make_shared(mWindow, _("SLIDESHOW SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); row.addElement(makeArrow(mWindow), false); row.makeAcceptInputHandler(std::bind(&GuiGeneralScreensaverOptions::openSlideshowScreensaverOptions, this)); addRow(row); + + // Allow ScreenSaver Controls - ScreenSaverControls + auto ss_controls = std::make_shared(mWindow); + ss_controls->setState(Settings::getInstance()->getBool("ScreenSaverControls")); + addWithLabel(_("SCREENSAVER CONTROLS"), ss_controls); + addSaveFunc([ss_controls] { Settings::getInstance()->setBool("ScreenSaverControls", ss_controls->getState()); }); } GuiGeneralScreensaverOptions::~GuiGeneralScreensaverOptions() @@ -67,10 +68,10 @@ GuiGeneralScreensaverOptions::~GuiGeneralScreensaverOptions() } void GuiGeneralScreensaverOptions::openVideoScreensaverOptions() { - mWindow->pushGui(new GuiVideoScreensaverOptions(mWindow, "VIDEO SCREENSAVER")); + mWindow->pushGui(new GuiVideoScreensaverOptions(mWindow, _("VIDEO SCREENSAVER").c_str())); } void GuiGeneralScreensaverOptions::openSlideshowScreensaverOptions() { - mWindow->pushGui(new GuiSlideshowScreensaverOptions(mWindow, "SLIDESHOW SCREENSAVER")); + mWindow->pushGui(new GuiSlideshowScreensaverOptions(mWindow, _("SLIDESHOW SCREENSAVER").c_str())); } diff --git a/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp b/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp index 53285498b8..700a7a9751 100644 --- a/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp +++ b/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp @@ -20,6 +20,12 @@ GuiSlideshowScreensaverOptions::GuiSlideshowScreensaverOptions(Window* window, c Settings::getInstance()->setInt("ScreenSaverSwapImageTimeout", playNextTimeout); PowerSaver::updateTimeouts(); }); + + // SHOW GAME NAME + auto ss_controls = std::make_shared(mWindow); + ss_controls->setState(Settings::getInstance()->getBool("SlideshowScreenSaverGameName")); + addWithLabel(row, _("SHOW GAME NAME"), ss_controls); + addSaveFunc([ss_controls] { Settings::getInstance()->setBool("SlideshowScreenSaverGameName", ss_controls->getState()); }); // stretch auto sss_stretch = std::make_shared(mWindow); @@ -29,13 +35,14 @@ GuiSlideshowScreensaverOptions::GuiSlideshowScreensaverOptions(Window* window, c Settings::getInstance()->setBool("SlideshowScreenSaverStretch", sss_stretch->getState()); }); + /* // background audio file auto sss_bg_audio_file = std::make_shared(mWindow, "", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); addEditableTextComponent(row, "BACKGROUND AUDIO", sss_bg_audio_file, Settings::getInstance()->getString("SlideshowScreenSaverBackgroundAudioFile")); addSaveFunc([sss_bg_audio_file] { Settings::getInstance()->setString("SlideshowScreenSaverBackgroundAudioFile", sss_bg_audio_file->getValue()); }); - + */ // image source auto sss_custom_source = std::make_shared(mWindow); sss_custom_source->setState(Settings::getInstance()->getBool("SlideshowScreenSaverCustomImageSource")); @@ -63,6 +70,7 @@ GuiSlideshowScreensaverOptions::GuiSlideshowScreensaverOptions(Window* window, c addSaveFunc([sss_image_filter] { Settings::getInstance()->setString("SlideshowScreenSaverImageFilter", sss_image_filter->getValue()); }); + } GuiSlideshowScreensaverOptions::~GuiSlideshowScreensaverOptions() diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 268e225f1e..b6982fff90 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -1,358 +1,358 @@ -#include "Settings.h" - -#include "utils/FileSystemUtil.h" -#include "Log.h" -#include "Scripting.h" -#include "platform.h" -#include -#include -#include - -Settings* Settings::sInstance = NULL; -static std::string mEmptyString = ""; - -// these values are NOT saved to es_settings.xml -// since they're set through command-line arguments, and not the in-program settings menu -std::vector settings_dont_save { - { "Debug" }, - { "DebugGrid" }, - { "DebugText" }, - { "DebugImage" }, - { "ForceKid" }, - { "ForceKiosk" }, - { "IgnoreGamelist" }, - { "HideConsole" }, - { "ShowExit" }, - { "SplashScreen" }, - { "SplashScreenProgress" }, -#if !defined(_WIN32) - { "VSync" }, -#endif - { "FullscreenBorderless" }, - { "Windowed" }, - { "WindowWidth" }, - { "WindowHeight" }, - { "ScreenWidth" }, - { "ScreenHeight" }, - { "ScreenOffsetX" }, - { "ScreenOffsetY" }, - { "ScreenRotate" }, - { "MonitorID" }, - { "ExePath" }, - { "FixedCarousel" }, - { "HomePath" }, +#include "Settings.h" + +#include "utils/FileSystemUtil.h" +#include "Log.h" +#include "Scripting.h" +#include "platform.h" +#include +#include +#include + +Settings* Settings::sInstance = NULL; +static std::string mEmptyString = ""; + +// these values are NOT saved to es_settings.xml +// since they're set through command-line arguments, and not the in-program settings menu +std::vector settings_dont_save { + { "Debug" }, + { "DebugGrid" }, + { "DebugText" }, + { "DebugImage" }, + { "ForceKid" }, + { "ForceKiosk" }, + { "IgnoreGamelist" }, + { "HideConsole" }, + { "ShowExit" }, + { "SplashScreen" }, + { "SplashScreenProgress" }, +#if !defined(_WIN32) + { "VSync" }, +#endif + { "FullscreenBorderless" }, + { "Windowed" }, + { "WindowWidth" }, + { "WindowHeight" }, + { "ScreenWidth" }, + { "ScreenHeight" }, + { "ScreenOffsetX" }, + { "ScreenOffsetY" }, + { "ScreenRotate" }, + { "MonitorID" }, + { "ExePath" }, + { "FixedCarousel" }, + { "HomePath" }, { "MusicDirectory"}, - { "UserMusicDirectory" } -}; - -Settings::Settings() -{ - mHasConfigRoot = false; - setDefaults(); - loadFile(); -} - -Settings* Settings::getInstance() -{ - if(sInstance == NULL) - sInstance = new Settings(); - - return sInstance; -} - -void Settings::setDefaults() -{ - mWasChanged = false; - - mBoolMap.clear(); - mIntMap.clear(); - -#if _DEBUG - mBoolMap["FixedCarousel"] = false; -#else - mBoolMap["FixedCarousel"] = false; -#endif - - mBoolMap["BackgroundJoystickInput"] = false; - mBoolMap["ParseGamelistOnly"] = false; - mBoolMap["ShowHiddenFiles"] = false; - mBoolMap["DrawFramerate"] = false; - mBoolMap["ShowExit"] = true; - -#if WIN32 - mBoolMap["ShowOnlyExit"] = true; - mBoolMap["FullscreenBorderless"] = true; -#else - mBoolMap["ShowOnlyExit"] = false; - mBoolMap["FullscreenBorderless"] = false; -#endif - - mBoolMap["Windowed"] = false; - mBoolMap["SplashScreen"] = true; - mBoolMap["SplashScreenProgress"] = true; - mBoolMap["PreloadUI"] = false; - mBoolMap["StartupOnGameList"] = false; - mBoolMap["HideSystemView"] = false; - - mStringMap["StartupSystem"] = ""; - - mBoolMap["FlatFolders"] = false; - - mBoolMap["VSync"] = true; - mBoolMap["EnableSounds"] = true; - mBoolMap["ShowHelpPrompts"] = true; - mBoolMap["ScrapeRatings"] = true; - mBoolMap["IgnoreGamelist"] = false; - mBoolMap["HideConsole"] = true; - mBoolMap["QuickSystemSelect"] = true; - mBoolMap["MoveCarousel"] = true; - mBoolMap["SaveGamelistsOnExit"] = true; - mBoolMap["OptimizeVRAM"] = true; - mBoolMap["ThreadedLoading"] = true; - mBoolMap["MusicTitles"] = true; - - mBoolMap["Debug"] = false; - mBoolMap["DebugGrid"] = false; - mBoolMap["DebugText"] = false; - mBoolMap["DebugImage"] = false; - - mIntMap["ScreenSaverTime"] = 5*60*1000; // 5 minutes - mIntMap["ScraperResizeWidth"] = 400; - mIntMap["ScraperResizeHeight"] = 0; - -#if defined(_WIN32) - mIntMap["MaxVRAM"] = 256; -#else - #ifdef _RPI_ - mIntMap["MaxVRAM"] = 80; - #else - mIntMap["MaxVRAM"] = 100; - #endif -#endif - -#if defined(_WIN32) - mBoolMap["HideWindow"] = false; -#else - mBoolMap["HideWindow"] = true; -#endif - mStringMap["GameTransitionStyle"] = "fade"; - mStringMap["TransitionStyle"] = "slide"; - mStringMap["Language"] = "en"; - mStringMap["ThemeSet"] = ""; - mStringMap["ScreenSaverBehavior"] = "dim"; - mStringMap["GamelistViewStyle"] = "automatic"; - mStringMap["DefaultGridSize"] = ""; - - mStringMap["ThemeColorSet"] = ""; - mStringMap["ThemeIconSet"] = ""; - mStringMap["ThemeMenu"] = ""; - mStringMap["ThemeSystemView"] = ""; - mStringMap["ThemeGamelistView"] = ""; - mStringMap["ThemeRegionName"] = "eu"; - - mBoolMap["ScreenSaverControls"] = true; - mStringMap["ScreenSaverGameInfo"] = "never"; - mBoolMap["StretchVideoOnScreenSaver"] = false; - mStringMap["PowerSaverMode"] = "disabled"; - - mIntMap["ScreenSaverSwapImageTimeout"] = 10000; - mBoolMap["SlideshowScreenSaverStretch"] = false; - mStringMap["SlideshowScreenSaverBackgroundAudioFile"] = Utils::FileSystem::getHomePath() + "/.emulationstation/slideshow/audio/slideshow_bg.wav"; - mBoolMap["SlideshowScreenSaverCustomImageSource"] = false; - mStringMap["SlideshowScreenSaverImageDir"] = Utils::FileSystem::getHomePath() + "/.emulationstation/slideshow/image"; - mStringMap["SlideshowScreenSaverImageFilter"] = ".png,.jpg"; - mBoolMap["SlideshowScreenSaverRecurse"] = false; - - // This setting only applies to raspberry pi but set it for all platforms so - // we don't get a warning if we encounter it on a different platform - mBoolMap["VideoOmxPlayer"] = false; - #ifdef _RPI_ - // we're defaulting to OMX Player for full screen video on the Pi - mBoolMap["ScreenSaverOmxPlayer"] = true; - // use OMX Player defaults - mStringMap["SubtitleFont"] = "/usr/share/fonts/truetype/freefont/FreeSans.ttf"; - mStringMap["SubtitleItalicFont"] = "/usr/share/fonts/truetype/freefont/FreeSansOblique.ttf"; - mIntMap["SubtitleSize"] = 55; - mStringMap["SubtitleAlignment"] = "left"; - #else - mBoolMap["ScreenSaverOmxPlayer"] = false; - #endif - - mIntMap["ScreenSaverSwapVideoTimeout"] = 30000; - - mBoolMap["VideoAudio"] = true; - mBoolMap["CaptionsCompatibility"] = true; - // Audio out device for Video playback using OMX player. - mStringMap["OMXAudioDev"] = "both"; - mStringMap["CollectionSystemsAuto"] = ""; - mStringMap["CollectionSystemsCustom"] = ""; - mBoolMap["CollectionShowSystemInfo"] = true; - mBoolMap["SortAllSystems"] = false; - mBoolMap["UseCustomCollectionsSystem"] = true; - mBoolMap["FavoritesFirst"] = true; - - - mBoolMap["LocalArt"] = false; - - // Audio out device for volume control - #ifdef _RPI_ - mStringMap["AudioDevice"] = "PCM"; - #else - mStringMap["AudioDevice"] = "Master"; - #endif - - mStringMap["AudioCard"] = "default"; - mStringMap["UIMode"] = "Full"; - mStringMap["UIMode_passkey"] = "uuddlrlrba"; - mBoolMap["ForceKiosk"] = false; - mBoolMap["ForceKid"] = false; - mBoolMap["ForceDisableFilters"] = false; - - mIntMap["WindowWidth"] = 0; - mIntMap["WindowHeight"] = 0; - mIntMap["ScreenWidth"] = 0; - mIntMap["ScreenHeight"] = 0; - mIntMap["ScreenOffsetX"] = 0; - mIntMap["ScreenOffsetY"] = 0; - mIntMap["ScreenRotate"] = 0; - mIntMap["MonitorID"] = -1; - mStringMap["ExePath"] = ""; - - mStringMap["Scraper"] = "ScreenScraper"; + { "UserMusicDirectory" } +}; + +Settings::Settings() +{ + mHasConfigRoot = false; + setDefaults(); + loadFile(); +} + +Settings* Settings::getInstance() +{ + if(sInstance == NULL) + sInstance = new Settings(); + + return sInstance; +} + +void Settings::setDefaults() +{ + mWasChanged = false; + + mBoolMap.clear(); + mIntMap.clear(); + +#if _DEBUG + mBoolMap["FixedCarousel"] = false; +#else + mBoolMap["FixedCarousel"] = false; +#endif + + mBoolMap["BackgroundJoystickInput"] = false; + mBoolMap["ParseGamelistOnly"] = false; + mBoolMap["ShowHiddenFiles"] = false; + mBoolMap["DrawFramerate"] = false; + mBoolMap["ShowExit"] = true; + +#if WIN32 + mBoolMap["ShowOnlyExit"] = true; + mBoolMap["FullscreenBorderless"] = true; +#else + mBoolMap["ShowOnlyExit"] = false; + mBoolMap["FullscreenBorderless"] = false; +#endif + + mBoolMap["Windowed"] = false; + mBoolMap["SplashScreen"] = true; + mBoolMap["SplashScreenProgress"] = true; + mBoolMap["PreloadUI"] = false; + mBoolMap["StartupOnGameList"] = false; + mBoolMap["HideSystemView"] = false; + + mStringMap["StartupSystem"] = ""; + + mBoolMap["FlatFolders"] = false; + + mBoolMap["VSync"] = true; + mBoolMap["EnableSounds"] = true; + mBoolMap["ShowHelpPrompts"] = true; + mBoolMap["ScrapeRatings"] = true; + mBoolMap["IgnoreGamelist"] = false; + mBoolMap["HideConsole"] = true; + mBoolMap["QuickSystemSelect"] = true; + mBoolMap["MoveCarousel"] = true; + mBoolMap["SaveGamelistsOnExit"] = true; + mBoolMap["OptimizeVRAM"] = true; + mBoolMap["ThreadedLoading"] = true; + mBoolMap["MusicTitles"] = true; + + mBoolMap["Debug"] = false; + mBoolMap["DebugGrid"] = false; + mBoolMap["DebugText"] = false; + mBoolMap["DebugImage"] = false; + + mIntMap["ScreenSaverTime"] = 5*60*1000; // 5 minutes + mIntMap["ScraperResizeWidth"] = 400; + mIntMap["ScraperResizeHeight"] = 0; + +#if defined(_WIN32) + mIntMap["MaxVRAM"] = 256; +#else + #ifdef _RPI_ + mIntMap["MaxVRAM"] = 80; + #else + mIntMap["MaxVRAM"] = 100; + #endif +#endif + +#if defined(_WIN32) + mBoolMap["HideWindow"] = false; +#else + mBoolMap["HideWindow"] = true; +#endif + mStringMap["GameTransitionStyle"] = "fade"; + mStringMap["TransitionStyle"] = "slide"; + mStringMap["Language"] = "en"; + mStringMap["ThemeSet"] = ""; + mStringMap["ScreenSaverBehavior"] = "dim"; + mStringMap["GamelistViewStyle"] = "automatic"; + mStringMap["DefaultGridSize"] = ""; + + mStringMap["ThemeColorSet"] = ""; + mStringMap["ThemeIconSet"] = ""; + mStringMap["ThemeMenu"] = ""; + mStringMap["ThemeSystemView"] = ""; + mStringMap["ThemeGamelistView"] = ""; + mStringMap["ThemeRegionName"] = "eu"; + + mBoolMap["ScreenSaverControls"] = true; + mStringMap["ScreenSaverGameInfo"] = "never"; + mBoolMap["StretchVideoOnScreenSaver"] = false; + mStringMap["PowerSaverMode"] = "disabled"; + + mIntMap["ScreenSaverSwapImageTimeout"] = 10000; + mBoolMap["SlideshowScreenSaverStretch"] = false; + mBoolMap["SlideshowScreenSaverCustomImageSource"] = false; + mStringMap["SlideshowScreenSaverImageDir"] = Utils::FileSystem::getHomePath() + "/.emulationstation/slideshow/image"; + mStringMap["SlideshowScreenSaverImageFilter"] = ".png,.jpg"; + mBoolMap["SlideshowScreenSaverRecurse"] = false; + mBoolMap["SlideshowScreenSaverGameName"] = true; + + // This setting only applies to raspberry pi but set it for all platforms so + // we don't get a warning if we encounter it on a different platform + mBoolMap["VideoOmxPlayer"] = false; + #ifdef _RPI_ + // we're defaulting to OMX Player for full screen video on the Pi + mBoolMap["ScreenSaverOmxPlayer"] = true; + // use OMX Player defaults + mStringMap["SubtitleFont"] = "/usr/share/fonts/truetype/freefont/FreeSans.ttf"; + mStringMap["SubtitleItalicFont"] = "/usr/share/fonts/truetype/freefont/FreeSansOblique.ttf"; + mIntMap["SubtitleSize"] = 55; + mStringMap["SubtitleAlignment"] = "left"; + #else + mBoolMap["ScreenSaverOmxPlayer"] = false; + #endif + + mIntMap["ScreenSaverSwapVideoTimeout"] = 30000; + + mBoolMap["VideoAudio"] = true; + mBoolMap["CaptionsCompatibility"] = true; + // Audio out device for Video playback using OMX player. + mStringMap["OMXAudioDev"] = "both"; + mStringMap["CollectionSystemsAuto"] = ""; + mStringMap["CollectionSystemsCustom"] = ""; + mBoolMap["CollectionShowSystemInfo"] = true; + mBoolMap["SortAllSystems"] = false; + mBoolMap["UseCustomCollectionsSystem"] = true; + mBoolMap["FavoritesFirst"] = true; + + + mBoolMap["LocalArt"] = false; + + // Audio out device for volume control + #ifdef _RPI_ + mStringMap["AudioDevice"] = "PCM"; + #else + mStringMap["AudioDevice"] = "Master"; + #endif + + mStringMap["AudioCard"] = "default"; + mStringMap["UIMode"] = "Full"; + mStringMap["UIMode_passkey"] = "uuddlrlrba"; + mBoolMap["ForceKiosk"] = false; + mBoolMap["ForceKid"] = false; + mBoolMap["ForceDisableFilters"] = false; + + mIntMap["WindowWidth"] = 0; + mIntMap["WindowHeight"] = 0; + mIntMap["ScreenWidth"] = 0; + mIntMap["ScreenHeight"] = 0; + mIntMap["ScreenOffsetX"] = 0; + mIntMap["ScreenOffsetY"] = 0; + mIntMap["ScreenRotate"] = 0; + mIntMap["MonitorID"] = -1; + mStringMap["ExePath"] = ""; + + mStringMap["Scraper"] = "ScreenScraper"; mStringMap["ScrapperImageSrc"] = "ss"; - mStringMap["ScrapperThumbSrc"] = "box-2D"; - - mBoolMap["ScrapeMarquee"] = false; - mBoolMap["ScrapeVideos"] = false; - - mBoolMap["audio.bgmusic"] = true; - mBoolMap["audio.persystem"] = false; - mBoolMap["audio.thememusics"] = true; - - mStringMap["MusicDirectory"] = ""; - mStringMap["UserMusicDirectory"] = ""; - - mDefaultBoolMap = mBoolMap; - mDefaultIntMap = mIntMap; - mDefaultFloatMap = mFloatMap; - mDefaultStringMap = mStringMap; -} - -template -void saveMap(pugi::xml_node& doc, std::map& map, const char* type, std::map& defaultMap) -{ - for(auto iter = map.cbegin(); iter != map.cend(); iter++) - { - // key is on the "don't save" list, so don't save it - if(std::find(settings_dont_save.cbegin(), settings_dont_save.cend(), iter->first) != settings_dont_save.cend()) - continue; - - auto def = defaultMap.find(iter->first); - if (def != defaultMap.cend() && def->second == iter->second) - continue; - - pugi::xml_node node = doc.append_child(type); - node.append_attribute("name").set_value(iter->first.c_str()); - node.append_attribute("value").set_value(iter->second); - } -} - -bool Settings::saveFile() -{ + mStringMap["ScrapperThumbSrc"] = "box-2D"; + + mBoolMap["ScrapeMarquee"] = false; + mBoolMap["ScrapeVideos"] = false; + + mBoolMap["audio.bgmusic"] = true; + mBoolMap["audio.persystem"] = false; + mBoolMap["audio.thememusics"] = true; + + mStringMap["MusicDirectory"] = ""; + mStringMap["UserMusicDirectory"] = ""; + + mDefaultBoolMap = mBoolMap; + mDefaultIntMap = mIntMap; + mDefaultFloatMap = mFloatMap; + mDefaultStringMap = mStringMap; +} + +template +void saveMap(pugi::xml_node& doc, std::map& map, const char* type, std::map& defaultMap) +{ + for(auto iter = map.cbegin(); iter != map.cend(); iter++) + { + // key is on the "don't save" list, so don't save it + if(std::find(settings_dont_save.cbegin(), settings_dont_save.cend(), iter->first) != settings_dont_save.cend()) + continue; + + auto def = defaultMap.find(iter->first); + if (def != defaultMap.cend() && def->second == iter->second) + continue; + + pugi::xml_node node = doc.append_child(type); + node.append_attribute("name").set_value(iter->first.c_str()); + node.append_attribute("value").set_value(iter->second); + } +} + +bool Settings::saveFile() +{ if (!mWasChanged) return false; - mWasChanged = false; - - LOG(LogDebug) << "Settings::saveFile() : Saving Settings to file."; - const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_settings.cfg"; - - pugi::xml_document doc; - pugi::xml_node root = doc; - - if (mHasConfigRoot) - root = doc.append_child("config"); // batocera, root element - - saveMap(root, mBoolMap, "bool", mDefaultBoolMap); - saveMap(root, mIntMap, "int", mDefaultIntMap); - saveMap(root, mFloatMap, "float", mDefaultFloatMap); - - //saveMap(doc, mStringMap, "string"); - for(auto iter = mStringMap.cbegin(); iter != mStringMap.cend(); iter++) - { - // key is on the "don't save" list, so don't save it - if (std::find(settings_dont_save.cbegin(), settings_dont_save.cend(), iter->first) != settings_dont_save.cend()) - continue; - - auto def = mDefaultStringMap.find(iter->first); - if (def == mDefaultStringMap.cend() && iter->second.empty()) - continue; - - if (def != mDefaultStringMap.cend() && def->second == iter->second) - continue; - - pugi::xml_node node = root.append_child("string"); - node.append_attribute("name").set_value(iter->first.c_str()); - node.append_attribute("value").set_value(iter->second.c_str()); - } - - doc.save_file(path.c_str()); - - Scripting::fireEvent("config-changed"); - Scripting::fireEvent("settings-changed"); - - return true; -} - -void Settings::loadFile() -{ - const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_settings.cfg"; - - if(!Utils::FileSystem::exists(path)) - return; - - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(path.c_str()); - if(!result) - { - LOG(LogError) << "Could not parse Settings file!\n " << result.description(); - return; - } - - pugi::xml_node root = doc; - - // Batocera has a root element, learn reading them - pugi::xml_node config = doc.child("config"); - if (config) - { - mHasConfigRoot = true; - root = config; - } - - for(pugi::xml_node node = root.child("bool"); node; node = node.next_sibling("bool")) - setBool(node.attribute("name").as_string(), node.attribute("value").as_bool()); - for(pugi::xml_node node = root.child("int"); node; node = node.next_sibling("int")) - setInt(node.attribute("name").as_string(), node.attribute("value").as_int()); - for(pugi::xml_node node = root.child("float"); node; node = node.next_sibling("float")) - setFloat(node.attribute("name").as_string(), node.attribute("value").as_float()); - for(pugi::xml_node node = root.child("string"); node; node = node.next_sibling("string")) - setString(node.attribute("name").as_string(), node.attribute("value").as_string()); - - mWasChanged = false; -} - -//Print a warning message if the setting we're trying to get doesn't already exist in the map, then return the value in the map. -#define SETTINGS_GETSET(type, mapName, getMethodName, setMethodName, defaultValue) type Settings::getMethodName(const std::string& name) \ -{ \ - if(mapName.find(name) == mapName.cend()) \ - { \ - /*LOG(LogError) << "Tried to use unset setting " << name << "!";*/ \ - return defaultValue; \ - } \ - return mapName[name]; \ -} \ -bool Settings::setMethodName(const std::string& name, type value) \ -{ \ + mWasChanged = false; + + LOG(LogDebug) << "Settings::saveFile() : Saving Settings to file."; + const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_settings.cfg"; + + pugi::xml_document doc; + pugi::xml_node root = doc; + + if (mHasConfigRoot) + root = doc.append_child("config"); // batocera, root element + + saveMap(root, mBoolMap, "bool", mDefaultBoolMap); + saveMap(root, mIntMap, "int", mDefaultIntMap); + saveMap(root, mFloatMap, "float", mDefaultFloatMap); + + //saveMap(doc, mStringMap, "string"); + for(auto iter = mStringMap.cbegin(); iter != mStringMap.cend(); iter++) + { + // key is on the "don't save" list, so don't save it + if (std::find(settings_dont_save.cbegin(), settings_dont_save.cend(), iter->first) != settings_dont_save.cend()) + continue; + + auto def = mDefaultStringMap.find(iter->first); + if (def == mDefaultStringMap.cend() && iter->second.empty()) + continue; + + if (def != mDefaultStringMap.cend() && def->second == iter->second) + continue; + + pugi::xml_node node = root.append_child("string"); + node.append_attribute("name").set_value(iter->first.c_str()); + node.append_attribute("value").set_value(iter->second.c_str()); + } + + doc.save_file(path.c_str()); + + Scripting::fireEvent("config-changed"); + Scripting::fireEvent("settings-changed"); + + return true; +} + +void Settings::loadFile() +{ + const std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/es_settings.cfg"; + + if(!Utils::FileSystem::exists(path)) + return; + + pugi::xml_document doc; + pugi::xml_parse_result result = doc.load_file(path.c_str()); + if(!result) + { + LOG(LogError) << "Could not parse Settings file!\n " << result.description(); + return; + } + + pugi::xml_node root = doc; + + // Batocera has a root element, learn reading them + pugi::xml_node config = doc.child("config"); + if (config) + { + mHasConfigRoot = true; + root = config; + } + + for(pugi::xml_node node = root.child("bool"); node; node = node.next_sibling("bool")) + setBool(node.attribute("name").as_string(), node.attribute("value").as_bool()); + for(pugi::xml_node node = root.child("int"); node; node = node.next_sibling("int")) + setInt(node.attribute("name").as_string(), node.attribute("value").as_int()); + for(pugi::xml_node node = root.child("float"); node; node = node.next_sibling("float")) + setFloat(node.attribute("name").as_string(), node.attribute("value").as_float()); + for(pugi::xml_node node = root.child("string"); node; node = node.next_sibling("string")) + setString(node.attribute("name").as_string(), node.attribute("value").as_string()); + + mWasChanged = false; +} + +//Print a warning message if the setting we're trying to get doesn't already exist in the map, then return the value in the map. +#define SETTINGS_GETSET(type, mapName, getMethodName, setMethodName, defaultValue) type Settings::getMethodName(const std::string& name) \ +{ \ + if(mapName.find(name) == mapName.cend()) \ + { \ + /*LOG(LogError) << "Tried to use unset setting " << name << "!";*/ \ + return defaultValue; \ + } \ + return mapName[name]; \ +} \ +bool Settings::setMethodName(const std::string& name, type value) \ +{ \ if (mapName.count(name) == 0 || mapName[name] != value) { \ mapName[name] = value; \ \ @@ -361,10 +361,10 @@ bool Settings::setMethodName(const std::string& name, type value) \ \ return true; \ } \ - return false; \ -} - -SETTINGS_GETSET(bool, mBoolMap, getBool, setBool, false); -SETTINGS_GETSET(int, mIntMap, getInt, setInt, 0); -SETTINGS_GETSET(float, mFloatMap, getFloat, setFloat, 0.0f); -SETTINGS_GETSET(const std::string&, mStringMap, getString, setString, mEmptyString); + return false; \ +} + +SETTINGS_GETSET(bool, mBoolMap, getBool, setBool, false); +SETTINGS_GETSET(int, mIntMap, getInt, setInt, 0); +SETTINGS_GETSET(float, mFloatMap, getFloat, setFloat, 0.0f); +SETTINGS_GETSET(const std::string&, mStringMap, getString, setString, mEmptyString); diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index c18b3444d8..4aa0ff40de 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -60,6 +60,7 @@ std::map> The { "folderImage", PATH }, { "showVideoAtDelay", FLOAT }, { "scrollDirection", STRING }, + { "scrollSound", PATH }, { "centerSelection", BOOLEAN }, { "scrollLoop", BOOLEAN } } }, { "gridtile", { diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index d769104159..76eac42249 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -108,7 +108,16 @@ bool Window::init(bool initRenderer) return true; } -void Window::deinit(bool deinitRenderer) +void Window::reactivateGui() +{ + for (auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) + (*i)->onShow(); + + if (peekGui()) + peekGui()->updateHelpPrompts(); +} + +void Window::deinit() { // Hide all GUI elements on uninitialisation - this disable for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) @@ -142,10 +151,9 @@ void Window::input(InputConfig* config, Input input) { if(config->isMappedLike("right", input) || config->isMappedTo("select", input)) { - if (input.value != 0) { - // handle screensaver control + if (input.value != 0) // handle screensaver control mScreenSaver->nextVideo(); - } + return; } else if(config->isMappedTo("start", input) && input.value != 0) diff --git a/es-core/src/Window.h b/es-core/src/Window.h index a34ddbc9ea..dee4437730 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -91,6 +91,7 @@ class Window void unRegisterNotificationComponent(AsyncNotificationComponent* pc); void postToUiThread(const std::function& func); + void reactivateGui(); private: void processPostedFunctions(); diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 823bd3d85e..5713ea14da 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -77,6 +77,7 @@ class ImageGridComponent : public IList protected: virtual void onCursorChanged(const CursorState& state) override; + virtual void onScroll(int /*amt*/) { if (!mScrollSound.empty()) Sound::get(mScrollSound)->play(); } private: // TILES @@ -103,6 +104,8 @@ class ImageGridComponent : public IList Vector2i mGridDimension; Vector2f mGridSizeOverride; + std::string mScrollSound; + std::shared_ptr mTheme; std::vector< std::shared_ptr > mTiles; @@ -443,7 +446,7 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, if (elem->has("showVideoAtDelay")) { mVideoDelay = elem->get("showVideoAtDelay"); - mAllowVideo = true; + mAllowVideo = (mVideoDelay >= 0); } else mAllowVideo = false; @@ -499,6 +502,9 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, } } + if (elem->has("scrollSound")) + mScrollSound = elem->get("scrollSound"); + // We still need to manually get the grid tile size here, // so we can recalculate the new grid dimension, and THEN (re)build the tiles elem = theme->getElement(view, "default", "gridtile"); From a7928eeae831532e142f410bf28a43367b3da286 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 9 Oct 2019 10:40:56 +0200 Subject: [PATCH 160/336] ScreenSaver : better video screensaver with game /system name. Add option to display marquee in screensaver instead of game name. Add option to add random decoration in screensaver. + Screenscraper : Manual scrapping : use jeuInfos before trying ( automatically ) jeuRecherche ( if no result ) + ScreenScraper : Fix atari800 platform ID. # Conflicts: # es-app/src/guis/GuiMenu.h # es-app/src/guis/GuiVideoScreensaverOptions.cpp # es-core/src/Settings.cpp # es-core/src/components/SwitchComponent.h # es-core/src/components/VideoVlcComponent.cpp --- es-app/src/SystemScreenSaver.cpp | 484 ++++++++++++------ es-app/src/SystemScreenSaver.h | 55 +- .../src/components/ScraperSearchComponent.cpp | 29 +- es-app/src/guis/GuiMenu.h | 1 + es-app/src/guis/GuiMetaDataEd.cpp | 2 +- .../guis/GuiSlideshowScreensaverOptions.cpp | 12 +- .../src/guis/GuiVideoScreensaverOptions.cpp | 73 ++- es-app/src/scrapers/ScreenScraper.cpp | 2 +- es-core/src/components/SwitchComponent.cpp | 3 + es-core/src/components/SwitchComponent.h | 6 + es-core/src/components/VideoVlcComponent.cpp | 31 +- 11 files changed, 470 insertions(+), 228 deletions(-) diff --git a/es-app/src/SystemScreenSaver.cpp b/es-app/src/SystemScreenSaver.cpp index 94eec14b85..084773763b 100644 --- a/es-app/src/SystemScreenSaver.cpp +++ b/es-app/src/SystemScreenSaver.cpp @@ -17,8 +17,10 @@ #include "components/TextComponent.h" #include #include +#include "AudioManager.h" +#include "math/Vector2i.h" -#define FADE_TIME 600 +#define FADE_TIME 500 SystemScreenSaver::SystemScreenSaver(Window* window) : mVideoScreensaver(NULL), @@ -50,15 +52,8 @@ SystemScreenSaver::~SystemScreenSaver() // Delete subtitle file, if existing remove(getTitlePath().c_str()); mCurrentGame = NULL; - - if (mVideoScreensaver != nullptr) - { - delete mVideoScreensaver; - mVideoScreensaver = nullptr; - } } - bool SystemScreenSaver::allowSleep() { return (mVideoScreensaver == nullptr && mImageScreensaver == nullptr); @@ -76,13 +71,24 @@ void SystemScreenSaver::startScreenSaver() std::string screensaver_behavior = Settings::getInstance()->getString("ScreenSaverBehavior"); if (!mVideoScreensaver && (screensaver_behavior == "random video")) { + if (Settings::getInstance()->getBool("VideoAudio")) + AudioManager::getInstance()->deinit(); + // Configure to fade out the windows, Skip Fading if Instant mode mState = PowerSaver::getMode() == PowerSaver::INSTANT ? STATE_SCREENSAVER_ACTIVE : STATE_FADE_OUT_WINDOW; - mVideoChangeTime = Settings::getInstance()->getInt("ScreenSaverSwapVideoTimeout"); - mOpacity = 0.0f; + mVideoChangeTime = Settings::getInstance()->getInt("ScreenSaverSwapVideoTimeout"); + + if (mState == STATE_FADE_OUT_WINDOW) + { + mState = STATE_FADE_IN_VIDEO; + mOpacity = 1.0f; + } + else + mOpacity = 0.0f; + // Load a random video std::string path = ""; pickRandomVideo(path); @@ -96,31 +102,10 @@ void SystemScreenSaver::startScreenSaver() if (!path.empty() && Utils::FileSystem::exists(path)) { -#ifdef _RPI_ - // Create the correct type of video component - if (Settings::getInstance()->getBool("ScreenSaverOmxPlayer")) - mVideoScreensaver = new VideoPlayerComponent(mWindow, getTitlePath()); - else - mVideoScreensaver = new VideoVlcComponent(mWindow, getTitlePath()); -#else - mVideoScreensaver = new VideoVlcComponent(mWindow, getTitlePath()); -#endif - - mVideoScreensaver->topWindow(true); - mVideoScreensaver->setOrigin(0.5f, 0.5f); - mVideoScreensaver->setPosition(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f); - - if (Settings::getInstance()->getBool("StretchVideoOnScreenSaver")) - { - mVideoScreensaver->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - } - else - { - mVideoScreensaver->setMaxSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - } + mVideoScreensaver = std::make_shared(mWindow); + mVideoScreensaver->setGame(mCurrentGame); mVideoScreensaver->setVideo(path); - mVideoScreensaver->setScreensaverMode(true); - mVideoScreensaver->onShow(); + PowerSaver::runningScreenSaver(true); mTimer = 0; return; @@ -154,29 +139,17 @@ void SystemScreenSaver::startScreenSaver() else pickRandomGameListImage(path); - mImageScreensaver = std::make_shared(mWindow); - mTimer = 0; - mImageScreensaver->setImage(path); + mImageScreensaver = std::make_shared(mWindow); mImageScreensaver->setGame(mCurrentGame); - /* - std::string bg_audio_file = Settings::getInstance()->getString("SlideshowScreenSaverBackgroundAudioFile"); - if ((!mBackgroundAudio) && (bg_audio_file != "")) - { - if (Utils::FileSystem::exists(bg_audio_file)) - { - // paused PS so that the background audio keeps playing - PowerSaver::pause(); - mBackgroundAudio = Sound::get(bg_audio_file); - mBackgroundAudio->play(); - } - } - */ + mImageScreensaver->setImage(path); + PowerSaver::runningScreenSaver(true); mTimer = 0; return; } + // No videos. Just use a standard screensaver mState = STATE_SCREENSAVER_ACTIVE; mCurrentGame = NULL; @@ -188,25 +161,18 @@ void SystemScreenSaver::stopScreenSaver() mFadingImageScreensaver = mImageScreensaver; else mFadingImageScreensaver = nullptr; - /* - if (mBackgroundAudio && !mLoadingNext) - { - mBackgroundAudio->stop(); - mBackgroundAudio.reset(); - // if we were playing audio, we paused PS - PowerSaver::resume(); + // Exiting video screen saver -> Restore sound + if (!mLoadingNext && Settings::getInstance()->getBool("VideoAudio") && mVideoScreensaver) + { + AudioManager::getInstance()->init(); + AudioManager::getInstance()->playRandomMusic(); } - */ + // so that we stop the background audio next time, unless we're restarting the screensaver mLoadingNext = false; - if (mVideoScreensaver != nullptr) - { - delete mVideoScreensaver; - mVideoScreensaver = nullptr; - } - + mVideoScreensaver = nullptr; mImageScreensaver = nullptr; // we need this to loop through different videos @@ -227,7 +193,12 @@ void SystemScreenSaver::renderScreenSaver() // Only render the video if the state requires it if ((int)mState >= STATE_FADE_IN_VIDEO) + { + unsigned int opacity = 255 - (unsigned char)(mOpacity * 255); + + mVideoScreensaver->setOpacity(opacity); mVideoScreensaver->render(transform); + } } else if (mImageScreensaver && screensaver_behavior == "slideshow") { @@ -453,6 +424,7 @@ void SystemScreenSaver::update(int deltaTime) // If we have a loaded video then update it if (mVideoScreensaver) mVideoScreensaver->update(deltaTime); + if (mImageScreensaver) mImageScreensaver->update(deltaTime); } @@ -461,9 +433,6 @@ void SystemScreenSaver::nextVideo() { mLoadingNext = true; startScreenSaver(); - - if (mFadingImageScreensaver == nullptr) - mState = STATE_SCREENSAVER_ACTIVE; } FileData* SystemScreenSaver::getCurrentGame() @@ -489,24 +458,31 @@ void SystemScreenSaver::launchGame() // ------------------------------------------------------------------------------------------------------------------------ -// IMAGE SCREEN SAVER CLASS +// GAME SCREEN SAVER BASE CLASS // ------------------------------------------------------------------------------------------------------------------------ -ImageScreenSaver::ImageScreenSaver(Window* window) : GuiComponent(window) +GameScreenSaverBase::GameScreenSaverBase(Window* window) : GuiComponent(window), + mViewport(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight()) { - mImage = nullptr; + mDecoration = nullptr; mMarquee = nullptr; mLabelGame = nullptr; mLabelSystem = nullptr; } -ImageScreenSaver::~ImageScreenSaver() +GameScreenSaverBase::~GameScreenSaverBase() { if (mMarquee != nullptr) + { delete mMarquee; + mMarquee = nullptr; + } - if (mImage != nullptr) - delete mImage; + if (mDecoration != nullptr) + { + delete mDecoration; + mDecoration = nullptr; + } if (mLabelGame != nullptr) { @@ -521,66 +497,13 @@ ImageScreenSaver::~ImageScreenSaver() } } -void ImageScreenSaver::setImage(const std::string path) -{ - if (mImage == nullptr) - { - mImage = new ImageComponent(mWindow, true); - mImage->setOrigin(0.5f, 0.5f); - mImage->setPosition(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f); +#include "guis/GuiMenu.h" +#include +#include +#include - if (Settings::getInstance()->getBool("SlideshowScreenSaverStretch")) - mImage->setMinSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - else - mImage->setMaxSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - } - - mImage->setImage(path); -} - -bool ImageScreenSaver::hasImage() -{ - return mImage != nullptr && mImage->hasImage(); -} - -void ImageScreenSaver::render(const Transform4x4f& transform) -{ - if (mImage) - { - mImage->setOpacity(mOpacity); - mImage->render(transform); - } - - if (mMarquee) - { - mMarquee->setOpacity(mOpacity); - mMarquee->render(transform); - } - else if (mLabelGame) - { - mLabelGame->setOpacity(mOpacity); - mLabelGame->render(transform); - } - - if (mLabelSystem) - { - mLabelSystem->setOpacity(mOpacity); - mLabelSystem->render(transform); - } -} - -void ImageScreenSaver::update(int deltaTime) -{ - GuiComponent::update(deltaTime); -} - -void ImageScreenSaver::setOpacity(unsigned char opacity) -{ - mOpacity = opacity; -} - -void ImageScreenSaver::setGame(FileData* game) -{ +void GameScreenSaverBase::setGame(FileData* game) +{ if (mLabelGame != nullptr) { delete mLabelGame; @@ -598,33 +521,94 @@ void ImageScreenSaver::setGame(FileData* game) delete mMarquee; mMarquee = nullptr; } - + + if (mDecoration != nullptr) + { + delete mDecoration; + mDecoration = nullptr; + } + if (game == nullptr) return; + mViewport = Renderer::Rect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight()); + +#ifdef _RPI_ + if (!Settings::getInstance()->getBool("ScreenSaverOmxPlayer")) +#endif + if (Settings::getInstance()->getBool("ScreenSaverDecoration")) + { + auto sets = GuiMenu::getDecorationsSets(game->getSystem()); + int setId = (int)(((float)rand() / float(RAND_MAX)) * (float)sets.size()); + + if (setId >= 0 && setId < sets.size() && Utils::FileSystem::exists(sets[setId].imageUrl)) + { + std::string infoFile = Utils::String::replace(sets[setId].imageUrl, ".png", ".info"); + if (Utils::FileSystem::exists(infoFile)) + { + FILE* fp = fopen(infoFile.c_str(), "r"); // non-Windows use "r" + if (fp) + { + char readBuffer[65536]; + rapidjson::FileReadStream is(fp, readBuffer, sizeof(readBuffer)); + rapidjson::Document doc; + doc.ParseStream(is); + + if (!doc.HasParseError()) + { + if (doc.HasMember("top") && doc.HasMember("left") && doc.HasMember("bottom") && doc.HasMember("right") && doc.HasMember("width") && doc.HasMember("height")) + { + auto width = doc["width"].GetInt(); + auto height = doc["height"].GetInt(); + if (width > 0 && height > 0) + { + float px = Renderer::getScreenWidth() / (float)width; + float py = Renderer::getScreenHeight() / (float)height; + + auto top = doc["top"].GetInt(); + auto left = doc["left"].GetInt(); + auto bottom = doc["bottom"].GetInt(); + auto right = doc["right"].GetInt(); + + mViewport = Renderer::Rect(left * px, top * py, (width - right) * px, (height - bottom) * py); + } + } + } + + fclose(fp); + } + } + + mDecoration = new ImageComponent(mWindow, true); + mDecoration->setImage(sets[setId].imageUrl); + mDecoration->setOrigin(0.5f, 0.5f); + mDecoration->setPosition(Renderer::getScreenWidth() / 2.0f, (float)Renderer::getScreenHeight() / 2.0f); + mDecoration->setMaxSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + } + } + if (!Settings::getInstance()->getBool("SlideshowScreenSaverGameName")) return; - /* - if (Utils::FileSystem::exists(game->getMarqueePath())) + if (Settings::getInstance()->getBool("ScreenSaverMarquee") && Utils::FileSystem::exists(game->getMarqueePath())) { mMarquee = new ImageComponent(mWindow, true); mMarquee->setImage(game->getMarqueePath()); mMarquee->setOrigin(0.5f, 0.5f); - mMarquee->setPosition(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() * 0.78f); - mMarquee->setMaxSize((float)Renderer::getScreenWidth() * 0.40f, (float)Renderer::getScreenHeight() * 0.20f); + mMarquee->setPosition(mViewport.x / 2.0f + mViewport.w * 0.50f, mViewport.y / 2.0f + mViewport.h * 0.18f); + mMarquee->setMaxSize((float)mViewport.w * 0.40f, (float)mViewport.h * 0.22f); } - */ + auto ph = ThemeData::getMenuTheme()->Text.font->getPath(); - auto sz = Renderer::getScreenHeight() / 16.f; + auto sz = mViewport.h / 16.f; auto font = Font::get(sz, ph); - int h = Renderer::getScreenHeight() / 4.0f; + int h = mViewport.h / 4.0f; int fh = font->getLetterHeight(); mLabelGame = new TextComponent(mWindow); - mLabelGame->setPosition(0, Renderer::getScreenHeight() - h - fh / 2); - mLabelGame->setSize(Renderer::getScreenWidth(), h - fh / 2); + mLabelGame->setPosition(mViewport.x / 2.0f, mViewport.y / 2.0f + mViewport.h - h - fh / 2); + mLabelGame->setSize(mViewport.w, h - fh / 2); mLabelGame->setHorizontalAlignment(ALIGN_CENTER); mLabelGame->setVerticalAlignment(ALIGN_CENTER); mLabelGame->setColor(0xFFFFFFFF); @@ -634,8 +618,8 @@ void ImageScreenSaver::setGame(FileData* game) mLabelGame->setText(game->getName()); mLabelSystem = new TextComponent(mWindow); - mLabelSystem->setPosition(0, Renderer::getScreenHeight() - h + fh / 2); - mLabelSystem->setSize(Renderer::getScreenWidth(), h + fh / 2); + mLabelSystem->setPosition(mViewport.x / 2.0f, mViewport.y / 2.0f + mViewport.h - h + fh / 2); + mLabelSystem->setSize(mViewport.w, h + fh / 2); mLabelSystem->setHorizontalAlignment(ALIGN_CENTER); mLabelSystem->setVerticalAlignment(ALIGN_CENTER); mLabelSystem->setColor(0xD0D0D0FF); @@ -643,4 +627,216 @@ void ImageScreenSaver::setGame(FileData* game) mLabelSystem->setGlowSize(2); mLabelSystem->setFont(ph, sz * 0.66); mLabelSystem->setText(game->getSystem()->getFullName()); +} + +void GameScreenSaverBase::render(const Transform4x4f& transform) +{ + if (mMarquee) + { + mMarquee->setOpacity(mOpacity); + mMarquee->render(transform); + } + else if (mLabelGame) + { + mLabelGame->setOpacity(mOpacity); + mLabelGame->render(transform); + } + + if (mLabelSystem) + { + mLabelSystem->setOpacity(mOpacity); + mLabelSystem->render(transform); + } + + if (mDecoration) + { + mDecoration->setOpacity(mOpacity); + mDecoration->render(transform); + } +} + +void GameScreenSaverBase::setOpacity(unsigned char opacity) +{ + mOpacity = opacity; +} + + +// ------------------------------------------------------------------------------------------------------------------------ +// IMAGE SCREEN SAVER CLASS +// ------------------------------------------------------------------------------------------------------------------------ + +ImageScreenSaver::ImageScreenSaver(Window* window) : GameScreenSaverBase(window) +{ + mImage = nullptr; +} + +ImageScreenSaver::~ImageScreenSaver() +{ + if (mImage != nullptr) + delete mImage; +} + +void ImageScreenSaver::setImage(const std::string path) +{ + if (mImage == nullptr) + { + mImage = new ImageComponent(mWindow, true); + mImage->setOrigin(0.5f, 0.5f); + mImage->setPosition((mViewport.x + mViewport.w) / 2.0f, (mViewport.y + mViewport.h) / 2.0f); + + if (Settings::getInstance()->getBool("SlideshowScreenSaverStretch") || mDecoration) + mImage->setMinSize((float)mViewport.w, (float)mViewport.h); + else + mImage->setMaxSize((float)mViewport.w, (float)mViewport.h); + } + + mImage->setImage(path); +} + +bool ImageScreenSaver::hasImage() +{ + return mImage != nullptr && mImage->hasImage(); +} + +void ImageScreenSaver::render(const Transform4x4f& transform) +{ + if (mImage) + { + mImage->setOpacity(mOpacity); + + Renderer::pushClipRect(Vector2i(mViewport.x, mViewport.y), Vector2i(mViewport.w, mViewport.h)); + mImage->render(transform); + Renderer::popClipRect(); + } + + GameScreenSaverBase::render(transform); +} + + +// ------------------------------------------------------------------------------------------------------------------------ +// VIDEO SCREEN SAVER CLASS +// ------------------------------------------------------------------------------------------------------------------------ + +VideoScreenSaver::VideoScreenSaver(Window* window) : GameScreenSaverBase(window) +{ + mVideo = nullptr; + mTime = 0; + mFade = 1.0; +} + +VideoScreenSaver::~VideoScreenSaver() +{ + if (mVideo != nullptr) + delete mVideo; +} + +void VideoScreenSaver::setVideo(const std::string path) +{ + if (mVideo == nullptr) + { +#ifdef _RPI_ + // Create the correct type of video component + if (Settings::getInstance()->getBool("ScreenSaverOmxPlayer")) + mVideo = new VideoPlayerComponent(mWindow, getTitlePath()); + else +#else + mVideo = new VideoVlcComponent(mWindow); +#endif + + mVideo->topWindow(true); + mVideo->setOrigin(0.5f, 0.5f); + + mVideo->setPosition((mViewport.x + mViewport.w) / 2.0f, (mViewport.y + mViewport.h) / 2.0f); + + if (Settings::getInstance()->getBool("StretchVideoOnScreenSaver") || mDecoration) + mVideo->setMinSize((float)mViewport.w, (float)mViewport.h); + else + mVideo->setMaxSize((float)mViewport.w, (float)mViewport.h); + + mVideo->setVideo(path); + mVideo->setScreensaverMode(true); + mVideo->onShow(); + } + + mFade = 1.0; + mTime = 0; + mVideo->setVideo(path); +} + +#define SUBTITLE_DURATION 4000 +#define SUBTITLE_FADE 150 + +void VideoScreenSaver::render(const Transform4x4f& transform) +{ + if (mVideo) + { + mVideo->setOpacity(mOpacity); + + Renderer::pushClipRect(Vector2i(mViewport.x, mViewport.y), Vector2i(mViewport.w, mViewport.h)); + mVideo->render(transform); + Renderer::popClipRect(); + } + +#ifdef _RPI_ + if (Settings::getInstance()->getBool("ScreenSaverOmxPlayer")) + return; +#endif + + if (Settings::getInstance()->getString("ScreenSaverGameInfo") == "never") + return; + + if (mMarquee && mFade != 0) + { + mMarquee->setOpacity(mOpacity * mFade); + mMarquee->render(transform); + } + else if (mLabelGame && mFade != 0) + { + mLabelGame->setOpacity(mOpacity * mFade); + mLabelGame->render(transform); + } + + if (mLabelSystem && mFade != 0) + { + mLabelSystem->setOpacity(mOpacity * mFade); + mLabelSystem->render(transform); + } + + if (mDecoration) + { + mDecoration->setOpacity(mOpacity); + mDecoration->render(transform); + } +} + +void VideoScreenSaver::update(int deltaTime) +{ + GameScreenSaverBase::update(deltaTime); + + if (mVideo) + { + if (Settings::getInstance()->getString("ScreenSaverGameInfo") == "start & end") + { + int duration = SUBTITLE_DURATION; + int end = Settings::getInstance()->getInt("ScreenSaverSwapVideoTimeout") - duration; + + if (mTime >= duration - SUBTITLE_FADE && mTime < duration) + { + mFade -= (float)deltaTime / SUBTITLE_FADE; + if (mFade < 0) + mFade = 0; + } + else if (mTime >= end - SUBTITLE_FADE && mTime < end) + { + mFade += (float)deltaTime / SUBTITLE_FADE; + if (mFade > 1) + mFade = 1; + } + else if (mTime > duration && mTime < end - SUBTITLE_FADE) + mFade = 0; + } + + mTime += deltaTime; + mVideo->update(deltaTime); + } } \ No newline at end of file diff --git a/es-app/src/SystemScreenSaver.h b/es-app/src/SystemScreenSaver.h index f0089a5473..4a669e3c42 100644 --- a/es-app/src/SystemScreenSaver.h +++ b/es-app/src/SystemScreenSaver.h @@ -4,32 +4,65 @@ #include "Window.h" #include "GuiComponent.h" +#include "renderers/Renderer.h" class ImageComponent; class Sound; class VideoComponent; class TextComponent; -class ImageScreenSaver : GuiComponent +class GameScreenSaverBase : public GuiComponent +{ +public: + GameScreenSaverBase(Window* window); + ~GameScreenSaverBase(); + + virtual void setGame(FileData* mCurrentGame); + + void render(const Transform4x4f& transform) override; + + void setOpacity(unsigned char opacity) override; + +protected: + ImageComponent* mMarquee; + TextComponent* mLabelGame; + TextComponent* mLabelSystem; + + ImageComponent* mDecoration; + + Renderer::Rect mViewport; +}; + +class ImageScreenSaver : public GameScreenSaverBase { public: ImageScreenSaver(Window* window); ~ImageScreenSaver(); - void setGame(FileData* mCurrentGame); void setImage(const std::string path); bool hasImage(); + void render(const Transform4x4f& transform) override; + +private: + ImageComponent* mImage; +}; + +class VideoScreenSaver : public GameScreenSaverBase +{ +public: + VideoScreenSaver(Window* window); + ~VideoScreenSaver(); + + void setVideo(const std::string path); void render(const Transform4x4f& transform) override; void update(int deltaTime) override; - void setOpacity(unsigned char opacity) override; - private: - ImageComponent* mImage; - ImageComponent* mMarquee; - TextComponent* mLabelGame; - TextComponent* mLabelSystem; + VideoComponent* mVideo; + + int mTime; + float mFade; }; // Screensaver implementation for main window @@ -69,11 +102,13 @@ class SystemScreenSaver : public Window::ScreenSaver private: bool mVideosCounted; - unsigned long mVideoCount; - VideoComponent* mVideoScreensaver; + unsigned long mVideoCount; bool mImagesCounted; unsigned long mImageCount; + //VideoComponent* mVideoScreensaver; + std::shared_ptr mVideoScreensaver; + std::shared_ptr mFadingImageScreensaver; std::shared_ptr mImageScreensaver; diff --git a/es-app/src/components/ScraperSearchComponent.cpp b/es-app/src/components/ScraperSearchComponent.cpp index 362a0208f6..b8bc53ed0f 100644 --- a/es-app/src/components/ScraperSearchComponent.cpp +++ b/es-app/src/components/ScraperSearchComponent.cpp @@ -403,17 +403,28 @@ void ScraperSearchComponent::update(int deltaTime) 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) + + if (status == ASYNC_DONE && results.size() == 0 && mSearchType == NEVER_AUTO_ACCEPT && + mLastSearch.nameOverride.empty() && Settings::getInstance()->getString("Scraper") == "ScreenScraper") { - onSearchDone(results); - }else if(status == ASYNC_ERROR) + // ScreenScraper in UI mode -> jeuInfo has no result, try with jeuRecherche + mLastSearch.nameOverride = mLastSearch.game->getName(); + mSearchHandle = startScraperSearch(mLastSearch); + } + else { - onSearchError(statusString); + // 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); + } } } diff --git a/es-app/src/guis/GuiMenu.h b/es-app/src/guis/GuiMenu.h index 956d10330f..04b9cf797f 100644 --- a/es-app/src/guis/GuiMenu.h +++ b/es-app/src/guis/GuiMenu.h @@ -36,6 +36,7 @@ class GuiMenu : public GuiComponent MenuComponent mMenu; TextComponent mVersion; + }; #endif // ES_APP_GUIS_GUI_MENU_H diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index b497207cd8..fbff4c16a8 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -325,7 +325,7 @@ void GuiMetaDataEd::save() void GuiMetaDataEd::fetch() { - mScraperParams.nameOverride = mScraperParams.game->getName(); +// mScraperParams.nameOverride = mScraperParams.game->getName(); GuiGameScraper* scr = new GuiGameScraper(mWindow, mScraperParams, std::bind(&GuiMetaDataEd::fetchDone, this, std::placeholders::_1)); mWindow->pushGui(scr); diff --git a/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp b/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp index 700a7a9751..142043a599 100644 --- a/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp +++ b/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp @@ -24,9 +24,19 @@ GuiSlideshowScreensaverOptions::GuiSlideshowScreensaverOptions(Window* window, c // SHOW GAME NAME auto ss_controls = std::make_shared(mWindow); ss_controls->setState(Settings::getInstance()->getBool("SlideshowScreenSaverGameName")); - addWithLabel(row, _("SHOW GAME NAME"), ss_controls); + addWithLabel(row, _("SHOW GAME INFO"), ss_controls); addSaveFunc([ss_controls] { Settings::getInstance()->setBool("SlideshowScreenSaverGameName", ss_controls->getState()); }); + auto marquee_screensaver = std::make_shared(mWindow); + marquee_screensaver->setState(Settings::getInstance()->getBool("ScreenSaverMarquee")); + addWithLabel(row, _("USE MARQUEE AS GAME INFO"), marquee_screensaver); + addSaveFunc([marquee_screensaver] { Settings::getInstance()->setBool("ScreenSaverMarquee", marquee_screensaver->getState()); }); + + auto decoration_screensaver = std::make_shared(mWindow); + decoration_screensaver->setState(Settings::getInstance()->getBool("ScreenSaverDecoration")); + addWithLabel(row, _("USE RANDOM DECORATION"), decoration_screensaver); + addSaveFunc([decoration_screensaver] { Settings::getInstance()->setBool("ScreenSaverDecoration", decoration_screensaver->getState()); }); + // stretch auto sss_stretch = std::make_shared(mWindow); sss_stretch->setState(Settings::getInstance()->getBool("SlideshowScreenSaverStretch")); diff --git a/es-app/src/guis/GuiVideoScreensaverOptions.cpp b/es-app/src/guis/GuiVideoScreensaverOptions.cpp index 3eb54a64a3..b3c2ff4474 100644 --- a/es-app/src/guis/GuiVideoScreensaverOptions.cpp +++ b/es-app/src/guis/GuiVideoScreensaverOptions.cpp @@ -18,16 +18,22 @@ GuiVideoScreensaverOptions::GuiVideoScreensaverOptions(Window* window, const cha PowerSaver::updateTimeouts(); }); - auto stretch_screensaver = std::make_shared(mWindow); - stretch_screensaver->setState(Settings::getInstance()->getBool("StretchVideoOnScreenSaver")); - addWithLabel("STRETCH VIDEO ON SCREENSAVER", stretch_screensaver); - addSaveFunc([stretch_screensaver] { Settings::getInstance()->setBool("StretchVideoOnScreenSaver", stretch_screensaver->getState()); }); #ifdef _RPI_ auto ss_omx = std::make_shared(mWindow); ss_omx->setState(Settings::getInstance()->getBool("ScreenSaverOmxPlayer")); addWithLabel("USE OMX PLAYER FOR SCREENSAVER", ss_omx); addSaveFunc([ss_omx, this] { Settings::getInstance()->setBool("ScreenSaverOmxPlayer", ss_omx->getState()); }); + + ss_omx->setOnChangedCallback([this, ss_omx, window]() + { + if (Settings::getInstance()->setBool("ScreenSaverOmxPlayer", ss_omx->getState())) + { + Window* pw = mWindow; + delete this; + pw->pushGui(new GuiVideoScreensaverOptions(pw, _("VIDEO SCREENSAVER").c_str())); + } + }); #endif // Render Video Game Name as subtitles @@ -41,49 +47,30 @@ GuiVideoScreensaverOptions::GuiVideoScreensaverOptions(Window* window, const cha addWithLabel("SHOW GAME INFO ON SCREENSAVER", ss_info); addSaveFunc([ss_info, this] { Settings::getInstance()->setString("ScreenSaverGameInfo", ss_info->getSelected()); }); -#ifdef _RPI_ - ComponentListRow row; - // Set subtitle position - auto ss_omx_subs_align = std::make_shared< OptionListComponent >(mWindow, "GAME INFO ALIGNMENT", false); - std::vector align_mode; - align_mode.push_back("left"); - align_mode.push_back("center"); - for(auto it = align_mode.cbegin(); it != align_mode.cend(); it++) - ss_omx_subs_align->add(*it, *it, Settings::getInstance()->getString("SubtitleAlignment") == *it); - addWithLabel("GAME INFO ALIGNMENT", ss_omx_subs_align); - addSaveFunc([ss_omx_subs_align, this] { Settings::getInstance()->setString("SubtitleAlignment", ss_omx_subs_align->getSelected()); }); + bool advancedOptions = true; - // Set font size - auto ss_omx_font_size = std::make_shared(mWindow, 1.f, 64.f, 1.f, "h"); - ss_omx_font_size->setValue((float)(Settings::getInstance()->getInt("SubtitleSize"))); - addWithLabel("GAME INFO FONT SIZE", ss_omx_font_size); - addSaveFunc([ss_omx_font_size] { - int subSize = (int)Math::round(ss_omx_font_size->getValue()); - Settings::getInstance()->setInt("SubtitleSize", subSize); - }); +//#ifdef _RPI_ + advancedOptions = !Settings::getInstance()->getBool("ScreenSaverOmxPlayer"); +//#endif - // Define subtitle font - auto ss_omx_font_file = std::make_shared(mWindow, "", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); - addEditableTextComponent(row, "PATH TO FONT FILE", ss_omx_font_file, Settings::getInstance()->getString("SubtitleFont")); - addSaveFunc([ss_omx_font_file] { - Settings::getInstance()->setString("SubtitleFont", ss_omx_font_file->getValue()); - }); + if (advancedOptions) + { + auto marquee_screensaver = std::make_shared(mWindow); + marquee_screensaver->setState(Settings::getInstance()->getBool("ScreenSaverMarquee")); + addWithLabel(_("USE MARQUEE AS GAME INFO"), marquee_screensaver); + addSaveFunc([marquee_screensaver] { Settings::getInstance()->setBool("ScreenSaverMarquee", marquee_screensaver->getState()); }); - // Define subtitle italic font - auto ss_omx_italic_font_file = std::make_shared(mWindow, "", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); - addEditableTextComponent(row, "PATH TO ITALIC FONT FILE", ss_omx_italic_font_file, Settings::getInstance()->getString("SubtitleItalicFont")); - addSaveFunc([ss_omx_italic_font_file] { - Settings::getInstance()->setString("SubtitleItalicFont", ss_omx_italic_font_file->getValue()); - }); -#endif - -#ifndef _RPI_ - auto captions_compatibility = std::make_shared(mWindow); - captions_compatibility->setState(Settings::getInstance()->getBool("CaptionsCompatibility")); - addWithLabel("USE COMPATIBLE LOW RESOLUTION FOR CAPTIONS", captions_compatibility); - addSaveFunc([captions_compatibility] { Settings::getInstance()->setBool("CaptionsCompatibility", captions_compatibility->getState()); }); -#endif + auto decoration_screensaver = std::make_shared(mWindow); + decoration_screensaver->setState(Settings::getInstance()->getBool("ScreenSaverDecoration")); + addWithLabel(_("USE RANDOM DECORATION"), decoration_screensaver); + addSaveFunc([decoration_screensaver] { Settings::getInstance()->setBool("ScreenSaverDecoration", decoration_screensaver->getState()); }); + } + + auto stretch_screensaver = std::make_shared(mWindow); + stretch_screensaver->setState(Settings::getInstance()->getBool("StretchVideoOnScreenSaver")); + addWithLabel(_("STRETCH VIDEO ON SCREENSAVER"), stretch_screensaver); + addSaveFunc([stretch_screensaver] { Settings::getInstance()->setBool("StretchVideoOnScreenSaver", stretch_screensaver->getState()); }); } GuiVideoScreensaverOptions::~GuiVideoScreensaverOptions() diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index 803a85d6ca..b4f2cada47 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -23,7 +23,7 @@ const std::map screenscraper_platformid_map{ { AMSTRAD_CPC, 65 }, { APPLE_II, 86 }, { ARCADE, 75 }, - { ATARI_800, 26 }, // Use ATARI_2600 as an alias for atari 800 + { ATARI_800, 43 }, { ATARI_2600, 26 }, { ATARI_5200, 40 }, { ATARI_7800, 41 }, diff --git a/es-core/src/components/SwitchComponent.cpp b/es-core/src/components/SwitchComponent.cpp index f661013706..ce1fe892d6 100644 --- a/es-core/src/components/SwitchComponent.cpp +++ b/es-core/src/components/SwitchComponent.cpp @@ -77,6 +77,9 @@ void SwitchComponent::onStateChanged() { auto theme = ThemeData::getMenuTheme(); mImage.setImage(mState ? theme->Icons.on : theme->Icons.off); + + if (mOnChangedCallback != nullptr) + mOnChangedCallback(); } std::vector SwitchComponent::getHelpPrompts() diff --git a/es-core/src/components/SwitchComponent.h b/es-core/src/components/SwitchComponent.h index 6cc981fb99..d77525e06f 100644 --- a/es-core/src/components/SwitchComponent.h +++ b/es-core/src/components/SwitchComponent.h @@ -25,11 +25,17 @@ class SwitchComponent : public GuiComponent void setColor(unsigned int color); + inline void setOnChangedCallback(const std::function& callback) { + mOnChangedCallback = callback; + } + private: void onStateChanged(); ImageComponent mImage; bool mState; + + std::function mOnChangedCallback; // batocera }; #endif // ES_CORE_COMPONENTS_SWITCH_COMPONENT_H diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 49b161c903..76faa1cb08 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -421,28 +421,21 @@ void VideoVlcComponent::startVideo() // Make sure we found a valid video track if ((mVideoWidth > 0) && (mVideoHeight > 0)) { -#ifndef _RPI_ - if (mScreensaverMode) + if (Settings::getInstance()->getBool("OptimizeVideo")) { - if (!Settings::getInstance()->getBool("CaptionsCompatibility")) { - - Vector2f resizeScale((Renderer::getScreenWidth() / (float)mVideoWidth), (Renderer::getScreenHeight() / (float)mVideoHeight)); - - if (resizeScale.x() < resizeScale.y()) - { - mVideoWidth = (unsigned int)(mVideoWidth * resizeScale.x()); - mVideoHeight = (unsigned int)(mVideoHeight * resizeScale.x()); - } - else { - mVideoWidth = (unsigned int)(mVideoWidth * resizeScale.y()); - mVideoHeight = (unsigned int)(mVideoHeight * resizeScale.y()); - } - } - } + // Avoid videos bigger than resolution + Vector2f maxSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + +#ifdef _RPI_ + // Temporary -> RPI -> Try to limit videos to 400x300 for performance benchmark + if (!Renderer::isSmallScreen()) + maxSize = Vector2f(400, 300); #endif - if (Settings::getInstance()->getBool("OptimizeVideo") && !mTargetSize.empty()) - { + if (!mTargetSize.empty() && (mTargetSize.x() < maxSize.x() || mTargetSize.y() < maxSize.y())) + maxSize = mTargetSize; + + // If video is bigger than display, ask VLC for a smaller image auto sz = ImageIO::adjustPictureSize(Vector2i(mVideoWidth, mVideoHeight), Vector2i(mTargetSize.x(), mTargetSize.y()), mTargetIsMin); if (sz.x() < mVideoWidth || sz.y() < mVideoHeight) From 9c0f4a4f2b88fc4fb1b5d3974aa651526cf47a73 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 9 Oct 2019 11:25:41 +0200 Subject: [PATCH 161/336] Marge --- es-app/src/SystemScreenSaver.cpp | 3 ++- es-app/src/guis/GuiGeneralScreensaverOptions.cpp | 2 ++ es-core/src/Window.cpp | 2 +- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/es-app/src/SystemScreenSaver.cpp b/es-app/src/SystemScreenSaver.cpp index 084773763b..576b82929c 100644 --- a/es-app/src/SystemScreenSaver.cpp +++ b/es-app/src/SystemScreenSaver.cpp @@ -533,6 +533,7 @@ void GameScreenSaverBase::setGame(FileData* game) mViewport = Renderer::Rect(0, 0, Renderer::getScreenWidth(), Renderer::getScreenHeight()); + /* #ifdef _RPI_ if (!Settings::getInstance()->getBool("ScreenSaverOmxPlayer")) #endif @@ -586,7 +587,7 @@ void GameScreenSaverBase::setGame(FileData* game) mDecoration->setMaxSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); } } - + */ if (!Settings::getInstance()->getBool("SlideshowScreenSaverGameName")) return; diff --git a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp index 38ddc2fa99..d42eca1134 100644 --- a/es-app/src/guis/GuiGeneralScreensaverOptions.cpp +++ b/es-app/src/guis/GuiGeneralScreensaverOptions.cpp @@ -10,6 +10,8 @@ GuiGeneralScreensaverOptions::GuiGeneralScreensaverOptions(Window* window, std::string title) : GuiScreensaverOptions(window, title) { + auto theme = ThemeData::getMenuTheme(); + // screensaver time auto screensaver_time = std::make_shared(mWindow, 0.f, 120.0f, 1.f, "m"); screensaver_time->setValue((float)(Settings::getInstance()->getInt("ScreenSaverTime") / (1000 * 60))); diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 76eac42249..a9042e8049 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -117,7 +117,7 @@ void Window::reactivateGui() peekGui()->updateHelpPrompts(); } -void Window::deinit() +void Window::deinit(bool deinitRenderer) { // Hide all GUI elements on uninitialisation - this disable for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) From c218b7f794b6d4f89e814189a91f4c62adca04ac Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 9 Oct 2019 11:46:39 +0200 Subject: [PATCH 162/336] SystemView logos : Bugfix --- es-app/src/views/SystemView.cpp | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 9ee31a440e..37c9157d11 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -87,8 +87,7 @@ void SystemView::populate() { // Remove dynamic flags for png & jpg files : themes can contain oversized images that can't be unloaded by the TextureResource manager ImageComponent* logo = new ImageComponent(mWindow, false, Utils::String::toLower(Utils::FileSystem::getExtension(path)) != ".svg"); - logo->setMaxSize(carouselLogoSize() * mCarousel.logoScale); - + logo->setMaxSize(carouselLogoSize() * mCarousel.logoScale); logo->applyTheme(theme, "system", "logo", ThemeFlags::COLOR | ThemeFlags::ALIGNMENT | ThemeFlags::VISIBLE); // ThemeFlags::PATH | // Process here to be enable to set max picture size @@ -97,9 +96,9 @@ void SystemView::populate() { auto path = elem->get("path"); if (Utils::FileSystem::exists(path)) - logo->setImage(path, (elem->has("tile") && elem->get("tile")), MaxSizeInfo(mCarousel.logoSize * mCarousel.logoScale)); + logo->setImage(path, (elem->has("tile") && elem->get("tile")), MaxSizeInfo(carouselLogoSize() * mCarousel.logoScale)); } - + logo->setRotateByTargetSize(true); e.data.logo = std::shared_ptr(logo); e.data.logoIsImage = true; From e056f69f8205d628db84e551719e522f9c3765d7 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 9 Oct 2019 17:28:07 +0200 Subject: [PATCH 163/336] Emulator/Core : allow changing default configuration. + Changed on/off images + Better French locale --- es-app/src/SystemData.cpp | 1 + es-app/src/SystemData.h | 15 ++ es-app/src/guis/GuiMenu.cpp | 222 ++++++++++++++++-- es-app/src/guis/GuiMenu.h | 3 + es-app/src/guis/GuiMetaDataEd.cpp | 2 +- .../guis/GuiSlideshowScreensaverOptions.cpp | 16 +- .../src/guis/GuiVideoScreensaverOptions.cpp | 13 +- es-core/src/components/OptionListComponent.h | 13 +- resources/locale/fr/emulationstation2.po | 194 ++++++++++++++- resources/off.svg | 60 ++++- resources/on.svg | 55 ++++- 11 files changed, 551 insertions(+), 43 deletions(-) diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index c0c3bc1cd1..d0dc5b0cb9 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -343,6 +343,7 @@ SystemData* SystemData::loadSystem(pugi::xml_node system) //create the system runtime environment data SystemEnvironmentData* envData = new SystemEnvironmentData; + envData->mSystemName = name; envData->mStartPath = path; envData->mSearchExtensions = extensions; envData->mLaunchCommand = cmd; diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index e0853465f9..0d1afee124 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -13,6 +13,7 @@ #include #include "FileFilterIndex.h" +#include "Settings.h" class FileData; class FolderData; @@ -28,6 +29,8 @@ struct EmulatorData struct SystemEnvironmentData { + std::string mSystemName; + std::string mStartPath; std::vector mSearchExtensions; std::string mLaunchCommand; @@ -52,6 +55,12 @@ struct SystemEnvironmentData std::string getDefaultEmulator() { + std::string currentEmul = Settings::getInstance()->getString(mSystemName + ".emulator"); + + for (auto& emulator : mEmulators) + if (currentEmul == emulator.mName) + return emulator.mName; + for (auto& emulator : mEmulators) return emulator.mName; @@ -60,10 +69,16 @@ struct SystemEnvironmentData std::string getDefaultCore(std::string emulatorName) { + std::string currentCore = Settings::getInstance()->getString(mSystemName + ".core"); + for (auto& emulator : mEmulators) { if (emulatorName == emulator.mName) { + for (auto core : emulator.mCores) + if (core == currentCore) + return core; + for (auto core : emulator.mCores) return core; } diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 18661a37e9..6fc4faef81 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -671,6 +671,15 @@ void GuiMenu::openUISettings() } } + // screensaver + ComponentListRow screensaver_row; + screensaver_row.elements.clear(); + screensaver_row.addElement(std::make_shared(mWindow, _("SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); + screensaver_row.addElement(makeArrow(mWindow), false); + screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); + s->addRow(screensaver_row); + + //#ifndef WIN32 //UI mode auto UImodeSelection = std::make_shared< OptionListComponent >(mWindow, _("UI MODE"), false); @@ -700,7 +709,7 @@ void GuiMenu::openUISettings() //#endif // LANGUAGE - + /* std::vector langues; langues.push_back("en"); @@ -745,7 +754,7 @@ void GuiMenu::openUISettings() }); } } - + */ // transition style auto transition_style = std::make_shared< OptionListComponent >(mWindow, _("TRANSITION STYLE"), false); std::vector transitions; @@ -874,14 +883,6 @@ void GuiMenu::openUISettings() }); - // screensaver - ComponentListRow screensaver_row; - screensaver_row.elements.clear(); - screensaver_row.addElement(std::make_shared(mWindow, _("SCREENSAVER SETTINGS"), theme->Text.font, theme->Text.color), true); - screensaver_row.addElement(makeArrow(mWindow), false); - screensaver_row.makeAcceptInputHandler(std::bind(&GuiMenu::openScreensaverOptions, this)); - s->addRow(screensaver_row); - s->onFinalize([s, pthis, window] { if (s->getVariable("reloadCollections")) @@ -904,15 +905,139 @@ void GuiMenu::openUISettings() mWindow->pushGui(s); } +void GuiMenu::openSystemEmulatorSettings(SystemData* system) +{ + auto theme = ThemeData::getMenuTheme(); + + GuiSettings* s = new GuiSettings(mWindow, system->getFullName().c_str()); + + auto emul_choice = std::make_shared>(mWindow, _("EMULATOR"), false); + auto core_choice = std::make_shared>(mWindow, _("CORE"), false); + + std::string currentEmul = Settings::getInstance()->getString(system->getName() + ".emulator"); + std::string defaultEmul = (system->getSystemEnvData()->mEmulators.size() == 0 ? "" : system->getSystemEnvData()->mEmulators[0].mName); + +// if (defaultEmul.length() == 0) + emul_choice->add(_("AUTO"), "", false); +// else +// emul_choice->add(_("AUTO") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); + + bool found = false; + for (auto core : system->getSystemEnvData()->mEmulators) + { + if (core.mName == currentEmul) + found = true; + + emul_choice->add(core.mName, core.mName, core.mName == currentEmul); + } + + if (!found) + emul_choice->selectFirstItem(); + + ComponentListRow row; + row.addElement(std::make_shared(mWindow, _("EMULATOR"), theme->Text.font, theme->Text.color), true); + row.addElement(emul_choice, false); + + s->addRow(row); + + emul_choice->setSelectedChangedCallback([this, system, core_choice](std::string emulatorName) + { + std::string currentCore = Settings::getInstance()->getString(system->getName() + ".core"); + std::string defaultCore; + + for (auto& emulator : system->getSystemEnvData()->mEmulators) + { + if (emulatorName == emulator.mName) + { + for (auto core : emulator.mCores) + { + defaultCore = core; + break; + } + } + } + + core_choice->clear(); + + // if (defaultCore.length() == 0) + core_choice->add(_("AUTO"), "", false); + // else + // core_choice->add(_("AUTO") + " (" + defaultCore + ")", "", false); + + std::vector cores = system->getSystemEnvData()->getCores(emulatorName); + + bool found = false; + + for (auto it = cores.begin(); it != cores.end(); it++) + { + std::string core = *it; + core_choice->add(core, core, currentCore == core); + if (currentCore == core) + found = true; + } + + if (!found) + core_choice->selectFirstItem(); + else + core_choice->invalidate(); + }); + + row.elements.clear(); + row.addElement(std::make_shared(mWindow, "CORE", theme->Text.font, theme->Text.color), true); + row.addElement(core_choice, false); + s->addRow(row); + + // force change event to load core list + emul_choice->invalidate(); + + + s->addSaveFunc([system, emul_choice, core_choice] + { + Settings::getInstance()->setString(system->getName() + ".emulator", emul_choice->getSelected()); + Settings::getInstance()->setString(system->getName() + ".core", core_choice->getSelected()); + }); + + mWindow->pushGui(s); +} + +void GuiMenu::openEmulatorSettings() +{ + GuiSettings* configuration = new GuiSettings(mWindow, _("ADVANCED").c_str()); + Window* window = mWindow; + + // For each activated system + for (auto system : SystemData::sSystemVector) + { + if (system->isCollection()) + continue; + + if (system->getSystemEnvData()->mEmulators.size() == 0) + continue; + + if (system->getSystemEnvData()->mEmulators.size() == 1 && system->getSystemEnvData()->mEmulators[0].mCores.size() <= 1) + continue; + + configuration->addEntry(system->getFullName(), true, [this, system] { openSystemEmulatorSettings(system); }); + } + + window->pushGui(configuration); +} + void GuiMenu::openOtherSettings() { + Window* window = mWindow; auto s = new GuiSettings(mWindow, _("ADVANCED SETTINGS")); - // maximum vram - auto max_vram = std::make_shared(mWindow, 40.f, 1000.f, 10.f, "Mb"); - max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); - s->addWithLabel(_("VRAM LIMIT"), max_vram); - s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); + + // Emulator settings + for (auto system : SystemData::sSystemVector) + { + if (system->isCollection() || system->getSystemEnvData()->mEmulators.size() == 0 || (system->getSystemEnvData()->mEmulators.size() == 1 && system->getSystemEnvData()->mEmulators[0].mCores.size() <= 1)) + continue; + + s->addEntry(_("EMULATOR SETTINGS"), true, [this] { openEmulatorSettings(); }); + break; + } // power saver auto power_saver = std::make_shared< OptionListComponent >(mWindow, _("POWER SAVER MODES"), false); @@ -939,6 +1064,61 @@ void GuiMenu::openOtherSettings() PowerSaver::init(); }); + + // LANGUAGE + + std::vector langues; + langues.push_back("en"); + + std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/splash.svg"); + if (xmlpath.length() > 0) + { + xmlpath = Utils::FileSystem::getParent(xmlpath) + "/locale/"; + + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(xmlpath, true); + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if (Utils::FileSystem::isDirectory(*it)) + continue; + + std::string name = *it; + + if (name.rfind("emulationstation2.po") == std::string::npos) + continue; + + name = Utils::FileSystem::getParent(name); + name = Utils::FileSystem::getFileName(name); + + if (name != "en") + langues.push_back(name); + } + + if (langues.size() > 1) + { + auto language = std::make_shared< OptionListComponent >(mWindow, _("LANGUAGE"), false); + + for (auto it = langues.cbegin(); it != langues.cend(); it++) + language->add(*it, *it, Settings::getInstance()->getString("Language") == *it); + + s->addWithLabel(_("LANGUAGE"), language); + s->addSaveFunc([language, window, s] { + + if (language->getSelected() != Settings::getInstance()->getString("Language")) + { + if (Settings::getInstance()->setString("Language", language->getSelected())) + s->setVariable("reloadGuiMenu", true); + } + }); + } + } + + + // maximum vram + auto max_vram = std::make_shared(mWindow, 40.f, 1000.f, 10.f, "Mb"); + max_vram->setValue((float)(Settings::getInstance()->getInt("MaxVRAM"))); + s->addWithLabel(_("VRAM LIMIT"), max_vram); + s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); + // gamelists auto save_gamelists = std::make_shared(mWindow); save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); @@ -1052,6 +1232,18 @@ void GuiMenu::openOtherSettings() }); s->updatePosition(); + + auto pthis = this; + + s->onFinalize([s, pthis, window] + { + if (s->getVariable("reloadGuiMenu")) + { + delete pthis; + window->pushGui(new GuiMenu(window, false)); + } + }); + mWindow->pushGui(s); } diff --git a/es-app/src/guis/GuiMenu.h b/es-app/src/guis/GuiMenu.h index 04b9cf797f..0d49b5db1a 100644 --- a/es-app/src/guis/GuiMenu.h +++ b/es-app/src/guis/GuiMenu.h @@ -7,6 +7,7 @@ #include "GuiComponent.h" class GuiSettings; +class SystemData; class GuiMenu : public GuiComponent { @@ -31,6 +32,8 @@ class GuiMenu : public GuiComponent void openSoundSettings(); void openUISettings(); void openThemeConfiguration(GuiSettings* s, std::shared_ptr> theme_set); + void openEmulatorSettings(); + void openSystemEmulatorSettings(SystemData* system); void createInputTextRow(GuiSettings *gui, std::string title, const char *settingsID, bool password); diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index fbff4c16a8..3ae3b25f5e 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -89,7 +89,7 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector else emul_choice->add(_("DEFAULT") + " (" + defaultEmul + ")", "", currentEmul.length() == 0); - for (auto core : file->getSystemEnvData()->mEmulators) + for (auto core : file->getSystem()->getSystemEnvData()->mEmulators) emul_choice->add(core.mName, core.mName, core.mName == currentEmul); row.addElement(std::make_shared(mWindow, _("EMULATOR"), theme->Text.font, theme->Text.color), true); diff --git a/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp b/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp index 142043a599..6d7dd28d37 100644 --- a/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp +++ b/es-app/src/guis/GuiSlideshowScreensaverOptions.cpp @@ -14,7 +14,7 @@ GuiSlideshowScreensaverOptions::GuiSlideshowScreensaverOptions(Window* window, c // image duration (seconds) auto sss_image_sec = std::make_shared(mWindow, 1.f, 60.f, 1.f, "s"); sss_image_sec->setValue((float)(Settings::getInstance()->getInt("ScreenSaverSwapImageTimeout") / (1000))); - addWithLabel(row, "SWAP IMAGE AFTER (SECS)", sss_image_sec); + addWithLabel(row, _("SWAP IMAGE AFTER (SECS)"), sss_image_sec); addSaveFunc([sss_image_sec] { int playNextTimeout = (int)Math::round(sss_image_sec->getValue()) * (1000); Settings::getInstance()->setInt("ScreenSaverSwapImageTimeout", playNextTimeout); @@ -31,16 +31,16 @@ GuiSlideshowScreensaverOptions::GuiSlideshowScreensaverOptions(Window* window, c marquee_screensaver->setState(Settings::getInstance()->getBool("ScreenSaverMarquee")); addWithLabel(row, _("USE MARQUEE AS GAME INFO"), marquee_screensaver); addSaveFunc([marquee_screensaver] { Settings::getInstance()->setBool("ScreenSaverMarquee", marquee_screensaver->getState()); }); - + /* auto decoration_screensaver = std::make_shared(mWindow); decoration_screensaver->setState(Settings::getInstance()->getBool("ScreenSaverDecoration")); addWithLabel(row, _("USE RANDOM DECORATION"), decoration_screensaver); addSaveFunc([decoration_screensaver] { Settings::getInstance()->setBool("ScreenSaverDecoration", decoration_screensaver->getState()); }); - + */ // stretch auto sss_stretch = std::make_shared(mWindow); sss_stretch->setState(Settings::getInstance()->getBool("SlideshowScreenSaverStretch")); - addWithLabel(row, "STRETCH IMAGES", sss_stretch); + addWithLabel(row, _("STRETCH IMAGES"), sss_stretch); addSaveFunc([sss_stretch] { Settings::getInstance()->setBool("SlideshowScreenSaverStretch", sss_stretch->getState()); }); @@ -56,12 +56,12 @@ GuiSlideshowScreensaverOptions::GuiSlideshowScreensaverOptions(Window* window, c // image source auto sss_custom_source = std::make_shared(mWindow); sss_custom_source->setState(Settings::getInstance()->getBool("SlideshowScreenSaverCustomImageSource")); - addWithLabel(row, "USE CUSTOM IMAGES", sss_custom_source); + addWithLabel(row, _("USE CUSTOM IMAGES"), sss_custom_source); addSaveFunc([sss_custom_source] { Settings::getInstance()->setBool("SlideshowScreenSaverCustomImageSource", sss_custom_source->getState()); }); // custom image directory auto sss_image_dir = std::make_shared(mWindow, "", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); - addEditableTextComponent(row, "CUSTOM IMAGE DIR", sss_image_dir, Settings::getInstance()->getString("SlideshowScreenSaverImageDir")); + addEditableTextComponent(row, _("CUSTOM IMAGE DIR"), sss_image_dir, Settings::getInstance()->getString("SlideshowScreenSaverImageDir")); addSaveFunc([sss_image_dir] { Settings::getInstance()->setString("SlideshowScreenSaverImageDir", sss_image_dir->getValue()); }); @@ -69,14 +69,14 @@ GuiSlideshowScreensaverOptions::GuiSlideshowScreensaverOptions(Window* window, c // recurse custom image directory auto sss_recurse = std::make_shared(mWindow); sss_recurse->setState(Settings::getInstance()->getBool("SlideshowScreenSaverRecurse")); - addWithLabel(row, "CUSTOM IMAGE DIR RECURSIVE", sss_recurse); + addWithLabel(row, _("CUSTOM IMAGE DIR RECURSIVE"), sss_recurse); addSaveFunc([sss_recurse] { Settings::getInstance()->setBool("SlideshowScreenSaverRecurse", sss_recurse->getState()); }); // custom image filter auto sss_image_filter = std::make_shared(mWindow, "", ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); - addEditableTextComponent(row, "CUSTOM IMAGE FILTER", sss_image_filter, Settings::getInstance()->getString("SlideshowScreenSaverImageFilter")); + addEditableTextComponent(row, _("CUSTOM IMAGE FILTER"), sss_image_filter, Settings::getInstance()->getString("SlideshowScreenSaverImageFilter")); addSaveFunc([sss_image_filter] { Settings::getInstance()->setString("SlideshowScreenSaverImageFilter", sss_image_filter->getValue()); }); diff --git a/es-app/src/guis/GuiVideoScreensaverOptions.cpp b/es-app/src/guis/GuiVideoScreensaverOptions.cpp index b3c2ff4474..575265177e 100644 --- a/es-app/src/guis/GuiVideoScreensaverOptions.cpp +++ b/es-app/src/guis/GuiVideoScreensaverOptions.cpp @@ -11,7 +11,7 @@ GuiVideoScreensaverOptions::GuiVideoScreensaverOptions(Window* window, const cha // timeout to swap videos auto swap = std::make_shared(mWindow, 10.f, 1000.f, 1.f, "s"); swap->setValue((float)(Settings::getInstance()->getInt("ScreenSaverSwapVideoTimeout") / (1000))); - addWithLabel("SWAP VIDEO AFTER (SECS)", swap); + addWithLabel(_("SWAP VIDEO AFTER (SECS)"), swap); addSaveFunc([swap] { int playNextTimeout = (int)Math::round(swap->getValue()) * (1000); Settings::getInstance()->setInt("ScreenSaverSwapVideoTimeout", playNextTimeout); @@ -22,7 +22,7 @@ GuiVideoScreensaverOptions::GuiVideoScreensaverOptions(Window* window, const cha #ifdef _RPI_ auto ss_omx = std::make_shared(mWindow); ss_omx->setState(Settings::getInstance()->getBool("ScreenSaverOmxPlayer")); - addWithLabel("USE OMX PLAYER FOR SCREENSAVER", ss_omx); + addWithLabel(_("USE OMX PLAYER FOR SCREENSAVER"), ss_omx); addSaveFunc([ss_omx, this] { Settings::getInstance()->setBool("ScreenSaverOmxPlayer", ss_omx->getState()); }); ss_omx->setOnChangedCallback([this, ss_omx, window]() @@ -37,14 +37,14 @@ GuiVideoScreensaverOptions::GuiVideoScreensaverOptions(Window* window, const cha #endif // Render Video Game Name as subtitles - auto ss_info = std::make_shared< OptionListComponent >(mWindow, "SHOW GAME INFO", false); + auto ss_info = std::make_shared< OptionListComponent >(mWindow, _("SHOW GAME INFO"), false); std::vector info_type; info_type.push_back("always"); info_type.push_back("start & end"); info_type.push_back("never"); for(auto it = info_type.cbegin(); it != info_type.cend(); it++) - ss_info->add(*it, *it, Settings::getInstance()->getString("ScreenSaverGameInfo") == *it); - addWithLabel("SHOW GAME INFO ON SCREENSAVER", ss_info); + ss_info->add(_(it->c_str()), *it, Settings::getInstance()->getString("ScreenSaverGameInfo") == *it); + addWithLabel(_("SHOW GAME INFO"), ss_info); addSaveFunc([ss_info, this] { Settings::getInstance()->setString("ScreenSaverGameInfo", ss_info->getSelected()); }); @@ -60,11 +60,12 @@ GuiVideoScreensaverOptions::GuiVideoScreensaverOptions(Window* window, const cha marquee_screensaver->setState(Settings::getInstance()->getBool("ScreenSaverMarquee")); addWithLabel(_("USE MARQUEE AS GAME INFO"), marquee_screensaver); addSaveFunc([marquee_screensaver] { Settings::getInstance()->setBool("ScreenSaverMarquee", marquee_screensaver->getState()); }); - +/* auto decoration_screensaver = std::make_shared(mWindow); decoration_screensaver->setState(Settings::getInstance()->getBool("ScreenSaverDecoration")); addWithLabel(_("USE RANDOM DECORATION"), decoration_screensaver); addSaveFunc([decoration_screensaver] { Settings::getInstance()->setBool("ScreenSaverDecoration", decoration_screensaver->getState()); }); +*/ } auto stretch_screensaver = std::make_shared(mWindow); diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index 966be8a916..1c0c27bf5e 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -5,6 +5,7 @@ #include "GuiComponent.h" #include "Log.h" #include "Window.h" +#include "EsLocale.h" //Used to display a list of options. //Can select one or multiple options. @@ -375,9 +376,15 @@ class OptionListComponent : public GuiComponent if(mMultiSelect) { // display # selected - std::stringstream ss; - ss << getSelectedObjects().size() << " SELECTED"; - mText.setText(ss.str()); + + + + char strbuf[256]; + int x = getSelectedObjects().size(); + snprintf(strbuf, 256, EsLocale::nGetText("%i SELECTED", "%i SELECTED", x).c_str(), x); + mText.setText(strbuf); + + mText.setSize(0, mText.getSize().y()); setSize(mText.getSize().x() + mRightArrow.getSize().x() + 24, mText.getSize().y()); if(mParent) // hack since theres no "on child size changed" callback atm... diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index 59d3ac34ca..aca89fc6c3 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -8,6 +8,164 @@ msgstr "" "Language: fr\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" +#: +msgid "EMULATOR SETTINGS" +msgstr "PARAMÈTRES DES EMULATEURS" + +#: +msgid "LOG LEVEL" +msgstr "NIVEAU DE LOG" + +#: +msgid "PLAY THEME MUSICS" +msgstr "JOUER LES MUSIQUES DES THEMES" + +#: +msgid "DISPLAY SONG TITLES" +msgstr "AFFICHER LES NOMS DES MUSIQUES" + +#: +msgid "SHOW FAVORITES ON TOP" +msgstr "AFFICHER LES FAVORIS EN PREMIER" + +#: +msgid "PREFERED IMAGE SOURCE" +msgstr "SOURCE D'IMAGE PREFERÉE" + +#: +msgid "PREFERED THUMBNAIL SOURCE" +msgstr "SOURCE DE MINIATURE PRÉFÉRÉE" + +#: +msgid "SCRAPE MARQUEE" +msgstr "SCRAPER LES LOGOS DE JEU" + +#, c-format +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i SÉLECTIONNÉ" +msgstr[1] "%i SÉLECTIONNÉS" + +#: +msgid "VIDEO SCREENSAVER SETTINGS" +msgstr "PARAMÈTRES ÉCRAN DE VEILLE VIDÉO" + +#: +msgid "SLIDESHOW SCREENSAVER SETTINGS" +msgstr "PARAMÈTRES ÉCRAN DE VEILLE SLIDESHOW" + +#: +msgid "SCREENSAVER CONTROLS" +msgstr "CONTRÔLE DE l'ÉCONOMISEUR D'ÉCRAN" + +#: +msgid "All Games" +msgstr "Tous les jeux" + +#: +msgid "Only missing medias" +msgstr "Seulement les médias manquants" + +#: +msgid "always" +msgstr "toujours" + +#: +msgid "start & end" +msgstr "début et fin" + +#: +msgid "never" +msgstr "jamais" + +#: +msgid "SCRAPE VIDEOS" +msgstr "SCRAPER LES VIDÉOS" + +#: +msgid "RESET GAMELIST CUSTOMISATIONS" +msgstr "RÉINITIALISER LES PERSONNALISATIONS PAR SYSTÈME" + +#: +msgid "RESET" +msgstr "RÉINITIALISER" + +#: +msgid "SHOW FOLDERS" +msgstr "AFFICHER LES DOSSIERS" + +#: +msgid "ARE YOU SURE YOU WANT TO CONFIGURE INPUT?" +msgstr "VOULEZ-VOUS MODIFIER LA CONFIGURATION DES MANETTES ?" + +#: +msgid "disabled" +msgstr "désactivé" + +#: +msgid "default" +msgstr "par défaut" + +#: +msgid "warning" +msgstr "avertissement" + +#: +msgid "error" +msgstr "erreur" + +#: +msgid "SWAP IMAGE AFTER (SECS)" +msgstr "CHANGER L'IMAGE APRÈS (SECS)" + +#: +msgid "SHOW GAME NAME" +msgstr "AFFICHER LE NOM DU JEU" + +#: +msgid "STRETCH IMAGES" +msgstr "ÉTIRER LES IMAGES" + +#: +msgid "USE CUSTOM IMAGES" +msgstr "UTILISER DES IMAGES PERSONNALISÉES" + +#: +msgid "CUSTOM IMAGE DIR" +msgstr "RÉPERTOIRE D'IMAGES PERSONNALISÉES" + +#: +msgid "CUSTOM IMAGE DIR RECURSIVE" +msgstr "RECHERCHER RÉCURSIVEMENT DANS LE RÉPERTOIRE" + +#: +msgid "CUSTOM IMAGE FILTER" +msgstr "FILTRE SUR LES IMAGES" + +#: +msgid "SWAP VIDEO AFTER (SECS)" +msgstr "CHANGER LA VIDÉO APRÈS (SECS)" + +#: +msgid "USE OMX PLAYER FOR SCREENSAVER" +msgstr "UTILISER OMX POUR L'ÉCRAN DE VEILLE" + +#: +msgid "SHOW GAME INFO" +msgstr "AFFICHER LES INFORMATIONS DU JEU" + +#: +msgid "SHOW GAME INFO ON SCREENSAVER" +msgstr "AFFICHER LES INFORMATIONS DU JEU" + +#: +msgid "USE MARQUEE AS GAME INFO" +msgstr "AFFICHER LE LOGO DU JEU" + +#: +msgid "STRETCH VIDEO ON SCREENSAVER" +msgstr "ÉTIRER LA VIDÉO DE L'ÉCRAN DE VEILLE" + #: msgid "This collection is empty" msgstr "Cette collection est vide" @@ -50,11 +208,11 @@ msgstr "Terminer l'édition" #: msgid "GRID SIZE" -msgstr "DIMENSIONS DE LA GRILLE" +msgstr "TAILLE DE LA GRILLE" #: msgid "DEFAULT GRID SIZE" -msgstr "DIMENSIONS DE LA GRILLE PAR DÉFAUT" +msgstr "TAILLE DE LA GRILLE PAR DÉFAUT" #: msgid "UI MODE" @@ -583,7 +741,23 @@ msgstr "STYLE DE LA TRANSITION" #: msgid "SCREENSAVER BEHAVIOR" -msgstr "COMPORTEMENT DE L'ÉCRAN DE VEILLE" +msgstr "TYPE D'ÉCRAN DE VEILLE" + +#: +msgid "random video" +msgstr "vidéo" + +#: +msgid "slideshow" +msgstr "slideshow" + +#: +msgid "dim" +msgstr "grisé" + +#: +msgid "black" +msgstr "noir" #: msgid "SHOW FRAMERATE" @@ -619,7 +793,11 @@ msgstr "MUSIQUE DE FOND" #: msgid "ONLY PLAY SYSTEM-SPECIFIC MUSIC FOLDER" -msgstr "JOUER UNIQUEMENT LES MUSIQUES DES SYSTÈMES" +msgstr "MUSIQUES DES SYSTÈMES UNIQUEMENT" + +#: +msgid "FILTER GAMES BY TEXT" +msgstr "RECHERCHE TEXTUELLE" #: msgid "OUTPUT DEVICE" @@ -792,8 +970,8 @@ msgid "OPTIONS" msgstr "OPTIONS" #: -msgid "JUMP TO LETTER" -msgstr "ALLER À LA LETTRE" +msgid "JUMP TO..." +msgstr "ALLER À..." #: msgid "SORT GAMES BY" @@ -1588,7 +1766,7 @@ msgstr "ANIMATION DU BANDEAU" #: msgid "ENABLE FILTERS" -msgstr "FILTRES" +msgstr "ACTIVER LES FILTRES" #: msgid "THEME CONFIGURATION" @@ -1992,7 +2170,7 @@ msgstr "Liste vide" #: msgid "COMPLETE QUIT MENU" -msgstr "AFFICHER TOUTES LES COMMANDES DU MENU QUITTER" +msgstr "AFFICHER LE MENU QUITTER COMPLET" #: Retroarch ratio msgid "Auto" diff --git a/resources/off.svg b/resources/off.svg index 0f6c8a8b8e..5a293cd401 100644 --- a/resources/off.svg +++ b/resources/off.svg @@ -1 +1,59 @@ - \ No newline at end of file + + + + + + image/svg+xml + + + + + + + + + diff --git a/resources/on.svg b/resources/on.svg index 01702df8fe..726d7bf46e 100644 --- a/resources/on.svg +++ b/resources/on.svg @@ -1 +1,54 @@ - \ No newline at end of file + + + + + + image/svg+xml + + + + + + + + From 97f444e288bd71dc8c83879b34bb8ba062f1ef02 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 9 Oct 2019 19:53:54 +0200 Subject: [PATCH 164/336] Image upscaling is blurry since Renderers refactorings --- es-core/src/renderers/Renderer_GL21.cpp | 2 +- es-core/src/renderers/Renderer_GLES10.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/es-core/src/renderers/Renderer_GL21.cpp b/es-core/src/renderers/Renderer_GL21.cpp index 5cf1cca5c1..b7a6f0922b 100644 --- a/es-core/src/renderers/Renderer_GL21.cpp +++ b/es-core/src/renderers/Renderer_GL21.cpp @@ -105,7 +105,7 @@ namespace Renderer glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, _repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, _linear ? GL_LINEAR : GL_NEAREST); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, _linear ? GL_LINEAR : GL_NEAREST); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glPixelStorei(GL_PACK_ALIGNMENT, 1); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); diff --git a/es-core/src/renderers/Renderer_GLES10.cpp b/es-core/src/renderers/Renderer_GLES10.cpp index 9a02683d26..ab381a353a 100644 --- a/es-core/src/renderers/Renderer_GLES10.cpp +++ b/es-core/src/renderers/Renderer_GLES10.cpp @@ -105,7 +105,7 @@ namespace Renderer glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, _repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, _linear ? GL_LINEAR : GL_NEAREST); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, _linear ? GL_LINEAR : GL_NEAREST); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); glPixelStorei(GL_PACK_ALIGNMENT, 1); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); From 1d947c637cf6d51a99d1440c8d52d63165e27f15 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 9 Oct 2019 20:27:54 +0200 Subject: [PATCH 165/336] Detailed Gamelist : disable async for game image -> Makes fade --- es-app/src/views/gamelist/DetailedGameListView.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 9caca5cc93..39f3479e28 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -101,7 +101,7 @@ void DetailedGameListView::createImage() const float padding = 0.01f; // Image - mImage = new ImageComponent(mWindow); + mImage = new ImageComponent(mWindow, true); 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); From 558fdaa205a297f2426d111a6abdb069ef58075c Mon Sep 17 00:00:00 2001 From: trngaje Date: Fri, 11 Oct 2019 20:40:57 +0900 Subject: [PATCH 166/336] fix build errors in rpi --- es-app/src/FileData.h | 4 ++++ es-app/src/guis/GuiMenu.cpp | 5 +++++ es-app/src/views/gamelist/DetailedGameListView.cpp | 2 +- es-app/src/views/gamelist/GridGameListView.cpp | 2 +- es-core/src/components/ImageGridComponent.h | 3 +++ es-core/src/renderers/Renderer_GLES10.cpp | 2 +- es-core/src/resources/TextureDataManager.h | 3 +++ 7 files changed, 18 insertions(+), 3 deletions(-) diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 1eef06fdbd..7d0eb9fded 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -152,7 +152,11 @@ class FolderData : public FileData void createChildrenByFilenameMap(std::unordered_map& map); private: +#ifdef _RPI_ + std::vector getFlatGameList(bool displayedOnly, SystemData* system) const; +#else std::vector FolderData::getFlatGameList(bool displayedOnly, SystemData* system) const; +#endif std::vector mChildren; }; diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 6fc4faef81..50d1608d1f 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -1369,7 +1369,12 @@ std::string getBuildTime() iss_time >> hour >> min >> sec; char buffer[100]; +#ifdef _RPI_ + sprintf(buffer, "%4d%.2d%.2d%.2d%.2d%.2d\n", year, month, day, hour, min, sec); +#else sprintf_s(buffer, "%4d%.2d%.2d%.2d%.2d%.2d\n", year, month, day, hour, min, sec); +#endif + return buffer; } diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 39f3479e28..446c544826 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -120,7 +120,7 @@ void DetailedGameListView::createVideo() // Create the correct type of video window #ifdef _RPI_ if (Settings::getInstance()->getBool("VideoOmxPlayer")) - mVideo = new VideoPlayerComponent(window, ""); + mVideo = new VideoPlayerComponent(mWindow, ""); else #endif mVideo = new VideoVlcComponent(mWindow, ""); diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index ed57f66ce8..468d823c1a 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -111,7 +111,7 @@ void GridGameListView::createVideo() // Create the correct type of video window #ifdef _RPI_ if (Settings::getInstance()->getBool("VideoOmxPlayer")) - mVideo = new VideoPlayerComponent(window, ""); + mVideo = new VideoPlayerComponent(mWindow, ""); else #endif mVideo = new VideoVlcComponent(mWindow,""); diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 5713ea14da..cc40b96cfa 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -8,6 +8,9 @@ #include "GridTileComponent.h" #include "animations/LambdaAnimation.h" #include "Settings.h" +#ifdef _RPI_ +#include "Sound.h" +#endif #define EXTRAITEMS 2 diff --git a/es-core/src/renderers/Renderer_GLES10.cpp b/es-core/src/renderers/Renderer_GLES10.cpp index ab381a353a..689e3c420c 100644 --- a/es-core/src/renderers/Renderer_GLES10.cpp +++ b/es-core/src/renderers/Renderer_GLES10.cpp @@ -132,7 +132,7 @@ namespace Renderer glTexImage2D(GL_TEXTURE_2D, 0, type, _width, _height, 0, type, GL_UNSIGNED_BYTE, _data); } else - glTexSubImage2D(GL_TEXTURE_2D, 0, _x, _y, _width, _height, convertTextureType(_type), GL_UNSIGNED_BYTE, _data);* + glTexSubImage2D(GL_TEXTURE_2D, 0, _x, _y, _width, _height, convertTextureType(_type), GL_UNSIGNED_BYTE, _data); bindTexture(0); diff --git a/es-core/src/resources/TextureDataManager.h b/es-core/src/resources/TextureDataManager.h index ed54139279..69043d3c24 100644 --- a/es-core/src/resources/TextureDataManager.h +++ b/es-core/src/resources/TextureDataManager.h @@ -9,6 +9,9 @@ #include #include #include +#ifdef _RPI_ +#include +#endif class TextureData; class TextureResource; From 90696da3a587ef9033f69c31a84996105bbb8918 Mon Sep 17 00:00:00 2001 From: trngaje Date: Sat, 12 Oct 2019 06:45:26 +0900 Subject: [PATCH 167/336] fix build errors --- es-app/src/FileData.h | 4 ---- es-app/src/guis/GuiMenu.cpp | 5 +---- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 7d0eb9fded..4d1c06b99e 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -152,11 +152,7 @@ class FolderData : public FileData void createChildrenByFilenameMap(std::unordered_map& map); private: -#ifdef _RPI_ std::vector getFlatGameList(bool displayedOnly, SystemData* system) const; -#else - std::vector FolderData::getFlatGameList(bool displayedOnly, SystemData* system) const; -#endif std::vector mChildren; }; diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 50d1608d1f..d9a56b2632 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -1369,11 +1369,8 @@ std::string getBuildTime() iss_time >> hour >> min >> sec; char buffer[100]; -#ifdef _RPI_ + sprintf(buffer, "%4d%.2d%.2d%.2d%.2d%.2d\n", year, month, day, hour, min, sec); -#else - sprintf_s(buffer, "%4d%.2d%.2d%.2d%.2d%.2d\n", year, month, day, hour, min, sec); -#endif return buffer; } From 71034c37904138491e5fb1428d42c9f833984640 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 15 Oct 2019 10:28:48 +0200 Subject: [PATCH 168/336] Theming : Support for "lang" attribute ( manage multiple languages in the theme ) --- es-core/src/ThemeData.cpp | 85 +++++++++++++++++++++++++++++++-------- 1 file changed, 69 insertions(+), 16 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 4aa0ff40de..83e28799f8 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -335,6 +335,18 @@ ThemeData::ThemeData() mSystemview = Settings::getInstance()->getString("ThemeSystemView"); mGamelistview = Settings::getInstance()->getString("ThemeGamelistView"); + std::string language = EsLocale::getLanguage(); + if (language.empty()) + language = "en"; + else + { + auto shortNameDivider = language.find("_"); + if (shortNameDivider != std::string::npos) + language = Utils::String::toLower(language.substr(0, shortNameDivider)); + } + + mLanguage = Utils::String::toLower(language); + mVersion = 0; } @@ -480,6 +492,9 @@ void ThemeData::parseVariables(const pugi::xml_node& root) if(!variables) return; + if (!parseRegion(variables) || !parseLanguage(variables)) + return; + for(pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) { std::string key = it->name(); @@ -724,35 +739,60 @@ void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view, bool over continue; } - if (parseRegion(node)) + if (!parseRegion(node) || !parseLanguage(node)) + continue; + + + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) { - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - while (off != std::string::npos || prevOff != std::string::npos) - { - std::string elemKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); - parseElement(node, elemTypeIt->second, - view.elements.insert(std::pair(elemKey, ThemeElement())).first->second, overwriteElements); + parseElement(node, elemTypeIt->second, + view.elements.insert(std::pair(elemKey, ThemeElement())).first->second, overwriteElements); - if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) - view.orderedKeys.push_back(elemKey); - } + if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) + view.orderedKeys.push_back(elemKey); } } } +bool ThemeData::parseLanguage(const pugi::xml_node& node) +{ + if (!node.attribute("lang")) + return true; + + const std::string nameAttr = Utils::String::toLower(node.attribute("lang").as_string()); + if (nameAttr.empty() || nameAttr == "default") + return true; + + const char* delim = " \t\r\n,"; + + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + while (off != std::string::npos || prevOff != std::string::npos) + { + std::string elemKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); + if (elemKey == mLanguage) + return true; + } + + return false; +} + bool ThemeData::parseRegion(const pugi::xml_node& node) { if (!node.attribute("region")) return true; const std::string nameAttr = node.attribute("region").as_string(); - if (!nameAttr.empty()) { bool add = true; @@ -797,6 +837,9 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map Date: Tue, 15 Oct 2019 10:29:02 +0200 Subject: [PATCH 169/336] Theme Language --- es-core/src/ThemeData.h | 2 ++ 1 file changed, 2 insertions(+) diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 5ad50bf6a4..fb97c27e0a 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -280,6 +280,7 @@ class ThemeData void parseView(const pugi::xml_node& viewNode, ThemeView& view, bool overwriteElements = true); void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element, bool overwrite = true); bool parseRegion(const pugi::xml_node& node); + bool parseLanguage(const pugi::xml_node& node); bool parseSubset(const pugi::xml_node& node); bool isFirstSubset(const pugi::xml_node& node); @@ -295,6 +296,7 @@ class ThemeData std::string mMenu; std::string mSystemview; std::string mGamelistview; + std::string mLanguage; std::string mSystemThemeFolder; From e5ceae1a864b1df1da61b43c7db1627aa1267c22 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 15 Oct 2019 10:40:08 +0200 Subject: [PATCH 170/336] Update themes --- es-app/src/views/SystemView.cpp | 2 +- es-core/src/ThemeData.cpp | 727 +++++++++++++++----------------- es-core/src/ThemeData.h | 95 +++-- 3 files changed, 388 insertions(+), 436 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 37c9157d11..d4212a0167 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -77,7 +77,7 @@ void SystemView::populate() // make logo const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); - if (logoElem) + if (logoElem && logoElem->has("path")) { std::string path = logoElem->get("path"); std::string defaultPath = logoElem->has("default") ? logoElem->get("default") : ""; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 83e28799f8..9d805535fc 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -3,21 +3,16 @@ #include "components/ImageComponent.h" #include "components/TextComponent.h" #include "components/NinePatchComponent.h" +#include "components/VideoVlcComponent.h" #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" #include "Log.h" #include "platform.h" #include "Settings.h" #include -#include "utils/StringUtil.h" - -#ifdef _RPI_ -#include "components/VideoPlayerComponent.h" -#endif -#include "components/VideoVlcComponent.h" - -std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" },{ "grid" },{ "video" }, { "menu" } }; -std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" } }; +std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" }, { "grid" }, { "video" }, { "menu" }, { "screen" } }; +std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" }, { "visible" } }; std::map> ThemeData::sElementMap { { "image", { @@ -37,7 +32,7 @@ std::map> The { "visible", BOOLEAN }, { "reflexion", NORMALIZED_PAIR }, { "reflexionOnFrame", BOOLEAN }, - { "horizontalAlignment", STRING }, + { "horizontalAlignment", STRING }, { "verticalAlignment", STRING }, { "flipX", BOOLEAN }, { "flipY", BOOLEAN }, @@ -46,19 +41,15 @@ std::map> The { "pos", NORMALIZED_PAIR }, { "size", NORMALIZED_PAIR }, { "margin", NORMALIZED_PAIR }, - { "padding", NORMALIZED_RECT }, - { "autoLayout", NORMALIZED_PAIR }, { "autoLayoutSelectedZoom", FLOAT }, - { "animateSelection", BOOLEAN }, - { "imageSource", STRING }, // image, thumbnail, marquee { "zIndex", FLOAT }, { "gameImage", PATH }, { "folderImage", PATH }, - { "showVideoAtDelay", FLOAT }, + { "showVideoAtDelay", FLOAT }, { "scrollDirection", STRING }, { "scrollSound", PATH }, { "centerSelection", BOOLEAN }, @@ -68,11 +59,11 @@ std::map> The { "padding", NORMALIZED_PAIR }, { "imageColor", COLOR }, { "backgroundImage", PATH }, - { "backgroundCornerSize", NORMALIZED_PAIR }, + { "backgroundCornerSize", NORMALIZED_PAIR }, { "backgroundColor", COLOR }, { "backgroundCenterColor", COLOR }, - { "backgroundEdgeColor", COLOR }, - { "selectionMode", STRING }, + { "backgroundEdgeColor", COLOR }, + { "selectionMode", STRING }, { "reflexion", NORMALIZED_PAIR }, { "imageSizeMode", STRING } } }, { "text", { @@ -87,6 +78,7 @@ std::map> The { "fontSize", FLOAT }, { "color", COLOR }, { "alignment", STRING }, + { "verticalAlignment", STRING }, { "forceUppercase", BOOLEAN }, { "lineSpacing", FLOAT }, { "value", STRING }, @@ -95,9 +87,7 @@ std::map> The { "glowColor", COLOR }, { "glowSize", FLOAT }, { "glowOffset", NORMALIZED_PAIR }, - { "padding", NORMALIZED_RECT }, - { "visible", BOOLEAN }, { "zIndex", FLOAT } } }, { "textlist", { @@ -133,12 +123,10 @@ std::map> The { "size", NORMALIZED_PAIR }, { "path", PATH }, { "visible", BOOLEAN }, - { "color", COLOR }, - { "cornerSize", NORMALIZED_PAIR }, + { "cornerSize", NORMALIZED_PAIR }, { "centerColor", COLOR }, { "edgeColor", COLOR }, - { "zIndex", FLOAT } } }, { "datetime", { { "pos", NORMALIZED_PAIR }, @@ -171,6 +159,17 @@ std::map> The { "zIndex", FLOAT } } }, { "sound", { { "path", PATH } } }, + { "controllerActivity", { + { "pos", NORMALIZED_PAIR }, + { "size", NORMALIZED_PAIR }, + { "itemSpacing", FLOAT }, + { "horizontalAlignment", STRING }, + { "imagePath", PATH }, + { "color", COLOR }, + { "activityColor", COLOR }, + { "hotkeyColor", COLOR }, + { "visible", BOOLEAN }, + { "zIndex", FLOAT } } }, { "helpsystem", { { "pos", NORMALIZED_PAIR }, { "origin", NORMALIZED_PAIR }, @@ -202,7 +201,8 @@ std::map> The { "delay", FLOAT }, { "effect", STRING }, { "visible", BOOLEAN }, - { "zIndex", FLOAT }, + { "zIndex", FLOAT }, + { "snapshotSource", STRING }, // image, thumbnail, marquee { "showSnapshotNoVideo", BOOLEAN }, { "showSnapshotDelay", BOOLEAN } } }, { "carousel", { @@ -220,9 +220,8 @@ std::map> The { "logoPos", NORMALIZED_PAIR }, { "logoAlignment", STRING }, { "maxLogoCount", FLOAT }, - { "systemInfoDelay", FLOAT }, + { "systemInfoDelay", FLOAT }, { "zIndex", FLOAT } } }, - { "menuText", { { "fontPath", PATH }, { "fontSize", FLOAT }, @@ -232,18 +231,17 @@ std::map> The { "selectorGradientType", STRING }, { "selectedColor", COLOR }, { "color", COLOR } } }, - { "menuTextSmall", { { "fontPath", PATH }, { "fontSize", FLOAT }, { "color", COLOR } } }, - { "menuBackground", { { "path", PATH }, { "fadePath", PATH }, - { "color", COLOR } } }, - - { "menuIcons",{ + { "color", COLOR }, + { "centerColor", COLOR }, + { "cornerSize", NORMALIZED_PAIR } } }, + { "menuIcons", { { "iconSystem", PATH }, { "iconUpdates", PATH }, { "iconControllers", PATH }, @@ -254,25 +252,21 @@ std::map> The { "iconScraper", PATH }, { "iconAdvanced", PATH }, { "iconQuit", PATH } } }, - { "menuSwitch",{ { "pathOn", PATH }, { "pathOff", PATH } } }, - { "menuTextEdit",{ { "active", PATH }, { "inactive", PATH } } }, - { "menuSlider",{ { "path", PATH } } }, - { "menuButton",{ { "path", PATH }, { "filledPath", PATH } } }, }; std::shared_ptr ThemeData::mMenuTheme; -ThemeData* ThemeData::mCurrentTheme = nullptr; +ThemeData* ThemeData::mDefaultTheme = nullptr; #define MINIMUM_THEME_FORMAT_VERSION 3 #define CURRENT_THEME_FORMAT_VERSION 6 @@ -286,10 +280,10 @@ unsigned int getHexColor(const char* str) //throw error << "Empty color"; LOG(LogWarning) << "Empty color"; return 0; - } + } size_t len = strlen(str); - if (len != 6 && len != 8) + if(len != 6 && len != 8) { //throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; LOG(LogWarning) << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)"; @@ -333,7 +327,10 @@ ThemeData::ThemeData() mIconset = Settings::getInstance()->getString("ThemeIconSet"); mMenu = Settings::getInstance()->getString("ThemeMenu"); mSystemview = Settings::getInstance()->getString("ThemeSystemView"); - mGamelistview = Settings::getInstance()->getString("ThemeGamelistView"); + mGamelistview = Settings::getInstance()->getString("ThemeGamelistView"); + mRegion = Settings::getInstance()->getString("ThemeRegionName"); + if (mRegion.empty()) + mRegion = "eu"; std::string language = EsLocale::getLanguage(); if (language.empty()) @@ -346,11 +343,10 @@ ThemeData::ThemeData() } mLanguage = Utils::String::toLower(language); - mVersion = 0; } -void ThemeData::loadFile(std::string system, std::map sysDataMap, const std::string& path) +void ThemeData::loadFile(const std::string system, std::map sysDataMap, const std::string& path) { mPaths.push_back(path); @@ -359,19 +355,18 @@ void ThemeData::loadFile(std::string system, std::map if(!Utils::FileSystem::exists(path)) throw error << "File does not exist!"; - - mSubsets.clear(); + mVersion = 0; mViews.clear(); mSystemThemeFolder = system; - mVariables.clear(); + mVariables.clear(); mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend()); + mVariables["lang"] = mLanguage; pugi::xml_document doc; - pugi::xml_parse_result res = doc.load_file(path.c_str()); - + pugi::xml_parse_result res = doc.load_file(path.c_str()); if(!res) throw error << "XML parsing error: \n " << res.description(); @@ -387,20 +382,17 @@ void ThemeData::loadFile(std::string system, std::map if(mVersion < MINIMUM_THEME_FORMAT_VERSION) throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << "."; - if (root.attribute("defaultView")) - mDefaultView = root.attribute("defaultView").as_string(); - parseVariables(root); parseTheme(root); - + mMenuTheme = nullptr; - mCurrentTheme = this; + mDefaultTheme = this; } const std::shared_ptr& ThemeData::getMenuTheme() -{ - if (mMenuTheme == nullptr && mCurrentTheme != nullptr) - mMenuTheme = std::shared_ptr(new ThemeMenu(mCurrentTheme)); +{ + if (mMenuTheme == nullptr && mDefaultTheme != nullptr) + mMenuTheme = std::shared_ptr(new ThemeMenu(mDefaultTheme)); else if (mMenuTheme == nullptr) { auto emptyData = ThemeData(); @@ -464,22 +456,115 @@ bool ThemeData::parseSubset(const pugi::xml_node& node) mSubsets.push_back(Subset(subsetAttr, nameAttr)); } - if (subsetAttr == "colorset" && (nameAttr == mColorset || (mColorset.empty() && isFirstSubset(node)))) - return true; + + if (subsetAttr == "colorset") + { + if (nameAttr == mColorset || (mColorset.empty() && isFirstSubset(node))) + return true; + } + else if (subsetAttr == "iconset") + { + if (nameAttr == mIconset || (mIconset.empty() && isFirstSubset(node))) + return true; + } + else if (subsetAttr == "menu") + { + if (nameAttr == mMenu || (mMenu.empty() && isFirstSubset(node))) + return true; + } + else if (subsetAttr == "systemview") + { + if (nameAttr == mSystemview || (mSystemview.empty() && isFirstSubset(node))) + return true; + } + else if (subsetAttr == "gamelistview") + { + if (nameAttr == mGamelistview || (mGamelistview.empty() && isFirstSubset(node))) + return true; + } + else + { + std::string setID = Settings::getInstance()->getString("subset."+ subsetAttr); + if (nameAttr == setID || (setID.empty() && isFirstSubset(node))) + return true; + } - if (subsetAttr == "iconset" && (nameAttr == mIconset || (mIconset.empty() && isFirstSubset(node)))) - return true; + return false; +} - if (subsetAttr == "menu" && (nameAttr == mMenu || (mMenu.empty() && isFirstSubset(node)))) - return true; +void ThemeData::parseInclude(const pugi::xml_node& node) +{ + if (!parseFilterAttributes(node)) + return; - if (subsetAttr == "systemview" && (nameAttr == mSystemview || (mSystemview.empty() && isFirstSubset(node)))) - return true; + if (!parseSubset(node)) + return; - if (subsetAttr == "gamelistview" && (nameAttr == mGamelistview || (mGamelistview.empty() && isFirstSubset(node)))) - return true; + std::string relPath = resolvePlaceholders(node.text().as_string()); + std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); + path = resolveSystemVariable(mSystemThemeFolder, path); - return false; + if (!ResourceManager::getInstance()->fileExists(path)) + { + LOG(LogWarning) << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; + return; + } + + mPaths.push_back(path); + + pugi::xml_document includeDoc; + pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); + if (!result) + { + LOG(LogWarning) << "Error parsing file: \n " << result.description() << " from included file \"" << relPath << "\":\n "; + return; + } + + pugi::xml_node theme = includeDoc.child("theme"); + if (!theme) + { + LOG(LogWarning) << "Missing tag!" << " from included file \"" << relPath << "\":\n "; + return; + } + + parseVariables(theme); + parseTheme(theme); + + mPaths.pop_back(); +} + +void ThemeData::parseFeature(const pugi::xml_node& node) +{ + if (!node.attribute("supported")) + { + LOG(LogWarning) << "Feature missing \"supported\" attribute!"; + return; + } + + if (!parseFilterAttributes(node)) + return; + + const std::string supportedAttr = node.attribute("supported").as_string(); + + if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) + parseViews(node); +} + +void ThemeData::parseVariable(const pugi::xml_node& node) +{ + std::string key = node.name(); + if (key.empty()) + return; + + if (!parseFilterAttributes(node)) + return; + + std::string val = node.text().as_string(); + if (val.empty()) + return; + + mVariables.erase(key); + mVariables.insert(std::pair(key, val)); } void ThemeData::parseVariables(const pugi::xml_node& root) @@ -488,65 +573,117 @@ void ThemeData::parseVariables(const pugi::xml_node& root) error.setFiles(mPaths); pugi::xml_node variables = root.child("variables"); - if(!variables) return; - - if (!parseRegion(variables) || !parseLanguage(variables)) + + if (!parseFilterAttributes(variables)) return; for(pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) + parseVariable(*it); +} + +void ThemeData::parseViewElement(const pugi::xml_node& node) +{ + if (!node.attribute("name")) + { + LOG(LogWarning) << "View missing \"name\" attribute!"; + return; + } + + const char* delim = " \t\r\n,"; + const std::string nameAttr = node.attribute("name").as_string(); + size_t prevOff = nameAttr.find_first_not_of(delim, 0); + size_t off = nameAttr.find_first_of(delim, prevOff); + std::string viewKey; + while (off != std::string::npos || prevOff != std::string::npos) { - std::string key = it->name(); - std::string val = it->text().as_string(); + viewKey = nameAttr.substr(prevOff, off - prevOff); + prevOff = nameAttr.find_first_not_of(delim, off); + off = nameAttr.find_first_of(delim, prevOff); - if (!val.empty()) + if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) { - mVariables.erase(key); - mVariables.insert(std::pair(key, val)); + ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; + parseView(node, view); + + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + { + if (it->second.isCustomView && it->second.baseType == viewKey) + { + ThemeView& customView = (ThemeView&)it->second; + parseView(node, customView); + } + } } } } -void ThemeData::parseViews(const pugi::xml_node& root) +bool ThemeData::parseFilterAttributes(const pugi::xml_node& node) { - ThemeException error; - error.setFiles(mPaths); + if (!parseRegion(node)) + return false; - // parse views - for(pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) + if (!parseLanguage(node)) + return false; + + if (node.attribute("tinyScreen")) { - if (!node.attribute("name")) - continue; + const std::string tinyScreenAttr = node.attribute("tinyScreen").as_string(); - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - std::string viewKey; + if (!Renderer::isSmallScreen() && tinyScreenAttr == "true") + return false; + else if (Renderer::isSmallScreen() && tinyScreenAttr == "false") + return false; + } - while(off != std::string::npos || prevOff != std::string::npos) - { - viewKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - - if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) - { - ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; - parseView(node, view); + if (node.attribute("ifHelpPrompts")) + { + const std::string helpVisibleAttr = node.attribute("ifHelpPrompts").as_string(); + bool help = Settings::getInstance()->getBool("ShowHelpPrompts"); - for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) - { - if (it->second.isCustomView && it->second.baseType == viewKey) - { - ThemeView& customView = (ThemeView&)it->second; - parseView(node, customView); - } - } - } - } + if (!help && helpVisibleAttr == "true") + return false; + else if (help && helpVisibleAttr == "false") + return false; } + + return true; +} + +void ThemeData::parseTheme(const pugi::xml_node& root) +{ + if (root.attribute("defaultView")) + mDefaultView = root.attribute("defaultView").as_string(); + + for (pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + { + if (!parseFilterAttributes(node)) + continue; + + std::string name = node.name(); + + if (name == "include") + parseInclude(node); + else if (name == "view") + parseViewElement(node); + else if (name == "customView") + parseCustomView(node, root); + } + + // Unfortunately, recalbox does not do things in order, features have to be loaded after + for (pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) + parseFeature(node); +} + +void ThemeData::parseViews(const pugi::xml_node& root) +{ + ThemeException error; + error.setFiles(mPaths); + + // parse views + for (pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) + parseViewElement(node); } void ThemeData::parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& view, std::string baseClass) @@ -581,7 +718,7 @@ void ThemeData::parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& if (found) return; - + // base class is a customview ? for (pugi::xml_node nodec = root.child("customView"); nodec; nodec = nodec.next_sibling("customView")) { @@ -601,83 +738,13 @@ void ThemeData::parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& } } -std::vector ThemeData::getViewsOfTheme() -{ - std::vector ret; - for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) - { - if (it->first == "menu" || it->first == "system") - continue; - - ret.push_back(it->first); - } - - return ret; -} - -void ThemeData::parseViewElement(const pugi::xml_node& node) -{ - if (!node.attribute("name")) - { - LOG(LogWarning) << "View missing \"name\" attribute!"; - return; - } - - const char* delim = " \t\r\n,"; - const std::string nameAttr = node.attribute("name").as_string(); - size_t prevOff = nameAttr.find_first_not_of(delim, 0); - size_t off = nameAttr.find_first_of(delim, prevOff); - std::string viewKey; - while (off != std::string::npos || prevOff != std::string::npos) - { - viewKey = nameAttr.substr(prevOff, off - prevOff); - prevOff = nameAttr.find_first_not_of(delim, off); - off = nameAttr.find_first_of(delim, prevOff); - - if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) - { - ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; - parseView(node, view); - - for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) - { - if (it->second.isCustomView && it->second.baseType == viewKey) - { - ThemeView& customView = (ThemeView&)it->second; - parseView(node, customView); - } - } - } - } -} - void ThemeData::parseCustomView(const pugi::xml_node& node, const pugi::xml_node& root) { if (!node.attribute("name")) return; - if (node.attribute("tinyScreen")) - { - const std::string tinyScreenAttr = node.attribute("tinyScreen").as_string(); - - if (!Renderer::isSmallScreen() && tinyScreenAttr == "true") - return; - - if (Renderer::isSmallScreen() && tinyScreenAttr == "false") - return; - } - - if (node.attribute("ifHelpPrompts")) - { - const std::string helpVisibleAttr = node.attribute("ifHelpPrompts").as_string(); - bool help = Settings::getInstance()->getBool("ShowHelpPrompts"); - - if (!help && helpVisibleAttr == "true") - return; - - if (help && helpVisibleAttr == "false") - return; - } + if (!parseFilterAttributes(node)) + return; std::string viewKey = node.attribute("name").as_string(); @@ -699,50 +766,27 @@ void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view, bool over ThemeException error; error.setFiles(mPaths); - if (root.attribute("tinyScreen")) - { - const std::string tinyScreenAttr = root.attribute("tinyScreen").as_string(); - - if (!Renderer::isSmallScreen() && tinyScreenAttr == "true") - return; - - if (Renderer::isSmallScreen() && tinyScreenAttr == "false") - return; - } - - if (root.attribute("ifHelpPrompts")) - { - const std::string helpVisibleAttr = root.attribute("ifHelpPrompts").as_string(); - bool help = Settings::getInstance()->getBool("ShowHelpPrompts"); - - if (!help && helpVisibleAttr == "true") - return; - - if (help && helpVisibleAttr == "false") - return; - } + if (!parseFilterAttributes(root)) + return; - for (pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + for(pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) { - if (!node.attribute("name")) - { - //throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; + if(!node.attribute("name")) + { LOG(LogWarning) << "Element of type \"" << node.name() << "\" missing \"name\" attribute!"; continue; - } + } auto elemTypeIt = sElementMap.find(node.name()); - if (elemTypeIt == sElementMap.cend()) - { - //throw error << "Unknown element of type \"" << node.name() << "\"!"; + if(elemTypeIt == sElementMap.cend()) + { LOG(LogWarning) << "Unknown element of type \"" << node.name() << "\"!"; continue; - } + } - if (!parseRegion(node) || !parseLanguage(node)) + if (!parseFilterAttributes(node)) continue; - - + const char* delim = " \t\r\n,"; const std::string nameAttr = node.attribute("name").as_string(); size_t prevOff = nameAttr.find_first_not_of(delim, 0); @@ -758,7 +802,7 @@ void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view, bool over if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) == view.orderedKeys.cend()) view.orderedKeys.push_back(elemKey); - } + } } } @@ -792,27 +836,23 @@ bool ThemeData::parseRegion(const pugi::xml_node& node) if (!node.attribute("region")) return true; - const std::string nameAttr = node.attribute("region").as_string(); - if (!nameAttr.empty()) - { - bool add = true; + const std::string nameAttr = Utils::String::toLower(node.attribute("region").as_string()); + if (nameAttr.empty() || nameAttr == "default") + return true; - for (auto sb : mSubsets) { - if (sb.subset == "region" && sb.name == nameAttr) { - add = false; break; - } - } + bool add = true; - if (add) - mSubsets.push_back(Subset("region", nameAttr)); + for (auto sb : mSubsets) { + if (sb.subset == "region" && sb.name == nameAttr) { + add = false; break; + } } - std::string regionsetting = Settings::getInstance()->getString("ThemeRegionName"); - if (regionsetting.empty()) - regionsetting = "eu"; + if (add) + mSubsets.push_back(Subset("region", nameAttr)); const char* delim = " \t\r\n,"; - + size_t prevOff = nameAttr.find_first_not_of(delim, 0); size_t off = nameAttr.find_first_of(delim, prevOff); while (off != std::string::npos || prevOff != std::string::npos) @@ -820,7 +860,7 @@ bool ThemeData::parseRegion(const pugi::xml_node& node) std::string elemKey = nameAttr.substr(prevOff, off - prevOff); prevOff = nameAttr.find_first_not_of(delim, off); off = nameAttr.find_first_of(delim, prevOff); - if (elemKey == regionsetting) + if (elemKey == mRegion) return true; } @@ -836,28 +876,27 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapsecond; - + if (!overwrite && element.properties.find(node.name()) != element.properties.cend()) continue; @@ -871,7 +910,7 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapfileExists(path)) + if(!ResourceManager::getInstance()->fileExists(path)) { std::stringstream ss; ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda ss << "could not find file \"" << node.text().get() << "\" "; if(node.text().get() != path) ss << "(which resolved to \"" << path << "\") "; - - LOG(LogWarning) << ss.str(); - - auto test = element.properties[node.name()]; - if (test.s.empty()) - element.properties[node.name()] = path; + LOG(LogWarning) << ss.str(); } else element.properties[node.name()] = path; @@ -957,17 +990,11 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map ThemeData::makeExtras(const std::shared_ptrsecond.elements.at(*it); if(elem.extra) { - GuiComponent* comp = NULL; + GuiComponent* comp = nullptr; const std::string& t = elem.type; if(t == "image") @@ -1053,19 +1086,12 @@ std::vector ThemeData::makeExtras(const std::shared_ptrgetBool("VideoOmxPlayer")) - comp = new VideoPlayerComponent(window, ""); - else -#endif*/ - comp = new VideoVlcComponent(window, ""); - } + comp = new VideoVlcComponent(window); - if (comp == NULL) + if (comp == nullptr) continue; + comp->setTag((*it).c_str()); comp->setDefaultZIndex(10); comp->applyTheme(theme, view, *it, ThemeFlags::ALL); comps.push_back(comp); @@ -1082,8 +1108,8 @@ std::map ThemeData::getThemeSets() static const size_t pathCount = 2; std::string paths[pathCount] = { - "/etc/emulationstation/themes", - Utils::FileSystem::getHomePath() + "/.emulationstation/themes" + "/etc/emulationstation/themes", + Utils::FileSystem::getHomePath() + "/.emulationstation/themes" }; for(size_t i = 0; i < pathCount; i++) @@ -1133,8 +1159,6 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) Text.font = Font::get(FONT_SIZE_MEDIUM); TextSmall.font = Font::get(FONT_SIZE_SMALL); - - auto elem = theme->getElement("menu", "menubg", "menuBackground"); if (elem) { @@ -1145,7 +1169,20 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) Background.fadePath = elem->get("fadePath"); if (elem->has("color")) + { Background.color = elem->get("color"); + Background.centerColor = Background.color; + } + + if (elem->has("centerColor")) + { + Background.centerColor = elem->get("centerColor"); + if (!elem->has("color")) + Background.color = Background.centerColor; + } + + if (elem->has("cornerSize")) + Background.cornerSize = elem->get("cornerSize"); } elem = theme->getElement("menu", "menutitle", "menuText"); @@ -1155,6 +1192,8 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) Title.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_LARGE)); if (elem->has("color")) Title.color = elem->get("color"); + if (elem->has("selectorColor")) + Title.selectorColor = elem->get("selectorColor"); } elem = theme->getElement("menu", "menufooter", "menuText"); @@ -1164,6 +1203,8 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) Footer.font = Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_SMALL)); if (elem->has("color")) Footer.color = elem->get("color"); + if (elem->has("selectorColor")) + Footer.selectorColor = elem->get("selectorColor"); } elem = theme->getElement("menu", "menutextsmall", "menuTextSmall"); @@ -1200,7 +1241,7 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) if (elem->has("selectorColorEnd")) Text.selectorGradientColor = elem->get("selectorColorEnd"); if (elem->has("selectorGradientType")) - Text.selectorGradientType = elem->get("selectorGradientType").compare("horizontal"); + Text.selectorGradientType = (elem->get("selectorGradientType").compare("horizontal")); } elem = theme->getElement("menu", "menubutton", "menuButton"); @@ -1235,147 +1276,55 @@ ThemeData::ThemeMenu::ThemeMenu(ThemeData* theme) Icons.knob = elem->get("path"); elem = theme->getElement("menu", "menuicons", "menuIcons"); - if (elem) + if (elem) { for (auto prop : elem->properties) { std::string path = prop.second.s; if (!path.empty() && ResourceManager::getInstance()->fileExists(path)) - mMenuIcons[prop.first] = path; - } + mMenuIcons[prop.first] = path; + } } } -std::vector ThemeData::getSubSet(const std::vector& subsets, const std::string& subset) -{ - std::vector ret; - - for (const auto& it : subsets) - { - if (it.subset == subset) - ret.push_back(it.name); - } - - return ret; -} +void ThemeData::setDefaultTheme(ThemeData* theme) +{ + mDefaultTheme = theme; + mMenuTheme = nullptr; +}; -void ThemeData::parseTheme(const pugi::xml_node& root) +std::vector ThemeData::getViewsOfTheme() { - if (root.attribute("defaultView")) - mDefaultView = root.attribute("defaultView").as_string(); - - for (pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + std::vector ret; + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) { - if (!parseRegion(node) || !parseLanguage(node)) + if (it->first == "menu" || it->first == "system") continue; - std::string name = node.name(); - - if (name == "include") - parseInclude(node); - else if (name == "view") - parseViewElement(node); - else if (name == "customView") - parseCustomView(node, root); - } - - // Unfortunately, recalbox does not do things in order, features have to be loaded after - for (pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) - parseFeature(node); -} - -void ThemeData::parseInclude(const pugi::xml_node& node) -{ - if (!parseSubset(node)) - return; - - if (node.attribute("tinyScreen")) - { - const std::string tinyScreenAttr = node.attribute("tinyScreen").as_string(); - - if (!Renderer::isSmallScreen() && tinyScreenAttr == "true") - return; - - if (Renderer::isSmallScreen() && tinyScreenAttr == "false") - return; - } - - if (node.attribute("ifHelpPrompts")) - { - const std::string helpVisibleAttr = node.attribute("ifHelpPrompts").as_string(); - bool help = Settings::getInstance()->getBool("ShowHelpPrompts"); - - if (!help && helpVisibleAttr == "true") - return; - - if (help && helpVisibleAttr == "false") - return; - } - - std::string relPath = resolvePlaceholders(node.text().as_string()); - std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); - path = resolveSystemVariable(mSystemThemeFolder, path); - - if (!ResourceManager::getInstance()->fileExists(path)) - { - LOG(LogWarning) << "Included file \"" << relPath << "\" not found! (resolved to \"" << path << "\")"; - return; - } - - mPaths.push_back(path); - - pugi::xml_document includeDoc; - pugi::xml_parse_result result = includeDoc.load_file(path.c_str()); - if (!result) - { - LOG(LogWarning) << "Error parsing file: \n " << result.description() << " from included file \"" << relPath << "\":\n "; - return; - } - - pugi::xml_node theme = includeDoc.child("theme"); - if (!theme) - { - LOG(LogWarning) << "Missing tag!" << " from included file \"" << relPath << "\":\n "; - return; + ret.push_back(it->first); } - parseVariables(theme); - parseTheme(theme); - - mPaths.pop_back(); + return ret; } -void ThemeData::parseFeature(const pugi::xml_node& node) +std::vector ThemeData::getSubSet(const std::vector& subsets, const std::string& subset) { - if (!node.attribute("supported")) - { - LOG(LogWarning) << "Feature missing \"supported\" attribute!"; - return; - } - - - if (!parseRegion(node) || !parseLanguage(node)) - return; + std::vector ret; - const std::string supportedAttr = node.attribute("supported").as_string(); + for (const auto& it : subsets) + if (it.subset == subset) + ret.push_back(it.name); - if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) != sSupportedFeatures.cend()) - parseViews(node); + return ret; } -void ThemeData::parseVariable(const pugi::xml_node& node) +std::vector ThemeData::getSubSetNames() { - std::string key = node.name(); - if (key.empty()) - return; - - if (!parseRegion(node) || !parseLanguage(node)) - return; + std::vector ret; - std::string val = node.text().as_string(); - if (val.empty()) - return; + for (const auto& it : mSubsets) + if (std::find(ret.cbegin(), ret.cend(), it.subset) == ret.cend()) + ret.push_back(it.subset); - mVariables.erase(key); - mVariables.insert(std::pair(key, val)); + return ret; } diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index fb97c27e0a..94f7f1f679 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -5,14 +5,13 @@ #include "math/Vector2f.h" #include "math/Vector4f.h" #include "utils/FileSystemUtil.h" -#include - #include #include #include #include #include #include +#include namespace pugi { class xml_node; } @@ -60,7 +59,7 @@ class ThemeException : public std::exception template friend ThemeException& operator<<(ThemeException& e, T msg); - + inline void setFiles(const std::deque& deque) { *this << "from theme \"" << deque.front() << "\"\n"; @@ -87,6 +86,7 @@ struct ThemeSet inline std::string getThemePath(const std::string& system) const { return path + "/" + system + "/theme.xml"; } }; + struct Subset { Subset(const std::string set, const std::string nm) @@ -99,19 +99,29 @@ struct Subset std::string name; }; -struct MenuElement { +struct MenuElement +{ unsigned int color; unsigned int selectedColor; - unsigned int selectorColor; + unsigned int selectorColor; unsigned int separatorColor; unsigned int selectorGradientColor; bool selectorGradientType; std::string path; - std::string fadePath; std::shared_ptr font; }; -struct IconElement { +struct MenuBackground +{ + unsigned int color; + unsigned int centerColor; + std::string path; + std::string fadePath; + Vector2f cornerSize; +}; + +struct IconElement +{ std::string button; std::string button_filled; std::string on; @@ -131,12 +141,12 @@ class ThemeData public: ThemeMenu(ThemeData* theme); - MenuElement Background{ 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, 0xFFFFFFFF, true, ":/frame.png", ":/scroll_gradient.png", nullptr }; - MenuElement Title{ 0x555555FF, 0x555555FF, 0x555555FF, 0xFFFFFFFF, 0x555555FF, true, "", "", nullptr }; - MenuElement Text{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0x878787FF, true, "", "", nullptr }; - MenuElement TextSmall{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0x878787FF, true, "", "", nullptr }; - MenuElement Footer{ 0xC6C6C6FF, 0xC6C6C6FF, 0xC6C6C6FF, 0xFFFFFFFF, 0xC6C6C6FF, true, "", "", nullptr }; - IconElement Icons { ":/button.png", ":/button_filled.png", ":/on.svg", ":/off.svg", ":/option_arrow.svg", ":/arrow.svg", ":/slider_knob.svg", ":/textinput_ninepatch.png", ":/textinput_ninepatch_active.png" }; + MenuBackground Background{ 0xFFFFFFFF, 0xFFFFFFFF, ":/frame.png", ":/scroll_gradient.png", Vector2f(16, 16) }; + MenuElement Title{ 0x555555FF, 0x555555FF, 0x555555FF, 0xFFFFFFFF, 0x555555FF, true, "", nullptr }; + MenuElement Text{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0x878787FF, true, "", nullptr }; + MenuElement TextSmall{ 0x777777FF, 0xFFFFFFFF, 0x878787FF, 0xC6C7C6FF, 0x878787FF, true, "", nullptr }; + MenuElement Footer{ 0xC6C6C6FF, 0xC6C6C6FF, 0xC6C6C6FF, 0xFFFFFFFF, 0xC6C6C6FF, true, "", nullptr }; + IconElement Icons{ ":/button.png", ":/button_filled.png", ":/on.svg", ":/off.svg", ":/option_arrow.svg", ":/arrow.svg", ":/slider_knob.svg", ":/textinput_ninepatch.png", ":/textinput_ninepatch_active.png" }; std::string getMenuIcon(const std::string name) { @@ -151,7 +161,6 @@ class ThemeData std::map mMenuIcons; }; - class ThemeElement { public: @@ -160,19 +169,19 @@ class ThemeData struct Property { - void operator= (const Vector4f& value) { r = value; v = Vector2f(value.x(), value.y()); } void operator= (const Vector2f& value) { v = value; } void operator= (const std::string& value) { s = value; } void operator= (const unsigned int& value) { i = value; } void operator= (const float& value) { f = value; } void operator= (const bool& value) { b = value; } + void operator= (const Vector4f& value) { r = value; v = Vector2f(value.x(), value.y()); } - Vector4f r; Vector2f v; std::string s; unsigned int i; float f; bool b; + Vector4f r; }; std::map< std::string, Property > properties; @@ -204,13 +213,12 @@ class ThemeData bool isCustomView; }; - public: ThemeData(); // throws ThemeException - void loadFile(std::string system, std::map sysDataMap, const std::string& path); + void loadFile(const std::string system, std::map sysDataMap, const std::string& path); enum ElementPropertyType { @@ -237,75 +245,70 @@ class ThemeData static std::map getThemeSets(); static std::string getThemeFromCurrentSet(const std::string& system); - - std::string getDefaultView() { return mDefaultView; }; - std::vector getViewsOfTheme(); - bool hasSubsets() { return mSubsets.size() > 0; } - std::vector getSubSets() { return mSubsets; } - static std::vector getSubSet(const std::vector& subsets, const std::string& subset); - static const std::shared_ptr& getMenuTheme(); - static void setDefaultTheme(ThemeData* theme) { mCurrentTheme = theme; }; - static ThemeData* getDefaultTheme() { return mCurrentTheme; }; + std::vector getSubSets() { return mSubsets; } + std::vector getSubSetNames(); - std::string getSystemThemeFolder() { - return mSystemThemeFolder; - } + static std::vector getSubSet(const std::vector& subsets, const std::string& subset); -private: - static void findRegion(const pugi::xml_document& doc, std::vector& sets); - static void crawlIncludes(const pugi::xml_node& root, std::vector& sets, std::deque& dequepath); + static void setDefaultTheme(ThemeData* theme); + static ThemeData* getDefaultTheme() { return mDefaultTheme; } + + std::string getSystemThemeFolder() { return mSystemThemeFolder; } + + std::vector getViewsOfTheme(); + std::string getDefaultView() { return mDefaultView; }; +private: static std::map< std::string, std::map > sElementMap; static std::vector sSupportedFeatures; static std::vector sSupportedViews; std::deque mPaths; float mVersion; - std::string mDefaultView; - + void parseTheme(const pugi::xml_node& root); - void parseFeature(const pugi::xml_node& node); - void parseInclude(const pugi::xml_node& node); + void parseFeature(const pugi::xml_node& node); + void parseInclude(const pugi::xml_node& node); void parseVariable(const pugi::xml_node& node); void parseVariables(const pugi::xml_node& root); void parseViews(const pugi::xml_node& themeRoot); - void parseCustomView(const pugi::xml_node& node, const pugi::xml_node& root); + void parseCustomView(const pugi::xml_node& node, const pugi::xml_node& root); void parseViewElement(const pugi::xml_node& node); void parseView(const pugi::xml_node& viewNode, ThemeView& view, bool overwriteElements = true); void parseElement(const pugi::xml_node& elementNode, const std::map& typeMap, ThemeElement& element, bool overwrite = true); bool parseRegion(const pugi::xml_node& node); - bool parseLanguage(const pugi::xml_node& node); bool parseSubset(const pugi::xml_node& node); bool isFirstSubset(const pugi::xml_node& node); + bool parseLanguage(const pugi::xml_node& node); + bool parseFilterAttributes(const pugi::xml_node& node); void parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& view, std::string baseClass); std::string resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path); std::string resolvePlaceholders(const char* in); - - std::map mViews; std::string mColorset; std::string mIconset; std::string mMenu; std::string mSystemview; std::string mGamelistview; + std::string mSystemThemeFolder; std::string mLanguage; + std::string mRegion; - std::string mSystemThemeFolder; + std::map mVariables; + std::map mViews; - std::map mVariables; + std::vector mSubsets; static std::shared_ptr mMenuTheme; - static ThemeData* mCurrentTheme; - - std::vector mSubsets; + static ThemeData* mDefaultTheme; }; #endif // ES_CORE_THEME_DATA_H From 614479562f0bd7219357eb06e0119ad321766247 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 15 Oct 2019 10:54:11 +0200 Subject: [PATCH 171/336] Themes : Support for user defined subsets. Support for "shaders" Menu background -> Support for custom corner size ( usefull for menu with shadows ) # Conflicts: # es-app/src/guis/GuiGameScraper.cpp # es-app/src/guis/GuiMenu.cpp # es-core/src/EsLocale.cpp # es-core/src/ThemeData.cpp # es-core/src/ThemeData.h # es-core/src/Window.cpp # es-core/src/Window.h # es-core/src/components/BusyComponent.cpp # es-core/src/components/MenuComponent.cpp # es-core/src/components/NinePatchComponent.cpp # es-core/src/guis/GuiDetectDevice.cpp # es-core/src/guis/GuiInfoPopup.cpp # es-core/src/guis/GuiInputConfig.cpp # es-core/src/guis/GuiTextEditPopup.cpp --- es-app/src/guis/GuiGameScraper.cpp | 5 +- es-app/src/guis/GuiMenu.cpp | 65 ++++++++++--------- es-app/src/guis/GuiMetaDataEd.cpp | 3 +- es-app/src/guis/GuiScraperMulti.cpp | 5 +- es-core/src/EsLocale.cpp | 7 +- es-core/src/Settings.h | 2 + es-core/src/Window.cpp | 29 +++++++-- es-core/src/Window.h | 2 + .../components/AsyncNotificationComponent.cpp | 3 +- es-core/src/components/BusyComponent.cpp | 7 +- es-core/src/components/MenuComponent.cpp | 6 +- es-core/src/components/NinePatchComponent.cpp | 6 +- es-core/src/guis/GuiDetectDevice.cpp | 5 +- es-core/src/guis/GuiInfoPopup.cpp | 2 +- es-core/src/guis/GuiInputConfig.cpp | 4 +- es-core/src/guis/GuiMsgBox.cpp | 3 +- es-core/src/guis/GuiTextEditPopup.cpp | 3 +- es-core/src/guis/GuiTextEditPopupKeyboard.cpp | 3 +- 18 files changed, 99 insertions(+), 61 deletions(-) diff --git a/es-app/src/guis/GuiGameScraper.cpp b/es-app/src/guis/GuiGameScraper.cpp index 2ba3b0e5ef..7bcaceb587 100644 --- a/es-app/src/guis/GuiGameScraper.cpp +++ b/es-app/src/guis/GuiGameScraper.cpp @@ -14,9 +14,10 @@ GuiGameScraper::GuiGameScraper(Window* window, ScraperSearchParams params, std:: mClose(false) { auto theme = ThemeData::getMenuTheme(); - mBox.setImagePath(theme->Background.path); // ":/frame.png" - mBox.setCenterColor(theme->Background.color); + mBox.setImagePath(theme->Background.path); mBox.setEdgeColor(theme->Background.color); + mBox.setCenterColor(theme->Background.centerColor); + mBox.setCornerSize(theme->Background.cornerSize); PowerSaver::pause(); addChild(&mBox); diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index d9a56b2632..0675628ca3 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -432,34 +432,36 @@ void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptr> theme_gamelistview = nullptr; if (themeGamelistViewSets.size() > 0) { - auto selectedGamelistViewSet = std::find(themeGamelistViewSets.cbegin(), themeGamelistViewSets.cend(), Settings::getInstance()->getString("ThemeGamelistView")); - if (selectedGamelistViewSet == themeGamelistViewSets.end()) - selectedGamelistViewSet = themeGamelistViewSets.begin(); + std::string settingName = "subset." + subset; - theme_gamelistview = std::make_shared >(mWindow, _("THEME GAMELISTVIEW"), false); + if (subset == "colorset") settingName = "ThemeColorSet"; + else if (subset == "iconset") settingName = "ThemeIconSet"; + else if (subset == "menu") settingName = "ThemeMenu"; + else if (subset == "systemview") settingName = "ThemeSystemView"; + else if (subset == "gamelistview") settingName = "ThemeGamelistView"; + else if (subset == "region") settingName = "ThemeRegionName"; - for (auto it = themeGamelistViewSets.begin(); it != themeGamelistViewSets.end(); it++) - theme_gamelistview->add(*it, *it, it == selectedGamelistViewSet); + auto themeColorSets = ThemeData::getSubSet(themeSubSets, subset); - if (!themeGamelistViewSets.empty()) - themeconfig->addWithLabel(_("THEME GAMELISTVIEW"), theme_gamelistview); - } - - // themeregion - std::shared_ptr> theme_region = nullptr; - if (themeRegions.size() > 0) - { - auto selectedRegion = std::find(themeRegions.cbegin(), themeRegions.cend(), Settings::getInstance()->getString("ThemeRegionName")); - if (selectedRegion == themeRegions.end()) - selectedRegion = themeRegions.begin(); + if (themeColorSets.size() > 0) + { + auto selectedColorSet = std::find(themeColorSets.cbegin(), themeColorSets.cend(), Settings::getInstance()->getString(settingName)); + if (selectedColorSet == themeColorSets.end()) + selectedColorSet = themeColorSets.begin(); + + std::shared_ptr> item = std::make_shared >(mWindow, _(("THEME "+Utils::String::toUpper(subset)).c_str()), false); + item->setTag(settingName); - theme_region = std::make_shared >(mWindow, _("THEME REGION"), false); + for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) + item->add(*it, *it, it == selectedColorSet); - for (auto it = themeRegions.begin(); it != themeRegions.end(); it++) - theme_region->add(*it, *it, it == selectedRegion); + if (!themeColorSets.empty()) + themeconfig->addWithLabel(_(("THEME " + Utils::String::toUpper(subset)).c_str()), item); - if (!themeRegions.empty()) - themeconfig->addWithLabel(_("THEME REGION"), theme_region); + options[settingName] = item; + } + else + options[settingName] = nullptr; } // gamelist_style @@ -531,15 +533,14 @@ void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptrclose(); }); - themeconfig->addSaveFunc([this, themeconfig, theme_set, theme_colorset, theme_iconset, theme_menu, theme_systemview, theme_gamelistview, theme_region, gamelist_style, mGridSize, window] + // theme_colorset, theme_iconset, theme_menu, theme_systemview, theme_gamelistview, theme_region, + themeconfig->addSaveFunc([this, themeconfig, theme_set, options, gamelist_style, mGridSize, window] { bool reloadAll = Settings::getInstance()->setString("ThemeSet", theme_set == nullptr ? "" : theme_set->getSelected()); - reloadAll |= Settings::getInstance()->setString("ThemeColorSet", theme_colorset == nullptr ? "" : theme_colorset->getSelected()); - reloadAll |= Settings::getInstance()->setString("ThemeIconSet", theme_iconset == nullptr ? "" : theme_iconset->getSelected()); - reloadAll |= Settings::getInstance()->setString("ThemeMenu", theme_menu == nullptr ? "" : theme_menu->getSelected()); - reloadAll |= Settings::getInstance()->setString("ThemeSystemView", theme_systemview == nullptr ? "" : theme_systemview->getSelected()); - reloadAll |= Settings::getInstance()->setString("ThemeGamelistView", theme_gamelistview == nullptr ? "" : theme_gamelistview->getSelected()); - reloadAll |= Settings::getInstance()->setString("ThemeRegionName", theme_region == nullptr ? "" : theme_region->getSelected()); + + for (auto option : options) + reloadAll |= Settings::getInstance()->setString(option.first, option.second == nullptr ? "" : option.second->getSelected()); + reloadAll |= Settings::getInstance()->setString("GamelistViewStyle", gamelist_style == nullptr ? "" : gamelist_style->getSelected()); if (mGridSize != nullptr) @@ -615,7 +616,11 @@ void GuiMenu::openUISettings() Settings::getInstance()->setString("ThemeGamelistView", ""); Settings::getInstance()->setString("GamelistViewStyle", ""); Settings::getInstance()->setString("DefaultGridSize", ""); - + + for(auto sm : Settings::getInstance()->getStringMap()) + if (Utils::String::startsWith(sm.first, "subset.")) + Settings::getInstance()->setString(sm.first, ""); + for (auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) (*sysIt)->setSystemViewMode("automatic", Vector2f(0, 0)); diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 3ae3b25f5e..5d77590342 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -33,8 +33,9 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector { auto theme = ThemeData::getMenuTheme(); mBackground.setImagePath(theme->Background.path); // ":/frame.png" - mBackground.setCenterColor(theme->Background.color); mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); addChild(&mBackground); addChild(&mGrid); diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 2fb0a5ab6f..1adf3ea6b8 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -16,9 +16,10 @@ GuiScraperMulti::GuiScraperMulti(Window* window, const std::queueBackground.path); // ":/frame.png" - mBackground.setCenterColor(theme->Background.color); + mBackground.setImagePath(theme->Background.path); // ":/frame.png" mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); assert(mSearchQueue.size()); diff --git a/es-core/src/EsLocale.cpp b/es-core/src/EsLocale.cpp index 3fd6f03151..4d3f09c2b7 100644 --- a/es-core/src/EsLocale.cpp +++ b/es-core/src/EsLocale.cpp @@ -187,9 +187,10 @@ void EsLocale::checkLocalisationLoaded() if (end != std::string::npos) { std::string msgstr = line.substr(start + 1, end - start - 1); - if (!msgid.empty() && !msgstr.empty()) - mItems[msgid] = msgstr; - + if (!msgid.empty() && !msgstr.empty()) + if (idx.empty() || idx == "0") + mItems[msgid] = msgstr; + if (!msgid_plural.empty() && !msgstr.empty()) { if (!idx.empty() && idx != "0") diff --git a/es-core/src/Settings.h b/es-core/src/Settings.h index 5c2d28664a..60dc0438ae 100644 --- a/es-core/src/Settings.h +++ b/es-core/src/Settings.h @@ -24,6 +24,8 @@ class Settings bool setFloat(const std::string& name, float value); bool setString(const std::string& name, const std::string& value); + std::map& getStringMap() { return mStringMap; } + private: static Settings* sInstance; diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index a9042e8049..95c98fe912 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -144,8 +144,9 @@ void Window::textInput(const char* text) void Window::input(InputConfig* config, Input input) { if (mScreenSaver) { - if(mScreenSaver->isScreenSaverActive() && Settings::getInstance()->getBool("ScreenSaverControls") && - (Settings::getInstance()->getString("ScreenSaverBehavior") == "random video")) + if (mScreenSaver->isScreenSaverActive() && Settings::getInstance()->getBool("ScreenSaverControls") && + ((Settings::getInstance()->getString("ScreenSaverBehavior") == "slideshow") || + (Settings::getInstance()->getString("ScreenSaverBehavior") == "random video"))) { if(mScreenSaver->getCurrentGame() != NULL && (config->isMappedLike("right", input) || config->isMappedTo("start", input) || config->isMappedTo("select", input))) { @@ -296,14 +297,15 @@ void Window::render() if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) startScreenSaver(); - // Always call the screensaver render function regardless of whether the screensaver is active - // or not because it may perform a fade on transition - renderScreenSaver(); - if(!mRenderScreenSaver && mInfoPopup) mInfoPopup->render(transform); renderRegisteredNotificationComponents(transform); + + + // Always call the screensaver render function regardless of whether the screensaver is active + // or not because it may perform a fade on transition + renderScreenSaver(); if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) { @@ -338,6 +340,21 @@ void Window::endRenderLoadingScreen() mSplash = NULL; mCustomSplash = ""; + if (ThemeData::getMenuTheme()->Background.shaderPath.empty()) + mImageShader = nullptr; + else + { + if (mImageShader == nullptr) + mImageShader = std::make_shared(this, true, false); + + auto theme = ThemeData::getMenuTheme()->Background; + + mImageShader->setImage(theme.shaderPath, theme.shaderTiled); + mImageShader->setColorShift(theme.shaderColor); + mImageShader->setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + + } + // Window has not way to apply Theme -> As a workaround : endRenderLoadingScreen is always called when theme changes. mBackgroundOverlay->setImage(ThemeData::getMenuTheme()->Background.fadePath); } diff --git a/es-core/src/Window.h b/es-core/src/Window.h index dee4437730..78b12b5b9d 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -119,6 +119,8 @@ class Window std::shared_ptr mSplash; std::string mCustomSplash; + std::shared_ptr mImageShader; + std::vector mGuiStack; std::vector< std::shared_ptr > mDefaultFonts; diff --git a/es-core/src/components/AsyncNotificationComponent.cpp b/es-core/src/components/AsyncNotificationComponent.cpp index d16d630898..22be9c6aa6 100644 --- a/es-core/src/components/AsyncNotificationComponent.cpp +++ b/es-core/src/components/AsyncNotificationComponent.cpp @@ -30,8 +30,9 @@ AsyncNotificationComponent::AsyncNotificationComponent(Window* window, bool acti mFrame = new NinePatchComponent(window); mFrame->setImagePath(theme->Background.path); - mFrame->setCenterColor(theme->Background.color); mFrame->setEdgeColor(theme->Background.color); + mFrame->setCenterColor(theme->Background.centerColor); + mFrame->setCornerSize(theme->Background.cornerSize); mFrame->fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); addChild(mFrame); diff --git a/es-core/src/components/BusyComponent.cpp b/es-core/src/components/BusyComponent.cpp index e7543af9b1..ad5d2de4de 100644 --- a/es-core/src/components/BusyComponent.cpp +++ b/es-core/src/components/BusyComponent.cpp @@ -17,10 +17,11 @@ BusyComponent::BusyComponent(Window* window) : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(5, 3)) { auto theme = ThemeData::getMenuTheme(); - mBackground.setImagePath(theme->Background.path); // ":/frame.png" - mBackground.setCenterColor(theme->Background.color); + mBackground.setImagePath(theme->Background.path); mBackground.setEdgeColor(theme->Background.color); - + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); + mAnimation = std::make_shared(mWindow); mAnimation->load(&BUSY_ANIMATION_DEF); mText = std::make_shared(mWindow, _("WORKING..."), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index fc83efb2e9..ee7f9928d0 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -14,10 +14,10 @@ MenuComponent::MenuComponent(Window* window, std::string title, const std::share addChild(&mBackground); addChild(&mGrid); - - mBackground.setImagePath(theme->Background.path); // ":/frame.png" - mBackground.setCenterColor(theme->Background.color); + mBackground.setImagePath(theme->Background.path); mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); // set up title mTitle = std::make_shared(mWindow); diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index 69d30ca5a2..a3749bf119 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -38,11 +38,11 @@ void NinePatchComponent::updateColors() const unsigned int edgeColor = Renderer::convertColor(mEdgeColor & 0xFFFFFF00 | (unsigned char)((mEdgeColor & 0xFF) * opacity)); const unsigned int centerColor = Renderer::convertColor(mCenterColor & 0xFFFFFF00 | (unsigned char)((mCenterColor & 0xFF) * opacity)); - for (int i = 0; i < 6 * 9; ++i) + for(int i = 0; i < 6*9; i++) mVertices[i].col = edgeColor; - for (int i = 6 * 4; i < 6; ++i) - mVertices[(6 * 4) + i].col = centerColor; + for(int i = 0; i < 6; i++) + mVertices[(4 * 6) + i].col = centerColor; } void NinePatchComponent::buildVertices() diff --git a/es-core/src/guis/GuiDetectDevice.cpp b/es-core/src/guis/GuiDetectDevice.cpp index 1e82281bcc..1112ced71a 100644 --- a/es-core/src/guis/GuiDetectDevice.cpp +++ b/es-core/src/guis/GuiDetectDevice.cpp @@ -14,9 +14,10 @@ GuiDetectDevice::GuiDetectDevice(Window* window, bool firstRun, const std::funct mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 5)) { auto theme = ThemeData::getMenuTheme(); - mBackground.setImagePath(theme->Background.path); // ":/frame.png" - mBackground.setCenterColor(theme->Background.color); + mBackground.setImagePath(theme->Background.path); mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); mGrid.setSeparatorColor(theme->Text.separatorColor); mHoldingConfig = NULL; diff --git a/es-core/src/guis/GuiInfoPopup.cpp b/es-core/src/guis/GuiInfoPopup.cpp index 855abc0a7c..4703402bb1 100644 --- a/es-core/src/guis/GuiInfoPopup.cpp +++ b/es-core/src/guis/GuiInfoPopup.cpp @@ -54,7 +54,7 @@ GuiInfoPopup::GuiInfoPopup(Window* window, std::string message, int duration) : mFrame->setImagePath(theme->Background.path); mFrame->setCenterColor(mBackColor); mFrame->setEdgeColor(mBackColor); - + mFrame->setCornerSize(theme->Background.cornerSize); mFrame->fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); addChild(mFrame); diff --git a/es-core/src/guis/GuiInputConfig.cpp b/es-core/src/guis/GuiInputConfig.cpp index 9cb9bd2038..20c2231253 100644 --- a/es-core/src/guis/GuiInputConfig.cpp +++ b/es-core/src/guis/GuiInputConfig.cpp @@ -56,8 +56,10 @@ GuiInputConfig::GuiInputConfig(Window* window, InputConfig* target, bool reconfi { auto theme = ThemeData::getMenuTheme(); mBackground.setImagePath(theme->Background.path); // ":/frame.png" - mBackground.setCenterColor(theme->Background.color); mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); + mGrid.setSeparatorColor(theme->Text.separatorColor); LOG(LogInfo) << "Configuring device " << target->getDeviceId() << " (" << target->getDeviceName() << ")."; diff --git a/es-core/src/guis/GuiMsgBox.cpp b/es-core/src/guis/GuiMsgBox.cpp index 382701f091..3f7779e1ce 100644 --- a/es-core/src/guis/GuiMsgBox.cpp +++ b/es-core/src/guis/GuiMsgBox.cpp @@ -27,8 +27,9 @@ GuiMsgBox::GuiMsgBox(Window* window, const std::string& text, { auto theme = ThemeData::getMenuTheme(); mBackground.setImagePath(theme->Background.path); - mBackground.setCenterColor(theme->Background.color); mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); float width = Renderer::getScreenWidth() * 0.6f; // max width float minWidth = Renderer::getScreenWidth() * 0.3f; // minimum width diff --git a/es-core/src/guis/GuiTextEditPopup.cpp b/es-core/src/guis/GuiTextEditPopup.cpp index c0c0b26b6c..94674bd62d 100644 --- a/es-core/src/guis/GuiTextEditPopup.cpp +++ b/es-core/src/guis/GuiTextEditPopup.cpp @@ -10,8 +10,9 @@ GuiTextEditPopup::GuiTextEditPopup(Window* window, const std::string& title, con { auto theme = ThemeData::getMenuTheme(); mBackground.setImagePath(theme->Background.path); // ":/frame.png" - mBackground.setCenterColor(theme->Background.color); mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); addChild(&mBackground); addChild(&mGrid); diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp index f301d39f4d..b05332f569 100644 --- a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp @@ -45,8 +45,9 @@ GuiTextEditPopupKeyboard::GuiTextEditPopupKeyboard(Window* window, const std::st { auto theme = ThemeData::getMenuTheme(); mBackground.setImagePath(theme->Background.path); - mBackground.setCenterColor(theme->Background.color); mBackground.setEdgeColor(theme->Background.color); + mBackground.setCenterColor(theme->Background.centerColor); + mBackground.setCornerSize(theme->Background.cornerSize); addChild(&mBackground); addChild(&mGrid); From bfe0045671158e1e4ad6797ac0cccd6be12c793f Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 15 Oct 2019 11:03:28 +0200 Subject: [PATCH 172/336] Theming View/ Screen : Add to manage fixed components that are to be statically displayed ( crt shader, clock ) Clock : Totally rewritten - Now customisable in Video component : element snapshotSource, allow defining the image snapshot source. Vlc Video : RPI only : A lot of videos are encoded in 60fps on screenscraper. Add a limit at 30fps for transfers to opengl textures to save CPU. Screenscraper : Temp Fix. We sometimes receive #screenscraperserveur# in the url instead of the server. # Conflicts: # es-app/src/SystemData.cpp # es-app/src/views/ViewController.cpp # es-app/src/views/gamelist/DetailedGameListView.cpp # es-app/src/views/gamelist/VideoGameListView.cpp # es-core/src/HelpStyle.cpp # es-core/src/HelpStyle.h # es-core/src/Window.cpp # es-core/src/Window.h # es-core/src/components/TextComponent.cpp # es-core/src/components/VideoComponent.cpp --- es-app/src/SystemData.cpp | 6 +- es-app/src/SystemScreenSaver.cpp | 152 ++++++++------- es-app/src/SystemScreenSaver.h | 9 +- es-app/src/scrapers/ScreenScraper.cpp | 16 +- es-app/src/scrapers/ScreenScraper.h | 1 + es-app/src/views/ViewController.cpp | 13 +- es-app/src/views/ViewController.h | 2 + .../views/gamelist/DetailedGameListView.cpp | 13 +- .../src/views/gamelist/VideoGameListView.cpp | 18 +- es-core/src/HelpStyle.cpp | 5 +- es-core/src/HelpStyle.h | 2 +- es-core/src/HttpReq.cpp | 6 +- es-core/src/Window.cpp | 180 +++++++++++++++--- es-core/src/Window.h | 20 +- es-core/src/components/TextComponent.cpp | 74 ++++--- es-core/src/components/TextComponent.h | 3 +- es-core/src/components/VideoComponent.cpp | 51 +++-- es-core/src/components/VideoComponent.h | 14 +- es-core/src/components/VideoVlcComponent.cpp | 27 ++- es-core/src/components/VideoVlcComponent.h | 3 + 20 files changed, 446 insertions(+), 169 deletions(-) diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index d0dc5b0cb9..de305d56dd 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -15,6 +15,7 @@ #include "utils/ThreadPool.h" #include "GuiComponent.h" #include "Window.h" +#include "views/ViewController.h" using namespace Utils; @@ -493,7 +494,10 @@ bool SystemData::loadConfig(Window* window) } if (SystemData::sSystemVector.size() > 0) - ThemeData::setDefaultTheme(SystemData::sSystemVector.at(0)->getTheme().get()); + { + auto theme = SystemData::sSystemVector.at(0)->getTheme(); + ViewController::get()->onThemeChanged(theme); + } return true; } diff --git a/es-app/src/SystemScreenSaver.cpp b/es-app/src/SystemScreenSaver.cpp index 576b82929c..240431fafa 100644 --- a/es-app/src/SystemScreenSaver.cpp +++ b/es-app/src/SystemScreenSaver.cpp @@ -66,20 +66,22 @@ bool SystemScreenSaver::isScreenSaverActive() void SystemScreenSaver::startScreenSaver() { + bool loadingNext = mLoadingNext; + stopScreenSaver(); std::string screensaver_behavior = Settings::getInstance()->getString("ScreenSaverBehavior"); - if (!mVideoScreensaver && (screensaver_behavior == "random video")) + if (screensaver_behavior == "random video") { - if (Settings::getInstance()->getBool("VideoAudio")) + if (!loadingNext && Settings::getInstance()->getBool("VideoAudio")) AudioManager::getInstance()->deinit(); + mVideoChangeTime = Settings::getInstance()->getInt("ScreenSaverSwapVideoTimeout"); + // Configure to fade out the windows, Skip Fading if Instant mode mState = PowerSaver::getMode() == PowerSaver::INSTANT ? STATE_SCREENSAVER_ACTIVE : STATE_FADE_OUT_WINDOW; - - mVideoChangeTime = Settings::getInstance()->getInt("ScreenSaverSwapVideoTimeout"); if (mState == STATE_FADE_OUT_WINDOW) { @@ -90,18 +92,19 @@ void SystemScreenSaver::startScreenSaver() mOpacity = 0.0f; // Load a random video - std::string path = ""; - pickRandomVideo(path); + std::string path = pickRandomVideo(); - int retry = 200; - while(retry > 0 && ((path.empty() || !Utils::FileSystem::exists(path)) || mCurrentGame == NULL)) + int retry = 10; + while (retry > 0 && !Utils::FileSystem::exists(path)) { retry--; - pickRandomVideo(path); + path = pickRandomVideo(); } if (!path.empty() && Utils::FileSystem::exists(path)) { + LOG(LogDebug) << "VideoScreenSaver::startScreenSaver " << path.c_str(); + mVideoScreensaver = std::make_shared(mWindow); mVideoScreensaver->setGame(mCurrentGame); mVideoScreensaver->setVideo(path); @@ -113,13 +116,13 @@ void SystemScreenSaver::startScreenSaver() } else if (screensaver_behavior == "slideshow") { + mVideoChangeTime = Settings::getInstance()->getInt("ScreenSaverSwapImageTimeout"); + // Configure to fade out the windows, Skip Fading if Instant mode mState = PowerSaver::getMode() == PowerSaver::INSTANT ? STATE_SCREENSAVER_ACTIVE : STATE_FADE_OUT_WINDOW; - mVideoChangeTime = Settings::getInstance()->getInt("ScreenSaverSwapImageTimeout"); - if (mState == STATE_FADE_OUT_WINDOW) { mState = STATE_FADE_IN_VIDEO; @@ -129,25 +132,28 @@ void SystemScreenSaver::startScreenSaver() mOpacity = 0.0f; // Load a random image - std::string path = ""; + std::string path; if (Settings::getInstance()->getBool("SlideshowScreenSaverCustomImageSource")) { - pickRandomCustomImage(path); + path = pickRandomCustomImage(); // Custom images are not tied to the game list mCurrentGame = NULL; } else - pickRandomGameListImage(path); + path = pickRandomGameListImage(); - mTimer = 0; + if (!path.empty() && Utils::FileSystem::exists(path)) + { + LOG(LogDebug) << "ImageScreenSaver::startScreenSaver " << path.c_str(); - mImageScreensaver = std::make_shared(mWindow); - mImageScreensaver->setGame(mCurrentGame); - mImageScreensaver->setImage(path); - - PowerSaver::runningScreenSaver(true); - mTimer = 0; - return; + mImageScreensaver = std::make_shared(mWindow); + mImageScreensaver->setGame(mCurrentGame); + mImageScreensaver->setImage(path); + + PowerSaver::runningScreenSaver(true); + mTimer = 0; + return; + } } // No videos. Just use a standard screensaver @@ -157,35 +163,36 @@ void SystemScreenSaver::startScreenSaver() void SystemScreenSaver::stopScreenSaver() { + bool isExitingScreenSaver = !mLoadingNext; + if (mLoadingNext) mFadingImageScreensaver = mImageScreensaver; else mFadingImageScreensaver = nullptr; - // Exiting video screen saver -> Restore sound - if (!mLoadingNext && Settings::getInstance()->getBool("VideoAudio") && mVideoScreensaver) - { - AudioManager::getInstance()->init(); - AudioManager::getInstance()->playRandomMusic(); - } - // so that we stop the background audio next time, unless we're restarting the screensaver mLoadingNext = false; mVideoScreensaver = nullptr; mImageScreensaver = nullptr; - + // we need this to loop through different videos mState = STATE_INACTIVE; PowerSaver::runningScreenSaver(false); + + // Exiting video screen saver -> Restore sound + if (isExitingScreenSaver && Settings::getInstance()->getBool("VideoAudio") && mVideoScreensaver) + { + AudioManager::getInstance()->init(); + AudioManager::getInstance()->playRandomMusic(); + } } void SystemScreenSaver::renderScreenSaver() { Transform4x4f transform = Transform4x4f::Identity(); - - std::string screensaver_behavior = Settings::getInstance()->getString("ScreenSaverBehavior"); - if (mVideoScreensaver && screensaver_behavior == "random video") + + if (mVideoScreensaver) { // Render black background Renderer::setMatrix(Transform4x4f::Identity()); @@ -200,7 +207,7 @@ void SystemScreenSaver::renderScreenSaver() mVideoScreensaver->render(transform); } } - else if (mImageScreensaver && screensaver_behavior == "slideshow") + else if (mImageScreensaver) { // Render black background Renderer::setMatrix(transform); @@ -222,16 +229,11 @@ void SystemScreenSaver::renderScreenSaver() mImageScreensaver->render(transform); } } - - // Check if we need to restart the background audio - /* - if ((mBackgroundAudio) && (Settings::getInstance()->getString("SlideshowScreenSaverBackgroundAudioFile") != "")) - if (!mBackgroundAudio->isPlaying()) - mBackgroundAudio->play(); - */ } else if (mState != STATE_INACTIVE) { + std::string screensaver_behavior = Settings::getInstance()->getString("ScreenSaverBehavior"); + Renderer::setMatrix(Transform4x4f::Identity()); unsigned char color = screensaver_behavior == "dim" ? 0x000000A0 : 0x000000FF; Renderer::drawRect(0.0f, 0.0f, Renderer::getScreenWidth(), Renderer::getScreenHeight(), color, color); @@ -250,11 +252,11 @@ unsigned long SystemScreenSaver::countGameListNodes(const char *nodeName) FolderData* rootFileData = (*it)->getRootFolder(); - FileType type = GAME; - std::vector allFiles = rootFileData->getFilesRecursive(type, true); + std::vector allFiles = rootFileData->getFilesRecursive(GAME, true); std::vector::const_iterator itf; // declare an iterator to a vector of strings - for(itf=allFiles.cbegin() ; itf < allFiles.cend(); itf++) { + for (itf=allFiles.cbegin() ; itf < allFiles.cend(); itf++) + { if ((strcmp(nodeName, "video") == 0 && (*itf)->getVideoPath() != "") || (strcmp(nodeName, "image") == 0 && (*itf)->getImagePath() != "")) { @@ -283,8 +285,10 @@ void SystemScreenSaver::countImages() } } -void SystemScreenSaver::pickGameListNode(unsigned long index, const char *nodeName, std::string& path) +std::string SystemScreenSaver::pickGameListNode(unsigned long index, const char *nodeName) { + std::string path; + std::vector::const_iterator it; for (it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); ++it) { @@ -298,57 +302,67 @@ void SystemScreenSaver::pickGameListNode(unsigned long index, const char *nodeNa std::vector allFiles = rootFileData->getFilesRecursive(type, true); std::vector::const_iterator itf; // declare an iterator to a vector of strings - for(itf=allFiles.cbegin() ; itf < allFiles.cend(); itf++) { + for(itf=allFiles.cbegin() ; itf < allFiles.cend(); itf++) + { if ((strcmp(nodeName, "video") == 0 && (*itf)->getVideoPath() != "") || (strcmp(nodeName, "image") == 0 && (*itf)->getImagePath() != "")) { - if (index-- == 0) + if (index-- <= 0) { // We have it - path = ""; if (strcmp(nodeName, "video") == 0) path = (*itf)->getVideoPath(); else if (strcmp(nodeName, "image") == 0) path = (*itf)->getImagePath(); + + if (!Utils::FileSystem::exists(path)) + continue; + mSystemName = (*it)->getFullName(); mGameName = (*itf)->getName(); mCurrentGame = (*itf); - // end of getting FileData - if (Settings::getInstance()->getString("ScreenSaverGameInfo") != "never") - writeSubtitle(mGameName.c_str(), mSystemName.c_str(), - (Settings::getInstance()->getString("ScreenSaverGameInfo") == "always")); - return; +#ifdef _RPI_ + if (Settings::getInstance()->getBool("ScreenSaverOmxPlayer")) + if (Settings::getInstance()->getString("ScreenSaverGameInfo") != "never" && strcmp(nodeName, "video") == 0) + writeSubtitle(mGameName.c_str(), mSystemName.c_str(), (Settings::getInstance()->getString("ScreenSaverGameInfo") == "always")); +#endif + + return path; } } } } + + return ""; } -void SystemScreenSaver::pickRandomVideo(std::string& path) +std::string SystemScreenSaver::pickRandomVideo() { countVideos(); mCurrentGame = NULL; - if (mVideoCount > 0) - { - int video = (int)(((float)rand() / float(RAND_MAX)) * (float)mVideoCount); - pickGameListNode(video, "video", path); - } + if (mVideoCount == 0) + return ""; + + int video = (int)(((float)rand() / float(RAND_MAX)) * (float)mVideoCount); + return pickGameListNode(video, "video"); } -void SystemScreenSaver::pickRandomGameListImage(std::string& path) +std::string SystemScreenSaver::pickRandomGameListImage() { countImages(); mCurrentGame = NULL; - if (mImageCount > 0) - { - int image = (int)(((float)rand() / float(RAND_MAX)) * (float)mImageCount); - pickGameListNode(image, "image", path); - } + if (mImageCount == 0) + return ""; + + int image = (int)(((float)rand() / float(RAND_MAX)) * (float)mImageCount); + return pickGameListNode(image, "image"); } -void SystemScreenSaver::pickRandomCustomImage(std::string& path) +std::string SystemScreenSaver::pickRandomCustomImage() { + std::string path; + std::string imageDir = Settings::getInstance()->getString("SlideshowScreenSaverImageDir"); if ((imageDir != "") && (Utils::FileSystem::exists(imageDir))) { @@ -386,6 +400,8 @@ void SystemScreenSaver::pickRandomCustomImage(std::string& path) { LOG(LogError) << "Slideshow Screensaver - Image directory does not exist: " << imageDir << "\n"; } + + return path; } void SystemScreenSaver::update(int deltaTime) @@ -685,7 +701,7 @@ void ImageScreenSaver::setImage(const std::string path) mImage->setOrigin(0.5f, 0.5f); mImage->setPosition((mViewport.x + mViewport.w) / 2.0f, (mViewport.y + mViewport.h) / 2.0f); - if (Settings::getInstance()->getBool("SlideshowScreenSaverStretch") || mDecoration) + if (Settings::getInstance()->getBool("SlideshowScreenSaverStretch")) mImage->setMinSize((float)mViewport.w, (float)mViewport.h); else mImage->setMaxSize((float)mViewport.w, (float)mViewport.h); @@ -749,7 +765,7 @@ void VideoScreenSaver::setVideo(const std::string path) mVideo->setPosition((mViewport.x + mViewport.w) / 2.0f, (mViewport.y + mViewport.h) / 2.0f); - if (Settings::getInstance()->getBool("StretchVideoOnScreenSaver") || mDecoration) + if (Settings::getInstance()->getBool("StretchVideoOnScreenSaver")) mVideo->setMinSize((float)mViewport.w, (float)mViewport.h); else mVideo->setMaxSize((float)mViewport.w, (float)mViewport.h); diff --git a/es-app/src/SystemScreenSaver.h b/es-app/src/SystemScreenSaver.h index 4a669e3c42..54ecac7743 100644 --- a/es-app/src/SystemScreenSaver.h +++ b/es-app/src/SystemScreenSaver.h @@ -88,10 +88,11 @@ class SystemScreenSaver : public Window::ScreenSaver unsigned long countGameListNodes(const char *nodeName); void countVideos(); void countImages(); - void pickGameListNode(unsigned long index, const char *nodeName, std::string& path); - void pickRandomVideo(std::string& path); - void pickRandomGameListImage(std::string& path); - void pickRandomCustomImage(std::string& path); + + std::string pickGameListNode(unsigned long index, const char *nodeName); + std::string pickRandomVideo(); + std::string pickRandomGameListImage(); + std::string pickRandomCustomImage(); enum STATE { STATE_INACTIVE, diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index b4f2cada47..ea96cc3dd4 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -13,6 +13,14 @@ using namespace PlatformIds; +std::string ScreenScraperRequest::ensureUrl(const std::string url) +{ + return Utils::String::replace( + Utils::String::replace(url, " ", "%20") , + "#screenscraperserveur#", "https://www.screenscraper.fr/"); +} + + /** List of systems and their IDs from https://www.screenscraper.fr/api/systemesListe.php?devid=xxx&devpassword=yyy&softname=zzz&output=XML @@ -348,7 +356,7 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve { // Sending a 'softname' containing space will make the image URLs returned by the API also contain the space. // Escape any spaces in the URL here - result.imageUrl = Utils::String::replace(art.text().get(), " ", "%20"); + result.imageUrl = ensureUrl(art.text().get()); // Get the media type returned by ScreenScraper std::string media_type = art.attribute("format").value(); @@ -372,7 +380,7 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve if (art) { // Ask for the same image, but with a smaller size, for the thumbnail displayed during scraping - result.thumbnailUrl = Utils::String::replace(art.text().get(), " ", "%20"); + result.thumbnailUrl = ensureUrl(art.text().get()); } else LOG(LogDebug) << "Failed to find media XML node for thumbnail"; @@ -386,7 +394,7 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve { pugi::xml_node art = findMedia(media_list, ripList, region); if (art) - result.marqueeUrl = Utils::String::replace(art.text().get(), " ", "%20"); + result.marqueeUrl = ensureUrl(art.text().get()); else LOG(LogDebug) << "Failed to find media XML node for video"; } @@ -396,7 +404,7 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve { pugi::xml_node art = findMedia(media_list, "video", region); if (art) - result.videoUrl = Utils::String::replace(art.text().get(), " ", "%20"); + result.videoUrl = ensureUrl(art.text().get()); else LOG(LogDebug) << "Failed to find media XML node for video"; } diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index ea038d4649..b76c76b5c1 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -58,6 +58,7 @@ class ScreenScraperRequest : public ScraperHttpRequest protected: void process(const std::unique_ptr& req, std::vector& results) override; + std::string ensureUrl(const std::string url); void processList(const pugi::xml_document& xmldoc, std::vector& results); void processGame(const pugi::xml_document& xmldoc, std::vector& results); diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 1a801d049a..b70956edd1 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -643,8 +643,8 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) } if (SystemData::sSystemVector.size() > 0 && reloadTheme) - ThemeData::setDefaultTheme(SystemData::sSystemVector.at(0)->getTheme().get()); - + ViewController::get()->onThemeChanged(SystemData::sSystemVector.at(0)->getTheme()); + // Redisplay the current view if (mCurrentView) mCurrentView->onShow(); @@ -697,7 +697,7 @@ void ViewController::reloadAll(Window* window) } if (SystemData::sSystemVector.size() > 0) - ThemeData::setDefaultTheme(SystemData::sSystemVector.at(0)->getTheme().get()); + ViewController::get()->onThemeChanged(SystemData::sSystemVector.at(0)->getTheme()); // Rebuild SystemListView mSystemListView.reset(); @@ -740,3 +740,10 @@ HelpStyle ViewController::getHelpStyle() return mCurrentView->getHelpStyle(); } + + +void ViewController::onThemeChanged(const std::shared_ptr& theme) +{ + ThemeData::setDefaultTheme(theme.get()); + mWindow->onThemeChanged(theme); +} \ No newline at end of file diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h index 9061775983..227cb9b47b 100644 --- a/es-app/src/views/ViewController.h +++ b/es-app/src/views/ViewController.h @@ -87,6 +87,8 @@ class ViewController : public GuiComponent std::shared_ptr getSystemListView(); void removeGameListView(SystemData* system); + void onThemeChanged(const std::shared_ptr& theme); + private: ViewController(Window* window); static ViewController* sInstance; diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 446c544826..df8f7e5643 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -125,6 +125,9 @@ void DetailedGameListView::createVideo() #endif mVideo = new VideoVlcComponent(mWindow, ""); + // Default is IMAGE in Recalbox themes -> video view does not exist + mVideo->setSnapshotSource(IMAGE); + mVideo->setOrigin(0.5f, 0.5f); mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); @@ -325,7 +328,15 @@ void DetailedGameListView::updateInfoPanel() if (!mVideo->setVideo(file->getVideoPath())) mVideo->setDefaultVideo(); - mVideo->setImage(imagePath, false, mVideo->getMaxSizeInfo()); + std::string snapShot = imagePath; + + auto src = mVideo->getSnapshotSource(); + if (src == MARQUEE && !file->getMarqueePath().empty()) + snapShot = file->getMarqueePath(); + if (src == THUMBNAIL && !file->getThumbnailPath().empty()) + snapShot = file->getThumbnailPath(); + + mVideo->setImage(snapShot); } if (mImage != nullptr) diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index b318bf7fb2..1c7a76d6e5 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -38,6 +38,9 @@ VideoGameListView::VideoGameListView(Window* window, FolderData* root) : mVideo = new VideoVlcComponent(window, ""); #endif + // Default is thumbnail in Retropie themes & video view + mVideo->setSnapshotSource(THUMBNAIL); + mList.setPosition(mSize.x() * (0.50f + padding), mList.getPosition().y()); mList.setSize(mSize.x() * (0.50f - padding), mList.getSize().y()); mList.setAlignment(TextListComponent::ALIGN_LEFT); @@ -275,12 +278,19 @@ void VideoGameListView::updateInfoPanel() mVideo->setDefaultVideo(); } mVideoPlaying = true; + + std::string snapShot = file->getThumbnailPath(); + + auto src = mVideo->getSnapshotSource(); + if (src == MARQUEE && !file->getMarqueePath().empty()) + snapShot = file->getMarqueePath(); + if (src == IMAGE && !file->getImagePath().empty()) + snapShot = file->getImagePath(); - mVideo->setImage(file->getThumbnailPath(), false, mVideo->getMaxSizeInfo()); - mMarquee.setImage(file->getMarqueePath(), false, mMarquee.getMaxSizeInfo()); + mVideo->setImage(snapShot); - if (mImage != nullptr) - mImage->setImage(file->getImagePath(), false, mImage->getMaxSizeInfo()); + mMarquee.setImage(file->getMarqueePath()/*, false, mMarquee.getMaxSizeInfo()*/); // Too slow on pi + mImage.setImage(file->getImagePath(), false, mImage.getMaxSizeInfo()); mDescription.setText(file->metadata.get("desc")); mDescContainer.reset(); diff --git a/es-core/src/HelpStyle.cpp b/es-core/src/HelpStyle.cpp index 937ea22174..67635176c2 100644 --- a/es-core/src/HelpStyle.cpp +++ b/es-core/src/HelpStyle.cpp @@ -8,11 +8,10 @@ HelpStyle::HelpStyle() origin = Vector2f(0.0f, 0.0f); iconColor = 0x777777FF; textColor = 0x777777FF; + font = nullptr; - if(FONT_SIZE_SMALL != 0) + if (FONT_SIZE_SMALL != 0) font = Font::get(FONT_SIZE_SMALL); - else - font = nullptr; } void HelpStyle::applyTheme(const std::shared_ptr& theme, const std::string& view) diff --git a/es-core/src/HelpStyle.h b/es-core/src/HelpStyle.h index d1cab3fa5d..72aa8df9b2 100644 --- a/es-core/src/HelpStyle.h +++ b/es-core/src/HelpStyle.h @@ -18,7 +18,7 @@ struct HelpStyle unsigned int textColor; std::shared_ptr font; std::map iconMap; - + HelpStyle(); // default values void applyTheme(const std::shared_ptr& theme, const std::string& view); }; diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index 432d67b00f..54edd2a052 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -378,9 +378,13 @@ int HttpReq::saveContent(const std::string filename, bool checkMedia) if (!Utils::FileSystem::exists(mStreamPath)) return false; - if (checkMedia && Utils::FileSystem::getFileSize(mStreamPath) < 300) + if (checkMedia && Utils::FileSystem::getFileSize(mStreamPath) < 1024) { auto data = Utils::String::toUpper(getContent()); + + if (data.find(">>>>>> e915ef84... Theming View/ Screen : Add to manage fixed components that are to be statically displayed ( crt shader, clock ) mHelp = new HelpComponent(this); mBackgroundOverlay = new ImageComponent(this); @@ -24,6 +29,9 @@ Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCoun Window::~Window() { + for (auto extra : mScreenExtras) + delete extra; + delete mBackgroundOverlay; // delete all our GUIs @@ -101,6 +109,17 @@ bool Window::init(bool initRenderer) mBackgroundOverlay->setImage(":/scroll_gradient.png"); mBackgroundOverlay->setResize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + if (mClock == nullptr) + { + mClock = std::make_shared(this); + mClock->setFont(Font::get(FONT_SIZE_SMALL)); + mClock->setHorizontalAlignment(ALIGN_RIGHT); + mClock->setVerticalAlignment(ALIGN_TOP); + mClock->setPosition(Renderer::getScreenWidth()*0.94, Renderer::getScreenHeight()*0.9965 - Font::get(FONT_SIZE_SMALL)->getHeight()); + mClock->setSize(Renderer::getScreenWidth()*0.05, 0); + mClock->setColor(0x777777FF); + } + // update our help because font sizes probably changed if (peekGui()) peekGui()->updateHelpPrompts(); @@ -110,6 +129,9 @@ bool Window::init(bool initRenderer) void Window::reactivateGui() { + for (auto extra : mScreenExtras) + extra->onShow(); + for (auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) (*i)->onShow(); @@ -119,16 +141,19 @@ void Window::reactivateGui() void Window::deinit(bool deinitRenderer) { + for (auto extra : mScreenExtras) + extra->onHide(); + // Hide all GUI elements on uninitialisation - this disable for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) - { (*i)->onHide(); - } if (deinitRenderer) InputManager::getInstance()->deinit(); TextureResource::resetCache(); + TextureResource::clearQueue(); + ResourceManager::getInstance()->unloadAll(); if (deinitRenderer) @@ -244,6 +269,39 @@ void Window::update(int deltaTime) mFrameCountElapsed = 0; } + /* draw the clock */ // batocera + if (Settings::getInstance()->getBool("DrawClock") && mClock) + { + mClockElapsed -= deltaTime; + if (mClockElapsed <= 0) + { + time_t clockNow = time(0); + struct tm clockTstruct = *localtime(&clockNow); + + if (clockTstruct.tm_year > 100) + { + // Display the clock only if year is more than 1900+100 ; rpi have no internal clock and out of the networks, the date time information has no value */ + // Visit http://en.cppreference.com/w/cpp/chrono/c/strftime for more information about date/time format + + char clockBuf[32]; + strftime(clockBuf, sizeof(clockBuf), "%H:%M", &clockTstruct); + mClock->setText(clockBuf); + } + + mClockElapsed = 1000; // next update in 1000ms + } + } + + // hide pads // batocera + for (int i = 0; i < MAX_PLAYERS; i++) { + if (mplayerPads[i] > 0) { + mplayerPads[i] -= deltaTime; + if (mplayerPads[i] < 0) { + mplayerPads[i] = 0; + } + } + } + mTimeSinceLastInput += deltaTime; if(peekGui()) @@ -293,6 +351,47 @@ void Window::render() mDefaultFonts.at(1)->renderTextCache(mFrameDataText.get()); } + + // clock // batocera + if (Settings::getInstance()->getBool("DrawClock") && mClock && (mGuiStack.size() < 2 || !Renderer::isSmallScreen())) + { + mClock->render(transform); + // Renderer::setMatrix(Transform4x4f::Identity()); + /* + if (mClockFont == nullptr) + mClockFont = mDefaultFonts.at(0); + + mClockFont->renderTextCache(mClockText.get());*/ + } + + // pads // batocera + Renderer::setMatrix(Transform4x4f::Identity()); + + if (Settings::getInstance()->getBool("ShowControllerActivity")) + { + std::map playerJoysticks = InputManager::getInstance()->lastKnownPlayersDeviceIndexes(); + for (int player = 0; player < MAX_PLAYERS; player++) + { + unsigned int padcolor = 0xFFFFFF99; + +#ifndef _DEBUG + if (playerJoysticks.count(player) != 1) + continue; + + int idx = playerJoysticks[player]; + if (idx < 0 || idx >= MAX_PLAYERS) + continue; + + if (mplayerPads[idx] > 0) + padcolor = mplayerPadsIsHotkey ? 0x0000FF66 : 0xFF000066; +#endif + + float sz = Renderer::getScreenHeight() / 100.0f; + + Renderer::drawRect((player*(sz + 4)) + 2, Renderer::getScreenHeight() - sz - 2, sz, sz, padcolor); + } + } + unsigned int screensaverTime = (unsigned int)Settings::getInstance()->getInt("ScreenSaverTime"); if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) startScreenSaver(); @@ -307,6 +406,10 @@ void Window::render() // or not because it may perform a fade on transition renderScreenSaver(); + + for (auto extra : mScreenExtras) + extra->render(transform); + if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) { if (!isProcessing() && mAllowSleep && (!mScreenSaver || mScreenSaver->allowSleep())) @@ -339,24 +442,6 @@ void Window::endRenderLoadingScreen() { mSplash = NULL; mCustomSplash = ""; - - if (ThemeData::getMenuTheme()->Background.shaderPath.empty()) - mImageShader = nullptr; - else - { - if (mImageShader == nullptr) - mImageShader = std::make_shared(this, true, false); - - auto theme = ThemeData::getMenuTheme()->Background; - - mImageShader->setImage(theme.shaderPath, theme.shaderTiled); - mImageShader->setColorShift(theme.shaderColor); - mImageShader->setSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); - - } - - // Window has not way to apply Theme -> As a workaround : endRenderLoadingScreen is always called when theme changes. - mBackgroundOverlay->setImage(ThemeData::getMenuTheme()->Background.fadePath); } void Window::renderLoadingScreen(std::string text, float percent, unsigned char opacity) @@ -488,6 +573,8 @@ void Window::setHelpPrompts(const std::vector& prompts, const HelpSt mHelp->clearPrompts(); mHelp->setStyle(style); + + mClockElapsed = -1; std::vector addPrompts; @@ -573,6 +660,9 @@ void Window::startScreenSaver() { if (mScreenSaver && !mRenderScreenSaver) { + for (auto extra : mScreenExtras) + extra->onScreenSaverActivate(); + // Tell the GUI components the screensaver is starting for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) (*i)->onScreenSaverActivate(); @@ -585,7 +675,7 @@ void Window::startScreenSaver() bool Window::cancelScreenSaver() { if (mScreenSaver && mRenderScreenSaver) - { + { mScreenSaver->stopScreenSaver(); mRenderScreenSaver = false; mScreenSaver->resetCounts(); @@ -594,6 +684,9 @@ bool Window::cancelScreenSaver() for(auto i = mGuiStack.cbegin(); i != mGuiStack.cend(); i++) (*i)->onScreenSaverDeactivate(); + for (auto extra : mScreenExtras) + extra->onScreenSaverDeactivate(); + return true; } @@ -698,4 +791,43 @@ void Window::processPostedFunctions() func(this); mFunctions.clear(); -} \ No newline at end of file +} + +void Window::onThemeChanged(const std::shared_ptr& theme) +{ + for (auto extra : mScreenExtras) + delete extra; + + mScreenExtras.clear(); + mScreenExtras = ThemeData::makeExtras(theme, "screen", this); + + std::stable_sort(mScreenExtras.begin(), mScreenExtras.end(), [](GuiComponent* a, GuiComponent* b) { return b->getZIndex() > a->getZIndex(); }); + + if (mBackgroundOverlay) + mBackgroundOverlay->setImage(ThemeData::getMenuTheme()->Background.fadePath); + + if (mClock) + { + mClock->setFont(Font::get(FONT_SIZE_SMALL)); + mClock->setColor(0x777777FF); + mClock->setHorizontalAlignment(ALIGN_RIGHT); + mClock->setVerticalAlignment(ALIGN_TOP); + + // if clock element does not exist in screen view -> + // skin it from system.helpsystem -> ) + if (!theme->getElement("screen", "clock", "text")) + { + auto elem = theme->getElement("system", "help", "helpsystem"); + if (elem && elem->has("textColor")) + mClock->setColor(elem->get("textColor")); + + if (elem && (elem->has("fontPath") || elem->has("fontSize"))) + mClock->setFont(Font::getFromTheme(elem, ThemeFlags::ALL, Font::get(FONT_SIZE_MEDIUM))); + } + + mClock->setPosition(Renderer::getScreenWidth()*0.94, Renderer::getScreenHeight()*0.9965 - mClock->getFont()->getHeight()); + mClock->setSize(Renderer::getScreenWidth()*0.05, 0); + + mClock->applyTheme(theme, "screen", "clock", ThemeFlags::ALL ^ (ThemeFlags::TEXT)); + } +} diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 78b12b5b9d..9e1552ef4c 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -19,6 +19,8 @@ class TextCache; class Transform4x4f; class TextureResource; class AsyncNotificationComponent; +class ThemeData; +class TextComponent; struct HelpStyle; @@ -93,6 +95,8 @@ class Window void postToUiThread(const std::function& func); void reactivateGui(); + void onThemeChanged(const std::shared_ptr& theme); + private: void processPostedFunctions(); @@ -118,10 +122,9 @@ class Window std::shared_ptr mSplash; std::string mCustomSplash; - - std::shared_ptr mImageShader; - - std::vector mGuiStack; + + std::vector mScreenExtras; + std::vector mGuiStack; std::vector< std::shared_ptr > mDefaultFonts; @@ -131,6 +134,15 @@ class Window std::unique_ptr mFrameDataText; + // clock // batocera + int mClockElapsed; + + std::shared_ptr mClock; + + // pads // batocera + int mplayerPads[MAX_PLAYERS]; + bool mplayerPadsIsHotkey; + bool mNormalizeNextUpdate; bool mAllowSleep; diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index ab3f120bcc..7e299ac78b 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -9,7 +9,8 @@ TextComponent::TextComponent(Window* window) : GuiComponent(window), mHorizontalAlignment(ALIGN_LEFT), mVerticalAlignment(ALIGN_CENTER), mLineSpacing(1.5f), mBgColor(0), mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)), mGlowOffset(Vector2f(0, 0)), mReflection(0.0f, 0.0f), mReflectOnBorders(false) -{ +{ + } TextComponent::TextComponent(Window* window, const std::string& text, const std::shared_ptr& font, unsigned int color, Alignment align, @@ -304,6 +305,9 @@ void TextComponent::onColorChanged() void TextComponent::setHorizontalAlignment(Alignment align) { + if (mHorizontalAlignment == align) + return; + mHorizontalAlignment = align; onTextChanged(); } @@ -339,32 +343,41 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, const st if (!elem) return; - if (properties & COLOR && elem->has("color")) - setColor(elem->get("color")); + if (properties & ALIGNMENT) + { + if (elem->has("alignment")) + { + std::string str = elem->get("alignment"); + if (str == "left") + setHorizontalAlignment(ALIGN_LEFT); + else if (str == "center") + setHorizontalAlignment(ALIGN_CENTER); + else if (str == "right") + setHorizontalAlignment(ALIGN_RIGHT); + else + LOG(LogError) << "Unknown text alignment string: " << str; + } - setRenderBackground(false); - if (properties & COLOR && elem->has("backgroundColor")) { - setBackgroundColor(elem->get("backgroundColor")); - setRenderBackground(true); - } + if (elem->has("verticalAlignment")) + { + std::string str = elem->get("verticalAlignment"); + if (str == "top") + setVerticalAlignment(ALIGN_TOP); + else if (str == "center") + setVerticalAlignment(ALIGN_CENTER); + else if (str == "bottom") + setVerticalAlignment(ALIGN_BOTTOM); + else + LOG(LogError) << "Unknown text alignment string: " << str; + } - if (properties & ALIGNMENT && elem->has("alignment")) - { - std::string str = elem->get("alignment"); - if (str == "left") - setHorizontalAlignment(ALIGN_LEFT); - else if (str == "center") - setHorizontalAlignment(ALIGN_CENTER); - else if (str == "right") - setHorizontalAlignment(ALIGN_RIGHT); + if (elem->has("padding")) + { + Vector2f scale = getParent() ? getParent()->getSize() : Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + mPadding = elem->get("padding") * Vector4f(scale.x(), scale.y(), scale.x(), scale.y()); + } else - LOG(LogError) << "Unknown text alignment string: " << str; - } - - if (properties & ALIGNMENT && elem->has("padding")) - { - Vector2f scale = getParent() ? getParent()->getSize() : Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - mPadding = elem->get("padding") * Vector4f(scale.x(), scale.y(), scale.x(), scale.y()); + mPadding = Vector4f::Zero(); } if (properties & TEXT && elem->has("text")) @@ -378,8 +391,21 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, const st if (properties & COLOR) { + if (elem->has("color")) + setColor(elem->get("color")); + + if (elem->has("backgroundColor")) + { + setBackgroundColor(elem->get("backgroundColor")); + setRenderBackground(true); + } + else + setRenderBackground(false); + if (elem->has("glowColor")) mGlowColor = elem->get("glowColor"); + else + mGlowColor = 0; if (elem->has("glowSize")) mGlowSize = (int)elem->get("glowSize"); diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index 2bb021e18d..50b79370f3 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -76,8 +76,7 @@ class TextComponent : public GuiComponent unsigned int mGlowSize; Vector2f mGlowOffset; Vector4f mPadding; - - + Vector2f mReflection; bool mReflectOnBorders; }; diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index d5178b4bca..716916b961 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -21,29 +21,30 @@ std::string getTitleFolder() { void writeSubtitle(const char* gameName, const char* systemName, bool always) { FILE* file = fopen(getTitlePath().c_str(), "w"); - int end = (int)(Settings::getInstance()->getInt("ScreenSaverSwapVideoTimeout") / (1000)); - if (always) { - fprintf(file, "1\n00:00:01,000 --> 00:00:%d,000\n", end); - } - else + if (file) { - fprintf(file, "1\n00:00:01,000 --> 00:00:08,000\n"); - } - - fprintf(file, "%s\n", gameName); - fprintf(file, "%s\n\n", systemName); - //fprintf(file, "%s\n\n", systemName); - - if (!always) { - if (end > 12) + int end = (int)(Settings::getInstance()->getInt("ScreenSaverSwapVideoTimeout") / (1000)); + if (always) { + fprintf(file, "1\n00:00:01,000 --> 00:00:%d,000\n", end); + } + else { - fprintf(file, "2\n00:00:%d,000 --> 00:00:%d,000\n%s\n%s\n", end-4, end, gameName, systemName); + fprintf(file, "1\n00:00:01,000 --> 00:00:08,000\n"); } - } + fprintf(file, "%s\n", gameName); + fprintf(file, "%s\n\n", systemName); - fflush(file); - fclose(file); - file = NULL; + if (!always) { + if (end > 12) + { + fprintf(file, "2\n00:00:%d,000 --> 00:00:%d,000\n%s\n%s\n", end - 4, end, gameName, systemName); + } + } + + fflush(file); + fclose(file); + file = NULL; + } } void VideoComponent::setScreensaverMode(bool isScreensaver) @@ -74,6 +75,7 @@ VideoComponent::VideoComponent(Window* window) : // Setup the default configuration mConfig.showSnapshotDelay = false; mConfig.showSnapshotNoVideo = false; + mConfig.snapshotSource = IMAGE; mConfig.startDelay = 0; if (mWindow->getGuiStackSize() > 1) { topWindow(false); @@ -243,6 +245,17 @@ void VideoComponent::applyTheme(const std::shared_ptr& theme, const s if (elem->has("showSnapshotDelay")) mConfig.showSnapshotDelay = elem->get("showSnapshotDelay"); + if (elem->has("snapshotSource")) + { + auto direction = elem->get("snapshotSource"); + if (direction == "image") + mConfig.snapshotSource = IMAGE; + else if (direction == "marquee") + mConfig.snapshotSource = MARQUEE; + else + mConfig.snapshotSource = THUMBNAIL; + } + if(properties & ThemeFlags::ROTATION) { if(elem->has("rotation")) setRotationDegrees(elem->get("rotation")); diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index 4f4ee71cbc..15b8eb076d 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -3,6 +3,7 @@ #define ES_CORE_COMPONENTS_VIDEO_COMPONENT_H #include "components/ImageComponent.h" +#include "components/ImageGridComponent.h" #include "GuiComponent.h" #include @@ -11,7 +12,14 @@ class TextureResource; std::string getTitlePath(); std::string getTitleFolder(); void writeSubtitle(const char* gameName, const char* systemName, bool always); - +/* +enum ImageSource +{ + THUMBNAIL, + IMAGE, + MARQUEE +}; +*/ class VideoComponent : public GuiComponent { // Structure that groups together the configuration of the video component @@ -20,6 +28,7 @@ class VideoComponent : public GuiComponent unsigned startDelay; bool showSnapshotNoVideo; bool showSnapshotDelay; + ImageSource snapshotSource; std::string defaultVideoPath; }; @@ -94,6 +103,9 @@ class VideoComponent : public GuiComponent return MaxSizeInfo(mTargetSize, mTargetIsMax); }; + ImageSource getSnapshotSource() { return mConfig.snapshotSource; }; + void setSnapshotSource(ImageSource source) { mConfig.snapshotSource = source; }; + private: // Start the video Immediately virtual void startVideo() = 0; diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 76faa1cb08..319de9681a 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -61,6 +61,8 @@ VideoVlcComponent::VideoVlcComponent(Window* window, std::string subtitles) : mMediaPlayer(nullptr), mMedia(nullptr) { + mElapsed = 0; + // Get an empty texture for rendering the video mTexture = nullptr;// TextureResource::get(""); mEffect = VideoVlcFlags::VideoVlcEffect::BUMP; @@ -234,10 +236,19 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) resize(); } - mContext.mutexes[frame].lock(); - mTexture->initFromExternalPixels(mContext.surfaces[frame], mVideoWidth, mVideoHeight); - mContext.hasFrame[frame] = false; - mContext.mutexes[frame].unlock(); +#ifdef _RPI_ + // Rpi : A lot of videos are encoded in 60fps on screenscraper + // Try to limit transfert to opengl textures to 30fps to save CPU + if (!Settings::getInstance()->getBool("OptimizeVideo") || mElapsed >= 40) // 40ms = 25fps, 33.33 = 30 fps +#endif + { + mContext.mutexes[frame].lock(); + mTexture->initFromExternalPixels(mContext.surfaces[frame], mVideoWidth, mVideoHeight); + mContext.hasFrame[frame] = false; + mContext.mutexes[frame].unlock(); + + mElapsed = 0; + } } } @@ -504,4 +515,10 @@ void VideoVlcComponent::applyTheme(const std::shared_ptr& theme, cons else mEffect = VideoVlcFlags::VideoVlcEffect::NONE; } -} \ No newline at end of file +} + +void VideoVlcComponent::update(int deltaTime) +{ + mElapsed += deltaTime; + VideoComponent::update(deltaTime); +} diff --git a/es-core/src/components/VideoVlcComponent.h b/es-core/src/components/VideoVlcComponent.h index bd697e11f8..eab753e970 100644 --- a/es-core/src/components/VideoVlcComponent.h +++ b/es-core/src/components/VideoVlcComponent.h @@ -73,6 +73,7 @@ class VideoVlcComponent : public VideoComponent void setMinSize(float width, float height); virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); + virtual void update(int deltaTime); private: // Calculates the correct mSize from our resizing information (set by setResize/setMaxSize). @@ -103,6 +104,8 @@ class VideoVlcComponent : public VideoComponent std::string mSubtitleTmpFile; VideoVlcFlags::VideoVlcEffect mEffect; + + int mElapsed; }; #endif // ES_CORE_COMPONENTS_VIDEO_VLC_COMPONENT_H From bde61e2b1530219639fd1a5b842bfa0f4aedc1ea Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 15 Oct 2019 11:04:37 +0200 Subject: [PATCH 173/336] Fix immediate mode for game count # Conflicts: # es-app/src/views/SystemView.cpp --- es-app/src/views/SystemView.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index d4212a0167..ea2eac9bcd 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -334,7 +334,7 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) // ViewController::get()->getGameListView(mEntries.at(mCursor).object); // fake preload // wait 600ms to fade in - setAnimation(infoFadeIn, systemInfoDelay, [this] { + setAnimation(infoFadeIn, goFast ? 0 : systemInfoDelay, [this] { ViewController::get()->getGameListView(mEntries.at(mCursor).object); // fake preload }, false, 2); From 79dffb088be1e34b02b322b0da5020a969907d1c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 15 Oct 2019 11:04:57 +0200 Subject: [PATCH 174/336] VideoScreenSaver using VLC : RPI Crash fix --- es-app/src/SystemScreenSaver.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/es-app/src/SystemScreenSaver.cpp b/es-app/src/SystemScreenSaver.cpp index 240431fafa..075ed64ae3 100644 --- a/es-app/src/SystemScreenSaver.cpp +++ b/es-app/src/SystemScreenSaver.cpp @@ -756,9 +756,8 @@ void VideoScreenSaver::setVideo(const std::string path) if (Settings::getInstance()->getBool("ScreenSaverOmxPlayer")) mVideo = new VideoPlayerComponent(mWindow, getTitlePath()); else -#else - mVideo = new VideoVlcComponent(mWindow); #endif + mVideo = new VideoVlcComponent(mWindow); mVideo->topWindow(true); mVideo->setOrigin(0.5f, 0.5f); From 8801f2c87e55a40bb5b1585686068d4b71ae5323 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 15 Oct 2019 11:05:19 +0200 Subject: [PATCH 175/336] Scrapper : odyssey not supported in scrapers. --- es-app/src/PlatformId.cpp | 1 + es-app/src/PlatformId.h | 1 + es-app/src/scrapers/GamesDBJSONScraper.cpp | 1 + es-app/src/scrapers/ScreenScraper.cpp | 3 ++- 4 files changed, 5 insertions(+), 1 deletion(-) diff --git a/es-app/src/PlatformId.cpp b/es-app/src/PlatformId.cpp index 54a1a977e0..9997a4c974 100644 --- a/es-app/src/PlatformId.cpp +++ b/es-app/src/PlatformId.cpp @@ -72,6 +72,7 @@ namespace PlatformIds "vectrex", "trs-80", "coco", + "odyssey2", "ignore", // do not allow scraping for this system "invalid" diff --git a/es-app/src/PlatformId.h b/es-app/src/PlatformId.h index a4a53f861b..2b8e06fd61 100644 --- a/es-app/src/PlatformId.h +++ b/es-app/src/PlatformId.h @@ -73,6 +73,7 @@ namespace PlatformIds VECTREX, TRS80_COLOR_COMPUTER, TANDY, + ODYSSEY2, PLATFORM_IGNORE, // do not allow scraping for this system PLATFORM_COUNT diff --git a/es-app/src/scrapers/GamesDBJSONScraper.cpp b/es-app/src/scrapers/GamesDBJSONScraper.cpp index ad4ddbf606..2ab016385d 100644 --- a/es-app/src/scrapers/GamesDBJSONScraper.cpp +++ b/es-app/src/scrapers/GamesDBJSONScraper.cpp @@ -97,6 +97,7 @@ const std::map gamesdb_new_platformid_map{ { VECTREX, "4939" }, { TRS80_COLOR_COMPUTER, "4941" }, { TANDY, "4941" }, + { ODYSSEY2, "4927" } }; void thegamesdb_generate_json_scraper_requests(const ScraperSearchParams& params, diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index ea96cc3dd4..036c8df42d 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -90,7 +90,8 @@ const std::map screenscraper_platformid_map{ { VIDEOPAC_ODYSSEY2, 104 }, { VECTREX, 102 }, { TRS80_COLOR_COMPUTER, 144 }, - { TANDY, 144 } + { TANDY, 144 }, + { ODYSSEY2, 104 } }; From 8269b2d48a79c066dd20b2293251f1500d33797c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 15 Oct 2019 11:05:33 +0200 Subject: [PATCH 176/336] Scrapper : SUPERGRAFX not supported/known in platforms for scrapers --- es-app/src/PlatformId.cpp | 1 + es-app/src/PlatformId.h | 1 + es-app/src/scrapers/GamesDBJSONScraper.cpp | 3 ++- es-app/src/scrapers/ScreenScraper.cpp | 3 ++- 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/es-app/src/PlatformId.cpp b/es-app/src/PlatformId.cpp index 9997a4c974..7b4cd01462 100644 --- a/es-app/src/PlatformId.cpp +++ b/es-app/src/PlatformId.cpp @@ -73,6 +73,7 @@ namespace PlatformIds "trs-80", "coco", "odyssey2", + "supergrafx", "ignore", // do not allow scraping for this system "invalid" diff --git a/es-app/src/PlatformId.h b/es-app/src/PlatformId.h index 2b8e06fd61..71a9d634d1 100644 --- a/es-app/src/PlatformId.h +++ b/es-app/src/PlatformId.h @@ -74,6 +74,7 @@ namespace PlatformIds TRS80_COLOR_COMPUTER, TANDY, ODYSSEY2, + SUPERGRAFX, PLATFORM_IGNORE, // do not allow scraping for this system PLATFORM_COUNT diff --git a/es-app/src/scrapers/GamesDBJSONScraper.cpp b/es-app/src/scrapers/GamesDBJSONScraper.cpp index 2ab016385d..70e96ecc90 100644 --- a/es-app/src/scrapers/GamesDBJSONScraper.cpp +++ b/es-app/src/scrapers/GamesDBJSONScraper.cpp @@ -97,7 +97,8 @@ const std::map gamesdb_new_platformid_map{ { VECTREX, "4939" }, { TRS80_COLOR_COMPUTER, "4941" }, { TANDY, "4941" }, - { ODYSSEY2, "4927" } + { ODYSSEY2, "4927" }, + { SUPERGRAFX, "34" } // The code is TurboGrafx 16, but they manage SUPERGRAFX into this one.... }; void thegamesdb_generate_json_scraper_requests(const ScraperSearchParams& params, diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index 036c8df42d..3bce891982 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -91,7 +91,8 @@ const std::map screenscraper_platformid_map{ { VECTREX, 102 }, { TRS80_COLOR_COMPUTER, 144 }, { TANDY, 144 }, - { ODYSSEY2, 104 } + { ODYSSEY2, 104 }, + { SUPERGRAFX, 105 } }; From 61bcea205697f246a1fd6715f3e746755cac5d92 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 15 Oct 2019 11:06:49 +0200 Subject: [PATCH 177/336] ScreenScraper : Add md5 checksum in http queries to increase scraping quality. + Handle retry when "maximum threads per minute" is reached # Conflicts: # es-app/CMakeLists.txt # es-app/src/scrapers/ScreenScraper.cpp --- es-app/CMakeLists.txt | 2 + es-app/src/scrapers/GamesDBJSONScraper.cpp | 16 +- es-app/src/scrapers/GamesDBJSONScraper.h | 2 +- es-app/src/scrapers/Scraper.cpp | 18 +- es-app/src/scrapers/Scraper.h | 3 +- es-app/src/scrapers/ScreenScraper.cpp | 41 ++- es-app/src/scrapers/ScreenScraper.h | 2 +- es-app/src/scrapers/md5.cpp | 369 +++++++++++++++++++++ es-app/src/scrapers/md5.h | 95 ++++++ es-core/src/HttpReq.cpp | 2 + es-core/src/HttpReq.h | 3 + 11 files changed, 539 insertions(+), 14 deletions(-) create mode 100644 es-app/src/scrapers/md5.cpp create mode 100644 es-app/src/scrapers/md5.h diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 0873517c2e..8b62288c92 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -42,6 +42,7 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBJSONScraperResources.h ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ScreenScraper.h ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ThreadedScraper.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/md5.h # Views ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/BasicGameListView.h @@ -100,6 +101,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBJSONScraperResources.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ScreenScraper.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/ThreadedScraper.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/md5.cpp # Views ${CMAKE_CURRENT_SOURCE_DIR}/src/views/gamelist/BasicGameListView.cpp diff --git a/es-app/src/scrapers/GamesDBJSONScraper.cpp b/es-app/src/scrapers/GamesDBJSONScraper.cpp index 70e96ecc90..182597aafe 100644 --- a/es-app/src/scrapers/GamesDBJSONScraper.cpp +++ b/es-app/src/scrapers/GamesDBJSONScraper.cpp @@ -340,7 +340,8 @@ void processGame(const Value& game, const Value& boxart, std::vector& req, std::vector& results) + // Process should return false only when we reached a maximum scrap by minute, to retry +bool TheGamesDBJSONRequest::process(const std::unique_ptr& req, std::vector& results) { assert(req->status() == HttpReq::REQ_SUCCESS); @@ -353,14 +354,14 @@ void TheGamesDBJSONRequest::process(const std::unique_ptr& req, std::ve std::string("TheGamesDBJSONRequest - Error parsing JSON. \n\t") + GetParseError_En(doc.GetParseError()); setError(err); LOG(LogError) << err; - return; + return true; } if (!doc.HasMember("data") || !doc["data"].HasMember("games") || !doc["data"]["games"].IsArray()) { std::string warn = "TheGamesDBJSONRequest - Response had no game data.\n"; LOG(LogWarning) << warn; - return; + return true; } const Value& games = doc["data"]["games"]; @@ -368,7 +369,7 @@ void TheGamesDBJSONRequest::process(const std::unique_ptr& req, std::ve { std::string warn = "TheGamesDBJSONRequest - Response had no include boxart data.\n"; LOG(LogWarning) << warn; - return; + return true; } const Value& boxart = doc["include"]["boxart"]; @@ -377,12 +378,11 @@ void TheGamesDBJSONRequest::process(const std::unique_ptr& req, std::ve { std::string warn = "TheGamesDBJSONRequest - Response include had no usable boxart data.\n"; LOG(LogWarning) << warn; - return; + return true; } resources.ensureResources(); - - + for (int i = 0; i < (int)games.Size(); ++i) { auto& v = games[i]; @@ -395,4 +395,6 @@ void TheGamesDBJSONRequest::process(const std::unique_ptr& req, std::ve LOG(LogError) << "Error while processing game: " << e.what(); } } + + return true; } diff --git a/es-app/src/scrapers/GamesDBJSONScraper.h b/es-app/src/scrapers/GamesDBJSONScraper.h index 505dbcf2be..d0f1aeb2fb 100644 --- a/es-app/src/scrapers/GamesDBJSONScraper.h +++ b/es-app/src/scrapers/GamesDBJSONScraper.h @@ -28,7 +28,7 @@ class TheGamesDBJSONRequest : public ScraperHttpRequest } protected: - void process(const std::unique_ptr& req, std::vector& results) override; + bool process(const std::unique_ptr& req, std::vector& results) override; bool isGameRequest() { return !mRequestQueue; } std::queue>* mRequestQueue; diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index 8d8f58bb80..b6eae6051e 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -10,6 +10,7 @@ #include #include "utils/FileSystemUtil.h" #include "utils/StringUtil.h" +#include // batocera const std::map scraper_request_funcs { @@ -113,6 +114,7 @@ ScraperHttpRequest::ScraperHttpRequest(std::vector& results { setStatus(ASYNC_IN_PROGRESS); mReq = std::unique_ptr(new HttpReq(url)); + mRetryCount = 0; } void ScraperHttpRequest::update() @@ -121,7 +123,21 @@ void ScraperHttpRequest::update() if(status == HttpReq::REQ_SUCCESS) { setStatus(ASYNC_DONE); // if process() has an error, status will be changed to ASYNC_ERROR - process(mReq, mResults); + + if (!process(mReq, mResults)) + { + mRetryCount++; + if (mRetryCount > 4) + return; + + // If the scrapper fails & return false, then retry the request + setStatus(ASYNC_IN_PROGRESS); + + std::string url = mReq->getUrl(); + std::this_thread::sleep_for(std::chrono::seconds(5)); + mReq = std::unique_ptr(new HttpReq(url)); + } + return; } diff --git a/es-app/src/scrapers/Scraper.h b/es-app/src/scrapers/Scraper.h index 08064d81c8..386b93a336 100644 --- a/es-app/src/scrapers/Scraper.h +++ b/es-app/src/scrapers/Scraper.h @@ -95,10 +95,11 @@ class ScraperHttpRequest : public ScraperRequest virtual void update() override; protected: - virtual void process(const std::unique_ptr& req, std::vector& results) = 0; + virtual bool process(const std::unique_ptr& req, std::vector& results) = 0; private: std::unique_ptr mReq; + int mRetryCount; }; // a request to get a list of results diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index 3bce891982..5c20824409 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -10,6 +10,8 @@ #include #include #include "EsLocale.h" +#include "md5.h" +#include using namespace PlatformIds; @@ -148,6 +150,34 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, //path += "&romtype=jeu"; } + // Use md5 to search scrapped game if <= 16 Mo + int length = Utils::FileSystem::getFileSize(params.game->getFullPath()); + if (length <= 16384 * 1024) + { + std::ifstream inBigArrayfile; + inBigArrayfile.open(params.game->getFullPath(), std::ios::binary | std::ios::in); + if (inBigArrayfile.is_open()) + { + try + { + char* InFileData = new char[length]; + if (InFileData) + { + inBigArrayfile.read(InFileData, length); + + MD5 md5 = MD5(InFileData, length); + std::string Temp = md5.hexdigest(); + path += "&md5=" + md5.hexdigest(); + + delete[] InFileData; + } + } + catch (std::bad_alloc& ex) { } + + inBigArrayfile.close(); + } + } + auto& platforms = params.system->getPlatformIds(); std::vector p_ids; @@ -179,7 +209,8 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, } } -void ScreenScraperRequest::process(const std::unique_ptr& req, std::vector& results) +// Process should return false only when we reached a maximum scrap by minute, to retry +bool ScreenScraperRequest::process(const std::unique_ptr& req, std::vector& results) { assert(req->status() == HttpReq::REQ_SUCCESS); @@ -195,11 +226,15 @@ void ScreenScraperRequest::process(const std::unique_ptr& req, std::vec std::string err = ss.str(); //setError(err); Don't consider it an error -> Request is a success. Simply : Game is not found LOG(LogWarning) << err; - - return; + + if (Utils::String::toLower(content).find("maximum threads per minute reached") != std::string::npos) + return false; + + return true; } processGame(doc, results); + return true; } pugi::xml_node ScreenScraperRequest::findMedia(pugi::xml_node media_list, std::vector mediaNames, std::string region) diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index b76c76b5c1..16f066472f 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -57,7 +57,7 @@ class ScreenScraperRequest : public ScraperHttpRequest } configuration; protected: - void process(const std::unique_ptr& req, std::vector& results) override; + bool process(const std::unique_ptr& req, std::vector& results) override; std::string ensureUrl(const std::string url); void processList(const pugi::xml_document& xmldoc, std::vector& results); diff --git a/es-app/src/scrapers/md5.cpp b/es-app/src/scrapers/md5.cpp new file mode 100644 index 0000000000..c876ab00b2 --- /dev/null +++ b/es-app/src/scrapers/md5.cpp @@ -0,0 +1,369 @@ +/* MD5 +converted to C++ class by Frank Thilo (thilo@unix-ag.org) +for bzflag (http://www.bzflag.org) + +based on: + +md5.h and md5.c +reference implemantion of RFC 1321 + +Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All +rights reserved. + +License to copy and use this software is granted provided that it +is identified as the "RSA Data Security, Inc. MD5 Message-Digest +Algorithm" in all material mentioning or referencing this software +or this function. + +License is also granted to make and use derivative works provided +that such works are identified as "derived from the RSA Data +Security, Inc. MD5 Message-Digest Algorithm" in all material +mentioning or referencing the derived work. + +RSA Data Security, Inc. makes no representations concerning either +the merchantability of this software or the suitability of this +software for any particular purpose. It is provided "as is" +without express or implied warranty of any kind. + +These notices must be retained in any copies of any part of this +documentation and/or software. + +*/ + +/* interface header */ +#include "md5.h" + +/* system implementation headers */ +#include + + +// Constants for MD5Transform routine. +#define S11 7 +#define S12 12 +#define S13 17 +#define S14 22 +#define S21 5 +#define S22 9 +#define S23 14 +#define S24 20 +#define S31 4 +#define S32 11 +#define S33 16 +#define S34 23 +#define S41 6 +#define S42 10 +#define S43 15 +#define S44 21 + +/////////////////////////////////////////////// + +// F, G, H and I are basic MD5 functions. +inline MD5::uint4 MD5::F(uint4 x, uint4 y, uint4 z) { + return x&y | ~x&z; +} + +inline MD5::uint4 MD5::G(uint4 x, uint4 y, uint4 z) { + return x&z | y&~z; +} + +inline MD5::uint4 MD5::H(uint4 x, uint4 y, uint4 z) { + return x^y^z; +} + +inline MD5::uint4 MD5::I(uint4 x, uint4 y, uint4 z) { + return y ^ (x | ~z); +} + +// rotate_left rotates x left n bits. +inline MD5::uint4 MD5::rotate_left(uint4 x, int n) { + return (x << n) | (x >> (32 - n)); +} + +// FF, GG, HH, and II transformations for rounds 1, 2, 3, and 4. +// Rotation is separate from addition to prevent recomputation. +inline void MD5::FF(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac) { + a = rotate_left(a + F(b, c, d) + x + ac, s) + b; +} + +inline void MD5::GG(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac) { + a = rotate_left(a + G(b, c, d) + x + ac, s) + b; +} + +inline void MD5::HH(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac) { + a = rotate_left(a + H(b, c, d) + x + ac, s) + b; +} + +inline void MD5::II(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac) { + a = rotate_left(a + I(b, c, d) + x + ac, s) + b; +} + +////////////////////////////////////////////// + +// default ctor, just initailize +MD5::MD5() +{ + init(); +} + +////////////////////////////////////////////// + +// nifty shortcut ctor, compute MD5 for string and finalize it right away +MD5::MD5(const std::string &text) +{ + init(); + update(text.c_str(), text.length()); + finalize(); +} + +MD5::MD5(char * Input, long length) +{ + init(); + update(Input, length); + finalize(); +} + +////////////////////////////// + +void MD5::init() +{ + finalized = false; + + count[0] = 0; + count[1] = 0; + + // load magic initialization constants. + state[0] = 0x67452301; + state[1] = 0xefcdab89; + state[2] = 0x98badcfe; + state[3] = 0x10325476; +} + +////////////////////////////// + +// decodes input (unsigned char) into output (uint4). Assumes len is a multiple of 4. +void MD5::decode(uint4 output[], const uint1 input[], size_type len) +{ + for (unsigned int i = 0, j = 0; j < len; i++, j += 4) + output[i] = ((uint4)input[j]) | (((uint4)input[j + 1]) << 8) | + (((uint4)input[j + 2]) << 16) | (((uint4)input[j + 3]) << 24); +} + +////////////////////////////// + +// encodes input (uint4) into output (unsigned char). Assumes len is +// a multiple of 4. +void MD5::encode(uint1 output[], const uint4 input[], size_type len) +{ + for (size_type i = 0, j = 0; j < len; i++, j += 4) { + output[j] = input[i] & 0xff; + output[j + 1] = (input[i] >> 8) & 0xff; + output[j + 2] = (input[i] >> 16) & 0xff; + output[j + 3] = (input[i] >> 24) & 0xff; + } +} + +////////////////////////////// + +// apply MD5 algo on a block +void MD5::transform(const uint1 block[blocksize]) +{ + uint4 a = state[0], b = state[1], c = state[2], d = state[3], x[16]; + decode(x, block, blocksize); + + /* Round 1 */ + FF(a, b, c, d, x[0], S11, 0xd76aa478); /* 1 */ + FF(d, a, b, c, x[1], S12, 0xe8c7b756); /* 2 */ + FF(c, d, a, b, x[2], S13, 0x242070db); /* 3 */ + FF(b, c, d, a, x[3], S14, 0xc1bdceee); /* 4 */ + FF(a, b, c, d, x[4], S11, 0xf57c0faf); /* 5 */ + FF(d, a, b, c, x[5], S12, 0x4787c62a); /* 6 */ + FF(c, d, a, b, x[6], S13, 0xa8304613); /* 7 */ + FF(b, c, d, a, x[7], S14, 0xfd469501); /* 8 */ + FF(a, b, c, d, x[8], S11, 0x698098d8); /* 9 */ + FF(d, a, b, c, x[9], S12, 0x8b44f7af); /* 10 */ + FF(c, d, a, b, x[10], S13, 0xffff5bb1); /* 11 */ + FF(b, c, d, a, x[11], S14, 0x895cd7be); /* 12 */ + FF(a, b, c, d, x[12], S11, 0x6b901122); /* 13 */ + FF(d, a, b, c, x[13], S12, 0xfd987193); /* 14 */ + FF(c, d, a, b, x[14], S13, 0xa679438e); /* 15 */ + FF(b, c, d, a, x[15], S14, 0x49b40821); /* 16 */ + + /* Round 2 */ + GG(a, b, c, d, x[1], S21, 0xf61e2562); /* 17 */ + GG(d, a, b, c, x[6], S22, 0xc040b340); /* 18 */ + GG(c, d, a, b, x[11], S23, 0x265e5a51); /* 19 */ + GG(b, c, d, a, x[0], S24, 0xe9b6c7aa); /* 20 */ + GG(a, b, c, d, x[5], S21, 0xd62f105d); /* 21 */ + GG(d, a, b, c, x[10], S22, 0x2441453); /* 22 */ + GG(c, d, a, b, x[15], S23, 0xd8a1e681); /* 23 */ + GG(b, c, d, a, x[4], S24, 0xe7d3fbc8); /* 24 */ + GG(a, b, c, d, x[9], S21, 0x21e1cde6); /* 25 */ + GG(d, a, b, c, x[14], S22, 0xc33707d6); /* 26 */ + GG(c, d, a, b, x[3], S23, 0xf4d50d87); /* 27 */ + GG(b, c, d, a, x[8], S24, 0x455a14ed); /* 28 */ + GG(a, b, c, d, x[13], S21, 0xa9e3e905); /* 29 */ + GG(d, a, b, c, x[2], S22, 0xfcefa3f8); /* 30 */ + GG(c, d, a, b, x[7], S23, 0x676f02d9); /* 31 */ + GG(b, c, d, a, x[12], S24, 0x8d2a4c8a); /* 32 */ + + /* Round 3 */ + HH(a, b, c, d, x[5], S31, 0xfffa3942); /* 33 */ + HH(d, a, b, c, x[8], S32, 0x8771f681); /* 34 */ + HH(c, d, a, b, x[11], S33, 0x6d9d6122); /* 35 */ + HH(b, c, d, a, x[14], S34, 0xfde5380c); /* 36 */ + HH(a, b, c, d, x[1], S31, 0xa4beea44); /* 37 */ + HH(d, a, b, c, x[4], S32, 0x4bdecfa9); /* 38 */ + HH(c, d, a, b, x[7], S33, 0xf6bb4b60); /* 39 */ + HH(b, c, d, a, x[10], S34, 0xbebfbc70); /* 40 */ + HH(a, b, c, d, x[13], S31, 0x289b7ec6); /* 41 */ + HH(d, a, b, c, x[0], S32, 0xeaa127fa); /* 42 */ + HH(c, d, a, b, x[3], S33, 0xd4ef3085); /* 43 */ + HH(b, c, d, a, x[6], S34, 0x4881d05); /* 44 */ + HH(a, b, c, d, x[9], S31, 0xd9d4d039); /* 45 */ + HH(d, a, b, c, x[12], S32, 0xe6db99e5); /* 46 */ + HH(c, d, a, b, x[15], S33, 0x1fa27cf8); /* 47 */ + HH(b, c, d, a, x[2], S34, 0xc4ac5665); /* 48 */ + + /* Round 4 */ + II(a, b, c, d, x[0], S41, 0xf4292244); /* 49 */ + II(d, a, b, c, x[7], S42, 0x432aff97); /* 50 */ + II(c, d, a, b, x[14], S43, 0xab9423a7); /* 51 */ + II(b, c, d, a, x[5], S44, 0xfc93a039); /* 52 */ + II(a, b, c, d, x[12], S41, 0x655b59c3); /* 53 */ + II(d, a, b, c, x[3], S42, 0x8f0ccc92); /* 54 */ + II(c, d, a, b, x[10], S43, 0xffeff47d); /* 55 */ + II(b, c, d, a, x[1], S44, 0x85845dd1); /* 56 */ + II(a, b, c, d, x[8], S41, 0x6fa87e4f); /* 57 */ + II(d, a, b, c, x[15], S42, 0xfe2ce6e0); /* 58 */ + II(c, d, a, b, x[6], S43, 0xa3014314); /* 59 */ + II(b, c, d, a, x[13], S44, 0x4e0811a1); /* 60 */ + II(a, b, c, d, x[4], S41, 0xf7537e82); /* 61 */ + II(d, a, b, c, x[11], S42, 0xbd3af235); /* 62 */ + II(c, d, a, b, x[2], S43, 0x2ad7d2bb); /* 63 */ + II(b, c, d, a, x[9], S44, 0xeb86d391); /* 64 */ + + state[0] += a; + state[1] += b; + state[2] += c; + state[3] += d; + + // Zeroize sensitive information. + memset(x, 0, sizeof x); +} + +////////////////////////////// + +// MD5 block update operation. Continues an MD5 message-digest +// operation, processing another message block +void MD5::update(const unsigned char input[], size_type length) +{ + // compute number of bytes mod 64 + size_type index = count[0] / 8 % blocksize; + + // Update number of bits + if ((count[0] += (length << 3)) < (length << 3)) + count[1]++; + count[1] += (length >> 29); + + // number of bytes we need to fill in buffer + size_type firstpart = 64 - index; + + size_type i; + + // transform as many times as possible. + if (length >= firstpart) + { + // fill buffer first, transform + memcpy(&buffer[index], input, firstpart); + transform(buffer); + + // transform chunks of blocksize (64 bytes) + for (i = firstpart; i + blocksize <= length; i += blocksize) + transform(&input[i]); + + index = 0; + } + else + i = 0; + + // buffer remaining input + memcpy(&buffer[index], &input[i], length - i); +} + +////////////////////////////// + +// for convenience provide a verson with signed char +void MD5::update(const char input[], size_type length) +{ + update((const unsigned char*)input, length); +} + +////////////////////////////// + +// MD5 finalization. Ends an MD5 message-digest operation, writing the +// the message digest and zeroizing the context. +MD5& MD5::finalize() +{ + static unsigned char padding[64] = { + 0x80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + + if (!finalized) { + // Save number of bits + unsigned char bits[8]; + encode(bits, count, 8); + + // pad out to 56 mod 64. + size_type index = count[0] / 8 % 64; + size_type padLen = (index < 56) ? (56 - index) : (120 - index); + update(padding, padLen); + + // Append length (before padding) + update(bits, 8); + + // Store state in digest + encode(digest, state, 16); + + // Zeroize sensitive information. + memset(buffer, 0, sizeof buffer); + memset(count, 0, sizeof count); + + finalized = true; + } + + return *this; +} + +////////////////////////////// + +// return hex representation of digest as string +std::string MD5::hexdigest() const +{ + if (!finalized) + return ""; + + char buf[33]; + for (int i = 0; i<16; i++) + sprintf(buf + i * 2, "%02x", digest[i]); + buf[32] = 0; + + return std::string(buf); +} + +////////////////////////////// + +std::ostream& operator<<(std::ostream& out, MD5 md5) +{ + return out << md5.hexdigest(); +} + +////////////////////////////// + +std::string md5(const std::string str) +{ + MD5 md5 = MD5(str); + + return md5.hexdigest(); +} \ No newline at end of file diff --git a/es-app/src/scrapers/md5.h b/es-app/src/scrapers/md5.h new file mode 100644 index 0000000000..3ab1baf375 --- /dev/null +++ b/es-app/src/scrapers/md5.h @@ -0,0 +1,95 @@ +/* MD5 +converted to C++ class by Frank Thilo (thilo@unix-ag.org) +for bzflag (http://www.bzflag.org) + +based on: + +md5.h and md5.c +reference implementation of RFC 1321 + +Copyright (C) 1991-2, RSA Data Security, Inc. Created 1991. All +rights reserved. + +License to copy and use this software is granted provided that it +is identified as the "RSA Data Security, Inc. MD5 Message-Digest +Algorithm" in all material mentioning or referencing this software +or this function. + +License is also granted to make and use derivative works provided +that such works are identified as "derived from the RSA Data +Security, Inc. MD5 Message-Digest Algorithm" in all material +mentioning or referencing the derived work. + +RSA Data Security, Inc. makes no representations concerning either +the merchantability of this software or the suitability of this +software for any particular purpose. It is provided "as is" +without express or implied warranty of any kind. + +These notices must be retained in any copies of any part of this +documentation and/or software. + +*/ + +#ifndef BZF_MD5_H +#define BZF_MD5_H + +#include +#include + + +// a small class for calculating MD5 hashes of strings or byte arrays +// it is not meant to be fast or secure +// +// usage: 1) feed it blocks of uchars with update() +// 2) finalize() +// 3) get hexdigest() string +// or +// MD5(std::string).hexdigest() +// +// assumes that char is 8 bit and int is 32 bit +class MD5 +{ +public: + typedef unsigned int size_type; // must be 32bit + + MD5(); + MD5(const std::string& text); + MD5(char* Input, long length); + + void update(const unsigned char *buf, size_type length); + void update(const char *buf, size_type length); + MD5& finalize(); + std::string hexdigest() const; + friend std::ostream& operator<<(std::ostream&, MD5 md5); + +private: + void init(); + typedef unsigned char uint1; // 8bit + typedef unsigned int uint4; // 32bit + enum { blocksize = 64 }; // VC6 won't eat a const static int here + + void transform(const uint1 block[blocksize]); + static void decode(uint4 output[], const uint1 input[], size_type len); + static void encode(uint1 output[], const uint4 input[], size_type len); + + bool finalized; + uint1 buffer[blocksize]; // bytes that didn't fit in last 64 byte chunk + uint4 count[2]; // 64bit counter for number of bits (lo, hi) + uint4 state[4]; // digest so far + uint1 digest[16]; // the result + + // low level logic operations + static inline uint4 F(uint4 x, uint4 y, uint4 z); + static inline uint4 G(uint4 x, uint4 y, uint4 z); + static inline uint4 H(uint4 x, uint4 y, uint4 z); + static inline uint4 I(uint4 x, uint4 y, uint4 z); + static inline uint4 rotate_left(uint4 x, int n); + static inline void FF(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac); + static inline void GG(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac); + static inline void HH(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac); + static inline void II(uint4 &a, uint4 b, uint4 c, uint4 d, uint4 x, uint4 s, uint4 ac); +}; + +std::string md5(const std::string str); + +#endif \ No newline at end of file diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index 54edd2a052..9d8fac2658 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -94,6 +94,8 @@ std::string _regGetString(HKEY hKey, const std::string &strPath, const std::stri HttpReq::HttpReq(const std::string& url) : mStatus(REQ_IN_PROGRESS), mHandle(NULL) { + mUrl = url; + mPercent = -1; mHandle = curl_easy_init(); diff --git a/es-core/src/HttpReq.h b/es-core/src/HttpReq.h index 35843fd654..0230c7a463 100644 --- a/es-core/src/HttpReq.h +++ b/es-core/src/HttpReq.h @@ -55,6 +55,8 @@ class HttpReq int getPercent() { return mPercent; } + std::string getUrl() { return mUrl; } + private: static size_t write_content(void* buff, size_t size, size_t nmemb, void* req_ptr); //static int update_progress(void* req_ptr, double dlTotal, double dlNow, double ulTotal, double ulNow); @@ -75,6 +77,7 @@ class HttpReq std::ofstream mStream; std::string mErrorMsg; + std::string mUrl; int mPercent; }; From 01d2ac83412777b394104320548bb1c2ba81e9d2 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 15 Oct 2019 17:46:46 +0200 Subject: [PATCH 178/336] Merge fix + scraper : add missing systems --- es-app/src/PlatformId.cpp | 201 +++++++++++------- es-app/src/PlatformId.h | 40 +++- es-app/src/guis/GuiMenu.cpp | 93 ++------ es-app/src/guis/GuiMetaDataEd.cpp | 5 +- es-app/src/guis/GuiScraperStart.cpp | 10 +- es-app/src/scrapers/GamesDBJSONScraper.cpp | 19 +- es-app/src/scrapers/Scraper.cpp | 40 +++- es-app/src/scrapers/ScreenScraper.cpp | 39 +++- .../src/views/gamelist/VideoGameListView.cpp | 4 +- es-core/src/ImageIO.h | 1 + es-core/src/Settings.cpp | 2 + es-core/src/Window.cpp | 46 +--- es-core/src/Window.h | 4 - es-core/src/resources/TextureData.cpp | 14 +- es-core/src/utils/FileSystemUtil.cpp | 3 + 15 files changed, 290 insertions(+), 231 deletions(-) diff --git a/es-app/src/PlatformId.cpp b/es-app/src/PlatformId.cpp index 7b4cd01462..e27eb6a781 100644 --- a/es-app/src/PlatformId.cpp +++ b/es-app/src/PlatformId.cpp @@ -1,100 +1,143 @@ #include "PlatformId.h" +#include #include namespace PlatformIds { - const char* PlatformNames[PLATFORM_COUNT + 1] = { - "unknown", // nothing set + static std::map Platforms = + { + { "unknown", PLATFORM_UNKNOWN }, + { "3do", THREEDO }, + { "amiga", AMIGA }, + { "amstradcpc", AMSTRAD_CPC }, + { "apple2", APPLE_II }, + { "arcade", ARCADE }, + { "atari800", ATARI_800 }, + { "atari2600", ATARI_2600 }, + { "atari5200", ATARI_5200 }, + { "atari7800", ATARI_7800 }, + { "atarilynx", ATARI_LYNX }, + { "atarist", ATARI_ST }, + { "atarijaguar", ATARI_JAGUAR }, + { "atarijaguarcd", ATARI_JAGUAR_CD }, + { "atarixe", ATARI_XE }, + { "colecovision", COLECOVISION }, + { "c64", COMMODORE_64 }, + { "intellivision", INTELLIVISION }, + { "macintosh", MAC_OS }, + { "xbox", XBOX }, + { "xbox360", XBOX_360 }, + { "msx", MSX }, + { "neogeo", NEOGEO }, + { "ngp", NEOGEO_POCKET }, + { "ngpc", NEOGEO_POCKET_COLOR }, + { "n3ds", NINTENDO_3DS }, + { "n64", NINTENDO_64 }, + { "nds", NINTENDO_DS }, + { "fds", FAMICOM_DISK_SYSTEM }, + { "nes", NINTENDO_ENTERTAINMENT_SYSTEM }, + { "gb", GAME_BOY }, + { "gba", GAME_BOY_ADVANCE }, + { "gbc", GAME_BOY_COLOR }, + { "gc", NINTENDO_GAMECUBE }, + { "wii", NINTENDO_WII }, + { "wiiu", NINTENDO_WII_U }, + { "virtualboy", NINTENDO_VIRTUAL_BOY }, + { "gameandwatch", NINTENDO_GAME_AND_WATCH }, + { "pc", PC }, + { "sega32x", SEGA_32X }, + { "segacd", SEGA_CD }, + { "dreamcast", SEGA_DREAMCAST }, + { "gamegear", SEGA_GAME_GEAR }, + { "genesis", SEGA_GENESIS }, + { "mastersystem", SEGA_MASTER_SYSTEM }, + { "megadrive", SEGA_MEGA_DRIVE }, + { "saturn", SEGA_SATURN }, + { "sg-1000", SEGA_SG1000 }, + { "psx", PLAYSTATION }, + { "ps2", PLAYSTATION_2 }, + { "ps3", PLAYSTATION_3 }, + { "ps4", PLAYSTATION_4 }, + { "psvita", PLAYSTATION_VITA }, + { "psp", PLAYSTATION_PORTABLE }, + { "snes", SUPER_NINTENDO }, + { "scummvm", SCUMMVM }, + { "x68000", SHARP_X6800 }, + { "pcengine", TURBOGRAFX_16 }, // (aka PC Engine) HuCards onlyy + { "pcenginecd", TURBOGRAFX_CD }, // (aka PC Engine) CD-ROMs onlynly + { "wonderswan", WONDERSWAN }, + { "wonderswancolor", WONDERSWAN_COLOR }, + { "zxspectrum", ZX_SPECTRUM }, + { "videopac", VIDEOPAC_ODYSSEY2 }, + { "vectrex", VECTREX }, + { "trs-80", TRS80_COLOR_COMPUTER }, + { "coco", TANDY }, + { "supergrafx", SUPERGRAFX }, + { "amigacd32", AMIGACD32 }, + { "amigacdtv", AMIGACDTV }, + { "atomiswave", ATOMISWAVE }, + { "cavestory", CAVESTORY }, + { "gx4000", GX4000 }, + { "lutro", LUTRO }, + { "moonlight", MOONLIGHT }, + { "naomi", NAOMI }, + { "neogeocd", NEOGEO_CD }, + { "pcfx", PCFX }, + { "pokemini", POKEMINI }, + { "prboom", PRBOOM }, + { "satellaview", SATELLAVIEW }, + { "sufami", SUFAMITURBO }, + { "zx81", ZX81 }, + + // batocera specific names + { "gb2players", GAME_BOY }, + { "gbc2players", GAME_BOY_COLOR }, + { "3ds", NINTENDO_3DS }, + { "sg1000", SEGA_SG1000 }, + { "odyssey2", VIDEOPAC_ODYSSEY2 }, + { "oricatmos", ORICATMOS }, - "3do", - "amiga", - "amstradcpc", - "apple2", - "arcade", - "atari800", - "atari2600", - "atari5200", - "atari7800", - "atarilynx", - "atarist", - "atarijaguar", - "atarijaguarcd", - "atarixe", - "colecovision", - "c64", // commodore 64 - "intellivision", - "macintosh", - "xbox", - "xbox360", - "msx", - "neogeo", - "ngp", // neo geo pocket - "ngpc", // neo geo pocket color - "n3ds", // nintendo 3DS - "n64", // nintendo 64 - "nds", // nintendo DS - "fds", // Famicom Disk System - "nes", // nintendo entertainment system - "gb", // game boy - "gba", // game boy advance - "gbc", // game boy color - "gc", // gamecube - "wii", - "wiiu", - "virtualboy", - "gameandwatch", - "pc", - "sega32x", - "segacd", - "dreamcast", - "gamegear", - "genesis", // sega genesis - "mastersystem", // sega master system - "megadrive", // sega megadrive - "saturn", // sega saturn - "sg-1000", - "psx", - "ps2", - "ps3", - "ps4", - "psvita", - "psp", // playstation portable - "snes", // super nintendo entertainment system - "scummvm", - "x6800", - "pcengine", // (aka turbografx-16) HuCards only - "pcenginecd", // (aka turbografx-16) CD-ROMs only - "wonderswan", - "wonderswancolor", - "zxspectrum", - "videopac", - "vectrex", - "trs-80", - "coco", - "odyssey2", - "supergrafx", + // windows specific systems & names + { "windows", MOONLIGHT }, + { "vpinball", VISUALPINBALL }, + { "fpinball", FUTUREPINBALL }, + { "o2em", VIDEOPAC_ODYSSEY2 }, - "ignore", // do not allow scraping for this system - "invalid" + // Misc systems + { "channelf", CHANNELF }, + { "oric", ORICATMOS }, + { "thomson", THOMSON_TO_MO }, + { "samcoupe", SAMCOUPE }, + { "OpenBOR", OPENBOR }, + { "uzebox", UZEBOX }, + { "apple2gs", APPLE2GS }, + { "Spectravideo", SPECTRAVIDEO }, + { "palm", PALMOS }, + { "daphne", DAPHNEE }, + + { "ignore", PLATFORM_IGNORE }, + { "invalid", PLATFORM_COUNT } }; PlatformId getPlatformId(const char* str) { - if(str == NULL) + if (str == nullptr) return PLATFORM_UNKNOWN; - for(unsigned int i = 1; i < PLATFORM_COUNT; i++) - { - if(strcmp(PlatformNames[i], str) == 0) - return (PlatformId)i; - } + auto it = Platforms.find(str); + if (it != Platforms.end()) + return (*it).second; return PLATFORM_UNKNOWN; } - const char* getPlatformName(PlatformId id) + std::string getPlatformName(PlatformId id) { - return PlatformNames[id]; + for (auto& it : Platforms) + if (it.second == id) + return it.first; + + return "unknown"; } } diff --git a/es-app/src/PlatformId.h b/es-app/src/PlatformId.h index 71a9d634d1..affc6bee4c 100644 --- a/es-app/src/PlatformId.h +++ b/es-app/src/PlatformId.h @@ -2,6 +2,8 @@ #ifndef ES_APP_PLATFORM_ID_H #define ES_APP_PLATFORM_ID_H +#include + namespace PlatformIds { enum PlatformId : unsigned int @@ -72,16 +74,46 @@ namespace PlatformIds VIDEOPAC_ODYSSEY2, VECTREX, TRS80_COLOR_COMPUTER, - TANDY, - ODYSSEY2, + TANDY, SUPERGRAFX, + AMIGACD32, + AMIGACDTV, + ATOMISWAVE, + CAVESTORY, + GX4000, + LUTRO, + MOONLIGHT, + NAOMI, + NEOGEO_CD, + PCFX, + POKEMINI, + PRBOOM, + SATELLAVIEW, + SUFAMITURBO, + ZX81, + + // Windows Specific + VISUALPINBALL, + FUTUREPINBALL, + + // Misc systems + CHANNELF, + ORICATMOS, + THOMSON_TO_MO, + SAMCOUPE, + OPENBOR, + UZEBOX, + APPLE2GS, + SPECTRAVIDEO, + PALMOS, + DAPHNEE, PLATFORM_IGNORE, // do not allow scraping for this system PLATFORM_COUNT }; - PlatformId getPlatformId(const char* str); - const char* getPlatformName(PlatformId id); + PlatformId getPlatformId(const char* str); + std::string getPlatformName(PlatformId id); } #endif // ES_APP_PLATFORM_ID_H diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 0675628ca3..dda9562319 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -353,84 +353,10 @@ void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptrgetSubSets(); - auto themeColorSets = ThemeData::getSubSet(themeSubSets, "colorset"); - auto themeIconSets = ThemeData::getSubSet(themeSubSets, "iconset"); - auto themeMenus = ThemeData::getSubSet(themeSubSets, "menu"); - auto themeSystemviewSets = ThemeData::getSubSet(themeSubSets, "systemview"); - auto themeGamelistViewSets = ThemeData::getSubSet(themeSubSets, "gamelistview"); - auto themeRegions = ThemeData::getSubSet(themeSubSets, "region"); - - // colorset - std::shared_ptr> theme_colorset = nullptr; - if (themeColorSets.size() > 0) - { - auto selectedColorSet = std::find(themeColorSets.cbegin(), themeColorSets.cend(), Settings::getInstance()->getString("ThemeColorSet")); - if (selectedColorSet == themeColorSets.end()) - selectedColorSet = themeColorSets.begin(); - - theme_colorset = std::make_shared >(mWindow, _("THEME COLORSET"), false); - for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) - theme_colorset->add(*it, *it, it == selectedColorSet); - - if (!themeColorSets.empty()) - themeconfig->addWithLabel(_("THEME COLORSET"), theme_colorset); - } + std::map>> options; - // iconset - std::shared_ptr> theme_iconset = nullptr; - if (themeIconSets.size() > 0) - { - auto selectedIconSet = std::find(themeIconSets.cbegin(), themeIconSets.cend(), Settings::getInstance()->getString("ThemeIconSet")); - if (selectedIconSet == themeIconSets.end()) - selectedIconSet = themeIconSets.begin(); - - theme_iconset = std::make_shared >(mWindow, _("THEME ICONSET"), false); - - for (auto it = themeIconSets.begin(); it != themeIconSets.end(); it++) - theme_iconset->add(*it, *it, it == selectedIconSet); - - if (!themeIconSets.empty()) - themeconfig->addWithLabel(_("THEME ICONSET"), theme_iconset); - } - - // menu - std::shared_ptr> theme_menu = nullptr; - if (themeMenus.size() > 0) - { - auto selectedMenu = std::find(themeMenus.cbegin(), themeMenus.cend(), Settings::getInstance()->getString("ThemeMenu")); - if (selectedMenu == themeMenus.end()) - selectedMenu = themeMenus.begin(); - - theme_menu = std::make_shared >(mWindow, _("THEME MENU"), false); - - for (auto it = themeMenus.begin(); it != themeMenus.end(); it++) - theme_menu->add(*it, *it, it == selectedMenu); - - if (!themeMenus.empty()) - themeconfig->addWithLabel(_("THEME MENU"), theme_menu); - } - - // systemview - std::shared_ptr> theme_systemview = nullptr; - if (themeSystemviewSets.size() > 0) - { - auto selectedSystemviewSet = std::find(themeSystemviewSets.cbegin(), themeSystemviewSets.cend(), Settings::getInstance()->getString("ThemeSystemView")); - if (selectedSystemviewSet == themeSystemviewSets.end()) - selectedSystemviewSet = themeSystemviewSets.begin(); - - theme_systemview = std::make_shared >(mWindow, _("THEME SYSTEMVIEW"), false); - - for (auto it = themeSystemviewSets.begin(); it != themeSystemviewSets.end(); it++) - theme_systemview->add(*it, *it, it == selectedSystemviewSet); - - if (!themeSystemviewSets.empty()) - themeconfig->addWithLabel(_("THEME SYSTEMVIEW"), theme_systemview); - } - - // gamelistview - std::shared_ptr> theme_gamelistview = nullptr; - if (themeGamelistViewSets.size() > 0) + for (std::string subset : theme->getSubSetNames()) { std::string settingName = "subset." + subset; @@ -448,8 +374,8 @@ void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptrgetString(settingName)); if (selectedColorSet == themeColorSets.end()) selectedColorSet = themeColorSets.begin(); - - std::shared_ptr> item = std::make_shared >(mWindow, _(("THEME "+Utils::String::toUpper(subset)).c_str()), false); + + std::shared_ptr> item = std::make_shared >(mWindow, _(("THEME " + Utils::String::toUpper(subset)).c_str()), false); item->setTag(settingName); for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) @@ -458,9 +384,9 @@ void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptraddWithLabel(_(("THEME " + Utils::String::toUpper(subset)).c_str()), item); - options[settingName] = item; + options[settingName] = item; } - else + else options[settingName] = nullptr; } @@ -867,6 +793,13 @@ void GuiMenu::openUISettings() Settings::getInstance()->setBool("MoveCarousel", move_carousel->getState()); }); + // clock + auto clock = std::make_shared(mWindow); + clock->setState(Settings::getInstance()->getBool("DrawClock")); + s->addWithLabel(_("SHOW CLOCK"), clock); + s->addSaveFunc( + [clock] { Settings::getInstance()->setBool("DrawClock", clock->getState()); }); + // show help auto show_help = std::make_shared(mWindow); show_help->setState(Settings::getInstance()->getBool("ShowHelpPrompts")); diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 5d77590342..118a1a88b0 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -221,7 +221,10 @@ GuiMetaDataEd::GuiMetaDataEd(Window* window, MetaDataList* md, const std::vector 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 GuiTextEditPopupKeyboard(mWindow, title, ed->getValue(), updateVal, multiLine)); + if (multiLine) + mWindow->pushGui(new GuiTextEditPopup(mWindow, title, ed->getValue(), updateVal, multiLine)); + else + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, title, ed->getValue(), updateVal, multiLine)); }); break; } diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index ae0cf39213..4b10f04176 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -27,22 +27,22 @@ GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), if (Settings::getInstance()->getString("Scraper") == "ScreenScraper") { - if (!Settings::getInstance()->getString("ScrapperImageSrc").empty() && g->metadata.get("image").empty()) + if (!Settings::getInstance()->getString("ScrapperImageSrc").empty() && !Utils::FileSystem::exists(g->metadata.get("image"))) return true; - if (Settings::getInstance()->getString("ScrapperThumbSrc").empty() && g->metadata.get("thumbnail").empty()) + if (Settings::getInstance()->getString("ScrapperThumbSrc").empty() && !Utils::FileSystem::exists(g->metadata.get("thumbnail"))) return true; - if (Settings::getInstance()->getBool("ScrapeVideos") && g->metadata.get("video").empty()) + if (Settings::getInstance()->getBool("ScrapeVideos") && !Utils::FileSystem::exists(g->metadata.get("video"))) return true; - if (Settings::getInstance()->getBool("ScrapeMarquee") && g->metadata.get("marquee").empty()) + if (Settings::getInstance()->getBool("ScrapeMarquee") && !Utils::FileSystem::exists(g->metadata.get("marquee"))) return true; return false; } else - return g->metadata.get("image").empty(); + return !Utils::FileSystem::exists(g->metadata.get("image")); }, true); diff --git a/es-app/src/scrapers/GamesDBJSONScraper.cpp b/es-app/src/scrapers/GamesDBJSONScraper.cpp index 182597aafe..20701c385b 100644 --- a/es-app/src/scrapers/GamesDBJSONScraper.cpp +++ b/es-app/src/scrapers/GamesDBJSONScraper.cpp @@ -97,8 +97,23 @@ const std::map gamesdb_new_platformid_map{ { VECTREX, "4939" }, { TRS80_COLOR_COMPUTER, "4941" }, { TANDY, "4941" }, - { ODYSSEY2, "4927" }, - { SUPERGRAFX, "34" } // The code is TurboGrafx 16, but they manage SUPERGRAFX into this one.... + { SUPERGRAFX, "34" }, // The code is TurboGrafx 16, but they manage SUPERGRAFX into this one.... + + { AMIGACD32, "4947" }, + // { AMIGACDTV, ?? }, + // { ATOMISWAVE, ?? }, + { CAVESTORY, "1" }, + // { GX4000, ?? }, + // { LUTRO, ?? }, + // { NAOMI, ?? }, + { NEOGEO_CD, "24" }, + { PCFX, "4930" }, + { POKEMINI, "4957" }, + { PRBOOM, "1" }, + { SATELLAVIEW, "6" }, + { SUFAMITURBO, "6" }, + // { ZX81, ?? }, + { MOONLIGHT, "1" }, // "PC" }; void thegamesdb_generate_json_scraper_requests(const ScraperSearchParams& params, diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index b6eae6051e..e32106cab5 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -183,12 +183,23 @@ MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const Scrape auto tmp = Settings::getInstance()->getString("ScrapperImageSrc"); auto md = search.game->metadata.get("image"); - if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperImageSrc").empty() && !search.game->metadata.get("image").empty()) + if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperImageSrc").empty() && Utils::FileSystem::exists(search.game->metadata.get("image"))) mResult.mdl.set("image", search.game->metadata.get("image")); else if (!result.imageUrl.empty()) { std::string imgPath = getSaveAsPath(search, "image", ext); + if (!search.overWriteMedias && Utils::FileSystem::exists(imgPath)) + { + mResult.mdl.set("image", imgPath); + + if (mResult.thumbnailUrl.find(mResult.imageUrl) == 0) + mResult.thumbnailUrl = ""; + + mResult.imageUrl = ""; + } + else + mFuncs.push_back(new ResolvePair( [this, result, imgPath] { @@ -205,12 +216,19 @@ MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const Scrape }, "image", result.mdl.getName())); } - if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperThumbSrc").empty() && !search.game->metadata.get("thumbnail").empty()) + if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperThumbSrc").empty() && Utils::FileSystem::exists(search.game->metadata.get("thumbnail"))) mResult.mdl.set("thumbnail", search.game->metadata.get("thumbnail")); else if (!result.thumbnailUrl.empty() && result.thumbnailUrl.find(result.imageUrl) != 0) { std::string thumbPath = getSaveAsPath(search, "thumb", ext); + if (!search.overWriteMedias && Utils::FileSystem::exists(thumbPath)) + { + mResult.mdl.set("thumbnail", thumbPath); + mResult.thumbnailUrl = ""; + } + else + mFuncs.push_back(new ResolvePair( [this, result, thumbPath] { @@ -223,12 +241,19 @@ MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const Scrape }, "thumbnail", result.mdl.getName())); } - if (!search.overWriteMedias && Settings::getInstance()->getBool("ScrapeMarquee") && !search.game->metadata.get("marquee").empty()) + if (!search.overWriteMedias && Settings::getInstance()->getBool("ScrapeMarquee") && Utils::FileSystem::exists(search.game->metadata.get("marquee"))) mResult.mdl.set("marquee", search.game->metadata.get("marquee")); else if (!result.marqueeUrl.empty()) { std::string marqueePath = getSaveAsPath(search, "marquee", ext); + if (!search.overWriteMedias && Utils::FileSystem::exists(marqueePath)) + { + mResult.mdl.set("marquee", marqueePath); + mResult.marqueeUrl = ""; + } + else + mFuncs.push_back(new ResolvePair( [this, result, marqueePath] { @@ -241,12 +266,19 @@ MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const Scrape }, "marquee", result.mdl.getName())); } - if (!search.overWriteMedias && Settings::getInstance()->getBool("ScrapeVideos") && !search.game->metadata.get("video").empty()) + if (!search.overWriteMedias && Settings::getInstance()->getBool("ScrapeVideos") && Utils::FileSystem::exists(search.game->metadata.get("video"))) mResult.mdl.set("video", search.game->metadata.get("video")); else if (!result.videoUrl.empty()) { std::string videoPath = getSaveAsPath(search, "video", ".mp4"); + if (!search.overWriteMedias && Utils::FileSystem::exists(videoPath)) + { + mResult.mdl.set("video", videoPath); + mResult.videoUrl = ""; + } + else + mFuncs.push_back(new ResolvePair( [this, result, videoPath] { diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index 5c20824409..63f537df72 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -92,9 +92,42 @@ const std::map screenscraper_platformid_map{ { VIDEOPAC_ODYSSEY2, 104 }, { VECTREX, 102 }, { TRS80_COLOR_COMPUTER, 144 }, - { TANDY, 144 }, - { ODYSSEY2, 104 }, - { SUPERGRAFX, 105 } + { TANDY, 144 }, + { SUPERGRAFX, 105 }, + + { AMIGACD32, 130 }, + { AMIGACDTV, 129 }, + { ATOMISWAVE, 53 }, + { CAVESTORY, 135 }, + { GX4000, 87 }, + { LUTRO, 206 }, + { NAOMI, 56 }, + { NEOGEO_CD, 142 }, + { PCFX, 72 }, + { POKEMINI, 211 }, + { PRBOOM, 135 }, + { SATELLAVIEW, 107 }, + { SUFAMITURBO, 108 }, + { ZX81, 77 }, + { MOONLIGHT, 138 }, // "PC Windows" + + // Windows + { VISUALPINBALL, 198 }, + { FUTUREPINBALL, 199 }, + + // Misc + { ORICATMOS, 131 }, + { CHANNELF, 80 }, + { THOMSON_TO_MO, 141 }, + { SAMCOUPE, 213 }, + { OPENBOR, 214 }, + { UZEBOX, 216 }, + { APPLE2GS, 217 }, + { SPECTRAVIDEO, 218 }, + { PALMOS, 219 }, + { DAPHNEE, 49 }, + + }; diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index 1c7a76d6e5..2416db0b95 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -290,7 +290,9 @@ void VideoGameListView::updateInfoPanel() mVideo->setImage(snapShot); mMarquee.setImage(file->getMarqueePath()/*, false, mMarquee.getMaxSizeInfo()*/); // Too slow on pi - mImage.setImage(file->getImagePath(), false, mImage.getMaxSizeInfo()); + + if (mImage != nullptr) + mImage->setImage(file->getImagePath()); mDescription.setText(file->metadata.get("desc")); mDescContainer.reset(); diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index a599bd5236..5ccc324b3a 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -19,6 +19,7 @@ class ImageIO static void flipPixelsVert(unsigned char* imagePx, const size_t& width, const size_t& height); static Vector2i adjustPictureSize(Vector2i imageSize, Vector2i maxSize, bool externSize = false); static Vector2f adjustExternPictureSizef(Vector2f imageSize, Vector2f maxSize); + static Vector2f adjustPictureSizef(Vector2f imageSize, Vector2f maxSize); }; #endif // ES_CORE_IMAGE_IO diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index b6982fff90..bbba655df9 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -232,6 +232,8 @@ void Settings::setDefaults() mStringMap["MusicDirectory"] = ""; mStringMap["UserMusicDirectory"] = ""; + mBoolMap["DrawClock"] = true; + mDefaultBoolMap = mBoolMap; mDefaultIntMap = mIntMap; mDefaultFloatMap = mFloatMap; diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 2dbc98e561..90bbf271ba 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -17,10 +17,6 @@ Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL), mClockElapsed(0) // batocera { - mTransiting = nullptr; - mTransitionOffset = 0; - ->>>>>>> e915ef84... Theming View/ Screen : Add to manage fixed components that are to be statically displayed ( crt shader, clock ) mHelp = new HelpComponent(this); mBackgroundOverlay = new ImageComponent(this); @@ -152,7 +148,6 @@ void Window::deinit(bool deinitRenderer) InputManager::getInstance()->deinit(); TextureResource::resetCache(); - TextureResource::clearQueue(); ResourceManager::getInstance()->unloadAll(); @@ -234,6 +229,9 @@ void Window::input(InputConfig* config, Input input) void Window::update(int deltaTime) { + processPostedFunctions(); + processNotificationMessages(); + if(mNormalizeNextUpdate) { mNormalizeNextUpdate = false; @@ -292,16 +290,6 @@ void Window::update(int deltaTime) } } - // hide pads // batocera - for (int i = 0; i < MAX_PLAYERS; i++) { - if (mplayerPads[i] > 0) { - mplayerPads[i] -= deltaTime; - if (mplayerPads[i] < 0) { - mplayerPads[i] = 0; - } - } - } - mTimeSinceLastInput += deltaTime; if(peekGui()) @@ -367,31 +355,6 @@ void Window::render() // pads // batocera Renderer::setMatrix(Transform4x4f::Identity()); - if (Settings::getInstance()->getBool("ShowControllerActivity")) - { - std::map playerJoysticks = InputManager::getInstance()->lastKnownPlayersDeviceIndexes(); - for (int player = 0; player < MAX_PLAYERS; player++) - { - unsigned int padcolor = 0xFFFFFF99; - -#ifndef _DEBUG - if (playerJoysticks.count(player) != 1) - continue; - - int idx = playerJoysticks[player]; - if (idx < 0 || idx >= MAX_PLAYERS) - continue; - - if (mplayerPads[idx] > 0) - padcolor = mplayerPadsIsHotkey ? 0x0000FF66 : 0xFF000066; -#endif - - float sz = Renderer::getScreenHeight() / 100.0f; - - Renderer::drawRect((player*(sz + 4)) + 2, Renderer::getScreenHeight() - sz - 2, sz, sz, padcolor); - } - } - unsigned int screensaverTime = (unsigned int)Settings::getInstance()->getInt("ScreenSaverTime"); if(mTimeSinceLastInput >= screensaverTime && screensaverTime != 0) startScreenSaver(); @@ -405,8 +368,7 @@ void Window::render() // Always call the screensaver render function regardless of whether the screensaver is active // or not because it may perform a fade on transition renderScreenSaver(); - - + for (auto extra : mScreenExtras) extra->render(transform); diff --git a/es-core/src/Window.h b/es-core/src/Window.h index 9e1552ef4c..55187f7a45 100644 --- a/es-core/src/Window.h +++ b/es-core/src/Window.h @@ -139,10 +139,6 @@ class Window std::shared_ptr mClock; - // pads // batocera - int mplayerPads[MAX_PLAYERS]; - bool mplayerPadsIsHotkey; - bool mNormalizeNextUpdate; bool mAllowSleep; diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 58853724c9..a3e5799f15 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -68,11 +68,8 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length else mSourceWidth = (mSourceHeight * svgImage->width) / svgImage->height; // FCATMP : Always keep source aspect ratio -// mWidth = (size_t)Math::round(mSourceWidth); -// mHeight = (size_t)Math::round(mSourceHeight); - - mWidth = (int) mSourceWidth; - mHeight = (int) mSourceHeight; + mWidth = (size_t)Math::round(mSourceWidth); + mHeight = (size_t)Math::round(mSourceHeight); if (mWidth == 0) { @@ -107,8 +104,13 @@ bool TextureData::initSVGFromMemory(const unsigned char* fileData, size_t length unsigned char* dataRGBA = new unsigned char[mWidth * mHeight * 4]; + double scale = ((float) ((int) mHeight)) / svgImage->height; + double scaleV = ((float) ((int) mWidth)) / svgImage->width; + if (scaleV < scale) + scale = scaleV; + NSVGrasterizer* rast = nsvgCreateRasterizer(); - nsvgRasterize(rast, svgImage, 0, 0, mHeight / svgImage->height, dataRGBA, (int)mWidth, (int)mHeight, (int)mWidth * 4); + nsvgRasterize(rast, svgImage, 0, 0, scale, dataRGBA, (int)mWidth, (int)mHeight, (int)mWidth * 4); nsvgDeleteRasterizer(rast); ImageIO::flipPixelsVert(dataRGBA, mWidth, mHeight); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 123383a700..63f704b7da 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -705,6 +705,9 @@ namespace Utils bool exists(const std::string& _path) { + if (_path.empty()) + return false; + #ifdef WIN32 DWORD dwAttr = GetFileAttributes(_path.c_str()); if (0xFFFFFFFF == dwAttr) From 7ac09232a80320cfb604141a1b1732acede4a12a Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 16 Oct 2019 17:41:09 +0200 Subject: [PATCH 179/336] md5 : use buffered reading. + fix odroid compilation --- es-app/src/FileData.cpp | 21 ------- es-app/src/FileData.h | 2 - es-app/src/Gamelist.cpp | 28 ---------- es-app/src/Gamelist.h | 1 - es-app/src/SystemData.cpp | 4 -- es-app/src/scrapers/ScreenScraper.cpp | 55 +++++++++++-------- es-app/src/scrapers/md5.cpp | 7 --- es-app/src/scrapers/md5.h | 1 - .../views/gamelist/ISimpleGameListView.cpp | 6 -- es-core/src/ThemeData.cpp | 2 +- es-core/src/components/ImageGridComponent.h | 2 - es-core/src/guis/GuiInfoPopup.cpp | 4 +- es-core/src/resources/TextureDataManager.h | 2 - 13 files changed, 34 insertions(+), 101 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 5f887ec819..fb198ba157 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -361,27 +361,6 @@ FolderData::SortType getSortTypeFromString(std::string desc) { return FileSorts::SortTypes.at(0); } -FileData* FolderData::findUniqueGameForFolder() -{ - std::vector children = getChildren(); - - if (children.size() == 1 && children.at(0)->getType() == GAME) - return children.at(0); - - for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) - { - if ((*it)->getType() == GAME) - return NULL; - - FolderData* folder = (FolderData*)(*it); - FileData* ret = folder->findUniqueGameForFolder(); - if (ret != NULL) - return ret; - } - - return NULL; -} - const std::vector FolderData::getChildrenListToDisplay() { std::vector ret; diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 4d1c06b99e..0a1f3100be 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -138,8 +138,6 @@ class FolderData : public FileData void sort(ComparisonFunction& comparator, bool ascending = true); void sort(const SortType& type); - FileData* findUniqueGameForFolder(); - FileData* FindByPath(const std::string& path); inline const std::vector& getChildren() const { return mChildren; } diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 5bf58c9368..89b6d2172d 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -94,34 +94,6 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType return NULL; } -void refactorGameFolders(SystemData* system) -{ - FolderData* root = system->getRootFolder(); - if (root == nullptr) - return; - - auto childs = root->getChildren(); - for (int i = childs.size() - 1; i >= 0; i--) - { - FileData* item = childs.at(i); - if (item != nullptr && item->getType() == FOLDER) - { - FolderData* folder = (FolderData*)item; - FileData* uniqueGame = folder->findUniqueGameForFolder(); - if (uniqueGame != nullptr) - { - childs.erase(childs.begin() + i); - - FileData* newFile = new FileData(GAME, uniqueGame->getPath(), system); - newFile->metadata = uniqueGame->metadata; - root->addChild(newFile); - - delete folder; - } - } - } -} - void parseGamelist(SystemData* system, std::unordered_map& fileMap) { std::string xmlpath = system->getGamelistPath(false); diff --git a/es-app/src/Gamelist.h b/es-app/src/Gamelist.h index 4623a0a737..efc402b792 100644 --- a/es-app/src/Gamelist.h +++ b/es-app/src/Gamelist.h @@ -12,6 +12,5 @@ void parseGamelist(SystemData* system, std::unordered_mapgetBool("IgnoreGamelist")) parseGamelist(this, fileMap); - refactorGameFolders(this); - if (mSortId >= 0 && mSortId < FileSorts::SortTypes.size()) mRootFolder->sort(FileSorts::SortTypes.at(mSortId)); else mRootFolder->sort(FileSorts::SortTypes.at(0)); - - //indexAllGameFilters(mRootFolder); } else { diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index 63f537df72..96e654a783 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -175,42 +175,49 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, if (params.nameOverride.length() == 0) { path = ssConfig.getGameSearchUrl(params.game->getFileName()); + path = Utils::String::replace(path, "%20-%20", "%20"); path += "&romtype=rom"; - } - else - { - path = ssConfig.getGameSearchUrl(params.nameOverride, true); - //path += "&romtype=jeu"; - } - // Use md5 to search scrapped game if <= 16 Mo - int length = Utils::FileSystem::getFileSize(params.game->getFullPath()); - if (length <= 16384 * 1024) - { - std::ifstream inBigArrayfile; - inBigArrayfile.open(params.game->getFullPath(), std::ios::binary | std::ios::in); - if (inBigArrayfile.is_open()) + // Use md5 to search scrapped game + int length = Utils::FileSystem::getFileSize(params.game->getFullPath()); + if (length <= 131072 * 1024) // 128 Mb max { try { - char* InFileData = new char[length]; - if (InFileData) + // 64 Kb blocks + #define MD5BUFFERSIZE 64 * 1024 + + char* buffer = new char[MD5BUFFERSIZE]; + if (buffer) { - inBigArrayfile.read(InFileData, length); + size_t size; + + FILE* file = fopen(params.game->getFullPath().c_str(), "rb"); + if (file) + { + MD5 md5 = MD5(); - MD5 md5 = MD5(InFileData, length); - std::string Temp = md5.hexdigest(); - path += "&md5=" + md5.hexdigest(); + while (size = fread(buffer, 1, MD5BUFFERSIZE, file)) + md5.update(buffer, size); - delete[] InFileData; + md5.finalize(); + + std::string val = md5.hexdigest(); + if (!val.empty()) + path += "&md5=" + val; + + fclose(file); + } + + delete buffer; } } - catch (std::bad_alloc& ex) { } - - inBigArrayfile.close(); + catch (std::bad_alloc& ex) {} } } - + else + path = ssConfig.getGameSearchUrl(params.nameOverride, true); + auto& platforms = params.system->getPlatformIds(); std::vector p_ids; diff --git a/es-app/src/scrapers/md5.cpp b/es-app/src/scrapers/md5.cpp index c876ab00b2..631e5b1973 100644 --- a/es-app/src/scrapers/md5.cpp +++ b/es-app/src/scrapers/md5.cpp @@ -115,13 +115,6 @@ MD5::MD5(const std::string &text) finalize(); } -MD5::MD5(char * Input, long length) -{ - init(); - update(Input, length); - finalize(); -} - ////////////////////////////// void MD5::init() diff --git a/es-app/src/scrapers/md5.h b/es-app/src/scrapers/md5.h index 3ab1baf375..5f1dcad422 100644 --- a/es-app/src/scrapers/md5.h +++ b/es-app/src/scrapers/md5.h @@ -54,7 +54,6 @@ class MD5 MD5(); MD5(const std::string& text); - MD5(char* Input, long length); void update(const unsigned char *buf, size_type length); void update(const char *buf, size_type length); diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 3f67112809..0cb45086c6 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -88,14 +88,8 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) FolderData* folder = NULL; if (cursor->getType() == FOLDER) - { folder = (FolderData*)cursor; - FileData* gameOfFolder = folder->findUniqueGameForFolder(); - if (gameOfFolder != NULL) - cursor = gameOfFolder; - } - if (cursor->getType() == GAME) { Sound::getFromTheme(getTheme(), getName(), "launch")->play(); diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 9d805535fc..023bf2ec31 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -1298,7 +1298,7 @@ std::vector ThemeData::getViewsOfTheme() std::vector ret; for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) { - if (it->first == "menu" || it->first == "system") + if (it->first == "menu" || it->first == "system" || it->first == "screen") continue; ret.push_back(it->first); diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index cc40b96cfa..39c39112a8 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -8,9 +8,7 @@ #include "GridTileComponent.h" #include "animations/LambdaAnimation.h" #include "Settings.h" -#ifdef _RPI_ #include "Sound.h" -#endif #define EXTRAITEMS 2 diff --git a/es-core/src/guis/GuiInfoPopup.cpp b/es-core/src/guis/GuiInfoPopup.cpp index 4703402bb1..dc859e258d 100644 --- a/es-core/src/guis/GuiInfoPopup.cpp +++ b/es-core/src/guis/GuiInfoPopup.cpp @@ -46,8 +46,8 @@ GuiInfoPopup::GuiInfoPopup(Window* window, std::string message, int duration) : float posY = Renderer::getScreenHeight() * 0.02f; // FCA TopRight - posX = Renderer::getScreenWidth()*0.98f - mSize.x()*0.98f; - posY = Renderer::getScreenHeight() * 0.02f; + // posX = Renderer::getScreenWidth()*0.98f - mSize.x()*0.98f; + // posY = Renderer::getScreenHeight() * 0.02f; setPosition(posX, posY, 0); diff --git a/es-core/src/resources/TextureDataManager.h b/es-core/src/resources/TextureDataManager.h index 69043d3c24..6ab1bd9076 100644 --- a/es-core/src/resources/TextureDataManager.h +++ b/es-core/src/resources/TextureDataManager.h @@ -9,9 +9,7 @@ #include #include #include -#ifdef _RPI_ #include -#endif class TextureData; class TextureResource; From be91f25b3df8809abd3db08d06b5b9ce33c68dab Mon Sep 17 00:00:00 2001 From: JooYoung Date: Thu, 17 Oct 2019 10:51:57 +0900 Subject: [PATCH 180/336] Translation to Korean of ES --- resources/locale/ko/emulationstation2.po | 2192 ++++++++++++++++++++++ 1 file changed, 2192 insertions(+) create mode 100644 resources/locale/ko/emulationstation2.po diff --git a/resources/locale/ko/emulationstation2.po b/resources/locale/ko/emulationstation2.po new file mode 100644 index 0000000000..56e3a7e722 --- /dev/null +++ b/resources/locale/ko/emulationstation2.po @@ -0,0 +1,2192 @@ +msgid "" +msgstr "" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"X-Generator: POEditor.com\n" +"Project-Id-Version: emulationstation\n" +"Language: ko\n" +"Plural-Forms: nplurals=1; plural=0;\n" + +#: +msgid "EMULATOR SETTINGS" +msgstr "ì—뮬레ì´í„° 설정" + +#: +msgid "LOG LEVEL" +msgstr "로그 레벨" + +#: +msgid "PLAY THEME MUSICS" +msgstr "테마 ìŒì•… 재ìƒ" + +#: +msgid "DISPLAY SONG TITLES" +msgstr "노래 제목 표시" + +#: +msgid "SHOW FAVORITES ON TOP" +msgstr "ìƒë‹¨ì— ì¦ê²¨ì°¾ê¸° 보기" + +#: +msgid "PREFERED IMAGE SOURCE" +msgstr "선호하는 ì´ë¯¸ì§€ 소스" + +#: +msgid "PREFERED THUMBNAIL SOURCE" +msgstr "선호하는 THUMBNAIL 소스" + +#: +msgid "SCRAPE MARQUEE" +msgstr "스í¬ëž˜ì¹˜ 마퀴" + +#, c-format +msgid "%i SELECTED" +msgid_plural "%i ì„ íƒ" +msgstr[0] "%i ì„ íƒ" +msgstr[1] "%i ì„ íƒ" + +#: +msgid "VIDEO SCREENSAVER SETTINGS" +msgstr "비디오 스í¬ë¦°ì„¸ì´ë²„ 설정" + +#: +msgid "SLIDESHOW SCREENSAVER SETTINGS" +msgstr "슬ë¼ì´ë“œ 쇼 화면 보호기 설정" + +#: +msgid "SCREENSAVER CONTROLS" +msgstr "스í¬ë¦°ì„¸ì´ë²„ 컨트롤" + +#: +msgid "All Games" +msgstr "모든 게임" + +#: +msgid "Only missing medias" +msgstr "ëˆ„ë½ ëœ ë¯¸ë””ì–´ ë§Œ" + +#: +msgid "always" +msgstr "í•­ìƒ" + +#: +msgid "start & end" +msgstr "시작 & ë" + +#: +msgid "never" +msgstr "결코아님" + +#: +msgid "SCRAPE VIDEOS" +msgstr "스í¬ëž© 비디오" + +#: +msgid "RESET GAMELIST CUSTOMISATIONS" +msgstr "게임 ëª©ë¡ ë§žì¶¤ 설정 초기화" + +#: +msgid "RESET" +msgstr "초기화" + +#: +msgid "SHOW FOLDERS" +msgstr "í´ë” 표시" + +#: +msgid "ARE YOU SURE YOU WANT TO CONFIGURE INPUT?" +msgstr "컨트롤러 ìž…ë ¥(INPUT)ì„ êµ¬ì„± 하시겠습니까?" + +#: +msgid "disabled" +msgstr "비활성화" + +#: +msgid "default" +msgstr "기본" + +#: +msgid "warning" +msgstr "경고" + +#: +msgid "error" +msgstr "오류" + +#: +msgid "SWAP IMAGE AFTER (SECS)" +msgstr "스왑 ì´ë¯¸ì§€ 후 (SECS)" + +#: +msgid "SHOW GAME NAME" +msgstr "게임 ì´ë¦„ 표시" + +#: +msgid "STRETCH IMAGES" +msgstr "스트레치 ì´ë¯¸ì§€" + +#: +msgid "USE CUSTOM IMAGES" +msgstr "커스텀 ì´ë¯¸ì§€ 사용" + +#: +msgid "CUSTOM IMAGE DIR" +msgstr "커스텀 ì´ë¯¸ì§€ 디렉토리" + +#: +msgid "CUSTOM IMAGE DIR RECURSIVE" +msgstr "커스텀 ì´ë¯¸ì§€ 디렉토리 RECURSIVE" + +#: +msgid "CUSTOM IMAGE FILTER" +msgstr "커스텀 ì´ë¯¸ì§€ í•„í„°" + +#: +msgid "SWAP VIDEO AFTER (SECS)" +msgstr "스왑 비디오 애프터 (SECS)" + +#: +msgid "USE OMX PLAYER FOR SCREENSAVER" +msgstr "SCREENSAVERì— OMX 플레ì´ì–´ 사용" + +#: +msgid "SHOW GAME INFO" +msgstr "게임 ì •ë³´ 표시" + +#: +msgid "SHOW GAME INFO ON SCREENSAVER" +msgstr "게임 ì •ë³´ 표시" + +#: +msgid "USE MARQUEE AS GAME INFO" +msgstr "게임 정보로 마퀴 사용" + +#: +msgid "STRETCH VIDEO ON SCREENSAVER" +msgstr "스í¬ë¦°ì„¸ì´ë²„ 스트레치 비디오" + +#: +msgid "This collection is empty" +msgstr "ì´ ì»¬ë ‰ì…˜ì€ ë¹„ì–´ 있습니다" + +#: +msgid "None" +msgstr "ì—†ìŒ" + +#: +msgid "Various" +msgstr "다양한" + +#: +msgid "among other titles." +msgstr "다른 타ì´í‹€ 중ì—서ë„" + +#: +msgid "This collection contains" +msgstr "ì´ ì»¬ë ‰ì…˜ì—는" + +#: +msgid "games, including" +msgstr "를 í¬í•¨í•œ 게임" + +#: +msgid "Loading..." +msgstr "불러오는 중 ..." + +#: +msgid "Preloading UI" +msgstr "사전로드 UI" + +#: +msgid "ADD/REMOVE GAMES TO THIS GAME COLLECTION" +msgstr "ì´ ê²Œìž„ ì»¬ë ‰ì…˜ì— ê²Œìž„ 추가 / 제거" + +#: +msgid "FINISH EDITING" +msgstr "편집 마무리" + +#: +msgid "GRID SIZE" +msgstr "GRID í¬ê¸°" + +#: +msgid "DEFAULT GRID SIZE" +msgstr "기본 GRID í¬ê¸°" + +#: +msgid "UI MODE" +msgstr "UI 모드" + +#: +msgid "PRELOAD UI" +msgstr "사전로드 UI" + +#: +msgid "VSYNC" +msgstr "VSYNC" + +#: +msgid "DISPLAY FAVORITES FIRST IN GAMELIST" +msgstr "ìƒë‹¨ì— ì¦ê²¨ì°¾ê¸° 보기" + +#: +msgid "SHOW SYSTEM NAME IN COLLECTIONS" +msgstr "컬렉션ì—서 시스템 ì´ë¦„ 표시" + +#: +msgid "OPTIMIZE IMAGES VRAM USE" +msgstr "ì´ë¯¸ì§€ 최ì í™” ë””ìŠ¤í¬ ì‚¬ìš©" + +#: +msgid "Full" +msgstr "Full" + +#: +msgid "Kiosk" +msgstr "Kiosk" + +#: +msgid "Kid" +msgstr "Kid" + +#: +msgid "GAME COLLECTION SETTINGS" +msgstr "게임 컬렉션 설정" +" " +#: +msgid "CANCEL" +msgstr "취소" + +#: +msgid "automatic" +msgstr "오토매틱" + +#: +msgid "basic" +msgstr "기본" + +#: +msgid "detailed" +msgstr "ìƒì„¸í•œ" + +#: +msgid "grid" +msgstr "그리드" + +#: +msgid "fade" +msgstr "Fade" + +#: +msgid "instant" +msgstr "즉시" + +#: +msgid "slide" +msgstr "slide" + +#: +msgid "gridex" +msgstr "그리드" + +#: +msgid "ENABLE NAVIGATION SOUNDS" +msgstr "íƒìƒ‰ 소리 활성화" + +#: +msgid "ENABLE VIDEO AUDIO" +msgstr "비디오 오디오 활성화" + +#: +msgid "APPLY FILTER" +msgstr "í•„í„° ì ìš©" + +#: +msgid "AUTOMATIC GAME COLLECTIONS" +msgstr "ìžë™ 게임 컬렉션" + +#: +msgid "SELECT COLLECTIONS" +msgstr "컬렉션 ì„ íƒ" + +#: +msgid "CUSTOM GAME COLLECTIONS" +msgstr "커스텀 게임 컬렉션" + +#: +msgid "CREATE NEW CUSTOM COLLECTION FROM THEME" +msgstr "테마ì—서 새로운 ì‚¬ìš©ìž ì •ì˜ ì»¬ë ‰ì…˜ 만들기" + +#: +msgid "GAME COLLECTION SETTINGS" +msgstr "게임 컬렉션 설정" + +#: +msgid "SELECT THEME FOLDER" +msgstr "테마 í´ë” ì„ íƒ" + +#: +msgid "CREATE NEW CUSTOM COLLECTION" +msgstr "새로운 커스텀 컬렉션 ìƒì„±" + +#: +msgid "New Collection Name" +msgstr "새로운 컬렉션 ì´ë¦„" + +#: +msgid "Loading..." +msgstr "불러오는 중 ..." + +#: +msgid "GROUP UNTHEMED CUSTOM COLLECTIONS" +msgstr "ê·¸ë£¹ì´ ìƒê°í•˜ì§€ ì•Šì€ ì»¤ìŠ¤í…€ 컬렉션" + +#: +msgid "SORT CUSTOM COLLECTIONS AND SYSTEMS" +msgstr "ì •ë ¬ 시스템 ì‚¬ìš©ìž ì •ì˜ ë° ì‹œìŠ¤í…œ" + +#: +msgid "FINISH EDITING COLLECTION" +msgstr "마무리 편집 모ìŒ" + +#: +msgid "PARSE GAMESLISTS ONLY" +msgstr "GAMESLISTS ONLY 파싱 ëª©ë¡ êµ¬ì„±" + +#: +msgid "POWER SAVER MODES" +msgstr "절전 모드" + +#: +msgid "SAVE METADATA ON EXIT" +msgstr "종료시 메타 ë°ì´í„° 저장" + +#: +msgid "SHOW HIDDEN FILES" +msgstr "숨겨진 íŒŒì¼ í‘œì‹œ" + +#: +msgid "VRAM LIMIT" +msgstr "VRAM 제한" + +#: +msgid "RESET ALL FILTERS" +msgstr "모든 í•„í„° 재설정" + +#: +msgid "GAMELIST VIEW STYLE" +msgstr "게임리스트 스타ì¼" + +#: +msgid "GAME LAUNCH TRANSITION" +msgstr "게임 시작 전환" + +#: +msgid "THREADED LOADING" +msgstr "스레드 로딩" + +#: +msgid "RANDOM" +msgstr "무작위" + +#: +msgid "Rating" +msgstr "í‰ê°€" + +#: +msgid "Released" +msgstr "출시" + +#: +msgid "Developer" +msgstr "개발ìž" + +#: +msgid "Publisher" +msgstr "발행ìž" + +#: +msgid "Genre" +msgstr "유형" + +#: +msgid "Players" +msgstr "선수" + +#: +msgid "NO GAMES FOUND - SKIP" +msgstr "ê²Œìž„ì„ ì°¾ì„ ìˆ˜ ì—†ìŒ-건너 뛰기" + +#: +msgid "RETRY" +msgstr "다시 í•´ 보다" + +#: +msgid "SKIP" +msgstr "건너 뛰기" + +#: +msgid "SEARCH FOR" +msgstr "검색 FOR" + +#: +msgid "SEARCH" +msgstr "검색 FOR" + +#: +msgid "SCRAPING IN PROGRESS" +msgstr "ì§„í–‰ì¤‘ì¸ ìŠ¤í¬ëž©" + +#: +msgid "SYSTEM" +msgstr "시스템" + +#: +msgid "subtitle text" +msgstr "ìžë§‰ í…스트" + +#: +msgid "INPUT" +msgstr "ìž…ë ¥" + +#: +msgid "search" +msgstr "검색 FOR" + +#: +msgid "STOP" +msgstr "중지" + +#: +msgid "stop (progress saved)" +msgstr "중지 (진행률 저장)" + +#: +msgid "GAME %i OF %i" +msgstr "%iì˜ ê²Œìž„ %i" + +#: +msgid "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." +msgstr "ì–´ë–¤ ì‹œìŠ¤í…œë„ ì°¾ì„ ìˆ˜ 없습니다! \n" +"시스템 구성 파ì¼ì—서 경로가 올바른지 확ì¸í•˜ê³  게임 디렉토리가 올바른 확장 ê¸°ëŠ¥ì„ ê°€ì§„ 마지막 게임 중 í•˜ë‚˜ì— ìžˆëŠ”ì§€ 확ì¸í•˜ì‹­ì‹œì˜¤. \n" +"\n" +"ìžì„¸í•œ ë‚´ìš©ì€ EMULATIONSTATION.ORG를 방문하십시오." + +#: +msgid "%i GAME SUCCESSFULLY SCRAPED!" +msgid_plural "%i ê²Œìž„ì´ ì„±ê³µì ìœ¼ë¡œ 스í¬ëž©!" +msgstr[0] "%i ê²Œìž„ì´ ì„±ê³µì ìœ¼ë¡œ 스í¬ëž©!" +msgstr[1] "%i ê²Œìž„ì´ ì„±ê³µì ìœ¼ë¡œ 스í¬ëž©!" + +#: +msgid "%i GAME SKIPPED." +msgid_plural "%i 게임 건너 뛰기." +msgstr[0] "%i 게임 건너 뛰기." +msgstr[1] "%i 게임 건너 뛰기." + +#: +msgid "OK" +msgstr "승ì¸" + +#: +msgid "EDIT METADATA" +msgstr "메타 ë°ì´í„° 편집" + +#: +msgid "SCRAPE" +msgstr "SCRAPE" + +#: +msgid "SAVE" +msgstr "저장" + +#: +msgid "THIS WILL DELETE A FILE!\n" +"ARE YOU SURE?" +msgstr "파ì¼ì„ 삭제합니다! \n" +"확실합니까?" + +#: +msgid "YES" +msgstr "예" + +#: +msgid "NO" +msgstr "아니요" + +#: +msgid "DELETE" +msgstr "지우다" + +#: +msgid "SAVE CHANGES?" +msgstr "변경 ì‚¬í•­ì„ ì €ìž¥í•©ë‹ˆê¹Œ?" + +#: +msgid "BACK" +msgstr "뒤로" + +#: +msgid "CLOSE" +msgstr "닫기" + +#: +msgid "MAIN MENU" +msgstr "ë©”ì¸ ë©”ë‰´" + +#: +msgid "KODI MEDIA CENTER" +msgstr "코디 미디어 센터" + +#: +msgid "SYSTEM SETTINGS" +msgstr "환경 설정" + +#: +msgid "VERSION" +msgstr "버전" + +#: +msgid "DISK USAGE" +msgstr "ë””ìŠ¤í¬ ì‚¬ìš©ëŸ‰" + +#: +msgid "STORAGE DEVICE" +msgstr "저장 장치" + +#: +msgid "LANGUAGE" +msgstr "LANGUAGE" + +#: +msgid "OVERCLOCK" +msgstr "오버 í´ëŸ­" + +#: +msgid "EXTREM (1100Mhz)" +msgstr "EXTREM (1100MHz)" + +#: +msgid "TURBO (1000Mhz)" +msgstr "í„°ë³´ (1000Mhz)" + +#: +msgid "HIGH (950Mhz)" +msgstr "ë†’ìŒ (950Mhz)" + +#: +msgid "NONE (700Mhz)" +msgstr "ì—†ìŒ (700Mhz)" + +#: +msgid "TURBO (1050Mhz)+" +msgstr "í„°ë³´ (1050Mhz)+" + +#: +msgid "HIGH (1050Mhz)" +msgstr "ë†’ìŒ (1050Mhz)" + +#: +msgid "NONE (900Mhz)" +msgstr "ì—†ìŒ (900Mhz)" + +#: +msgid "NONE (1200Mhz)" +msgstr "ì—†ìŒ (1200Mhz)" + +#: +msgid "NONE" +msgstr "ì—†ìŒ" + +#. NEW SETTINGS ORGANIZATION +#: +msgid "UPDATES" +msgstr "ì—…ë°ì´íЏ" + +#: +msgid "AUTO UPDATES" +msgstr "ìžë™ ì—…ë°ì´íЏ" + +#: +msgid "START UPDATE" +msgstr "ì—…ë°ì´íЏ 시작" + +#: +msgid "KODI SETTINGS" +msgstr "코디 설정" + +#: +msgid "ENABLE KODI" +msgstr "코디 활성화" + +#: +msgid "KODI AT START" +msgstr "시작시 KODI" + +#: +msgid "START KODI WITH X" +msgstr "X로 KODI 시작" + +#: +msgid "SECURITY" +msgstr "보안" + +#: +msgid "ENFORCE SECURITY" +msgstr "보안 ê°•í™”" + +#: +msgid "ROOT PASSWORD" +msgstr "루트 비밀번호" + +#: +msgid "THE SYSTEM WILL NOW REBOOT" +msgstr "ì´ì œ ì‹œìŠ¤í…œì´ ë‹¤ì‹œ 부팅ë©ë‹ˆë‹¤" + +#: +msgid "GAMES SETTINGS" +msgstr "게임 설정" + +#: +msgid "GAME RATIO" +msgstr "게임 비율" + +#: +msgid "SMOOTH GAMES" +msgstr "부드러운 게임" + +#: +msgid "REWIND" +msgstr "ë˜ê°ê¸°" + +#: +msgid "AUTO SAVE/LOAD" +msgstr "ìžë™ 저장 /로드" + +#: +msgid "SHADERS SET" +msgstr "ì‰ì´ë” 세트" + +#: +msgid "SCANLINES" +msgstr "스캔" + +#: +msgid "RETRO" +msgstr "레트로" + +#: +msgid "RETROACHIEVEMENTS SETTINGS" +msgstr "RETROACHIEVEMENTS 설정" + +#: +msgid "RETROACHIEVEMENTS" +msgstr "RETROACHIEVEMENTS" + +#: +msgid "HARDCORE MODE" +msgstr "하드 코어 모드" + +#: +msgid "USERNAME" +msgstr "ì‚¬ìš©ìž ì´ë¦„" + +#: +msgid "PASSWORD" +msgstr "암호" + +#: +msgid "ADVANCED" +msgstr "ADVANCED" + +#: +msgid "REALLY UPDATE GAMES LISTS ?" +msgstr "REALLY UPDATE GAMES LISTS ?" + +#: +msgid "UPDATE GAMES LISTS" +msgstr "게임 ëª©ë¡ ì—…ë°ì´íЏ" + +#: +msgid "CONTROLLERS SETTINGS" +msgstr "컨트롤러 설정" + +#: +msgid "UI SETTINGS" +msgstr "UI 설정" + +#: +msgid "OVERSCAN" +msgstr "오버 스캔" + +#: +msgid "LAUNCH SCREENSAVER" +msgstr "LAUNCH SCREENSAVER" + +#: +msgid "SCREENSAVER SETTINGS" +msgstr "LAUNCH SCREENSAVER" + +#: +msgid "SCREENSAVER AFTER" +msgstr "스í¬ë¦° 세ì´ë²„" + +#: +msgid "TRANSITION STYLE" +msgstr "전환 스타ì¼" + +#: +msgid "SCREENSAVER BEHAVIOR" +msgstr "스í¬ë¦° 세ì´ë²„ í–‰ë™" + +#: +msgid "random video" +msgstr "ëžœë¤ ë¹„ë””ì˜¤" + +#: +msgid "slideshow" +msgstr "슬ë¼ì´ë“œ 쇼" + +#: +msgid "dim" +msgstr "dim" + +#: +msgid "black" +msgstr "black" + +#: +msgid "SHOW FRAMERATE" +msgstr "FRAMERATE 표시" + +#: +msgid "ON-SCREEN HELP" +msgstr "ON-SCREEN HELP" + +#: +msgid "HIDE WHEN RUNNING GAME" +msgstr "ê²Œìž„ì„ ì‹¤í–‰í•  때 숨기기" + +#: +msgid "QUICK SYSTEM SELECT" +msgstr "빠른 시스템 ì„ íƒ" + +#: +msgid "THEME SET" +msgstr "THEME 설정" + +#: +msgid "SOUND SETTINGS" +msgstr "사운드 설정" + +#: +msgid "SYSTEM VOLUME" +msgstr "시스템 볼륨" + +#: +msgid "FRONTEND MUSIC" +msgstr "프론트 뮤ì§" + +#: +msgid "ONLY PLAY SYSTEM-SPECIFIC MUSIC FOLDER" +msgstr "시스템 특정 ìŒì•… í´ë” ë§Œ 재ìƒ" + +#: +msgid "FILTER GAMES BY TEXT" +msgstr "í…스트 별 í•„í„° 게임" + +#: +msgid "OUTPUT DEVICE" +msgstr "출력 장치" + +#: +msgid "HDMI" +msgstr "HDMI" + +#: +msgid "JACK" +msgstr "JACK" + +#: +msgid "AUTO" +msgstr "ìžë™" + +#: +msgid "NETWORK SETTINGS" +msgstr "ë„¤íŠ¸ì›Œí¬ ì„¤ì •" + +#: +msgid "CONNECTED" +msgstr "ì—°ê²°ë¨" + +#: +msgid "NOT CONNECTED" +msgstr "ì—°ê²°ë˜ì§€ 않ì€" + +#: +msgid "STATUS" +msgstr "ìƒíƒœ" + +#: +msgid "IP ADDRESS" +msgstr "IP 주소" + +#: +msgid "HOSTNAME" +msgstr "호스트 ì´ë¦„" + +#: +msgid "ENABLE WIFI" +msgstr "와ì´íŒŒì´ 활성화" + +#: +msgid "WIFI SSID" +msgstr "와ì´íŒŒì´ SSID" + +#: +msgid "WIFI KEY" +msgstr "와ì´íŒŒì´ 키" + +#: +msgid "WIFI ENABLED" +msgstr "와ì´íŒŒì´ 가능" + +#: +msgid "WIFI CONFIGURATION ERROR" +msgstr "와ì´íŒŒì´ 구성 오류" + +#: +msgid "SCRAPER" +msgstr "스í¬ëž©í¼" + +#: +msgid "SCRAPE FROM" +msgstr "스í¬ëž©í¼ FROM" + +#: +msgid "SCRAPE RATINGS" +msgstr "스í¬ëž©í¼ RATINGS" + +#: +msgid "SCRAPE NOW" +msgstr "스í¬ëž©í¼ NOW" + +#: +msgid "QUIT" +msgstr "종료" + +#: +msgid "QUIT EMULATIONSTATION" +msgstr "ES 종료" + +#: +msgid "RESTART EMULATIONSTATION" +msgstr "ES 재시작" + +#: +msgid "REALLY QUIT?" +msgstr "종료합니까?" + +#: +msgid "REALLY RESTART?" +msgstr "다시 시작 합니까?" + +#: +msgid "RESTART SYSTEM" +msgstr "시스템 재시작" + +#: +msgid "REALLY SHUTDOWN?" +msgstr "ì‹œìŠ¤í…œì„ ì¢…ë£Œí•©ë‹ˆê¹Œ?" + +#: +msgid "SHUTDOWN SYSTEM" +msgstr "셧다운 시스템" + +#: +msgid "Emulator" +msgstr "ì—뮬레ì´í„°" + +#: +msgid "Core" +msgstr "Core" + +#: +msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." +msgstr "컨트롤러를 구성하려고합니다. í•˜ë‚˜ì˜ ì¡°ì´ìŠ¤í‹± 만있는 경우 ë²„íŠ¼ì„ ëˆŒëŸ¬ 방향키와 건너 뛰기 ì¡°ì´ìŠ¤í‹± êµ¬ì„±ì„ êµ¬ì„±í•˜ì‹­ì‹œì˜¤. 핫키 ìš© 특수 키가없는 경우, ì„ íƒ ë²„íŠ¼ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤. 열쇠를 잡고 있지 ì•Šì€ ëª¨ë“  단추를 건너 뛰십시오. 버튼 ì´ë¦„ì€ SNES 컨트롤러를 기반으로합니다." + +#. GUIMENU +#: +msgid "CONFIGURE A CONTROLLER" +msgstr "컨트롤러 구성" + +#. Bluetooth +#: +msgid "CONTROLLER PAIRED" +msgstr "컨트롤러가 페어ë§ë¨" + +#: +msgid "UNABLE TO PAIR CONTROLLER" +msgstr "컨트롤러 íŽ˜ì–´ë§ ë¶ˆê°€" + +#: +msgid "AN ERROR OCCURED" +msgstr "오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤" + +#: +msgid "NO CONTROLLERS FOUND" +msgstr "컨트롤러 ì—†ìŒ" + +#: +msgid "PAIR A BLUETOOTH CONTROLLER" +msgstr "블루투스 컨트롤러 페어ë§" + +#: +msgid "CONTROLLERS LINKS HAVE BEEN DELETED." +msgstr "컨트롤러 ë§í¬ê°€ ì‚­ì œë˜ì—ˆìŠµë‹ˆë‹¤." + +#: +msgid "FORGET BLUETOOTH CONTROLLERS" +msgstr "블루투스 컨트롤러 FORGET" + +#: +msgid "INPUT P%i" +msgstr "ìž…ë ¥ P%i" + +#: +msgid "CHOOSE" +msgstr "ì„ íƒ" + +#: +msgid "SELECT" +msgstr "ì„ íƒ" + +#: +msgid "OPTIONS" +msgstr "옵션" + +#: +msgid "JUMP TO..." +msgstr "ì í”„ ..." + +#: +msgid "SORT GAMES BY" +msgstr "게임 ì •ë ¬ ë°©ì‹" + +#. FAVORITES +#: +msgid "FAVORITES ONLY" +msgstr "ì¦ê²¨ì°¾ê¸° 목ë¡ë§Œ" + +#: +msgid "EDIT THIS GAME'S METADATA" +msgstr "게임 메타 ë°ì´í„° 편집" + +#: +msgid "SCRAPE THESE GAMES" +msgstr "SCRAPE THESE 게임" + +#: +msgid "All Games" +msgstr "모든 게임" + +#. MISSING SCRAPPER TRANSLATIONS +#: +msgid "Only missing image" +msgstr "ëˆ„ë½ ëœ ì´ë¯¸ì§€ ë§Œ" + +#: +msgid "FILTER" +msgstr "í•„í„°" + +#: +msgid "SCRAPE THESE SYSTEMS" +msgstr "스í¬ëž©í¼ THESE 시스템" + +#: +msgid "SYSTEMS" +msgstr "시스템" + +#: +msgid "USER DECIDES ON CONFLICTS" +msgstr "사용ìžê°€ ì¶©ëŒì— 대해 ê²°ì •" + +#: +msgid "START" +msgstr "시작" + +#: +msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" +"CONTINUE ANYWAY?" +msgstr "경고 : ì„ íƒí•œ 시스템 중 ì¼ë¶€ì— í”Œëž«í¼ ì„¸íŠ¸ê°€ 없습니다. 결과가 í‰ì†Œë³´ë‹¤ ë” ì •í™•í•˜ì§€ ì•Šì„ ìˆ˜ 있습니다! \n" +"계ì†í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" + +#: +msgid "NO GAMES FIT THAT CRITERIA." +msgstr "í•„í„°ì— ë§žëŠ” ê²Œìž„ì€ ì—†ìŠµë‹ˆë‹¤." + +#: +msgid "REALLY UPDATE?" +msgstr "ì—…ë°ì´íЏ 합니까?" + +#: +msgid "NETWORK CONNECTION NEEDED" +msgstr "ë„¤íŠ¸ì›Œí¬ ì—°ê²° í•„ìš”" + +#: +msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" +msgstr "ì—…ë°ì´íŠ¸ê°€ 다운로드ë˜ë©´ ì‹œìŠ¤í…œì´ ë‹¤ì‹œ 부팅ë©ë‹ˆë‹¤" + +#: +msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" +msgstr "ì—…ë°ì´íŠ¸ì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. ì´ì œ ì‹œìŠ¤í…œì´ ë‹¤ì‹œ 부팅ë©ë‹ˆë‹¤" + +#: +msgid "NO UPDATE AVAILABLE" +msgstr "사용 가능한 ì—…ë°ì´íЏ ì—†ìŒ" + +#: +msgid "enter emulator" +msgstr "ì—뮬레ì´í„°ë¥¼ ìž…ë ¥" + +#: +msgid "enter core" +msgstr "core를 ìž…ë ¥" + +#: +msgid "Ratio" +msgstr "비율" + +#: +msgid "enter ratio" +msgstr "ë¹„ìœ¨ì„ ìž…ë ¥" + +#: +msgid "Name" +msgstr "ì´ë¦„" + +#: +msgid "enter game name" +msgstr "ê²Œìž„ëª…ì„ ìž…ë ¥í•˜ì‹­ì‹œì˜¤" + +#: +msgid "Description" +msgstr "ìƒì„¸ì„¤ëª…" + +#: +msgid "enter description" +msgstr "ìƒì„¸ì„¤ëª…ì„ ìž…ë ¥" + +#: +msgid "Image" +msgstr "ì´ë¯¸ì§€" + +#: +msgid "enter path to image" +msgstr "ì´ë¯¸ì§€ 경로를 입력하십시오" + +#: +msgid "Thumbnail" +msgstr "ì¸ë„¤ì¼" + +#: +msgid "enter path to thumbnail" +msgstr "ì„¬ë„¤ì¼ ê²½ë¡œë¥¼ 입력하십시오" + +#: +msgid "enter rating" +msgstr "Rating ìž…ë ¥" + +#: +msgid "Release date" +msgstr "출시ì¼" + +#: +msgid "enter release date" +msgstr "출시ì¼ì„ 입력하십시오" + +#: +msgid "enter game developer" +msgstr "게임 개발ìžë¥¼ ìž…ë ¥" + +#: +msgid "enter game publisher" +msgstr "게임 게시ìžë¥¼ ìž…ë ¥" + +#: +msgid "enter game genre" +msgstr "게임 ìž¥ë¥´ì— ë“¤ì–´ê°€ë‹¤" + +#: +msgid "enter number of players" +msgstr "플레ì´ì–´ 수를 입력하십시오" + +#: +msgid "Favorite" +msgstr "ì¦ê²¨ì°¾ê¸°" + +#: +msgid "enter favorite" +msgstr "ì¦ê²¨ì°¾ê¸° 등ë¡" + +#: +msgid "Region" +msgstr "지역" + +#: +msgid "enter region" +msgstr "ì§€ì—­ì„ ìž…ë ¥" + +#: +msgid "Romtype" +msgstr "롬 타입" + +#: +msgid "enter romtype" +msgstr "롬 타입 ìž…ë ¥" + +#: +msgid "Hidden" +msgstr "숨겨진" + +#: +msgid "HIDDEN" +msgstr "숨겨진" + +#: +msgid "NAME" +msgstr "ì´ë¦„" + +#: +msgid "EMULATOR" +msgstr "ì—뮬레ì´í„°" + +#: +msgid "FAVORITE" +msgstr "ì¦ê²¨ì°¾ê¸°" + +#: +msgid "set hidden" +msgstr "숨기기 활성화" + +#: +msgid "Play count" +msgstr "í”Œë ˆì´ íšŸìˆ˜" + +#: +msgid "enter number of times played" +msgstr "í”Œë ˆì´ íšŸìˆ˜ë¥¼ 입력하십시오" + +#: +msgid "Last played" +msgstr "마지막 플레ì´" + +#: +msgid "enter last played date" +msgstr "마지막 í”Œë ˆì´ ë‚ ì§œë¥¼ 입력하십시오" + +#: +msgid "GAMES AVAILABLE" +msgstr "í”Œë ˆì´ ê°€ëŠ¥í•œ 게임" + +#: +msgid "%i GAME AVAILABLE" +msgid_plural "%i 게임 플레ì´ê°€ëŠ¥" +msgstr[0] "%i 게임 플레ì´ê°€ëŠ¥" +msgstr[1] "%i 게임 플레ì´ê°€ëŠ¥" + +#: +msgid "%i FAVORITE" +msgid_plural "%i ì¦ê²¨ì°¾ê¸°" +msgstr[0] "%i ì¦ê²¨ì°¾ê¸°" +msgstr[1] "%i ì¦ê²¨ì°¾ê¸°" + +#: +msgid "%i GAME SUCCESSFULLY SCRAPED!" +msgid_plural "%i ê²Œìž„ì´ ì„±ê³µì ìœ¼ë¡œ 스í¬ëž©!" +msgstr[0] "%i ê²Œìž„ì´ ì„±ê³µì ìœ¼ë¡œ 스í¬ëž©!" +msgstr[1] "%i ê²Œìž„ì´ ì„±ê³µì ìœ¼ë¡œ 스í¬ëž©!" + +#: +msgid "SCROLL" +msgstr "스í¬ë¡¤" + +#: +msgid "LAUNCH" +msgstr "실행" + +#: +msgid "Times played" +msgstr "í”Œë ˆì´ íšŸìˆ˜" + +#: +msgid "MENU" +msgstr "메뉴" + +#: +msgid "FILENAME, ASCENDING" +msgstr "FILENAME, 오름차순" + +#: +msgid "FILENAME, DESCENDING" +msgstr "FILENAME, 내림차순" + +#: +msgid "RATING, ASCENDING" +msgstr "RATING, 오름차순" + +#: +msgid "RATING, DESCENDING" +msgstr "RATING, ê²°ì •" + +#: +msgid "TIMES PLAYED, ASCENDING" +msgstr "í”Œë ˆì´ í•œ 시간, 오름차순" + +#: +msgid "TIMES PLAYED, DESCENDING" +msgstr "í”Œë ˆì´ í•œ 시간, 내림차순" + +#: +msgid "LAST PLAYED, ASCENDING" +msgstr "마지막으로 플레ì´, 오름차순" + +#: +msgid "LAST PLAYED, DESCENDING" +msgstr "마지막 플레ì´, ê²°ì •" + +#: +msgid "WORKING..." +msgstr "WORKING..." + +#: +msgid "CHANGE" +msgstr "CHANGE" + +#: +msgid "never" +msgstr "결코아님" + +#: +msgid "just now" +msgstr "just now" + +#: +msgid "%i sec ago" +msgid_plural "%i ì´ˆ ì „" +msgstr[0] "%i ì´ˆ ì „" +msgstr[1] "%i ì´ˆ ì „" + +#: +msgid "%i min ago" +msgid_plural "%i ë¶„ ì „" +msgstr[0] "%i ë¶„ ì „" +msgstr[1] "%i ë¶„ ì „" + +#: +msgid "%i hour ago" +msgid_plural "%i 시간 ì „" +msgstr[0] "%i 시간 ì „" +msgstr[1] "%i 시간 ì „" + +#: +msgid "%i day ago" +msgid_plural "%i ì¼ ì „" +msgstr[0] "%i ì¼ ì „" +msgstr[1] "%i ì¼ ì „" + +#: +msgid "unknown" +msgstr "알 수 없는" + +#: +msgid "SELECT ALL" +msgstr "ëª¨ë‘ ì„ íƒ" + +#: +msgid "SELECT NONE" +msgstr "ì—†ìŒ" + +#: +msgid "%i SELECTED" +msgid_plural "%i SELECTED" +msgstr[0] "%i ì„ íƒ" +msgstr[1] "%i ì„ íƒ's" + +#: +msgid "UP" +msgstr "위(UP)" + +#: +msgid "DOWN" +msgstr "아래(DOWN)" + +#: +msgid "LEFT" +msgstr "좌(LEFT)" + +#: +msgid "RIGHT" +msgstr "ìš°(RIGHT)" + +#: +msgid "JOYSTICK 1 UP" +msgstr "ì¡°ì´ìŠ¤í‹± 1 위(UP)" + +#: +msgid "JOYSTICK 1 LEFT" +msgstr "ì¡°ì´ìŠ¤í‹± 1 좌(LEFT)" + +#: +msgid "JOYSTICK 2 UP" +msgstr "ì¡°ì´ìŠ¤í‹± 2 위(UP)" + +#: +msgid "JOYSTICK 2 LEFT" +msgstr "ì¡°ì´ìŠ¤í‹± 2 좌(LEFT)" + +#: +msgid "PAGE UP" +msgstr "페ì´ì§€ 위로" + +#: +msgid "PAGE DOWN" +msgstr "페ì´ì§€ 아래로" + +#: +msgid "HOTKEY" +msgstr "핫키" + +#: +msgid "CONFIGURING" +msgstr "구성" + +#: +msgid "KEYBOARD" +msgstr "키보드" + +#: +msgid "GAMEPAD %i" +msgstr "게임패드 %i" + +#. Config controllers missing translation +#: +msgid "PRESS ANYTHING" +msgstr "아무버튼ì´ë‚˜ 누르십시오" + +#: +msgid "ALREADY TAKEN" +msgstr "ì´ë¯¸ 사용중" + +#: +msgid "DISCARD CHANGES" +msgstr "변경 ì‚¬í•­ì„ ì·¨ì†Œ" + +#: +msgid "WELCOME" +msgstr "어서 오세요." + +#: +msgid "CONFIGURE INPUT" +msgstr "컨트롤러 ìž…ë ¥ 구성" + +#: +msgid "%i GAMEPAD DETECTED" +msgid_plural "%i GAMEPADS DETECTED" +msgstr[0] "%i 컨트롤러 ê°ì§€" +msgstr[1] "%i 여러 컨트롤러 ê°ì§€" + +#: +msgid "NO GAMEPADS DETECTED" +msgstr "컨트롤러 ì—†ìŒ" + +#: +msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." +msgstr "장치를 구성하기 위해 Aë²„íŠ¼ì„ ê¾¸ìš± 누릅니다." + +#: +msgid "PRESS F4 TO QUIT AT ANY TIME." +msgstr "언제든지 종료하려면 F4를 누르십시오." + +#: +msgid "PRESS ESC OR THE HOTKEY TO CANCEL." +msgstr "ESC ë˜ëŠ” HOTKEY를 눌러 취소하십시오." + +#: +msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" +msgstr "KODI 미디어 센터를 시작 하시겠습니까?" + +#: +msgid "LOADING..." +msgstr "로딩 중 ..." + +#: +msgid "PLEASE WAIT..." +msgstr "기다려주십시오 ..." + +#: +msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" +msgstr "메타 ë°ì´í„°ë¥¼ 저장하지 않고 완전히 종료 하시겠습니까?" + +#: +msgid "FAST SHUTDOWN SYSTEM" +msgstr "빠른 시스템 종료" + +#: +msgid "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." +msgstr "ì–´ë–¤ ì‹œìŠ¤í…œë„ ì°¾ì„ ìˆ˜ 없습니다! \n" +"\n" +"ìžì„¸í•œ ë‚´ìš©ì€ emulationstation.org를 방문하십시오!" + +#: +msgid "ON SCREEN KEYBOARD" +msgstr "ON 스í¬ë¦° 키보드" + +#: +msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" +msgstr "SHIFTS FOR UPPER,LOWER, AND SPECIAL" + +#: +msgid "SPACE" +msgstr "SPACE" + +#: +msgid "DELETE A CHAR" +msgstr "DELETE A CHAR" + +#: +msgid "SHIFT" +msgstr "SHIFT" + +#: +msgid "STOP EDITING" +msgstr "편집 중지" + +#: +msgid "MOVE CURSOR" +msgstr "ì´ë™ 커서" + +#: +msgid "EDIT" +msgstr "편집" + +#: +msgid "ACCEPT RESULT" +msgstr "ìˆ˜ë½ ê²°ê³¼" + +#: +msgid "FILENAME" +msgstr "íŒŒì¼ ì´ë¦„" + +#: +msgid "RATING" +msgstr "í‰ê°€" + +#: +msgid "TIMES PLAYED" +msgstr "í”Œë ˆì´ í•œ 시간" + +#: +msgid "LAST PLAYED" +msgstr "마지막으로 재ìƒ" + +#: +msgid "NUMBER OF PLAYERS" +msgstr "플레ì´ì–´ 수" + +#: +msgid "DEVELOPER" +msgstr "개발ìž" + +#: +msgid "GENRE" +msgstr "유형" + +#: +msgid "SHOW HIDDEN" +msgstr "숨겨진 íŒŒì¼ í‘œì‹œ" + +#: +msgid "EXTREM (1400Mhz)" +msgstr "EXTREM (1400Mhz)" + +#: +msgid "TURBO (1350Mhz)" +msgstr "í„°ë³´ (1350Mhz)" + +#: +msgid "HIGH (1300Mhz)" +msgstr "ë†’ìŒ (1300Mhz)" + +#: +msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" +"IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." +msgstr "í„°ë³´ ë° ê·¹í•œ 오버 í´ëŸ­ 사전 설정으로 ì¸í•´ ì‹œìŠ¤í…œì´ ë¶ˆì•ˆì •í•´ì§ˆ 수 있으므로 ìžì‹ ì˜ ì±…ìž„í•˜ì— ì‚¬ìš©í•´ì•¼í•©ë‹ˆë‹¤. \n" +"SI VOUS CONTINUEZ, LE SYSTÈME REDÉMARRERA MAINTENANT." + +#: +msgid "%i GAME HIDDEN" +msgid_plural "%i GAMES HIDDEN" +msgstr[0] "%i 숨겨진 게임" +msgstr[1] "%i 숨겨진 게임's" + +#: +msgid "Start kodi media player." +msgstr "kodi 미디어 플레ì´ì–´ë¥¼ 시작하십시오." + +#: +msgid "Show how much space is used on your SHARE partition, located either on the SDCARD or on an external drive. The information shows how much GB are used and how much GB your storage has overall (example 13GB/26GB)." +msgstr "SDCARD ë˜ëŠ” 외부 드ë¼ì´ë¸Œì—있는 SHARE 파티션ì—서 사용 ëœ ê³µê°„ì„ í‘œì‹œí•˜ì‹­ì‹œì˜¤. ì´ ì •ë³´ëŠ” 사용 ëœ GB 용량과 ìŠ¤í† ë¦¬ì§€ì˜ ì „ì²´ GB 용량 (예 : 13GB / 26GB)ì„ ë³´ì—¬ì¤ë‹ˆë‹¤." + +#: +msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." +msgstr "게임 표시, 비율, í•„í„° (ì‰ì´ë”), ìžë™ 저장 ë°ë¡œë“œ ë° ì†Œê¸‰ ê³„ì •ì„ êµ¬ì„±í•©ë‹ˆë‹¤." + +#: +msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." +msgstr "게임 ë¹„ìœ¨ì€ ì´ë¯¸ì§€ 너비와 ì´ë¯¸ì§€ ë†’ì´ ê°„ì˜ ë¹„ìœ¨ìž…ë‹ˆë‹¤. AUTO를 사용하여 ì—뮬레ì´í„°ê°€ ì›ëž˜ 게임 ë¹„ìœ¨ì„ ì„ íƒí•˜ë„ë¡í•˜ì—¬ 최ìƒì˜ 레트로 게임 ê²½í—˜ì„ ì œê³µí•©ë‹ˆë‹¤." + +#: +msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." +msgstr "게임 ì´ë¯¸ì§€ë¥¼ 부드럽게합니다. ì´ ì˜µì…˜ì€ ì´ì¤‘ 선형 í•„í„°ë§ì„ 사용하여 ì´ë¯¸ì§€ë¥¼ 부드럽게 만듭니다." + +#: +msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." +msgstr "ì´ ì˜µì…˜ì„ ì‚¬ìš©í•˜ë©´ 몬스터가 죽거나 다른 실수를 한 경우 ê²Œìž„ì„ ë˜ê°ìŠµë‹ˆë‹¤. 게임 ë‚´ì—서 HOTKEY + LEFT ëª…ë ¹ì„ ì‚¬ìš©í•˜ì—¬ ë˜ê°ìŠµë‹ˆë‹¤." + +#: +msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." +msgstr "ê²Œìž„ì„ ì¢…ë£Œ í•  때 ìƒíƒœë¥¼ ìžë™ 저장하고 ê²Œìž„ì„ ì‹œìž‘í•  때 마지막으로 ì €ìž¥ëœ ìƒíƒœë¥¼ ìžë™ë¡œë“œí•©ë‹ˆë‹¤." + +#: +msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." +#VALUE! + +#: +msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." +msgstr "ì…°ì´ë”는 게임 ë Œë”ë§ì„위한 필터와 같습니다. 여기ì—서 ê° ì‹œìŠ¤í…œì— ëŒ€í•´ ì„ íƒëœ ì‰ì´ë” ëª¨ìŒ ì¸ ì‰ì´ë” 세트를 ì„ íƒí•  수 있습니다. HOTKEY + L2 ë˜ëŠ” HOTKEY + R2를 사용하여 게임 ë‚´ì—서 ì…°ì´ë”를 변경할 ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤." + +#: +msgid "Enable or disable RetroAchievements in games." +msgstr "게임ì—서 RetroAchievements를 활성화 ë˜ëŠ” 비활성화합니다." + +#: +msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." +#VALUE! + + +#: +msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" +msgstr "retroachievements.org 웹 사ì´íŠ¸ëŠ” NES, SNES, GB, GBC, GBA, Genesis / Megadrive, TurboGrafx16 / PCEngine 등과 ê°™ì€ í”Œëž«í¼ì—서 ë„ì „ / ì—…ì  / 트로피를 제안합니다! retroachievements.orgì— ê³„ì •ì„ ë§Œë“¤ê³  ë„ì „ 과제를 시작하십시오!" + +#: +msgid "Add and configure up to 5 controllers." +msgstr "최대 5 ê°œì˜ ì»¨íŠ¸ë¡¤ëŸ¬ë¥¼ 추가하고 구성하십시오." + +#: +msgid "Start the screensaver after N minutes." +msgstr "N ë¶„ 후 화면 보호기를 시작하십시오." + +#: +msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." +msgstr "화면 보호기 ë™ìž‘ì„ ì„¤ì •í•˜ì‹­ì‹œì˜¤. DIMì€ í™”ë©´ ì¡°ëª…ì„ ì¤„ì´ê³  BLACKì€ í™”ë©´ì„ ê²€ê²Œ 만듭니다." + +#: +msgid "Shows a help at the bottom of the screen which displays commands you can use." +msgstr "화면 í•˜ë‹¨ì— ì‚¬ìš©í•  수있는 ëª…ë ¹ì„ í‘œì‹œí•˜ëŠ” ë„움ë§ì´ 표시ë©ë‹ˆë‹¤." + +#: +msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." +msgstr "활성화ë˜ë©´ LEFT ë˜ëŠ” RIGHT를 눌러 게임 목ë¡ì„ íƒìƒ‰í•˜ëŠ” ë™ì•ˆ ì‹œìŠ¤í…œê°„ì— ì „í™˜ í•  수 있습니다." + +#: +msgid "Updates the gamelists, if you added games since the last boot." +msgstr "마지막 부팅 ì´í›„ ê²Œìž„ì„ ì¶”ê°€ 한 경우 게임 목ë¡ì„ ì—…ë°ì´íŠ¸í•©ë‹ˆë‹¤." + +#: +msgid "Set the volume of the sound output for the frontend and the games." +msgstr "프론트 엔드 ë° ê²Œìž„ì˜ ì‚¬ìš´ë“œ 출력 ë³¼ë¥¨ì„ ì„¤ì •í•˜ì‹­ì‹œì˜¤." + +#: +msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." +msgstr "ê²Œìž„ì— ëŒ€í•œ 정보와 시ê°ì  정보를 얻으십시오. 스í¬ë ˆì´í¼ëŠ” 다른 서버ì—서 ê²Œìž„ì— ëŒ€í•œ 메타 ë°ì´í„°ì™€ ë¹„ì£¼ì–¼ì„ ë‹¤ìš´ë¡œë“œí•˜ê³  EmulationStationì˜ ì‚¬ìš©ìž ê²½í—˜ì„ ì™„ì „ížˆ í–¥ìƒì‹œí‚µë‹ˆë‹¤." + +#: +msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." +msgstr "스í¬ëž© í•  서버를 ì„ íƒí•˜ì‹­ì‹œì˜¤. SCREENSCRAPER 서버는 권장ë˜ë©° www.screenscraper.frì„ ê¸°ë°˜ìœ¼ë¡œí•˜ë©° 사용 가능한 언어로 게임 ë°ì´í„°ë¥¼ 스í¬ëž©í•©ë‹ˆë‹¤." + +#: +msgid "Begin the scrape process with the configuration shown below." +msgstr "아래 í‘œì‹œëœ êµ¬ì„±ìœ¼ë¡œ 스í¬ëž© 프로세스를 시작하십시오." + +#: +msgid "Scrape and display game ratings." +msgstr "게임 ë“±ê¸‰ì„ ê¸ì–´ 모으고 표시하십시오." + +#: +msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." +msgstr "고급 설정. ì´ ë©”ë‰´ì—서 ê°’ì„ ë³€ê²½í•˜ê¸° ì „ì— í˜„ìž¬ ìˆ˜í–‰ì¤‘ì¸ ìž‘ì—…ì„ ì•Œê³  있는지 확ì¸í•˜ì‹­ì‹œì˜¤." + +#: +msgid "Overclock your board to increase the performance.\n" + +msgstr "ì„±ëŠ¥ì„ ë†’ì´ë ¤ë©´ 보드를 오버 í´ë¡œí‚¹í•˜ì‹­ì‹œì˜¤. \n" + + + +#: +msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" + +msgstr "roms 디렉토리ì—있는 gamelist.xml 파ì¼ì— í¬í•¨ ëœ ê²Œìž„ ë§Œ 표시합니다. \n" + + +#: +msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." +msgstr "ì´ ì˜µì…˜ì„ ì‚¬ìš©í•˜ë©´ ì„ íƒí•œ ì‹œìŠ¤í…œì„ ê³ ì • 모드로 설정할 수 있습니다. ì´ ì˜µì…˜ì„ í™œì„±í™”í•˜ë©´ 사용ìžëŠ” 다른 ì‹œìŠ¤í…œì— ì•¡ì„¸ìŠ¤ í•  수 없습니다." + + +#: +msgid "Always display the basic gamelist view, even if you have scraped your games." +msgstr "ê²Œìž„ì„ í기 한 경우ì—ë„ í•­ìƒ ê¸°ë³¸ 게임 목ë¡ë³´ê¸°ë¥¼ 표시하십시오." + +#: +msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" +msgstr "Kodi 활성화 ë˜ëŠ” 비활성화, Kodi 시작 ì‚¬ìš©ìž ì •ì˜, X ë²„íŠ¼ì„ í™œì„±í™”í•˜ì—¬ Kodi 시작" + +#: +msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." +msgstr "Kodi를 활성화 ë˜ëŠ” 비활성화합니다. kodiê°€ 비활성화ë˜ì–´ 있으면 X 버튼으로 시작하거나 부팅시 ìžë™ìœ¼ë¡œ 시작할 수 없습니다. 메뉴 í•­ëª©ë„ ì œê±°ë©ë‹ˆë‹¤." + +#: +msgid "Use the X button to start Kodi." +msgstr "X ë²„íŠ¼ì„ ì‚¬ìš©í•˜ì—¬ Kodi를 시작하십시오." + +#: +msgid "Automatically start into Kodi on boot." +msgstr "부팅시 Kodi를 ìžë™ìœ¼ë¡œ 시작합니다." + +#: +msgid "Show the framerate in EmulationStation and in game." +msgstr "EmulationStation ë° ê²Œìž„ì—서 프레임 ì†ë„를 표시하십시오." + +#: +msgid "Select which emulator to use when you start a game for this system." +msgstr "ì´ ì‹œìŠ¤í…œì—서 ê²Œìž„ì„ ì‹œìž‘í•  때 사용할 ì—뮬레ì´í„°ë¥¼ ì„ íƒí•˜ì‹­ì‹œì˜¤." + +#: +msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." +msgstr "ì„ íƒí•œ ì—뮬레ì´í„°ì— 사용할 코어를 ì„ íƒí•˜ì‹­ì‹œì˜¤. 예를 들어, LIBRETRO ì—뮬레ì´í„°ì—는 Super Nintendo ê²Œìž„ì„ ì‹¤í–‰í•˜ê¸°ìœ„í•œ ë§Žì€ ì½”ì–´ê°€ 있습니다. 여기서 ì„ íƒí•œ 기본 코어는 게임 별 설정ì—서 ìž¬ì •ì˜ ë  ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤." + +#: +msgid "USE COMPOSED VISUALS" +msgstr "USE COMPOSED VISUALS" + +#: +msgid "CHECK UPDATES" +msgstr "ì—…ë°ì´íЏ 확ì¸" + +#: +msgid "UPDATE TYPE" +msgstr "ì—…ë°ì´íЏ 유형" + +#: +msgid "INTEGER SCALE (PIXEL PERFECT)" +msgstr "INTEGER SCALE (PIXEL PERFECT)" + +#: +msgid "ADVANCED SETTINGS" +msgstr "고급 설정" + +#: +msgid "BOOT SETTINGS" +msgstr "부팅 설정" + +#: +msgid "GAMELIST ONLY" +msgstr "GAMELIST ONLY" + +#: +msgid "START ON SYSTEM" +msgstr "START ON SYSTEM" + +#: +msgid "BOOT ON GAMELIST" +msgstr "BOOT ON GAMELIST" + +#: +msgid "HIDE SYSTEM VIEW" +msgstr "HIDE SYSTEM VIEW" + +#: +msgid "EMULATOR ADVANCED CONFIGURATION" +msgstr "ì—뮬레ì´í„° 고급 구성" + +#: +msgid "ADVANCED EMULATOR CONFIGURATION" +msgstr "ì—뮬레ì´í„° 고급 구성" + +#: +msgid "HELP" +msgstr "ë„움" + +#: +msgid "THE SYSTEM IS UP TO DATE" +msgstr "ì‹œìŠ¤í…œì´ ìµœì‹ ìž…ë‹ˆë‹¤" + +#: +msgid "FORCE BASIC GAMELIST VIEW" +msgstr "FORCE BASIC GAMELIST VIEW" + +#: +msgid "DOWNLOADED" +msgstr "다운로드" + +#: +msgid "UPDATE VERSION:" +msgstr "ì—…ë°ì´íЏ 버전 :" + +#: +msgid "UPDATE CHANGELOG:" +msgstr "ì—…ë°ì´íЏ 변경 로그 :" + +#: +msgid "MORE DETAILS" +msgstr "ìžì„¸í•œ ë‚´ìš©ì€" + +#: +msgid "CAROUSEL TRANSITIONS" +msgstr "CAROUSEL TRANSITIONS" + +#: +msgid "ENABLE FILTERS" +msgstr "í•„í„° 활성화" + +#: +msgid "THEME CONFIGURATION" +msgstr "테마 구성" + +#: +msgid "THEME COLORSET" +msgstr "테마 색ìƒ" + +#: +msgid "THEME ICONSET" +msgstr "테마 ì•„ì´ì½˜" + +#: +msgid "THEME MENU" +msgstr "테마 메뉴" + +#: +msgid "THEME SYSTEMVIEW" +msgstr "테마 시스템" + +#: +msgid "THEME GAMELISTVIEW" +msgstr "테마 게임리스트" + +#: +msgid "THEME REGION" +msgstr "테마 지역" + +#: +msgid "THIS THEME HAS NO OPTION" +msgstr "ì´ í…Œë§ˆì—는 ì˜µì…˜ì´ ì—†ìŠµë‹ˆë‹¤" + +#: +msgid "MANUAL INPUT" +msgstr "ìˆ˜ë™ ìž…ë ¥" + +#: +msgid "AN ERROR OCCURED - DOWNLOADED" +msgstr "오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤-다운로드" + +#: +msgid "START KODI" +msgstr "KODI 시작" + +#: +msgid "Shows the current available update version." +msgstr "사용 가능한 현재 ì—…ë°ì´íЏ ë²„ì „ì„ í‘œì‹œí•©ë‹ˆë‹¤." + +#: +msgid "Shows the current available update changelog." +msgstr "현재 사용 가능한 ì—…ë°ì´íЏ 변경 로그를 표시합니다." + +#: +msgid "Configure an associated controller. Your controller has to be associated / plugged before." +msgstr "ì—°ê²°ëœ ì»¨íŠ¸ë¡¤ëŸ¬ë¥¼ 구성하십시오. 컨트롤러를 연결하고 연결해야합니다." + +#: +msgid "Choose if carousel will be animated or not during transitions" +msgstr "전환하는 ë™ì•ˆ 슬ë¼ì´ë“œ ì‡¼ì— ì• ë‹ˆë©”ì´ì…˜ì„ ì ìš©í• ì§€ 여부를 ì„ íƒí•©ë‹ˆë‹¤" + +#: +msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" +msgstr "ê²Œìž„ì„ ì‹œìž‘í•  때 ë°œìƒí•˜ëŠ” 전환 ìœ í˜•ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤. INSTANT는 아무 것ë„하지 않고 FADE는 ì–´ë‘워지고 SLIDE는 게임 커버를 확대합니다 (ë˜ëŠ” 스í¬ëž© 정보가없는 경우 ì´ë¦„)." + +#: +msgid "Select exisiting colorset options for this theme." +msgstr "ì´ í…Œë§ˆì— ëŒ€í•œ 기존 ìƒ‰ìƒ ì„¸íŠ¸ ì˜µì…˜ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤." + +#: +msgid "Select exisiting iconset options for this theme." +msgstr "ì´ í…Œë§ˆì— ëŒ€í•œ 기존 ì•„ì´ì½˜ 세트 ì˜µì…˜ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤." + +#: +msgid "Select exisiting menu style options for this theme." +msgstr "ì´ í…Œë§ˆì— ëŒ€í•œ 기존 메뉴 ìŠ¤íƒ€ì¼ ì˜µì…˜ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤." + +#: +msgid "Select exisiting system view options for this theme." +msgstr "ì´ í…Œë§ˆì— ëŒ€í•œ 기존 시스템보기 ì˜µì…˜ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤." + +#: +msgid "Select exisiting gamelist view options for this theme." +msgstr "ì´ í…Œë§ˆì— ëŒ€í•œ 기존 게임 목ë¡ë³´ê¸° ì˜µì…˜ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤." + +#: +msgid "Configure theme options if available." +msgstr "가능한 경우 테마 ì˜µì…˜ì„ êµ¬ì„±í•˜ì‹­ì‹œì˜¤." + +#: +msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" +msgstr "ì¼ë¶€ êµ­ê°€ì—서는 다른 ì‹œìŠ¤í…œì˜ ë¡œê³  ì˜ì—­, ê·¸ë¦¼ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤. 예 : EUì˜ Megadrive / ë¯¸êµ­ì˜ Genesis" + +#: +msgid "Type the name of your SSID if it is hidden or not listed" +msgstr "SSIDê°€ 숨겨져 있거나 나열ë˜ì§€ ì•Šì€ ê²½ìš° ì´ë¦„ì„ ìž…ë ¥í•˜ì‹­ì‹œì˜¤." + +#: +msgid "Select a letter and the listing will go directly on the first game starting with this letter." +msgstr "문ìžë¥¼ ì„ íƒí•˜ë©´ì´ 문ìžë¡œ 시작하는 첫 번째 게임ì—서 목ë¡ì´ 바로 표시ë©ë‹ˆë‹¤." + +#: +msgid "Select the way the game list is sortered (alphabetically, by notation...)." +msgstr "게임 목ë¡ì„ 정렬하는 ë°©ë²•ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤ (알파벳, 표기법 ...)." + +#: +msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." +msgstr "ì¦ê²¨ 찾기 게임ë¿ë§Œ ì•„ë‹ˆë¼ seingê°„ì— ì „í™˜ í•  수 있습니다. ì¦ê²¨ 찾기 목ë¡ì— ê²Œìž„ì„ ì¶”ê°€í•˜ë ¤ë©´ ê²Œìž„ì„ ì„ íƒí•˜ê³  'Y'를 사용하여 ìƒíƒœë¥¼ 토글하십시오." + +#: +msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." +msgstr "숨겨진 게임 사ì´ë¥¼ 전환할지 여부를 전환하십시오. ê²Œìž„ì„ ìˆ¨ê¸°ë ¤ë©´ 해당 ë°ì´í„°ë¥¼ 편집하고 '숨기기'를 ì„ íƒí•˜ì‹­ì‹œì˜¤." + +#: +msgid "This option display a menu which allows to change game data and many others options." +msgstr "ì´ ì˜µì…˜ì€ ê²Œìž„ ë°ì´í„° ë° ê¸°íƒ€ 여러 ì˜µì…˜ì„ ë³€ê²½í•  수있는 메뉴를 표시합니다." + +#: +msgid "AVAILABLE UPDATE" +msgstr "사용 가능한 ì—…ë°ì´íЏ" + +#: +msgid "UPDATE CHANGELOG" +msgstr "ì—…ë°ì´íЏ 변경" + +#: +msgid "CLOCK IN MENU" +msgstr "CLOCK IN MENU" + +#: +msgid "Now playing" +msgstr "플레ì´ì¤‘" + +#: +msgid "DEFAULT (%1%)" +msgstr "기본 (%1%)" + +#: +msgid "INPUT REQUIRED" +msgstr "INPUT REQUIRED" + +#: +msgid "(skipped)" +msgstr "(skipped)" + +#: +msgid "UP/DOWN TO SKIP" +msgstr "UP/DOWN TO SKIP" + +#: +msgid "A TO UNSET" +msgstr "A TO UNSET" + +#: +msgid "DOWN TO SKIP AND KEEP [%1%]" +msgstr "건너 ë›°ê³  유지 [%1%]" + +#: +msgid "UP/DOWN TO SKIP AND KEEP [%1%]" +msgstr "건너 뛰기 ë° ìœ ì§€ë¡œ ì—… / 다운 [%1%]" + +#: +msgid "Set duration of help popups, 0 means no popup." +msgstr "ë„ì›€ë§ íŒì—… ê¸°ê°„ì„ ì„¤ì •í•©ë‹ˆë‹¤. 0ì€ íŒì—…ì´ ì—†ìŒì„ ì˜ë¯¸í•©ë‹ˆë‹¤." + +#: +msgid "HELP POPUP DURATION" +msgstr "íŒì—… ì§€ì† ì‹œê°„ ì§€ì›" + +#: +msgid "Set duration of music popups, 0 means no popup." +msgstr "ìŒì•… íŒì—… ê¸°ê°„ì„ ì„¤ì •í•©ë‹ˆë‹¤. 0ì€ íŒì—…ì´ ì—†ìŒì„ ì˜ë¯¸í•©ë‹ˆë‹¤." + +#: +msgid "MUSIC POPUP DURATION" +msgstr "ìŒì•… íŒì—… 기간" + +#: +msgid "POPUP SETTINGS" +msgstr "íŒì—… 설정" + +#: +msgid "POPUP POSITION" +msgstr "íŒì—… 위치" + +#: +msgid "Select the position of popups on screen." +msgstr "화면ì—서 íŒì—… 위치를 ì„ íƒí•˜ì‹­ì‹œì˜¤." + +#: +msgid "Set position and duration of popups." +msgstr "íŒì—…ì˜ ìœ„ì¹˜ì™€ ì§€ì† ì‹œê°„ì„ ì„¤ì •í•˜ì‹­ì‹œì˜¤." + +#: +msgid "TOP/RIGHT" +msgstr "맨 위 오른쪽" + +#: +msgid "BOTTOM/RIGHT" +msgstr "하단 / 오른쪽" + +#: +msgid "BOTTOM/LEFT" +msgstr "하단 / 왼쪽" + +#: +msgid "TOP/LEFT" +msgstr "ìƒë‹¨ / 왼쪽" + +#: +msgid "SHOW FOLDERS CONTENT" +msgstr "í´ë” ë‚´ìš© 표시" + +#: +msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." +msgstr "í´ë” 구조보기와 í‰í‰í•œ 최ìƒìœ„ ë ˆë²¨ì˜ ëª¨ë“  게임보기 사ì´ë¥¼ 전환하십시오." + +#: +msgid "NETPLAY" +msgstr "ë„· 플레ì´" + +#: +msgid "NETPLAY SETTINGS" +msgstr "ë„¤íŠ¸ì›Œí¬ ì„¤ì •" + +#: +msgid "NETPLAY LOBBY" +msgstr "ë„· í”Œë ˆì´ ë¡œë¹„" + +#: +msgid "Enable or disable Netplay in games." +msgstr "게임ì—서 Netplay를 활성화 ë˜ëŠ” 비활성화합니다." + +#: +msgid "PORT" +msgstr "í¬íЏ" + +#: +msgid "NICKNAME" +msgstr "닉네임" + +#: +msgid "RELAY SERVER" +msgstr "ë¦´ë ˆì´ ì„œë²„" + +#: +msgid "Enable or disable connections throught relay servers." +msgstr "ë¦´ë ˆì´ ì„œë²„ë¥¼ 통한 ì—°ê²°ì„ í™œì„±í™” ë˜ëŠ” 비활성화합니다." + +#: +msgid "KODI/NETPLAY" +msgstr "KODI / NETPLAY" + +#: +msgid "NO GAMES OR NO CONNECTION" +msgstr "게임ì´ë‚˜ ì—°ê²°ì´ ì—†ìŠµë‹ˆë‹¤" + +#: +msgid "HASH NOW" +msgstr "HASH NOW" + +#: +msgid "HASH THESE SYSTEMS" +msgstr "HASH THESE SYSTEMS" + +#: +msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." +msgstr "Netplayì—서보다 정확한 결과를 얻으려면 게임 목ë¡ì— 롬 해시를 추가하십시오." + +#: +msgid "HASH ROMS" +msgstr "HASH ROMS" + +#: +msgid "Only missing hashs" +msgstr "Only missing hashs" + +#: +msgid "Username" +msgstr "ì´ë¦„" + +#: +msgid "Country" +msgstr "êµ­ê°€" + +#: +msgid "Latency" +msgstr "지연 시간" + +#: +msgid "Host arch." +msgstr "호스트 아치." + +#: +msgid "Core ver." +msgstr "핵심 ver." + +#: +msgid "RA ver." +msgstr "RA ver." + +#: +msgid "Can join" +msgstr "가입 가능" + +#: +msgid "Rom and core match" +msgstr "롬과 코어 매치" + +#: +msgid "Rom found" +msgstr "롬 발견" + +#: +msgid "No rom match" +msgstr "롬 ì¼ì¹˜ ì—†ìŒ" + +#: +msgid "Match" +msgstr "시합" + +#: +msgid "No Match" +msgstr "ì¼ì¹˜í•˜ì§€ 않습니다" + +#: +msgid "Rom file" +msgstr "롬 파ì¼" + +#: +msgid "Rom hash" +msgstr "롬 해시" + +#: +msgid "THIS COULD TAKE A WHILE, CONFIRM?" +msgstr "ì´ ë¬¸ì œë¥¼ 해결하는 ë™ì•ˆ í™•ì¸ í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" + +#: +msgid "good" +msgstr "좋ì€" + +#: +msgid "bad" +msgstr "나ìœ" + +#: +msgid "medium" +msgstr "매질" + +#: +msgid "NETPLAY POPUP DURATION" +msgstr "ë„· í”Œë ˆì´ íŒì—… ì§€ì† ì‹œê°„" + +#: +msgid "Set duration of netplay popups, 0 means no popup." +msgstr "ë„· í”Œë ˆì´ íŒì—… ì§€ì† ì‹œê°„ì„ ì„¤ì •í•©ë‹ˆë‹¤. 0ì€ íŒì—…ì´ ì—†ìŒì„ ì˜ë¯¸í•©ë‹ˆë‹¤." + +#: +msgid "Player" +msgstr "플레ì´ì–´" + +#: +msgid "Game" +msgstr "Game" + +#: +msgid "Play online on games running through Retroarch like NES, SNES, FBA, Genesis/Megadrive and more!" +msgstr "NES, SNES, FBA, Genesis / Megadrive 등과 ê°™ì€ Retroarch를 통해 실행ë˜ëŠ” 게임ì—서 온ë¼ì¸ìœ¼ë¡œ 플레ì´í•˜ì‹­ì‹œì˜¤!" + +#: +msgid "Rom, hash and core match" +msgstr "롬, 해시 ë° ì½”ì–´ 매치" + +#: +msgid "No core match" +msgstr "No core match" + +#: +msgid "Add a clock in the main menu." +msgstr "ë©”ì¸ ë©”ë‰´ì— ì‹œê³„ë¥¼ 추가" + +#: +msgid "UPGRADING" +msgstr "업그레ì´ë“œ" + +#: +msgid "PREPARING" +msgstr "준비" + +#: +msgid "Starting UI" +msgstr "UI 시작" + +#: +msgid "VERIFYING" +msgstr "확ì¸" + +#: +msgid "EMPTY LIST" +msgstr "빈 목ë¡" + +#: +msgid "COMPLETE QUIT MENU" +msgstr "완전한 종료 메뉴" + +#: Retroarch ratio +msgid "Auto" +msgstr "오토매틱" + +msgid "Square pixel" +msgstr "정사ê°í˜• 픽셀" + +msgid "Retroarch Config" +msgstr "레트로 ì•„í¬ êµ¬ì„±" + +msgid "Retroarch Custom" +msgstr "Retroarch Custom" + +msgid "Core provided" +msgstr "핵심 제공" + +msgid "Do not set" +msgstr "설정하지 마십시오" \ No newline at end of file From 72960879c0e61491e1952308437b6dd0e166f4b1 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 18 Oct 2019 17:32:08 +0200 Subject: [PATCH 181/336] Preparing autoupdate --- es-app/CMakeLists.txt | 2 + es-app/src/ApiSystem.cpp | 155 +++++++++++++++++++++++++++ es-app/src/ApiSystem.h | 14 +++ es-core/src/utils/FileSystemUtil.cpp | 8 ++ es-core/src/utils/FileSystemUtil.h | 1 + 5 files changed, 180 insertions(+) create mode 100644 es-app/src/ApiSystem.cpp create mode 100644 es-app/src/ApiSystem.h diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 8b62288c92..6d2b85b0de 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -13,6 +13,7 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/FileFilterIndex.h ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemScreenSaver.h ${CMAKE_CURRENT_SOURCE_DIR}/src/CollectionSystemManager.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/ApiSystem.h # GuiComponents ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.h @@ -73,6 +74,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/FileFilterIndex.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemScreenSaver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CollectionSystemManager.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ApiSystem.cpp # GuiComponents ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.cpp diff --git a/es-app/src/ApiSystem.cpp b/es-app/src/ApiSystem.cpp new file mode 100644 index 0000000000..602ff4faf9 --- /dev/null +++ b/es-app/src/ApiSystem.cpp @@ -0,0 +1,155 @@ +#include "ApiSystem.h" +#include "HttpReq.h" +#include "utils/FileSystemUtil.h" +#include + +std::string ApiSystem::checkUpdateVersion() +{ + std::string localVersion; + std::string localVersionFile = Utils::FileSystem::getExePath() + "/version.info"; + if (Utils::FileSystem::exists(localVersion)) + localVersion = Utils::FileSystem::readAllText(localVersionFile); + + std::shared_ptr httpreq = std::make_shared("https://github.com/fabricecaruso/EmulationStation/releases/download/continuous-master/version.info"); + + while (httpreq->status() == HttpReq::REQ_IN_PROGRESS) + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + + if (httpreq->status() == HttpReq::REQ_SUCCESS) + { + std::string serverVersion = httpreq->getContent(); + if (!serverVersion.empty() && serverVersion != localVersion) + return serverVersion; + } + + return ""; +} + +#if WIN32 +#include +#include // #include for _bstr_t +#pragma comment(lib, "shell32.lib") +#pragma comment (lib, "comsuppw.lib" ) // link with "comsuppw.lib" (or debug version: "comsuppwd.lib") + +bool unzipFile(const std::string fileName, const std::string dest) +{ + bool ret = false; + + HRESULT hResult; + IShellDispatch* pISD; + Folder* pFromZip = nullptr; + VARIANT vDir, vFile, vOpt; + + OleInitialize(NULL); + CoInitialize(NULL); + + hResult = CoCreateInstance(CLSID_Shell, NULL, CLSCTX_INPROC_SERVER, IID_IShellDispatch, (void **)&pISD); + + if (SUCCEEDED(hResult)) + { + VariantInit(&vDir); + vDir.vt = VT_BSTR; + + int zipDirLen = (lstrlenA(fileName.c_str()) + 1) * sizeof(WCHAR); + BSTR bstrZip = SysAllocStringByteLen(NULL, zipDirLen); + MultiByteToWideChar(CP_ACP, 0, fileName.c_str(), -1, bstrZip, zipDirLen); + vDir.bstrVal = bstrZip; + + hResult = pISD->NameSpace(vDir, &pFromZip); + + if (hResult == S_OK && pFromZip != nullptr) + { + if (!Utils::FileSystem::exists(dest)) + Utils::FileSystem::createDirectory(dest); + + Folder *pToFolder = NULL; + + VariantInit(&vFile); + vFile.vt = VT_BSTR; + + int fnLen = (lstrlenA(dest.c_str()) + 1) * sizeof(WCHAR); + BSTR bstrFolder = SysAllocStringByteLen(NULL, fnLen); + MultiByteToWideChar(CP_ACP, 0, dest.c_str(), -1, bstrFolder, fnLen); + vFile.bstrVal = bstrFolder; + + hResult = pISD->NameSpace(vFile, &pToFolder); + if (hResult == S_OK && pToFolder) + { + FolderItems *fi = NULL; + pFromZip->Items(&fi); + + VariantInit(&vOpt); + vOpt.vt = VT_I4; + vOpt.lVal = FOF_NO_UI; //4; // Do not display a progress dialog box + + VARIANT newV; + VariantInit(&newV); + newV.vt = VT_DISPATCH; + newV.pdispVal = fi; + hResult = pToFolder->CopyHere(newV, vOpt); + if (hResult == S_OK) + ret = true; + + pFromZip->Release(); + pToFolder->Release(); + } + } + pISD->Release(); + } + + CoUninitialize(); + return ret; +} +#endif + +/* +// BusyComponent* ui +std::pair ApiSystem::updateSystem(const std::function& func) +{ +#if defined(WIN32) && defined(_DEBUG) + for (int i = 0; i < 100; i += 2) + { + if (func != nullptr) + func(std::string("Downloading >>> " + std::to_string(i) + " %")); + + ::Sleep(200); + } + + if (func != nullptr) + func(std::string("Extracting files")); + + ::Sleep(750); + + return std::pair("done.", 0); +#endif + + LOG(LogDebug) << "ApiSystem::updateSystem"; + + std::string updatecommand = "batocera-upgrade"; + + FILE *pipe = popen(updatecommand.c_str(), "r"); + if (pipe == nullptr) + return std::pair(std::string("Cannot call update command"), -1); + + char line[1024] = ""; + FILE *flog = fopen("/userdata/system/logs/batocera-upgrade.log", "w"); + while (fgets(line, 1024, pipe)) + { + strtok(line, "\n"); + if (flog != nullptr) + fprintf(flog, "%s\n", line); + + if (func != nullptr) + func(std::string(line)); + } + + int exitCode = pclose(pipe); + + if (flog != NULL) + { + fprintf(flog, "Exit code : %d\n", exitCode); + fclose(flog); + } + + return std::pair(std::string(line), exitCode); +}*/ \ No newline at end of file diff --git a/es-app/src/ApiSystem.h b/es-app/src/ApiSystem.h new file mode 100644 index 0000000000..c61e6b2874 --- /dev/null +++ b/es-app/src/ApiSystem.h @@ -0,0 +1,14 @@ +#ifndef API_SYSTEM +#define API_SYSTEM + +#include + +class ApiSystem +{ +public: + //std::pair updateSystem(const std::function& func = nullptr); + std::string checkUpdateVersion(); + +}; + +#endif \ No newline at end of file diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 63f704b7da..332fae591c 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -49,6 +49,14 @@ namespace Utils return (first.path.length() < second.path.length()); } + std::string readAllText(const std::string fileName) + { + std::ifstream t(fileName); + std::stringstream buffer; + buffer << t.rdbuf(); + return buffer.str(); + } + void writeAllText(const std::string fileName, const std::string text) { std::fstream fs; diff --git a/es-core/src/utils/FileSystemUtil.h b/es-core/src/utils/FileSystemUtil.h index 87fb1ac94b..c222571eba 100644 --- a/es-core/src/utils/FileSystemUtil.h +++ b/es-core/src/utils/FileSystemUtil.h @@ -55,6 +55,7 @@ namespace Utils fileList getDirInfo(const std::string& _path/*, const bool _recursive = false*/); + std::string readAllText(const std::string fileName); void writeAllText (const std::string fileName, const std::string text); bool copyFile(const std::string src, const std::string dst); } // FileSystem:: From a8441b56aeffa0e80467617b8f967d1cd0d84238 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 18 Oct 2019 22:57:23 +0200 Subject: [PATCH 182/336] AutoUpdate Windows Version with Git continuous build --- es-app/CMakeLists.txt | 4 +- es-app/src/ApiSystem.cpp | 217 ++++++++++++++++++++++----- es-app/src/ApiSystem.h | 20 ++- es-app/src/NetworkThread.cpp | 55 +++++++ es-app/src/NetworkThread.h | 21 +++ es-app/src/guis/GuiMenu.cpp | 43 ++++++ es-app/src/main.cpp | 14 +- es-core/src/HttpReq.cpp | 12 +- es-core/src/Settings.cpp | 2 + es-core/src/utils/FileSystemUtil.cpp | 8 +- 10 files changed, 352 insertions(+), 44 deletions(-) create mode 100644 es-app/src/NetworkThread.cpp create mode 100644 es-app/src/NetworkThread.h diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 6d2b85b0de..9f8fc5bd77 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -13,7 +13,8 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/FileFilterIndex.h ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemScreenSaver.h ${CMAKE_CURRENT_SOURCE_DIR}/src/CollectionSystemManager.h - ${CMAKE_CURRENT_SOURCE_DIR}/src/ApiSystem.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/ApiSystem.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/NetworkThread.h # GuiComponents ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.h @@ -75,6 +76,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemScreenSaver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CollectionSystemManager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ApiSystem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/NetworkThread.cpp # GuiComponents ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.cpp diff --git a/es-app/src/ApiSystem.cpp b/es-app/src/ApiSystem.cpp index 602ff4faf9..e77d48f82e 100644 --- a/es-app/src/ApiSystem.cpp +++ b/es-app/src/ApiSystem.cpp @@ -1,15 +1,108 @@ #include "ApiSystem.h" #include "HttpReq.h" #include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" #include +#include +#include + +#include "Window.h" +#include "components/AsyncNotificationComponent.h" + +UpdateState::State ApiSystem::state = UpdateState::State::NO_UPDATE; + +class ThreadedUpdater +{ +public: + ThreadedUpdater(Window* window) : mWindow(window) + { + ApiSystem::state = UpdateState::State::UPDATER_RUNNING; + + mWndNotification = new AsyncNotificationComponent(window, false); + mWndNotification->updateTitle(_U("\uF019 ") + _("EMULATIONSTATION")); + + mWindow->registerNotificationComponent(mWndNotification); + mHandle = new std::thread(&ThreadedUpdater::threadUpdate, this); + } + + ~ThreadedUpdater() + { + mWindow->unRegisterNotificationComponent(mWndNotification); + delete mWndNotification; + } + + void threadUpdate() + { + std::pair updateStatus = ApiSystem::updateSystem([this](const std::string info) + { + auto pos = info.find(">>>"); + if (pos != std::string::npos) + { + std::string percent(info.substr(pos)); + percent = Utils::String::replace(percent, ">", ""); + percent = Utils::String::replace(percent, "%", ""); + percent = Utils::String::replace(percent, " ", ""); + + int value = atoi(percent.c_str()); + + std::string text(info.substr(0, pos)); + text = Utils::String::trim(text); + + mWndNotification->updatePercent(value); + mWndNotification->updateText(text); + } + else + { + mWndNotification->updatePercent(-1); + mWndNotification->updateText(info); + } + }); + + if (updateStatus.second == 0) + { + ApiSystem::state = UpdateState::State::UPDATE_READY; + + mWndNotification->updateTitle(_U("\uF019 ") + _("UPDATE IS READY")); + mWndNotification->updateText(_("RESTART EMULATIONSTATION TO APPLY")); + + std::this_thread::yield(); + std::this_thread::sleep_for(std::chrono::hours(12)); + } + else + { + ApiSystem::state = UpdateState::State::NO_UPDATE; + + std::string error = _("AN ERROR OCCURED") + std::string(": ") + updateStatus.first; + mWindow->displayNotificationMessage(error); + } + + delete this; + } + +private: + std::thread* mHandle; + AsyncNotificationComponent* mWndNotification; + Window* mWindow; +}; + +void ApiSystem::startUpdate(Window* c) +{ +#if WIN32 + new ThreadedUpdater(c); +#endif +} std::string ApiSystem::checkUpdateVersion() { +#if WIN32 std::string localVersion; std::string localVersionFile = Utils::FileSystem::getExePath() + "/version.info"; - if (Utils::FileSystem::exists(localVersion)) + if (Utils::FileSystem::exists(localVersionFile)) + { localVersion = Utils::FileSystem::readAllText(localVersionFile); - + localVersion = Utils::String::replace(Utils::String::replace(localVersion, "\r", ""), "\n", ""); + } + std::shared_ptr httpreq = std::make_shared("https://github.com/fabricecaruso/EmulationStation/releases/download/continuous-master/version.info"); while (httpreq->status() == HttpReq::REQ_IN_PROGRESS) @@ -18,10 +111,12 @@ std::string ApiSystem::checkUpdateVersion() if (httpreq->status() == HttpReq::REQ_SUCCESS) { std::string serverVersion = httpreq->getContent(); + serverVersion = Utils::String::replace(Utils::String::replace(serverVersion, "\r", ""), "\n", ""); if (!serverVersion.empty() && serverVersion != localVersion) return serverVersion; } - +#endif + return ""; } @@ -100,56 +195,104 @@ bool unzipFile(const std::string fileName, const std::string dest) CoUninitialize(); return ret; } -#endif -/* -// BusyComponent* ui -std::pair ApiSystem::updateSystem(const std::function& func) +std::shared_ptr downloadFile(const std::string url, const std::string label, const std::function& func) { -#if defined(WIN32) && defined(_DEBUG) - for (int i = 0; i < 100; i += 2) + if (func != nullptr) + func("Downloading " + label); + + std::shared_ptr httpreq = std::make_shared(url); + + while (httpreq->status() == HttpReq::REQ_IN_PROGRESS) { if (func != nullptr) - func(std::string("Downloading >>> " + std::to_string(i) + " %")); + func(std::string("Downloading " + label + " >>> " + std::to_string(httpreq->getPercent()) + " %")); - ::Sleep(200); + std::this_thread::sleep_for(std::chrono::milliseconds(20)); } - if (func != nullptr) - func(std::string("Extracting files")); + if (httpreq->status() != HttpReq::REQ_SUCCESS) + return nullptr; + + return httpreq; +} - ::Sleep(750); - return std::pair("done.", 0); +void deleteDirectoryFiles(const std::string path) +{ + auto files = Utils::FileSystem::getDirContent(path, true, true); + std::reverse(std::begin(files), std::end(files)); + for (auto file : files) + { + if (Utils::FileSystem::isDirectory(file)) + ::RemoveDirectory(file.c_str()); + else + Utils::FileSystem::removeFile(file); + } +} #endif - LOG(LogDebug) << "ApiSystem::updateSystem"; +std::pair ApiSystem::updateSystem(const std::function& func) +{ +#if WIN32 + std::string url = "https://github.com/fabricecaruso/EmulationStation/releases/download/continuous-master/EmulationStation-Win32.zip"; + auto req = downloadFile(url, "update", func); + if (req != nullptr && req->status() == HttpReq::REQ_SUCCESS) + { + if (func != nullptr) + func(std::string("Extracting update")); - std::string updatecommand = "batocera-upgrade"; + std::string fileName = Utils::FileSystem::getFileName(url); + std::string path = Utils::FileSystem::getHomePath() + "\\.emulationstation\\update"; + path = Utils::String::replace(path, "/", "\\"); - FILE *pipe = popen(updatecommand.c_str(), "r"); - if (pipe == nullptr) - return std::pair(std::string("Cannot call update command"), -1); + if (!Utils::FileSystem::exists(path)) + Utils::FileSystem::createDirectory(path); + else + deleteDirectoryFiles(path); - char line[1024] = ""; - FILE *flog = fopen("/userdata/system/logs/batocera-upgrade.log", "w"); - while (fgets(line, 1024, pipe)) - { - strtok(line, "\n"); - if (flog != nullptr) - fprintf(flog, "%s\n", line); + std::string zipFile = path + "\\" + fileName; + req->saveContent(zipFile); - if (func != nullptr) - func(std::string(line)); - } + unzipFile(zipFile, path); + Utils::FileSystem::removeFile(zipFile); - int exitCode = pclose(pipe); + auto files = Utils::FileSystem::getDirContent(path, true, true); + for (auto file : files) + { + + std::string relative = Utils::FileSystem::createRelativePath(file, path, false); + if (Utils::String::startsWith(relative, "./")) + relative = relative.substr(2); - if (flog != NULL) - { - fprintf(flog, "Exit code : %d\n", exitCode); - fclose(flog); + std::string localPath = Utils::FileSystem::getExePath() + "/" + relative; + + if (Utils::FileSystem::isDirectory(file)) + { + if (!Utils::FileSystem::exists(localPath)) + Utils::FileSystem::createDirectory(localPath); + } + else + { + if (Utils::FileSystem::exists(localPath)) + { + Utils::FileSystem::removeFile(localPath + ".old"); + rename(localPath.c_str(), (localPath + ".old").c_str()); + } + + if (Utils::FileSystem::copyFile(file, localPath)) + { + Utils::FileSystem::removeFile(localPath + ".old"); + Utils::FileSystem::removeFile(file); + } + } + } + + deleteDirectoryFiles(path); + + return std::pair("done.", 0); } +#endif - return std::pair(std::string(line), exitCode); -}*/ \ No newline at end of file + return std::pair("error.", 1); +} \ No newline at end of file diff --git a/es-app/src/ApiSystem.h b/es-app/src/ApiSystem.h index c61e6b2874..2ab36b59fb 100644 --- a/es-app/src/ApiSystem.h +++ b/es-app/src/ApiSystem.h @@ -2,13 +2,29 @@ #define API_SYSTEM #include +#include + + +class Window; + +namespace UpdateState +{ + enum State + { + NO_UPDATE, + UPDATER_RUNNING, + UPDATE_READY + }; +} class ApiSystem { public: - //std::pair updateSystem(const std::function& func = nullptr); - std::string checkUpdateVersion(); + static UpdateState::State state; + static std::pair updateSystem(const std::function& func = nullptr); + static std::string checkUpdateVersion(); + static void startUpdate(Window* c); }; #endif \ No newline at end of file diff --git a/es-app/src/NetworkThread.cpp b/es-app/src/NetworkThread.cpp new file mode 100644 index 0000000000..2812ad54ba --- /dev/null +++ b/es-app/src/NetworkThread.cpp @@ -0,0 +1,55 @@ +#include "NetworkThread.h" +#include "ApiSystem.h" +#include "guis/GuiMsgBox.h" +#include "Log.h" +#include +#include + +NetworkThread::NetworkThread(Window* window) : mWindow(window) +{ + LOG(LogDebug) << "NetworkThread : Starting"; + + // creer le thread + mFirstRun = true; + mRunning = true; + mThread = new std::thread(&NetworkThread::run, this); +} + +NetworkThread::~NetworkThread() +{ + LOG(LogDebug) << "NetworkThread : Exit"; + + mRunning = false; + mThread->join(); + delete mThread; +} + +void NetworkThread::run() +{ + while (mRunning) + { + if (mFirstRun) + { + std::this_thread::sleep_for(std::chrono::seconds(5)); + mFirstRun = false; + } + else + std::this_thread::sleep_for(std::chrono::hours(1)); + + if (Settings::getInstance()->getBool("updates.enabled")) + { + LOG(LogDebug) << "NetworkThread : Checking for updates"; + + std::string version = ApiSystem::checkUpdateVersion(); + if (!version.empty()) + { + mWindow->displayNotificationMessage(_U("\uF019 ") + _("UPDATE AVAILABLE") + std::string(": ") + version); + mRunning = false; + } + else + { + LOG(LogDebug) << "NetworkThread : No update found"; + } + } + } +} \ No newline at end of file diff --git a/es-app/src/NetworkThread.h b/es-app/src/NetworkThread.h new file mode 100644 index 0000000000..35acc17edd --- /dev/null +++ b/es-app/src/NetworkThread.h @@ -0,0 +1,21 @@ +#pragma once + +#include "Window.h" +#include + +class NetworkThread +{ +public: + NetworkThread(Window * window); + virtual ~NetworkThread(); + +private: + Window* mWindow; + bool mRunning; + bool mFirstRun; + std::thread* mThread; + + void run(); +}; + + diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index dda9562319..5278fb031f 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -26,6 +26,8 @@ #include "renderers/Renderer.h" // setSwapInterval() #include "guis/GuiTextEditPopupKeyboard.h" #include "scrapers/ThreadedScraper.h" +#include "ApiSystem.h" +#include "platform.h" GuiMenu::GuiMenu(Window* window, bool animate) : GuiComponent(window), mMenu(window, _("MAIN MENU")), mVersion(window) { @@ -1057,6 +1059,44 @@ void GuiMenu::openOtherSettings() s->addWithLabel(_("VRAM LIMIT"), max_vram); s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); + + +#if WIN32 + + // Enable updates + auto updates_enabled = std::make_shared(mWindow); + updates_enabled->setState(Settings::getInstance()->getBool("updates.enabled")); + s->addWithLabel(_("AUTO UPDATES"), updates_enabled); + s->addSaveFunc([updates_enabled] + { + Settings::getInstance()->setBool("updates.enabled", updates_enabled->getState()); + }); + + // Start update + s->addEntry(ApiSystem::state == UpdateState::State::UPDATE_READY ? _("APPLY UPDATE") : _("START UPDATE"), true, [this] + { + if (ApiSystem::checkUpdateVersion().empty()) + { + mWindow->pushGui(new GuiMsgBox(mWindow, _("NO UPDATE AVAILABLE"))); + return; + } + + if (ApiSystem::state == UpdateState::State::UPDATE_READY) + { + if (quitES(QuitMode::QUIT)) + LOG(LogWarning) << "Reboot terminated with non-zero result!"; + } + else if (ApiSystem::state == UpdateState::State::UPDATER_RUNNING) + mWindow->pushGui(new GuiMsgBox(mWindow, _("UPDATE IS ALREADY RUNNING"))); + else + ApiSystem::startUpdate(mWindow); + }); +#endif + + + + + // gamelists auto save_gamelists = std::make_shared(mWindow); save_gamelists->setState(Settings::getInstance()->getBool("SaveGamelistsOnExit")); @@ -1169,6 +1209,9 @@ void GuiMenu::openOtherSettings() } }); + + + s->updatePosition(); auto pthis = this; diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index b0913c4bb7..2efa940091 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -28,6 +28,7 @@ #include "resources/TextureData.h" #include #include "AudioManager.h" +#include "NetworkThread.h" bool scrape_cmdline = false; @@ -305,6 +306,8 @@ void processAudioTitles(Window* window) } } +#include "ApiSystem.h" + int main(int argc, char* argv[]) { srand((unsigned int)time(NULL)); @@ -317,7 +320,11 @@ int main(int argc, char* argv[]) if(!parseArgs(argc, argv)) return 0; - +/* + ApiSystem::checkUpdateVersion(); + ApiSystem::updateSystem(nullptr); + return 0; + */ // only show the console on Windows if HideConsole is false #ifdef WIN32 // MSVC has a "SubSystem" option, with two primary options: "WINDOWS" and "CONSOLE". @@ -422,6 +429,11 @@ int main(int argc, char* argv[]) if (scrape_cmdline) return run_scraper_cmdline(); +#if WIN32 + if (Settings::getInstance()->getBool("updates.enabled")) + NetworkThread* nthread = new NetworkThread(&window); +#endif + //dont generate joystick events while we're loading (hopefully fixes "automatically started emulator" bug) SDL_JoystickEventState(SDL_DISABLE); diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index 9d8fac2658..694c61aa0f 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -297,7 +297,17 @@ HttpReq::Status HttpReq::status() if(msg->data.result == CURLE_OK) { - req->mStatus = REQ_SUCCESS; + int http_status_code; + curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &http_status_code); + + if (http_status_code < 200 || http_status_code > 299) + { + req->mStatus = REQ_IO_ERROR; + std::string err = "HTTP status " + std::to_string(http_status_code); + req->onError(err.c_str()); + } + else + req->mStatus = REQ_SUCCESS; } else { diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index bbba655df9..9e38417bce 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -232,6 +232,8 @@ void Settings::setDefaults() mStringMap["MusicDirectory"] = ""; mStringMap["UserMusicDirectory"] = ""; + mBoolMap["updates.enabled"] = true; + mBoolMap["DrawClock"] = true; mDefaultBoolMap = mBoolMap; diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 332fae591c..8035142d8d 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -175,8 +175,11 @@ namespace Utils contentList.push_back(fullName); - if(_recursive && (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) - contentList.merge(getDirContent(fullName, true, includeHidden)); + if (_recursive && (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) + { + for (auto child : getDirContent(fullName, true, includeHidden)) + contentList.push_back(child); + } } while(FindNextFileW(hFind, &findData)); @@ -836,6 +839,7 @@ namespace Utils } // isHidden + std::string combine(const std::string& _path, const std::string& filename) { std::string gp = getGenericPath(_path); From cc8ae9acf6c31cf50034d73ea385566a37a5802a Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 19 Oct 2019 11:22:12 +0200 Subject: [PATCH 183/336] Add --video & --videoduration command line to play videos ( usefull for scripts with custom splash screen ) --- es-app/src/main.cpp | 90 ++++++++++++++++++++ es-core/src/components/VideoComponent.h | 7 ++ es-core/src/components/VideoVlcComponent.cpp | 10 +++ 3 files changed, 107 insertions(+) diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 2efa940091..14e5957b11 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -32,6 +32,78 @@ bool scrape_cmdline = false; +#include "components/VideoVlcComponent.h" + +static std::string gPlayVideo; +static int gPlayVideoDuration = 0; + +void playVideo() +{ + Window window; + if (!window.init(true)) + { + LOG(LogError) << "Window failed to initialize!"; + return; + } + + Settings::getInstance()->setBool("VideoAudio", true); + + bool exitLoop = false; + + VideoVlcComponent vid(&window); + vid.setVideo(gPlayVideo); + vid.setOrigin(0.5f, 0.5f); + vid.setPosition(Renderer::getScreenWidth() / 2.0f, Renderer::getScreenHeight() / 2.0f); + vid.setMaxSize(Renderer::getScreenWidth(), Renderer::getScreenHeight()); + + vid.setOnVideoEnded([&exitLoop]() + { + exitLoop = true; + return false; + }); + + window.pushGui(&vid); + + vid.onShow(); + vid.topWindow(true); + + int lastTime = SDL_GetTicks(); + int totalTime = 0; + + while (!exitLoop) + { + SDL_Event event; + + if (SDL_PollEvent(&event)) + { + do + { + if (event.type == SDL_QUIT) + return; + } while (SDL_PollEvent(&event)); + } + + int curTime = SDL_GetTicks(); + int deltaTime = curTime - lastTime; + + if (vid.isPlaying()) + { + totalTime += deltaTime; + + if (gPlayVideoDuration > 0 && totalTime > gPlayVideoDuration * 100) + break; + } + + Transform4x4f transform = Transform4x4f::Identity(); + vid.update(deltaTime); + vid.render(transform); + + Renderer::swapBuffers(); + } + + window.deinit(true); +} + bool parseArgs(int argc, char* argv[]) { Utils::FileSystem::setExePath(argv[0]); @@ -54,6 +126,18 @@ bool parseArgs(int argc, char* argv[]) for(int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--videoduration") == 0) + { + gPlayVideoDuration = atoi(argv[i + 1]); + i++; // skip the argument value + } + else + if (strcmp(argv[i], "--video") == 0) + { + gPlayVideo = argv[i + 1]; + i++; // skip the argument value + } + else if (strcmp(argv[i], "--monitor") == 0) { if (i >= argc - 1) @@ -363,6 +447,12 @@ int main(int argc, char* argv[]) FreeImage_Initialise(); #endif + if (!gPlayVideo.empty()) + { + playVideo(); + return 0; + } + //if ~/.emulationstation doesn't exist and cannot be created, bail if(!verifyHomeFolderExists()) return 1; diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index 15b8eb076d..8e7dabe4c4 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -106,6 +106,13 @@ class VideoComponent : public GuiComponent ImageSource getSnapshotSource() { return mConfig.snapshotSource; }; void setSnapshotSource(ImageSource source) { mConfig.snapshotSource = source; }; + inline void setOnVideoEnded(const std::function& callback) { + mVideoEnded = callback; + } + +protected: + std::function mVideoEnded; + private: // Start the video Immediately virtual void startVideo() = 0; diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 319de9681a..1781db6846 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -377,6 +377,16 @@ void VideoVlcComponent::handleLooping() libvlc_state_t state = libvlc_media_player_get_state(mMediaPlayer); if (state == libvlc_Ended) { + if (mVideoEnded != nullptr) + { + bool cont = mVideoEnded(); + if (!cont) + { + stopVideo(); + return; + } + } + if (!Settings::getInstance()->getBool("VideoAudio")) { libvlc_audio_set_mute(mMediaPlayer, 1); From bc4bafd7ba18604327720a9ec504b54e89789984 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 19 Oct 2019 11:35:15 +0200 Subject: [PATCH 184/336] Video mode : Add SDL_WINDOW_ALWAYS_ON_TOP flag --- es-app/src/main.cpp | 2 ++ es-core/src/renderers/Renderer.cpp | 3 +++ 2 files changed, 5 insertions(+) diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 14e5957b11..843f71149b 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -39,6 +39,8 @@ static int gPlayVideoDuration = 0; void playVideo() { + Settings::getInstance()->setBool("AlwaysOnTop", true); + Window window; if (!window.init(true)) { diff --git a/es-core/src/renderers/Renderer.cpp b/es-core/src/renderers/Renderer.cpp index 72b6d4df05..bbbbc27f08 100644 --- a/es-core/src/renderers/Renderer.cpp +++ b/es-core/src/renderers/Renderer.cpp @@ -118,6 +118,9 @@ namespace Renderer unsigned int windowFlags = (Settings::getInstance()->getBool("Windowed") ? 0 : (Settings::getInstance()->getBool("FullscreenBorderless") ? SDL_WINDOW_BORDERLESS : SDL_WINDOW_FULLSCREEN)) | getWindowFlags(); + if (Settings::getInstance()->getBool("AlwaysOnTop")) + windowFlags |= SDL_WINDOW_ALWAYS_ON_TOP; + if((sdlWindow = SDL_CreateWindow("EmulationStation", sdlWindowPosition.x(), sdlWindowPosition.y(), From c5f142e88bbb191fbc38df2bb2ddbcd7361be813 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 24 Oct 2019 19:55:04 +0200 Subject: [PATCH 185/336] Fix linux build --- es-core/src/utils/FileSystemUtil.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 8035142d8d..67084dcf2a 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -24,7 +24,8 @@ #include #include #endif // _WIN32 -#include +#include +#include namespace Utils { From 75c9a4a9763b8de1a6317985239b09d579994485 Mon Sep 17 00:00:00 2001 From: JooYoung Date: Fri, 25 Oct 2019 09:43:07 +0900 Subject: [PATCH 186/336] Update emulationstation2.po Update emulationstation2.po( korean ) --- resources/locale/ko/emulationstation2.po | 1582 ++++++++-------------- 1 file changed, 579 insertions(+), 1003 deletions(-) diff --git a/resources/locale/ko/emulationstation2.po b/resources/locale/ko/emulationstation2.po index 56e3a7e722..55f96b2599 100644 --- a/resources/locale/ko/emulationstation2.po +++ b/resources/locale/ko/emulationstation2.po @@ -8,2185 +8,1761 @@ msgstr "" "Language: ko\n" "Plural-Forms: nplurals=1; plural=0;\n" -#: msgid "EMULATOR SETTINGS" msgstr "ì—뮬레ì´í„° 설정" -#: msgid "LOG LEVEL" msgstr "로그 레벨" -#: msgid "PLAY THEME MUSICS" msgstr "테마 ìŒì•… 재ìƒ" -#: msgid "DISPLAY SONG TITLES" -msgstr "노래 제목 표시" +msgstr "노래 곡목 표시" -#: msgid "SHOW FAVORITES ON TOP" -msgstr "ìƒë‹¨ì— ì¦ê²¨ì°¾ê¸° 보기" +msgstr "ìƒë‹¨ì— ì¦ê²¨ì°¾ê¸° 표시" -#: msgid "PREFERED IMAGE SOURCE" -msgstr "선호하는 ì´ë¯¸ì§€ 소스" +msgstr "선호 ì´ë¯¸ì§€ 소스" -#: msgid "PREFERED THUMBNAIL SOURCE" -msgstr "선호하는 THUMBNAIL 소스" +msgstr "선호 미리보기 소스" -#: msgid "SCRAPE MARQUEE" -msgstr "스í¬ëž˜ì¹˜ 마퀴" +msgstr "마키 스í¬ëž©" -#, c-format msgid "%i SELECTED" -msgid_plural "%i ì„ íƒ" -msgstr[0] "%i ì„ íƒ" -msgstr[1] "%i ì„ íƒ" - -#: +msgid_plural "%i SELECTED" +msgstr[0] "%i ì„ íƒë¨" +msgstr[1] "%i ì„ íƒë¨" msgid "VIDEO SCREENSAVER SETTINGS" -msgstr "비디오 스í¬ë¦°ì„¸ì´ë²„ 설정" +msgstr "ë™ì˜ìƒ 화면보호기 설정" -#: msgid "SLIDESHOW SCREENSAVER SETTINGS" -msgstr "슬ë¼ì´ë“œ 쇼 화면 보호기 설정" +msgstr "슬ë¼ì´ë“œì‡¼ 화면보호기 설정" -#: msgid "SCREENSAVER CONTROLS" -msgstr "스í¬ë¦°ì„¸ì´ë²„ 컨트롤" +msgstr "화면보호기 제어" -#: msgid "All Games" -msgstr "모든 게임" +msgstr "ì „ì²´ 게임" -#: msgid "Only missing medias" msgstr "ëˆ„ë½ ëœ ë¯¸ë””ì–´ ë§Œ" -#: msgid "always" msgstr "í•­ìƒ" -#: msgid "start & end" -msgstr "시작 & ë" +msgstr "시작 ë° ì¢…ë£Œì‹œ" -#: msgid "never" -msgstr "결코아님" +msgstr "사용안함" -#: msgid "SCRAPE VIDEOS" -msgstr "스í¬ëž© 비디오" +msgstr "ë™ì˜ìƒ 스í¬ëž©" -#: msgid "RESET GAMELIST CUSTOMISATIONS" -msgstr "게임 ëª©ë¡ ë§žì¶¤ 설정 초기화" +msgstr "ê²Œìž„ëª©ë¡ ì‚¬ìš©ìž ì„¤ì • 초기화" -#: msgid "RESET" msgstr "초기화" -#: msgid "SHOW FOLDERS" msgstr "í´ë” 표시" -#: msgid "ARE YOU SURE YOU WANT TO CONFIGURE INPUT?" -msgstr "컨트롤러 ìž…ë ¥(INPUT)ì„ êµ¬ì„± 하시겠습니까?" +msgstr "ìž…ë ¥ ì„¤ì •ì„ ì§„í–‰í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" -#: msgid "disabled" -msgstr "비활성화" +msgstr "비활성" -#: -msgid "default" -msgstr "기본" +msgid "DEFAULT" +msgstr "기본값" -#: msgid "warning" msgstr "경고" -#: msgid "error" msgstr "오류" -#: msgid "SWAP IMAGE AFTER (SECS)" -msgstr "스왑 ì´ë¯¸ì§€ 후 (SECS)" +msgstr "ë‹¤ìŒ ì‹œê°„ 후 ì´ë¯¸ì§€ 변경 (ì´ˆ)" -#: msgid "SHOW GAME NAME" msgstr "게임 ì´ë¦„ 표시" -#: msgid "STRETCH IMAGES" -msgstr "스트레치 ì´ë¯¸ì§€" +msgstr "ì´ë¯¸ì§€ 늘리기" -#: msgid "USE CUSTOM IMAGES" -msgstr "커스텀 ì´ë¯¸ì§€ 사용" +msgstr "ì‚¬ìš©ìž ì´ë¯¸ì§€ 사용" -#: msgid "CUSTOM IMAGE DIR" -msgstr "커스텀 ì´ë¯¸ì§€ 디렉토리" +msgstr "ì‚¬ìš©ìž ì´ë¯¸ì§€ 경로" +"" -#: msgid "CUSTOM IMAGE DIR RECURSIVE" -msgstr "커스텀 ì´ë¯¸ì§€ 디렉토리 RECURSIVE" +msgstr "ì‚¬ìš©ìž ì´ë¯¸ì§€ 경로 순환" -#: msgid "CUSTOM IMAGE FILTER" -msgstr "커스텀 ì´ë¯¸ì§€ í•„í„°" +msgstr "ì‚¬ìš©ìž ì´ë¯¸ì§€ í•„í„°" -#: msgid "SWAP VIDEO AFTER (SECS)" -msgstr "스왑 비디오 애프터 (SECS)" +msgstr "ë‹¤ìŒ ì‹œê°„ 후 ë™ì˜ìƒ 변경 (ì´ˆ)" -#: msgid "USE OMX PLAYER FOR SCREENSAVER" -msgstr "SCREENSAVERì— OMX 플레ì´ì–´ 사용" +msgstr "í™”ë©´ë³´í˜¸ê¸°ì— OMX 플레ì´ì–´ 사용" -#: msgid "SHOW GAME INFO" msgstr "게임 ì •ë³´ 표시" -#: msgid "SHOW GAME INFO ON SCREENSAVER" -msgstr "게임 ì •ë³´ 표시" +msgstr "í™”ë©´ë³´í˜¸ê¸°ì— ê²Œìž„ ì •ë³´ 표시" -#: msgid "USE MARQUEE AS GAME INFO" -msgstr "게임 정보로 마퀴 사용" +msgstr "게임 ì •ë³´ì— ë§ˆí‚¤ 사용" -#: msgid "STRETCH VIDEO ON SCREENSAVER" -msgstr "스í¬ë¦°ì„¸ì´ë²„ 스트레치 비디오" +msgstr "화면보호기 ë™ì˜ìƒ í¬ê¸° 확장" -#: msgid "This collection is empty" -msgstr "ì´ ì»¬ë ‰ì…˜ì€ ë¹„ì–´ 있습니다" +msgstr "현재 ì»¬ë ‰ì…˜ì€ ë¹„ì–´ 있습니다" -#: msgid "None" msgstr "ì—†ìŒ" -#: msgid "Various" -msgstr "다양한" +msgstr "다양" -#: msgid "among other titles." -msgstr "다른 타ì´í‹€ 중ì—서ë„" +msgstr "í¬í•¨í•œ 여러 타ì´í‹€ì´ 있습니다." -#: msgid "This collection contains" -msgstr "ì´ ì»¬ë ‰ì…˜ì—는" +msgstr "현재 컬렉션ì—는 " -#: msgid "games, including" -msgstr "를 í¬í•¨í•œ 게임" +msgstr "ê°œì˜ ê²Œìž„ì´ ìžˆìœ¼ë©° " -#: msgid "Loading..." -msgstr "불러오는 중 ..." +msgstr "불러오는 중..." -#: msgid "Preloading UI" -msgstr "사전로드 UI" +msgstr "UI 불러오는 중" -#: msgid "ADD/REMOVE GAMES TO THIS GAME COLLECTION" -msgstr "ì´ ê²Œìž„ ì»¬ë ‰ì…˜ì— ê²Œìž„ 추가 / 제거" +msgstr "현재 게임 ì»¬ë ‰ì…˜ì— ê²Œìž„ 추가/ì‚­ì œ" -#: msgid "FINISH EDITING" -msgstr "편집 마무리" +msgstr "편집 종료" -#: msgid "GRID SIZE" -msgstr "GRID í¬ê¸°" +msgstr "그리드 í¬ê¸°" -#: msgid "DEFAULT GRID SIZE" -msgstr "기본 GRID í¬ê¸°" +msgstr "기본 그리드 í¬ê¸°" -#: msgid "UI MODE" msgstr "UI 모드" -#: msgid "PRELOAD UI" -msgstr "사전로드 UI" +msgstr "UI 미리ì½ê¸°" -#: msgid "VSYNC" msgstr "VSYNC" -#: msgid "DISPLAY FAVORITES FIRST IN GAMELIST" -msgstr "ìƒë‹¨ì— ì¦ê²¨ì°¾ê¸° 보기" +msgstr "게임ë¡ë¡ ì•žìª½ì— ì¦ê²¨ì°¾ê¸° 표시" -#: msgid "SHOW SYSTEM NAME IN COLLECTIONS" -msgstr "컬렉션ì—서 시스템 ì´ë¦„ 표시" +msgstr "ì»¬ë ‰ì…˜ì— ì‹œìŠ¤í…œ ì´ë¦„ 표시" -#: msgid "OPTIMIZE IMAGES VRAM USE" -msgstr "ì´ë¯¸ì§€ 최ì í™” ë””ìŠ¤í¬ ì‚¬ìš©" +msgstr "ì´ë¯¸ì§€ VRAM 사용 최ì í™”" -#: msgid "Full" -msgstr "Full" +msgstr "ì „ì²´" -#: msgid "Kiosk" -msgstr "Kiosk" +msgstr "키오스í¬" -#: msgid "Kid" -msgstr "Kid" +msgstr "ì•„ë™ìš©" -#: msgid "GAME COLLECTION SETTINGS" msgstr "게임 컬렉션 설정" -" " -#: + msgid "CANCEL" msgstr "취소" -#: msgid "automatic" -msgstr "오토매틱" +msgstr "ìžë™" -#: msgid "basic" msgstr "기본" -#: msgid "detailed" -msgstr "ìƒì„¸í•œ" +msgstr "ìƒì„¸" -#: msgid "grid" msgstr "그리드" -#: msgid "fade" -msgstr "Fade" +msgstr "어둡게" -#: msgid "instant" msgstr "즉시" -#: msgid "slide" -msgstr "slide" +msgstr "슬ë¼ì´ë“œ" -#: msgid "gridex" -msgstr "그리드" +msgstr "그리ë±ìФ" -#: msgid "ENABLE NAVIGATION SOUNDS" -msgstr "íƒìƒ‰ 소리 활성화" +msgstr "ì´ë™ íš¨ê³¼ìŒ ì‚¬ìš©" -#: msgid "ENABLE VIDEO AUDIO" -msgstr "비디오 오디오 활성화" +msgstr "ë™ì˜ìƒ 오디오 사용" -#: msgid "APPLY FILTER" msgstr "í•„í„° ì ìš©" -#: msgid "AUTOMATIC GAME COLLECTIONS" msgstr "ìžë™ 게임 컬렉션" -#: msgid "SELECT COLLECTIONS" msgstr "컬렉션 ì„ íƒ" -#: msgid "CUSTOM GAME COLLECTIONS" -msgstr "커스텀 게임 컬렉션" +msgstr "ì‚¬ìš©ìž ê²Œìž„ 컬렉션" -#: msgid "CREATE NEW CUSTOM COLLECTION FROM THEME" -msgstr "테마ì—서 새로운 ì‚¬ìš©ìž ì •ì˜ ì»¬ë ‰ì…˜ 만들기" - -#: -msgid "GAME COLLECTION SETTINGS" -msgstr "게임 컬렉션 설정" +msgstr "테마ì—서 새 ì‚¬ìš©ìž ì»¬ë ‰ì…˜ 만들기" -#: msgid "SELECT THEME FOLDER" msgstr "테마 í´ë” ì„ íƒ" -#: msgid "CREATE NEW CUSTOM COLLECTION" -msgstr "새로운 커스텀 컬렉션 ìƒì„±" +msgstr "새 ì‚¬ìš©ìž ì»¬ë ‰ì…˜ ìƒì„±" -#: msgid "New Collection Name" -msgstr "새로운 컬렉션 ì´ë¦„" - -#: -msgid "Loading..." -msgstr "불러오는 중 ..." +msgstr "새 컬렉션 ì´ë¦„" -#: msgid "GROUP UNTHEMED CUSTOM COLLECTIONS" -msgstr "ê·¸ë£¹ì´ ìƒê°í•˜ì§€ ì•Šì€ ì»¤ìŠ¤í…€ 컬렉션" +msgstr "테마가 없는 ì‚¬ìš©ìž ì»¬ë ‰ì…˜ 묶기" -#: msgid "SORT CUSTOM COLLECTIONS AND SYSTEMS" -msgstr "ì •ë ¬ 시스템 ì‚¬ìš©ìž ì •ì˜ ë° ì‹œìŠ¤í…œ" +msgstr "ì‚¬ìš©ìž ì»¬ë ‰ì…˜ ë° ì‹œìŠ¤í…œ ì •ë ¬" -#: msgid "FINISH EDITING COLLECTION" -msgstr "마무리 편집 모ìŒ" +msgstr "컬렉션 편집 종료" -#: msgid "PARSE GAMESLISTS ONLY" -msgstr "GAMESLISTS ONLY 파싱 ëª©ë¡ êµ¬ì„±" +msgstr "ê²Œìž„ëª©ë¡ í•­ëª©ë§Œ 표시" -#: msgid "POWER SAVER MODES" msgstr "절전 모드" -#: msgid "SAVE METADATA ON EXIT" -msgstr "종료시 메타 ë°ì´í„° 저장" +msgstr "종료시 메타ë°ì´í„° 저장" -#: msgid "SHOW HIDDEN FILES" -msgstr "숨겨진 íŒŒì¼ í‘œì‹œ" +msgstr "ìˆ¨ê¹€íŒŒì¼ í‘œì‹œ" -#: msgid "VRAM LIMIT" msgstr "VRAM 제한" -#: msgid "RESET ALL FILTERS" -msgstr "모든 í•„í„° 재설정" +msgstr "ì „ì²´ í•„í„° 초기화" -#: msgid "GAMELIST VIEW STYLE" -msgstr "게임리스트 스타ì¼" +msgstr "ê²Œìž„ëª©ë¡ ë³´ê¸° 스타ì¼" -#: msgid "GAME LAUNCH TRANSITION" -msgstr "게임 시작 전환" +msgstr "게임 실행 전환효과" -#: msgid "THREADED LOADING" -msgstr "스레드 로딩" +msgstr "ìŠ¤ë ˆë“œëœ ë¡œë”©" -#: msgid "RANDOM" msgstr "무작위" -#: -msgid "Rating" -msgstr "í‰ê°€" +msgid "RATING" +msgstr "í‰ì " -#: -msgid "Released" +msgid "RELEASED" msgstr "출시" -#: -msgid "Developer" -msgstr "개발ìž" +msgid "DEVELOPER" +msgstr "개발사" -#: -msgid "Publisher" -msgstr "발행ìž" +msgid "PUBLISHER" +msgstr "íŒë§¤ì‚¬" -#: -msgid "Genre" -msgstr "유형" +msgid "GENRE" +msgstr "장르" -#: -msgid "Players" -msgstr "선수" +msgid "PLAYERS" +msgstr "플레ì´ì–´" -#: msgid "NO GAMES FOUND - SKIP" -msgstr "ê²Œìž„ì„ ì°¾ì„ ìˆ˜ ì—†ìŒ-건너 뛰기" +msgstr "ê²Œìž„ì„ ì°¾ì„ ìˆ˜ ì—†ìŒ - 건너뛰기" -#: msgid "RETRY" -msgstr "다시 í•´ 보다" +msgstr "다시 시ë„" -#: msgid "SKIP" -msgstr "건너 뛰기" +msgstr "건너뛰기" -#: msgid "SEARCH FOR" -msgstr "검색 FOR" +msgstr "검색" -#: msgid "SEARCH" -msgstr "검색 FOR" +msgstr "검색" -#: msgid "SCRAPING IN PROGRESS" -msgstr "ì§„í–‰ì¤‘ì¸ ìŠ¤í¬ëž©" +msgstr "스í¬ëž© 진행중" -#: msgid "SYSTEM" msgstr "시스템" -#: msgid "subtitle text" msgstr "ìžë§‰ í…스트" -#: msgid "INPUT" msgstr "ìž…ë ¥" -#: -msgid "search" -msgstr "검색 FOR" - -#: msgid "STOP" msgstr "중지" -#: msgid "stop (progress saved)" -msgstr "중지 (진행률 저장)" +msgstr "중지 (ì§„í–‰ìƒí™© 저장ë¨)" -#: msgid "GAME %i OF %i" -msgstr "%iì˜ ê²Œìž„ %i" +msgstr "게임 %i / %i" -#: msgid "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." -msgstr "ì–´ë–¤ ì‹œìŠ¤í…œë„ ì°¾ì„ ìˆ˜ 없습니다! \n" -"시스템 구성 파ì¼ì—서 경로가 올바른지 확ì¸í•˜ê³  게임 디렉토리가 올바른 확장 ê¸°ëŠ¥ì„ ê°€ì§„ 마지막 게임 중 í•˜ë‚˜ì— ìžˆëŠ”ì§€ 확ì¸í•˜ì‹­ì‹œì˜¤. \n" +msgid "ì‹œìŠ¤í…œì„ ì°¾ì„ ìˆ˜ 없습니다!\n" +"시스템 설정 파ì¼ì—서 경로가 올바른지 확ì¸í•˜ê³  í´ë”ì— ìµœì†Œ 한 ê°œ ì´ìƒì˜ 올바른 확장ìžë¥¼ 가진 ê²Œìž„ì´ ìžˆëŠ”ì§€ 확ì¸í•˜ì‹­ì‹œì˜¤.\n" "\n" -"ìžì„¸í•œ ë‚´ìš©ì€ EMULATIONSTATION.ORG를 방문하십시오." +"ìžì„¸í•œ 정보는 EMULATIONSTATION.ORGì—서 확ì¸í•˜ì„¸ìš”." -#: msgid "%i GAME SUCCESSFULLY SCRAPED!" -msgid_plural "%i ê²Œìž„ì´ ì„±ê³µì ìœ¼ë¡œ 스í¬ëž©!" -msgstr[0] "%i ê²Œìž„ì´ ì„±ê³µì ìœ¼ë¡œ 스í¬ëž©!" -msgstr[1] "%i ê²Œìž„ì´ ì„±ê³µì ìœ¼ë¡œ 스í¬ëž©!" +msgid_plural "%i GAMES SUCCESSFULLY SCRAPED!" +msgstr[0] "%i 게임 스í¬ëž©ë¨!" +msgstr[1] "%i 게임 스í¬ëž©ë¨!" -#: msgid "%i GAME SKIPPED." -msgid_plural "%i 게임 건너 뛰기." -msgstr[0] "%i 게임 건너 뛰기." -msgstr[1] "%i 게임 건너 뛰기." +msgid_plural "%i GAMES SKIPPED." +msgstr[0] "%i 게임 건너뜀." +msgstr[1] "%i 게임 건너뜀." -#: msgid "OK" -msgstr "승ì¸" +msgstr "확ì¸" -#: msgid "EDIT METADATA" -msgstr "메타 ë°ì´í„° 편집" +msgstr "메타ë°ì´í„° 수정" -#: msgid "SCRAPE" -msgstr "SCRAPE" +msgstr "스í¬ëž©" -#: msgid "SAVE" msgstr "저장" -#: msgid "THIS WILL DELETE A FILE!\n" "ARE YOU SURE?" msgstr "파ì¼ì„ 삭제합니다! \n" "확실합니까?" -#: msgid "YES" msgstr "예" -#: msgid "NO" -msgstr "아니요" +msgstr "아니오" -#: msgid "DELETE" -msgstr "지우다" +msgstr "ì‚­ì œ" -#: -msgid "SAVE CHANGES?" -msgstr "변경 ì‚¬í•­ì„ ì €ìž¥í•©ë‹ˆê¹Œ?" +msgid "SAVE CHANGES ?" +msgstr "ë³€ê²½ì‚¬í•­ì„ ì €ìž¥í•©ë‹ˆê¹Œ?" -#: msgid "BACK" -msgstr "뒤로" +msgstr "ì´ì „" -#: msgid "CLOSE" msgstr "닫기" -#: msgid "MAIN MENU" msgstr "ë©”ì¸ ë©”ë‰´" -#: msgid "KODI MEDIA CENTER" -msgstr "코디 미디어 센터" +msgstr "KODI 미디어 센터" -#: msgid "SYSTEM SETTINGS" msgstr "환경 설정" -#: msgid "VERSION" msgstr "버전" -#: msgid "DISK USAGE" msgstr "ë””ìŠ¤í¬ ì‚¬ìš©ëŸ‰" -#: msgid "STORAGE DEVICE" msgstr "저장 장치" -#: msgid "LANGUAGE" -msgstr "LANGUAGE" +msgstr "언어" -#: msgid "OVERCLOCK" msgstr "오버 í´ëŸ­" -#: msgid "EXTREM (1100Mhz)" -msgstr "EXTREM (1100MHz)" +msgstr "극한 (1100MHz)" -#: msgid "TURBO (1000Mhz)" msgstr "í„°ë³´ (1000Mhz)" -#: msgid "HIGH (950Mhz)" msgstr "ë†’ìŒ (950Mhz)" -#: msgid "NONE (700Mhz)" msgstr "ì—†ìŒ (700Mhz)" -#: msgid "TURBO (1050Mhz)+" msgstr "í„°ë³´ (1050Mhz)+" -#: msgid "HIGH (1050Mhz)" msgstr "ë†’ìŒ (1050Mhz)" -#: msgid "NONE (900Mhz)" msgstr "ì—†ìŒ (900Mhz)" -#: msgid "NONE (1200Mhz)" msgstr "ì—†ìŒ (1200Mhz)" -#: -msgid "NONE" -msgstr "ì—†ìŒ" - -#. NEW SETTINGS ORGANIZATION -#: msgid "UPDATES" msgstr "ì—…ë°ì´íЏ" -#: msgid "AUTO UPDATES" msgstr "ìžë™ ì—…ë°ì´íЏ" -#: msgid "START UPDATE" msgstr "ì—…ë°ì´íЏ 시작" -#: msgid "KODI SETTINGS" -msgstr "코디 설정" +msgstr "KODI 설정" -#: msgid "ENABLE KODI" -msgstr "코디 활성화" +msgstr "KODI 활성화" -#: msgid "KODI AT START" -msgstr "시작시 KODI" +msgstr "시작시 KODI 실행" -#: msgid "START KODI WITH X" -msgstr "X로 KODI 시작" +msgstr "시작시 X버튼으로 KODI 실행" -#: msgid "SECURITY" msgstr "보안" -#: msgid "ENFORCE SECURITY" msgstr "보안 ê°•í™”" -#: msgid "ROOT PASSWORD" msgstr "루트 비밀번호" -#: msgid "THE SYSTEM WILL NOW REBOOT" msgstr "ì´ì œ ì‹œìŠ¤í…œì´ ë‹¤ì‹œ 부팅ë©ë‹ˆë‹¤" -#: msgid "GAMES SETTINGS" msgstr "게임 설정" -#: msgid "GAME RATIO" -msgstr "게임 비율" +msgstr "게임 화면 비율" -#: msgid "SMOOTH GAMES" -msgstr "부드러운 게임" +msgstr "게임 화면 부드럽게" -#: msgid "REWIND" msgstr "ë˜ê°ê¸°" -#: msgid "AUTO SAVE/LOAD" -msgstr "ìžë™ 저장 /로드" +msgstr "ìžë™ 저장/불러오기" -#: msgid "SHADERS SET" -msgstr "ì‰ì´ë” 세트" +msgstr "ì‰ì´ë”" -#: msgid "SCANLINES" -msgstr "스캔" +msgstr "스캔ë¼ì¸" -#: msgid "RETRO" msgstr "레트로" -#: msgid "RETROACHIEVEMENTS SETTINGS" msgstr "RETROACHIEVEMENTS 설정" -#: msgid "RETROACHIEVEMENTS" msgstr "RETROACHIEVEMENTS" -#: msgid "HARDCORE MODE" msgstr "하드 코어 모드" -#: msgid "USERNAME" -msgstr "ì‚¬ìš©ìž ì´ë¦„" +msgstr "사용ìžëª…" -#: msgid "PASSWORD" msgstr "암호" -#: msgid "ADVANCED" -msgstr "ADVANCED" +msgstr "고급" -#: msgid "REALLY UPDATE GAMES LISTS ?" -msgstr "REALLY UPDATE GAMES LISTS ?" +msgstr "게임 목ë¡ì„ ì—…ë°ì´íЏ 합니까?" -#: msgid "UPDATE GAMES LISTS" msgstr "게임 ëª©ë¡ ì—…ë°ì´íЏ" -#: msgid "CONTROLLERS SETTINGS" msgstr "컨트롤러 설정" -#: msgid "UI SETTINGS" msgstr "UI 설정" -#: msgid "OVERSCAN" msgstr "오버 스캔" -#: msgid "LAUNCH SCREENSAVER" -msgstr "LAUNCH SCREENSAVER" +msgstr "화면보호기 실행" -#: msgid "SCREENSAVER SETTINGS" -msgstr "LAUNCH SCREENSAVER" +msgstr "화면보호기 설정" -#: msgid "SCREENSAVER AFTER" -msgstr "스í¬ë¦° 세ì´ë²„" +msgstr "화면보호기 실행시간" -#: msgid "TRANSITION STYLE" msgstr "전환 스타ì¼" -#: msgid "SCREENSAVER BEHAVIOR" -msgstr "스í¬ë¦° 세ì´ë²„ í–‰ë™" +msgstr "화면보호기 ë°©ì‹" -#: msgid "random video" -msgstr "ëžœë¤ ë¹„ë””ì˜¤" +msgstr "무작위 ë™ì˜ìƒ" -#: msgid "slideshow" -msgstr "슬ë¼ì´ë“œ 쇼" +msgstr "슬ë¼ì´ë“œì‡¼" -#: msgid "dim" -msgstr "dim" +msgstr "í리게" -#: msgid "black" -msgstr "black" +msgstr "ê²€ì€ í™”ë©´" -#: msgid "SHOW FRAMERATE" -msgstr "FRAMERATE 표시" +msgstr "FPS 표시" -#: msgid "ON-SCREEN HELP" -msgstr "ON-SCREEN HELP" +msgstr "온스í¬ë¦° ë„움ë§" -#: msgid "HIDE WHEN RUNNING GAME" -msgstr "ê²Œìž„ì„ ì‹¤í–‰í•  때 숨기기" +msgstr "게임 실행시 숨김" -#: msgid "QUICK SYSTEM SELECT" -msgstr "빠른 시스템 ì„ íƒ" +msgstr "빠른 시스템 전환" -#: msgid "THEME SET" -msgstr "THEME 설정" +msgstr "테마" -#: msgid "SOUND SETTINGS" msgstr "사운드 설정" -#: msgid "SYSTEM VOLUME" msgstr "시스템 볼륨" -#: msgid "FRONTEND MUSIC" -msgstr "프론트 뮤ì§" +msgstr "프론트엔드 ìŒì•…" -#: msgid "ONLY PLAY SYSTEM-SPECIFIC MUSIC FOLDER" msgstr "시스템 특정 ìŒì•… í´ë” ë§Œ 재ìƒ" -#: msgid "FILTER GAMES BY TEXT" -msgstr "í…스트 별 í•„í„° 게임" +msgstr "문ìžë¡œ 게임 í•„í„°" -#: msgid "OUTPUT DEVICE" msgstr "출력 장치" -#: msgid "HDMI" msgstr "HDMI" -#: msgid "JACK" -msgstr "JACK" +msgstr "오디오잭" -#: msgid "AUTO" msgstr "ìžë™" -#: msgid "NETWORK SETTINGS" msgstr "ë„¤íŠ¸ì›Œí¬ ì„¤ì •" -#: msgid "CONNECTED" msgstr "ì—°ê²°ë¨" -#: msgid "NOT CONNECTED" -msgstr "ì—°ê²°ë˜ì§€ 않ì€" +msgstr "ì—°ê²°ë˜ì§€ 않ìŒ" -#: msgid "STATUS" msgstr "ìƒíƒœ" -#: msgid "IP ADDRESS" msgstr "IP 주소" -#: msgid "HOSTNAME" msgstr "호스트 ì´ë¦„" -#: msgid "ENABLE WIFI" -msgstr "와ì´íŒŒì´ 활성화" +msgstr "WIFI 사용" -#: msgid "WIFI SSID" -msgstr "와ì´íŒŒì´ SSID" +msgstr "WIFI SSID" -#: msgid "WIFI KEY" -msgstr "와ì´íŒŒì´ 키" +msgstr "WIFI 키" -#: msgid "WIFI ENABLED" -msgstr "와ì´íŒŒì´ 가능" +msgstr "WIFI 활성중" -#: msgid "WIFI CONFIGURATION ERROR" -msgstr "와ì´íŒŒì´ 구성 오류" +msgstr "WIFI 구성 오류" -#: msgid "SCRAPER" -msgstr "스í¬ëž©í¼" +msgstr "스í¬ëž˜í¼" -#: msgid "SCRAPE FROM" -msgstr "스í¬ëž©í¼ FROM" +msgstr "스í¬ëž© " -#: msgid "SCRAPE RATINGS" -msgstr "스í¬ëž©í¼ RATINGS" +msgstr "í‰ì  스í¬ëž©" -#: msgid "SCRAPE NOW" -msgstr "스í¬ëž©í¼ NOW" +msgstr "지금 스í¬ëž©" -#: msgid "QUIT" msgstr "종료" -#: msgid "QUIT EMULATIONSTATION" -msgstr "ES 종료" +msgstr "ì—뮬레ì´ì…˜ìŠ¤í…Œì´ì…˜ 나가기" -#: msgid "RESTART EMULATIONSTATION" -msgstr "ES 재시작" +msgstr "ì—뮬레ì´ì…˜ìŠ¤í…Œì´ì…˜ 재시작" -#: msgid "REALLY QUIT?" -msgstr "종료합니까?" +msgstr "ì •ë§ë¡œ 나갑니까?" -#: msgid "REALLY RESTART?" -msgstr "다시 시작 합니까?" +msgstr "ì •ë§ ë‹¤ì‹œ 시작합니까?" -#: msgid "RESTART SYSTEM" msgstr "시스템 재시작" -#: msgid "REALLY SHUTDOWN?" -msgstr "ì‹œìŠ¤í…œì„ ì¢…ë£Œí•©ë‹ˆê¹Œ?" +msgstr "ì •ë§ë¡œ 종료합니까?" -#: msgid "SHUTDOWN SYSTEM" -msgstr "셧다운 시스템" +msgstr "시스템 종료" -#: -msgid "Emulator" +msgid "EMULATOR" msgstr "ì—뮬레ì´í„°" -#: -msgid "Core" -msgstr "Core" +msgid "CORE" +msgstr "코어" -#: msgid "YOU ARE GOING TO CONFIGURE A CONTROLLER. IF YOU HAVE ONLY ONE JOYSTICK, CONFIGURE THE DIRECTIONS KEYS AND SKIP JOYSTICK CONFIG BY HOLDING A BUTTON. IF YOU DO NOT HAVE A SPECIAL KEY FOR HOTKEY, CHOOSE THE SELECT BUTTON. SKIP ALL BUTTONS YOU DO NOT HAVE BY HOLDING A KEY. BUTTONS NAMES ARE BASED ON THE SNES CONTROLLER." msgstr "컨트롤러를 구성하려고합니다. í•˜ë‚˜ì˜ ì¡°ì´ìŠ¤í‹± 만있는 경우 ë²„íŠ¼ì„ ëˆŒëŸ¬ 방향키와 건너 뛰기 ì¡°ì´ìŠ¤í‹± êµ¬ì„±ì„ êµ¬ì„±í•˜ì‹­ì‹œì˜¤. 핫키 ìš© 특수 키가없는 경우, ì„ íƒ ë²„íŠ¼ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤. 열쇠를 잡고 있지 ì•Šì€ ëª¨ë“  단추를 건너 뛰십시오. 버튼 ì´ë¦„ì€ SNES 컨트롤러를 기반으로합니다." -#. GUIMENU -#: msgid "CONFIGURE A CONTROLLER" msgstr "컨트롤러 구성" -#. Bluetooth -#: msgid "CONTROLLER PAIRED" msgstr "컨트롤러가 페어ë§ë¨" -#: msgid "UNABLE TO PAIR CONTROLLER" msgstr "컨트롤러 íŽ˜ì–´ë§ ë¶ˆê°€" -#: msgid "AN ERROR OCCURED" -msgstr "오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤" +msgstr "오류 ë°œìƒ" -#: msgid "NO CONTROLLERS FOUND" msgstr "컨트롤러 ì—†ìŒ" -#: msgid "PAIR A BLUETOOTH CONTROLLER" msgstr "블루투스 컨트롤러 페어ë§" -#: msgid "CONTROLLERS LINKS HAVE BEEN DELETED." -msgstr "컨트롤러 ë§í¬ê°€ ì‚­ì œë˜ì—ˆìŠµë‹ˆë‹¤." +msgstr "컨트롤러 ì—°ê²°ì´ ì‚­ì œë˜ì—ˆìŠµë‹ˆë‹¤." -#: msgid "FORGET BLUETOOTH CONTROLLERS" -msgstr "블루투스 컨트롤러 FORGET" +msgstr "블루투스 컨트롤러 ì‚­ì œ" -#: msgid "INPUT P%i" msgstr "ìž…ë ¥ P%i" -#: msgid "CHOOSE" -msgstr "ì„ íƒ" +msgstr "ì´ë™" -#: msgid "SELECT" msgstr "ì„ íƒ" -#: msgid "OPTIONS" msgstr "옵션" -#: msgid "JUMP TO..." -msgstr "ì í”„ ..." +msgstr "ì´ë™..." -#: msgid "SORT GAMES BY" -msgstr "게임 ì •ë ¬ ë°©ì‹" +msgstr "게임 ì •ë ¬" -#. FAVORITES -#: msgid "FAVORITES ONLY" -msgstr "ì¦ê²¨ì°¾ê¸° 목ë¡ë§Œ" +msgstr "ì¦ê²¨ì°¾ê¸°ë§Œ" -#: msgid "EDIT THIS GAME'S METADATA" -msgstr "게임 메타 ë°ì´í„° 편집" +msgstr "현재 ê²Œìž„ì˜ ë©”íƒ€ë°ì´í„° 수정" -#: msgid "SCRAPE THESE GAMES" -msgstr "SCRAPE THESE 게임" +msgstr "ë‹¤ìŒ ê²Œìž„ 스í¬ëž©" -#: -msgid "All Games" -msgstr "모든 게임" - -#. MISSING SCRAPPER TRANSLATIONS -#: msgid "Only missing image" -msgstr "ëˆ„ë½ ëœ ì´ë¯¸ì§€ ë§Œ" +msgstr "ì´ë¯¸ì§€ê°€ 없는 게임" -#: msgid "FILTER" msgstr "í•„í„°" -#: msgid "SCRAPE THESE SYSTEMS" -msgstr "스í¬ëž©í¼ THESE 시스템" +msgstr "ë‹¤ìŒ ì‹œìŠ¤í…œ 스í¬ëž©" -#: msgid "SYSTEMS" msgstr "시스템" -#: msgid "USER DECIDES ON CONFLICTS" -msgstr "사용ìžê°€ ì¶©ëŒì— 대해 ê²°ì •" +msgstr "ì¶©ëŒì‹œ 사용ìžê°€ ì„ íƒ" -#: msgid "START" msgstr "시작" -#: msgid "WARNING: SOME OF YOUR SELECTED SYSTEMS DO NOT HAVE A PLATFORM SET. RESULTS MAY BE EVEN MORE INACCURATE THAN USUAL!\n" "CONTINUE ANYWAY?" -msgstr "경고 : ì„ íƒí•œ 시스템 중 ì¼ë¶€ì— í”Œëž«í¼ ì„¸íŠ¸ê°€ 없습니다. 결과가 í‰ì†Œë³´ë‹¤ ë” ì •í™•í•˜ì§€ ì•Šì„ ìˆ˜ 있습니다! \n" +msgid "경고: ì„ íƒëœ 시스템중 ì¼ë¶€ê°€ í”Œëž«í¼ ì„¸íŠ¸ê°€ 없습니다. 결과가 정확하지 ì•Šì„ ìˆ˜ 있습니다!\n" "계ì†í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" -#: msgid "NO GAMES FIT THAT CRITERIA." -msgstr "í•„í„°ì— ë§žëŠ” ê²Œìž„ì€ ì—†ìŠµë‹ˆë‹¤." +msgstr "ì¡°ê±´ì— ë§žëŠ” ê²Œìž„ì´ ì—†ìŠµë‹ˆë‹¤." -#: msgid "REALLY UPDATE?" msgstr "ì—…ë°ì´íЏ 합니까?" -#: msgid "NETWORK CONNECTION NEEDED" msgstr "ë„¤íŠ¸ì›Œí¬ ì—°ê²° í•„ìš”" -#: msgid "UPDATE DOWNLOADED, THE SYSTEM WILL NOW REBOOT" -msgstr "ì—…ë°ì´íŠ¸ê°€ 다운로드ë˜ë©´ ì‹œìŠ¤í…œì´ ë‹¤ì‹œ 부팅ë©ë‹ˆë‹¤" +msgstr "ì—…ë°ì´íŠ¸ê°€ 다운로드 ë˜ì—ˆìŠµë‹ˆë‹¤. ì‹œìŠ¤í…œì„ ë‹¤ì‹œ 시작합니다" -#: msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" -msgstr "ì—…ë°ì´íŠ¸ì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. ì´ì œ ì‹œìŠ¤í…œì´ ë‹¤ì‹œ 부팅ë©ë‹ˆë‹¤" +msgstr "ì—…ë°ì´íŠ¸ì— ì‹¤íŒ¨í–ˆìŠµë‹ˆë‹¤. ì‹œìŠ¤í…œì„ ë‹¤ì‹œ 시작합니다" -#: msgid "NO UPDATE AVAILABLE" -msgstr "사용 가능한 ì—…ë°ì´íЏ ì—†ìŒ" +msgstr "ì´ìš© 가능한 ì—…ë°ì´íЏ ì—†ìŒ" -#: msgid "enter emulator" -msgstr "ì—뮬레ì´í„°ë¥¼ ìž…ë ¥" +msgstr "ì—뮬레ì´í„° ìž…ë ¥" -#: msgid "enter core" -msgstr "core를 ìž…ë ¥" +msgstr "코어 ìž…ë ¥" -#: -msgid "Ratio" +msgid "RATIO" msgstr "비율" -#: msgid "enter ratio" -msgstr "ë¹„ìœ¨ì„ ìž…ë ¥" +msgstr "비율 ìž…ë ¥" -#: -msgid "Name" +msgid "NAME" msgstr "ì´ë¦„" -#: msgid "enter game name" -msgstr "ê²Œìž„ëª…ì„ ìž…ë ¥í•˜ì‹­ì‹œì˜¤" +msgstr "게임 ì´ë¦„ ìž…ë ¥" -#: -msgid "Description" -msgstr "ìƒì„¸ì„¤ëª…" +msgid "DESCRIPTION" +msgstr "설명" -#: msgid "enter description" -msgstr "ìƒì„¸ì„¤ëª…ì„ ìž…ë ¥" +msgstr "설명 ìž…ë ¥" -#: -msgid "Image" +msgid "IMAGE" msgstr "ì´ë¯¸ì§€" -#: msgid "enter path to image" -msgstr "ì´ë¯¸ì§€ 경로를 입력하십시오" +msgstr "ì´ë¯¸ì§€ 경로 ìž…ë ¥" -#: -msgid "Thumbnail" -msgstr "ì¸ë„¤ì¼" +msgid "THUMBNAIL" +msgstr "미리보기" -#: msgid "enter path to thumbnail" -msgstr "ì„¬ë„¤ì¼ ê²½ë¡œë¥¼ 입력하십시오" +msgstr "미리보기 ì´ë¯¸ì§€ 경로 ìž…ë ¥" -#: msgid "enter rating" -msgstr "Rating ìž…ë ¥" +msgstr "í‰ì  ìž…ë ¥" -#: -msgid "Release date" -msgstr "출시ì¼" +msgid "RELEASE DATE" +msgstr "발매ì¼" -#: msgid "enter release date" -msgstr "출시ì¼ì„ 입력하십시오" +msgstr "ë°œë§¤ì¼ ìž…ë ¥" -#: msgid "enter game developer" -msgstr "게임 개발ìžë¥¼ ìž…ë ¥" +msgstr "개발사 ìž…ë ¥" -#: msgid "enter game publisher" -msgstr "게임 게시ìžë¥¼ ìž…ë ¥" +msgstr "íŒë§¤ì‚¬ ìž…ë ¥" -#: msgid "enter game genre" -msgstr "게임 ìž¥ë¥´ì— ë“¤ì–´ê°€ë‹¤" +msgstr "장르 ìž…ë ¥" -#: msgid "enter number of players" -msgstr "플레ì´ì–´ 수를 입력하십시오" +msgstr "플레ì´ì–´ 수 ìž…ë ¥" -#: -msgid "Favorite" -msgstr "ì¦ê²¨ì°¾ê¸°" - -#: msgid "enter favorite" -msgstr "ì¦ê²¨ì°¾ê¸° 등ë¡" +msgstr "ì¦ê²¨ì°¾ê¸° ìž…ë ¥" -#: msgid "Region" msgstr "지역" -#: msgid "enter region" -msgstr "ì§€ì—­ì„ ìž…ë ¥" +msgstr "지역 ìž…ë ¥" -#: msgid "Romtype" msgstr "롬 타입" -#: msgid "enter romtype" msgstr "롬 타입 ìž…ë ¥" -#: -msgid "Hidden" -msgstr "숨겨진" - -#: msgid "HIDDEN" -msgstr "숨겨진" - -#: -msgid "NAME" -msgstr "ì´ë¦„" - -#: -msgid "EMULATOR" -msgstr "ì—뮬레ì´í„°" +msgstr "숨김" -#: -msgid "FAVORITE" -msgstr "ì¦ê²¨ì°¾ê¸°" - -#: msgid "set hidden" -msgstr "숨기기 활성화" +msgstr "숨김 설정" -#: msgid "Play count" -msgstr "í”Œë ˆì´ íšŸìˆ˜" +msgstr "실행 횟수" -#: msgid "enter number of times played" -msgstr "í”Œë ˆì´ íšŸìˆ˜ë¥¼ 입력하십시오" +msgstr "실행 횟수 ìž…ë ¥" -#: msgid "Last played" -msgstr "마지막 플레ì´" +msgstr "최근 실행" -#: msgid "enter last played date" -msgstr "마지막 í”Œë ˆì´ ë‚ ì§œë¥¼ 입력하십시오" +msgstr "최근 실행 ì¼ìž ìž…ë ¥" -#: msgid "GAMES AVAILABLE" -msgstr "í”Œë ˆì´ ê°€ëŠ¥í•œ 게임" +msgid "게임 ì´ìš©ê°€ëŠ¥" -#: msgid "%i GAME AVAILABLE" -msgid_plural "%i 게임 플레ì´ê°€ëŠ¥" -msgstr[0] "%i 게임 플레ì´ê°€ëŠ¥" -msgstr[1] "%i 게임 플레ì´ê°€ëŠ¥" +msgid_plural "%i GAMES AVAILABLE" +msgstr[0] "%iê°œì˜ ê²Œìž„ ì´ìš©ê°€ëŠ¥" +msgstr[1] "%iê°œì˜ ê²Œìž„ ì´ìš©ê°€ëŠ¥" -#: msgid "%i FAVORITE" -msgid_plural "%i ì¦ê²¨ì°¾ê¸°" -msgstr[0] "%i ì¦ê²¨ì°¾ê¸°" -msgstr[1] "%i ì¦ê²¨ì°¾ê¸°" - -#: -msgid "%i GAME SUCCESSFULLY SCRAPED!" -msgid_plural "%i ê²Œìž„ì´ ì„±ê³µì ìœ¼ë¡œ 스í¬ëž©!" -msgstr[0] "%i ê²Œìž„ì´ ì„±ê³µì ìœ¼ë¡œ 스í¬ëž©!" -msgstr[1] "%i ê²Œìž„ì´ ì„±ê³µì ìœ¼ë¡œ 스í¬ëž©!" +msgid_plural "%i FAVORITES" +msgstr[0] "%iê°œì˜ ì¦ê²¨ì°¾ê¸°" +msgstr[0] "%iê°œì˜ ì¦ê²¨ì°¾ê¸°" -#: msgid "SCROLL" msgstr "스í¬ë¡¤" -#: msgid "LAUNCH" msgstr "실행" -#: msgid "Times played" -msgstr "í”Œë ˆì´ íšŸìˆ˜" +msgstr "실행 횟수" -#: msgid "MENU" msgstr "메뉴" -#: msgid "FILENAME, ASCENDING" -msgstr "FILENAME, 오름차순" +msgstr "파ì¼ëª… 오름차순" -#: msgid "FILENAME, DESCENDING" -msgstr "FILENAME, 내림차순" +msgstr "파ì¼ëª… 내림차순" -#: msgid "RATING, ASCENDING" -msgstr "RATING, 오름차순" +msgstr "í‰ì  오름차순" -#: msgid "RATING, DESCENDING" -msgstr "RATING, ê²°ì •" +msgstr "í‰ì  내림차순" -#: msgid "TIMES PLAYED, ASCENDING" -msgstr "í”Œë ˆì´ í•œ 시간, 오름차순" +msgstr "실행 횟수 오름차순" -#: msgid "TIMES PLAYED, DESCENDING" -msgstr "í”Œë ˆì´ í•œ 시간, 내림차순" +msgstr "실행 횟수 내림차순" -#: msgid "LAST PLAYED, ASCENDING" -msgstr "마지막으로 플레ì´, 오름차순" +msgstr "최근 실행 오름차순" -#: msgid "LAST PLAYED, DESCENDING" -msgstr "마지막 플레ì´, ê²°ì •" +msgstr "최근 실행 내림차순" -#: msgid "WORKING..." -msgstr "WORKING..." +msgstr "작업 중..." -#: msgid "CHANGE" -msgstr "CHANGE" +msgstr "변경" -#: -msgid "never" -msgstr "결코아님" - -#: msgid "just now" -msgstr "just now" +msgid "방금" -#: msgid "%i sec ago" -msgid_plural "%i ì´ˆ ì „" -msgstr[0] "%i ì´ˆ ì „" -msgstr[1] "%i ì´ˆ ì „" +msgid_plural "%i secs ago" +msgstr[0] "%iì´ˆ ì „" +msgstr[1] "%iì´ˆ ì „" -#: msgid "%i min ago" -msgid_plural "%i ë¶„ ì „" -msgstr[0] "%i ë¶„ ì „" -msgstr[1] "%i ë¶„ ì „" +msgid_plural "%i mins ago" +msgstr[0] "%ië¶„ ì „" +msgstr[1] "%ië¶„ ì „" -#: msgid "%i hour ago" -msgid_plural "%i 시간 ì „" -msgstr[0] "%i 시간 ì „" -msgstr[1] "%i 시간 ì „" +msgid_plural "%i hours ago" +msgstr[0] "%i시간 ì „" +msgstr[1] "%i시간 ì „" -#: msgid "%i day ago" -msgid_plural "%i ì¼ ì „" -msgstr[0] "%i ì¼ ì „" -msgstr[1] "%i ì¼ ì „" +msgid_plural "%i days ago" +msgstr[0] "%iì¼ ì „" +msgstr[1] "%iì¼ ì „" -#: msgid "unknown" -msgstr "알 수 없는" +msgstr "알수없ìŒ" -#: msgid "SELECT ALL" msgstr "ëª¨ë‘ ì„ íƒ" -#: msgid "SELECT NONE" -msgstr "ì—†ìŒ" +msgstr "ì„ íƒ í•´ì œ" -#: -msgid "%i SELECTED" -msgid_plural "%i SELECTED" -msgstr[0] "%i ì„ íƒ" -msgstr[1] "%i ì„ íƒ's" - -#: msgid "UP" -msgstr "위(UP)" +msgstr "위" -#: msgid "DOWN" -msgstr "아래(DOWN)" +msgstr "아래" -#: msgid "LEFT" -msgstr "좌(LEFT)" +msgstr "좌" -#: msgid "RIGHT" -msgstr "ìš°(RIGHT)" +msgstr "ìš°" -#: msgid "JOYSTICK 1 UP" -msgstr "ì¡°ì´ìŠ¤í‹± 1 위(UP)" +msgstr "ì¡°ì´ìŠ¤í‹± 1 위" -#: msgid "JOYSTICK 1 LEFT" -msgstr "ì¡°ì´ìŠ¤í‹± 1 좌(LEFT)" +msgstr "ì¡°ì´ìŠ¤í‹± 1 좌" -#: msgid "JOYSTICK 2 UP" -msgstr "ì¡°ì´ìŠ¤í‹± 2 위(UP)" +msgstr "ì¡°ì´ìŠ¤í‹± 2 위" -#: msgid "JOYSTICK 2 LEFT" -msgstr "ì¡°ì´ìŠ¤í‹± 2 좌(LEFT)" +msgstr "ì¡°ì´ìŠ¤í‹± 2 좌" -#: msgid "PAGE UP" msgstr "페ì´ì§€ 위로" -#: msgid "PAGE DOWN" msgstr "페ì´ì§€ 아래로" -#: msgid "HOTKEY" msgstr "핫키" -#: msgid "CONFIGURING" -msgstr "구성" +msgstr "설정중" -#: msgid "KEYBOARD" msgstr "키보드" -#: msgid "GAMEPAD %i" msgstr "게임패드 %i" -#. Config controllers missing translation -#: msgid "PRESS ANYTHING" -msgstr "아무버튼ì´ë‚˜ 누르십시오" +msgstr "아무 버튼ì´ë‚˜ 누르세요" -#: msgid "ALREADY TAKEN" -msgstr "ì´ë¯¸ 사용중" +msgstr "ì´ë¯¸ 사용중입니다" -#: msgid "DISCARD CHANGES" -msgstr "변경 ì‚¬í•­ì„ ì·¨ì†Œ" +msgstr "변경 취소" -#: msgid "WELCOME" -msgstr "어서 오세요." +msgstr "환ì˜í•©ë‹ˆë‹¤" -#: msgid "CONFIGURE INPUT" -msgstr "컨트롤러 ìž…ë ¥ 구성" +msgstr "ìž…ë ¥ 설정" -#: msgid "%i GAMEPAD DETECTED" msgid_plural "%i GAMEPADS DETECTED" -msgstr[0] "%i 컨트롤러 ê°ì§€" -msgstr[1] "%i 여러 컨트롤러 ê°ì§€" +msgstr[0] "%iê°œì˜ ê²Œìž„íŒ¨ë“œ íƒì§€ë¨" +msgstr[1] "%iê°œì˜ ê²Œìž„íŒ¨ë“œ íƒì§€ë¨" -#: msgid "NO GAMEPADS DETECTED" -msgstr "컨트롤러 ì—†ìŒ" +msgstr "ê²€ìƒ‰ëœ ê²Œìž„íŒ¨ë“œ ì—†ìŒ" -#: msgid "HOLD A BUTTON ON YOUR DEVICE TO CONFIGURE IT." -msgstr "장치를 구성하기 위해 Aë²„íŠ¼ì„ ê¾¸ìš± 누릅니다." +msgstr "설정 하려면 ìž¥ì¹˜ì˜ ë²„íŠ¼ì„ ê¸¸ê²Œ 누르세요." -#: msgid "PRESS F4 TO QUIT AT ANY TIME." -msgstr "언제든지 종료하려면 F4를 누르십시오." +msgstr "F4 키를 누르면 언제든 종료할 수 있습니다." -#: msgid "PRESS ESC OR THE HOTKEY TO CANCEL." -msgstr "ESC ë˜ëŠ” HOTKEY를 눌러 취소하십시오." +msgstr "취소하려면 ESC키나 핫키를 누르세요." -#: msgid "DO YOU WANT TO START KODI MEDIA CENTER ?" msgstr "KODI 미디어 센터를 시작 하시겠습니까?" -#: msgid "LOADING..." -msgstr "로딩 중 ..." +msgstr "불러오는 중..." -#: msgid "PLEASE WAIT..." -msgstr "기다려주십시오 ..." +msgstr "기다려주십시오..." -#: msgid "REALLY SHUTDOWN WITHOUT SAVING METADATAS?" -msgstr "메타 ë°ì´í„°ë¥¼ 저장하지 않고 완전히 종료 하시겠습니까?" +msgstr "메타ë°ì´í„°ë¥¼ 저장하지 않고 완전히 종료 하시겠습니까?" -#: msgid "FAST SHUTDOWN SYSTEM" msgstr "빠른 시스템 종료" -#: -msgid "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." -msgstr "ì–´ë–¤ ì‹œìŠ¤í…œë„ ì°¾ì„ ìˆ˜ 없습니다! \n" -"\n" -"ìžì„¸í•œ ë‚´ìš©ì€ emulationstation.org를 방문하십시오!" - -#: msgid "ON SCREEN KEYBOARD" -msgstr "ON 스í¬ë¦° 키보드" +msgstr "온스í¬ë¦° 키보드" -#: msgid "SHIFTS FOR UPPER,LOWER, AND SPECIAL" -msgstr "SHIFTS FOR UPPER,LOWER, AND SPECIAL" +msgstr "시프트키로 ëŒ€ì†Œë¬¸ìž ë° íŠ¹ìˆ˜ë¬¸ìž ì „í™˜" -#: msgid "SPACE" -msgstr "SPACE" +msgstr "스페ì´ìФ" -#: msgid "DELETE A CHAR" -msgstr "DELETE A CHAR" +msgstr "ë¬¸ìž ì‚­ì œ" -#: msgid "SHIFT" -msgstr "SHIFT" +msgstr "시프트" -#: msgid "STOP EDITING" -msgstr "편집 중지" +msgstr "수정 종료" -#: msgid "MOVE CURSOR" -msgstr "ì´ë™ 커서" +msgstr "커서 ì´ë™" -#: msgid "EDIT" -msgstr "편집" +msgstr "수정" -#: msgid "ACCEPT RESULT" -msgstr "ìˆ˜ë½ ê²°ê³¼" +msgstr "ê²°ê³¼ 확ì¸" -#: msgid "FILENAME" -msgstr "íŒŒì¼ ì´ë¦„" - -#: -msgid "RATING" -msgstr "í‰ê°€" +msgstr "파ì¼ëª…" -#: msgid "TIMES PLAYED" -msgstr "í”Œë ˆì´ í•œ 시간" +msgstr "실행 횟수" -#: msgid "LAST PLAYED" -msgstr "마지막으로 재ìƒ" +msgstr "최근 실행" -#: msgid "NUMBER OF PLAYERS" msgstr "플레ì´ì–´ 수" -#: -msgid "DEVELOPER" -msgstr "개발ìž" - -#: -msgid "GENRE" -msgstr "유형" - -#: msgid "SHOW HIDDEN" -msgstr "숨겨진 íŒŒì¼ í‘œì‹œ" +msgstr "숨김 표시" -#: msgid "EXTREM (1400Mhz)" -msgstr "EXTREM (1400Mhz)" +msgstr "극한 (1400Mhz)" -#: msgid "TURBO (1350Mhz)" msgstr "í„°ë³´ (1350Mhz)" -#: msgid "HIGH (1300Mhz)" msgstr "ë†’ìŒ (1300Mhz)" -#: msgid "TURBO AND EXTREM OVERCLOCK PRESETS MAY CAUSE SYSTEM UNSTABILITIES, SO USE THEM AT YOUR OWN RISK.\n" "IF YOU CONTINUE, THE SYSTEM WILL REBOOT NOW." -msgstr "í„°ë³´ ë° ê·¹í•œ 오버 í´ëŸ­ 사전 설정으로 ì¸í•´ ì‹œìŠ¤í…œì´ ë¶ˆì•ˆì •í•´ì§ˆ 수 있으므로 ìžì‹ ì˜ ì±…ìž„í•˜ì— ì‚¬ìš©í•´ì•¼í•©ë‹ˆë‹¤. \n" -"SI VOUS CONTINUEZ, LE SYSTÈME REDÉMARRERA MAINTENANT." +msgstr "í„°ë³´ ë° ê·¹í•œ 오버 í´ëŸ­ ì„¤ì •ì€ ì‹œìŠ¤í…œì„ ë¶ˆì•ˆì •í•˜ê²Œ í•  수 있으니 ë³¸ì¸ ì±…ìž„í•˜ì— ì‚¬ìš©í•˜ì‹­ì‹œì˜¤.\n" +"계ì†í•˜ë©´ ì‹œìŠ¤í…œì„ ë‹¤ì‹œ 시작 합니다." -#: msgid "%i GAME HIDDEN" msgid_plural "%i GAMES HIDDEN" -msgstr[0] "%i 숨겨진 게임" -msgstr[1] "%i 숨겨진 게임's" +msgstr[0] "%i 게임 숨김" +msgstr[1] "%i 게임 숨김" -#: msgid "Start kodi media player." -msgstr "kodi 미디어 플레ì´ì–´ë¥¼ 시작하십시오." +msgstr "kodi 미디어 플레ì´ì–´ë¥¼ 시작합니다." -#: msgid "Show how much space is used on your SHARE partition, located either on the SDCARD or on an external drive. The information shows how much GB are used and how much GB your storage has overall (example 13GB/26GB)." -msgstr "SDCARD ë˜ëŠ” 외부 드ë¼ì´ë¸Œì—있는 SHARE 파티션ì—서 사용 ëœ ê³µê°„ì„ í‘œì‹œí•˜ì‹­ì‹œì˜¤. ì´ ì •ë³´ëŠ” 사용 ëœ GB 용량과 ìŠ¤í† ë¦¬ì§€ì˜ ì „ì²´ GB 용량 (예 : 13GB / 26GB)ì„ ë³´ì—¬ì¤ë‹ˆë‹¤." +msgid "SDCARD나 외장드ë¼ì´ë¸Œì— 공유 íŒŒí‹°ì…˜ì´ ì‚¬ìš©ë˜ëŠ” ìš©ëŸ‰ì„ í‘œì‹œí•©ë‹ˆë‹¤. 정보는 ì‚¬ìš©ëœ ìš©ëŸ‰(GB)ê³¼ ì „ì²´ 저장소 용량(GB)으로 표시ë©ë‹ˆë‹¤. (예 13GB/26GB)" -#: msgid "Configure games display, ratio, filters (shaders), auto save and load and retroachievement account." -msgstr "게임 표시, 비율, í•„í„° (ì‰ì´ë”), ìžë™ 저장 ë°ë¡œë“œ ë° ì†Œê¸‰ ê³„ì •ì„ êµ¬ì„±í•©ë‹ˆë‹¤." +msgstr "게임 화면, 비율, í•„í„° (ì‰ì´ë”), ìžë™ 저장/불러오기 ë° retroachievement ê³„ì •ì„ ì„¤ì •í•©ë‹ˆë‹¤." -#: msgid "The game ratio is the ratio between image width and image height. Use AUTO to let the emulator choose the original game ratio, that will give you the best retrogaming experience." -msgstr "게임 ë¹„ìœ¨ì€ ì´ë¯¸ì§€ 너비와 ì´ë¯¸ì§€ ë†’ì´ ê°„ì˜ ë¹„ìœ¨ìž…ë‹ˆë‹¤. AUTO를 사용하여 ì—뮬레ì´í„°ê°€ ì›ëž˜ 게임 ë¹„ìœ¨ì„ ì„ íƒí•˜ë„ë¡í•˜ì—¬ 최ìƒì˜ 레트로 게임 ê²½í—˜ì„ ì œê³µí•©ë‹ˆë‹¤." +msgstr "게임 ë¹„ìœ¨ì€ ì´ë¯¸ì§€ 너비와 ë†’ì´ ì‚¬ì´ì˜ 비율입니다. ì—뮬레ì´í„°ê°€ ì›ë³¸ 게임 ë¹„ìœ¨ì„ ì„ íƒí•˜ê²Œ 해서 최ìƒì˜ 레트로 게임 ê²½í—˜ì„ ì œê³µë°›ìœ¼ë ¤ë©´ ìžë™ ì„¤ì •ì„ ì‚¬ìš©í•˜ì‹­ì‹œì˜¤." -#: msgid "Smooth the game image. This option makes the image smoother, using bilinear filtering." msgstr "게임 ì´ë¯¸ì§€ë¥¼ 부드럽게합니다. ì´ ì˜µì…˜ì€ ì´ì¤‘ 선형 í•„í„°ë§ì„ 사용하여 ì´ë¯¸ì§€ë¥¼ 부드럽게 만듭니다." -#: msgid "This option allows you to rewind the game if you get killed by a monster, or if you make any other mistake. Use the HOTKEY + LEFT command within the game to rewind." -msgstr "ì´ ì˜µì…˜ì„ ì‚¬ìš©í•˜ë©´ 몬스터가 죽거나 다른 실수를 한 경우 ê²Œìž„ì„ ë˜ê°ìŠµë‹ˆë‹¤. 게임 ë‚´ì—서 HOTKEY + LEFT ëª…ë ¹ì„ ì‚¬ìš©í•˜ì—¬ ë˜ê°ìŠµë‹ˆë‹¤." +msgstr "ì´ ì˜µì…˜ì„ ì‚¬ìš©í•˜ë©´ ì ì—게 죽거나 실수 í–ˆì„ ë•Œ ê²Œìž„ì„ ë’¤ë¡œ ë˜ê°ê²Œ í•´ì¤ë‹ˆë‹¤. 게임 ë‚´ì—서 핫키 + 좌 를 입력하십시오" -#: msgid "Auto save the state when you quit a game, and auto load last saved state when you start a game." -msgstr "ê²Œìž„ì„ ì¢…ë£Œ í•  때 ìƒíƒœë¥¼ ìžë™ 저장하고 ê²Œìž„ì„ ì‹œìž‘í•  때 마지막으로 ì €ìž¥ëœ ìƒíƒœë¥¼ ìžë™ë¡œë“œí•©ë‹ˆë‹¤." +msgstr "ê²Œìž„ì„ ì¢…ë£Œ í•  때 ìƒíƒœë¥¼ ìžë™ 저장하고 ê²Œìž„ì„ ì‹œìž‘í•  때 마지막으로 ì €ìž¥ëœ ìƒíƒœë¥¼ ìžë™ìœ¼ë¡œ 불러옵니다." -#: msgid "Integer scaling is scaling by a factor of a whole number, such as 2x, 3x, 4x, etc. This option scales the image up to the greatest integer scale below the set resolution. So for instance, if you set your fullscreen resolution to 1920x1080 and enable integer scaling, it will only scale a 320x240 image up to 1280x960, and leave black borders all around. This is to maintain a 1:1 pixel ratio with the original source image, so that pixels are not unevenly duplicated." -#VALUE! +msgid "정수단위 확대는 2x, 3x, 4x 등 정수 ë‹¨ìœ„ë¡œì˜ í™•ëŒ€ë¥¼ ë§í•©ë‹ˆë‹¤. ì´ ì˜µì…˜ì€ ì„¤ì •ëœ í•´ìƒë„ 안ì—서 가장 í° ì •ìˆ˜ 단위 ì´ë¯¸ì§€ë¥¼ 확대합니다. 예를 들어 화면 í•´ìƒë„ê°€ 1920x1080ì´ê³  정수단위 확대가 켜져있으면 320x240 í™”ë©´ì€ 1280x960로 확대ë˜ê³  나머지 빈 ê³µê°„ì€ ê²€ì€ ê³µë§¥ìœ¼ë¡œ 남게 ë©ë‹ˆë‹¤. ì´ ì„¤ì •ìœ¼ë¡œ ì›ë³¸ 소스 ì´ë¯¸ì§€ì˜ 1:1픽셀 ë¹„ìœ¨ì„ ìœ ì§€í•˜ê²Œ ë©ë‹ˆë‹¤." -#: msgid "Shaders are like filters for the game rendering. You can select a shader set here, which is a collection of shaders selected for each system. You can also change the shader within the game with HOTKEY + L2 or HOTKEY + R2." -msgstr "ì…°ì´ë”는 게임 ë Œë”ë§ì„위한 필터와 같습니다. 여기ì—서 ê° ì‹œìŠ¤í…œì— ëŒ€í•´ ì„ íƒëœ ì‰ì´ë” ëª¨ìŒ ì¸ ì‰ì´ë” 세트를 ì„ íƒí•  수 있습니다. HOTKEY + L2 ë˜ëŠ” HOTKEY + R2를 사용하여 게임 ë‚´ì—서 ì…°ì´ë”를 변경할 ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤." +msgstr "ì…°ì´ë”는 게임 ë Œë”ë§ì„위한 필터와 같습니다. 여기ì—서 ê°ê°ì˜ ì‹œìŠ¤í…œì— ëŒ€í•œ ì‰ì´ë” 모ìŒì„ ì„ íƒí•  수 있습니다. 핫키 + L2 ë˜ëŠ” 핫키 + R2를 사용해 게임 ë‚´ì—ì„œë„ ì…°ì´ë”를 변경할 ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤." -#: msgid "Enable or disable RetroAchievements in games." -msgstr "게임ì—서 RetroAchievements를 활성화 ë˜ëŠ” 비활성화합니다." +msgid "게임ì—서 RetroAchievements 를 활성화 ë˜ëŠ” 비활성화합니다." -#: msgid "Hardcore mode disables *all* savestate and rewind functions within the emulator: you will not be able to save and reload at any time. You will have to complete the game and get the achievements first time, just like on the original console. In reward for this, you will earn both the standard and the hardcore achievement, in effect earning double points! A regular game worth 400 points, is now worth 800 if you complete it on hardcore! For example: if you complete the game for 400 points, you then have the opportunity to earn another 400 on hardcore." -#VALUE! +msgid "하드코어 모드 설정시 모든 ìƒíƒœì €ìž¥ ë° ë˜ê°ê¸° ê¸°ëŠ¥ì´ ë¹„í™œì„±ë©ë‹ˆë‹¤ : 아무때나 불러오고 저장하지 못하게 ë˜ë©° ì˜¤ë¦¬ì§€ë„ ì½˜ì†”ì—서 처럼 한 ë²ˆì— ê²Œìž„ì„ ë내야 ë„전과제를 ì–»ì„ ìˆ˜ 있습니다. ë³´ìƒìœ¼ë¡œ ì¼ë°˜ ë° í•˜ë“œì½”ì–´ ë„전과제를 ë™ì‹œì— 얻게 ë˜ê³  ë‘ë°°ì˜ í¬ì´íŠ¸ë¥¼ ë°›ì„ ìˆ˜ 있습니다! - -#: msgid "The website retroachievements.org proposes challenges/achievements/trophies on platforms like NES, SNES, GB, GBC, GBA, Genesis/Megadrive, TurboGrafx16/PCEngine and more! Create your account on retroachievements.org and start your quest for achievements!" -msgstr "retroachievements.org 웹 사ì´íŠ¸ëŠ” NES, SNES, GB, GBC, GBA, Genesis / Megadrive, TurboGrafx16 / PCEngine 등과 ê°™ì€ í”Œëž«í¼ì—서 ë„ì „ / ì—…ì  / 트로피를 제안합니다! retroachievements.orgì— ê³„ì •ì„ ë§Œë“¤ê³  ë„ì „ 과제를 시작하십시오!" +msgid "retroachievements.orgì—서 NES, SNES, GB, GBC, GBA, 메가드ë¼ì´ë¸Œ, PC엔진 등 플랫í¼ì— 대한 ë„전과제를 제공합니다. retroachievements.orgì—서 ê³„ì •ì„ ìƒì„±í•˜ê³  ë„ì „ê³¼ì œì— ë„ì „í•´ 보세요!" -#: msgid "Add and configure up to 5 controllers." msgstr "최대 5 ê°œì˜ ì»¨íŠ¸ë¡¤ëŸ¬ë¥¼ 추가하고 구성하십시오." -#: msgid "Start the screensaver after N minutes." -msgstr "N ë¶„ 후 화면 보호기를 시작하십시오." +msgstr "Në¶„ 후 화면 보호기를 시작합니다." -#: msgid "Set the screensaver behavior. DIM will reduce the screen light, and BLACK will turn the screen black." -msgstr "화면 보호기 ë™ìž‘ì„ ì„¤ì •í•˜ì‹­ì‹œì˜¤. DIMì€ í™”ë©´ ì¡°ëª…ì„ ì¤„ì´ê³  BLACKì€ í™”ë©´ì„ ê²€ê²Œ 만듭니다." +msgstr "화면보호기 ë™ìž‘ì„ ì„¤ì •í•©ë‹ˆë‹¤. 어둡게는 화면 ì¡°ëª…ì„ ì–´ë‘¡ê²Œí•˜ê³  ê²€ì€í™”ë©´ì€ í™”ë©´ì„ ê²€ê²Œ 만듭니다." -#: msgid "Shows a help at the bottom of the screen which displays commands you can use." -msgstr "화면 í•˜ë‹¨ì— ì‚¬ìš©í•  수있는 ëª…ë ¹ì„ í‘œì‹œí•˜ëŠ” ë„움ë§ì´ 표시ë©ë‹ˆë‹¤." +msgstr "화면 í•˜ë‹¨ì— ì‚¬ìš© 가능한 ëª…ë ¹ì„ ë‚˜ì˜¨ ë„움ë§ì´ 표시ë©ë‹ˆë‹¤." -#: msgid "When enabled, you can switch between systems while browsing a gamelist by pressing LEFT or RIGHT." -msgstr "활성화ë˜ë©´ LEFT ë˜ëŠ” RIGHT를 눌러 게임 목ë¡ì„ íƒìƒ‰í•˜ëŠ” ë™ì•ˆ ì‹œìŠ¤í…œê°„ì— ì „í™˜ í•  수 있습니다." +msgstr "활성화하면 게임 목ë¡ì„ íƒìƒ‰í•˜ëŠ” ë™ì•ˆ 좌/ìš° ë²„íŠ¼ì„ ëˆŒëŸ¬ 시스템간 ì „í™˜ì´ ê°€ëŠ¥í•©ë‹ˆë‹¤." -#: msgid "Updates the gamelists, if you added games since the last boot." msgstr "마지막 부팅 ì´í›„ ê²Œìž„ì„ ì¶”ê°€ 한 경우 게임 목ë¡ì„ ì—…ë°ì´íŠ¸í•©ë‹ˆë‹¤." -#: msgid "Set the volume of the sound output for the frontend and the games." -msgstr "프론트 엔드 ë° ê²Œìž„ì˜ ì‚¬ìš´ë“œ 출력 ë³¼ë¥¨ì„ ì„¤ì •í•˜ì‹­ì‹œì˜¤." +msgstr "프론트엔드 ë° ê²Œìž„ì˜ ì‚¬ìš´ë“œ 출력 ë³¼ë¥¨ì„ ì„¤ì •í•©ë‹ˆë‹¤." -#: msgid "Get informations and visual for your games. The scraper downloads metadata and visuals for your games from different servers and enhances the user experience in EmulationStation completely." -msgstr "ê²Œìž„ì— ëŒ€í•œ 정보와 시ê°ì  정보를 얻으십시오. 스í¬ë ˆì´í¼ëŠ” 다른 서버ì—서 ê²Œìž„ì— ëŒ€í•œ 메타 ë°ì´í„°ì™€ ë¹„ì£¼ì–¼ì„ ë‹¤ìš´ë¡œë“œí•˜ê³  EmulationStationì˜ ì‚¬ìš©ìž ê²½í—˜ì„ ì™„ì „ížˆ í–¥ìƒì‹œí‚µë‹ˆë‹¤." +msgstr "게임 정보와 ì´ë¯¸ì§€ë¥¼ 가져옵니다. 스í¬ëž˜í¼ëŠ” 다른 서버ì—서 ê²Œìž„ì— ëŒ€í•œ 메타 ë°ì´í„°ì™€ ì‹œê° ìžë£Œë¥¼ 다운로드하고 EmulationStationì˜ ì‚¬ìš©ìž ê²½í—˜ì„ í–¥ìƒì‹œí‚µë‹ˆë‹¤." -#: msgid "Select a server to scrape from. The SCREENSCRAPER server is recommended and is based on www.screenscraper.fr and scrapes game data in your language, if available." -msgstr "스í¬ëž© í•  서버를 ì„ íƒí•˜ì‹­ì‹œì˜¤. SCREENSCRAPER 서버는 권장ë˜ë©° www.screenscraper.frì„ ê¸°ë°˜ìœ¼ë¡œí•˜ë©° 사용 가능한 언어로 게임 ë°ì´í„°ë¥¼ 스í¬ëž©í•©ë‹ˆë‹¤." +msgstr "스í¬ëž© í•  서버를 ì„ íƒí•˜ì‹­ì‹œì˜¤. SCREENSCRAPER 서버를 권장하며 www.screenscraper.frì„ ê¸°ë°˜ìœ¼ë¡œ 게임 ë°ì´í„°ë¥¼ 스í¬ëž©í•©ë‹ˆë‹¤." -#: msgid "Begin the scrape process with the configuration shown below." -msgstr "아래 í‘œì‹œëœ êµ¬ì„±ìœ¼ë¡œ 스í¬ëž© 프로세스를 시작하십시오." +msgstr "아래 í‘œì‹œëœ êµ¬ì„±ìœ¼ë¡œ 스í¬ëž© ìž‘ì—…ì„ ì‹œìž‘í•©ë‹ˆë‹¤." -#: msgid "Scrape and display game ratings." -msgstr "게임 ë“±ê¸‰ì„ ê¸ì–´ 모으고 표시하십시오." +msgstr "게임 í‰ì ì„ 가져오고 표시합니다." -#: msgid "Advanced settings. Please make sure you really know what you're doing, before changing any values in this menu." -msgstr "고급 설정. ì´ ë©”ë‰´ì—서 ê°’ì„ ë³€ê²½í•˜ê¸° ì „ì— í˜„ìž¬ ìˆ˜í–‰ì¤‘ì¸ ìž‘ì—…ì„ ì•Œê³  있는지 확ì¸í•˜ì‹­ì‹œì˜¤." +msgstr "고급 설정입니다. 메뉴ì—서 ê°’ì„ ë³€ê²½í•˜ê¸° ì „ì— ë¬´ì—‡ì„ ìˆ˜ì •í•˜ëŠ”ì§€ 알고 있는지 확ì¸í•˜ì‹­ì‹œì˜¤." -#: msgid "Overclock your board to increase the performance.\n" +msgstr "ì„±ëŠ¥ì„ ë†’ì´ë ¤ë©´ 오버 í´ë¡œí‚¹í•˜ì‹­ì‹œì˜¤. \n" -msgstr "ì„±ëŠ¥ì„ ë†’ì´ë ¤ë©´ 보드를 오버 í´ë¡œí‚¹í•˜ì‹­ì‹œì˜¤. \n" - - - -#: msgid "Only show games contained in the gamelist.xml file (located in your roms directories).\n" +msgstr "gamelist.xml ì— í¬í•¨ëœ 게임만 표시합니다\n" -msgstr "roms 디렉토리ì—있는 gamelist.xml 파ì¼ì— í¬í•¨ ëœ ê²Œìž„ ë§Œ 표시합니다. \n" - - -#: msgid "This option allows you to set the selected system to fixed mode. With this option activated, the user cannot access other systems." -msgstr "ì´ ì˜µì…˜ì„ ì‚¬ìš©í•˜ë©´ ì„ íƒí•œ ì‹œìŠ¤í…œì„ ê³ ì • 모드로 설정할 수 있습니다. ì´ ì˜µì…˜ì„ í™œì„±í™”í•˜ë©´ 사용ìžëŠ” 다른 ì‹œìŠ¤í…œì— ì•¡ì„¸ìŠ¤ í•  수 없습니다." +msgstr "ì´ ì˜µì…˜ì„ ì‚¬ìš©í•˜ë©´ ì„ íƒí•œ ì‹œìŠ¤í…œì„ ê³ ì • 모드로 설정할 수 있습니다. 활성화하면 사용ìžëŠ” 다른 ì‹œìŠ¤í…œì— ì•¡ì„¸ìŠ¤ í•  수 없게ë©ë‹ˆë‹¤." - -#: msgid "Always display the basic gamelist view, even if you have scraped your games." -msgstr "ê²Œìž„ì„ í기 한 경우ì—ë„ í•­ìƒ ê¸°ë³¸ 게임 목ë¡ë³´ê¸°ë¥¼ 표시하십시오." +msgstr "게임 정보를 가져온 경우ì—ë„ í•­ìƒ ê¸°ë³¸ 게임 목ë¡ë³´ê¸°ë¥¼ 표시합니다." -#: msgid "Enable or disable Kodi, customize the Kodi startup, enable the X button to start Kodi" -msgstr "Kodi 활성화 ë˜ëŠ” 비활성화, Kodi 시작 ì‚¬ìš©ìž ì •ì˜, X ë²„íŠ¼ì„ í™œì„±í™”í•˜ì—¬ Kodi 시작" +msgstr "Kodi 활성화 ë˜ëŠ” 비활성화, Kodi 시작 ì‚¬ìš©ìž ì •ì˜, X 버튼으로 Kodi 시작" -#: msgid "Enable or disable Kodi. If kodi is disabled, you won't be able to start it with the X button, or start it automatically at boot. The menu entry will be removed as well." msgstr "Kodi를 활성화 ë˜ëŠ” 비활성화합니다. kodiê°€ 비활성화ë˜ì–´ 있으면 X 버튼으로 시작하거나 부팅시 ìžë™ìœ¼ë¡œ 시작할 수 없습니다. 메뉴 í•­ëª©ë„ ì œê±°ë©ë‹ˆë‹¤." -#: msgid "Use the X button to start Kodi." msgstr "X ë²„íŠ¼ì„ ì‚¬ìš©í•˜ì—¬ Kodi를 시작하십시오." -#: msgid "Automatically start into Kodi on boot." msgstr "부팅시 Kodi를 ìžë™ìœ¼ë¡œ 시작합니다." -#: msgid "Show the framerate in EmulationStation and in game." -msgstr "EmulationStation ë° ê²Œìž„ì—서 프레임 ì†ë„를 표시하십시오." +msgstr "EmulationStation ë° ê²Œìž„ì—서 프레임레ì´íŠ¸ë¥¼ 표시하십시오." -#: msgid "Select which emulator to use when you start a game for this system." -msgstr "ì´ ì‹œìŠ¤í…œì—서 ê²Œìž„ì„ ì‹œìž‘í•  때 사용할 ì—뮬레ì´í„°ë¥¼ ì„ íƒí•˜ì‹­ì‹œì˜¤." +msgstr "현재 시스템ì—서 ê²Œìž„ì„ ì‹œìž‘í•  때 사용할 ì—뮬레ì´í„°ë¥¼ ì„ íƒí•˜ì‹­ì‹œì˜¤." -#: msgid "Select which core to use for the selected emulator. For example, the LIBRETRO emulator has many cores to run Super Nintendo games. The default core you choose here can also be overridden in game specific settings." -msgstr "ì„ íƒí•œ ì—뮬레ì´í„°ì— 사용할 코어를 ì„ íƒí•˜ì‹­ì‹œì˜¤. 예를 들어, LIBRETRO ì—뮬레ì´í„°ì—는 Super Nintendo ê²Œìž„ì„ ì‹¤í–‰í•˜ê¸°ìœ„í•œ ë§Žì€ ì½”ì–´ê°€ 있습니다. 여기서 ì„ íƒí•œ 기본 코어는 게임 별 설정ì—서 ìž¬ì •ì˜ ë  ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤." +msgstr "ì„ íƒí•œ ì—뮬레ì´í„°ì— 사용할 코어를 ì„ íƒí•˜ì‹­ì‹œì˜¤. 예를 들어, LIBRETRO ì—뮬레ì´í„°ì—는 Super Nintendo ê²Œìž„ì„ ì‹¤í–‰í•˜ê¸° 위한 ë§Žì€ ì½”ì–´ê°€ 있습니다. 기본 코어는 게임 별 설정ì—서 ìž¬ì •ì˜ ë  ìˆ˜ë„ ìžˆìŠµë‹ˆë‹¤." -#: msgid "USE COMPOSED VISUALS" -msgstr "USE COMPOSED VISUALS" +msgstr "ì¡°í•©ëœ ì´ë¯¸ì§€ë¥¼ 사용" -#: msgid "CHECK UPDATES" msgstr "ì—…ë°ì´íЏ 확ì¸" -#: msgid "UPDATE TYPE" msgstr "ì—…ë°ì´íЏ 유형" -#: msgid "INTEGER SCALE (PIXEL PERFECT)" -msgstr "INTEGER SCALE (PIXEL PERFECT)" +msgstr "정수단위 확대" -#: msgid "ADVANCED SETTINGS" msgstr "고급 설정" -#: msgid "BOOT SETTINGS" -msgstr "부팅 설정" +msgstr "부트 설정" -#: msgid "GAMELIST ONLY" -msgstr "GAMELIST ONLY" +msgid "ê²Œìž„ëª©ë¡ ë§Œ" -#: msgid "START ON SYSTEM" -msgstr "START ON SYSTEM" +msgstr "시작 시스템" -#: msgid "BOOT ON GAMELIST" -msgstr "BOOT ON GAMELIST" +msgid "게임목ë¡ìœ¼ë¡œ 시작" -#: msgid "HIDE SYSTEM VIEW" -msgstr "HIDE SYSTEM VIEW" +msgstr "시스템 보기 숨김" -#: msgid "EMULATOR ADVANCED CONFIGURATION" -msgstr "ì—뮬레ì´í„° 고급 구성" +msgstr "고급 ì—뮬레ì´í„° 설정" -#: msgid "ADVANCED EMULATOR CONFIGURATION" -msgstr "ì—뮬레ì´í„° 고급 구성" +msgstr "고급 ì—뮬레ì´í„° 설정" -#: msgid "HELP" -msgstr "ë„움" +msgstr "ë„움ë§" -#: msgid "THE SYSTEM IS UP TO DATE" msgstr "ì‹œìŠ¤í…œì´ ìµœì‹ ìž…ë‹ˆë‹¤" -#: msgid "FORCE BASIC GAMELIST VIEW" -msgstr "FORCE BASIC GAMELIST VIEW" +msgstr "강제로 기본 ê²Œìž„ëª©ë¡ ì„¤ì •" -#: msgid "DOWNLOADED" -msgstr "다운로드" +msgstr "다운로드ë¨" -#: msgid "UPDATE VERSION:" -msgstr "ì—…ë°ì´íЏ 버전 :" +msgstr "ì—…ë°ì´íЏ 버전:" -#: msgid "UPDATE CHANGELOG:" -msgstr "ì—…ë°ì´íЏ 변경 로그 :" +msgstr "ì—…ë°ì´íЏ 변경 로그:" -#: msgid "MORE DETAILS" -msgstr "ìžì„¸í•œ ë‚´ìš©ì€" +msgstr "세부 ì •ë³´" -#: msgid "CAROUSEL TRANSITIONS" -msgstr "CAROUSEL TRANSITIONS" +msgstr "회전메뉴 전환 효과" -#: msgid "ENABLE FILTERS" -msgstr "í•„í„° 활성화" +msgstr "í•„í„° 사용" -#: msgid "THEME CONFIGURATION" msgstr "테마 구성" -#: msgid "THEME COLORSET" msgstr "테마 색ìƒ" -#: msgid "THEME ICONSET" msgstr "테마 ì•„ì´ì½˜" -#: msgid "THEME MENU" msgstr "테마 메뉴" -#: msgid "THEME SYSTEMVIEW" -msgstr "테마 시스템" +msgstr "테마 시스템보기" -#: msgid "THEME GAMELISTVIEW" -msgstr "테마 게임리스트" +msgstr "테마 게임목ë¡" -#: msgid "THEME REGION" msgstr "테마 지역" -#: msgid "THIS THEME HAS NO OPTION" -msgstr "ì´ í…Œë§ˆì—는 ì˜µì…˜ì´ ì—†ìŠµë‹ˆë‹¤" +msgstr "현재 테마ì—는 ì˜µì…˜ì´ ì—†ìŠµë‹ˆë‹¤" -#: msgid "MANUAL INPUT" msgstr "ìˆ˜ë™ ìž…ë ¥" -#: msgid "AN ERROR OCCURED - DOWNLOADED" msgstr "오류가 ë°œìƒí–ˆìŠµë‹ˆë‹¤-다운로드" -#: msgid "START KODI" msgstr "KODI 시작" -#: msgid "Shows the current available update version." -msgstr "사용 가능한 현재 ì—…ë°ì´íЏ ë²„ì „ì„ í‘œì‹œí•©ë‹ˆë‹¤." +msgstr "현재 사용 가능한 ì—…ë°ì´íЏ ë²„ì „ì„ í‘œì‹œí•©ë‹ˆë‹¤." -#: msgid "Shows the current available update changelog." msgstr "현재 사용 가능한 ì—…ë°ì´íЏ 변경 로그를 표시합니다." -#: msgid "Configure an associated controller. Your controller has to be associated / plugged before." -msgstr "ì—°ê²°ëœ ì»¨íŠ¸ë¡¤ëŸ¬ë¥¼ 구성하십시오. 컨트롤러를 연결하고 연결해야합니다." +msgstr "ì—°ê²°ëœ ì»¨íŠ¸ë¡¤ëŸ¬ë¥¼ 구성합니다. 컨트롤러를 먼저 연결해야합니다." -#: msgid "Choose if carousel will be animated or not during transitions" -msgstr "전환하는 ë™ì•ˆ 슬ë¼ì´ë“œ ì‡¼ì— ì• ë‹ˆë©”ì´ì…˜ì„ ì ìš©í• ì§€ 여부를 ì„ íƒí•©ë‹ˆë‹¤" +msgstr "전환하는 ë™ì•ˆ 애니메ì´ì…˜ì„ ì ìš©í• ì§€ 여부를 ì„ íƒí•©ë‹ˆë‹¤" -#: msgid "Select the type of transition that occurs when you start a game. INSTANT will do nothing, FADE will fade to dark, and SLIDE will zoom on the game cover (or name if there is no scrape information)" -msgstr "ê²Œìž„ì„ ì‹œìž‘í•  때 ë°œìƒí•˜ëŠ” 전환 ìœ í˜•ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤. INSTANT는 아무 것ë„하지 않고 FADE는 ì–´ë‘워지고 SLIDE는 게임 커버를 확대합니다 (ë˜ëŠ” 스í¬ëž© 정보가없는 경우 ì´ë¦„)." +msgstr "게임 시작 시 ì ìš©í•  전환 ìœ í˜•ì„ ì„ íƒí•©ë‹ˆë‹¤. 즉시는 아무 것ë„하지 않고 어둡게는 ì–´ë‘워지고 슬ë¼ì´ë“œëŠ” 게임 ì´ë¯¸ì§€ë¥¼ 확대커버ì—서 확대ë©ë‹ˆë‹¤(ì´ë¯¸ì§€ê°€ ì—†ì„ ê²½ìš° ì´ë¦„ì´ í™•ëŒ€)." -#: msgid "Select exisiting colorset options for this theme." -msgstr "ì´ í…Œë§ˆì— ëŒ€í•œ 기존 ìƒ‰ìƒ ì„¸íŠ¸ ì˜µì…˜ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤." +msgstr "í…Œë§ˆì— ëŒ€í•œ ìƒ‰ìƒ ì„¸íŠ¸ ì˜µì…˜ì„ ì„ íƒí•©ë‹ˆë‹¤." -#: msgid "Select exisiting iconset options for this theme." -msgstr "ì´ í…Œë§ˆì— ëŒ€í•œ 기존 ì•„ì´ì½˜ 세트 ì˜µì…˜ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤." +msgstr "í…Œë§ˆì— ëŒ€í•œ ì•„ì´ì½˜ 세트 ì˜µì…˜ì„ ì„ íƒí•©ë‹ˆë‹¤." -#: msgid "Select exisiting menu style options for this theme." -msgstr "ì´ í…Œë§ˆì— ëŒ€í•œ 기존 메뉴 ìŠ¤íƒ€ì¼ ì˜µì…˜ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤." +msgstr "í…Œë§ˆì— ëŒ€í•œ 메뉴 ìŠ¤íƒ€ì¼ ì˜µì…˜ì„ ì„ íƒí•©ë‹ˆë‹¤." -#: msgid "Select exisiting system view options for this theme." -msgstr "ì´ í…Œë§ˆì— ëŒ€í•œ 기존 시스템보기 ì˜µì…˜ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤." +msgstr "í…Œë§ˆì— ëŒ€í•œ 시스템 보기 ì˜µì…˜ì„ ì„ íƒí•©ë‹ˆë‹¤." -#: msgid "Select exisiting gamelist view options for this theme." -msgstr "ì´ í…Œë§ˆì— ëŒ€í•œ 기존 게임 목ë¡ë³´ê¸° ì˜µì…˜ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤." +msgstr "í…Œë§ˆì— ëŒ€í•œ ê²Œìž„ëª©ë¡ ë³´ê¸° ì˜µì…˜ì„ ì„ íƒí•©ë‹ˆë‹¤." -#: msgid "Configure theme options if available." -msgstr "가능한 경우 테마 ì˜µì…˜ì„ êµ¬ì„±í•˜ì‹­ì‹œì˜¤." +msgstr "ì´ìš©ê°€ëŠ¥í•œ 테마 ì˜µì…˜ì„ ì„¤ì •í•©ë‹ˆë‹¤." -#: msgid "Select Region of logos, pictures for system that are different for some countries. E.g. Megadrive in EU / Genesis in US" -msgstr "ì¼ë¶€ êµ­ê°€ì—서는 다른 ì‹œìŠ¤í…œì˜ ë¡œê³  ì˜ì—­, ê·¸ë¦¼ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤. 예 : EUì˜ Megadrive / ë¯¸êµ­ì˜ Genesis" +msgid "국가별로 다른 ì‹œìŠ¤í…œì˜ ë¡œê³ , ì‚¬ì§„ì— ëŒ€í•œ 지역 ì„¤ì •ì„ í•©ë‹ˆë‹¤. 예: 아시아 유럽 메가드ë¼ì´ë¸Œ / ë¶ë¯¸ 제네시스" -#: msgid "Type the name of your SSID if it is hidden or not listed" -msgstr "SSIDê°€ 숨겨져 있거나 나열ë˜ì§€ ì•Šì€ ê²½ìš° ì´ë¦„ì„ ìž…ë ¥í•˜ì‹­ì‹œì˜¤." +msgstr "SSIDê°€ 숨겨져 있거나 목ë¡ì— ì—†ì„ ê²½ìš° SSID를 ì§ì ‘ 입력합니다." -#: msgid "Select a letter and the listing will go directly on the first game starting with this letter." msgstr "문ìžë¥¼ ì„ íƒí•˜ë©´ì´ 문ìžë¡œ 시작하는 첫 번째 게임ì—서 목ë¡ì´ 바로 표시ë©ë‹ˆë‹¤." -#: msgid "Select the way the game list is sortered (alphabetically, by notation...)." -msgstr "게임 목ë¡ì„ 정렬하는 ë°©ë²•ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤ (알파벳, 표기법 ...)." +msgstr "게임목ë¡ì„ 정렬하는 ë°©ë²•ì„ ì„ íƒí•˜ì‹­ì‹œì˜¤(알파벳순 등...)" -#: msgid "Switch between seing or not only the favorites games. To add a game in the favorite list, select the game and toggle its state using 'Y'." -msgstr "ì¦ê²¨ 찾기 게임ë¿ë§Œ ì•„ë‹ˆë¼ seingê°„ì— ì „í™˜ í•  수 있습니다. ì¦ê²¨ 찾기 목ë¡ì— ê²Œìž„ì„ ì¶”ê°€í•˜ë ¤ë©´ ê²Œìž„ì„ ì„ íƒí•˜ê³  'Y'를 사용하여 ìƒíƒœë¥¼ 토글하십시오." +msgstr "ì¦ê²¨ì°¾ê¸° 게임 표시를 전환합니다. ì¦ê²¨ì°¾ê¸° 목ë¡ì— ê²Œìž„ì„ ì¶”ê°€í•˜ë ¤ë©´ ê²Œìž„ì„ ì„ íƒí•˜ê³  'Y'를 누르십시오." -#: msgid "Switch between seing or not the hidden games. To hide a game, edit its data and select 'Hide'." -msgstr "숨겨진 게임 사ì´ë¥¼ 전환할지 여부를 전환하십시오. ê²Œìž„ì„ ìˆ¨ê¸°ë ¤ë©´ 해당 ë°ì´í„°ë¥¼ 편집하고 '숨기기'를 ì„ íƒí•˜ì‹­ì‹œì˜¤." +msgstr "숨겨진 게임 표시를 전환합니다. ê²Œìž„ì„ ìˆ¨ê¸°ë ¤ë©´ 해당 ë°ì´í„°ë¥¼ 편집하고 '숨기기'를 ì„ íƒí•˜ì‹­ì‹œì˜¤." -#: msgid "This option display a menu which allows to change game data and many others options." msgstr "ì´ ì˜µì…˜ì€ ê²Œìž„ ë°ì´í„° ë° ê¸°íƒ€ 여러 ì˜µì…˜ì„ ë³€ê²½í•  수있는 메뉴를 표시합니다." -#: msgid "AVAILABLE UPDATE" msgstr "사용 가능한 ì—…ë°ì´íЏ" -#: msgid "UPDATE CHANGELOG" -msgstr "ì—…ë°ì´íЏ 변경" +msgstr "변경사항 ì—…ë°ì´íЏ" -#: msgid "CLOCK IN MENU" -msgstr "CLOCK IN MENU" +msgstr "ë©”ë‰´ì— ì‹œê³„ 표시" -#: msgid "Now playing" -msgstr "플레ì´ì¤‘" +msgstr "실행 중" -#: msgid "DEFAULT (%1%)" msgstr "기본 (%1%)" -#: msgid "INPUT REQUIRED" -msgstr "INPUT REQUIRED" +msgstr "ìž…ë ¥ í•„ìš”" -#: msgid "(skipped)" -msgstr "(skipped)" +msgstr "(건너뜀)" -#: msgid "UP/DOWN TO SKIP" -msgstr "UP/DOWN TO SKIP" +msgstr "건너뛰려면 위/아래" -#: msgid "A TO UNSET" -msgstr "A TO UNSET" +msgstr "A버튼으로 설정해제" -#: msgid "DOWN TO SKIP AND KEEP [%1%]" -msgstr "건너 ë›°ê³  유지 [%1%]" +msgstr "건너 뛰려면 아래 [%1%] 유지" -#: msgid "UP/DOWN TO SKIP AND KEEP [%1%]" -msgstr "건너 뛰기 ë° ìœ ì§€ë¡œ ì—… / 다운 [%1%]" +msgstr "건너 뛰려면 위/아래 [%1%] 유지" -#: msgid "Set duration of help popups, 0 means no popup." -msgstr "ë„ì›€ë§ íŒì—… ê¸°ê°„ì„ ì„¤ì •í•©ë‹ˆë‹¤. 0ì€ íŒì—…ì´ ì—†ìŒì„ ì˜ë¯¸í•©ë‹ˆë‹¤." +msgstr "ë„ì›€ë§ í‘œì‹œ ì‹œê°„ì„ ì„¤ì •í•©ë‹ˆë‹¤. 0ì€ í‘œì‹œ 안함." -#: msgid "HELP POPUP DURATION" -msgstr "íŒì—… ì§€ì† ì‹œê°„ ì§€ì›" +msgstr "ë„ì›€ë§ í‘œì‹œ 시간" -#: msgid "Set duration of music popups, 0 means no popup." -msgstr "ìŒì•… íŒì—… ê¸°ê°„ì„ ì„¤ì •í•©ë‹ˆë‹¤. 0ì€ íŒì—…ì´ ì—†ìŒì„ ì˜ë¯¸í•©ë‹ˆë‹¤." +msgstr "ìŒì•… 표시 ì‹œê°„ì„ ì„¤ì •í•©ë‹ˆë‹¤. 0ì€ í‘œì‹œ 안함." -#: msgid "MUSIC POPUP DURATION" -msgstr "ìŒì•… íŒì—… 기간" +msgstr "ìŒì•… 표시 시간" -#: msgid "POPUP SETTINGS" -msgstr "íŒì—… 설정" +msgstr "íŒì—… 알람 설정" -#: msgid "POPUP POSITION" -msgstr "íŒì—… 위치" +msgstr "íŒì—… 알람 위치" -#: msgid "Select the position of popups on screen." -msgstr "화면ì—서 íŒì—… 위치를 ì„ íƒí•˜ì‹­ì‹œì˜¤." +msgstr "화면ì—서 íŒì—… 알람 위치를 ì„ íƒí•˜ì‹­ì‹œì˜¤." -#: msgid "Set position and duration of popups." -msgstr "íŒì—…ì˜ ìœ„ì¹˜ì™€ ì§€ì† ì‹œê°„ì„ ì„¤ì •í•˜ì‹­ì‹œì˜¤." +msgstr "íŒì—… ì•ŒëžŒì˜ ìœ„ì¹˜ì™€ ì§€ì† ì‹œê°„ì„ ì„¤ì •í•˜ì‹­ì‹œì˜¤." -#: msgid "TOP/RIGHT" -msgstr "맨 위 오른쪽" +msgstr "ìƒë‹¨ / 오른쪽" -#: msgid "BOTTOM/RIGHT" msgstr "하단 / 오른쪽" -#: msgid "BOTTOM/LEFT" msgstr "하단 / 왼쪽" -#: msgid "TOP/LEFT" msgstr "ìƒë‹¨ / 왼쪽" -#: msgid "SHOW FOLDERS CONTENT" msgstr "í´ë” ë‚´ìš© 표시" -#: msgid "Switch between seeing the folders structure and seeing all games in a flatten top level." -msgstr "í´ë” 구조보기와 í‰í‰í•œ 최ìƒìœ„ ë ˆë²¨ì˜ ëª¨ë“  게임보기 사ì´ë¥¼ 전환하십시오." +msgstr "í´ë” 구조 보기와 ì „ì²´ 게임 보기 사ì´ë¥¼ 전환합니다." -#: msgid "NETPLAY" -msgstr "ë„· 플레ì´" +msgstr "넷플레ì´" -#: msgid "NETPLAY SETTINGS" msgstr "ë„¤íŠ¸ì›Œí¬ ì„¤ì •" -#: msgid "NETPLAY LOBBY" -msgstr "ë„· í”Œë ˆì´ ë¡œë¹„" +msgstr "ë„·í”Œë ˆì´ ë¡œë¹„" -#: msgid "Enable or disable Netplay in games." -msgstr "게임ì—서 Netplay를 활성화 ë˜ëŠ” 비활성화합니다." +msgstr "게임ì—서 넷플레ì´ë¥¼ 활성화 ë˜ëŠ” 비활성화합니다." -#: msgid "PORT" msgstr "í¬íЏ" -#: msgid "NICKNAME" msgstr "닉네임" -#: msgid "RELAY SERVER" -msgstr "ë¦´ë ˆì´ ì„œë²„" +msgstr "중계 서버" -#: msgid "Enable or disable connections throught relay servers." -msgstr "ë¦´ë ˆì´ ì„œë²„ë¥¼ 통한 ì—°ê²°ì„ í™œì„±í™” ë˜ëŠ” 비활성화합니다." +msgstr "중계 서버를 통한 ì—°ê²°ì„ í™œì„±í™” ë˜ëŠ” 비활성화합니다." -#: msgid "KODI/NETPLAY" -msgstr "KODI / NETPLAY" +msgstr "KODI/넷플레ì´" -#: msgid "NO GAMES OR NO CONNECTION" msgstr "게임ì´ë‚˜ ì—°ê²°ì´ ì—†ìŠµë‹ˆë‹¤" -#: msgid "HASH NOW" -msgstr "HASH NOW" +msgstr "해시 확ì¸" -#: msgid "HASH THESE SYSTEMS" -msgstr "HASH THESE SYSTEMS" +msgstr "ë‹¤ìŒ ì‹œìŠ¤í…œ 해시" -#: msgid "Add hash of roms in your gamelists to have more accurate results in Netplay." -msgstr "Netplayì—서보다 정확한 결과를 얻으려면 게임 목ë¡ì— 롬 해시를 추가하십시오." +msgstr "넷플레ì´ì—서 보다 정확한 결과를 얻으려면 게임 목ë¡ì— 롬 해시를 추가하십시오." -#: msgid "HASH ROMS" -msgstr "HASH ROMS" +msgstr "롬 해시" -#: msgid "Only missing hashs" -msgstr "Only missing hashs" +msgstr "없는 해시만" -#: msgid "Username" -msgstr "ì´ë¦„" +msgstr "사용ìžëª…" -#: msgid "Country" msgstr "êµ­ê°€" -#: msgid "Latency" msgstr "지연 시간" -#: msgid "Host arch." -msgstr "호스트 아치." +msgstr "호스트" -#: msgid "Core ver." -msgstr "핵심 ver." +msgstr "코어 버전" -#: msgid "RA ver." -msgstr "RA ver." +msgstr "ë ˆíŠ¸ë¡œì•„í¬ ë²„ì „" -#: msgid "Can join" -msgstr "가입 가능" +msgstr "ì¡°ì¸ ê°€ëŠ¥" -#: msgid "Rom and core match" -msgstr "롬과 코어 매치" +msgstr "롬 ë° ì½”ì–´ 매치" -#: msgid "Rom found" msgstr "롬 발견" -#: msgid "No rom match" msgstr "롬 ì¼ì¹˜ ì—†ìŒ" -#: msgid "Match" -msgstr "시합" +msgstr "ì¼ì¹˜" -#: msgid "No Match" -msgstr "ì¼ì¹˜í•˜ì§€ 않습니다" +msgstr "ì¼ì¹˜ 안함" -#: msgid "Rom file" msgstr "롬 파ì¼" -#: msgid "Rom hash" msgstr "롬 해시" -#: msgid "THIS COULD TAKE A WHILE, CONFIRM?" -msgstr "ì´ ë¬¸ì œë¥¼ 해결하는 ë™ì•ˆ í™•ì¸ í•˜ì‹œê² ìŠµë‹ˆê¹Œ?" +msgstr "ì‹œê°„ì´ ê±¸ë¦´ 수 있습니다, ì§„í–‰ 합니까?" -#: msgid "good" -msgstr "좋ì€" +msgstr "좋ìŒ" -#: msgid "bad" -msgstr "나ìœ" +msgstr "나ì¨" -#: msgid "medium" -msgstr "매질" +msgstr "중간" -#: msgid "NETPLAY POPUP DURATION" -msgstr "ë„· í”Œë ˆì´ íŒì—… ì§€ì† ì‹œê°„" +msgstr "ë„·í”Œë ˆì´ íŒì—… ì§€ì† ì‹œê°„" -#: msgid "Set duration of netplay popups, 0 means no popup." -msgstr "ë„· í”Œë ˆì´ íŒì—… ì§€ì† ì‹œê°„ì„ ì„¤ì •í•©ë‹ˆë‹¤. 0ì€ íŒì—…ì´ ì—†ìŒì„ ì˜ë¯¸í•©ë‹ˆë‹¤." +msgstr "ë„·í”Œë ˆì´ íŒì—… ì§€ì† ì‹œê°„ì„ ì„¤ì •í•©ë‹ˆë‹¤. 0ì€ íŒì—… ì—†ìŒ." -#: msgid "Player" msgstr "플레ì´ì–´" -#: msgid "Game" -msgstr "Game" +msgstr "게임" -#: msgid "Play online on games running through Retroarch like NES, SNES, FBA, Genesis/Megadrive and more!" -msgstr "NES, SNES, FBA, Genesis / Megadrive 등과 ê°™ì€ Retroarch를 통해 실행ë˜ëŠ” 게임ì—서 온ë¼ì¸ìœ¼ë¡œ 플레ì´í•˜ì‹­ì‹œì˜¤!" +msgstr "NES, SNES, FBA, 메가드ë¼ì´ë¸Œë“± Retroarch를 통해 실행ë˜ëŠ” ê²Œìž„ì„ ì˜¨ë¼ì¸ìœ¼ë¡œ 플레ì´í•˜ì‹­ì‹œì˜¤!" -#: msgid "Rom, hash and core match" -msgstr "롬, 해시 ë° ì½”ì–´ 매치" +msgstr "롬, 해시 ë° ì½”ì–´ ì¼ì¹˜" -#: msgid "No core match" -msgstr "No core match" +msgstr "코어 ì¼ì§€ 안함" -#: msgid "Add a clock in the main menu." msgstr "ë©”ì¸ ë©”ë‰´ì— ì‹œê³„ë¥¼ 추가" -#: msgid "UPGRADING" -msgstr "업그레ì´ë“œ" +msgstr "업그레ì´ë“œ 중" -#: msgid "PREPARING" -msgstr "준비" +msgstr "준비 중" -#: msgid "Starting UI" -msgstr "UI 시작" +msgstr "UI 시작 중" -#: msgid "VERIFYING" -msgstr "확ì¸" +msgstr "í™•ì¸ ì¤‘" -#: msgid "EMPTY LIST" -msgstr "빈 목ë¡" +msgstr "ëª©ë¡ ë¹„ìš°ê¸°" -#: msgid "COMPLETE QUIT MENU" -msgstr "완전한 종료 메뉴" +msgstr "ì „ì²´ 종료 메뉴" -#: Retroarch ratio msgid "Auto" -msgstr "오토매틱" +msgstr "ìžë™" msgid "Square pixel" msgstr "정사ê°í˜• 픽셀" msgid "Retroarch Config" -msgstr "레트로 ì•„í¬ êµ¬ì„±" +msgstr "ë ˆíŠ¸ë¡œì•„í¬ êµ¬ì„±" msgid "Retroarch Custom" -msgstr "Retroarch Custom" +msgstr "ë ˆíŠ¸ë¡œì•„í¬ ì‚¬ìš©ìžì„¤ì •" msgid "Core provided" -msgstr "핵심 제공" +msgstr "코어 제공" msgid "Do not set" -msgstr "설정하지 마십시오" \ No newline at end of file +msgstr "설정하지 마십시오" + +msgid "FAVORITES" +msgstr "ì¦ê²¨ì°¾ê¸°" + +msgid "menu" +msgstr "메뉴" + +msgid "SHOW CLOCK" +msgstr "시계 표시" + +msgid "MODIFIER" +msgstr "변경" + +msgid "SELECTIONNER" +msgstr "ì„ íƒ" + +msgid "NONE" +msgstr "ì—†ìŒ" + +msgid "PRESS ESC TO CANCEL." +msgstr "취소하려면 ESC키를 누르세요." + +msgctxt "button" +msgid "D-PAD UP" +msgstr "D-패드 위" + +msgctxt "button" +msgid "D-PAD DOWN" +msgstr "D-패드 아래" + +msgctxt "button" +msgid "D-PAD LEFT" +msgstr "D-패드 좌" + +msgctxt "button" +msgid "D-PAD RIGHT" +msgstr "D-패드 ìš°" + +msgctxt "button" +msgid "START" +msgstr "스타트" + +msgctxt "button" +msgid "SELECT" +msgstr "셀렉트" + +msgctxt "button" +msgid "BUTTON A / EAST" +msgstr "A버튼" + +msgctxt "button" +msgid "BUTTON B / SOUTH" +msgstr "B 버튼" + +msgctxt "button" +msgid "BUTTON X / NORTH" +msgstr "X 버튼" + +msgctxt "button" +msgid "BUTTON Y / WEST" +msgstr "Y 버튼" + +msgctxt "button" +msgid "LEFT SHOULDER" +msgstr "왼쪽 숄ë”" + +msgctxt "button" +msgid "RIGHT SHOULDER" +msgstr "오른쪽 숄ë”" + +msgctxt "button" +msgid "LEFT TRIGGER" +msgstr "왼쪽 트리거" + +msgctxt "button" +msgid "RIGHT TRIGGER" +msgstr "오른쪽 트리거" + +msgctxt "button" +msgid "LEFT THUMB" +msgstr "왼쪽 엄지버튼" + +msgctxt "button" +msgid "RIGHT THUMB" +msgstr "오른쪽 엄지버튼" + +msgctxt "button" +msgid "LEFT ANALOG UP" +msgstr "왼쪽 아날로그 위" + +msgctxt "button" +msgid "LEFT ANALOG DOWN" +msgstr "왼쪽 아날로그 아래" + +msgctxt "button" +msgid "LEFT ANALOG LEFT" +msgstr "왼쪽 아날로그 좌" + +msgctxt "button" +msgid "LEFT ANALOG RIGHT" +msgstr "왼쪽 아날로그 ìš°" + +msgctxt "button" +msgid "RIGHT ANALOG UP" +msgstr "오른쪽 아날로그 위" + +msgctxt "button" +msgid "RIGHT ANALOG DOWN" +msgstr "오른쪽 아날로그 아래" + +msgctxt "button" +msgid "RIGHT ANALOG LEFT" +msgstr "오른쪽 아날로그 좌" + +msgctxt "button" +msgid "RIGHT ANALOG RIGHT" +msgstr "오른쪽 아날로그 ìš°" + +msgctxt "button" +msgid "HOTKEY ENABLE" +msgstr "핫키" + +msgid "CONFIxGURING" +msgstr "설정중" + +msgid "KEYBOARD" +msgstr "키보드" + +msgid "GAMEPAD %i" +msgstr "게임패드 %i" + +msgid "HOLD ANY BUTTON TO SKIP" +msgstr "건너뛰려면 ë²„íŠ¼ì„ ê¸¸ê²Œ 누르세요" + +msgid "-NOT DEFINED-" +msgstr "-ì •ì˜ ì•ˆë¨-" + +msgid "" +"YOU DIDN'T CHOOSE A HOTKEY ENABLE BUTTON. THIS IS REQUIRED FOR EXITING GAMES " +"WITH A CONTROLLER. DO YOU WANT TO USE THE SELECT BUTTON DEFAULT ? PLEASE " +"ANSWER YES TO USE SELECT OR NO TO NOT SET A HOTKEY ENABLE BUTTON." +msgstr "" +"핫키 ì„¤ì •ì„ ì„ íƒí•˜ì§€ 않았습니다. 컨트롤러를 ì´ìš©í•´ 게임 종료를 하려면 반드시 필요한 설정입니다. " +"셀렉트 ë²„íŠ¼ì„ ê¸°ë³¸ 핫키로 사용하시겠습니까?" +"예를 ì„ íƒí•˜ë©´ 셀렉트 버튼으로 설정ë˜ê³  아니오를 ì„ íƒí•˜ë©´ 핫키를 설정하지 않습니다." + +msgid "HOLD FOR %iS TO SKIP" +msgid_plural "HOLD FOR %iS TO SKIP" +msgstr[0] "건너 뛰려면 %i ì´ˆ ë™ì•ˆ 누르세요" +msgstr[1] "건너 뛰려면 %i ì´ˆ ë™ì•ˆ 누르세요" + +msgid "PRESS ANYTHING" +msgstr "아무 버튼ì´ë‚˜ 누르세요" + +msgid "ALREADY TAKEN" +msgstr "ì´ë¯¸ 사용중입니다" + +msgid "THEME" +msgstr "테마" + +msgid "ALL GAMES" +msgstr "ì „ì²´ 게임" + +msgid "MARQUEE" +msgstr "마키" + +msgid "VIDEO" +msgstr "ë™ì˜ìƒ" + +msgid "KIDGAME" +msgstr "ì•„ë™ìš©" + +msgid "FAVORITE" +msgstr "ì¦ê²¨ì°¾ê¸°" + +msgid "PLAYERS" +msgstr "플레ì´ì–´" From 6e894436b9fbba7a9ade87b3502f78659d69b2db Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 29 Oct 2019 09:38:45 +0100 Subject: [PATCH 187/336] Fix : Folder metadatas are not used for folders - we have game metadatas instead. # Conflicts: # es-app/src/MetaData.cpp --- es-app/src/Gamelist.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 89b6d2172d..98cbe51828 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -150,7 +150,7 @@ void parseGamelist(SystemData* system, std::unordered_mapisArcadeAsset()) { std::string defaultName = file->metadata.get("name"); - file->metadata = MetaDataList::createFromXML(GAME_METADATA, fileNode, system); + file->metadata = MetaDataList::createFromXML(type == FOLDER ? FOLDER_METADATA : GAME_METADATA, fileNode, system); //make sure name gets set if one didn't exist if (file->metadata.get("name").empty()) From d000567e0b9ad70492de45025a0da2725333aab3 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 29 Oct 2019 09:39:57 +0100 Subject: [PATCH 188/336] Scraper : Better management for 404 & 429 errors --- es-app/src/scrapers/Scraper.cpp | 30 ++++++++++++++++++------------ es-core/src/HttpReq.cpp | 8 +++++++- es-core/src/HttpReq.h | 3 ++- 3 files changed, 27 insertions(+), 14 deletions(-) diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index e32106cab5..46f5d2cf38 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -123,28 +123,34 @@ void ScraperHttpRequest::update() if(status == HttpReq::REQ_SUCCESS) { setStatus(ASYNC_DONE); // if process() has an error, status will be changed to ASYNC_ERROR + process(mReq, mResults); + return; + } - if (!process(mReq, mResults)) - { - mRetryCount++; - if (mRetryCount > 4) - return; + if (status == HttpReq::REQ_429_TOOMANYREQUESTS) + { + mRetryCount++; + if (mRetryCount > 4) + return; - // If the scrapper fails & return false, then retry the request - setStatus(ASYNC_IN_PROGRESS); + setStatus(ASYNC_IN_PROGRESS); - std::string url = mReq->getUrl(); - std::this_thread::sleep_for(std::chrono::seconds(5)); - mReq = std::unique_ptr(new HttpReq(url)); - } + std::string url = mReq->getUrl(); + std::this_thread::sleep_for(std::chrono::seconds(15)); + mReq = std::unique_ptr(new HttpReq(url)); + return; + } + if (status == HttpReq::REQ_404_NOTFOUND) + { + setStatus(ASYNC_DONE); 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()); diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index 694c61aa0f..b637ed0a0a 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -302,7 +302,13 @@ HttpReq::Status HttpReq::status() if (http_status_code < 200 || http_status_code > 299) { - req->mStatus = REQ_IO_ERROR; + if(http_status_code == 404) + req->mStatus = REQ_404_NOTFOUND; + else if (http_status_code == 429) + req->mStatus = REQ_429_TOOMANYREQUESTS; + else + req->mStatus = REQ_IO_ERROR; + std::string err = "HTTP status " + std::to_string(http_status_code); req->onError(err.c_str()); } diff --git a/es-core/src/HttpReq.h b/es-core/src/HttpReq.h index 0230c7a463..8621c4d725 100644 --- a/es-core/src/HttpReq.h +++ b/es-core/src/HttpReq.h @@ -37,7 +37,8 @@ class HttpReq REQ_SUCCESS, //request completed successfully, get it with getContent() REQ_IO_ERROR, //some error happened, get it with getErrorMsg() - REQ_BAD_STATUS_CODE, //some invalid HTTP response status code happened (non-200) + REQ_404_NOTFOUND, //HTTP response status code 404 happened + REQ_429_TOOMANYREQUESTS, //HTTP response status code 429 happened REQ_INVALID_RESPONSE //the HTTP response was invalid }; From 739612468ff938e4dd96efef3e0043f0392413c9 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 29 Oct 2019 09:44:01 +0100 Subject: [PATCH 189/336] Fix : VERTICAL_WHEEL carousel logo position on some themes using rotation by logo size + ensure loading order for themes <= 4 Fix : Rating Component drawing on some themes. Fix : ImageComponent clipping with rotation # Conflicts: # es-app/src/views/SystemView.cpp # es-core/src/components/ImageComponent.cpp --- es-app/src/components/RatingComponent.cpp | 10 ++--- es-app/src/views/SystemView.cpp | 6 ++- es-core/src/ThemeData.cpp | 45 ++++++++++++++++++----- es-core/src/components/ImageComponent.cpp | 4 +- 4 files changed, 46 insertions(+), 19 deletions(-) diff --git a/es-app/src/components/RatingComponent.cpp b/es-app/src/components/RatingComponent.cpp index d7048cb334..7db6f4e9f4 100644 --- a/es-app/src/components/RatingComponent.cpp +++ b/es-app/src/components/RatingComponent.cpp @@ -118,18 +118,18 @@ void RatingComponent::render(const Transform4x4f& parentTrans) Renderer::setMatrix(trans); - if (mFilledTexture->bind()) + if (mUnfilledTexture->bind()) { - Renderer::drawTriangleStrips(&mVertices[0], 4); + Renderer::drawTriangleStrips(&mVertices[4], 4); Renderer::bindTexture(0); } - if (mUnfilledTexture->bind()) + if (mFilledTexture->bind()) { - Renderer::drawTriangleStrips(&mVertices[4], 4); + Renderer::drawTriangleStrips(&mVertices[0], 4); Renderer::bindTexture(0); } - + renderChildren(trans); } diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index ea2eac9bcd..eeec65de48 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -98,8 +98,10 @@ void SystemView::populate() if (Utils::FileSystem::exists(path)) logo->setImage(path, (elem->has("tile") && elem->get("tile")), MaxSizeInfo(carouselLogoSize() * mCarousel.logoScale)); } - - logo->setRotateByTargetSize(true); + + if (mCarousel.size.x() != carouselLogoSize().x() & mCarousel.size.y() != carouselLogoSize().y()) + logo->setRotateByTargetSize(true); + e.data.logo = std::shared_ptr(logo); e.data.logoIsImage = true; } diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 023bf2ec31..3812eeb912 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -656,19 +656,34 @@ void ThemeData::parseTheme(const pugi::xml_node& root) if (root.attribute("defaultView")) mDefaultView = root.attribute("defaultView").as_string(); - for (pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + if (mVersion <= 4) { - if (!parseFilterAttributes(node)) - continue; - - std::string name = node.name(); - - if (name == "include") + // Unfortunately, recalbox does not do things in order, features have to be loaded after + for (pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) parseInclude(node); - else if (name == "view") + + for (pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) parseViewElement(node); - else if (name == "customView") - parseCustomView(node, root); + + for (pugi::xml_node node = root.child("customView"); node; node = node.next_sibling("customView")) + parseCustomView(node, root); + } + else + { + for (pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) + { + if (!parseFilterAttributes(node)) + continue; + + std::string name = node.name(); + + if (name == "include") + parseInclude(node); + else if (name == "view") + parseViewElement(node); + else if (name == "customView") + parseCustomView(node, root); + } } // Unfortunately, recalbox does not do things in order, features have to be loaded after @@ -947,6 +962,16 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapfileExists(path)) { std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index e8df835066..f679103921 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -365,8 +365,8 @@ void ImageComponent::render(const Transform4x4f& parentTrans) Transform4x4f trans = parentTrans * getTransform(); - Vector2f clipPos(trans.translation().x(), trans.translation().y()); - if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + // Don't use soft clip if rotation applied : let renderer do the work + if (mRotation == 0 && !Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) return; Renderer::setMatrix(trans); From b800b748c206ce8236eb24b34b2c8c77fe255cd0 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 29 Oct 2019 09:44:29 +0100 Subject: [PATCH 190/336] Scraper : Crash when resizing images if source size = dest size... --- es-app/src/scrapers/Scraper.cpp | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index 46f5d2cf38..3c1781136a 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -446,7 +446,13 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight) { maxHeight = (int)((maxWidth / width) * height); } - + + if (width <= maxWidth && height <= maxHeight) + { + FreeImage_Unload(image); + return true; + } + FIBITMAP* imageRescaled = FreeImage_Rescale(image, maxWidth, maxHeight, FILTER_BILINEAR); FreeImage_Unload(image); @@ -456,7 +462,14 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight) return false; } - bool saved = (FreeImage_Save(format, imageRescaled, path.c_str()) != 0); + bool saved = false; + + try + { + saved = (FreeImage_Save(format, imageRescaled, path.c_str()) != 0); + } + catch(...) { } + FreeImage_Unload(imageRescaled); if(!saved) From 7ebf97cfbfae1c0a896db9e913ddcabea11163cc Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 29 Oct 2019 09:48:36 +0100 Subject: [PATCH 191/336] Screenshot system : load images directly as "image" & exclude imageViewer from standard gamelist management. # Conflicts: # es-app/src/FileData.cpp # es-app/src/Gamelist.cpp # es-app/src/SystemData.cpp # es-app/src/views/ViewController.cpp --- es-app/src/Gamelist.cpp | 3 +++ es-app/src/SystemData.cpp | 2 +- es-app/src/views/ViewController.cpp | 8 ++++++++ 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 98cbe51828..0d91898905 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -198,6 +198,9 @@ void updateGamelist(SystemData* system) if(Settings::getInstance()->getBool("IgnoreGamelist")) return; + if (system != nullptr && system->getName() == "imageviewer") + return; + int numUpdated = 0; pugi::xml_document doc; diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index b28f44dbe2..dc7d57a4c7 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -46,7 +46,7 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys return; } - if (!Settings::getInstance()->getBool("IgnoreGamelist")) + if (!Settings::getInstance()->getBool("IgnoreGamelist") && mName != "imageviewer") parseGamelist(this, fileMap); if (mSortId >= 0 && mSortId < FileSorts::SortTypes.size()) diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index b70956edd1..b525484f92 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -472,8 +472,16 @@ bool ViewController::input(InputConfig* config, Input input) if (config->getDeviceId() == DEVICE_KEYBOARD && input.value && input.id == SDLK_F5) { mWindow->render(); + + ResourceManager::getInstance()->unloadAll(); + ResourceManager::getInstance()->reloadAll(); + mWindow->renderLoadingScreen(_("Loading..."), -1, 180); +#if WIN32 + EsLocale::reset(); +#endif + ViewController::get()->reloadAll(); mWindow->endRenderLoadingScreen(); return true; From ff511bb7e1aec02695ff8cc27d66c91f934e12db Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 29 Oct 2019 09:49:31 +0100 Subject: [PATCH 192/336] OSK : Add x pad button to "reset" & validate ( useful for text filters ) --- es-core/src/guis/GuiTextEditPopupKeyboard.cpp | 17 +++++++++++++++-- es-core/src/guis/GuiTextEditPopupKeyboard.h | 3 +++ 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp index b05332f569..76ed1f5400 100644 --- a/es-core/src/guis/GuiTextEditPopupKeyboard.cpp +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.cpp @@ -43,6 +43,8 @@ GuiTextEditPopupKeyboard::GuiTextEditPopupKeyboard(Window* window, const std::st const std::function& okCallback, bool multiLine, const std::string acceptBtnText) : GuiComponent(window), mBackground(window, ":/frame.png"), mGrid(window, Vector2i(1, 7)), mMultiLine(multiLine) { + mOkCallback = okCallback; + auto theme = ThemeData::getMenuTheme(); mBackground.setImagePath(theme->Background.path); mBackground.setEdgeColor(theme->Background.color); @@ -222,14 +224,14 @@ bool GuiTextEditPopupKeyboard::input(InputConfig* config, Input input) } // For deleting a chara (Left Top Button) - if (config->isMappedTo("PageUp", input) && input.value) { + if (config->isMappedTo("pageup", input) && input.value) { mText->startEditing(); mText->textInput("\b"); mText->stopEditing(); } // For Adding a space (Right Top Button) - if (config->isMappedTo("PageDown", input) && input.value) { + if (config->isMappedTo("pagedown", input) && input.value) { mText->startEditing(); mText->textInput(" "); } @@ -243,6 +245,13 @@ bool GuiTextEditPopupKeyboard::input(InputConfig* config, Input input) + if (config->isMappedTo("x", input) && input.value && mOkCallback != nullptr) + { + mOkCallback(""); + delete this; + return true; + } + return false; } /* @@ -271,6 +280,10 @@ void GuiTextEditPopupKeyboard::shiftKeys() std::vector GuiTextEditPopupKeyboard::getHelpPrompts() { std::vector prompts = mGrid.getHelpPrompts(); + + if (mOkCallback != nullptr) + prompts.push_back(HelpPrompt("x", _("RESET"))); + prompts.push_back(HelpPrompt("y", _("SHIFT"))); prompts.push_back(HelpPrompt("b", _("BACK"))); prompts.push_back(HelpPrompt("r", _("SPACE"))); diff --git a/es-core/src/guis/GuiTextEditPopupKeyboard.h b/es-core/src/guis/GuiTextEditPopupKeyboard.h index 73f86cea57..98f8d90fad 100644 --- a/es-core/src/guis/GuiTextEditPopupKeyboard.h +++ b/es-core/src/guis/GuiTextEditPopupKeyboard.h @@ -5,6 +5,7 @@ #include "components/ComponentGrid.h" #include "components/TextEditComponent.h" #include "components/TextComponent.h" +#include class GuiTextEditPopupKeyboard : public GuiComponent { @@ -42,6 +43,8 @@ class GuiTextEditPopupKeyboard : public GuiComponent std::shared_ptr mKeyboardGrid; std::shared_ptr mButtons; + std::function mOkCallback; + bool mMultiLine; bool mShift = false; }; From 90d9a79fc5383048c27ac28c8fc9d0e3ac1ee4fa Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 29 Oct 2019 09:50:54 +0100 Subject: [PATCH 193/336] VideoScreenSaver : decoration not visible if ScreenSaverGameInfo is set to never --- es-app/src/SystemScreenSaver.cpp | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/es-app/src/SystemScreenSaver.cpp b/es-app/src/SystemScreenSaver.cpp index 075ed64ae3..03f0f87d4b 100644 --- a/es-app/src/SystemScreenSaver.cpp +++ b/es-app/src/SystemScreenSaver.cpp @@ -798,24 +798,24 @@ void VideoScreenSaver::render(const Transform4x4f& transform) return; #endif - if (Settings::getInstance()->getString("ScreenSaverGameInfo") == "never") - return; - - if (mMarquee && mFade != 0) - { - mMarquee->setOpacity(mOpacity * mFade); - mMarquee->render(transform); - } - else if (mLabelGame && mFade != 0) + if (Settings::getInstance()->getString("ScreenSaverGameInfo") != "never") { - mLabelGame->setOpacity(mOpacity * mFade); - mLabelGame->render(transform); - } + if (mMarquee && mFade != 0) + { + mMarquee->setOpacity(mOpacity * mFade); + mMarquee->render(transform); + } + else if (mLabelGame && mFade != 0) + { + mLabelGame->setOpacity(mOpacity * mFade); + mLabelGame->render(transform); + } - if (mLabelSystem && mFade != 0) - { - mLabelSystem->setOpacity(mOpacity * mFade); - mLabelSystem->render(transform); + if (mLabelSystem && mFade != 0) + { + mLabelSystem->setOpacity(mOpacity * mFade); + mLabelSystem->render(transform); + } } if (mDecoration) From d3e01687b83c35d495ec6da667f291896f048eca Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 29 Oct 2019 09:52:46 +0100 Subject: [PATCH 194/336] VideoComponent : Sometimes, video starts if the view has just been hidden (at end delay), and plays in background --- es-core/src/components/VideoComponent.cpp | 4 ++++ es-core/src/components/VideoVlcComponent.cpp | 1 + 2 files changed, 5 insertions(+) diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index 716916b961..db67774d56 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -409,6 +409,8 @@ void VideoComponent::manageState() // We will only show if the component is on display and the screensaver // is not active bool show = mShowing && !mScreensaverActive && !mDisable; + if (!show) + mStartDelayed = false; // See if we're already playing if (mIsPlaying || mIsWaitingForVideoToStart) @@ -417,6 +419,7 @@ void VideoComponent::manageState() if (!show) { mIsWaitingForVideoToStart = false; + mStartDelayed = false; stopVideo(); } else @@ -425,6 +428,7 @@ void VideoComponent::manageState() { // Path changed. Stop the video. We will start it again below because // mIsPlaying will be modified by stopVideo to be false + mStartDelayed = false; mIsWaitingForVideoToStart = false; stopVideo(); } diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 1781db6846..f90de4b40f 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -490,6 +490,7 @@ void VideoVlcComponent::startVideo() void VideoVlcComponent::stopVideo() { mIsPlaying = false; + mIsWaitingForVideoToStart = false; mStartDelayed = false; // Release the media player so it stops calling back to us From b34cab0e4f5cefe1fb4d7c0146fda2bb71db23bd Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 29 Oct 2019 09:53:21 +0100 Subject: [PATCH 195/336] Fix : Async download notification width & progress bar color --- es-core/src/components/AsyncNotificationComponent.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/es-core/src/components/AsyncNotificationComponent.cpp b/es-core/src/components/AsyncNotificationComponent.cpp index 22be9c6aa6..38a2a4aa03 100644 --- a/es-core/src/components/AsyncNotificationComponent.cpp +++ b/es-core/src/components/AsyncNotificationComponent.cpp @@ -13,10 +13,11 @@ AsyncNotificationComponent::AsyncNotificationComponent(Window* window, bool acti { mPercent = -1; - float width = Renderer::getScreenWidth() * 0.14f; - auto theme = ThemeData::getMenuTheme(); + // Note : Don't localize this text -> It is only used to guess width calculation for the component. + float width = theme->TextSmall.font->sizeText("TEXT FOR SIZE CALCULATION").x(); // Renderer::getScreenWidth() * 0.14f; + mTitle = std::make_shared(mWindow, "", theme->TextSmall.font, theme->TextSmall.color, ALIGN_LEFT); mGameName = std::make_shared(mWindow, "", theme->TextSmall.font, theme->Text.color, ALIGN_LEFT); @@ -123,7 +124,7 @@ void AsyncNotificationComponent::render(const Transform4x4f& parentTrans) percent = 100; auto theme = ThemeData::getMenuTheme(); - auto color = (theme->Text.selectedColor & 0xFFFFFF00) | 0x40; + auto color = theme->Text.color & 0xFFFFFF00 | (unsigned char)((theme->Text.color & 0xFF) * (mOpacity / 255.0)); Renderer::drawRect(x, y, (w*percent), h, color); } From 79b915bdd89a93c6c3c0655f7c0d013fcad6e630 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 29 Oct 2019 09:56:21 +0100 Subject: [PATCH 196/336] Loading speed : Don't try to load roms from images/videos subfolders # Conflicts: # es-app/src/SystemData.cpp --- es-app/src/SystemData.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index dc7d57a4c7..963f2f324f 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -173,7 +173,11 @@ void SystemData::populateFolder(FolderData* folder, std::unordered_map Date: Tue, 29 Oct 2019 09:57:03 +0100 Subject: [PATCH 197/336] Fix : Navigating through folders don't display first game metadatas & images # Conflicts: # es-app/src/views/gamelist/GridGameListView.cpp --- es-app/src/views/gamelist/DetailedGameListView.cpp | 6 +++++- es-app/src/views/gamelist/GridGameListView.cpp | 6 +++++- es-app/src/views/gamelist/VideoGameListView.cpp | 4 ++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index df8f7e5643..d2ba04163e 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -364,6 +364,10 @@ void DetailedGameListView::updateInfoPanel() fadingOut = false; } + + // We're clearing / populating : don't setup fade animations + if (file == nullptr && mList.getObjects().size() == 0 && mList.getCursorIndex() == 0 && mList.getScrollingVelocity() == 0) + return; std::vector comps = getMDValues(); @@ -397,7 +401,7 @@ void DetailedGameListView::updateInfoPanel() }; bool isFadeOut = fadingOut; - comp->setAnimation(new LambdaAnimation(func, 150), 0, [this, isFadeOut] + comp->setAnimation(new LambdaAnimation(func, 150), 0, [this, isFadeOut, file] { if (isFadeOut) { diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 468d823c1a..4d13c65af7 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -454,7 +454,11 @@ void GridGameListView::updateInfoPanel() fadingOut = false; } - + + // We're clearing / populating : don't setup fade animations + if (file == nullptr && mGrid.getObjects().size() == 0 && mGrid.getCursorIndex() == 0 && mGrid.getScrollingVelocity() == 0) + return; + std::vector comps = getMDValues(); if (mVideo != nullptr) diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index 2416db0b95..4e23c8428d 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -314,6 +314,10 @@ void VideoGameListView::updateInfoPanel() fadingOut = false; } + // We're clearing / populating : don't setup fade animations + if (file == nullptr && mList.getObjects().size() == 0 && mList.getCursorIndex() == 0 && mList.getScrollingVelocity() == 0) + return; + std::vector comps = getMDValues(); comps.push_back(&mMarquee); comps.push_back(mVideo); From 46226af58c89764e88dae1745ac5d101e42bf940 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 29 Oct 2019 09:58:22 +0100 Subject: [PATCH 198/336] SHOW FOLDERS option : Split in 3 modes : always, never and "having multiple games" (usefull for systems like scrummvm ) # Conflicts: # es-app/src/FileData.cpp # es-app/src/guis/GuiGamelistOptions.cpp # es-core/src/Settings.cpp --- es-app/src/FileData.h | 2 ++ es-app/src/guis/GuiGamelistOptions.cpp | 26 ++++++++++++++++---------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 0a1f3100be..85850a7430 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -149,6 +149,8 @@ class FolderData : public FileData void createChildrenByFilenameMap(std::unordered_map& map); + FileData* findUniqueGameForFolder(); + private: std::vector getFlatGameList(bool displayedOnly, SystemData* system) const; diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 254327f1d0..841f5ee87d 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -99,10 +99,10 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool char curChar = (char)toupper(getGamelist()->getCursor()->getName()[0]); - if (std::find(letters.begin(), letters.end(), std::string(1, curChar)) == letters.end()) - curChar = letters.at(0)[0]; + if (std::find(letters.begin(), letters.end(), std::string(1, curChar)) == letters.end()) + curChar = letters.at(0)[0]; - for (auto letter : letters) + for (auto letter : letters) mJumpToLetterList->add(letter, letter[0], letter[0] == curChar); row.addElement(std::make_shared(mWindow, _("JUMP TO..."), theme->Text.font, theme->Text.color), true); // batocera @@ -221,17 +221,23 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool mReloadAll = true; }); - // Flat folders - auto flatFolders = std::make_shared(mWindow); - flatFolders->setState(!Settings::getInstance()->getBool("FlatFolders")); - mMenu.addWithLabel(_("SHOW FOLDERS"), flatFolders); // batocera - addSaveFunc([flatFolders, this] + // Folder View Mode + auto foldersBehavior = std::make_shared< OptionListComponent >(mWindow, _("SHOW FOLDERS"), false); + std::vector folders; + folders.push_back("always"); + folders.push_back("never"); + folders.push_back("having multiple games"); + + for (auto it = folders.cbegin(); it != folders.cend(); it++) + foldersBehavior->add(_(it->c_str()), *it, Settings::getInstance()->getString("FolderViewMode") == *it); + + mMenu.addWithLabel(_("SHOW FOLDERS"), foldersBehavior); + addSaveFunc([this, foldersBehavior] { - if (Settings::getInstance()->setBool("FlatFolders", !flatFolders->getState())) + if (Settings::getInstance()->setString("FolderViewMode", foldersBehavior->getSelected())) mReloadAll = true; }); - std::map customCollections = CollectionSystemManager::get()->getCustomCollectionSystems(); if(UIModeController::getInstance()->isUIModeFull() && From 6ba6862d3a8024cc9d21ea3980c8a524be605445 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 29 Oct 2019 09:59:54 +0100 Subject: [PATCH 199/336] Localisation of customviews ( using variables ) # Conflicts: # es-app/src/guis/GuiGamelistOptions.cpp # es-app/src/guis/GuiMenu.cpp --- es-app/src/guis/GuiGamelistOptions.cpp | 10 ++++---- es-app/src/guis/GuiMenu.cpp | 14 +++++------ es-core/src/ThemeData.cpp | 32 +++++++++++++------------- es-core/src/ThemeData.h | 5 +++- 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 841f5ee87d..57e77bfaf8 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -141,8 +141,8 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool // GameList view style mViewMode = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); - std::vector styles; - styles.push_back("automatic"); + std::vector> styles; + styles.push_back(std::pair("automatic", _("automatic"))); auto mViews = system->getTheme()->getViewsOfTheme(); for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) @@ -152,12 +152,12 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool bool found = false; for (auto it = styles.cbegin(); it != styles.cend(); it++) - { - bool sel = (viewMode.empty() && *it == "automatic") || viewMode == *it; + { + bool sel = (viewMode.empty() && it->first == "automatic") || viewMode == it->first; if (sel) found = true; - mViewMode->add(_(*it), *it, sel); + mViewMode->add(it->second, it->first, sel); } if (!found) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 5278fb031f..22b6017a97 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -568,8 +568,8 @@ void GuiMenu::openUISettings() else // GameList view style only, acts like Retropie for simple themes { auto gamelist_style = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); - std::vector styles; - styles.push_back("automatic"); + std::vector> styles; + styles.push_back(std::pair("automatic", _("automatic"))); auto system = ViewController::get()->getState().getSystem(); if (system != NULL) @@ -580,10 +580,10 @@ void GuiMenu::openUISettings() } else { - styles.push_back("basic"); - styles.push_back("detailed"); - styles.push_back("video"); - styles.push_back("grid"); + styles.push_back(std::pair("basic", _("basic"))); + styles.push_back(std::pair("detailed", _("detailed"))); + styles.push_back(std::pair("video", _("video"))); + styles.push_back(std::pair("grid", _("grid"))); } auto viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); @@ -591,7 +591,7 @@ void GuiMenu::openUISettings() viewPreference = "automatic"; for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(_(it->c_str()), *it, viewPreference == *it); + gamelist_style->add(it->second, it->first, viewPreference == it->first); s->addWithLabel(_("GAMELIST VIEW STYLE"), gamelist_style); s->addSaveFunc([s, gamelist_style, window] { diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 3812eeb912..cb42cc7115 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -416,18 +416,18 @@ std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolde bool ThemeData::isFirstSubset(const pugi::xml_node& node) { - const std::string subsetToFind = node.attribute("subset").as_string(); + const std::string subsetToFind = resolvePlaceholders(node.attribute("subset").as_string()); const std::string name = node.attribute("name").as_string(); pugi::xml_node root = node.parent(); for (pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) { - const std::string subsetAttr = node.attribute("subset").as_string(); + const std::string subsetAttr = resolvePlaceholders(node.attribute("subset").as_string()); if (subsetAttr.empty() || subsetAttr != subsetToFind) continue; - const std::string nameAttr = node.attribute("name").as_string(); + const std::string nameAttr = resolvePlaceholders(node.attribute("name").as_string()); return (nameAttr == name); } @@ -439,8 +439,8 @@ bool ThemeData::parseSubset(const pugi::xml_node& node) if (!node.attribute("subset")) return true; - const std::string subsetAttr = node.attribute("subset").as_string(); - const std::string nameAttr = node.attribute("name").as_string(); + const std::string subsetAttr = resolvePlaceholders(node.attribute("subset").as_string()); + const std::string nameAttr = resolvePlaceholders(node.attribute("name").as_string()); if (!subsetAttr.empty()) { @@ -572,15 +572,14 @@ void ThemeData::parseVariables(const pugi::xml_node& root) ThemeException error; error.setFiles(mPaths); - pugi::xml_node variables = root.child("variables"); - if(!variables) - return; - - if (!parseFilterAttributes(variables)) - return; + for (pugi::xml_node variables = root.child("variables"); variables; variables = variables.next_sibling("variables")) + { + if (!parseFilterAttributes(variables)) + return; - for(pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) - parseVariable(*it); + for (pugi::xml_node_iterator it = variables.begin(); it != variables.end(); ++it) + parseVariable(*it); + } } void ThemeData::parseViewElement(const pugi::xml_node& node) @@ -764,6 +763,7 @@ void ThemeData::parseCustomView(const pugi::xml_node& node, const pugi::xml_node std::string viewKey = node.attribute("name").as_string(); ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; + view.displayName = node.attribute("displayName") ? resolvePlaceholders(node.attribute("displayName").as_string()) : viewKey; view.isCustomView = true; std::string inherits = node.attribute("inherits").as_string(); @@ -1318,15 +1318,15 @@ void ThemeData::setDefaultTheme(ThemeData* theme) mMenuTheme = nullptr; }; -std::vector ThemeData::getViewsOfTheme() +std::vector> ThemeData::getViewsOfTheme() { - std::vector ret; + std::vector> ret; for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) { if (it->first == "menu" || it->first == "system" || it->first == "screen") continue; - ret.push_back(it->first); + ret.push_back(std::pair(it->first, it->second.displayName.empty() ? it->first : it->second.displayName)); } return ret; diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 94f7f1f679..7d390e8f47 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -210,6 +210,9 @@ class ThemeData std::map elements; std::vector orderedKeys; std::string baseType; + + std::string displayName; + bool isCustomView; }; @@ -259,7 +262,7 @@ class ThemeData std::string getSystemThemeFolder() { return mSystemThemeFolder; } - std::vector getViewsOfTheme(); + std::vector> getViewsOfTheme(); std::string getDefaultView() { return mDefaultView; }; private: From c23f4314d6c3dcd76d8c18fdc4848c8ec39d24b5 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 29 Oct 2019 10:01:39 +0100 Subject: [PATCH 200/336] Finalisations on Themes localisation support # Conflicts: # es-app/src/FileSorts.cpp # es-app/src/guis/GuiMenu.cpp # es-app/src/guis/GuiVideoScreensaverOptions.cpp # locale/lang/fr/LC_MESSAGES/emulationstation2.po --- es-app/src/guis/GuiGamelistOptions.cpp | 7 ++- .../src/guis/GuiVideoScreensaverOptions.cpp | 2 +- es-core/src/ThemeData.cpp | 19 ++++-- es-core/src/ThemeData.h | 59 +++++++++++++++++-- es-core/src/components/GridTileComponent.cpp | 39 ++++++------ 5 files changed, 97 insertions(+), 29 deletions(-) diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 57e77bfaf8..544023cee6 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -146,7 +146,12 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool auto mViews = system->getTheme()->getViewsOfTheme(); for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) - styles.push_back(*it); + { + if (it->first == "basic" || it->first == "detailed" || it->first == "grid") + styles.push_back(std::pair(it->first, _(it->first.c_str()))); + else + styles.push_back(*it); + } std::string viewMode = system->getSystemViewMode(); diff --git a/es-app/src/guis/GuiVideoScreensaverOptions.cpp b/es-app/src/guis/GuiVideoScreensaverOptions.cpp index 575265177e..734eb72692 100644 --- a/es-app/src/guis/GuiVideoScreensaverOptions.cpp +++ b/es-app/src/guis/GuiVideoScreensaverOptions.cpp @@ -44,7 +44,7 @@ GuiVideoScreensaverOptions::GuiVideoScreensaverOptions(Window* window, const cha info_type.push_back("never"); for(auto it = info_type.cbegin(); it != info_type.cend(); it++) ss_info->add(_(it->c_str()), *it, Settings::getInstance()->getString("ScreenSaverGameInfo") == *it); - addWithLabel(_("SHOW GAME INFO"), ss_info); + addWithLabel(_("SHOW GAME INFO ON SCREENSAVER"), ss_info); addSaveFunc([ss_info, this] { Settings::getInstance()->setString("ScreenSaverGameInfo", ss_info->getSelected()); }); diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index cb42cc7115..9cb8b23f17 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -444,6 +444,10 @@ bool ThemeData::parseSubset(const pugi::xml_node& node) if (!subsetAttr.empty()) { + std::string displayNameAttr = resolvePlaceholders(node.attribute("displayName").as_string()); + if (displayNameAttr.empty()) + displayNameAttr = nameAttr; + bool add = true; for (auto sb : mSubsets) { @@ -453,7 +457,7 @@ bool ThemeData::parseSubset(const pugi::xml_node& node) } if (add) - mSubsets.push_back(Subset(subsetAttr, nameAttr)); + mSubsets.push_back(Subset(subsetAttr, nameAttr, displayNameAttr)); } @@ -602,7 +606,7 @@ void ThemeData::parseViewElement(const pugi::xml_node& node) off = nameAttr.find_first_of(delim, prevOff); if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) != sSupportedViews.cend()) - { + { ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; parseView(node, view); @@ -764,6 +768,9 @@ void ThemeData::parseCustomView(const pugi::xml_node& node, const pugi::xml_node ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; view.displayName = node.attribute("displayName") ? resolvePlaceholders(node.attribute("displayName").as_string()) : viewKey; + if (view.displayName.empty()) + view.displayName = viewKey; + view.isCustomView = true; std::string inherits = node.attribute("inherits").as_string(); @@ -864,7 +871,7 @@ bool ThemeData::parseRegion(const pugi::xml_node& node) } if (add) - mSubsets.push_back(Subset("region", nameAttr)); + mSubsets.push_back(Subset("region", nameAttr, nameAttr)); const char* delim = " \t\r\n,"; @@ -1332,13 +1339,13 @@ std::vector> ThemeData::getViewsOfTheme() return ret; } -std::vector ThemeData::getSubSet(const std::vector& subsets, const std::string& subset) +std::vector ThemeData::getSubSet(const std::vector& subsets, const std::string& subset) { - std::vector ret; + std::vector ret; for (const auto& it : subsets) if (it.subset == subset) - ret.push_back(it.name); + ret.push_back(it); return ret; } diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 7d390e8f47..f2c7bb0b5b 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -89,14 +89,17 @@ struct ThemeSet struct Subset { - Subset(const std::string set, const std::string nm) + Subset(const std::string set, const std::string nm, const std::string dn) { subset = set; name = nm; + displayName = dn; } std::string subset; - std::string name; + std::string name; + + std::string displayName; }; struct MenuElement @@ -255,7 +258,7 @@ class ThemeData std::vector getSubSets() { return mSubsets; } std::vector getSubSetNames(); - static std::vector getSubSet(const std::vector& subsets, const std::string& subset); + static std::vector getSubSet(const std::vector& subsets, const std::string& subset); static void setDefaultTheme(ThemeData* theme); static ThemeData* getDefaultTheme() { return mDefaultTheme; } @@ -265,6 +268,14 @@ class ThemeData std::vector> getViewsOfTheme(); std::string getDefaultView() { return mDefaultView; }; + std::string getVariable(std::string name) + { + if (mVariables.find(name) != mVariables.cend()) + return mVariables[name]; + + return ""; + } + private: static std::map< std::string, std::map > sElementMap; static std::vector sSupportedFeatures; @@ -306,7 +317,47 @@ class ThemeData std::string mRegion; std::map mVariables; - std::map mViews; + + class UnsortedViewMap : public std::vector> + { + public: + std::vector>::const_iterator find(std::string view) const + { + for (std::vector>::const_iterator it = cbegin(); it != cend(); it++) + if (it->first == view) + return it; + + return cend(); + } + + std::vector>::iterator find(std::string view) + { + for (std::vector>::iterator it = begin(); it != end(); it++) + if (it->first == view) + return it; + + return end(); + } + + std::pair>::iterator, bool> insert(std::pair& item) + { + std::pair>::iterator, bool> ret; + + ret.first = find(item.first); + ret.second = ret.first != cend(); + + if (ret.first == cend()) + { + push_back(item); + ret.first = find(item.first); + } + + return ret; + } + }; + + UnsortedViewMap mViews; + // std::map mViews; std::vector mSubsets; diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index e332617951..e5593b8830 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -26,6 +26,7 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mVideo = nullptr; mMarquee = nullptr; + mLabelVisible = false; mLabelMerged = false; resetProperties(); @@ -508,7 +509,7 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons { float sh = (float)Renderer::getScreenHeight(); - if (elem && elem->has("pos")) + if (elem->has("pos")) { mDefaultProperties.mLabelPos = elem->get("pos"); mSelectedProperties.mLabelPos = mDefaultProperties.mLabelPos; @@ -517,7 +518,7 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons else mLabelMerged = false; - if (elem && elem->has("size")) + if (elem->has("size")) { mDefaultProperties.mLabelSize = elem->get("size"); mSelectedProperties.mLabelSize = mDefaultProperties.mLabelSize; @@ -526,70 +527,74 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mLabelMerged = mDefaultProperties.mLabelSize.x() == 0; } - if (elem && elem->has("color")) + if (elem->has("color")) { mDefaultProperties.mLabelColor = elem->get("color"); mSelectedProperties.mLabelColor = mDefaultProperties.mLabelColor; } - if (elem && elem->has("backgroundColor")) + if (elem->has("backgroundColor")) { mDefaultProperties.mLabelBackColor = elem->get("backgroundColor"); mSelectedProperties.mLabelBackColor = mDefaultProperties.mLabelBackColor; } - if (elem && elem->has("glowSize")) + if (elem->has("glowSize")) { mDefaultProperties.mLabelGlowSize = (unsigned int)elem->get("glowSize"); mSelectedProperties.mLabelGlowSize = mDefaultProperties.mLabelGlowSize; } - if (elem && elem->has("glowColor")) + if (elem->has("glowColor")) { mDefaultProperties.mLabelGlowColor = elem->get("glowColor"); mSelectedProperties.mLabelGlowColor = mDefaultProperties.mLabelGlowColor; } - if (elem && elem->has("fontSize")) + if (elem->has("fontSize")) { mDefaultProperties.mFontSize = elem->get("fontSize") * sh; mSelectedProperties.mFontSize = mDefaultProperties.mFontSize; } - if (elem && elem->has("fontPath")) + if (elem->has("fontPath")) { mDefaultProperties.mFontPath = elem->get("fontPath"); mSelectedProperties.mFontPath = mDefaultProperties.mFontPath; } + + if (elem->has("visible")) + mLabelVisible = elem->get("visible"); + else + mLabelVisible = true; - mLabelVisible = true; mLabel.applyTheme(theme, view, element, properties); elem = theme->getElement(view, "gridtile_selected", "text"); if (elem != NULL) { - if (elem && elem->has("pos") && mDefaultProperties.mLabelPos.x() >= 0) + if (elem->has("pos") && mDefaultProperties.mLabelPos.x() >= 0) mSelectedProperties.mLabelPos = elem->get("pos"); - if (elem && elem->has("size")) + if (elem->has("size")) mSelectedProperties.mLabelSize = elem->get("size"); - if (elem && elem->has("color")) + if (elem->has("color")) mSelectedProperties.mLabelColor = elem->get("color"); - if (elem && elem->has("backgroundColor")) + if (elem->has("backgroundColor")) mSelectedProperties.mLabelBackColor = elem->get("backgroundColor"); - if (elem && elem->has("glowSize")) + if (elem->has("glowSize")) mSelectedProperties.mLabelGlowSize = (unsigned int)elem->get("glowSize"); - if (elem && elem->has("glowColor")) + if (elem->has("glowColor")) mSelectedProperties.mLabelGlowColor = elem->get("glowColor"); - if (elem && elem->has("fontSize")) + if (elem->has("fontSize")) mSelectedProperties.mFontSize = elem->get("fontSize") * sh; - if (elem && elem->has("fontPath")) + if (elem->has("fontPath")) mSelectedProperties.mFontPath = elem->get("fontPath"); } } From 6c4b17f73a6bbe7d9a12740b8f17209314ccdfa3 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 29 Oct 2019 11:02:43 +0100 Subject: [PATCH 201/336] Fix GridGameListView : md_thumbnail & md_marquee objects are missing # Conflicts: # es-app/src/views/gamelist/GridGameListView.cpp # es-app/src/views/gamelist/GridGameListView.h --- es-app/src/FileData.cpp | 49 ++++- es-app/src/guis/GuiMenu.cpp | 39 ++-- es-app/src/views/ViewController.cpp | 6 +- .../src/views/gamelist/GridGameListView.cpp | 167 +++++++++++++----- es-app/src/views/gamelist/GridGameListView.h | 11 +- es-core/src/EsLocale.h | 2 + es-core/src/Settings.cpp | 2 +- es-core/src/ThemeData.cpp | 2 +- 8 files changed, 215 insertions(+), 63 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index fb198ba157..dc588d20d9 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -365,7 +365,8 @@ const std::vector FolderData::getChildrenListToDisplay() { std::vector ret; - bool flatFolders = Settings::getInstance()->getBool("FlatFolders"); + std::string showFoldersMode = Settings::getInstance()->getString("FolderViewMode"); + bool showHiddenFiles = Settings::getInstance()->getBool("ShowHiddenFiles"); bool filterKidGame = false; @@ -387,12 +388,14 @@ const std::vector FolderData::getChildrenListToDisplay() std::vector* items = &mChildren; std::vector flatGameList; - if (flatFolders) + if (showFoldersMode == "never") { flatGameList = getFlatGameList(false, sys); items = &flatGameList; } + bool refactorUniqueGameFolders = (showFoldersMode == "having multiple games"); + for (auto it = items->cbegin(); it != items->cend(); it++) { if (idx != nullptr && !idx->showFile((*it))) @@ -404,12 +407,54 @@ const std::vector FolderData::getChildrenListToDisplay() if (filterKidGame && !(*it)->getKidGame()) continue; + if ((*it)->getType() == FOLDER && refactorUniqueGameFolders) + { + FolderData* pFolder = (FolderData*)(*it); + auto fd = pFolder->findUniqueGameForFolder(); + if (fd != nullptr) + { + if (idx != nullptr && !idx->showFile(fd)) + continue; + + if (!showHiddenFiles && fd->getHidden()) + continue; + + if (filterKidGame && !fd->getKidGame()) + continue; + + ret.push_back(fd); + + continue; + } + } + ret.push_back(*it); } return ret; } +FileData* FolderData::findUniqueGameForFolder() +{ + std::vector children = getChildren(); + + if (children.size() == 1 && children.at(0)->getType() == GAME) + return children.at(0); + + for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + { + if ((*it)->getType() == GAME) + return NULL; + + FolderData* folder = (FolderData*)(*it); + FileData* ret = folder->findUniqueGameForFolder(); + if (ret != NULL) + return ret; + } + + return NULL; +} + std::vector FolderData::getFlatGameList(bool displayedOnly, SystemData* system) const { std::vector ret = getFilesRecursive(GAME, displayedOnly, system); diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 22b6017a97..c77873c9c7 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -373,18 +373,30 @@ void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptr 0) { - auto selectedColorSet = std::find(themeColorSets.cbegin(), themeColorSets.cend(), Settings::getInstance()->getString(settingName)); - if (selectedColorSet == themeColorSets.end()) - selectedColorSet = themeColorSets.begin(); + auto selectedColorSet = themeColorSets.end(); + auto selectedName = Settings::getInstance()->getString(settingName); + + for (auto it = themeColorSets.begin(); it != themeColorSets.end() && selectedColorSet == themeColorSets.end(); it++) + if (it->name == selectedName) + selectedColorSet = it; std::shared_ptr> item = std::make_shared >(mWindow, _(("THEME " + Utils::String::toUpper(subset)).c_str()), false); item->setTag(settingName); for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) - item->add(*it, *it, it == selectedColorSet); + item->add(it->displayName, it->name, it == selectedColorSet); + + if (selectedColorSet == themeColorSets.end()) + item->selectFirstItem(); if (!themeColorSets.empty()) - themeconfig->addWithLabel(_(("THEME " + Utils::String::toUpper(subset)).c_str()), item); + { + std::string displayName = theme->getVariable("subset." + subset); + if (!displayName.empty()) + themeconfig->addWithLabel(displayName, item); + else + themeconfig->addWithLabel(_(("THEME " + Utils::String::toUpper(subset)).c_str()), item); + } options[settingName] = item; } @@ -398,19 +410,24 @@ void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptr >(mWindow, _("GAMELIST VIEW STYLE"), false); - std::vector styles; - styles.push_back("automatic"); + std::vector> styles; + styles.push_back(std::pair("automatic", _("automatic"))); if (system != NULL) { auto mViews = theme->getViewsOfTheme(); for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) - styles.push_back(*it); + { + if (it->first == "basic" || it->first == "detailed" || it->first == "grid") + styles.push_back(std::pair(it->first, _(it->first.c_str()))); + else + styles.push_back(*it); + } } else { - styles.push_back("basic"); - styles.push_back("detailed"); + styles.push_back(std::pair("basic", _("basic"))); + styles.push_back(std::pair("detailed", _("detailed"))); } auto viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); @@ -418,7 +435,7 @@ void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptradd(_(it->c_str()), *it, viewPreference == *it); + gamelist_style->add(it->second, it->first, viewPreference == it->first); themeconfig->addWithLabel(_("GAMELIST VIEW STYLE"), gamelist_style); } diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index b525484f92..76e0faa2b8 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -476,13 +476,11 @@ bool ViewController::input(InputConfig* config, Input input) ResourceManager::getInstance()->unloadAll(); ResourceManager::getInstance()->reloadAll(); - mWindow->renderLoadingScreen(_("Loading..."), -1, 180); - #if WIN32 - EsLocale::reset(); + EsLocale::reset(); #endif - ViewController::get()->reloadAll(); + ViewController::get()->reloadAll(mWindow); mWindow->endRenderLoadingScreen(); return true; } diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 4d13c65af7..2b67d592a3 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -18,8 +18,7 @@ GridGameListView::GridGameListView(Window* window, FolderData* root, const std:: ISimpleGameListView(window, root), mGrid(window), mDescContainer(window), mDescription(window), - mImage(window, true), - mVideo(nullptr), + mImage(nullptr), mVideo(nullptr), mMarquee(nullptr), mThumbnail(nullptr), mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -37,13 +36,6 @@ GridGameListView::GridGameListView(Window* window, FolderData* root, const std:: mGrid.setCursorChangedCallback([&](const CursorState& /*state*/) { updateInfoPanel(); }); addChild(&mGrid); - // 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); - mImage.setDefaultZIndex(30); - addChild(&mImage); - // metadata labels + values mLblRating.setText("Rating: "); addChild(&mLblRating); @@ -116,6 +108,7 @@ void GridGameListView::createVideo() #endif mVideo = new VideoVlcComponent(mWindow,""); + mVideo->setSnapshotSource(IMAGE); mVideo->setOrigin(0.5f, 0.5f); mVideo->setPosition(mSize.x() * 0.25f, mSize.y() * 0.4f); mVideo->setSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.4f); @@ -133,6 +126,15 @@ void GridGameListView::onShow() GridGameListView::~GridGameListView() { + if (mImage != nullptr) + delete mImage; + + if (mThumbnail != nullptr) + delete mThumbnail; + + if (mMarquee != nullptr) + delete mMarquee; + if (mVideo != nullptr) delete mVideo; } @@ -259,14 +261,6 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) mGrid.applyTheme(theme, getName(), "gamegrid", ALL); mName.applyTheme(theme, getName(), "md_name", ALL); - if (theme->getElement(getName(), "md_image", "image")) - { - mImageVisible = true; - mImage.applyTheme(theme, getName(), "md_image", ALL ^ (PATH)); - } - else - mImageVisible = false; - if (theme->getElement(getName(), "md_video", "video")) { createVideo(); @@ -279,6 +273,42 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) mVideo = nullptr; } + if (theme->getElement(getName(), "md_image", "image")) + { + createImage(); + mImage->applyTheme(theme, getName(), "md_image", ALL ^ (PATH)); + } + else if (mImage != nullptr) + { + removeChild(mImage); + delete mImage; + mImage = nullptr; + } + + if (theme->getElement(getName(), "md_thumbnail", "image")) + { + createThumbnail(); + mThumbnail->applyTheme(theme, getName(), "md_thumbnail", ALL ^ (PATH)); + } + else if (mThumbnail != nullptr) + { + removeChild(mThumbnail); + delete mThumbnail; + mThumbnail = nullptr; + } + + if (theme->getElement(getName(), "md_marquee", "image")) + { + createMarquee(); + mMarquee->applyTheme(theme, getName(), "md_marquee", ALL ^ (PATH)); + } + else if (mMarquee != nullptr) + { + removeChild(mMarquee); + delete mMarquee; + mMarquee = nullptr; + } + initMDLabels(); std::vector labels = getMDLabels(); assert(labels.size() == 8); @@ -306,7 +336,6 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) values[i]->applyTheme(theme, getName(), valElements[i], ALL ^ ThemeFlags::TEXT); } - if (theme->getElement(getName(), "md_description", "text")) { mDescContainer.applyTheme(theme, getName(), "md_description", POSITION | ThemeFlags::SIZE | Z_INDEX); @@ -319,11 +348,58 @@ void GridGameListView::onThemeChanged(const std::shared_ptr& theme) else removeChild(&mDescContainer); - sortChildren(); updateInfoPanel(); } +void GridGameListView::createImage() +{ + if (mImage != nullptr) + return; + + const float padding = 0.01f; + + // Image + mImage = new ImageComponent(mWindow); + mImage->setAllowFading(false); + mImage->setOrigin(0.5f, 0.5f); + mImage->setPosition(mSize.x() * 0.25f, mGrid.getPosition().y() + mSize.y() * 0.2125f); + mImage->setMaxSize(mSize.x() * (0.50f - 2 * padding), mSize.y() * 0.4f); + mImage->setDefaultZIndex(30); + addChild(mImage); +} + +void GridGameListView::createThumbnail() +{ + if (mThumbnail != nullptr) + return; + + const float padding = 0.01f; + + // Image + mThumbnail = new ImageComponent(mWindow); + mThumbnail->setAllowFading(false); + mThumbnail->setOrigin(0.5f, 0.5f); + mThumbnail->setPosition(mSize.x() * 0.25f, mGrid.getPosition().y() + mSize.y() * 0.2125f); + mThumbnail->setMaxSize(mSize.x() * (0.50f - 2 * padding), mSize.y() * 0.4f); + mThumbnail->setDefaultZIndex(30); + addChild(mThumbnail); +} + +void GridGameListView::createMarquee() +{ + const float padding = 0.01f; + + // Marquee + mMarquee = new ImageComponent(mWindow); + mMarquee->setAllowFading(false); + mMarquee->setOrigin(0.5f, 0.5f); + mMarquee->setPosition(mSize.x() * 0.25f, mSize.y() * 0.10f); + mMarquee->setMaxSize(mSize.x() * (0.5f - 2 * padding), mSize.y() * 0.18f); + mMarquee->setDefaultZIndex(35); + addChild(mMarquee); +} + void GridGameListView::initMDLabels() { std::vector components = getMDLabels(); @@ -402,38 +478,41 @@ void GridGameListView::updateInfoPanel() mVideo->setImage(""); } - mImage.setImage(""); + if(mImage != nullptr) + mImage->setImage(""); + // mVideo->setImage(""); // mDescription.setText(""); fadingOut = true; } else { + std::string imagePath = file->getImagePath().empty() ? file->getThumbnailPath() : file->getImagePath(); + if (mVideo != nullptr) { if (!mVideo->setVideo(file->getVideoPath())) mVideo->setDefaultVideo(); - } - if (mImageVisible) - { - if (file->getImagePath().empty()) - { - if (mVideo != nullptr) - mVideo->setImage(file->getThumbnailPath(), false, mVideo->getMaxSizeInfo()); + std::string snapShot = imagePath; - mImage.setImage(file->getThumbnailPath(), false, mImage.getMaxSizeInfo()); - } - else - { - if (mVideo != nullptr) - mVideo->setImage(file->getImagePath(), false, mVideo->getMaxSizeInfo()); + auto src = mVideo->getSnapshotSource(); + if (src == MARQUEE && !file->getMarqueePath().empty()) + snapShot = file->getMarqueePath(); + if (src == THUMBNAIL && !file->getThumbnailPath().empty()) + snapShot = file->getThumbnailPath(); - mImage.setImage(file->getImagePath(), false, mImage.getMaxSizeInfo()); - } + mVideo->setImage(snapShot); } - else - mImage.setImage(""); + + if (mImage != nullptr) + mImage->setImage(imagePath); + + if (mThumbnail != nullptr) + mThumbnail->setImage(file->getThumbnailPath()); + + if (mMarquee != nullptr) + mMarquee->setImage(file->getMarqueePath(), false, mMarquee->getMaxSizeInfo()); mDescription.setText(file->metadata.get("desc")); mDescContainer.reset(); @@ -464,7 +543,15 @@ void GridGameListView::updateInfoPanel() if (mVideo != nullptr) comps.push_back(mVideo); - comps.push_back(&mImage); + if (mMarquee != nullptr) + comps.push_back(mMarquee); + + if (mThumbnail != nullptr) + comps.push_back(mThumbnail); + + if (mImage != nullptr) + comps.push_back(mImage); + comps.push_back(&mDescription); comps.push_back(&mName); @@ -502,8 +589,8 @@ void GridGameListView::launch(FileData* game) if (mVideoVisible && mVideo) target = Vector3f(mVideo->getCenter().x(), mVideo->getCenter().y(), 0); - else if (mImageVisible) - target = Vector3f(mImage.getCenter().x(), mImage.getCenter().y(), 0); + else if (mImage != nullptr) + target = Vector3f(mImage->getCenter().x(), mImage->getCenter().y(), 0); else { auto tile = mGrid.getSelectedTile(); diff --git a/es-app/src/views/gamelist/GridGameListView.h b/es-app/src/views/gamelist/GridGameListView.h index f9c78065b3..9e542ec48f 100644 --- a/es-app/src/views/gamelist/GridGameListView.h +++ b/es-app/src/views/gamelist/GridGameListView.h @@ -52,7 +52,10 @@ class GridGameListView : public ISimpleGameListView private: void updateInfoPanel(); const std::string getImagePath(FileData* file); - + + void createMarquee(); + void createImage(); + void createThumbnail(); void createVideo(); void initMDLabels(); @@ -70,9 +73,9 @@ class GridGameListView : public ISimpleGameListView TextComponent mPlayCount; TextComponent mName; - ImageComponent mImage; - bool mImageVisible; - + ImageComponent* mImage; + ImageComponent* mThumbnail; + ImageComponent* mMarquee; VideoComponent* mVideo; bool mVideoVisible; diff --git a/es-core/src/EsLocale.h b/es-core/src/EsLocale.h index dc25c496bd..e6b0b82dfc 100644 --- a/es-core/src/EsLocale.h +++ b/es-core/src/EsLocale.h @@ -20,6 +20,8 @@ class EsLocale static const std::string getLanguage() { return mCurrentLanguage; } + static const void reset() { mCurrentLanguageLoaded = false; } + private: static void checkLocalisationLoaded(); static std::map mItems; diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 9e38417bce..a492f5cbcc 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -96,7 +96,7 @@ void Settings::setDefaults() mStringMap["StartupSystem"] = ""; - mBoolMap["FlatFolders"] = false; + mStringMap["FolderViewMode"] = "never"; mBoolMap["VSync"] = true; mBoolMap["EnableSounds"] = true; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 9cb8b23f17..d69f9be2ff 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -972,7 +972,7 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map Date: Tue, 29 Oct 2019 12:33:32 +0100 Subject: [PATCH 202/336] GuiMenu : display version.info content as version --- es-app/src/guis/GuiMenu.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index c77873c9c7..abd529a718 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -1386,7 +1386,20 @@ void GuiMenu::addVersionInfo() mVersion.setColor(theme->Footer.color); mVersion.setLineSpacing(0); - mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + " BUILD " + buildDate); + +#if WIN32 + std::string localVersion; + std::string localVersionFile = Utils::FileSystem::getExePath() + "/version.info"; + if (Utils::FileSystem::exists(localVersionFile)) + { + localVersion = Utils::FileSystem::readAllText(localVersionFile); + localVersion = Utils::String::replace(Utils::String::replace(localVersion, "\r", ""), "\n", ""); + mVersion.setText("EMULATIONSTATION V" + localVersion+" FCAMOD"); + } + else +#endif + mVersion.setText("EMULATIONSTATION V" + Utils::String::toUpper(PROGRAM_VERSION_STRING) + " BUILD " + buildDate); + mVersion.setHorizontalAlignment(ALIGN_CENTER); mVersion.setVerticalAlignment(ALIGN_CENTER); addChild(&mVersion); From 3e9494467c9f1e470331fd94bfb3b9a6d87256b1 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 30 Oct 2019 19:19:53 +0100 Subject: [PATCH 203/336] Folder navigation & better sortings --- es-app/src/CollectionSystemManager.cpp | 2140 ++++++++--------- es-app/src/FileData.h | 15 +- es-app/src/FileSorts.cpp | 25 +- es-app/src/FileSorts.h | 26 + es-app/src/SystemData.cpp | 8 +- es-app/src/guis/GuiFastSelect.cpp | 7 +- es-app/src/guis/GuiGamelistOptions.cpp | 8 +- es-app/src/guis/GuiMenu.cpp | 14 + .../src/views/gamelist/BasicGameListView.cpp | 15 +- .../src/views/gamelist/GridGameListView.cpp | 6 + .../views/gamelist/ISimpleGameListView.cpp | 42 +- es-core/src/components/GridTileComponent.cpp | 20 +- es-core/src/components/GridTileComponent.h | 4 +- es-core/src/components/ImageGridComponent.h | 6 +- 14 files changed, 1211 insertions(+), 1125 deletions(-) diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index cad9819230..0b9abe282f 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -1,317 +1,313 @@ -#include "CollectionSystemManager.h" - -#include "guis/GuiInfoPopup.h" -#include "utils/FileSystemUtil.h" -#include "utils/StringUtil.h" -#include "views/gamelist/IGameListView.h" -#include "views/ViewController.h" -#include "FileData.h" -#include "FileFilterIndex.h" -#include "Log.h" -#include "Settings.h" -#include "SystemData.h" -#include "ThemeData.h" -#include -#include - -std::string myCollectionsName = "collections"; - -#define LAST_PLAYED_MAX 50 - -/* Handling the getting, initialization, deinitialization, saving and deletion of - * a CollectionSystemManager Instance */ -CollectionSystemManager* CollectionSystemManager::sInstance = NULL; - -CollectionSystemManager::CollectionSystemManager(Window* window) : mWindow(window) -{ - CollectionSystemDecl systemDecls[] = { - //type name long name //default sort // theme folder // isCustom - { AUTO_ALL_GAMES, "all", "all games", "filename, ascending", "auto-allgames", false }, - { AUTO_LAST_PLAYED, "recent", "last played", "last played, descending", "auto-lastplayed", false }, - { AUTO_FAVORITES, "favorites", "favorites", "filename, ascending", "auto-favorites", false }, - { CUSTOM_COLLECTION, myCollectionsName, "collections", "filename, ascending", "custom-collections", true } - }; - - // create a map - std::vector tempSystemDecl = std::vector(systemDecls, systemDecls + sizeof(systemDecls) / sizeof(systemDecls[0])); - - for (std::vector::const_iterator it = tempSystemDecl.cbegin(); it != tempSystemDecl.cend(); ++it ) - { - mCollectionSystemDeclsIndex[(*it).name] = (*it); - } - - // creating standard environment data - mCollectionEnvData = new SystemEnvironmentData; - mCollectionEnvData->mStartPath = ""; - std::vector exts; - mCollectionEnvData->mSearchExtensions = exts; - mCollectionEnvData->mLaunchCommand = ""; - std::vector allPlatformIds; - allPlatformIds.push_back(PlatformIds::PLATFORM_IGNORE); - mCollectionEnvData->mPlatformIds = allPlatformIds; - - std::string path = getCollectionsFolder(); - if(!Utils::FileSystem::exists(path)) - Utils::FileSystem::createDirectory(path); - - mIsEditingCustom = false; - mEditingCollection = "Favorites"; - mEditingCollectionSystemData = NULL; - mCustomCollectionsBundle = NULL; -} - -CollectionSystemManager::~CollectionSystemManager() -{ - assert(sInstance == this); - removeCollectionsFromDisplayedSystems(); - - // iterate the map - for(std::map::const_iterator it = mCustomCollectionSystemsData.cbegin() ; it != mCustomCollectionSystemsData.cend() ; it++ ) - { - if (it->second.isPopulated) - { - saveCustomCollection(it->second.system); - } - delete it->second.system; - } - sInstance = NULL; -} - -CollectionSystemManager* CollectionSystemManager::get() -{ - assert(sInstance); - return sInstance; -} - -void CollectionSystemManager::init(Window* window) -{ - assert(!sInstance); - sInstance = new CollectionSystemManager(window); -} - -void CollectionSystemManager::deinit() -{ - if (sInstance) - { - delete sInstance; - } -} - -void CollectionSystemManager::saveCustomCollection(SystemData* sys) -{ - std::string name = sys->getName(); - auto games = sys->getRootFolder()->getChildren(); - - bool found = mCustomCollectionSystemsData.find(name) != mCustomCollectionSystemsData.cend(); - if (found) - { - CollectionSystemData sysData = mCustomCollectionSystemsData.at(name); - if (sysData.needsSave) - { - auto home = Utils::FileSystem::getHomePath(); - - std::ofstream configFile; - configFile.open(getCustomCollectionConfigPath(name)); - for(auto iter = games.cbegin(); iter != games.cend(); ++iter) - { - std::string path = (*iter)->getKey(); - - path = Utils::FileSystem::createRelativePath(path, "portnawak", true); - - configFile << path << std::endl; - } - configFile.close(); - } - } - else - { - LOG(LogError) << "Couldn't find collection to save! " << name; - } -} - -/* Methods to load all Collections into memory, and handle enabling the active ones */ -// loads all Collection Systems -void CollectionSystemManager::loadCollectionSystems(bool async) -{ - initAutoCollectionSystems(); - CollectionSystemDecl decl = mCollectionSystemDeclsIndex[myCollectionsName]; - mCustomCollectionsBundle = createNewCollectionEntry(decl.name, decl, false); - // we will also load custom systems here - initCustomCollectionSystems(); - if(Settings::getInstance()->getString("CollectionSystemsAuto") != "" || Settings::getInstance()->getString("CollectionSystemsCustom") != "") - { - // Now see which ones are enabled - loadEnabledListFromSettings(); - - - // add to the main System Vector, and create Views as needed - if (!async) - updateSystemsList(); - } -} - -// loads settings -void CollectionSystemManager::loadEnabledListFromSettings() -{ - // we parse the auto collection settings list - std::vector autoSelected = Utils::String::commaStringToVector(Settings::getInstance()->getString("CollectionSystemsAuto")); - - // iterate the map - for(std::map::iterator it = mAutoCollectionSystemsData.begin() ; it != mAutoCollectionSystemsData.end() ; it++ ) - { - it->second.isEnabled = (std::find(autoSelected.cbegin(), autoSelected.cend(), it->first) != autoSelected.cend()); - } - - // we parse the custom collection settings list - std::vector customSelected = Utils::String::commaStringToVector(Settings::getInstance()->getString("CollectionSystemsCustom")); - - // iterate the map - for(std::map::iterator it = mCustomCollectionSystemsData.begin() ; it != mCustomCollectionSystemsData.end() ; it++ ) - { - it->second.isEnabled = (std::find(customSelected.cbegin(), customSelected.cend(), it->first) != customSelected.cend()); - } -} - -// updates enabled system list in System View -void CollectionSystemManager::updateSystemsList() -{ - // remove all Collection Systems - removeCollectionsFromDisplayedSystems(); - +#include "CollectionSystemManager.h" + +#include "guis/GuiInfoPopup.h" +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" +#include "views/gamelist/IGameListView.h" +#include "views/ViewController.h" +#include "FileData.h" +#include "FileFilterIndex.h" +#include "Log.h" +#include "Settings.h" +#include "SystemData.h" +#include "ThemeData.h" +#include +#include + +std::string myCollectionsName = "collections"; + +#define LAST_PLAYED_MAX 50 + +/* Handling the getting, initialization, deinitialization, saving and deletion of + * a CollectionSystemManager Instance */ +CollectionSystemManager* CollectionSystemManager::sInstance = NULL; + +CollectionSystemManager::CollectionSystemManager(Window* window) : mWindow(window) +{ + CollectionSystemDecl systemDecls[] = { + //type name long name //default sort // theme folder // isCustom + { AUTO_ALL_GAMES, "all", "all games", "filename, ascending", "auto-allgames", false }, + { AUTO_LAST_PLAYED, "recent", "last played", "last played, descending", "auto-lastplayed", false }, + { AUTO_FAVORITES, "favorites", "favorites", "filename, ascending", "auto-favorites", false }, + { CUSTOM_COLLECTION, myCollectionsName, "collections", "filename, ascending", "custom-collections", true } + }; + + // create a map + std::vector tempSystemDecl = std::vector(systemDecls, systemDecls + sizeof(systemDecls) / sizeof(systemDecls[0])); + + for (std::vector::const_iterator it = tempSystemDecl.cbegin(); it != tempSystemDecl.cend(); ++it ) + { + mCollectionSystemDeclsIndex[(*it).name] = (*it); + } + + // creating standard environment data + mCollectionEnvData = new SystemEnvironmentData; + mCollectionEnvData->mStartPath = ""; + std::vector exts; + mCollectionEnvData->mSearchExtensions = exts; + mCollectionEnvData->mLaunchCommand = ""; + std::vector allPlatformIds; + allPlatformIds.push_back(PlatformIds::PLATFORM_IGNORE); + mCollectionEnvData->mPlatformIds = allPlatformIds; + + std::string path = getCollectionsFolder(); + if(!Utils::FileSystem::exists(path)) + Utils::FileSystem::createDirectory(path); + + mIsEditingCustom = false; + mEditingCollection = "Favorites"; + mEditingCollectionSystemData = NULL; + mCustomCollectionsBundle = NULL; +} + +CollectionSystemManager::~CollectionSystemManager() +{ + assert(sInstance == this); + removeCollectionsFromDisplayedSystems(); + + // iterate the map + for(std::map::const_iterator it = mCustomCollectionSystemsData.cbegin() ; it != mCustomCollectionSystemsData.cend() ; it++ ) + { + if (it->second.isPopulated) + { + saveCustomCollection(it->second.system); + } + delete it->second.system; + } + sInstance = NULL; +} + +CollectionSystemManager* CollectionSystemManager::get() +{ + assert(sInstance); + return sInstance; +} + +void CollectionSystemManager::init(Window* window) +{ + assert(!sInstance); + sInstance = new CollectionSystemManager(window); +} + +void CollectionSystemManager::deinit() +{ + if (sInstance) + { + delete sInstance; + } +} + +void CollectionSystemManager::saveCustomCollection(SystemData* sys) +{ + std::string name = sys->getName(); + auto games = sys->getRootFolder()->getChildren(); + + bool found = mCustomCollectionSystemsData.find(name) != mCustomCollectionSystemsData.cend(); + if (found) + { + CollectionSystemData sysData = mCustomCollectionSystemsData.at(name); + if (sysData.needsSave) + { + auto home = Utils::FileSystem::getHomePath(); + + std::ofstream configFile; + configFile.open(getCustomCollectionConfigPath(name)); + for(auto iter = games.cbegin(); iter != games.cend(); ++iter) + { + std::string path = (*iter)->getKey(); + + path = Utils::FileSystem::createRelativePath(path, "portnawak", true); + + configFile << path << std::endl; + } + configFile.close(); + } + } + else + { + LOG(LogError) << "Couldn't find collection to save! " << name; + } +} + +/* Methods to load all Collections into memory, and handle enabling the active ones */ +// loads all Collection Systems +void CollectionSystemManager::loadCollectionSystems(bool async) +{ + initAutoCollectionSystems(); + CollectionSystemDecl decl = mCollectionSystemDeclsIndex[myCollectionsName]; + mCustomCollectionsBundle = createNewCollectionEntry(decl.name, decl, false); + // we will also load custom systems here + initCustomCollectionSystems(); + if(Settings::getInstance()->getString("CollectionSystemsAuto") != "" || Settings::getInstance()->getString("CollectionSystemsCustom") != "") + { + // Now see which ones are enabled + loadEnabledListFromSettings(); + + + // add to the main System Vector, and create Views as needed + if (!async) + updateSystemsList(); + } +} + +// loads settings +void CollectionSystemManager::loadEnabledListFromSettings() +{ + // we parse the auto collection settings list + std::vector autoSelected = Utils::String::commaStringToVector(Settings::getInstance()->getString("CollectionSystemsAuto")); + + // iterate the map + for(std::map::iterator it = mAutoCollectionSystemsData.begin() ; it != mAutoCollectionSystemsData.end() ; it++ ) + { + it->second.isEnabled = (std::find(autoSelected.cbegin(), autoSelected.cend(), it->first) != autoSelected.cend()); + } + + // we parse the custom collection settings list + std::vector customSelected = Utils::String::commaStringToVector(Settings::getInstance()->getString("CollectionSystemsCustom")); + + // iterate the map + for(std::map::iterator it = mCustomCollectionSystemsData.begin() ; it != mCustomCollectionSystemsData.end() ; it++ ) + { + it->second.isEnabled = (std::find(customSelected.cbegin(), customSelected.cend(), it->first) != customSelected.cend()); + } +} + +// updates enabled system list in System View +void CollectionSystemManager::updateSystemsList() +{ + // remove all Collection Systems + removeCollectionsFromDisplayedSystems(); + std::unordered_map map; - getAllGamesCollection()->getRootFolder()->createChildrenByFilenameMap(map); - - // add custom enabled ones - addEnabledCollectionsToDisplayedSystems(&mCustomCollectionSystemsData, &map); - - if (Settings::getInstance()->getBool("SortAllSystems")) - { - // sort custom individual systems with other systems - std::sort(SystemData::sSystemVector.begin(), SystemData::sSystemVector.end(), systemSort); - - // move RetroPie system to end, before auto collections - for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); ) - { - if ((*sysIt)->getName() == "retropie") - { - SystemData* retroPieSystem = (*sysIt); - sysIt = SystemData::sSystemVector.erase(sysIt); - SystemData::sSystemVector.push_back(retroPieSystem); - break; - } - else - { - sysIt++; - } - } - } - - if(mCustomCollectionsBundle->getRootFolder()->getChildren().size() > 0) - { - mCustomCollectionsBundle->getRootFolder()->sort(getSortTypeFromString(mCollectionSystemDeclsIndex[myCollectionsName].defaultSort)); - SystemData::sSystemVector.push_back(mCustomCollectionsBundle); - } - - // add auto enabled ones - addEnabledCollectionsToDisplayedSystems(&mAutoCollectionSystemsData, &map); - /* - // create views for collections, before reload - for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) - { - if ((*sysIt)->isCollection()) - { - ViewController::get()->getGameListView((*sysIt)); - } - }*/ - - // if we were editing a custom collection, and it's no longer enabled, exit edit mode - if(mIsEditingCustom && !mEditingCollectionSystemData->isEnabled) - { - exitEditMode(); - } -} - -/* Methods to manage collection files related to a source FileData */ -// updates all collection files related to the source file -void CollectionSystemManager::refreshCollectionSystems(FileData* file) -{ - if (!file->getSystem()->isGameSystem() || file->getType() != GAME) - return; - - std::map allCollections; - allCollections.insert(mAutoCollectionSystemsData.cbegin(), mAutoCollectionSystemsData.cend()); - allCollections.insert(mCustomCollectionSystemsData.cbegin(), mCustomCollectionSystemsData.cend()); - - for(auto sysDataIt = allCollections.cbegin(); sysDataIt != allCollections.cend(); sysDataIt++) - { - updateCollectionSystem(file, sysDataIt->second); - } -} - -void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionSystemData sysData) -{ - if (sysData.isPopulated) - { - // collection files use the full path as key, to avoid clashes - std::string key = file->getFullPath(); - - SystemData* curSys = sysData.system; - FileData* collectionEntry = curSys->getRootFolder()->FindByPath(key); - - FolderData* rootFolder = curSys->getRootFolder(); - - std::string name = curSys->getName(); - - if (collectionEntry != nullptr) - { - // if we found it, we need to update it - // remove from index, so we can re-index metadata after refreshing - curSys->removeFromIndex(collectionEntry); - collectionEntry->refreshMetadata(); - // found and we are removing - if (name == "favorites" && file->metadata.get("favorite") == "false") { - // need to check if still marked as favorite, if not remove - ViewController::get()->getGameListView(curSys).get()->remove(collectionEntry, false); - - ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); - ViewController::get()->getGameListView(curSys)->onFileChanged(collectionEntry, FILE_METADATA_CHANGED); - } - else - { - // re-index with new metadata - curSys->addToIndex(collectionEntry); - ViewController::get()->onFileChanged(collectionEntry, FILE_METADATA_CHANGED); - } - } - else - { - // we didn't find it here - we need to check if we should add it - if (name == "recent" && file->metadata.get("playcount") > "0" && includeFileInAutoCollections(file) || - name == "favorites" && file->metadata.get("favorite") == "true") { - CollectionFileData* newGame = new CollectionFileData(file, curSys); - rootFolder->addChild(newGame); - curSys->addToIndex(newGame); - - ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); - ViewController::get()->getGameListView(curSys)->onFileChanged(newGame, FILE_METADATA_CHANGED); - } - } - - curSys->updateDisplayedGameCount(); - - rootFolder->sort(getSortTypeFromString(mCollectionSystemDeclsIndex[name].defaultSort)); - if (name == "recent") - { - trimCollectionCount(rootFolder, LAST_PLAYED_MAX); - ViewController::get()->onFileChanged(rootFolder, FILE_METADATA_CHANGED); - } - else - ViewController::get()->onFileChanged(rootFolder, FILE_SORTED); - } -} - -void CollectionSystemManager::trimCollectionCount(FolderData* rootFolder, int limit) -{ + getAllGamesCollection()->getRootFolder()->createChildrenByFilenameMap(map); + + // add custom enabled ones + addEnabledCollectionsToDisplayedSystems(&mCustomCollectionSystemsData, &map); + + if (Settings::getInstance()->getBool("SortAllSystems")) + { + // sort custom individual systems with other systems + std::sort(SystemData::sSystemVector.begin(), SystemData::sSystemVector.end(), systemSort); + + // move RetroPie system to end, before auto collections + for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); ) + { + if ((*sysIt)->getName() == "retropie") + { + SystemData* retroPieSystem = (*sysIt); + sysIt = SystemData::sSystemVector.erase(sysIt); + SystemData::sSystemVector.push_back(retroPieSystem); + break; + } + else + { + sysIt++; + } + } + } + + if(mCustomCollectionsBundle->getRootFolder()->getChildren().size() > 0) + SystemData::sSystemVector.push_back(mCustomCollectionsBundle); + + // add auto enabled ones + addEnabledCollectionsToDisplayedSystems(&mAutoCollectionSystemsData, &map); + /* + // create views for collections, before reload + for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) + { + if ((*sysIt)->isCollection()) + { + ViewController::get()->getGameListView((*sysIt)); + } + }*/ + + // if we were editing a custom collection, and it's no longer enabled, exit edit mode + if(mIsEditingCustom && !mEditingCollectionSystemData->isEnabled) + { + exitEditMode(); + } +} + +/* Methods to manage collection files related to a source FileData */ +// updates all collection files related to the source file +void CollectionSystemManager::refreshCollectionSystems(FileData* file) +{ + if (!file->getSystem()->isGameSystem() || file->getType() != GAME) + return; + + std::map allCollections; + allCollections.insert(mAutoCollectionSystemsData.cbegin(), mAutoCollectionSystemsData.cend()); + allCollections.insert(mCustomCollectionSystemsData.cbegin(), mCustomCollectionSystemsData.cend()); + + for(auto sysDataIt = allCollections.cbegin(); sysDataIt != allCollections.cend(); sysDataIt++) + { + updateCollectionSystem(file, sysDataIt->second); + } +} + +void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionSystemData sysData) +{ + if (sysData.isPopulated) + { + // collection files use the full path as key, to avoid clashes + std::string key = file->getFullPath(); + + SystemData* curSys = sysData.system; + FileData* collectionEntry = curSys->getRootFolder()->FindByPath(key); + + FolderData* rootFolder = curSys->getRootFolder(); + + std::string name = curSys->getName(); + + if (collectionEntry != nullptr) + { + // if we found it, we need to update it + // remove from index, so we can re-index metadata after refreshing + curSys->removeFromIndex(collectionEntry); + collectionEntry->refreshMetadata(); + // found and we are removing + if (name == "favorites" && file->metadata.get("favorite") == "false") { + // need to check if still marked as favorite, if not remove + ViewController::get()->getGameListView(curSys).get()->remove(collectionEntry, false); + + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); + ViewController::get()->getGameListView(curSys)->onFileChanged(collectionEntry, FILE_METADATA_CHANGED); + } + else + { + // re-index with new metadata + curSys->addToIndex(collectionEntry); + ViewController::get()->onFileChanged(collectionEntry, FILE_METADATA_CHANGED); + } + } + else + { + // we didn't find it here - we need to check if we should add it + if (name == "recent" && file->metadata.get("playcount") > "0" && includeFileInAutoCollections(file) || + name == "favorites" && file->metadata.get("favorite") == "true") { + CollectionFileData* newGame = new CollectionFileData(file, curSys); + rootFolder->addChild(newGame); + curSys->addToIndex(newGame); + + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); + ViewController::get()->getGameListView(curSys)->onFileChanged(newGame, FILE_METADATA_CHANGED); + } + } + + curSys->updateDisplayedGameCount(); + + if (name == "recent") + { + trimCollectionCount(rootFolder, LAST_PLAYED_MAX); + ViewController::get()->onFileChanged(rootFolder, FILE_METADATA_CHANGED); + } + else + ViewController::get()->onFileChanged(rootFolder, FILE_SORTED); + } +} + +void CollectionSystemManager::trimCollectionCount(FolderData* rootFolder, int limit) +{ SystemData* curSys = rootFolder->getSystem(); std::shared_ptr listView = ViewController::get()->getGameListView(curSys, false); @@ -323,771 +319,767 @@ void CollectionSystemManager::trimCollectionCount(FolderData* rootFolder, int li delete gameToRemove; else listView.get()->remove(gameToRemove, false); - } -} - -// deletes all collection files from collection systems related to the source file -void CollectionSystemManager::deleteCollectionFiles(FileData* file) -{ - // collection files use the full path as key, to avoid clashes - std::string key = file->getFullPath(); - // find games in collection systems - std::map allCollections; - allCollections.insert(mAutoCollectionSystemsData.cbegin(), mAutoCollectionSystemsData.cend()); - allCollections.insert(mCustomCollectionSystemsData.cbegin(), mCustomCollectionSystemsData.cend()); - - for(auto sysDataIt = allCollections.begin(); sysDataIt != allCollections.end(); sysDataIt++) - { - if (sysDataIt->second.isPopulated) - { - FileData* collectionEntry = (sysDataIt->second.system)->getRootFolder()->FindByPath(key); - if (collectionEntry != nullptr) - { - sysDataIt->second.needsSave = true; - SystemData* systemViewToUpdate = getSystemToView(sysDataIt->second.system); - ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(collectionEntry, false); - } - } - } -} - -// returns whether the current theme is compatible with Automatic or Custom Collections -bool CollectionSystemManager::isThemeGenericCollectionCompatible(bool genericCustomCollections) -{ - std::vector cfgSys = getCollectionThemeFolders(genericCustomCollections); - for(auto sysIt = cfgSys.cbegin(); sysIt != cfgSys.cend(); sysIt++) - { - if(!themeFolderExists(*sysIt)) - return false; - } - return true; -} - -bool CollectionSystemManager::isThemeCustomCollectionCompatible(std::vector stringVector) -{ - if (isThemeGenericCollectionCompatible(true)) - return true; - - // get theme path - auto themeSets = ThemeData::getThemeSets(); - auto set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(set != themeSets.cend()) - { - std::string defaultThemeFilePath = set->second.path + "/theme.xml"; - if (Utils::FileSystem::exists(defaultThemeFilePath)) - { - return true; - } - } - - for(auto sysIt = stringVector.cbegin(); sysIt != stringVector.cend(); sysIt++) - { - if(!themeFolderExists(*sysIt)) - return false; - } - return true; -} - -std::string CollectionSystemManager::getValidNewCollectionName(std::string inName, int index) -{ - std::string name = inName; - - if(index == 0) - { - size_t remove = std::string::npos; - - // get valid name - while((remove = name.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-[]() ")) != std::string::npos) - { - name.erase(remove, 1); - } - } - else - { - name += " (" + std::to_string(index) + ")"; - } - - if(name == "") - { - name = "New Collection"; - } - - if(name != inName) - { - LOG(LogInfo) << "Had to change name, from: " << inName << " to: " << name; - } - - // get used systems in es_systems.cfg - std::vector systemsInUse = getSystemsFromConfig(); - // get folders assigned to custom collections - std::vector autoSys = getCollectionThemeFolders(false); - // get folder assigned to custom collections - std::vector customSys = getCollectionThemeFolders(true); - // get folders assigned to user collections - std::vector userSys = getUserCollectionThemeFolders(); - // add them all to the list of systems in use - systemsInUse.insert(systemsInUse.cend(), autoSys.cbegin(), autoSys.cend()); - systemsInUse.insert(systemsInUse.cend(), customSys.cbegin(), customSys.cend()); - systemsInUse.insert(systemsInUse.cend(), userSys.cbegin(), userSys.cend()); - for(auto sysIt = systemsInUse.cbegin(); sysIt != systemsInUse.cend(); sysIt++) - { - if (*sysIt == name) - { - if(index > 0) { - name = name.substr(0, name.size()-4); - } - return getValidNewCollectionName(name, index+1); - } - } - // if it matches one of the custom collections reserved names - if (mCollectionSystemDeclsIndex.find(name) != mCollectionSystemDeclsIndex.cend()) - return getValidNewCollectionName(name, index+1); - return name; -} - -void CollectionSystemManager::setEditMode(std::string collectionName) -{ - if (mCustomCollectionSystemsData.find(collectionName) == mCustomCollectionSystemsData.cend()) - { - LOG(LogError) << "Tried to edit a non-existing collection: " << collectionName; - return; - } - mIsEditingCustom = true; - mEditingCollection = collectionName; - - CollectionSystemData* sysData = &(mCustomCollectionSystemsData.at(mEditingCollection)); - if (!sysData->isPopulated) - populateCustomCollection(sysData); - - // if it's bundled, this needs to be the bundle system - mEditingCollectionSystemData = sysData; - - char strbuf[512]; - snprintf(strbuf, 512, _("Editing the '%s' Collection. Add/remove games with Y.").c_str(), Utils::String::toUpper(collectionName).c_str()); - mWindow->displayNotificationMessage(strbuf, 10000); -} - -void CollectionSystemManager::exitEditMode() -{ - char strbuf[512]; - snprintf(strbuf, 512, _("Finished editing the '%s' Collection.").c_str(), mEditingCollection.c_str()); - mWindow->displayNotificationMessage(strbuf, 10000); - mIsEditingCustom = false; - mEditingCollection = "Favorites"; -} - -// adds or removes a game from a specific collection -bool CollectionSystemManager::toggleGameInCollection(FileData* file) -{ - if (file->getType() == GAME) - { - GuiInfoPopup* s; - bool adding = true; - std::string name = file->getName(); - std::string sysName = mEditingCollection; - if (mIsEditingCustom) - { - SystemData* sysData = mEditingCollectionSystemData->system; - mEditingCollectionSystemData->needsSave = true; - if (!mEditingCollectionSystemData->isPopulated) - populateCustomCollection(mEditingCollectionSystemData); - - std::string key = file->getFullPath(); - FolderData* rootFolder = sysData->getRootFolder(); - - FileData* collectionEntry = rootFolder->FindByPath(key); - - std::string name = sysData->getName(); - - SystemData* systemViewToUpdate = getSystemToView(sysData); - - if (collectionEntry != nullptr) { - adding = false; - // if we found it, we need to remove it - // remove from index - sysData->removeFromIndex(collectionEntry); - // remove from bundle index as well, if needed - if (systemViewToUpdate != sysData) - systemViewToUpdate->removeFromIndex(collectionEntry); - - ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(collectionEntry, false); - } - else - { - // we didn't find it here, we should add it - CollectionFileData* newGame = new CollectionFileData(file, sysData); - rootFolder->addChild(newGame); - sysData->addToIndex(newGame); - ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(newGame, FILE_METADATA_CHANGED); - rootFolder->sort(getSortTypeFromString(mEditingCollectionSystemData->decl.defaultSort)); - ViewController::get()->onFileChanged(systemViewToUpdate->getRootFolder(), FILE_SORTED); - // add to bundle index as well, if needed - if(systemViewToUpdate != sysData) - { - systemViewToUpdate->addToIndex(newGame); - } - } - updateCollectionFolderMetadata(sysData); - } - else - { - SystemData* sysData = file->getSourceFileData()->getSystem(); - sysData->removeFromIndex(file); - - MetaDataList* md = &file->getSourceFileData()->metadata; - - std::string value = md->get("favorite"); - if (value == "false") - md->set("favorite", "true"); - else - { - adding = false; - md->set("favorite", "false"); - } - sysData->addToIndex(file); - - refreshCollectionSystems(file->getSourceFileData()); - - SystemData* systemViewToUpdate = getSystemToView(sysData); - if (systemViewToUpdate != NULL) - { - ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); - ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(file, FILE_METADATA_CHANGED); - } - } - - char trstring[512]; - + } +} + +// deletes all collection files from collection systems related to the source file +void CollectionSystemManager::deleteCollectionFiles(FileData* file) +{ + // collection files use the full path as key, to avoid clashes + std::string key = file->getFullPath(); + // find games in collection systems + std::map allCollections; + allCollections.insert(mAutoCollectionSystemsData.cbegin(), mAutoCollectionSystemsData.cend()); + allCollections.insert(mCustomCollectionSystemsData.cbegin(), mCustomCollectionSystemsData.cend()); + + for(auto sysDataIt = allCollections.begin(); sysDataIt != allCollections.end(); sysDataIt++) + { + if (sysDataIt->second.isPopulated) + { + FileData* collectionEntry = (sysDataIt->second.system)->getRootFolder()->FindByPath(key); + if (collectionEntry != nullptr) + { + sysDataIt->second.needsSave = true; + SystemData* systemViewToUpdate = getSystemToView(sysDataIt->second.system); + ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(collectionEntry, false); + } + } + } +} + +// returns whether the current theme is compatible with Automatic or Custom Collections +bool CollectionSystemManager::isThemeGenericCollectionCompatible(bool genericCustomCollections) +{ + std::vector cfgSys = getCollectionThemeFolders(genericCustomCollections); + for(auto sysIt = cfgSys.cbegin(); sysIt != cfgSys.cend(); sysIt++) + { + if(!themeFolderExists(*sysIt)) + return false; + } + return true; +} + +bool CollectionSystemManager::isThemeCustomCollectionCompatible(std::vector stringVector) +{ + if (isThemeGenericCollectionCompatible(true)) + return true; + + // get theme path + auto themeSets = ThemeData::getThemeSets(); + auto set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if(set != themeSets.cend()) + { + std::string defaultThemeFilePath = set->second.path + "/theme.xml"; + if (Utils::FileSystem::exists(defaultThemeFilePath)) + { + return true; + } + } + + for(auto sysIt = stringVector.cbegin(); sysIt != stringVector.cend(); sysIt++) + { + if(!themeFolderExists(*sysIt)) + return false; + } + return true; +} + +std::string CollectionSystemManager::getValidNewCollectionName(std::string inName, int index) +{ + std::string name = inName; + + if(index == 0) + { + size_t remove = std::string::npos; + + // get valid name + while((remove = name.find_first_not_of("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-[]() ")) != std::string::npos) + { + name.erase(remove, 1); + } + } + else + { + name += " (" + std::to_string(index) + ")"; + } + + if(name == "") + { + name = "New Collection"; + } + + if(name != inName) + { + LOG(LogInfo) << "Had to change name, from: " << inName << " to: " << name; + } + + // get used systems in es_systems.cfg + std::vector systemsInUse = getSystemsFromConfig(); + // get folders assigned to custom collections + std::vector autoSys = getCollectionThemeFolders(false); + // get folder assigned to custom collections + std::vector customSys = getCollectionThemeFolders(true); + // get folders assigned to user collections + std::vector userSys = getUserCollectionThemeFolders(); + // add them all to the list of systems in use + systemsInUse.insert(systemsInUse.cend(), autoSys.cbegin(), autoSys.cend()); + systemsInUse.insert(systemsInUse.cend(), customSys.cbegin(), customSys.cend()); + systemsInUse.insert(systemsInUse.cend(), userSys.cbegin(), userSys.cend()); + for(auto sysIt = systemsInUse.cbegin(); sysIt != systemsInUse.cend(); sysIt++) + { + if (*sysIt == name) + { + if(index > 0) { + name = name.substr(0, name.size()-4); + } + return getValidNewCollectionName(name, index+1); + } + } + // if it matches one of the custom collections reserved names + if (mCollectionSystemDeclsIndex.find(name) != mCollectionSystemDeclsIndex.cend()) + return getValidNewCollectionName(name, index+1); + return name; +} + +void CollectionSystemManager::setEditMode(std::string collectionName) +{ + if (mCustomCollectionSystemsData.find(collectionName) == mCustomCollectionSystemsData.cend()) + { + LOG(LogError) << "Tried to edit a non-existing collection: " << collectionName; + return; + } + mIsEditingCustom = true; + mEditingCollection = collectionName; + + CollectionSystemData* sysData = &(mCustomCollectionSystemsData.at(mEditingCollection)); + if (!sysData->isPopulated) + populateCustomCollection(sysData); + + // if it's bundled, this needs to be the bundle system + mEditingCollectionSystemData = sysData; + + char strbuf[512]; + snprintf(strbuf, 512, _("Editing the '%s' Collection. Add/remove games with Y.").c_str(), Utils::String::toUpper(collectionName).c_str()); + mWindow->displayNotificationMessage(strbuf, 10000); +} + +void CollectionSystemManager::exitEditMode() +{ + char strbuf[512]; + snprintf(strbuf, 512, _("Finished editing the '%s' Collection.").c_str(), mEditingCollection.c_str()); + mWindow->displayNotificationMessage(strbuf, 10000); + mIsEditingCustom = false; + mEditingCollection = "Favorites"; +} + +// adds or removes a game from a specific collection +bool CollectionSystemManager::toggleGameInCollection(FileData* file) +{ + if (file->getType() == GAME) + { + GuiInfoPopup* s; + bool adding = true; + std::string name = file->getName(); + std::string sysName = mEditingCollection; + if (mIsEditingCustom) + { + SystemData* sysData = mEditingCollectionSystemData->system; + mEditingCollectionSystemData->needsSave = true; + if (!mEditingCollectionSystemData->isPopulated) + populateCustomCollection(mEditingCollectionSystemData); + + std::string key = file->getFullPath(); + FolderData* rootFolder = sysData->getRootFolder(); + + FileData* collectionEntry = rootFolder->FindByPath(key); + + std::string name = sysData->getName(); + + SystemData* systemViewToUpdate = getSystemToView(sysData); + + if (collectionEntry != nullptr) { + adding = false; + // if we found it, we need to remove it + // remove from index + sysData->removeFromIndex(collectionEntry); + // remove from bundle index as well, if needed + if (systemViewToUpdate != sysData) + systemViewToUpdate->removeFromIndex(collectionEntry); + + ViewController::get()->getGameListView(systemViewToUpdate).get()->remove(collectionEntry, false); + } + else + { + // we didn't find it here, we should add it + CollectionFileData* newGame = new CollectionFileData(file, sysData); + rootFolder->addChild(newGame); + sysData->addToIndex(newGame); + ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(newGame, FILE_METADATA_CHANGED); + ViewController::get()->onFileChanged(systemViewToUpdate->getRootFolder(), FILE_SORTED); + // add to bundle index as well, if needed + if(systemViewToUpdate != sysData) + { + systemViewToUpdate->addToIndex(newGame); + } + } + updateCollectionFolderMetadata(sysData); + } + else + { + SystemData* sysData = file->getSourceFileData()->getSystem(); + sysData->removeFromIndex(file); + + MetaDataList* md = &file->getSourceFileData()->metadata; + + std::string value = md->get("favorite"); + if (value == "false") + md->set("favorite", "true"); + else + { + adding = false; + md->set("favorite", "false"); + } + sysData->addToIndex(file); + + refreshCollectionSystems(file->getSourceFileData()); + + SystemData* systemViewToUpdate = getSystemToView(sysData); + if (systemViewToUpdate != NULL) + { + ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); + ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(file, FILE_METADATA_CHANGED); + } + } + + char trstring[512]; + if (adding) snprintf(trstring, 512, _("Added '%s' to '%s'").c_str(), Utils::String::removeParenthesis(name).c_str(), Utils::String::toUpper(sysName).c_str()); // batocera else snprintf(trstring, 512, _("Removed '%s' from '%s'").c_str(), Utils::String::removeParenthesis(name).c_str(), Utils::String::toUpper(sysName).c_str()); // batocera - mWindow->displayNotificationMessage(trstring, 4000); - - return true; - } - return false; -} - -SystemData* CollectionSystemManager::getSystemToView(SystemData* sys) -{ - SystemData* systemToView = sys; - FileData* rootFolder = sys->getRootFolder(); - - FolderData* bundleRootFolder = mCustomCollectionsBundle->getRootFolder(); - - // is the rootFolder bundled in the "My Collections" system? - bool sysFoundInBundle = bundleRootFolder->FindByPath(rootFolder->getKey()) != nullptr; - if (sysFoundInBundle && sys->isCollection()) - { - systemToView = mCustomCollectionsBundle; - } - return systemToView; -} - -/* Handles loading a collection system, creating an empty one, and populating on demand */ -// loads Automatic Collection systems (All, Favorites, Last Played) -void CollectionSystemManager::initAutoCollectionSystems() -{ - for(std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin() ; it != mCollectionSystemDeclsIndex.cend() ; it++ ) - { - CollectionSystemDecl sysDecl = it->second; - if (!sysDecl.isCustom) - { - createNewCollectionEntry(sysDecl.name, sysDecl); - } - } -} - -// this may come in handy if at any point in time in the future we want to -// automatically generate metadata for a folder -void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) -{ - FolderData* rootFolder = sys->getRootFolder(); - - std::string desc = _("This collection is empty."); - std::string rating = "0"; - std::string players = "1"; - std::string releasedate = "N/A"; - std::string developer = _("None"); - std::string genre = _("None"); - std::string video = ""; - std::string thumbnail = ""; - std::string image = ""; - - auto games = rootFolder->getChildren(); - - if(games.size() > 0) - { - std::string games_list = ""; - int games_counter = 0; - for(auto iter = games.cbegin(); iter != games.cend(); ++iter) - { - games_counter++; - FileData* file = *iter; - - std::string new_rating = file->metadata.get("rating"); - std::string new_releasedate = file->metadata.get("releasedate"); - std::string new_developer = file->metadata.get("developer"); - std::string new_genre = file->metadata.get("genre"); - std::string new_players = file->metadata.get("players"); - - rating = (new_rating > rating ? (new_rating != "" ? new_rating : rating) : rating); - players = (new_players > players ? (new_players != "" ? new_players : players) : players); - releasedate = (new_releasedate < releasedate ? (new_releasedate != "" ? new_releasedate : releasedate) : releasedate); - developer = (developer == _("None") ? new_developer : (new_developer != developer ? _("Various") : new_developer)); - genre = (genre == _("None") ? new_genre : (new_genre != genre ? _("Various") : new_genre)); - - switch(games_counter) - { - case 2: - case 3: - games_list += ", "; - case 1: - games_list += "'" + file->getName() + "'"; - break; - case 4: - games_list += " " + _("among other titles."); - } - } - - desc = _("This collection contains") + " " + std::to_string(games_counter) + " " + _("games, including") + " " + games_list; - - FileData* randomGame = sys->getRandomGame(); - if (randomGame != nullptr) - { - video = randomGame->getVideoPath(); - thumbnail = randomGame->getThumbnailPath(); - image = randomGame->getImagePath(); - } - } - - - rootFolder->metadata.set("desc", desc); - rootFolder->metadata.set("rating", rating); - rootFolder->metadata.set("players", players); - rootFolder->metadata.set("genre", genre); - rootFolder->metadata.set("releasedate", releasedate); - rootFolder->metadata.set("developer", developer); - rootFolder->metadata.set("video", video); - rootFolder->metadata.set("thumbnail", thumbnail); - rootFolder->metadata.set("image", image); -} - -void CollectionSystemManager::initCustomCollectionSystems() -{ - std::vector systems = getCollectionsFromConfigFolder(); - for (auto nameIt = systems.cbegin(); nameIt != systems.cend(); nameIt++) - { - addNewCustomCollection(*nameIt); - } -} - -SystemData* CollectionSystemManager::getAllGamesCollection() -{ - CollectionSystemData* allSysData = &mAutoCollectionSystemsData["all"]; - if (!allSysData->isPopulated) - { - populateAutoCollection(allSysData); - } - return allSysData->system; -} - -SystemData* CollectionSystemManager::addNewCustomCollection(std::string name) -{ - CollectionSystemDecl decl = mCollectionSystemDeclsIndex[myCollectionsName]; - decl.themeFolder = name; - decl.name = name; - decl.longName = name; - return createNewCollectionEntry(name, decl); -} - -// creates a new, empty Collection system, based on the name and declaration -SystemData* CollectionSystemManager::createNewCollectionEntry(std::string name, CollectionSystemDecl sysDecl, bool index) -{ - SystemData* newSys = new SystemData(name, sysDecl.longName, mCollectionEnvData, sysDecl.themeFolder, true); - - CollectionSystemData newCollectionData; - newCollectionData.system = newSys; - newCollectionData.decl = sysDecl; - newCollectionData.isEnabled = false; - newCollectionData.isPopulated = false; - newCollectionData.needsSave = false; - - if (index) - { - if (!sysDecl.isCustom) - { - mAutoCollectionSystemsData[name] = newCollectionData; - } - else - { - mCustomCollectionSystemsData[name] = newCollectionData; - } - } - - return newSys; -} - -// populates an Automatic Collection System -void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysData) -{ - SystemData* newSys = sysData->system; - CollectionSystemDecl sysDecl = sysData->decl; - FolderData* rootFolder = newSys->getRootFolder(); - - for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) - { - // we won't iterate all collections - if ((*sysIt)->isGameSystem() && !(*sysIt)->isCollection()) - { - std::vector files = (*sysIt)->getRootFolder()->getFilesRecursive(GAME); - for(auto gameIt = files.cbegin(); gameIt != files.cend(); gameIt++) - { - bool include = includeFileInAutoCollections((*gameIt)); - switch(sysDecl.type) { - case AUTO_LAST_PLAYED: - include = include && (*gameIt)->metadata.get("playcount") > "0"; - break; - case AUTO_FAVORITES: - // we may still want to add files we don't want in auto collections in "favorites" - include = (*gameIt)->metadata.get("favorite") == "true"; - break; - } - - if (include) { - CollectionFileData* newGame = new CollectionFileData(*gameIt, newSys); - rootFolder->addChild(newGame); - newSys->addToIndex(newGame); - } - } - } - } - rootFolder->sort(getSortTypeFromString(sysDecl.defaultSort)); - if (sysDecl.type == AUTO_LAST_PLAYED) - trimCollectionCount(rootFolder, LAST_PLAYED_MAX); - sysData->isPopulated = true; -} - -// populates a Custom Collection System -void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sysData, std::unordered_map* pMap) -{ - SystemData* newSys = sysData->system; - sysData->isPopulated = true; - CollectionSystemDecl sysDecl = sysData->decl; - std::string path = getCustomCollectionConfigPath(newSys->getName()); - - if(!Utils::FileSystem::exists(path)) - { - LOG(LogInfo) << "Couldn't find custom collection config file at " << path; - return; - } - LOG(LogInfo) << "Loading custom collection config file at " << path; - - FolderData* rootFolder = newSys->getRootFolder(); - - // get Configuration for this Custom System - std::ifstream input(path); - - FolderData* folder = getAllGamesCollection()->getRootFolder(); - - std::unordered_map map; - - if (pMap == nullptr) - { - folder->createChildrenByFilenameMap(map); - pMap = ↦ - } - - // iterate list of files in config file - for(std::string gameKey; getline(input, gameKey); ) - { - gameKey = Utils::FileSystem::resolveRelativePath(gameKey, "portnawak", true); - + mWindow->displayNotificationMessage(trstring, 4000); + + return true; + } + return false; +} + +SystemData* CollectionSystemManager::getSystemToView(SystemData* sys) +{ + SystemData* systemToView = sys; + FileData* rootFolder = sys->getRootFolder(); + + FolderData* bundleRootFolder = mCustomCollectionsBundle->getRootFolder(); + + // is the rootFolder bundled in the "My Collections" system? + bool sysFoundInBundle = bundleRootFolder->FindByPath(rootFolder->getKey()) != nullptr; + if (sysFoundInBundle && sys->isCollection()) + { + systemToView = mCustomCollectionsBundle; + } + return systemToView; +} + +/* Handles loading a collection system, creating an empty one, and populating on demand */ +// loads Automatic Collection systems (All, Favorites, Last Played) +void CollectionSystemManager::initAutoCollectionSystems() +{ + for(std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin() ; it != mCollectionSystemDeclsIndex.cend() ; it++ ) + { + CollectionSystemDecl sysDecl = it->second; + if (!sysDecl.isCustom) + { + createNewCollectionEntry(sysDecl.name, sysDecl); + } + } +} + +// this may come in handy if at any point in time in the future we want to +// automatically generate metadata for a folder +void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) +{ + FolderData* rootFolder = sys->getRootFolder(); + + std::string desc = _("This collection is empty."); + std::string rating = "0"; + std::string players = "1"; + std::string releasedate = "N/A"; + std::string developer = _("None"); + std::string genre = _("None"); + std::string video = ""; + std::string thumbnail = ""; + std::string image = ""; + + auto games = rootFolder->getChildren(); + + if(games.size() > 0) + { + std::string games_list = ""; + int games_counter = 0; + for(auto iter = games.cbegin(); iter != games.cend(); ++iter) + { + games_counter++; + FileData* file = *iter; + + std::string new_rating = file->metadata.get("rating"); + std::string new_releasedate = file->metadata.get("releasedate"); + std::string new_developer = file->metadata.get("developer"); + std::string new_genre = file->metadata.get("genre"); + std::string new_players = file->metadata.get("players"); + + rating = (new_rating > rating ? (new_rating != "" ? new_rating : rating) : rating); + players = (new_players > players ? (new_players != "" ? new_players : players) : players); + releasedate = (new_releasedate < releasedate ? (new_releasedate != "" ? new_releasedate : releasedate) : releasedate); + developer = (developer == _("None") ? new_developer : (new_developer != developer ? _("Various") : new_developer)); + genre = (genre == _("None") ? new_genre : (new_genre != genre ? _("Various") : new_genre)); + + switch(games_counter) + { + case 2: + case 3: + games_list += ", "; + case 1: + games_list += "'" + file->getName() + "'"; + break; + case 4: + games_list += " " + _("among other titles."); + } + } + + desc = _("This collection contains") + " " + std::to_string(games_counter) + " " + _("games, including") + " " + games_list; + + FileData* randomGame = sys->getRandomGame(); + if (randomGame != nullptr) + { + video = randomGame->getVideoPath(); + thumbnail = randomGame->getThumbnailPath(); + image = randomGame->getImagePath(); + } + } + + + rootFolder->metadata.set("desc", desc); + rootFolder->metadata.set("rating", rating); + rootFolder->metadata.set("players", players); + rootFolder->metadata.set("genre", genre); + rootFolder->metadata.set("releasedate", releasedate); + rootFolder->metadata.set("developer", developer); + rootFolder->metadata.set("video", video); + rootFolder->metadata.set("thumbnail", thumbnail); + rootFolder->metadata.set("image", image); +} + +void CollectionSystemManager::initCustomCollectionSystems() +{ + std::vector systems = getCollectionsFromConfigFolder(); + for (auto nameIt = systems.cbegin(); nameIt != systems.cend(); nameIt++) + { + addNewCustomCollection(*nameIt); + } +} + +SystemData* CollectionSystemManager::getAllGamesCollection() +{ + CollectionSystemData* allSysData = &mAutoCollectionSystemsData["all"]; + if (!allSysData->isPopulated) + { + populateAutoCollection(allSysData); + } + return allSysData->system; +} + +SystemData* CollectionSystemManager::addNewCustomCollection(std::string name) +{ + CollectionSystemDecl decl = mCollectionSystemDeclsIndex[myCollectionsName]; + decl.themeFolder = name; + decl.name = name; + decl.longName = name; + return createNewCollectionEntry(name, decl); +} + +// creates a new, empty Collection system, based on the name and declaration +SystemData* CollectionSystemManager::createNewCollectionEntry(std::string name, CollectionSystemDecl sysDecl, bool index) +{ + SystemData* newSys = new SystemData(name, sysDecl.longName, mCollectionEnvData, sysDecl.themeFolder, true); + + CollectionSystemData newCollectionData; + newCollectionData.system = newSys; + newCollectionData.decl = sysDecl; + newCollectionData.isEnabled = false; + newCollectionData.isPopulated = false; + newCollectionData.needsSave = false; + + if (index) + { + if (!sysDecl.isCustom) + { + mAutoCollectionSystemsData[name] = newCollectionData; + } + else + { + mCustomCollectionSystemsData[name] = newCollectionData; + } + } + + return newSys; +} + +// populates an Automatic Collection System +void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysData) +{ + SystemData* newSys = sysData->system; + CollectionSystemDecl sysDecl = sysData->decl; + FolderData* rootFolder = newSys->getRootFolder(); + + for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) + { + // we won't iterate all collections + if ((*sysIt)->isGameSystem() && !(*sysIt)->isCollection()) + { + std::vector files = (*sysIt)->getRootFolder()->getFilesRecursive(GAME); + for(auto gameIt = files.cbegin(); gameIt != files.cend(); gameIt++) + { + bool include = includeFileInAutoCollections((*gameIt)); + switch(sysDecl.type) { + case AUTO_LAST_PLAYED: + include = include && (*gameIt)->metadata.get("playcount") > "0"; + break; + case AUTO_FAVORITES: + // we may still want to add files we don't want in auto collections in "favorites" + include = (*gameIt)->metadata.get("favorite") == "true"; + break; + } + + if (include) { + CollectionFileData* newGame = new CollectionFileData(*gameIt, newSys); + rootFolder->addChild(newGame); + newSys->addToIndex(newGame); + } + } + } + } + if (sysDecl.type == AUTO_LAST_PLAYED) + trimCollectionCount(rootFolder, LAST_PLAYED_MAX); + sysData->isPopulated = true; +} + +// populates a Custom Collection System +void CollectionSystemManager::populateCustomCollection(CollectionSystemData* sysData, std::unordered_map* pMap) +{ + SystemData* newSys = sysData->system; + sysData->isPopulated = true; + CollectionSystemDecl sysDecl = sysData->decl; + std::string path = getCustomCollectionConfigPath(newSys->getName()); + + if(!Utils::FileSystem::exists(path)) + { + LOG(LogInfo) << "Couldn't find custom collection config file at " << path; + return; + } + LOG(LogInfo) << "Loading custom collection config file at " << path; + + FolderData* rootFolder = newSys->getRootFolder(); + + // get Configuration for this Custom System + std::ifstream input(path); + + FolderData* folder = getAllGamesCollection()->getRootFolder(); + + std::unordered_map map; + + if (pMap == nullptr) + { + folder->createChildrenByFilenameMap(map); + pMap = ↦ + } + + // iterate list of files in config file + for(std::string gameKey; getline(input, gameKey); ) + { + gameKey = Utils::FileSystem::resolveRelativePath(gameKey, "portnawak", true); + std::unordered_map::const_iterator it = pMap->find(gameKey); if (it != pMap->cend()) - { - CollectionFileData* newGame = new CollectionFileData(it->second, newSys); - rootFolder->addChild(newGame); - newSys->addToIndex(newGame); - } - else - { - LOG(LogInfo) << "Couldn't find game referenced at '" << gameKey << "' for system config '" << path << "'"; - } - } - - rootFolder->sort(getSortTypeFromString(sysDecl.defaultSort)); - updateCollectionFolderMetadata(newSys); -} - -/* Handle System View removal and insertion of Collections */ -void CollectionSystemManager::removeCollectionsFromDisplayedSystems() -{ - // remove all Collection Systems - for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); ) - { - if ((*sysIt)->isCollection()) - { - sysIt = SystemData::sSystemVector.erase(sysIt); - } - else - { - sysIt++; - } - } - - // remove all custom collections in bundle - // this should not delete the objects from memory! - FolderData* customRoot = mCustomCollectionsBundle->getRootFolder(); - std::vector mChildren = customRoot->getChildren(); - for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - customRoot->removeChild(*it); - } - // clear index - mCustomCollectionsBundle->resetIndex(); - // remove view so it's re-created as needed - ViewController::get()->removeGameListView(mCustomCollectionsBundle); -} - -void CollectionSystemManager::addEnabledCollectionsToDisplayedSystems(std::map* colSystemData, std::unordered_map* pMap) -{ - // add auto enabled ones - for(std::map::iterator it = colSystemData->begin() ; it != colSystemData->end() ; it++ ) - { - if(it->second.isEnabled) - { - // check if populated, otherwise populate - if (!it->second.isPopulated) - { - if(it->second.decl.isCustom) - { - populateCustomCollection(&(it->second), pMap); - } - else - { - populateAutoCollection(&(it->second)); - } - } - // check if it has its own view - if(!it->second.decl.isCustom || themeFolderExists(it->first) || !Settings::getInstance()->getBool("UseCustomCollectionsSystem")) - { - // exists theme folder, or we chose not to bundle it under the custom-collections system - // so we need to create a view - SystemData::sSystemVector.push_back(it->second.system); - } - else - { - FileData* newSysRootFolder = it->second.system->getRootFolder(); - mCustomCollectionsBundle->getRootFolder()->addChild(newSysRootFolder); - mCustomCollectionsBundle->getIndex(true)->importIndex(it->second.system->getIndex(true)); - } - } - } -} - -/* Auxiliary methods to get available custom collection possibilities */ -std::vector CollectionSystemManager::getSystemsFromConfig() -{ - std::vector systems; - std::string path = SystemData::getConfigPath(false); - - if(!Utils::FileSystem::exists(path)) - { - return systems; - } - - pugi::xml_document doc; - pugi::xml_parse_result res = doc.load_file(path.c_str()); - - if(!res) - { - return systems; - } - - //actually read the file - pugi::xml_node systemList = doc.child("systemList"); - - if(!systemList) - { - return systems; - } - - for(pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) - { - // theme folder - std::string themeFolder = system.child("theme").text().get(); - systems.push_back(themeFolder); - } - std::sort(systems.begin(), systems.end()); - return systems; -} - -// gets all folders from the current theme path -std::vector CollectionSystemManager::getSystemsFromTheme() -{ - std::vector systems; - - auto themeSets = ThemeData::getThemeSets(); - if(themeSets.empty()) - { - // no theme sets available - return systems; - } - - std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); - if(set == themeSets.cend()) - { - // currently selected theme set is missing, so just pick the first available set - set = themeSets.cbegin(); - Settings::getInstance()->setString("ThemeSet", set->first); - } - - std::string themePath = set->second.path; - - if (Utils::FileSystem::exists(themePath)) - { - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(themePath); - - for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if (Utils::FileSystem::isDirectory(*it)) - { - //... here you have a directory - std::string folder = *it; - folder = folder.substr(themePath.size()+1); - - if(Utils::FileSystem::exists(set->second.getThemePath(folder))) - { - systems.push_back(folder); - } - } - } - } - std::sort(systems.begin(), systems.end()); - return systems; -} - -// returns the unused folders from current theme path -std::vector CollectionSystemManager::getUnusedSystemsFromTheme() -{ - // get used systems in es_systems.cfg - std::vector systemsInUse = getSystemsFromConfig(); - // get available folders in theme - std::vector themeSys = getSystemsFromTheme(); - // get folders assigned to custom collections - std::vector autoSys = getCollectionThemeFolders(false); - // get folder assigned to custom collections - std::vector customSys = getCollectionThemeFolders(true); - // get folders assigned to user collections - std::vector userSys = getUserCollectionThemeFolders(); - // add them all to the list of systems in use - systemsInUse.insert(systemsInUse.cend(), autoSys.cbegin(), autoSys.cend()); - systemsInUse.insert(systemsInUse.cend(), customSys.cbegin(), customSys.cend()); - systemsInUse.insert(systemsInUse.cend(), userSys.cbegin(), userSys.cend()); - - for(auto sysIt = themeSys.cbegin(); sysIt != themeSys.cend(); ) - { - if (std::find(systemsInUse.cbegin(), systemsInUse.cend(), *sysIt) != systemsInUse.cend()) - { - sysIt = themeSys.erase(sysIt); - } - else - { - sysIt++; - } - } - return themeSys; -} - -// returns which collection config files exist in the user folder -std::vector CollectionSystemManager::getCollectionsFromConfigFolder() -{ - std::vector systems; - std::string configPath = getCollectionsFolder(); - - if (Utils::FileSystem::exists(configPath)) - { - Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(configPath); - for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) - { - if (Utils::FileSystem::isRegularFile(*it)) - { - // it's a file - std::string filename = Utils::FileSystem::getFileName(*it); - - // need to confirm filename matches config format - if (filename != "custom-.cfg" && Utils::String::startsWith(filename, "custom-") && Utils::String::endsWith(filename, ".cfg")) - { - filename = filename.substr(7, filename.size()-11); - systems.push_back(filename); - } - else - { - LOG(LogInfo) << "Found non-collection config file in collections folder: " << filename; - } - } - } - } - return systems; -} - -// returns the theme folders for Automatic Collections (All, Favorites, Last Played) or generic Custom Collections folder -std::vector CollectionSystemManager::getCollectionThemeFolders(bool custom) -{ - std::vector systems; - for(std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin() ; it != mCollectionSystemDeclsIndex.cend() ; it++ ) - { - CollectionSystemDecl sysDecl = it->second; - if (sysDecl.isCustom == custom) - { - systems.push_back(sysDecl.themeFolder); - } - } - return systems; -} - -// returns the theme folders in use for the user-defined Custom Collections -std::vector CollectionSystemManager::getUserCollectionThemeFolders() -{ - std::vector systems; - for(std::map::const_iterator it = mCustomCollectionSystemsData.cbegin() ; it != mCustomCollectionSystemsData.cend() ; it++ ) - { - systems.push_back(it->second.decl.themeFolder); - } - return systems; -} - -// returns whether a specific folder exists in the theme -bool CollectionSystemManager::themeFolderExists(std::string folder) -{ - std::vector themeSys = getSystemsFromTheme(); - return std::find(themeSys.cbegin(), themeSys.cend(), folder) != themeSys.cend(); -} - -bool CollectionSystemManager::includeFileInAutoCollections(FileData* file) -{ - // we exclude non-game files from collections (i.e. "kodi", entries from non-game systems) - // if/when there are more in the future, maybe this can be a more complex method, with a proper list - // but for now a simple string comparison is more performant - return file->getName() != "kodi" && file->getSystem()->isGameSystem(); -} - -std::string getCustomCollectionConfigPath(std::string collectionName) -{ - return getCollectionsFolder() + "/custom-" + collectionName + ".cfg"; -} - -std::string getCollectionsFolder() -{ - return Utils::FileSystem::getGenericPath(Utils::FileSystem::getHomePath() + "/.emulationstation/collections"); -} - -bool systemSort(SystemData* sys1, SystemData* sys2) -{ - std::string name1 = Utils::String::toUpper(sys1->getName()); - std::string name2 = Utils::String::toUpper(sys2->getName()); - return name1.compare(name2) < 0; -} + { + CollectionFileData* newGame = new CollectionFileData(it->second, newSys); + rootFolder->addChild(newGame); + newSys->addToIndex(newGame); + } + else + { + LOG(LogInfo) << "Couldn't find game referenced at '" << gameKey << "' for system config '" << path << "'"; + } + } + updateCollectionFolderMetadata(newSys); +} + +/* Handle System View removal and insertion of Collections */ +void CollectionSystemManager::removeCollectionsFromDisplayedSystems() +{ + // remove all Collection Systems + for(auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); ) + { + if ((*sysIt)->isCollection()) + { + sysIt = SystemData::sSystemVector.erase(sysIt); + } + else + { + sysIt++; + } + } + + // remove all custom collections in bundle + // this should not delete the objects from memory! + FolderData* customRoot = mCustomCollectionsBundle->getRootFolder(); + std::vector mChildren = customRoot->getChildren(); + for(auto it = mChildren.cbegin(); it != mChildren.cend(); it++) + { + customRoot->removeChild(*it); + } + // clear index + mCustomCollectionsBundle->resetIndex(); + // remove view so it's re-created as needed + ViewController::get()->removeGameListView(mCustomCollectionsBundle); +} + +void CollectionSystemManager::addEnabledCollectionsToDisplayedSystems(std::map* colSystemData, std::unordered_map* pMap) +{ + // add auto enabled ones + for(std::map::iterator it = colSystemData->begin() ; it != colSystemData->end() ; it++ ) + { + if(it->second.isEnabled) + { + // check if populated, otherwise populate + if (!it->second.isPopulated) + { + if(it->second.decl.isCustom) + { + populateCustomCollection(&(it->second), pMap); + } + else + { + populateAutoCollection(&(it->second)); + } + } + // check if it has its own view + if(!it->second.decl.isCustom || themeFolderExists(it->first) || !Settings::getInstance()->getBool("UseCustomCollectionsSystem")) + { + // exists theme folder, or we chose not to bundle it under the custom-collections system + // so we need to create a view + SystemData::sSystemVector.push_back(it->second.system); + } + else + { + FileData* newSysRootFolder = it->second.system->getRootFolder(); + mCustomCollectionsBundle->getRootFolder()->addChild(newSysRootFolder); + mCustomCollectionsBundle->getIndex(true)->importIndex(it->second.system->getIndex(true)); + } + } + } +} + +/* Auxiliary methods to get available custom collection possibilities */ +std::vector CollectionSystemManager::getSystemsFromConfig() +{ + std::vector systems; + std::string path = SystemData::getConfigPath(false); + + if(!Utils::FileSystem::exists(path)) + { + return systems; + } + + pugi::xml_document doc; + pugi::xml_parse_result res = doc.load_file(path.c_str()); + + if(!res) + { + return systems; + } + + //actually read the file + pugi::xml_node systemList = doc.child("systemList"); + + if(!systemList) + { + return systems; + } + + for(pugi::xml_node system = systemList.child("system"); system; system = system.next_sibling("system")) + { + // theme folder + std::string themeFolder = system.child("theme").text().get(); + systems.push_back(themeFolder); + } + std::sort(systems.begin(), systems.end()); + return systems; +} + +// gets all folders from the current theme path +std::vector CollectionSystemManager::getSystemsFromTheme() +{ + std::vector systems; + + auto themeSets = ThemeData::getThemeSets(); + if(themeSets.empty()) + { + // no theme sets available + return systems; + } + + std::map::const_iterator set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if(set == themeSets.cend()) + { + // currently selected theme set is missing, so just pick the first available set + set = themeSets.cbegin(); + Settings::getInstance()->setString("ThemeSet", set->first); + } + + std::string themePath = set->second.path; + + if (Utils::FileSystem::exists(themePath)) + { + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(themePath); + + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if (Utils::FileSystem::isDirectory(*it)) + { + //... here you have a directory + std::string folder = *it; + folder = folder.substr(themePath.size()+1); + + if(Utils::FileSystem::exists(set->second.getThemePath(folder))) + { + systems.push_back(folder); + } + } + } + } + std::sort(systems.begin(), systems.end()); + return systems; +} + +// returns the unused folders from current theme path +std::vector CollectionSystemManager::getUnusedSystemsFromTheme() +{ + // get used systems in es_systems.cfg + std::vector systemsInUse = getSystemsFromConfig(); + // get available folders in theme + std::vector themeSys = getSystemsFromTheme(); + // get folders assigned to custom collections + std::vector autoSys = getCollectionThemeFolders(false); + // get folder assigned to custom collections + std::vector customSys = getCollectionThemeFolders(true); + // get folders assigned to user collections + std::vector userSys = getUserCollectionThemeFolders(); + // add them all to the list of systems in use + systemsInUse.insert(systemsInUse.cend(), autoSys.cbegin(), autoSys.cend()); + systemsInUse.insert(systemsInUse.cend(), customSys.cbegin(), customSys.cend()); + systemsInUse.insert(systemsInUse.cend(), userSys.cbegin(), userSys.cend()); + + for(auto sysIt = themeSys.cbegin(); sysIt != themeSys.cend(); ) + { + if (std::find(systemsInUse.cbegin(), systemsInUse.cend(), *sysIt) != systemsInUse.cend()) + { + sysIt = themeSys.erase(sysIt); + } + else + { + sysIt++; + } + } + return themeSys; +} + +// returns which collection config files exist in the user folder +std::vector CollectionSystemManager::getCollectionsFromConfigFolder() +{ + std::vector systems; + std::string configPath = getCollectionsFolder(); + + if (Utils::FileSystem::exists(configPath)) + { + Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(configPath); + for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin(); it != dirContent.cend(); ++it) + { + if (Utils::FileSystem::isRegularFile(*it)) + { + // it's a file + std::string filename = Utils::FileSystem::getFileName(*it); + + // need to confirm filename matches config format + if (filename != "custom-.cfg" && Utils::String::startsWith(filename, "custom-") && Utils::String::endsWith(filename, ".cfg")) + { + filename = filename.substr(7, filename.size()-11); + systems.push_back(filename); + } + else + { + LOG(LogInfo) << "Found non-collection config file in collections folder: " << filename; + } + } + } + } + return systems; +} + +// returns the theme folders for Automatic Collections (All, Favorites, Last Played) or generic Custom Collections folder +std::vector CollectionSystemManager::getCollectionThemeFolders(bool custom) +{ + std::vector systems; + for(std::map::const_iterator it = mCollectionSystemDeclsIndex.cbegin() ; it != mCollectionSystemDeclsIndex.cend() ; it++ ) + { + CollectionSystemDecl sysDecl = it->second; + if (sysDecl.isCustom == custom) + { + systems.push_back(sysDecl.themeFolder); + } + } + return systems; +} + +// returns the theme folders in use for the user-defined Custom Collections +std::vector CollectionSystemManager::getUserCollectionThemeFolders() +{ + std::vector systems; + for(std::map::const_iterator it = mCustomCollectionSystemsData.cbegin() ; it != mCustomCollectionSystemsData.cend() ; it++ ) + { + systems.push_back(it->second.decl.themeFolder); + } + return systems; +} + +// returns whether a specific folder exists in the theme +bool CollectionSystemManager::themeFolderExists(std::string folder) +{ + std::vector themeSys = getSystemsFromTheme(); + return std::find(themeSys.cbegin(), themeSys.cend(), folder) != themeSys.cend(); +} + +bool CollectionSystemManager::includeFileInAutoCollections(FileData* file) +{ + // we exclude non-game files from collections (i.e. "kodi", entries from non-game systems) + // if/when there are more in the future, maybe this can be a more complex method, with a proper list + // but for now a simple string comparison is more performant + return file->getName() != "kodi" && file->getSystem()->isGameSystem(); +} + +std::string getCustomCollectionConfigPath(std::string collectionName) +{ + return getCollectionsFolder() + "/custom-" + collectionName + ".cfg"; +} + +std::string getCollectionsFolder() +{ + return Utils::FileSystem::getGenericPath(Utils::FileSystem::getHomePath() + "/.emulationstation/collections"); +} + +bool systemSort(SystemData* sys1, SystemData* sys2) +{ + std::string name1 = Utils::String::toUpper(sys1->getName()); + std::string name2 = Utils::String::toUpper(sys2->getName()); + return name1.compare(name2) < 0; +} diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 85850a7430..f9c77900ac 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -80,6 +80,8 @@ class FileData MetaDataList metadata; + static void resetSettings(); + protected: FolderData* mParent; std::string mPath; @@ -124,19 +126,6 @@ class FolderData : public FileData mChildren.clear(); } - 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); FileData* FindByPath(const std::string& path); diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index 012ecb63e2..c22a68db11 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -41,9 +41,28 @@ namespace FileSorts //returns if file1 should come before file2 bool compareName(const FileData* file1, const FileData* file2) { - std::string name1 = Utils::String::toUpper(file1->metadata.getName()); - std::string name2 = Utils::String::toUpper(file2->metadata.getName()); - return name1.compare(name2) < 0; + if (file1->getType() != file2->getType()) + return file1->getType() == FOLDER; + + // we compare the actual metadata name, as collection files have the system appended which messes up the order + std::string name1 = ((FileData*)file1)->getName(); + std::string name2 = ((FileData*)file2)->getName(); + + for (auto ap = name1.c_str(), bp = name2.c_str(); ; ap++, bp++) + { + if (*ap == 0 & *bp != 0) + return true; + + if (*ap == 0 || *bp == 0) + return false; + + auto c1 = toupper(*ap); + auto c2 = toupper(*bp); + if (c1 != c2) + return c1 < c2; + } + + return false; } bool compareRating(const FileData* file1, const FileData* file2) diff --git a/es-app/src/FileSorts.h b/es-app/src/FileSorts.h index 9851cbd534..ca12f93542 100644 --- a/es-app/src/FileSorts.h +++ b/es-app/src/FileSorts.h @@ -7,6 +7,32 @@ namespace FileSorts { + typedef bool ComparisonFunction(const FileData* a, const FileData* b); + + struct SortType + { + int id; + ComparisonFunction* comparisonFunction; + bool ascending; + std::string description; + std::string icon; + + SortType(int sortId, ComparisonFunction* sortFunction, bool sortAscending, const std::string & sortDescription, const std::string & iconId = "") + : id(sortId), comparisonFunction(sortFunction), ascending(sortAscending), description(sortDescription), icon(iconId) {} + }; + + class Singleton + { + public: + Singleton(); + + std::vector mSortTypes; + }; + + void reset(); + SortType getSortType(int sortId); + const std::vector& getSortTypes(); + bool compareName(const FileData* file1, const FileData* file2); bool compareRating(const FileData* file1, const FileData* file2); bool compareTimesPlayed(const FileData* file1, const FileData* fil2); diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 963f2f324f..1e0edceacd 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -48,11 +48,11 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys if (!Settings::getInstance()->getBool("IgnoreGamelist") && mName != "imageviewer") parseGamelist(this, fileMap); - - if (mSortId >= 0 && mSortId < FileSorts::SortTypes.size()) - mRootFolder->sort(FileSorts::SortTypes.at(mSortId)); + /* + if (mSortId >= 0 && mSortId < FileSorts::getSortTypes().size()) + mRootFolder->sort(FileSorts::getSortTypes().at(mSortId)); else - mRootFolder->sort(FileSorts::SortTypes.at(0)); + mRootFolder->sort(FileSorts::getSortTypes().at(0));*/ } else { diff --git a/es-app/src/guis/GuiFastSelect.cpp b/es-app/src/guis/GuiFastSelect.cpp index 13bdb962ab..a781e8e4a0 100644 --- a/es-app/src/guis/GuiFastSelect.cpp +++ b/es-app/src/guis/GuiFastSelect.cpp @@ -131,12 +131,7 @@ void GuiFastSelect::updateSortText() void GuiFastSelect::updateGameListSort() { - const FolderData::SortType& sort = FileSorts::SortTypes.at(mSortId); - FolderData* 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); } @@ -145,7 +140,7 @@ void GuiFastSelect::updateGameListCursor() const std::vector& list = mGameList->getCursor()->getParent()->getChildren(); // only skip by letter when the sort mode is alphabetical - const FolderData::SortType& sort = FileSorts::SortTypes.at(mSortId); + const FileSorts::SortType& sort = FileSorts::getSortTypes().at(mSortId); if(sort.comparisonFunction != &FileSorts::compareName) return; diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 544023cee6..f6b9279916 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -133,8 +133,12 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool mListSort = std::make_shared(mWindow, _("SORT GAMES BY"), false); for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) { - const FolderData::SortType& sort = FileSorts::SortTypes.at(i); - mListSort->add(_(Utils::String::toUpper(sort.description)), i, i == currentSortId); + const FileSorts::SortType& sort = FileSorts::getSortTypes().at(i); + mListSort->add(sort.icon + sort.description, sort.id, sort.id == currentSortId); // TODO - actually make the sort type persistent + } + + mMenu.addWithLabel(_("SORT GAMES BY"), mListSort); // batocera + } mMenu.addWithLabel(_("SORT GAMES BY"), mListSort); diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index abd529a718..d4ec03d85d 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -829,6 +829,20 @@ void GuiMenu::openUISettings() s->setVariable("reloadAll", true); }); + // filenames + auto hidden_files = std::make_shared(mWindow); + hidden_files->setState(Settings::getInstance()->getBool("ShowFilenames")); + s->addWithLabel(_("SHOW FILENAMES IN LISTS"), hidden_files); + s->addSaveFunc([hidden_files, s] + { + if (Settings::getInstance()->setBool("ShowFilenames", hidden_files->getState())) + { + FileData::resetSettings(); + s->setVariable("reloadCollections", true); + s->setVariable("reloadAll", true); + } + }); + // enable filters (ForceDisableFilters) auto enable_filter = std::make_shared(mWindow); enable_filter->setState(!Settings::getInstance()->getBool("ForceDisableFilters")); diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 755f269855..a191b0f01a 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -73,6 +73,12 @@ void BasicGameListView::populateList(const std::vector& files) if (files.size() > 0) { + if (mCursorStack.size()) + { + FileData* placeholder = new FileData(PLACEHOLDER, "..", this->mRoot->getSystem()); + mList.add(". .", placeholder, (placeholder->getType() == PLACEHOLDER)); + } + if (favoritesFirst) { for (auto it = files.cbegin(); it != files.cend(); it++) @@ -82,6 +88,8 @@ void BasicGameListView::populateList(const std::vector& files) if (showFavoriteIcon) mList.add(_U("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + else if (((*it)->getType() == FOLDER)) + mList.add(_U("\uF114 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); else mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); } @@ -100,8 +108,11 @@ void BasicGameListView::populateList(const std::vector& files) continue; } } - - mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + + if (((*it)->getType() == FOLDER)) + mList.add(_U("\uF114 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + else + mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); } } else diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 2b67d592a3..2b4b8a2495 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -208,6 +208,12 @@ void GridGameListView::populateList(const std::vector& files) if (files.size() > 0) { + if (mCursorStack.size()) + { + FileData* placeholder = new FileData(PLACEHOLDER, "..", this->mRoot->getSystem()); + mGrid.add(". .", "", "", "", placeholder); + } + std::string systemName = mRoot->getSystem()->getFullName(); bool favoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 0cb45086c6..7df5ce90e4 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -87,35 +87,47 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) FileData* cursor = getCursor(); FolderData* folder = NULL; - if (cursor->getType() == FOLDER) - folder = (FolderData*)cursor; - - if (cursor->getType() == GAME) + if (mCursorStack.size() && cursor->getType() == PLACEHOLDER && cursor->getPath() == "..") { - Sound::getFromTheme(getTheme(), getName(), "launch")->play(); - launch(cursor); + auto top = mCursorStack.top(); + mCursorStack.pop(); + + populateList(top->getParent()->getChildrenListToDisplay()); + setCursor(top); + Sound::getFromTheme(getTheme(), getName(), "back")->play(); } else { - // it's a folder ? ( or PLACEHOLDER... ) - if(folder != nullptr && folder->getChildren().size() > 0) + if (cursor->getType() == FOLDER) + folder = (FolderData*)cursor; + + if (cursor->getType() == GAME) { - mCursorStack.push(cursor); - populateList(folder->getChildrenListToDisplay()); - FileData* cursor = getCursor(); - setCursor(cursor); + Sound::getFromTheme(getTheme(), getName(), "launch")->play(); + launch(cursor); + } + else { + // it's a folder + if (folder != nullptr && folder->getChildren().size() > 0) + { + mCursorStack.push(cursor); + populateList(folder->getChildrenListToDisplay()); + FileData* cursor = getCursor(); + setCursor(cursor); + } } } - return true; } else if(config->isMappedTo("b", input)) { if (mCursorStack.size()) { - populateList(mCursorStack.top()->getParent()->getChildren()); - setCursor(mCursorStack.top()); + auto top = mCursorStack.top(); mCursorStack.pop(); + + populateList(top->getParent()->getChildrenListToDisplay()); + setCursor(top); Sound::getFromTheme(getTheme(), getName(), "back")->play(); } else if (!hideSystemView) diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index e5593b8830..ed6dbba551 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -25,6 +25,7 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mAnimPosition = Vector3f(0, 0); mVideo = nullptr; mMarquee = nullptr; + mIsDefaultImage = false; mLabelVisible = false; mLabelMerged = false; @@ -120,6 +121,7 @@ void GridTileComponent::resize() float height = (int)(size.y() * currentProperties.mLabelSize.y()); float labelHeight = height; + mLabel.setVisible(mLabelVisible || mIsDefaultImage); if (mLabelVisible) { @@ -145,7 +147,7 @@ void GridTileComponent::resize() } } } - + if (!mLabelVisible || mLabelMerged || currentProperties.mLabelSize.x() == 0) height = 0; @@ -261,6 +263,17 @@ void GridTileComponent::resize() bkposition = Vector3f(x - mPosition.x(), y - mPosition.y(), 0); } + if (!mLabelVisible && mIsDefaultImage) + { + mLabel.setColor(0xFFFFFFFF); + mLabel.setGlowColor(0x00000010); + mLabel.setGlowSize(2); + mLabel.setOpacity(255); + mLabel.setPosition(mSize.x() * 0.1, mSize.y() * 0.2); + mLabel.setSize(mSize.x() - mSize.x() * 0.2, mSize.y() - mSize.y() * 0.3); + } + + mBackground.setPosition(bkposition); mBackground.setSize(bkSize); mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); @@ -330,6 +343,8 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) mMarquee->render(trans); else if (mLabelVisible && currentProperties.mLabelSize.y()>0) mLabel.render(trans); + else if (!mLabelVisible && mIsDefaultImage) + mLabel.render(trans); if (mLabelMerged && currentProperties.mImageSizeMode == "minSize") Renderer::popClipRect(); @@ -621,8 +636,9 @@ bool GridTileComponent::isSelected() const return mSelected; } -void GridTileComponent::setImage(const std::string& path) +void GridTileComponent::setImage(const std::string& path, bool isDefaultImage) { + mIsDefaultImage = isDefaultImage; if (mCurrentPath == path) return; diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index f4aecda525..edc5594fba 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -61,7 +61,7 @@ class GridTileComponent : public GuiComponent void setLabel(std::string name); void setVideo(const std::string& path, float defaultDelay = -1.0); - void setImage(const std::string& path); + void setImage(const std::string& path, bool isDefaultImage = false); void setMarquee(const std::string& path); void setSelected(bool selected, bool allowAnimation = true, Vector3f* pPosition = NULL, bool force = false); @@ -114,6 +114,8 @@ class GridTileComponent : public GuiComponent bool mSelected; bool mVisible; + bool mIsDefaultImage; + Vector3f mAnimPosition; VideoComponent* mVideo; diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 39c39112a8..1e654caad9 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -800,10 +800,10 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA if (ResourceManager::getInstance()->fileExists(imagePath)) tile->setImage(imagePath); - else if (mEntries.at(imgPos).object->getType() == 2) - tile->setImage(mDefaultFolderTexture); + else if (mEntries.at(imgPos).object->getType() == 2 || (mEntries.at(imgPos).object->getType() == PLACEHOLDER && mEntries.at(imgPos).object->getPath() == "..")) + tile->setImage(mDefaultFolderTexture, true); else - tile->setImage(mDefaultGameTexture); + tile->setImage(mDefaultGameTexture, true); // Marquee std::string marqueePath = mEntries.at(imgPos).data.marqueePath; From a21aa4c9b43a96d27c121e06f93657a5987fd20e Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 30 Oct 2019 19:19:58 +0100 Subject: [PATCH 204/336] Folder navigation & better sortings --- es-app/src/FileData.cpp | 73 ++++++++++------------- es-app/src/FileData.h | 2 - es-app/src/FileSorts.cpp | 80 ++++++++++++++++---------- es-app/src/FileSorts.h | 29 ++++++++-- es-app/src/guis/GuiFastSelect.cpp | 6 +- es-app/src/guis/GuiGamelistOptions.cpp | 12 +--- es-core/src/Settings.cpp | 4 +- 7 files changed, 115 insertions(+), 91 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index dc588d20d9..9ec9cad7d2 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -115,8 +115,27 @@ const bool FileData::getKidGame() return metadata.get("kidgame") != "false"; } +static std::shared_ptr showFilenames; + +void FileData::resetSettings() +{ + showFilenames = nullptr; +} + const std::string FileData::getName() { + if (showFilenames == nullptr) + showFilenames = std::make_shared(Settings::getInstance()->getBool("ShowFilenames")); + + // Faster than accessing map each time + if (*showFilenames) + { + if (mSystem != nullptr && !mSystem->hasPlatformId(PlatformIds::ARCADE) && !mSystem->hasPlatformId(PlatformIds::NEOGEO)) + return Utils::FileSystem::getStem(getPath()); + else + return getDisplayName(); + } + return metadata.getName(); } @@ -345,22 +364,6 @@ const std::string CollectionFileData::getName() return Utils::String::removeParenthesis(mSourceFileData->metadata.get("name")); } -// returns Sort Type based on a string description -FolderData::SortType getSortTypeFromString(std::string desc) { - std::vector SortTypes = FileSorts::SortTypes; - // find it - for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) - { - const FolderData::SortType& sort = FileSorts::SortTypes.at(i); - if(sort.description == desc) - { - return sort; - } - } - // if not found default to name, ascending - return FileSorts::SortTypes.at(0); -} - const std::vector FolderData::getChildrenListToDisplay() { std::vector ret; @@ -431,6 +434,16 @@ const std::vector FolderData::getChildrenListToDisplay() ret.push_back(*it); } + unsigned int currentSortId = sys->getSortId(); + if (currentSortId > FileSorts::getSortTypes().size()) + currentSortId = 0; + + const FileSorts::SortType& sort = FileSorts::getSortTypes().at(currentSortId); + std::sort(ret.begin(), ret.end(), sort.comparisonFunction); + + if (!sort.ascending) + std::reverse(ret.begin(), ret.end()); + return ret; } @@ -460,10 +473,10 @@ std::vector FolderData::getFlatGameList(bool displayedOnly, SystemDat std::vector ret = getFilesRecursive(GAME, displayedOnly, system); unsigned int currentSortId = system->getSortId(); - if (currentSortId < 0 || currentSortId >FileSorts::SortTypes.size()) + if (currentSortId < 0 || currentSortId >FileSorts::getSortTypes().size()) currentSortId = 0; - auto sort = FileSorts::SortTypes.at(currentSortId); + auto sort = FileSorts::getSortTypes().at(currentSortId); std::stable_sort(ret.begin(), ret.end(), sort.comparisonFunction); @@ -532,30 +545,6 @@ void FolderData::removeChild(FileData* file) } -void FolderData::sort(ComparisonFunction& comparator, bool ascending) -{ - std::stable_sort(mChildren.begin(), mChildren.end(), comparator); - - for (auto it = mChildren.cbegin(); it != mChildren.cend(); it++) - { - if ((*it)->getType() != FOLDER) - continue; - - FolderData* folder = (FolderData*)(*it); - - if (folder->getChildren().size() > 0) - folder->sort(comparator, ascending); - } - - if (!ascending) - std::reverse(mChildren.begin(), mChildren.end()); -} - -void FolderData::sort(const SortType& type) -{ - sort(*type.comparisonFunction, type.ascending); -} - FileData* FolderData::FindByPath(const std::string& path) { std::vector children = getChildren(); diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index f9c77900ac..ce33e8341d 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -146,6 +146,4 @@ class FolderData : public FileData std::vector mChildren; }; -FolderData::SortType getSortTypeFromString(std::string desc); - #endif // ES_APP_FILE_DATA_H diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index c22a68db11..36a618c8d8 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -1,42 +1,64 @@ #include "FileSorts.h" - #include "utils/StringUtil.h" +#include "EsLocale.h" namespace FileSorts { - const FolderData::SortType typesArr[] = { - FolderData::SortType(&compareName, true, "filename, ascending"), - FolderData::SortType(&compareName, false, "filename, descending"), - - FolderData::SortType(&compareRating, true, "rating, ascending"), - FolderData::SortType(&compareRating, false, "rating, descending"), + static Singleton* sInstance = nullptr; - FolderData::SortType(&compareTimesPlayed, true, "times played, ascending"), - FolderData::SortType(&compareTimesPlayed, false, "times played, descending"), - - FolderData::SortType(&compareLastPlayed, true, "last played, ascending"), - FolderData::SortType(&compareLastPlayed, false, "last played, descending"), + Singleton* getInstance() + { + if (sInstance == nullptr) + sInstance = new Singleton(); - FolderData::SortType(&compareNumPlayers, true, "number of players, ascending"), - FolderData::SortType(&compareNumPlayers, false, "number of players, descending"), + return sInstance; + } - FolderData::SortType(&compareReleaseDate, true, "release date, ascending"), - FolderData::SortType(&compareReleaseDate, false, "release date, descending"), + void reset() + { + if (sInstance != nullptr) + delete sInstance; - FolderData::SortType(&compareGenre, true, "genre, ascending"), - FolderData::SortType(&compareGenre, false, "genre, descending"), + sInstance = nullptr; + } - FolderData::SortType(&compareDeveloper, true, "developer, ascending"), - FolderData::SortType(&compareDeveloper, false, "developer, descending"), + const std::vector& getSortTypes() + { + return getInstance()->mSortTypes; + } - FolderData::SortType(&comparePublisher, true, "publisher, ascending"), - FolderData::SortType(&comparePublisher, false, "publisher, descending"), + SortType getSortType(int sortId) + { + for (auto sort : getSortTypes()) + if (sort.id == sortId) + return sort; - FolderData::SortType(&compareSystem, true, "system, ascending"), - FolderData::SortType(&compareSystem, false, "system, descending") - }; + return getSortTypes().at(0); + } - const std::vector SortTypes(typesArr, typesArr + sizeof(typesArr)/sizeof(typesArr[0])); + Singleton::Singleton() + { + mSortTypes.push_back(SortType(FILENAME_ASCENDING, &compareName, true, _("FILENAME, ASCENDING"), _U("\uF15d "))); + mSortTypes.push_back(SortType(FILENAME_DESCENDING, &compareName, false, _("FILENAME, DESCENDING"), _U("\uF15e "))); + mSortTypes.push_back(SortType(RATING_ASCENDING, &compareRating, true, _("RATING, ASCENDING"), _U("\uF165 "))); + mSortTypes.push_back(SortType(RATING_DESCENDING, &compareRating, false, _("RATING, DESCENDING"), _U("\uF164 "))); + mSortTypes.push_back(SortType(TIMESPLAYED_ASCENDING, &compareTimesPlayed, true, _("TIMES PLAYED, ASCENDING"), _U("\uF160 "))); + mSortTypes.push_back(SortType(TIMESPLAYED_DESCENDING, &compareTimesPlayed, false, _("TIMES PLAYED, DESCENDING"), _U("\uF161 "))); + mSortTypes.push_back(SortType(LASTPLAYED_ASCENDING, &compareLastPlayed, true, _("LAST PLAYED, ASCENDING"), _U("\uF160 "))); + mSortTypes.push_back(SortType(LASTPLAYED_DESCENDING, &compareLastPlayed, false, _("LAST PLAYED, DESCENDING"), _U("\uF161 "))); + mSortTypes.push_back(SortType(NUMBERPLAYERS_ASCENDING, &compareNumPlayers, true, _("NUMBER PLAYERS, ASCENDING"), _U("\uF162 "))); + mSortTypes.push_back(SortType(NUMBERPLAYERS_DESCENDING, &compareNumPlayers, false, _("NUMBER PLAYERS, DESCENDING"), _U("\uF163 "))); + mSortTypes.push_back(SortType(RELEASEDATE_ASCENDING, &compareReleaseDate, true, _("RELEASE DATE, ASCENDING"), _U("\uF160 "))); + mSortTypes.push_back(SortType(RELEASEDATE_DESCENDING, &compareReleaseDate, false, _("RELEASE DATE, DESCENDING"), _U("\uF161 "))); + mSortTypes.push_back(SortType(GENRE_ASCENDING, &compareGenre, true, _("GENRE, ASCENDING"), _U("\uF15d "))); + mSortTypes.push_back(SortType(GENRE_DESCENDING, &compareGenre, false, _("GENRE, DESCENDING"), _U("\uF15e "))); + mSortTypes.push_back(SortType(DEVELOPER_ASCENDING, &compareDeveloper, true, _("DEVELOPER, ASCENDING"), _U("\uF15d "))); + mSortTypes.push_back(SortType(DEVELOPER_DESCENDING, &compareDeveloper, false, _("DEVELOPER, DESCENDING"), _U("\uF15e "))); + mSortTypes.push_back(SortType(PUBLISHER_ASCENDING, &comparePublisher, true, _("PUBLISHER, ASCENDING"), _U("\uF15d "))); + mSortTypes.push_back(SortType(PUBLISHER_DESCENDING, &comparePublisher, false, _("PUBLISHER, DESCENDING"), _U("\uF15e "))); + mSortTypes.push_back(SortType(SYSTEM_ASCENDING, &compareSystem, true, _("SYSTEM, ASCENDING"), _U("\uF15d "))); + mSortTypes.push_back(SortType(SYSTEM_DESCENDING, &compareSystem, false, _("SYSTEM, DESCENDING"), _U("\uF15e "))); + } //returns if file1 should come before file2 bool compareName(const FileData* file1, const FileData* file2) @@ -49,7 +71,7 @@ namespace FileSorts std::string name2 = ((FileData*)file2)->getName(); for (auto ap = name1.c_str(), bp = name2.c_str(); ; ap++, bp++) - { + { if (*ap == 0 & *bp != 0) return true; @@ -59,7 +81,7 @@ namespace FileSorts auto c1 = toupper(*ap); auto c2 = toupper(*bp); if (c1 != c2) - return c1 < c2; + return c1 < c2; } return false; @@ -73,7 +95,7 @@ namespace FileSorts bool compareTimesPlayed(const FileData* file1, const FileData* file2) { //only games have playcount metadata - if(file1->metadata.getType() == GAME_METADATA && file2->metadata.getType() == GAME_METADATA) + if (file1->metadata.getType() == GAME_METADATA && file2->metadata.getType() == GAME_METADATA) { return (file1)->metadata.getInt("playcount") < (file2)->metadata.getInt("playcount"); } diff --git a/es-app/src/FileSorts.h b/es-app/src/FileSorts.h index ca12f93542..046c622fce 100644 --- a/es-app/src/FileSorts.h +++ b/es-app/src/FileSorts.h @@ -7,6 +7,30 @@ namespace FileSorts { + enum SortId : unsigned int + { + FILENAME_ASCENDING = 0, + FILENAME_DESCENDING = 1, + RATING_ASCENDING = 2, + RATING_DESCENDING = 3, + TIMESPLAYED_ASCENDING = 4, + TIMESPLAYED_DESCENDING = 5, + LASTPLAYED_ASCENDING = 6, + LASTPLAYED_DESCENDING = 7, + NUMBERPLAYERS_ASCENDING = 8, + NUMBERPLAYERS_DESCENDING = 9, + RELEASEDATE_ASCENDING = 10, + RELEASEDATE_DESCENDING = 11, + GENRE_ASCENDING = 12, + GENRE_DESCENDING = 13, + DEVELOPER_ASCENDING = 14, + DEVELOPER_DESCENDING = 15, + PUBLISHER_ASCENDING = 16, + PUBLISHER_DESCENDING = 17, + SYSTEM_ASCENDING = 18, + SYSTEM_DESCENDING = 19 + }; + typedef bool ComparisonFunction(const FileData* a, const FileData* b); struct SortType @@ -25,7 +49,7 @@ namespace FileSorts { public: Singleton(); - + std::vector mSortTypes; }; @@ -43,8 +67,5 @@ namespace FileSorts bool compareDeveloper(const FileData* file1, const FileData* file2); bool comparePublisher(const FileData* file1, const FileData* file2); bool compareSystem(const FileData* file1, const FileData* file2); - - extern const std::vector SortTypes; }; - #endif // ES_APP_FILE_SORTS_H diff --git a/es-app/src/guis/GuiFastSelect.cpp b/es-app/src/guis/GuiFastSelect.cpp index a781e8e4a0..4b71eb5b99 100644 --- a/es-app/src/guis/GuiFastSelect.cpp +++ b/es-app/src/guis/GuiFastSelect.cpp @@ -73,14 +73,14 @@ bool GuiFastSelect::input(InputConfig* config, Input input) return true; }else if(config->isMappedLike("left", input) && input.value != 0) { - mSortId = (mSortId + 1) % FileSorts::SortTypes.size(); + mSortId = (mSortId + 1) % FileSorts::getSortTypes().size(); updateSortText(); return true; }else if(config->isMappedLike("right", input) && input.value != 0) { mSortId--; if(mSortId < 0) - mSortId += (int)FileSorts::SortTypes.size(); + mSortId += (int)FileSorts::getSortTypes().size(); updateSortText(); return true; @@ -125,7 +125,7 @@ void GuiFastSelect::scroll() void GuiFastSelect::updateSortText() { std::stringstream ss; - ss << "<- " << FileSorts::SortTypes.at(mSortId).description << " ->"; + ss << "<- " << FileSorts::getSortTypes().at(mSortId).description << " ->"; mSortText.setText(ss.str()); } diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index f6b9279916..5daaa5adab 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -126,21 +126,17 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool // sort list by unsigned int currentSortId = mSystem->getSortId(); - if (currentSortId > FileSorts::SortTypes.size()) { + if (currentSortId > FileSorts::getSortTypes().size()) { currentSortId = 0; } mListSort = std::make_shared(mWindow, _("SORT GAMES BY"), false); - for(unsigned int i = 0; i < FileSorts::SortTypes.size(); i++) + for(unsigned int i = 0; i < FileSorts::getSortTypes().size(); i++) { const FileSorts::SortType& sort = FileSorts::getSortTypes().at(i); mListSort->add(sort.icon + sort.description, sort.id, sort.id == currentSortId); // TODO - actually make the sort type persistent } - mMenu.addWithLabel(_("SORT GAMES BY"), mListSort); // batocera - - } - mMenu.addWithLabel(_("SORT GAMES BY"), mListSort); // GameList view style @@ -295,10 +291,6 @@ GuiGamelistOptions::~GuiGamelistOptions() mSystem->setSortId(mListSort->getSelected()); FolderData* root = mSystem->getRootFolder(); - const FolderData::SortType& sort = FileSorts::SortTypes.at(mListSort->getSelected()); - root->sort(sort); - - // notify that the root folder was sorted getGamelist()->onFileChanged(root, FILE_SORTED); } diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index a492f5cbcc..72f30bf454 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -161,7 +161,9 @@ void Settings::setDefaults() mStringMap["SlideshowScreenSaverImageDir"] = Utils::FileSystem::getHomePath() + "/.emulationstation/slideshow/image"; mStringMap["SlideshowScreenSaverImageFilter"] = ".png,.jpg"; mBoolMap["SlideshowScreenSaverRecurse"] = false; - mBoolMap["SlideshowScreenSaverGameName"] = true; + mBoolMap["SlideshowScreenSaverGameName"] = true; + + mBoolMap["ShowFilenames"] = false; // This setting only applies to raspberry pi but set it for all platforms so // we don't get a warning if we encounter it on a different platform From b992b7bb1851b3f5330849ac4c0b3ef559b6d26c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 1 Nov 2019 19:22:06 +0100 Subject: [PATCH 205/336] ScreenScraper : Added my own ApiKey + Management for Http error 246 BLACKLISTED --- es-app/src/EmulationStation.h | 8 ++++---- es-app/src/scrapers/Scraper.cpp | 8 +++++++- es-app/src/scrapers/ScreenScraper.h | 7 ++++--- es-app/src/scrapers/ThreadedScraper.cpp | 13 +++++++++++++ es-core/src/HttpReq.cpp | 11 +++++++++++ es-core/src/HttpReq.h | 1 + 6 files changed, 40 insertions(+), 8 deletions(-) diff --git a/es-app/src/EmulationStation.h b/es-app/src/EmulationStation.h index c131ea3f60..ccd907d3f6 100644 --- a/es-app/src/EmulationStation.h +++ b/es-app/src/EmulationStation.h @@ -7,14 +7,14 @@ #define PROGRAM_VERSION_MAJOR 2 #define PROGRAM_VERSION_MINOR 10 #define PROGRAM_VERSION_MAINTENANCE 2 -#define PROGRAM_VERSION_BUILD 1 +#define PROGRAM_VERSION_BUILD 2 -#define PROGRAM_VERSION_STRING "2.10.2.1 FCAMOD" -#define SCRAPER_VERSION_STRING "2.10.2.1" +#define PROGRAM_VERSION_STRING "2.10.2.2 FCAMOD" +#define SCRAPER_VERSION_STRING "2.10.2.2" #define PROGRAM_BUILT_STRING __DATE__ " - " __TIME__ -#define RESOURCE_VERSION_STRING "2,10,2,1\0" +#define RESOURCE_VERSION_STRING "2,10,2,2\0" #define RESOURCE_VERSION PROGRAM_VERSION_MAJOR,PROGRAM_VERSION_MINOR,PROGRAM_VERSION_MAINTENANCE,PROGRAM_VERSION_BUILD #endif // ES_APP_EMULATION_STATION_H diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index 3c1781136a..4a5d6b9d1a 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -147,10 +147,16 @@ void ScraperHttpRequest::update() return; } + if (status == HttpReq::REQ_426_BLACKLISTED) + { + setError("THE SOFTWARE HAS BEEN BLACKLISTED (426)"); + 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()); diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index 16f066472f..dcde1b2513 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -24,9 +24,10 @@ class ScreenScraperRequest : public ScraperHttpRequest std::string getGameSearchUrl(const std::string gameName, bool jeuRecherche=false) const; // Access to the API - const std::string API_DEV_U = { 91, 32, 7, 17 }; - const std::string API_DEV_P = { 108, 28, 54, 55, 83, 43, 91, 44, 30, 22, 41, 12, 0, 108, 38, 29 }; - const std::string API_DEV_KEY = { 54, 73, 115, 100, 101, 67, 111, 107, 79, 66, 68, 66, 67, 56, 118, 77, 54, 88, 101, 54 }; + const std::string API_DEV_U = { 60, 10, 23, 6, 22, 69, 25, 23, 80, 21 }; + const std::string API_DEV_P = { 104, 86, 49, 16, 32, 98, 49, 60, 47, 85, 87, 47, 81, 67, 10, 58 }; + const std::string API_DEV_KEY = { 80, 101, 97, 99, 101, 32, 97, 110, 100, 32, 98, 101, 32, 119, 105, 108, 100 }; + const std::string API_URL_BASE = "https://www.screenscraper.fr/api2"; const std::string API_SOFT_NAME = "Emulationstation " + static_cast(PROGRAM_VERSION_STRING); diff --git a/es-app/src/scrapers/ThreadedScraper.cpp b/es-app/src/scrapers/ThreadedScraper.cpp index a77c71998a..e59b2d96b0 100644 --- a/es-app/src/scrapers/ThreadedScraper.cpp +++ b/es-app/src/scrapers/ThreadedScraper.cpp @@ -3,6 +3,7 @@ #include "FileData.h" #include "components/AsyncNotificationComponent.h" #include "EsLocale.h" +#include "guis/GuiMsgBox.h" #define GUIICON _U("\uF03E ") @@ -80,7 +81,19 @@ void ThreadedScraper::run() } } else if (status == ASYNC_ERROR) + { + if (statusString.find("426") != std::string::npos) // Blacklist + { + mExit = true; + mWindow->postToUiThread([](Window* w) + { + w->pushGui(new GuiMsgBox(w, _("SCRAPE FAILED : THE APPLICATION HAS BEEN BLACKLISTED"))); + }); + break; + } + else mErrors.push_back(statusString); + } } if (mMDResolveHandle && mMDResolveHandle->status() != ASYNC_IN_PROGRESS) diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index b637ed0a0a..4e67fefdba 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -160,6 +160,15 @@ HttpReq::HttpReq(const std::string& url) return; } + // Set fake user agent + err = curl_easy_setopt(mHandle, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows NT x.y; Win64; x64; rv:10.0) Gecko/20100101 Firefox/10.0"); + if (err != CURLE_OK) + { + mStatus = REQ_IO_ERROR; + onError(curl_easy_strerror(err)); + return; + } + #ifdef WIN32 // Setup system proxy on Windows if required if (_regGetDWORD(HKEY_CURRENT_USER, "Software\\Microsoft\\Windows\\CurrentVersion\\Internet Settings", "ProxyEnable")) @@ -306,6 +315,8 @@ HttpReq::Status HttpReq::status() req->mStatus = REQ_404_NOTFOUND; else if (http_status_code == 429) req->mStatus = REQ_429_TOOMANYREQUESTS; + else if (http_status_code == 426) + req->mStatus = REQ_426_BLACKLISTED; else req->mStatus = REQ_IO_ERROR; diff --git a/es-core/src/HttpReq.h b/es-core/src/HttpReq.h index 8621c4d725..ed65eee1f4 100644 --- a/es-core/src/HttpReq.h +++ b/es-core/src/HttpReq.h @@ -38,6 +38,7 @@ class HttpReq REQ_IO_ERROR, //some error happened, get it with getErrorMsg() REQ_404_NOTFOUND, //HTTP response status code 404 happened + REQ_426_BLACKLISTED, //HTTP response status code 426 happened REQ_429_TOOMANYREQUESTS, //HTTP response status code 429 happened REQ_INVALID_RESPONSE //the HTTP response was invalid }; From cd4983bb37520caaa81bcbc5bf6b3e9ca465c6b6 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 2 Nov 2019 13:07:22 +0100 Subject: [PATCH 206/336] Locale : try to use "C" for LC_NUMERIC & LC_TIME --- es-app/src/main.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 843f71149b..d109afd938 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -400,6 +400,9 @@ int main(int argc, char* argv[]) #if WIN32 std::locale::global(std::locale("en-US")); + setlocale(LC_NUMERIC, "C"); + setlocale(LC_TIME, "C"); + setlocale(LC_MONETARY, "C"); #else std::locale::global(std::locale("C")); #endif From 74d01385d0dd97f8303a6aee687e446f25599ef1 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 3 Nov 2019 02:34:55 +0100 Subject: [PATCH 207/336] Image in Video View should now prefer thumbnail as thumbnail prefered source is box2d --- es-app/src/views/gamelist/VideoGameListView.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index 4e23c8428d..393a9c2c32 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -292,7 +292,7 @@ void VideoGameListView::updateInfoPanel() mMarquee.setImage(file->getMarqueePath()/*, false, mMarquee.getMaxSizeInfo()*/); // Too slow on pi if (mImage != nullptr) - mImage->setImage(file->getImagePath()); + mImage->setImage(file->getThumbnailPath()); mDescription.setText(file->metadata.get("desc")); mDescContainer.reset(); From e2fec344f8d0f9a9fd0bf5dbad6c00308023d876 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 3 Nov 2019 11:14:41 +0100 Subject: [PATCH 208/336] Scraper : Simplified image/box/logo sources to avoid confusion --- es-app/src/MetaData.cpp | 8 +-- es-app/src/guis/GuiMenu.cpp | 52 +++++++++---------- es-app/src/guis/GuiScraperStart.cpp | 2 +- es-app/src/scrapers/Scraper.cpp | 2 +- es-app/src/scrapers/ScreenScraper.cpp | 8 +-- .../src/views/gamelist/VideoGameListView.cpp | 5 +- es-core/src/Settings.cpp | 4 +- es-core/src/components/OptionListComponent.h | 9 ++++ 8 files changed, 50 insertions(+), 40 deletions(-) diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index 10831f6b6d..4c34f8ea44 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -274,14 +274,14 @@ void MetaDataList::importScrappedMetadata(const MetaDataList& source) if (Settings::getInstance()->getString("ScrapperImageSrc").empty()) type &= ~MetaDataImportType::Types::IMAGE; - if (!Settings::getInstance()->getString("ScrapperThumbSrc").empty()) + if (Settings::getInstance()->getString("ScrapperThumbSrc").empty()) type &= ~MetaDataImportType::Types::THUMB; + if (Settings::getInstance()->getString("ScrapperLogoSrc").empty()) + type &= ~MetaDataImportType::Types::MARQUEE; + if (!Settings::getInstance()->getBool("ScrapeVideos")) type &= ~MetaDataImportType::Types::VIDEO; - - if (!Settings::getInstance()->getBool("ScrapeMarquee")) - type &= ~MetaDataImportType::Types::MARQUEE; } for (auto mdd : getMDD()) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index d4ec03d85d..b5559c722f 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -85,39 +85,45 @@ void GuiMenu::openScraperSettings() if (scraper == "ScreenScraper") { - // image source + // Image source : tag std::string imageSourceName = Settings::getInstance()->getString("ScrapperImageSrc"); - auto imageSource = std::make_shared< OptionListComponent >(mWindow, _("PREFERED IMAGE SOURCE"), false); + auto imageSource = std::make_shared< OptionListComponent >(mWindow, _("IMAGE SOURCE"), false); imageSource->add(_("NONE"), "", imageSourceName.empty()); imageSource->add(_("SCREENSHOT"), "ss", imageSourceName == "ss"); imageSource->add(_("TITLE SCREENSHOT"), "sstitle", imageSourceName == "sstitle"); - imageSource->add(_("BOX 2D"), "box-2D", imageSourceName == "box-2D"); - imageSource->add(_("BOX 3D"), "box-3D", imageSourceName == "box-3D"); imageSource->add(_("MIX"), "mixrbv1", imageSourceName == "mixrbv1"); - imageSource->add(_("WHEEL"), "wheel", imageSourceName == "wheel"); - s->addWithLabel(_("PREFERED IMAGE SOURCE"), imageSource); - s->addSaveFunc([imageSource] { - if (Settings::getInstance()->getString("ScrapperImageSrc") != imageSource->getSelected()) - Settings::getInstance()->setString("ScrapperImageSrc", imageSource->getSelected()); - }); + if (!imageSource->hasSelection()) + imageSource->selectFirstItem(); + s->addWithLabel(_("IMAGE SOURCE"), imageSource); + s->addSaveFunc([imageSource] { Settings::getInstance()->setString("ScrapperImageSrc", imageSource->getSelected()); }); + // Box source : tag std::string thumbSourceName = Settings::getInstance()->getString("ScrapperThumbSrc"); - auto thumbSource = std::make_shared< OptionListComponent >(mWindow, _("PREFERED THUMBNAIL SOURCE"), false); + auto thumbSource = std::make_shared< OptionListComponent >(mWindow, _("BOX SOURCE"), false); thumbSource->add(_("NONE"), "", thumbSourceName.empty()); - thumbSource->add(_("SCREENSHOT"), "ss", thumbSourceName == "ss"); - thumbSource->add(_("TITLE SCREENSHOT"), "sstitle", thumbSourceName == "sstitle"); thumbSource->add(_("BOX 2D"), "box-2D", thumbSourceName == "box-2D"); thumbSource->add(_("BOX 3D"), "box-3D", thumbSourceName == "box-3D"); - thumbSource->add(_("MIX"), "mixrbv1", thumbSourceName == "mixrbv1"); - thumbSource->add(_("WHEEL"), "wheel", thumbSourceName == "wheel"); - s->addWithLabel(_("PREFERED THUMBNAIL SOURCE"), thumbSource); - s->addSaveFunc([thumbSource] { - if (Settings::getInstance()->getString("ScrapperThumbSrc") != thumbSource->getSelected()) - Settings::getInstance()->setString("ScrapperThumbSrc", thumbSource->getSelected()); - }); + if (!thumbSource->hasSelection()) + thumbSource->selectFirstItem(); + + s->addWithLabel(_("BOX SOURCE"), thumbSource); + s->addSaveFunc([thumbSource] { Settings::getInstance()->setString("ScrapperThumbSrc", thumbSource->getSelected()); }); + + // Logo source : tag + std::string logoSourceName = Settings::getInstance()->getString("ScrapperLogoSrc"); + auto logoSource = std::make_shared< OptionListComponent >(mWindow, _("LOGO SOURCE"), false); + logoSource->add(_("NONE"), "", logoSourceName.empty()); + logoSource->add(_("WHEEL"), "wheel", logoSourceName == "wheel"); + logoSource->add(_("MARQUEE"), "marquee", logoSourceName == "marquee"); + + if (!logoSource->hasSelection()) + logoSource->selectFirstItem(); + + s->addWithLabel(_("LOGO SOURCE"), logoSource); + s->addSaveFunc([logoSource] { Settings::getInstance()->setString("ScrapperLogoSrc", logoSource->getSelected()); }); // scrape ratings auto scrape_ratings = std::make_shared(mWindow); @@ -125,12 +131,6 @@ void GuiMenu::openScraperSettings() s->addWithLabel(_("SCRAPE RATINGS"), scrape_ratings); s->addSaveFunc([scrape_ratings] { Settings::getInstance()->setBool("ScrapeRatings", scrape_ratings->getState()); }); - // scrape marquee - auto scrape_marquee = std::make_shared(mWindow); - scrape_marquee->setState(Settings::getInstance()->getBool("ScrapeMarquee")); - s->addWithLabel(_("SCRAPE MARQUEE"), scrape_marquee); - s->addSaveFunc([scrape_marquee] { Settings::getInstance()->setBool("ScrapeMarquee", scrape_marquee->getState()); }); - // scrape video auto scrape_video = std::make_shared(mWindow); scrape_video->setState(Settings::getInstance()->getBool("ScrapeVideos")); diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index 4b10f04176..bb4c119e6a 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -36,7 +36,7 @@ GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), if (Settings::getInstance()->getBool("ScrapeVideos") && !Utils::FileSystem::exists(g->metadata.get("video"))) return true; - if (Settings::getInstance()->getBool("ScrapeMarquee") && !Utils::FileSystem::exists(g->metadata.get("marquee"))) + if (Settings::getInstance()->getString("ScrapperLogoSrc").empty() && !Utils::FileSystem::exists(g->metadata.get("marquee"))) return true; return false; diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index 4a5d6b9d1a..2e34dda8fc 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -253,7 +253,7 @@ MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const Scrape }, "thumbnail", result.mdl.getName())); } - if (!search.overWriteMedias && Settings::getInstance()->getBool("ScrapeMarquee") && Utils::FileSystem::exists(search.game->metadata.get("marquee"))) + if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperLogoSrc").empty() && Utils::FileSystem::exists(search.game->metadata.get("marquee"))) mResult.mdl.set("marquee", search.game->metadata.get("marquee")); else if (!result.marqueeUrl.empty()) { diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index 96e654a783..04806d0005 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -336,8 +336,10 @@ std::vector ScreenScraperRequest::getRipList(std::string imageSourc ripList = { "box-2D", "box-3D" }; else if (imageSource == "box-3D") ripList = { "box-3D", "box-2D" }; - else if (imageSource == "wheel" || imageSource == "marquee") + else if (imageSource == "wheel") ripList = { "wheel", "wheel-hd", "wheel-steel", "wheel-carbon", "screenmarqueesmall", "screenmarquee" }; + else if (imageSource == "marquee") + ripList = { "screenmarqueesmall", "screenmarquee", "wheel", "wheel-hd", "wheel-steel", "wheel-carbon" }; return ripList; } @@ -464,9 +466,9 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve } } - if (Settings::getInstance()->getBool("ScrapeMarquee")) + if (!Settings::getInstance()->getString("ScrapperLogoSrc").empty()) { - ripList = getRipList("marquee"); + ripList = getRipList(Settings::getInstance()->getString("ScrapperLogoSrc")); if (!ripList.empty()) { pugi::xml_node art = findMedia(media_list, ripList, region); diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index 393a9c2c32..fbb281e4ac 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -37,9 +37,8 @@ VideoGameListView::VideoGameListView(Window* window, FolderData* root) : #else mVideo = new VideoVlcComponent(window, ""); #endif - - // Default is thumbnail in Retropie themes & video view - mVideo->setSnapshotSource(THUMBNAIL); + + mVideo->setSnapshotSource(IMAGE); mList.setPosition(mSize.x() * (0.50f + padding), mList.getPosition().y()); mList.setSize(mSize.x() * (0.50f - padding), mList.getSize().y()); diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 72f30bf454..75a19d467e 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -223,8 +223,8 @@ void Settings::setDefaults() mStringMap["Scraper"] = "ScreenScraper"; mStringMap["ScrapperImageSrc"] = "ss"; mStringMap["ScrapperThumbSrc"] = "box-2D"; - - mBoolMap["ScrapeMarquee"] = false; + mStringMap["ScrapperLogoSrc"] = "wheel"; + mBoolMap["ScrapeVideos"] = false; mBoolMap["audio.bgmusic"] = true; diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index 1c0c27bf5e..ab7b0ebd26 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -321,6 +321,15 @@ class OptionListComponent : public GuiComponent onSelectedChanged(); } + bool hasSelection() + { + for (unsigned int i = 0; i < mEntries.size(); i++) + if (mEntries.at(i).selected) + return true; + + return false; + } + void selectFirstItem() { for (unsigned int i = 0; i < mEntries.size(); i++) From 1aa9abb9670a5036631aace8242e379979263ea5 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 3 Nov 2019 11:21:48 +0100 Subject: [PATCH 209/336] Scaper : minor fix for Only missing medias mode --- es-app/src/guis/GuiScraperStart.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index bb4c119e6a..c8f9409ab8 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -30,13 +30,13 @@ GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), if (!Settings::getInstance()->getString("ScrapperImageSrc").empty() && !Utils::FileSystem::exists(g->metadata.get("image"))) return true; - if (Settings::getInstance()->getString("ScrapperThumbSrc").empty() && !Utils::FileSystem::exists(g->metadata.get("thumbnail"))) + if (!Settings::getInstance()->getString("ScrapperThumbSrc").empty() && !Utils::FileSystem::exists(g->metadata.get("thumbnail"))) return true; - if (Settings::getInstance()->getBool("ScrapeVideos") && !Utils::FileSystem::exists(g->metadata.get("video"))) + if (!Settings::getInstance()->getString("ScrapperLogoSrc").empty() && !Utils::FileSystem::exists(g->metadata.get("marquee"))) return true; - if (Settings::getInstance()->getString("ScrapperLogoSrc").empty() && !Utils::FileSystem::exists(g->metadata.get("marquee"))) + if (Settings::getInstance()->getBool("ScrapeVideos") && !Utils::FileSystem::exists(g->metadata.get("video"))) return true; return false; From 6dd00160f912829e02ae0f15f9a23c93743f0756 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 4 Nov 2019 10:16:12 +0100 Subject: [PATCH 210/336] Screenscraper video scrap : prefer download new "video-normalized" format which is smaller. --- es-app/src/ApiSystem.cpp | 2 +- es-app/src/scrapers/ScreenScraper.cpp | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/es-app/src/ApiSystem.cpp b/es-app/src/ApiSystem.cpp index e77d48f82e..f73165a14a 100644 --- a/es-app/src/ApiSystem.cpp +++ b/es-app/src/ApiSystem.cpp @@ -225,7 +225,7 @@ void deleteDirectoryFiles(const std::string path) for (auto file : files) { if (Utils::FileSystem::isDirectory(file)) - ::RemoveDirectory(file.c_str()); + ::RemoveDirectoryA(file.c_str()); else Utils::FileSystem::removeFile(file); } diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index 04806d0005..2a32972039 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -340,6 +340,8 @@ std::vector ScreenScraperRequest::getRipList(std::string imageSourc ripList = { "wheel", "wheel-hd", "wheel-steel", "wheel-carbon", "screenmarqueesmall", "screenmarquee" }; else if (imageSource == "marquee") ripList = { "screenmarqueesmall", "screenmarquee", "wheel", "wheel-hd", "wheel-steel", "wheel-carbon" }; + else if (imageSource == "video") + ripList = { "video-normalized", "video" }; return ripList; } @@ -481,13 +483,16 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve if (Settings::getInstance()->getBool("ScrapeVideos")) { - pugi::xml_node art = findMedia(media_list, "video", region); - if (art) - result.videoUrl = ensureUrl(art.text().get()); - else - LOG(LogDebug) << "Failed to find media XML node for video"; + ripList = getRipList("video"); + if (!ripList.empty()) + { + pugi::xml_node art = findMedia(media_list, ripList, region); + if (art) + result.videoUrl = ensureUrl(art.text().get()); + else + LOG(LogDebug) << "Failed to find media XML node for video"; + } } - } out_results.push_back(result); From 6e1d3cdd4ce3176312774e91592b7e836d7aa4a7 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 4 Nov 2019 21:14:11 +0100 Subject: [PATCH 211/336] Fix linux build --- es-core/src/components/ImageGridComponent.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 1e654caad9..eb534cbe4a 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -800,7 +800,7 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA if (ResourceManager::getInstance()->fileExists(imagePath)) tile->setImage(imagePath); - else if (mEntries.at(imgPos).object->getType() == 2 || (mEntries.at(imgPos).object->getType() == PLACEHOLDER && mEntries.at(imgPos).object->getPath() == "..")) + else if (mEntries.at(imgPos).object->getType() == 2 || (mEntries.at(imgPos).object->getType() == 3 && mEntries.at(imgPos).object->getPath() == "..")) tile->setImage(mDefaultFolderTexture, true); else tile->setImage(mDefaultGameTexture, true); From 641e1179a7f7d94521b5563c0ab7ae4ce81a4255 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 5 Nov 2019 11:30:32 +0100 Subject: [PATCH 212/336] Fix linux build --- es-core/src/ThemeData.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index f2c7bb0b5b..12c75fac2d 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -339,7 +339,7 @@ class ThemeData return end(); } - std::pair>::iterator, bool> insert(std::pair& item) + std::pair>::iterator, bool> insert(std::pair item) { std::pair>::iterator, bool> ret; From 7c2298a49acf8f3b9d87612a24f087684ba60cc4 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 5 Nov 2019 11:52:23 +0100 Subject: [PATCH 213/336] Fix : Many videos can be loaded/played together when returning from a game --- es-app/src/views/ViewController.cpp | 6 ++++++ es-app/src/views/ViewController.h | 2 ++ es-core/src/ThemeData.cpp | 13 ++++++++----- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 76e0faa2b8..0fbf84ee89 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -752,4 +752,10 @@ void ViewController::onThemeChanged(const std::shared_ptr& theme) { ThemeData::setDefaultTheme(theme.get()); mWindow->onThemeChanged(theme); +} + +void ViewController::onShow() +{ + if (mCurrentView) + mCurrentView->onShow(); } \ No newline at end of file diff --git a/es-app/src/views/ViewController.h b/es-app/src/views/ViewController.h index 227cb9b47b..c65e262ee1 100644 --- a/es-app/src/views/ViewController.h +++ b/es-app/src/views/ViewController.h @@ -89,6 +89,8 @@ class ViewController : public GuiComponent void onThemeChanged(const std::shared_ptr& theme); + virtual void onShow() override; + private: ViewController(Window* window); static ViewController* sInstance; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index d69f9be2ff..fc571473de 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -391,12 +391,15 @@ void ThemeData::loadFile(const std::string system, std::map& ThemeData::getMenuTheme() { - if (mMenuTheme == nullptr && mDefaultTheme != nullptr) - mMenuTheme = std::shared_ptr(new ThemeMenu(mDefaultTheme)); - else if (mMenuTheme == nullptr) + if (mMenuTheme == nullptr) { - auto emptyData = ThemeData(); - return std::shared_ptr(new ThemeMenu(&emptyData)); + if (mDefaultTheme != nullptr) + mMenuTheme = std::shared_ptr(new ThemeMenu(mDefaultTheme)); + else + { + auto emptyData = ThemeData(); + mMenuTheme = std::shared_ptr(new ThemeMenu(&emptyData)); + } } return mMenuTheme; From ea2578fc7c126cbdbb828292068c34ddc05a08d3 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 5 Nov 2019 11:55:40 +0100 Subject: [PATCH 214/336] Fix : Folder navigation when returning from a game --- .../src/views/gamelist/BasicGameListView.cpp | 40 +++++++++++-------- .../src/views/gamelist/GridGameListView.cpp | 38 ++++++++++++++++-- es-core/src/components/GridTileComponent.cpp | 18 +++++---- es-core/src/components/ImageComponent.cpp | 7 +++- 4 files changed, 76 insertions(+), 27 deletions(-) diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index a191b0f01a..990a340819 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -133,28 +133,36 @@ void BasicGameListView::setCursor(FileData* cursor) { if(!mList.setCursor(cursor) && (!cursor->isPlaceHolder())) { - populateList(cursor->getParent()->getChildrenListToDisplay()); - mList.setCursor(cursor); + auto children = mRoot->getChildrenListToDisplay(); - // 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()) + auto gameIter = std::find(children.cbegin(), children.cend(), cursor); + if (gameIter == children.cend()) { - std::stack tmp; - FileData* ptr = cursor->getParent(); - while(ptr && ptr != mRoot) - { - tmp.push(ptr); - ptr = ptr->getParent(); - } + children = cursor->getParent()->getChildrenListToDisplay(); - // flip the stack and put it in mCursorStack - mCursorStack = std::stack(); - while(!tmp.empty()) + // 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()) { - mCursorStack.push(tmp.top()); - tmp.pop(); + std::stack 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(); + while (!tmp.empty()) + { + mCursorStack.push(tmp.top()); + tmp.pop(); + } } } + + populateList(children); + mList.setCursor(cursor); } } diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 2b4b8a2495..8829aa45b1 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -155,10 +155,39 @@ FileData* GridGameListView::getCursor() void GridGameListView::setCursor(FileData* file) { - if(!mGrid.setCursor(file) && file->getParent() != nullptr) + if (!mGrid.setCursor(file) && file->getParent() != nullptr && !file->isPlaceHolder()) { - populateList(file->getParent()->getChildrenListToDisplay()); + auto children = mRoot->getChildrenListToDisplay(); + + auto gameIter = std::find(children.cbegin(), children.cend(), file); + if (gameIter == children.cend()) + { + children = file->getParent()->getChildrenListToDisplay(); + + // update our cursor stack in case our cursor just got set to some folder we weren't in before + if (mCursorStack.empty() || mCursorStack.top() != file->getParent()) + { + std::stack tmp; + FileData* ptr = file->getParent(); + while (ptr && ptr != mRoot) + { + tmp.push(ptr); + ptr = ptr->getParent(); + } + + // flip the stack and put it in mCursorStack + mCursorStack = std::stack(); + while (!tmp.empty()) + { + mCursorStack.push(tmp.top()); + tmp.pop(); + } + } + } + + populateList(children); mGrid.setCursor(file); + } } @@ -249,7 +278,10 @@ void GridGameListView::populateList(const std::vector& files) } } - mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), *it); + if (((*it)->getType() == FOLDER) && Utils::FileSystem::exists(getImagePath(*it))) + mGrid.add(_U("\uF114 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), *it); + else + mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), *it); } } else diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index ed6dbba551..8d591146f0 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -118,7 +118,9 @@ void GridTileComponent::resize() if (mSize != size) setSize(size); - float height = (int)(size.y() * currentProperties.mLabelSize.y()); + bool isDefaultImage = mIsDefaultImage && (mCurrentPath == ":/folder.svg" || mCurrentPath == ":/cartridge.svg"); + + float height = (int) (size.y() * currentProperties.mLabelSize.y()); float labelHeight = height; mLabel.setVisible(mLabelVisible || mIsDefaultImage); @@ -175,7 +177,7 @@ void GridTileComponent::resize() mImage->setColorShift(currentProperties.mImageColor); mImage->setMirroring(currentProperties.mMirror); - if (currentProperties.mImageSizeMode == "minSize") + if (currentProperties.mImageSizeMode == "minSize" && !isDefaultImage) mImage->setMinSize(imageWidth, imageHeight); else if (currentProperties.mImageSizeMode == "size") mImage->setSize(imageWidth, imageHeight); @@ -272,8 +274,7 @@ void GridTileComponent::resize() mLabel.setPosition(mSize.x() * 0.1, mSize.y() * 0.2); mLabel.setSize(mSize.x() - mSize.x() * 0.2, mSize.y() - mSize.y() * 0.3); } - - + mBackground.setPosition(bkposition); mBackground.setSize(bkSize); mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); @@ -326,8 +327,11 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) Vector2i pos((int)Math::round(trans.translation()[0] + padding), (int)Math::round(trans.translation()[1] + topPadding)); Vector2i size((int)Math::round(mSize.x() - 2 * padding), (int)Math::round(mSize.y() - topPadding - bottomPadding)); + + bool isDefaultImage = mIsDefaultImage && (mCurrentPath == ":/folder.svg" || mCurrentPath == ":/cartridge.svg"); + bool isMinSize = currentProperties.mImageSizeMode == "minSize" && !isDefaultImage; - if (currentProperties.mImageSizeMode == "minSize") + if (isMinSize) Renderer::pushClipRect(pos, size); if (mImage != NULL) @@ -336,7 +340,7 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) if (mSelected && !mVideoPath.empty() && mVideo != nullptr) mVideo->render(trans); - if (!mLabelMerged && currentProperties.mImageSizeMode == "minSize") + if (!mLabelMerged && isMinSize) Renderer::popClipRect(); if (mMarquee != nullptr && mMarquee->hasImage()) @@ -346,7 +350,7 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) else if (!mLabelVisible && mIsDefaultImage) mLabel.render(trans); - if (mLabelMerged && currentProperties.mImageSizeMode == "minSize") + if (mLabelMerged && isMinSize) Renderer::popClipRect(); } diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index f679103921..d25a0f5780 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -52,6 +52,7 @@ void ImageComponent::resize() if (mTexture->isTiled()) { + uncrop(); mSize = mTargetSize; } else @@ -64,6 +65,7 @@ void ImageComponent::resize() if(mTargetIsMax) { + uncrop(); mSize = textureSize; Vector2f resizeScale((mTargetSize.x() / mSize.x()), (mTargetSize.y() / mSize.y())); @@ -106,7 +108,10 @@ void ImageComponent::resize() mSize[1] = Math::max(Math::round(mSize[1]), mTargetSize.y()); mSize[0] = Math::max((mSize[1] / textureSize.y()) * textureSize.x(), mTargetSize.x()); - }else{ + } + else + { + uncrop(); // if both components are set, we just stretch // if no components are set, we don't resize at all mSize = mTargetSize == Vector2f::Zero() ? textureSize : mTargetSize; From f6b41498c762486804031ab31d9786f7ae09b570 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 5 Nov 2019 20:58:25 +0100 Subject: [PATCH 215/336] Fix : Duplicated folder when using "gamelist only" option --- es-app/src/Gamelist.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 0d91898905..91162311dd 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -41,8 +41,13 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType std::string key = Utils::FileSystem::combine(treeNode->getPath(), *path_it); FileData* item = (fileMap.find(key) != fileMap.end()) ? fileMap[key] : nullptr; if (item != nullptr) - return item; - + { + if (item->getType() == FOLDER) + treeNode = (FolderData*) item; + else + return item; + } + // this is the end if(path_it == --pathList.end()) { @@ -82,8 +87,10 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType LOG(LogWarning) << "gameList: folder doesn't already exist, won't create"; return NULL; } - - FolderData* folder = new FolderData(Utils::FileSystem::getStem(treeNode->getPath()) + "/" + *path_it, system); + + // create missing folder + FolderData* folder = new FolderData(Utils::FileSystem::getStem(treeNode->getPath()) + "/" + *path_it, system); + fileMap[key] = folder; treeNode->addChild(folder); treeNode = folder; } From bd1b861da9266e0176d32a0d52942c6fa59b2e6e Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 7 Nov 2019 17:48:32 +0100 Subject: [PATCH 216/336] Perfs/Memory : Optimized Collections loading & memory usage + Optimized use of mame names --- es-app/src/CollectionSystemManager.cpp | 40 ++++++++--------- es-app/src/FileData.cpp | 44 +++++++++---------- es-app/src/FileData.h | 16 ++++++- es-app/src/FileFilterIndex.cpp | 18 ++++---- es-app/src/FileSorts.cpp | 24 +++++----- es-app/src/Gamelist.cpp | 16 +++---- es-app/src/SystemData.cpp | 7 +-- es-app/src/guis/GuiGamelistOptions.cpp | 2 +- es-app/src/guis/GuiScraperMulti.cpp | 2 +- es-app/src/guis/GuiScraperStart.cpp | 10 ++--- es-app/src/scrapers/Scraper.cpp | 18 ++++---- es-app/src/scrapers/ThreadedScraper.cpp | 4 +- .../views/gamelist/DetailedGameListView.cpp | 2 +- .../src/views/gamelist/GridGameListView.cpp | 20 ++++----- .../src/views/gamelist/VideoGameListView.cpp | 20 ++++----- es-core/src/MameNames.cpp | 31 +++---------- es-core/src/MameNames.h | 6 +-- es-core/src/ThemeData.cpp | 40 +++++++++-------- 18 files changed, 154 insertions(+), 166 deletions(-) diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 0b9abe282f..6b67dd9c2e 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -266,7 +266,7 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS curSys->removeFromIndex(collectionEntry); collectionEntry->refreshMetadata(); // found and we are removing - if (name == "favorites" && file->metadata.get("favorite") == "false") { + if (name == "favorites" && file->getMetadata().get("favorite") == "false") { // need to check if still marked as favorite, if not remove ViewController::get()->getGameListView(curSys).get()->remove(collectionEntry, false); @@ -283,8 +283,8 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS else { // we didn't find it here - we need to check if we should add it - if (name == "recent" && file->metadata.get("playcount") > "0" && includeFileInAutoCollections(file) || - name == "favorites" && file->metadata.get("favorite") == "true") { + if (name == "recent" && file->getMetadata().get("playcount") > "0" && includeFileInAutoCollections(file) || + name == "favorites" && file->getMetadata().get("favorite") == "true") { CollectionFileData* newGame = new CollectionFileData(file, curSys); rootFolder->addChild(newGame); curSys->addToIndex(newGame); @@ -529,7 +529,7 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) SystemData* sysData = file->getSourceFileData()->getSystem(); sysData->removeFromIndex(file); - MetaDataList* md = &file->getSourceFileData()->metadata; + MetaDataList* md = &file->getSourceFileData()->getMetadata(); std::string value = md->get("favorite"); if (value == "false") @@ -622,11 +622,11 @@ void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) games_counter++; FileData* file = *iter; - std::string new_rating = file->metadata.get("rating"); - std::string new_releasedate = file->metadata.get("releasedate"); - std::string new_developer = file->metadata.get("developer"); - std::string new_genre = file->metadata.get("genre"); - std::string new_players = file->metadata.get("players"); + std::string new_rating = file->getMetadata().get("rating"); + std::string new_releasedate = file->getMetadata().get("releasedate"); + std::string new_developer = file->getMetadata().get("developer"); + std::string new_genre = file->getMetadata().get("genre"); + std::string new_players = file->getMetadata().get("players"); rating = (new_rating > rating ? (new_rating != "" ? new_rating : rating) : rating); players = (new_players > players ? (new_players != "" ? new_players : players) : players); @@ -659,15 +659,15 @@ void CollectionSystemManager::updateCollectionFolderMetadata(SystemData* sys) } - rootFolder->metadata.set("desc", desc); - rootFolder->metadata.set("rating", rating); - rootFolder->metadata.set("players", players); - rootFolder->metadata.set("genre", genre); - rootFolder->metadata.set("releasedate", releasedate); - rootFolder->metadata.set("developer", developer); - rootFolder->metadata.set("video", video); - rootFolder->metadata.set("thumbnail", thumbnail); - rootFolder->metadata.set("image", image); + rootFolder->getMetadata().set("desc", desc); + rootFolder->getMetadata().set("rating", rating); + rootFolder->getMetadata().set("players", players); + rootFolder->getMetadata().set("genre", genre); + rootFolder->getMetadata().set("releasedate", releasedate); + rootFolder->getMetadata().set("developer", developer); + rootFolder->getMetadata().set("video", video); + rootFolder->getMetadata().set("thumbnail", thumbnail); + rootFolder->getMetadata().set("image", image); } void CollectionSystemManager::initCustomCollectionSystems() @@ -743,11 +743,11 @@ void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysDa bool include = includeFileInAutoCollections((*gameIt)); switch(sysDecl.type) { case AUTO_LAST_PLAYED: - include = include && (*gameIt)->metadata.get("playcount") > "0"; + include = include && (*gameIt)->getMetadata().get("playcount") > "0"; break; case AUTO_FAVORITES: // we may still want to add files we don't want in auto collections in "favorites" - include = (*gameIt)->metadata.get("favorite") == "true"; + include = (*gameIt)->getMetadata().get("favorite") == "true"; break; } diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 9ec9cad7d2..113ec9cc40 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -18,17 +18,17 @@ #include FileData::FileData(FileType type, const std::string& path, SystemData* system) - : mType(type), mSystem(system), mParent(NULL), metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor! + : mType(type), mSystem(system), mParent(NULL), mMetadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor! { mPath = Utils::FileSystem::createRelativePath(path, getSystemEnvData()->mStartPath, false); // TRACE("FileData : " << mPath); // metadata needs at least a name field (since that's what getName() will return) - if (metadata.get("name").empty()) - metadata.set("name", getDisplayName()); + if (mMetadata.get("name").empty()) + mMetadata.set("name", getDisplayName()); - metadata.resetChangedFlag(); + mMetadata.resetChangedFlag(); } const std::string FileData::getPath() const @@ -74,12 +74,12 @@ std::string FileData::getCleanName() const const std::string FileData::getThumbnailPath() const { - std::string thumbnail = metadata.get("thumbnail"); + std::string thumbnail = getMetadata().get("thumbnail"); // no thumbnail, try image if(thumbnail.empty()) { - thumbnail = metadata.get("image"); + thumbnail = getMetadata().get("image"); // no image, try to use local image if(thumbnail.empty() && Settings::getInstance()->getBool("LocalArt")) @@ -102,17 +102,17 @@ const std::string FileData::getThumbnailPath() const const bool FileData::getFavorite() { - return metadata.get("favorite") == "true"; + return getMetadata().get("favorite") == "true"; } const bool FileData::getHidden() { - return metadata.get("hidden") == "true"; + return getMetadata().get("hidden") == "true"; } const bool FileData::getKidGame() { - return metadata.get("kidgame") != "false"; + return getMetadata().get("kidgame") != "false"; } static std::shared_ptr showFilenames; @@ -136,22 +136,22 @@ const std::string FileData::getName() return getDisplayName(); } - return metadata.getName(); + return getMetadata().getName(); } const std::string FileData::getCore() const { - return metadata.get("core"); + return getMetadata().get("core"); } const std::string FileData::getEmulator() const { - return metadata.get("emulator"); + return getMetadata().get("emulator"); } const std::string FileData::getVideoPath() const { - std::string video = metadata.get("video"); + std::string video = getMetadata().get("video"); // no video, try to use local video if(video.empty() && Settings::getInstance()->getBool("LocalArt")) @@ -166,7 +166,7 @@ const std::string FileData::getVideoPath() const const std::string FileData::getMarqueePath() const { - std::string marquee = metadata.get("marquee"); + std::string marquee = getMetadata().get("marquee"); // no marquee, try to use local marquee if (marquee.empty() && Settings::getInstance()->getBool("LocalArt")) @@ -188,7 +188,7 @@ const std::string FileData::getMarqueePath() const const std::string FileData::getImagePath() const { - std::string image = metadata.get("image"); + std::string image = getMetadata().get("image"); // no image, try to use local image if(image.empty()) @@ -290,11 +290,11 @@ void FileData::launchGame(Window* window) { FileData* gameToUpdate = getSourceFileData(); - int timesPlayed = gameToUpdate->metadata.getInt("playcount") + 1; - gameToUpdate->metadata.set("playcount", std::to_string(static_cast(timesPlayed))); + int timesPlayed = gameToUpdate->getMetadata().getInt("playcount") + 1; + gameToUpdate->getMetadata().set("playcount", std::to_string(static_cast(timesPlayed))); //update last played time - gameToUpdate->metadata.set("lastplayed", Utils::Time::DateTime(Utils::Time::now())); + gameToUpdate->getMetadata().set("lastplayed", Utils::Time::DateTime(Utils::Time::now())); CollectionSystemManager::get()->refreshCollectionSystems(gameToUpdate); } @@ -308,7 +308,7 @@ CollectionFileData::CollectionFileData(FileData* file, SystemData* system) { mSourceFileData = file->getSourceFileData(); mParent = NULL; - metadata = mSourceFileData->metadata; + // metadata = mSourceFileData->metadata; mDirty = true; } @@ -346,14 +346,14 @@ FileData* CollectionFileData::getSourceFileData() void CollectionFileData::refreshMetadata() { - metadata = mSourceFileData->metadata; + // metadata = mSourceFileData->metadata; mDirty = true; } const std::string CollectionFileData::getName() { if (mDirty) { - mCollectionFileName = Utils::String::removeParenthesis(mSourceFileData->metadata.get("name")); + mCollectionFileName = Utils::String::removeParenthesis(mSourceFileData->getMetadata().get("name")); mCollectionFileName += " [" + Utils::String::toUpper(mSourceFileData->getSystem()->getName()) + "]"; mDirty = false; } @@ -361,7 +361,7 @@ const std::string CollectionFileData::getName() if (Settings::getInstance()->getBool("CollectionShowSystemInfo")) return mCollectionFileName; - return Utils::String::removeParenthesis(mSourceFileData->metadata.get("name")); + return Utils::String::removeParenthesis(mSourceFileData->getMetadata().get("name")); } const std::vector FolderData::getChildrenListToDisplay() diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index ce33e8341d..c8fc23d464 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -78,9 +78,18 @@ class FileData void launchGame(Window* window); - MetaDataList metadata; - static void resetSettings(); + + virtual const MetaDataList& getMetadata() const { return mMetadata; } + virtual MetaDataList& getMetadata() { return mMetadata; } + + void setMetadata(MetaDataList value) { getMetadata() = value; } + + std::string getMetadata(const std::string& key) { return getMetadata().get(key); } + void setMetadata(const std::string& key, const std::string& value) { getMetadata().set(key, value); } + +private: + MetaDataList mMetadata; protected: FolderData* mParent; @@ -103,6 +112,9 @@ class CollectionFileData : public FileData virtual std::string getSystemName() const; virtual SystemEnvironmentData* getSystemEnvData() const; + virtual const MetaDataList& getMetadata() const { return mSourceFileData->getMetadata(); } + virtual MetaDataList& getMetadata() { return mSourceFileData->getMetadata(); } + private: // needs to be updated when metadata changes std::string mCollectionFileName; diff --git a/es-app/src/FileFilterIndex.cpp b/es-app/src/FileFilterIndex.cpp index dfa27cafa7..23556d17ba 100644 --- a/es-app/src/FileFilterIndex.cpp +++ b/es-app/src/FileFilterIndex.cpp @@ -92,7 +92,7 @@ std::string FileFilterIndex::getIndexableKey(FileData* game, FilterIndexType typ { case GENRE_FILTER: { - key = Utils::String::toUpper(game->metadata.get("genre")); + key = Utils::String::toUpper(game->getMetadata().get("genre")); key = Utils::String::trim(key); if (getSecondary && !key.empty()) { std::istringstream f(key); @@ -114,18 +114,18 @@ std::string FileFilterIndex::getIndexableKey(FileData* game, FilterIndexType typ if (getSecondary) break; - key = game->metadata.get("players"); + key = game->getMetadata().get("players"); break; } case PUBDEV_FILTER: { - key = Utils::String::toUpper(game->metadata.get("publisher")); + key = Utils::String::toUpper(game->getMetadata().get("publisher")); key = Utils::String::trim(key); if ((getSecondary && !key.empty()) || (!getSecondary && key.empty())) - key = Utils::String::toUpper(game->metadata.get("developer")); + key = Utils::String::toUpper(game->getMetadata().get("developer")); else - key = Utils::String::toUpper(game->metadata.get("publisher")); + key = Utils::String::toUpper(game->getMetadata().get("publisher")); break; } case RATINGS_FILTER: @@ -133,7 +133,7 @@ std::string FileFilterIndex::getIndexableKey(FileData* game, FilterIndexType typ int ratingNumber = 0; if (!getSecondary) { - std::string ratingString = game->metadata.get("rating"); + std::string ratingString = game->getMetadata().get("rating"); if (!ratingString.empty()) { try { ratingNumber = (int)((std::stod(ratingString)*5)+0.5); @@ -154,21 +154,21 @@ std::string FileFilterIndex::getIndexableKey(FileData* game, FilterIndexType typ { if (game->getType() != GAME) return "FALSE"; - key = Utils::String::toUpper(game->metadata.get("favorite")); + key = Utils::String::toUpper(game->getMetadata().get("favorite")); break; } case HIDDEN_FILTER: { if (game->getType() != GAME) return "FALSE"; - key = Utils::String::toUpper(game->metadata.get("hidden")); + key = Utils::String::toUpper(game->getMetadata().get("hidden")); break; } case KIDGAME_FILTER: { if (game->getType() != GAME) return "FALSE"; - key = Utils::String::toUpper(game->metadata.get("kidgame")); + key = Utils::String::toUpper(game->getMetadata().get("kidgame")); break; } } diff --git a/es-app/src/FileSorts.cpp b/es-app/src/FileSorts.cpp index 36a618c8d8..d82c33cfce 100644 --- a/es-app/src/FileSorts.cpp +++ b/es-app/src/FileSorts.cpp @@ -89,15 +89,15 @@ namespace FileSorts bool compareRating(const FileData* file1, const FileData* file2) { - return file1->metadata.getFloat("rating") < file2->metadata.getFloat("rating"); + return file1->getMetadata().getFloat("rating") < file2->getMetadata().getFloat("rating"); } bool compareTimesPlayed(const FileData* file1, const FileData* file2) { //only games have playcount metadata - if (file1->metadata.getType() == GAME_METADATA && file2->metadata.getType() == GAME_METADATA) + if (file1->getMetadata().getType() == GAME_METADATA && file2->getMetadata().getType() == GAME_METADATA) { - return (file1)->metadata.getInt("playcount") < (file2)->metadata.getInt("playcount"); + return (file1)->getMetadata().getInt("playcount") < (file2)->getMetadata().getInt("playcount"); } return false; @@ -107,39 +107,39 @@ namespace FileSorts { // since it's stored as an ISO string (YYYYMMDDTHHMMSS), we can compare as a string // as it's a lot faster than the time casts and then time comparisons - return (file1)->metadata.get("lastplayed") < (file2)->metadata.get("lastplayed"); + return (file1)->getMetadata().get("lastplayed") < (file2)->getMetadata().get("lastplayed"); } bool compareNumPlayers(const FileData* file1, const FileData* file2) { - return (file1)->metadata.getInt("players") < (file2)->metadata.getInt("players"); + return (file1)->getMetadata().getInt("players") < (file2)->getMetadata().getInt("players"); } bool compareReleaseDate(const FileData* file1, const FileData* file2) { // since it's stored as an ISO string (YYYYMMDDTHHMMSS), we can compare as a string // as it's a lot faster than the time casts and then time comparisons - return (file1)->metadata.get("releasedate") < (file2)->metadata.get("releasedate"); + return (file1)->getMetadata().get("releasedate") < (file2)->getMetadata().get("releasedate"); } bool compareGenre(const FileData* file1, const FileData* file2) { - std::string genre1 = Utils::String::toUpper(file1->metadata.get("genre")); - std::string genre2 = Utils::String::toUpper(file2->metadata.get("genre")); + std::string genre1 = Utils::String::toUpper(file1->getMetadata().get("genre")); + std::string genre2 = Utils::String::toUpper(file2->getMetadata().get("genre")); return genre1.compare(genre2) < 0; } bool compareDeveloper(const FileData* file1, const FileData* file2) { - std::string developer1 = Utils::String::toUpper(file1->metadata.get("developer")); - std::string developer2 = Utils::String::toUpper(file2->metadata.get("developer")); + std::string developer1 = Utils::String::toUpper(file1->getMetadata().get("developer")); + std::string developer2 = Utils::String::toUpper(file2->getMetadata().get("developer")); return developer1.compare(developer2) < 0; } bool comparePublisher(const FileData* file1, const FileData* file2) { - std::string publisher1 = Utils::String::toUpper(file1->metadata.get("publisher")); - std::string publisher2 = Utils::String::toUpper(file2->metadata.get("publisher")); + std::string publisher1 = Utils::String::toUpper(file1->getMetadata().get("publisher")); + std::string publisher2 = Utils::String::toUpper(file2->getMetadata().get("publisher")); return publisher1.compare(publisher2) < 0; } diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 91162311dd..5ae1737853 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -156,17 +156,17 @@ void parseGamelist(SystemData* system, std::unordered_mapisArcadeAsset()) { - std::string defaultName = file->metadata.get("name"); - file->metadata = MetaDataList::createFromXML(type == FOLDER ? FOLDER_METADATA : GAME_METADATA, fileNode, system); + std::string defaultName = file->getMetadata().get("name"); + file->setMetadata(MetaDataList::createFromXML(type == FOLDER ? FOLDER_METADATA : GAME_METADATA, fileNode, system)); //make sure name gets set if one didn't exist - if (file->metadata.get("name").empty()) - file->metadata.set("name", defaultName); + if (file->getMetadata().get("name").empty()) + file->setMetadata("name", defaultName); if (Utils::FileSystem::isHidden(path)) - file->metadata.set("hidden", "true"); + file->getMetadata().set("hidden", "true"); - file->metadata.resetChangedFlag(); + file->getMetadata().resetChangedFlag(); } } } @@ -177,7 +177,7 @@ bool addFileDataNode(pugi::xml_node& parent, const FileData* file, const char* t pugi::xml_node newNode = parent.append_child(tag); //write metadata - file->metadata.appendToXML(newNode, true, system->getStartPath()); + file->getMetadata().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 @@ -249,7 +249,7 @@ void updateGamelist(SystemData* system) const char* tag = ((*fit)->getType() == GAME) ? "game" : "folder"; // do not touch if it wasn't changed anyway - if (!(*fit)->metadata.wasChanged()) + if (!(*fit)->getMetadata().wasChanged()) continue; bool removed = false; diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 1e0edceacd..886e1e9ba8 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -35,7 +35,7 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys if (!CollectionSystem) { mRootFolder = new FolderData(mEnvData->mStartPath, this); - mRootFolder->metadata.set("name", mFullName); + mRootFolder->getMetadata().set("name", mFullName); std::unordered_map fileMap; @@ -48,11 +48,6 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys if (!Settings::getInstance()->getBool("IgnoreGamelist") && mName != "imageviewer") parseGamelist(this, fileMap); - /* - if (mSortId >= 0 && mSortId < FileSorts::getSortTypes().size()) - mRootFolder->sort(FileSorts::getSortTypes().at(mSortId)); - else - mRootFolder->sort(FileSorts::getSortTypes().at(0));*/ } else { diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 5daaa5adab..652e1e4ab9 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -450,7 +450,7 @@ void GuiGamelistOptions::openMetaDataEd() }; } - mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->metadata, file->metadata.getMDD(), p, Utils::FileSystem::getFileName(file->getPath()), + mWindow->pushGui(new GuiMetaDataEd(mWindow, &file->getMetadata(), file->getMetadata().getMDD(), p, Utils::FileSystem::getFileName(file->getPath()), std::bind(&IGameListView::onFileChanged, ViewController::get()->getGameListView(file->getSystem()).get(), file, FILE_METADATA_CHANGED), deleteBtnFunc, file)); } diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 1adf3ea6b8..1054b799a7 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -119,7 +119,7 @@ void GuiScraperMulti::acceptResult(const ScraperSearchResult& result) { ScraperSearchParams& search = mSearchQueue.front(); - search.game->metadata.importScrappedMetadata(result.mdl); + search.game->getMetadata().importScrappedMetadata(result.mdl); updateGamelist(search.system); mSearchQueue.pop(); diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index c8f9409ab8..b94189bf73 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -27,22 +27,22 @@ GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), if (Settings::getInstance()->getString("Scraper") == "ScreenScraper") { - if (!Settings::getInstance()->getString("ScrapperImageSrc").empty() && !Utils::FileSystem::exists(g->metadata.get("image"))) + if (!Settings::getInstance()->getString("ScrapperImageSrc").empty() && !Utils::FileSystem::exists(g->getMetadata().get("image"))) return true; - if (!Settings::getInstance()->getString("ScrapperThumbSrc").empty() && !Utils::FileSystem::exists(g->metadata.get("thumbnail"))) + if (!Settings::getInstance()->getString("ScrapperThumbSrc").empty() && !Utils::FileSystem::exists(g->getMetadata().get("thumbnail"))) return true; - if (!Settings::getInstance()->getString("ScrapperLogoSrc").empty() && !Utils::FileSystem::exists(g->metadata.get("marquee"))) + if (!Settings::getInstance()->getString("ScrapperLogoSrc").empty() && !Utils::FileSystem::exists(g->getMetadata().get("marquee"))) return true; - if (Settings::getInstance()->getBool("ScrapeVideos") && !Utils::FileSystem::exists(g->metadata.get("video"))) + if (Settings::getInstance()->getBool("ScrapeVideos") && !Utils::FileSystem::exists(g->getMetadata().get("video"))) return true; return false; } else - return !Utils::FileSystem::exists(g->metadata.get("image")); + return !Utils::FileSystem::exists(g->getMetadata().get("image")); }, true); diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index 2e34dda8fc..b6489f839b 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -193,10 +193,10 @@ MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const Scrape bool ss = Settings::getInstance()->getString("Scraper") == "ScreenScraper"; auto tmp = Settings::getInstance()->getString("ScrapperImageSrc"); - auto md = search.game->metadata.get("image"); + auto md = search.game->getMetadata().get("image"); - if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperImageSrc").empty() && Utils::FileSystem::exists(search.game->metadata.get("image"))) - mResult.mdl.set("image", search.game->metadata.get("image")); + if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperImageSrc").empty() && Utils::FileSystem::exists(search.game->getMetadata().get("image"))) + mResult.mdl.set("image", search.game->getMetadata().get("image")); else if (!result.imageUrl.empty()) { std::string imgPath = getSaveAsPath(search, "image", ext); @@ -228,8 +228,8 @@ MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const Scrape }, "image", result.mdl.getName())); } - if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperThumbSrc").empty() && Utils::FileSystem::exists(search.game->metadata.get("thumbnail"))) - mResult.mdl.set("thumbnail", search.game->metadata.get("thumbnail")); + if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperThumbSrc").empty() && Utils::FileSystem::exists(search.game->getMetadata().get("thumbnail"))) + mResult.mdl.set("thumbnail", search.game->getMetadata().get("thumbnail")); else if (!result.thumbnailUrl.empty() && result.thumbnailUrl.find(result.imageUrl) != 0) { std::string thumbPath = getSaveAsPath(search, "thumb", ext); @@ -253,8 +253,8 @@ MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const Scrape }, "thumbnail", result.mdl.getName())); } - if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperLogoSrc").empty() && Utils::FileSystem::exists(search.game->metadata.get("marquee"))) - mResult.mdl.set("marquee", search.game->metadata.get("marquee")); + if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperLogoSrc").empty() && Utils::FileSystem::exists(search.game->getMetadata().get("marquee"))) + mResult.mdl.set("marquee", search.game->getMetadata().get("marquee")); else if (!result.marqueeUrl.empty()) { std::string marqueePath = getSaveAsPath(search, "marquee", ext); @@ -278,8 +278,8 @@ MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const Scrape }, "marquee", result.mdl.getName())); } - if (!search.overWriteMedias && Settings::getInstance()->getBool("ScrapeVideos") && Utils::FileSystem::exists(search.game->metadata.get("video"))) - mResult.mdl.set("video", search.game->metadata.get("video")); + if (!search.overWriteMedias && Settings::getInstance()->getBool("ScrapeVideos") && Utils::FileSystem::exists(search.game->getMetadata().get("video"))) + mResult.mdl.set("video", search.game->getMetadata().get("video")); else if (!result.videoUrl.empty()) { std::string videoPath = getSaveAsPath(search, "video", ".mp4"); diff --git a/es-app/src/scrapers/ThreadedScraper.cpp b/es-app/src/scrapers/ThreadedScraper.cpp index e59b2d96b0..3d47ba5535 100644 --- a/es-app/src/scrapers/ThreadedScraper.cpp +++ b/es-app/src/scrapers/ThreadedScraper.cpp @@ -151,13 +151,13 @@ void ThreadedScraper::processMedias(ScraperSearchResult result) if (result.hadMedia()) mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch); - search.game->metadata.importScrappedMetadata(result.mdl); + search.game->getMetadata().importScrappedMetadata(result.mdl); } void ThreadedScraper::acceptResult(const ScraperSearchResult& result) { ScraperSearchParams& search = mSearchQueue.front(); - search.game->metadata = result.mdl; + search.game->getMetadata().importScrappedMetadata(result.mdl);// = result.mdl; } void ThreadedScraper::start(Window* window, const std::queue& searches) diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index d2ba04163e..35b7aeb178 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -296,7 +296,7 @@ void DetailedGameListView::initMDValues() std::string DetailedGameListView::getMetadata(FileData* file, std::string name) { - std::string ret = file->metadata.get(name); + std::string ret = file->getMetadata().get(name); if (ret == "unknown") return _(ret); diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 8829aa45b1..ca700eafda 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -552,21 +552,21 @@ void GridGameListView::updateInfoPanel() if (mMarquee != nullptr) mMarquee->setImage(file->getMarqueePath(), false, mMarquee->getMaxSizeInfo()); - mDescription.setText(file->metadata.get("desc")); + mDescription.setText(file->getMetadata().get("desc")); mDescContainer.reset(); - 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")); - mName.setValue(file->metadata.get("name")); + mRating.setValue(file->getMetadata().get("rating")); + mReleaseDate.setValue(file->getMetadata().get("releasedate")); + mDeveloper.setValue(file->getMetadata().get("developer")); + mPublisher.setValue(file->getMetadata().get("publisher")); + mGenre.setValue(file->getMetadata().get("genre")); + mPlayers.setValue(file->getMetadata().get("players")); + mName.setValue(file->getMetadata().get("name")); if(file->getType() == GAME) { - mLastPlayed.setValue(file->metadata.get("lastplayed")); - mPlayCount.setValue(file->metadata.get("playcount")); + mLastPlayed.setValue(file->getMetadata().get("lastplayed")); + mPlayCount.setValue(file->getMetadata().get("playcount")); } fadingOut = false; diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index fbb281e4ac..98790932bf 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -293,21 +293,21 @@ void VideoGameListView::updateInfoPanel() if (mImage != nullptr) mImage->setImage(file->getThumbnailPath()); - mDescription.setText(file->metadata.get("desc")); + mDescription.setText(file->getMetadata().get("desc")); mDescContainer.reset(); - 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")); - mName.setValue(file->metadata.get("name")); + mRating.setValue(file->getMetadata().get("rating")); + mReleaseDate.setValue(file->getMetadata().get("releasedate")); + mDeveloper.setValue(file->getMetadata().get("developer")); + mPublisher.setValue(file->getMetadata().get("publisher")); + mGenre.setValue(file->getMetadata().get("genre")); + mPlayers.setValue(file->getMetadata().get("players")); + mName.setValue(file->getMetadata().get("name")); if(file->getType() == GAME) { - mLastPlayed.setValue(file->metadata.get("lastplayed")); - mPlayCount.setValue(file->metadata.get("playcount")); + mLastPlayed.setValue(file->getMetadata().get("lastplayed")); + mPlayCount.setValue(file->getMetadata().get("playcount")); } fadingOut = false; diff --git a/es-core/src/MameNames.cpp b/es-core/src/MameNames.cpp index e0dee5c4b6..7230b8a9df 100644 --- a/es-core/src/MameNames.cpp +++ b/es-core/src/MameNames.cpp @@ -77,7 +77,7 @@ MameNames::MameNames() for(pugi::xml_node biosNode = doc.child("bios"); biosNode; biosNode = biosNode.next_sibling("bios")) { std::string bios = biosNode.text().get(); - mMameBioses.push_back(bios); + mMameBioses.insert(bios); } // Read devices @@ -97,9 +97,9 @@ MameNames::MameNames() } for(pugi::xml_node deviceNode = doc.child("device"); deviceNode; deviceNode = deviceNode.next_sibling("device")) - { + { std::string device = deviceNode.text().get(); - mMameDevices.push_back(device); + mMameDevices.insert(device); } } // MameNames @@ -130,31 +130,10 @@ std::string MameNames::getRealName(const std::string& _mameName) const bool MameNames::isBios(const std::string& _biosName) { - return MameNames::find(mMameBioses, _biosName); - + return (mMameBioses.find(_biosName) != mMameBioses.cend()); } // isBios const bool MameNames::isDevice(const std::string& _deviceName) { - return MameNames::find(mMameDevices, _deviceName); - + return (mMameDevices.find(_deviceName) != mMameDevices.cend()); } // isDevice - -const bool MameNames::find(std::vector devices, const std::string& name) -{ - size_t start = 0; - size_t end = devices.size(); - - while(start < end) - { - const size_t index = (start + end) / 2; - const int compare = strcmp(devices[index].c_str(), name.c_str()); - - if(compare < 0) start = index + 1; - else if( compare > 0) end = index; - else return true; - } - - return false; - -} diff --git a/es-core/src/MameNames.h b/es-core/src/MameNames.h index 02036d65d4..45d21ec84b 100644 --- a/es-core/src/MameNames.h +++ b/es-core/src/MameNames.h @@ -4,6 +4,7 @@ #include #include +#include class MameNames { @@ -32,10 +33,9 @@ class MameNames static MameNames* sInstance; namePairVector mNamePairs; - std::vector mMameBioses; - std::vector mMameDevices; - const bool find(const std::vector devices, const std::string& name); + std::unordered_set mMameBioses; + std::unordered_set mMameDevices; }; // MameNames diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index fc571473de..599bbc6812 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -274,7 +274,7 @@ ThemeData* ThemeData::mDefaultTheme = nullptr; // helper unsigned int getHexColor(const char* str) { - ThemeException error; +// ThemeException error; if (!str) { //throw error << "Empty color"; @@ -303,15 +303,19 @@ unsigned int getHexColor(const char* str) std::string ThemeData::resolvePlaceholders(const char* in) { + if (in == nullptr || in[0] == '/0') + return in; + std::string inStr(in); +// if(inStr.empty()) +// return inStr; - if(inStr.empty()) + const size_t variableBegin = inStr.find("${"); + if (variableBegin == std::string::npos) return inStr; - const size_t variableBegin = inStr.find("${"); const size_t variableEnd = inStr.find("}", variableBegin); - - if((variableBegin == std::string::npos) || (variableEnd == std::string::npos)) + if(variableEnd == std::string::npos) return inStr; std::string prefix = inStr.substr(0, variableBegin); @@ -353,7 +357,7 @@ void ThemeData::loadFile(const std::string system, std::map& ThemeData::getMenuTheme() std::string ThemeData::resolveSystemVariable(const std::string& systemThemeFolder, const std::string& path) { - std::string result = path; - - size_t start_pos = result.find("$system"); + size_t start_pos = path.find("$system"); if (start_pos == std::string::npos) return path; + std::string result = path; result.replace(start_pos, 7, systemThemeFolder); return result; } @@ -576,8 +579,8 @@ void ThemeData::parseVariable(const pugi::xml_node& node) void ThemeData::parseVariables(const pugi::xml_node& root) { - ThemeException error; - error.setFiles(mPaths); + // ThemeException error; + // error.setFiles(mPaths); for (pugi::xml_node variables = root.child("variables"); variables; variables = variables.next_sibling("variables")) { @@ -699,8 +702,8 @@ void ThemeData::parseTheme(const pugi::xml_node& root) void ThemeData::parseViews(const pugi::xml_node& root) { - ThemeException error; - error.setFiles(mPaths); + // ThemeException error; + // error.setFiles(mPaths); // parse views for (pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) @@ -788,8 +791,8 @@ void ThemeData::parseCustomView(const pugi::xml_node& node, const pugi::xml_node void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view, bool overwriteElements) { - ThemeException error; - error.setFiles(mPaths); + // ThemeException error; + // error.setFiles(mPaths); if (!parseFilterAttributes(root)) return; @@ -894,8 +897,8 @@ bool ThemeData::parseRegion(const pugi::xml_node& node) void ThemeData::parseElement(const pugi::xml_node& root, const std::map& typeMap, ThemeElement& element, bool overwrite) { - ThemeException error; - error.setFiles(mPaths); + // ThemeException error; + // error.setFiles(mPaths); element.type = root.name(); element.extra = root.attribute("extra").as_bool(false); @@ -992,8 +995,7 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapfileExists(path)) { std::stringstream ss; - ss << " Warning " << error.msg; // "from theme yadda yadda, included file yadda yadda - ss << "could not find file \"" << node.text().get() << "\" "; + ss << " Warning : could not find file \"" << node.text().get() << "\" "; if(node.text().get() != path) ss << "(which resolved to \"" << path << "\") "; LOG(LogWarning) << ss.str(); From 5852a1234e320eeb07623b441fb39b736af6ef3e Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 8 Nov 2019 11:02:02 +0100 Subject: [PATCH 217/336] Windows : use FindFirstFileExW to speed up directory enumerations --- es-core/src/utils/FileSystemUtil.cpp | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 67084dcf2a..1440a8e971 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -79,7 +79,11 @@ namespace Utils WIN32_FIND_DATAW findData; std::string wildcard = path + "/*"; - HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); + // HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); + + HANDLE hFind = FindFirstFileExW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), + FINDEX_INFO_LEVELS::FindExInfoStandard, &findData, FINDEX_SEARCH_OPS::FindExSearchNameMatch + , NULL, FIND_FIRST_EX_LARGE_FETCH); if (hFind != INVALID_HANDLE_VALUE) { @@ -156,7 +160,13 @@ namespace Utils WIN32_FIND_DATAW findData; std::string wildcard = path + "/*"; - HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); + // HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); + + HANDLE hFind = FindFirstFileExW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), + FINDEX_INFO_LEVELS::FindExInfoStandard, &findData, FINDEX_SEARCH_OPS::FindExSearchNameMatch + , NULL, FIND_FIRST_EX_LARGE_FETCH); + + //handle = FindFirstFileEx(search, 0, &FindFileData, 2, NULL, 0); if(hFind != INVALID_HANDLE_VALUE) { From 8c5e3b051d9b67192ab081e3a066f0555fd7c731 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 10 Nov 2019 02:04:18 +0100 Subject: [PATCH 218/336] Scaper : Fixes + screenscraper http 400 management --- .../src/components/ScraperSearchComponent.cpp | 19 +++++++++- es-app/src/guis/GuiMenu.cpp | 2 +- es-app/src/guis/GuiScraperStart.cpp | 2 +- es-app/src/scrapers/Scraper.cpp | 11 ++++-- es-app/src/scrapers/ThreadedScraper.cpp | 14 ++++++-- es-core/src/AsyncHandle.h | 7 +++- es-core/src/HttpReq.h | 1 + es-core/src/components/BusyComponent.cpp | 36 +++++++++++++++++++ es-core/src/components/BusyComponent.h | 9 +++++ 9 files changed, 93 insertions(+), 8 deletions(-) diff --git a/es-app/src/components/ScraperSearchComponent.cpp b/es-app/src/components/ScraperSearchComponent.cpp index b8bc53ed0f..6a73a910cd 100644 --- a/es-app/src/components/ScraperSearchComponent.cpp +++ b/es-app/src/components/ScraperSearchComponent.cpp @@ -388,8 +388,25 @@ void ScraperSearchComponent::update(int deltaTime) { GuiComponent::update(deltaTime); - if(mBlockAccept) + if (mBlockAccept) { + if (mMDResolveHandle && mMDResolveHandle->status() == ASYNC_IN_PROGRESS) + { + if (mSearchType == ALWAYS_ACCEPT_FIRST_RESULT && !mResultThumbnail->hasImage()) + { + ScraperSearchResult result = mMDResolveHandle->getResult(); + + if (!result.mdl.get("thumbnail").empty()) + mResultThumbnail->setImage(result.mdl.get("thumbnail")); + else if (!result.mdl.get("image").empty()) + mResultThumbnail->setImage(result.mdl.get("image")); + } + + mBusyAnim.setText(_("DOWNLOADING") + " " + Utils::String::toUpper(mMDResolveHandle->getCurrentItem())); + } + else if (mSearchHandle && mSearchHandle->status() == ASYNC_IN_PROGRESS) + mBusyAnim.setText(_("SEARCHING")); + mBusyAnim.update(deltaTime); } diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index b5559c722f..64ea2a43fc 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -88,7 +88,7 @@ void GuiMenu::openScraperSettings() // Image source : tag std::string imageSourceName = Settings::getInstance()->getString("ScrapperImageSrc"); auto imageSource = std::make_shared< OptionListComponent >(mWindow, _("IMAGE SOURCE"), false); - imageSource->add(_("NONE"), "", imageSourceName.empty()); + //imageSource->add(_("NONE"), "", imageSourceName.empty()); imageSource->add(_("SCREENSHOT"), "ss", imageSourceName == "ss"); imageSource->add(_("TITLE SCREENSHOT"), "sstitle", imageSourceName == "sstitle"); imageSource->add(_("MIX"), "mixrbv1", imageSourceName == "mixrbv1"); diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index b94189bf73..afa16e4e56 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -83,7 +83,7 @@ void GuiScraperStart::pressedStart() if((*it)->getPlatformIds().empty()) { mWindow->pushGui(new GuiMsgBox(mWindow, - Utils::String::toUpper("Warning: some of your selected systems do not have a platform set. Results may be even more inaccurate than usual!\nContinue anyway?"), + _(Utils::String::toUpper("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; diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index b6489f839b..9f4530cdf5 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -147,9 +147,16 @@ void ScraperHttpRequest::update() return; } + if (status == HttpReq::REQ_400_TOOMANYSCRAPS) + { + setError(400, "SCRAP LIMIT REACHED TODAY (400)"); + return; + } + + if (status == HttpReq::REQ_426_BLACKLISTED) { - setError("THE SOFTWARE HAS BEEN BLACKLISTED (426)"); + setError(246, "THE SOFTWARE HAS BEEN BLACKLISTED (426)"); return; } @@ -230,7 +237,7 @@ MDResolveHandle::MDResolveHandle(const ScraperSearchResult& result, const Scrape if (!search.overWriteMedias && ss && !Settings::getInstance()->getString("ScrapperThumbSrc").empty() && Utils::FileSystem::exists(search.game->getMetadata().get("thumbnail"))) mResult.mdl.set("thumbnail", search.game->getMetadata().get("thumbnail")); - else if (!result.thumbnailUrl.empty() && result.thumbnailUrl.find(result.imageUrl) != 0) + else if (!result.thumbnailUrl.empty() && (result.imageUrl.empty() || result.thumbnailUrl.find(result.imageUrl) != 0)) { std::string thumbPath = getSaveAsPath(search, "thumb", ext); diff --git a/es-app/src/scrapers/ThreadedScraper.cpp b/es-app/src/scrapers/ThreadedScraper.cpp index 3d47ba5535..b745e59ca0 100644 --- a/es-app/src/scrapers/ThreadedScraper.cpp +++ b/es-app/src/scrapers/ThreadedScraper.cpp @@ -67,6 +67,7 @@ void ThreadedScraper::run() auto status = mSearchHandle->status(); auto results = mSearchHandle->getResults(); auto statusString = mSearchHandle->getStatusString(); + auto httpCode = mSearchHandle->getErrorCode(); mSearchHandle.reset(); @@ -82,7 +83,7 @@ void ThreadedScraper::run() } else if (status == ASYNC_ERROR) { - if (statusString.find("426") != std::string::npos) // Blacklist + if (httpCode == 426) // Blacklist { mExit = true; mWindow->postToUiThread([](Window* w) @@ -91,8 +92,17 @@ void ThreadedScraper::run() }); break; } + else if (httpCode == 400) // Too many scraps + { + mExit = true; + mWindow->postToUiThread([](Window* w) + { + w->pushGui(new GuiMsgBox(w, _("SCRAPE FAILED : SCRAP LIMIT REACHED TODAY"))); + }); + break; + } else - mErrors.push_back(statusString); + mErrors.push_back(statusString); } } diff --git a/es-core/src/AsyncHandle.h b/es-core/src/AsyncHandle.h index 5368dae968..5639bdd248 100644 --- a/es-core/src/AsyncHandle.h +++ b/es-core/src/AsyncHandle.h @@ -15,7 +15,7 @@ enum AsyncHandleStatus class AsyncHandle { public: - AsyncHandle() : mStatus(ASYNC_IN_PROGRESS) {}; + AsyncHandle() : mStatus(ASYNC_IN_PROGRESS), mErrorCode(200) {}; virtual ~AsyncHandle() {}; virtual void update() = 0; @@ -25,6 +25,8 @@ class AsyncHandle virtual int getPercent() { return -1; } + int getErrorCode() { return mErrorCode; } + // User-friendly string of our current status. Will return error message if status() == SEARCH_ERROR. inline std::string getStatusString() { @@ -43,8 +45,11 @@ class AsyncHandle protected: inline void setStatus(AsyncHandleStatus status) { mStatus = status; } + inline void setError(const std::string& error) { setStatus(ASYNC_ERROR); mError = error; } + inline void setError(int errorCode, const std::string& error) { setStatus(ASYNC_ERROR); mError = error; mErrorCode = errorCode; } + int mErrorCode; std::string mError; AsyncHandleStatus mStatus; }; diff --git a/es-core/src/HttpReq.h b/es-core/src/HttpReq.h index ed65eee1f4..ebb93695be 100644 --- a/es-core/src/HttpReq.h +++ b/es-core/src/HttpReq.h @@ -37,6 +37,7 @@ class HttpReq REQ_SUCCESS, //request completed successfully, get it with getContent() REQ_IO_ERROR, //some error happened, get it with getErrorMsg() + REQ_400_TOOMANYSCRAPS, //HTTP response status code 400 happened REQ_404_NOTFOUND, //HTTP response status code 404 happened REQ_426_BLACKLISTED, //HTTP response status code 426 happened REQ_429_TOOMANYREQUESTS, //HTTP response status code 429 happened diff --git a/es-core/src/components/BusyComponent.cpp b/es-core/src/components/BusyComponent.cpp index ad5d2de4de..aa911aab91 100644 --- a/es-core/src/components/BusyComponent.cpp +++ b/es-core/src/components/BusyComponent.cpp @@ -22,6 +22,8 @@ BusyComponent::BusyComponent(Window* window) : GuiComponent(window), mBackground.setCenterColor(theme->Background.centerColor); mBackground.setCornerSize(theme->Background.cornerSize); + mutex = SDL_CreateMutex(); + mAnimation = std::make_shared(mWindow); mAnimation->load(&BUSY_ANIMATION_DEF); mText = std::make_shared(mWindow, _("WORKING..."), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); @@ -34,6 +36,40 @@ BusyComponent::BusyComponent(Window* window) : GuiComponent(window), addChild(&mGrid); } +BusyComponent::~BusyComponent() +{ + SDL_DestroyMutex(mutex); +} + +void BusyComponent::setText(std::string txt) +{ + if (SDL_LockMutex(mutex) == 0) + { + if (threadMessage != txt) + { + threadMessage = txt; + threadMessagechanged = true; + } + + SDL_UnlockMutex(mutex); + } +} + +void BusyComponent::render(const Transform4x4f& parentTrans) +{ + if (SDL_LockMutex(mutex) == 0) + { + if (threadMessagechanged) + { + threadMessagechanged = false; + mText->setText(threadMessage); + onSizeChanged(); + } + SDL_UnlockMutex(mutex); + } + GuiComponent::render(parentTrans); +} + void BusyComponent::onSizeChanged() { mGrid.setSize(mSize); diff --git a/es-core/src/components/BusyComponent.h b/es-core/src/components/BusyComponent.h index fe996a31cc..e589bb0e73 100644 --- a/es-core/src/components/BusyComponent.h +++ b/es-core/src/components/BusyComponent.h @@ -5,6 +5,7 @@ #include "components/ComponentGrid.h" #include "components/NinePatchComponent.h" #include "GuiComponent.h" +#include class AnimatedImageComponent; class TextComponent; @@ -13,17 +14,25 @@ class BusyComponent : public GuiComponent { public: BusyComponent(Window* window); + ~BusyComponent(); void onSizeChanged() override; + void setText(std::string txt); void reset(); // reset to frame 0 + virtual void render(const Transform4x4f& parentTrans); + private: NinePatchComponent mBackground; ComponentGrid mGrid; std::shared_ptr mAnimation; std::shared_ptr mText; + + SDL_mutex *mutex; + bool threadMessagechanged; + std::string threadMessage; }; #endif // ES_CORE_COMPONENTS_BUSY_COMPONENT_H From bbcd68d6892c166f94579e40ce5922749afd78d3 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 12 Nov 2019 17:50:48 +0100 Subject: [PATCH 219/336] ImageGrid : Things... --- .../src/views/gamelist/GridGameListView.cpp | 17 +- es-core/src/ThemeData.cpp | 2 + es-core/src/components/GridTileComponent.cpp | 225 +++++++++++++----- es-core/src/components/GridTileComponent.h | 119 ++++++++- es-core/src/components/ImageComponent.cpp | 60 ++++- es-core/src/components/ImageComponent.h | 3 + es-core/src/components/ImageGridComponent.h | 15 +- es-core/src/components/VideoComponent.cpp | 1 + es-core/src/components/VideoComponent.h | 5 + es-core/src/components/VideoVlcComponent.cpp | 35 ++- es-core/src/renderers/Renderer.h | 5 + es-core/src/renderers/Renderer_GL21.cpp | 61 +++++ es-core/src/renderers/Renderer_GLES10.cpp | 55 +++++ 13 files changed, 515 insertions(+), 88 deletions(-) diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index ca700eafda..13012f345a 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -240,7 +240,7 @@ void GridGameListView::populateList(const std::vector& files) if (mCursorStack.size()) { FileData* placeholder = new FileData(PLACEHOLDER, "..", this->mRoot->getSystem()); - mGrid.add(". .", "", "", "", placeholder); + mGrid.add(". .", "", "", "", false, placeholder); } std::string systemName = mRoot->getSystem()->getFullName(); @@ -257,10 +257,7 @@ void GridGameListView::populateList(const std::vector& files) if (!(*it)->getFavorite()) continue; - if (showFavoriteIcon) - mGrid.add(_U("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), *it); - else - mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), *it); + mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), showFavoriteIcon, *it); } } @@ -273,15 +270,15 @@ void GridGameListView::populateList(const std::vector& files) if (showFavoriteIcon) { - mGrid.add(_U("\uF006 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), *it); + mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), true, *it); continue; } } if (((*it)->getType() == FOLDER) && Utils::FileSystem::exists(getImagePath(*it))) - mGrid.add(_U("\uF114 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), *it); + mGrid.add(_U("\uF114 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), (*it)->getFavorite(), *it); else - mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), *it); + mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), (*it)->getFavorite(), *it); } } else @@ -617,8 +614,8 @@ void GridGameListView::updateInfoPanel() void GridGameListView::addPlaceholder() { // empty grid - add a placeholder - FileData* placeholder = new FileData(PLACEHOLDER, "", this->mRoot->getSystem()); - mGrid.add(placeholder->getName(), "", "", "", placeholder); + FileData* placeholder = new FileData(PLACEHOLDER, "<" + _("No Entries Found") + ">", this->mRoot->getSystem()); + mGrid.add(placeholder->getName(), "", "", "", false, placeholder); } void GridGameListView::launch(FileData* game) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 599bbc6812..66ffbfc3d5 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -34,6 +34,7 @@ std::map> The { "reflexionOnFrame", BOOLEAN }, { "horizontalAlignment", STRING }, { "verticalAlignment", STRING }, + { "roundCorners", FLOAT }, { "flipX", BOOLEAN }, { "flipY", BOOLEAN }, { "zIndex", FLOAT } } }, @@ -201,6 +202,7 @@ std::map> The { "delay", FLOAT }, { "effect", STRING }, { "visible", BOOLEAN }, + { "roundCorners", FLOAT }, { "zIndex", FLOAT }, { "snapshotSource", STRING }, // image, thumbnail, marquee { "showSnapshotNoVideo", BOOLEAN }, diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 8d591146f0..2f980027d9 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -25,6 +25,7 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mAnimPosition = Vector3f(0, 0); mVideo = nullptr; mMarquee = nullptr; + mFavorite = nullptr; mIsDefaultImage = false; mLabelVisible = false; @@ -32,7 +33,7 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa resetProperties(); - mImage = std::make_shared(mWindow); + mImage = new ImageComponent(mWindow); mImage->setOrigin(0.5f, 0.5f); addChild(&mBackground); @@ -93,9 +94,21 @@ void GridTileComponent::forceSize(Vector2f size, float selectedZoom) GridTileComponent::~GridTileComponent() { + if (mImage != nullptr) + delete mImage; + + if (mFavorite != nullptr) + delete mFavorite; + + if (mMarquee != nullptr) + delete mMarquee; + if (mVideo != nullptr) delete mVideo; + mFavorite = nullptr; + mMarquee = nullptr; + mImage = nullptr; mVideo = nullptr; } @@ -160,18 +173,16 @@ void GridTileComponent::resize() float imageWidth = size.x() - paddingX * 2.0; float imageHeight = size.y() - topPadding - bottomPadding; - if (mMarquee != nullptr) - { - mMarquee->setPosition( - currentProperties.mMarqueePos.x() * size.x(), - currentProperties.mMarqueePos.y() * size.y()); - - mMarquee->setMaxSize( - currentProperties.mMarqueeSize.x() * size.x(), - currentProperties.mMarqueeSize.y() * size.y()); - } - if (mImage != nullptr) + if (currentProperties.Favorite.Loaded) + currentProperties.Favorite.updateImageComponent(mFavorite, Vector2f(size.x(), (size.y() - height)), false); + + if (currentProperties.Marquee.Loaded) + currentProperties.Marquee.updateImageComponent(mMarquee, Vector2f(size.x(), (size.y() - height)), true); + + if (currentProperties.Image.Loaded) + currentProperties.Image.updateImageComponent(mImage, Vector2f(size.x(), (size.y() - height)), false); + else if (mImage != nullptr) { mImage->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); mImage->setColorShift(currentProperties.mImageColor); @@ -216,6 +227,9 @@ void GridTileComponent::resize() if (currentProperties.mImageSizeMode == "minSize") { + if (mImage != nullptr) + mVideo->setRoundCorners(mImage->getRoundCorners()); + auto vs = mVideo->getVideoSize(); if (vs == Vector2f(0, 0)) vs = Vector2f(640, 480); @@ -350,6 +364,9 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) else if (!mLabelVisible && mIsDefaultImage) mLabel.render(trans); + if (mFavorite != nullptr && mFavorite->hasImage() && mFavorite->isVisible()) + mFavorite->render(trans); + if (mLabelMerged && isMinSize) Renderer::popClipRect(); } @@ -374,6 +391,19 @@ void GridTileComponent::createMarquee() addChild(mMarquee); } +void GridTileComponent::createFavorite() +{ + if (mFavorite != nullptr) + return; + + mFavorite = new ImageComponent(mWindow); + mFavorite->setOrigin(0.5f, 0.5f); + mFavorite->setDefaultZIndex(35); + mFavorite->setVisible(false); + + addChild(mFavorite); +} + void GridTileComponent::createVideo() { if (mVideo != nullptr) @@ -457,24 +487,47 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons } } + elem = theme->getElement(view, "gridtile.image", "image"); + if (elem) + { + mImage->applyTheme(theme, view, "gridtile.image", ThemeFlags::ALL ^ (ThemeFlags::PATH)); + + mDefaultProperties.Image.color = mDefaultProperties.mImageColor; + mDefaultProperties.Image.colorEnd = mDefaultProperties.mImageColor; + + mSelectedProperties.Image.color = mSelectedProperties.mImageColor; + mSelectedProperties.Image.colorEnd = mSelectedProperties.mImageColor; + + if (mDefaultProperties.mImageSizeMode == "minSize") + mDefaultProperties.Image.sizeMode = 1; + else if (mDefaultProperties.mImageSizeMode == "maxSize") + mDefaultProperties.Image.sizeMode = 2; + else + mDefaultProperties.Image.sizeMode = 0; + + mSelectedProperties.Image.sizeMode = mDefaultProperties.Image.sizeMode; + + mDefaultProperties.Image.loadElement(elem); + mSelectedProperties.Image.loadElement(elem); + + elem = theme->getElement(view, "gridtile.image:selected", "image"); + if (elem) + mSelectedProperties.Image.loadElement(elem); + } + + elem = theme->getElement(view, "gridtile.marquee", "image"); if (elem) { createMarquee(); mMarquee->applyTheme(theme, view, "gridtile.marquee", ThemeFlags::ALL ^ (ThemeFlags::PATH)); - if (elem->has("pos")) - mDefaultProperties.mMarqueePos = elem->get("pos"); - else - mDefaultProperties.mMarqueeSize = Vector2f(0, 0); - - if (elem->has("pos")) - mDefaultProperties.mMarqueeSize = elem->get("size"); - else - mDefaultProperties.mMarqueeSize = Vector2f(1, 1); + mDefaultProperties.Marquee.loadElement(elem); + mSelectedProperties.Marquee = mDefaultProperties.Marquee; - mSelectedProperties.mMarqueePos = mDefaultProperties.mMarqueePos; - mSelectedProperties.mMarqueeSize = mDefaultProperties.mMarqueeSize; + elem = theme->getElement(view, "gridtile.marquee:selected", "image"); + if (elem) + mSelectedProperties.Marquee.loadElement(elem); } else if (mMarquee != nullptr) { @@ -483,6 +536,27 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mMarquee = nullptr; } + elem = theme->getElement(view, "gridtile.favorite", "image"); + if (elem) + { + createFavorite(); + mFavorite->applyTheme(theme, view, "gridtile.favorite", ThemeFlags::ALL); + + mDefaultProperties.Favorite.sizeMode = 0; + mDefaultProperties.Favorite.loadElement(elem); + mSelectedProperties.Favorite = mDefaultProperties.Favorite; + + elem = theme->getElement(view, "gridtile.favorite:selected", "image"); + if (elem) + mSelectedProperties.Favorite.loadElement(elem); + } + else if (mFavorite != nullptr) + { + removeChild(mFavorite); + delete mFavorite; + mFavorite = nullptr; + } + // Apply theme to the selected gridtile // NOTE that some of the default gridtile properties influence on the selected gridtile properties // See THEMES.md for more informations @@ -590,6 +664,9 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mLabel.applyTheme(theme, view, element, properties); elem = theme->getElement(view, "gridtile_selected", "text"); + if (elem == nullptr) + elem = theme->getElement(view, "gridtile:selected", "text"); + if (elem != NULL) { if (elem->has("pos") && mDefaultProperties.mLabelPos.x() >= 0) @@ -674,6 +751,15 @@ void GridTileComponent::setMarquee(const std::string& path) resize(); } +void GridTileComponent::setFavorite(bool favorite) +{ + if (mFavorite == nullptr) + return; + + mFavorite->setVisible(favorite); + resize(); +} + void GridTileComponent::resetImages() { setLabel(""); @@ -838,63 +924,78 @@ void GridTileComponent::setVisible(bool visible) mVisible = visible; } +Vector2f GridTileComponent::mixVectors(const Vector2f& def, const Vector2f& sel, float percent) +{ + if (def == sel) + return def; + + float x = def.x() * (1.0 - percent) + sel.x() * percent; + float y = def.y() * (1.0 - percent) + sel.y() * percent; + return Vector2f(x, y); +} + +unsigned int GridTileComponent::mixUnsigned(const unsigned int def, const unsigned int sel, float percent) +{ + if (def == sel) + return def; + + return def * (1.0 - percent) + sel * percent; +} + GridTileProperties GridTileComponent::getCurrentProperties() { + using namespace Renderer; + GridTileProperties mMixedProperties; if (mSelectedZoomPercent == 0.0f || mSelectedZoomPercent == 1.0f) return mSelected ? mSelectedProperties : mDefaultProperties; - auto def = mSelected ? mSelectedProperties : mDefaultProperties; - mMixedProperties = mSelected ? mSelectedProperties : mDefaultProperties; - if (mDefaultProperties.mSize != mSelectedProperties.mSize) - { - float x = mDefaultProperties.mSize.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mSize.x() * mSelectedZoomPercent; - float y = mDefaultProperties.mSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mSize.y() * mSelectedZoomPercent; - mMixedProperties.mSize = Vector2f(x, y); - } + mMixedProperties.mSize = mixVectors(mDefaultProperties.mSize, mSelectedProperties.mSize, mSelectedZoomPercent); + mMixedProperties.mPadding = mixVectors(mDefaultProperties.mPadding, mSelectedProperties.mPadding, mSelectedZoomPercent); + + mMixedProperties.mLabelPos = mixVectors(mDefaultProperties.mLabelPos, mSelectedProperties.mLabelPos, mSelectedZoomPercent); + mMixedProperties.mLabelSize = mixVectors(mDefaultProperties.mLabelSize, mSelectedProperties.mLabelSize, mSelectedZoomPercent); + mMixedProperties.mLabelColor = mixColors(mDefaultProperties.mLabelColor, mSelectedProperties.mLabelColor, mSelectedZoomPercent); + mMixedProperties.mLabelBackColor = mixColors(mDefaultProperties.mLabelBackColor, mSelectedProperties.mLabelBackColor, mSelectedZoomPercent); + mMixedProperties.mLabelGlowColor = mixColors(mDefaultProperties.mLabelGlowColor, mSelectedProperties.mLabelGlowColor, mSelectedZoomPercent); + mMixedProperties.mLabelGlowSize = mixUnsigned(mDefaultProperties.mLabelGlowSize, mSelectedProperties.mLabelGlowSize, mSelectedZoomPercent); - if (mDefaultProperties.mPadding != mSelectedProperties.mPadding) + mMixedProperties.mImageColor = mixColors(mDefaultProperties.mImageColor, mSelectedProperties.mImageColor, mSelectedZoomPercent); + mMixedProperties.mMirror = mixVectors(mDefaultProperties.mMirror, mSelectedProperties.mMirror, mSelectedZoomPercent); + + if (mDefaultProperties.Image.Loaded) { - float x = mDefaultProperties.mPadding.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.x() * mSelectedZoomPercent; - float y = mDefaultProperties.mPadding.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mPadding.y() * mSelectedZoomPercent; - mMixedProperties.mPadding = Vector2f(x, y); + mMixedProperties.Image.pos = mixVectors(mDefaultProperties.Image.pos, mSelectedProperties.Image.pos, mSelectedZoomPercent); + mMixedProperties.Image.size = mixVectors(mDefaultProperties.Image.size, mSelectedProperties.Image.size, mSelectedZoomPercent); + mMixedProperties.Image.origin = mixVectors(mDefaultProperties.Image.origin, mSelectedProperties.Image.origin, mSelectedZoomPercent); + mMixedProperties.Image.color = mixColors(mDefaultProperties.Image.color, mSelectedProperties.Image.color, mSelectedZoomPercent); + mMixedProperties.Image.colorEnd = mixColors(mDefaultProperties.Image.colorEnd, mSelectedProperties.Image.colorEnd, mSelectedZoomPercent); + mMixedProperties.Image.reflexion = mixVectors(mDefaultProperties.Image.reflexion, mSelectedProperties.Image.reflexion, mSelectedZoomPercent); } - if (mDefaultProperties.mImageColor != mSelectedProperties.mImageColor) + if (mDefaultProperties.Marquee.Loaded) { - mMixedProperties.mImageColor = Renderer::mixColors(mDefaultProperties.mImageColor, mSelectedProperties.mImageColor, mSelectedZoomPercent); + mMixedProperties.Marquee.pos = mixVectors(mDefaultProperties.Marquee.pos, mSelectedProperties.Marquee.pos, mSelectedZoomPercent); + mMixedProperties.Marquee.size = mixVectors(mDefaultProperties.Marquee.size, mSelectedProperties.Marquee.size, mSelectedZoomPercent); + mMixedProperties.Marquee.origin = mixVectors(mDefaultProperties.Marquee.origin, mSelectedProperties.Marquee.origin, mSelectedZoomPercent); + mMixedProperties.Marquee.color = mixColors(mDefaultProperties.Marquee.color, mSelectedProperties.Marquee.color, mSelectedZoomPercent); + mMixedProperties.Marquee.colorEnd = mixColors(mDefaultProperties.Marquee.colorEnd, mSelectedProperties.Marquee.colorEnd, mSelectedZoomPercent); + mMixedProperties.Marquee.reflexion = mixVectors(mDefaultProperties.Marquee.reflexion, mSelectedProperties.Marquee.reflexion, mSelectedZoomPercent); } - if (mDefaultProperties.mLabelSize != mSelectedProperties.mLabelSize) - mMixedProperties.mLabelSize = Vector2f(mDefaultProperties.mLabelSize.x(), - mDefaultProperties.mLabelSize.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mLabelSize.y() * mSelectedZoomPercent); - - if (mDefaultProperties.mLabelColor != mSelectedProperties.mLabelColor) - mMixedProperties.mLabelColor = Renderer::mixColors(mDefaultProperties.mLabelColor, mSelectedProperties.mLabelColor, mSelectedZoomPercent); - - if (mDefaultProperties.mLabelBackColor != mSelectedProperties.mLabelBackColor) - mMixedProperties.mLabelBackColor = Renderer::mixColors(mDefaultProperties.mLabelBackColor, mSelectedProperties.mLabelBackColor, mSelectedZoomPercent); - - if (mDefaultProperties.mLabelGlowColor != mSelectedProperties.mLabelGlowColor) - mMixedProperties.mLabelGlowColor = Renderer::mixColors(mDefaultProperties.mLabelGlowColor, mSelectedProperties.mLabelGlowColor, mSelectedZoomPercent); - - if (mDefaultProperties.mLabelGlowSize != mSelectedProperties.mLabelGlowSize) - mMixedProperties.mLabelGlowSize = mDefaultProperties.mLabelGlowSize * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mLabelGlowSize * mSelectedZoomPercent; - - if (mDefaultProperties.mMirror != mSelectedProperties.mMirror) + if (mDefaultProperties.Favorite.Loaded) { - float x = mDefaultProperties.mMirror.x() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mMirror.x() * mSelectedZoomPercent; - float y = mDefaultProperties.mMirror.y() * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mMirror.y() * mSelectedZoomPercent; - mMixedProperties.mMirror = Vector2f(x, y); + mMixedProperties.Favorite.pos = mixVectors(mDefaultProperties.Favorite.pos, mSelectedProperties.Favorite.pos, mSelectedZoomPercent); + mMixedProperties.Favorite.size = mixVectors(mDefaultProperties.Favorite.size, mSelectedProperties.Favorite.size, mSelectedZoomPercent); + mMixedProperties.Favorite.origin = mixVectors(mDefaultProperties.Favorite.origin, mSelectedProperties.Favorite.origin, mSelectedZoomPercent); + mMixedProperties.Favorite.color = mixColors(mDefaultProperties.Favorite.color, mSelectedProperties.Favorite.color, mSelectedZoomPercent); + mMixedProperties.Favorite.colorEnd = mixColors(mDefaultProperties.Favorite.colorEnd, mSelectedProperties.Favorite.colorEnd, mSelectedZoomPercent); + mMixedProperties.Favorite.reflexion = mixVectors(mDefaultProperties.Favorite.reflexion, mSelectedProperties.Favorite.reflexion, mSelectedZoomPercent); } - - // Avoid to multiply font sizes in mem + it create strange sizings - // if (mDefaultProperties.mFontSize != mSelectedProperties.mFontSize) - // mMixedProperties.mFontSize = mDefaultProperties.mFontSize * (1.0 - mSelectedZoomPercent) + mSelectedProperties.mFontSize * mSelectedZoomPercent; - + return mMixedProperties; } diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index edc5594fba..651d732a99 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -5,9 +5,110 @@ #include "NinePatchComponent.h" #include "ImageComponent.h" #include "TextComponent.h" +#include "ThemeData.h" class VideoComponent; +struct ImageProperties +{ +public: + ImageProperties() + { + Loaded = false; + Visible = false; + + reflexion = Vector2f::Zero(); + pos = Vector2f(0.5f, 0.5f); + size = Vector2f(1.0f, 1.0f); + origin = Vector2f(0.5f, 0.5f); + color = colorEnd = 0xFFFFFFFF; + sizeMode = 2; + } + + void updateImageComponent(ImageComponent* image, Vector2f parentSize, bool disableSize = false) + { + if (image == nullptr) + return; + + image->setPosition(pos.x() * parentSize.x(), pos.y() * parentSize.y()); + + if (!disableSize && sizeMode == 0) + image->setSize(size.x() * parentSize.x(), size.y() * parentSize.y()); + else if (sizeMode == 1) + image->setMinSize(size.x() * parentSize.x(), size.y() * parentSize.y()); + else + image->setMaxSize(size.x() * parentSize.x(), size.y() * parentSize.y()); + + image->setOrigin(origin); + image->setColorShift(color); + image->setColorShiftEnd(colorEnd); + image->setMirroring(reflexion); + } + + bool loadElement(const ThemeData::ThemeElement* elem) + { + if (!elem) + { + Visible = false; + return false; + } + + Loaded = true; + Visible = true; + + if (elem && elem->has("visible")) + Visible = elem->get("visible"); + + if (elem && elem->has("origin")) + origin = elem->get("origin"); + + if (elem && elem->has("pos")) + pos = elem->get("pos"); + + if (elem && elem->has("size")) + { + sizeMode = 0; + size = elem->get("size"); + } + else if (elem && elem->has("minSize")) + { + sizeMode = 1; + size = elem->get("minSize"); + } + else if (elem && elem->has("maxSize")) + { + sizeMode = 2; + size = elem->get("maxSize"); + } + + if (elem && elem->has("color")) + color = colorEnd = elem->get("color"); + + if (elem && elem->has("colorEnd")) + colorEnd = elem->get("colorEnd"); + + if (elem && elem->has("reflexion")) + reflexion = elem->get("reflexion"); + + return true; + } + + bool Loaded; + bool Visible; + + Vector2f pos; + Vector2f size; + + Vector2f origin; + + Vector2f reflexion; + + unsigned int color; + unsigned int colorEnd; + + int sizeMode; +}; + struct GridTileProperties { Vector2f mSize; @@ -35,9 +136,9 @@ struct GridTileProperties Vector2f mMirror; - - Vector2f mMarqueePos; - Vector2f mMarqueeSize; + ImageProperties Image; + ImageProperties Marquee; + ImageProperties Favorite; }; class GridTileComponent : public GuiComponent @@ -63,6 +164,9 @@ class GridTileComponent : public GuiComponent void setImage(const std::string& path, bool isDefaultImage = false); void setMarquee(const std::string& path); + + void setFavorite(bool favorite); + bool hasFavoriteMedia() { return mFavorite != nullptr; } void setSelected(bool selected, bool allowAnimation = true, Vector3f* pPosition = NULL, bool force = false); void setVisible(bool visible); @@ -86,13 +190,16 @@ class GridTileComponent : public GuiComponent void resetProperties(); void createVideo(); void createMarquee(); + void createFavorite(); void startVideo(); void stopVideo(); void resize(); - GridTileProperties getCurrentProperties(); + + inline static unsigned int mixUnsigned(const unsigned int def, const unsigned int sel, float percent); + static Vector2f mixVectors(const Vector2f& def, const Vector2f& sel, float percent); - std::shared_ptr mImage; + GridTileProperties getCurrentProperties(); TextComponent mLabel; @@ -119,7 +226,9 @@ class GridTileComponent : public GuiComponent Vector3f mAnimPosition; VideoComponent* mVideo; + ImageComponent* mImage; ImageComponent* mMarquee; + ImageComponent* mFavorite; bool mVideoPlaying; bool mShown; diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index d25a0f5780..d13155a30a 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -32,6 +32,7 @@ ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : G mReflectOnBorders = false; mLoadingTexture = nullptr; mAllowFading = true; + mRoundCorners = 0.0f; } ImageComponent::~ImageComponent() @@ -356,6 +357,31 @@ void ImageComponent::updateVertices() } } +#include + +#define N_ROUNDING_PIECES 10 + +void DrawGLRoundedCorner(int x, int y, double sa, double arc, float r) +{ + // centre of the arc, for clockwise sense + float cent_x = x + r * cos(sa + ES_PI / 2); + float cent_y = y + r * sin(sa + ES_PI / 2); + + // build up piecemeal including end of the arc + int n = ceil(N_ROUNDING_PIECES * arc / ES_PI * 2); + for (int i = 0; i <= n; i++) + { + double ang = sa + arc * (double)i / (double)n; + + // compute the next point + float next_x = cent_x + r * sin(ang); + float next_y = cent_y - r * cos(ang); + + glColor3f(1.0f, 1.0f, 1.0f); + glVertex2f(next_x, next_y); + } +} + void ImageComponent::render(const Transform4x4f& parentTrans) { if (!mVisible) @@ -399,14 +425,13 @@ void ImageComponent::render(const Transform4x4f& parentTrans) if (mHorizontalAlignment == ALIGN_LEFT) trans.translate(Vector3f(targetSizePos.x(), 0, 0.0f)); - else if (mHorizontalAlignment == ALIGN_BOTTOM) - trans.translate(Vector3f(targetSizePos.x(), targetSizePos.y() + mTargetSize.y() - mSize.y(), 0.0f)); + else if (mHorizontalAlignment == ALIGN_RIGHT) + trans.translate(Vector3f(targetSizePos.x() + mTargetSize.x() - mSize.x(), targetSizePos.y(), 0.0f)); Renderer::setMatrix(trans); fadeIn(true); - float opacity = (mOpacity * (mFading ? mFadeOpacity / 255.0 : 1.0)) / 255.0; const unsigned int color = Renderer::convertColor(mColorShift & 0xFFFFFF00 | (unsigned char)((mColorShift & 0xFF) * opacity)); @@ -417,7 +442,31 @@ void ImageComponent::render(const Transform4x4f& parentTrans) mVertices[2].col = mColorGradientHorizontal ? color : colorEnd; mVertices[3].col = colorEnd; + if (mRoundCorners > 0) + { + int x = 0; + int y = 0; + int size_x = mSize.x(); + int size_y = mSize.y(); + int radius = Math::max(size_x, size_y) * mRoundCorners; + + if (mTargetIsMin) + { + x = targetSizePos.x(); + y = targetSizePos.y(); + size_x = mTargetSize.x(); + size_y = mTargetSize.y(); + } + + Renderer::enableRoundCornerStencil(x, y, size_x, size_y, radius); + + mTexture->bind(); + } + Renderer::drawTriangleStrips(&mVertices[0], 4); + + if (mRoundCorners > 0) + Renderer::disableStencil(); if (mMirror.x() != 0 || mMirror.y() != 0) { @@ -553,7 +602,7 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s if(properties & PATH && elem->has("path")) { auto path = elem->get("path"); - if (Utils::FileSystem::exists(path)) + if (ResourceManager::getInstance()->fileExists(path)) { bool tile = (elem->has("tile") && elem->get("tile")); setImage(path, tile/*, Vector2f(mTargetSize.x(), mTargetSize.y())*/); @@ -624,6 +673,9 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s else setZIndex(getDefaultZIndex()); + if (properties & ALIGNMENT && elem->has("roundCorners")) + mRoundCorners = elem->get("roundCorners"); + if(properties & ThemeFlags::VISIBLE && elem->has("visible")) setVisible(elem->get("visible")); else diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 3b8c2daf3d..b8f514b376 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -104,6 +104,8 @@ class ImageComponent : public GuiComponent void setHorizontalAlignment(Alignment align) { mHorizontalAlignment = align; } void setVerticalAlignment(Alignment align) { mVerticalAlignment = align; } + float getRoundCorners() { return mRoundCorners; } + private: Vector2f mTargetSize; @@ -136,6 +138,7 @@ class ImageComponent : public GuiComponent bool mVisible; bool mAllowFading; bool mAllowAsync; + float mRoundCorners; Vector2f mTopLeftCrop; Vector2f mBottomRightCrop; diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index eb534cbe4a..bfeda04efc 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -9,6 +9,7 @@ #include "animations/LambdaAnimation.h" #include "Settings.h" #include "Sound.h" +#include "EsLocale.h" #define EXTRAITEMS 2 @@ -30,6 +31,7 @@ struct ImageGridData std::string texturePath; std::string marqueePath; std::string videoPath; + bool favorite; }; template @@ -54,7 +56,7 @@ class ImageGridComponent : public IList ImageGridComponent(Window* window); - void add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, const T& obj); + void add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, bool favorite, const T& obj); bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; @@ -165,7 +167,7 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList -void ImageGridComponent::add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, const T& obj) +void ImageGridComponent::add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, bool favorite, const T& obj) { typename IList::Entry entry; entry.name = name; @@ -173,6 +175,7 @@ void ImageGridComponent::add(const std::string& name, const std::string& imag entry.data.texturePath = imagePath; entry.data.videoPath = videoPath; entry.data.marqueePath = marqueePath; + entry.data.favorite = favorite; static_cast*>(this)->add(entry); mEntriesDirty = true; @@ -794,7 +797,11 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA tile->setVisible(true); std::string name = mEntries.at(imgPos).name; // .object->getName(); - tile->setLabel(name); + + if (tile->hasFavoriteMedia()) + tile->setLabel(name); + else + tile->setLabel(_U("\uF006 ") + name); std::string imagePath = mEntries.at(imgPos).data.texturePath; @@ -813,6 +820,8 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA else tile->setMarquee(""); + tile->setFavorite(mEntries.at(imgPos).data.favorite); + // Video if (mAllowVideo && imgPos == mCursor) { diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index db67774d56..f64b8f7565 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -67,6 +67,7 @@ VideoComponent::VideoComponent(Window* window) : mTargetIsMin(false), mTargetSize(0, 0) { + mRoundCorners = 0.0f; mFadeIn = 0.0f; mIsWaitingForVideoToStart = false; diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index 8e7dabe4c4..ab1f5ce82c 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -110,6 +110,9 @@ class VideoComponent : public GuiComponent mVideoEnded = callback; } + float getRoundCorners() { return mRoundCorners; } + void setRoundCorners(float value) { mRoundCorners = value; } + protected: std::function mVideoEnded; @@ -156,6 +159,8 @@ class VideoComponent : public GuiComponent bool mIsWaitingForVideoToStart; + float mRoundCorners; + Configuration mConfig; }; diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index f90de4b40f..e095a0f6cf 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -291,18 +291,42 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) if (mTexture->bind()) { - if (mTargetIsMin) - { - Vector2f targetPos = (mTargetSize - mSize) * mOrigin * -1; + Vector2f targetSizePos = (mTargetSize - mSize) * mOrigin * -1; - Vector2i pos(trans.translation().x() + (int)targetPos.x(), trans.translation().y() + (int)targetPos.y()); + if (mTargetIsMin) + { + Vector2i pos(trans.translation().x() + (int)targetSizePos.x(), trans.translation().y() + (int)targetSizePos.y()); Vector2i size((int)mTargetSize.round().x(), (int)mTargetSize.round().y()); Renderer::pushClipRect(pos, size); } + if (mRoundCorners > 0) + { + int x = 0; + int y = 0; + int size_x = mSize.x(); + int size_y = mSize.y(); + int radius = Math::max(size_x, size_y) * mRoundCorners; + + if (mTargetIsMin) + { + x = targetSizePos.x(); + y = targetSizePos.y(); + size_x = mTargetSize.x(); + size_y = mTargetSize.y(); + } + + Renderer::enableRoundCornerStencil(x, y, size_x, size_y, radius); + + mTexture->bind(); + } + // Render it Renderer::drawTriangleStrips(&vertices[0], 4); + if (mRoundCorners > 0) + Renderer::disableStencil(); + if (mTargetIsMin) Renderer::popClipRect(); @@ -526,6 +550,9 @@ void VideoVlcComponent::applyTheme(const std::shared_ptr& theme, cons else mEffect = VideoVlcFlags::VideoVlcEffect::NONE; } + + if (elem && elem->has("roundCorners")) + mRoundCorners = elem->get("roundCorners"); } void VideoVlcComponent::update(int deltaTime) diff --git a/es-core/src/renderers/Renderer.h b/es-core/src/renderers/Renderer.h index 88b43577cf..c105b9a1ee 100644 --- a/es-core/src/renderers/Renderer.h +++ b/es-core/src/renderers/Renderer.h @@ -107,6 +107,11 @@ namespace Renderer bool isSmallScreen(); unsigned int mixColors(unsigned int first, unsigned int second, float percent); + + + void enableRoundCornerStencil(int x, int y, int size_x, int size_y, int radius); + void disableStencil(); + } // Renderer:: #endif // ES_CORE_RENDERER_RENDERER_H diff --git a/es-core/src/renderers/Renderer_GL21.cpp b/es-core/src/renderers/Renderer_GL21.cpp index b7a6f0922b..bbf4330357 100644 --- a/es-core/src/renderers/Renderer_GL21.cpp +++ b/es-core/src/renderers/Renderer_GL21.cpp @@ -62,6 +62,8 @@ namespace Renderer void setupWindow() { + SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 1); + SDL_GL_SetAttribute(SDL_GL_RED_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_GREEN_SIZE, 8); SDL_GL_SetAttribute(SDL_GL_BLUE_SIZE, 8); @@ -268,6 +270,65 @@ namespace Renderer } // swapBuffers + #define N_ROUNDING_PIECES 10 + + void drawGLRoundedCorner(int x, int y, double sa, double arc, float r) + { + // centre of the arc, for clockwise sense + float cent_x = x + r * cos(sa + ES_PI / 2); + float cent_y = y + r * sin(sa + ES_PI / 2); + + // build up piecemeal including end of the arc + int n = ceil(N_ROUNDING_PIECES * arc / ES_PI * 2); + for (int i = 0; i <= n; i++) + { + double ang = sa + arc * (double)i / (double)n; + + // compute the next point + float next_x = cent_x + r * sin(ang); + float next_y = cent_y - r * cos(ang); + + glColor3f(1.0f, 1.0f, 1.0f); + glVertex2f(next_x, next_y); + } + } + + void enableRoundCornerStencil(int x, int y, int size_x, int size_y, int radius) + { + Renderer::bindTexture(0); + + glClear(GL_DEPTH_BUFFER_BIT); + glEnable(GL_STENCIL_TEST); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + glDepthMask(GL_FALSE); + glStencilFunc(GL_NEVER, 1, 0xFF); + glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP); + + glStencilMask(0xFF); + glClear(GL_STENCIL_BUFFER_BIT); + + glBegin(GL_POLYGON); + drawGLRoundedCorner(x, y + radius, 3 * ES_PI / 2, ES_PI / 2, radius); + drawGLRoundedCorner(x + size_x - radius, y, 0.0, ES_PI / 2, radius); + drawGLRoundedCorner(x + size_x, y + size_y - radius, ES_PI / 2, ES_PI / 2, radius); + drawGLRoundedCorner(x + radius, y + size_y, ES_PI, ES_PI / 2, radius); + glEnd(); + + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glDepthMask(GL_TRUE); + glStencilMask(0x00); + glStencilFunc(GL_EQUAL, 0, 0xFF); + glStencilFunc(GL_EQUAL, 1, 0xFF); + } + + void disableStencil() + { + glDisable(GL_STENCIL_TEST); + } + + + + } // Renderer:: #endif // USE_OPENGL_21 diff --git a/es-core/src/renderers/Renderer_GLES10.cpp b/es-core/src/renderers/Renderer_GLES10.cpp index 689e3c420c..ea9e155714 100644 --- a/es-core/src/renderers/Renderer_GLES10.cpp +++ b/es-core/src/renderers/Renderer_GLES10.cpp @@ -268,6 +268,61 @@ namespace Renderer } // swapBuffers +#define N_ROUNDING_PIECES 10 + + void drawGLRoundedCorner(int x, int y, double sa, double arc, float r) + { + // centre of the arc, for clockwise sense + float cent_x = x + r * cos(sa + ES_PI / 2); + float cent_y = y + r * sin(sa + ES_PI / 2); + + // build up piecemeal including end of the arc + int n = ceil(N_ROUNDING_PIECES * arc / ES_PI * 2); + for (int i = 0; i <= n; i++) + { + double ang = sa + arc * (double)i / (double)n; + + // compute the next point + float next_x = cent_x + r * sin(ang); + float next_y = cent_y - r * cos(ang); + + glColor3f(1.0f, 1.0f, 1.0f); + glVertex2f(next_x, next_y); + } + } + + void enableRoundCornerStencil(int x, int y, int size_x, int size_y, int radius) + { + Renderer::bindTexture(0); + + glClear(GL_DEPTH_BUFFER_BIT); + glEnable(GL_STENCIL_TEST); + glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE); + glDepthMask(GL_FALSE); + glStencilFunc(GL_NEVER, 1, 0xFF); + glStencilOp(GL_REPLACE, GL_KEEP, GL_KEEP); + + glStencilMask(0xFF); + glClear(GL_STENCIL_BUFFER_BIT); + + glBegin(GL_POLYGON); + drawGLRoundedCorner(x, y + radius, 3 * ES_PI / 2, ES_PI / 2, radius); + drawGLRoundedCorner(x + size_x - radius, y, 0.0, ES_PI / 2, radius); + drawGLRoundedCorner(x + size_x, y + size_y - radius, ES_PI / 2, ES_PI / 2, radius); + drawGLRoundedCorner(x + radius, y + size_y, ES_PI, ES_PI / 2, radius); + glEnd(); + + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); + glDepthMask(GL_TRUE); + glStencilMask(0x00); + glStencilFunc(GL_EQUAL, 0, 0xFF); + glStencilFunc(GL_EQUAL, 1, 0xFF); + } + + void disableStencil() + { + glDisable(GL_STENCIL_TEST); + } } // Renderer:: #endif // USE_OPENGLES_10 From 594082da0365dc940d9d23d9f654eaf3b8871864 Mon Sep 17 00:00:00 2001 From: Justin Kinnaird Date: Tue, 12 Nov 2019 20:29:24 -0600 Subject: [PATCH 220/336] fix: make exePath on Linux more reliable --- es-app/src/main.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index d109afd938..dd9d5652c7 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -21,8 +21,12 @@ #include #include #include -#ifdef WIN32 + +#if defined(_WIN32) #include +#elif defined(__linux__) +#include +#include #endif #include "resources/TextureData.h" @@ -108,8 +112,17 @@ void playVideo() bool parseArgs(int argc, char* argv[]) { - Utils::FileSystem::setExePath(argv[0]); - +#if defined(__linux__) + char* result = new char[PATH_MAX]; + ssize_t len = readlink("/proc/self/exe", result, PATH_MAX); + if (len != -1) { + result[len] = 0; + Utils::FileSystem::setExePath(dirname(result)); + } + delete [] result; +#else + Utils::FileSystem::setExePath(argv[0]); +#endif // We need to process --home before any call to Settings::getInstance(), because settings are loaded from homepath for (int i = 1; i < argc; i++) { From 70006ed5205872f5fac3bb8b55a8ca7c06f17a50 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 13 Nov 2019 16:57:09 +0100 Subject: [PATCH 221/336] GridView : Other things... --- es-app/src/main.cpp | 3 + es-core/src/ThemeData.cpp | 9 +- es-core/src/components/GridTileComponent.cpp | 631 +++++++++--------- es-core/src/components/GridTileComponent.h | 147 ++-- es-core/src/components/ImageComponent.cpp | 2 +- es-core/src/components/NinePatchComponent.cpp | 14 +- es-core/src/components/NinePatchComponent.h | 2 +- es-core/src/renderers/Renderer.h | 2 +- es-core/src/renderers/Renderer_GL21.cpp | 30 +- es-core/src/renderers/Renderer_GLES10.cpp | 14 +- 10 files changed, 433 insertions(+), 421 deletions(-) diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index d109afd938..c0c5099245 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -29,6 +29,7 @@ #include #include "AudioManager.h" #include "NetworkThread.h" +#include "scrapers/ThreadedScraper.h" bool scrape_cmdline = false; @@ -660,6 +661,8 @@ int main(int argc, char* argv[]) */ } + ThreadedScraper::stop(); + while(window.peekGui() != ViewController::get()) delete window.peekGui(); diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 66ffbfc3d5..d41892a097 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -958,7 +958,14 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map GridTileComponent::getTexture(bool marquee) { if (marquee && mMarquee != nullptr) @@ -133,37 +120,10 @@ void GridTileComponent::resize() bool isDefaultImage = mIsDefaultImage && (mCurrentPath == ":/folder.svg" || mCurrentPath == ":/cartridge.svg"); - float height = (int) (size.y() * currentProperties.mLabelSize.y()); + float height = (int) (size.y() * currentProperties.Label.size.y()); float labelHeight = height; - mLabel.setVisible(mLabelVisible || mIsDefaultImage); - - if (mLabelVisible) - { - mLabel.setColor(currentProperties.mLabelColor); - mLabel.setBackgroundColor(currentProperties.mLabelBackColor); - mLabel.setGlowColor(currentProperties.mLabelGlowColor); - mLabel.setGlowSize(currentProperties.mLabelGlowSize); - - if (mDefaultProperties.mFontPath != mSelectedProperties.mFontPath || mDefaultProperties.mFontSize != mSelectedProperties.mFontSize) - mLabel.setFont(currentProperties.mFontPath, currentProperties.mFontSize); - - if (currentProperties.mLabelPos.x() < 0) - { - if (mLabelMerged) - { - mLabel.setPosition(currentProperties.mPadding.x(), mSize.y() - height - currentProperties.mPadding.y()); - mLabel.setSize(size.x() - 2 * currentProperties.mPadding.x(), height); - } - else - { - mLabel.setPosition(0, mSize.y() - height); - mLabel.setSize(size.x(), height); - } - } - } - - if (!mLabelVisible || mLabelMerged || currentProperties.mLabelSize.x() == 0) + if (!currentProperties.Label.Visible || mLabelMerged || currentProperties.Label.size.x() == 0) height = 0; float topPadding = currentProperties.mPadding.y(); @@ -173,59 +133,75 @@ void GridTileComponent::resize() float imageWidth = size.x() - paddingX * 2.0; float imageHeight = size.y() - topPadding - bottomPadding; - - if (currentProperties.Favorite.Loaded) - currentProperties.Favorite.updateImageComponent(mFavorite, Vector2f(size.x(), (size.y() - height)), false); - - if (currentProperties.Marquee.Loaded) - currentProperties.Marquee.updateImageComponent(mMarquee, Vector2f(size.x(), (size.y() - height)), true); + Vector2f imageSize(size.x(), (size.y() - height)); + // Image if (currentProperties.Image.Loaded) - currentProperties.Image.updateImageComponent(mImage, Vector2f(size.x(), (size.y() - height)), false); + { + currentProperties.Image.updateImageComponent(mImage, imageSize, false); + + if (mImage != nullptr && currentProperties.Image.sizeMode != "maxSize" && isDefaultImage) + mImage->setMaxSize(imageWidth, imageHeight); + } else if (mImage != nullptr) { - mImage->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); - mImage->setColorShift(currentProperties.mImageColor); - mImage->setMirroring(currentProperties.mMirror); + // Retrocompatibility : imagegrid.image is not defined + mImage->setPosition(imageSize.x() / 2.0f, imageSize.y() / 2.0f); + mImage->setColorShift(currentProperties.Image.color); + mImage->setMirroring(currentProperties.Image.reflexion); - if (currentProperties.mImageSizeMode == "minSize" && !isDefaultImage) + if (currentProperties.Image.sizeMode == "minSize" && !isDefaultImage) mImage->setMinSize(imageWidth, imageHeight); - else if (currentProperties.mImageSizeMode == "size") + else if (currentProperties.Image.sizeMode == "size") mImage->setSize(imageWidth, imageHeight); else - mImage->setMaxSize(imageWidth, imageHeight); + mImage->setMaxSize(imageWidth, imageHeight); + } + + // Text + mLabel.setVisible(currentProperties.Label.Visible || mIsDefaultImage); - if (mLabelVisible && currentProperties.mLabelPos.x() < 0) + if (currentProperties.Label.Visible) + { + currentProperties.Label.updateTextComponent(&mLabel, mSize); + + if (currentProperties.Label.pos.x() < 0 && mLabelMerged) { - if (mLabelMerged) - { - mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, currentProperties.mPadding.y() + mImage->getSize().y() - labelHeight); - mLabel.setSize(mImage->getSize().x(), labelHeight); - } - else if (currentProperties.mPadding.x() == 0) - { - mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, mImage->getSize().y()); - mLabel.setSize(mImage->getSize().x(), labelHeight); - } + mLabel.setPosition(0, mSize.y() - labelHeight); + mLabel.setSize(size.x(), labelHeight); + } + else if (mImage != nullptr && !mLabelMerged) + { + mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, mImage->getSize().y()); + mLabel.setSize(mImage->getSize().x(), labelHeight); } } - - if (currentProperties.mLabelPos.x() >= 0) + else if (mIsDefaultImage) { - mLabel.setPosition( - currentProperties.mLabelPos.x() * size.x(), - currentProperties.mLabelPos.y() * size.y()); - - mLabel.setSize( - currentProperties.mLabelSize.x() * size.x(), - currentProperties.mLabelSize.y() * size.y()); + mLabel.setColor(0xFFFFFFFF); + mLabel.setGlowColor(0x00000010); + mLabel.setGlowSize(2); + mLabel.setOpacity(255); + mLabel.setPosition(mSize.x() * 0.1, mSize.y() * 0.2); + mLabel.setSize(mSize.x() - mSize.x() * 0.2, mSize.y() - mSize.y() * 0.3); } + // Other controls ( Favorite / Marquee / Overlay ) + if (currentProperties.Favorite.Loaded) + currentProperties.Favorite.updateImageComponent(mFavorite, Vector2f(size.x(), (size.y() - height)), false); + + if (currentProperties.Marquee.Loaded) + currentProperties.Marquee.updateImageComponent(mMarquee, Vector2f(size.x(), (size.y() - height)), true); + + if (currentProperties.ImageOverlay.Loaded) + currentProperties.ImageOverlay.updateImageComponent(mImageOverlay, Vector2f(size.x(), (size.y() - height)), false); + + // Video if (mVideo != nullptr && mVideo->isPlaying()) { mVideo->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); - if (currentProperties.mImageSizeMode == "minSize") + if (currentProperties.Image.sizeMode == "minSize") { if (mImage != nullptr) mVideo->setRoundCorners(mImage->getRoundCorners()); @@ -237,18 +213,19 @@ void GridTileComponent::resize() mVideo->setSize(ImageIO::adjustExternPictureSizef(vs, Vector2f(imageWidth, imageHeight))); } else - if (currentProperties.mImageSizeMode == "size") + if (currentProperties.Image.sizeMode == "size") mVideo->setSize(imageWidth, size.y() - topPadding - bottomPadding); else mVideo->setMaxSize(imageWidth, size.y() - topPadding - bottomPadding); } + // Background Vector3f bkposition = Vector3f(0, 0); Vector2f bkSize = size; if (mImage != NULL && currentProperties.mSelectionMode == "image" && mImage->getSize() != Vector2f(0, 0)) { - if (currentProperties.mImageSizeMode == "minSize") + if (currentProperties.Image.sizeMode == "minSize") bkSize = Vector2f(size.x(), size.y() - bottomPadding + topPadding); else if (mAnimPosition == Vector3f(0, 0, 0)) { @@ -277,20 +254,11 @@ void GridTileComponent::resize() y = mAnimPosition.y() * (1.0 - mSelectedZoomPercent) + y * mSelectedZoomPercent; bkposition = Vector3f(x - mPosition.x(), y - mPosition.y(), 0); - } - - if (!mLabelVisible && mIsDefaultImage) - { - mLabel.setColor(0xFFFFFFFF); - mLabel.setGlowColor(0x00000010); - mLabel.setGlowSize(2); - mLabel.setOpacity(255); - mLabel.setPosition(mSize.x() * 0.1, mSize.y() * 0.2); - mLabel.setSize(mSize.x() - mSize.x() * 0.2, mSize.y() - mSize.y() * 0.3); - } + } mBackground.setPosition(bkposition); mBackground.setSize(bkSize); + mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); mBackground.setCenterColor(currentProperties.mBackgroundCenterColor); mBackground.setEdgeColor(currentProperties.mBackgroundEdgeColor); @@ -336,14 +304,14 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) float topPadding = currentProperties.mPadding.y(); float bottomPadding = topPadding; - if (mLabelVisible && !mLabelMerged) - bottomPadding = std::max((int)topPadding, (int)(mSize.y() * currentProperties.mLabelSize.y())); + if (currentProperties.Label.Visible && !mLabelMerged) + bottomPadding = std::max((int)topPadding, (int)(mSize.y() * currentProperties.Label.size.y())); Vector2i pos((int)Math::round(trans.translation()[0] + padding), (int)Math::round(trans.translation()[1] + topPadding)); Vector2i size((int)Math::round(mSize.x() - 2 * padding), (int)Math::round(mSize.y() - topPadding - bottomPadding)); bool isDefaultImage = mIsDefaultImage && (mCurrentPath == ":/folder.svg" || mCurrentPath == ":/cartridge.svg"); - bool isMinSize = currentProperties.mImageSizeMode == "minSize" && !isDefaultImage; + bool isMinSize = currentProperties.Image.sizeMode == "minSize" && !isDefaultImage; if (isMinSize) Renderer::pushClipRect(pos, size); @@ -359,14 +327,17 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) if (mMarquee != nullptr && mMarquee->hasImage()) mMarquee->render(trans); - else if (mLabelVisible && currentProperties.mLabelSize.y()>0) + else if (currentProperties.Label.Visible && currentProperties.Label.size.y()>0) mLabel.render(trans); - else if (!mLabelVisible && mIsDefaultImage) + else if (!currentProperties.Label.Visible && mIsDefaultImage) mLabel.render(trans); if (mFavorite != nullptr && mFavorite->hasImage() && mFavorite->isVisible()) mFavorite->render(trans); + if (mImageOverlay != nullptr && mImageOverlay->hasImage() && mImageOverlay->isVisible()) + mImageOverlay->render(trans); + if (mLabelMerged && isMinSize) Renderer::popClipRect(); } @@ -404,6 +375,19 @@ void GridTileComponent::createFavorite() addChild(mFavorite); } +void GridTileComponent::createImageOverlay() +{ + if (mImageOverlay != nullptr) + return; + + mImageOverlay = new ImageComponent(mWindow); + mImageOverlay->setOrigin(0.5f, 0.5f); + mImageOverlay->setDefaultZIndex(35); + mImageOverlay->setVisible(false); + + addChild(mImageOverlay); +} + void GridTileComponent::createVideo() { if (mVideo != nullptr) @@ -418,12 +402,144 @@ void GridTileComponent::createVideo() addChild(mVideo); } -void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) +void GridTileComponent::applyThemeToProperties(const ThemeData::ThemeElement* elem, GridTileProperties& properties) { - resetProperties(); + if (elem == nullptr) + return; Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); + if (elem->has("size")) + properties.mSize = elem->get("size") * screen; + + if (elem->has("padding")) + properties.mPadding = elem->get("padding"); + + if (elem->has("backgroundImage")) + properties.mBackgroundImage = elem->get("backgroundImage"); + + if (elem->has("backgroundCornerSize")) + properties.mBackgroundCornerSize = elem->get("backgroundCornerSize"); + + if (elem->has("backgroundColor")) + { + properties.mBackgroundCenterColor = elem->get("backgroundColor"); + properties.mBackgroundEdgeColor = elem->get("backgroundColor"); + } + + if (elem->has("backgroundCenterColor")) + properties.mBackgroundCenterColor = elem->get("backgroundCenterColor"); + + if (elem->has("backgroundEdgeColor")) + properties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); + + if (elem && elem->has("selectionMode")) + properties.mSelectionMode = elem->get("selectionMode"); + + if (elem && elem->has("reflexion")) + properties.Image.reflexion = elem->get("reflexion"); + + if (elem->has("imageColor")) + properties.Image.color = properties.Image.colorEnd = elem->get("imageColor"); + + if (elem && elem->has("imageSizeMode")) + properties.Image.sizeMode = elem->get("imageSizeMode"); +} + +bool GridImageProperties::applyTheme(const ThemeData::ThemeElement* elem) +{ + if (!elem) + { + Visible = false; + return false; + } + + Loaded = true; + Visible = true; + + if (elem && elem->has("visible")) + Visible = elem->get("visible"); + + if (elem && elem->has("origin")) + origin = elem->get("origin"); + + if (elem && elem->has("pos")) + pos = elem->get("pos"); + + if (elem && elem->has("size")) + { + sizeMode = "size"; + size = elem->get("size"); + } + else if (elem && elem->has("minSize")) + { + sizeMode = "minSize"; + size = elem->get("minSize"); + } + else if (elem && elem->has("maxSize")) + { + sizeMode = "maxSize"; + size = elem->get("maxSize"); + } + + if (elem && elem->has("color")) + color = colorEnd = elem->get("color"); + + if (elem && elem->has("colorEnd")) + colorEnd = elem->get("colorEnd"); + + if (elem && elem->has("reflexion")) + reflexion = elem->get("reflexion"); + + return true; +} + +bool GridTextProperties::applyTheme(const ThemeData::ThemeElement* elem) +{ + if (!elem) + { + Visible = false; + return false; + } + + Loaded = true; + Visible = true; + + if (elem && elem->has("visible")) + Visible = elem->get("visible"); + + if (elem && elem->has("pos")) + pos = elem->get("pos"); + + if (elem && elem->has("size")) + size = elem->get("size"); + + if (elem && elem->has("color")) + color = elem->get("color"); + + if (elem && elem->has("backgroundColor")) + backColor = elem->get("backgroundColor"); + + if (elem && elem->has("glowColor")) + glowColor = elem->get("glowColor"); + + if (elem && elem->has("glowSize")) + glowSize = elem->get("glowSize"); + + if (elem && elem->has("fontSize")) + fontSize = elem->get("fontSize"); + + if (elem && elem->has("fontPath")) + fontPath = elem->get("fontPath"); + + return true; +} + + +void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) +{ + resetProperties(); + const ThemeData::ThemeElement* grid = theme->getElement(view, "gamegrid", "imagegrid"); if (grid && grid->has("showVideoAtDelay")) { @@ -441,93 +557,46 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons const ThemeData::ThemeElement* elem = theme->getElement(view, "default", "gridtile"); if (elem) { - if (elem->has("size")) - mDefaultProperties.mSize = elem->get("size") * screen; - - if (elem->has("padding")) - mDefaultProperties.mPadding = elem->get("padding"); - - if (elem->has("imageColor")) - mDefaultProperties.mImageColor = elem->get("imageColor"); - - if (elem->has("backgroundImage")) - mDefaultProperties.mBackgroundImage = elem->get("backgroundImage"); - - if (elem->has("backgroundCornerSize")) - mDefaultProperties.mBackgroundCornerSize = elem->get("backgroundCornerSize"); - - if (elem->has("backgroundColor")) - { - mDefaultProperties.mBackgroundCenterColor = elem->get("backgroundColor"); - mDefaultProperties.mBackgroundEdgeColor = elem->get("backgroundColor"); - } - - if (elem->has("backgroundCenterColor")) - mDefaultProperties.mBackgroundCenterColor = elem->get("backgroundCenterColor"); - - if (elem->has("backgroundEdgeColor")) - mDefaultProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); - - if (elem && elem->has("imageSizeMode")) - { - mDefaultProperties.mImageSizeMode = elem->get("imageSizeMode"); - mSelectedProperties.mImageSizeMode = mDefaultProperties.mImageSizeMode; - } + applyThemeToProperties(elem, mDefaultProperties); + applyThemeToProperties(elem, mSelectedProperties); + } - if (elem && elem->has("selectionMode")) - { - mDefaultProperties.mSelectionMode = elem->get("selectionMode"); - mSelectedProperties.mSelectionMode = mDefaultProperties.mSelectionMode; - } + // Apply theme to the selected gridtile + elem = theme->getElement(view, "selected", "gridtile"); + if (elem) + applyThemeToProperties(elem, mSelectedProperties); + - if (elem && elem->has("reflexion")) - { - mDefaultProperties.mMirror = elem->get("reflexion"); - mSelectedProperties.mMirror = mDefaultProperties.mMirror; - } - } - + // Apply theme to the element elem = theme->getElement(view, "gridtile.image", "image"); if (elem) { mImage->applyTheme(theme, view, "gridtile.image", ThemeFlags::ALL ^ (ThemeFlags::PATH)); - mDefaultProperties.Image.color = mDefaultProperties.mImageColor; - mDefaultProperties.Image.colorEnd = mDefaultProperties.mImageColor; - - mSelectedProperties.Image.color = mSelectedProperties.mImageColor; - mSelectedProperties.Image.colorEnd = mSelectedProperties.mImageColor; - - if (mDefaultProperties.mImageSizeMode == "minSize") - mDefaultProperties.Image.sizeMode = 1; - else if (mDefaultProperties.mImageSizeMode == "maxSize") - mDefaultProperties.Image.sizeMode = 2; - else - mDefaultProperties.Image.sizeMode = 0; - - mSelectedProperties.Image.sizeMode = mDefaultProperties.Image.sizeMode; - - mDefaultProperties.Image.loadElement(elem); - mSelectedProperties.Image.loadElement(elem); + mDefaultProperties.Image.applyTheme(elem); + mSelectedProperties.Image.applyTheme(elem); + // Apply theme to the element elem = theme->getElement(view, "gridtile.image:selected", "image"); if (elem) - mSelectedProperties.Image.loadElement(elem); + mSelectedProperties.Image.applyTheme(elem); } + - + // Apply theme to the element elem = theme->getElement(view, "gridtile.marquee", "image"); if (elem) { createMarquee(); mMarquee->applyTheme(theme, view, "gridtile.marquee", ThemeFlags::ALL ^ (ThemeFlags::PATH)); - mDefaultProperties.Marquee.loadElement(elem); + mDefaultProperties.Marquee.applyTheme(elem); mSelectedProperties.Marquee = mDefaultProperties.Marquee; + // Apply theme to the element elem = theme->getElement(view, "gridtile.marquee:selected", "image"); if (elem) - mSelectedProperties.Marquee.loadElement(elem); + mSelectedProperties.Marquee.applyTheme(elem); } else if (mMarquee != nullptr) { @@ -536,19 +605,22 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mMarquee = nullptr; } + + // Apply theme to the element elem = theme->getElement(view, "gridtile.favorite", "image"); if (elem) { createFavorite(); mFavorite->applyTheme(theme, view, "gridtile.favorite", ThemeFlags::ALL); - mDefaultProperties.Favorite.sizeMode = 0; - mDefaultProperties.Favorite.loadElement(elem); + mDefaultProperties.Favorite.sizeMode = "size"; + mDefaultProperties.Favorite.applyTheme(elem); mSelectedProperties.Favorite = mDefaultProperties.Favorite; + // Apply theme to the element elem = theme->getElement(view, "gridtile.favorite:selected", "image"); if (elem) - mSelectedProperties.Favorite.loadElement(elem); + mSelectedProperties.Favorite.applyTheme(elem); } else if (mFavorite != nullptr) { @@ -557,145 +629,55 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mFavorite = nullptr; } - // Apply theme to the selected gridtile - // NOTE that some of the default gridtile properties influence on the selected gridtile properties - // See THEMES.md for more informations - elem = theme->getElement(view, "selected", "gridtile"); - - mSelectedProperties.mSize = elem && elem->has("size") ? - elem->get("size") * screen : - mDefaultProperties.mSize; - //getSelectedTileSize(); - - mSelectedProperties.mPadding = elem && elem->has("padding") ? - elem->get("padding") : - mDefaultProperties.mPadding; - if (elem && elem->has("imageColor")) - mSelectedProperties.mImageColor = elem->get("imageColor"); - - mSelectedProperties.mBackgroundImage = elem && elem->has("backgroundImage") ? - elem->get("backgroundImage") : - mDefaultProperties.mBackgroundImage; + // Apply theme to the element + elem = theme->getElement(view, "gridtile.overlay", "image"); + if (elem) + { + createImageOverlay(); + mImageOverlay->applyTheme(theme, view, "gridtile.overlay", ThemeFlags::ALL); - mSelectedProperties.mBackgroundCornerSize = elem && elem->has("backgroundCornerSize") ? - elem->get("backgroundCornerSize") : - mDefaultProperties.mBackgroundCornerSize; + mDefaultProperties.ImageOverlay.sizeMode = "size"; + mDefaultProperties.ImageOverlay.applyTheme(elem); + mSelectedProperties.ImageOverlay = mDefaultProperties.ImageOverlay; - if (elem && elem->has("backgroundColor")) + // Apply theme to the element + elem = theme->getElement(view, "gridtile.overlay:selected", "image"); + if (elem) + mSelectedProperties.ImageOverlay.applyTheme(elem); + } + else if (mImageOverlay != nullptr) { - mSelectedProperties.mBackgroundCenterColor = elem->get("backgroundColor"); - mSelectedProperties.mBackgroundEdgeColor = elem->get("backgroundColor"); + removeChild(mImageOverlay); + delete mImageOverlay; + mImageOverlay = nullptr; } - if (elem && elem->has("backgroundCenterColor")) - mSelectedProperties.mBackgroundCenterColor = elem->get("backgroundCenterColor"); - - if (elem && elem->has("backgroundEdgeColor")) - mSelectedProperties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); - - if (elem && elem->has("reflexion")) - mSelectedProperties.mMirror = elem->get("reflexion"); + // Apply theme to the element elem = theme->getElement(view, "gridtile", "text"); + if (elem == nullptr) // Apply theme to the element + elem = theme->getElement(view, "gridtile.text", "text"); + if (elem != NULL) { - float sh = (float)Renderer::getScreenHeight(); - - if (elem->has("pos")) - { - mDefaultProperties.mLabelPos = elem->get("pos"); - mSelectedProperties.mLabelPos = mDefaultProperties.mLabelPos; - mLabelMerged = true; - } - else - mLabelMerged = false; - - if (elem->has("size")) - { - mDefaultProperties.mLabelSize = elem->get("size"); - mSelectedProperties.mLabelSize = mDefaultProperties.mLabelSize; - - if (!mLabelMerged) - mLabelMerged = mDefaultProperties.mLabelSize.x() == 0; - } - - if (elem->has("color")) - { - mDefaultProperties.mLabelColor = elem->get("color"); - mSelectedProperties.mLabelColor = mDefaultProperties.mLabelColor; - } - - if (elem->has("backgroundColor")) - { - mDefaultProperties.mLabelBackColor = elem->get("backgroundColor"); - mSelectedProperties.mLabelBackColor = mDefaultProperties.mLabelBackColor; - } - - if (elem->has("glowSize")) - { - mDefaultProperties.mLabelGlowSize = (unsigned int)elem->get("glowSize"); - mSelectedProperties.mLabelGlowSize = mDefaultProperties.mLabelGlowSize; - } - - if (elem->has("glowColor")) - { - mDefaultProperties.mLabelGlowColor = elem->get("glowColor"); - mSelectedProperties.mLabelGlowColor = mDefaultProperties.mLabelGlowColor; - } - - if (elem->has("fontSize")) - { - mDefaultProperties.mFontSize = elem->get("fontSize") * sh; - mSelectedProperties.mFontSize = mDefaultProperties.mFontSize; - } - - if (elem->has("fontPath")) - { - mDefaultProperties.mFontPath = elem->get("fontPath"); - mSelectedProperties.mFontPath = mDefaultProperties.mFontPath; - } - - if (elem->has("visible")) - mLabelVisible = elem->get("visible"); - else - mLabelVisible = true; - mLabel.applyTheme(theme, view, element, properties); - elem = theme->getElement(view, "gridtile_selected", "text"); - if (elem == nullptr) - elem = theme->getElement(view, "gridtile:selected", "text"); - - if (elem != NULL) - { - if (elem->has("pos") && mDefaultProperties.mLabelPos.x() >= 0) - mSelectedProperties.mLabelPos = elem->get("pos"); - - if (elem->has("size")) - mSelectedProperties.mLabelSize = elem->get("size"); + mDefaultProperties.Label.applyTheme(elem); + mSelectedProperties.Label = mDefaultProperties.Label; - if (elem->has("color")) - mSelectedProperties.mLabelColor = elem->get("color"); + mLabelMerged = elem->has("pos"); + if (!mLabelMerged && elem->has("size")) + mLabelMerged = mDefaultProperties.Label.size.x() == 0; - if (elem->has("backgroundColor")) - mSelectedProperties.mLabelBackColor = elem->get("backgroundColor"); + // Apply theme to the element + elem = theme->getElement(view, "gridtile:selected", "text"); + if (elem == nullptr) // Apply theme to the element + elem = theme->getElement(view, "gridtile.text:selected", "text"); - if (elem->has("glowSize")) - mSelectedProperties.mLabelGlowSize = (unsigned int)elem->get("glowSize"); - - if (elem->has("glowColor")) - mSelectedProperties.mLabelGlowColor = elem->get("glowColor"); - - if (elem->has("fontSize")) - mSelectedProperties.mFontSize = elem->get("fontSize") * sh; - - if (elem->has("fontPath")) - mSelectedProperties.mFontPath = elem->get("fontPath"); - } + if (elem) + mSelectedProperties.Label.applyTheme(elem); } - else - mLabelVisible = false; } // Made this a static function because the ImageGridComponent need to know the default tile size @@ -703,7 +685,6 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons Vector2f GridTileComponent::getDefaultTileSize() { Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); - return screen * 0.22f; } @@ -726,9 +707,9 @@ void GridTileComponent::setImage(const std::string& path, bool isDefaultImage) mCurrentPath = path; if (mSelectedProperties.mSize.x() > mSize.x()) - mImage->setImage(path, false, MaxSizeInfo(mSelectedProperties.mSize, mSelectedProperties.mImageSizeMode != "maxSize")); + mImage->setImage(path, false, MaxSizeInfo(mSelectedProperties.mSize, mSelectedProperties.Image.sizeMode != "maxSize")); else - mImage->setImage(path, false, MaxSizeInfo(mSize, mSelectedProperties.mImageSizeMode != "maxSize")); + mImage->setImage(path, false, MaxSizeInfo(mSize, mSelectedProperties.Image.sizeMode != "maxSize")); resize(); } @@ -813,7 +794,7 @@ void GridTileComponent::startVideo() if (mVideo != nullptr) { // Inform video component about size before staring in order to be able to use OptimizeVideo parameter - if (mSelectedProperties.mImageSizeMode == "minSize") + if (mSelectedProperties.Image.sizeMode == "minSize") mVideo->setMinSize(mSelectedProperties.mSize); else mVideo->setResize(mSelectedProperties.mSize); @@ -942,6 +923,15 @@ unsigned int GridTileComponent::mixUnsigned(const unsigned int def, const unsign return def * (1.0 - percent) + sel * percent; } +float GridTileComponent::mixFloat(const float def, const float sel, float percent) +{ + if (def == sel) + return def; + + return def * (1.0 - percent) + sel * percent; +} + + GridTileProperties GridTileComponent::getCurrentProperties() { using namespace Renderer; @@ -955,16 +945,17 @@ GridTileProperties GridTileComponent::getCurrentProperties() mMixedProperties.mSize = mixVectors(mDefaultProperties.mSize, mSelectedProperties.mSize, mSelectedZoomPercent); mMixedProperties.mPadding = mixVectors(mDefaultProperties.mPadding, mSelectedProperties.mPadding, mSelectedZoomPercent); - - mMixedProperties.mLabelPos = mixVectors(mDefaultProperties.mLabelPos, mSelectedProperties.mLabelPos, mSelectedZoomPercent); - mMixedProperties.mLabelSize = mixVectors(mDefaultProperties.mLabelSize, mSelectedProperties.mLabelSize, mSelectedZoomPercent); - mMixedProperties.mLabelColor = mixColors(mDefaultProperties.mLabelColor, mSelectedProperties.mLabelColor, mSelectedZoomPercent); - mMixedProperties.mLabelBackColor = mixColors(mDefaultProperties.mLabelBackColor, mSelectedProperties.mLabelBackColor, mSelectedZoomPercent); - mMixedProperties.mLabelGlowColor = mixColors(mDefaultProperties.mLabelGlowColor, mSelectedProperties.mLabelGlowColor, mSelectedZoomPercent); - mMixedProperties.mLabelGlowSize = mixUnsigned(mDefaultProperties.mLabelGlowSize, mSelectedProperties.mLabelGlowSize, mSelectedZoomPercent); - mMixedProperties.mImageColor = mixColors(mDefaultProperties.mImageColor, mSelectedProperties.mImageColor, mSelectedZoomPercent); - mMixedProperties.mMirror = mixVectors(mDefaultProperties.mMirror, mSelectedProperties.mMirror, mSelectedZoomPercent); + if (mDefaultProperties.Label.Loaded) + { + mMixedProperties.Label.pos = mixVectors(mDefaultProperties.Label.pos, mSelectedProperties.Label.pos, mSelectedZoomPercent); + mMixedProperties.Label.size = mixVectors(mDefaultProperties.Label.size, mSelectedProperties.Label.size, mSelectedZoomPercent); + mMixedProperties.Label.color = mixColors(mDefaultProperties.Label.color, mSelectedProperties.Label.color, mSelectedZoomPercent); + mMixedProperties.Label.backColor = mixColors(mDefaultProperties.Label.backColor, mSelectedProperties.Label.backColor, mSelectedZoomPercent); + mMixedProperties.Label.glowColor = mixColors(mDefaultProperties.Label.glowColor, mSelectedProperties.Label.glowColor, mSelectedZoomPercent); + mMixedProperties.Label.glowSize = mixFloat(mDefaultProperties.Label.glowSize, mSelectedProperties.Label.glowSize, mSelectedZoomPercent); + mMixedProperties.Label.fontSize = mixFloat(mDefaultProperties.Label.fontSize, mSelectedProperties.Label.fontSize, mSelectedZoomPercent); + } if (mDefaultProperties.Image.Loaded) { @@ -995,6 +986,16 @@ GridTileProperties GridTileComponent::getCurrentProperties() mMixedProperties.Favorite.colorEnd = mixColors(mDefaultProperties.Favorite.colorEnd, mSelectedProperties.Favorite.colorEnd, mSelectedZoomPercent); mMixedProperties.Favorite.reflexion = mixVectors(mDefaultProperties.Favorite.reflexion, mSelectedProperties.Favorite.reflexion, mSelectedZoomPercent); } + + if (mDefaultProperties.ImageOverlay.Loaded) + { + mMixedProperties.ImageOverlay.pos = mixVectors(mDefaultProperties.ImageOverlay.pos, mSelectedProperties.ImageOverlay.pos, mSelectedZoomPercent); + mMixedProperties.ImageOverlay.size = mixVectors(mDefaultProperties.ImageOverlay.size, mSelectedProperties.ImageOverlay.size, mSelectedZoomPercent); + mMixedProperties.ImageOverlay.origin = mixVectors(mDefaultProperties.ImageOverlay.origin, mSelectedProperties.ImageOverlay.origin, mSelectedZoomPercent); + mMixedProperties.ImageOverlay.color = mixColors(mDefaultProperties.ImageOverlay.color, mSelectedProperties.ImageOverlay.color, mSelectedZoomPercent); + mMixedProperties.ImageOverlay.colorEnd = mixColors(mDefaultProperties.ImageOverlay.colorEnd, mSelectedProperties.ImageOverlay.colorEnd, mSelectedZoomPercent); + mMixedProperties.ImageOverlay.reflexion = mixVectors(mDefaultProperties.ImageOverlay.reflexion, mSelectedProperties.ImageOverlay.reflexion, mSelectedZoomPercent); + } return mMixedProperties; } diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index 651d732a99..305ffac14f 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -9,10 +9,10 @@ class VideoComponent; -struct ImageProperties +struct GridImageProperties { public: - ImageProperties() + GridImageProperties() { Loaded = false; Visible = false; @@ -22,7 +22,7 @@ struct ImageProperties size = Vector2f(1.0f, 1.0f); origin = Vector2f(0.5f, 0.5f); color = colorEnd = 0xFFFFFFFF; - sizeMode = 2; + sizeMode = "maxSize"; } void updateImageComponent(ImageComponent* image, Vector2f parentSize, bool disableSize = false) @@ -32,9 +32,9 @@ struct ImageProperties image->setPosition(pos.x() * parentSize.x(), pos.y() * parentSize.y()); - if (!disableSize && sizeMode == 0) + if (!disableSize && sizeMode == "size") image->setSize(size.x() * parentSize.x(), size.y() * parentSize.y()); - else if (sizeMode == 1) + else if (sizeMode == "minSize") image->setMinSize(size.x() * parentSize.x(), size.y() * parentSize.y()); else image->setMaxSize(size.x() * parentSize.x(), size.y() * parentSize.y()); @@ -45,100 +45,87 @@ struct ImageProperties image->setMirroring(reflexion); } - bool loadElement(const ThemeData::ThemeElement* elem) - { - if (!elem) - { - Visible = false; - return false; - } - - Loaded = true; - Visible = true; - - if (elem && elem->has("visible")) - Visible = elem->get("visible"); - - if (elem && elem->has("origin")) - origin = elem->get("origin"); - - if (elem && elem->has("pos")) - pos = elem->get("pos"); - - if (elem && elem->has("size")) - { - sizeMode = 0; - size = elem->get("size"); - } - else if (elem && elem->has("minSize")) - { - sizeMode = 1; - size = elem->get("minSize"); - } - else if (elem && elem->has("maxSize")) - { - sizeMode = 2; - size = elem->get("maxSize"); - } - - if (elem && elem->has("color")) - color = colorEnd = elem->get("color"); - - if (elem && elem->has("colorEnd")) - colorEnd = elem->get("colorEnd"); - - if (elem && elem->has("reflexion")) - reflexion = elem->get("reflexion"); - - return true; - } + bool applyTheme(const ThemeData::ThemeElement* elem); bool Loaded; bool Visible; Vector2f pos; Vector2f size; - Vector2f origin; - Vector2f reflexion; unsigned int color; unsigned int colorEnd; - int sizeMode; + std::string sizeMode; }; -struct GridTileProperties +struct GridTextProperties { - Vector2f mSize; - Vector2f mPadding; - unsigned int mImageColor; - std::string mBackgroundImage; - Vector2f mBackgroundCornerSize; - unsigned int mBackgroundCenterColor; - unsigned int mBackgroundEdgeColor; +public: + GridTextProperties() + { + Loaded = false; + Visible = false; + + pos = Vector2f(-1, -1); + size = Vector2f(1.0f, 0.30f); + color = 0xFFFFFFFF; + backColor = 0; + fontSize = 0; + glowColor = 0; + glowSize = 0; + } - std::string mImageSizeMode; - std::string mSelectionMode; + void updateTextComponent(TextComponent* text, Vector2f parentSize, bool disableSize = false) + { + if (text == nullptr) + return; - Vector2f mLabelPos; - Vector2f mLabelSize; + text->setPosition(pos.x() * parentSize.x(), pos.y() * parentSize.y()); + text->setSize(size.x() * parentSize.x(), size.y() * parentSize.y()); + text->setColor(color); + text->setBackgroundColor(backColor); + text->setGlowColor(glowColor); + text->setGlowSize(glowSize); + text->setFont(fontPath, fontSize * (float)Renderer::getScreenHeight()); + } - unsigned int mLabelColor; - unsigned int mLabelBackColor; + bool applyTheme(const ThemeData::ThemeElement* elem); - unsigned int mLabelGlowColor; - unsigned int mLabelGlowSize; + bool Loaded; + bool Visible; - std::string mFontPath; - unsigned int mFontSize; + Vector2f pos; + Vector2f size; - Vector2f mMirror; + unsigned int color; + unsigned int backColor; - ImageProperties Image; - ImageProperties Marquee; - ImageProperties Favorite; + unsigned int glowColor; + float glowSize; + + std::string fontPath; + float fontSize; +}; + +struct GridTileProperties +{ + Vector2f mSize; + Vector2f mPadding; + std::string mSelectionMode; + + std::string mBackgroundImage; + Vector2f mBackgroundCornerSize; + unsigned int mBackgroundCenterColor; + unsigned int mBackgroundEdgeColor; + + GridTextProperties Label; + GridImageProperties Image; + GridImageProperties Marquee; + GridImageProperties Favorite; + GridImageProperties ImageOverlay; }; class GridTileComponent : public GuiComponent @@ -191,19 +178,22 @@ class GridTileComponent : public GuiComponent void createVideo(); void createMarquee(); void createFavorite(); + void createImageOverlay(); void startVideo(); void stopVideo(); void resize(); inline static unsigned int mixUnsigned(const unsigned int def, const unsigned int sel, float percent); + inline static float mixFloat(const float def, const float sel, float percent); static Vector2f mixVectors(const Vector2f& def, const Vector2f& sel, float percent); + static void applyThemeToProperties(const ThemeData::ThemeElement* elem, GridTileProperties& properties); GridTileProperties getCurrentProperties(); TextComponent mLabel; - bool mLabelVisible; + // bool mLabelVisible; bool mLabelMerged; NinePatchComponent mBackground; @@ -229,6 +219,7 @@ class GridTileComponent : public GuiComponent ImageComponent* mImage; ImageComponent* mMarquee; ImageComponent* mFavorite; + ImageComponent* mImageOverlay; bool mVideoPlaying; bool mShown; diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index d13155a30a..8f95d4147a 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -457,7 +457,7 @@ void ImageComponent::render(const Transform4x4f& parentTrans) size_x = mTargetSize.x(); size_y = mTargetSize.y(); } - + Renderer::enableRoundCornerStencil(x, y, size_x, size_y, radius); mTexture->bind(); diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index a3749bf119..22566d2199 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -115,13 +115,23 @@ void NinePatchComponent::render(const Transform4x4f& parentTrans) if (!Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) return; - if (mCornerSize.x() == 0 && mCornerSize.y() == 0) + if (mCornerSize.x() <= 1 && mCornerSize.y() <= 1 && mCornerSize.x() == mCornerSize.y()) { float opacity = mOpacity / 255.0; const unsigned int edgeColor = mEdgeColor & 0xFFFFFF00 | (unsigned char)((mEdgeColor & 0xFF) * opacity); Renderer::setMatrix(trans); + + if (mCornerSize.x() > 0) + { + int radius = Math::max(mSize.x(), mSize.y()) * mCornerSize.x(); + Renderer::enableRoundCornerStencil(0, 0, mSize.x(), mSize.y(), radius); + } + Renderer::drawRect(0.0, 0.0, mSize.x(), mSize.y(), edgeColor, edgeColor); + + if (mCornerSize.x() > 0) + Renderer::disableStencil(); } else if (mTexture->bind()) { @@ -147,7 +157,7 @@ const Vector2f& NinePatchComponent::getCornerSize() const return mCornerSize; } -void NinePatchComponent::setCornerSize(int sizeX, int sizeY) +void NinePatchComponent::setCornerSize(float sizeX, float sizeY) { if (mCornerSize.x() == sizeX && mCornerSize.y() == sizeY) return; diff --git a/es-core/src/components/NinePatchComponent.h b/es-core/src/components/NinePatchComponent.h index 76c2dd2cb5..52bcede7b1 100644 --- a/es-core/src/components/NinePatchComponent.h +++ b/es-core/src/components/NinePatchComponent.h @@ -37,7 +37,7 @@ class NinePatchComponent : public GuiComponent virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) override; const Vector2f& getCornerSize() const; - void setCornerSize(int sizeX, int sizeY); + void setCornerSize(float sizeX, float sizeY); inline void setCornerSize(const Vector2f& size) { setCornerSize(size.x(), size.y()); } virtual void setOpacity(unsigned char opacity); diff --git a/es-core/src/renderers/Renderer.h b/es-core/src/renderers/Renderer.h index c105b9a1ee..0eda871eb1 100644 --- a/es-core/src/renderers/Renderer.h +++ b/es-core/src/renderers/Renderer.h @@ -109,7 +109,7 @@ namespace Renderer unsigned int mixColors(unsigned int first, unsigned int second, float percent); - void enableRoundCornerStencil(int x, int y, int size_x, int size_y, int radius); + void enableRoundCornerStencil(float x, float y, float size_x, float size_y, float radius); void disableStencil(); } // Renderer:: diff --git a/es-core/src/renderers/Renderer_GL21.cpp b/es-core/src/renderers/Renderer_GL21.cpp index bbf4330357..1adcbb803f 100644 --- a/es-core/src/renderers/Renderer_GL21.cpp +++ b/es-core/src/renderers/Renderer_GL21.cpp @@ -270,30 +270,30 @@ namespace Renderer } // swapBuffers - #define N_ROUNDING_PIECES 10 + #define ROUNDING_PIECES 8.0f - void drawGLRoundedCorner(int x, int y, double sa, double arc, float r) + void drawGLRoundedCorner(float x, float y, double sa, double arc, float r) { // centre of the arc, for clockwise sense - float cent_x = x + r * cos(sa + ES_PI / 2); - float cent_y = y + r * sin(sa + ES_PI / 2); + float cent_x = x + r * Math::cosf(sa + ES_PI / 2.0f); + float cent_y = y + r * Math::sinf(sa + ES_PI / 2.0f); // build up piecemeal including end of the arc - int n = ceil(N_ROUNDING_PIECES * arc / ES_PI * 2); + int n = ceil(ROUNDING_PIECES * arc / ES_PI * 2.0f); for (int i = 0; i <= n; i++) { - double ang = sa + arc * (double)i / (double)n; + float ang = sa + arc * (double)i / (double)n; // compute the next point - float next_x = cent_x + r * sin(ang); - float next_y = cent_y - r * cos(ang); + float next_x = cent_x + r * Math::sinf(ang); + float next_y = cent_y - r * Math::cosf(ang); glColor3f(1.0f, 1.0f, 1.0f); glVertex2f(next_x, next_y); } } - - void enableRoundCornerStencil(int x, int y, int size_x, int size_y, int radius) + + void enableRoundCornerStencil(float x, float y, float width, float height, float radius) { Renderer::bindTexture(0); @@ -308,12 +308,12 @@ namespace Renderer glClear(GL_STENCIL_BUFFER_BIT); glBegin(GL_POLYGON); - drawGLRoundedCorner(x, y + radius, 3 * ES_PI / 2, ES_PI / 2, radius); - drawGLRoundedCorner(x + size_x - radius, y, 0.0, ES_PI / 2, radius); - drawGLRoundedCorner(x + size_x, y + size_y - radius, ES_PI / 2, ES_PI / 2, radius); - drawGLRoundedCorner(x + radius, y + size_y, ES_PI, ES_PI / 2, radius); + drawGLRoundedCorner(x, y + radius, 3.0f * ES_PI / 2.0f, ES_PI / 2.0f, radius); + drawGLRoundedCorner(x + width - radius, y, 0.0, ES_PI / 2.0f, radius); + drawGLRoundedCorner(x + width, y + height - radius, ES_PI / 2.0f, ES_PI / 2.0f, radius); + drawGLRoundedCorner(x + radius, y + height, ES_PI, ES_PI / 2.0f, radius); glEnd(); - + glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); glDepthMask(GL_TRUE); glStencilMask(0x00); diff --git a/es-core/src/renderers/Renderer_GLES10.cpp b/es-core/src/renderers/Renderer_GLES10.cpp index ea9e155714..a700a77034 100644 --- a/es-core/src/renderers/Renderer_GLES10.cpp +++ b/es-core/src/renderers/Renderer_GLES10.cpp @@ -268,13 +268,13 @@ namespace Renderer } // swapBuffers -#define N_ROUNDING_PIECES 10 + #define N_ROUNDING_PIECES 8.0f - void drawGLRoundedCorner(int x, int y, double sa, double arc, float r) + void drawGLRoundedCorner(float x, float y, float sa, float arc, float r) { // centre of the arc, for clockwise sense - float cent_x = x + r * cos(sa + ES_PI / 2); - float cent_y = y + r * sin(sa + ES_PI / 2); + float cent_x = x + r * Math::cosf(sa + ES_PI / 2); + float cent_y = y + r * Math::sinf(sa + ES_PI / 2); // build up piecemeal including end of the arc int n = ceil(N_ROUNDING_PIECES * arc / ES_PI * 2); @@ -283,15 +283,15 @@ namespace Renderer double ang = sa + arc * (double)i / (double)n; // compute the next point - float next_x = cent_x + r * sin(ang); - float next_y = cent_y - r * cos(ang); + float next_x = cent_x + r * Math::sinf(ang); + float next_y = cent_y - r * Math::cosf(ang); glColor3f(1.0f, 1.0f, 1.0f); glVertex2f(next_x, next_y); } } - void enableRoundCornerStencil(int x, int y, int size_x, int size_y, int radius) + void enableRoundCornerStencil(float x, float y, float width, float height, float radius) { Renderer::bindTexture(0); From 5d2b1534e17252336d32a39ca3e34e00ac699930 Mon Sep 17 00:00:00 2001 From: Justin Kinnaird Date: Wed, 13 Nov 2019 10:36:19 -0600 Subject: [PATCH 222/336] Remove redundant dirname. --- es-app/src/main.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index dd9d5652c7..0486e2504b 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -117,7 +117,7 @@ bool parseArgs(int argc, char* argv[]) ssize_t len = readlink("/proc/self/exe", result, PATH_MAX); if (len != -1) { result[len] = 0; - Utils::FileSystem::setExePath(dirname(result)); + Utils::FileSystem::setExePath(result); } delete [] result; #else From 7479002e23f124c61d26ea6f662079111a9edaf9 Mon Sep 17 00:00:00 2001 From: Justin Kinnaird Date: Wed, 13 Nov 2019 10:40:29 -0600 Subject: [PATCH 223/336] Remove unused include --- es-app/src/main.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 0486e2504b..45862a2b99 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -25,7 +25,6 @@ #if defined(_WIN32) #include #elif defined(__linux__) -#include #include #endif From fc054bbef38d9c743838284f15853316ea2819d8 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 15 Nov 2019 16:57:55 +0100 Subject: [PATCH 224/336] GridView misc & Randoms first version --- es-app/src/components/RatingComponent.cpp | 24 +- es-app/src/components/RatingComponent.h | 1 + es-app/src/views/SystemView.cpp | 44 ++ es-core/src/ThemeData.cpp | 8 + es-core/src/components/GridTileComponent.cpp | 380 ++++++++++-------- es-core/src/components/GridTileComponent.h | 81 +++- es-core/src/components/ImageComponent.cpp | 18 +- es-core/src/components/ImageComponent.h | 1 + es-core/src/components/NinePatchComponent.cpp | 56 ++- es-core/src/components/NinePatchComponent.h | 9 + es-core/src/components/VideoComponent.cpp | 19 + es-core/src/components/VideoComponent.h | 25 +- es-core/src/components/VideoVlcComponent.cpp | 36 +- es-core/src/renderers/Renderer.h | 2 + es-core/src/renderers/Renderer_GL21.cpp | 49 ++- es-core/src/renderers/Renderer_GLES10.cpp | 53 ++- 16 files changed, 568 insertions(+), 238 deletions(-) diff --git a/es-app/src/components/RatingComponent.cpp b/es-app/src/components/RatingComponent.cpp index 7db6f4e9f4..357c36bb5d 100644 --- a/es-app/src/components/RatingComponent.cpp +++ b/es-app/src/components/RatingComponent.cpp @@ -3,7 +3,7 @@ #include "resources/TextureResource.h" #include "ThemeData.h" -RatingComponent::RatingComponent(Window* window) : GuiComponent(window), mColorShift(0xFFFFFFFF) +RatingComponent::RatingComponent(Window* window) : GuiComponent(window), mColorShift(0xFFFFFFFF), mUnfilledColor(0xFFFFFFFF) { mFilledTexture = TextureResource::get(":/star_filled.svg", true); mUnfilledTexture = TextureResource::get(":/star_unfilled.svg", true); @@ -120,8 +120,18 @@ void RatingComponent::render(const Transform4x4f& parentTrans) if (mUnfilledTexture->bind()) { + if (mUnfilledColor != mColorShift) + { + const unsigned int color = Renderer::convertColor(mUnfilledColor); + for (int i = 0; i < 8; ++i) + mVertices[i].col = color; + } + Renderer::drawTriangleStrips(&mVertices[4], 4); Renderer::bindTexture(0); + + if (mUnfilledColor != mColorShift) + updateColors(); } if (mFilledTexture->bind()) @@ -170,8 +180,16 @@ void RatingComponent::applyTheme(const std::shared_ptr& theme, const } - if(properties & COLOR && elem->has("color")) - setColorShift(elem->get("color")); + if (properties & COLOR) + { + if (elem->has("color")) + setColorShift(elem->get("color")); + + if (elem->has("unfilledColor")) + mUnfilledColor = elem->get("unfilledColor"); + else + mUnfilledColor = mColorShift; + } if(imgChanged) onSizeChanged(); diff --git a/es-app/src/components/RatingComponent.h b/es-app/src/components/RatingComponent.h index ae2c3ae7e2..efc6cac9f9 100644 --- a/es-app/src/components/RatingComponent.h +++ b/es-app/src/components/RatingComponent.h @@ -45,6 +45,7 @@ class RatingComponent : public GuiComponent Renderer::Vertex mVertices[8]; unsigned int mColorShift; + unsigned int mUnfilledColor; std::shared_ptr mFilledTexture; std::shared_ptr mUnfilledTexture; diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index eeec65de48..5c06c8c976 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -9,6 +9,7 @@ #include "SystemData.h" #include "Window.h" #include "AudioManager.h" +#include "components/VideoComponent.h" // buffer values for scrolling velocity (left, stopped, right) const int logoBuffersLeft[] = { -5, -2, -1 }; @@ -56,6 +57,39 @@ void SystemView::clearEntries() mEntries.clear(); } +class SystemVideoPlaylist : public IVideoPlaylist +{ +public: + SystemVideoPlaylist(SystemData* system) + { + mSystem = system; + } + + std::string getNextVideo() + { + if (mFirstRun) + { + std::vector files = mSystem->getRootFolder()->getFilesRecursive(GAME, false); + for (auto file : files) + if (!file->getVideoPath().empty()) + mVideos.push_back(file->getVideoPath()); + } + + int video = (int) ((float) rand() * mVideos.size()) / float(RAND_MAX); + + // (int)(((float)rand() / float(RAND_MAX)) * (float)mVideos.size()); + if (video >= 0 && video < mVideos.size()) + return mVideos[video]; + + return ""; + } + +private: + SystemData* mSystem; + bool mFirstRun; + std::vector mVideos; +}; + void SystemView::populate() { clearEntries(); @@ -159,6 +193,16 @@ void SystemView::populate() // make background extras e.data.backgroundExtras = ThemeData::makeExtras((*it)->getTheme(), "system", mWindow); + for (auto bx : e.data.backgroundExtras) + { + if (bx->getValue() != "VideoComponent") + continue; + + auto elem = (*it)->getTheme()->getElement("system", bx->getTag(), "video"); + if (elem != nullptr && elem->has("path") && elem->get("path") == "*") + ((VideoComponent*)bx)->setPlaylist(std::make_shared(*it)); + } + // sort the extras by z-index std::stable_sort(e.data.backgroundExtras.begin(), e.data.backgroundExtras.end(), [](GuiComponent* a, GuiComponent* b) { return b->getZIndex() > a->getZIndex(); diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index d41892a097..bc8796c538 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -128,6 +128,8 @@ std::map> The { "cornerSize", NORMALIZED_PAIR }, { "centerColor", COLOR }, { "edgeColor", COLOR }, + { "animateColor", COLOR }, + { "animateColorTime", FLOAT }, { "zIndex", FLOAT } } }, { "datetime", { { "pos", NORMALIZED_PAIR }, @@ -154,6 +156,7 @@ std::map> The { "rotation", FLOAT }, { "rotationOrigin", NORMALIZED_PAIR }, { "color", COLOR }, + { "unfilledColor", COLOR }, { "filledPath", PATH }, { "unfilledPath", PATH }, { "visible", BOOLEAN }, @@ -983,6 +986,11 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapsetMaxSize(imageWidth, imageHeight); + mImage->setMaxSize(imageSize.x(), imageSize.y()); } else if (mImage != nullptr) { // Retrocompatibility : imagegrid.image is not defined - mImage->setPosition(imageSize.x() / 2.0f, imageSize.y() / 2.0f); + mImage->setOrigin(0.5f, 0.5f); + mImage->setPosition(imageOffset.x() + imageSize.x() / 2.0f, imageOffset.y() + imageSize.y() / 2.0f); mImage->setColorShift(currentProperties.Image.color); mImage->setMirroring(currentProperties.Image.reflexion); if (currentProperties.Image.sizeMode == "minSize" && !isDefaultImage) - mImage->setMinSize(imageWidth, imageHeight); + mImage->setMinSize(imageSize.x(), imageSize.y()); else if (currentProperties.Image.sizeMode == "size") - mImage->setSize(imageWidth, imageHeight); + mImage->setSize(imageSize.x(), imageSize.x()); else - mImage->setMaxSize(imageWidth, imageHeight); - } + mImage->setMaxSize(imageSize.x(), imageSize.y()); +// imageOffset = Vector2f::Zero(); + } + + // Recompute final image size if necessary + if (mImage != nullptr && currentProperties.Image.sizeMode == "maxSize") + { + auto origin = mImage->getOrigin(); + auto pos = mImage->getPosition(); + imageSize = mImage->getSize(); + imageOffset = Vector2f(pos.x() - imageSize.x() * origin.x(), pos.y() - imageSize.y() * origin.y()); + } + // Text mLabel.setVisible(currentProperties.Label.Visible || mIsDefaultImage); @@ -188,82 +189,75 @@ void GridTileComponent::resize() // Other controls ( Favorite / Marquee / Overlay ) if (currentProperties.Favorite.Loaded) - currentProperties.Favorite.updateImageComponent(mFavorite, Vector2f(size.x(), (size.y() - height)), false); + currentProperties.Favorite.updateImageComponent(mFavorite, imageOffset, imageSize, false); if (currentProperties.Marquee.Loaded) - currentProperties.Marquee.updateImageComponent(mMarquee, Vector2f(size.x(), (size.y() - height)), true); + currentProperties.Marquee.updateImageComponent(mMarquee, imageOffset, imageSize, true); if (currentProperties.ImageOverlay.Loaded) - currentProperties.ImageOverlay.updateImageComponent(mImageOverlay, Vector2f(size.x(), (size.y() - height)), false); + currentProperties.ImageOverlay.updateImageComponent(mImageOverlay, imageOffset, imageSize, false); // Video if (mVideo != nullptr && mVideo->isPlaying()) { - mVideo->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); - if (currentProperties.Image.sizeMode == "minSize") { + mVideo->setOrigin(0, 0); + mVideo->setPosition(imageOffset.x(), imageOffset.y()); + mVideo->setMinSize(imageSize.x(), imageSize.y()); + if (mImage != nullptr) mVideo->setRoundCorners(mImage->getRoundCorners()); - - auto vs = mVideo->getVideoSize(); - if (vs == Vector2f(0, 0)) - vs = Vector2f(640, 480); - - mVideo->setSize(ImageIO::adjustExternPictureSizef(vs, Vector2f(imageWidth, imageHeight))); } else + { + mVideo->setOrigin(0.5f, 0.5f); + mVideo->setPosition(size.x() / 2.0f, (size.y() - height) / 2.0f); + if (currentProperties.Image.sizeMode == "size") - mVideo->setSize(imageWidth, size.y() - topPadding - bottomPadding); + mVideo->setSize(imageSize.x(), size.y() - topPadding - bottomPadding); else - mVideo->setMaxSize(imageWidth, size.y() - topPadding - bottomPadding); + mVideo->setMaxSize(imageSize.x(), size.y() - topPadding - bottomPadding); + } } - // Background - Vector3f bkposition = Vector3f(0, 0); + // Background when SelectionMode == "image" + Vector3f bkPosition = Vector3f::Zero(); Vector2f bkSize = size; - if (mImage != NULL && currentProperties.mSelectionMode == "image" && mImage->getSize() != Vector2f(0, 0)) + if (mImage != NULL && currentProperties.SelectionMode == "image" && mImage->getSize() != Vector2f(0, 0)) { if (currentProperties.Image.sizeMode == "minSize") bkSize = Vector2f(size.x(), size.y() - bottomPadding + topPadding); - else if (mAnimPosition == Vector3f(0, 0, 0)) - { - bkposition = Vector3f( - mImage->getPosition().x() - mImage->getSize().x() / 2 - mSelectedProperties.mPadding.x(), - mImage->getPosition().y() - mImage->getSize().y() / 2 - mSelectedProperties.mPadding.y(), 0); - - bkSize = Vector2f(mImage->getSize().x() + 2 * mSelectedProperties.mPadding.x(), mImage->getSize().y() + 2 * mSelectedProperties.mPadding.y()); - } else { - bkposition = Vector3f( - mImage->getPosition().x() - mImage->getSize().x() / 2 - currentProperties.mPadding.x(), - mImage->getPosition().y() - mImage->getSize().y() / 2 - currentProperties.mPadding.y(), 0); - - bkSize = Vector2f(mImage->getSize().x() + 2 * currentProperties.mPadding.x(), mImage->getSize().y() + 2 * currentProperties.mPadding.y()); + bkPosition = Vector3f(imageOffset.x() - mSelectedProperties.Padding.x(), imageOffset.y() - mSelectedProperties.Padding.y(), 0); + bkSize = Vector2f(imageSize.x() + 2 * mSelectedProperties.Padding.x(), imageSize.y() + 2 * mSelectedProperties.Padding.y()); } } - + + // Background when animating if (mSelectedZoomPercent != 1.0f && mAnimPosition.x() != 0 && mAnimPosition.y() != 0 && mSelected) { - float x = mPosition.x() + bkposition.x(); - float y = mPosition.y() + bkposition.y(); + float x = mPosition.x() + bkPosition.x(); + float y = mPosition.y() + bkPosition.y(); x = mAnimPosition.x() * (1.0 - mSelectedZoomPercent) + x * mSelectedZoomPercent; y = mAnimPosition.y() * (1.0 - mSelectedZoomPercent) + y * mSelectedZoomPercent; - bkposition = Vector3f(x - mPosition.x(), y - mPosition.y(), 0); + bkPosition = Vector3f(x - mPosition.x(), y - mPosition.y(), 0); } - mBackground.setPosition(bkposition); + mBackground.setPosition(bkPosition); mBackground.setSize(bkSize); - mBackground.setCornerSize(currentProperties.mBackgroundCornerSize); - mBackground.setCenterColor(currentProperties.mBackgroundCenterColor); - mBackground.setEdgeColor(currentProperties.mBackgroundEdgeColor); - mBackground.setImagePath(currentProperties.mBackgroundImage); - + currentProperties.Background.updateNinePatchComponent(&mBackground); + /* + mBackground.setCornerSize(currentProperties.Background.cornerSize); + mBackground.setCenterColor(currentProperties.Background.centerColor); + mBackground.setEdgeColor(currentProperties.Background.edgeColor); + mBackground.setImagePath(currentProperties.Background.path); + */ if (mSelected && mAnimPosition == Vector3f(0, 0, 0) && mSelectedZoomPercent != 1.0) mBackground.setOpacity(mSelectedZoomPercent * 255); else @@ -298,10 +292,10 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) return; - auto currentProperties = getCurrentProperties(); + auto currentProperties = getCurrentProperties(false); - float padding = currentProperties.mPadding.x(); - float topPadding = currentProperties.mPadding.y(); + float padding = currentProperties.Padding.x(); + float topPadding = currentProperties.Padding.y(); float bottomPadding = topPadding; if (currentProperties.Label.Visible && !mLabelMerged) @@ -312,19 +306,22 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) bool isDefaultImage = mIsDefaultImage && (mCurrentPath == ":/folder.svg" || mCurrentPath == ":/cartridge.svg"); bool isMinSize = currentProperties.Image.sizeMode == "minSize" && !isDefaultImage; - + if (isMinSize) Renderer::pushClipRect(pos, size); - + if (mImage != NULL) - mImage->render(trans); + { + if (!isMinSize || !mSelected || mVideo == nullptr || !(mVideo->isPlaying() && !mVideo->isFading())) + mImage->render(trans); + } if (mSelected && !mVideoPath.empty() && mVideo != nullptr) mVideo->render(trans); - + if (!mLabelMerged && isMinSize) Renderer::popClipRect(); - + if (mMarquee != nullptr && mMarquee->hasImage()) mMarquee->render(trans); else if (currentProperties.Label.Visible && currentProperties.Label.size.y()>0) @@ -337,7 +334,7 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) if (mImageOverlay != nullptr && mImageOverlay->hasImage() && mImageOverlay->isVisible()) mImageOverlay->render(trans); - + if (mLabelMerged && isMinSize) Renderer::popClipRect(); } @@ -410,32 +407,34 @@ void GridTileComponent::applyThemeToProperties(const ThemeData::ThemeElement* el Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); if (elem->has("size")) - properties.mSize = elem->get("size") * screen; + properties.Size = elem->get("size") * screen; if (elem->has("padding")) - properties.mPadding = elem->get("padding"); + properties.Padding = elem->get("padding"); + if (elem && elem->has("selectionMode")) + properties.SelectionMode = elem->get("selectionMode"); + + // Retrocompatibility for Background properties if (elem->has("backgroundImage")) - properties.mBackgroundImage = elem->get("backgroundImage"); + properties.Background.path = elem->get("backgroundImage"); if (elem->has("backgroundCornerSize")) - properties.mBackgroundCornerSize = elem->get("backgroundCornerSize"); + properties.Background.cornerSize = elem->get("backgroundCornerSize"); if (elem->has("backgroundColor")) { - properties.mBackgroundCenterColor = elem->get("backgroundColor"); - properties.mBackgroundEdgeColor = elem->get("backgroundColor"); + properties.Background.centerColor = elem->get("backgroundColor"); + properties.Background.edgeColor = elem->get("backgroundColor"); } if (elem->has("backgroundCenterColor")) - properties.mBackgroundCenterColor = elem->get("backgroundCenterColor"); + properties.Background.centerColor = elem->get("backgroundCenterColor"); if (elem->has("backgroundEdgeColor")) - properties.mBackgroundEdgeColor = elem->get("backgroundEdgeColor"); - - if (elem && elem->has("selectionMode")) - properties.mSelectionMode = elem->get("selectionMode"); + properties.Background.edgeColor = elem->get("backgroundEdgeColor"); + // Retrocompatibility for Image properties if (elem && elem->has("reflexion")) properties.Image.reflexion = elem->get("reflexion"); @@ -491,6 +490,9 @@ bool GridImageProperties::applyTheme(const ThemeData::ThemeElement* elem) if (elem && elem->has("reflexion")) reflexion = elem->get("reflexion"); + if (elem && elem->has("roundCorners")) + roundCorners = elem->get("roundCorners"); + return true; } @@ -535,6 +537,49 @@ bool GridTextProperties::applyTheme(const ThemeData::ThemeElement* elem) return true; } +bool GridNinePatchProperties::applyTheme(const ThemeData::ThemeElement* elem) +{ + if (!elem) + { + Visible = false; + return false; + } + + Loaded = true; + Visible = true; + + if (elem && elem->has("visible")) + Visible = elem->get("visible"); + /* + if (elem && elem->has("pos")) + pos = elem->get("pos"); + + if (elem && elem->has("size")) + size = elem->get("size"); + */ + if (elem && elem->has("color")) + centerColor = edgeColor = elem->get("color"); + + if (elem && elem->has("centerColor")) + centerColor = elem->get("centerColor"); + + if (elem && elem->has("edgeColor")) + edgeColor = elem->get("edgeColor"); + + if (elem && elem->has("cornerSize")) + cornerSize = elem->get("cornerSize"); + + if (elem && elem->has("path")) + path = elem->get("path"); + + if (elem && elem->has("animateColor")) + animateColor = elem->get("animateColor"); + + if (elem && elem->has("animateColorTime")) + animateTime = elem->get("animateColorTime"); + + return true; +} void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { @@ -575,12 +620,12 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mDefaultProperties.Image.applyTheme(elem); mSelectedProperties.Image.applyTheme(elem); + } - // Apply theme to the element - elem = theme->getElement(view, "gridtile.image:selected", "image"); - if (elem) - mSelectedProperties.Image.applyTheme(elem); - } + // Apply theme to the element + elem = theme->getElement(view, "gridtile.image:selected", "image"); + if (elem) + mSelectedProperties.Image.applyTheme(elem); // Apply theme to the element @@ -664,7 +709,7 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mLabel.applyTheme(theme, view, element, properties); mDefaultProperties.Label.applyTheme(elem); - mSelectedProperties.Label = mDefaultProperties.Label; + mSelectedProperties.Label.applyTheme(elem); mLabelMerged = elem->has("pos"); if (!mLabelMerged && elem->has("size")) @@ -678,6 +723,26 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem) mSelectedProperties.Label.applyTheme(elem); } + + // Apply theme to the element + elem = theme->getElement(view, "gridtile", "ninepatch"); + if (elem == nullptr) // Apply theme to the element + elem = theme->getElement(view, "gridtile.background", "ninepatch"); + + if (elem != NULL) + { + mBackground.applyTheme(theme, view, element, properties); + mDefaultProperties.Background.applyTheme(elem); + mSelectedProperties.Background.applyTheme(elem); + } + + // Apply theme to the element + elem = theme->getElement(view, "gridtile:selected", "ninepatch"); + if (elem == nullptr) // Apply theme to the element + elem = theme->getElement(view, "gridtile.background:selected", "ninepatch"); + + if (elem) + mSelectedProperties.Background.applyTheme(elem); } // Made this a static function because the ImageGridComponent need to know the default tile size @@ -690,7 +755,7 @@ Vector2f GridTileComponent::getDefaultTileSize() Vector2f GridTileComponent::getSelectedTileSize() const { - return mDefaultProperties.mSize * 1.2f; + return mDefaultProperties.Size * 1.2f; } bool GridTileComponent::isSelected() const @@ -706,8 +771,8 @@ void GridTileComponent::setImage(const std::string& path, bool isDefaultImage) mCurrentPath = path; - if (mSelectedProperties.mSize.x() > mSize.x()) - mImage->setImage(path, false, MaxSizeInfo(mSelectedProperties.mSize, mSelectedProperties.Image.sizeMode != "maxSize")); + if (mSelectedProperties.Size.x() > mSize.x()) + mImage->setImage(path, false, MaxSizeInfo(mSelectedProperties.Size, mSelectedProperties.Image.sizeMode != "maxSize")); else mImage->setImage(path, false, MaxSizeInfo(mSize, mSelectedProperties.Image.sizeMode != "maxSize")); @@ -724,8 +789,8 @@ void GridTileComponent::setMarquee(const std::string& path) mCurrentMarquee = path; - if (mSelectedProperties.mSize.x() > mSize.x()) - mMarquee->setImage(path, false, MaxSizeInfo(mSelectedProperties.mSize)); + if (mSelectedProperties.Size.x() > mSize.x()) + mMarquee->setImage(path, false, MaxSizeInfo(mSelectedProperties.Size)); else mMarquee->setImage(path, false, MaxSizeInfo(mSize)); @@ -795,9 +860,9 @@ void GridTileComponent::startVideo() { // Inform video component about size before staring in order to be able to use OptimizeVideo parameter if (mSelectedProperties.Image.sizeMode == "minSize") - mVideo->setMinSize(mSelectedProperties.mSize); + mVideo->setMinSize(mSelectedProperties.Size); else - mVideo->setResize(mSelectedProperties.mSize); + mVideo->setResize(mSelectedProperties.Size); mVideo->setVideo(mVideoPath); } @@ -905,7 +970,12 @@ void GridTileComponent::setVisible(bool visible) mVisible = visible; } -Vector2f GridTileComponent::mixVectors(const Vector2f& def, const Vector2f& sel, float percent) +Vector3f GridTileComponent::getBackgroundPosition() +{ + return Vector3f(mBackground.getPosition().x() + mPosition.x(), mBackground.getPosition().y() + mPosition.y(), 0); +} + +static Vector2f mixVectors(const Vector2f& def, const Vector2f& sel, float percent) { if (def == sel) return def; @@ -915,7 +985,7 @@ Vector2f GridTileComponent::mixVectors(const Vector2f& def, const Vector2f& sel, return Vector2f(x, y); } -unsigned int GridTileComponent::mixUnsigned(const unsigned int def, const unsigned int sel, float percent) +static unsigned int mixUnsigned(const unsigned int def, const unsigned int sel, float percent) { if (def == sel) return def; @@ -923,7 +993,7 @@ unsigned int GridTileComponent::mixUnsigned(const unsigned int def, const unsign return def * (1.0 - percent) + sel * percent; } -float GridTileComponent::mixFloat(const float def, const float sel, float percent) +static float mixFloat(const float def, const float sel, float percent) { if (def == sel) return def; @@ -931,76 +1001,58 @@ float GridTileComponent::mixFloat(const float def, const float sel, float percen return def * (1.0 - percent) + sel * percent; } - -GridTileProperties GridTileComponent::getCurrentProperties() +void GridImageProperties::mixProperties(GridImageProperties& def, GridImageProperties& sel, float percent) { + if (!def.Loaded) + return; + using namespace Renderer; - GridTileProperties mMixedProperties; + pos = mixVectors(def.pos, sel.pos, percent); + size = mixVectors(def.size, sel.size, percent); + origin = mixVectors(def.origin, sel.origin, percent); + color = mixColors(def.color, sel.color, percent); + colorEnd = mixColors(def.colorEnd, sel.colorEnd, percent); + reflexion = mixVectors(def.reflexion, sel.reflexion, percent); + roundCorners = mixFloat(def.roundCorners, sel.roundCorners, percent); +} - if (mSelectedZoomPercent == 0.0f || mSelectedZoomPercent == 1.0f) - return mSelected ? mSelectedProperties : mDefaultProperties; +void GridTextProperties::mixProperties(GridTextProperties& def, GridTextProperties& sel, float percent) +{ + if (!def.Loaded) + return; - mMixedProperties = mSelected ? mSelectedProperties : mDefaultProperties; + using namespace Renderer; - mMixedProperties.mSize = mixVectors(mDefaultProperties.mSize, mSelectedProperties.mSize, mSelectedZoomPercent); - mMixedProperties.mPadding = mixVectors(mDefaultProperties.mPadding, mSelectedProperties.mPadding, mSelectedZoomPercent); + pos = mixVectors(def.pos, sel.pos, percent); + size = mixVectors(def.size, sel.size, percent); + color = mixColors(def.color, sel.color, percent); + backColor = mixColors(def.backColor, sel.backColor, percent); + glowColor = mixColors(def.glowColor, sel.glowColor, percent); + glowSize = mixFloat(def.glowSize, sel.glowSize, percent); + fontSize = mixFloat(def.fontSize, sel.fontSize, percent); +} - if (mDefaultProperties.Label.Loaded) - { - mMixedProperties.Label.pos = mixVectors(mDefaultProperties.Label.pos, mSelectedProperties.Label.pos, mSelectedZoomPercent); - mMixedProperties.Label.size = mixVectors(mDefaultProperties.Label.size, mSelectedProperties.Label.size, mSelectedZoomPercent); - mMixedProperties.Label.color = mixColors(mDefaultProperties.Label.color, mSelectedProperties.Label.color, mSelectedZoomPercent); - mMixedProperties.Label.backColor = mixColors(mDefaultProperties.Label.backColor, mSelectedProperties.Label.backColor, mSelectedZoomPercent); - mMixedProperties.Label.glowColor = mixColors(mDefaultProperties.Label.glowColor, mSelectedProperties.Label.glowColor, mSelectedZoomPercent); - mMixedProperties.Label.glowSize = mixFloat(mDefaultProperties.Label.glowSize, mSelectedProperties.Label.glowSize, mSelectedZoomPercent); - mMixedProperties.Label.fontSize = mixFloat(mDefaultProperties.Label.fontSize, mSelectedProperties.Label.fontSize, mSelectedZoomPercent); - } +GridTileProperties GridTileComponent::getCurrentProperties(bool mixValues) +{ + GridTileProperties prop; - if (mDefaultProperties.Image.Loaded) - { - mMixedProperties.Image.pos = mixVectors(mDefaultProperties.Image.pos, mSelectedProperties.Image.pos, mSelectedZoomPercent); - mMixedProperties.Image.size = mixVectors(mDefaultProperties.Image.size, mSelectedProperties.Image.size, mSelectedZoomPercent); - mMixedProperties.Image.origin = mixVectors(mDefaultProperties.Image.origin, mSelectedProperties.Image.origin, mSelectedZoomPercent); - mMixedProperties.Image.color = mixColors(mDefaultProperties.Image.color, mSelectedProperties.Image.color, mSelectedZoomPercent); - mMixedProperties.Image.colorEnd = mixColors(mDefaultProperties.Image.colorEnd, mSelectedProperties.Image.colorEnd, mSelectedZoomPercent); - mMixedProperties.Image.reflexion = mixVectors(mDefaultProperties.Image.reflexion, mSelectedProperties.Image.reflexion, mSelectedZoomPercent); - } + if (mSelectedZoomPercent == 0.0f || mSelectedZoomPercent == 1.0f) + return mSelected ? mSelectedProperties : mDefaultProperties; - if (mDefaultProperties.Marquee.Loaded) - { - mMixedProperties.Marquee.pos = mixVectors(mDefaultProperties.Marquee.pos, mSelectedProperties.Marquee.pos, mSelectedZoomPercent); - mMixedProperties.Marquee.size = mixVectors(mDefaultProperties.Marquee.size, mSelectedProperties.Marquee.size, mSelectedZoomPercent); - mMixedProperties.Marquee.origin = mixVectors(mDefaultProperties.Marquee.origin, mSelectedProperties.Marquee.origin, mSelectedZoomPercent); - mMixedProperties.Marquee.color = mixColors(mDefaultProperties.Marquee.color, mSelectedProperties.Marquee.color, mSelectedZoomPercent); - mMixedProperties.Marquee.colorEnd = mixColors(mDefaultProperties.Marquee.colorEnd, mSelectedProperties.Marquee.colorEnd, mSelectedZoomPercent); - mMixedProperties.Marquee.reflexion = mixVectors(mDefaultProperties.Marquee.reflexion, mSelectedProperties.Marquee.reflexion, mSelectedZoomPercent); - } + prop = mSelected ? mSelectedProperties : mDefaultProperties; - if (mDefaultProperties.Favorite.Loaded) + if (mixValues) { - mMixedProperties.Favorite.pos = mixVectors(mDefaultProperties.Favorite.pos, mSelectedProperties.Favorite.pos, mSelectedZoomPercent); - mMixedProperties.Favorite.size = mixVectors(mDefaultProperties.Favorite.size, mSelectedProperties.Favorite.size, mSelectedZoomPercent); - mMixedProperties.Favorite.origin = mixVectors(mDefaultProperties.Favorite.origin, mSelectedProperties.Favorite.origin, mSelectedZoomPercent); - mMixedProperties.Favorite.color = mixColors(mDefaultProperties.Favorite.color, mSelectedProperties.Favorite.color, mSelectedZoomPercent); - mMixedProperties.Favorite.colorEnd = mixColors(mDefaultProperties.Favorite.colorEnd, mSelectedProperties.Favorite.colorEnd, mSelectedZoomPercent); - mMixedProperties.Favorite.reflexion = mixVectors(mDefaultProperties.Favorite.reflexion, mSelectedProperties.Favorite.reflexion, mSelectedZoomPercent); + prop.Size = mixVectors(mDefaultProperties.Size, mSelectedProperties.Size, mSelectedZoomPercent); + prop.Padding = mixVectors(mDefaultProperties.Padding, mSelectedProperties.Padding, mSelectedZoomPercent); + + prop.Label.mixProperties(mDefaultProperties.Label, mSelectedProperties.Label, mSelectedZoomPercent); + prop.Image.mixProperties(mDefaultProperties.Image, mSelectedProperties.Image, mSelectedZoomPercent); + prop.Marquee.mixProperties(mDefaultProperties.Marquee, mSelectedProperties.Marquee, mSelectedZoomPercent); + prop.Favorite.mixProperties(mDefaultProperties.Favorite, mSelectedProperties.Favorite, mSelectedZoomPercent); + prop.ImageOverlay.mixProperties(mDefaultProperties.ImageOverlay, mSelectedProperties.ImageOverlay, mSelectedZoomPercent); } - if (mDefaultProperties.ImageOverlay.Loaded) - { - mMixedProperties.ImageOverlay.pos = mixVectors(mDefaultProperties.ImageOverlay.pos, mSelectedProperties.ImageOverlay.pos, mSelectedZoomPercent); - mMixedProperties.ImageOverlay.size = mixVectors(mDefaultProperties.ImageOverlay.size, mSelectedProperties.ImageOverlay.size, mSelectedZoomPercent); - mMixedProperties.ImageOverlay.origin = mixVectors(mDefaultProperties.ImageOverlay.origin, mSelectedProperties.ImageOverlay.origin, mSelectedZoomPercent); - mMixedProperties.ImageOverlay.color = mixColors(mDefaultProperties.ImageOverlay.color, mSelectedProperties.ImageOverlay.color, mSelectedZoomPercent); - mMixedProperties.ImageOverlay.colorEnd = mixColors(mDefaultProperties.ImageOverlay.colorEnd, mSelectedProperties.ImageOverlay.colorEnd, mSelectedZoomPercent); - mMixedProperties.ImageOverlay.reflexion = mixVectors(mDefaultProperties.ImageOverlay.reflexion, mSelectedProperties.ImageOverlay.reflexion, mSelectedZoomPercent); - } - - return mMixedProperties; + return prop; } - -Vector3f GridTileComponent::getBackgroundPosition() -{ - return Vector3f(mBackground.getPosition().x() + mPosition.x(), mBackground.getPosition().y() + mPosition.y(), 0); -} \ No newline at end of file diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index 305ffac14f..64db8ca767 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -23,14 +23,18 @@ struct GridImageProperties origin = Vector2f(0.5f, 0.5f); color = colorEnd = 0xFFFFFFFF; sizeMode = "maxSize"; + roundCorners = 0; } - void updateImageComponent(ImageComponent* image, Vector2f parentSize, bool disableSize = false) + void mixProperties(GridImageProperties& def, GridImageProperties& sel, float percent); + bool applyTheme(const ThemeData::ThemeElement* elem); + + void updateImageComponent(ImageComponent* image, Vector2f offsetPos, Vector2f parentSize, bool disableSize = false) { if (image == nullptr) return; - image->setPosition(pos.x() * parentSize.x(), pos.y() * parentSize.y()); + image->setPosition(offsetPos.x() + pos.x() * parentSize.x(), offsetPos.y() + pos.y() * parentSize.y()); if (!disableSize && sizeMode == "size") image->setSize(size.x() * parentSize.x(), size.y() * parentSize.y()); @@ -43,9 +47,10 @@ struct GridImageProperties image->setColorShift(color); image->setColorShiftEnd(colorEnd); image->setMirroring(reflexion); + image->setRoundCorners(roundCorners); } - bool applyTheme(const ThemeData::ThemeElement* elem); + bool Loaded; bool Visible; @@ -59,6 +64,8 @@ struct GridImageProperties unsigned int colorEnd; std::string sizeMode; + + float roundCorners; }; struct GridTextProperties @@ -78,6 +85,9 @@ struct GridTextProperties glowSize = 0; } + void mixProperties(GridTextProperties& def, GridTextProperties& sel, float percent); + bool applyTheme(const ThemeData::ThemeElement* elem); + void updateTextComponent(TextComponent* text, Vector2f parentSize, bool disableSize = false) { if (text == nullptr) @@ -92,7 +102,7 @@ struct GridTextProperties text->setFont(fontPath, fontSize * (float)Renderer::getScreenHeight()); } - bool applyTheme(const ThemeData::ThemeElement* elem); + bool Loaded; bool Visible; @@ -110,16 +120,58 @@ struct GridTextProperties float fontSize; }; +struct GridNinePatchProperties +{ +public: + GridNinePatchProperties() + { + Loaded = false; + Visible = false; + + edgeColor = centerColor = animateColor = 0xFFFFFFFF; + cornerSize = Vector2f(16, 16); + path = ":/frame.png"; + animateTime = 0; + } + + + + bool applyTheme(const ThemeData::ThemeElement* elem); + + void updateNinePatchComponent(NinePatchComponent* ctl) + { + if (ctl == nullptr) + return; + + ctl->setCenterColor(centerColor); + ctl->setEdgeColor(edgeColor); + ctl->setCornerSize(cornerSize); + ctl->setAnimateTiming(animateTime); + ctl->setAnimateColor(animateColor); + ctl->setImagePath(path); + } + + + + bool Loaded; + bool Visible; + + Vector2f cornerSize; + unsigned int centerColor; + unsigned int edgeColor; + std::string path; + + unsigned int animateColor; + float animateTime; +}; + struct GridTileProperties { - Vector2f mSize; - Vector2f mPadding; - std::string mSelectionMode; + Vector2f Size; + Vector2f Padding; + std::string SelectionMode; - std::string mBackgroundImage; - Vector2f mBackgroundCornerSize; - unsigned int mBackgroundCenterColor; - unsigned int mBackgroundEdgeColor; + GridNinePatchProperties Background; GridTextProperties Label; GridImageProperties Image; @@ -183,13 +235,10 @@ class GridTileComponent : public GuiComponent void stopVideo(); void resize(); - - inline static unsigned int mixUnsigned(const unsigned int def, const unsigned int sel, float percent); - inline static float mixFloat(const float def, const float sel, float percent); - static Vector2f mixVectors(const Vector2f& def, const Vector2f& sel, float percent); + static void applyThemeToProperties(const ThemeData::ThemeElement* elem, GridTileProperties& properties); - GridTileProperties getCurrentProperties(); + GridTileProperties getCurrentProperties(bool mixValues = true); TextComponent mLabel; diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 8f95d4147a..17627e135e 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -444,20 +444,22 @@ void ImageComponent::render(const Transform4x4f& parentTrans) if (mRoundCorners > 0) { - int x = 0; - int y = 0; - int size_x = mSize.x(); - int size_y = mSize.y(); - int radius = Math::max(size_x, size_y) * mRoundCorners; - + float x = 0; + float y = 0; + float size_x = mSize.x(); + float size_y = mSize.y(); + + if (mTargetIsMin) { x = targetSizePos.x(); y = targetSizePos.y(); size_x = mTargetSize.x(); size_y = mTargetSize.y(); - } - + } + + float radius = Math::max(size_x, size_y) * mRoundCorners; + Renderer::enableRoundCornerStencil(x, y, size_x, size_y, radius); mTexture->bind(); diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index b8f514b376..f3bca98cbc 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -105,6 +105,7 @@ class ImageComponent : public GuiComponent void setVerticalAlignment(Alignment align) { mVerticalAlignment = align; } float getRoundCorners() { return mRoundCorners; } + void setRoundCorners(float value) { mRoundCorners = value; } private: Vector2f mTargetSize; diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index 22566d2199..b04fbc90a1 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -9,6 +9,10 @@ mCornerSize(16, 16), mEdgeColor(edgeColor), mCenterColor(centerColor), mVertices(NULL) { + mTimer = 0; + mAnimateTiming = 0; + mAnimateColor = 0xFFFFFFFF; + mPreviousSize = Vector2f(0, 0); setImagePath(path); } @@ -28,6 +32,18 @@ NinePatchComponent::~NinePatchComponent() delete[] mVertices; } +void NinePatchComponent::update(int deltaTime) +{ + GuiComponent::update(deltaTime); + + if (mAnimateTiming > 0) + { + mTimer += deltaTime; + if (mTimer >= 2 * mAnimateTiming) + mTimer = 0; + } +} + void NinePatchComponent::updateColors() { if (mVertices == nullptr) @@ -35,8 +51,18 @@ void NinePatchComponent::updateColors() float opacity = mOpacity / 255.0; - const unsigned int edgeColor = Renderer::convertColor(mEdgeColor & 0xFFFFFF00 | (unsigned char)((mEdgeColor & 0xFF) * opacity)); - const unsigned int centerColor = Renderer::convertColor(mCenterColor & 0xFFFFFF00 | (unsigned char)((mCenterColor & 0xFF) * opacity)); + unsigned int e = mEdgeColor; + unsigned int c = mCenterColor; + + if (mAnimateTiming > 0) + { + float percent = std::abs(mAnimateTiming - mTimer) / mAnimateTiming; + e = Renderer::mixColors(e, mAnimateColor, percent); + c = Renderer::mixColors(e, mAnimateColor, percent); + } + + const unsigned int edgeColor = Renderer::convertColor(e & 0xFFFFFF00 | (unsigned char)((e & 0xFF) * opacity)); + const unsigned int centerColor = Renderer::convertColor(c & 0xFFFFFF00 | (unsigned char)((c & 0xFF) * opacity)); for(int i = 0; i < 6*9; i++) mVertices[i].col = edgeColor; @@ -118,20 +144,26 @@ void NinePatchComponent::render(const Transform4x4f& parentTrans) if (mCornerSize.x() <= 1 && mCornerSize.y() <= 1 && mCornerSize.x() == mCornerSize.y()) { float opacity = mOpacity / 255.0; - const unsigned int edgeColor = mEdgeColor & 0xFFFFFF00 | (unsigned char)((mEdgeColor & 0xFF) * opacity); - Renderer::setMatrix(trans); + unsigned int e = mEdgeColor; - if (mCornerSize.x() > 0) + if (mAnimateTiming > 0) { - int radius = Math::max(mSize.x(), mSize.y()) * mCornerSize.x(); - Renderer::enableRoundCornerStencil(0, 0, mSize.x(), mSize.y(), radius); + float percent = std::abs(mAnimateTiming - mTimer) / mAnimateTiming; + e = Renderer::mixColors(e, mAnimateColor, percent); } - Renderer::drawRect(0.0, 0.0, mSize.x(), mSize.y(), edgeColor, edgeColor); + const unsigned int edgeColor = e & 0xFFFFFF00 | (unsigned char)((e & 0xFF) * opacity); + Renderer::setMatrix(trans); + if (mCornerSize.x() > 0) - Renderer::disableStencil(); + { + int radius = Math::max(mSize.x(), mSize.y()) * mCornerSize.x(); + Renderer::drawRoundRect(0, 0, mSize.x(), mSize.y(), radius, edgeColor); + } + else + Renderer::drawRect(0.0, 0.0, mSize.x(), mSize.y(), edgeColor, edgeColor); } else if (mTexture->bind()) { @@ -235,4 +267,10 @@ void NinePatchComponent::applyTheme(const std::shared_ptr& theme, con if (elem->has("cornerSize")) setCornerSize(elem->get("cornerSize")); + + if (elem->has("animateColor")) + setAnimateColor(elem->get("animateColor")); + + if (elem->has("animateColorTime")) + setAnimateTiming(elem->get("animateColorTime")); } diff --git a/es-core/src/components/NinePatchComponent.h b/es-core/src/components/NinePatchComponent.h index 52bcede7b1..cb4ff95d34 100644 --- a/es-core/src/components/NinePatchComponent.h +++ b/es-core/src/components/NinePatchComponent.h @@ -25,6 +25,7 @@ class NinePatchComponent : public GuiComponent virtual ~NinePatchComponent(); void render(const Transform4x4f& parentTrans) override; + void update(int deltaTime) override; void onSizeChanged() override; @@ -42,6 +43,9 @@ class NinePatchComponent : public GuiComponent virtual void setOpacity(unsigned char opacity); + void setAnimateColor(unsigned int color) { mAnimateColor = color; }; + void setAnimateTiming(float timing) { mAnimateTiming = timing; }; + private: void buildVertices(); void updateColors(); @@ -55,6 +59,11 @@ class NinePatchComponent : public GuiComponent std::shared_ptr mTexture; Vector2f mPreviousSize; + + + float mTimer; + float mAnimateTiming; + unsigned int mAnimateColor; }; #endif // ES_CORE_COMPONENTS_NINE_PATCH_COMPONENT_H diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index f64b8f7565..d5dabd5241 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -448,6 +448,13 @@ void VideoComponent::manageState() void VideoComponent::onShow() { + if (!mShowing && mPlaylist != nullptr && !mVideoPath.empty()) + { + auto video = mPlaylist->getNextVideo(); + if (!video.empty()) + mVideoPath = video; + } + mShowing = true; manageState(); } @@ -475,3 +482,15 @@ void VideoComponent::topWindow(bool isTop) mDisable = !isTop; manageState(); } + + +void VideoComponent::setPlaylist(std::shared_ptr playList) +{ + mPlaylist = playList; + if (mPlaylist == nullptr) + return; + + auto video = mPlaylist->getNextVideo(); + if (!video.empty()) + setVideo(video); +} \ No newline at end of file diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index ab1f5ce82c..aa6cbf05e9 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -20,6 +20,13 @@ enum ImageSource MARQUEE }; */ + +class IVideoPlaylist +{ +public: + virtual std::string getNextVideo() = 0; +}; + class VideoComponent : public GuiComponent { // Structure that groups together the configuration of the video component @@ -36,6 +43,8 @@ class VideoComponent : public GuiComponent VideoComponent(Window* window); virtual ~VideoComponent(); + std::string getValue() const override { return "VideoComponent"; } + // Loads the video at the given filepath bool setVideo(std::string path); // Loads a static image that is displayed if the video cannot be played @@ -112,8 +121,23 @@ class VideoComponent : public GuiComponent float getRoundCorners() { return mRoundCorners; } void setRoundCorners(float value) { mRoundCorners = value; } + + bool isFading() { + return mIsPlaying && mFadeIn < 1.0; + } + + std::string getVideoPath() + { + if (mPlayingVideoPath.empty()) + return mPlayingVideoPath; + + return mVideoPath; + } + + void setPlaylist(std::shared_ptr playList); protected: + std::shared_ptr mPlaylist; std::function mVideoEnded; private: @@ -134,7 +158,6 @@ class VideoComponent : public GuiComponent void manageState(); - protected: unsigned mVideoWidth; diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index e095a0f6cf..d90ba7b14d 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -216,11 +216,12 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) if (t == 0.0) return; + Transform4x4f trans = parentTrans * getTransform(); - - if (!Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) + + if (mRotation == 0 && !mTargetIsMin && !Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) return; - + GuiComponent::renderChildren(trans); Renderer::setMatrix(trans); @@ -254,7 +255,7 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) if (mTexture == nullptr) return; - + const unsigned int fadeIn = t * 255.0f; const unsigned int color = Renderer::convertColor(0xFFFFFF00 | fadeIn); Renderer::Vertex vertices[4]; @@ -302,12 +303,11 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) if (mRoundCorners > 0) { - int x = 0; - int y = 0; - int size_x = mSize.x(); - int size_y = mSize.y(); - int radius = Math::max(size_x, size_y) * mRoundCorners; - + float x = 0; + float y = 0; + float size_x = mSize.x(); + float size_y = mSize.y(); + if (mTargetIsMin) { x = targetSizePos.x(); @@ -315,7 +315,8 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) size_x = mTargetSize.x(); size_y = mTargetSize.y(); } - + + float radius = Math::max(size_x, size_y) * mRoundCorners; Renderer::enableRoundCornerStencil(x, y, size_x, size_y, radius); mTexture->bind(); @@ -401,6 +402,19 @@ void VideoVlcComponent::handleLooping() libvlc_state_t state = libvlc_media_player_get_state(mMediaPlayer); if (state == libvlc_Ended) { + if (mPlaylist != nullptr) + { + auto nextVideo = mPlaylist->getNextVideo(); + if (!nextVideo.empty()) + { + stopVideo(); + setVideo(nextVideo); + return; + } + else + mPlaylist = nullptr; + } + if (mVideoEnded != nullptr) { bool cont = mVideoEnded(); diff --git a/es-core/src/renderers/Renderer.h b/es-core/src/renderers/Renderer.h index 0eda871eb1..79d01f4347 100644 --- a/es-core/src/renderers/Renderer.h +++ b/es-core/src/renderers/Renderer.h @@ -109,6 +109,8 @@ namespace Renderer unsigned int mixColors(unsigned int first, unsigned int second, float percent); + void drawRoundRect(float x, float y, float w, float h, float radius, unsigned int color, const Blend::Factor _srcBlendFactor = Blend::SRC_ALPHA, const Blend::Factor _dstBlendFactor = Blend::ONE_MINUS_SRC_ALPHA); + void enableRoundCornerStencil(float x, float y, float size_x, float size_y, float radius); void disableStencil(); diff --git a/es-core/src/renderers/Renderer_GL21.cpp b/es-core/src/renderers/Renderer_GL21.cpp index 1adcbb803f..4a8b2619b7 100644 --- a/es-core/src/renderers/Renderer_GL21.cpp +++ b/es-core/src/renderers/Renderer_GL21.cpp @@ -73,6 +73,9 @@ namespace Renderer SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1); + // Antialias + SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4); + } // setupWindow void createContext() @@ -272,8 +275,13 @@ namespace Renderer #define ROUNDING_PIECES 8.0f - void drawGLRoundedCorner(float x, float y, double sa, double arc, float r) + void drawGLRoundedCorner(float x, float y, double sa, double arc, float r, unsigned int color) { + float red = (((color & 0xff000000) >> 24) & 255) / 255.0f; + float g = (((color & 0x00ff0000) >> 16) & 255) / 255.0f; + float b = (((color & 0x0000ff00) >> 8) & 255) / 255.0f; + float a = (((color & 0x000000ff)) & 255) / 255.0f; + // centre of the arc, for clockwise sense float cent_x = x + r * Math::cosf(sa + ES_PI / 2.0f); float cent_y = y + r * Math::sinf(sa + ES_PI / 2.0f); @@ -288,14 +296,36 @@ namespace Renderer float next_x = cent_x + r * Math::sinf(ang); float next_y = cent_y - r * Math::cosf(ang); - glColor3f(1.0f, 1.0f, 1.0f); + //glColor4bv((const GLbyte*) &color); + glColor4f(red, g, b, a); glVertex2f(next_x, next_y); } } + void drawRoundRect(float x, float y, float width, float height, float radius, unsigned int color, const Blend::Factor _srcBlendFactor, const Blend::Factor _dstBlendFactor) + { + bindTexture(0); + + glEnable(GL_MULTISAMPLE); + + glEnable(GL_BLEND); + glBlendFunc(convertBlendFactor(_srcBlendFactor), convertBlendFactor(_dstBlendFactor)); + + glBegin(GL_POLYGON); + drawGLRoundedCorner(x, y + radius, 3.0f * ES_PI / 2.0f, ES_PI / 2.0f, radius, color); + drawGLRoundedCorner(x + width - radius, y, 0.0, ES_PI / 2.0f, radius, color); + drawGLRoundedCorner(x + width, y + height - radius, ES_PI / 2.0f, ES_PI / 2.0f, radius, color); + drawGLRoundedCorner(x + radius, y + height, ES_PI, ES_PI / 2.0f, radius, color); + glEnd(); + + glDisable(GL_BLEND); + glDisable(GL_MULTISAMPLE); + } + void enableRoundCornerStencil(float x, float y, float width, float height, float radius) { - Renderer::bindTexture(0); + bool tx = glIsEnabled(GL_TEXTURE_2D); + glDisable(GL_TEXTURE_2D); glClear(GL_DEPTH_BUFFER_BIT); glEnable(GL_STENCIL_TEST); @@ -307,18 +337,16 @@ namespace Renderer glStencilMask(0xFF); glClear(GL_STENCIL_BUFFER_BIT); - glBegin(GL_POLYGON); - drawGLRoundedCorner(x, y + radius, 3.0f * ES_PI / 2.0f, ES_PI / 2.0f, radius); - drawGLRoundedCorner(x + width - radius, y, 0.0, ES_PI / 2.0f, radius); - drawGLRoundedCorner(x + width, y + height - radius, ES_PI / 2.0f, ES_PI / 2.0f, radius); - drawGLRoundedCorner(x + radius, y + height, ES_PI, ES_PI / 2.0f, radius); - glEnd(); + drawRoundRect(x, y, width, height, radius, 0xFFFFFFFF); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); glDepthMask(GL_TRUE); glStencilMask(0x00); glStencilFunc(GL_EQUAL, 0, 0xFF); glStencilFunc(GL_EQUAL, 1, 0xFF); + + if (tx) + glEnable(GL_TEXTURE_2D); } void disableStencil() @@ -326,9 +354,6 @@ namespace Renderer glDisable(GL_STENCIL_TEST); } - - - } // Renderer:: #endif // USE_OPENGL_21 diff --git a/es-core/src/renderers/Renderer_GLES10.cpp b/es-core/src/renderers/Renderer_GLES10.cpp index a700a77034..ce49ab7c27 100644 --- a/es-core/src/renderers/Renderer_GLES10.cpp +++ b/es-core/src/renderers/Renderer_GLES10.cpp @@ -268,32 +268,59 @@ namespace Renderer } // swapBuffers - #define N_ROUNDING_PIECES 8.0f +#define ROUNDING_PIECES 8.0f - void drawGLRoundedCorner(float x, float y, float sa, float arc, float r) + void drawGLRoundedCorner(float x, float y, double sa, double arc, float r, unsigned int color) { + float red = (((color & 0xff000000) >> 24) & 255) / 255.0f; + float g = (((color & 0x00ff0000) >> 16) & 255) / 255.0f; + float b = (((color & 0x0000ff00) >> 8) & 255) / 255.0f; + float a = (((color & 0x000000ff)) & 255) / 255.0f; + // centre of the arc, for clockwise sense - float cent_x = x + r * Math::cosf(sa + ES_PI / 2); - float cent_y = y + r * Math::sinf(sa + ES_PI / 2); + float cent_x = x + r * Math::cosf(sa + ES_PI / 2.0f); + float cent_y = y + r * Math::sinf(sa + ES_PI / 2.0f); // build up piecemeal including end of the arc - int n = ceil(N_ROUNDING_PIECES * arc / ES_PI * 2); + int n = ceil(ROUNDING_PIECES * arc / ES_PI * 2.0f); for (int i = 0; i <= n; i++) { - double ang = sa + arc * (double)i / (double)n; + float ang = sa + arc * (double)i / (double)n; // compute the next point float next_x = cent_x + r * Math::sinf(ang); float next_y = cent_y - r * Math::cosf(ang); - glColor3f(1.0f, 1.0f, 1.0f); + //glColor4bv((const GLbyte*) &color); + glColor4f(red, g, b, a); glVertex2f(next_x, next_y); } } + void drawRoundRect(float x, float y, float width, float height, float radius, unsigned int color, const Blend::Factor _srcBlendFactor, const Blend::Factor _dstBlendFactor) + { + bindTexture(0); + + // glEnable(GL_MULTISAMPLE); + + glEnable(GL_BLEND); + glBlendFunc(convertBlendFactor(_srcBlendFactor), convertBlendFactor(_dstBlendFactor)); + + glBegin(GL_POLYGON); + drawGLRoundedCorner(x, y + radius, 3.0f * ES_PI / 2.0f, ES_PI / 2.0f, radius, color); + drawGLRoundedCorner(x + width - radius, y, 0.0, ES_PI / 2.0f, radius, color); + drawGLRoundedCorner(x + width, y + height - radius, ES_PI / 2.0f, ES_PI / 2.0f, radius, color); + drawGLRoundedCorner(x + radius, y + height, ES_PI, ES_PI / 2.0f, radius, color); + glEnd(); + + glDisable(GL_BLEND); + // glDisable(GL_MULTISAMPLE); + } + void enableRoundCornerStencil(float x, float y, float width, float height, float radius) { - Renderer::bindTexture(0); + bool tx = glIsEnabled(GL_TEXTURE_2D); + glDisable(GL_TEXTURE_2D); glClear(GL_DEPTH_BUFFER_BIT); glEnable(GL_STENCIL_TEST); @@ -305,18 +332,16 @@ namespace Renderer glStencilMask(0xFF); glClear(GL_STENCIL_BUFFER_BIT); - glBegin(GL_POLYGON); - drawGLRoundedCorner(x, y + radius, 3 * ES_PI / 2, ES_PI / 2, radius); - drawGLRoundedCorner(x + size_x - radius, y, 0.0, ES_PI / 2, radius); - drawGLRoundedCorner(x + size_x, y + size_y - radius, ES_PI / 2, ES_PI / 2, radius); - drawGLRoundedCorner(x + radius, y + size_y, ES_PI, ES_PI / 2, radius); - glEnd(); + drawRoundRect(x, y, width, height, radius, 0xFFFFFFFF); glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE); glDepthMask(GL_TRUE); glStencilMask(0x00); glStencilFunc(GL_EQUAL, 0, 0xFF); glStencilFunc(GL_EQUAL, 1, 0xFF); + + if (tx) + glEnable(GL_TEXTURE_2D); } void disableStencil() From cba1f7a96f751d0d2bb195fe5cf9c6df9628397c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 15 Nov 2019 17:42:10 +0100 Subject: [PATCH 225/336] Random SystemView Playists... --- es-app/src/views/SystemView.cpp | 95 +++++++++++++++----- es-core/src/ThemeData.cpp | 2 +- es-core/src/components/ImageComponent.cpp | 55 ++++++++++++ es-core/src/components/ImageComponent.h | 18 ++++ es-core/src/components/VideoComponent.cpp | 6 +- es-core/src/components/VideoComponent.h | 10 +-- es-core/src/components/VideoVlcComponent.cpp | 2 +- 7 files changed, 155 insertions(+), 33 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 5c06c8c976..c5ce4dc6a2 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -57,37 +57,75 @@ void SystemView::clearEntries() mEntries.clear(); } -class SystemVideoPlaylist : public IVideoPlaylist +class SystemRandomPlaylist : public IPlaylist { public: - SystemVideoPlaylist(SystemData* system) + enum PlaylistType { + IMAGE, + THUMBNAIL, + MARQUEE, + VIDEO + }; + + SystemRandomPlaylist(SystemData* system, PlaylistType type) + { + mFirstRun = true; mSystem = system; + mType = type; } - - std::string getNextVideo() + + std::string getNextItem() { if (mFirstRun) { std::vector files = mSystem->getRootFolder()->getFilesRecursive(GAME, false); + for (auto file : files) - if (!file->getVideoPath().empty()) - mVideos.push_back(file->getVideoPath()); + { + switch (mType) + { + case IMAGE: + if (!file->getImagePath().empty()) + mPaths.push_back(file->getImagePath()); + break; + + case THUMBNAIL: + if (!file->getThumbnailPath().empty()) + mPaths.push_back(file->getThumbnailPath()); + break; + + case MARQUEE: + if (!file->getMarqueePath().empty()) + mPaths.push_back(file->getMarqueePath()); + break; + + case VIDEO: + if (!file->getVideoPath().empty()) + mPaths.push_back(file->getVideoPath()); + break; + } + } + + mFirstRun = false; } - int video = (int) ((float) rand() * mVideos.size()) / float(RAND_MAX); - - // (int)(((float)rand() / float(RAND_MAX)) * (float)mVideos.size()); - if (video >= 0 && video < mVideos.size()) - return mVideos[video]; + int idx = (int) ((float) rand() * mPaths.size()) / float(RAND_MAX); + + if (idx >= 0 && idx < mPaths.size()) + return mPaths[idx]; return ""; } + + private: - SystemData* mSystem; - bool mFirstRun; - std::vector mVideos; + SystemData* mSystem; + bool mFirstRun; + PlaylistType mType; + + std::vector mPaths; }; void SystemView::populate() @@ -195,12 +233,29 @@ void SystemView::populate() for (auto bx : e.data.backgroundExtras) { - if (bx->getValue() != "VideoComponent") - continue; - - auto elem = (*it)->getTheme()->getElement("system", bx->getTag(), "video"); - if (elem != nullptr && elem->has("path") && elem->get("path") == "*") - ((VideoComponent*)bx)->setPlaylist(std::make_shared(*it)); + if (bx->getValue() == "VideoComponent") + { + auto elem = (*it)->getTheme()->getElement("system", bx->getTag(), "video"); + if (elem != nullptr && elem->has("path") && elem->get("path") == "*") + ((VideoComponent*)bx)->setPlaylist(std::make_shared(*it, SystemRandomPlaylist::VIDEO)); + } + else if (bx->getValue() == "ImageComponent") + { + auto elem = (*it)->getTheme()->getElement("system", bx->getTag(), "image"); + if (elem != nullptr && elem->has("path") && Utils::String::startsWith(elem->get("path"), "*")) + { + std::string src = elem->get("path"); + + SystemRandomPlaylist::PlaylistType type = SystemRandomPlaylist::IMAGE; + + if (src == "*.thumbnail") + type = SystemRandomPlaylist::THUMBNAIL; + else if (src == "*.marquee") + type = SystemRandomPlaylist::MARQUEE; + + ((ImageComponent*)bx)->setPlaylist(std::make_shared(*it, type)); + } + } } // sort the extras by z-index diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index bc8796c538..644f911ec4 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -986,7 +986,7 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map ImageComponent::getHelpPrompts() ret.push_back(HelpPrompt("a", _("SELECT"))); return ret; } + + + +void ImageComponent::setPlaylist(std::shared_ptr playList) +{ + mPlaylist = playList; + if (mPlaylist == nullptr) + return; + + auto image = mPlaylist->getNextItem(); + if (!image.empty()) + setImage(image); +} + +void ImageComponent::onShow() +{ + GuiComponent::onShow(); + + if (!mShowing && mPlaylist != nullptr && !mPath.empty()) + { + auto item = mPlaylist->getNextItem(); + if (!item.empty()) + setImage(item, false, getMaxSizeInfo()); + } + + mShowing = true; +} + +void ImageComponent::onHide() +{ + GuiComponent::onHide(); + mShowing = false; +} + + +void ImageComponent::update(int deltaTime) +{ + GuiComponent::update(deltaTime); + + if (mPlaylist != nullptr && mShowing) + { + mPlaylistTimer += deltaTime; + + if (mPlaylistTimer >= 10000) + { + auto item = mPlaylist->getNextItem(); + if (!item.empty()) + setImage(item, false, getMaxSizeInfo()); + + mPlaylistTimer = 0.0; + } + } +} diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index f3bca98cbc..a759a63067 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -8,12 +8,20 @@ #include "resources/TextureResource.h" #include "resources/Font.h" +class IPlaylist +{ +public: + virtual std::string getNextItem() = 0; +}; + class ImageComponent : public GuiComponent { public: ImageComponent(Window* window, bool forceLoad = false, bool dynamic = true); virtual ~ImageComponent(); + std::string getValue() const override { return "ImageComponent"; } + void setDefaultImage(std::string path); //Loads the image at the given filepath. Will tile if tile is true (retrieves texture as tiling, creates vertices accordingly). @@ -107,6 +115,12 @@ class ImageComponent : public GuiComponent float getRoundCorners() { return mRoundCorners; } void setRoundCorners(float value) { mRoundCorners = value; } + virtual void onShow() override; + virtual void onHide() override; + virtual void update(int deltaTime); + + void setPlaylist(std::shared_ptr playList); + private: Vector2f mTargetSize; @@ -153,6 +167,10 @@ class ImageComponent : public GuiComponent Vector4f mPadding; Alignment mHorizontalAlignment; Alignment mVerticalAlignment; + + bool mShowing; + std::shared_ptr mPlaylist; + float mPlaylistTimer; }; #endif // ES_CORE_COMPONENTS_IMAGE_COMPONENT_H diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index d5dabd5241..eb88f6e398 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -450,7 +450,7 @@ void VideoComponent::onShow() { if (!mShowing && mPlaylist != nullptr && !mVideoPath.empty()) { - auto video = mPlaylist->getNextVideo(); + auto video = mPlaylist->getNextItem(); if (!video.empty()) mVideoPath = video; } @@ -484,13 +484,13 @@ void VideoComponent::topWindow(bool isTop) } -void VideoComponent::setPlaylist(std::shared_ptr playList) +void VideoComponent::setPlaylist(std::shared_ptr playList) { mPlaylist = playList; if (mPlaylist == nullptr) return; - auto video = mPlaylist->getNextVideo(); + auto video = mPlaylist->getNextItem(); if (!video.empty()) setVideo(video); } \ No newline at end of file diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index aa6cbf05e9..c58ad3a118 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -21,12 +21,6 @@ enum ImageSource }; */ -class IVideoPlaylist -{ -public: - virtual std::string getNextVideo() = 0; -}; - class VideoComponent : public GuiComponent { // Structure that groups together the configuration of the video component @@ -134,10 +128,10 @@ class VideoComponent : public GuiComponent return mVideoPath; } - void setPlaylist(std::shared_ptr playList); + void setPlaylist(std::shared_ptr playList); protected: - std::shared_ptr mPlaylist; + std::shared_ptr mPlaylist; std::function mVideoEnded; private: diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index d90ba7b14d..216e02569d 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -404,7 +404,7 @@ void VideoVlcComponent::handleLooping() { if (mPlaylist != nullptr) { - auto nextVideo = mPlaylist->getNextVideo(); + auto nextVideo = mPlaylist->getNextItem(); if (!nextVideo.empty()) { stopVideo(); From e8122a8e4838cabbd3e097d948e089c70b7b44cd Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 17 Nov 2019 01:02:08 +0100 Subject: [PATCH 226/336] GridView Fixes --- es-core/src/components/GridTileComponent.cpp | 24 ++++++++++++-------- es-core/src/renderers/Renderer_GL21.cpp | 4 ++-- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 6d73e0a10e..eaf98c0045 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -165,16 +165,20 @@ void GridTileComponent::resize() if (currentProperties.Label.Visible) { currentProperties.Label.updateTextComponent(&mLabel, mSize); - - if (currentProperties.Label.pos.x() < 0 && mLabelMerged) - { - mLabel.setPosition(0, mSize.y() - labelHeight); - mLabel.setSize(size.x(), labelHeight); - } - else if (mImage != nullptr && !mLabelMerged) - { - mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, mImage->getSize().y()); - mLabel.setSize(mImage->getSize().x(), labelHeight); + + // Automatic layout for not merged labels + if (currentProperties.Label.pos.x() < 0 && !mLabelMerged) + { + if (currentProperties.Padding.x() == 0) + { + mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, mImage->getSize().y()); + mLabel.setSize(mImage->getSize().x(), labelHeight); + } + else + { + mLabel.setPosition(0, mSize.y() - labelHeight); + mLabel.setSize(size.x(), labelHeight); + } } } else if (mIsDefaultImage) diff --git a/es-core/src/renderers/Renderer_GL21.cpp b/es-core/src/renderers/Renderer_GL21.cpp index 4a8b2619b7..e085134af3 100644 --- a/es-core/src/renderers/Renderer_GL21.cpp +++ b/es-core/src/renderers/Renderer_GL21.cpp @@ -73,8 +73,8 @@ namespace Renderer SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 2); SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 1); - // Antialias - SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 4); + // Antialias : Not supported on every machine + // SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, 2); } // setupWindow From 16a0e537e002786836a426d801022c654108aeb9 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 17 Nov 2019 01:02:20 +0100 Subject: [PATCH 227/336] Small optim --- es-app/src/CollectionSystemManager.cpp | 2 -- es-app/src/SystemData.cpp | 4 ++-- es-app/src/SystemData.h | 7 ++++--- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 6b67dd9c2e..2784042250 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -43,8 +43,6 @@ CollectionSystemManager::CollectionSystemManager(Window* window) : mWindow(windo // creating standard environment data mCollectionEnvData = new SystemEnvironmentData; mCollectionEnvData->mStartPath = ""; - std::vector exts; - mCollectionEnvData->mSearchExtensions = exts; mCollectionEnvData->mLaunchCommand = ""; std::vector allPlatformIds; allPlatformIds.push_back(PlatformIds::PLATFORM_IGNORE); diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 886e1e9ba8..ead029265f 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -282,13 +282,13 @@ SystemData* SystemData::loadSystem(pugi::xml_node system) // convert extensions list from a string into a vector of strings std::vector list = readList(system.child("extension").text().get()); - std::vector extensions; + std::unordered_set extensions; for (auto extension = list.cbegin(); extension != list.cend(); extension++) { std::string xt = Utils::String::toLower(*extension); if (std::find(extensions.begin(), extensions.end(), xt) == extensions.end()) - extensions.push_back(xt); + extensions.insert(xt); } cmd = system.child("command").text().get(); diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 0d1afee124..2877a0cdb9 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -11,6 +11,7 @@ #include #include "math/Vector2f.h" #include +#include #include "FileFilterIndex.h" #include "Settings.h" @@ -32,14 +33,14 @@ struct SystemEnvironmentData std::string mSystemName; std::string mStartPath; - std::vector mSearchExtensions; + std::unordered_set mSearchExtensions; std::string mLaunchCommand; std::vector mPlatformIds; std::vector mEmulators; bool isValidExtension(const std::string extension) { - return std::find(mSearchExtensions.cbegin(), mSearchExtensions.cend(), extension) != mSearchExtensions.cend(); + return mSearchExtensions.find(extension) != mSearchExtensions.cend(); } std::vector getCores(std::string emulatorName) @@ -107,7 +108,7 @@ class SystemData inline const std::string& getName() const { return mName; } inline const std::string& getFullName() const { return mFullName; } inline const std::string& getStartPath() const { return mEnvData->mStartPath; } - inline const std::vector& getExtensions() const { return mEnvData->mSearchExtensions; } + //inline const std::vector& getExtensions() const { return mEnvData->mSearchExtensions; } inline const std::string& getThemeFolder() const { return mThemeFolder; } inline SystemEnvironmentData* getSystemEnvData() const { return mEnvData; } inline const std::vector& getPlatformIds() const { return mEnvData->mPlatformIds; } From 4a2a9deccc168e41625fffd445d078794cc70347 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 18 Nov 2019 15:16:27 +0100 Subject: [PATCH 228/336] Random Playlists : use {random} namings --- es-app/src/views/SystemView.cpp | 8 ++++---- es-core/src/ThemeData.cpp | 19 +++++++++++++++++-- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index c5ce4dc6a2..414948b0ca 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -236,21 +236,21 @@ void SystemView::populate() if (bx->getValue() == "VideoComponent") { auto elem = (*it)->getTheme()->getElement("system", bx->getTag(), "video"); - if (elem != nullptr && elem->has("path") && elem->get("path") == "*") + if (elem != nullptr && elem->has("path") && Utils::String::startsWith(elem->get("path"), "{random")) ((VideoComponent*)bx)->setPlaylist(std::make_shared(*it, SystemRandomPlaylist::VIDEO)); } else if (bx->getValue() == "ImageComponent") { auto elem = (*it)->getTheme()->getElement("system", bx->getTag(), "image"); - if (elem != nullptr && elem->has("path") && Utils::String::startsWith(elem->get("path"), "*")) + if (elem != nullptr && elem->has("path") && Utils::String::startsWith(elem->get("path"), "{random")) { std::string src = elem->get("path"); SystemRandomPlaylist::PlaylistType type = SystemRandomPlaylist::IMAGE; - if (src == "*.thumbnail") + if (src == "{random:thumbnail}") type = SystemRandomPlaylist::THUMBNAIL; - else if (src == "*.marquee") + else if (src == "{random:marquee}") type = SystemRandomPlaylist::MARQUEE; ((ImageComponent*)bx)->setPlaylist(std::make_shared(*it, type)); diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 644f911ec4..4523d4320c 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -986,9 +986,24 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map Date: Mon, 18 Nov 2019 15:17:43 +0100 Subject: [PATCH 229/336] Avoid loosing gamelist datas using recovery system. --- es-app/src/CollectionSystemManager.cpp | 3 + es-app/src/FileData.cpp | 3 + es-app/src/Gamelist.cpp | 133 +++++++++++++++++++++--- es-app/src/Gamelist.h | 3 + es-app/src/MetaData.h | 1 + es-app/src/SystemData.cpp | 21 ++++ es-app/src/SystemData.h | 5 + es-app/src/guis/GuiMetaDataEd.cpp | 5 + es-app/src/guis/GuiScraperMulti.cpp | 3 +- es-app/src/main.cpp | 5 +- es-app/src/scrapers/ThreadedScraper.cpp | 3 + 11 files changed, 169 insertions(+), 16 deletions(-) diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 2784042250..314cb30833 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -13,6 +13,7 @@ #include "ThemeData.h" #include #include +#include "Gamelist.h" std::string myCollectionsName = "collections"; @@ -549,6 +550,8 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) } } + saveToGamelistRecovery(file); + char trstring[512]; if (adding) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 113ec9cc40..29ffe72b95 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -16,6 +16,7 @@ #include "Window.h" #include "views/UIModeController.h" #include +#include "Gamelist.h" FileData::FileData(FileType type, const std::string& path, SystemData* system) : mType(type), mSystem(system), mParent(NULL), mMetadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor! @@ -296,6 +297,8 @@ void FileData::launchGame(Window* window) //update last played time gameToUpdate->getMetadata().set("lastplayed", Utils::Time::DateTime(Utils::Time::now())); CollectionSystemManager::get()->refreshCollectionSystems(gameToUpdate); + + saveToGamelistRecovery(gameToUpdate); } // music diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 5ae1737853..484b3dfce7 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -11,6 +11,9 @@ #ifdef WIN32 #include +#include +#else +#include #endif FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType type, std::unordered_map& fileMap) @@ -101,12 +104,8 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType return NULL; } -void parseGamelist(SystemData* system, std::unordered_map& fileMap) -{ - std::string xmlpath = system->getGamelistPath(false); - if (!Utils::FileSystem::exists(xmlpath)) - return; - +void loadGamelistFile (const std::string xmlpath, SystemData* system, std::unordered_map& fileMap, size_t checkSize = SIZE_MAX) +{ bool trustGamelist = Settings::getInstance()->getBool("ParseGamelistOnly"); LOG(LogInfo) << "Parsing XML file \"" << xmlpath << "\"..."; @@ -114,18 +113,28 @@ void parseGamelist(SystemData* system, std::unordered_map node in gamelist \"" << xmlpath << "\"!"; return; } + + if (checkSize != SIZE_MAX) + { + auto parentSize = root.attribute("parentHash").as_uint(); + if (parentSize != checkSize) + { + LOG(LogWarning) << "gamelist size don't match !"; + return; + } + } std::string relativeTo = system->getStartPath(); @@ -139,22 +148,23 @@ void parseGamelist(SystemData* system, std::unordered_mapisArcadeAsset()) + else if (!file->isArcadeAsset()) { std::string defaultName = file->getMetadata().get("name"); file->setMetadata(MetaDataList::createFromXML(type == FOLDER ? FOLDER_METADATA : GAME_METADATA, fileNode, system)); @@ -166,11 +176,56 @@ void parseGamelist(SystemData* system, std::unordered_mapgetMetadata().set("hidden", "true"); - file->getMetadata().resetChangedFlag(); + if (checkSize != SIZE_MAX) + file->getMetadata().setDirty(); + else + file->getMetadata().resetChangedFlag(); } } } +std::string getTemporaryGamelistRecovery(SystemData* system) +{ + return Utils::FileSystem::getHomePath() + "/.emulationstation/recovery/" + system->getName(); +} + +void clearTemporaryGamelistRecovery(SystemData* system) +{ + auto path = getTemporaryGamelistRecovery(system); + + auto files = Utils::FileSystem::getDirContent(path, true, false); + if (files.size() > 0) + { + for (auto file : files) + if (!Utils::FileSystem::isDirectory(file)) + Utils::FileSystem::removeFile(file); + + std::reverse(std::begin(files), std::end(files)); + + for (auto file : files) + if (Utils::FileSystem::isDirectory(file)) + rmdir(file.c_str()); + } + + rmdir(path.c_str()); +} + +void parseGamelist(SystemData* system, std::unordered_map& fileMap) +{ + std::string xmlpath = system->getGamelistPath(false); + + auto size = Utils::FileSystem::getFileSize(xmlpath); + if (size != 0) + loadGamelistFile(xmlpath, system, fileMap); + + auto files = Utils::FileSystem::getDirContent(getTemporaryGamelistRecovery(system), true, false); + for (auto file : files) + loadGamelistFile(file, system, fileMap, size); + + if (size != SIZE_MAX) + system->setGamelistHash(size); +} + bool addFileDataNode(pugi::xml_node& parent, const FileData* file, const char* tag, SystemData* system) { //create game and add to parent node @@ -195,6 +250,55 @@ bool addFileDataNode(pugi::xml_node& parent, const FileData* file, const char* t return true; } +bool saveToGamelistRecovery(FileData* file) +{ + pugi::xml_document doc; + pugi::xml_node root = doc.append_child("gameList"); + + const char* tag = file->getType() == GAME ? "game" : "folder"; + + SystemData* system = file->getSourceFileData()->getSystem(); + root.append_attribute("parentHash").set_value(system->getGamelistHash()); + + if (addFileDataNode(root, file, tag, system)) + { + std::string fp = file->getFullPath(); + fp = Utils::FileSystem::createRelativePath(file->getFullPath(), system->getRootFolder()->getFullPath(), true); + fp = Utils::FileSystem::getParent(fp) + "/" + Utils::FileSystem::getStem(fp) + ".xml"; + + std::string path = Utils::FileSystem::getAbsolutePath(fp, getTemporaryGamelistRecovery(system)); + path = Utils::FileSystem::getCanonicalPath(path); + + std::string folder = Utils::FileSystem::getParent(path); + + if (!Utils::FileSystem::exists(folder)) + Utils::FileSystem::createDirectory(folder); + + if (!doc.save_file(path.c_str())) + { + LOG(LogError) << "Error saving gamelist.xml to \"" << path << "\" (for system " << system->getName() << ")!"; + return false; + } + + return true; + } + + return false; +} + +bool hasDirtyFile(SystemData* system) +{ + FolderData* rootFolder = system->getRootFolder(); + if (rootFolder == nullptr) + return false; + + for (auto file : rootFolder->getFilesRecursive(GAME | FOLDER)) + if (file->getMetadata().wasChanged()) + return true; + + return false; +} + void updateGamelist(SystemData* system) { //We do this by reading the XML again, adding changes and then writing it back, @@ -327,6 +431,7 @@ void updateGamelist(SystemData* system) if (std::rename(tmpFile.c_str(), xmlWritePath.c_str()) != 0) LOG(LogError) << "Unable to rename \"" << tmpFile << "to " << xmlWritePath << "\"!"; + clearTemporaryGamelistRecovery(system); } else Utils::FileSystem::removeFile(tmpFile); diff --git a/es-app/src/Gamelist.h b/es-app/src/Gamelist.h index efc402b792..df7021ea68 100644 --- a/es-app/src/Gamelist.h +++ b/es-app/src/Gamelist.h @@ -13,4 +13,7 @@ void parseGamelist(SystemData* system, std::unordered_map& getMDD() const { return getMDDByType(getType()); } diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index ead029265f..4518a75481 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -24,6 +24,7 @@ std::vector SystemData::sSystemVector; SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) { + mGameListHash = 0; mGameCount = -1; mSortId = Settings::getInstance()->getInt(getName() + ".sort"), @@ -543,6 +544,26 @@ void SystemData::writeExampleConfig(const std::string& path) LOG(LogError) << "Example config written! Go read it at \"" << path << "\"!"; } +bool SystemData::hasDirtySystems() +{ + bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); + if (!saveOnExit) + return false; + + for (unsigned int i = 0; i < sSystemVector.size(); i++) + { + SystemData* pData = sSystemVector.at(i); + if (pData->mIsCollectionSystem) + continue; + + + if (hasDirtyFile(pData)) + return true; + } + + return false; +} + void SystemData::deleteSystems() { bool saveOnExit = !Settings::getInstance()->getBool("IgnoreGamelist") && Settings::getInstance()->getBool("SaveGamelistsOnExit"); diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 2877a0cdb9..f32300ff98 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -130,6 +130,7 @@ class SystemData int getDisplayedGameCount(); void updateDisplayedGameCount(); + static bool hasDirtySystems(); static void deleteSystems(); static bool loadConfig(Window* window); //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); @@ -179,9 +180,13 @@ class SystemData unsigned int getSortId() const { return mSortId; }; void setSortId(const unsigned int sortId = 0); + void setGamelistHash(size_t size) { mGameListHash = size; } + size_t getGamelistHash() { return mGameListHash; } + private: static SystemData* loadSystem(pugi::xml_node system); + size_t mGameListHash; bool mIsCollectionSystem; bool mIsGameSystem; std::string mName; diff --git a/es-app/src/guis/GuiMetaDataEd.cpp b/es-app/src/guis/GuiMetaDataEd.cpp index 118a1a88b0..394e34ee80 100644 --- a/es-app/src/guis/GuiMetaDataEd.cpp +++ b/es-app/src/guis/GuiMetaDataEd.cpp @@ -296,6 +296,9 @@ void GuiMetaDataEd::onSizeChanged() mHeaderGrid->setRowHeightPerc(3, subtitleHeight / mHeaderGrid->getSize().y()); } + +#include "Gamelist.h" + void GuiMetaDataEd::save() { // remove game from index @@ -323,6 +326,8 @@ void GuiMetaDataEd::save() if (mSavedCallback) mSavedCallback(); + saveToGamelistRecovery(mScraperParams.game); + // update respective Collection Entries CollectionSystemManager::get()->refreshCollectionSystems(mScraperParams.game); } diff --git a/es-app/src/guis/GuiScraperMulti.cpp b/es-app/src/guis/GuiScraperMulti.cpp index 1054b799a7..7150d48cba 100644 --- a/es-app/src/guis/GuiScraperMulti.cpp +++ b/es-app/src/guis/GuiScraperMulti.cpp @@ -120,7 +120,8 @@ void GuiScraperMulti::acceptResult(const ScraperSearchResult& result) ScraperSearchParams& search = mSearchQueue.front(); search.game->getMetadata().importScrappedMetadata(result.mdl); - updateGamelist(search.system); + saveToGamelistRecovery(search.game); + // updateGamelist(search.system); mSearchQueue.pop(); mCurrentGame++; diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index f6c0e2e372..f37eb20544 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -678,7 +678,8 @@ int main(int argc, char* argv[]) while(window.peekGui() != ViewController::get()) delete window.peekGui(); - window.deinit(true); + if (SystemData::hasDirtySystems()) + window.renderLoadingScreen(_("SAVING DATA. PLEASE WAIT...")); MameNames::deinit(); CollectionSystemManager::deinit(); @@ -689,6 +690,8 @@ int main(int argc, char* argv[]) FreeImage_DeInitialise(); #endif + window.deinit(true); + processQuitMode(); LOG(LogInfo) << "EmulationStation cleanly shutting down."; diff --git a/es-app/src/scrapers/ThreadedScraper.cpp b/es-app/src/scrapers/ThreadedScraper.cpp index b745e59ca0..625142e599 100644 --- a/es-app/src/scrapers/ThreadedScraper.cpp +++ b/es-app/src/scrapers/ThreadedScraper.cpp @@ -4,6 +4,7 @@ #include "components/AsyncNotificationComponent.h" #include "EsLocale.h" #include "guis/GuiMsgBox.h" +#include "Gamelist.h" #define GUIICON _U("\uF03E ") @@ -162,12 +163,14 @@ void ThreadedScraper::processMedias(ScraperSearchResult result) mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch); search.game->getMetadata().importScrappedMetadata(result.mdl); + saveToGamelistRecovery(search.game); } void ThreadedScraper::acceptResult(const ScraperSearchResult& result) { ScraperSearchParams& search = mSearchQueue.front(); search.game->getMetadata().importScrappedMetadata(result.mdl);// = result.mdl; + saveToGamelistRecovery(search.game); } void ThreadedScraper::start(Window* window, const std::queue& searches) From efac2912f2c439feccff63a8eef1dae1a3746abf Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 18 Nov 2019 17:45:18 +0100 Subject: [PATCH 230/336] VideoVlcComponent : Fix color / Opacity --- es-core/src/ThemeData.cpp | 1 + es-core/src/components/VideoComponent.cpp | 1 + es-core/src/components/VideoVlcComponent.cpp | 18 ++++++++++++++++-- es-core/src/components/VideoVlcComponent.h | 3 +++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 4523d4320c..4a9578e8ba 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -206,6 +206,7 @@ std::map> The { "effect", STRING }, { "visible", BOOLEAN }, { "roundCorners", FLOAT }, + { "color", COLOR }, { "zIndex", FLOAT }, { "snapshotSource", STRING }, // image, thumbnail, marquee { "showSnapshotNoVideo", BOOLEAN }, diff --git a/es-core/src/components/VideoComponent.cpp b/es-core/src/components/VideoComponent.cpp index eb88f6e398..42b3f850af 100644 --- a/es-core/src/components/VideoComponent.cpp +++ b/es-core/src/components/VideoComponent.cpp @@ -67,6 +67,7 @@ VideoComponent::VideoComponent(Window* window) : mTargetIsMin(false), mTargetSize(0, 0) { + mVideoEnded = nullptr; mRoundCorners = 0.0f; mFadeIn = 0.0f; mIsWaitingForVideoToStart = false; diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 216e02569d..64de265376 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -62,6 +62,7 @@ VideoVlcComponent::VideoVlcComponent(Window* window, std::string subtitles) : mMedia(nullptr) { mElapsed = 0; + mColorShift = 0xFFFFFFFF; // Get an empty texture for rendering the video mTexture = nullptr;// TextureResource::get(""); @@ -185,6 +186,11 @@ void VideoVlcComponent::resize() onSizeChanged(); } +void VideoVlcComponent::setColorShift(unsigned int color) +{ + mColorShift = color; +} + void VideoVlcComponent::render(const Transform4x4f& parentTrans) { if (!isVisible()) @@ -255,9 +261,11 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) if (mTexture == nullptr) return; - + const unsigned int fadeIn = t * 255.0f; - const unsigned int color = Renderer::convertColor(0xFFFFFF00 | fadeIn); + float opacity = mOpacity * fadeIn; + const unsigned int color = Renderer::convertColor(mColorShift & 0xFFFFFF00 | (unsigned char)((mColorShift & 0xFF) * opacity)); + Renderer::Vertex vertices[4]; if (mEffect == VideoVlcFlags::VideoVlcEffect::BUMP && mFadeIn > 0.0 && mFadeIn < 1.0 && mConfig.startDelay > 0) @@ -567,6 +575,12 @@ void VideoVlcComponent::applyTheme(const std::shared_ptr& theme, cons if (elem && elem->has("roundCorners")) mRoundCorners = elem->get("roundCorners"); + + if (properties & COLOR) + { + if (elem && elem->has("color")) + setColorShift(elem->get("color")); + } } void VideoVlcComponent::update(int deltaTime) diff --git a/es-core/src/components/VideoVlcComponent.h b/es-core/src/components/VideoVlcComponent.h index eab753e970..35df164031 100644 --- a/es-core/src/components/VideoVlcComponent.h +++ b/es-core/src/components/VideoVlcComponent.h @@ -75,6 +75,8 @@ class VideoVlcComponent : public VideoComponent virtual void applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties); virtual void update(int deltaTime); + void setColorShift(unsigned int color); + private: // Calculates the correct mSize from our resizing information (set by setResize/setMaxSize). // Used internally whenever the resizing parameters or texture change. @@ -105,6 +107,7 @@ class VideoVlcComponent : public VideoComponent VideoVlcFlags::VideoVlcEffect mEffect; + unsigned int mColorShift; int mElapsed; }; From 8abbbcd2a97abf4f4f8c7b66de031124ed2a9095 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 19 Nov 2019 19:16:05 +0100 Subject: [PATCH 231/336] md_thumbnail support + support for gridview videoplaying state --- .../src/views/gamelist/BasicGameListView.cpp | 30 +++---- .../views/gamelist/DetailedGameListView.cpp | 47 ++++++++++- .../src/views/gamelist/DetailedGameListView.h | 2 + .../src/views/gamelist/GridGameListView.cpp | 20 +++-- .../src/views/gamelist/VideoGameListView.cpp | 65 +++++++++++++++- es-app/src/views/gamelist/VideoGameListView.h | 2 + es-core/src/components/GridTileComponent.cpp | 78 ++++++++++++++----- es-core/src/components/GridTileComponent.h | 3 +- es-core/src/components/VideoComponent.h | 8 ++ es-core/src/components/VideoVlcComponent.cpp | 49 ++++++++++-- es-core/src/components/VideoVlcComponent.h | 4 +- 11 files changed, 248 insertions(+), 60 deletions(-) diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 990a340819..90066788c5 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -81,38 +81,38 @@ void BasicGameListView::populateList(const std::vector& files) if (favoritesFirst) { - for (auto it = files.cbegin(); it != files.cend(); it++) + for (auto file : files) { - if (!(*it)->getFavorite()) + if (!file->getFavorite()) continue; - + if (showFavoriteIcon) - mList.add(_U("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); - else if (((*it)->getType() == FOLDER)) - mList.add(_U("\uF114 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + mList.add(_U("\uF006 ") + file->getName(), file, file->getType() == FOLDER); + else if (file->getType() == FOLDER) + mList.add(_U("\uF114 ") + file->getName(), file, true); else - mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + mList.add(file->getName(), file, false); } } - for (auto it = files.cbegin(); it != files.cend(); it++) + for (auto file : files) { - if ((*it)->getFavorite()) + if (file->getFavorite()) { if (favoritesFirst) continue; if (showFavoriteIcon) { - mList.add(_U("\uF006 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + mList.add(_U("\uF006 ") + file->getName(), file, file->getType() == FOLDER); continue; } } - if (((*it)->getType() == FOLDER)) - mList.add(_U("\uF114 ") + (*it)->getName(), *it, ((*it)->getType() == FOLDER)); + if (file->getType() == FOLDER) + mList.add(_U("\uF114 ") + file->getName(), file, true); else - mList.add((*it)->getName(), *it, ((*it)->getType() == FOLDER)); + mList.add(file->getName(), file, false); } } else @@ -169,8 +169,8 @@ void BasicGameListView::setCursor(FileData* cursor) void BasicGameListView::addPlaceholder() { // empty list - add a placeholder - FileData* placeholder = new FileData(PLACEHOLDER, "", this->mRoot->getSystem()); - mList.add(placeholder->getName(), placeholder, (placeholder->getType() == PLACEHOLDER)); + FileData* placeholder = new FileData(PLACEHOLDER, "<" + _("No Entries Found") + ">", mRoot->getSystem()); + mList.add(placeholder->getName(), placeholder, true); } std::string BasicGameListView::getQuickSystemSelectRightButton() diff --git a/es-app/src/views/gamelist/DetailedGameListView.cpp b/es-app/src/views/gamelist/DetailedGameListView.cpp index 35b7aeb178..d9053f458d 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.cpp +++ b/es-app/src/views/gamelist/DetailedGameListView.cpp @@ -15,7 +15,7 @@ DetailedGameListView::DetailedGameListView(Window* window, FolderData* root) : BasicGameListView(window, root), mDescContainer(window), mDescription(window), - mImage(nullptr), mMarquee(nullptr), mVideo(nullptr), + mImage(nullptr), mMarquee(nullptr), mVideo(nullptr), mThumbnail(nullptr), mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -83,6 +83,9 @@ DetailedGameListView::DetailedGameListView(Window* window, FolderData* root) : DetailedGameListView::~DetailedGameListView() { + if (mThumbnail != nullptr) + delete mThumbnail; + if (mImage != nullptr) delete mImage; @@ -109,6 +112,23 @@ void DetailedGameListView::createImage() addChild(mImage); } +void DetailedGameListView::createThumbnail() +{ + if (mThumbnail != nullptr) + return; + + const float padding = 0.01f; + + // Image + mThumbnail = new ImageComponent(mWindow); + mThumbnail->setAllowFading(false); + mThumbnail->setOrigin(0.5f, 0.5f); + mThumbnail->setPosition(mSize.x() * 0.25f, mList.getPosition().y() + mSize.y() * 0.2125f); + mThumbnail->setMaxSize(mSize.x() * (0.50f - 2 * padding), mSize.y() * 0.4f); + mThumbnail->setDefaultZIndex(30); + addChild(mThumbnail); +} + void DetailedGameListView::createVideo() { if (mVideo != nullptr) @@ -182,6 +202,18 @@ void DetailedGameListView::onThemeChanged(const std::shared_ptr& them mImage = nullptr; } + if (theme->getElement(getName(), "md_thumbnail", "image")) + { + createThumbnail(); + mThumbnail->applyTheme(theme, getName(), "md_thumbnail", ALL ^ (PATH)); + } + else if (mThumbnail != nullptr) + { + removeChild(mThumbnail); + delete mThumbnail; + mThumbnail = nullptr; + } + if (theme->getElement(getName(), "md_marquee", "image")) { createMarquee(); @@ -338,9 +370,12 @@ void DetailedGameListView::updateInfoPanel() mVideo->setImage(snapShot); } + + if (mThumbnail != nullptr) + mThumbnail->setImage(file->getThumbnailPath()); if (mImage != nullptr) - mImage->setImage(imagePath, false, mImage->getMaxSizeInfo()); + mImage->setImage(imagePath); if (mMarquee != nullptr) mMarquee->setImage(file->getMarqueePath(), false, mMarquee->getMaxSizeInfo()); @@ -377,6 +412,9 @@ void DetailedGameListView::updateInfoPanel() if (mImage != nullptr) comps.push_back(mImage); + if (mThumbnail != nullptr) + comps.push_back(mThumbnail); + if (mMarquee != nullptr) comps.push_back(mMarquee); @@ -407,6 +445,7 @@ void DetailedGameListView::updateInfoPanel() { if (mVideo != nullptr) mVideo->setImage(""); if (mImage != nullptr) mImage->setImage(""); + if (mThumbnail != nullptr) mThumbnail->setImage(""); if (mMarquee != nullptr) mMarquee->setImage(""); } }, fadingOut); @@ -422,7 +461,9 @@ void DetailedGameListView::launch(FileData* game) target = Vector3f(mVideo->getCenter().x(), mVideo->getCenter().y(), 0); else if (mImage != nullptr && mImage->hasImage()) target = Vector3f(mImage->getCenter().x(), mImage->getCenter().y(), 0); - + else if (mThumbnail != nullptr && mThumbnail->hasImage()) + target = Vector3f(mThumbnail->getCenter().x(), mThumbnail->getCenter().y(), 0); + ViewController::get()->launch(game, target); } diff --git a/es-app/src/views/gamelist/DetailedGameListView.h b/es-app/src/views/gamelist/DetailedGameListView.h index 1b1cdb6fb5..dbda8b39c7 100644 --- a/es-app/src/views/gamelist/DetailedGameListView.h +++ b/es-app/src/views/gamelist/DetailedGameListView.h @@ -34,12 +34,14 @@ class DetailedGameListView : public BasicGameListView void createVideo(); void createMarquee(); void createImage(); + void createThumbnail(); void initMDLabels(); void initMDValues(); std::string getMetadata(FileData* file, std::string name); ImageComponent* mImage; + ImageComponent* mThumbnail; ImageComponent* mMarquee; VideoComponent* mVideo; diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 13012f345a..6c83fcd751 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -252,33 +252,31 @@ void GridGameListView::populateList(const std::vector& files) if (favoritesFirst) { - for (auto it = files.cbegin(); it != files.cend(); it++) + for (auto file : files) { - if (!(*it)->getFavorite()) - continue; - - mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), showFavoriteIcon, *it); + if (file->getFavorite() && showFavoriteIcon) + mGrid.add(file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), true, file); } } - for (auto it = files.cbegin(); it != files.cend(); it++) + for (auto file : files) { - if ((*it)->getFavorite()) + if (file->getFavorite()) { if (favoritesFirst) continue; if (showFavoriteIcon) { - mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), true, *it); + mGrid.add(_U("\uF006 ") + file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), true, file); continue; } } - if (((*it)->getType() == FOLDER) && Utils::FileSystem::exists(getImagePath(*it))) - mGrid.add(_U("\uF114 ") + (*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), (*it)->getFavorite(), *it); + if (file->getType() == FOLDER && Utils::FileSystem::exists(getImagePath(file))) + mGrid.add(_U("\uF114 ") + file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), file->getFavorite(), file); else - mGrid.add((*it)->getName(), getImagePath(*it), (*it)->getVideoPath(), (*it)->getMarqueePath(), (*it)->getFavorite(), *it); + mGrid.add(file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), file->getFavorite(), file); } } else diff --git a/es-app/src/views/gamelist/VideoGameListView.cpp b/es-app/src/views/gamelist/VideoGameListView.cpp index 98790932bf..170af90f69 100644 --- a/es-app/src/views/gamelist/VideoGameListView.cpp +++ b/es-app/src/views/gamelist/VideoGameListView.cpp @@ -18,6 +18,7 @@ VideoGameListView::VideoGameListView(Window* window, FolderData* root) : mImage(nullptr), mVideo(nullptr), mVideoPlaying(false), + mThumbnail(nullptr), mLblRating(window), mLblReleaseDate(window), mLblDeveloper(window), mLblPublisher(window), mLblGenre(window), mLblPlayers(window), mLblLastPlayed(window), mLblPlayCount(window), @@ -109,6 +110,9 @@ VideoGameListView::VideoGameListView(Window* window, FolderData* root) : VideoGameListView::~VideoGameListView() { + if (mThumbnail != nullptr) + delete mThumbnail; + if (mImage != nullptr) delete mImage; @@ -116,6 +120,23 @@ VideoGameListView::~VideoGameListView() delete mVideo; } +void VideoGameListView::createThumbnail() +{ + if (mThumbnail != nullptr) + return; + + const float padding = 0.01f; + + // Image + mThumbnail = new ImageComponent(mWindow); + mThumbnail->setAllowFading(false); + mThumbnail->setOrigin(0.5f, 0.5f); + mThumbnail->setPosition(mSize.x() * 0.25f, mList.getPosition().y() + mSize.y() * 0.2125f); + mThumbnail->setMaxSize(mSize.x() * (0.50f - 2 * padding), mSize.y() * 0.4f); + mThumbnail->setDefaultZIndex(30); + addChild(mThumbnail); +} + void VideoGameListView::createImage() { if (mImage != nullptr) @@ -154,6 +175,18 @@ void VideoGameListView::onThemeChanged(const std::shared_ptr& theme) mVideo->applyTheme(theme, getName(), "md_video", ALL ^ (PATH)); mName.applyTheme(theme, getName(), "md_name", ALL); + if (theme->getElement(getName(), "md_thumbnail", "image")) + { + createThumbnail(); + mThumbnail->applyTheme(theme, getName(), "md_thumbnail", ALL ^ (PATH)); + } + else if (mThumbnail != nullptr) + { + removeChild(mThumbnail); + delete mThumbnail; + mThumbnail = nullptr; + } + initMDLabels(); std::vector labels = getMDLabels(); assert(labels.size() == 8); @@ -290,7 +323,15 @@ void VideoGameListView::updateInfoPanel() mMarquee.setImage(file->getMarqueePath()/*, false, mMarquee.getMaxSizeInfo()*/); // Too slow on pi - if (mImage != nullptr) + if (mThumbnail != nullptr) + { + if (mImage != nullptr) + mImage->setImage(file->getImagePath(), false, mImage->getMaxSizeInfo()); + + mThumbnail->setImage(file->getThumbnailPath(), false, mThumbnail->getMaxSizeInfo()); + } + else if (mImage != nullptr) + mImage->setImage(file->getThumbnailPath()); mDescription.setText(file->getMetadata().get("desc")); @@ -325,6 +366,9 @@ void VideoGameListView::updateInfoPanel() if (mImage != nullptr) comps.push_back(mImage); + if (mThumbnail != nullptr) + comps.push_back(mThumbnail); + comps.push_back(&mName); std::vector labels = getMDLabels(); comps.insert(comps.cend(), labels.cbegin(), labels.cend()); @@ -343,7 +387,19 @@ void VideoGameListView::updateInfoPanel() { comp->setOpacity((unsigned char)(Math::lerp(0.0f, 1.0f, t)*255)); }; - comp->setAnimation(new LambdaAnimation(func, 150), 0, nullptr, fadingOut); + + bool isFadeOut = fadingOut; + comp->setAnimation(new LambdaAnimation(func, 150), 0, [this, isFadeOut] + { + if (isFadeOut) + { + if (mVideo != nullptr) mVideo->setImage(""); + if (mThumbnail != nullptr) mThumbnail->setImage(""); + if (mImage != nullptr) mImage->setImage(""); + + mMarquee.setImage(""); + } + }, fadingOut); } } } @@ -378,6 +434,11 @@ void VideoGameListView::launch(FileData* game) { target = Vector3f(mVideo->getCenter().x(), mVideo->getCenter().y(), 0); } + else if (mThumbnail != nullptr && mThumbnail->getPosition().x() < screenWidth && mThumbnail->getPosition().x() > 0.0f && + mThumbnail->getPosition().y() < screenHeight && mThumbnail->getPosition().y() > 0.0f) + { + target = Vector3f(mThumbnail->getCenter().x(), mThumbnail->getCenter().y(), 0); + } ViewController::get()->launch(game, target); } diff --git a/es-app/src/views/gamelist/VideoGameListView.h b/es-app/src/views/gamelist/VideoGameListView.h index c7bf438d5f..92cc090fe9 100644 --- a/es-app/src/views/gamelist/VideoGameListView.h +++ b/es-app/src/views/gamelist/VideoGameListView.h @@ -35,6 +35,7 @@ class VideoGameListView : public BasicGameListView private: void updateInfoPanel(); void createImage(); + void createThumbnail(); void initMDLabels(); void initMDValues(); @@ -42,6 +43,7 @@ class VideoGameListView : public BasicGameListView ImageComponent mMarquee; VideoComponent* mVideo; ImageComponent* mImage; + ImageComponent* mThumbnail; TextComponent mLblRating, mLblReleaseDate, mLblDeveloper, mLblPublisher, mLblGenre, mLblPlayers, mLblLastPlayed, mLblPlayCount; diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index eaf98c0045..399cce227b 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -60,12 +60,15 @@ void GridTileComponent::resetProperties() mDefaultProperties.Background.centerColor = mDefaultProperties.Background.edgeColor = 0xAAAAEEFF; mDefaultProperties.Image.color = mDefaultProperties.Image.colorEnd = 0xFFFFFFDD; + + mVideoPlayingProperties = mSelectedProperties; } void GridTileComponent::forceSize(Vector2f size, float selectedZoom) { mDefaultProperties.Size = size; mSelectedProperties.Size = size * selectedZoom; + mVideoPlayingProperties.Size = mSelectedProperties.Size; } GridTileComponent::~GridTileComponent() @@ -272,7 +275,7 @@ void GridTileComponent::update(int deltaTime) { GuiComponent::update(deltaTime); - if (mVideo != nullptr && mVideo->isPlaying()) + if (mVideo != nullptr && mVideo->isPlaying() && mVideo->isFading()) resize(); } @@ -452,10 +455,7 @@ void GridTileComponent::applyThemeToProperties(const ThemeData::ThemeElement* el bool GridImageProperties::applyTheme(const ThemeData::ThemeElement* elem) { if (!elem) - { - Visible = false; return false; - } Loaded = true; Visible = true; @@ -747,6 +747,16 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (elem) mSelectedProperties.Background.applyTheme(elem); + + mVideoPlayingProperties = mSelectedProperties; + + if (!mVideoPlayingProperties.Label.applyTheme(theme->getElement(view, "gridtile:videoplaying", "text"))) + mVideoPlayingProperties.Label.applyTheme(theme->getElement(view, "gridtile.text:videoplaying", "text")); + + mVideoPlayingProperties.Image.applyTheme(theme->getElement(view, "gridtile.image:videoplaying", "image")); + mVideoPlayingProperties.Marquee.applyTheme(theme->getElement(view, "gridtile.marquee:videoplaying", "image")); + mVideoPlayingProperties.Favorite.applyTheme(theme->getElement(view, "gridtile.favorite:selected", "image")); + mVideoPlayingProperties.ImageOverlay.applyTheme(theme->getElement(view, "gridtile.overlay:videoplaying", "image")); } // Made this a static function because the ImageGridComponent need to know the default tile size @@ -981,9 +991,12 @@ Vector3f GridTileComponent::getBackgroundPosition() static Vector2f mixVectors(const Vector2f& def, const Vector2f& sel, float percent) { - if (def == sel) + if (def == sel || percent == 0) return def; + if (percent == 1) + return sel; + float x = def.x() * (1.0 - percent) + sel.x() * percent; float y = def.y() * (1.0 - percent) + sel.y() * percent; return Vector2f(x, y); @@ -991,17 +1004,23 @@ static Vector2f mixVectors(const Vector2f& def, const Vector2f& sel, float perce static unsigned int mixUnsigned(const unsigned int def, const unsigned int sel, float percent) { - if (def == sel) + if (def == sel || percent == 0) return def; + if (percent == 1) + return sel; + return def * (1.0 - percent) + sel * percent; } static float mixFloat(const float def, const float sel, float percent) { - if (def == sel) + if (def == sel || percent == 0) return def; + if (percent == 1) + return sel; + return def * (1.0 - percent) + sel * percent; } @@ -1039,24 +1058,41 @@ void GridTextProperties::mixProperties(GridTextProperties& def, GridTextProperti GridTileProperties GridTileComponent::getCurrentProperties(bool mixValues) { - GridTileProperties prop; - - if (mSelectedZoomPercent == 0.0f || mSelectedZoomPercent == 1.0f) - return mSelected ? mSelectedProperties : mDefaultProperties; - - prop = mSelected ? mSelectedProperties : mDefaultProperties; + if (mSelectedZoomPercent == 0 && !mSelected) + return mDefaultProperties; + + GridTileProperties prop = mSelected ? mSelectedProperties : mDefaultProperties; if (mixValues) { - prop.Size = mixVectors(mDefaultProperties.Size, mSelectedProperties.Size, mSelectedZoomPercent); - prop.Padding = mixVectors(mDefaultProperties.Padding, mSelectedProperties.Padding, mSelectedZoomPercent); - - prop.Label.mixProperties(mDefaultProperties.Label, mSelectedProperties.Label, mSelectedZoomPercent); - prop.Image.mixProperties(mDefaultProperties.Image, mSelectedProperties.Image, mSelectedZoomPercent); - prop.Marquee.mixProperties(mDefaultProperties.Marquee, mSelectedProperties.Marquee, mSelectedZoomPercent); - prop.Favorite.mixProperties(mDefaultProperties.Favorite, mSelectedProperties.Favorite, mSelectedZoomPercent); - prop.ImageOverlay.mixProperties(mDefaultProperties.ImageOverlay, mSelectedProperties.ImageOverlay, mSelectedZoomPercent); + GridTileProperties* from = &mDefaultProperties; + GridTileProperties* to = &mSelectedProperties; + float pc = mSelectedZoomPercent; + + if (mSelected && mVideo != nullptr && mVideo->isPlaying()) + { + if (!mVideo->isFading()) + return mVideoPlayingProperties; + + from = &mSelectedProperties; + to = &mVideoPlayingProperties; + + float t = mVideo->getFade() - 1; // cubic ease in + pc = Math::lerp(0, 1, t*t*t + 1); + } + + prop.Size = mixVectors(from->Size, to->Size, pc); + prop.Padding = mixVectors(from->Padding, to->Padding, pc); + + prop.Label.mixProperties(from->Label, to->Label, pc); + prop.Image.mixProperties(from->Image, to->Image, pc); + prop.Marquee.mixProperties(from->Marquee, to->Marquee, pc); + + prop.Favorite.mixProperties(from->Favorite, to->Favorite, pc); + prop.ImageOverlay.mixProperties(from->ImageOverlay, to->ImageOverlay, pc); } + else if (mSelected && mVideo != nullptr && mVideo->isPlaying() && !mVideo->isFading()) + return mVideoPlayingProperties; return prop; } diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index 64db8ca767..95b8268fd8 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -50,8 +50,6 @@ struct GridImageProperties image->setRoundCorners(roundCorners); } - - bool Loaded; bool Visible; @@ -249,6 +247,7 @@ class GridTileComponent : public GuiComponent GridTileProperties mDefaultProperties; GridTileProperties mSelectedProperties; + GridTileProperties mVideoPlayingProperties; std::string mCurrentMarquee; std::string mCurrentPath; diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index c58ad3a118..b4db4aeb2a 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -120,6 +120,14 @@ class VideoComponent : public GuiComponent return mIsPlaying && mFadeIn < 1.0; } + float getFade() + { + if (!mIsPlaying) + return 0.0; + + return mFadeIn; + } + std::string getVideoPath() { if (mPlayingVideoPath.empty()) diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 64de265376..1b68e1a605 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -193,6 +193,9 @@ void VideoVlcComponent::setColorShift(unsigned int color) void VideoVlcComponent::render(const Transform4x4f& parentTrans) { + if (!mShowing) + return; + if (!isVisible()) return; @@ -262,13 +265,45 @@ void VideoVlcComponent::render(const Transform4x4f& parentTrans) if (mTexture == nullptr) return; - const unsigned int fadeIn = t * 255.0f; - float opacity = mOpacity * fadeIn; - const unsigned int color = Renderer::convertColor(mColorShift & 0xFFFFFF00 | (unsigned char)((mColorShift & 0xFF) * opacity)); - + float opacity = (mOpacity / 255.0f) * t; + unsigned int color = Renderer::convertColor(mColorShift & 0xFFFFFF00 | (unsigned char)((mColorShift & 0xFF) * opacity)); + Renderer::Vertex vertices[4]; - if (mEffect == VideoVlcFlags::VideoVlcEffect::BUMP && mFadeIn > 0.0 && mFadeIn < 1.0 && mConfig.startDelay > 0) + if (mEffect == VideoVlcFlags::VideoVlcEffect::SLIDERIGHT && mFadeIn > 0.0 && mFadeIn < 1.0 && mConfig.startDelay > 0) + { + float t = 1.0 - mFadeIn; + t -= 1; // cubic ease in + t = Math::lerp(0, 1, t*t*t + 1); + //t = 1.0 - t; + + vertices[0] = { { 0.0f , 0.0f },{ t, 0.0f }, color }; + vertices[1] = { { 0.0f , mSize.y() },{ t, 1.0f }, color }; + vertices[2] = { { mSize.x(), 0.0f },{ t + 1.0f, 0.0f }, color }; + vertices[3] = { { mSize.x(), mSize.y() },{ t + 1.0f, 1.0f }, color }; + } + else + if (mEffect == VideoVlcFlags::VideoVlcEffect::SIZE && mFadeIn > 0.0 && mFadeIn < 1.0 && mConfig.startDelay > 0) + { + float t = 1.0 - mFadeIn; + t -= 1; // cubic ease in + t = Math::lerp(0, 1, t*t*t + 1); + t = 1.0 - t; + + float w = mSize.x() * t; + float h = mSize.y() * t; + float centerX = mSize.x() / 2.0f; + float centerY = mSize.y() / 2.0f; + + Vector2f topLeft(Math::round(centerX - w / 2.0f), Math::round(centerY - h / 2.0f)); + Vector2f bottomRight(Math::round(centerX + w / 2.0f), Math::round(centerY + h / 2.0f)); + + vertices[0] = { { topLeft.x() , topLeft.y() },{ 0.0f, 0.0f }, color }; + vertices[1] = { { topLeft.x() , bottomRight.y() },{ 0.0f, 1.0f }, color }; + vertices[2] = { { bottomRight.x() , topLeft.y() },{ 1.0f, 0.0f }, color }; + vertices[3] = { { bottomRight.x() , bottomRight.y() },{ 1.0f, 1.0f }, color }; + } + else if (mEffect == VideoVlcFlags::VideoVlcEffect::BUMP && mFadeIn > 0.0 && mFadeIn < 1.0 && mConfig.startDelay > 0) { // Bump Effect float bump = sin((MATHPI / 2.0) * mFadeIn) + sin(MATHPI * mFadeIn) / 2.0; @@ -469,6 +504,10 @@ void VideoVlcComponent::startVideo() mMedia = libvlc_media_new_path(mVLC, path.c_str()); if (mMedia) { + // If we have a playlist : most videos have a fader, skip it 1 second + if (mPlaylist != nullptr && mConfig.startDelay == 0 && !mConfig.showSnapshotDelay && !mConfig.showSnapshotNoVideo) + libvlc_media_add_option(mMedia, ":start-time=0.7"); + unsigned track_count; // Get the media metadata so we can find the aspect ratio libvlc_media_parse(mMedia); diff --git a/es-core/src/components/VideoVlcComponent.h b/es-core/src/components/VideoVlcComponent.h index 35df164031..14641b1019 100644 --- a/es-core/src/components/VideoVlcComponent.h +++ b/es-core/src/components/VideoVlcComponent.h @@ -37,7 +37,9 @@ namespace VideoVlcFlags enum VideoVlcEffect { NONE, - BUMP + BUMP, + SIZE, + SLIDERIGHT }; } From 434f0d7fd186e6e45cca2ddfa2ddc63c51bbcca6 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 20 Nov 2019 01:05:05 +0100 Subject: [PATCH 232/336] GridView : Fix carousel view --- es-core/src/components/GridTileComponent.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 399cce227b..d6a36fd9ff 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -860,6 +860,7 @@ void GridTileComponent::onShow() { GuiComponent::onShow(); mShown = true; + resize(); } void GridTileComponent::onHide() @@ -1057,18 +1058,19 @@ void GridTextProperties::mixProperties(GridTextProperties& def, GridTextProperti } GridTileProperties GridTileComponent::getCurrentProperties(bool mixValues) -{ - if (mSelectedZoomPercent == 0 && !mSelected) - return mDefaultProperties; - +{ GridTileProperties prop = mSelected ? mSelectedProperties : mDefaultProperties; + if (mSelectedZoomPercent == 0.0f || mSelectedZoomPercent == 1.0f) + if (!mSelected || (mVideo != nullptr && !mVideo->isPlaying())) + return prop; + if (mixValues) { GridTileProperties* from = &mDefaultProperties; GridTileProperties* to = &mSelectedProperties; float pc = mSelectedZoomPercent; - + if (mSelected && mVideo != nullptr && mVideo->isPlaying()) { if (!mVideo->isFading()) @@ -1087,12 +1089,10 @@ GridTileProperties GridTileComponent::getCurrentProperties(bool mixValues) prop.Label.mixProperties(from->Label, to->Label, pc); prop.Image.mixProperties(from->Image, to->Image, pc); prop.Marquee.mixProperties(from->Marquee, to->Marquee, pc); - + prop.Favorite.mixProperties(from->Favorite, to->Favorite, pc); prop.ImageOverlay.mixProperties(from->ImageOverlay, to->ImageOverlay, pc); } - else if (mSelected && mVideo != nullptr && mVideo->isPlaying() && !mVideo->isFading()) - return mVideoPlayingProperties; return prop; } From 981f4bd3706a506fc829067bbd0010304def8ff8 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 20 Nov 2019 01:28:04 +0100 Subject: [PATCH 233/336] GridView : Fix favorite display + Locale, support for LC_MESSAGE hierarchy --- es-app/src/guis/GuiMenu.cpp | 12 +++++++++++- es-core/src/EsLocale.cpp | 17 +++++++++++++++-- es-core/src/components/ImageGridComponent.h | 2 +- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 64ea2a43fc..9dfdfb2be3 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -1057,7 +1057,17 @@ void GuiMenu::openOtherSettings() if (name.rfind("emulationstation2.po") == std::string::npos) continue; - name = Utils::FileSystem::getParent(name); + name = Utils::FileSystem::createRelativePath(name, xmlpath, false); + if (Utils::String::startsWith(name, "./")) + { + name = name.substr(2); + + while (name.find("/") != std::string::npos) + name = Utils::FileSystem::getParent(name); + } + else + name = Utils::FileSystem::getParent(name); + name = Utils::FileSystem::getFileName(name); if (name != "en") diff --git a/es-core/src/EsLocale.cpp b/es-core/src/EsLocale.cpp index 4d3f09c2b7..5b69ea5e33 100644 --- a/es-core/src/EsLocale.cpp +++ b/es-core/src/EsLocale.cpp @@ -92,8 +92,21 @@ void EsLocale::checkLocalisationLoaded() std::string xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/" + mCurrentLanguage + "/emulationstation2.po"); if (!Utils::FileSystem::exists(xmlpath)) - return; - + xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/" + mCurrentLanguage + "/LC_MESSAGES/emulationstation2.po"); + + if (!Utils::FileSystem::exists(xmlpath)) + { + auto shortNameDivider = mCurrentLanguage.find("_"); + if (shortNameDivider != std::string::npos) + { + auto shortName = mCurrentLanguage.substr(0, shortNameDivider); + + xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/" + shortName + "/emulationstation2.po"); + if (!Utils::FileSystem::exists(xmlpath)) + xmlpath = ResourceManager::getInstance()->getResourcePath(":/locale/" + shortName + "/LC_MESSAGES/emulationstation2.po"); + } + } + std::string msgid; std::string msgid_plural; diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index bfeda04efc..08c3b37bc6 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -798,7 +798,7 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA std::string name = mEntries.at(imgPos).name; // .object->getName(); - if (tile->hasFavoriteMedia()) + if (!mEntries.at(imgPos).data.favorite || tile->hasFavoriteMedia()) tile->setLabel(name); else tile->setLabel(_U("\uF006 ") + name); From f7e49182d8e2396ce8fb81a8c4baf46bc4260ab8 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 21 Nov 2019 17:59:14 +0100 Subject: [PATCH 234/336] Loading speed improvements --- es-app/CMakeLists.txt | 2 + es-app/src/ApiSystem.cpp | 165 ++- es-app/src/ApiSystem.h | 12 +- es-app/src/ContentInstaller.cpp | 139 ++ es-app/src/ContentInstaller.h | 45 + es-app/src/FileData.cpp | 4 +- es-app/src/Gamelist.cpp | 164 +-- es-app/src/SystemData.cpp | 3 + es-app/src/guis/GuiGamelistOptions.cpp | 3 + es-app/src/views/ViewController.cpp | 25 +- es-core/src/HttpReq.cpp | 7 +- es-core/src/HttpReq.h | 2 + es-core/src/ThemeData.cpp | 46 +- es-core/src/platform.cpp | 5 + es-core/src/utils/FileSystemUtil.cpp | 1818 +++++++++++++----------- es-core/src/utils/FileSystemUtil.h | 11 + es-core/src/utils/StringUtil.cpp | 20 + es-core/src/utils/StringUtil.h | 1 + es-core/src/utils/ThreadPool.cpp | 2 +- 19 files changed, 1518 insertions(+), 956 deletions(-) create mode 100644 es-app/src/ContentInstaller.cpp create mode 100644 es-app/src/ContentInstaller.h diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index 9f8fc5bd77..d22aef62ae 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -14,6 +14,7 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemScreenSaver.h ${CMAKE_CURRENT_SOURCE_DIR}/src/CollectionSystemManager.h ${CMAKE_CURRENT_SOURCE_DIR}/src/ApiSystem.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/ContentInstaller.h ${CMAKE_CURRENT_SOURCE_DIR}/src/NetworkThread.h # GuiComponents @@ -76,6 +77,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/SystemScreenSaver.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/CollectionSystemManager.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/ApiSystem.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/ContentInstaller.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/NetworkThread.cpp # GuiComponents diff --git a/es-app/src/ApiSystem.cpp b/es-app/src/ApiSystem.cpp index f73165a14a..ec002ef0d8 100644 --- a/es-app/src/ApiSystem.cpp +++ b/es-app/src/ApiSystem.cpp @@ -5,7 +5,7 @@ #include #include #include - +#include "Log.h" #include "Window.h" #include "components/AsyncNotificationComponent.h" @@ -243,9 +243,9 @@ std::pair ApiSystem::updateSystem(const std::function ApiSystem::updateSystem(const std::function("error.", 1); +} + +std::vector ApiSystem::getThemesList() +{ + LOG(LogDebug) << "ApiSystem::getThemesList"; + + std::vector res; + + std::shared_ptr httpreq = std::make_shared("https://batocera.org/upgrades/themes.txt"); + + while (httpreq->status() == HttpReq::REQ_IN_PROGRESS) + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + + if (httpreq->status() == HttpReq::REQ_SUCCESS) + { + auto lines = Utils::String::split(httpreq->getContent(), '\n'); + for (auto line : lines) + { + auto parts = Utils::String::splitAny(line, " \t"); + if (parts.size() > 1) + { + auto themeName = parts[0]; + std::string themeUrl = Utils::FileSystem::getFileName(parts[1]); + + bool themeExists = false; + + std::vector paths{ + Utils::FileSystem::getHomePath() + "/.emulationstation/themes", + "/etc/emulationstation/themes", + "/userdata/themes" + }; + + for (auto path : paths) + { + if (Utils::FileSystem::isDirectory(path + "/" + themeUrl + "-master")) + { + themeExists = true; + break; + } + else if (Utils::FileSystem::isDirectory(path + "/" + themeUrl)) + { + themeExists = true; + break; + } + else if (Utils::FileSystem::isDirectory(path + "/" + themeName)) + { + themeExists = true; + break; + } + } + + ThemeDownloadInfo info; + info.installed = themeExists; + info.name = themeName; + info.url = themeUrl; + + res.push_back(info); + } + } + } + + return res; +} + +std::shared_ptr downloadGitRepository(const std::string url, const std::string label, const std::function& func) +{ + if (func != nullptr) + func("Downloading " + label); + + long downloadSize = 0; + + std::string statUrl = Utils::String::replace(url, "https://github.com/", "https://api.github.com/repos/"); + if (statUrl != url) + { + std::shared_ptr statreq = std::make_shared(statUrl); + + while (statreq->status() == HttpReq::REQ_IN_PROGRESS) + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + + if (statreq->status() == HttpReq::REQ_SUCCESS) + { + std::string content = statreq->getContent(); + auto pos = content.find("\"size\": "); + if (pos != std::string::npos) + { + auto end = content.find(",", pos); + if (end != std::string::npos) + downloadSize = atoi(content.substr(pos + 8, end - pos - 8).c_str()) * 1024; + } + } + } + + std::shared_ptr httpreq = std::make_shared(url + "/archive/master.zip"); + + int curPos = -1; + while (httpreq->status() == HttpReq::REQ_IN_PROGRESS) + { + if (downloadSize > 0) + { + double pos = httpreq->getPosition(); + if (pos > 0 && curPos != pos) + { + if (func != nullptr) + { + std::string pc = std::to_string((int)(pos * 100.0 / downloadSize)); + func(std::string("Downloading " + label + " >>> " + pc + " %")); + } + + curPos = pos; + } + } + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + } + + if (httpreq->status() != HttpReq::REQ_SUCCESS) + return nullptr; + + return httpreq; +} + +std::pair ApiSystem::installTheme(std::string themeName, const std::function& func) +{ +#if WIN32 + for (auto theme : getThemesList()) + { + if (theme.name != themeName) + continue; + + std::shared_ptr httpreq = downloadGitRepository(theme.url, themeName, func); + if (httpreq != nullptr && httpreq->status() == HttpReq::REQ_SUCCESS) + { + if (func != nullptr) + func("Extracting " + themeName); + + std::string themeFileName = Utils::FileSystem::getFileName(theme.url); + std::string zipFile = Utils::FileSystem::getHomePath() + "/.emulationstation/themes/" + themeFileName + ".zip"; + zipFile = Utils::String::replace(zipFile, "/", "\\"); + httpreq->saveContent(zipFile); + + unzipFile(zipFile, Utils::String::replace(Utils::FileSystem::getHomePath() + "/.emulationstation/themes", "/", "\\")); + + std::string folderName = Utils::FileSystem::getHomePath() + "/.emulationstation/themes/" + themeFileName + "-master"; + std::string finalfolderName = Utils::String::replace(folderName, "-master", ""); + + rename(folderName.c_str(), finalfolderName.c_str()); + + Utils::FileSystem::removeFile(zipFile); + + return std::pair(std::string("OK"), 0); + } + + break; + } +#endif + + return std::pair(std::string(""), 1); } \ No newline at end of file diff --git a/es-app/src/ApiSystem.h b/es-app/src/ApiSystem.h index 2ab36b59fb..e539056614 100644 --- a/es-app/src/ApiSystem.h +++ b/es-app/src/ApiSystem.h @@ -3,7 +3,7 @@ #include #include - +#include class Window; @@ -17,6 +17,13 @@ namespace UpdateState }; } +struct ThemeDownloadInfo +{ + bool installed; + std::string name; + std::string url; +}; + class ApiSystem { public: @@ -25,6 +32,9 @@ class ApiSystem static std::pair updateSystem(const std::function& func = nullptr); static std::string checkUpdateVersion(); static void startUpdate(Window* c); + + static std::vector getThemesList(); + static std::pair ApiSystem::installTheme(std::string themeName, const std::function& func = nullptr); }; #endif \ No newline at end of file diff --git a/es-app/src/ContentInstaller.cpp b/es-app/src/ContentInstaller.cpp new file mode 100644 index 0000000000..7668e9b20e --- /dev/null +++ b/es-app/src/ContentInstaller.cpp @@ -0,0 +1,139 @@ +#include "ContentInstaller.h" +#include "Window.h" +#include "components/AsyncNotificationComponent.h" +#include "utils/StringUtil.h" +#include "ApiSystem.h" +#include "EsLocale.h" + +#define ICONINDEX _U("\uF019 ") + +ContentInstaller* ContentInstaller::mInstance = nullptr; +std::mutex ContentInstaller::mLock; +std::list> ContentInstaller::mQueue; +std::list> ContentInstaller::mProcessingQueue; + +void ContentInstaller::Enqueue(Window* window, ContentType type, const std::string contentName) +{ + std::unique_lock lock(mLock); + + for (auto item : mProcessingQueue) + if (item.first == type && item.second == contentName) + return; + + for (auto item : mQueue) + if (item.first == type && item.second == contentName) + return; + + mQueue.push_back(std::pair(type, contentName)); + + if (mInstance == nullptr) + mInstance = new ContentInstaller(window); + + mInstance->updateNotificationComponentTitle(true); +} + +ContentInstaller::ContentInstaller(Window* window) +{ + mInstance = this; + + mCurrent = 0; + mQueueSize = 0; + + mWindow = window; + + mWndNotification = new AsyncNotificationComponent(window, false); + mWindow->registerNotificationComponent(mWndNotification); + + mHandle = new std::thread(&ContentInstaller::threadUpdate, this); +} + +ContentInstaller::~ContentInstaller() +{ + mHandle = nullptr; + mWindow->unRegisterNotificationComponent(mWndNotification); + delete mWndNotification; +} + +void ContentInstaller::updateNotificationComponentTitle(bool incQueueSize) +{ + if (incQueueSize) + mQueueSize++; + + std::string cnt = " " + std::to_string(mCurrent) + "/" + std::to_string(mQueueSize); + mWndNotification->updateTitle(ICONINDEX + _("DOWNLOADING")+ cnt); +} + +void ContentInstaller::updateNotificationComponentContent(const std::string info) +{ + auto pos = info.find(">>>"); + if (pos != std::string::npos) + { + std::string percent(info.substr(pos)); + percent = Utils::String::replace(percent, ">", ""); + percent = Utils::String::replace(percent, "%", ""); + percent = Utils::String::replace(percent, " ", ""); + + int value = atoi(percent.c_str()); + + std::string text(info.substr(0, pos)); + text = Utils::String::trim(text); + + mWndNotification->updatePercent(value); + mWndNotification->updateText(text); + } + else + { + mWndNotification->updatePercent(-1); + mWndNotification->updateText(info); + } +} + +void ContentInstaller::threadUpdate() +{ + mCurrent = 0; + + while (true) + { + // Wait for an event to say there is something in the queue + std::unique_lock lock(mLock); + + if (mQueue.empty()) + break; + + mCurrent++; + updateNotificationComponentTitle(false); + + auto data = mQueue.front(); + mQueue.pop_front(); + mProcessingQueue.push_back(data); + + lock.unlock(); + + std::pair updateStatus; + + if (data.first == ContentType::CONTENT_THEME) + { + updateStatus = ApiSystem::installTheme(data.second, [this](const std::string info) + { + updateNotificationComponentContent(info); + }); + + if (updateStatus.second == 0) + mWindow->displayNotificationMessage(ICONINDEX + data.second + " : " + _("THEME INSTALLED SUCCESSFULLY")); + else + { + std::string error = _("AN ERROR OCCURED") + std::string(": ") + updateStatus.first; + mWindow->displayNotificationMessage(ICONINDEX + error); + } + + } + + lock.lock(); + mProcessingQueue.remove(data); + } + + std::unique_lock lock(mLock); + + delete this; + mInstance = nullptr; +} diff --git a/es-app/src/ContentInstaller.h b/es-app/src/ContentInstaller.h new file mode 100644 index 0000000000..724199f256 --- /dev/null +++ b/es-app/src/ContentInstaller.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include +#include +#include +#include + +class Window; +class AsyncNotificationComponent; + + +class ContentInstaller +{ +public: + enum ContentType : int + { + CONTENT_THEME = 0 + }; + + static void Enqueue(Window* window, ContentType type, const std::string contentName); + +private: // Methods + ContentInstaller(Window* window); + ~ContentInstaller(); + + void updateNotificationComponentTitle(bool incQueueSize); + void updateNotificationComponentContent(const std::string info); + + void threadUpdate(); + +private: + AsyncNotificationComponent* mWndNotification; + Window* mWindow; + std::thread* mHandle; + + int mCurrent; + int mQueueSize; + +private: + static ContentInstaller* mInstance; + static std::mutex mLock; + static std::list> mQueue; + static std::list> mProcessingQueue; +}; diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 29ffe72b95..1acabfe3f6 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -438,7 +438,7 @@ const std::vector FolderData::getChildrenListToDisplay() } unsigned int currentSortId = sys->getSortId(); - if (currentSortId > FileSorts::getSortTypes().size()) + if (currentSortId >= FileSorts::getSortTypes().size()) currentSortId = 0; const FileSorts::SortType& sort = FileSorts::getSortTypes().at(currentSortId); @@ -476,7 +476,7 @@ std::vector FolderData::getFlatGameList(bool displayedOnly, SystemDat std::vector ret = getFilesRecursive(GAME, displayedOnly, system); unsigned int currentSortId = system->getSortId(); - if (currentSortId < 0 || currentSortId >FileSorts::getSortTypes().size()) + if (currentSortId < 0 || currentSortId >= FileSorts::getSortTypes().size()) currentSortId = 0; auto sort = FileSorts::getSortTypes().at(currentSortId); diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 484b3dfce7..36648c61ad 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -150,8 +150,6 @@ void loadGamelistFile (const std::string xmlpath, SystemData* system, std::unord continue; const std::string path = Utils::FileSystem::resolveRelativePath(fileNode.child("path").text().get(), relativeTo, false); - - if (!trustGamelist && !Utils::FileSystem::exists(path)) { LOG(LogWarning) << "File \"" << path << "\" does not exist! Ignoring."; @@ -173,7 +171,7 @@ void loadGamelistFile (const std::string xmlpath, SystemData* system, std::unord if (file->getMetadata().get("name").empty()) file->setMetadata("name", defaultName); - if (Utils::FileSystem::isHidden(path)) + if (!file->getHidden() && Utils::FileSystem::isHidden(path)) file->getMetadata().set("hidden", "true"); if (checkSize != SIZE_MAX) @@ -309,9 +307,25 @@ void updateGamelist(SystemData* system) if(Settings::getInstance()->getBool("IgnoreGamelist")) return; - if (system != nullptr && system->getName() == "imageviewer") + if (system != nullptr && system->getName() == "imageviewer" || system->isCollection()) return; - + + FolderData* rootFolder = system->getRootFolder(); + if (rootFolder == nullptr) + { + LOG(LogError) << "Found no root folder for system \"" << system->getName() << "\"!"; + return; + } + + std::vector dirtyFiles; + std::vector files = rootFolder->getFilesRecursive(GAME | FOLDER); + for (auto file : files) + if (file->getMetadata().wasChanged()) + dirtyFiles.push_back(file); + + if (dirtyFiles.size() == 0) + return; + int numUpdated = 0; pugi::xml_document doc; @@ -340,104 +354,90 @@ void updateGamelist(SystemData* system) 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 - FolderData* rootFolder = system->getRootFolder(); - if (rootFolder != nullptr) + //iterate through all files, checking if they're already in the XML + for(auto file : dirtyFiles) { - //get only files, no folders - std::vector files = rootFolder->getFilesRecursive(GAME | FOLDER); - //iterate through all files, checking if they're already in the XML - for(std::vector::const_iterator fit = files.cbegin(); fit != files.cend(); ++fit) - { - const char* tag = ((*fit)->getType() == GAME) ? "game" : "folder"; - - // do not touch if it wasn't changed anyway - if (!(*fit)->getMetadata().wasChanged()) - continue; + const char* tag = (file->getType() == GAME) ? "game" : "folder"; - bool removed = false; + bool removed = false; - // 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)) + // 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) { - pugi::xml_node pathNode = fileNode.child("path"); - if(!pathNode) - { - LOG(LogError) << "<" << tag << "> node contains no child!"; - continue; - } - - std::string nodePath = Utils::FileSystem::getCanonicalPath(Utils::FileSystem::resolveRelativePath(pathNode.text().get(), system->getStartPath(), true)); - std::string gamePath = Utils::FileSystem::getCanonicalPath((*fit)->getPath()); - if(nodePath == gamePath) - { - // found it - removed = true; - root.remove_child(fileNode); - break; - } + LOG(LogError) << "<" << tag << "> node contains no child!"; + continue; } - // it was either removed or never existed to begin with; either way, we can add it now - if (addFileDataNode(root, *fit, tag, system)) - ++numUpdated; // Only if really added - else if (removed) - ++numUpdated; // Only if really removed + std::string nodePath = Utils::FileSystem::getCanonicalPath(Utils::FileSystem::resolveRelativePath(pathNode.text().get(), system->getStartPath(), true)); + std::string gamePath = Utils::FileSystem::getCanonicalPath(file->getPath()); + if(nodePath == gamePath) + { + // found it + removed = true; + root.remove_child(fileNode); + break; + } } - //now write the file + // it was either removed or never existed to begin with; either way, we can add it now + if (addFileDataNode(root, file, tag, system)) + ++numUpdated; // Only if really added + else if (removed) + ++numUpdated; // Only if really removed + } + + //now write the file - if (numUpdated > 0) { - //make sure the folders leading up to this path exist (or the write will fail) - std::string xmlWritePath(system->getGamelistPath(true)); - Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(xmlWritePath)); + if (numUpdated > 0) { + //make sure the folders leading up to this path exist (or the write will fail) + std::string xmlWritePath(system->getGamelistPath(true)); + Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(xmlWritePath)); - LOG(LogInfo) << "Added/Updated " << numUpdated << " entities in '" << xmlReadPath << "'"; + LOG(LogInfo) << "Added/Updated " << numUpdated << " entities in '" << xmlReadPath << "'"; - // Secure XML writing -> Write to a temporary file first - std::string tmpFile = xmlWritePath + ".tmp"; - if (Utils::FileSystem::exists(tmpFile)) - Utils::FileSystem::removeFile(tmpFile); + // Secure XML writing -> Write to a temporary file first + std::string tmpFile = xmlWritePath + ".tmp"; + if (Utils::FileSystem::exists(tmpFile)) + Utils::FileSystem::removeFile(tmpFile); - if (!doc.save_file(tmpFile.c_str())) { - LOG(LogError) << "Error saving gamelist.xml to \"" << xmlWritePath << "\" (for system " << system->getName() << ")!"; - } - else if (Utils::FileSystem::exists(tmpFile)) - { - doc.reset(); + if (!doc.save_file(tmpFile.c_str())) { + LOG(LogError) << "Error saving gamelist.xml to \"" << xmlWritePath << "\" (for system " << system->getName() << ")!"; + } + else if (Utils::FileSystem::exists(tmpFile)) + { + doc.reset(); #ifdef WIN32 - ::Sleep(50); // Introduce a small sleep + ::Sleep(50); // Introduce a small sleep #endif - // Secure XML writing - if (Utils::FileSystem::getFileSize(tmpFile) != 0) - { - std::string savFile = xmlWritePath + ".old"; + // Secure XML writing + if (Utils::FileSystem::getFileSize(tmpFile) != 0) + { + std::string savFile = xmlWritePath + ".old"; - // remove previous gamelist.xml.old file - if (Utils::FileSystem::exists(savFile)) - Utils::FileSystem::removeFile(savFile); + // remove previous gamelist.xml.old file + if (Utils::FileSystem::exists(savFile)) + Utils::FileSystem::removeFile(savFile); - // rename gamelist.xml to gamelist.xml.old - if (Utils::FileSystem::exists(xmlWritePath)) - std::rename(xmlWritePath.c_str(), savFile.c_str()); - else - LOG(LogError) << "Unable to rename \"" << xmlWritePath << "to " << savFile << "\"!"; + // rename gamelist.xml to gamelist.xml.old + if (Utils::FileSystem::exists(xmlWritePath)) + std::rename(xmlWritePath.c_str(), savFile.c_str()); + else + LOG(LogError) << "Unable to rename \"" << xmlWritePath << "to " << savFile << "\"!"; - // rename gamelist.tmp.xml to gamelist.xml - if (std::rename(tmpFile.c_str(), xmlWritePath.c_str()) != 0) - LOG(LogError) << "Unable to rename \"" << tmpFile << "to " << xmlWritePath << "\"!"; + // rename gamelist.tmp.xml to gamelist.xml + if (std::rename(tmpFile.c_str(), xmlWritePath.c_str()) != 0) + LOG(LogError) << "Unable to rename \"" << tmpFile << "to " << xmlWritePath << "\"!"; - clearTemporaryGamelistRecovery(system); - } - else - Utils::FileSystem::removeFile(tmpFile); + clearTemporaryGamelistRecovery(system); } + else + Utils::FileSystem::removeFile(tmpFile); } - }else{ - LOG(LogError) << "Found no root folder for system \"" << system->getName() << "\"!"; } } diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 4518a75481..e4aa976705 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -336,6 +336,7 @@ SystemData* SystemData::loadSystem(pugi::xml_node system) { path.erase(0, 1); path.insert(0, Utils::FileSystem::getHomePath()); + path = Utils::FileSystem::getCanonicalPath(path); } //create the system runtime environment data @@ -404,6 +405,8 @@ bool SystemData::loadConfig(Window* window) systemCount++; } + Utils::FileSystem::FileSystemCacheActivator fsc; + int currentSystem = 0; typedef SystemData* SystemDataPtr; diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 652e1e4ab9..2dc6d7c18e 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -137,6 +137,9 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool mListSort->add(sort.icon + sort.description, sort.id, sort.id == currentSortId); // TODO - actually make the sort type persistent } + if (!mListSort->hasSelection()) + mListSort->selectFirstItem(); + mMenu.addWithLabel(_("SORT GAMES BY"), mListSort); // GameList view style diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 0fbf84ee89..1c4de97003 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -17,6 +17,8 @@ #include "SystemData.h" #include "Window.h" #include "AudioManager.h" +#include "utils/ThreadPool.h" +#include ViewController* ViewController::sInstance = NULL; @@ -658,6 +660,8 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) void ViewController::reloadAll(Window* window) { + Utils::FileSystem::FileSystemCacheActivator fsc; + ThemeData::setDefaultTheme(nullptr); SystemData* system = nullptr; @@ -687,11 +691,28 @@ void ViewController::reloadAll(Window* window) float idx = 0; + if (window) + window->renderLoadingScreen(_("Loading theme...")); + + Utils::ThreadPool pool; + + for (auto it = cursorMap.cbegin(); it != cursorMap.cend(); it++) + { + auto system = it->first; + pool.queueWorkItem([system] + { + system->loadTheme(); + system->resetFilters(); + }); + } + + pool.wait(); + // load themes, create gamelistviews and reset filters for(auto it = cursorMap.cbegin(); it != cursorMap.cend(); it++) { - it->first->loadTheme(); - it->first->resetFilters(); + // it->first->loadTheme(); + // it->first->resetFilters(); if (it->second != NULL) getGameListView(it->first)->setCursor(it->second); diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index 4e67fefdba..8cf62410ef 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -96,6 +96,7 @@ HttpReq::HttpReq(const std::string& url) { mUrl = url; + mPosition = -1; mPercent = -1; mHandle = curl_easy_init(); @@ -382,13 +383,13 @@ size_t HttpReq::write_content(void* buff, size_t size, size_t nmemb, void* req_p double cl; if (!curl_easy_getinfo(request->mHandle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &cl)) { + double position = (double)ss.tellp(); + request->mPosition = position; + if (cl <= 0) request->mPercent = -1; else - { - double position = (double)ss.tellp(); request->mPercent = (int) (position * 100.0 / cl); - } } return nmemb; diff --git a/es-core/src/HttpReq.h b/es-core/src/HttpReq.h index ebb93695be..8593c4a26c 100644 --- a/es-core/src/HttpReq.h +++ b/es-core/src/HttpReq.h @@ -57,6 +57,7 @@ class HttpReq static bool isUrl(const std::string& s); int getPercent() { return mPercent; } + int getPosition() { return mPosition; } std::string getUrl() { return mUrl; } @@ -83,6 +84,7 @@ class HttpReq std::string mUrl; int mPercent; + double mPosition; }; #endif // ES_CORE_HTTP_REQ_H diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 4a9578e8ba..987f014dc7 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -280,6 +280,8 @@ ThemeData* ThemeData::mDefaultTheme = nullptr; // helper unsigned int getHexColor(const char* str) { + + // ThemeException error; if (!str) { @@ -297,9 +299,7 @@ unsigned int getHexColor(const char* str) } unsigned int val; - std::stringstream ss; - ss << str; - ss >> std::hex >> val; + sscanf(str, "%x", &val); if(len == 6) val = (val << 8) | 0xFF; @@ -309,7 +309,7 @@ unsigned int getHexColor(const char* str) std::string ThemeData::resolvePlaceholders(const char* in) { - if (in == nullptr || in[0] == '/0') + if (in == nullptr || in[0] == 0) return in; std::string inStr(in); @@ -517,7 +517,7 @@ void ThemeData::parseInclude(const pugi::xml_node& node) return; std::string relPath = resolvePlaceholders(node.text().as_string()); - std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true); + std::string path = Utils::FileSystem::resolveRelativePath(relPath, Utils::FileSystem::getParent(mPaths.back()), true); path = resolveSystemVariable(mSystemThemeFolder, path); if (!ResourceManager::getInstance()->fileExists(path)) @@ -973,12 +973,9 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapfileExists(path)) + if(!ResourceManager::getInstance()->fileExists(path)) { - std::string rootPath = Utils::FileSystem::resolveRelativePath(str, mPaths.front(), true); - if (ResourceManager::getInstance()->fileExists(rootPath)) + std::string rootPath = Utils::FileSystem::resolveRelativePath(str, Utils::FileSystem::getParent(mPaths.front()), true); + if (rootPath != path && ResourceManager::getInstance()->fileExists(rootPath)) path = rootPath; } @@ -1043,8 +1043,8 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map(strtod(str.c_str(), 0)); - element.properties[node.name()] = floatVal; + //float floatVal = atof(str.c_str()); static_cast(strtod(str.c_str(), 0)); + element.properties[node.name()] = (float) atof(str.c_str()); //floatVal; break; } diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index 79d101bc3d..f340560883 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -89,6 +89,11 @@ BOOL CALLBACK MonitorEnumProc(HMONITOR hMonitor, HDC hdcMonitor, LPRECT lprcMoni int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Window* window) { #ifdef WIN32 + +#if _DEBUG + return 0; +#endif + if (window != NULL) window->renderGameLoadingScreen(); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 1440a8e971..08cdcc2abd 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -1,275 +1,381 @@ -#define _FILE_OFFSET_BITS 64 - -#include "utils/FileSystemUtil.h" -#include "utils/StringUtil.h" - -#include "Settings.h" -#include -#include -#include "platform.h" - -#if defined(_WIN32) -// because windows... -#include -#include -#include -#define getcwd _getcwd -#define mkdir(x,y) _mkdir(x) -#define snprintf _snprintf -#define stat64 _stat64 -#define unlink _unlink -#define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) -#define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) -#else // _WIN32 -#include -#include -#endif // _WIN32 -#include +#define _FILE_OFFSET_BITS 64 + +#include "utils/FileSystemUtil.h" +#include "utils/StringUtil.h" + +#include "Settings.h" +#include +#include +#include "platform.h" + +#if defined(_WIN32) +// because windows... +#include +#include +#include +#define getcwd _getcwd +#define mkdir(x,y) _mkdir(x) +#define snprintf _snprintf +#define stat64 _stat64 +#define unlink _unlink +#define S_ISREG(x) (((x) & S_IFMT) == S_IFREG) +#define S_ISDIR(x) (((x) & S_IFMT) == S_IFDIR) +#else // _WIN32 +#include +#include +#include +#endif // _WIN32 +#include #include - -namespace Utils -{ - namespace FileSystem - { - static std::string homePath; - static std::string exePath; - -#if defined(_WIN32) - static std::mutex mFileMutex; -#endif - - bool compareFileInfo(const FileInfo& first, const FileInfo& second) - { - unsigned int i = 0; - while ((i < first.path.length()) && (i < second.path.length())) - { - if (tolower(first.path[i]) < tolower(second.path[i])) return true; - else if (tolower(first.path[i]) > tolower(second.path[i])) return false; - ++i; - } - return (first.path.length() < second.path.length()); - } - - std::string readAllText(const std::string fileName) - { - std::ifstream t(fileName); - std::stringstream buffer; - buffer << t.rdbuf(); - return buffer.str(); - } - - void writeAllText(const std::string fileName, const std::string text) + +namespace Utils +{ + namespace FileSystem + { + static std::string homePath; + static std::string exePath; + + struct FileCache { - std::fstream fs; - fs.open(fileName.c_str(), std::fstream::out); - fs << text; - fs.close(); - } - - fileList getDirInfo(const std::string& _path/*, const bool _recursive*/) - { - std::string path = getGenericPath(_path); - fileList contentList; - - // only parse the directory, if it's a directory - if (isDirectory(path)) - { -#if defined(_WIN32) - std::unique_lock lock(mFileMutex); - - WIN32_FIND_DATAW findData; - std::string wildcard = path + "/*"; - // HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); - - HANDLE hFind = FindFirstFileExW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), - FINDEX_INFO_LEVELS::FindExInfoStandard, &findData, FINDEX_SEARCH_OPS::FindExSearchNameMatch - , NULL, FIND_FIRST_EX_LARGE_FETCH); - - if (hFind != INVALID_HANDLE_VALUE) - { - // loop over all files in the directory - do - { - std::string name = Utils::String::convertFromWideString(findData.cFileName); - - if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY && name == "." || name == "..") - continue; - - FileInfo fi; - fi.path = path + "/" + name; - fi.hidden = (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN; - fi.directory = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY; - contentList.push_back(fi); - } - while (FindNextFileW(hFind, &findData)); - - FindClose(hFind); - } -#else // _WIN32 - DIR* dir = opendir(path.c_str()); - - if (dir != NULL) - { - struct dirent* entry; - - // loop over all files in the directory - while ((entry = readdir(dir)) != NULL) - { - std::string name(entry->d_name); - - // ignore "." and ".." - if ((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - - FileInfo fi; - fi.path = fullName; - fi.hidden = Utils::FileSystem::isHidden(fullName); - fi.directory = isDirectory(fullName); - contentList.push_back(fi); - } - } - - closedir(dir); - } -#endif // _WIN32 - - } - - // sort the content list - // Why loose time -> It will be sorted later ???? contentList.sort(compareFileInfo); - - // return the content list - return contentList; - - } // getDirContent - - stringList getDirContent(const std::string& _path, const bool _recursive, const bool includeHidden) - { - std::string path = getGenericPath(_path); - stringList contentList; - - // only parse the directory, if it's a directory - if(isDirectory(path)) - { -#if defined(_WIN32) - std::unique_lock* pLock = nullptr; - - if (!_recursive) - pLock = new std::unique_lock(mFileMutex); - - WIN32_FIND_DATAW findData; - std::string wildcard = path + "/*"; - // HANDLE hFind = FindFirstFileW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), &findData); - - HANDLE hFind = FindFirstFileExW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), - FINDEX_INFO_LEVELS::FindExInfoStandard, &findData, FINDEX_SEARCH_OPS::FindExSearchNameMatch - , NULL, FIND_FIRST_EX_LARGE_FETCH); - - //handle = FindFirstFileEx(search, 0, &FindFileData, 2, NULL, 0); - - if(hFind != INVALID_HANDLE_VALUE) - { - // loop over all files in the directory - do - { - std::string name = Utils::String::convertFromWideString(findData.cFileName); - - // ignore "." and ".." - if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY && name == "." || name == "..") - continue; - - std::string fullName(getGenericPath(path + "/" + name)); - - if (!includeHidden && (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN) - continue; - - contentList.push_back(fullName); - - if (_recursive && (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) - { - for (auto child : getDirContent(fullName, true, includeHidden)) - contentList.push_back(child); - } - } - while(FindNextFileW(hFind, &findData)); - - FindClose(hFind); - } - - if (pLock != nullptr) - delete pLock; -#else // _WIN32 - DIR* dir = opendir(path.c_str()); - - if(dir != NULL) - { - struct dirent* entry; - - // loop over all files in the directory - while((entry = readdir(dir)) != NULL) - { - std::string name(entry->d_name); - - // ignore "." and ".." - if((name != ".") && (name != "..")) - { - std::string fullName(getGenericPath(path + "/" + name)); - - if (!includeHidden && Utils::FileSystem::isHidden(fullName)) - continue; - - contentList.push_back(fullName); - - if(_recursive && isDirectory(fullName)) - contentList.merge(getDirContent(fullName, true)); - } - } - - closedir(dir); - } -#endif // _WIN32 - - } - - // sort the content list -// Why loose time -> It will be sorted later ???? contentList.sort(); - - // return the content list - return contentList; - - } // getDirContent - - stringList getPathList(const std::string& _path) - { - stringList pathList; - std::string path = getGenericPath(_path); - size_t start = 0; - size_t end = 0; - - // split at '/' - while((end = path.find("/", start)) != std::string::npos) - { - if(end != start) - pathList.push_back(std::string(path, start, end - start)); - - start = end + 1; - } - - // add last folder / file to pathList - if(start != path.size()) - pathList.push_back(std::string(path, start, path.size() - start)); - - // return the path list - return pathList; - - } // getPathList - - void setHomePath(const std::string& _path) - { - homePath = getGenericPath(_path); - } - + FileCache() {} + + FileCache(bool _exists, bool _dir) + { + directory = _dir; + exists = _exists; + hidden = false; + isSymLink = false; + } + +#if WIN32 + FileCache(DWORD dwFileAttributes) + { + if (0xFFFFFFFF == dwFileAttributes) + { + directory = false; + exists = false; + hidden = false; + isSymLink = false; + } + else + { + exists = true; + directory = dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY; + hidden = dwFileAttributes & FILE_ATTRIBUTE_HIDDEN; + isSymLink = dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT; + } + } +#else + FileCache(const std::string& name, dirent* entry) + { + exists = true; + hidden = (getFileName(name)[0] == '.'); + directory = (entry->d_type == 4); // DT_DIR; + isSymLink = (entry->d_type == 10); // DT_LNK; + } + + FileCache(dirent* entry, bool _hidden) + { + exists = true; + hidden = _hidden; + directory = (entry->d_type == 4); // DT_DIR; + isSymLink = (entry->d_type == 10); // DT_LNK; + } +#endif + + bool exists; + bool directory; + bool hidden; + bool isSymLink; + + static int fromStat64(const std::string& key, struct stat64* info) + { + int ret = stat64(key.c_str(), info); + + std::unique_lock lock(mFileCacheMutex); + + FileCache cache(ret == 0, false); + if (cache.exists) + { + cache.directory = S_ISDIR(info->st_mode); +#ifndef WIN32 + cache.isSymLink = S_ISLNK(info->st_mode); +#endif + } + + mFileCache[key] = cache; + + return ret; + } + + static void add(const std::string& key, FileCache cache) + { + if (!mEnabled) + return; + + std::unique_lock lock(mFileCacheMutex); + mFileCache[key] = cache; + } + + static FileCache* get(const std::string& key) + { + if (!mEnabled) + return nullptr; + + std::unique_lock lock(mFileCacheMutex); + + auto it = mFileCache.find(key); + if (it != mFileCache.cend()) + return &it->second; + + it = mFileCache.find(Utils::FileSystem::getParent(key)+"/*"); + if (it != mFileCache.cend()) + { + mFileCache[key] = FileCache(false, false); + return &mFileCache[key]; + } + + return nullptr; + } + + static void resetCache() + { + std::unique_lock lock(mFileCacheMutex); + mFileCache.clear(); + } + + static void setEnabled(bool value) { mEnabled = value; } + + private: + static std::map mFileCache; + static std::mutex mFileCacheMutex; + static bool mEnabled; + }; + + std::map FileCache::mFileCache; + std::mutex FileCache::mFileCacheMutex; + bool FileCache::mEnabled = false; + + FileSystemCacheActivator::FileSystemCacheActivator() + { + if (mReferenceCount == 0) + { + FileCache::setEnabled(true); + FileCache::resetCache(); + } + + mReferenceCount++; + } + + FileSystemCacheActivator::~FileSystemCacheActivator() + { + mReferenceCount--; + + if (mReferenceCount <= 0) + { + FileCache::setEnabled(false); + FileCache::resetCache(); + } + } + + int FileSystemCacheActivator::mReferenceCount = 0; + + fileList getDirInfo(const std::string& _path/*, const bool _recursive*/) + { + std::string path = getGenericPath(_path); + fileList contentList; + + // only parse the directory, if it's a directory + if (isDirectory(path)) + { + FileCache::add(path + "/*", FileCache(true, true)); + +#if defined(_WIN32) + WIN32_FIND_DATAW findData; + std::string wildcard = path + "/*"; + + HANDLE hFind = FindFirstFileExW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), + FINDEX_INFO_LEVELS::FindExInfoStandard, &findData, FINDEX_SEARCH_OPS::FindExSearchNameMatch + , NULL, FIND_FIRST_EX_LARGE_FETCH); + + if (hFind != INVALID_HANDLE_VALUE) + { + // loop over all files in the directory + do + { + std::string name = Utils::String::convertFromWideString(findData.cFileName); + + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY && name == "." || name == "..") + continue; + + FileInfo fi; + fi.path = path + "/" + getGenericPath(name); + fi.hidden = (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN; + fi.directory = (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY; + contentList.push_back(fi); + + FileCache::add(fi.path, FileCache((DWORD)findData.dwFileAttributes)); + } + while (FindNextFileW(hFind, &findData)); + + FindClose(hFind); + } +#else // _WIN32 + DIR* dir = opendir(path.c_str()); + + if (dir != NULL) + { + struct dirent* entry; + + // loop over all files in the directory + while ((entry = readdir(dir)) != NULL) + { + std::string name(entry->d_name); + + // ignore "." and ".." + if ((name != ".") && (name != "..")) + { + std::string fullName(path + "/" + getGenericPath(name)); + + FileInfo fi; + fi.path = fullName; + fi.hidden = Utils::FileSystem::isHidden(fullName); + fi.directory = (entry->d_type == 4); // DT_DIR; + contentList.push_back(fi); + + FileCache::add(fullName, FileCache(entry, fi.hidden)); + } + } + + closedir(dir); + } +#endif // _WIN32 + + } + + // return the content list + return contentList; + + } // getDirContent + + stringList getDirContent(const std::string& _path, const bool _recursive, const bool includeHidden) + { + std::string path = getGenericPath(_path); + stringList contentList; + + // only parse the directory, if it's a directory + if(isDirectory(path)) + { + FileCache::add(path + "/*", FileCache(true, true)); + +#if defined(_WIN32) + WIN32_FIND_DATAW findData; + std::string wildcard = path + "/*"; + + HANDLE hFind = FindFirstFileExW(std::wstring(wildcard.begin(), wildcard.end()).c_str(), + FINDEX_INFO_LEVELS::FindExInfoStandard, &findData, FINDEX_SEARCH_OPS::FindExSearchNameMatch + , NULL, FIND_FIRST_EX_LARGE_FETCH); + + if(hFind != INVALID_HANDLE_VALUE) + { + // loop over all files in the directory + do + { + std::string name = Utils::String::convertFromWideString(findData.cFileName); + + // ignore "." and ".." + if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY && name == "." || name == "..") + continue; + + std::string fullName(path + "/" + getGenericPath(name)); + + if (!includeHidden && (findData.dwFileAttributes & FILE_ATTRIBUTE_HIDDEN) == FILE_ATTRIBUTE_HIDDEN) + continue; + + contentList.push_back(fullName); + + FileCache::add(fullName, FileCache((DWORD)findData.dwFileAttributes)); + + if (_recursive && (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) == FILE_ATTRIBUTE_DIRECTORY) + { + for (auto child : getDirContent(fullName, true, includeHidden)) + contentList.push_back(child); + } + } + while(FindNextFileW(hFind, &findData)); + + FindClose(hFind); + } +#else // _WIN32 + DIR* dir = opendir(path.c_str()); + + if(dir != NULL) + { + struct dirent* entry; + + // loop over all files in the directory + while((entry = readdir(dir)) != NULL) + { + std::string name(entry->d_name); + + // ignore "." and ".." + if((name != ".") && (name != "..")) + { + std::string fullName(getGenericPath(path + "/" + name)); + + FileCache::add(fullName, FileCache(fullName, entry)); + + if (!includeHidden && Utils::FileSystem::isHidden(fullName)) + continue; + + contentList.push_back(fullName); + + if(_recursive && isDirectory(fullName)) + contentList.merge(getDirContent(fullName, true)); + } + } + + closedir(dir); + } +#endif // _WIN32 + + } + + // return the content list + return contentList; + + } // getDirContent + + stringList getPathList(const std::string& _path) + { + stringList pathList; + std::string path = getGenericPath(_path); + size_t start = 0; + size_t end = 0; + + // split at '/' + while((end = path.find("/", start)) != std::string::npos) + { + if(end != start) + pathList.push_back(std::string(path, start, end - start)); + + start = end + 1; + } + + // add last folder / file to pathList + if(start != path.size()) + pathList.push_back(std::string(path, start, path.size() - start)); + + // return the path list + return pathList; + + } // getPathList + + void setHomePath(const std::string& _path) + { + homePath = Utils::FileSystem::getGenericPath(_path); + } + std::string getHomePath() { // only construct the homepath once @@ -303,17 +409,19 @@ namespace Utils if (!homePath.length()) homePath = getCWDPath(); + homePath = getGenericPath(homePath); + // return constructed homepath return homePath; - } // getHomePath - - std::string getCWDPath() - { - char temp[512]; - return (getcwd(temp, 512) ? getGenericPath(temp) : ""); - } // getCWDPath - + } // getHomePath + + std::string getCWDPath() + { + char temp[512]; + return (getcwd(temp, 512) ? getGenericPath(temp) : ""); + } // getCWDPath + void setExePath(const std::string& _path) { exePath = getCanonicalPath(_path); @@ -328,356 +436,352 @@ namespace Utils // return constructed exepath return exePath; - } // getExePath - - std::string getPreferredPath(const std::string& _path) - { - std::string path = _path; - size_t offset = std::string::npos; -#if defined(_WIN32) - // convert '/' to '\\' - while((offset = path.find('/')) != std::string::npos) - path.replace(offset, 1, "\\"); -#endif // _WIN32 - return path; - } - - std::string getGenericPath(const std::string& _path) - { - std::string path = _path; - size_t offset = std::string::npos; - - // remove "\\\\?\\" - if((path.find("\\\\?\\")) == 0) - path.erase(0, 4); - - // convert '\\' to '/' - while((offset = path.find('\\')) != std::string::npos) - path.replace(offset, 1 ,"/"); - - // remove double '/' - while((offset = path.find("//")) != std::string::npos) - path.erase(offset, 1); - - // remove trailing '/' - while(path.length() && ((offset = path.find_last_of('/')) == (path.length() - 1))) - path.erase(offset, 1); - - // return generic path - return path; - - } // getGenericPath - - std::string getEscapedPath(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // windows escapes stuff by just putting everything in quotes - return '"' + getPreferredPath(path) + '"'; -#else // _WIN32 - // insert a backslash before most characters that would mess up a bash path - const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; - const char* invalidChar = invalidChars; - - while(*invalidChar) - { - size_t start = 0; - size_t offset = 0; - - while((offset = path.find(*invalidChar, start)) != std::string::npos) - { - start = offset + 1; - - if((offset == 0) || (path[offset - 1] != '\\')) - { - path.insert(offset, 1, '\\'); - ++start; - } - } - - ++invalidChar; - } - - // return escaped path - return path; -#endif // _WIN32 - - } // getEscapedPath - - std::string getCanonicalPath(const std::string& _path) - { - // temporary hack for builtin resources - if((_path[0] == ':') && (_path[1] == '/')) - return _path; - - std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); - - // cleanup path - bool scan = true; - while(scan) - { - stringList pathList = getPathList(path); - - path.clear(); - scan = false; - - for(stringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); ++it) - { - // ignore empty - if((*it).empty()) - continue; - - // remove "/./" - if((*it) == ".") - continue; - - // resolve "/../" - if((*it) == "..") - { - path = getParent(path); - continue; - } - -#if defined(_WIN32) - // append folder to path - path += (path.size() == 0) ? (*it) : ("/" + (*it)); -#else // _WIN32 - // append folder to path - path += ("/" + (*it)); -#endif // _WIN32 - - // resolve symlink - if(isSymlink(path)) - { - std::string resolved = resolveSymlink(path); - - if(resolved.empty()) - return ""; - - if(isAbsolute(resolved)) - path = resolved; - else - path = getParent(path) + "/" + resolved; - - for(++it; it != pathList.cend(); ++it) - path += (path.size() == 0) ? (*it) : ("/" + (*it)); - - scan = true; - break; - } - } - } - - // return canonical path - return path; - - } // getCanonicalPath - - std::string getAbsolutePath(const std::string& _path, const std::string& _base) - { - std::string path = getGenericPath(_path); - std::string base = isAbsolute(_base) ? getGenericPath(_base) : getAbsolutePath(_base); - - // return absolute path - return isAbsolute(path) ? path : getGenericPath(base + "/" + path); - - } // getAbsolutePath - - std::string getParent(const std::string& _path) - { - std::string path = getGenericPath(_path); - size_t offset = std::string::npos; - - // find last '/' and erase it - if((offset = path.find_last_of('/')) != std::string::npos) - return path.erase(offset); - - // no parent found - return path; - - } // getParent - - std::string getFileName(const std::string& _path) - { - std::string path = getGenericPath(_path); - size_t offset = std::string::npos; - - // find last '/' and return the filename - if((offset = path.find_last_of('/')) != std::string::npos) - return ((path[offset + 1] == 0) ? "." : std::string(path, offset + 1)); - - // no '/' found, entire path is a filename - return path; - - } // getFileName - - std::string getStem(const std::string& _path) - { - std::string fileName = getFileName(_path); - size_t offset = std::string::npos; - - // empty fileName - if(fileName == ".") - return fileName; - - // find last '.' and erase the extension - if((offset = fileName.find_last_of('.')) != std::string::npos) - return fileName.erase(offset); - - // no '.' found, filename has no extension - return fileName; - - } // getStem - - std::string getExtension(const std::string& _path) - { - std::string fileName = getFileName(_path); - size_t offset = std::string::npos; - - // empty fileName - if(fileName == ".") - return fileName; - - // find last '.' and return the extension - if((offset = fileName.find_last_of('.')) != std::string::npos) - return std::string(fileName, offset); - - // no '.' found, filename has no extension - return "."; - - } // getExtension - - std::string resolveRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) - { - std::string path = getGenericPath(_path); - std::string relativeTo = isDirectory(_relativeTo) ? getGenericPath(_relativeTo) : getParent(_relativeTo); - - // nothing to resolve - if(!path.length()) - return path; - - // replace '.' with relativeTo - if((path[0] == '.') && (path[1] == '/')) - return (relativeTo + &(path[1])); - - // replace '~' with homePath - if(_allowHome && (path[0] == '~') && (path[1] == '/')) - return (getGenericPath(getHomePath()) + &(path[1])); - - // nothing to resolve - return path; - - } // resolveRelativePath - - std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) - { - if (_relativeTo.empty()) - return _path; - - if (_path == _relativeTo) - return ""; - - bool contains = false; - std::string path = removeCommonPath(_path, _relativeTo, contains); - - if(contains) - { - // success - return ("./" + path); - } - - if(_allowHome) - { - path = removeCommonPath(_path, getHomePath(), contains); - - if(contains) - { - // success - return ("~/" + path); - } - } - - // nothing to resolve - return path; - - } // createRelativePath - - std::string removeCommonPath(const std::string& _path, const std::string& _common, bool& _contains) - { - std::string path = getGenericPath(_path); - std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); - - // check if path contains common - if(path.find(common) == 0 && path != common) - { - _contains = true; - return path.substr(common.length() + 1); - } - - // it didn't - _contains = false; - return path; - - } // removeCommonPath - - std::string resolveSymlink(const std::string& _path) - { - std::string path = getGenericPath(_path); - std::string resolved; - -#if defined(_WIN32) - HANDLE hFile = CreateFile(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); - - if(hFile != INVALID_HANDLE_VALUE) - { - resolved.resize(GetFinalPathNameByHandle(hFile, nullptr, 0, FILE_NAME_NORMALIZED) + 1); - if(GetFinalPathNameByHandle(hFile, (LPSTR)resolved.data(), (DWORD)resolved.size(), FILE_NAME_NORMALIZED) > 0) - { - resolved.resize(resolved.size() - 1); - resolved = getGenericPath(resolved); - } - CloseHandle(hFile); - } -#else // _WIN32 - struct stat info; - - // check if lstat succeeded - if(lstat(path.c_str(), &info) == 0) - { - resolved.resize(info.st_size); - if(readlink(path.c_str(), (char*)resolved.data(), resolved.size()) > 0) - resolved = getGenericPath(resolved); - } -#endif // _WIN32 - - // return resolved path - return resolved; - - } // resolveSymlink - - bool removeFile(const std::string& _path) - { - std::string path = getGenericPath(_path); - - // don't remove if it doesn't exists - if(!exists(path)) - return true; - - // try to remove file - return (unlink(path.c_str()) == 0); - - } // removeFile - - bool copyFile(const std::string src, const std::string dst) - { - std::string path = getGenericPath(src); - std::string pathD = getGenericPath(dst); - - // don't remove if it doesn't exists - if (!exists(path)) - return true; - + } // getExePath + + std::string getPreferredPath(const std::string& _path) + { + std::string path = _path; + size_t offset = std::string::npos; +#if defined(_WIN32) + // convert '/' to '\\' + while((offset = path.find('/')) != std::string::npos) + path.replace(offset, 1, "\\"); +#endif // _WIN32 + return path; + } + + std::string getGenericPath(const std::string& _path) + { + std::string path = _path; + size_t offset = std::string::npos; + + // remove "\\\\?\\" + if((path.find("\\\\?\\")) == 0) + path.erase(0, 4); + + // convert '\\' to '/' + while((offset = path.find('\\')) != std::string::npos) + path.replace(offset, 1 ,"/"); + + // remove double '/' + while((offset = path.find("//")) != std::string::npos) + path.erase(offset, 1); + + // remove trailing '/' + while(path.length() && ((offset = path.find_last_of('/')) == (path.length() - 1))) + path.erase(offset, 1); + + // return generic path + return path; + + } // getGenericPath + + std::string getEscapedPath(const std::string& _path) + { + std::string path = getGenericPath(_path); + +#if defined(_WIN32) + // windows escapes stuff by just putting everything in quotes + return '"' + getPreferredPath(path) + '"'; +#else // _WIN32 + // insert a backslash before most characters that would mess up a bash path + const char* invalidChars = "\\ '\"!$^&*(){}[]?;<>"; + const char* invalidChar = invalidChars; + + while(*invalidChar) + { + size_t start = 0; + size_t offset = 0; + + while((offset = path.find(*invalidChar, start)) != std::string::npos) + { + start = offset + 1; + + if((offset == 0) || (path[offset - 1] != '\\')) + { + path.insert(offset, 1, '\\'); + ++start; + } + } + + ++invalidChar; + } + + // return escaped path + return path; +#endif // _WIN32 + + } // getEscapedPath + + std::string getCanonicalPath(const std::string& _path) + { + // temporary hack for builtin resources + if(_path.size() >= 2 && _path[0] == ':' && _path[1] == '/') + return _path; + + std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); + + // cleanup path + bool scan = true; + while(scan) + { + stringList pathList = getPathList(path); + + path.clear(); + scan = false; + + for(stringList::const_iterator it = pathList.cbegin(); it != pathList.cend(); ++it) + { + // ignore empty + if((*it).empty()) + continue; + + // remove "/./" + if((*it) == ".") + continue; + + // resolve "/../" + if((*it) == "..") + { + path = getParent(path); + continue; + } + +#if defined(_WIN32) + // append folder to path + path += (path.size() == 0) ? (*it) : ("/" + (*it)); +#else // _WIN32 + // append folder to path + path += ("/" + (*it)); +#endif // _WIN32 + + // resolve symlink + if(isSymlink(path)) + { + std::string resolved = resolveSymlink(path); + + if(resolved.empty()) + return ""; + + if(isAbsolute(resolved)) + path = resolved; + else + path = getParent(path) + "/" + resolved; + + for(++it; it != pathList.cend(); ++it) + path += (path.size() == 0) ? (*it) : ("/" + (*it)); + + scan = true; + break; + } + } + } + + // return canonical path + return path; + + } // getCanonicalPath + + std::string getAbsolutePath(const std::string& _path, const std::string& _base) + { + if (_path.empty() || isAbsolute(_path)) + return getGenericPath(_path); + + return getCanonicalPath(_base + "/" + _path); + } // getAbsolutePath + + std::string getParent(const std::string& _path) + { + std::string path = getGenericPath(_path); + size_t offset = std::string::npos; + + // find last '/' and erase it + if((offset = path.find_last_of('/')) != std::string::npos) + return path.erase(offset); + + // no parent found + return path; + + } // getParent + + std::string getFileName(const std::string& _path) + { + std::string path = getGenericPath(_path); + size_t offset = std::string::npos; + + // find last '/' and return the filename + if((offset = path.find_last_of('/')) != std::string::npos) + return ((path[offset + 1] == 0) ? "." : std::string(path, offset + 1)); + + // no '/' found, entire path is a filename + return path; + + } // getFileName + + std::string getStem(const std::string& _path) + { + std::string fileName = getFileName(_path); + size_t offset = std::string::npos; + + // empty fileName + if(fileName == ".") + return fileName; + + // find last '.' and erase the extension + if((offset = fileName.find_last_of('.')) != std::string::npos) + return fileName.erase(offset); + + // no '.' found, filename has no extension + return fileName; + + } // getStem + + std::string getExtension(const std::string& _path) + { + std::string fileName = getFileName(_path); + size_t offset = std::string::npos; + + // empty fileName + if(fileName == ".") + return fileName; + + // find last '.' and return the extension + if((offset = fileName.find_last_of('.')) != std::string::npos) + return std::string(fileName, offset); + + // no '.' found, filename has no extension + return "."; + + } // getExtension + + std::string resolveRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) + { + // nothing to resolve + if(!_path.length()) + return _path; + + // replace '.' with relativeTo + if((_path[0] == '.') && (_path[1] == '/')) + return getGenericPath(_relativeTo + &(_path[1])); + + // replace '~' with homePath + if(_allowHome && (_path[0] == '~') && (_path[1] == '/')) + return getCanonicalPath(getHomePath() + &(_path[1])); + + // nothing to resolve + return getGenericPath(_path); + + } // resolveRelativePath + + std::string createRelativePath(const std::string& _path, const std::string& _relativeTo, const bool _allowHome) + { + if (_relativeTo.empty()) + return _path; + + if (_path == _relativeTo) + return ""; + + bool contains = false; + std::string path = removeCommonPath(_path, _relativeTo, contains); + + if(contains) + { + // success + return ("./" + path); + } + + if(_allowHome) + { + path = removeCommonPath(_path, getHomePath(), contains); + + if(contains) + { + // success + return ("~/" + path); + } + } + + // nothing to resolve + return path; + + } // createRelativePath + + std::string removeCommonPath(const std::string& _path, const std::string& _common, bool& _contains) + { + std::string path = _path; // getGenericPath(_path); + //std::string common = isDirectory(_common) ? getGenericPath(_common) : getParent(_common); + + // check if path contains common + if(path.find(_common) == 0 && path != _common) + { + _contains = true; + int trailingSlash = _common.find_last_of('/') == (_common.length() - 1) ? 0 : 1; + return path.substr(_common.length() + trailingSlash); + } + + // it didn't + _contains = false; + return path; + + } // removeCommonPath + + std::string resolveSymlink(const std::string& _path) + { + std::string path = getGenericPath(_path); + std::string resolved; + +#if defined(_WIN32) + HANDLE hFile = CreateFile(path.c_str(), FILE_READ_ATTRIBUTES, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, 0); + + if(hFile != INVALID_HANDLE_VALUE) + { + resolved.resize(GetFinalPathNameByHandle(hFile, nullptr, 0, FILE_NAME_NORMALIZED) + 1); + if(GetFinalPathNameByHandle(hFile, (LPSTR)resolved.data(), (DWORD)resolved.size(), FILE_NAME_NORMALIZED) > 0) + { + resolved.resize(resolved.size() - 1); + resolved = getGenericPath(resolved); + } + CloseHandle(hFile); + } +#else // _WIN32 + struct stat info; + + // check if lstat succeeded + if(lstat(path.c_str(), &info) == 0) + { + resolved.resize(info.st_size); + if(readlink(path.c_str(), (char*)resolved.data(), resolved.size()) > 0) + resolved = getGenericPath(resolved); + } +#endif // _WIN32 + + // return resolved path + return resolved; + + } // resolveSymlink + + bool removeFile(const std::string& _path) + { + std::string path = getGenericPath(_path); + + // don't remove if it doesn't exists + if(!exists(path)) + return true; + + // try to remove file + return (unlink(path.c_str()) == 0); + + } // removeFile + + bool copyFile(const std::string src, const std::string dst) + { + std::string path = getGenericPath(src); + std::string pathD = getGenericPath(dst); + + // don't remove if it doesn't exists + if (!exists(path)) + return true; + char buf[512]; size_t size; @@ -697,213 +801,251 @@ namespace Utils fclose(dest); fclose(source); - - return true; - } // removeFile - - bool createDirectory(const std::string& _path) - { - std::string path = getGenericPath(_path); - - // don't create if it already exists - if(exists(path)) - return true; - - // try to create directory - if(mkdir(path.c_str(), 0755) == 0) - return true; - - // failed to create directory, try to create the parent - std::string parent = getParent(path); - - // only try to create parent if it's not identical to path - if(parent != path) - createDirectory(parent); - - // try to create directory again now that the parent should exist - return (mkdir(path.c_str(), 0755) == 0); - - } // createDirectory - - bool exists(const std::string& _path) - { - if (_path.empty()) - return false; - -#ifdef WIN32 - DWORD dwAttr = GetFileAttributes(_path.c_str()); - if (0xFFFFFFFF == dwAttr) - return false; - - return true; -#else - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - return (stat64(path.c_str(), &info) == 0); -#endif - } // exists - - size_t getFileSize(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - if ((stat64(path.c_str(), &info) == 0)) - return (size_t) info.st_size; - - return 0; - } - - bool isAbsolute(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - return ((path.size() > 1) && (path[1] == ':')); -#else // _WIN32 - return ((path.size() > 0) && (path[0] == '/')); -#endif // _WIN32 - - } // isAbsolute - - bool isRegularFile(const std::string& _path) - { - std::string path = getGenericPath(_path); - struct stat64 info; - - // check if stat64 succeeded - if(stat64(path.c_str(), &info) != 0) - return false; - - // check for S_IFREG attribute - return (S_ISREG(info.st_mode)); - - } // isRegularFile - - bool isDirectory(const std::string& _path) - { -#ifdef WIN32 - DWORD dwAttr = GetFileAttributes(_path.c_str()); - if (0xFFFFFFFF == dwAttr) - return false; - - return (dwAttr & FILE_ATTRIBUTE_DIRECTORY); -#else - - std::string path = getGenericPath(_path); - struct stat info; - - // check if stat succeeded - if(stat(path.c_str(), &info) != 0) - return false; - - // check for S_IFDIR attribute - return (S_ISDIR(info.st_mode)); -#endif - } // isDirectory - - bool isSymlink(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // check for symlink attribute - const DWORD Attributes = GetFileAttributes(path.c_str()); - if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT)) - return true; -#else // _WIN32 - struct stat info; - - // check if lstat succeeded - if(lstat(path.c_str(), &info) != 0) - return false; - - // check for S_IFLNK attribute - return (S_ISLNK(info.st_mode)); -#endif // _WIN32 - - // not a symlink - return false; - - } // isSymlink - - bool isHidden(const std::string& _path) - { - std::string path = getGenericPath(_path); - -#if defined(_WIN32) - // check for hidden attribute - const DWORD Attributes = GetFileAttributes(path.c_str()); - if((Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_HIDDEN)) - return true; -#endif // _WIN32 - - // filenames starting with . are hidden in linux, we do this check for windows as well - if (getFileName(path)[0] == '.') - return true; - - // not hidden - return false; - - } // isHidden - - - std::string combine(const std::string& _path, const std::string& filename) - { - std::string gp = getGenericPath(_path); - - if (Utils::String::startsWith(filename, "/..")) - { - auto f = getPathList(filename); - - int count = 0; - for (auto it = f.cbegin(); it != f.cend(); ++it) - { - if (*it != "..") - break; - - count++; - } - - if (count > 0) - { - auto list = getPathList(gp); - std::vector p(list.begin(), list.end()); - - std::string result; - - for (int i = 0; i < p.size() - count; i++) - { - if (result.empty()) - result = p.at(i); - else - result = result + "/" + p.at(i); - } - - std::vector fn(f.begin(), f.end()); - for (int i = count; i < fn.size(); i++) - { - if (result.empty()) - result = fn.at(i); - else - result = result + "/" + fn.at(i); - } - - return result; - } - } - - - if (!Utils::String::endsWith(gp, "/") && !Utils::String::endsWith(gp, "\\")) - if (!Utils::String::startsWith(filename, "/") && !Utils::String::startsWith(filename, "\\")) - gp += "/"; - - return gp + filename; - } - - } // FileSystem:: - -} // Utils:: + + return true; + } // removeFile + + bool createDirectory(const std::string& _path) + { + FileCache::resetCache(); + + std::string path = getGenericPath(_path); + + // don't create if it already exists + if(exists(path)) + return true; + + // try to create directory + if(mkdir(path.c_str(), 0755) == 0) + return true; + + // failed to create directory, try to create the parent + std::string parent = getParent(path); + + // only try to create parent if it's not identical to path + if(parent != path) + createDirectory(parent); + + // try to create directory again now that the parent should exist + return (mkdir(path.c_str(), 0755) == 0); + + } // createDirectory + + bool exists(const std::string& _path) + { + if (_path.empty()) + return false; + + auto it = FileCache::get(_path); + if (it != nullptr) + return it->exists; + +#ifdef WIN32 + DWORD dwAttr = GetFileAttributes(_path.c_str()); + FileCache::add(_path, FileCache(dwAttr)); + if (0xFFFFFFFF == dwAttr) + return false; + + return true; +#else + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + return FileCache::fromStat64(path, &info) == 0; +#endif + } // exists + + size_t getFileSize(const std::string& _path) + { + if (!exists(_path)) + return 0; + + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + if ((stat64(path.c_str(), &info) == 0)) + return (size_t) info.st_size; + + return 0; + } + + bool isAbsolute(const std::string& _path) + { + if (_path.size() >= 2 && _path[0] == ':' && _path[1] == '/') + return true; + + std::string path = getGenericPath(_path); + +#ifdef WIN32 + return ((path.size() > 1) && (path[1] == ':')); +#else // _WIN32 + return ((path.size() > 0) && (path[0] == '/')); +#endif // _WIN32 + + } // isAbsolute + + bool isRegularFile(const std::string& _path) + { + auto it = FileCache::get(_path); + if (it != nullptr) + return it->exists && !it->directory && !it->isSymLink; + + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat64 succeeded + if (FileCache::fromStat64(path, &info) != 0) //if(stat64(path.c_str(), &info) != 0) + return false; + + // check for S_IFREG attribute + return (S_ISREG(info.st_mode)); + + } // isRegularFile + + bool isDirectory(const std::string& _path) + { + auto it = FileCache::get(_path); + if (it != nullptr) + return it->exists && it->directory; + +#ifdef WIN32 + DWORD dwAttr = GetFileAttributes(_path.c_str()); + FileCache::add(_path, FileCache(dwAttr)); + return (dwAttr != INVALID_FILE_ATTRIBUTES) && (dwAttr & FILE_ATTRIBUTE_DIRECTORY); +#else + std::string path = getGenericPath(_path); + struct stat64 info; + + // check if stat succeeded + if (FileCache::fromStat64(path, &info) != 0) //if(stat64(path.c_str(), &info) != 0) + return false; + + // check for S_IFDIR attribute + return (S_ISDIR(info.st_mode)); +#endif + } // isDirectory + + bool isSymlink(const std::string& _path) + { + auto it = FileCache::get(_path); + if (it != nullptr) + return it->exists && it->isSymLink; + + std::string path = getGenericPath(_path); + +#ifdef WIN32 + // check for symlink attribute + DWORD Attributes = GetFileAttributes(path.c_str()); + FileCache::add(_path, FileCache(Attributes)); + return (Attributes != INVALID_FILE_ATTRIBUTES) && (Attributes & FILE_ATTRIBUTE_REPARSE_POINT); +#else // WIN32 + struct stat64 info; + + // check if lstat succeeded + if (FileCache::fromStat64(path, &info) != 0) //if(stat64(path.c_str(), &info) != 0) + return false; + + // check for S_IFLNK attribute + return (S_ISLNK(info.st_mode)); +#endif //_WIN32 + + } // isSymlink + + bool isHidden(const std::string& _path) + { + auto it = FileCache::get(_path); + if (it != nullptr) + return it->exists && it->hidden; + + std::string path = getGenericPath(_path); + +#ifdef WIN32 + // check for hidden attribute + DWORD Attributes = GetFileAttributes(path.c_str()); + FileCache::add(_path, FileCache(Attributes)); + return (Attributes != INVALID_FILE_ATTRIBUTES && Attributes & FILE_ATTRIBUTE_HIDDEN); +#endif // _WIN32 + + // filenames starting with . are hidden in linux, we do not do this check for windows as well + if (getFileName(path)[0] == '.') + return true; + + // not hidden + return false; + + } // isHidden + + + std::string combine(const std::string& _path, const std::string& filename) + { + std::string gp = getGenericPath(_path); + + if (Utils::String::startsWith(filename, "/..")) + { + auto f = getPathList(filename); + + int count = 0; + for (auto it = f.cbegin(); it != f.cend(); ++it) + { + if (*it != "..") + break; + + count++; + } + + if (count > 0) + { + auto list = getPathList(gp); + std::vector p(list.begin(), list.end()); + + std::string result; + + for (int i = 0; i < p.size() - count; i++) + { + if (result.empty()) + result = p.at(i); + else + result = result + "/" + p.at(i); + } + + std::vector fn(f.begin(), f.end()); + for (int i = count; i < fn.size(); i++) + { + if (result.empty()) + result = fn.at(i); + else + result = result + "/" + fn.at(i); + } + + return result; + } + } + + + if (!Utils::String::endsWith(gp, "/") && !Utils::String::endsWith(gp, "\\")) + if (!Utils::String::startsWith(filename, "/") && !Utils::String::startsWith(filename, "\\")) + gp += "/"; + + return gp + filename; + } + + std::string readAllText(const std::string fileName) + { + std::ifstream t(fileName); + std::stringstream buffer; + buffer << t.rdbuf(); + return buffer.str(); + } + + void writeAllText(const std::string fileName, const std::string text) + { + std::fstream fs; + fs.open(fileName.c_str(), std::fstream::out); + fs << text; + fs.close(); + } + } // FileSystem:: + +} // Utils:: diff --git a/es-core/src/utils/FileSystemUtil.h b/es-core/src/utils/FileSystemUtil.h index c222571eba..9d3e88f825 100644 --- a/es-core/src/utils/FileSystemUtil.h +++ b/es-core/src/utils/FileSystemUtil.h @@ -58,6 +58,17 @@ namespace Utils std::string readAllText(const std::string fileName); void writeAllText (const std::string fileName, const std::string text); bool copyFile(const std::string src, const std::string dst); + + class FileSystemCacheActivator + { + public: + FileSystemCacheActivator(); + ~FileSystemCacheActivator(); + + private: + static int mReferenceCount; + }; + } // FileSystem:: diff --git a/es-core/src/utils/StringUtil.cpp b/es-core/src/utils/StringUtil.cpp index 069692ee5a..df90c1c9be 100644 --- a/es-core/src/utils/StringUtil.cpp +++ b/es-core/src/utils/StringUtil.cpp @@ -328,6 +328,8 @@ namespace Utils return output; } + + #if defined(_WIN32) const std::string convertFromWideString(const std::wstring wstring) { @@ -352,6 +354,24 @@ namespace Utils } #endif + std::vector splitAny(const std::string& s, const std::string& seperator) + { + std::vector output; + + char* str = new char[s.length() + 1]; + std::strcpy(str, s.c_str()); + + char* pch = strtok(str, seperator.c_str()); + while (pch != NULL) + { + output.push_back(pch); + pch = strtok(NULL, seperator.c_str()); + } + + delete str; + + return output; + } } // String:: } // Utils:: diff --git a/es-core/src/utils/StringUtil.h b/es-core/src/utils/StringUtil.h index 37b46f0045..79159b68ba 100644 --- a/es-core/src/utils/StringUtil.h +++ b/es-core/src/utils/StringUtil.h @@ -28,6 +28,7 @@ namespace Utils std::string format (const char* _string, ...); std::string scramble (const std::string& _input, const std::string& key); std::vector split (const std::string& s, char seperator); + std::vector splitAny(const std::string& s, const std::string& seperator); #if defined(_WIN32) const std::string convertFromWideString(const std::wstring wstring); diff --git a/es-core/src/utils/ThreadPool.cpp b/es-core/src/utils/ThreadPool.cpp index 3108cb62ab..bc1f35e054 100644 --- a/es-core/src/utils/ThreadPool.cpp +++ b/es-core/src/utils/ThreadPool.cpp @@ -8,7 +8,7 @@ namespace Utils { ThreadPool::ThreadPool() : mRunning(true), mWaiting(false), mNumWork(0) { - size_t num_threads = std::thread::hardware_concurrency() - 1; + size_t num_threads = std::thread::hardware_concurrency() * 2; auto doWork = [&](size_t id) { From 8766ab97223284d0b7f0d2196f1b331b700c9e1d Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 21 Nov 2019 19:36:54 +0100 Subject: [PATCH 235/336] Created "UPDATES & DOWNLOADS" menu + Added Theme downloader --- es-app/CMakeLists.txt | 2 + es-app/src/ApiSystem.cpp | 16 ++-- es-app/src/guis/GuiMenu.cpp | 87 ++++++++++++++++++++-- es-app/src/guis/GuiMenu.h | 1 + es-app/src/guis/GuiThemeInstall.cpp | 93 ++++++++++++++++++++++++ es-app/src/guis/GuiThemeInstall.h | 55 ++++++++++++++ resources/locale/fr/emulationstation2.po | 64 ++++++++++++++++ 7 files changed, 306 insertions(+), 12 deletions(-) create mode 100644 es-app/src/guis/GuiThemeInstall.cpp create mode 100644 es-app/src/guis/GuiThemeInstall.h diff --git a/es-app/CMakeLists.txt b/es-app/CMakeLists.txt index d22aef62ae..fece7e1532 100644 --- a/es-app/CMakeLists.txt +++ b/es-app/CMakeLists.txt @@ -37,6 +37,7 @@ set(ES_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiThemeInstall.h ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.h # Scrapers @@ -99,6 +100,7 @@ set(ES_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperMulti.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiScraperStart.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiGamelistFilter.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiThemeInstall.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiCollectionSystemsOptions.cpp # Scrapers diff --git a/es-app/src/ApiSystem.cpp b/es-app/src/ApiSystem.cpp index ec002ef0d8..ef933be768 100644 --- a/es-app/src/ApiSystem.cpp +++ b/es-app/src/ApiSystem.cpp @@ -317,7 +317,9 @@ std::vector ApiSystem::getThemesList() if (parts.size() > 1) { auto themeName = parts[0]; - std::string themeUrl = Utils::FileSystem::getFileName(parts[1]); + + std::string themeUrl = parts[1]; + std::string themeFolder = Utils::FileSystem::getFileName(themeUrl); bool themeExists = false; @@ -329,17 +331,17 @@ std::vector ApiSystem::getThemesList() for (auto path : paths) { - if (Utils::FileSystem::isDirectory(path + "/" + themeUrl + "-master")) + if (Utils::FileSystem::isDirectory(path + "/" + themeFolder + "-master")) { themeExists = true; break; } - else if (Utils::FileSystem::isDirectory(path + "/" + themeUrl)) + else if (Utils::FileSystem::isDirectory(path + "/" + themeFolder)) { themeExists = true; break; } - else if (Utils::FileSystem::isDirectory(path + "/" + themeName)) + else if (Utils::FileSystem::isDirectory(path + "/" + themeFolder)) { themeExists = true; break; @@ -362,7 +364,7 @@ std::vector ApiSystem::getThemesList() std::shared_ptr downloadGitRepository(const std::string url, const std::string label, const std::function& func) { if (func != nullptr) - func("Downloading " + label); + func(_("Downloading") + " " + label); long downloadSize = 0; @@ -400,7 +402,7 @@ std::shared_ptr downloadGitRepository(const std::string url, const std: if (func != nullptr) { std::string pc = std::to_string((int)(pos * 100.0 / downloadSize)); - func(std::string("Downloading " + label + " >>> " + pc + " %")); + func(std::string(_("Downloading") + " " + label + " >>> " + pc + " %")); } curPos = pos; @@ -428,7 +430,7 @@ std::pair ApiSystem::installTheme(std::string themeName, const if (httpreq != nullptr && httpreq->status() == HttpReq::REQ_SUCCESS) { if (func != nullptr) - func("Extracting " + themeName); + func(_("Extracting") + " " + themeName); std::string themeFileName = Utils::FileSystem::getFileName(theme.url); std::string zipFile = Utils::FileSystem::getHomePath() + "/.emulationstation/themes/" + themeFileName + ".zip"; diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 9dfdfb2be3..78f8cb1565 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -21,6 +21,7 @@ #include "AudioManager.h" #include "resources/TextureData.h" #include "animations/LambdaAnimation.h" +#include "guis/GuiThemeInstall.h" #include "GuiGamelistOptions.h" // grid sizes #include "platform.h" #include "renderers/Renderer.h" // setSwapInterval() @@ -46,7 +47,23 @@ GuiMenu::GuiMenu(Window* window, bool animate) : GuiComponent(window), mMenu(win if (isFullUI) { addEntry(_("GAME COLLECTION SETTINGS"), true, [this] { openCollectionSystemSettings(); }, "iconGames"); + + // Emulator settings + for (auto system : SystemData::sSystemVector) + { + if (system->isCollection() || system->getSystemEnvData()->mEmulators.size() == 0 || (system->getSystemEnvData()->mEmulators.size() == 1 && system->getSystemEnvData()->mEmulators[0].mCores.size() <= 1)) + continue; + + addEntry(_("EMULATOR SETTINGS"), true, [this] { openEmulatorSettings(); }, "iconSystem"); + break; + } + addEntry(_("SCRAPER"), true, [this] { openScraperSettings(); }, "iconScraper"); + +#if WIN32 + addEntry(_("DOWNLOADS AND UPDATES"), true, [this] { openUpdateSettings(); }, "iconUpdates"); +#endif + addEntry(_("ADVANCED SETTINGS"), true, [this] { openOtherSettings(); }, "iconAdvanced"); } @@ -994,22 +1011,82 @@ void GuiMenu::openEmulatorSettings() window->pushGui(configuration); } +void GuiMenu::openUpdateSettings() +{ + Window* window = mWindow; + auto s = new GuiSettings(mWindow, _("DOWNLOADS AND UPDATES")); + + // themes installer/browser + s->addEntry(_("THEME INSTALLER"), true, [this] + { + mWindow->pushGui(new GuiThemeInstall(mWindow)); + }); + + // Enable updates + auto updates_enabled = std::make_shared(mWindow); + updates_enabled->setState(Settings::getInstance()->getBool("updates.enabled")); + s->addWithLabel(_("AUTO UPDATES"), updates_enabled); + s->addSaveFunc([updates_enabled] + { + Settings::getInstance()->setBool("updates.enabled", updates_enabled->getState()); + }); + + // Start update + s->addEntry(ApiSystem::state == UpdateState::State::UPDATE_READY ? _("APPLY UPDATE") : _("START UPDATE"), true, [this, s] + { + if (ApiSystem::checkUpdateVersion().empty()) + { + mWindow->pushGui(new GuiMsgBox(mWindow, _("NO UPDATE AVAILABLE"))); + return; + } + + if (ApiSystem::state == UpdateState::State::UPDATE_READY) + { + if (quitES(QuitMode::QUIT)) + LOG(LogWarning) << "Reboot terminated with non-zero result!"; + } + else if (ApiSystem::state == UpdateState::State::UPDATER_RUNNING) + mWindow->pushGui(new GuiMsgBox(mWindow, _("UPDATE IS ALREADY RUNNING"))); + else + { + ApiSystem::startUpdate(mWindow); + + s->setVariable("closeGuiMenu", true); + s->close(); + } + }); + + s->updatePosition(); + + auto pthis = this; + + s->onFinalize([s, pthis, window] + { + if (s->getVariable("closeGuiMenu")) + delete pthis; + }); + + mWindow->pushGui(s); + +} + + void GuiMenu::openOtherSettings() { Window* window = mWindow; auto s = new GuiSettings(mWindow, _("ADVANCED SETTINGS")); - + /* // Emulator settings for (auto system : SystemData::sSystemVector) { if (system->isCollection() || system->getSystemEnvData()->mEmulators.size() == 0 || (system->getSystemEnvData()->mEmulators.size() == 1 && system->getSystemEnvData()->mEmulators[0].mCores.size() <= 1)) continue; - s->addEntry(_("EMULATOR SETTINGS"), true, [this] { openEmulatorSettings(); }); + s->addEntry(_("EMULATOR SETTINGS"), true, [this] { openEmulatorSettings(); }, "iconGames"); break; } - + */ // power saver auto power_saver = std::make_shared< OptionListComponent >(mWindow, _("POWER SAVER MODES"), false); std::vector modes; @@ -1101,7 +1178,7 @@ void GuiMenu::openOtherSettings() s->addSaveFunc([max_vram] { Settings::getInstance()->setInt("MaxVRAM", (int)Math::round(max_vram->getValue())); }); - + /* #if WIN32 // Enable updates @@ -1133,7 +1210,7 @@ void GuiMenu::openOtherSettings() ApiSystem::startUpdate(mWindow); }); #endif - +*/ diff --git a/es-app/src/guis/GuiMenu.h b/es-app/src/guis/GuiMenu.h index 0d49b5db1a..462bf66001 100644 --- a/es-app/src/guis/GuiMenu.h +++ b/es-app/src/guis/GuiMenu.h @@ -31,6 +31,7 @@ class GuiMenu : public GuiComponent void openScreensaverOptions(); void openSoundSettings(); void openUISettings(); + void openUpdateSettings(); void openThemeConfiguration(GuiSettings* s, std::shared_ptr> theme_set); void openEmulatorSettings(); void openSystemEmulatorSettings(SystemData* system); diff --git a/es-app/src/guis/GuiThemeInstall.cpp b/es-app/src/guis/GuiThemeInstall.cpp new file mode 100644 index 0000000000..9aa1808b70 --- /dev/null +++ b/es-app/src/guis/GuiThemeInstall.cpp @@ -0,0 +1,93 @@ +#include "guis/GuiThemeInstall.h" + +#include "ApiSystem.h" +#include "components/OptionListComponent.h" +#include "guis/GuiSettings.h" +#include "views/ViewController.h" +#include "utils/StringUtil.h" +#include "components/ComponentGrid.h" +#include "EsLocale.h" +#include "ContentInstaller.h" + +GuiThemeInstall::GuiThemeInstall(Window* window) + : GuiComponent(window), mMenu(window, _("SELECT THEME TO INSTALL").c_str()) +{ + auto theme = ThemeData::getMenuTheme(); + + addChild(&mMenu); + + auto themes = ApiSystem::getThemesList(); + for(auto utheme : themes) + { + std::string themeName = utheme.name; + + ComponentListRow row; + + // icon + auto icon = std::make_shared(mWindow); + icon->setImage(utheme.installed ? ":/star_filled.svg" : ":/star_unfilled.svg"); + icon->setColorShift(theme->Text.color); + icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); + row.addElement(icon, false); + + // spacer between icon and text + auto spacer = std::make_shared(mWindow); + spacer->setSize(10, 0); + row.addElement(spacer, false); + + auto grid = std::make_shared(window, utheme.name, utheme.url); + row.addElement(grid, true); + row.makeAcceptInputHandler([this, themeName] { start(themeName); }); + + mMenu.addRow(row); + } + + mMenu.addButton(_("BACK"), "back", [&] { delete this; }); + + if (Renderer::isSmallScreen()) + mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); + else + mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, Renderer::getScreenHeight() * 0.15f); +} + +void GuiThemeInstall::start(std::string themeName) +{ + if (themeName.empty()) + return; + + char trstring[256]; + snprintf(trstring, 256, _("'%s' ADDED TO DOWNLOAD QUEUE").c_str(), themeName.c_str()); // batocera + mWindow->displayNotificationMessage(_U("\uF019 ") + std::string(trstring)); + + ContentInstaller::Enqueue(mWindow, ContentInstaller::CONTENT_THEME, themeName); + delete this; +} + +bool GuiThemeInstall::input(InputConfig* config, Input input) +{ + if(GuiComponent::input(config, input)) + 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 GuiThemeInstall::getHelpPrompts() +{ + std::vector prompts = mMenu.getHelpPrompts(); + prompts.push_back(HelpPrompt("b", _("BACK"))); + prompts.push_back(HelpPrompt("start", _("CLOSE"))); + return prompts; +} diff --git a/es-app/src/guis/GuiThemeInstall.h b/es-app/src/guis/GuiThemeInstall.h new file mode 100644 index 0000000000..a67f549879 --- /dev/null +++ b/es-app/src/guis/GuiThemeInstall.h @@ -0,0 +1,55 @@ +#pragma once + +#include +#include "GuiComponent.h" +#include "components/MenuComponent.h" +#include "components/ComponentGrid.h" +#include "components/TextComponent.h" + +template +class OptionListComponent; + +// Batocera +class GuiThemeInstall : public GuiComponent +{ +public: + GuiThemeInstall(Window* window); + bool input(InputConfig* config, Input input) override; + + virtual std::vector getHelpPrompts() override; + +private: + void start(std::string themeName); + + MenuComponent mMenu; +}; + + +class MultiLineMenuEntry : public ComponentGrid +{ +public: + MultiLineMenuEntry(Window* window, const std::string& text, const std::string& substring) : + ComponentGrid(window, Vector2i(1, 2)) + { + auto theme = ThemeData::getMenuTheme(); + + mText = std::make_shared(mWindow, text.c_str(), theme->Text.font, theme->Text.color); + mText->setVerticalAlignment(ALIGN_TOP); + + mSubstring = std::make_shared(mWindow, substring.c_str(), theme->TextSmall.font, theme->Text.color); + + setEntry(mText, Vector2i(0, 0), true, true); + setEntry(mSubstring, Vector2i(0, 1), false, true); + + setSize(Vector2f(0, mText->getSize().y() + mSubstring->getSize().y())); + } + + virtual void setColor(unsigned int color) + { + mText->setColor(color); + mSubstring->setColor(color); + } + + std::shared_ptr mText; + std::shared_ptr mSubstring; +}; diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index aca89fc6c3..6e5f324492 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -36,6 +36,18 @@ msgstr "SOURCE D'IMAGE PREFERÉE" msgid "PREFERED THUMBNAIL SOURCE" msgstr "SOURCE DE MINIATURE PRÉFÉRÉE" +#: +msgid "SELECT THEME TO INSTALL" +msgstr "CHOIX DU THEME A INSTALLER" + +#: +msgid "'%s' ADDED TO DOWNLOAD QUEUE" +msgstr "'%s' SERA PROCHAINEMENT TELÉCHARGÉ" + +#: +msgid "'%s' ADDED TO DOWNLOAD QUEUE" +msgstr "'%s' SERA PROCHAINEMENT TELÉCHARGÉ" + #: msgid "SCRAPE MARQUEE" msgstr "SCRAPER LES LOGOS DE JEU" @@ -62,6 +74,22 @@ msgstr "CONTRÔLE DE l'ÉCONOMISEUR D'ÉCRAN" msgid "All Games" msgstr "Tous les jeux" +#: +msgid "DOWNLOADING" +msgstr "TÉLÉCHARGEMENT" + +#: +msgid "THEME INSTALLED SUCCESSFULLY" +msgstr "THEME INSTALLÉ" + +#: +msgid "Downloading" +msgstr "Telechargement" + +#: +msgid "Extracting" +msgstr "Extraction" + #: msgid "Only missing medias" msgstr "Seulement les médias manquants" @@ -1045,6 +1073,42 @@ msgstr "MISE À JOUR TÉLÉCHARGÉE, REDÉMARRAGE EN COURS" msgid "UPDATE FAILED, THE SYSTEM WILL NOW REBOOT" msgstr "ÉCHEC DE LA MISE À JOUR" +#: +msgid "START UPDATE" +msgstr "TELÉCHARGER LA MISE À JOUR" + +#: +msgid "APPLY UPDATE" +msgstr "APPLIQUER LA MISE À JOUR" + +#: +msgid "DOWNLOADS AND UPDATES" +msgstr "TELÉCHARGEMENTS ET MISES À JOUR" + +#: +msgid "THEME INSTALLER" +msgstr "TELÉCHARGER DES THEMES" + +#: +msgid "IMAGE SOURCE" +msgstr "TYPE D'IMAGE" + +#: +msgid "BOX SOURCE" +msgstr "TYPE DE BOITE" + +#: +msgid "LOGO SOURCE" +msgstr "TYPE DE LOGO" + +#: +msgid "SHOW FILENAMES IN LISTS" +msgstr "AFFICHER LES NOMS DE FICHIER" + +#: +msgid "SHOW CLOCK" +msgstr "AFFICHER L'HEURE" + #: msgid "NO UPDATE AVAILABLE" msgstr "AUCUNE MISE À JOUR DISPONIBLE" From 6a1a602712ab5d439fe0d57e44cec90b8385902b Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 22 Nov 2019 17:50:48 +0100 Subject: [PATCH 236/336] Theme installer : better UI MenuComponent : Support for subtitle --- es-app/src/ApiSystem.cpp | 46 ++++++++-------- es-app/src/guis/GuiMenu.cpp | 3 +- es-app/src/guis/GuiSettings.h | 3 ++ es-app/src/guis/GuiThemeInstall.cpp | 15 ++++++ es-app/src/guis/GuiThemeInstall.h | 1 + es-core/src/components/MenuComponent.cpp | 68 +++++++++++++++++++++--- es-core/src/components/MenuComponent.h | 22 +++++++- 7 files changed, 126 insertions(+), 32 deletions(-) diff --git a/es-app/src/ApiSystem.cpp b/es-app/src/ApiSystem.cpp index ef933be768..0d8d2ea9b0 100644 --- a/es-app/src/ApiSystem.cpp +++ b/es-app/src/ApiSystem.cpp @@ -331,21 +331,12 @@ std::vector ApiSystem::getThemesList() for (auto path : paths) { - if (Utils::FileSystem::isDirectory(path + "/" + themeFolder + "-master")) - { - themeExists = true; + themeExists = Utils::FileSystem::isDirectory(path + "/" + themeName) || + Utils::FileSystem::isDirectory(path + "/" + themeFolder) || + Utils::FileSystem::isDirectory(path + "/" + themeFolder + "-master"); + + if (themeExists) break; - } - else if (Utils::FileSystem::isDirectory(path + "/" + themeFolder)) - { - themeExists = true; - break; - } - else if (Utils::FileSystem::isDirectory(path + "/" + themeFolder)) - { - themeExists = true; - break; - } } ThemeDownloadInfo info; @@ -437,21 +428,32 @@ std::pair ApiSystem::installTheme(std::string themeName, const zipFile = Utils::String::replace(zipFile, "/", "\\"); httpreq->saveContent(zipFile); - unzipFile(zipFile, Utils::String::replace(Utils::FileSystem::getHomePath() + "/.emulationstation/themes", "/", "\\")); + if (!unzipFile(zipFile, Utils::String::replace(Utils::FileSystem::getHomePath() + "/.emulationstation/themes", "/", "\\"))) + return std::pair(std::string("An error occured while extracting"), 1); std::string folderName = Utils::FileSystem::getHomePath() + "/.emulationstation/themes/" + themeFileName + "-master"; - std::string finalfolderName = Utils::String::replace(folderName, "-master", ""); + if (Utils::FileSystem::isDirectory(folderName)) + { + std::string finalfolderName = Utils::FileSystem::getParent(folderName) + "/" + themeName; + + if (Utils::FileSystem::isDirectory(finalfolderName)) + { + deleteDirectoryFiles(finalfolderName); + } - rename(folderName.c_str(), finalfolderName.c_str()); + rename(folderName.c_str(), finalfolderName.c_str()); - Utils::FileSystem::removeFile(zipFile); + Utils::FileSystem::removeFile(zipFile); - return std::pair(std::string("OK"), 0); - } + return std::pair(std::string("OK"), 0); + } - break; + return std::pair(std::string("Invalid extraction folder"), 1); + } + + return std::pair(std::string("An error occured while downloading"), 1); } #endif - return std::pair(std::string(""), 1); + return std::pair(std::string("Theme not found"), 1); } \ No newline at end of file diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 78f8cb1565..520b2e31fc 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -990,7 +990,8 @@ void GuiMenu::openSystemEmulatorSettings(SystemData* system) void GuiMenu::openEmulatorSettings() { - GuiSettings* configuration = new GuiSettings(mWindow, _("ADVANCED").c_str()); + GuiSettings* configuration = new GuiSettings(mWindow, _("EMULATOR SETTINGS").c_str()); + Window* window = mWindow; // For each activated system diff --git a/es-app/src/guis/GuiSettings.h b/es-app/src/guis/GuiSettings.h index 440063d797..168ce52646 100644 --- a/es-app/src/guis/GuiSettings.h +++ b/es-app/src/guis/GuiSettings.h @@ -43,6 +43,9 @@ class GuiSettings : public GuiComponent void setVariable(const std::string name, bool value) { mVariableMap[name] = value; } void updatePosition(); + void setTitle(const std::string title) { mMenu.setTitle(title); } + void setSubTitle(const std::string text) { mMenu.setSubTitle(text); } + protected: MenuComponent mMenu; diff --git a/es-app/src/guis/GuiThemeInstall.cpp b/es-app/src/guis/GuiThemeInstall.cpp index 9aa1808b70..b3d63d89f6 100644 --- a/es-app/src/guis/GuiThemeInstall.cpp +++ b/es-app/src/guis/GuiThemeInstall.cpp @@ -24,11 +24,26 @@ GuiThemeInstall::GuiThemeInstall(Window* window) ComponentListRow row; // icon + /* auto icon = std::make_shared(mWindow); icon->setImage(utheme.installed ? ":/star_filled.svg" : ":/star_unfilled.svg"); icon->setColorShift(theme->Text.color); icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); row.addElement(icon, false); + */ + + auto icon = std::make_shared(mWindow); + icon->setColor(theme->Text.color); + + if (utheme.installed) + icon->setOpacity(192); + + icon->setFont(theme->Text.font); + icon->setText(utheme.installed ? _U("\uF021") : _U("\uF019")); + icon->setSize(theme->Text.font->getLetterHeight() * 1.5f, 0); + //icon->setResize(0, theme->Text.font->getLetterHeight() * 1.25f); + row.addElement(icon, false); + // spacer between icon and text auto spacer = std::make_shared(mWindow); diff --git a/es-app/src/guis/GuiThemeInstall.h b/es-app/src/guis/GuiThemeInstall.h index a67f549879..0b0ec1064b 100644 --- a/es-app/src/guis/GuiThemeInstall.h +++ b/es-app/src/guis/GuiThemeInstall.h @@ -37,6 +37,7 @@ class MultiLineMenuEntry : public ComponentGrid mText->setVerticalAlignment(ALIGN_TOP); mSubstring = std::make_shared(mWindow, substring.c_str(), theme->TextSmall.font, theme->Text.color); + mSubstring->setOpacity(192); setEntry(mText, Vector2i(0, 0), true, true); setEntry(mSubstring, Vector2i(0, 1), false, true); diff --git a/es-core/src/components/MenuComponent.cpp b/es-core/src/components/MenuComponent.cpp index ee7f9928d0..02b7c10459 100644 --- a/es-core/src/components/MenuComponent.cpp +++ b/es-core/src/components/MenuComponent.cpp @@ -5,11 +5,16 @@ #define BUTTON_GRID_VERT_PADDING (Renderer::getScreenHeight()*0.0296296) //32 #define BUTTON_GRID_HORIZ_PADDING (Renderer::getScreenWidth()*0.0052083333) //10 -#define TITLE_HEIGHT (mTitle->getFont()->getLetterHeight() + TITLE_VERT_PADDING) +#define TITLE_HEIGHT (mTitle->getFont()->getLetterHeight() + (mSubtitle ? TITLE_WITHSUB_VERT_PADDING : TITLE_VERT_PADDING) + (mSubtitle ? mSubtitle->getSize().y() + SUBTITLE_VERT_PADDING : 0)) -MenuComponent::MenuComponent(Window* window, std::string title, const std::shared_ptr& titleFont) : GuiComponent(window), +MenuComponent::MenuComponent(Window* window, + const std::string title, const std::shared_ptr& titleFont, + const std::string subTitle) + : GuiComponent(window), mBackground(window), mGrid(window, Vector2i(1, 3)) { + mMaxHeight = 0; + auto theme = ThemeData::getMenuTheme(); addChild(&mBackground); @@ -30,8 +35,14 @@ MenuComponent::MenuComponent(Window* window, std::string title, const std::share mTitle->setRenderBackground(true); } - setTitle(title.c_str(), theme->Title.font); // titleFont - mGrid.setEntry(mTitle, Vector2i(0, 0), false); + mHeaderGrid = std::make_shared(mWindow, Vector2i(1, 2)); + mHeaderGrid->setEntry(mTitle, Vector2i(0, 0), false, true); + + setTitle(title, theme->Title.font); // titleFont + setSubTitle(subTitle); + + mGrid.setEntry(mHeaderGrid, Vector2i(0, 0), false, true); + //mGrid.setEntry(mTitle, Vector2i(0, 0), false); // set up list which will never change (externally, anyway) mList = std::make_shared(mWindow); @@ -130,10 +141,52 @@ void MenuComponent::addEntry(const std::string name, bool add_arrow, const std:: addRow(row, setCursorHere); } -void MenuComponent::setTitle(const char* title, const std::shared_ptr& font) +void MenuComponent::setTitle(const std::string title, const std::shared_ptr& font) { mTitle->setText(Utils::String::toUpper(title)); - mTitle->setFont(font); + + if (font != nullptr) + mTitle->setFont(font); +} + +void MenuComponent::setSubTitle(const std::string text) +{ + if (text.empty()) + { + if (mSubtitle != nullptr) + { + mHeaderGrid->removeEntry(mSubtitle); + mSubtitle = nullptr; + } + + mHeaderGrid->setRowHeightPerc(0, 1); + mHeaderGrid->setRowHeightPerc(1, 0); + + return; + } + + if (mSubtitle == nullptr) + { + auto theme = ThemeData::getMenuTheme(); + + mSubtitle = std::make_shared(mWindow, + Utils::String::toUpper(Utils::FileSystem::getFileName(text)), + theme->TextSmall.font, theme->TextSmall.color, ALIGN_CENTER); + + mHeaderGrid->setEntry(mSubtitle, Vector2i(0, 1), false, true); + } + + mSubtitle->setText(Utils::String::toUpper(text)); + mSubtitle->setVerticalAlignment(Alignment::ALIGN_TOP); + mSubtitle->setSize(Renderer::getScreenWidth() * 0.88f, 0); + mSubtitle->setLineSpacing(1.1); + + updateSize(); + + const float titleHeight = mTitle->getFont()->getLetterHeight() + (mSubtitle ? TITLE_WITHSUB_VERT_PADDING : TITLE_VERT_PADDING); + const float subtitleHeight = mSubtitle->getSize().y() + SUBTITLE_VERT_PADDING; + + mHeaderGrid->setRowHeightPerc(0, titleHeight / TITLE_HEIGHT); } float MenuComponent::getButtonGridHeight() const @@ -152,7 +205,8 @@ void MenuComponent::updateSize() return; } - const float maxHeight = Renderer::getScreenHeight() * 0.75f; + const float maxHeight = mMaxHeight <= 0 ? Renderer::getScreenHeight() * 0.75f : mMaxHeight; + float height = TITLE_HEIGHT + mList->getTotalRowHeight() + getButtonGridHeight() + 2; if(height > maxHeight) { diff --git a/es-core/src/components/MenuComponent.h b/es-core/src/components/MenuComponent.h index 1498c5c743..d3d4ad54b0 100644 --- a/es-core/src/components/MenuComponent.h +++ b/es-core/src/components/MenuComponent.h @@ -16,11 +16,15 @@ std::shared_ptr makeMultiDimButtonGrid(Window* window, const std: std::shared_ptr makeArrow(Window* window); #define TITLE_VERT_PADDING (Renderer::getScreenHeight()*0.0637f) +#define TITLE_WITHSUB_VERT_PADDING (Renderer::getScreenHeight()*0.05f) +#define SUBTITLE_VERT_PADDING (Renderer::getScreenHeight()*0.019f) class MenuComponent : public GuiComponent { public: - MenuComponent(Window* window, std::string title, const std::shared_ptr& titleFont = Font::get(FONT_SIZE_LARGE)); + MenuComponent(Window* window, + const std::string title, const std::shared_ptr& titleFont = Font::get(FONT_SIZE_LARGE), + const std::string subTitle = ""); void onSizeChanged() override; @@ -31,7 +35,8 @@ class MenuComponent : public GuiComponent void addButton(const std::string& label, const std::string& helpText, const std::function& callback); - void setTitle(const char* title, const std::shared_ptr& font); + void setTitle(const std::string title, const std::shared_ptr& font = nullptr); + void setSubTitle(const std::string text); inline void setCursorToList() { mGrid.setCursorTo(mList); } inline void setCursorToButtons() { assert(mButtonGrid); mGrid.setCursorTo(mButtonGrid); } @@ -40,6 +45,15 @@ class MenuComponent : public GuiComponent float getButtonGridHeight() const; + void setMaxHeight(float maxHeight) + { + if (mMaxHeight == maxHeight) + return; + + mMaxHeight = maxHeight; + updateSize(); + } + private: void updateSize(); void updateGrid(); @@ -47,10 +61,14 @@ class MenuComponent : public GuiComponent NinePatchComponent mBackground; ComponentGrid mGrid; + std::shared_ptr mHeaderGrid; std::shared_ptr mTitle; + std::shared_ptr mSubtitle; std::shared_ptr mList; std::shared_ptr mButtonGrid; std::vector< std::shared_ptr > mButtons; + + float mMaxHeight; }; #endif // ES_CORE_COMPONENTS_MENU_COMPONENT_H From 36e7049a2e0334301a8736f6449a6f0e4fdc2937 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 24 Nov 2019 19:18:22 +0100 Subject: [PATCH 237/336] Round corners : fix for openglES10 & ARM linux --- es-core/src/renderers/Renderer_GL21.cpp | 44 +++++++++++++++++----- es-core/src/renderers/Renderer_GLES10.cpp | 46 ++++++++++++++++------- 2 files changed, 67 insertions(+), 23 deletions(-) diff --git a/es-core/src/renderers/Renderer_GL21.cpp b/es-core/src/renderers/Renderer_GL21.cpp index e085134af3..866b032d7b 100644 --- a/es-core/src/renderers/Renderer_GL21.cpp +++ b/es-core/src/renderers/Renderer_GL21.cpp @@ -7,6 +7,7 @@ #include #include +#include namespace Renderer { @@ -275,7 +276,7 @@ namespace Renderer #define ROUNDING_PIECES 8.0f - void drawGLRoundedCorner(float x, float y, double sa, double arc, float r, unsigned int color) + void drawGLRoundedCorner(float x, float y, double sa, double arc, float r, unsigned int color, std::vector &vertex) { float red = (((color & 0xff000000) >> 24) & 255) / 255.0f; float g = (((color & 0x00ff0000) >> 16) & 255) / 255.0f; @@ -296,14 +297,28 @@ namespace Renderer float next_x = cent_x + r * Math::sinf(ang); float next_y = cent_y - r * Math::cosf(ang); - //glColor4bv((const GLbyte*) &color); - glColor4f(red, g, b, a); - glVertex2f(next_x, next_y); + Vertex vx; + vx.pos = Vector2f(next_x, next_y); + vx.tex = Vector2f(0, 0); + vx.col = color; + vertex.push_back(vx); } } void drawRoundRect(float x, float y, float width, float height, float radius, unsigned int color, const Blend::Factor _srcBlendFactor, const Blend::Factor _dstBlendFactor) { + auto finalColor = convertColor(color); + + std::vector vertex; + drawGLRoundedCorner(x, y + radius, 3.0f * ES_PI / 2.0f, ES_PI / 2.0f, radius, finalColor, vertex); + drawGLRoundedCorner(x + width - radius, y, 0.0, ES_PI / 2.0f, radius, finalColor, vertex); + drawGLRoundedCorner(x + width, y + height - radius, ES_PI / 2.0f, ES_PI / 2.0f, radius, finalColor, vertex); + drawGLRoundedCorner(x + radius, y + height, ES_PI, ES_PI / 2.0f, radius, finalColor, vertex); + + Vertex* vxs = new Vertex[vertex.size()]; + for (int i = 0; i < vertex.size(); i++) + vxs[i] = vertex[i]; + bindTexture(0); glEnable(GL_MULTISAMPLE); @@ -311,12 +326,21 @@ namespace Renderer glEnable(GL_BLEND); glBlendFunc(convertBlendFactor(_srcBlendFactor), convertBlendFactor(_dstBlendFactor)); - glBegin(GL_POLYGON); - drawGLRoundedCorner(x, y + radius, 3.0f * ES_PI / 2.0f, ES_PI / 2.0f, radius, color); - drawGLRoundedCorner(x + width - radius, y, 0.0, ES_PI / 2.0f, radius, color); - drawGLRoundedCorner(x + width, y + height - radius, ES_PI / 2.0f, ES_PI / 2.0f, radius, color); - drawGLRoundedCorner(x + radius, y + height, ES_PI, ES_PI / 2.0f, radius, color); - glEnd(); + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glEnableClientState(GL_COLOR_ARRAY); + + glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &vxs[0].pos); + glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &vxs[0].tex); + glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(Vertex), &vxs[0].col); + + glDrawArrays(GL_TRIANGLE_FAN, 0, vertex.size()); + + glDisableClientState(GL_COLOR_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + glDisableClientState(GL_VERTEX_ARRAY); + + delete[] vxs; glDisable(GL_BLEND); glDisable(GL_MULTISAMPLE); diff --git a/es-core/src/renderers/Renderer_GLES10.cpp b/es-core/src/renderers/Renderer_GLES10.cpp index ce49ab7c27..57bf1b5218 100644 --- a/es-core/src/renderers/Renderer_GLES10.cpp +++ b/es-core/src/renderers/Renderer_GLES10.cpp @@ -7,6 +7,7 @@ #include #include +#include namespace Renderer { @@ -270,7 +271,7 @@ namespace Renderer #define ROUNDING_PIECES 8.0f - void drawGLRoundedCorner(float x, float y, double sa, double arc, float r, unsigned int color) + void drawGLRoundedCorner(float x, float y, double sa, double arc, float r, unsigned int color, std::vector &vertex) { float red = (((color & 0xff000000) >> 24) & 255) / 255.0f; float g = (((color & 0x00ff0000) >> 16) & 255) / 255.0f; @@ -291,30 +292,49 @@ namespace Renderer float next_x = cent_x + r * Math::sinf(ang); float next_y = cent_y - r * Math::cosf(ang); - //glColor4bv((const GLbyte*) &color); - glColor4f(red, g, b, a); - glVertex2f(next_x, next_y); + Vertex vx; + vx.pos = Vector2f(next_x, next_y); + vx.tex = Vector2f(0, 0); + vx.col = color; + vertex.push_back(vx); } } void drawRoundRect(float x, float y, float width, float height, float radius, unsigned int color, const Blend::Factor _srcBlendFactor, const Blend::Factor _dstBlendFactor) { - bindTexture(0); + auto finalColor = convertColor(color); + + std::vector vertex; + drawGLRoundedCorner(x, y + radius, 3.0f * ES_PI / 2.0f, ES_PI / 2.0f, radius, finalColor, vertex); + drawGLRoundedCorner(x + width - radius, y, 0.0, ES_PI / 2.0f, radius, finalColor, vertex); + drawGLRoundedCorner(x + width, y + height - radius, ES_PI / 2.0f, ES_PI / 2.0f, radius, finalColor, vertex); + drawGLRoundedCorner(x + radius, y + height, ES_PI, ES_PI / 2.0f, radius, finalColor, vertex); - // glEnable(GL_MULTISAMPLE); + Vertex* vxs = new Vertex[vertex.size()]; + for (int i = 0; i < vertex.size(); i++) + vxs[i] = vertex[i]; + + bindTexture(0); glEnable(GL_BLEND); glBlendFunc(convertBlendFactor(_srcBlendFactor), convertBlendFactor(_dstBlendFactor)); - glBegin(GL_POLYGON); - drawGLRoundedCorner(x, y + radius, 3.0f * ES_PI / 2.0f, ES_PI / 2.0f, radius, color); - drawGLRoundedCorner(x + width - radius, y, 0.0, ES_PI / 2.0f, radius, color); - drawGLRoundedCorner(x + width, y + height - radius, ES_PI / 2.0f, ES_PI / 2.0f, radius, color); - drawGLRoundedCorner(x + radius, y + height, ES_PI, ES_PI / 2.0f, radius, color); - glEnd(); + glEnableClientState(GL_VERTEX_ARRAY); + glEnableClientState(GL_TEXTURE_COORD_ARRAY); + glEnableClientState(GL_COLOR_ARRAY); + glVertexPointer(2, GL_FLOAT, sizeof(Vertex), &vxs[0].pos); + glTexCoordPointer(2, GL_FLOAT, sizeof(Vertex), &vxs[0].tex); + glColorPointer(4, GL_UNSIGNED_BYTE, sizeof(Vertex), &vxs[0].col); + + glDrawArrays(GL_TRIANGLE_FAN, 0, vertex.size()); + + glDisableClientState(GL_COLOR_ARRAY); + glDisableClientState(GL_TEXTURE_COORD_ARRAY); + glDisableClientState(GL_VERTEX_ARRAY); glDisable(GL_BLEND); - // glDisable(GL_MULTISAMPLE); + + delete[] vxs; } void enableRoundCornerStencil(float x, float y, float width, float height, float radius) From 5096847fd631ca718f3402be63b256d5fae70612 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 24 Nov 2019 20:16:32 +0100 Subject: [PATCH 238/336] Add "VISIBLE SYSTEMS" option --- es-app/src/SystemData.cpp | 17 +++++++++-- es-app/src/guis/GuiMenu.cpp | 38 +++++++++++++++++++++++- es-app/src/views/SystemView.cpp | 9 ++++++ resources/locale/fr/emulationstation2.po | 4 +++ 4 files changed, 64 insertions(+), 4 deletions(-) diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index e4aa976705..6755e2b87e 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -595,9 +595,20 @@ std::string SystemData::getConfigPath(bool forWrite) bool SystemData::isVisible() { - return (getDisplayedGameCount() > 0 || - (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || - (mIsCollectionSystem && mName == "favorites")); + if ((getDisplayedGameCount() > 0 || + (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || + (mIsCollectionSystem && mName == "favorites"))) + { + if (!mIsCollectionSystem) + { + auto hiddenSystems = Utils::String::split(Settings::getInstance()->getString("HiddenSystems"), ';'); + return std::find(hiddenSystems.cbegin(), hiddenSystems.cend(), getName()) == hiddenSystems.cend(); + } + + return true; + } + + return false; } SystemData* SystemData::getNext() const diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 520b2e31fc..f9ac0f314a 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -779,6 +779,42 @@ void GuiMenu::openUISettings() s->addSaveFunc([systemfocus_list] { Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); }); + + + + // Select systems to hide + auto hiddenSystems = Utils::String::split(Settings::getInstance()->getString("HiddenSystems"), ';'); + + auto displayedSystems = std::make_shared>(mWindow, _("VISIBLE SYSTEMS"), true); + + for (auto system : SystemData::sSystemVector) + displayedSystems->add(system->getFullName(), system, std::find(hiddenSystems.cbegin(), hiddenSystems.cend(), system->getName()) == hiddenSystems.cend()); + + s->addWithLabel(_("VISIBLE SYSTEMS"), displayedSystems); + s->addSaveFunc([s, displayedSystems] + { + std::string hiddenSystems; + + std::vector sys = displayedSystems->getSelectedObjects(); + + for (auto system : SystemData::sSystemVector) + { + if (std::find(sys.cbegin(), sys.cend(), system) == sys.cend()) + { + if (hiddenSystems.empty()) + hiddenSystems = system->getName(); + else + hiddenSystems = hiddenSystems + ";" + system->getName(); + } + } + + if (Settings::getInstance()->setString("HiddenSystems", hiddenSystems)) + { + Settings::getInstance()->saveFile(); + s->setVariable("reloadAll", true); + } + }); + // Open gamelist at start auto bootOnGamelist = std::make_shared(mWindow); @@ -798,7 +834,7 @@ void GuiMenu::openUISettings() if (!hideSysView && hideSystemView->getState()) ViewController::get()->goToStart(true); }); - + #if defined(_WIN32) // quick system select (left/right in game list view) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 414948b0ca..c01752bbac 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -274,6 +274,15 @@ void SystemView::populate() Settings::getInstance()->setString("UIMode", "Full"); mWindow->pushGui(new GuiMsgBox(mWindow, "The selected UI mode has nothing to show,\n returning to UI mode: FULL", "OK", nullptr)); } + + if (Settings::getInstance()->setString("HiddenSystems", "")) + { + Settings::getInstance()->saveFile(); + + // refresh GUI + populate(); + mWindow->pushGui(new GuiMsgBox(mWindow, "ERROR: EVERY SYSTEM IS HIDDEN, RE-DISPLAYING ALL OF THEM NOW", "OK", nullptr)); + } } } diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index 6e5f324492..a24a766dff 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -2232,6 +2232,10 @@ msgstr "Verification" msgid "EMPTY LIST" msgstr "Liste vide" +#: +msgid "VISIBLE SYSTEMS" +msgstr "SYSTÈMES A AFFICHER" + #: msgid "COMPLETE QUIT MENU" msgstr "AFFICHER LE MENU QUITTER COMPLET" From 30ee7b74fa9b2d8e1613b7b632b05e21ad85bc81 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 24 Nov 2019 20:18:21 +0100 Subject: [PATCH 239/336] Visible systems : hide collections from system list --- es-app/src/guis/GuiMenu.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index f9ac0f314a..228cec1c53 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -788,7 +788,8 @@ void GuiMenu::openUISettings() auto displayedSystems = std::make_shared>(mWindow, _("VISIBLE SYSTEMS"), true); for (auto system : SystemData::sSystemVector) - displayedSystems->add(system->getFullName(), system, std::find(hiddenSystems.cbegin(), hiddenSystems.cend(), system->getName()) == hiddenSystems.cend()); + if(!system->isCollection()) + displayedSystems->add(system->getFullName(), system, std::find(hiddenSystems.cbegin(), hiddenSystems.cend(), system->getName()) == hiddenSystems.cend()); s->addWithLabel(_("VISIBLE SYSTEMS"), displayedSystems); s->addSaveFunc([s, displayedSystems] @@ -799,6 +800,9 @@ void GuiMenu::openUISettings() for (auto system : SystemData::sSystemVector) { + if (system->isCollection()) + continue; + if (std::find(sys.cbegin(), sys.cend(), system) == sys.cend()) { if (hiddenSystems.empty()) From 755a2a173a9bd5b5b87e5fd68a4af3a6cc75203d Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 25 Nov 2019 00:57:07 +0100 Subject: [PATCH 240/336] TextComponent : Add "autoScroll" property text.autoScroll small fix AutoScroll defaulted to false --- es-core/src/ThemeData.cpp | 1 + es-core/src/components/GridTileComponent.cpp | 5 +- es-core/src/components/TextComponent.cpp | 158 +++++++++++++++---- es-core/src/components/TextComponent.h | 9 ++ 4 files changed, 144 insertions(+), 29 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 987f014dc7..b0aaa325f5 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -88,6 +88,7 @@ std::map> The { "glowColor", COLOR }, { "glowSize", FLOAT }, { "glowOffset", NORMALIZED_PAIR }, + { "autoScroll", BOOLEAN }, { "padding", NORMALIZED_RECT }, { "visible", BOOLEAN }, { "zIndex", FLOAT } } }, diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index d6a36fd9ff..5a2c781a52 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -587,6 +587,9 @@ bool GridNinePatchProperties::applyTheme(const ThemeData::ThemeElement* elem) void GridTileComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { + if (mSize == Vector2f::Zero()) + setSize(getDefaultTileSize()); + resetProperties(); const ThemeData::ThemeElement* grid = theme->getElement(view, "gamegrid", "imagegrid"); @@ -701,7 +704,7 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons delete mImageOverlay; mImageOverlay = nullptr; } - + // Apply theme to the element elem = theme->getElement(view, "gridtile", "text"); diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index 7e299ac78b..2d41c97f7a 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -10,6 +10,11 @@ TextComponent::TextComponent(Window* window) : GuiComponent(window), mRenderBackground(false), mGlowColor(0), mGlowSize(2), mPadding(Vector4f(0, 0, 0, 0)), mGlowOffset(Vector2f(0, 0)), mReflection(0.0f, 0.0f), mReflectOnBorders(false) { + mMarqueeOffset = 0; + mMarqueeOffset2 = 0; + mMarqueeTime = 0; + + mAutoScroll = false; } @@ -26,6 +31,12 @@ TextComponent::TextComponent(Window* window, const std::string& text, const std: setText(text); setPosition(pos); setSize(size); + + mMarqueeOffset = 0; + mMarqueeOffset2 = 0; + mMarqueeTime = 0; + + mAutoScroll = false; } void TextComponent::onSizeChanged() @@ -87,7 +98,14 @@ void TextComponent::setOpacity(unsigned char opacity) void TextComponent::setText(const std::string& text) { + if (mText == text) + return; + mText = text; + mMarqueeOffset = 0; + mMarqueeOffset2 = 0; + mMarqueeTime = 0; + onTextChanged(); } @@ -114,6 +132,29 @@ void TextComponent::renderSingleGlow(const Transform4x4f& parentTrans, float yOf mFont->renderTextCache(mTextCache.get()); } +void TextComponent::renderGlow(const Transform4x4f& parentTrans, float yOff, float xOff) +{ + Transform4x4f glowTrans = parentTrans; + if (xOff != 0.0) + glowTrans.translate(Vector3f(xOff, 0, 0)); + + int x = -mGlowSize; + int y = -mGlowSize; + renderSingleGlow(glowTrans, yOff, x, y); + + for (int i = 0; i < 2 * mGlowSize; i++) + renderSingleGlow(glowTrans, yOff, ++x, y); + + for (int i = 0; i < 2 * mGlowSize; i++) + renderSingleGlow(glowTrans, yOff, x, ++y); + + for (int i = 0; i < 2 * mGlowSize; i++) + renderSingleGlow(glowTrans, yOff, --x, y); + + for (int i = 0; i < 2 * mGlowSize; i++) + renderSingleGlow(glowTrans, yOff, x, --y); +} + void TextComponent::render(const Transform4x4f& parentTrans) { if (!isVisible()) @@ -159,27 +200,18 @@ void TextComponent::render(const Transform4x4f& parentTrans) if ((mGlowColor & 0x000000FF) != 0 && mGlowSize > 0) { - int x = -mGlowSize; - int y = -mGlowSize; - renderSingleGlow(parentTrans, yOff, x, y); - - for (int i = 0; i < 2 * mGlowSize; i++) - renderSingleGlow(parentTrans, yOff, ++x, y); - - for (int i = 0; i < 2 * mGlowSize; i++) - renderSingleGlow(parentTrans, yOff, x, ++y); - - for (int i = 0; i < 2 * mGlowSize; i++) - renderSingleGlow(parentTrans, yOff, --x, y); - - for (int i = 0; i < 2 * mGlowSize; i++) - renderSingleGlow(parentTrans, yOff, x, --y); - - // Restore text color + renderGlow(parentTrans, yOff, -mMarqueeOffset); onColorChanged(); } - trans.translate(off); + Transform4x4f drawTrans = trans; + + if (mMarqueeOffset > 0) + trans.translate(off - Vector3f((float)mMarqueeOffset, 0, 0)); + else + trans.translate(off); + +// trans.translate(off); Renderer::setMatrix(trans); // draw the text area, where the text actually is going @@ -201,6 +233,25 @@ void TextComponent::render(const Transform4x4f& parentTrans) mFont->renderTextCache(mTextCache.get()); + // render currently selected item text again if + // marquee is scrolled far enough for it to repeat + + if (mMarqueeOffset2 < 0) + { + trans = drawTrans; + trans.translate(off - Vector3f((float)mMarqueeOffset2, 0, 0)); + + if ((mGlowColor & 0x000000FF) != 0 && mGlowSize > 0) + { + renderGlow(parentTrans, yOff, -mMarqueeOffset2); + onColorChanged(); + } + + Renderer::setMatrix(trans); + mFont->renderTextCache(mTextCache.get()); + Renderer::setMatrix(drawTrans); + } + if (mReflection.x() != 0 || mReflection.y() != 0) { Transform4x4f mirror = trans; @@ -274,18 +325,21 @@ void TextComponent::onTextChanged() Vector2f size = f->sizeText(text); if (!isMultiline && sx && text.size() && (size.x() > sx || addAbbrev)) { - // abbreviate text - const std::string abbrev = "..."; - Vector2f abbrevSize = f->sizeText(abbrev); - - while (text.size() && size.x() + abbrevSize.x() > sx) + if (!mAutoScroll) { - size_t newSize = Utils::String::prevCursor(text, text.size()); - text.erase(newSize, text.size() - newSize); - size = f->sizeText(text); - } + // abbreviate text + const std::string abbrev = "..."; + Vector2f abbrevSize = f->sizeText(abbrev); - text.append(abbrev); + while (text.size() && size.x() + abbrevSize.x() > sx) + { + size_t newSize = Utils::String::prevCursor(text, text.size()); + text.erase(newSize, text.size() - newSize); + size = f->sizeText(text); + } + + text.append(abbrev); + } mTextCache = std::shared_ptr(f->buildTextCache(text, Vector2f(0, 0), color, sx, mHorizontalAlignment, mLineSpacing)); } @@ -294,6 +348,49 @@ void TextComponent::onTextChanged() } } +void TextComponent::update(int deltaTime) +{ + GuiComponent::update(deltaTime); + + int sy = mSize.y() - mPadding.y() - mPadding.w(); + const bool isMultiline = (mSize.y() == 0 || sy > mFont->getHeight()*1.2f); + + if (mAutoScroll && !isMultiline && mSize.x() > 0) + { + // always reset the marquee offsets + mMarqueeOffset = 0; + mMarqueeOffset2 = 0; + + std::string text = mUppercase ? Utils::String::toUpper(mText) : mText; + + // if we're not scrolling and this object's text goes outside our size, marquee it! + const float textLength = mFont->sizeText(text).x(); + const float limit = mSize.x() - mPadding.x() - mPadding.z(); + + if (textLength > limit) + { + // loop + // pixels per second ( based on nes-mini font at 1920x1080 to produce a speed of 200 ) + const float speed = mFont->sizeText("ABCDEFGHIJKLMNOPQRSTUVWXYZ").x() * 0.247f; + const float delay = 3000; + const float scrollLength = textLength; + const float returnLength = speed * 1.5f; + const float scrollTime = (scrollLength * 1000) / speed; + const float returnTime = (returnLength * 1000) / speed; + const int maxTime = (int)(delay + scrollTime + returnTime); + + mMarqueeTime += deltaTime; + while (mMarqueeTime > maxTime) + mMarqueeTime -= maxTime; + + mMarqueeOffset = (int)(Math::Scroll::loop(delay, scrollTime + returnTime, (float)mMarqueeTime, scrollLength + returnLength)); + + if (mMarqueeOffset > (scrollLength - (limit - returnLength))) + mMarqueeOffset2 = (int)(mMarqueeOffset - (scrollLength + returnLength)); + } + } +} + void TextComponent::onColorChanged() { if (mTextCache) @@ -422,6 +519,11 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, const st mReflectOnBorders = elem->get("reflexionOnFrame"); else mReflectOnBorders = false; + + if (elem->has("autoScroll")) + mAutoScroll = elem->get("autoScroll"); + else + mAutoScroll = false; } setFont(Font::getFromTheme(elem, properties, mFont)); diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index 50b79370f3..bf4c712d18 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -48,6 +48,8 @@ class TextComponent : public GuiComponent void setPadding(const Vector4f padding) { mPadding = padding; } + virtual void update(int deltaTime); + protected: virtual void onTextChanged(); @@ -57,6 +59,7 @@ class TextComponent : public GuiComponent private: void calculateExtent(); void renderSingleGlow(const Transform4x4f& parentTrans, float yOff, float x, float y); + void renderGlow(const Transform4x4f& parentTrans, float yOff, float xOff); void onColorChanged(); @@ -79,6 +82,12 @@ class TextComponent : public GuiComponent Vector2f mReflection; bool mReflectOnBorders; + + int mMarqueeOffset; + int mMarqueeOffset2; + int mMarqueeTime; + + bool mAutoScroll; }; #endif // ES_CORE_COMPONENTS_TEXT_COMPONENT_H From 8ca54c71045ccca35e408fe9569250abc3789a13 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 25 Nov 2019 10:24:05 +0100 Subject: [PATCH 241/336] GridView : Fix text default position --- es-core/src/components/GridTileComponent.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 5a2c781a52..0af999b085 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -170,9 +170,9 @@ void GridTileComponent::resize() currentProperties.Label.updateTextComponent(&mLabel, mSize); // Automatic layout for not merged labels - if (currentProperties.Label.pos.x() < 0 && !mLabelMerged) + if (currentProperties.Label.pos.x() < 0) { - if (currentProperties.Padding.x() == 0) + if (currentProperties.Padding.x() == 0 && !mLabelMerged) { mLabel.setPosition(mImage->getPosition().x() - mImage->getSize().x() / 2, mImage->getSize().y()); mLabel.setSize(mImage->getSize().x(), labelHeight); From 370aaca8637ec397e6f91c9e25df340b85c3e994 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 25 Nov 2019 10:25:19 +0100 Subject: [PATCH 242/336] Gamelist Recovery : Fix for custom collections & "SaveOnExit" setting to false --- es-app/src/CollectionSystemManager.cpp | 4 ++-- es-app/src/Gamelist.cpp | 3 +++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 314cb30833..4fe640f390 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -548,9 +548,9 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) ViewController::get()->onFileChanged(file, FILE_METADATA_CHANGED); ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(file, FILE_METADATA_CHANGED); } - } - saveToGamelistRecovery(file); + saveToGamelistRecovery(file); + } char trstring[512]; diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 36648c61ad..dc748f1d7f 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -250,6 +250,9 @@ bool addFileDataNode(pugi::xml_node& parent, const FileData* file, const char* t bool saveToGamelistRecovery(FileData* file) { + if (!Settings::getInstance()->getBool("SaveGamelistsOnExit")) + return false; + pugi::xml_document doc; pugi::xml_node root = doc.append_child("gameList"); From f7d7d7e589551b9155373e7ca5454c2195fab5af Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 26 Nov 2019 17:37:45 +0100 Subject: [PATCH 243/336] GameList : Small optimisation when saving --- es-app/src/Gamelist.cpp | 53 ++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 27 deletions(-) diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index dc748f1d7f..9f78676d14 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -18,6 +18,10 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType type, std::unordered_map& fileMap) { + auto pGame = fileMap.find(path); + if (pGame != fileMap.end()) + return pGame->second; + // first, verify that path is within the system's root folder FolderData* root = system->getRootFolder(); @@ -30,10 +34,6 @@ FileData* findOrCreateFile(SystemData* system, const std::string& path, FileType return NULL; } - auto pGame = fileMap.find(path); - if (pGame != fileMap.end()) - return pGame->second; - Utils::FileSystem::stringList pathList = Utils::FileSystem::getPathList(relative); auto path_it = pathList.begin(); FolderData* treeNode = root; @@ -357,34 +357,33 @@ void updateGamelist(SystemData* system) root = doc.append_child("gameList"); } - //iterate through all files, checking if they're already in the XML + std::map xmlMap; + + for (pugi::xml_node fileNode : root.children()) + { + pugi::xml_node path = fileNode.child("path"); + if (path) + { + std::string nodePath = Utils::FileSystem::getCanonicalPath(Utils::FileSystem::resolveRelativePath(path.text().get(), system->getStartPath(), true)); + xmlMap[nodePath] = fileNode; + } + } + + // iterate through all files, checking if they're already in the XML for(auto file : dirtyFiles) { - const char* tag = (file->getType() == GAME) ? "game" : "folder"; - bool removed = false; // 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)) + auto xmf = xmlMap.find(Utils::FileSystem::getCanonicalPath(file->getPath())); + if (xmf != xmlMap.cend()) { - pugi::xml_node pathNode = fileNode.child("path"); - if(!pathNode) - { - LOG(LogError) << "<" << tag << "> node contains no child!"; - continue; - } - - std::string nodePath = Utils::FileSystem::getCanonicalPath(Utils::FileSystem::resolveRelativePath(pathNode.text().get(), system->getStartPath(), true)); - std::string gamePath = Utils::FileSystem::getCanonicalPath(file->getPath()); - if(nodePath == gamePath) - { - // found it - removed = true; - root.remove_child(fileNode); - break; - } + removed = true; + root.remove_child(xmf->second); } + + const char* tag = (file->getType() == GAME) ? "game" : "folder"; // it was either removed or never existed to begin with; either way, we can add it now if (addFileDataNode(root, file, tag, system)) @@ -393,9 +392,9 @@ void updateGamelist(SystemData* system) ++numUpdated; // Only if really removed } - //now write the file - - if (numUpdated > 0) { + // Now write the file + if (numUpdated > 0) + { //make sure the folders leading up to this path exist (or the write will fail) std::string xmlWritePath(system->getGamelistPath(true)); Utils::FileSystem::createDirectory(Utils::FileSystem::getParent(xmlWritePath)); From e36a2df11d18ce2f903411633868d85b95c595f4 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 26 Nov 2019 17:38:05 +0100 Subject: [PATCH 244/336] GridView : fixes --- es-app/src/views/SystemView.cpp | 18 ++++++---- es-app/src/views/ViewController.cpp | 2 +- .../src/views/gamelist/GridGameListView.cpp | 12 +++---- es-core/src/ThemeData.cpp | 2 +- es-core/src/components/GridTileComponent.cpp | 13 ++++--- es-core/src/components/ImageGridComponent.h | 36 ++++++++++--------- es-core/src/components/TextComponent.cpp | 25 +++++++------ 7 files changed, 63 insertions(+), 45 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index c01752bbac..717e95c05a 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -24,7 +24,7 @@ SystemView::SystemView(Window* window) : IList(wind mExtrasFadeOpacity = 0.0f; mLastSystem = nullptr; mScreensaverActive = false; - mDisable = false; + mDisable = false; mShowing = false; mLastCursor = 0; mStaticBackground = nullptr; @@ -537,8 +537,14 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) void SystemView::render(const Transform4x4f& parentTrans) { - if (size() == 0) + if (size() == 0 || !mVisible) return; // nothing to render + + Transform4x4f trans = getTransform() * parentTrans; + + Vector2f clipPos(trans.translation().x(), trans.translation().y()); + if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + return; if (mSize.x() != Renderer::getScreenWidth() || mSize.x() != Renderer::getScreenHeight()) { @@ -560,11 +566,7 @@ void SystemView::render(const Transform4x4f& parentTrans) } } - Transform4x4f trans = getTransform() * parentTrans; - - Vector2f clipPos(trans.translation().x(), trans.translation().y()); - if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) - return; + auto systemInfoZIndex = mSystemInfo.getZIndex(); auto minMax = std::minmax(mCarousel.zIndex, systemInfoZIndex); @@ -1009,12 +1011,14 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) void SystemView::onShow() { + GuiComponent::onShow(); mShowing = true; activateExtras(mCursor); } void SystemView::onHide() { + GuiComponent::onHide(); mShowing = false; updateExtras([this](GuiComponent* p) { p->onHide(); }); } diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 1c4de97003..be1728b576 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -548,7 +548,7 @@ void ViewController::render(const Transform4x4f& parentTrans) Vector3f guiStart = it->second->getPosition(); Vector3f guiEnd = it->second->getPosition() + 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()) + if (guiEnd.x() > viewStart.x() && guiEnd.y() >= viewStart.y() && guiStart.x() < viewEnd.x() && guiStart.y() <= viewEnd.y()) it->second->render(trans); } diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 6c83fcd751..4a9d4e7dbd 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -240,7 +240,7 @@ void GridGameListView::populateList(const std::vector& files) if (mCursorStack.size()) { FileData* placeholder = new FileData(PLACEHOLDER, "..", this->mRoot->getSystem()); - mGrid.add(". .", "", "", "", false, placeholder); + mGrid.add(". .", "", "", "", false, true, placeholder); } std::string systemName = mRoot->getSystem()->getFullName(); @@ -255,7 +255,7 @@ void GridGameListView::populateList(const std::vector& files) for (auto file : files) { if (file->getFavorite() && showFavoriteIcon) - mGrid.add(file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), true, file); + mGrid.add(file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), true, file->getType() != GAME, file); } } @@ -268,15 +268,15 @@ void GridGameListView::populateList(const std::vector& files) if (showFavoriteIcon) { - mGrid.add(_U("\uF006 ") + file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), true, file); + mGrid.add(_U("\uF006 ") + file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), true, file->getType() != GAME, file); continue; } } if (file->getType() == FOLDER && Utils::FileSystem::exists(getImagePath(file))) - mGrid.add(_U("\uF114 ") + file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), file->getFavorite(), file); + mGrid.add(_U("\uF114 ") + file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), file->getFavorite(), file->getType() != GAME, file); else - mGrid.add(file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), file->getFavorite(), file); + mGrid.add(file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), file->getFavorite(), file->getType() != GAME, file); } } else @@ -613,7 +613,7 @@ void GridGameListView::addPlaceholder() { // empty grid - add a placeholder FileData* placeholder = new FileData(PLACEHOLDER, "<" + _("No Entries Found") + ">", this->mRoot->getSystem()); - mGrid.add(placeholder->getName(), "", "", "", false, placeholder); + mGrid.add(placeholder->getName(), "", "", "", false, false, placeholder); } void GridGameListView::launch(FileData* game) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index b0aaa325f5..06bc165655 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -88,7 +88,7 @@ std::map> The { "glowColor", COLOR }, { "glowSize", FLOAT }, { "glowOffset", NORMALIZED_PAIR }, - { "autoScroll", BOOLEAN }, + { "singleLineScroll", BOOLEAN }, { "padding", NORMALIZED_RECT }, { "visible", BOOLEAN }, { "zIndex", FLOAT } } }, diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 0af999b085..ece982c9f9 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -66,6 +66,7 @@ void GridTileComponent::resetProperties() void GridTileComponent::forceSize(Vector2f size, float selectedZoom) { + mSize = size; mDefaultProperties.Size = size; mSelectedProperties.Size = size * selectedZoom; mVideoPlayingProperties.Size = mSelectedProperties.Size; @@ -134,7 +135,7 @@ void GridTileComponent::resize() if (mImage != nullptr && currentProperties.Image.sizeMode != "maxSize" && isDefaultImage) mImage->setMaxSize(imageSize.x(), imageSize.y()); - } + } else if (mImage != nullptr) { // Retrocompatibility : imagegrid.image is not defined @@ -167,7 +168,9 @@ void GridTileComponent::resize() if (currentProperties.Label.Visible) { - currentProperties.Label.updateTextComponent(&mLabel, mSize); + auto szRef = mLabelMerged ? mSize - imageOffset : mSize; + + currentProperties.Label.updateTextComponent(&mLabel, szRef); // Automatic layout for not merged labels if (currentProperties.Label.pos.x() < 0) @@ -179,7 +182,7 @@ void GridTileComponent::resize() } else { - mLabel.setPosition(0, mSize.y() - labelHeight); + mLabel.setPosition(0, szRef.y() - labelHeight); mLabel.setSize(size.x(), labelHeight); } } @@ -723,7 +726,9 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mLabelMerged = mDefaultProperties.Label.size.x() == 0; // Apply theme to the element - elem = theme->getElement(view, "gridtile:selected", "text"); + elem = theme->getElement(view, "gridtile_selected", "text"); + if (elem == nullptr) + elem = theme->getElement(view, "gridtile:selected", "text"); if (elem == nullptr) // Apply theme to the element elem = theme->getElement(view, "gridtile.text:selected", "text"); diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 08c3b37bc6..fc322b1839 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -32,6 +32,7 @@ struct ImageGridData std::string marqueePath; std::string videoPath; bool favorite; + bool folder; }; template @@ -56,7 +57,7 @@ class ImageGridComponent : public IList ImageGridComponent(Window* window); - void add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, bool favorite, const T& obj); + void add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, bool favorite, bool folder, const T& obj); bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; @@ -167,7 +168,7 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList -void ImageGridComponent::add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, bool favorite, const T& obj) +void ImageGridComponent::add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, bool favorite, bool folder, const T& obj) { typename IList::Entry entry; entry.name = name; @@ -176,6 +177,7 @@ void ImageGridComponent::add(const std::string& name, const std::string& imag entry.data.videoPath = videoPath; entry.data.marqueePath = marqueePath; entry.data.favorite = favorite; + entry.data.folder = folder; static_cast*>(this)->add(entry); mEntriesDirty = true; @@ -383,12 +385,12 @@ void ImageGridComponent::render(const Transform4x4f& parentTrans) template void ImageGridComponent::applyTheme(const std::shared_ptr& theme, const std::string& view, const std::string& element, unsigned int properties) { + // Keep the theme pointer to apply it on the tiles later on + mTheme = nullptr; + // Apply theme to GuiComponent but not size property, which will be applied at the end of this function GuiComponent::applyTheme(theme, view, element, properties ^ ThemeFlags::SIZE); - // Keep the theme pointer to apply it on the tiles later on - mTheme = theme; - Vector2f screen = Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); const ThemeData::ThemeElement* elem = theme->getElement(view, element, "imagegrid"); @@ -520,16 +522,22 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, // Apply size property, will trigger a call to onSizeChanged() which will build the tiles GuiComponent::applyTheme(theme, view, element, ThemeFlags::SIZE | ThemeFlags::Z_INDEX); + // Keep the theme pointer to apply it on the tiles later on + mTheme = theme; + // Trigger the call manually if the theme have no "imagegrid" element - if (!elem) - buildTiles(); + buildTiles(); + updateTiles(false, false); } template void ImageGridComponent::onSizeChanged() { + if (mTheme == nullptr) + return; + buildTiles(); - updateTiles(); + updateTiles(false, false); } template @@ -796,7 +804,7 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA { tile->setVisible(true); - std::string name = mEntries.at(imgPos).name; // .object->getName(); + std::string name = mEntries.at(imgPos).name; if (!mEntries.at(imgPos).data.favorite || tile->hasFavoriteMedia()) tile->setLabel(name); @@ -807,7 +815,7 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA if (ResourceManager::getInstance()->fileExists(imagePath)) tile->setImage(imagePath); - else if (mEntries.at(imgPos).object->getType() == 2 || (mEntries.at(imgPos).object->getType() == 3 && mEntries.at(imgPos).object->getPath() == "..")) + else if (mEntries.at(imgPos).data.folder && Utils::String::startsWith(name, ".")) tile->setImage(mDefaultFolderTexture, true); else tile->setImage(mDefaultGameTexture, true); @@ -829,8 +837,6 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA if (!videoPath.empty() && ResourceManager::getInstance()->fileExists(videoPath)) tile->setVideo(videoPath, mVideoDelay); - else if (mEntries.at(imgPos).object->getType() == 2) - tile->setVideo(""); else tile->setVideo(""); } @@ -912,12 +918,10 @@ void ImageGridComponent::buildTiles() // In Horizontal mod, tiles are ordered from top to bottom, then from left to right X = vert ? x : y - EXTRAITEMS; Y = vert ? y - EXTRAITEMS : x; - - //if (!isVertical()) - // X--; + tile->setOrigin(0.5f, 0.5f); tile->setPosition(X * tileDistance.x() + startPosition.x(), Y * tileDistance.y() + startPosition.y()); - tile->setOrigin(0.5f, 0.5f); + tile->setSize(mTileSize); if (mTheme) tile->applyTheme(mTheme, mName, "gridtile", ThemeFlags::ALL); diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index 2d41c97f7a..683f0bde48 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -206,7 +206,7 @@ void TextComponent::render(const Transform4x4f& parentTrans) Transform4x4f drawTrans = trans; - if (mMarqueeOffset > 0) + if (mMarqueeOffset != 0.0) trans.translate(off - Vector3f((float)mMarqueeOffset, 0, 0)); else trans.translate(off); @@ -236,7 +236,7 @@ void TextComponent::render(const Transform4x4f& parentTrans) // render currently selected item text again if // marquee is scrolled far enough for it to repeat - if (mMarqueeOffset2 < 0) + if (mMarqueeOffset2 != 0.0) { trans = drawTrans; trans.translate(off - Vector3f((float)mMarqueeOffset2, 0, 0)); @@ -310,7 +310,7 @@ void TextComponent::onTextChanged() std::string text = mUppercase ? Utils::String::toUpper(mText) : mText; std::shared_ptr f = mFont; - const bool isMultiline = (mSize.y() == 0 || sy > f->getHeight()*1.2f); + const bool isMultiline = !mAutoScroll && (mSize.y() == 0 || sy > f->getHeight()*1.2f); bool addAbbrev = false; if (!isMultiline) @@ -323,9 +323,9 @@ void TextComponent::onTextChanged() auto color = mColor & 0xFFFFFF00 | (unsigned char)((mColor & 0xFF) * (mOpacity / 255.0)); Vector2f size = f->sizeText(text); - if (!isMultiline && sx && text.size() && (size.x() > sx || addAbbrev)) + if (!isMultiline) { - if (!mAutoScroll) + if (sx && text.size() && (size.x() > sx || addAbbrev) && !mAutoScroll) { // abbreviate text const std::string abbrev = "..."; @@ -343,9 +343,8 @@ void TextComponent::onTextChanged() mTextCache = std::shared_ptr(f->buildTextCache(text, Vector2f(0, 0), color, sx, mHorizontalAlignment, mLineSpacing)); } - else { + else mTextCache = std::shared_ptr(f->buildTextCache(f->wrapText(text, sx), Vector2f(0, 0), color, sx, mHorizontalAlignment, mLineSpacing)); - } } void TextComponent::update(int deltaTime) @@ -353,7 +352,7 @@ void TextComponent::update(int deltaTime) GuiComponent::update(deltaTime); int sy = mSize.y() - mPadding.y() - mPadding.w(); - const bool isMultiline = (mSize.y() == 0 || sy > mFont->getHeight()*1.2f); + const bool isMultiline = !mAutoScroll && (mSize.y() == 0 || sy > mFont->getHeight()*1.2f); if (mAutoScroll && !isMultiline && mSize.x() > 0) { @@ -389,6 +388,12 @@ void TextComponent::update(int deltaTime) mMarqueeOffset2 = (int)(mMarqueeOffset - (scrollLength + returnLength)); } } + else + { + mMarqueeTime = 0; + mMarqueeOffset = 0; + mMarqueeOffset2 = 0; + } } void TextComponent::onColorChanged() @@ -520,8 +525,8 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, const st else mReflectOnBorders = false; - if (elem->has("autoScroll")) - mAutoScroll = elem->get("autoScroll"); + if (elem->has("singleLineScroll")) + mAutoScroll = elem->get("singleLineScroll"); else mAutoScroll = false; } From d2cf7d058a3ba15a2bbfe92f8c792d24c907b742 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 26 Nov 2019 18:53:38 +0100 Subject: [PATCH 245/336] Fix : update mecanism (file copy after extraction) not working anymore --- es-app/src/ApiSystem.cpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/es-app/src/ApiSystem.cpp b/es-app/src/ApiSystem.cpp index 0d8d2ea9b0..4d8b1a26f8 100644 --- a/es-app/src/ApiSystem.cpp +++ b/es-app/src/ApiSystem.cpp @@ -225,7 +225,7 @@ void deleteDirectoryFiles(const std::string path) for (auto file : files) { if (Utils::FileSystem::isDirectory(file)) - ::RemoveDirectoryA(file.c_str()); + ::RemoveDirectoryA(Utils::FileSystem::getPreferredPath(file).c_str()); else Utils::FileSystem::removeFile(file); } @@ -235,7 +235,7 @@ void deleteDirectoryFiles(const std::string path) std::pair ApiSystem::updateSystem(const std::function& func) { #if WIN32 - std::string url = "https://github.com/fabricecaruso/EmulationStation/releases/download/continuous-master/EmulationStation-Win32.zip"; + std::string url = "https://github.com/fabricecaruso/EmulationStation/releases/download/continuous-master/EmulationStation-Win32-no-deps.zip"; auto req = downloadFile(url, "update", func); if (req != nullptr && req->status() == HttpReq::REQ_SUCCESS) { @@ -244,17 +244,17 @@ std::pair ApiSystem::updateSystem(const std::functionsaveContent(zipFile); - unzipFile(zipFile, path); + unzipFile(Utils::FileSystem::getPreferredPath(zipFile), Utils::FileSystem::getPreferredPath(path)); Utils::FileSystem::removeFile(zipFile); auto files = Utils::FileSystem::getDirContent(path, true, true); From 521bf21d1ec561781972a3ee7f3020868b9ad0da Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 27 Nov 2019 01:40:44 +0100 Subject: [PATCH 246/336] gridtile.padding : changer to NORMALIZED_RECT to support 4 points padding Ninepatch : support color animation when texture set --- es-core/src/ThemeData.cpp | 2 +- es-core/src/components/GridTileComponent.cpp | 30 +++++++++++++++---- es-core/src/components/GridTileComponent.h | 2 +- es-core/src/components/NinePatchComponent.cpp | 23 ++++++++++++++ 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 06bc165655..b4e87cafc7 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -57,7 +57,7 @@ std::map> The { "scrollLoop", BOOLEAN } } }, { "gridtile", { { "size", NORMALIZED_PAIR }, - { "padding", NORMALIZED_PAIR }, + { "padding", NORMALIZED_RECT }, { "imageColor", COLOR }, { "backgroundImage", PATH }, { "backgroundCornerSize", NORMALIZED_PAIR }, diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index ece982c9f9..ae21b43bce 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -47,7 +47,7 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa void GridTileComponent::resetProperties() { mDefaultProperties.Size = getDefaultTileSize(); - mDefaultProperties.Padding = Vector2f(16.0f, 16.0f); + mDefaultProperties.Padding = Vector4f(16.0f, 16.0f, 16.0f, 16.0f); mSelectedProperties.Size = getSelectedTileSize(); mSelectedProperties.Padding = mDefaultProperties.Padding; @@ -123,10 +123,10 @@ void GridTileComponent::resize() height = 0; float topPadding = currentProperties.Padding.y(); - float bottomPadding = std::max(topPadding, height); + float bottomPadding = std::max(currentProperties.Padding.w(), height); - Vector2f imageOffset = currentProperties.Padding; - Vector2f imageSize(size.x() - currentProperties.Padding.x() * 2.0, size.y() - topPadding - bottomPadding); + Vector2f imageOffset = Vector2f(currentProperties.Padding.x(), currentProperties.Padding.y()); + Vector2f imageSize(size.x() - currentProperties.Padding.x() - currentProperties.Padding.z(), size.y() - topPadding - bottomPadding); // Image if (currentProperties.Image.Loaded) @@ -238,7 +238,10 @@ void GridTileComponent::resize() if (mImage != NULL && currentProperties.SelectionMode == "image" && mImage->getSize() != Vector2f(0, 0)) { if (currentProperties.Image.sizeMode == "minSize") - bkSize = Vector2f(size.x(), size.y() - bottomPadding + topPadding); + { + if (!mLabelMerged) + bkSize = Vector2f(size.x(), size.y() - bottomPadding + topPadding); + } else { bkPosition = Vector3f(imageOffset.x() - mSelectedProperties.Padding.x(), imageOffset.y() - mSelectedProperties.Padding.y(), 0); @@ -420,7 +423,7 @@ void GridTileComponent::applyThemeToProperties(const ThemeData::ThemeElement* el properties.Size = elem->get("size") * screen; if (elem->has("padding")) - properties.Padding = elem->get("padding"); + properties.Padding = elem->get("padding"); if (elem && elem->has("selectionMode")) properties.SelectionMode = elem->get("selectionMode"); @@ -1011,6 +1014,21 @@ static Vector2f mixVectors(const Vector2f& def, const Vector2f& sel, float perce return Vector2f(x, y); } +static Vector4f mixVectors(const Vector4f& def, const Vector4f& sel, float percent) +{ + if (def == sel || percent == 0) + return def; + + if (percent == 1) + return sel; + + float x = def.x() * (1.0 - percent) + sel.x() * percent; + float y = def.y() * (1.0 - percent) + sel.y() * percent; + float z = def.z() * (1.0 - percent) + sel.z() * percent; + float w = def.w() * (1.0 - percent) + sel.w() * percent; + return Vector4f(x, y, z, w); +} + static unsigned int mixUnsigned(const unsigned int def, const unsigned int sel, float percent) { if (def == sel || percent == 0) diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index 95b8268fd8..fcabd31565 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -166,7 +166,7 @@ struct GridNinePatchProperties struct GridTileProperties { Vector2f Size; - Vector2f Padding; + Vector4f Padding; std::string SelectionMode; GridNinePatchProperties Background; diff --git a/es-core/src/components/NinePatchComponent.cpp b/es-core/src/components/NinePatchComponent.cpp index b04fbc90a1..5fbd7c46c0 100644 --- a/es-core/src/components/NinePatchComponent.cpp +++ b/es-core/src/components/NinePatchComponent.cpp @@ -167,9 +167,32 @@ void NinePatchComponent::render(const Transform4x4f& parentTrans) } else if (mTexture->bind()) { + if (mAnimateTiming > 0) + { + float opacity = mOpacity / 255.0; + + unsigned int e = mEdgeColor; + unsigned int c = mCenterColor; + + float percent = std::abs(mAnimateTiming - mTimer) / mAnimateTiming; + e = Renderer::mixColors(e, mAnimateColor, percent); + c = Renderer::mixColors(c, mAnimateColor, percent); + + const unsigned int edgeColor = Renderer::convertColor(e & 0xFFFFFF00 | (unsigned char)((e & 0xFF) * opacity)); + const unsigned int centerColor = Renderer::convertColor(c & 0xFFFFFF00 | (unsigned char)((c & 0xFF) * opacity)); + + for (int i = 0; i < 6 * 9; i++) + mVertices[i].col = edgeColor; + + for (int i = 0; i < 6; i++) + mVertices[(4 * 6) + i].col = centerColor; + } + Renderer::setMatrix(trans); Renderer::drawTriangleStrips(&mVertices[0], 6 * 9); Renderer::bindTexture(0); + + updateColors(); } renderChildren(trans); From 76ccc620d43ca6b97048438029b4acb6eaff7ee0 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 27 Nov 2019 10:25:11 +0100 Subject: [PATCH 247/336] Gridview : text, marquee, favorite & overlay can now be zOrdered --- es-core/src/components/GridTileComponent.cpp | 33 ++++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index ae21b43bce..18631f5d01 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -36,6 +36,8 @@ GridTileComponent::GridTileComponent(Window* window) : GuiComponent(window), mBa mImage = new ImageComponent(mWindow); mImage->setOrigin(0.5f, 0.5f); + mLabel.setDefaultZIndex(10); + addChild(&mBackground); addChild(&(*mImage)); addChild(&mLabel); @@ -334,20 +336,25 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) if (!mLabelMerged && isMinSize) Renderer::popClipRect(); - + + std::vector zOrdered; + if (mMarquee != nullptr && mMarquee->hasImage()) - mMarquee->render(trans); - else if (currentProperties.Label.Visible && currentProperties.Label.size.y()>0) - mLabel.render(trans); - else if (!currentProperties.Label.Visible && mIsDefaultImage) - mLabel.render(trans); + zOrdered.push_back(mMarquee); + else + zOrdered.push_back(&mLabel); if (mFavorite != nullptr && mFavorite->hasImage() && mFavorite->isVisible()) - mFavorite->render(trans); + zOrdered.push_back(mFavorite); if (mImageOverlay != nullptr && mImageOverlay->hasImage() && mImageOverlay->isVisible()) - mImageOverlay->render(trans); - + zOrdered.push_back(mImageOverlay); + + std::stable_sort(zOrdered.begin(), zOrdered.end(), [](GuiComponent* a, GuiComponent* b) { return b->getZIndex() > a->getZIndex(); }); + + for (auto comp : zOrdered) + comp->render(trans); + if (mLabelMerged && isMinSize) Renderer::popClipRect(); } @@ -368,7 +375,7 @@ void GridTileComponent::createMarquee() mMarquee = new ImageComponent(mWindow); mMarquee->setOrigin(0.5f, 0.5f); - mMarquee->setDefaultZIndex(35); + mMarquee->setDefaultZIndex(20); addChild(mMarquee); } @@ -379,7 +386,7 @@ void GridTileComponent::createFavorite() mFavorite = new ImageComponent(mWindow); mFavorite->setOrigin(0.5f, 0.5f); - mFavorite->setDefaultZIndex(35); + mFavorite->setDefaultZIndex(15); mFavorite->setVisible(false); addChild(mFavorite); @@ -392,7 +399,7 @@ void GridTileComponent::createImageOverlay() mImageOverlay = new ImageComponent(mWindow); mImageOverlay->setOrigin(0.5f, 0.5f); - mImageOverlay->setDefaultZIndex(35); + mImageOverlay->setDefaultZIndex(25); mImageOverlay->setVisible(false); addChild(mImageOverlay); @@ -408,7 +415,7 @@ void GridTileComponent::createVideo() // video mVideo->setOrigin(0.5f, 0.5f); mVideo->setStartDelay(VIDEODELAY); - mVideo->setDefaultZIndex(30); + mVideo->setDefaultZIndex(11); addChild(mVideo); } From a591d978fa8c9958d4c0fdbf8907773a126c23e0 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 27 Nov 2019 11:19:52 +0100 Subject: [PATCH 248/336] SystemView : Add support for 'defaultTransition' and 'scrollSound' --- es-app/src/guis/GuiMenu.cpp | 5 +++ es-app/src/views/SystemView.cpp | 47 +++++++++++++---------------- es-app/src/views/SystemView.h | 3 ++ es-app/src/views/ViewController.cpp | 2 +- es-core/src/Settings.cpp | 2 +- es-core/src/ThemeData.cpp | 4 ++- 6 files changed, 34 insertions(+), 29 deletions(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 228cec1c53..5178a19517 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -725,12 +725,17 @@ void GuiMenu::openUISettings() // transition style auto transition_style = std::make_shared< OptionListComponent >(mWindow, _("TRANSITION STYLE"), false); std::vector transitions; + transitions.push_back("auto"); transitions.push_back("fade"); transitions.push_back("slide"); transitions.push_back("instant"); + for (auto it = transitions.cbegin(); it != transitions.cend(); it++) transition_style->add(_(*it), *it, Settings::getInstance()->getString("TransitionStyle") == *it); + if (!transition_style->hasSelection()) + transition_style->selectFirstItem(); + s->addWithLabel(_("TRANSITION STYLE"), transition_style); s->addSaveFunc([transition_style] { if (Settings::getInstance()->getString("TransitionStyle") == "instant" diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 717e95c05a..78ed2fe4de 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -171,7 +171,7 @@ void SystemView::populate() logo->setImage(path, (elem->has("tile") && elem->get("tile")), MaxSizeInfo(carouselLogoSize() * mCarousel.logoScale)); } - if (mCarousel.size.x() != carouselLogoSize().x() & mCarousel.size.y() != carouselLogoSize().y()) + if (carouselSize().x() != carouselLogoSize().x() & carouselSize().y() != carouselLogoSize().y()) logo->setRotateByTargetSize(true); e.data.logo = std::shared_ptr(logo); @@ -405,6 +405,13 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) cancelAnimation(2); std::string transition_style = Settings::getInstance()->getString("TransitionStyle"); + if (transition_style == "auto") + { + if (mCarousel.defaultTransition == "instant" || mCarousel.defaultTransition == "fade" || mCarousel.defaultTransition == "slide") + transition_style = mCarousel.defaultTransition; + else + transition_style = "slide"; + } int systemInfoDelay = mCarousel.systemInfoDelay; bool goFast = transition_style == "instant" || systemInfoDelay == 0; @@ -456,6 +463,9 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) if (mLastCursor == mCursor) return; + if (!mCarousel.scrollSound.empty()) + Sound::get(mCarousel.scrollSound)->play(); + int oldCursor = mLastCursor; mLastCursor = mCursor; @@ -542,32 +552,9 @@ void SystemView::render(const Transform4x4f& parentTrans) Transform4x4f trans = getTransform() * parentTrans; - Vector2f clipPos(trans.translation().x(), trans.translation().y()); - if (!Renderer::isVisibleOnScreen(clipPos.x(), clipPos.y(), mSize.x(), mSize.y())) + if (!Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) return; - - if (mSize.x() != Renderer::getScreenWidth() || mSize.x() != Renderer::getScreenHeight()) - { - for (int i = 0; i < mEntries.size(); i++) - { - if (mEntries[i].data.logo) - { - Vector2f denormalized = carouselLogoSize() * mEntries[i].data.logo->getOrigin(); - mEntries[i].data.logo->setPosition(denormalized.x(), denormalized.y(), 0.0); - - if (mEntries[i].data.logoIsImage) - { - ImageComponent* img = (ImageComponent*)mEntries[i].data.logo.get(); - img->setMaxSize(carouselLogoSize() * mCarousel.logoScale); - } - else - mEntries[i].data.logo->setSize(carouselLogoSize() * mCarousel.logoScale); - } - } - } - - - + auto systemInfoZIndex = mSystemInfo.getZIndex(); auto minMax = std::minmax(mCarousel.zIndex, systemInfoZIndex); @@ -890,6 +877,8 @@ void SystemView::getDefaultElements(void) mCarousel.maxLogoCount = 3; mCarousel.zIndex = 40; mCarousel.systemInfoDelay = 2000; + mCarousel.scrollSound = ""; + mCarousel.defaultTransition = ""; // System Info Bar mSystemInfo.setSize(mSize.x(), mSystemInfo.getFont()->getLetterHeight()*2.2f); @@ -1007,6 +996,12 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) } if (elem->has("systemInfoDelay")) mCarousel.systemInfoDelay = elem->get("systemInfoDelay"); + + if (elem->has("scrollSound")) + mCarousel.scrollSound = elem->get("scrollSound"); + + if (elem->has("defaultTransition")) + mCarousel.defaultTransition = elem->get("defaultTransition"); } void SystemView::onShow() diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index b70f5c3ebf..13c5be4ed9 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -45,6 +45,9 @@ struct SystemViewCarousel Vector2f logoPos; float zIndex; float systemInfoDelay; + + std::string defaultTransition; + std::string scrollSound; }; class SystemView : public IList diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index be1728b576..854200d965 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -209,7 +209,7 @@ void ViewController::playViewTransition(bool forceImmediate) else advanceAnimation(0, (int)(mFadeOpacity * FADE_DURATION)); } - else if (!forceImmediate && transition_style == "slide") + else if (!forceImmediate && (transition_style == "slide" || transition_style == "auto")) { // slide or simple slide setAnimation(new MoveCameraAnimation(mCamera, target)); diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 75a19d467e..0aa65f65ee 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -136,7 +136,7 @@ void Settings::setDefaults() mBoolMap["HideWindow"] = true; #endif mStringMap["GameTransitionStyle"] = "fade"; - mStringMap["TransitionStyle"] = "slide"; + mStringMap["TransitionStyle"] = "auto"; mStringMap["Language"] = "en"; mStringMap["ThemeSet"] = ""; mStringMap["ScreenSaverBehavior"] = "dim"; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index b4e87cafc7..d49ebc7ac3 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -227,7 +227,9 @@ std::map> The { "logoPos", NORMALIZED_PAIR }, { "logoAlignment", STRING }, { "maxLogoCount", FLOAT }, - { "systemInfoDelay", FLOAT }, + { "systemInfoDelay", FLOAT }, + { "defaultTransition", STRING }, + { "scrollSound", PATH }, { "zIndex", FLOAT } } }, { "menuText", { { "fontPath", PATH }, From 5bbd7f05a2a0aa959d6a2249019db98aec88d1b5 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 27 Nov 2019 19:24:03 +0100 Subject: [PATCH 249/336] SystemView : reworked Fade transition ( to use with systemcarousel.defaultTransition ) --- es-app/src/FileData.cpp | 47 +++++-- es-app/src/FileData.h | 8 +- es-app/src/views/SystemView.cpp | 142 ++++++++++++++++++++-- es-app/src/views/SystemView.h | 1 + es-core/src/components/ImageComponent.cpp | 5 + es-core/src/components/ImageComponent.h | 3 + 6 files changed, 183 insertions(+), 23 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 1acabfe3f6..80d178abea 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -73,7 +73,7 @@ std::string FileData::getCleanName() const return Utils::String::removeParenthesis(this->getDisplayName()); } -const std::string FileData::getThumbnailPath() const +const std::string FileData::getThumbnailPath() { std::string thumbnail = getMetadata().get("thumbnail"); @@ -90,8 +90,32 @@ const std::string FileData::getThumbnailPath() const { if(thumbnail.empty()) { - std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; - if(Utils::FileSystem::exists(path)) + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-thumb" + extList[i]; + if (Utils::FileSystem::exists(path)) + { + setMetadata("thumbnail", path); + thumbnail = path; + } + } + } + } + + if (thumbnail.empty()) + thumbnail = getMetadata().get("image"); + + // no image, try to use local image + if (thumbnail.empty() && Settings::getInstance()->getBool("LocalArt")) + { + const char* extList[2] = { ".png", ".jpg" }; + for (int i = 0; i < 2; i++) + { + if (thumbnail.empty()) + { + std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; + if (!Utils::FileSystem::exists(path)) + path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + extList[i]; + + if (Utils::FileSystem::exists(path)) thumbnail = path; } } @@ -150,7 +174,7 @@ const std::string FileData::getEmulator() const return getMetadata().get("emulator"); } -const std::string FileData::getVideoPath() const +const std::string FileData::getVideoPath() { std::string video = getMetadata().get("video"); @@ -159,13 +183,16 @@ const std::string FileData::getVideoPath() const { std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-video.mp4"; if (Utils::FileSystem::exists(path)) + { + setMetadata("video", path); video = path; + } } return video; } -const std::string FileData::getMarqueePath() const +const std::string FileData::getMarqueePath() { std::string marquee = getMetadata().get("marquee"); @@ -179,7 +206,10 @@ const std::string FileData::getMarqueePath() const { std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-marquee" + extList[i]; if(Utils::FileSystem::exists(path)) + { + setMetadata("marquee", path); marquee = path; + } } } } @@ -187,7 +217,7 @@ const std::string FileData::getMarqueePath() const return marquee; } -const std::string FileData::getImagePath() const +const std::string FileData::getImagePath() { std::string image = getMetadata().get("image"); @@ -201,7 +231,10 @@ const std::string FileData::getImagePath() const { std::string path = getSystemEnvData()->mStartPath + "/images/" + getDisplayName() + "-image" + extList[i]; if(Utils::FileSystem::exists(path)) - image = path; + { + setMetadata("image", path); + image = path; + } } } } diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index c8fc23d464..e0c2e0780f 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -47,10 +47,10 @@ class FileData virtual SystemEnvironmentData* getSystemEnvData() const; - virtual const std::string getThumbnailPath() const; - virtual const std::string getVideoPath() const; - virtual const std::string getMarqueePath() const; - virtual const std::string getImagePath() const; + virtual const std::string getThumbnailPath(); + virtual const std::string getVideoPath(); + virtual const std::string getMarqueePath(); + virtual const std::string getImagePath(); virtual const std::string getCore() const; virtual const std::string getEmulator() const; diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 78ed2fe4de..a3033ec756 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -28,7 +28,8 @@ SystemView::SystemView(Window* window) : IList(wind mShowing = false; mLastCursor = 0; mStaticBackground = nullptr; - + mExtrasFadeOldCursor = -1; + setSize((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); populate(); } @@ -475,8 +476,10 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) { float startExtrasFade = mExtrasFadeOpacity; anim = new LambdaAnimation( - [this, startExtrasFade, startPos, endPos, posMax, move_carousel](float t) + [this, startExtrasFade, startPos, endPos, posMax, move_carousel, oldCursor](float t) { + mExtrasFadeOldCursor = oldCursor; + t -= 1; float f = Math::lerp(startPos, endPos, t*t*t + 1); if(f < 0) @@ -487,14 +490,22 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) this->mCamOffset = move_carousel ? f : endPos; t += 1; + /* if(t < 0.3f) this->mExtrasFadeOpacity = Math::lerp(0.0f, 1.0f, t / 0.3f + startExtrasFade); else if(t < 0.7f) this->mExtrasFadeOpacity = 1.0f; else this->mExtrasFadeOpacity = Math::lerp(1.0f, 0.0f, (t - 0.7f) / 0.3f); + */ - if(t > 0.5f) + if (t < 0.3f) + this->mExtrasFadeOpacity = 1.0f; + else + this->mExtrasFadeOpacity = Math::lerp(1.0f, 0.0f, (t - 0.3f) / 0.7f); + //this->mExtrasFadeOpacity = Math::lerp(1.0f, 0.0f, t); + + //if(t > 0.5f) this->mExtrasCamOffset = endPos; }, 500); @@ -538,6 +549,8 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) setAnimation(anim, 0, [this] { + mExtrasFadeOldCursor = -1; + for (int i = 0; i < mEntries.size(); i++) if (i != mCursor) activateExtras(i, false); @@ -782,6 +795,8 @@ void SystemView::renderInfoBar(const Transform4x4f& trans) mSystemInfo.render(trans); } +#include + // Draw background extras void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upper) { @@ -793,7 +808,69 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp if (Settings::getInstance()->getBool("FixedCarousel")) Renderer::pushClipRect(Vector2i((int)mPosition.x(), (int)mPosition.y()), Vector2i((int)mSize.x(), (int)mSize.y())); else - Renderer::pushClipRect(Vector2i(0, 0), Vector2i((int)mSize.x(), (int)mSize.y())); + Renderer::pushClipRect(Vector2i::Zero(), Vector2i((int)mSize.x(), (int)mSize.y())); + + std::unordered_set allPaths; + std::unordered_set paths; + std::unordered_set allValues; + std::unordered_set values; + + if (mExtrasFadeOpacity && mExtrasFadeOldCursor >= 0 && mExtrasFadeOldCursor < mEntries.size() && mExtrasFadeOldCursor != mCursor) + { + // ExtrasFadeOpacity : Collect images paths & text values + // paths & values must have only the elements that are not common + if (mCursor >= 0 && mCursor < mEntries.size()) + { + for (GuiComponent *extra : mEntries.at(mCursor).data.backgroundExtras) + { + if (extra->getZIndex() < lower || extra->getZIndex() >= upper) + continue; + + std::string value = extra->getValue(); + if (value == "ImageComponent") + paths.insert(Utils::FileSystem::getCanonicalPath(((ImageComponent*)extra)->getImagePath())); + else if (value != "VideoComponent") + values.insert(value); + } + + allValues = values; + allPaths = paths; + + for (GuiComponent *extra : mEntries.at(mExtrasFadeOldCursor).data.backgroundExtras) + { + if (extra->getZIndex() < lower || extra->getZIndex() >= upper) + continue; + + std::string value = extra->getValue(); + if (value == "ImageComponent") + paths.erase(Utils::FileSystem::getCanonicalPath(((ImageComponent*)extra)->getImagePath())); + else if (value != "VideoComponent") + values.erase(value); + } + } + + Renderer::pushClipRect(Vector2i((int)trans.translation()[0], (int)trans.translation()[1]), Vector2i((int)mSize.x(), (int)mSize.y())); + + // ExtrasFadeOpacity : Render only items with different paths or values + for (GuiComponent *extra : mEntries.at(mExtrasFadeOldCursor).data.backgroundExtras) + { + if (extra->getZIndex() < lower || extra->getZIndex() >= upper) + continue; + + std::string value = extra->getValue(); + if (value == "ImageComponent") + { + if (allPaths.find(Utils::FileSystem::getCanonicalPath(((ImageComponent*)extra)->getImagePath())) == allPaths.cend()) + extra->render(trans); + else if (((ImageComponent*)extra)->isTiled() && extra->getPosition() == Vector3f::Zero() && extra->getSize() == Vector2f(Renderer::getScreenWidth(), Renderer::getScreenHeight())) + extra->render(trans); + } + else if (allValues.find(value) == allValues.cend()) + extra->render(trans); + } + + Renderer::popClipRect(); + } for (int i = extrasCenter + logoBuffersLeft[bufferIndex]; i <= extrasCenter + logoBuffersRight[bufferIndex]; i++) { @@ -803,6 +880,9 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp while (index >= (int)mEntries.size()) index -= (int)mEntries.size(); + if (mExtrasFadeOpacity && (index == mExtrasFadeOldCursor || index != mCursor)) + continue; + //Only render selected system when not showing if (mShowing || index == mCursor) { @@ -826,15 +906,53 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp extrasTrans.translate(Vector3f(0,(i - mExtrasCamOffset) * mSize.y(), 0)); } - Renderer::pushClipRect(Vector2i((int)extrasTrans.translation()[0], (int)extrasTrans.translation()[1]), - Vector2i((int)mSize.x(), (int)mSize.y())); - SystemViewData data = mEntries.at(index).data; - for (unsigned int j = 0; j < data.backgroundExtras.size(); j++) { - GuiComponent *extra = data.backgroundExtras[j]; - if (extra->getZIndex() >= lower && extra->getZIndex() < upper) { - extra->render(extrasTrans); + if (mExtrasFadeOpacity && mExtrasFadeOldCursor == index) + extrasTrans = trans; + + Renderer::pushClipRect(Vector2i((int)extrasTrans.translation()[0], (int)extrasTrans.translation()[1]),Vector2i((int)mSize.x(), (int)mSize.y())); + + for (GuiComponent* extra : mEntries.at(index).data.backgroundExtras) + { + if (extra->getZIndex() < lower || extra->getZIndex() >= upper) + continue; + + // ExtrasFadeOpacity : Apply opacity only on elements that are not common with the original view + if (mExtrasFadeOpacity) + { + std::string value = extra->getValue(); + if (value == "ImageComponent") + { + std::string imagePath = Utils::FileSystem::getCanonicalPath(((ImageComponent*)extra)->getImagePath()); + if (paths.find(imagePath) != paths.cend()) + { + auto opa = extra->getOpacity(); + extra->setOpacity((1.0f - mExtrasFadeOpacity) * opa); + extra->render(extrasTrans); + extra->setOpacity(opa); + continue; + } + else if (((ImageComponent*)extra)->isTiled() && extra->getPosition() == Vector3f::Zero() && extra->getSize() == Vector2f(Renderer::getScreenWidth(), Renderer::getScreenHeight())) + { + auto opa = extra->getOpacity(); + extra->setOpacity((1.0f - mExtrasFadeOpacity) * opa); + extra->render(extrasTrans); + extra->setOpacity(opa); + continue; + } + } + else if (values.find(value) != values.cend()) + { + auto opa = extra->getOpacity(); + extra->setOpacity((1.0f - mExtrasFadeOpacity) * opa); + extra->render(extrasTrans); + extra->setOpacity(opa); + continue; + } } + + extra->render(extrasTrans); } + Renderer::popClipRect(); } } @@ -848,7 +966,7 @@ void SystemView::renderFade(const Transform4x4f& trans) { unsigned int fadeColor = 0x00000000 | (unsigned char)(mExtrasFadeOpacity * 255); Renderer::setMatrix(trans); - Renderer::drawRect(mPosition.x(), mPosition.y(), mSize.x(), mSize.y(), fadeColor); + //Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), fadeColor, fadeColor); } } diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index 13c5be4ed9..39f5a9f543 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -108,6 +108,7 @@ class SystemView : public IList float mCamOffset; float mExtrasCamOffset; float mExtrasFadeOpacity; + int mExtrasFadeOldCursor; bool mViewNeedsReload; bool mShowing; diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 888d003b4e..e8714e5c53 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -745,3 +745,8 @@ void ImageComponent::update(int deltaTime) } } } + +bool ImageComponent::isTiled() +{ + return mTexture != nullptr && mTexture->isTiled(); +} \ No newline at end of file diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index a759a63067..5bafd5034e 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -121,6 +121,9 @@ class ImageComponent : public GuiComponent void setPlaylist(std::shared_ptr playList); + std::string getImagePath() { return mPath; } + bool isTiled(); + private: Vector2f mTargetSize; From 9e27d629090f2a05a3e9945f51dec1296d3c7f15 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 28 Nov 2019 00:17:53 +0100 Subject: [PATCH 250/336] GriView : Small fix when minSize is used without a label --- es-core/src/components/GridTileComponent.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 18631f5d01..c5399866c6 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -241,7 +241,7 @@ void GridTileComponent::resize() { if (currentProperties.Image.sizeMode == "minSize") { - if (!mLabelMerged) + if (!mLabelMerged && currentProperties.Label.Visible) bkSize = Vector2f(size.x(), size.y() - bottomPadding + topPadding); } else From 971386758bd7d4fe4beb815d7960b63d33bc0eb3 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 28 Nov 2019 00:28:01 +0100 Subject: [PATCH 251/336] Scraper: Fix for OpenBor + Support for Solarus --- es-app/src/PlatformId.cpp | 8 +++++--- es-app/src/PlatformId.h | 1 + es-app/src/scrapers/ScreenScraper.cpp | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/es-app/src/PlatformId.cpp b/es-app/src/PlatformId.cpp index e27eb6a781..46fb1b9cfd 100644 --- a/es-app/src/PlatformId.cpp +++ b/es-app/src/PlatformId.cpp @@ -2,6 +2,7 @@ #include #include +#include "utils/StringUtil.h" namespace PlatformIds { @@ -109,12 +110,13 @@ namespace PlatformIds { "oric", ORICATMOS }, { "thomson", THOMSON_TO_MO }, { "samcoupe", SAMCOUPE }, - { "OpenBOR", OPENBOR }, + { "openbor", OPENBOR }, { "uzebox", UZEBOX }, { "apple2gs", APPLE2GS }, - { "Spectravideo", SPECTRAVIDEO }, + { "spectravideo", SPECTRAVIDEO }, { "palm", PALMOS }, { "daphne", DAPHNEE }, + { "solarus", SOLARUS }, { "ignore", PLATFORM_IGNORE }, { "invalid", PLATFORM_COUNT } @@ -125,7 +127,7 @@ namespace PlatformIds if (str == nullptr) return PLATFORM_UNKNOWN; - auto it = Platforms.find(str); + auto it = Platforms.find(Utils::String::toLower(str).c_str()); if (it != Platforms.end()) return (*it).second; diff --git a/es-app/src/PlatformId.h b/es-app/src/PlatformId.h index affc6bee4c..59c2ad5864 100644 --- a/es-app/src/PlatformId.h +++ b/es-app/src/PlatformId.h @@ -107,6 +107,7 @@ namespace PlatformIds SPECTRAVIDEO, PALMOS, DAPHNEE, + SOLARUS, PLATFORM_IGNORE, // do not allow scraping for this system PLATFORM_COUNT diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index 2a32972039..9e27ccb96d 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -126,7 +126,7 @@ const std::map screenscraper_platformid_map{ { SPECTRAVIDEO, 218 }, { PALMOS, 219 }, { DAPHNEE, 49 }, - + { SOLARUS, 223 } }; From aaced3dd52303456ec39b9f2f3e7710b88668383 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 28 Nov 2019 13:07:11 +0100 Subject: [PATCH 252/336] GuiComponents : simplification for type comparison --- es-app/src/views/SystemView.cpp | 45 +++++++++++------------ es-core/src/GuiComponent.h | 3 ++ es-core/src/Window.cpp | 3 +- es-core/src/components/ImageComponent.cpp | 12 +++--- es-core/src/components/ImageComponent.h | 2 +- es-core/src/components/VideoComponent.h | 8 +++- 6 files changed, 42 insertions(+), 31 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index a3033ec756..8a8f8077f3 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -232,17 +232,17 @@ void SystemView::populate() // make background extras e.data.backgroundExtras = ThemeData::makeExtras((*it)->getTheme(), "system", mWindow); - for (auto bx : e.data.backgroundExtras) + for (auto extra : e.data.backgroundExtras) { - if (bx->getValue() == "VideoComponent") + if (extra->isKindOf()) { - auto elem = (*it)->getTheme()->getElement("system", bx->getTag(), "video"); + auto elem = (*it)->getTheme()->getElement("system", extra->getTag(), "video"); if (elem != nullptr && elem->has("path") && Utils::String::startsWith(elem->get("path"), "{random")) - ((VideoComponent*)bx)->setPlaylist(std::make_shared(*it, SystemRandomPlaylist::VIDEO)); + ((VideoComponent*)extra)->setPlaylist(std::make_shared(*it, SystemRandomPlaylist::VIDEO)); } - else if (bx->getValue() == "ImageComponent") + else if (extra->isKindOf()) { - auto elem = (*it)->getTheme()->getElement("system", bx->getTag(), "image"); + auto elem = (*it)->getTheme()->getElement("system", extra->getTag(), "image"); if (elem != nullptr && elem->has("path") && Utils::String::startsWith(elem->get("path"), "{random")) { std::string src = elem->get("path"); @@ -254,7 +254,7 @@ void SystemView::populate() else if (src == "{random:marquee}") type = SystemRandomPlaylist::MARQUEE; - ((ImageComponent*)bx)->setPlaylist(std::make_shared(*it, type)); + ((ImageComponent*)extra)->setPlaylist(std::make_shared(*it, type)); } } } @@ -821,30 +821,30 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp // paths & values must have only the elements that are not common if (mCursor >= 0 && mCursor < mEntries.size()) { - for (GuiComponent *extra : mEntries.at(mCursor).data.backgroundExtras) + for (GuiComponent* extra : mEntries.at(mCursor).data.backgroundExtras) { if (extra->getZIndex() < lower || extra->getZIndex() >= upper) continue; std::string value = extra->getValue(); - if (value == "ImageComponent") - paths.insert(Utils::FileSystem::getCanonicalPath(((ImageComponent*)extra)->getImagePath())); - else if (value != "VideoComponent") + if (extra->isKindOf()) + paths.insert(value); + else if (extra->isKindOf()) values.insert(value); } allValues = values; allPaths = paths; - for (GuiComponent *extra : mEntries.at(mExtrasFadeOldCursor).data.backgroundExtras) + for (GuiComponent* extra : mEntries.at(mExtrasFadeOldCursor).data.backgroundExtras) { if (extra->getZIndex() < lower || extra->getZIndex() >= upper) continue; std::string value = extra->getValue(); - if (value == "ImageComponent") - paths.erase(Utils::FileSystem::getCanonicalPath(((ImageComponent*)extra)->getImagePath())); - else if (value != "VideoComponent") + if (extra->isKindOf()) + paths.erase(value); + else if (extra->isKindOf()) values.erase(value); } } @@ -852,20 +852,20 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp Renderer::pushClipRect(Vector2i((int)trans.translation()[0], (int)trans.translation()[1]), Vector2i((int)mSize.x(), (int)mSize.y())); // ExtrasFadeOpacity : Render only items with different paths or values - for (GuiComponent *extra : mEntries.at(mExtrasFadeOldCursor).data.backgroundExtras) + for (GuiComponent* extra : mEntries.at(mExtrasFadeOldCursor).data.backgroundExtras) { if (extra->getZIndex() < lower || extra->getZIndex() >= upper) continue; std::string value = extra->getValue(); - if (value == "ImageComponent") + if (extra->isKindOf()) { - if (allPaths.find(Utils::FileSystem::getCanonicalPath(((ImageComponent*)extra)->getImagePath())) == allPaths.cend()) + if (allPaths.find(value) == allPaths.cend()) extra->render(trans); else if (((ImageComponent*)extra)->isTiled() && extra->getPosition() == Vector3f::Zero() && extra->getSize() == Vector2f(Renderer::getScreenWidth(), Renderer::getScreenHeight())) extra->render(trans); } - else if (allValues.find(value) == allValues.cend()) + else if (extra->isKindOf() && allValues.find(value) == allValues.cend()) extra->render(trans); } @@ -920,10 +920,9 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp if (mExtrasFadeOpacity) { std::string value = extra->getValue(); - if (value == "ImageComponent") + if (extra->isKindOf()) { - std::string imagePath = Utils::FileSystem::getCanonicalPath(((ImageComponent*)extra)->getImagePath()); - if (paths.find(imagePath) != paths.cend()) + if (paths.find(value) != paths.cend()) { auto opa = extra->getOpacity(); extra->setOpacity((1.0f - mExtrasFadeOpacity) * opa); @@ -940,7 +939,7 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp continue; } } - else if (values.find(value) != values.cend()) + else if (extra->isKindOf() && values.find(value) != values.cend()) { auto opa = extra->getOpacity(); extra->setOpacity((1.0f - mExtrasFadeOpacity) * opa); diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 070755c942..56d8afd940 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -35,6 +35,9 @@ class GuiComponent GuiComponent(Window* window); virtual ~GuiComponent(); + template + bool isKindOf() { return (dynamic_cast(this) != nullptr); } + static bool ALLOWANIMATIONS; virtual void textInput(const char* text); diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 90bbf271ba..de142430d2 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -13,6 +13,7 @@ #include #include "guis/GuiInfoPopup.h" #include "components/AsyncNotificationComponent.h" +#include "guis/GuiMsgBox.h" Window::Window() : mNormalizeNextUpdate(false), mFrameTimeElapsed(0), mFrameCountElapsed(0), mAverageDeltaTime(10), mAllowSleep(true), mSleeping(false), mTimeSinceLastInput(0), mScreenSaver(NULL), mRenderScreenSaver(false), mInfoPopup(NULL), mClockElapsed(0) // batocera @@ -315,7 +316,7 @@ void Window::render() bottom->render(transform); if(bottom != top) { - if (top->getValue() == "GuiMsgBox" && mGuiStack.size() > 2) + if (top->isKindOf() && mGuiStack.size() > 2) { auto& middle = mGuiStack.at(mGuiStack.size() - 2); if (middle != bottom) diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index e8714e5c53..48c1fe8846 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -7,6 +7,7 @@ #include "ThemeData.h" #include "resources/TextureData.h" +#include "utils/FileSystemUtil.h" Vector2i ImageComponent::getTextureSize() const { @@ -153,17 +154,18 @@ void ImageComponent::setDefaultImage(std::string path) void ImageComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) { - if (mPath == path) + std::string canonicalPath = Utils::FileSystem::getCanonicalPath(path); + if (mPath == canonicalPath) return; - - mPath = path; + + mPath = canonicalPath; // If the previous image is in the async queue, remove it TextureResource::cancelAsync(mLoadingTexture); TextureResource::cancelAsync(mTexture); mLoadingTexture.reset(); - if (path.empty() || !ResourceManager::getInstance()->fileExists(path)) + if (mPath.empty() || !ResourceManager::getInstance()->fileExists(mPath)) { if (mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) mTexture.reset(); @@ -172,7 +174,7 @@ void ImageComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) } else { - std::shared_ptr texture = TextureResource::get(path, tile, mForceLoad, mDynamic, true, maxSize); + std::shared_ptr texture = TextureResource::get(mPath, tile, mForceLoad, mDynamic, true, maxSize); if (!mForceLoad && mDynamic && !mAllowFading && texture != nullptr && !texture->isLoaded()) mLoadingTexture = texture; diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index 5bafd5034e..cd01709962 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -20,7 +20,7 @@ class ImageComponent : public GuiComponent ImageComponent(Window* window, bool forceLoad = false, bool dynamic = true); virtual ~ImageComponent(); - std::string getValue() const override { return "ImageComponent"; } + std::string getValue() const override { return mPath; } void setDefaultImage(std::string path); diff --git a/es-core/src/components/VideoComponent.h b/es-core/src/components/VideoComponent.h index b4db4aeb2a..04157958c2 100644 --- a/es-core/src/components/VideoComponent.h +++ b/es-core/src/components/VideoComponent.h @@ -37,7 +37,13 @@ class VideoComponent : public GuiComponent VideoComponent(Window* window); virtual ~VideoComponent(); - std::string getValue() const override { return "VideoComponent"; } + std::string getValue() const override + { + if (mPlayingVideoPath.empty()) + return mPlayingVideoPath; + + return mVideoPath; + } // Loads the video at the given filepath bool setVideo(std::string path); From 88d8f4fb14a8252513a99ebe9df214c019877b5a Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 28 Nov 2019 13:52:55 +0100 Subject: [PATCH 253/336] SystemView : remove useless FixedCarousel management I added for testings purposes --- es-app/src/views/SystemView.cpp | 259 +++++++++++++--------------- es-app/src/views/SystemView.h | 7 - es-app/src/views/ViewController.cpp | 34 +--- es-core/src/Settings.cpp | 7 - 4 files changed, 123 insertions(+), 184 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 8a8f8077f3..b484a540b6 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -24,7 +24,7 @@ SystemView::SystemView(Window* window) : IList(wind mExtrasFadeOpacity = 0.0f; mLastSystem = nullptr; mScreensaverActive = false; - mDisable = false; + mDisable = false; mShowing = false; mLastCursor = 0; mStaticBackground = nullptr; @@ -133,11 +133,11 @@ void SystemView::populate() { clearEntries(); - for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) + for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { const std::shared_ptr& theme = (*it)->getTheme(); - if (mViewNeedsReload) + if(mViewNeedsReload) getViewElements(theme); if (!(*it)->isVisible()) @@ -146,7 +146,6 @@ void SystemView::populate() Entry e; e.name = (*it)->getName(); e.object = *it; - e.data.logoIsImage = false; // make logo const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); @@ -160,7 +159,7 @@ void SystemView::populate() { // Remove dynamic flags for png & jpg files : themes can contain oversized images that can't be unloaded by the TextureResource manager ImageComponent* logo = new ImageComponent(mWindow, false, Utils::String::toLower(Utils::FileSystem::getExtension(path)) != ".svg"); - logo->setMaxSize(carouselLogoSize() * mCarousel.logoScale); + logo->setMaxSize(mCarousel.logoSize * mCarousel.logoScale); logo->applyTheme(theme, "system", "logo", ThemeFlags::COLOR | ThemeFlags::ALIGNMENT | ThemeFlags::VISIBLE); // ThemeFlags::PATH | // Process here to be enable to set max picture size @@ -169,14 +168,13 @@ void SystemView::populate() { auto path = elem->get("path"); if (Utils::FileSystem::exists(path)) - logo->setImage(path, (elem->has("tile") && elem->get("tile")), MaxSizeInfo(carouselLogoSize() * mCarousel.logoScale)); + logo->setImage(path, (elem->has("tile") && elem->get("tile")), MaxSizeInfo(mCarousel.logoSize * mCarousel.logoScale)); } - if (carouselSize().x() != carouselLogoSize().x() & carouselSize().y() != carouselLogoSize().y()) + if (mCarousel.size.x() != mCarousel.logoSize.x() & mCarousel.size.y() != mCarousel.logoSize.y()) logo->setRotateByTargetSize(true); e.data.logo = std::shared_ptr(logo); - e.data.logoIsImage = true; } } @@ -188,7 +186,7 @@ void SystemView::populate() Font::get(FONT_SIZE_LARGE), 0x000000FF, ALIGN_CENTER); - text->setSize(carouselLogoSize() * mCarousel.logoScale); + text->setSize(mCarousel.logoSize * mCarousel.logoScale); text->applyTheme((*it)->getTheme(), "system", "logoText", ThemeFlags::FONT_PATH | ThemeFlags::FONT_SIZE | ThemeFlags::COLOR | ThemeFlags::FORCE_UPPERCASE | ThemeFlags::LINE_SPACING | ThemeFlags::TEXT); e.data.logo = std::shared_ptr(text); @@ -197,7 +195,8 @@ void SystemView::populate() text->setHorizontalAlignment(mCarousel.logoAlignment); text->setVerticalAlignment(ALIGN_CENTER); } - else { + else + { text->setHorizontalAlignment(ALIGN_CENTER); text->setVerticalAlignment(mCarousel.logoAlignment); } @@ -222,7 +221,7 @@ void SystemView::populate() } - Vector2f denormalized = carouselLogoSize() * e.data.logo->getOrigin(); + Vector2f denormalized = mCarousel.logoSize * e.data.logo->getOrigin(); e.data.logo->setPosition(denormalized.x(), denormalized.y(), 0.0); // delete any existing extras for (auto extra : e.data.backgroundExtras) @@ -320,6 +319,34 @@ bool SystemView::input(InputConfig* config, Input input) listInput(1); return true; } + if (config->isMappedTo("pagedown", input)) + { + int cursor = mCursor + 10; + if (cursor < 0) + cursor += (int)mEntries.size(); + + if (cursor >= (int)mEntries.size()) + cursor -= (int)mEntries.size(); + + auto sd = mEntries.at(cursor).object; + ViewController::get()->goToSystemView(sd, true); + //listInput(10); + return true; + } + if (config->isMappedTo("pageup", input)) + { + int cursor = mCursor - 10; + if (cursor < 0) + cursor += (int)mEntries.size(); + if (cursor >= (int)mEntries.size()) + cursor -= (int)mEntries.size(); + + auto sd = mEntries.at(cursor).object; + ViewController::get()->goToSystemView(sd, true); + //listInput(-10); + return true; + } + break; case HORIZONTAL: case HORIZONTAL_WHEEL: @@ -334,6 +361,34 @@ bool SystemView::input(InputConfig* config, Input input) listInput(1); return true; } + if (config->isMappedTo("pagedown", input) && mEntries.size() > 10) + { + int cursor = mCursor + 10; + if (cursor < 0) + cursor += (int)mEntries.size(); + + if (cursor >= (int)mEntries.size()) + cursor -= (int)mEntries.size(); + + auto sd = mEntries.at(cursor).object; + ViewController::get()->goToSystemView(sd, true); + //listInput(10); + return true; + } + if (config->isMappedTo("pageup", input) && mEntries.size() > 10) + { + int cursor = mCursor - 10; + if (cursor < 0) + cursor += (int)mEntries.size(); + if (cursor >= (int)mEntries.size()) + cursor -= (int)mEntries.size(); + + auto sd = mEntries.at(cursor).object; + ViewController::get()->goToSystemView(sd, true); + //listInput(-10); + return true; + } + break; } @@ -354,7 +409,9 @@ bool SystemView::input(InputConfig* config, Input input) if(config->isMappedLike("left", input) || config->isMappedLike("right", input) || config->isMappedLike("up", input) || - config->isMappedLike("down", input)) + config->isMappedLike("down", input) || + config->isMappedLike("pagedown", input) || + config->isMappedLike("pageup", input)) listInput(0); if(!UIModeController::getInstance()->isUIModeKid() && config->isMappedTo("select", input) && Settings::getInstance()->getBool("ScreenSaverControls")) { @@ -415,8 +472,8 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) } int systemInfoDelay = mCarousel.systemInfoDelay; - bool goFast = transition_style == "instant" || systemInfoDelay == 0; + bool goFast = transition_style == "instant" || systemInfoDelay == 0; const float infoStartOpacity = mSystemInfo.getOpacity() / 255.f; Animation* infoFadeOut = new LambdaAnimation( @@ -449,18 +506,15 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) mSystemInfo.setOpacity((unsigned char)(Math::lerp(0.f, 1.f, t) * 255)); }, goFast ? 10 : 300); -// ViewController::get()->getGameListView(mEntries.at(mCursor).object); // fake preload - // wait 600ms to fade in - setAnimation(infoFadeIn, goFast ? 0 : systemInfoDelay, [this] { - ViewController::get()->getGameListView(mEntries.at(mCursor).object); // fake preload - }, false, 2); + setAnimation(infoFadeIn, goFast ? 0 : systemInfoDelay, nullptr, false, 2); + // fake preload + //setAnimation(infoFadeIn, goFast ? 0 : systemInfoDelay, [this] { ViewController::get()->getGameListView(mEntries.at(mCursor).object); }, false, 2); // no need to animate transition, we're not going anywhere (probably mEntries.size() == 1) if(endPos == mCamOffset && endPos == mExtrasCamOffset) return; - if (mLastCursor == mCursor) return; @@ -501,12 +555,12 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) if (t < 0.3f) this->mExtrasFadeOpacity = 1.0f; + else if (t >= 0.7f) + this->mExtrasFadeOpacity = 0.0f; else - this->mExtrasFadeOpacity = Math::lerp(1.0f, 0.0f, (t - 0.3f) / 0.7f); - //this->mExtrasFadeOpacity = Math::lerp(1.0f, 0.0f, t); + this->mExtrasFadeOpacity = Math::lerp(1.0f, 0.0f, (t - 0.3f) / 0.4f); - //if(t > 0.5f) - this->mExtrasCamOffset = endPos; + this->mExtrasCamOffset = endPos; }, 500); } else if (transition_style == "slide") { @@ -540,7 +594,7 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) this->mExtrasCamOffset = endPos; }, move_carousel ? 500 : 1); } - + for (int i = 0; i < mEntries.size(); i++) if (i != oldCursor && i != mCursor) activateExtras(i, false); @@ -554,7 +608,7 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) for (int i = 0; i < mEntries.size(); i++) if (i != mCursor) activateExtras(i, false); - + }, false, 0); } @@ -562,22 +616,21 @@ void SystemView::render(const Transform4x4f& parentTrans) { if (size() == 0 || !mVisible) return; // nothing to render - + Transform4x4f trans = getTransform() * parentTrans; if (!Renderer::isVisibleOnScreen(trans.translation().x(), trans.translation().y(), mSize.x(), mSize.y())) return; - + auto systemInfoZIndex = mSystemInfo.getZIndex(); auto minMax = std::minmax(mCarousel.zIndex, systemInfoZIndex); renderExtras(trans, INT16_MIN, minMax.first); + // renderFade(trans); if (mStaticBackground != nullptr) mStaticBackground->render(trans); - renderFade(trans); - if (mCarousel.zIndex > mSystemInfo.getZIndex()) { renderInfoBar(trans); } else { @@ -652,14 +705,14 @@ void SystemView::getViewElements(const std::shared_ptr& theme) { if (mStaticBackground == nullptr) mStaticBackground = new ImageComponent(mWindow, false); - + mStaticBackground->applyTheme(theme, "system", "staticBackground", ThemeFlags::ALL); } else if (mStaticBackground != nullptr) { delete mStaticBackground; mStaticBackground = nullptr; - } + } mViewNeedsReload = false; } @@ -669,25 +722,15 @@ void SystemView::renderCarousel(const Transform4x4f& trans) { // background box behind logos Transform4x4f carouselTrans = trans; - - if (Settings::getInstance()->getBool("FixedCarousel")) - carouselTrans.translate(Vector3f(mPosition.x() + carouselPos().x(), mPosition.y() + carouselPos().y(), 0.0)); - else - carouselTrans.translate(Vector3f(carouselPos().x(), carouselPos().y(), 0.0)); - - carouselTrans.translate(Vector3f(mCarousel.origin.x() * carouselSize().x() * -1, mCarousel.origin.y() * carouselSize().y() * -1, 0.0f)); + carouselTrans.translate(Vector3f(mCarousel.pos.x(), mCarousel.pos.y(), 0.0)); + carouselTrans.translate(Vector3f(mCarousel.origin.x() * mCarousel.size.x() * -1, mCarousel.origin.y() * mCarousel.size.y() * -1, 0.0f)); Vector2f clipPos(carouselTrans.translation().x(), carouselTrans.translation().y()); - Renderer::pushClipRect(Vector2i((int)clipPos.x(), (int)clipPos.y()), Vector2i((int)carouselSize().x(), (int)carouselSize().y())); + Renderer::pushClipRect(Vector2i((int)clipPos.x(), (int)clipPos.y()), Vector2i((int)mCarousel.size.x(), (int)mCarousel.size.y())); Renderer::setMatrix(carouselTrans); Renderer::drawRect(0.0f, 0.0f, mCarousel.size.x(), mCarousel.size.y(), mCarousel.color, mCarousel.colorEnd, mCarousel.colorGradientHorizontal); - if (mCarousel.colorEnd != 0) - Renderer::drawRect(0.0, 0.0, carouselSize().x(), carouselSize().y(), mCarousel.color, mCarousel.colorEnd, mCarousel.colorGradientHorizontal); - else - Renderer::drawRect(0.0, 0.0, carouselSize().x(), carouselSize().y(), mCarousel.color); - // draw logos Vector2f logoSpacing(0.0, 0.0); // NB: logoSpacing will include the size of the logo itself as well! float xOff = 0.0; @@ -696,53 +739,53 @@ void SystemView::renderCarousel(const Transform4x4f& trans) switch (mCarousel.type) { case VERTICAL_WHEEL: - yOff = (carouselSize().y() - carouselLogoSize().y()) / 2.f - (mCamOffset * logoSpacing[1]); + yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2.f - (mCamOffset * logoSpacing[1]); if (mCarousel.logoAlignment == ALIGN_LEFT) - xOff = carouselLogoSize().x() / 10.f; + xOff = mCarousel.logoSize.x() / 10.f; else if (mCarousel.logoAlignment == ALIGN_RIGHT) - xOff = carouselSize().x() - (carouselLogoSize().x() * 1.1f); + xOff = mCarousel.size.x() - (mCarousel.logoSize.x() * 1.1f); else - xOff = (carouselSize().x() - carouselLogoSize().x()) / 2.f; + xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2.f; break; case VERTICAL: - logoSpacing[1] = ((carouselSize().y() - (carouselLogoSize().y() * mCarousel.maxLogoCount)) / (mCarousel.maxLogoCount)) + carouselLogoSize().y(); - yOff = (carouselSize().y() - carouselLogoSize().y()) / 2.f - (mCamOffset * logoSpacing[1]); + logoSpacing[1] = ((mCarousel.size.y() - (mCarousel.logoSize.y() * mCarousel.maxLogoCount)) / (mCarousel.maxLogoCount)) + mCarousel.logoSize.y(); + yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2.f - (mCamOffset * logoSpacing[1]); if (mCarousel.logoAlignment == ALIGN_LEFT) - xOff = carouselLogoSize().x() / 10.f; + xOff = mCarousel.logoSize.x() / 10.f; else if (mCarousel.logoAlignment == ALIGN_RIGHT) - xOff = carouselSize().x() - (carouselLogoSize().x() * 1.1f); + xOff = mCarousel.size.x() - (mCarousel.logoSize.x() * 1.1f); else - xOff = (carouselSize().x() - carouselLogoSize().x()) / 2; + xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2; break; case HORIZONTAL_WHEEL: - xOff = (carouselSize().x() - carouselLogoSize().x()) / 2 - (mCamOffset * logoSpacing[1]); + xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2 - (mCamOffset * logoSpacing[1]); if (mCarousel.logoAlignment == ALIGN_TOP) - yOff = carouselLogoSize().y() / 10; + yOff = mCarousel.logoSize.y() / 10; else if (mCarousel.logoAlignment == ALIGN_BOTTOM) - yOff = carouselSize().y() - (carouselLogoSize().y() * 1.1f); + yOff = mCarousel.size.y() - (mCarousel.logoSize.y() * 1.1f); else - yOff = (carouselSize().y() - carouselLogoSize().y()) / 2; + yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2; break; case HORIZONTAL: default: - logoSpacing[0] = ((carouselSize().x() - (carouselLogoSize().x() * mCarousel.maxLogoCount)) / (mCarousel.maxLogoCount)) + carouselLogoSize().x(); - xOff = (carouselSize().x() - carouselLogoSize().x()) / 2.f - (mCamOffset * logoSpacing[0]); + logoSpacing[0] = ((mCarousel.size.x() - (mCarousel.logoSize.x() * mCarousel.maxLogoCount)) / (mCarousel.maxLogoCount)) + mCarousel.logoSize.x(); + xOff = (mCarousel.size.x() - mCarousel.logoSize.x()) / 2.f - (mCamOffset * logoSpacing[0]); if (mCarousel.logoAlignment == ALIGN_TOP) - yOff = carouselLogoSize().y() / 10.f; + yOff = mCarousel.logoSize.y() / 10.f; else if (mCarousel.logoAlignment == ALIGN_BOTTOM) - yOff = carouselSize().y() - (carouselLogoSize().y() * 1.1f); + yOff = mCarousel.size.y() - (mCarousel.logoSize.y() * 1.1f); else - yOff = (carouselSize().y() - carouselLogoSize().y()) / 2.f; + yOff = (mCarousel.size.y() - mCarousel.logoSize.y()) / 2.f; break; } if (mCarousel.logoPos.x() >= 0) - xOff = carouselLogoPos().x() - (mCarousel.type == HORIZONTAL ? (mCamOffset * logoSpacing[0]) : 0); + xOff = mCarousel.logoPos.x() - (mCarousel.type == HORIZONTAL ? (mCamOffset * logoSpacing[0]) : 0); if (mCarousel.logoPos.y() >= 0) - yOff = carouselLogoPos().y() - (mCarousel.type == VERTICAL ? (mCamOffset * logoSpacing[1]) : 0); + yOff = mCarousel.logoPos.y() - (mCarousel.type == VERTICAL ? (mCamOffset * logoSpacing[1]) : 0); int center = (int)(mCamOffset); int logoCount = Math::min(mCarousel.maxLogoCount, (int)mEntries.size()); @@ -805,10 +848,7 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp // Adding texture loading buffers depending on scrolling speed and status int bufferIndex = getScrollingVelocity() + 1; - if (Settings::getInstance()->getBool("FixedCarousel")) - Renderer::pushClipRect(Vector2i((int)mPosition.x(), (int)mPosition.y()), Vector2i((int)mSize.x(), (int)mSize.y())); - else - Renderer::pushClipRect(Vector2i::Zero(), Vector2i((int)mSize.x(), (int)mSize.y())); + Renderer::pushClipRect(Vector2i::Zero(), Vector2i((int)mSize.x(), (int)mSize.y())); std::unordered_set allPaths; std::unordered_set paths; @@ -887,24 +927,10 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp if (mShowing || index == mCursor) { Transform4x4f extrasTrans = trans; - - if (Settings::getInstance()->getBool("FixedCarousel")) - { - if (mCarousel.type == HORIZONTAL || mCarousel.type == HORIZONTAL_WHEEL) - extrasTrans.translate(Vector3f(mPosition.x() + (i - mExtrasCamOffset) * mSize.x(), mPosition.y(), 0)); - else - extrasTrans.translate(Vector3f(mPosition.x(), mPosition.y() + (i - mExtrasCamOffset) * mSize.y(), 0)); - - if (mSize.x() != (float)Renderer::getScreenWidth() || mSize.y() != (float)Renderer::getScreenHeight()) - extrasTrans.scale(Vector3f(mSize.x() / (float)Renderer::getScreenWidth(), mSize.y() / (float)Renderer::getScreenHeight(), 0.0)); - } + if (mCarousel.type == HORIZONTAL || mCarousel.type == HORIZONTAL_WHEEL) + extrasTrans.translate(Vector3f((i - mExtrasCamOffset) * mSize.x(), 0, 0)); else - { - if (mCarousel.type == HORIZONTAL || mCarousel.type == HORIZONTAL_WHEEL) - extrasTrans.translate(Vector3f((i - mExtrasCamOffset) * mSize.x(), 0, 0)); - else - extrasTrans.translate(Vector3f(0,(i - mExtrasCamOffset) * mSize.y(), 0)); - } + extrasTrans.translate(Vector3f(0, (i - mExtrasCamOffset) * mSize.y(), 0)); if (mExtrasFadeOpacity && mExtrasFadeOldCursor == index) extrasTrans = trans; @@ -975,10 +1001,10 @@ void SystemView::getDefaultElements(void) // Carousel mCarousel.type = HORIZONTAL; mCarousel.logoAlignment = ALIGN_CENTER; - mCarousel.size.x() = 1; // mSize.x(); - mCarousel.size.y() = 0.2325f; // *mSize.y(); + mCarousel.size.x() = mSize.x(); + mCarousel.size.y() = 0.2325f * mSize.y(); mCarousel.pos.x() = 0.0f; - mCarousel.pos.y() = 0.5f * (1 - 0.2325f);//- mCarousel.size.y(); // *(mSize.y() - mCarousel.size.y()); + mCarousel.pos.y() = 0.5f * (mSize.y() - mCarousel.size.y()); mCarousel.origin.x() = 0.0f; mCarousel.origin.y() = 0.0f; mCarousel.color = 0xFFFFFFD8; @@ -988,8 +1014,8 @@ void SystemView::getDefaultElements(void) mCarousel.logoRotation = 7.5; mCarousel.logoRotationOrigin.x() = -5; mCarousel.logoRotationOrigin.y() = 0.5; - mCarousel.logoSize.x() = 0.25f; // *mSize.x(); - mCarousel.logoSize.y() = 0.155f; // *mSize.y(); + mCarousel.logoSize.x() = 0.25f * mSize.x(); + mCarousel.logoSize.y() = 0.155f * mSize.y(); mCarousel.logoPos = Vector2f(-1, -1); mCarousel.maxLogoCount = 3; mCarousel.zIndex = 40; @@ -999,7 +1025,7 @@ void SystemView::getDefaultElements(void) // System Info Bar mSystemInfo.setSize(mSize.x(), mSystemInfo.getFont()->getLetterHeight()*2.2f); - mSystemInfo.setPosition(0, (carouselPos().y() + carouselSize().y() - 0.2f)); + mSystemInfo.setPosition(0, (mCarousel.pos.y() + mCarousel.size.y() - 0.2f)); mSystemInfo.setBackgroundColor(0xDDDDDDD8); mSystemInfo.setRenderBackground(true); mSystemInfo.setFont(Font::get((int)(0.035f * mSize.y()), Font::getDefaultPath())); @@ -1014,48 +1040,6 @@ void SystemView::getDefaultElements(void) } } -void SystemView::onSizeChanged() -{ - mSystemInfo.setPosition(0, (carouselPos().y() + carouselSize().y() - 0.2f)); - - if (SystemData::sSystemVector.size() == 0) - return; - - const std::shared_ptr& theme = SystemData::sSystemVector.at(0)->getTheme(); - const ThemeData::ThemeElement* sysInfoElem = theme->getElement("system", "systemInfo", "text"); - if (sysInfoElem) - mSystemInfo.applyTheme(theme, "system", "systemInfo", ThemeFlags::POSITION); -} - -Vector2f SystemView::carouselSize() -{ - if (Settings::getInstance()->getBool("FixedCarousel")) - return mSize; - - return mCarousel.size * mSize; -} - -Vector2f SystemView::carouselLogoPos() -{ - return mCarousel.logoPos * mSize; -} - -Vector2f SystemView::carouselPos() -{ - if (Settings::getInstance()->getBool("FixedCarousel")) - return Vector2f(0, 0); - - return mCarousel.pos * mSize; -} - -Vector2f SystemView::carouselLogoSize() -{ - if (Settings::getInstance()->getBool("FixedCarousel")) - return mCarousel.logoSize / mCarousel.size * mSize; - - return mCarousel.logoSize * mSize; -} - void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) { if (elem->has("type")) @@ -1070,9 +1054,9 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) mCarousel.type = HORIZONTAL; } if (elem->has("size")) - mCarousel.size = elem->get("size"); // *mSize; + mCarousel.size = elem->get("size") * mSize; if (elem->has("pos")) - mCarousel.pos = elem->get("pos"); // *mSize; + mCarousel.pos = elem->get("pos") * mSize; if (elem->has("origin")) mCarousel.origin = elem->get("origin"); if (elem->has("color")) @@ -1087,9 +1071,9 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) if (elem->has("logoScale")) mCarousel.logoScale = elem->get("logoScale"); if (elem->has("logoSize")) - mCarousel.logoSize = elem->get("logoSize"); // *mSize; + mCarousel.logoSize = elem->get("logoSize") * mSize; if (elem->has("logoPos")) - mCarousel.logoPos = elem->get("logoPos"); // *mSize; + mCarousel.logoPos = elem->get("logoPos") * mSize; if (elem->has("maxLogoCount")) mCarousel.maxLogoCount = (int)Math::round(elem->get("maxLogoCount")); if (elem->has("zIndex")) @@ -1111,6 +1095,7 @@ void SystemView::getCarouselFromTheme(const ThemeData::ThemeElement* elem) else mCarousel.logoAlignment = ALIGN_CENTER; } + if (elem->has("systemInfoDelay")) mCarousel.systemInfoDelay = elem->get("systemInfoDelay"); diff --git a/es-app/src/views/SystemView.h b/es-app/src/views/SystemView.h index 39f5a9f543..49e7bf5521 100644 --- a/es-app/src/views/SystemView.h +++ b/es-app/src/views/SystemView.h @@ -23,7 +23,6 @@ enum CarouselType : unsigned int struct SystemViewData { std::shared_ptr logo; - bool logoIsImage; std::vector backgroundExtras; }; @@ -69,17 +68,11 @@ class SystemView : public IList std::vector getHelpPrompts() override; virtual HelpStyle getHelpStyle() override; - void onSizeChanged() override; protected: void onCursorChanged(const CursorState& state) override; private: - Vector2f carouselLogoPos(); - Vector2f carouselLogoSize(); - Vector2f carouselPos(); - Vector2f carouselSize(); - void activateExtras(int cursor, bool activate = true); void updateExtras(const std::function& func); void clearEntries(); diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 854200d965..78f9932d6f 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -121,10 +121,6 @@ void ViewController::goToNextGameList() goToGameList(system); AudioManager::getInstance()->themeChanged(system->getTheme()); - - // FixedCarousel - if (Settings::getInstance()->getBool("FixedCarousel")) - getSystemListView()->goToSystem(system, true); } void ViewController::goToPrevGameList() @@ -137,10 +133,6 @@ void ViewController::goToPrevGameList() goToGameList(system); AudioManager::getInstance()->themeChanged(system->getTheme()); - - // FixedCarousel - if (Settings::getInstance()->getBool("FixedCarousel")) - getSystemListView()->goToSystem(system, true); } void ViewController::goToGameList(SystemData* system, bool forceImmediate) @@ -509,13 +501,7 @@ bool ViewController::input(InputConfig* config, Input input) void ViewController::update(int deltaTime) { if(mCurrentView) - { - // FixedCarousel - if (Settings::getInstance()->getBool("FixedCarousel")) - getSystemListView()->update(deltaTime); - mCurrentView->update(deltaTime); - } updateSelf(deltaTime); } @@ -538,7 +524,7 @@ void ViewController::render(const Transform4x4f& parentTrans) Vector3f sysEnd = getSystemListView()->getPosition() + Vector3f(getSystemListView()->getSize().x(), getSystemListView()->getSize().y(), 0); // draw systemview - if (!Settings::getInstance()->getBool("HideSystemView") && !Settings::getInstance()->getBool("FixedCarousel")) + if (!Settings::getInstance()->getBool("HideSystemView")) getSystemListView()->render(trans); // draw gamelists @@ -552,24 +538,6 @@ void ViewController::render(const Transform4x4f& parentTrans) it->second->render(trans); } - - // FixedCarousel - if (Settings::getInstance()->getBool("FixedCarousel")) - { - getSystemListView()->setPosition(Vector3f(0, 0, 0)); - getSystemListView()->setSize(Vector2f(Renderer::getScreenWidth(), Renderer::getScreenHeight() * 0.1)); - - Transform4x4f ts = Transform4x4f::Identity(); - Transform4x4f transInverse; - transInverse.invert(getSystemListView()->getTransform()); - - getSystemListView()->render(transInverse); - } - - - - - if(mWindow->peekGui() == this) mWindow->renderHelpPromptsEarly(); diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index 0aa65f65ee..e5928abe47 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -39,7 +39,6 @@ std::vector settings_dont_save { { "ScreenRotate" }, { "MonitorID" }, { "ExePath" }, - { "FixedCarousel" }, { "HomePath" }, { "MusicDirectory"}, { "UserMusicDirectory" } @@ -67,12 +66,6 @@ void Settings::setDefaults() mBoolMap.clear(); mIntMap.clear(); -#if _DEBUG - mBoolMap["FixedCarousel"] = false; -#else - mBoolMap["FixedCarousel"] = false; -#endif - mBoolMap["BackgroundJoystickInput"] = false; mBoolMap["ParseGamelistOnly"] = false; mBoolMap["ShowHiddenFiles"] = false; From 0d19a0c482b58adb4d5a30e54065a3347a1e511d Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 28 Nov 2019 14:59:51 +0100 Subject: [PATCH 254/336] Clock : Support for 12-hour format ( system defined ) --- es-core/src/Window.cpp | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index de142430d2..421d7adcf8 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -283,7 +283,24 @@ void Window::update(int deltaTime) // Visit http://en.cppreference.com/w/cpp/chrono/c/strftime for more information about date/time format char clockBuf[32]; - strftime(clockBuf, sizeof(clockBuf), "%H:%M", &clockTstruct); + +#if WIN32 + std::string oldLocale = setlocale(LC_TIME, nullptr); + setlocale(LC_TIME, ""); + + char ampm[32]; + strftime(ampm, sizeof(ampm), "%p", &clockTstruct); + + if (!std::string(&m[0]).empty()) + strftime(clockBuf, sizeof(clockBuf), "%I:%M %p", &clockTstruct); + else +#endif + strftime(clockBuf, sizeof(clockBuf), "%H:%M", &clockTstruct); + +#if WIN32 + setlocale(LC_TIME, oldLocale.c_str()); +#endif + mClock->setText(clockBuf); } From 5b1b3cc87c5990043769e7e3d81df123c321463c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 28 Nov 2019 15:02:32 +0100 Subject: [PATCH 255/336] ScrollableContainer : Increment delay to 3 seconds --- es-core/src/components/ScrollableContainer.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-core/src/components/ScrollableContainer.cpp b/es-core/src/components/ScrollableContainer.cpp index 5157b977bf..0ddb3e5668 100644 --- a/es-core/src/components/ScrollableContainer.cpp +++ b/es-core/src/components/ScrollableContainer.cpp @@ -4,8 +4,8 @@ #include "renderers/Renderer.h" #define AUTO_SCROLL_RESET_DELAY 3000 // ms to reset to top after we reach the bottom -#define AUTO_SCROLL_DELAY 1000 // ms to wait before we start to scroll -#define AUTO_SCROLL_SPEED 50 // ms between scrolls +#define AUTO_SCROLL_DELAY 3000 // ms to wait before we start to scroll +#define AUTO_SCROLL_SPEED 30 // ms between scrolls ScrollableContainer::ScrollableContainer(Window* window) : GuiComponent(window), mAutoScrollDelay(0), mAutoScrollSpeed(0), mAutoScrollAccumulator(0), mScrollPos(0, 0), mScrollDir(0, 0), mAutoScrollResetAccumulator(0) From 4b90ca5f6ae52e867a9d2364d446527e368e67b9 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 28 Nov 2019 15:20:04 +0100 Subject: [PATCH 256/336] ScrollableContainer : Fix scroll speed --- es-core/src/components/ScrollableContainer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-core/src/components/ScrollableContainer.cpp b/es-core/src/components/ScrollableContainer.cpp index 0ddb3e5668..ac95c979ca 100644 --- a/es-core/src/components/ScrollableContainer.cpp +++ b/es-core/src/components/ScrollableContainer.cpp @@ -5,7 +5,7 @@ #define AUTO_SCROLL_RESET_DELAY 3000 // ms to reset to top after we reach the bottom #define AUTO_SCROLL_DELAY 3000 // ms to wait before we start to scroll -#define AUTO_SCROLL_SPEED 30 // ms between scrolls +#define AUTO_SCROLL_SPEED 50 // ms between scrolls ScrollableContainer::ScrollableContainer(Window* window) : GuiComponent(window), mAutoScrollDelay(0), mAutoScrollSpeed(0), mAutoScrollAccumulator(0), mScrollPos(0, 0), mScrollDir(0, 0), mAutoScrollResetAccumulator(0) From 1b9a209c9336106f0d4cfe250aac2ca05b4bc48a Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 28 Nov 2019 15:25:11 +0100 Subject: [PATCH 257/336] VideoComponent : Add "loops" property to define the number of loops the video is played ( -1 = infinite ) --- es-core/src/ThemeData.cpp | 1 + es-core/src/components/GridTileComponent.cpp | 6 ++++- es-core/src/components/VideoVlcComponent.cpp | 23 ++++++++++++++++++++ es-core/src/components/VideoVlcComponent.h | 3 +++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index d49ebc7ac3..202185abe1 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -210,6 +210,7 @@ std::map> The { "color", COLOR }, { "zIndex", FLOAT }, { "snapshotSource", STRING }, // image, thumbnail, marquee + { "loops", FLOAT }, // Number of loops to do -1 (default) is infinite { "showSnapshotNoVideo", BOOLEAN }, { "showSnapshotDelay", BOOLEAN } } }, { "carousel", { diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index c5399866c6..f919e62c47 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -609,7 +609,11 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons if (grid && grid->has("showVideoAtDelay")) { createVideo(); - mVideo->applyTheme(theme, view, element, properties); + + if (theme->getElement(view, "gridtile", "video")) + mVideo->applyTheme(theme, view, "gridtile", ThemeFlags::ALL ^ (ThemeFlags::PATH)); + else if (theme->getElement(view, "gridtile.video", "video")) + mVideo->applyTheme(theme, view, "gridtile.video", ThemeFlags::ALL ^ (ThemeFlags::PATH)); } else if (mVideo != nullptr) { diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index 1b68e1a605..cafd2c0ab4 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -63,6 +63,9 @@ VideoVlcComponent::VideoVlcComponent(Window* window, std::string subtitles) : { mElapsed = 0; mColorShift = 0xFFFFFFFF; + + mLoops = -1; + mCurrentLoop = 0; // Get an empty texture for rendering the video mTexture = nullptr;// TextureResource::get(""); @@ -445,6 +448,20 @@ void VideoVlcComponent::handleLooping() libvlc_state_t state = libvlc_media_player_get_state(mMediaPlayer); if (state == libvlc_Ended) { + if (mLoops >= 0) + { + mCurrentLoop++; + if (mCurrentLoop > mLoops) + { + stopVideo(); + + mFadeIn = 0.0; + mPlayingVideoPath = ""; + mVideoPath = ""; + return; + } + } + if (mPlaylist != nullptr) { auto nextVideo = mPlaylist->getNextItem(); @@ -486,6 +503,7 @@ void VideoVlcComponent::startVideo() if (mIsPlaying) return; + mCurrentLoop = 0; mVideoWidth = 0; mVideoHeight = 0; @@ -620,6 +638,11 @@ void VideoVlcComponent::applyTheme(const std::shared_ptr& theme, cons if (elem && elem->has("color")) setColorShift(elem->get("color")); } + + if (elem && elem->has("loops")) + mLoops = (int)elem->get("loops"); + else + mLoops = -1; } void VideoVlcComponent::update(int deltaTime) diff --git a/es-core/src/components/VideoVlcComponent.h b/es-core/src/components/VideoVlcComponent.h index 14641b1019..030aa56be2 100644 --- a/es-core/src/components/VideoVlcComponent.h +++ b/es-core/src/components/VideoVlcComponent.h @@ -111,6 +111,9 @@ class VideoVlcComponent : public VideoComponent unsigned int mColorShift; int mElapsed; + + int mCurrentLoop; + int mLoops; }; #endif // ES_CORE_COMPONENTS_VIDEO_VLC_COMPONENT_H From 3b16b7761ce1fc7080507be13fbbcf06d01701ee Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 28 Nov 2019 17:15:12 +0100 Subject: [PATCH 258/336] VideoComponent : Support for slideRight & size in video.effect --- es-core/src/components/VideoVlcComponent.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/es-core/src/components/VideoVlcComponent.cpp b/es-core/src/components/VideoVlcComponent.cpp index cafd2c0ab4..97e0faa596 100644 --- a/es-core/src/components/VideoVlcComponent.cpp +++ b/es-core/src/components/VideoVlcComponent.cpp @@ -624,7 +624,11 @@ void VideoVlcComponent::applyTheme(const std::shared_ptr& theme, cons const ThemeData::ThemeElement* elem = theme->getElement(view, element, "video"); if (elem && elem->has("effect")) { - if (!(elem->get("effect").compare("bump"))) + if (!(elem->get("effect").compare("slideRight"))) + mEffect = VideoVlcFlags::VideoVlcEffect::SLIDERIGHT; + else if (!(elem->get("effect").compare("size"))) + mEffect = VideoVlcFlags::VideoVlcEffect::SIZE; + else if (!(elem->get("effect").compare("bump"))) mEffect = VideoVlcFlags::VideoVlcEffect::BUMP; else mEffect = VideoVlcFlags::VideoVlcEffect::NONE; From b115411741958fda6cf3a51b0be055e94eb2c3c6 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 29 Nov 2019 15:46:08 +0100 Subject: [PATCH 259/336] Windows : Support for DPI != 96 --- es-core/src/renderers/Renderer.cpp | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/es-core/src/renderers/Renderer.cpp b/es-core/src/renderers/Renderer.cpp index bbbbc27f08..855e5eb6c2 100644 --- a/es-core/src/renderers/Renderer.cpp +++ b/es-core/src/renderers/Renderer.cpp @@ -10,6 +10,10 @@ #include #include +#if WIN32 +#include +#endif + namespace Renderer { static std::stack clipStack; @@ -75,6 +79,16 @@ namespace Renderer SDL_DisplayMode dispMode; SDL_GetDesktopDisplayMode(0, &dispMode); + +#if WIN32 + if (!Settings::getInstance()->getBool("Windowed")) + { + ::SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE); + dispMode.w = ::GetSystemMetrics(SM_CXSCREEN); + dispMode.h = ::GetSystemMetrics(SM_CYSCREEN); + } +#endif + windowWidth = Settings::getInstance()->getInt("WindowWidth") ? Settings::getInstance()->getInt("WindowWidth") : dispMode.w; windowHeight = Settings::getInstance()->getInt("WindowHeight") ? Settings::getInstance()->getInt("WindowHeight") : dispMode.h; screenWidth = Settings::getInstance()->getInt("ScreenWidth") ? Settings::getInstance()->getInt("ScreenWidth") : windowWidth; @@ -121,6 +135,8 @@ namespace Renderer if (Settings::getInstance()->getBool("AlwaysOnTop")) windowFlags |= SDL_WINDOW_ALWAYS_ON_TOP; + windowFlags |= SDL_WINDOW_ALLOW_HIGHDPI; + if((sdlWindow = SDL_CreateWindow("EmulationStation", sdlWindowPosition.x(), sdlWindowPosition.y(), From bf618f70d7b8dee279012e29944b74967db6170d Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 29 Nov 2019 19:15:27 +0100 Subject: [PATCH 260/336] DPI : Use SetProcessDPIAware instead of SetThreadDpiAwarenessContext --- es-core/src/renderers/Renderer.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-core/src/renderers/Renderer.cpp b/es-core/src/renderers/Renderer.cpp index 855e5eb6c2..983156c05b 100644 --- a/es-core/src/renderers/Renderer.cpp +++ b/es-core/src/renderers/Renderer.cpp @@ -83,7 +83,7 @@ namespace Renderer #if WIN32 if (!Settings::getInstance()->getBool("Windowed")) { - ::SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_SYSTEM_AWARE); + ::SetProcessDPIAware(); dispMode.w = ::GetSystemMetrics(SM_CXSCREEN); dispMode.h = ::GetSystemMetrics(SM_CYSCREEN); } From f5a228033102b0ddd337e2ce479f744411bf796e Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 29 Nov 2019 20:49:25 +0100 Subject: [PATCH 261/336] Fix linux build --- es-core/src/utils/StringUtil.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/es-core/src/utils/StringUtil.cpp b/es-core/src/utils/StringUtil.cpp index df90c1c9be..3a693c8f1d 100644 --- a/es-core/src/utils/StringUtil.cpp +++ b/es-core/src/utils/StringUtil.cpp @@ -2,6 +2,7 @@ #include #include +#include #if defined(_WIN32) #include From f5ef3759582733fde3f9643d3df4bc9f7e461ad8 Mon Sep 17 00:00:00 2001 From: Justin Kinnaird Date: Fri, 29 Nov 2019 14:22:27 -0600 Subject: [PATCH 262/336] fix: Compilation issue --- es-app/src/ApiSystem.h | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/es-app/src/ApiSystem.h b/es-app/src/ApiSystem.h index e539056614..603197f512 100644 --- a/es-app/src/ApiSystem.h +++ b/es-app/src/ApiSystem.h @@ -34,7 +34,8 @@ class ApiSystem static void startUpdate(Window* c); static std::vector getThemesList(); - static std::pair ApiSystem::installTheme(std::string themeName, const std::function& func = nullptr); + static std::pair installTheme(std::string themeName, const std::function& func = nullptr); }; -#endif \ No newline at end of file +#endif + From 773748aebdfd1786280ac9a25cb9423184b9190b Mon Sep 17 00:00:00 2001 From: Justin Kinnaird Date: Fri, 29 Nov 2019 15:24:18 -0600 Subject: [PATCH 263/336] fix: cross-compatible method for setExepath --- es-app/src/main.cpp | 12 +----------- es-core/src/utils/FileSystemUtil.cpp | 22 ++++++++++++++++++---- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index f37eb20544..c8a0ea7c98 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -112,17 +112,7 @@ void playVideo() bool parseArgs(int argc, char* argv[]) { -#if defined(__linux__) - char* result = new char[PATH_MAX]; - ssize_t len = readlink("/proc/self/exe", result, PATH_MAX); - if (len != -1) { - result[len] = 0; - Utils::FileSystem::setExePath(result); - } - delete [] result; -#else - Utils::FileSystem::setExePath(argv[0]); -#endif + Utils::FileSystem::setExePath(argv[0]); // We need to process --home before any call to Settings::getInstance(), because settings are loaded from homepath for (int i = 1; i < argc; i++) { diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 08cdcc2abd..1ebdad22bd 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -424,11 +424,25 @@ namespace Utils void setExePath(const std::string& _path) { - exePath = getCanonicalPath(_path); - - if (isRegularFile(exePath)) + constexpr int path_max = 32767; +#if defined(_WIN32) + std::wstring result(path_max, 0); + if (GetModuleFileNameW(nullptr, &result[0], path_max) != 0) { + exePath = getCanonicalPath(convertFromWideString(result)); + } +#else + std::string result(path_max, 0); + if (readlink("/proc/self/exe", &result[0], path_max) != -1) { + exePath = getCanonicalPath(result); + } +#endif + // If the native implementations fail, fallback to argv[0] + if (exePath.empty()) { + exePath = getCanonicalPath(_path); + } + if (isRegularFile(exePath)) { exePath = getParent(exePath); - + } } // setExePath std::string getExePath() From 85fb50ff100dfedd862697b342ad475918312c36 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 29 Nov 2019 22:25:51 +0100 Subject: [PATCH 264/336] sound name="bgsound" : don't replay music when changing system if the music is the same --- es-core/src/AudioManager.cpp | 10 ++++++++-- es-core/src/AudioManager.h | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/es-core/src/AudioManager.cpp b/es-core/src/AudioManager.cpp index 7a28b56b3a..8e58763a82 100644 --- a/es-core/src/AudioManager.cpp +++ b/es-core/src/AudioManager.cpp @@ -219,6 +219,7 @@ void AudioManager::playMusic(std::string path) } Mix_HookMusicFinished(AudioManager::onMusicFinished); + mCurrentMusicPath = path; mCurrentSong = Utils::FileSystem::getStem(path); } @@ -235,7 +236,8 @@ void AudioManager::stopMusic() Mix_HookMusicFinished(nullptr); Mix_HaltMusic(); Mix_FreeMusic(mCurrentMusic); - + + mCurrentMusicPath = ""; mCurrentMusic = NULL; } @@ -261,7 +263,11 @@ void AudioManager::themeChanged(const std::shared_ptr& theme, bool fo elem = theme->getElement("system", "bgsound", "sound"); if (elem && elem->has("path") && Utils::FileSystem::exists(elem->get("path"))) - bgSound = elem->get("path"); + { + bgSound = Utils::FileSystem::getCanonicalPath(elem->get("path")); + if (bgSound == mCurrentMusicPath) + return; + } // Found a music for the system if (!bgSound.empty()) diff --git a/es-core/src/AudioManager.h b/es-core/src/AudioManager.h index 147b9d42d1..caceec1146 100644 --- a/es-core/src/AudioManager.h +++ b/es-core/src/AudioManager.h @@ -62,6 +62,7 @@ class AudioManager void playMusic(std::string path); std::string mCurrentSong; + std::string mCurrentMusicPath; std::string mSystemName; std::string mCurrentThemeMusicDirectory; bool mRunningFromPlaylist; From fc14e2beae95e727b47eaf8fe62aef7df00a85b1 Mon Sep 17 00:00:00 2001 From: Justin Kinnaird Date: Fri, 29 Nov 2019 15:49:56 -0600 Subject: [PATCH 265/336] Fix symbol --- es-core/src/utils/FileSystemUtil.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 1ebdad22bd..684c6944b8 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -428,7 +428,7 @@ namespace Utils #if defined(_WIN32) std::wstring result(path_max, 0); if (GetModuleFileNameW(nullptr, &result[0], path_max) != 0) { - exePath = getCanonicalPath(convertFromWideString(result)); + exePath = getCanonicalPath(Utils::String::convertFromWideString(result)); } #else std::string result(path_max, 0); From 48dcb7739eca14e12140c6b6a81868513f9a6f99 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 30 Nov 2019 09:11:30 +0100 Subject: [PATCH 266/336] Small fix for gridtile text visible & size elements --- es-core/src/components/GridTileComponent.cpp | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index f919e62c47..ce790d211b 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -531,7 +531,11 @@ bool GridTextProperties::applyTheme(const ThemeData::ThemeElement* elem) pos = elem->get("pos"); if (elem && elem->has("size")) + { size = elem->get("size"); + if (size.y() == 0) + Visible = false; + } if (elem && elem->has("color")) color = elem->get("color"); @@ -735,6 +739,7 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons mDefaultProperties.Label.applyTheme(elem); mSelectedProperties.Label.applyTheme(elem); + bool hasVisible = elem->has("visible"); mLabelMerged = elem->has("pos"); if (!mLabelMerged && elem->has("size")) mLabelMerged = mDefaultProperties.Label.size.x() == 0; @@ -747,7 +752,11 @@ void GridTileComponent::applyTheme(const std::shared_ptr& theme, cons elem = theme->getElement(view, "gridtile.text:selected", "text"); if (elem) + { mSelectedProperties.Label.applyTheme(elem); + if (hasVisible && !elem->has("visible")) + mSelectedProperties.Label.Visible = mDefaultProperties.Label.Visible; + } } // Apply theme to the element From 30b1ec46654879e1edd555fb617aff25a45194db Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 1 Dec 2019 11:14:50 +0100 Subject: [PATCH 267/336] Fix HighDpi Fullscreen resolution changes --- es-app/src/main.cpp | 1 + es-core/src/renderers/Renderer.cpp | 7 ++-- resources/splash.svg | 52 ++++++++++++++---------------- 3 files changed, 30 insertions(+), 30 deletions(-) diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index c8a0ea7c98..728a0b9bbc 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -168,6 +168,7 @@ bool parseArgs(int argc, char* argv[]) i += 2; // skip the argument value Settings::getInstance()->setInt("WindowWidth", width); Settings::getInstance()->setInt("WindowHeight", height); + Settings::getInstance()->setBool("FullscreenBorderless", false); } else if (strcmp(argv[i], "--screensize") == 0) { diff --git a/es-core/src/renderers/Renderer.cpp b/es-core/src/renderers/Renderer.cpp index 983156c05b..ece51d7085 100644 --- a/es-core/src/renderers/Renderer.cpp +++ b/es-core/src/renderers/Renderer.cpp @@ -77,11 +77,14 @@ namespace Renderer initialCursorState = (SDL_ShowCursor(0) != 0); + if (!Settings::getInstance()->getBool("Windowed")) + SDL_SetHint(SDL_HINT_VIDEO_HIGHDPI_DISABLED, "1"); + SDL_DisplayMode dispMode; SDL_GetDesktopDisplayMode(0, &dispMode); #if WIN32 - if (!Settings::getInstance()->getBool("Windowed")) + if (!Settings::getInstance()->getBool("Windowed") && !Settings::getInstance()->getInt("WindowWidth")) { ::SetProcessDPIAware(); dispMode.w = ::GetSystemMetrics(SM_CXSCREEN); @@ -136,7 +139,7 @@ namespace Renderer windowFlags |= SDL_WINDOW_ALWAYS_ON_TOP; windowFlags |= SDL_WINDOW_ALLOW_HIGHDPI; - + if((sdlWindow = SDL_CreateWindow("EmulationStation", sdlWindowPosition.x(), sdlWindowPosition.y(), diff --git a/resources/splash.svg b/resources/splash.svg index 7ae4f6bcc9..eb5f0b9f7f 100644 --- a/resources/splash.svg +++ b/resources/splash.svg @@ -1,36 +1,32 @@ - - - - - - + + + + + + image/svg+xml + + + + + + + + + - - + + - - + + - - Layer 1 - - - - - - - - - - - - - - - - + + Layer 1 + + + From 972003529dc8e59723a1bb019a7ad8dc0010dffa Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 1 Dec 2019 11:18:38 +0100 Subject: [PATCH 268/336] GridView : centerSelection -> add "partial" mode true -> always center the selection partial -> center the selection except if at begin or end of gamelist false -> just don't Default is "false" in vertical layout. Default is "partial" in horizontal layout. + TextComponent : fix multiline calculation --- es-core/src/ThemeData.cpp | 2 +- es-core/src/components/GridTileComponent.cpp | 3 + es-core/src/components/GridTileComponent.h | 2 + es-core/src/components/ImageGridComponent.h | 72 ++++++++++++++------ es-core/src/components/TextComponent.cpp | 15 +++- es-core/src/components/TextComponent.h | 3 + 6 files changed, 71 insertions(+), 26 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 202185abe1..9aee14842c 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -53,7 +53,7 @@ std::map> The { "showVideoAtDelay", FLOAT }, { "scrollDirection", STRING }, { "scrollSound", PATH }, - { "centerSelection", BOOLEAN }, + { "centerSelection", STRING }, { "scrollLoop", BOOLEAN } } }, { "gridtile", { { "size", NORMALIZED_PAIR }, diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index ce790d211b..a9ca057e53 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -555,6 +555,9 @@ bool GridTextProperties::applyTheme(const ThemeData::ThemeElement* elem) if (elem && elem->has("fontPath")) fontPath = elem->get("fontPath"); + if (elem->has("singleLineScroll")) + autoScroll = elem->get("singleLineScroll"); + return true; } diff --git a/es-core/src/components/GridTileComponent.h b/es-core/src/components/GridTileComponent.h index fcabd31565..7738acdb9a 100644 --- a/es-core/src/components/GridTileComponent.h +++ b/es-core/src/components/GridTileComponent.h @@ -97,6 +97,7 @@ struct GridTextProperties text->setBackgroundColor(backColor); text->setGlowColor(glowColor); text->setGlowSize(glowSize); + text->setAutoScroll(autoScroll); text->setFont(fontPath, fontSize * (float)Renderer::getScreenHeight()); } @@ -116,6 +117,7 @@ struct GridTextProperties std::string fontPath; float fontSize; + bool autoScroll; }; struct GridNinePatchProperties diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index fc322b1839..55b4cc8f8d 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -26,6 +26,14 @@ enum ImageSource MARQUEE }; +enum CenterSelection +{ + FULL, + PARTIAL, + NEVER +}; + + struct ImageGridData { std::string texturePath; @@ -124,7 +132,8 @@ class ImageGridComponent : public IList float mCameraDirection; // MISCELLANEOUS - bool mCenterSelection; + CenterSelection mCenterSelection; + bool mScrollLoop; ScrollDirection mScrollDirection; @@ -162,7 +171,7 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList::applyTheme(const std::shared_ptr& theme, auto direction = elem->get("scrollDirection"); if (direction == "horizontal") { - mCenterSelection = false; + mCenterSelection = CenterSelection::PARTIAL; mScrollDirection = SCROLL_HORIZONTALLY; } else if (direction == "horizontalCenter") { - mCenterSelection = true; + mCenterSelection = CenterSelection::FULL; mScrollDirection = SCROLL_HORIZONTALLY; } else if (direction == "verticalCenter") { - mCenterSelection = true; + mCenterSelection = CenterSelection::FULL; mScrollDirection = SCROLL_VERTICALLY; } else { - mCenterSelection = false; + mCenterSelection = CenterSelection::NEVER; mScrollDirection = SCROLL_VERTICALLY; } } @@ -458,9 +467,16 @@ void ImageGridComponent::applyTheme(const std::shared_ptr& theme, mAllowVideo = false; if (elem->has("centerSelection")) - mCenterSelection = (elem->get("centerSelection")); + { + if (!(elem->get("centerSelection").compare("true"))) + mCenterSelection = CenterSelection::FULL; + else if (!(elem->get("centerSelection").compare("partial"))) + mCenterSelection = CenterSelection::PARTIAL; + else + mCenterSelection = CenterSelection::NEVER; + } - if (mCenterSelection && elem->has("scrollLoop")) + if (mCenterSelection != CenterSelection::NEVER && elem->has("scrollLoop")) mScrollLoop = (elem->get("scrollLoop")); else mScrollLoop = false; @@ -636,24 +652,36 @@ void ImageGridComponent::onCursorChanged(const CursorState& state) int firstVisibleCol = mStartPosition / dimOpposite; - if ((col < centralCol || (col == 0 && col == centralCol)) && !mCenterSelection) - mStartPosition = 0; - else if ((col - centralCol) > lastScroll && !mCenterSelection && !mScrollLoop) - mStartPosition = lastScroll * dimOpposite; - else if (maxCentralCol != centralCol && col == firstVisibleCol + maxCentralCol || col == firstVisibleCol + centralCol) + if (mCenterSelection == CenterSelection::NEVER) { - if (col == firstVisibleCol + maxCentralCol) - mStartPosition = (col - maxCentralCol) * dimOpposite; - else - mStartPosition = (col - centralCol) * dimOpposite; + if (col == 0) + mStartPosition = 0; + if (col < firstVisibleCol) + mStartPosition = col * dimOpposite; + else if (col >= firstVisibleCol + dimScrollable) + mStartPosition = (col - dimScrollable + 1) * dimOpposite; } else { - if (oldCol == firstVisibleCol + maxCentralCol) - mStartPosition = (col - maxCentralCol) * dimOpposite; + if ((col < centralCol || (col == 0 && col == centralCol)) && mCenterSelection == CenterSelection::PARTIAL) + mStartPosition = 0; + else if ((col - centralCol) > lastScroll && mCenterSelection == CenterSelection::PARTIAL && !mScrollLoop) + mStartPosition = lastScroll * dimOpposite; + else if (maxCentralCol != centralCol && col == firstVisibleCol + maxCentralCol || col == firstVisibleCol + centralCol) + { + if (col == firstVisibleCol + maxCentralCol) + mStartPosition = (col - maxCentralCol) * dimOpposite; + else + mStartPosition = (col - centralCol) * dimOpposite; + } else - mStartPosition = (col - centralCol) * dimOpposite; - } + { + if (oldCol == firstVisibleCol + maxCentralCol) + mStartPosition = (col - maxCentralCol) * dimOpposite; + else + mStartPosition = (col - centralCol) * dimOpposite; + } + } auto lastCursor = mLastCursor; mLastCursor = mCursor; @@ -878,7 +906,7 @@ void ImageGridComponent::buildTiles() calcGridDimension(); - if (mCenterSelection) + if (mCenterSelection != CenterSelection::NEVER) { int dimScrollable = (isVertical() ? mGridDimension.y() : mGridDimension.x()) - 2 * EXTRAITEMS; mStartPosition -= (int)Math::floorf(dimScrollable / 2.0f); diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index 683f0bde48..d0af0f164a 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -310,7 +310,7 @@ void TextComponent::onTextChanged() std::string text = mUppercase ? Utils::String::toUpper(mText) : mText; std::shared_ptr f = mFont; - const bool isMultiline = !mAutoScroll && (mSize.y() == 0 || sy > f->getHeight()*1.2f); + const bool isMultiline = !mAutoScroll && (mSize.y() == 0 || sy > f->getHeight()*1.95f); bool addAbbrev = false; if (!isMultiline) @@ -352,7 +352,7 @@ void TextComponent::update(int deltaTime) GuiComponent::update(deltaTime); int sy = mSize.y() - mPadding.y() - mPadding.w(); - const bool isMultiline = !mAutoScroll && (mSize.y() == 0 || sy > mFont->getHeight()*1.2f); + const bool isMultiline = !mAutoScroll && (mSize.y() == 0 || sy > mFont->getHeight()*1.95f); if (mAutoScroll && !isMultiline && mSize.x() > 0) { @@ -371,7 +371,7 @@ void TextComponent::update(int deltaTime) // loop // pixels per second ( based on nes-mini font at 1920x1080 to produce a speed of 200 ) const float speed = mFont->sizeText("ABCDEFGHIJKLMNOPQRSTUVWXYZ").x() * 0.247f; - const float delay = 3000; + const float delay = 1000; const float scrollLength = textLength; const float returnLength = speed * 1.5f; const float scrollTime = (scrollLength * 1000) / speed; @@ -533,3 +533,12 @@ void TextComponent::applyTheme(const std::shared_ptr& theme, const st setFont(Font::getFromTheme(elem, properties, mFont)); } + +void TextComponent::setAutoScroll(bool value) +{ + if (mAutoScroll == value) + return; + + mAutoScroll = value; + onTextChanged(); +} \ No newline at end of file diff --git a/es-core/src/components/TextComponent.h b/es-core/src/components/TextComponent.h index bf4c712d18..4229c9eaa6 100644 --- a/es-core/src/components/TextComponent.h +++ b/es-core/src/components/TextComponent.h @@ -50,6 +50,9 @@ class TextComponent : public GuiComponent virtual void update(int deltaTime); + bool getAutoScroll() { return mAutoScroll; } + void setAutoScroll(bool value); + protected: virtual void onTextChanged(); From 203794291822c09d25a457199fbcb9893d04a2ac Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 1 Dec 2019 17:32:28 +0100 Subject: [PATCH 269/336] TextComponent.singleLineScroll : needs clipping... --- es-core/src/components/TextComponent.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/es-core/src/components/TextComponent.cpp b/es-core/src/components/TextComponent.cpp index d0af0f164a..1eaf4659c4 100644 --- a/es-core/src/components/TextComponent.cpp +++ b/es-core/src/components/TextComponent.cpp @@ -173,6 +173,9 @@ void TextComponent::render(const Transform4x4f& parentTrans) Renderer::drawRect(0.0f, 0.0f, mSize.x(), mSize.y(), bgColor, bgColor); } + if (mAutoScroll) + Renderer::pushClipRect(Vector2i(trans.translation().x(), trans.translation().y()), Vector2i(mSize.x(), mSize.y())); + if (mTextCache && mFont) { const Vector2f& textSize = mTextCache->metrics.size; @@ -277,6 +280,9 @@ void TextComponent::render(const Transform4x4f& parentTrans) mFont->renderGradientTextCache(mTextCache.get(), colorB, colorT); } + + if (mAutoScroll) + Renderer::popClipRect(); } } From 4ae388be4da20c5590b6a90221d7637f42d07669 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 5 Dec 2019 19:32:09 +0100 Subject: [PATCH 270/336] OpenGLEs : Fix compilation problem (related to unused code) --- es-core/src/components/ImageComponent.cpp | 25 ----------------------- 1 file changed, 25 deletions(-) diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index 48c1fe8846..e33c2862e3 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -361,31 +361,6 @@ void ImageComponent::updateVertices() } } -#include - -#define N_ROUNDING_PIECES 10 - -void DrawGLRoundedCorner(int x, int y, double sa, double arc, float r) -{ - // centre of the arc, for clockwise sense - float cent_x = x + r * cos(sa + ES_PI / 2); - float cent_y = y + r * sin(sa + ES_PI / 2); - - // build up piecemeal including end of the arc - int n = ceil(N_ROUNDING_PIECES * arc / ES_PI * 2); - for (int i = 0; i <= n; i++) - { - double ang = sa + arc * (double)i / (double)n; - - // compute the next point - float next_x = cent_x + r * sin(ang); - float next_y = cent_y - r * cos(ang); - - glColor3f(1.0f, 1.0f, 1.0f); - glVertex2f(next_x, next_y); - } -} - void ImageComponent::render(const Transform4x4f& parentTrans) { if (!mVisible) From 8125232f88927b1a4d2be0bc7548f3e5d4deb315 Mon Sep 17 00:00:00 2001 From: Justin Kinnaird Date: Sat, 7 Dec 2019 00:21:06 -0600 Subject: [PATCH 271/336] fix: isDirectory returning true even on symlinks --- es-core/src/utils/FileSystemUtil.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 684c6944b8..153bc76e87 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -921,7 +921,7 @@ namespace Utils bool isDirectory(const std::string& _path) { auto it = FileCache::get(_path); - if (it != nullptr) + if (it != nullptr && !it->isSymLink) return it->exists && it->directory; #ifdef WIN32 From 5626bb18f3525269019c2c1a409e320c59fb73e1 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 7 Dec 2019 19:31:59 +0100 Subject: [PATCH 272/336] Fix SliderComponent : menu continue scrolling when joystick hat is used --- es-core/src/components/SliderComponent.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-core/src/components/SliderComponent.cpp b/es-core/src/components/SliderComponent.cpp index dfd8fbf74e..25de9ff2a3 100644 --- a/es-core/src/components/SliderComponent.cpp +++ b/es-core/src/components/SliderComponent.cpp @@ -37,7 +37,7 @@ bool SliderComponent::input(InputConfig* config, Input input) mMoveRate = input.value ? -mSingleIncrement : 0; mMoveAccumulator = -MOVE_REPEAT_DELAY; - return true; + return input.value; } if(config->isMappedLike("right", input)) { @@ -46,7 +46,7 @@ bool SliderComponent::input(InputConfig* config, Input input) mMoveRate = input.value ? mSingleIncrement : 0; mMoveAccumulator = -MOVE_REPEAT_DELAY; - return true; + return input.value; } return GuiComponent::input(config, input); From d709ba18a0b95aa8ab615f70d3a4d68f1ed44e03 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 14 Dec 2019 12:35:26 +0100 Subject: [PATCH 273/336] Fix : HiddenSystems is always dirty -> UI options reload everytime --- es-core/src/Settings.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/es-core/src/Settings.cpp b/es-core/src/Settings.cpp index e5928abe47..3c7fc72af6 100644 --- a/es-core/src/Settings.cpp +++ b/es-core/src/Settings.cpp @@ -135,6 +135,7 @@ void Settings::setDefaults() mStringMap["ScreenSaverBehavior"] = "dim"; mStringMap["GamelistViewStyle"] = "automatic"; mStringMap["DefaultGridSize"] = ""; + mStringMap["HiddenSystems"] = ""; mStringMap["ThemeColorSet"] = ""; mStringMap["ThemeIconSet"] = ""; From b9e7bd8f7e5284b491a03c37b7030d2c0de831f3 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 14 Dec 2019 12:35:47 +0100 Subject: [PATCH 274/336] Fix GridTile : Video is no more centered in minSize layout --- es-core/src/components/GridTileComponent.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index a9ca057e53..acd8dbb856 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -214,8 +214,8 @@ void GridTileComponent::resize() { if (currentProperties.Image.sizeMode == "minSize") { - mVideo->setOrigin(0, 0); - mVideo->setPosition(imageOffset.x(), imageOffset.y()); + mVideo->setOrigin(0.5, 0.5); + mVideo->setPosition(imageOffset.x() + imageSize.x() / 2, imageOffset.y() + imageSize.y() / 2); mVideo->setMinSize(imageSize.x(), imageSize.y()); if (mImage != nullptr) From c2cfffdc4f303a609cc0aae4398297bc31e6688b Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 15 Dec 2019 17:52:26 +0100 Subject: [PATCH 275/336] fix 'DateTimeEditComponent' output formatting. --- es-core/src/components/DateTimeEditComponent.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/es-core/src/components/DateTimeEditComponent.cpp b/es-core/src/components/DateTimeEditComponent.cpp index 3b346ee604..48a2638cb4 100644 --- a/es-core/src/components/DateTimeEditComponent.cpp +++ b/es-core/src/components/DateTimeEditComponent.cpp @@ -204,6 +204,8 @@ std::string DateTimeEditComponent::getDisplayString(DisplayMode mode) const fmt = "%m/%d/%Y"; break; case DISP_DATE_TIME: + if(mTime.getTime() == 0) + return "unknown"; fmt = "%m/%d/%Y %H:%M:%S"; break; case DISP_RELATIVE_TO_NOW: From 793d98767810e9be33517c7750839cf7a92de823 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 19 Dec 2019 00:09:29 +0100 Subject: [PATCH 276/336] Update BR translation --- resources/locale/br/emulationstation2.po | 253 +++++++++++++++++++++-- 1 file changed, 232 insertions(+), 21 deletions(-) diff --git a/resources/locale/br/emulationstation2.po b/resources/locale/br/emulationstation2.po index 7224b8a534..fff73047d1 100644 --- a/resources/locale/br/emulationstation2.po +++ b/resources/locale/br/emulationstation2.po @@ -8,6 +8,10 @@ msgstr "" "Language: BR Traduzido por JTulho_LTDA\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" +#: +msgid "extracting update..." +msgstr "Extraindo Atualizações..." + #: msgid "Loading..." msgstr "Carregando..." @@ -24,6 +28,10 @@ msgstr "TAMANHO DA GRADE" msgid "DEFAULT GRID SIZE" msgstr "TAMANHO DE GRADE PADRÃO" +#: +msgid "RESET GAMELIST CUSTOMISATIONS" +msgstr "RESETAR CUSTOMIZAÇÕES DO GAMELIST" + #: msgid "UI MODE" msgstr "MODO DE INTERFACE" @@ -86,7 +94,7 @@ msgstr "desaparecer" #: msgid "instant" -msgstr "instantáneo" +msgstr "instantâneo" #: msgid "slide" @@ -152,6 +160,10 @@ msgstr "COLEÇÕES DE GRUPO SEM SISTEMA" msgid "SORT CUSTOM COLLECTIONS AND SYSTEMS" msgstr "ORGANIZAÇÃO DE COLEÇÕES E SISTEMAS" +#: +msgid "SHOW SYSTEM NAME IN COLLECTIONS" +msgstr "EXIBIR NOME DO SISTEMA NAS COLEÇÕES" + #: msgid "FINISH EDITING COLLECTION" msgstr "FINALIZAR COLEÇÃO" @@ -160,6 +172,10 @@ msgstr "FINALIZAR COLEÇÃO" msgid "PARSE GAMESLISTS ONLY" msgstr "APENAS LISTA DE JOGOS XML" +#: +msgid "EMULATOR SETTINGS" +msgstr "CONFIGURAÇÕES DO EMULADOR" + #: msgid "POWER SAVER MODES" msgstr "MODO DE POUPANÇA DE ENERGIA" @@ -367,7 +383,6 @@ msgstr "IDIOMA" #: msgid "OVERCLOCK" msgstr "OVERCLOCK" - #: msgid "EXTREM (1100Mhz)" msgstr "EXTREMO (1100Mhz)" @@ -409,6 +424,14 @@ msgstr "NENHUM" msgid "UPDATES" msgstr "ATUALIZAÇÕES" +#: +msgid "THEME INSTALLER" +msgstr "INSTALADOR DE TEMAS" + +#: +msgid "SELECT THEME TO INSTALL" +msgstr "SELECIONE O TEMA PARA INSTALAR" + #: msgid "AUTO UPDATES" msgstr "ATUALIZAÇÕES AUTOMÃTICAS" @@ -545,10 +568,138 @@ msgstr "ESTILO DE TRANSIÇÃO" msgid "SCREENSAVER BEHAVIOR" msgstr "COMPORTAMENTO DE TELA" +#: +msgid "dim" +msgstr "escura" + +#: +msgid "black" +msgstr "preta" + +#: +msgid "random video" +msgstr "vídeo aleatório" + +#: +msgid "slideshow" +msgstr "apresentação de slides" + +#: +msgid "VIDEO SCREENSAVER SETTINGS" +msgstr "CONFIGURAÇÕES DO PROTETOR DE TELA" + +#: +msgid "VIDEO SCREENSAVER" +msgstr "PROTETOR DE TELA EM VÃDEO" + +#: +msgid "SWAP VIDEO AFTER (SECS)" +msgstr "TROQUE O VÃDEO DEPOIS DE (Segundos)" + +#: +msgid "SHOW GAME INFO ON SCREENSAVER" +msgstr "MOSTRAR INFORMAÇÕES DO JOGO" + +#: +msgid "SHOW GAME INFO" +msgstr "MOSTRAR INFORMAÇÕES DO JOGO" + +#: +msgid "USE MARQUEE AS GAME INFO" +msgstr "UTILIZAR MARQUEE COMO INFORMAÇÕES DO JOGO" + +#: +msgid "STRETCH VIDEO ON SCREENSAVER" +msgstr "ESTICAR VÃDEO DO PROTETOR DE TELA" + +#: +msgid "SLIDESHOW SCREENSAVER SETTINGS" +msgstr "CONFIGURAÇÕES DA APRESENTAÇÃO DE SLIDES DE TELA" + +#: +msgid "SLIDESHOW SCREENSAVER" +msgstr "APRESENTAÇÃO DE SLIDES DE TELA" + +#: +msgid "SWAP IMAGE AFTER (SECS)" +msgstr "TROQUE A IMAGEM APÓS DE (SEGUNDOS)" + +#: +msgid "STRETCH IMAGES" +msgstr "ESTICAR IMAGENS" + +#: +msgid "USE CUSTOM IMAGES" +msgstr "UTILIZAR IMAGENS PERSONALIZADAS" + +#: +msgid "CUSTOM IMAGE DIR" +msgstr "DIRETÓRIO DE IMAGEM PERSONALIZADO" + +#: +msgid "CUSTOM IMAGE DIR RECURSIVE" +msgstr "DIRETÓRIO RECURSIVO DE IMAGEM PERSONALIZADO" + +#: +msgid "CUSTOM IMAGE FILTER" +msgstr "FILTRO DE IMAGEM PERSONALIZADO" + +#: +msgid "SCREENSAVER CONTROLS" +msgstr "CONTROLAR PROTETOR DE TELA" + #: msgid "SHOW FRAMERATE" msgstr "MOSTRAR TAXA DE QUADROS" +#: +msgid "THREADED LOADING" +msgstr "LOADING EM LINHA" + +#: +msgid "COMPLETE QUIT MENU" +msgstr "MENU DE SAÃDA COMPLETO" + +#: +msgid "LOG LEVEL" +msgstr "NÃVEL DE LOG" + +#: +msgid "enabled" +msgstr "ativado" + +#: +msgid "enable" +msgstr "ativado" + +#: +msgid "disabled" +msgstr "desativado" + +#: +msgid "disable" +msgstr "desativado" + +#: +msgid "default" +msgstr "padrão" + +#: +msgid "warning" +msgstr "atenção" + +#: +msgid "error" +msgstr "erro" + +#: +msgid "debug" +msgstr "depurar" + +#: +msgid "enhanced" +msgstr "melhorado" + #: msgid "ON-SCREEN HELP" msgstr "AJUDA NA TELA" @@ -577,6 +728,19 @@ msgstr "VOLUME DO SISTEMA" msgid "FRONTEND MUSIC" msgstr "MÚSICA DE FUNDO" +#: +msgid "DISPLAY SONG TITLES" +msgstr "EXIBIR TÃTULOS DE MÚSICAS" + +#: +msgid "ONLY PLAY SYSTEM-SPECIFIC MUSIC FOLDER" +msgstr "ATIVAR APENAS PASTA DE MÚSICA ESPECÃFICA DO SISTEMA" + +#: +msgid "PLAY THEME MUSICS" +msgstr "ATIVAR MÚSICAS TEMÃTICAS" + + #: msgid "OUTPUT DEVICE" msgstr "DISPOSITIVO DE SAIDA" @@ -627,7 +791,7 @@ msgstr "SSID WIFI" #: msgid "WIFI KEY" -msgstr "CHAVE DO WIFI" +msgstr "SENHA DO WIFI" #: msgid "WIFI ENABLED" @@ -641,14 +805,34 @@ msgstr "ERRO DE CONFIGURAÇÃO DO WIFI" msgid "SCRAPER" msgstr "SCRAPER DE JOGOS" +#: +msgid "DOWNLOADS AND UPDATES" +msgstr "DOWNLOADS E ATUALIZAÇÕES" + #: msgid "SCRAPE FROM" -msgstr "SCRAPE DESDE:" +msgstr "SERVIDOR SCRAPE" + +#: +msgid "IMAGE SOURCE" +msgstr "TIPO DE IMAGEM" + +#: +msgid "BOX SOURCE" +msgstr "TIPO DE BOX" + +#: +msgid "LOGO SOURCE" +msgstr "TIPO DE LOGO" #: msgid "SCRAPE RATINGS" msgstr "CLASSIFICAÇÕES DE SCRAPER" +#: +msgid "SCRAPE VIDEOS" +msgstr "VÃDEOS SCRAPE" + #: msgid "SCRAPE NOW" msgstr "SCRAPE AGORA"" @@ -658,20 +842,28 @@ msgid "QUIT" msgstr "SAIR" #: -msgid "REALLY RESTART?" -msgstr "¿QUER REALMENTE REINICIAR?" +msgid "REALLY QUIT?" +msgstr "QUER REALMENTE SAIR?" #: -msgid "RESTART SYSTEM" -msgstr "REINICIAR SISTEMA" +msgid "REALLY RESTART?" +msgstr "QUER REALMENTE REINICIAR?" #: msgid "REALLY SHUTDOWN?" -msgstr "¿QUER REALMENTE DESLIGAR?" +msgstr "QUER REALMENTE DESLIGAR?" + +#: +msgid "QUIT EMULATIONSTATION" +msgstr "SAIR DO RETRORAT" + +#: +msgid "RESTART SYSTEM" +msgstr "REINICIAR WINDOWS" #: msgid "SHUTDOWN SYSTEM" -msgstr "DESLIGANDO SISTEMA" +msgstr "DESLIGAR WINDOWS" #: msgid "Emulator" @@ -758,7 +950,7 @@ msgstr "SCRAPER PARA ESTES JOGOS" #: msgid "All Games" -msgstr "TODOS JOGOS" +msgstr "TODOS OS JOGOS" #. MISSING SCRAPPER TRANSLATIONS #: @@ -769,6 +961,10 @@ msgstr "SOMENTE FALTANDO IMAGEM" msgid "FILTER" msgstr "FILTRO" +#: +msgid "ONLY MISSING MEDIAS" +msgstr "SOMENTE MÃDIAS PERDIDAS" + #: msgid "SCRAPE THESE SYSTEMS" msgstr "SCRAPER PARA ESTES SISTEMAS" @@ -813,7 +1009,7 @@ msgstr "ATUALIZAÇÃO FALHOU, O SISTEMA REINICIARà AGORA" #: msgid "NO UPDATE AVAILABLE" -msgstr "NENHUMA ATUALIZAÇÃO DISPONIVEL" +msgstr "NENHUMA ATUALIZAÇÃO DISPONÃVEL" #: msgid "enter emulator" @@ -956,11 +1152,15 @@ msgid "enter last played date" msgstr "insira a última data reproduzida" #: -msgid "GAMES AVAILABLE" -msgstr "JOGOS DISPONIVEIS" +msgid "%i GAME AVAILABLE" +msgstr "%i ITEM DISPONÃVEL" + +#: +msgid "%i GAMES AVAILABLE" +msgstr "%i ITENS DISPONÃVEIS" #: -msgid "%i FAVORITE" +msgid "%i FAVORITES" msgid_plural "%i FAVORITES" msgstr[0] "%i FAVORITO" msgstr[1] "%i FAVORITOS" @@ -1021,6 +1221,14 @@ msgstr "TRABALHANDO..." msgid "CHANGE" msgstr "MUDAR" +#: +msgid "start & end" +msgstr "início e fim" + +#: +msgid "always" +msgstr "sempre" + #: msgid "never" msgstr "nunca" @@ -1526,11 +1734,11 @@ msgstr "CONFIGURAÇÕES DE TEMA" #: msgid "THEME COLORSET" -msgstr "COLORSET TEMA" +msgstr "COR DO TEMA" #: msgid "THEME ICONSET" -msgstr "ICONSET TEMA" +msgstr "ÃCONES DO TEMA" #: msgid "THEME MENU" @@ -1797,8 +2005,8 @@ msgid "Only missing hashs" msgstr "Apenas faltando hashs" #: -msgid "Username" -msgstr "Nome de usuario" +msgid "USERNAME" +msgstr "NOME DE USUÃRIO" #: msgid "Country" @@ -1897,8 +2105,11 @@ msgid "No core match" msgstr "Não á match de núcleo" #: -msgid "Add a clock in the main menu." -msgstr "Adicione um relógio no menu principal." +Add a clock in the main menu. +Adicione um relógio no menu principal + +msgid "SHOW CLOCK" +msgstr "ATIVAR RELÓGIO" #: msgid "UPGRADING" From 656f9178111bb22b47eab1f298737c4292e2f6b6 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 19 Dec 2019 00:10:38 +0100 Subject: [PATCH 277/336] Allow gamelist subsets storage/customisations per system ( using attribute appliesTo="viewname" ) Gamelist subsets : fix for default items --- es-app/src/guis/GuiGamelistOptions.cpp | 96 +++++++----- es-app/src/guis/GuiMenu.cpp | 198 +++++++++++++++++++------ es-app/src/guis/GuiMenu.h | 3 +- es-core/src/ThemeData.cpp | 142 ++++++++++++++++-- es-core/src/ThemeData.h | 21 ++- 5 files changed, 365 insertions(+), 95 deletions(-) diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 2dc6d7c18e..e10d93d2ad 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -16,6 +16,7 @@ #include "guis/GuiTextEditPopupKeyboard.h" #include "guis/GuiMsgBox.h" #include "scrapers/ThreadedScraper.h" +#include "guis/GuiMenu.h" std::vector GuiGamelistOptions::gridSizes { "automatic", @@ -142,61 +143,74 @@ GuiGamelistOptions::GuiGamelistOptions(Window* window, SystemData* system, bool mMenu.addWithLabel(_("SORT GAMES BY"), mListSort); - // GameList view style - mViewMode = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); - std::vector> styles; - styles.push_back(std::pair("automatic", _("automatic"))); - auto mViews = system->getTheme()->getViewsOfTheme(); - for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) - { - if (it->first == "basic" || it->first == "detailed" || it->first == "grid") - styles.push_back(std::pair(it->first, _(it->first.c_str()))); - else - styles.push_back(*it); - } - - std::string viewMode = system->getSystemViewMode(); - - bool found = false; - for (auto it = styles.cbegin(); it != styles.cend(); it++) - { - bool sel = (viewMode.empty() && it->first == "automatic") || viewMode == it->first; - if (sel) - found = true; - - mViewMode->add(it->second, it->first, sel); - } - - if (!found) - mViewMode->selectFirstItem(); - mMenu.addWithLabel(_("GAMELIST VIEW STYLE"), mViewMode); + auto glv = ViewController::get()->getGameListView(system); + std::string viewName = glv->getName(); + - // Grid size override - if (showGridFeatures) + //else { - auto gridOverride = system->getGridSizeOverride(); - auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); + // GameList view style + mViewMode = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); + std::vector> styles; + styles.push_back(std::pair("automatic", _("automatic"))); + + auto mViews = system->getTheme()->getViewsOfTheme(); + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + { + if (it->first == "basic" || it->first == "detailed" || it->first == "grid") + styles.push_back(std::pair(it->first, _(it->first.c_str()))); + else + styles.push_back(*it); + } - mGridSize = std::make_shared>(mWindow, _("GRID SIZE"), false); + std::string viewMode = system->getSystemViewMode(); - found = false; - for (auto it = gridSizes.cbegin(); it != gridSizes.cend(); it++) + bool found = false; + for (auto it = styles.cbegin(); it != styles.cend(); it++) { - bool sel = (gridOverride == Vector2f(0, 0) && *it == "automatic") || ovv == *it; + bool sel = (viewMode.empty() && it->first == "automatic") || viewMode == it->first; if (sel) found = true; - mGridSize->add(_(*it), *it, sel); + mViewMode->add(it->second, it->first, sel); } if (!found) - mGridSize->selectFirstItem(); + mViewMode->selectFirstItem(); - mMenu.addWithLabel(_("GRID SIZE"), mGridSize); + mMenu.addWithLabel(_("GAMELIST VIEW STYLE"), mViewMode); + + auto subsetNames = system->getTheme()->getSubSetNames(viewName); + if (subsetNames.size() > 0) + { + mMenu.addEntry(_("VIEW CUSTOMISATION"), true, [this, system]() { GuiMenu::openThemeConfiguration(mWindow, this, nullptr, system->getThemeFolder()); }); + } + else if (showGridFeatures) // Grid size override + { + auto gridOverride = system->getGridSizeOverride(); + auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); + + mGridSize = std::make_shared>(mWindow, _("GRID SIZE"), false); + + found = false; + for (auto it = gridSizes.cbegin(); it != gridSizes.cend(); it++) + { + bool sel = (gridOverride == Vector2f(0, 0) && *it == "automatic") || ovv == *it; + if (sel) + found = true; + + mGridSize->add(_(*it), *it, sel); + } + + if (!found) + mGridSize->selectFirstItem(); + + mMenu.addWithLabel(_("GRID SIZE"), mGridSize); + } } // show filtered menu @@ -307,11 +321,13 @@ GuiGamelistOptions::~GuiGamelistOptions() if (divider != std::string::npos) { std::string first = str.substr(0, divider); - std::string second = str.substr(divider+1, std::string::npos); + std::string second = str.substr(divider + 1, std::string::npos); gridSizeOverride = Vector2f((float)atof(first.c_str()), (float)atof(second.c_str())); } } + else + gridSizeOverride = mSystem->getGridSizeOverride(); bool viewModeChanged = mSystem->setSystemViewMode(mViewMode->getSelected(), gridSizeOverride); diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 5178a19517..a63b3d1d18 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -29,6 +29,7 @@ #include "scrapers/ThreadedScraper.h" #include "ApiSystem.h" #include "platform.h" +#include "views/gamelist/IGameListView.h" GuiMenu::GuiMenu(Window* window, bool animate) : GuiComponent(window), mMenu(window, _("MAIN MENU")), mVersion(window) { @@ -356,9 +357,16 @@ void GuiMenu::openSoundSettings() } -void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptr> theme_set) +struct ThemeConfigOption { - if (Settings::getInstance()->getString("ThemeSet") != theme_set->getSelected()) + std::string defaultSettingName; + std::string subset; + std::shared_ptr> component; +}; + +void GuiMenu::openThemeConfiguration(Window* mWindow, GuiComponent* s, std::shared_ptr> theme_set,const std::string systemTheme) +{ + if (theme_set != nullptr && Settings::getInstance()->getString("ThemeSet") != theme_set->getSelected()) { mWindow->pushGui(new GuiMsgBox(mWindow, _("YOU MUST APPLY THE THEME BEFORE EDIT CONFIGURATION"), _("OK"))); return; @@ -369,15 +377,27 @@ void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptrgetState().getSystem(); auto theme = system->getTheme(); - auto themeconfig = new GuiSettings(mWindow, _("THEME CONFIGURATION").c_str()); + auto themeconfig = new GuiSettings(mWindow, (systemTheme.empty() ? _("THEME CONFIGURATION") : _("VIEW CUSTOMISATION")).c_str()); auto themeSubSets = theme->getSubSets(); - std::map>> options; + std::string viewName; + bool showGridFeatures = true; + if (!systemTheme.empty()) + { + auto glv = ViewController::get()->getGameListView(system); + viewName = glv->getName(); + std::string baseType = theme->getCustomViewBaseType(viewName); - for (std::string subset : theme->getSubSetNames()) + showGridFeatures = (viewName == "grid" || baseType == "grid"); + } + + std::map options; + + for (std::string subset : theme->getSubSetNames(viewName)) { std::string settingName = "subset." + subset; + std::string perSystemSettingName = systemTheme.empty() ? "" : "subset." + systemTheme + "." + subset; if (subset == "colorset") settingName = "ThemeColorSet"; else if (subset == "iconset") settingName = "ThemeIconSet"; @@ -387,43 +407,84 @@ void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptr 0) { auto selectedColorSet = themeColorSets.end(); - auto selectedName = Settings::getInstance()->getString(settingName); + auto selectedName = !perSystemSettingName.empty() ? Settings::getInstance()->getString(perSystemSettingName) : Settings::getInstance()->getString(settingName); + + if (!perSystemSettingName.empty() && selectedName.empty()) + selectedName = Settings::getInstance()->getString(settingName); for (auto it = themeColorSets.begin(); it != themeColorSets.end() && selectedColorSet == themeColorSets.end(); it++) if (it->name == selectedName) selectedColorSet = it; std::shared_ptr> item = std::make_shared >(mWindow, _(("THEME " + Utils::String::toUpper(subset)).c_str()), false); - item->setTag(settingName); + item->setTag(!perSystemSettingName.empty()? perSystemSettingName : settingName); for (auto it = themeColorSets.begin(); it != themeColorSets.end(); it++) - item->add(it->displayName, it->name, it == selectedColorSet); + { + std::string displayName = it->displayName; + + if (!systemTheme.empty()) + { + std::string defaultValue = Settings::getInstance()->getString(settingName); + if (defaultValue.empty()) + defaultValue = system->getTheme()->getDefaultSubSetValue(subset); + + if (it->name == defaultValue) + displayName = displayName + " (" + _("DEFAULT") +")"; + } + + item->add(displayName, it->name, it == selectedColorSet); + } if (selectedColorSet == themeColorSets.end()) item->selectFirstItem(); if (!themeColorSets.empty()) - { - std::string displayName = theme->getVariable("subset." + subset); + { + std::string displayName = themeColorSets.cbegin()->subSetDisplayName; if (!displayName.empty()) - themeconfig->addWithLabel(displayName, item); + { + std::string prefix; + + if (systemTheme.empty()) + { + auto itSubsetName = themeColorSets.cbegin()->appliesTo.cbegin(); + if (itSubsetName != themeColorSets.cbegin()->appliesTo.cend()) + { + prefix = theme->getViewDisplayName(*itSubsetName); + if (!prefix.empty()) + prefix = prefix + " / "; + } + } + + themeconfig->addWithLabel(prefix + displayName, item); + } else themeconfig->addWithLabel(_(("THEME " + Utils::String::toUpper(subset)).c_str()), item); } - options[settingName] = item; + ThemeConfigOption opt; + opt.component = item; + opt.subset = subset; + opt.defaultSettingName = settingName; + options[!perSystemSettingName.empty() ? perSystemSettingName : settingName] = opt; } else - options[settingName] = nullptr; + { + ThemeConfigOption opt; + opt.component = nullptr; + options[!perSystemSettingName.empty() ? perSystemSettingName : settingName] = opt; + } } // gamelist_style std::shared_ptr> gamelist_style = nullptr; - //if (theme_gamelistview == nullptr) + + if (systemTheme.empty()) { gamelist_style = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); @@ -447,21 +508,27 @@ void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptr("detailed", _("detailed"))); } - auto viewPreference = Settings::getInstance()->getString("GamelistViewStyle"); + auto viewPreference = systemTheme.empty() ? Settings::getInstance()->getString("GamelistViewStyle") : system->getSystemViewMode(); if (!theme->hasView(viewPreference)) viewPreference = "automatic"; for (auto it = styles.cbegin(); it != styles.cend(); it++) gamelist_style->add(it->second, it->first, viewPreference == it->first); + if (!gamelist_style->hasSelection()) + gamelist_style->selectFirstItem(); + themeconfig->addWithLabel(_("GAMELIST VIEW STYLE"), gamelist_style); } // Default grid size std::shared_ptr> mGridSize = nullptr; - if (system != NULL && theme->hasView("grid")) + if (showGridFeatures && system != NULL && theme->hasView("grid")) { - Vector2f gridOverride = Vector2f::parseString(Settings::getInstance()->getString("DefaultGridSize")); + Vector2f gridOverride = + systemTheme.empty() ? Vector2f::parseString(Settings::getInstance()->getString("DefaultGridSize")) : + system->getGridSizeOverride(); + auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); mGridSize = std::make_shared>(mWindow, _("DEFAULT GRID SIZE"), false); @@ -482,28 +549,54 @@ void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptraddWithLabel(_("DEFAULT GRID SIZE"), mGridSize); } - - themeconfig->addEntry(_("RESET GAMELIST CUSTOMISATIONS"), false, [s, themeconfig, window] + if (systemTheme.empty()) { - Settings::getInstance()->setString("GamelistViewStyle", ""); - Settings::getInstance()->setString("DefaultGridSize", ""); + themeconfig->addEntry(_("RESET GAMELIST CUSTOMISATIONS"), false, [s, themeconfig, window] + { + Settings::getInstance()->setString("GamelistViewStyle", ""); + Settings::getInstance()->setString("DefaultGridSize", ""); - for (auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) - (*sysIt)->setSystemViewMode("automatic", Vector2f(0, 0)); + for (auto sysIt = SystemData::sSystemVector.cbegin(); sysIt != SystemData::sSystemVector.cend(); sysIt++) + (*sysIt)->setSystemViewMode("automatic", Vector2f(0, 0)); - themeconfig->setVariable("reloadAll", true); - themeconfig->close(); - }); + themeconfig->setVariable("reloadAll", true); + themeconfig->close(); + }); + } // theme_colorset, theme_iconset, theme_menu, theme_systemview, theme_gamelistview, theme_region, - themeconfig->addSaveFunc([this, themeconfig, theme_set, options, gamelist_style, mGridSize, window] + themeconfig->addSaveFunc([systemTheme, system, themeconfig, theme_set, options, gamelist_style, mGridSize, window] { - bool reloadAll = Settings::getInstance()->setString("ThemeSet", theme_set == nullptr ? "" : theme_set->getSelected()); + bool reloadAll = systemTheme.empty() ? Settings::getInstance()->setString("ThemeSet", theme_set == nullptr ? "" : theme_set->getSelected()) : false; - for (auto option : options) - reloadAll |= Settings::getInstance()->setString(option.first, option.second == nullptr ? "" : option.second->getSelected()); + for (auto option : options) + { + ThemeConfigOption& opt = option.second; + + std::string value; + + if (opt.component != nullptr) + { + value = opt.component->getSelected(); + + if (!systemTheme.empty() && !value.empty()) + { + std::string defaultValue = Settings::getInstance()->getString(opt.defaultSettingName); + if (defaultValue.empty()) + defaultValue = system->getTheme()->getDefaultSubSetValue(opt.subset); + + if (value == defaultValue) + value = ""; + } + else if (systemTheme.empty() && value == system->getTheme()->getDefaultSubSetValue(opt.subset)) + value = ""; + } + + if (value != Settings::getInstance()->getString(option.first)) + reloadAll |= Settings::getInstance()->setString(option.first, value); + } - reloadAll |= Settings::getInstance()->setString("GamelistViewStyle", gamelist_style == nullptr ? "" : gamelist_style->getSelected()); + Vector2f gridSizeOverride(0, 0); if (mGridSize != nullptr) { @@ -516,23 +609,44 @@ void GuiMenu::openThemeConfiguration(GuiSettings* s, std::shared_ptrsetString("DefaultGridSize", value); + if (systemTheme.empty()) + reloadAll |= Settings::getInstance()->setString("DefaultGridSize", value); } - else + else if (systemTheme.empty()) reloadAll |= Settings::getInstance()->setString("DefaultGridSize", ""); + + if (systemTheme.empty()) + reloadAll |= Settings::getInstance()->setString("GamelistViewStyle", gamelist_style == nullptr ? "" : gamelist_style->getSelected()); + else + { + std::string viewMode = gamelist_style == nullptr ? system->getSystemViewMode() : gamelist_style->getSelected(); + reloadAll |= system->setSystemViewMode(viewMode, gridSizeOverride); + } if (reloadAll || themeconfig->getVariable("reloadAll")) - { - CollectionSystemManager::get()->updateSystemsList(); - ViewController::get()->reloadAll(window); - window->endRenderLoadingScreen(); + { + if (systemTheme.empty()) + { + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->reloadAll(window); + window->endRenderLoadingScreen(); - std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); - Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + if (theme_set != nullptr) + { + std::string oldTheme = Settings::getInstance()->getString("ThemeSet"); + Scripting::fireEvent("theme-changed", theme_set->getSelected(), oldTheme); + } + } + else + { + system->loadTheme(); + system->resetFilters(); + ViewController::get()->reloadGameListView(system); + } } }); @@ -597,7 +711,7 @@ void GuiMenu::openUISettings() bool showThemeConfiguration = system->getTheme()->hasSubsets() || system->getTheme()->hasView("grid"); if (showThemeConfiguration) { - s->addSubMenu(_("THEME CONFIGURATION"), [this, s, theme_set]() { openThemeConfiguration(s, theme_set); }); + s->addSubMenu(_("THEME CONFIGURATION"), [this, s, theme_set]() { openThemeConfiguration(mWindow, s, theme_set); }); } else // GameList view style only, acts like Retropie for simple themes { diff --git a/es-app/src/guis/GuiMenu.h b/es-app/src/guis/GuiMenu.h index 462bf66001..0ad48948d1 100644 --- a/es-app/src/guis/GuiMenu.h +++ b/es-app/src/guis/GuiMenu.h @@ -19,6 +19,8 @@ class GuiMenu : public GuiComponent std::vector getHelpPrompts() override; HelpStyle getHelpStyle() override; + static void openThemeConfiguration(Window* mWindow, GuiComponent* s, std::shared_ptr> theme_set, const std::string systemTheme = ""); + private: void addEntry(std::string name, bool add_arrow, const std::function& func, const std::string iconName = ""); @@ -32,7 +34,6 @@ class GuiMenu : public GuiComponent void openSoundSettings(); void openUISettings(); void openUpdateSettings(); - void openThemeConfiguration(GuiSettings* s, std::shared_ptr> theme_set); void openEmulatorSettings(); void openSystemEmulatorSettings(SystemData* system); diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 9aee14842c..eb884e0103 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -450,6 +450,8 @@ bool ThemeData::isFirstSubset(const pugi::xml_node& node) return false; } + + bool ThemeData::parseSubset(const pugi::xml_node& node) { if (!node.attribute("subset")) @@ -464,6 +466,14 @@ bool ThemeData::parseSubset(const pugi::xml_node& node) if (displayNameAttr.empty()) displayNameAttr = nameAttr; + std::string subSetDisplayNameAttr = resolvePlaceholders(node.attribute("subSetDisplayName").as_string()); + if (subSetDisplayNameAttr.empty()) + { + std::string byVarName = getVariable("subset." + subsetAttr); + if (!byVarName.empty()) + subSetDisplayNameAttr = byVarName; + } + bool add = true; for (auto sb : mSubsets) { @@ -473,18 +483,38 @@ bool ThemeData::parseSubset(const pugi::xml_node& node) } if (add) - mSubsets.push_back(Subset(subsetAttr, nameAttr, displayNameAttr)); + { + Subset subSet(subsetAttr, nameAttr, displayNameAttr, subSetDisplayNameAttr); + + std::string appliesToAttr = resolvePlaceholders(node.attribute("appliesTo").as_string()); + if (!appliesToAttr.empty()) + subSet.appliesTo = Utils::String::splitAny(appliesToAttr, ","); + + mSubsets.push_back(subSet); + } } if (subsetAttr == "colorset") { - if (nameAttr == mColorset || (mColorset.empty() && isFirstSubset(node))) + std::string perSystemSetName = Settings::getInstance()->getString("subset." + mSystemThemeFolder + ".colorset"); + if (!perSystemSetName.empty()) + { + if (nameAttr == perSystemSetName) + return true; + } + else if (nameAttr == mColorset || (mColorset.empty() && isFirstSubset(node))) return true; } else if (subsetAttr == "iconset") { - if (nameAttr == mIconset || (mIconset.empty() && isFirstSubset(node))) + std::string perSystemSetName = Settings::getInstance()->getString("subset." + mSystemThemeFolder + ".iconset"); + if (!perSystemSetName.empty()) + { + if (nameAttr == perSystemSetName) + return true; + } + else if (nameAttr == mIconset || (mIconset.empty() && isFirstSubset(node))) return true; } else if (subsetAttr == "menu") @@ -499,19 +529,36 @@ bool ThemeData::parseSubset(const pugi::xml_node& node) } else if (subsetAttr == "gamelistview") { - if (nameAttr == mGamelistview || (mGamelistview.empty() && isFirstSubset(node))) + std::string perSystemSetName = Settings::getInstance()->getString("subset." + mSystemThemeFolder + ".gamelistview"); + if (!perSystemSetName.empty()) + { + if (nameAttr == perSystemSetName) + return true; + } + else if (nameAttr == mGamelistview || (mGamelistview.empty() && isFirstSubset(node))) return true; } else { - std::string setID = Settings::getInstance()->getString("subset."+ subsetAttr); - if (nameAttr == setID || (setID.empty() && isFirstSubset(node))) - return true; + std::string perSystemSetName = Settings::getInstance()->getString("subset." + mSystemThemeFolder + "." + subsetAttr); + if (!perSystemSetName.empty()) + { + if (nameAttr == perSystemSetName) + return true; + } + else + { + std::string setID = Settings::getInstance()->getString("subset." + subsetAttr); + if (nameAttr == setID || (setID.empty() && isFirstSubset(node))) + return true; + } } return false; } + + void ThemeData::parseInclude(const pugi::xml_node& node) { if (!parseFilterAttributes(node)) @@ -702,6 +749,8 @@ void ThemeData::parseTheme(const pugi::xml_node& root) parseViewElement(node); else if (name == "customView") parseCustomView(node, root); + else if (name == "subset") + parseSubsetElement(node); } } @@ -710,6 +759,33 @@ void ThemeData::parseTheme(const pugi::xml_node& root) parseFeature(node); } +void ThemeData::parseSubsetElement(const pugi::xml_node& root) +{ + const std::string name = root.attribute("name").as_string(); + const std::string displayName = resolvePlaceholders(root.attribute("displayName").as_string()); + const std::string appliesTo = root.attribute("appliesTo").as_string(); + + for (pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) + { + node.remove_attribute("subset"); + node.append_attribute("subset") = name.c_str(); + + if (!appliesTo.empty()) + { + node.remove_attribute("appliesTo"); + node.append_attribute("appliesTo") = appliesTo.c_str(); + } + + if (!displayName.empty()) + { + node.remove_attribute("subSetDisplayName"); + node.append_attribute("subSetDisplayName") = displayName.c_str(); + } + + parseInclude(node); + } +} + void ThemeData::parseViews(const pugi::xml_node& root) { // ThemeException error; @@ -764,6 +840,8 @@ void ThemeData::parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& if (!inherits.empty() && inherits != baseClass) { view.baseType = inherits; + view.baseTypes.push_back(inherits); + parseCustomViewBaseClass(root, view, inherits); } @@ -793,6 +871,7 @@ void ThemeData::parseCustomView(const pugi::xml_node& node, const pugi::xml_node if (!inherits.empty()) { view.baseType = inherits; + view.baseTypes.push_back(inherits); parseCustomViewBaseClass(root, view, inherits); } @@ -887,7 +966,7 @@ bool ThemeData::parseRegion(const pugi::xml_node& node) } if (add) - mSubsets.push_back(Subset("region", nameAttr, nameAttr)); + mSubsets.push_back(Subset("region", nameAttr, nameAttr, "region")); const char* delim = " \t\r\n,"; @@ -1078,6 +1157,15 @@ std::string ThemeData::getCustomViewBaseType(const std::string& view) return ""; } +std::string ThemeData::getViewDisplayName(const std::string& view) +{ + auto viewIt = mViews.find(view); + if (viewIt != mViews.cend()) + return viewIt->second.displayName; + + return ""; +} + bool ThemeData::isCustomView(const std::string& view) { auto viewIt = mViews.find(view); @@ -1392,13 +1480,47 @@ std::vector ThemeData::getSubSet(const std::vector& subsets, con return ret; } -std::vector ThemeData::getSubSetNames() +std::vector ThemeData::getSubSetNames(const std::string ofView) { std::vector ret; for (const auto& it : mSubsets) + { if (std::find(ret.cbegin(), ret.cend(), it.subset) == ret.cend()) - ret.push_back(it.subset); + { + if (ofView.empty()) + ret.push_back(it.subset); + else + { + if (std::find(it.appliesTo.cbegin(), it.appliesTo.cend(), ofView) != it.appliesTo.cend()) + ret.push_back(it.subset); + else + { + auto viewIt = mViews.find(ofView); + if (viewIt != mViews.cend()) + { + for (auto applyTo : it.appliesTo) + { + if (viewIt->second.isOfType(applyTo)) + { + ret.push_back(it.subset); + break; + } + } + } + } + } + } + } return ret; } + +std::string ThemeData::getDefaultSubSetValue(const std::string subsetname) +{ + for (const auto& it : mSubsets) + if (it.subset == subsetname) + return it.name; + + return ""; +} \ No newline at end of file diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 12c75fac2d..717679b15b 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -89,17 +89,22 @@ struct ThemeSet struct Subset { - Subset(const std::string set, const std::string nm, const std::string dn) + Subset(const std::string set, const std::string nm, const std::string dn, const std::string ssdn) { subset = set; name = nm; displayName = dn; + subSetDisplayName = ssdn; } std::string subset; std::string name; std::string displayName; + + std::vector appliesTo; + + std::string subSetDisplayName; }; struct MenuElement @@ -214,6 +219,13 @@ class ThemeData std::vector orderedKeys; std::string baseType; + std::vector baseTypes; + + bool isOfType(const std::string type) + { + return baseType == type || std::find(baseTypes.cbegin(), baseTypes.cend(), type) != baseTypes.cend(); + }; + std::string displayName; bool isCustomView; @@ -256,7 +268,9 @@ class ThemeData static const std::shared_ptr& getMenuTheme(); std::vector getSubSets() { return mSubsets; } - std::vector getSubSetNames(); + std::vector getSubSetNames(const std::string ofView = ""); + + std::string getDefaultSubSetValue(const std::string subsetname); static std::vector getSubSet(const std::vector& subsets, const std::string& subset); @@ -276,6 +290,8 @@ class ThemeData return ""; } + std::string getViewDisplayName(const std::string& view); + private: static std::map< std::string, std::map > sElementMap; static std::vector sSupportedFeatures; @@ -301,6 +317,7 @@ class ThemeData bool isFirstSubset(const pugi::xml_node& node); bool parseLanguage(const pugi::xml_node& node); bool parseFilterAttributes(const pugi::xml_node& node); + void parseSubsetElement(const pugi::xml_node& root); void parseCustomViewBaseClass(const pugi::xml_node& root, ThemeView& view, std::string baseClass); From 6fa71555122013a422c46cb81721367be2abdf88 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 19 Dec 2019 23:48:16 +0100 Subject: [PATCH 278/336] ThemeData : fix linux build error --- es-core/src/ThemeData.cpp | 7 ++++++- es-core/src/ThemeData.h | 5 +---- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index eb884e0103..e6868a129b 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -1523,4 +1523,9 @@ std::string ThemeData::getDefaultSubSetValue(const std::string subsetname) return it.name; return ""; -} \ No newline at end of file +} + +bool ThemeData::ThemeView::isOfType(const std::string type) +{ + return baseType == type || std::find(baseTypes.cbegin(), baseTypes.cend(), type) != baseTypes.cend(); +}; \ No newline at end of file diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index 717679b15b..dc6ba81397 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -221,10 +221,7 @@ class ThemeData std::vector baseTypes; - bool isOfType(const std::string type) - { - return baseType == type || std::find(baseTypes.cbegin(), baseTypes.cend(), type) != baseTypes.cend(); - }; + bool isOfType(const std::string type); std::string displayName; From 317d74bd5b03989f78901c50eb102503dff95b60 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 21 Dec 2019 16:59:26 +0100 Subject: [PATCH 279/336] Subset appliesTo & customViews : Fixes & multiple view support in customView changes (hard to describe) --- es-app/src/guis/GuiGamelistOptions.cpp | 13 +- es-app/src/guis/GuiMenu.cpp | 159 +++++++++++++------------ es-core/src/ThemeData.cpp | 41 +++++-- 3 files changed, 131 insertions(+), 82 deletions(-) diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index e10d93d2ad..20d37d5713 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -329,7 +329,16 @@ GuiGamelistOptions::~GuiGamelistOptions() else gridSizeOverride = mSystem->getGridSizeOverride(); - bool viewModeChanged = mSystem->setSystemViewMode(mViewMode->getSelected(), gridSizeOverride); + std::string viewMode = mViewMode->getSelected(); + + if (mSystem->getSystemViewMode() != (viewMode == "automatic" ? "" : viewMode)) + { + for (auto sm : Settings::getInstance()->getStringMap()) + if (Utils::String::startsWith(sm.first, "subset." + mSystem->getThemeFolder() + ".")) + Settings::getInstance()->setString(sm.first, ""); + } + + bool viewModeChanged = mSystem->setSystemViewMode(viewMode, gridSizeOverride); Settings::getInstance()->saveFile(); @@ -344,6 +353,8 @@ GuiGamelistOptions::~GuiGamelistOptions() // only reload full view if we came from a placeholder // as we need to re-display the remaining elements for whatever new // game is selected + mSystem->loadTheme(); + mSystem->resetFilters(); ViewController::get()->reloadGameListView(mSystem); } } diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index a63b3d1d18..cb8473070e 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -392,6 +392,75 @@ void GuiMenu::openThemeConfiguration(Window* mWindow, GuiComponent* s, std::shar showGridFeatures = (viewName == "grid" || baseType == "grid"); } + + // gamelist_style + std::shared_ptr> gamelist_style = nullptr; + + if (systemTheme.empty()) + { + gamelist_style = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); + + std::vector> styles; + styles.push_back(std::pair("automatic", _("automatic"))); + + if (system != NULL) + { + auto mViews = theme->getViewsOfTheme(); + for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) + { + if (it->first == "basic" || it->first == "detailed" || it->first == "grid") + styles.push_back(std::pair(it->first, _(it->first.c_str()))); + else + styles.push_back(*it); + } + } + else + { + styles.push_back(std::pair("basic", _("basic"))); + styles.push_back(std::pair("detailed", _("detailed"))); + } + + auto viewPreference = systemTheme.empty() ? Settings::getInstance()->getString("GamelistViewStyle") : system->getSystemViewMode(); + if (!theme->hasView(viewPreference)) + viewPreference = "automatic"; + + for (auto it = styles.cbegin(); it != styles.cend(); it++) + gamelist_style->add(it->second, it->first, viewPreference == it->first); + + if (!gamelist_style->hasSelection()) + gamelist_style->selectFirstItem(); + + themeconfig->addWithLabel(_("GAMELIST VIEW STYLE"), gamelist_style); + } + + // Default grid size + std::shared_ptr> mGridSize = nullptr; + if (showGridFeatures && system != NULL && theme->hasView("grid")) + { + Vector2f gridOverride = + systemTheme.empty() ? Vector2f::parseString(Settings::getInstance()->getString("DefaultGridSize")) : + system->getGridSizeOverride(); + + auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); + + mGridSize = std::make_shared>(mWindow, _("DEFAULT GRID SIZE"), false); + + bool found = false; + for (auto it = GuiGamelistOptions::gridSizes.cbegin(); it != GuiGamelistOptions::gridSizes.cend(); it++) + { + bool sel = (gridOverride == Vector2f(0, 0) && *it == "automatic") || ovv == *it; + if (sel) + found = true; + + mGridSize->add(_(it->c_str()), *it, sel); + } + + if (!found) + mGridSize->selectFirstItem(); + + themeconfig->addWithLabel(_("DEFAULT GRID SIZE"), mGridSize); + } + std::map options; for (std::string subset : theme->getSubSetNames(viewName)) @@ -452,16 +521,24 @@ void GuiMenu::openThemeConfiguration(Window* mWindow, GuiComponent* s, std::shar if (systemTheme.empty()) { - auto itSubsetName = themeColorSets.cbegin()->appliesTo.cbegin(); - if (itSubsetName != themeColorSets.cbegin()->appliesTo.cend()) + for (auto subsetName : themeColorSets.cbegin()->appliesTo) { - prefix = theme->getViewDisplayName(*itSubsetName); - if (!prefix.empty()) - prefix = prefix + " / "; + std::string pfx = theme->getViewDisplayName(subsetName); + if (!pfx.empty()) + { + if (prefix.empty()) + prefix = pfx; + else + prefix = prefix + ", " + pfx; + } } + + if (!prefix.empty()) + prefix = " ("+ prefix+")"; + } - themeconfig->addWithLabel(prefix + displayName, item); + themeconfig->addWithLabel(displayName + prefix, item); } else themeconfig->addWithLabel(_(("THEME " + Utils::String::toUpper(subset)).c_str()), item); @@ -481,73 +558,6 @@ void GuiMenu::openThemeConfiguration(Window* mWindow, GuiComponent* s, std::shar } } - // gamelist_style - std::shared_ptr> gamelist_style = nullptr; - - if (systemTheme.empty()) - { - gamelist_style = std::make_shared< OptionListComponent >(mWindow, _("GAMELIST VIEW STYLE"), false); - - std::vector> styles; - styles.push_back(std::pair("automatic", _("automatic"))); - - if (system != NULL) - { - auto mViews = theme->getViewsOfTheme(); - for (auto it = mViews.cbegin(); it != mViews.cend(); ++it) - { - if (it->first == "basic" || it->first == "detailed" || it->first == "grid") - styles.push_back(std::pair(it->first, _(it->first.c_str()))); - else - styles.push_back(*it); - } - } - else - { - styles.push_back(std::pair("basic", _("basic"))); - styles.push_back(std::pair("detailed", _("detailed"))); - } - - auto viewPreference = systemTheme.empty() ? Settings::getInstance()->getString("GamelistViewStyle") : system->getSystemViewMode(); - if (!theme->hasView(viewPreference)) - viewPreference = "automatic"; - - for (auto it = styles.cbegin(); it != styles.cend(); it++) - gamelist_style->add(it->second, it->first, viewPreference == it->first); - - if (!gamelist_style->hasSelection()) - gamelist_style->selectFirstItem(); - - themeconfig->addWithLabel(_("GAMELIST VIEW STYLE"), gamelist_style); - } - - // Default grid size - std::shared_ptr> mGridSize = nullptr; - if (showGridFeatures && system != NULL && theme->hasView("grid")) - { - Vector2f gridOverride = - systemTheme.empty() ? Vector2f::parseString(Settings::getInstance()->getString("DefaultGridSize")) : - system->getGridSizeOverride(); - - auto ovv = std::to_string((int)gridOverride.x()) + "x" + std::to_string((int)gridOverride.y()); - - mGridSize = std::make_shared>(mWindow, _("DEFAULT GRID SIZE"), false); - - bool found = false; - for (auto it = GuiGamelistOptions::gridSizes.cbegin(); it != GuiGamelistOptions::gridSizes.cend(); it++) - { - bool sel = (gridOverride == Vector2f(0, 0) && *it == "automatic") || ovv == *it; - if (sel) - found = true; - - mGridSize->add(_(it->c_str()), *it, sel); - } - - if (!found) - mGridSize->selectFirstItem(); - - themeconfig->addWithLabel(_("DEFAULT GRID SIZE"), mGridSize); - } if (systemTheme.empty()) { @@ -742,7 +752,8 @@ void GuiMenu::openUISettings() gamelist_style->add(it->second, it->first, viewPreference == it->first); s->addWithLabel(_("GAMELIST VIEW STYLE"), gamelist_style); - s->addSaveFunc([s, gamelist_style, window] { + s->addSaveFunc([s, gamelist_style, window] + { if (Settings::getInstance()->setString("GamelistViewStyle", gamelist_style->getSelected())) { s->setVariable("reloadAll", true); diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index e6868a129b..155b81c9f4 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -10,6 +10,7 @@ #include "platform.h" #include "Settings.h" #include +#include "EsLocale.h" std::vector ThemeData::sSupportedViews { { "system" }, { "basic" }, { "detailed" }, { "grid" }, { "video" }, { "menu" }, { "screen" } }; std::vector ThemeData::sSupportedFeatures { { "video" }, { "carousel" }, { "z-index" }, { "visible" } }; @@ -859,15 +860,36 @@ void ThemeData::parseCustomView(const pugi::xml_node& node, const pugi::xml_node return; std::string viewKey = node.attribute("name").as_string(); + std::string inherits = node.attribute("inherits").as_string(); + + if (viewKey.find(",") != std::string::npos && inherits.empty()) + { + for (auto name : Utils::String::split(viewKey, ',')) + { + std::string trim = Utils::String::trim(name); + if (mViews.find(trim) != mViews.cend()) + { + ThemeView& view = mViews.insert(std::pair(trim, ThemeView())).first->second; + + if (node.attribute("displayName")) + view.displayName = resolvePlaceholders(node.attribute("displayName").as_string()); + + parseView(node, view); + } + } + + return; + } ThemeView& view = mViews.insert(std::pair(viewKey, ThemeView())).first->second; - view.displayName = node.attribute("displayName") ? resolvePlaceholders(node.attribute("displayName").as_string()) : viewKey; - if (view.displayName.empty()) + + if (node.attribute("displayName")) + view.displayName = resolvePlaceholders(node.attribute("displayName").as_string()); + else if (view.displayName.empty()) view.displayName = viewKey; view.isCustomView = true; - std::string inherits = node.attribute("inherits").as_string(); if (!inherits.empty()) { view.baseType = inherits; @@ -1161,9 +1183,14 @@ std::string ThemeData::getViewDisplayName(const std::string& view) { auto viewIt = mViews.find(view); if (viewIt != mViews.cend()) - return viewIt->second.displayName; + { + if (viewIt->second.displayName.empty()) + return _(view.c_str()); - return ""; + return viewIt->second.displayName; + } + + return view; } bool ThemeData::isCustomView(const std::string& view) @@ -1494,7 +1521,7 @@ std::vector ThemeData::getSubSetNames(const std::string ofView) { if (std::find(it.appliesTo.cbegin(), it.appliesTo.cend(), ofView) != it.appliesTo.cend()) ret.push_back(it.subset); - else + /* else { auto viewIt = mViews.find(ofView); if (viewIt != mViews.cend()) @@ -1508,7 +1535,7 @@ std::vector ThemeData::getSubSetNames(const std::string ofView) } } } - } + }*/ } } } From bb192c8bc4317bce89adcc6f5c004c01157340d8 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 22 Dec 2019 20:09:08 +0100 Subject: [PATCH 280/336] Windows Fix : Bad gamelist file resolution when relative path starts with .\ instead of ./ --- es-core/src/utils/FileSystemUtil.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 153bc76e87..acb2cf3780 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -675,11 +675,11 @@ namespace Utils return _path; // replace '.' with relativeTo - if((_path[0] == '.') && (_path[1] == '/')) + if((_path[0] == '.') && (_path[1] == '/' || _path[1] == '\\')) return getGenericPath(_relativeTo + &(_path[1])); // replace '~' with homePath - if(_allowHome && (_path[0] == '~') && (_path[1] == '/')) + if(_allowHome && (_path[0] == '~') && (_path[1] == '/' || _path[1] == '\\')) return getCanonicalPath(getHomePath() + &(_path[1])); // nothing to resolve From c5fa62e0bda85876eef501559b9bb40f073473b8 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 23 Dec 2019 18:49:05 +0100 Subject: [PATCH 281/336] use GL_LINEAR for upscaling filter when images are designed for fullscreen ( height >= 480p ) "Pixel" themes must have background image height < 480 --- es-core/src/renderers/Renderer_GL21.cpp | 4 ++-- es-core/src/renderers/Renderer_GLES10.cpp | 4 ++-- es-core/src/resources/TextureData.cpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/es-core/src/renderers/Renderer_GL21.cpp b/es-core/src/renderers/Renderer_GL21.cpp index 866b032d7b..aee6aba9d2 100644 --- a/es-core/src/renderers/Renderer_GL21.cpp +++ b/es-core/src/renderers/Renderer_GL21.cpp @@ -110,8 +110,8 @@ namespace Renderer glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, _repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, _repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, _linear ? GL_LINEAR : GL_NEAREST); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, _linear ? GL_LINEAR : GL_NEAREST); glPixelStorei(GL_PACK_ALIGNMENT, 1); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); diff --git a/es-core/src/renderers/Renderer_GLES10.cpp b/es-core/src/renderers/Renderer_GLES10.cpp index 57bf1b5218..0963b97069 100644 --- a/es-core/src/renderers/Renderer_GLES10.cpp +++ b/es-core/src/renderers/Renderer_GLES10.cpp @@ -105,8 +105,8 @@ namespace Renderer glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, _repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE); glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, _repeat ? GL_REPEAT : GL_CLAMP_TO_EDGE); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, _linear ? GL_LINEAR : GL_NEAREST); - glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, _linear ? GL_LINEAR : GL_NEAREST); glPixelStorei(GL_PACK_ALIGNMENT, 1); glPixelStorei(GL_UNPACK_ALIGNMENT, 1); diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index a3e5799f15..172fb2b997 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -311,7 +311,7 @@ bool TextureData::uploadAndBind() if ((mWidth == 0) || (mHeight == 0) || (mDataRGBA == nullptr)) return false; - mTextureID = Renderer::createTexture(Renderer::Texture::RGBA, true, mTile, mWidth, mHeight, mDataRGBA); + mTextureID = Renderer::createTexture(Renderer::Texture::RGBA, mHeight >= 480, mTile, mWidth, mHeight, mDataRGBA); if (mTextureID) { if (mDataRGBA != nullptr && !mIsExternalDataRGBA) From a5d2134e77eeec4646aa044b7a8ecadbd547a66a Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 23 Dec 2019 19:35:34 +0100 Subject: [PATCH 282/336] Fix : IsFirstSubset, better resolution --- es-core/src/ThemeData.cpp | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 155b81c9f4..0563e4e2fe 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -436,23 +436,13 @@ bool ThemeData::isFirstSubset(const pugi::xml_node& node) const std::string subsetToFind = resolvePlaceholders(node.attribute("subset").as_string()); const std::string name = node.attribute("name").as_string(); - pugi::xml_node root = node.parent(); - - for (pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) - { - const std::string subsetAttr = resolvePlaceholders(node.attribute("subset").as_string()); - if (subsetAttr.empty() || subsetAttr != subsetToFind) - continue; - - const std::string nameAttr = resolvePlaceholders(node.attribute("name").as_string()); - return (nameAttr == name); - } + for (const auto& it : mSubsets) + if (it.subset == subsetToFind) + return it.name == name; return false; } - - bool ThemeData::parseSubset(const pugi::xml_node& node) { if (!node.attribute("subset")) From ed38056df556b77c0660d6c1621c06ca0fa15a3e Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 24 Dec 2019 10:55:52 +0100 Subject: [PATCH 283/336] Optimisation : Avoid filesystem concurrent accesses to avoid some lags with slow Hdds --- es-core/src/ImageIO.cpp | 30 ++++++++++++++++++++ es-core/src/resources/ResourceManager.cpp | 5 ++++ es-core/src/resources/ResourceManager.h | 4 +++ es-core/src/resources/TextureDataManager.cpp | 7 +++-- 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index 59686558ec..b9544b9f63 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -9,9 +9,28 @@ #include "math/Vector2i.h" #include "utils/FileSystemUtil.h" #include "utils/StringUtil.h" +#include "resources/ResourceManager.h" + +#include +#include + +static std::map sizeCache; bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) { + auto it = sizeCache.find(fn); + if (it != sizeCache.cend()) + { + if (*x < 0) + return false; + + *x = it->second.x(); + *y = it->second.y(); + return true; + } + + std::unique_lock lock(ResourceManager::FileSystemLock); + LOG(LogDebug) << "ImageIO::loadImageSize " << fn; auto ext = Utils::String::toLower(Utils::FileSystem::getExtension(fn)); @@ -25,6 +44,7 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) if (f == 0) { LOG(LogWarning) << "ImageIO::loadImageSize\tUnable to open file"; + sizeCache[fn] = Vector2i(-1, -1); return false; } @@ -35,7 +55,10 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) // In all formats, the file is at least 24 bytes big, so we'll read that always unsigned char buf[24]; if (fread(buf, 1, 24, f) != 24) + { + sizeCache[fn] = Vector2i(-1, -1); return false; + } // For JPEGs, we need to read the first 12 bytes of each chunk. // We'll read those 12 bytes at buf+2...buf+14, i.e. overwriting the existing buf. @@ -73,8 +96,12 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) LOG(LogDebug) << "ImageIO::loadImageSize\tJPG size " << std::string(std::to_string(*x) + "x" + std::to_string(*y)).c_str(); if (*x > 5000) // security ? + { + sizeCache[fn] = Vector2i(-1, -1); return false; + } + sizeCache[fn] = Vector2i(*x, *y); return true; } @@ -86,6 +113,7 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) LOG(LogDebug) << "ImageIO::loadImageSize\tGIF size " << std::string(std::to_string(*x) + "x" + std::to_string(*y)).c_str(); + sizeCache[fn] = Vector2i(*x, *y); return true; } @@ -97,9 +125,11 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) LOG(LogDebug) << "ImageIO::loadImageSize\tPNG size " << std::string(std::to_string(*x) + "x" + std::to_string(*y)).c_str(); + sizeCache[fn] = Vector2i(*x, *y); return true; } + sizeCache[fn] = Vector2i(-1, -1); LOG(LogWarning) << "ImageIO::loadImageSize\tUnable to extract size"; return false; } diff --git a/es-core/src/resources/ResourceManager.cpp b/es-core/src/resources/ResourceManager.cpp index c18bcd14ea..18b4bf28c7 100644 --- a/es-core/src/resources/ResourceManager.cpp +++ b/es-core/src/resources/ResourceManager.cpp @@ -7,6 +7,7 @@ auto array_deleter = [](unsigned char* p) { delete[] p; }; auto nop_deleter = [](unsigned char* /*p*/) { }; std::shared_ptr ResourceManager::sInstance = nullptr; +std::mutex ResourceManager::FileSystemLock; ResourceManager::ResourceManager() { @@ -64,8 +65,12 @@ const ResourceData ResourceManager::getFileData(const std::string& path) const return data; } + + ResourceData ResourceManager::loadFile(const std::string& path, size_t size) const { + std::unique_lock lock(FileSystemLock); + std::ifstream stream(path, std::ios::binary); if (size == 0) diff --git a/es-core/src/resources/ResourceManager.h b/es-core/src/resources/ResourceManager.h index df68c6c958..b5d26411a5 100644 --- a/es-core/src/resources/ResourceManager.h +++ b/es-core/src/resources/ResourceManager.h @@ -4,6 +4,8 @@ #include #include +#include + //The ResourceManager exists to... //Allow loading resources embedded into the executable like an actual file. @@ -39,6 +41,8 @@ class ResourceManager const ResourceData getFileData(const std::string& path) const; bool fileExists(const std::string& path) const; + static std::mutex FileSystemLock; + private: ResourceManager(); diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index 48d0c2b831..a664116f51 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -285,14 +285,17 @@ void TextureLoader::threadProc() if (textureData && !textureData->isLoaded()) { + std::this_thread::yield(); + textureData->load(); mManager->onTextureLoaded(textureData); lock.lock(); mProcessingTextureDataQ.remove(textureData); - - std::this_thread::yield(); + lock.unlock(); } + + std::this_thread::yield(); } } } From bf76f077ba735a0f7345aee6f307fe7e6ddb1a10 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 29 Dec 2019 00:35:48 +0100 Subject: [PATCH 284/336] Windows : set lpDirectory to exe's directory when running using ShellExecute --- es-core/src/platform.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index f340560883..2851164944 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -152,6 +152,8 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo } lpExecInfo.lpParameters = args.c_str(); // file name as an argument + lpExecInfo.lpDirectory = Utils::FileSystem::getParent(exe).c_str(); + ShellExecuteEx(&lpExecInfo); if (lpExecInfo.hProcess != NULL) From ce0fee6756dd2bca50875335e7392927a57ce03d Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 29 Dec 2019 01:33:31 +0100 Subject: [PATCH 285/336] ShellExecute : Rebuild absolute path if rom is a relative path --- es-core/src/platform.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index 2851164944..eb493732e3 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -151,8 +151,8 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo } } - lpExecInfo.lpParameters = args.c_str(); // file name as an argument - lpExecInfo.lpDirectory = Utils::FileSystem::getParent(exe).c_str(); + lpExecInfo.lpParameters = args.c_str(); // file name as an argument + lpExecInfo.lpDirectory = Utils::FileSystem::getAbsolutePath(Utils::FileSystem::getParent(exe)).c_str(); ShellExecuteEx(&lpExecInfo); From 627ae5e22f990c84869461ec7f718f663769c8c0 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 29 Dec 2019 13:23:10 +0100 Subject: [PATCH 286/336] imagecahe.db : Improves performance of grid loading with mecanical HDDs. --- es-app/src/main.cpp | 4 + es-core/src/ImageIO.cpp | 149 +++++++++++++++++-- es-core/src/ImageIO.h | 3 + es-core/src/components/ImageGridComponent.h | 3 + es-core/src/resources/TextureDataManager.cpp | 2 +- es-core/src/utils/FileSystemUtil.cpp | 6 + 6 files changed, 153 insertions(+), 14 deletions(-) diff --git a/es-app/src/main.cpp b/es-app/src/main.cpp index 728a0b9bbc..9592c14644 100644 --- a/es-app/src/main.cpp +++ b/es-app/src/main.cpp @@ -33,6 +33,7 @@ #include "AudioManager.h" #include "NetworkThread.h" #include "scrapers/ThreadedScraper.h" +#include "ImageIO.h" bool scrape_cmdline = false; @@ -355,6 +356,8 @@ bool loadSystemConfigFile(Window* window, const char** errorString) { *errorString = NULL; + ImageIO::loadImageCache(); + if (!SystemData::loadConfig(window)) { LOG(LogError) << "Error while parsing systems configuration file!"; @@ -672,6 +675,7 @@ int main(int argc, char* argv[]) if (SystemData::hasDirtySystems()) window.renderLoadingScreen(_("SAVING DATA. PLEASE WAIT...")); + ImageIO::saveImageCache(); MameNames::deinit(); CollectionSystemManager::deinit(); SystemData::deleteSystems(); diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index b9544b9f63..7ec5cf4aca 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -14,22 +14,142 @@ #include #include -static std::map sizeCache; +struct CachedFileInfo +{ + CachedFileInfo(int sz, int sx, int sy) + { + size = sz; + x = sx; + y = sy; + sizeResolved = true; + }; + + CachedFileInfo() + { + size = 0; + x = 0; + y = 0; + sizeResolved = false; + }; + + int size; + int x; + int y; + bool sizeResolved; +}; + +static std::map sizeCache; +static bool sizeCacheDirty = false; + +static void addtoCache(const std::string fn, int sz, int x, int y) +{ + sizeCache[fn] = CachedFileInfo(sz, x, y); + + if (sz > 0 && x > 0 && fn.find("/themes/") == std::string::npos) + sizeCacheDirty = true; +} + +#include +#include + +std::string getImageCacheFilename() +{ + return Utils::FileSystem::getHomePath() + "/.emulationstation/imagecache.db"; +} + +void ImageIO::loadImageCache() +{ + std::string fname = getImageCacheFilename(); + + std::ifstream f(fname.c_str()); + if (f.fail()) + return; + + std::string relativeTo = Utils::FileSystem::getParent(Utils::FileSystem::getHomePath()); + + std::string line; + while (std::getline(f, line)) + { + auto splits = Utils::String::split(line, '|'); + if (splits.size() == 4) + { + std::string file = splits[0]; + file = Utils::FileSystem::resolveRelativePath(splits[0], relativeTo, true); + + CachedFileInfo fi; + fi.size = atoi(splits[1].c_str()); + fi.x = atoi(splits[2].c_str()); + fi.y = atoi(splits[3].c_str()); + + sizeCache[file] = fi; + } + } + + f.close(); +} + +void ImageIO::saveImageCache() +{ + if (!sizeCacheDirty) + return; + + std::string fname = getImageCacheFilename(); + std::ofstream f(fname.c_str(), std::ios::binary); + if (f.fail()) + return; + + std::string relativeTo = Utils::FileSystem::getParent(Utils::FileSystem::getHomePath()); + for (auto it : sizeCache) + { + if (it.second.size < 0) + continue; + + if (it.first.find("/themes/") != std::string::npos) + continue; + + std::string path = Utils::FileSystem::createRelativePath(it.first, "_path_", true); + if (path[0] != '~') + path = Utils::FileSystem::createRelativePath(it.first, relativeTo, false); + + f << path; + f << "|"; + f << std::to_string(it.second.size); + f << "|"; + f << std::to_string(it.second.x); + f << "|"; + f << std::to_string(it.second.y); + f << "\n"; + } + + f.close(); +} + bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) { + auto it = sizeCache.find(fn); if (it != sizeCache.cend()) { if (*x < 0) return false; - *x = it->second.x(); - *y = it->second.y(); - return true; - } + bool resolved = true; - std::unique_lock lock(ResourceManager::FileSystemLock); + if (!it->second.sizeResolved) + { + auto size = Utils::FileSystem::getFileSize(fn); + if (it->second.size != size) + resolved = false; + } + + if (resolved) + { + *x = it->second.x; + *y = it->second.y; + return true; + } + } LOG(LogDebug) << "ImageIO::loadImageSize " << fn; @@ -40,11 +160,14 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) return false; } + std::unique_lock lock(ResourceManager::FileSystemLock); + auto size = Utils::FileSystem::getFileSize(fn); + FILE *f = fopen(fn, "rb"); if (f == 0) { LOG(LogWarning) << "ImageIO::loadImageSize\tUnable to open file"; - sizeCache[fn] = Vector2i(-1, -1); + addtoCache(fn, -1, -1, -1); return false; } @@ -56,7 +179,7 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) unsigned char buf[24]; if (fread(buf, 1, 24, f) != 24) { - sizeCache[fn] = Vector2i(-1, -1); + addtoCache(fn, -1, -1, -1); return false; } @@ -97,11 +220,11 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) if (*x > 5000) // security ? { - sizeCache[fn] = Vector2i(-1, -1); + addtoCache(fn, -1, -1, -1); return false; } - sizeCache[fn] = Vector2i(*x, *y); + addtoCache(fn, size, *x, *y); return true; } @@ -113,7 +236,7 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) LOG(LogDebug) << "ImageIO::loadImageSize\tGIF size " << std::string(std::to_string(*x) + "x" + std::to_string(*y)).c_str(); - sizeCache[fn] = Vector2i(*x, *y); + addtoCache(fn, size, *x, *y); return true; } @@ -125,11 +248,11 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) LOG(LogDebug) << "ImageIO::loadImageSize\tPNG size " << std::string(std::to_string(*x) + "x" + std::to_string(*y)).c_str(); - sizeCache[fn] = Vector2i(*x, *y); + addtoCache(fn, size, *x, *y); return true; } - sizeCache[fn] = Vector2i(-1, -1); + addtoCache(fn, -1, -1, -1); LOG(LogWarning) << "ImageIO::loadImageSize\tUnable to extract size"; return false; } diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index 5ccc324b3a..1048d64be8 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -20,6 +20,9 @@ class ImageIO static Vector2i adjustPictureSize(Vector2i imageSize, Vector2i maxSize, bool externSize = false); static Vector2f adjustExternPictureSizef(Vector2f imageSize, Vector2f maxSize); static Vector2f adjustPictureSizef(Vector2f imageSize, Vector2f maxSize); + + static void loadImageCache(); + static void saveImageCache(); }; #endif // ES_CORE_IMAGE_IO diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index 55b4cc8f8d..e0fd8b376c 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -729,6 +729,9 @@ void ImageGridComponent::updateTiles(bool allowAnimation, bool updateSelected if (!mTiles.size()) return; + if (!mEntries.size()) + return; + // Stop updating the tiles at highest scroll speed if (mScrollTier == 3) { diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index a664116f51..15297b9aee 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -288,7 +288,7 @@ void TextureLoader::threadProc() std::this_thread::yield(); textureData->load(); - mManager->onTextureLoaded(textureData); + // mManager->onTextureLoaded(textureData); lock.lock(); mProcessingTextureDataQ.remove(textureData); diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index acb2cf3780..29782ce4a8 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -533,7 +533,13 @@ namespace Utils if(_path.size() >= 2 && _path[0] == ':' && _path[1] == '/') return _path; +#if WIN32 + std::string path = _path[0] == '.' ? getAbsolutePath(_path) : getGenericPath(_path); + if (path.find("./") == std::string::npos) + return path; +#else std::string path = exists(_path) ? getAbsolutePath(_path) : getGenericPath(_path); +#endif // cleanup path bool scan = true; From 6142c7a725d384d67daa030bea615b159e457eff Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Tue, 31 Dec 2019 13:29:27 +0100 Subject: [PATCH 287/336] System Grouping : Support for group systems -> use ports to move a system as a folder of a new system called "ports". --- es-app/src/FileData.cpp | 11 ++- es-app/src/FileData.h | 14 ++-- es-app/src/Gamelist.cpp | 7 +- es-app/src/SystemData.cpp | 68 +++++++++++++++++++ es-app/src/SystemData.h | 9 +++ es-app/src/guis/GuiMenu.cpp | 12 ++-- es-app/src/guis/GuiScraperStart.cpp | 3 + es-app/src/views/SystemView.cpp | 2 +- es-app/src/views/ViewController.cpp | 7 +- .../src/views/gamelist/GridGameListView.cpp | 39 +++++++++-- es-app/src/views/gamelist/GridGameListView.h | 1 + .../views/gamelist/ISimpleGameListView.cpp | 11 ++- es-core/src/components/GridTileComponent.cpp | 23 +++++-- es-core/src/components/ImageGridComponent.h | 13 +++- 14 files changed, 181 insertions(+), 39 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index 80d178abea..abb28dc9c0 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -550,15 +550,14 @@ std::vector FolderData::getFilesRecursive(unsigned int typeMask, bool return out; } -void FolderData::addChild(FileData* file) +void FolderData::addChild(FileData* file, bool assignParent) { - assert(mType == FOLDER); - assert(file->getParent() == NULL); - - const std::string key = file->getKey(); + assert(file->getParent() == nullptr || !assignParent); mChildren.push_back(file); - file->setParent(this); + + if (assignParent) + file->setParent(this); } void FolderData::removeChild(FileData* file) diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index e0c2e0780f..425f739f16 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -126,18 +126,23 @@ class CollectionFileData : public FileData class FolderData : public FileData { public: - FolderData(const std::string& startpath, SystemData* system) : FileData(FOLDER, startpath, system) + FolderData(const std::string& startpath, SystemData* system, bool ownsChildrens=true) : FileData(FOLDER, startpath, system) { + mOwnsChildrens = ownsChildrens; } ~FolderData() { - for (int i = mChildren.size() - 1; i >= 0; i--) - delete mChildren.at(i); + if (mOwnsChildrens) + { + for (int i = mChildren.size() - 1; i >= 0; i--) + delete mChildren.at(i); + } mChildren.clear(); } + inline bool isVirtualFolder() { return !mOwnsChildrens; } FileData* FindByPath(const std::string& path); @@ -145,7 +150,7 @@ class FolderData : public FileData const std::vector getChildrenListToDisplay(); std::vector getFilesRecursive(unsigned int typeMask, bool displayedOnly = false, SystemData* system = nullptr) const; - void addChild(FileData* file); // Error if mType != FOLDER + void addChild(FileData* file, bool assignParent = true); // Error if mType != FOLDER void removeChild(FileData* file); //Error if mType != FOLDER void createChildrenByFilenameMap(std::unordered_map& map); @@ -156,6 +161,7 @@ class FolderData : public FileData std::vector getFlatGameList(bool displayedOnly, SystemData* system) const; std::vector mChildren; + bool mOwnsChildrens; }; #endif // ES_APP_FILE_DATA_H diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index 9f78676d14..b5bdd6d436 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -289,6 +289,9 @@ bool saveToGamelistRecovery(FileData* file) bool hasDirtyFile(SystemData* system) { + if (system == nullptr || !system->isGameSystem() || system->getName() == "imageviewer") + return false; + FolderData* rootFolder = system->getRootFolder(); if (rootFolder == nullptr) return false; @@ -307,10 +310,10 @@ void updateGamelist(SystemData* system) //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")) + if(system == nullptr || Settings::getInstance()->getBool("IgnoreGamelist")) return; - if (system != nullptr && system->getName() == "imageviewer" || system->isCollection()) + if (system->getName() == "imageviewer" || system->isCollection() || !system->isGameSystem()) return; FolderData* rootFolder = system->getRootFolder(); diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 6755e2b87e..a15d64fc2c 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -24,6 +24,7 @@ std::vector SystemData::sSystemVector; SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) { + mIsGroupSystem = false; mGameListHash = 0; mGameCount = -1; mSortId = Settings::getInstance()->getInt(getName() + ".sort"), @@ -347,6 +348,7 @@ SystemData* SystemData::loadSystem(pugi::xml_node system) envData->mLaunchCommand = cmd; envData->mPlatformIds = platformIds; envData->mEmulators = emulatorList; + envData->mGroup = system.child("group").text().get(); SystemData* newSys = new SystemData(name, fullname, envData, themeFolder); if (newSys->getRootFolder()->getChildren().size() == 0) @@ -360,6 +362,55 @@ SystemData* SystemData::loadSystem(pugi::xml_node system) return newSys; } +void SystemData::createGroupedSystems() +{ + std::map> map; + + for (auto it = sSystemVector.cbegin(); it != sSystemVector.cend(); it++) + { + SystemData* sys = *it; + if (!sys->isCollection() && !sys->getSystemEnvData()->mGroup.empty()) + map[sys->getSystemEnvData()->mGroup].push_back(sys); + } + + for (auto item : map) + { + SystemEnvironmentData* envData = new SystemEnvironmentData; + envData->mStartPath = ""; + envData->mLaunchCommand = ""; + + SystemData* system = new SystemData(item.first, item.first, envData, item.first, nullptr); + system->mIsGroupSystem = true; + system->mIsGameSystem = false; + + FolderData* root = system->getRootFolder(); + + for (auto childSystem : item.second) + { + auto folder = new FolderData(childSystem->getRootFolder()->getPath(), childSystem, false); + root->addChild(folder); + + auto theme = childSystem->getTheme(); + if (theme) + { + const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); + if (logoElem && logoElem->has("path")) + { + std::string path = logoElem->get("path"); + folder->setMetadata("image", path); + folder->setMetadata("thumbnail", path); + } + } + + auto children = childSystem->getRootFolder()->getFilesRecursive(GAME | FOLDER); + for (auto child : children) + folder->addChild(child, false); + } + + sSystemVector.push_back(system); + } +} + //creates systems from information located in a config file bool SystemData::loadConfig(Window* window) { @@ -482,6 +533,7 @@ bool SystemData::loadConfig(Window* window) if (window != NULL) window->renderLoadingScreen(_("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + createGroupedSystems(); CollectionSystemManager::get()->updateSystemsList(); } else @@ -489,6 +541,7 @@ bool SystemData::loadConfig(Window* window) if (window != NULL) window->renderLoadingScreen(_("Favorites"), systemCount == 0 ? 0 : currentSystem / systemCount); + createGroupedSystems(); CollectionSystemManager::get()->loadCollectionSystems(); } @@ -595,6 +648,9 @@ std::string SystemData::getConfigPath(bool forWrite) bool SystemData::isVisible() { + if (isGroupChildSystem()) + return false; + if ((getDisplayedGameCount() > 0 || (UIModeController::getInstance()->isUIModeFull() && mIsCollectionSystem) || (mIsCollectionSystem && mName == "favorites"))) @@ -791,3 +847,15 @@ void SystemData::deleteIndex() mFilterIndex = nullptr; } } + +SystemData* SystemData::getParentGroupSystem() +{ + if (!isGroupChildSystem() || isGroupSystem()) + return this; + + for (auto sys : SystemData::sSystemVector) + if (sys->isGroupSystem() && sys->getName() == mEnvData->mGroup) + return sys; + + return this; +} \ No newline at end of file diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index f32300ff98..879cefdb85 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -37,6 +37,7 @@ struct SystemEnvironmentData std::string mLaunchCommand; std::vector mPlatformIds; std::vector mEmulators; + std::string mGroup; bool isValidExtension(const std::string extension) { @@ -143,6 +144,9 @@ class SystemData inline bool isCollection() { return mIsCollectionSystem; }; inline bool isGameSystem() { return mIsGameSystem; }; + inline bool isGroupSystem() { return mIsGroupSystem; }; + inline bool isGroupChildSystem() { return mEnvData != nullptr && !mEnvData->mGroup.empty(); } + bool isVisible(); SystemData* getNext() const; @@ -183,12 +187,17 @@ class SystemData void setGamelistHash(size_t size) { mGameListHash = size; } size_t getGamelistHash() { return mGameListHash; } + SystemData* getParentGroupSystem(); + private: static SystemData* loadSystem(pugi::xml_node system); + static void createGroupedSystems(); size_t mGameListHash; bool mIsCollectionSystem; bool mIsGameSystem; + bool mIsGroupSystem; + std::string mName; std::string mFullName; SystemEnvironmentData* mEnvData; diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index cb8473070e..d25e77950b 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -898,13 +898,11 @@ void GuiMenu::openUISettings() // Optionally start in selected system auto systemfocus_list = std::make_shared< OptionListComponent >(mWindow, _("START ON SYSTEM"), false); systemfocus_list->add(_("NONE"), "", Settings::getInstance()->getString("StartupSystem") == ""); + for (auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) - { - if ("retropie" != (*it)->getName()) - { + if ("retropie" != (*it)->getName() && (*it)->isVisible()) systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); - } - } + s->addWithLabel(_("START ON SYSTEM"), systemfocus_list); s->addSaveFunc([systemfocus_list] { Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); @@ -918,7 +916,7 @@ void GuiMenu::openUISettings() auto displayedSystems = std::make_shared>(mWindow, _("VISIBLE SYSTEMS"), true); for (auto system : SystemData::sSystemVector) - if(!system->isCollection()) + if(!system->isCollection() && !system->isGroupChildSystem()) displayedSystems->add(system->getFullName(), system, std::find(hiddenSystems.cbegin(), hiddenSystems.cend(), system->getName()) == hiddenSystems.cend()); s->addWithLabel(_("VISIBLE SYSTEMS"), displayedSystems); @@ -930,7 +928,7 @@ void GuiMenu::openUISettings() for (auto system : SystemData::sSystemVector) { - if (system->isCollection()) + if (system->isCollection() || system->isGroupChildSystem()) continue; if (std::find(sys.cbegin(), sys.cend(), system) == sys.cend()) diff --git a/es-app/src/guis/GuiScraperStart.cpp b/es-app/src/guis/GuiScraperStart.cpp index afa16e4e56..ee7c67a2d7 100644 --- a/es-app/src/guis/GuiScraperStart.cpp +++ b/es-app/src/guis/GuiScraperStart.cpp @@ -57,6 +57,9 @@ GuiScraperStart::GuiScraperStart(Window* window) : GuiComponent(window), mSystems = std::make_shared< OptionListComponent >(mWindow, _("SCRAPE THESE SYSTEMS"), true); for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) { + if ((*it)->isGroupSystem()) + continue; + if (!(*it)->hasPlatformId(PlatformIds::PLATFORM_IGNORE)) mSystems->add((*it)->getFullName(), *it, currentSystem.empty() ? diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index b484a540b6..1aa75d143b 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -488,7 +488,7 @@ void SystemView::onCursorChanged(const CursorState& /*state*/) setAnimation(infoFadeOut, 0, [this, gameCount] { std::stringstream ss; - if (!getSelected()->isGameSystem()) + if (!getSelected()->isGameSystem() && !getSelected()->isGroupSystem()) ss << "CONFIGURATION"; else { diff --git a/es-app/src/views/ViewController.cpp b/es-app/src/views/ViewController.cpp index 78f9932d6f..553421d187 100644 --- a/es-app/src/views/ViewController.cpp +++ b/es-app/src/views/ViewController.cpp @@ -56,7 +56,7 @@ void ViewController::goToStart(bool forceImmediate) if("" != requestedSystem && "retropie" != requestedSystem) { for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++){ - if ((*it)->getName() == requestedSystem) + if ((*it)->getName() == requestedSystem && !(*it)->isGroupChildSystem()) { if (hideSystemView || startOnGamelist) goToGameList(*it, forceImmediate); @@ -564,7 +564,10 @@ void ViewController::preload() mWindow->renderLoadingScreen(_("Preloading UI"), (float)i / (float)max); for(auto it = SystemData::sSystemVector.cbegin(); it != SystemData::sSystemVector.cend(); it++) - { + { + if ((*it)->isGroupChildSystem()) + continue; + if (splash) { i++; diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 4a9d4e7dbd..569421363b 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -230,6 +230,11 @@ const std::string GridGameListView::getImagePath(FileData* file) return file->getThumbnailPath(); } +const bool GridGameListView::isVirtualFolder(FileData* file) +{ + return file->getType() == FOLDER && ((FolderData*)file)->isVirtualFolder(); +} + void GridGameListView::populateList(const std::vector& files) { mGrid.clear(); @@ -239,8 +244,28 @@ void GridGameListView::populateList(const std::vector& files) { if (mCursorStack.size()) { + auto top = mCursorStack.top(); + + std::string imagePath; + + // Find logo image from original system + if (mCursorStack.size() == 1 && top->getSystem()->isGroupChildSystem()) + { + std::string startPath = top->getSystem()->getStartPath(); + + auto parent = top->getSystem()->getParentGroupSystem(); + for (auto child : parent->getRootFolder()->getChildren()) + { + if (child->getPath() == startPath) + { + imagePath = child->getMetadata("image"); + break; + } + } + } + FileData* placeholder = new FileData(PLACEHOLDER, "..", this->mRoot->getSystem()); - mGrid.add(". .", "", "", "", false, true, placeholder); + mGrid.add(". .", imagePath, "", "", false, true, !imagePath.empty(), placeholder); } std::string systemName = mRoot->getSystem()->getFullName(); @@ -255,7 +280,7 @@ void GridGameListView::populateList(const std::vector& files) for (auto file : files) { if (file->getFavorite() && showFavoriteIcon) - mGrid.add(file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), true, file->getType() != GAME, file); + mGrid.add(file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), true, file->getType() != GAME, isVirtualFolder(file), file); } } @@ -268,15 +293,15 @@ void GridGameListView::populateList(const std::vector& files) if (showFavoriteIcon) { - mGrid.add(_U("\uF006 ") + file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), true, file->getType() != GAME, file); + mGrid.add(_U("\uF006 ") + file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), true, file->getType() != GAME, isVirtualFolder(file), file); continue; } } if (file->getType() == FOLDER && Utils::FileSystem::exists(getImagePath(file))) - mGrid.add(_U("\uF114 ") + file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), file->getFavorite(), file->getType() != GAME, file); + mGrid.add(_U("\uF114 ") + file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), file->getFavorite(), file->getType() != GAME, isVirtualFolder(file), file); else - mGrid.add(file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), file->getFavorite(), file->getType() != GAME, file); + mGrid.add(file->getName(), getImagePath(file), file->getVideoPath(), file->getMarqueePath(), file->getFavorite(), file->getType() != GAME, isVirtualFolder(file), file); } } else @@ -612,8 +637,8 @@ void GridGameListView::updateInfoPanel() void GridGameListView::addPlaceholder() { // empty grid - add a placeholder - FileData* placeholder = new FileData(PLACEHOLDER, "<" + _("No Entries Found") + ">", this->mRoot->getSystem()); - mGrid.add(placeholder->getName(), "", "", "", false, false, placeholder); + FileData* placeholder = new FileData(PLACEHOLDER, "<" + _("No Entries Found") + ">", mRoot->getSystem()); + mGrid.add(placeholder->getName(), "", "", "", false, false, false, placeholder); } void GridGameListView::launch(FileData* game) diff --git a/es-app/src/views/gamelist/GridGameListView.h b/es-app/src/views/gamelist/GridGameListView.h index 9e542ec48f..042227a702 100644 --- a/es-app/src/views/gamelist/GridGameListView.h +++ b/es-app/src/views/gamelist/GridGameListView.h @@ -52,6 +52,7 @@ class GridGameListView : public ISimpleGameListView private: void updateInfoPanel(); const std::string getImagePath(FileData* file); + const bool isVirtualFolder(FileData* file); void createMarquee(); void createImage(); diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 7df5ce90e4..52e79c0cf9 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -126,7 +126,11 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) auto top = mCursorStack.top(); mCursorStack.pop(); - populateList(top->getParent()->getChildrenListToDisplay()); + FolderData* folder = top->getParent(); + if (folder == nullptr) + folder = getCursor()->getSystem()->getParentGroupSystem()->getRootFolder(); + + populateList(folder->getChildrenListToDisplay()); setCursor(top); Sound::getFromTheme(getTheme(), getName(), "back")->play(); } @@ -134,7 +138,10 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) { onFocusLost(); SystemData* systemToView = getCursor()->getSystem(); - if (systemToView->isCollection()) + + if (systemToView->isGroupChildSystem()) + systemToView = systemToView->getParentGroupSystem(); + else if (systemToView->isCollection()) systemToView = CollectionSystemManager::get()->getSystemToView(systemToView); ViewController::get()->goToSystemView(systemToView); diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index acd8dbb856..da13ab000f 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -116,7 +116,7 @@ void GridTileComponent::resize() if (mSize != size) setSize(size); - bool isDefaultImage = mIsDefaultImage && (mCurrentPath == ":/folder.svg" || mCurrentPath == ":/cartridge.svg"); + bool isDefaultImage = mIsDefaultImage; // && (mCurrentPath == ":/folder.svg" || mCurrentPath == ":/cartridge.svg"); float height = (int) (size.y() * currentProperties.Label.size.y()); float labelHeight = height; @@ -133,8 +133,17 @@ void GridTileComponent::resize() // Image if (currentProperties.Image.Loaded) { + if (isDefaultImage) + { + imageOffset.x() += imageSize.x() * 0.05; + imageSize.x() *= 0.90; + } + currentProperties.Image.updateImageComponent(mImage, imageOffset, imageSize, false); + if (mImage != nullptr && isDefaultImage) + mImage->setRoundCorners(0); + if (mImage != nullptr && currentProperties.Image.sizeMode != "maxSize" && isDefaultImage) mImage->setMaxSize(imageSize.x(), imageSize.y()); } @@ -165,8 +174,8 @@ void GridTileComponent::resize() imageOffset = Vector2f(pos.x() - imageSize.x() * origin.x(), pos.y() - imageSize.y() * origin.y()); } - // Text - mLabel.setVisible(currentProperties.Label.Visible || mIsDefaultImage); + // Text + mLabel.setVisible(!mLabel.getText().empty() && (currentProperties.Label.Visible || mIsDefaultImage)); if (currentProperties.Label.Visible) { @@ -319,7 +328,7 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) Vector2i pos((int)Math::round(trans.translation()[0] + padding), (int)Math::round(trans.translation()[1] + topPadding)); Vector2i size((int)Math::round(mSize.x() - 2 * padding), (int)Math::round(mSize.y() - topPadding - bottomPadding)); - bool isDefaultImage = mIsDefaultImage && (mCurrentPath == ":/folder.svg" || mCurrentPath == ":/cartridge.svg"); + bool isDefaultImage = mIsDefaultImage; // && (mCurrentPath == ":/folder.svg" || mCurrentPath == ":/cartridge.svg"); bool isMinSize = currentProperties.Image.sizeMode == "minSize" && !isDefaultImage; if (isMinSize) @@ -813,7 +822,11 @@ bool GridTileComponent::isSelected() const void GridTileComponent::setImage(const std::string& path, bool isDefaultImage) { - mIsDefaultImage = isDefaultImage; + if (path == ":/folder.svg" || path == ":/cartridge.svg") + mIsDefaultImage = true; + else + mIsDefaultImage = isDefaultImage; + if (mCurrentPath == path) return; diff --git a/es-core/src/components/ImageGridComponent.h b/es-core/src/components/ImageGridComponent.h index e0fd8b376c..445fa06b25 100644 --- a/es-core/src/components/ImageGridComponent.h +++ b/es-core/src/components/ImageGridComponent.h @@ -41,6 +41,7 @@ struct ImageGridData std::string videoPath; bool favorite; bool folder; + bool virtualFolder; }; template @@ -65,7 +66,7 @@ class ImageGridComponent : public IList ImageGridComponent(Window* window); - void add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, bool favorite, bool folder, const T& obj); + void add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, bool favorite, bool folder, bool virtualFolder, const T& obj); bool input(InputConfig* config, Input input) override; void update(int deltaTime) override; @@ -177,7 +178,7 @@ ImageGridComponent::ImageGridComponent(Window* window) : IList -void ImageGridComponent::add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, bool favorite, bool folder, const T& obj) +void ImageGridComponent::add(const std::string& name, const std::string& imagePath, const std::string& videoPath, const std::string& marqueePath, bool favorite, bool folder, bool virtualFolder, const T& obj) { typename IList::Entry entry; entry.name = name; @@ -187,6 +188,7 @@ void ImageGridComponent::add(const std::string& name, const std::string& imag entry.data.marqueePath = marqueePath; entry.data.favorite = favorite; entry.data.folder = folder; + entry.data.virtualFolder = virtualFolder; static_cast*>(this)->add(entry); mEntriesDirty = true; @@ -845,7 +847,12 @@ void ImageGridComponent::updateTileAtPos(int tilePos, int imgPos, bool allowA std::string imagePath = mEntries.at(imgPos).data.texturePath; if (ResourceManager::getInstance()->fileExists(imagePath)) - tile->setImage(imagePath); + { + if (mEntries.at(imgPos).data.virtualFolder) + tile->setLabel(""); // _U("\uF114")); + + tile->setImage(imagePath, mEntries.at(imgPos).data.virtualFolder); + } else if (mEntries.at(imgPos).data.folder && Utils::String::startsWith(name, ".")) tile->setImage(mDefaultFolderTexture, true); else From 68b8f01317550a9239ff50f38b6825f7725a94d6 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 1 Jan 2020 16:42:37 +0100 Subject: [PATCH 288/336] Fix Grouped System can crash... --- es-app/src/SystemData.cpp | 47 ++++++++++++++++++++++----------------- es-app/src/SystemData.h | 2 +- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index a15d64fc2c..b8698c8f2a 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -21,10 +21,10 @@ using namespace Utils; std::vector SystemData::sSystemVector; -SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem) : +SystemData::SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem, bool groupedSystem) : mName(name), mFullName(fullName), mEnvData(envData), mThemeFolder(themeFolder), mIsCollectionSystem(CollectionSystem), mIsGameSystem(true) { - mIsGroupSystem = false; + mIsGroupSystem = groupedSystem; mGameListHash = 0; mGameCount = -1; mSortId = Settings::getInstance()->getInt(getName() + ".sort"), @@ -34,7 +34,7 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, Sys mFilterIndex = nullptr;// new FileFilterIndex(); // if it's an actual system, initialize it, if not, just create the data structure - if (!CollectionSystem) + if(!CollectionSystem && !mIsGroupSystem) { mRootFolder = new FolderData(mEnvData->mStartPath, this); mRootFolder->getMetadata().set("name", mFullName); @@ -379,35 +379,42 @@ void SystemData::createGroupedSystems() envData->mStartPath = ""; envData->mLaunchCommand = ""; - SystemData* system = new SystemData(item.first, item.first, envData, item.first, nullptr); + SystemData* system = new SystemData(item.first, item.first, envData, item.first, false, true); system->mIsGroupSystem = true; system->mIsGameSystem = false; FolderData* root = system->getRootFolder(); for (auto childSystem : item.second) - { - auto folder = new FolderData(childSystem->getRootFolder()->getPath(), childSystem, false); - root->addChild(folder); - - auto theme = childSystem->getTheme(); - if (theme) + { + auto children = childSystem->getRootFolder()->getFilesRecursive(GAME | FOLDER); + if (children.size() > 0) { - const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); - if (logoElem && logoElem->has("path")) + auto folder = new FolderData(childSystem->getRootFolder()->getPath(), childSystem, false); + root->addChild(folder); + + auto theme = childSystem->getTheme(); + if (theme) { - std::string path = logoElem->get("path"); - folder->setMetadata("image", path); - folder->setMetadata("thumbnail", path); + const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); + if (logoElem && logoElem->has("path")) + { + std::string path = logoElem->get("path"); + folder->setMetadata("image", path); + folder->setMetadata("thumbnail", path); + } } - } - auto children = childSystem->getRootFolder()->getFilesRecursive(GAME | FOLDER); - for (auto child : children) - folder->addChild(child, false); + for (auto child : children) + folder->addChild(child, false); + } } - sSystemVector.push_back(system); + if (root->getChildren().size() > 0) + { + system->loadTheme(); + sSystemVector.push_back(system); + } } } diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 879cefdb85..48db1d4fcb 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -102,7 +102,7 @@ struct SystemEnvironmentData class SystemData { public: - SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem = false); + SystemData(const std::string& name, const std::string& fullName, SystemEnvironmentData* envData, const std::string& themeFolder, bool CollectionSystem = false, bool groupedSystem = false); ~SystemData(); inline FolderData* getRootFolder() const { return mRootFolder; }; From f34d59cca188f921b848194ba83264c4c5975402 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 1 Jan 2020 19:07:07 +0100 Subject: [PATCH 289/336] Fix : Crash when removing a favorite when in favorite system --- es-app/src/CollectionSystemManager.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 4fe640f390..040bb6f8c0 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -539,6 +539,7 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) md->set("favorite", "false"); } sysData->addToIndex(file); + saveToGamelistRecovery(file); refreshCollectionSystems(file->getSourceFileData()); @@ -549,7 +550,7 @@ bool CollectionSystemManager::toggleGameInCollection(FileData* file) ViewController::get()->getGameListView(systemViewToUpdate)->onFileChanged(file, FILE_METADATA_CHANGED); } - saveToGamelistRecovery(file); + } char trstring[512]; From b119f650ec341dafaf2e4648433d30c9154a1a59 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 2 Jan 2020 00:15:11 +0100 Subject: [PATCH 290/336] Fix : Grouped systems - possible repetition of subfolder games & fix detection of single game in folder --- es-app/src/FileData.cpp | 18 +++++------------- es-app/src/SystemData.cpp | 2 +- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/es-app/src/FileData.cpp b/es-app/src/FileData.cpp index abb28dc9c0..6fb8de5767 100644 --- a/es-app/src/FileData.cpp +++ b/es-app/src/FileData.cpp @@ -485,23 +485,15 @@ const std::vector FolderData::getChildrenListToDisplay() FileData* FolderData::findUniqueGameForFolder() { - std::vector children = getChildren(); - - if (children.size() == 1 && children.at(0)->getType() == GAME) - return children.at(0); - - for (std::vector::const_iterator it = children.cbegin(); it != children.cend(); ++it) + auto games = getFilesRecursive(GAME); + if (games.size() == 1) { + auto it = games.cbegin(); if ((*it)->getType() == GAME) - return NULL; - - FolderData* folder = (FolderData*)(*it); - FileData* ret = folder->findUniqueGameForFolder(); - if (ret != NULL) - return ret; + return (*it); } - return NULL; + return nullptr; } std::vector FolderData::getFlatGameList(bool displayedOnly, SystemData* system) const diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index b8698c8f2a..680029af30 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -387,7 +387,7 @@ void SystemData::createGroupedSystems() for (auto childSystem : item.second) { - auto children = childSystem->getRootFolder()->getFilesRecursive(GAME | FOLDER); + auto children = childSystem->getRootFolder()->getChildren(); if (children.size() > 0) { auto folder = new FolderData(childSystem->getRootFolder()->getPath(), childSystem, false); From c47ad20d0ced8eb06cf9ebe4a210c276c86b42f7 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 2 Jan 2020 00:32:18 +0100 Subject: [PATCH 291/336] Grouped Views : fix possible crash when navigating to parent folder after "show folder" option change --- es-app/src/views/gamelist/ISimpleGameListView.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/es-app/src/views/gamelist/ISimpleGameListView.cpp b/es-app/src/views/gamelist/ISimpleGameListView.cpp index 52e79c0cf9..de7b7281ca 100644 --- a/es-app/src/views/gamelist/ISimpleGameListView.cpp +++ b/es-app/src/views/gamelist/ISimpleGameListView.cpp @@ -92,7 +92,11 @@ bool ISimpleGameListView::input(InputConfig* config, Input input) auto top = mCursorStack.top(); mCursorStack.pop(); - populateList(top->getParent()->getChildrenListToDisplay()); + FolderData* folder = top->getParent(); + if (folder == nullptr) + folder = getCursor()->getSystem()->getParentGroupSystem()->getRootFolder(); + + populateList(folder->getChildrenListToDisplay()); setCursor(top); Sound::getFromTheme(getTheme(), getName(), "back")->play(); } From 3f7457c97bc0288403f44964b133edbe10631966 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Thu, 2 Jan 2020 17:12:26 +0100 Subject: [PATCH 292/336] Fix : Gamelist filters not working anymore --- es-app/src/guis/GuiGamelistOptions.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/es-app/src/guis/GuiGamelistOptions.cpp b/es-app/src/guis/GuiGamelistOptions.cpp index 20d37d5713..d35b64bede 100644 --- a/es-app/src/guis/GuiGamelistOptions.cpp +++ b/es-app/src/guis/GuiGamelistOptions.cpp @@ -353,8 +353,7 @@ GuiGamelistOptions::~GuiGamelistOptions() // only reload full view if we came from a placeholder // as we need to re-display the remaining elements for whatever new // game is selected - mSystem->loadTheme(); - mSystem->resetFilters(); + mSystem->loadTheme(); ViewController::get()->reloadGameListView(mSystem); } } From 2edcb89d4edde41a339f552ae26dbe0334b0be4d Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 4 Jan 2020 22:08:35 +0100 Subject: [PATCH 293/336] GridView : Always show videos minSized to the size of the image (for video grid & box views) --- es-core/src/components/GridTileComponent.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index da13ab000f..75e6a07e7c 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -221,10 +221,10 @@ void GridTileComponent::resize() // Video if (mVideo != nullptr && mVideo->isPlaying()) { - if (currentProperties.Image.sizeMode == "minSize") + if (currentProperties.Image.sizeMode != "size") { mVideo->setOrigin(0.5, 0.5); - mVideo->setPosition(imageOffset.x() + imageSize.x() / 2, imageOffset.y() + imageSize.y() / 2); + mVideo->setPosition(imageOffset.x() + imageSize.x() / 2.0f, imageOffset.y() + imageSize.y() / 2.0f); mVideo->setMinSize(imageSize.x(), imageSize.y()); if (mImage != nullptr) @@ -329,7 +329,7 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) Vector2i size((int)Math::round(mSize.x() - 2 * padding), (int)Math::round(mSize.y() - topPadding - bottomPadding)); bool isDefaultImage = mIsDefaultImage; // && (mCurrentPath == ":/folder.svg" || mCurrentPath == ":/cartridge.svg"); - bool isMinSize = currentProperties.Image.sizeMode == "minSize" && !isDefaultImage; + bool isMinSize = currentProperties.Image.sizeMode != "size" && !isDefaultImage; if (isMinSize) Renderer::pushClipRect(pos, size); From 514ed2fb6f13932f314527dd52330ed394710289 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 5 Jan 2020 02:21:31 +0100 Subject: [PATCH 294/336] ImageComponent : Add boolean 'image.linearSmooth'. Allow smoothing for background images when upscaled. --- es-core/src/ThemeData.cpp | 1 + es-core/src/Window.cpp | 2 +- es-core/src/components/ImageComponent.cpp | 11 ++++++++--- es-core/src/components/ImageComponent.h | 5 +++++ es-core/src/resources/TextureData.cpp | 4 ++-- es-core/src/resources/TextureData.h | 3 ++- es-core/src/resources/TextureDataManager.cpp | 6 +++--- es-core/src/resources/TextureDataManager.h | 2 +- es-core/src/resources/TextureResource.cpp | 17 +++++++++-------- es-core/src/resources/TextureResource.h | 6 +++--- 10 files changed, 35 insertions(+), 22 deletions(-) diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 0563e4e2fe..86a2289ff4 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -38,6 +38,7 @@ std::map> The { "roundCorners", FLOAT }, { "flipX", BOOLEAN }, { "flipY", BOOLEAN }, + { "linearSmooth", BOOLEAN }, { "zIndex", FLOAT } } }, { "imagegrid", { { "pos", NORMALIZED_PAIR }, diff --git a/es-core/src/Window.cpp b/es-core/src/Window.cpp index 421d7adcf8..7135f43f02 100644 --- a/es-core/src/Window.cpp +++ b/es-core/src/Window.cpp @@ -488,7 +488,7 @@ void Window::loadCustomImageLoadingScreen(std::string imagePath, std::string cus if (mSplash != NULL) endRenderLoadingScreen(); - mSplash = TextureResource::get(imagePath, false, true, false, false, MaxSizeInfo(Renderer::getScreenWidth() * 0.60f, Renderer::getScreenHeight() * 0.60f)); + mSplash = TextureResource::get(imagePath, false, false, true, false, false, MaxSizeInfo(Renderer::getScreenWidth() * 0.60f, Renderer::getScreenHeight() * 0.60f)); mCustomSplash = customText; std::shared_ptr& rm = ResourceManager::getInstance(); diff --git a/es-core/src/components/ImageComponent.cpp b/es-core/src/components/ImageComponent.cpp index e33c2862e3..ca50bb3326 100644 --- a/es-core/src/components/ImageComponent.cpp +++ b/es-core/src/components/ImageComponent.cpp @@ -28,6 +28,7 @@ ImageComponent::ImageComponent(Window* window, bool forceLoad, bool dynamic) : G mTopLeftCrop(0.0f, 0.0f), mBottomRightCrop(1.0f, 1.0f), mMirror(0.0f, 0.0f), mAllowAsync(false), mPadding(Vector4f(0, 0, 0, 0)) { + mLinear = false; mHorizontalAlignment = ALIGN_CENTER; mVerticalAlignment = ALIGN_CENTER; mReflectOnBorders = false; @@ -170,11 +171,11 @@ void ImageComponent::setImage(std::string path, bool tile, MaxSizeInfo maxSize) if (mDefaultPath.empty() || !ResourceManager::getInstance()->fileExists(mDefaultPath)) mTexture.reset(); else - mTexture = TextureResource::get(mDefaultPath, tile, mForceLoad, mDynamic, true, maxSize); + mTexture = TextureResource::get(mDefaultPath, tile, mLinear, mForceLoad, mDynamic, true, maxSize); } else { - std::shared_ptr texture = TextureResource::get(mPath, tile, mForceLoad, mDynamic, true, maxSize); + std::shared_ptr texture = TextureResource::get(mPath, tile, mLinear, mForceLoad, mDynamic, true, maxSize); if (!mForceLoad && mDynamic && !mAllowFading && texture != nullptr && !texture->isLoaded()) mLoadingTexture = texture; @@ -553,6 +554,9 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s return; } + if (elem->has("linearSmooth")) + mLinear = elem->get("linearSmooth"); + Vector2f scale = getParent() ? getParent()->getSize() : Vector2f((float)Renderer::getScreenWidth(), (float)Renderer::getScreenHeight()); if(properties & POSITION && elem->has("pos")) @@ -570,7 +574,6 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s else if(elem->has("minSize")) setMinSize(elem->get("minSize") * scale); } - // position + size also implies origin if((properties & ORIGIN || (properties & POSITION && properties & ThemeFlags::SIZE)) && elem->has("origin")) @@ -580,6 +583,8 @@ void ImageComponent::applyTheme(const std::shared_ptr& theme, const s setDefaultImage(elem->get("default")); } + + if(properties & PATH && elem->has("path")) { auto path = elem->get("path"); diff --git a/es-core/src/components/ImageComponent.h b/es-core/src/components/ImageComponent.h index cd01709962..67226d5761 100644 --- a/es-core/src/components/ImageComponent.h +++ b/es-core/src/components/ImageComponent.h @@ -124,6 +124,9 @@ class ImageComponent : public GuiComponent std::string getImagePath() { return mPath; } bool isTiled(); + bool isLinear() { return mLinear; } + void setIsLinear(bool value) { mLinear = value; } + private: Vector2f mTargetSize; @@ -174,6 +177,8 @@ class ImageComponent : public GuiComponent bool mShowing; std::shared_ptr mPlaylist; float mPlaylistTimer; + + bool mLinear; }; #endif // ES_CORE_COMPONENTS_IMAGE_COMPONENT_H diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index 172fb2b997..c39dfbcd2e 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -14,7 +14,7 @@ bool TextureData::OPTIMIZEVRAM = false; -TextureData::TextureData(bool tile) : mTile(tile), mTextureID(0), mDataRGBA(nullptr), mScalable(false), +TextureData::TextureData(bool tile, bool linear) : mTile(tile), mLinear(linear), mTextureID(0), mDataRGBA(nullptr), mScalable(false), mWidth(0), mHeight(0), mSourceWidth(0.0f), mSourceHeight(0.0f), mMaxSize(MaxSizeInfo()), mPackedSize(Vector2i(0,0)), mBaseSize(Vector2i(0, 0)) { mIsExternalDataRGBA = false; @@ -311,7 +311,7 @@ bool TextureData::uploadAndBind() if ((mWidth == 0) || (mHeight == 0) || (mDataRGBA == nullptr)) return false; - mTextureID = Renderer::createTexture(Renderer::Texture::RGBA, mHeight >= 480, mTile, mWidth, mHeight, mDataRGBA); + mTextureID = Renderer::createTexture(Renderer::Texture::RGBA, mLinear, mTile, mWidth, mHeight, mDataRGBA); if (mTextureID) { if (mDataRGBA != nullptr && !mIsExternalDataRGBA) diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 7477c9bbe4..501f971849 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -14,7 +14,7 @@ class TextureData { public: - TextureData(bool tile); + TextureData(bool tile, bool linear); ~TextureData(); static bool OPTIMIZEVRAM; @@ -71,6 +71,7 @@ class TextureData private: std::mutex mMutex; bool mTile; + bool mLinear; unsigned char* mDataRGBA; size_t mWidth; size_t mHeight; diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index 15297b9aee..a28a0c6f35 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -10,7 +10,7 @@ TextureDataManager::TextureDataManager() { unsigned char data[5 * 5 * 4]; - mBlank = std::shared_ptr(new TextureData(false)); + mBlank = std::shared_ptr(new TextureData(false, false)); for (int i = 0; i < (5 * 5); ++i) { data[i * 4] = 0; // (i % 2) * 255; @@ -44,7 +44,7 @@ void TextureDataManager::onTextureLoaded(std::shared_ptr tex) } -std::shared_ptr TextureDataManager::add(const TextureResource* key, bool tiled) +std::shared_ptr TextureDataManager::add(const TextureResource* key, bool tiled, bool linear) { std::unique_lock lock(mMutex); @@ -58,7 +58,7 @@ std::shared_ptr TextureDataManager::add(const TextureResource* key, mTextureLookup.erase(it); } - std::shared_ptr data = std::make_shared(tiled); + std::shared_ptr data = std::make_shared(tiled, linear); mTextures.push_front(data); mTextureLookup[key] = mTextures.cbegin(); diff --git a/es-core/src/resources/TextureDataManager.h b/es-core/src/resources/TextureDataManager.h index 6ab1bd9076..d107548677 100644 --- a/es-core/src/resources/TextureDataManager.h +++ b/es-core/src/resources/TextureDataManager.h @@ -63,7 +63,7 @@ class TextureDataManager TextureDataManager(); ~TextureDataManager(); - std::shared_ptr add(const TextureResource* key, bool tiled); + std::shared_ptr add(const TextureResource* key, bool tiled, bool linear); // The texturedata being removed may be loading in a different thread. However it will // be referenced by a smart point so we only need to remove it from our array and it diff --git a/es-core/src/resources/TextureResource.cpp b/es-core/src/resources/TextureResource.cpp index f13fdd23af..e6149ea153 100644 --- a/es-core/src/resources/TextureResource.cpp +++ b/es-core/src/resources/TextureResource.cpp @@ -11,7 +11,7 @@ TextureDataManager TextureResource::sTextureDataManager; std::map< TextureResource::TextureKeyType, std::weak_ptr> TextureResource::sTextureMap; std::set TextureResource::sAllTextures; -TextureResource::TextureResource(const std::string& path, bool tile, bool dynamic, bool allowAsync, MaxSizeInfo maxSize) : mTextureData(nullptr), mForceLoad(false) +TextureResource::TextureResource(const std::string& path, bool tile, bool linear, bool dynamic, bool allowAsync, MaxSizeInfo maxSize) : mTextureData(nullptr), mForceLoad(false) { #if _DEBUG mPath = path; @@ -25,7 +25,7 @@ TextureResource::TextureResource(const std::string& path, bool tile, bool dynami std::shared_ptr data; if (dynamic) { - data = sTextureDataManager.add(this, tile); + data = sTextureDataManager.add(this, tile, linear); data->setMaxSize(maxSize); data->initFromPath(path); @@ -58,7 +58,7 @@ TextureResource::TextureResource(const std::string& path, bool tile, bool dynami } else { - mTextureData = std::shared_ptr(new TextureData(tile)); + mTextureData = std::shared_ptr(new TextureData(tile, linear)); data = mTextureData; data->setMaxSize(maxSize); @@ -73,7 +73,7 @@ TextureResource::TextureResource(const std::string& path, bool tile, bool dynami else { // Create a texture managed by this class because it cannot be dynamically loaded and unloaded - mTextureData = std::shared_ptr(new TextureData(tile)); + mTextureData = std::shared_ptr(new TextureData(tile, linear)); } if (sAllTextures.find(this) == sAllTextures.end()) @@ -178,14 +178,14 @@ void TextureResource::cancelAsync(std::shared_ptr texture) sTextureDataManager.cancelAsync(texture.get()); } -std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool forceLoad, bool dynamic, bool asReloadable, MaxSizeInfo maxSize) +std::shared_ptr TextureResource::get(const std::string& path, bool tile, bool linear, bool forceLoad, bool dynamic, bool asReloadable, MaxSizeInfo maxSize) { std::shared_ptr& rm = ResourceManager::getInstance(); const std::string canonicalPath = Utils::FileSystem::getCanonicalPath(path); if(canonicalPath.empty()) { - std::shared_ptr tex(new TextureResource("", tile, dynamic, !forceLoad, maxSize)); + std::shared_ptr tex(new TextureResource("", tile, linear, dynamic, !forceLoad, maxSize)); rm->addReloadable(tex); //make sure we get properly deinitialized even though we do nothing on reinitialization return tex; } @@ -194,7 +194,7 @@ std::shared_ptr TextureResource::get(const std::string& path, b if (canonicalPath.length() > 0 && canonicalPath[0] == ':') dynamic = false; - TextureKeyType key(canonicalPath, tile); + TextureKeyType key(canonicalPath, tile, linear); auto foundTexture = sTextureMap.find(key); if(foundTexture != sTextureMap.cend()) { @@ -226,7 +226,8 @@ std::shared_ptr TextureResource::get(const std::string& path, b // need to create it std::shared_ptr tex; - tex = std::shared_ptr(new TextureResource(key.first, tile, dynamic, !forceLoad, maxSize)); + + tex = std::shared_ptr(new TextureResource(std::get<0>(key), tile, linear, dynamic, !forceLoad, maxSize)); std::shared_ptr data = sTextureDataManager.get(tex.get(), !forceLoad); if (asReloadable) // // is it an SVG // if (key.first.substr(key.first.size() - 4, std::string::npos) != ".svg") // FCATMP diff --git a/es-core/src/resources/TextureResource.h b/es-core/src/resources/TextureResource.h index 254f263fdd..bbca831106 100644 --- a/es-core/src/resources/TextureResource.h +++ b/es-core/src/resources/TextureResource.h @@ -48,10 +48,10 @@ class MaxSizeInfo class TextureResource : public IReloadable { protected: - TextureResource(const std::string& path, bool tile, bool dynamic, bool allowAsync, MaxSizeInfo maxSize); + TextureResource(const std::string& path, bool tile, bool linear, bool dynamic, bool allowAsync, MaxSizeInfo maxSize); public: - static std::shared_ptr get(const std::string& path, bool tile = false, bool forceLoad = false, bool dynamic = true, bool asReloadable = true, MaxSizeInfo maxSize = MaxSizeInfo()); + static std::shared_ptr get(const std::string& path, bool tile = false, bool linear = false, bool forceLoad = false, bool dynamic = true, bool asReloadable = true, MaxSizeInfo maxSize = MaxSizeInfo()); static void cancelAsync(std::shared_ptr texture); void initFromPixels(const unsigned char* dataRGBA, size_t width, size_t height); @@ -92,7 +92,7 @@ class TextureResource : public IReloadable Vector2f mSourceSize; bool mForceLoad; - typedef std::pair TextureKeyType; + typedef std::tuple TextureKeyType; static std::map< TextureKeyType, std::weak_ptr > sTextureMap; // map of textures, used to prevent duplicate textures static std::map< TextureKeyType, std::shared_ptr > sPermanentTextureMap; // map of textures, used to prevent duplicate textures // FCAWEAK static std::set sAllTextures; // Set of all textures, used for memory management From 6b34561af96003a668c359c40085436c0cb05389 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 5 Jan 2020 12:02:54 +0100 Subject: [PATCH 295/336] ShellExecuteEx : Don't set directory for relative paths --- es-core/src/platform.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/es-core/src/platform.cpp b/es-core/src/platform.cpp index eb493732e3..e50a98e1d5 100644 --- a/es-core/src/platform.cpp +++ b/es-core/src/platform.cpp @@ -115,6 +115,7 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo std::string args; splitCommand(command, &exe, &args); + exe = Utils::FileSystem::getPreferredPath(exe); SHELLEXECUTEINFO lpExecInfo; lpExecInfo.cbSize = sizeof(SHELLEXECUTEINFO); @@ -152,7 +153,10 @@ int runSystemCommand(const std::string& cmd_utf8, const std::string& name, Windo } lpExecInfo.lpParameters = args.c_str(); // file name as an argument - lpExecInfo.lpDirectory = Utils::FileSystem::getAbsolutePath(Utils::FileSystem::getParent(exe)).c_str(); + + // Don't set directory for relative paths + if (!Utils::String::startsWith(exe, ".") && !Utils::String::startsWith(exe, "/") && !Utils::String::startsWith(exe, "\\")) + lpExecInfo.lpDirectory = Utils::FileSystem::getAbsolutePath(Utils::FileSystem::getParent(exe)).c_str(); ShellExecuteEx(&lpExecInfo); From aa2e947399e4d9baee713542065998e90a9a21bc Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 6 Jan 2020 17:23:12 +0100 Subject: [PATCH 296/336] SystemView : Support extra="static" items -> static items don't scroll with systems... --- es-app/src/views/SystemView.cpp | 132 ++++++++++++++++++++++---------- es-core/src/GuiComponent.cpp | 3 +- es-core/src/GuiComponent.h | 5 ++ es-core/src/ThemeData.cpp | 14 +++- es-core/src/ThemeData.h | 5 +- 5 files changed, 115 insertions(+), 44 deletions(-) diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 1aa75d143b..29ee991adb 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -865,6 +865,9 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp { if (extra->getZIndex() < lower || extra->getZIndex() >= upper) continue; + + if (extra->isStaticExtra()) + continue; std::string value = extra->getValue(); if (extra->isKindOf()) @@ -881,6 +884,9 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp if (extra->getZIndex() < lower || extra->getZIndex() >= upper) continue; + if (extra->isStaticExtra()) + continue; + std::string value = extra->getValue(); if (extra->isKindOf()) paths.erase(value); @@ -897,6 +903,9 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp if (extra->getZIndex() < lower || extra->getZIndex() >= upper) continue; + if (extra->isStaticExtra()) + continue; + std::string value = extra->getValue(); if (extra->isKindOf()) { @@ -924,63 +933,106 @@ void SystemView::renderExtras(const Transform4x4f& trans, float lower, float upp continue; //Only render selected system when not showing - if (mShowing || index == mCursor) + if (!mShowing && index != mCursor) + continue; + + Entry& entry = mEntries.at(index); + + Vector2i size = Vector2i(Math::round(mSize.x()), Math::round(mSize.y())); + + Transform4x4f extrasTrans = trans; + if (mCarousel.type == HORIZONTAL || mCarousel.type == HORIZONTAL_WHEEL) { - Transform4x4f extrasTrans = trans; - if (mCarousel.type == HORIZONTAL || mCarousel.type == HORIZONTAL_WHEEL) - extrasTrans.translate(Vector3f((i - mExtrasCamOffset) * mSize.x(), 0, 0)); - else - extrasTrans.translate(Vector3f(0, (i - mExtrasCamOffset) * mSize.y(), 0)); + extrasTrans.translate(Vector3f((i - mExtrasCamOffset) * mSize.x(), 0, 0)); + + if (extrasTrans.translation()[0] >= 0 && extrasTrans.translation()[0] <= Renderer::getScreenWidth() && extrasTrans.translation()[0] + mSize.x() > Renderer::getScreenWidth()) + size.x() = Renderer::getScreenWidth() - extrasTrans.translation()[0]; + } + else + { + extrasTrans.translate(Vector3f(0, (i - mExtrasCamOffset) * mSize.y(), 0)); - if (mExtrasFadeOpacity && mExtrasFadeOldCursor == index) - extrasTrans = trans; + if (extrasTrans.translation()[1] >= 0 && extrasTrans.translation()[1] <= Renderer::getScreenHeight() && extrasTrans.translation()[1] + mSize.y() > Renderer::getScreenHeight()) + size.y() = Renderer::getScreenHeight() - extrasTrans.translation()[1]; + } - Renderer::pushClipRect(Vector2i((int)extrasTrans.translation()[0], (int)extrasTrans.translation()[1]),Vector2i((int)mSize.x(), (int)mSize.y())); + if (!Renderer::isVisibleOnScreen(extrasTrans.translation()[0], extrasTrans.translation()[1], mSize.x(), mSize.y())) + continue; - for (GuiComponent* extra : mEntries.at(index).data.backgroundExtras) - { - if (extra->getZIndex() < lower || extra->getZIndex() >= upper) - continue; + if (mExtrasFadeOpacity && mExtrasFadeOldCursor == index) + extrasTrans = trans; + + Renderer::pushClipRect(Vector2i(Math::round(extrasTrans.translation()[0]), Math::round(extrasTrans.translation()[1])), Vector2i(Math::round(size.x()), Math::round(size.y()))); + + for (GuiComponent* extra : mEntries.at(index).data.backgroundExtras) + { + if (extra->getZIndex() < lower || extra->getZIndex() >= upper) + continue; - // ExtrasFadeOpacity : Apply opacity only on elements that are not common with the original view - if (mExtrasFadeOpacity) + // ExtrasFadeOpacity : Apply opacity only on elements that are not common with the original view + if (mExtrasFadeOpacity && !extra->isStaticExtra()) + { + std::string value = extra->getValue(); + if (extra->isKindOf()) { - std::string value = extra->getValue(); - if (extra->isKindOf()) - { - if (paths.find(value) != paths.cend()) - { - auto opa = extra->getOpacity(); - extra->setOpacity((1.0f - mExtrasFadeOpacity) * opa); - extra->render(extrasTrans); - extra->setOpacity(opa); - continue; - } - else if (((ImageComponent*)extra)->isTiled() && extra->getPosition() == Vector3f::Zero() && extra->getSize() == Vector2f(Renderer::getScreenWidth(), Renderer::getScreenHeight())) - { - auto opa = extra->getOpacity(); - extra->setOpacity((1.0f - mExtrasFadeOpacity) * opa); - extra->render(extrasTrans); - extra->setOpacity(opa); - continue; - } - } - else if (extra->isKindOf() && values.find(value) != values.cend()) + if (paths.find(value) != paths.cend()) + { + auto opa = extra->getOpacity(); + extra->setOpacity((1.0f - mExtrasFadeOpacity) * opa); + extra->render(extra->isStaticExtra() ? trans : extrasTrans); + extra->setOpacity(opa); + continue; + } + else if (((ImageComponent*)extra)->isTiled() && extra->getPosition() == Vector3f::Zero() && extra->getSize() == Vector2f(Renderer::getScreenWidth(), Renderer::getScreenHeight())) { auto opa = extra->getOpacity(); extra->setOpacity((1.0f - mExtrasFadeOpacity) * opa); - extra->render(extrasTrans); + extra->render(extra->isStaticExtra() ? trans : extrasTrans); extra->setOpacity(opa); continue; } } - - extra->render(extrasTrans); + else if (extra->isKindOf() && values.find(value) != values.cend()) + { + auto opa = extra->getOpacity(); + extra->setOpacity((1.0f - mExtrasFadeOpacity) * opa); + extra->render(extra->isStaticExtra() ? trans : extrasTrans); + extra->setOpacity(opa); + continue; + } } + + if (extra->isStaticExtra()) + { + if (extrasTrans.translation()[0] < 0) + { + int x = Math::round(size.x() + extrasTrans.translation()[0]); + if (x == 0) + continue; - Renderer::popClipRect(); + Renderer::pushClipRect(Vector2i(0, Math::round(extrasTrans.translation()[1])), Vector2i(x, Math::round(size.y()))); + } + else if (extrasTrans.translation()[1] < 0) + { + int y = Math::round(size.y() + extrasTrans.translation()[1]); + if (y == 0) + continue; + + Renderer::pushClipRect(Vector2i(Math::round(extrasTrans.translation()[0]), 0), Vector2i(Math::round(size.x()), y)); + } + + extra->render(trans); + + if (extrasTrans.translation()[0] < 0 || extrasTrans.translation()[1] < 0) + Renderer::popClipRect(); + } + else + extra->render(extrasTrans); } + + Renderer::popClipRect(); } + Renderer::popClipRect(); } diff --git a/es-core/src/GuiComponent.cpp b/es-core/src/GuiComponent.cpp index 8e5d69b2b3..aae592e10c 100644 --- a/es-core/src/GuiComponent.cpp +++ b/es-core/src/GuiComponent.cpp @@ -13,7 +13,8 @@ bool GuiComponent::ALLOWANIMATIONS = true; GuiComponent::GuiComponent(Window* window) : mWindow(window), mParent(NULL), mOpacity(255), mPosition(Vector3f::Zero()), mOrigin(Vector2f::Zero()), mRotationOrigin(0.5, 0.5), - mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false), mVisible(true) + mSize(Vector2f::Zero()), mTransform(Transform4x4f::Identity()), mIsProcessing(false), mVisible(true), + mStaticExtra(false) { for(unsigned char i = 0; i < MAX_ANIMATIONS; i++) mAnimationMap[i] = NULL; diff --git a/es-core/src/GuiComponent.h b/es-core/src/GuiComponent.h index 56d8afd940..e10e6bb07d 100644 --- a/es-core/src/GuiComponent.h +++ b/es-core/src/GuiComponent.h @@ -164,6 +164,9 @@ class GuiComponent void animateTo(Vector2f from, Vector2f to, unsigned int flags = 0xFFFFFFFF, int delay = 350); void animateTo(Vector2f from, unsigned int flags = AnimateFlags::OPACITY | AnimateFlags::SCALE, int delay = 350) { animateTo(from, from, flags, delay); } + bool isStaticExtra() const { return mStaticExtra; } + void setIsStaticExtra(bool value) { mStaticExtra = value; } + protected: void renderChildren(const Transform4x4f& transform) const; void updateSelf(int deltaTime); // updates animations @@ -191,6 +194,8 @@ class GuiComponent bool mIsProcessing; bool mVisible; + bool mStaticExtra; + public: const static unsigned char MAX_ANIMATIONS = 4; diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 86a2289ff4..e9a2f3b2f5 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -1003,8 +1003,17 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map ThemeData::makeExtras(const std::shared_ptrsetIsStaticExtra(elem.extra == 2); comp->setTag((*it).c_str()); comp->setDefaultZIndex(10); comp->applyTheme(theme, view, *it, ThemeFlags::ALL); diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index dc6ba81397..46d58b2ed5 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -172,7 +172,10 @@ class ThemeData class ThemeElement { public: - bool extra; + ThemeElement() { extra = 0; } + + int extra; + std::string type; struct Property From 42342196255fcdf1dd926cef6c42952b31245919 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 8 Jan 2020 19:18:26 +0100 Subject: [PATCH 297/336] Windows : Stop trying resolve simlinks (seems useless) --- es-core/src/utils/FileSystemUtil.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 29782ce4a8..7a34b421d2 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -573,7 +573,7 @@ namespace Utils #else // _WIN32 // append folder to path path += ("/" + (*it)); -#endif // _WIN32 + // resolve symlink if(isSymlink(path)) @@ -594,6 +594,7 @@ namespace Utils scan = true; break; } +#endif // _WIN32 } } From 6d56c2e77fd00fdae0e69c87461b695d5cfee4b7 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 11 Jan 2020 17:46:07 +0100 Subject: [PATCH 298/336] Fix : carousel view clipping --- es-core/src/components/GridTileComponent.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/es-core/src/components/GridTileComponent.cpp b/es-core/src/components/GridTileComponent.cpp index 75e6a07e7c..98b95513cc 100644 --- a/es-core/src/components/GridTileComponent.cpp +++ b/es-core/src/components/GridTileComponent.cpp @@ -329,8 +329,8 @@ void GridTileComponent::renderContent(const Transform4x4f& parentTrans) Vector2i size((int)Math::round(mSize.x() - 2 * padding), (int)Math::round(mSize.y() - topPadding - bottomPadding)); bool isDefaultImage = mIsDefaultImage; // && (mCurrentPath == ":/folder.svg" || mCurrentPath == ":/cartridge.svg"); - bool isMinSize = currentProperties.Image.sizeMode != "size" && !isDefaultImage; - + bool isMinSize = !isDefaultImage && currentProperties.Image.sizeMode == "minSize"; + if (isMinSize) Renderer::pushClipRect(pos, size); From a5d418f074a3692223f8a0b8b4422912d815dd1f Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 12 Jan 2020 17:46:20 +0100 Subject: [PATCH 299/336] ImageCache : better management --- es-core/src/ImageIO.cpp | 67 +++++++++++--------- es-core/src/ImageIO.h | 2 + es-core/src/resources/TextureData.cpp | 7 +- es-core/src/resources/TextureData.h | 2 +- es-core/src/resources/TextureDataManager.cpp | 2 +- 5 files changed, 46 insertions(+), 34 deletions(-) diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index 7ec5cf4aca..8f626ae12d 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -21,7 +21,6 @@ struct CachedFileInfo size = sz; x = sx; y = sy; - sizeResolved = true; }; CachedFileInfo() @@ -29,25 +28,16 @@ struct CachedFileInfo size = 0; x = 0; y = 0; - sizeResolved = false; }; int size; int x; int y; - bool sizeResolved; }; static std::map sizeCache; static bool sizeCacheDirty = false; -static void addtoCache(const std::string fn, int sz, int x, int y) -{ - sizeCache[fn] = CachedFileInfo(sz, x, y); - - if (sz > 0 && x > 0 && fn.find("/themes/") == std::string::npos) - sizeCacheDirty = true; -} #include #include @@ -124,30 +114,49 @@ void ImageIO::saveImageCache() f.close(); } +static std::mutex sizeCacheLock; -bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) +void ImageIO::updateImageCache(const std::string fn, int sz, int x, int y) { + std::unique_lock lock(sizeCacheLock); auto it = sizeCache.find(fn); if (it != sizeCache.cend()) { - if (*x < 0) - return false; - - bool resolved = true; - - if (!it->second.sizeResolved) + if (x != it->second.x || y != it->second.y || sz != it->second.size) { - auto size = Utils::FileSystem::getFileSize(fn); - if (it->second.size != size) - resolved = false; + auto& item = it->second; + + item.x = x; + item.y = y; + item.size = sz; + sizeCacheDirty = true; } + } + else + { + sizeCache[fn] = CachedFileInfo(sz, x, y); + + if (sz > 0 && x > 0 && fn.find("/themes/") == std::string::npos) + sizeCacheDirty = true; + } +} + - if (resolved) +bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) +{ + { + std::unique_lock lock(sizeCacheLock); + + auto it = sizeCache.find(fn); + if (it != sizeCache.cend()) { + if (it->second.size == -1) + return false; + *x = it->second.x; *y = it->second.y; - return true; + return true; } } @@ -167,7 +176,7 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) if (f == 0) { LOG(LogWarning) << "ImageIO::loadImageSize\tUnable to open file"; - addtoCache(fn, -1, -1, -1); + updateImageCache(fn, -1, -1, -1); return false; } @@ -179,7 +188,7 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) unsigned char buf[24]; if (fread(buf, 1, 24, f) != 24) { - addtoCache(fn, -1, -1, -1); + updateImageCache(fn, -1, -1, -1); return false; } @@ -220,11 +229,11 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) if (*x > 5000) // security ? { - addtoCache(fn, -1, -1, -1); + updateImageCache(fn, -1, -1, -1); return false; } - addtoCache(fn, size, *x, *y); + updateImageCache(fn, size, *x, *y); return true; } @@ -236,7 +245,7 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) LOG(LogDebug) << "ImageIO::loadImageSize\tGIF size " << std::string(std::to_string(*x) + "x" + std::to_string(*y)).c_str(); - addtoCache(fn, size, *x, *y); + updateImageCache(fn, size, *x, *y); return true; } @@ -248,11 +257,11 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) LOG(LogDebug) << "ImageIO::loadImageSize\tPNG size " << std::string(std::to_string(*x) + "x" + std::to_string(*y)).c_str(); - addtoCache(fn, size, *x, *y); + updateImageCache(fn, size, *x, *y); return true; } - addtoCache(fn, -1, -1, -1); + updateImageCache(fn, -1, -1, -1); LOG(LogWarning) << "ImageIO::loadImageSize\tUnable to extract size"; return false; } diff --git a/es-core/src/ImageIO.h b/es-core/src/ImageIO.h index 1048d64be8..35656a7a03 100644 --- a/es-core/src/ImageIO.h +++ b/es-core/src/ImageIO.h @@ -23,6 +23,8 @@ class ImageIO static void loadImageCache(); static void saveImageCache(); + + static void updateImageCache(const std::string fn, int sz, int x, int y); }; #endif // ES_CORE_IMAGE_IO diff --git a/es-core/src/resources/TextureData.cpp b/es-core/src/resources/TextureData.cpp index c39dfbcd2e..00a8f5d8fb 100644 --- a/es-core/src/resources/TextureData.cpp +++ b/es-core/src/resources/TextureData.cpp @@ -258,7 +258,7 @@ bool TextureData::initFromExternalRGBA(unsigned char* dataRGBA, size_t width, si return true; } -bool TextureData::load() +bool TextureData::load(bool updateCache) { bool retval = false; @@ -276,9 +276,10 @@ bool TextureData::load() } else retval = initImageFromMemory((const unsigned char*)data.ptr.get(), data.length); - } - + if (updateCache && retval) + ImageIO::updateImageCache(mPath, data.length, mBaseSize.x(), mBaseSize.y()); + } return retval; } diff --git a/es-core/src/resources/TextureData.h b/es-core/src/resources/TextureData.h index 501f971849..ef025ad5df 100644 --- a/es-core/src/resources/TextureData.h +++ b/es-core/src/resources/TextureData.h @@ -30,7 +30,7 @@ class TextureData bool initFromExternalRGBA(unsigned char* dataRGBA, size_t width, size_t height); // Read the data into memory if necessary - bool load(); + bool load(bool updateCache = false); bool isLoaded(); diff --git a/es-core/src/resources/TextureDataManager.cpp b/es-core/src/resources/TextureDataManager.cpp index a28a0c6f35..e0b94dce8c 100644 --- a/es-core/src/resources/TextureDataManager.cpp +++ b/es-core/src/resources/TextureDataManager.cpp @@ -287,7 +287,7 @@ void TextureLoader::threadProc() { std::this_thread::yield(); - textureData->load(); + textureData->load(true); // mManager->onTextureLoaded(textureData); lock.lock(); From 5da12602b88d7f3e15580fe1031709f13d500f95 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 12 Jan 2020 18:15:35 +0100 Subject: [PATCH 300/336] Image cache : minor fix --- es-core/src/ImageIO.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/es-core/src/ImageIO.cpp b/es-core/src/ImageIO.cpp index 8f626ae12d..9d64dd9704 100644 --- a/es-core/src/ImageIO.cpp +++ b/es-core/src/ImageIO.cpp @@ -130,7 +130,9 @@ void ImageIO::updateImageCache(const std::string fn, int sz, int x, int y) item.x = x; item.y = y; item.size = sz; - sizeCacheDirty = true; + + if (sz > 0 && x > 0 && fn.find("/themes/") == std::string::npos) + sizeCacheDirty = true; } } else @@ -151,7 +153,7 @@ bool ImageIO::getImageSize(const char *fn, unsigned int *x, unsigned int *y) auto it = sizeCache.find(fn); if (it != sizeCache.cend()) { - if (it->second.size == -1) + if (it->second.size < 0) return false; *x = it->second.x; From 2b3bfa6be1efc13a06a99158bffc95cda45be66e Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Wed, 15 Jan 2020 10:27:20 +0100 Subject: [PATCH 301/336] Unicode Strings : rewritten toupper/tolower methods --- es-core/src/utils/StringUtil.cpp | 244 +++++++++++++++++++++++++++---- 1 file changed, 218 insertions(+), 26 deletions(-) diff --git a/es-core/src/utils/StringUtil.cpp b/es-core/src/utils/StringUtil.cpp index 3a693c8f1d..f89b65fdf4 100644 --- a/es-core/src/utils/StringUtil.cpp +++ b/es-core/src/utils/StringUtil.cpp @@ -12,6 +12,188 @@ namespace Utils { namespace String { + static wchar_t unicode_lowers[] = + { + (wchar_t)0x0061, (wchar_t)0x0062, (wchar_t)0x0063, (wchar_t)0x0064, (wchar_t)0x0065, (wchar_t)0x0066, (wchar_t)0x0067, (wchar_t)0x0068, (wchar_t)0x0069, + (wchar_t)0x006A, (wchar_t)0x006B, (wchar_t)0x006C, (wchar_t)0x006D, (wchar_t)0x006E, (wchar_t)0x006F, (wchar_t)0x0070, (wchar_t)0x0071, (wchar_t)0x0072, + (wchar_t)0x0073, (wchar_t)0x0074, (wchar_t)0x0075, (wchar_t)0x0076, (wchar_t)0x0077, (wchar_t)0x0078, (wchar_t)0x0079, (wchar_t)0x007A, (wchar_t)0x00E0, + (wchar_t)0x00E1, (wchar_t)0x00E2, (wchar_t)0x00E3, (wchar_t)0x00E4, (wchar_t)0x00E5, (wchar_t)0x00E6, (wchar_t)0x00E7, (wchar_t)0x00E8, (wchar_t)0x00E9, + (wchar_t)0x00EA, (wchar_t)0x00EB, (wchar_t)0x00EC, (wchar_t)0x00ED, (wchar_t)0x00EE, (wchar_t)0x00EF, (wchar_t)0x00F0, (wchar_t)0x00F1, (wchar_t)0x00F2, + (wchar_t)0x00F3, (wchar_t)0x00F4, (wchar_t)0x00F5, (wchar_t)0x00F6, (wchar_t)0x00F8, (wchar_t)0x00F9, (wchar_t)0x00FA, (wchar_t)0x00FB, (wchar_t)0x00FC, + (wchar_t)0x00FD, (wchar_t)0x00FE, (wchar_t)0x00FF, (wchar_t)0x0101, (wchar_t)0x0103, (wchar_t)0x0105, (wchar_t)0x0107, (wchar_t)0x0109, (wchar_t)0x010B, + (wchar_t)0x010D, (wchar_t)0x010F, (wchar_t)0x0111, (wchar_t)0x0113, (wchar_t)0x0115, (wchar_t)0x0117, (wchar_t)0x0119, (wchar_t)0x011B, (wchar_t)0x011D, + (wchar_t)0x011F, (wchar_t)0x0121, (wchar_t)0x0123, (wchar_t)0x0125, (wchar_t)0x0127, (wchar_t)0x0129, (wchar_t)0x012B, (wchar_t)0x012D, (wchar_t)0x012F, + (wchar_t)0x0131, (wchar_t)0x0133, (wchar_t)0x0135, (wchar_t)0x0137, (wchar_t)0x013A, (wchar_t)0x013C, (wchar_t)0x013E, (wchar_t)0x0140, (wchar_t)0x0142, + (wchar_t)0x0144, (wchar_t)0x0146, (wchar_t)0x0148, (wchar_t)0x014B, (wchar_t)0x014D, (wchar_t)0x014F, (wchar_t)0x0151, (wchar_t)0x0153, (wchar_t)0x0155, + (wchar_t)0x0157, (wchar_t)0x0159, (wchar_t)0x015B, (wchar_t)0x015D, (wchar_t)0x015F, (wchar_t)0x0161, (wchar_t)0x0163, (wchar_t)0x0165, (wchar_t)0x0167, + (wchar_t)0x0169, (wchar_t)0x016B, (wchar_t)0x016D, (wchar_t)0x016F, (wchar_t)0x0171, (wchar_t)0x0173, (wchar_t)0x0175, (wchar_t)0x0177, (wchar_t)0x017A, + (wchar_t)0x017C, (wchar_t)0x017E, (wchar_t)0x0183, (wchar_t)0x0185, (wchar_t)0x0188, (wchar_t)0x018C, (wchar_t)0x0192, (wchar_t)0x0199, (wchar_t)0x01A1, + (wchar_t)0x01A3, (wchar_t)0x01A5, (wchar_t)0x01A8, (wchar_t)0x01AD, (wchar_t)0x01B0, (wchar_t)0x01B4, (wchar_t)0x01B6, (wchar_t)0x01B9, (wchar_t)0x01BD, + (wchar_t)0x01C6, (wchar_t)0x01C9, (wchar_t)0x01CC, (wchar_t)0x01CE, (wchar_t)0x01D0, (wchar_t)0x01D2, (wchar_t)0x01D4, (wchar_t)0x01D6, (wchar_t)0x01D8, + (wchar_t)0x01DA, (wchar_t)0x01DC, (wchar_t)0x01DF, (wchar_t)0x01E1, (wchar_t)0x01E3, (wchar_t)0x01E5, (wchar_t)0x01E7, (wchar_t)0x01E9, (wchar_t)0x01EB, + (wchar_t)0x01ED, (wchar_t)0x01EF, (wchar_t)0x01F3, (wchar_t)0x01F5, (wchar_t)0x01FB, (wchar_t)0x01FD, (wchar_t)0x01FF, (wchar_t)0x0201, (wchar_t)0x0203, + (wchar_t)0x0205, (wchar_t)0x0207, (wchar_t)0x0209, (wchar_t)0x020B, (wchar_t)0x020D, (wchar_t)0x020F, (wchar_t)0x0211, (wchar_t)0x0213, (wchar_t)0x0215, + (wchar_t)0x0217, (wchar_t)0x0253, (wchar_t)0x0254, (wchar_t)0x0257, (wchar_t)0x0258, (wchar_t)0x0259, (wchar_t)0x025B, (wchar_t)0x0260, (wchar_t)0x0263, + (wchar_t)0x0268, (wchar_t)0x0269, (wchar_t)0x026F, (wchar_t)0x0272, (wchar_t)0x0275, (wchar_t)0x0283, (wchar_t)0x0288, (wchar_t)0x028A, (wchar_t)0x028B, + (wchar_t)0x0292, (wchar_t)0x03AC, (wchar_t)0x03AD, (wchar_t)0x03AE, (wchar_t)0x03AF, (wchar_t)0x03B1, (wchar_t)0x03B2, (wchar_t)0x03B3, (wchar_t)0x03B4, + (wchar_t)0x03B5, (wchar_t)0x03B6, (wchar_t)0x03B7, (wchar_t)0x03B8, (wchar_t)0x03B9, (wchar_t)0x03BA, (wchar_t)0x03BB, (wchar_t)0x03BC, (wchar_t)0x03BD, + (wchar_t)0x03BE, (wchar_t)0x03BF, (wchar_t)0x03C0, (wchar_t)0x03C1, (wchar_t)0x03C3, (wchar_t)0x03C4, (wchar_t)0x03C5, (wchar_t)0x03C6, (wchar_t)0x03C7, + (wchar_t)0x03C8, (wchar_t)0x03C9, (wchar_t)0x03CA, (wchar_t)0x03CB, (wchar_t)0x03CC, (wchar_t)0x03CD, (wchar_t)0x03CE, (wchar_t)0x03E3, (wchar_t)0x03E5, + (wchar_t)0x03E7, (wchar_t)0x03E9, (wchar_t)0x03EB, (wchar_t)0x03ED, (wchar_t)0x03EF, (wchar_t)0x0430, (wchar_t)0x0431, (wchar_t)0x0432, (wchar_t)0x0433, + (wchar_t)0x0434, (wchar_t)0x0435, (wchar_t)0x0436, (wchar_t)0x0437, (wchar_t)0x0438, (wchar_t)0x0439, (wchar_t)0x043A, (wchar_t)0x043B, (wchar_t)0x043C, + (wchar_t)0x043D, (wchar_t)0x043E, (wchar_t)0x043F, (wchar_t)0x0440, (wchar_t)0x0441, (wchar_t)0x0442, (wchar_t)0x0443, (wchar_t)0x0444, (wchar_t)0x0445, + (wchar_t)0x0446, (wchar_t)0x0447, (wchar_t)0x0448, (wchar_t)0x0449, (wchar_t)0x044A, (wchar_t)0x044B, (wchar_t)0x044C, (wchar_t)0x044D, (wchar_t)0x044E, + (wchar_t)0x044F, (wchar_t)0x0451, (wchar_t)0x0452, (wchar_t)0x0453, (wchar_t)0x0454, (wchar_t)0x0455, (wchar_t)0x0456, (wchar_t)0x0457, (wchar_t)0x0458, + (wchar_t)0x0459, (wchar_t)0x045A, (wchar_t)0x045B, (wchar_t)0x045C, (wchar_t)0x045E, (wchar_t)0x045F, (wchar_t)0x0461, (wchar_t)0x0463, (wchar_t)0x0465, + (wchar_t)0x0467, (wchar_t)0x0469, (wchar_t)0x046B, (wchar_t)0x046D, (wchar_t)0x046F, (wchar_t)0x0471, (wchar_t)0x0473, (wchar_t)0x0475, (wchar_t)0x0477, + (wchar_t)0x0479, (wchar_t)0x047B, (wchar_t)0x047D, (wchar_t)0x047F, (wchar_t)0x0481, (wchar_t)0x0491, (wchar_t)0x0493, (wchar_t)0x0495, (wchar_t)0x0497, + (wchar_t)0x0499, (wchar_t)0x049B, (wchar_t)0x049D, (wchar_t)0x049F, (wchar_t)0x04A1, (wchar_t)0x04A3, (wchar_t)0x04A5, (wchar_t)0x04A7, (wchar_t)0x04A9, + (wchar_t)0x04AB, (wchar_t)0x04AD, (wchar_t)0x04AF, (wchar_t)0x04B1, (wchar_t)0x04B3, (wchar_t)0x04B5, (wchar_t)0x04B7, (wchar_t)0x04B9, (wchar_t)0x04BB, + (wchar_t)0x04BD, (wchar_t)0x04BF, (wchar_t)0x04C2, (wchar_t)0x04C4, (wchar_t)0x04C8, (wchar_t)0x04CC, (wchar_t)0x04D1, (wchar_t)0x04D3, (wchar_t)0x04D5, + (wchar_t)0x04D7, (wchar_t)0x04D9, (wchar_t)0x04DB, (wchar_t)0x04DD, (wchar_t)0x04DF, (wchar_t)0x04E1, (wchar_t)0x04E3, (wchar_t)0x04E5, (wchar_t)0x04E7, + (wchar_t)0x04E9, (wchar_t)0x04EB, (wchar_t)0x04EF, (wchar_t)0x04F1, (wchar_t)0x04F3, (wchar_t)0x04F5, (wchar_t)0x04F9, (wchar_t)0x0561, (wchar_t)0x0562, + (wchar_t)0x0563, (wchar_t)0x0564, (wchar_t)0x0565, (wchar_t)0x0566, (wchar_t)0x0567, (wchar_t)0x0568, (wchar_t)0x0569, (wchar_t)0x056A, (wchar_t)0x056B, + (wchar_t)0x056C, (wchar_t)0x056D, (wchar_t)0x056E, (wchar_t)0x056F, (wchar_t)0x0570, (wchar_t)0x0571, (wchar_t)0x0572, (wchar_t)0x0573, (wchar_t)0x0574, + (wchar_t)0x0575, (wchar_t)0x0576, (wchar_t)0x0577, (wchar_t)0x0578, (wchar_t)0x0579, (wchar_t)0x057A, (wchar_t)0x057B, (wchar_t)0x057C, (wchar_t)0x057D, + (wchar_t)0x057E, (wchar_t)0x057F, (wchar_t)0x0580, (wchar_t)0x0581, (wchar_t)0x0582, (wchar_t)0x0583, (wchar_t)0x0584, (wchar_t)0x0585, (wchar_t)0x0586, + (wchar_t)0x10D0, (wchar_t)0x10D1, (wchar_t)0x10D2, (wchar_t)0x10D3, (wchar_t)0x10D4, (wchar_t)0x10D5, (wchar_t)0x10D6, (wchar_t)0x10D7, (wchar_t)0x10D8, + (wchar_t)0x10D9, (wchar_t)0x10DA, (wchar_t)0x10DB, (wchar_t)0x10DC, (wchar_t)0x10DD, (wchar_t)0x10DE, (wchar_t)0x10DF, (wchar_t)0x10E0, (wchar_t)0x10E1, + (wchar_t)0x10E2, (wchar_t)0x10E3, (wchar_t)0x10E4, (wchar_t)0x10E5, (wchar_t)0x10E6, (wchar_t)0x10E7, (wchar_t)0x10E8, (wchar_t)0x10E9, (wchar_t)0x10EA, + (wchar_t)0x10EB, (wchar_t)0x10EC, (wchar_t)0x10ED, (wchar_t)0x10EE, (wchar_t)0x10EF, (wchar_t)0x10F0, (wchar_t)0x10F1, (wchar_t)0x10F2, (wchar_t)0x10F3, + (wchar_t)0x10F4, (wchar_t)0x10F5, (wchar_t)0x1E01, (wchar_t)0x1E03, (wchar_t)0x1E05, (wchar_t)0x1E07, (wchar_t)0x1E09, (wchar_t)0x1E0B, (wchar_t)0x1E0D, + (wchar_t)0x1E0F, (wchar_t)0x1E11, (wchar_t)0x1E13, (wchar_t)0x1E15, (wchar_t)0x1E17, (wchar_t)0x1E19, (wchar_t)0x1E1B, (wchar_t)0x1E1D, (wchar_t)0x1E1F, + (wchar_t)0x1E21, (wchar_t)0x1E23, (wchar_t)0x1E25, (wchar_t)0x1E27, (wchar_t)0x1E29, (wchar_t)0x1E2B, (wchar_t)0x1E2D, (wchar_t)0x1E2F, (wchar_t)0x1E31, + (wchar_t)0x1E33, (wchar_t)0x1E35, (wchar_t)0x1E37, (wchar_t)0x1E39, (wchar_t)0x1E3B, (wchar_t)0x1E3D, (wchar_t)0x1E3F, (wchar_t)0x1E41, (wchar_t)0x1E43, + (wchar_t)0x1E45, (wchar_t)0x1E47, (wchar_t)0x1E49, (wchar_t)0x1E4B, (wchar_t)0x1E4D, (wchar_t)0x1E4F, (wchar_t)0x1E51, (wchar_t)0x1E53, (wchar_t)0x1E55, + (wchar_t)0x1E57, (wchar_t)0x1E59, (wchar_t)0x1E5B, (wchar_t)0x1E5D, (wchar_t)0x1E5F, (wchar_t)0x1E61, (wchar_t)0x1E63, (wchar_t)0x1E65, (wchar_t)0x1E67, + (wchar_t)0x1E69, (wchar_t)0x1E6B, (wchar_t)0x1E6D, (wchar_t)0x1E6F, (wchar_t)0x1E71, (wchar_t)0x1E73, (wchar_t)0x1E75, (wchar_t)0x1E77, (wchar_t)0x1E79, + (wchar_t)0x1E7B, (wchar_t)0x1E7D, (wchar_t)0x1E7F, (wchar_t)0x1E81, (wchar_t)0x1E83, (wchar_t)0x1E85, (wchar_t)0x1E87, (wchar_t)0x1E89, (wchar_t)0x1E8B, + (wchar_t)0x1E8D, (wchar_t)0x1E8F, (wchar_t)0x1E91, (wchar_t)0x1E93, (wchar_t)0x1E95, (wchar_t)0x1EA1, (wchar_t)0x1EA3, (wchar_t)0x1EA5, (wchar_t)0x1EA7, + (wchar_t)0x1EA9, (wchar_t)0x1EAB, (wchar_t)0x1EAD, (wchar_t)0x1EAF, (wchar_t)0x1EB1, (wchar_t)0x1EB3, (wchar_t)0x1EB5, (wchar_t)0x1EB7, (wchar_t)0x1EB9, + (wchar_t)0x1EBB, (wchar_t)0x1EBD, (wchar_t)0x1EBF, (wchar_t)0x1EC1, (wchar_t)0x1EC3, (wchar_t)0x1EC5, (wchar_t)0x1EC7, (wchar_t)0x1EC9, (wchar_t)0x1ECB, + (wchar_t)0x1ECD, (wchar_t)0x1ECF, (wchar_t)0x1ED1, (wchar_t)0x1ED3, (wchar_t)0x1ED5, (wchar_t)0x1ED7, (wchar_t)0x1ED9, (wchar_t)0x1EDB, (wchar_t)0x1EDD, + (wchar_t)0x1EDF, (wchar_t)0x1EE1, (wchar_t)0x1EE3, (wchar_t)0x1EE5, (wchar_t)0x1EE7, (wchar_t)0x1EE9, (wchar_t)0x1EEB, (wchar_t)0x1EED, (wchar_t)0x1EEF, + (wchar_t)0x1EF1, (wchar_t)0x1EF3, (wchar_t)0x1EF5, (wchar_t)0x1EF7, (wchar_t)0x1EF9, (wchar_t)0x1F00, (wchar_t)0x1F01, (wchar_t)0x1F02, (wchar_t)0x1F03, + (wchar_t)0x1F04, (wchar_t)0x1F05, (wchar_t)0x1F06, (wchar_t)0x1F07, (wchar_t)0x1F10, (wchar_t)0x1F11, (wchar_t)0x1F12, (wchar_t)0x1F13, (wchar_t)0x1F14, + (wchar_t)0x1F15, (wchar_t)0x1F20, (wchar_t)0x1F21, (wchar_t)0x1F22, (wchar_t)0x1F23, (wchar_t)0x1F24, (wchar_t)0x1F25, (wchar_t)0x1F26, (wchar_t)0x1F27, + (wchar_t)0x1F30, (wchar_t)0x1F31, (wchar_t)0x1F32, (wchar_t)0x1F33, (wchar_t)0x1F34, (wchar_t)0x1F35, (wchar_t)0x1F36, (wchar_t)0x1F37, (wchar_t)0x1F40, + (wchar_t)0x1F41, (wchar_t)0x1F42, (wchar_t)0x1F43, (wchar_t)0x1F44, (wchar_t)0x1F45, (wchar_t)0x1F51, (wchar_t)0x1F53, (wchar_t)0x1F55, (wchar_t)0x1F57, + (wchar_t)0x1F60, (wchar_t)0x1F61, (wchar_t)0x1F62, (wchar_t)0x1F63, (wchar_t)0x1F64, (wchar_t)0x1F65, (wchar_t)0x1F66, (wchar_t)0x1F67, (wchar_t)0x1F80, + (wchar_t)0x1F81, (wchar_t)0x1F82, (wchar_t)0x1F83, (wchar_t)0x1F84, (wchar_t)0x1F85, (wchar_t)0x1F86, (wchar_t)0x1F87, (wchar_t)0x1F90, (wchar_t)0x1F91, + (wchar_t)0x1F92, (wchar_t)0x1F93, (wchar_t)0x1F94, (wchar_t)0x1F95, (wchar_t)0x1F96, (wchar_t)0x1F97, (wchar_t)0x1FA0, (wchar_t)0x1FA1, (wchar_t)0x1FA2, + (wchar_t)0x1FA3, (wchar_t)0x1FA4, (wchar_t)0x1FA5, (wchar_t)0x1FA6, (wchar_t)0x1FA7, (wchar_t)0x1FB0, (wchar_t)0x1FB1, (wchar_t)0x1FD0, (wchar_t)0x1FD1, + (wchar_t)0x1FE0, (wchar_t)0x1FE1, (wchar_t)0x24D0, (wchar_t)0x24D1, (wchar_t)0x24D2, (wchar_t)0x24D3, (wchar_t)0x24D4, (wchar_t)0x24D5, (wchar_t)0x24D6, + (wchar_t)0x24D7, (wchar_t)0x24D8, (wchar_t)0x24D9, (wchar_t)0x24DA, (wchar_t)0x24DB, (wchar_t)0x24DC, (wchar_t)0x24DD, (wchar_t)0x24DE, (wchar_t)0x24DF, + (wchar_t)0x24E0, (wchar_t)0x24E1, (wchar_t)0x24E2, (wchar_t)0x24E3, (wchar_t)0x24E4, (wchar_t)0x24E5, (wchar_t)0x24E6, (wchar_t)0x24E7, (wchar_t)0x24E8, + (wchar_t)0x24E9, (wchar_t)0xFF41, (wchar_t)0xFF42, (wchar_t)0xFF43, (wchar_t)0xFF44, (wchar_t)0xFF45, (wchar_t)0xFF46, (wchar_t)0xFF47, (wchar_t)0xFF48, + (wchar_t)0xFF49, (wchar_t)0xFF4A, (wchar_t)0xFF4B, (wchar_t)0xFF4C, (wchar_t)0xFF4D, (wchar_t)0xFF4E, (wchar_t)0xFF4F, (wchar_t)0xFF50, (wchar_t)0xFF51, + (wchar_t)0xFF52, (wchar_t)0xFF53, (wchar_t)0xFF54, (wchar_t)0xFF55, (wchar_t)0xFF56, (wchar_t)0xFF57, (wchar_t)0xFF58, (wchar_t)0xFF59, (wchar_t)0xFF5A}; + + static const wchar_t unicode_uppers[] = + { + (wchar_t)0x0041, (wchar_t)0x0042, (wchar_t)0x0043, (wchar_t)0x0044, (wchar_t)0x0045, (wchar_t)0x0046, (wchar_t)0x0047, (wchar_t)0x0048, (wchar_t)0x0049, + (wchar_t)0x004A, (wchar_t)0x004B, (wchar_t)0x004C, (wchar_t)0x004D, (wchar_t)0x004E, (wchar_t)0x004F, (wchar_t)0x0050, (wchar_t)0x0051, (wchar_t)0x0052, + (wchar_t)0x0053, (wchar_t)0x0054, (wchar_t)0x0055, (wchar_t)0x0056, (wchar_t)0x0057, (wchar_t)0x0058, (wchar_t)0x0059, (wchar_t)0x005A, (wchar_t)0x00C0, + (wchar_t)0x00C1, (wchar_t)0x00C2, (wchar_t)0x00C3, (wchar_t)0x00C4, (wchar_t)0x00C5, (wchar_t)0x00C6, (wchar_t)0x00C7, (wchar_t)0x00C8, (wchar_t)0x00C9, + (wchar_t)0x00CA, (wchar_t)0x00CB, (wchar_t)0x00CC, (wchar_t)0x00CD, (wchar_t)0x00CE, (wchar_t)0x00CF, (wchar_t)0x00D0, (wchar_t)0x00D1, (wchar_t)0x00D2, + (wchar_t)0x00D3, (wchar_t)0x00D4, (wchar_t)0x00D5, (wchar_t)0x00D6, (wchar_t)0x00D8, (wchar_t)0x00D9, (wchar_t)0x00DA, (wchar_t)0x00DB, (wchar_t)0x00DC, + (wchar_t)0x00DD, (wchar_t)0x00DE, (wchar_t)0x0178, (wchar_t)0x0100, (wchar_t)0x0102, (wchar_t)0x0104, (wchar_t)0x0106, (wchar_t)0x0108, (wchar_t)0x010A, + (wchar_t)0x010C, (wchar_t)0x010E, (wchar_t)0x0110, (wchar_t)0x0112, (wchar_t)0x0114, (wchar_t)0x0116, (wchar_t)0x0118, (wchar_t)0x011A, (wchar_t)0x011C, + (wchar_t)0x011E, (wchar_t)0x0120, (wchar_t)0x0122, (wchar_t)0x0124, (wchar_t)0x0126, (wchar_t)0x0128, (wchar_t)0x012A, (wchar_t)0x012C, (wchar_t)0x012E, + (wchar_t)0x0049, (wchar_t)0x0132, (wchar_t)0x0134, (wchar_t)0x0136, (wchar_t)0x0139, (wchar_t)0x013B, (wchar_t)0x013D, (wchar_t)0x013F, (wchar_t)0x0141, + (wchar_t)0x0143, (wchar_t)0x0145, (wchar_t)0x0147, (wchar_t)0x014A, (wchar_t)0x014C, (wchar_t)0x014E, (wchar_t)0x0150, (wchar_t)0x0152, (wchar_t)0x0154, + (wchar_t)0x0156, (wchar_t)0x0158, (wchar_t)0x015A, (wchar_t)0x015C, (wchar_t)0x015E, (wchar_t)0x0160, (wchar_t)0x0162, (wchar_t)0x0164, (wchar_t)0x0166, + (wchar_t)0x0168, (wchar_t)0x016A, (wchar_t)0x016C, (wchar_t)0x016E, (wchar_t)0x0170, (wchar_t)0x0172, (wchar_t)0x0174, (wchar_t)0x0176, (wchar_t)0x0179, + (wchar_t)0x017B, (wchar_t)0x017D, (wchar_t)0x0182, (wchar_t)0x0184, (wchar_t)0x0187, (wchar_t)0x018B, (wchar_t)0x0191, (wchar_t)0x0198, (wchar_t)0x01A0, + (wchar_t)0x01A2, (wchar_t)0x01A4, (wchar_t)0x01A7, (wchar_t)0x01AC, (wchar_t)0x01AF, (wchar_t)0x01B3, (wchar_t)0x01B5, (wchar_t)0x01B8, (wchar_t)0x01BC, + (wchar_t)0x01C4, (wchar_t)0x01C7, (wchar_t)0x01CA, (wchar_t)0x01CD, (wchar_t)0x01CF, (wchar_t)0x01D1, (wchar_t)0x01D3, (wchar_t)0x01D5, (wchar_t)0x01D7, + (wchar_t)0x01D9, (wchar_t)0x01DB, (wchar_t)0x01DE, (wchar_t)0x01E0, (wchar_t)0x01E2, (wchar_t)0x01E4, (wchar_t)0x01E6, (wchar_t)0x01E8, (wchar_t)0x01EA, + (wchar_t)0x01EC, (wchar_t)0x01EE, (wchar_t)0x01F1, (wchar_t)0x01F4, (wchar_t)0x01FA, (wchar_t)0x01FC, (wchar_t)0x01FE, (wchar_t)0x0200, (wchar_t)0x0202, + (wchar_t)0x0204, (wchar_t)0x0206, (wchar_t)0x0208, (wchar_t)0x020A, (wchar_t)0x020C, (wchar_t)0x020E, (wchar_t)0x0210, (wchar_t)0x0212, (wchar_t)0x0214, + (wchar_t)0x0216, (wchar_t)0x0181, (wchar_t)0x0186, (wchar_t)0x018A, (wchar_t)0x018E, (wchar_t)0x018F, (wchar_t)0x0190, (wchar_t)0x0193, (wchar_t)0x0194, + (wchar_t)0x0197, (wchar_t)0x0196, (wchar_t)0x019C, (wchar_t)0x019D, (wchar_t)0x019F, (wchar_t)0x01A9, (wchar_t)0x01AE, (wchar_t)0x01B1, (wchar_t)0x01B2, + (wchar_t)0x01B7, (wchar_t)0x0386, (wchar_t)0x0388, (wchar_t)0x0389, (wchar_t)0x038A, (wchar_t)0x0391, (wchar_t)0x0392, (wchar_t)0x0393, (wchar_t)0x0394, + (wchar_t)0x0395, (wchar_t)0x0396, (wchar_t)0x0397, (wchar_t)0x0398, (wchar_t)0x0399, (wchar_t)0x039A, (wchar_t)0x039B, (wchar_t)0x039C, (wchar_t)0x039D, + (wchar_t)0x039E, (wchar_t)0x039F, (wchar_t)0x03A0, (wchar_t)0x03A1, (wchar_t)0x03A3, (wchar_t)0x03A4, (wchar_t)0x03A5, (wchar_t)0x03A6, (wchar_t)0x03A7, + (wchar_t)0x03A8, (wchar_t)0x03A9, (wchar_t)0x03AA, (wchar_t)0x03AB, (wchar_t)0x038C, (wchar_t)0x038E, (wchar_t)0x038F, (wchar_t)0x03E2, (wchar_t)0x03E4, + (wchar_t)0x03E6, (wchar_t)0x03E8, (wchar_t)0x03EA, (wchar_t)0x03EC, (wchar_t)0x03EE, (wchar_t)0x0410, (wchar_t)0x0411, (wchar_t)0x0412, (wchar_t)0x0413, + (wchar_t)0x0414, (wchar_t)0x0415, (wchar_t)0x0416, (wchar_t)0x0417, (wchar_t)0x0418, (wchar_t)0x0419, (wchar_t)0x041A, (wchar_t)0x041B, (wchar_t)0x041C, + (wchar_t)0x041D, (wchar_t)0x041E, (wchar_t)0x041F, (wchar_t)0x0420, (wchar_t)0x0421, (wchar_t)0x0422, (wchar_t)0x0423, (wchar_t)0x0424, (wchar_t)0x0425, + (wchar_t)0x0426, (wchar_t)0x0427, (wchar_t)0x0428, (wchar_t)0x0429, (wchar_t)0x042A, (wchar_t)0x042B, (wchar_t)0x042C, (wchar_t)0x042D, (wchar_t)0x042E, + (wchar_t)0x042F, (wchar_t)0x0401, (wchar_t)0x0402, (wchar_t)0x0403, (wchar_t)0x0404, (wchar_t)0x0405, (wchar_t)0x0406, (wchar_t)0x0407, (wchar_t)0x0408, + (wchar_t)0x0409, (wchar_t)0x040A, (wchar_t)0x040B, (wchar_t)0x040C, (wchar_t)0x040E, (wchar_t)0x040F, (wchar_t)0x0460, (wchar_t)0x0462, (wchar_t)0x0464, + (wchar_t)0x0466, (wchar_t)0x0468, (wchar_t)0x046A, (wchar_t)0x046C, (wchar_t)0x046E, (wchar_t)0x0470, (wchar_t)0x0472, (wchar_t)0x0474, (wchar_t)0x0476, + (wchar_t)0x0478, (wchar_t)0x047A, (wchar_t)0x047C, (wchar_t)0x047E, (wchar_t)0x0480, (wchar_t)0x0490, (wchar_t)0x0492, (wchar_t)0x0494, (wchar_t)0x0496, + (wchar_t)0x0498, (wchar_t)0x049A, (wchar_t)0x049C, (wchar_t)0x049E, (wchar_t)0x04A0, (wchar_t)0x04A2, (wchar_t)0x04A4, (wchar_t)0x04A6, (wchar_t)0x04A8, + (wchar_t)0x04AA, (wchar_t)0x04AC, (wchar_t)0x04AE, (wchar_t)0x04B0, (wchar_t)0x04B2, (wchar_t)0x04B4, (wchar_t)0x04B6, (wchar_t)0x04B8, (wchar_t)0x04BA, + (wchar_t)0x04BC, (wchar_t)0x04BE, (wchar_t)0x04C1, (wchar_t)0x04C3, (wchar_t)0x04C7, (wchar_t)0x04CB, (wchar_t)0x04D0, (wchar_t)0x04D2, (wchar_t)0x04D4, + (wchar_t)0x04D6, (wchar_t)0x04D8, (wchar_t)0x04DA, (wchar_t)0x04DC, (wchar_t)0x04DE, (wchar_t)0x04E0, (wchar_t)0x04E2, (wchar_t)0x04E4, (wchar_t)0x04E6, + (wchar_t)0x04E8, (wchar_t)0x04EA, (wchar_t)0x04EE, (wchar_t)0x04F0, (wchar_t)0x04F2, (wchar_t)0x04F4, (wchar_t)0x04F8, (wchar_t)0x0531, (wchar_t)0x0532, + (wchar_t)0x0533, (wchar_t)0x0534, (wchar_t)0x0535, (wchar_t)0x0536, (wchar_t)0x0537, (wchar_t)0x0538, (wchar_t)0x0539, (wchar_t)0x053A, (wchar_t)0x053B, + (wchar_t)0x053C, (wchar_t)0x053D, (wchar_t)0x053E, (wchar_t)0x053F, (wchar_t)0x0540, (wchar_t)0x0541, (wchar_t)0x0542, (wchar_t)0x0543, (wchar_t)0x0544, + (wchar_t)0x0545, (wchar_t)0x0546, (wchar_t)0x0547, (wchar_t)0x0548, (wchar_t)0x0549, (wchar_t)0x054A, (wchar_t)0x054B, (wchar_t)0x054C, (wchar_t)0x054D, + (wchar_t)0x054E, (wchar_t)0x054F, (wchar_t)0x0550, (wchar_t)0x0551, (wchar_t)0x0552, (wchar_t)0x0553, (wchar_t)0x0554, (wchar_t)0x0555, (wchar_t)0x0556, + (wchar_t)0x10A0, (wchar_t)0x10A1, (wchar_t)0x10A2, (wchar_t)0x10A3, (wchar_t)0x10A4, (wchar_t)0x10A5, (wchar_t)0x10A6, (wchar_t)0x10A7, (wchar_t)0x10A8, + (wchar_t)0x10A9, (wchar_t)0x10AA, (wchar_t)0x10AB, (wchar_t)0x10AC, (wchar_t)0x10AD, (wchar_t)0x10AE, (wchar_t)0x10AF, (wchar_t)0x10B0, (wchar_t)0x10B1, + (wchar_t)0x10B2, (wchar_t)0x10B3, (wchar_t)0x10B4, (wchar_t)0x10B5, (wchar_t)0x10B6, (wchar_t)0x10B7, (wchar_t)0x10B8, (wchar_t)0x10B9, (wchar_t)0x10BA, + (wchar_t)0x10BB, (wchar_t)0x10BC, (wchar_t)0x10BD, (wchar_t)0x10BE, (wchar_t)0x10BF, (wchar_t)0x10C0, (wchar_t)0x10C1, (wchar_t)0x10C2, (wchar_t)0x10C3, + (wchar_t)0x10C4, (wchar_t)0x10C5, (wchar_t)0x1E00, (wchar_t)0x1E02, (wchar_t)0x1E04, (wchar_t)0x1E06, (wchar_t)0x1E08, (wchar_t)0x1E0A, (wchar_t)0x1E0C, + (wchar_t)0x1E0E, (wchar_t)0x1E10, (wchar_t)0x1E12, (wchar_t)0x1E14, (wchar_t)0x1E16, (wchar_t)0x1E18, (wchar_t)0x1E1A, (wchar_t)0x1E1C, (wchar_t)0x1E1E, + (wchar_t)0x1E20, (wchar_t)0x1E22, (wchar_t)0x1E24, (wchar_t)0x1E26, (wchar_t)0x1E28, (wchar_t)0x1E2A, (wchar_t)0x1E2C, (wchar_t)0x1E2E, (wchar_t)0x1E30, + (wchar_t)0x1E32, (wchar_t)0x1E34, (wchar_t)0x1E36, (wchar_t)0x1E38, (wchar_t)0x1E3A, (wchar_t)0x1E3C, (wchar_t)0x1E3E, (wchar_t)0x1E40, (wchar_t)0x1E42, + (wchar_t)0x1E44, (wchar_t)0x1E46, (wchar_t)0x1E48, (wchar_t)0x1E4A, (wchar_t)0x1E4C, (wchar_t)0x1E4E, (wchar_t)0x1E50, (wchar_t)0x1E52, (wchar_t)0x1E54, + (wchar_t)0x1E56, (wchar_t)0x1E58, (wchar_t)0x1E5A, (wchar_t)0x1E5C, (wchar_t)0x1E5E, (wchar_t)0x1E60, (wchar_t)0x1E62, (wchar_t)0x1E64, (wchar_t)0x1E66, + (wchar_t)0x1E68, (wchar_t)0x1E6A, (wchar_t)0x1E6C, (wchar_t)0x1E6E, (wchar_t)0x1E70, (wchar_t)0x1E72, (wchar_t)0x1E74, (wchar_t)0x1E76, (wchar_t)0x1E78, + (wchar_t)0x1E7A, (wchar_t)0x1E7C, (wchar_t)0x1E7E, (wchar_t)0x1E80, (wchar_t)0x1E82, (wchar_t)0x1E84, (wchar_t)0x1E86, (wchar_t)0x1E88, (wchar_t)0x1E8A, + (wchar_t)0x1E8C, (wchar_t)0x1E8E, (wchar_t)0x1E90, (wchar_t)0x1E92, (wchar_t)0x1E94, (wchar_t)0x1EA0, (wchar_t)0x1EA2, (wchar_t)0x1EA4, (wchar_t)0x1EA6, + (wchar_t)0x1EA8, (wchar_t)0x1EAA, (wchar_t)0x1EAC, (wchar_t)0x1EAE, (wchar_t)0x1EB0, (wchar_t)0x1EB2, (wchar_t)0x1EB4, (wchar_t)0x1EB6, (wchar_t)0x1EB8, + (wchar_t)0x1EBA, (wchar_t)0x1EBC, (wchar_t)0x1EBE, (wchar_t)0x1EC0, (wchar_t)0x1EC2, (wchar_t)0x1EC4, (wchar_t)0x1EC6, (wchar_t)0x1EC8, (wchar_t)0x1ECA, + (wchar_t)0x1ECC, (wchar_t)0x1ECE, (wchar_t)0x1ED0, (wchar_t)0x1ED2, (wchar_t)0x1ED4, (wchar_t)0x1ED6, (wchar_t)0x1ED8, (wchar_t)0x1EDA, (wchar_t)0x1EDC, + (wchar_t)0x1EDE, (wchar_t)0x1EE0, (wchar_t)0x1EE2, (wchar_t)0x1EE4, (wchar_t)0x1EE6, (wchar_t)0x1EE8, (wchar_t)0x1EEA, (wchar_t)0x1EEC, (wchar_t)0x1EEE, + (wchar_t)0x1EF0, (wchar_t)0x1EF2, (wchar_t)0x1EF4, (wchar_t)0x1EF6, (wchar_t)0x1EF8, (wchar_t)0x1F08, (wchar_t)0x1F09, (wchar_t)0x1F0A, (wchar_t)0x1F0B, + (wchar_t)0x1F0C, (wchar_t)0x1F0D, (wchar_t)0x1F0E, (wchar_t)0x1F0F, (wchar_t)0x1F18, (wchar_t)0x1F19, (wchar_t)0x1F1A, (wchar_t)0x1F1B, (wchar_t)0x1F1C, + (wchar_t)0x1F1D, (wchar_t)0x1F28, (wchar_t)0x1F29, (wchar_t)0x1F2A, (wchar_t)0x1F2B, (wchar_t)0x1F2C, (wchar_t)0x1F2D, (wchar_t)0x1F2E, (wchar_t)0x1F2F, + (wchar_t)0x1F38, (wchar_t)0x1F39, (wchar_t)0x1F3A, (wchar_t)0x1F3B, (wchar_t)0x1F3C, (wchar_t)0x1F3D, (wchar_t)0x1F3E, (wchar_t)0x1F3F, (wchar_t)0x1F48, + (wchar_t)0x1F49, (wchar_t)0x1F4A, (wchar_t)0x1F4B, (wchar_t)0x1F4C, (wchar_t)0x1F4D, (wchar_t)0x1F59, (wchar_t)0x1F5B, (wchar_t)0x1F5D, (wchar_t)0x1F5F, + (wchar_t)0x1F68, (wchar_t)0x1F69, (wchar_t)0x1F6A, (wchar_t)0x1F6B, (wchar_t)0x1F6C, (wchar_t)0x1F6D, (wchar_t)0x1F6E, (wchar_t)0x1F6F, (wchar_t)0x1F88, + (wchar_t)0x1F89, (wchar_t)0x1F8A, (wchar_t)0x1F8B, (wchar_t)0x1F8C, (wchar_t)0x1F8D, (wchar_t)0x1F8E, (wchar_t)0x1F8F, (wchar_t)0x1F98, (wchar_t)0x1F99, + (wchar_t)0x1F9A, (wchar_t)0x1F9B, (wchar_t)0x1F9C, (wchar_t)0x1F9D, (wchar_t)0x1F9E, (wchar_t)0x1F9F, (wchar_t)0x1FA8, (wchar_t)0x1FA9, (wchar_t)0x1FAA, + (wchar_t)0x1FAB, (wchar_t)0x1FAC, (wchar_t)0x1FAD, (wchar_t)0x1FAE, (wchar_t)0x1FAF, (wchar_t)0x1FB8, (wchar_t)0x1FB9, (wchar_t)0x1FD8, (wchar_t)0x1FD9, + (wchar_t)0x1FE8, (wchar_t)0x1FE9, (wchar_t)0x24B6, (wchar_t)0x24B7, (wchar_t)0x24B8, (wchar_t)0x24B9, (wchar_t)0x24BA, (wchar_t)0x24BB, (wchar_t)0x24BC, + (wchar_t)0x24BD, (wchar_t)0x24BE, (wchar_t)0x24BF, (wchar_t)0x24C0, (wchar_t)0x24C1, (wchar_t)0x24C2, (wchar_t)0x24C3, (wchar_t)0x24C4, (wchar_t)0x24C5, + (wchar_t)0x24C6, (wchar_t)0x24C7, (wchar_t)0x24C8, (wchar_t)0x24C9, (wchar_t)0x24CA, (wchar_t)0x24CB, (wchar_t)0x24CC, (wchar_t)0x24CD, (wchar_t)0x24CE, + (wchar_t)0x24CF, (wchar_t)0xFF21, (wchar_t)0xFF22, (wchar_t)0xFF23, (wchar_t)0xFF24, (wchar_t)0xFF25, (wchar_t)0xFF26, (wchar_t)0xFF27, (wchar_t)0xFF28, + (wchar_t)0xFF29, (wchar_t)0xFF2A, (wchar_t)0xFF2B, (wchar_t)0xFF2C, (wchar_t)0xFF2D, (wchar_t)0xFF2E, (wchar_t)0xFF2F, (wchar_t)0xFF30, (wchar_t)0xFF31, + (wchar_t)0xFF32, (wchar_t)0xFF33, (wchar_t)0xFF34, (wchar_t)0xFF35, (wchar_t)0xFF36, (wchar_t)0xFF37, (wchar_t)0xFF38, (wchar_t)0xFF39, (wchar_t)0xFF3A}; + + static int compareWchar(const void* a, const void* b) + { + if (*(wchar_t*)a < *(wchar_t*)b) + return -1; + else if (*(wchar_t*)a > *(wchar_t*)b) + return 1; + + return 0; + } + + static wchar_t tolowerUnicode(const wchar_t& c) + { + wchar_t* p = (wchar_t*)bsearch(&c, unicode_uppers, sizeof(unicode_uppers) / sizeof(wchar_t), sizeof(wchar_t), compareWchar); + if (p) + return *(unicode_lowers + (p - unicode_uppers)); + + return c; + } + + static wchar_t toupperUnicode(const wchar_t& c) + { + wchar_t* p = (wchar_t*)bsearch(&c, unicode_lowers, sizeof(unicode_lowers) / sizeof(wchar_t), sizeof(wchar_t), compareWchar); + if (p) + return *(unicode_uppers + (p - unicode_lowers)); + + return c; + } + unsigned int chars2Unicode(const std::string& _string, size_t& _cursor) { const char& c = _string[_cursor]; @@ -140,36 +322,46 @@ namespace Utils } // moveCursor - std::string toLower(const std::string& _string) + static std::string changeUnicodeCasing(const std::string& _string, bool toUpper) { - std::string string; + std::string text = _string; - for(size_t i = 0; i < _string.length(); ++i) - string += (char)tolower(_string[i]); - - return string; - - } // toLower - - std::string toUpper(const std::string& _string) - { - -#if defined(_WIN32) - std::wstring stringW = convertToWideString(_string); - - auto& f = std::use_facet>(std::locale()); - f.toupper(&stringW[0], &stringW[0] + stringW.size()); + size_t i = 0; + while (i < text.length()) + { + int pos = i; - return convertFromWideString(stringW); -#else - std::string string; + wchar_t character = (wchar_t)chars2Unicode(text, i); + wchar_t unicode = toUpper ? toupperUnicode(character) : tolowerUnicode(character); + if (unicode != character) + { + int charSize = i - pos; + if (charSize == 1) + { + text[pos] = (char)(unicode & 0xFF); + } + else if (charSize == 2) + { + text[pos] = (char)(((unicode >> 6) & 0xFF) | 0xC0); + text[pos + 1] = (char)((unicode & 0x3F) | 0x80); + } + else if (charSize == 3) + { + text[pos] += (char)(((unicode >> 12) & 0xFF) | 0xE0); + text[pos + 1] += (char)(((unicode >> 6) & 0x3F) | 0x80); + text[pos + 2] += (char)((unicode & 0x3F) | 0x80); + + } + } + else + text[i] = toUpper ? toupper(text[i]) : tolower(text[i]); + } - for(size_t i = 0; i < _string.length(); ++i) - string += (char)toupper(_string[i]); + return text; + } - return string; -#endif - } // toUpper + std::string toLower(const std::string& _string) { return changeUnicodeCasing(_string, false); } + std::string toUpper(const std::string& _string) { return changeUnicodeCasing(_string, true); } std::string trim(const std::string& _string) { @@ -182,7 +374,7 @@ namespace Utils return _string.substr(strBegin, strEnd - strBegin + 1); } // trim - + std::string replace(const std::string& _string, const std::string& _replace, const std::string& _with) { std::string string = _string; From 10d000b17d8ef2f376e4df480015b0407049e19a Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Fri, 17 Jan 2020 19:13:11 +0100 Subject: [PATCH 302/336] Scrapper : Fixed theGamesDB + ScreenScraper : ability to select boxes as main image + add mixv2 --- es-app/src/guis/GuiMenu.cpp | 18 ++++++++++++++- es-app/src/scrapers/GamesDBJSONScraper.cpp | 2 +- .../scrapers/GamesDBJSONScraperResources.cpp | 2 +- es-core/src/components/OptionListComponent.h | 22 +++++++++++++++++++ 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index d25e77950b..be7ab1c9b5 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -109,7 +109,10 @@ void GuiMenu::openScraperSettings() //imageSource->add(_("NONE"), "", imageSourceName.empty()); imageSource->add(_("SCREENSHOT"), "ss", imageSourceName == "ss"); imageSource->add(_("TITLE SCREENSHOT"), "sstitle", imageSourceName == "sstitle"); - imageSource->add(_("MIX"), "mixrbv1", imageSourceName == "mixrbv1"); + imageSource->add(_("MIX V1"), "mixrbv1", imageSourceName == "mixrbv1"); + imageSource->add(_("MIX V2"), "mixrbv2", imageSourceName == "mixrbv2"); + imageSource->add(_("BOX 2D"), "box-2D", imageSourceName == "box-2D"); + imageSource->add(_("BOX 3D"), "box-3D", imageSourceName == "box-3D"); if (!imageSource->hasSelection()) imageSource->selectFirstItem(); @@ -130,6 +133,19 @@ void GuiMenu::openScraperSettings() s->addWithLabel(_("BOX SOURCE"), thumbSource); s->addSaveFunc([thumbSource] { Settings::getInstance()->setString("ScrapperThumbSrc", thumbSource->getSelected()); }); + imageSource->setSelectedChangedCallback([this, thumbSource](std::string value) + { + if (value == "box-2D") + thumbSource->remove(_("BOX 2D")); + else + thumbSource->add(_("BOX 2D"), "box-2D", false); + + if (value == "box-3D") + thumbSource->remove(_("BOX 3D")); + else + thumbSource->add(_("BOX 3D"), "box-3D", false); + }); + // Logo source : tag std::string logoSourceName = Settings::getInstance()->getString("ScrapperLogoSrc"); auto logoSource = std::make_shared< OptionListComponent >(mWindow, _("LOGO SOURCE"), false); diff --git a/es-app/src/scrapers/GamesDBJSONScraper.cpp b/es-app/src/scrapers/GamesDBJSONScraper.cpp index 20701c385b..58bb8e104a 100644 --- a/es-app/src/scrapers/GamesDBJSONScraper.cpp +++ b/es-app/src/scrapers/GamesDBJSONScraper.cpp @@ -120,7 +120,7 @@ void thegamesdb_generate_json_scraper_requests(const ScraperSearchParams& params std::queue>& requests, std::vector& results) { resources.prepare(); - std::string path = "https://api.thegamesdb.net"; + std::string path = "https://api.thegamesdb.net/v1"; bool usingGameID = false; const std::string apiKey = std::string("apikey=") + resources.getApiKey(); std::string cleanName = params.nameOverride; diff --git a/es-app/src/scrapers/GamesDBJSONScraperResources.cpp b/es-app/src/scrapers/GamesDBJSONScraperResources.cpp index 809e8831f5..3246079603 100644 --- a/es-app/src/scrapers/GamesDBJSONScraperResources.cpp +++ b/es-app/src/scrapers/GamesDBJSONScraperResources.cpp @@ -153,7 +153,7 @@ bool TheGamesDBJSONRequestResources::saveResource(HttpReq* req, std::unordered_m std::unique_ptr TheGamesDBJSONRequestResources::fetchResource(const std::string& endpoint) { - std::string path = "https://api.thegamesdb.net"; + std::string path = "https://api.thegamesdb.net/v1"; path += endpoint; path += "?apikey=" + getApiKey(); diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index ab7b0ebd26..04150b1dd2 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -294,6 +294,10 @@ class OptionListComponent : public GuiComponent void add(const std::string& name, const T& obj, bool selected) { + for (auto sysIt = mEntries.cbegin(); sysIt != mEntries.cend(); sysIt++) + if (sysIt->name == name) + return; + OptionListData e; e.name = name; e.object = obj; @@ -303,6 +307,24 @@ class OptionListComponent : public GuiComponent onSelectedChanged(); } + void remove(const std::string& name) + { + for (auto sysIt = mEntries.cbegin(); sysIt != mEntries.cend(); sysIt++) + { + if (sysIt->name == name) + { + bool isSelect = sysIt->selected; + + mEntries.erase(sysIt); + + if (isSelect) + selectFirstItem(); + + break; + } + } + } + void selectAll() { for(unsigned int i = 0; i < mEntries.size(); i++) From 702701cb41b415b4595c639d011509b8e700fbb3 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 18 Jan 2020 00:23:58 +0100 Subject: [PATCH 303/336] Add Support for second.decl.isCustom || themeFolderExists(it->first) || !Settings::getInstance()->getBool("UseCustomCollectionsSystem")) { - // exists theme folder, or we chose not to bundle it under the custom-collections system - // so we need to create a view - SystemData::sSystemVector.push_back(it->second.system); + if (it->second.decl.displayIfEmpty || it->second.system->getRootFolder()->getChildren().size() > 0) + { + // exists theme folder, or we chose not to bundle it under the custom-collections system + // so we need to create a view + if (it->second.isEnabled) + SystemData::sSystemVector.push_back(it->second.system); + } } else { diff --git a/es-app/src/CollectionSystemManager.h b/es-app/src/CollectionSystemManager.h index 69c48fce62..1e41983ed4 100644 --- a/es-app/src/CollectionSystemManager.h +++ b/es-app/src/CollectionSystemManager.h @@ -17,8 +17,39 @@ enum CollectionSystemType { AUTO_ALL_GAMES, AUTO_LAST_PLAYED, + AUTO_AT2PLAYERS, + AUTO_AT4PLAYERS, + AUTO_NEVER_PLAYED, AUTO_FAVORITES, - CUSTOM_COLLECTION + + AUTO_ARCADE, + + CUSTOM_COLLECTION, + + CPS1_COLLECTION, + CPS2_COLLECTION, + CPS3_COLLECTION, + CAVE_COLLECTION, + NEOGEO_COLLECTION, + SEGA_COLLECTION, + IREM_COLLECTION, + MIDWAY_COLLECTION, + CAPCOM_COLLECTION, + TECMO_COLLECTION, + SNK_COLLECTION, + NAMCO_COLLECTION, + TAITO_COLLECTION, + KONAMI_COLLECTION, + JALECO_COLLECTION, + ATARI_COLLECTION, + NINTENDO_COLLECTION, + SAMMY_COLLECTION, + ACCLAIM_COLLECTION, + PSIKYO_COLLECTION, + KANEKO_COLLECTION, + COLECO_COLLECTION, + ATLUS_COLLECTION, + BANPRESTO_COLLECTION }; struct CollectionSystemDecl @@ -29,6 +60,7 @@ struct CollectionSystemDecl std::string defaultSort; std::string themeFolder; bool isCustom; + bool displayIfEmpty; }; struct CollectionSystemData @@ -43,6 +75,8 @@ struct CollectionSystemData class CollectionSystemManager { public: + static std::vector getSystemDecls(); + CollectionSystemManager(Window* window); ~CollectionSystemManager(); @@ -59,7 +93,7 @@ class CollectionSystemManager void updateCollectionSystem(FileData* file, CollectionSystemData sysData); void deleteCollectionFiles(FileData* file); - inline std::map getAutoCollectionSystems() { return mAutoCollectionSystemsData; }; + inline std::map& getAutoCollectionSystems() { return mAutoCollectionSystemsData; }; inline std::map getCustomCollectionSystems() { return mCustomCollectionSystemsData; }; inline SystemData* getCustomCollectionsBundle() { return mCustomCollectionsBundle; }; std::vector getUnusedSystemsFromTheme(); @@ -77,6 +111,7 @@ class CollectionSystemManager SystemData* getSystemToView(SystemData* sys); void updateCollectionFolderMetadata(SystemData* sys); + void populateAutoCollection(CollectionSystemData* sysData); private: static CollectionSystemManager* sInstance; @@ -93,7 +128,7 @@ class CollectionSystemManager void initCustomCollectionSystems(); SystemData* getAllGamesCollection(); SystemData* createNewCollectionEntry(std::string name, CollectionSystemDecl sysDecl, bool index = true); - void populateAutoCollection(CollectionSystemData* sysData); + void populateCustomCollection(CollectionSystemData* sysData, std::unordered_map* pMap = nullptr); void removeCollectionsFromDisplayedSystems(); diff --git a/es-app/src/MetaData.cpp b/es-app/src/MetaData.cpp index a4e01848be..54c87617a9 100644 --- a/es-app/src/MetaData.cpp +++ b/es-app/src/MetaData.cpp @@ -28,7 +28,8 @@ MetaDataDecl gameDecls[] = { { 16, "hidden", MD_BOOL, "false", false, "hidden", "enter hidden off/on" }, { 17, "kidgame", MD_BOOL, "false", false, "kidgame", "enter kidgame off/on" }, { 18, "playcount", MD_INT, "0", true, "play count", "enter number of times played"}, - { 19, "lastplayed", MD_TIME, "0", true, "last played", "enter last played date"} + { 19, "lastplayed", MD_TIME, "0", true, "last played", "enter last played date"}, + { 20, "arcadesystemname", MD_STRING, "", false, "arcade system", "enter arcade system name"} }; const std::vector gameMDD(gameDecls, gameDecls + sizeof(gameDecls) / sizeof(gameDecls[0])); diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index c1e17fdbe0..30fabc0dd1 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -8,6 +8,7 @@ #include "views/ViewController.h" #include "CollectionSystemManager.h" #include "Window.h" +#include "SystemData.h" GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) : GuiComponent(window), mMenu(window, _("GAME COLLECTION SETTINGS")) { @@ -145,14 +146,37 @@ GuiCollectionSystemsOptions::~GuiCollectionSystemsOptions() void GuiCollectionSystemsOptions::addSystemsToMenu() { - std::map autoSystems = CollectionSystemManager::get()->getAutoCollectionSystems(); + std::map &autoSystems = CollectionSystemManager::get()->getAutoCollectionSystems(); autoOptionList = std::make_shared< OptionListComponent >(mWindow, _("SELECT COLLECTIONS"), true); - // add Auto Systems - for(std::map::const_iterator it = autoSystems.cbegin() ; it != autoSystems.cend() ; it++ ) + bool hasGroup = false; + + // add Auto Systems && preserve order + for (auto systemDecl : CollectionSystemManager::getSystemDecls()) { - autoOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); + auto it = autoSystems.find(systemDecl.name); + if (it == autoSystems.cend()) + continue; + + if (it->second.decl.displayIfEmpty) + autoOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); + else + { + if (!it->second.isPopulated) + CollectionSystemManager::get()->populateAutoCollection(&(it->second)); + + if (it->second.system->getRootFolder()->getChildren().size() == 0) + continue; + + if (!hasGroup) + { + autoOptionList->addGroup(_("ARCADE SYSTEMS")); + hasGroup = true; + } + + autoOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); + } } mMenu.addWithLabel(_("AUTOMATIC GAME COLLECTIONS"), autoOptionList); diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index 9f4530cdf5..4d77fcda6a 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -21,17 +21,15 @@ const std::map scraper_request_func std::unique_ptr startScraperSearch(const ScraperSearchParams& params) { const std::string& name = Settings::getInstance()->getString("Scraper"); + std::unique_ptr handle(new ScraperSearchHandle()); // Check if the Scraper in the settings still exists as a registered scraping source. - if (scraper_request_funcs.find(name) == scraper_request_funcs.end()) - { - LOG(LogWarning) << "Configured scraper (" << name << ") unavailable, scraping aborted."; - } + auto it = scraper_request_funcs.find(name); + if (it != scraper_request_funcs.end()) + it->second(params, handle->mRequestQueue, handle->mResults); else - { - scraper_request_funcs.at(name)(params, handle->mRequestQueue, handle->mResults); - } + LOG(LogWarning) << "Configured scraper (" << name << ") unavailable, scraping aborted."; return handle; } diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index 9e27ccb96d..46ef333229 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -130,6 +130,35 @@ const std::map screenscraper_platformid_map{ }; +const std::map screenscraper_arcadesystemid_map{ + { 6, "cps1" }, + { 7, "cps2" }, + { 8, "cps3" }, + { 47, "cave" }, + { 68, "neogeo" }, + { 142, "neogeo" }, + { 147, "sega" }, + { 148, "irem" }, + { 150, "midway" }, + { 151, "capcom" }, + { 153, "tecmo" }, + { 154, "snk" }, + { 155, "namco" }, + { 156, "namco" }, + { 157, "taito" }, + { 158, "konami" }, + { 159, "jaleco" }, + { 160, "atari" }, + { 161, "nintendo" }, + { 162, "dataeast" }, + { 164, "sammy" }, + { 166, "acclaim" }, + { 167, "psikyo" }, + { 174, "kaneko" }, + { 183, "coleco" }, + { 185, "atlus" }, + { 186, "banpresto" } +}; // Helper XML parsing method, finding a node-by-name recursively. pugi::xml_node find_node_by_name_re(const pugi::xml_node& node, const std::vector node_names) { @@ -348,6 +377,8 @@ std::vector ScreenScraperRequest::getRipList(std::string imageSourc void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::vector& out_results) { + LOG(LogDebug) << "ScreenScraperRequest::processGame >>"; + pugi::xml_node data = xmldoc.child("Data"); if (data.child("jeux")) data = data.child("jeux"); @@ -384,12 +415,12 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve // Genre fallback language: EN. ( Xpath: Data/jeu[0]/genres/genre[*] ) result.mdl.set("genre", find_child_by_attribute_list(game.child("genres"), "genre", "langue", { language, "en" }).text().get()); - LOG(LogDebug) << "Genre: " << result.mdl.get("genre"); + //LOG(LogDebug) << "Genre: " << result.mdl.get("genre"); // Get the date proper. The API returns multiple 'date' children nodes to the 'dates' main child of 'jeu'. // Date fallback: WOR(LD), US, SS, JP, EU std::string _date = find_child_by_attribute_list(game.child("dates"), "date", "region", { region, "wor", "us", "ss", "jp", "eu" }).text().get(); - LOG(LogDebug) << "Release Date (unparsed): " << _date; + //LOG(LogDebug) << "Release Date (unparsed): " << _date; // Date can be YYYY-MM-DD or just YYYY. if (_date.length() > 4) @@ -400,7 +431,7 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve result.mdl.set("releasedate", Utils::Time::DateTime(Utils::Time::stringToTime(_date, "%Y"))); } - LOG(LogDebug) << "Release Date (parsed): " << result.mdl.get("releasedate"); + //LOG(LogDebug) << "Release Date (parsed): " << result.mdl.get("releasedate"); /// Developer for the game( Xpath: Data/jeu[0]/developpeur ) std::string developer = game.child("developpeur").text().get(); @@ -415,7 +446,20 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve // Players result.mdl.set("players", game.child("joueurs").text().get()); - // TODO: Validate rating + if(game.child("systeme").attribute("id")) + { + int systemId = game.child("systeme").attribute("id").as_int(); + + if(screenscraper_arcadesystemid_map.find(systemId) != screenscraper_arcadesystemid_map.cend()) + { + std::string systemName = screenscraper_arcadesystemid_map.at(game.child("systeme").attribute("id").as_int(0)); + result.mdl.set("arcadesystemname", systemName); + } + //else + // LOG(LogDebug) << "System " << systemId << " not found"; + } + + // TODO: Validate rating if (Settings::getInstance()->getBool("ScrapeRatings") && game.child("note")) { float ratingVal = (game.child("note").text().as_int() / 20.0f); @@ -497,6 +541,8 @@ void ScreenScraperRequest::processGame(const pugi::xml_document& xmldoc, std::ve out_results.push_back(result); } // game + + LOG(LogDebug) << "ScreenScraperRequest::processGame <<"; } // Currently not used in this module diff --git a/es-app/src/scrapers/ThreadedScraper.cpp b/es-app/src/scrapers/ThreadedScraper.cpp index 625142e599..2a3ade5cb0 100644 --- a/es-app/src/scrapers/ThreadedScraper.cpp +++ b/es-app/src/scrapers/ThreadedScraper.cpp @@ -5,6 +5,7 @@ #include "EsLocale.h" #include "guis/GuiMsgBox.h" #include "Gamelist.h" +#include "Log.h" #define GUIICON _U("\uF03E ") @@ -39,15 +40,23 @@ std::string ThreadedScraper::formatGameName(FileData* game) void ThreadedScraper::search(const ScraperSearchParams& params) { + LOG(LogDebug) << "ThreadedScraper::formatGameName"; + + std::string gameName = formatGameName(params.game); + + LOG(LogInfo) << "ThreadedScraper::search >> " << gameName; + mCurrentAction = ""; mLastSearch = params; mSearchHandle = startScraperSearch(params); - std::string idx = std::to_string(mTotal + 1- mSearchQueue.size()) + "/" + std::to_string(mTotal); + std::string idx = std::to_string(mTotal + 1 - mSearchQueue.size()) + "/" + std::to_string(mTotal); mWndNotification->updateTitle(GUIICON + _("SCRAPING") + "... " + idx); - mWndNotification->updateText(formatGameName(params.game), _("Searching")+"..."); + mWndNotification->updateText(gameName, _("Searching")+"..."); mWndNotification->updatePercent(-1); + + LOG(LogDebug) << "ThreadedScraper::search <<"; } void ThreadedScraper::run() @@ -70,6 +79,8 @@ void ThreadedScraper::run() auto statusString = mSearchHandle->getStatusString(); auto httpCode = mSearchHandle->getErrorCode(); + LOG(LogDebug) << "ThreadedScraper::SearchResponse : " << httpCode << " " << statusString; + mSearchHandle.reset(); if (status == ASYNC_DONE) @@ -113,6 +124,8 @@ void ThreadedScraper::run() auto result = mMDResolveHandle->getResult(); auto statusString = mMDResolveHandle->getStatusString(); + LOG(LogDebug) << "ThreadedScraper::ResolveResponse : " << statusString; + mCurrentAction = ""; mMDResolveHandle.reset(); @@ -138,8 +151,13 @@ void ThreadedScraper::run() { mSearchQueue.pop(); - if (!mSearchQueue.empty()) - search(mSearchQueue.front()); + if (mSearchQueue.empty()) + { + LOG(LogDebug) << "ThreadedScraper::finished"; + break; + } + + search(mSearchQueue.front()); } else { @@ -157,20 +175,29 @@ void ThreadedScraper::run() void ThreadedScraper::processMedias(ScraperSearchResult result) { - ScraperSearchParams& search = mSearchQueue.front(); - - if (result.hadMedia()) - mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch); - - search.game->getMetadata().importScrappedMetadata(result.mdl); - saveToGamelistRecovery(search.game); + LOG(LogDebug) << "ThreadedScraper::processMedias >>"; + mMDResolveHandle = resolveMetaDataAssets(result, mLastSearch); + LOG(LogDebug) << "ThreadedScraper::processMedias <<"; } void ThreadedScraper::acceptResult(const ScraperSearchResult& result) { + LOG(LogDebug) << "ThreadedScraper::acceptResult >>"; + ScraperSearchParams& search = mSearchQueue.front(); - search.game->getMetadata().importScrappedMetadata(result.mdl);// = result.mdl; - saveToGamelistRecovery(search.game); + + auto game = search.game; + + mWindow->postToUiThread([game, result](Window* w) + { + LOG(LogDebug) << "ThreadedScraper::importScrappedMetadata"; + game->getMetadata().importScrappedMetadata(result.mdl); + + LOG(LogDebug) << "ThreadedScraper::saveToGamelistRecovery"; + saveToGamelistRecovery(game); + }); + + LOG(LogDebug) << "ThreadedScraper::acceptResult <<"; } void ThreadedScraper::start(Window* window, const std::queue& searches) diff --git a/es-core/src/Log.cpp b/es-core/src/Log.cpp index a2634c171b..24b54afb25 100644 --- a/es-core/src/Log.cpp +++ b/es-core/src/Log.cpp @@ -5,6 +5,7 @@ #include #include #include "Settings.h" +#include #if WIN32 #include @@ -56,6 +57,9 @@ void Log::init() std::ostringstream& Log::get(LogLevel level) { + time_t t = time(nullptr); + os << std::put_time(localtime(&t), "%F %T\t"); + switch (level) { case LogError: diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index 83dadfe731..2223911fd9 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -1139,7 +1139,7 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::mapfileExists(path)) { std::stringstream ss; - ss << " Warning : could not find file \"" << node.text().get() << "\" "; + ss << "Warning : could not find file \"" << node.text().get() << "\" "; if(node.text().get() != path) ss << "(which resolved to \"" << path << "\") "; LOG(LogWarning) << ss.str(); From 3fb5e2036e91cac3ff21dc5448fcf253df926a82 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 22 Feb 2020 23:29:24 +0100 Subject: [PATCH 323/336] Scraper : Secure requests (when free space is missing & other situations ) --- es-app/src/scrapers/Scraper.cpp | 107 ++++++--- es-app/src/scrapers/Scraper.h | 4 +- es-app/src/scrapers/ThreadedScraper.cpp | 41 ++-- es-app/src/scrapers/ThreadedScraper.h | 3 +- es-core/src/HttpReq.cpp | 280 +++++++++++++++++------- es-core/src/HttpReq.h | 37 +++- 6 files changed, 319 insertions(+), 153 deletions(-) diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index 4d77fcda6a..0ae71f69cd 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -72,7 +72,7 @@ void ScraperSearchHandle::update() if(status == ASYNC_ERROR) { // propegate error - setError(req.getStatusString()); + setError(req.getErrorCode(), req.getStatusString()); // empty our queue while(!mRequestQueue.empty()) @@ -111,13 +111,18 @@ ScraperHttpRequest::ScraperHttpRequest(std::vector& results : ScraperRequest(resultsWrite) { setStatus(ASYNC_IN_PROGRESS); - mReq = std::unique_ptr(new HttpReq(url)); + mReq = std::unique_ptr(new HttpReq(url, false)); mRetryCount = 0; } void ScraperHttpRequest::update() { HttpReq::Status status = mReq->status(); + + // not ready yet + if (status == HttpReq::REQ_IN_PROGRESS) + return; + if(status == HttpReq::REQ_SUCCESS) { setStatus(ASYNC_DONE); // if process() has an error, status will be changed to ASYNC_ERROR @@ -129,39 +134,38 @@ void ScraperHttpRequest::update() { mRetryCount++; if (mRetryCount > 4) + { + setStatus(ASYNC_DONE); // Ignore error return; + } setStatus(ASYNC_IN_PROGRESS); + LOG(LogDebug) << "REQ_429_TOOMANYREQUESTS : Wait before Retrying"; + std::string url = mReq->getUrl(); - std::this_thread::sleep_for(std::chrono::seconds(15)); - mReq = std::unique_ptr(new HttpReq(url)); - return; - } + std::this_thread::sleep_for(std::chrono::seconds(mRetryCount < 3 ? 5 : 10)); + mReq = std::unique_ptr(new HttpReq(url, false)); - if (status == HttpReq::REQ_404_NOTFOUND) - { - setStatus(ASYNC_DONE); - return; - } + LOG(LogDebug) << "REQ_429_TOOMANYREQUESTS : Retrying"; - if (status == HttpReq::REQ_400_TOOMANYSCRAPS) - { - setError(400, "SCRAP LIMIT REACHED TODAY (400)"); return; } - - if (status == HttpReq::REQ_426_BLACKLISTED) + // Ignored errors + if (status == HttpReq::REQ_404_NOTFOUND || status == HttpReq::REQ_IO_ERROR) { - setError(246, "THE SOFTWARE HAS BEEN BLACKLISTED (426)"); + setStatus(ASYNC_DONE); return; } - // not ready yet - if(status == HttpReq::REQ_IN_PROGRESS) + // Blocking errors + if (status != HttpReq::REQ_SUCCESS) + { + setError(status, mReq->getErrorMsg()); return; - + } + // everything else is some sort of error LOG(LogError) << "ScraperHttpRequest network error (status: " << status << ") - " << mReq->getErrorMsg(); setError(mReq->getErrorMsg()); @@ -338,7 +342,7 @@ void MDResolveHandle::update() if (pPair->handle->status() == ASYNC_ERROR) { - setError(pPair->handle->getStatusString()); + setError(pPair->handle->getErrorCode(), pPair->handle->getStatusString()); for (auto fc : mFuncs) delete fc; @@ -370,8 +374,9 @@ std::unique_ptr downloadImageAsync(const std::string& url, } ImageDownloadHandle::ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight) : - mSavePath(path), mMaxWidth(maxWidth), mMaxHeight(maxHeight), mReq(new HttpReq(url)) + mSavePath(path), mMaxWidth(maxWidth), mMaxHeight(maxHeight) { + mReq = std::unique_ptr(new HttpReq(url)); } int ImageDownloadHandle::getPercent() @@ -384,18 +389,48 @@ int ImageDownloadHandle::getPercent() void ImageDownloadHandle::update() { - if(mReq->status() == HttpReq::REQ_IN_PROGRESS) + HttpReq::Status status = mReq->status(); + + if (status == HttpReq::REQ_IN_PROGRESS) return; + + if (status == HttpReq::REQ_429_TOOMANYREQUESTS) + { + mRetryCount++; + if (mRetryCount > 4) + { + setStatus(ASYNC_DONE); // Ignore error + return; + } + + setStatus(ASYNC_IN_PROGRESS); + + LOG(LogDebug) << "REQ_429_TOOMANYREQUESTS : Wait before Retrying"; + + std::string url = mReq->getUrl(); + std::this_thread::sleep_for(std::chrono::seconds(mRetryCount < 3 ? 5 : 10)); + mReq = std::unique_ptr(new HttpReq(url, false)); + + LOG(LogDebug) << "REQ_429_TOOMANYREQUESTS : Retrying"; - if(mReq->status() != HttpReq::REQ_SUCCESS) + return; + } + + // Ignored errors + if (status == HttpReq::REQ_404_NOTFOUND || status == HttpReq::REQ_IO_ERROR) { - std::stringstream ss; - ss << "Network error: " << mReq->getErrorMsg(); - setError(ss.str()); + setStatus(ASYNC_DONE); return; } - if (mStatus == ASYNC_IN_PROGRESS) + // Blocking errors + if (status != HttpReq::REQ_SUCCESS) + { + setError(status, mReq->getErrorMsg()); + return; + } + + if (status == HttpReq::REQ_SUCCESS && mStatus == ASYNC_IN_PROGRESS) { int ret = mReq->saveContent(mSavePath, true); if (ret == 2) @@ -412,7 +447,10 @@ void ImageDownloadHandle::update() // It's an image ? std::string ext = Utils::String::toLower(Utils::FileSystem::getExtension(mSavePath)); if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".bmp" || ext == ".gif") - resizeImage(mSavePath, mMaxWidth, mMaxHeight); + { + try { resizeImage(mSavePath, mMaxWidth, mMaxHeight); } + catch(...) { } + } } setStatus(ASYNC_DONE); @@ -450,13 +488,16 @@ bool resizeImage(const std::string& path, int maxWidth, int maxHeight) float width = (float)FreeImage_GetWidth(image); float height = (float)FreeImage_GetHeight(image); - if(maxWidth == 0) + if (width == 0 || height == 0) { + FreeImage_Unload(image); + return true; + } + + if(maxWidth == 0) maxWidth = (int)((maxHeight / height) * width); - }else if(maxHeight == 0) - { + else if(maxHeight == 0) maxHeight = (int)((maxWidth / width) * height); - } if (width <= maxWidth && height <= maxHeight) { diff --git a/es-app/src/scrapers/Scraper.h b/es-app/src/scrapers/Scraper.h index 386b93a336..26e14fd59b 100644 --- a/es-app/src/scrapers/Scraper.h +++ b/es-app/src/scrapers/Scraper.h @@ -74,6 +74,7 @@ struct ScraperSearchResult // a scraper search gathers results from (potentially multiple) ScraperRequests + class ScraperRequest : public AsyncHandle { public: @@ -86,7 +87,6 @@ class ScraperRequest : public AsyncHandle std::vector& mResults; }; - // a single HTTP request that needs to be processed to get the results class ScraperHttpRequest : public ScraperRequest { @@ -200,6 +200,8 @@ class ImageDownloadHandle : public AsyncHandle private: std::unique_ptr mReq; + int mRetryCount; + std::string mSavePath; int mMaxWidth; int mMaxHeight; diff --git a/es-app/src/scrapers/ThreadedScraper.cpp b/es-app/src/scrapers/ThreadedScraper.cpp index 2a3ade5cb0..134472bf92 100644 --- a/es-app/src/scrapers/ThreadedScraper.cpp +++ b/es-app/src/scrapers/ThreadedScraper.cpp @@ -59,6 +59,19 @@ void ThreadedScraper::search(const ScraperSearchParams& params) LOG(LogDebug) << "ThreadedScraper::search <<"; } +void ThreadedScraper::processError(int status, const std::string statusString) +{ + if (status == HttpReq::REQ_430_TOOMANYSCRAPS || status == HttpReq::REQ_430_TOOMANYFAILURES || + status == HttpReq::REQ_426_BLACKLISTED || status == HttpReq::REQ_FILESTREAM_ERROR || status == HttpReq::REQ_426_SERVERMAINTENANCE || + status == HttpReq::REQ_403_BADLOGIN || status == HttpReq::REQ_401_FORBIDDEN) + { + mExit = true; + mWindow->postToUiThread([statusString](Window* w) { w->pushGui(new GuiMsgBox(w, _("SCRAPE FAILED") + " : " + statusString)); }); + } + else + mErrors.push_back(statusString); +} + void ThreadedScraper::run() { while (!mExit && !mSearchQueue.empty()) @@ -94,28 +107,7 @@ void ThreadedScraper::run() } } else if (status == ASYNC_ERROR) - { - if (httpCode == 426) // Blacklist - { - mExit = true; - mWindow->postToUiThread([](Window* w) - { - w->pushGui(new GuiMsgBox(w, _("SCRAPE FAILED : THE APPLICATION HAS BEEN BLACKLISTED"))); - }); - break; - } - else if (httpCode == 400) // Too many scraps - { - mExit = true; - mWindow->postToUiThread([](Window* w) - { - w->pushGui(new GuiMsgBox(w, _("SCRAPE FAILED : SCRAP LIMIT REACHED TODAY"))); - }); - break; - } - else - mErrors.push_back(statusString); - } + processError(httpCode, statusString); } if (mMDResolveHandle && mMDResolveHandle->status() != ASYNC_IN_PROGRESS) @@ -123,6 +115,7 @@ void ThreadedScraper::run() auto status = mMDResolveHandle->status(); auto result = mMDResolveHandle->getResult(); auto statusString = mMDResolveHandle->getStatusString(); + auto httpCode = mMDResolveHandle->getErrorCode(); LOG(LogDebug) << "ThreadedScraper::ResolveResponse : " << statusString; @@ -132,7 +125,7 @@ void ThreadedScraper::run() if (status == ASYNC_DONE) acceptResult(result); else if (status == ASYNC_ERROR) - mErrors.push_back(statusString); + processError(httpCode, statusString); } if (mMDResolveHandle && mMDResolveHandle->status() == ASYNC_IN_PROGRESS) @@ -141,7 +134,7 @@ void ThreadedScraper::run() if (action != mCurrentAction) { mCurrentAction = action; - mWndNotification->updateText(formatGameName(mLastSearch.game), "Downloading "+ mCurrentAction); + mWndNotification->updateText(formatGameName(mLastSearch.game), _("Downloading") + " " + mCurrentAction); } mWndNotification->updatePercent(mMDResolveHandle->getPercent()); diff --git a/es-app/src/scrapers/ThreadedScraper.h b/es-app/src/scrapers/ThreadedScraper.h index 56428f002a..ade11ee411 100644 --- a/es-app/src/scrapers/ThreadedScraper.h +++ b/es-app/src/scrapers/ThreadedScraper.h @@ -36,7 +36,8 @@ class ThreadedScraper void search(const ScraperSearchParams& params); void processMedias(ScraperSearchResult result); void acceptResult(const ScraperSearchResult& result); - + void processError(int status, const std::string statusString); + std::string formatGameName(FileData* game); int mTotal; diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index 8cf62410ef..bbb1dbbb57 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -91,10 +91,11 @@ std::string _regGetString(HKEY hKey, const std::string &strPath, const std::stri } #endif -HttpReq::HttpReq(const std::string& url) +HttpReq::HttpReq(const std::string& url, bool useBinaryFileStream) : mStatus(REQ_IN_PROGRESS), mHandle(NULL) { mUrl = url; + mUseFileStream = useBinaryFileStream; mPosition = -1; mPercent = -1; @@ -201,39 +202,50 @@ HttpReq::HttpReq(const std::string& url) std::unique_lock lock(mMutex); -#if defined(WIN32) - srand(time(NULL) % getpid()); - std::string TempPath; - char lpTempPathBuffer[MAX_PATH]; - if (GetTempPathA(MAX_PATH, lpTempPathBuffer)) + if (mUseFileStream) { +#if defined(WIN32) + srand(time(NULL) % getpid()); + std::string TempPath; + char lpTempPathBuffer[MAX_PATH]; + if (GetTempPathA(MAX_PATH, lpTempPathBuffer)) + { - TCHAR szTempFileName[MAX_PATH]; + TCHAR szTempFileName[MAX_PATH]; - if (GetTempFileName(lpTempPathBuffer, TEXT("httpreq"), 0, szTempFileName)) - mStreamPath = std::string(szTempFileName); - else - { - do { mStreamPath = std::string(lpTempPathBuffer) + "httpreq" + std::to_string(rand() % 99999) + ".tmp"; } - while (Utils::FileSystem::exists(mStreamPath)); + if (GetTempFileName(lpTempPathBuffer, TEXT("httpreq"), 0, szTempFileName)) + mStreamPath = std::string(szTempFileName); + else + { + do { mStreamPath = std::string(lpTempPathBuffer) + "httpreq" + std::to_string(rand() % 99999) + ".tmp"; } while (Utils::FileSystem::exists(mStreamPath)); + } } - } - + +/* +#if _DEBUG + do { mStreamPath = Utils::FileSystem::getEsConfigPath() + "/tmp/httpreq" + std::to_string(rand() % 99999) + ".tmp"; } while (Utils::FileSystem::exists(mStreamPath)); +#endif +*/ #else - srand(time(NULL) % getpid() + getppid()); + srand(time(NULL) % getpid() + getppid()); - do { mStreamPath = "/tmp/httpreq" + std::to_string(rand() % 99999) + ".tmp"; } - while (Utils::FileSystem::exists(mStreamPath)); + do { mStreamPath = "/tmp/httpreq" + std::to_string(rand() % 99999) + ".tmp"; } while (Utils::FileSystem::exists(mStreamPath)); #endif - - mStream.open(mStreamPath, std::ios_base::out | std::ios_base::binary); + + mStream.open(mStreamPath, std::ios_base::out | std::ios_base::binary); + if (!mStream.is_open()) + { + mStatus = REQ_IO_ERROR; + onError("IO Error (disk is Readonly ?)"); + return; + } + } //add the handle to our multi CURLMcode merr = curl_multi_add_handle(s_multi_handle, mHandle); if(merr != CURLM_OK) { - if (mStream.is_open()) - mStream.close(); + closeStream(); mStatus = REQ_IO_ERROR; onError(curl_multi_strerror(merr)); @@ -243,17 +255,26 @@ HttpReq::HttpReq(const std::string& url) s_requests[mHandle] = this; } -HttpReq::~HttpReq() +void HttpReq::closeStream() { - std::unique_lock lock(mMutex); + if (!mUseFileStream) + return; if (mStream.is_open()) { mStream.flush(); mStream.close(); } +} - Utils::FileSystem::removeFile(mStreamPath); +HttpReq::~HttpReq() +{ + std::unique_lock lock(mMutex); + + closeStream(); + + if (mUseFileStream) + Utils::FileSystem::removeFile(mStreamPath); if(mHandle) { @@ -278,8 +299,7 @@ HttpReq::Status HttpReq::status() CURLMcode merr = curl_multi_perform(s_multi_handle, &handle_count); if(merr != CURLM_OK && merr != CURLM_CALL_MULTI_PERFORM) { - if (mStream.is_open()) - mStream.close(); + closeStream(); mStatus = REQ_IO_ERROR; onError(curl_multi_strerror(merr)); @@ -299,29 +319,35 @@ HttpReq::Status HttpReq::status() continue; } - if (req->mStream.is_open()) + req->closeStream(); + + if (req->mStatus == REQ_FILESTREAM_ERROR) { - req->mStream.flush(); - req->mStream.close(); + std::string err = "File stream error (disk full ?)"; + req->onError(err.c_str()); } - - if(msg->data.result == CURLE_OK) + else if(msg->data.result == CURLE_OK) { int http_status_code; curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &http_status_code); if (http_status_code < 200 || http_status_code > 299) { - if(http_status_code == 404) - req->mStatus = REQ_404_NOTFOUND; - else if (http_status_code == 429) - req->mStatus = REQ_429_TOOMANYREQUESTS; - else if (http_status_code == 426) - req->mStatus = REQ_426_BLACKLISTED; + std::string err; + + if (http_status_code >= 400 && http_status_code < 499) + { + if (!req->mUseFileStream) + err = req->getContent(); + + req->mStatus = (Status)http_status_code; + } else req->mStatus = REQ_IO_ERROR; + + if (err.empty()) + err = "HTTP status " + std::to_string(http_status_code); - std::string err = "HTTP status " + std::to_string(http_status_code); req->onError(err.c_str()); } else @@ -341,28 +367,38 @@ HttpReq::Status HttpReq::status() std::string HttpReq::getContent() { - assert(mStatus == REQ_SUCCESS); + if (!mUseFileStream) + return mContent.str(); - if (mStream.is_open()) + try { - mStream.flush(); - mStream.close(); - } - - std::ifstream ifs(mStreamPath, std::ios_base::in | std::ios_base::binary); - if (ifs.bad()) - return ""; + closeStream(); - std::stringstream ofs; - ofs << ifs.rdbuf(); - ifs.close(); + if (!Utils::FileSystem::exists(mStreamPath)) + return ""; - return ofs.str(); + std::ifstream ifs(mStreamPath, std::ios_base::in | std::ios_base::binary); + if (ifs.bad()) + return ""; + + std::stringstream ofs; + ofs << ifs.rdbuf(); + ifs.close(); + + return ofs.str(); + } + catch (...) + { + LOG(LogError) << "Error getting Http request content"; + } + + return ""; } void HttpReq::onError(const char* msg) { mErrorMsg = msg; + LOG(LogError) << "HttpReq::onError (" + std::to_string(mStatus) << ") : " + mErrorMsg; } std::string HttpReq::getErrorMsg() @@ -377,8 +413,44 @@ size_t HttpReq::write_content(void* buff, size_t size, size_t nmemb, void* req_p { HttpReq* request = ((HttpReq*)req_ptr); + if (!request->mUseFileStream) + { + ((HttpReq*)req_ptr)->mContent.write((char*)buff, size * nmemb); + return size * nmemb; + } + std::ofstream& ss = request->mStream; - ss.write((char*)buff, size * nmemb); + + try + { + if (!ss.is_open()) + return 0; + + ss.write((char*)buff, size * nmemb); + + if (ss.rdstate() != std::ofstream::goodbit) + { + request->closeStream(); + + Utils::FileSystem::removeFile(request->mStreamPath); + request->mStreamPath = ""; + request->mStatus = REQ_FILESTREAM_ERROR; + request->mErrorMsg = "IO ERROR (DISK FULL?)"; + + return 0; + } + } + catch(...) + { + request->closeStream(); + + Utils::FileSystem::removeFile(request->mStreamPath); + request->mStreamPath = ""; + request->mStatus = REQ_FILESTREAM_ERROR; + request->mErrorMsg = "IO ERROR (DISK FULL?)"; + + return 0; + } double cl; if (!curl_easy_getinfo(request->mHandle, CURLINFO_CONTENT_LENGTH_DOWNLOAD, &cl)) @@ -399,43 +471,85 @@ int HttpReq::saveContent(const std::string filename, bool checkMedia) { assert(mStatus == REQ_SUCCESS); - if (mStream.is_open()) + if (!mUseFileStream) { - mStream.flush(); - mStream.close(); - } + try + { + std::ofstream file(filename, std::ios::binary | std::ios::out); + if (!file.is_open()) + return 1; - if (!Utils::FileSystem::exists(mStreamPath)) - return false; + file << mContent.str(); + file.flush(); + file.close(); + } + catch (...) + { + return 1; + } + return 0; + } - if (checkMedia && Utils::FileSystem::getFileSize(mStreamPath) < 1024) + try { - auto data = Utils::String::toUpper(getContent()); - - if (data.find(" Date: Sat, 22 Feb 2020 23:43:38 +0100 Subject: [PATCH 324/336] Http request : direct download file as .tmp files in the destination folder like regular browser do & rename them when finished instead of copying them from /tmp/ folder --- es-app/src/ApiSystem.cpp | 99 +++++------ es-app/src/guis/GuiSettings.h | 2 +- es-app/src/scrapers/GamesDBJSONScraper.cpp | 6 +- es-app/src/scrapers/GamesDBJSONScraper.h | 2 +- es-app/src/scrapers/Scraper.cpp | 56 ++++--- es-app/src/scrapers/Scraper.h | 9 +- es-app/src/scrapers/ScreenScraper.cpp | 6 +- es-app/src/scrapers/ScreenScraper.h | 2 +- es-core/src/HttpReq.cpp | 186 +++++---------------- es-core/src/HttpReq.h | 34 ++-- 10 files changed, 146 insertions(+), 256 deletions(-) diff --git a/es-app/src/ApiSystem.cpp b/es-app/src/ApiSystem.cpp index 4d8b1a26f8..dd2d300f1d 100644 --- a/es-app/src/ApiSystem.cpp +++ b/es-app/src/ApiSystem.cpp @@ -103,14 +103,10 @@ std::string ApiSystem::checkUpdateVersion() localVersion = Utils::String::replace(Utils::String::replace(localVersion, "\r", ""), "\n", ""); } - std::shared_ptr httpreq = std::make_shared("https://github.com/fabricecaruso/EmulationStation/releases/download/continuous-master/version.info"); - - while (httpreq->status() == HttpReq::REQ_IN_PROGRESS) - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - - if (httpreq->status() == HttpReq::REQ_SUCCESS) + HttpReq httpreq("https://github.com/fabricecaruso/EmulationStation/releases/download/continuous-master/version.info"); + if (httpreq.wait()) { - std::string serverVersion = httpreq->getContent(); + std::string serverVersion = httpreq.getContent(); serverVersion = Utils::String::replace(Utils::String::replace(serverVersion, "\r", ""), "\n", ""); if (!serverVersion.empty() && serverVersion != localVersion) return serverVersion; @@ -196,25 +192,24 @@ bool unzipFile(const std::string fileName, const std::string dest) return ret; } -std::shared_ptr downloadFile(const std::string url, const std::string label, const std::function& func) +bool downloadFile(const std::string url, const std::string fileName, const std::string label, const std::function& func) { if (func != nullptr) func("Downloading " + label); - std::shared_ptr httpreq = std::make_shared(url); - - while (httpreq->status() == HttpReq::REQ_IN_PROGRESS) + HttpReq httpreq(url, fileName); + while (httpreq.status() == HttpReq::REQ_IN_PROGRESS) { if (func != nullptr) - func(std::string("Downloading " + label + " >>> " + std::to_string(httpreq->getPercent()) + " %")); + func(std::string("Downloading " + label + " >>> " + std::to_string(httpreq.getPercent()) + " %")); std::this_thread::sleep_for(std::chrono::milliseconds(20)); } - if (httpreq->status() != HttpReq::REQ_SUCCESS) - return nullptr; + if (httpreq.status() != HttpReq::REQ_SUCCESS) + return false; - return httpreq; + return true; } @@ -236,24 +231,22 @@ std::pair ApiSystem::updateSystem(const std::functionstatus() == HttpReq::REQ_SUCCESS) + + std::string fileName = Utils::FileSystem::getFileName(url); + std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/update"; + + if (!Utils::FileSystem::exists(path)) + Utils::FileSystem::createDirectory(path); + else + deleteDirectoryFiles(path); + + std::string zipFile = path + "/" + fileName; + + if (downloadFile(url, zipFile, "update", func)) { if (func != nullptr) func(std::string("Extracting update")); - std::string fileName = Utils::FileSystem::getFileName(url); - std::string path = Utils::FileSystem::getHomePath() + "/.emulationstation/update"; - - - if (!Utils::FileSystem::exists(path)) - Utils::FileSystem::createDirectory(path); - else - deleteDirectoryFiles(path); - - std::string zipFile = path + "/" + fileName; - req->saveContent(zipFile); - unzipFile(Utils::FileSystem::getPreferredPath(zipFile), Utils::FileSystem::getPreferredPath(path)); Utils::FileSystem::removeFile(zipFile); @@ -303,14 +296,10 @@ std::vector ApiSystem::getThemesList() std::vector res; - std::shared_ptr httpreq = std::make_shared("https://batocera.org/upgrades/themes.txt"); - - while (httpreq->status() == HttpReq::REQ_IN_PROGRESS) - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - - if (httpreq->status() == HttpReq::REQ_SUCCESS) + HttpReq httpreq("https://batocera.org/upgrades/themes.txt"); + if (httpreq.wait()) { - auto lines = Utils::String::split(httpreq->getContent(), '\n'); + auto lines = Utils::String::split(httpreq.getContent(), '\n'); for (auto line : lines) { auto parts = Utils::String::splitAny(line, " \t"); @@ -352,7 +341,7 @@ std::vector ApiSystem::getThemesList() return res; } -std::shared_ptr downloadGitRepository(const std::string url, const std::string label, const std::function& func) +bool downloadGitRepository(const std::string url, const std::string fileName, const std::string label, const std::function& func) { if (func != nullptr) func(_("Downloading") + " " + label); @@ -362,14 +351,10 @@ std::shared_ptr downloadGitRepository(const std::string url, const std: std::string statUrl = Utils::String::replace(url, "https://github.com/", "https://api.github.com/repos/"); if (statUrl != url) { - std::shared_ptr statreq = std::make_shared(statUrl); - - while (statreq->status() == HttpReq::REQ_IN_PROGRESS) - std::this_thread::sleep_for(std::chrono::milliseconds(20)); - - if (statreq->status() == HttpReq::REQ_SUCCESS) + HttpReq statreq(statUrl); + if (statreq.wait()) { - std::string content = statreq->getContent(); + std::string content = statreq.getContent(); auto pos = content.find("\"size\": "); if (pos != std::string::npos) { @@ -380,14 +365,14 @@ std::shared_ptr downloadGitRepository(const std::string url, const std: } } - std::shared_ptr httpreq = std::make_shared(url + "/archive/master.zip"); + HttpReq httpreq(url + "/archive/master.zip", fileName); int curPos = -1; - while (httpreq->status() == HttpReq::REQ_IN_PROGRESS) + while (httpreq.status() == HttpReq::REQ_IN_PROGRESS) { if (downloadSize > 0) { - double pos = httpreq->getPosition(); + double pos = httpreq.getPosition(); if (pos > 0 && curPos != pos) { if (func != nullptr) @@ -403,10 +388,10 @@ std::shared_ptr downloadGitRepository(const std::string url, const std: std::this_thread::sleep_for(std::chrono::milliseconds(20)); } - if (httpreq->status() != HttpReq::REQ_SUCCESS) - return nullptr; + if (httpreq.status() != HttpReq::REQ_SUCCESS) + return false; - return httpreq; + return true; } std::pair ApiSystem::installTheme(std::string themeName, const std::function& func) @@ -417,17 +402,15 @@ std::pair ApiSystem::installTheme(std::string themeName, const if (theme.name != themeName) continue; - std::shared_ptr httpreq = downloadGitRepository(theme.url, themeName, func); - if (httpreq != nullptr && httpreq->status() == HttpReq::REQ_SUCCESS) + std::string themeFileName = Utils::FileSystem::getFileName(theme.url); + std::string zipFile = Utils::FileSystem::getHomePath() + "/.emulationstation/themes/" + themeFileName + ".zip"; + zipFile = Utils::String::replace(zipFile, "/", "\\"); + + if (downloadGitRepository(theme.url, zipFile, themeName, func)) { if (func != nullptr) func(_("Extracting") + " " + themeName); - std::string themeFileName = Utils::FileSystem::getFileName(theme.url); - std::string zipFile = Utils::FileSystem::getHomePath() + "/.emulationstation/themes/" + themeFileName + ".zip"; - zipFile = Utils::String::replace(zipFile, "/", "\\"); - httpreq->saveContent(zipFile); - if (!unzipFile(zipFile, Utils::String::replace(Utils::FileSystem::getHomePath() + "/.emulationstation/themes", "/", "\\"))) return std::pair(std::string("An error occured while extracting"), 1); @@ -437,9 +420,7 @@ std::pair ApiSystem::installTheme(std::string themeName, const std::string finalfolderName = Utils::FileSystem::getParent(folderName) + "/" + themeName; if (Utils::FileSystem::isDirectory(finalfolderName)) - { deleteDirectoryFiles(finalfolderName); - } rename(folderName.c_str(), finalfolderName.c_str()); diff --git a/es-app/src/guis/GuiSettings.h b/es-app/src/guis/GuiSettings.h index 168ce52646..473edb99b4 100644 --- a/es-app/src/guis/GuiSettings.h +++ b/es-app/src/guis/GuiSettings.h @@ -15,7 +15,7 @@ class GuiSettings : public GuiComponent void save(); inline void addRow(const ComponentListRow& row) { mMenu.addRow(row); }; - inline void addWithLabel(const std::string& label, const std::shared_ptr& comp) { mMenu.addWithLabel(label, comp); }; + inline void addWithLabel(const std::string& label, const std::shared_ptr& comp, bool setCursorHere = false) { mMenu.addWithLabel(label, comp, "", setCursorHere); }; inline void addSaveFunc(const std::function& func) { mSaveFuncs.push_back(func); }; inline void addEntry(const std::string name, bool add_arrow = false, const std::function& func = nullptr, const std::string iconName = "") { mMenu.addEntry(name, add_arrow, func, iconName); }; diff --git a/es-app/src/scrapers/GamesDBJSONScraper.cpp b/es-app/src/scrapers/GamesDBJSONScraper.cpp index 58bb8e104a..e44fbf4088 100644 --- a/es-app/src/scrapers/GamesDBJSONScraper.cpp +++ b/es-app/src/scrapers/GamesDBJSONScraper.cpp @@ -356,12 +356,12 @@ void processGame(const Value& game, const Value& boxart, std::vector& req, std::vector& results) +bool TheGamesDBJSONRequest::process(HttpReq* request, std::vector& results) { - assert(req->status() == HttpReq::REQ_SUCCESS); + assert(request->status() == HttpReq::REQ_SUCCESS); Document doc; - doc.Parse(req->getContent().c_str()); + doc.Parse(request->getContent().c_str()); if (doc.HasParseError()) { diff --git a/es-app/src/scrapers/GamesDBJSONScraper.h b/es-app/src/scrapers/GamesDBJSONScraper.h index d0f1aeb2fb..672b7eddf0 100644 --- a/es-app/src/scrapers/GamesDBJSONScraper.h +++ b/es-app/src/scrapers/GamesDBJSONScraper.h @@ -28,7 +28,7 @@ class TheGamesDBJSONRequest : public ScraperHttpRequest } protected: - bool process(const std::unique_ptr& req, std::vector& results) override; + bool process(HttpReq* request, std::vector& results) override; bool isGameRequest() { return !mRequestQueue; } std::queue>* mRequestQueue; diff --git a/es-app/src/scrapers/Scraper.cpp b/es-app/src/scrapers/Scraper.cpp index 0ae71f69cd..c71924ab3a 100644 --- a/es-app/src/scrapers/Scraper.cpp +++ b/es-app/src/scrapers/Scraper.cpp @@ -111,13 +111,18 @@ ScraperHttpRequest::ScraperHttpRequest(std::vector& results : ScraperRequest(resultsWrite) { setStatus(ASYNC_IN_PROGRESS); - mReq = std::unique_ptr(new HttpReq(url, false)); + mRequest = new HttpReq(url); mRetryCount = 0; } +ScraperHttpRequest::~ScraperHttpRequest() +{ + delete mRequest; +} + void ScraperHttpRequest::update() { - HttpReq::Status status = mReq->status(); + HttpReq::Status status = mRequest->status(); // not ready yet if (status == HttpReq::REQ_IN_PROGRESS) @@ -126,7 +131,7 @@ void ScraperHttpRequest::update() if(status == HttpReq::REQ_SUCCESS) { setStatus(ASYNC_DONE); // if process() has an error, status will be changed to ASYNC_ERROR - process(mReq, mResults); + process(mRequest, mResults); return; } @@ -143,9 +148,11 @@ void ScraperHttpRequest::update() LOG(LogDebug) << "REQ_429_TOOMANYREQUESTS : Wait before Retrying"; - std::string url = mReq->getUrl(); + std::string url = mRequest->getUrl(); std::this_thread::sleep_for(std::chrono::seconds(mRetryCount < 3 ? 5 : 10)); - mReq = std::unique_ptr(new HttpReq(url, false)); + + delete mRequest; + mRequest = new HttpReq(url); LOG(LogDebug) << "REQ_429_TOOMANYREQUESTS : Retrying"; @@ -162,13 +169,13 @@ void ScraperHttpRequest::update() // Blocking errors if (status != HttpReq::REQ_SUCCESS) { - setError(status, mReq->getErrorMsg()); + setError(status, mRequest->getErrorMsg()); return; } // everything else is some sort of error - LOG(LogError) << "ScraperHttpRequest network error (status: " << status << ") - " << mReq->getErrorMsg(); - setError(mReq->getErrorMsg()); + LOG(LogError) << "ScraperHttpRequest network error (status: " << status << ") - " << mRequest->getErrorMsg(); + setError(mRequest->getErrorMsg()); } @@ -376,20 +383,25 @@ std::unique_ptr downloadImageAsync(const std::string& url, ImageDownloadHandle::ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight) : mSavePath(path), mMaxWidth(maxWidth), mMaxHeight(maxHeight) { - mReq = std::unique_ptr(new HttpReq(url)); + mRequest = new HttpReq(url, path); +} + +ImageDownloadHandle::~ImageDownloadHandle() +{ + delete mRequest; } int ImageDownloadHandle::getPercent() { - if (mReq->status() == HttpReq::REQ_IN_PROGRESS) - return mReq->getPercent(); + if (mRequest->status() == HttpReq::REQ_IN_PROGRESS) + return mRequest->getPercent(); return -1; } void ImageDownloadHandle::update() { - HttpReq::Status status = mReq->status(); + HttpReq::Status status = mRequest->status(); if (status == HttpReq::REQ_IN_PROGRESS) return; @@ -407,9 +419,11 @@ void ImageDownloadHandle::update() LOG(LogDebug) << "REQ_429_TOOMANYREQUESTS : Wait before Retrying"; - std::string url = mReq->getUrl(); + std::string url = mRequest->getUrl(); std::this_thread::sleep_for(std::chrono::seconds(mRetryCount < 3 ? 5 : 10)); - mReq = std::unique_ptr(new HttpReq(url, false)); + + delete mRequest; + mRequest = new HttpReq(url, mSavePath); LOG(LogDebug) << "REQ_429_TOOMANYREQUESTS : Retrying"; @@ -426,24 +440,12 @@ void ImageDownloadHandle::update() // Blocking errors if (status != HttpReq::REQ_SUCCESS) { - setError(status, mReq->getErrorMsg()); + setError(status, mRequest->getErrorMsg()); return; } if (status == HttpReq::REQ_SUCCESS && mStatus == ASYNC_IN_PROGRESS) { - int ret = mReq->saveContent(mSavePath, true); - if (ret == 2) - { - setError("Failed to save media : The server response is invalid"); - return; - } - else if (ret == 1) - { - setError("Failed to save image on disk. Disk full?"); - return; - } - // It's an image ? std::string ext = Utils::String::toLower(Utils::FileSystem::getExtension(mSavePath)); if (ext == ".jpg" || ext == ".jpeg" || ext == ".png" || ext == ".bmp" || ext == ".gif") diff --git a/es-app/src/scrapers/Scraper.h b/es-app/src/scrapers/Scraper.h index 26e14fd59b..671d9823e6 100644 --- a/es-app/src/scrapers/Scraper.h +++ b/es-app/src/scrapers/Scraper.h @@ -92,13 +92,15 @@ class ScraperHttpRequest : public ScraperRequest { public: ScraperHttpRequest(std::vector& resultsWrite, const std::string& url); + ~ScraperHttpRequest(); + virtual void update() override; protected: - virtual bool process(const std::unique_ptr& req, std::vector& results) = 0; + virtual bool process(HttpReq* request, std::vector& results) = 0; private: - std::unique_ptr mReq; + HttpReq* mRequest; int mRetryCount; }; @@ -193,13 +195,14 @@ class ImageDownloadHandle : public AsyncHandle { public: ImageDownloadHandle(const std::string& url, const std::string& path, int maxWidth, int maxHeight); + ~ImageDownloadHandle(); void update() override; virtual int getPercent(); private: - std::unique_ptr mReq; + HttpReq* mRequest; int mRetryCount; std::string mSavePath; diff --git a/es-app/src/scrapers/ScreenScraper.cpp b/es-app/src/scrapers/ScreenScraper.cpp index 46ef333229..64981a99bd 100644 --- a/es-app/src/scrapers/ScreenScraper.cpp +++ b/es-app/src/scrapers/ScreenScraper.cpp @@ -279,11 +279,11 @@ void screenscraper_generate_scraper_requests(const ScraperSearchParams& params, } // Process should return false only when we reached a maximum scrap by minute, to retry -bool ScreenScraperRequest::process(const std::unique_ptr& req, std::vector& results) +bool ScreenScraperRequest::process(HttpReq* request, std::vector& results) { - assert(req->status() == HttpReq::REQ_SUCCESS); + assert(request->status() == HttpReq::REQ_SUCCESS); - auto content = req->getContent(); + auto content = request->getContent(); pugi::xml_document doc; pugi::xml_parse_result parseResult = doc.load(content.c_str()); diff --git a/es-app/src/scrapers/ScreenScraper.h b/es-app/src/scrapers/ScreenScraper.h index dcde1b2513..fa0bb4ebb4 100644 --- a/es-app/src/scrapers/ScreenScraper.h +++ b/es-app/src/scrapers/ScreenScraper.h @@ -58,7 +58,7 @@ class ScreenScraperRequest : public ScraperHttpRequest } configuration; protected: - bool process(const std::unique_ptr& req, std::vector& results) override; + bool process(HttpReq* request, std::vector& results) override; std::string ensureUrl(const std::string url); void processList(const pugi::xml_document& xmldoc, std::vector& results); diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index bbb1dbbb57..377216afb5 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -91,11 +91,11 @@ std::string _regGetString(HKEY hKey, const std::string &strPath, const std::stri } #endif -HttpReq::HttpReq(const std::string& url, bool useBinaryFileStream) +HttpReq::HttpReq(const std::string& url, const std::string outputFilename) : mStatus(REQ_IN_PROGRESS), mHandle(NULL) { mUrl = url; - mUseFileStream = useBinaryFileStream; + mFilePath = outputFilename; mPosition = -1; mPercent = -1; @@ -202,43 +202,21 @@ HttpReq::HttpReq(const std::string& url, bool useBinaryFileStream) std::unique_lock lock(mMutex); - if (mUseFileStream) + if (!mFilePath.empty()) { -#if defined(WIN32) - srand(time(NULL) % getpid()); - std::string TempPath; - char lpTempPathBuffer[MAX_PATH]; - if (GetTempPathA(MAX_PATH, lpTempPathBuffer)) - { - - TCHAR szTempFileName[MAX_PATH]; - - if (GetTempFileName(lpTempPathBuffer, TEXT("httpreq"), 0, szTempFileName)) - mStreamPath = std::string(szTempFileName); - else - { - do { mStreamPath = std::string(lpTempPathBuffer) + "httpreq" + std::to_string(rand() % 99999) + ".tmp"; } while (Utils::FileSystem::exists(mStreamPath)); - } - } - -/* -#if _DEBUG - do { mStreamPath = Utils::FileSystem::getEsConfigPath() + "/tmp/httpreq" + std::to_string(rand() % 99999) + ".tmp"; } while (Utils::FileSystem::exists(mStreamPath)); -#endif -*/ -#else - srand(time(NULL) % getpid() + getppid()); - - do { mStreamPath = "/tmp/httpreq" + std::to_string(rand() % 99999) + ".tmp"; } while (Utils::FileSystem::exists(mStreamPath)); -#endif + mTempStreamPath = outputFilename + ".tmp"; + + Utils::FileSystem::removeFile(mTempStreamPath); - mStream.open(mStreamPath, std::ios_base::out | std::ios_base::binary); + mStream.open(mTempStreamPath, std::ios_base::out | std::ios_base::binary); if (!mStream.is_open()) { mStatus = REQ_IO_ERROR; onError("IO Error (disk is Readonly ?)"); return; } + + Utils::FileSystem::removeFile(outputFilename); } //add the handle to our multi @@ -257,7 +235,7 @@ HttpReq::HttpReq(const std::string& url, bool useBinaryFileStream) void HttpReq::closeStream() { - if (!mUseFileStream) + if (mFilePath.empty()) return; if (mStream.is_open()) @@ -273,8 +251,8 @@ HttpReq::~HttpReq() closeStream(); - if (mUseFileStream) - Utils::FileSystem::removeFile(mStreamPath); + if (!mTempStreamPath.empty()) + Utils::FileSystem::removeFile(mTempStreamPath); if(mHandle) { @@ -293,11 +271,11 @@ HttpReq::Status HttpReq::status() { std::unique_lock lock(mMutex); - if(mStatus == REQ_IN_PROGRESS) + if (mStatus == REQ_IN_PROGRESS) { int handle_count; CURLMcode merr = curl_multi_perform(s_multi_handle, &handle_count); - if(merr != CURLM_OK && merr != CURLM_CALL_MULTI_PERFORM) + if (merr != CURLM_OK && merr != CURLM_CALL_MULTI_PERFORM) { closeStream(); @@ -308,12 +286,12 @@ HttpReq::Status HttpReq::status() int msgs_left; CURLMsg* msg; - while((msg = curl_multi_info_read(s_multi_handle, &msgs_left)) != nullptr) + while ((msg = curl_multi_info_read(s_multi_handle, &msgs_left)) != nullptr) { - if(msg->msg == CURLMSG_DONE) + if (msg->msg == CURLMSG_DONE) { - HttpReq* req = s_requests[msg->easy_handle]; - if(req == NULL) + HttpReq* req = s_requests[msg->easy_handle]; + if (req == NULL) { LOG(LogError) << "Cannot find easy handle!"; continue; @@ -326,7 +304,7 @@ HttpReq::Status HttpReq::status() std::string err = "File stream error (disk full ?)"; req->onError(err.c_str()); } - else if(msg->data.result == CURLE_OK) + else if (msg->data.result == CURLE_OK) { int http_status_code; curl_easy_getinfo(msg->easy_handle, CURLINFO_RESPONSE_CODE, &http_status_code); @@ -337,21 +315,34 @@ HttpReq::Status HttpReq::status() if (http_status_code >= 400 && http_status_code < 499) { - if (!req->mUseFileStream) + if (mFilePath.empty()) err = req->getContent(); - req->mStatus = (Status)http_status_code; + req->mStatus = (Status)http_status_code; } else req->mStatus = REQ_IO_ERROR; - + if (err.empty()) err = "HTTP status " + std::to_string(http_status_code); req->onError(err.c_str()); } else - req->mStatus = REQ_SUCCESS; + { + if (!mFilePath.empty()) + { + if (std::rename(mTempStreamPath.c_str(), mFilePath.c_str()) == 0) + req->mStatus = REQ_SUCCESS; + else + { + req->mStatus = REQ_IO_ERROR; + req->onError("file rename failed"); + } + } + else + req->mStatus = REQ_SUCCESS; + } } else { @@ -367,17 +358,17 @@ HttpReq::Status HttpReq::status() std::string HttpReq::getContent() { - if (!mUseFileStream) + if (mFilePath.empty()) return mContent.str(); try { closeStream(); - if (!Utils::FileSystem::exists(mStreamPath)) + if (!Utils::FileSystem::exists(mTempStreamPath)) return ""; - std::ifstream ifs(mStreamPath, std::ios_base::in | std::ios_base::binary); + std::ifstream ifs(mTempStreamPath, std::ios_base::in | std::ios_base::binary); if (ifs.bad()) return ""; @@ -413,7 +404,7 @@ size_t HttpReq::write_content(void* buff, size_t size, size_t nmemb, void* req_p { HttpReq* request = ((HttpReq*)req_ptr); - if (!request->mUseFileStream) + if (request->mFilePath.empty()) { ((HttpReq*)req_ptr)->mContent.write((char*)buff, size * nmemb); return size * nmemb; @@ -430,10 +421,7 @@ size_t HttpReq::write_content(void* buff, size_t size, size_t nmemb, void* req_p if (ss.rdstate() != std::ofstream::goodbit) { - request->closeStream(); - - Utils::FileSystem::removeFile(request->mStreamPath); - request->mStreamPath = ""; + request->closeStream(); request->mStatus = REQ_FILESTREAM_ERROR; request->mErrorMsg = "IO ERROR (DISK FULL?)"; @@ -442,10 +430,7 @@ size_t HttpReq::write_content(void* buff, size_t size, size_t nmemb, void* req_p } catch(...) { - request->closeStream(); - - Utils::FileSystem::removeFile(request->mStreamPath); - request->mStreamPath = ""; + request->closeStream(); request->mStatus = REQ_FILESTREAM_ERROR; request->mErrorMsg = "IO ERROR (DISK FULL?)"; @@ -467,89 +452,10 @@ size_t HttpReq::write_content(void* buff, size_t size, size_t nmemb, void* req_p return nmemb; } -int HttpReq::saveContent(const std::string filename, bool checkMedia) +bool HttpReq::wait() { - assert(mStatus == REQ_SUCCESS); - - if (!mUseFileStream) - { - try - { - std::ofstream file(filename, std::ios::binary | std::ios::out); - if (!file.is_open()) - return 1; - - file << mContent.str(); - file.flush(); - file.close(); - } - catch (...) - { - return 1; - } - return 0; - } + while (status() == HttpReq::REQ_IN_PROGRESS) + std::this_thread::sleep_for(std::chrono::milliseconds(20)); - try - { - closeStream(); - - if (!Utils::FileSystem::exists(mStreamPath)) - return false; - - if (checkMedia && Utils::FileSystem::getFileSize(mStreamPath) < 1024) - { - auto data = Utils::String::toUpper(getContent()); - - if (data.find(" Date: Sat, 22 Feb 2020 23:44:05 +0100 Subject: [PATCH 325/336] UI : Fix buttons layout for small screens --- es-core/src/components/ButtonComponent.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/es-core/src/components/ButtonComponent.cpp b/es-core/src/components/ButtonComponent.cpp index 8f3b414e42..d19c262434 100644 --- a/es-core/src/components/ButtonComponent.cpp +++ b/es-core/src/components/ButtonComponent.cpp @@ -18,6 +18,9 @@ ButtonComponent::ButtonComponent(Window* window, const std::string& text, const mColor = menuTheme->Text.color; mColorFocused = menuTheme->Text.selectorColor; mRenderNonFocusedBackground = true; + + if (Renderer::isSmallScreen()) + mBox.setCornerSize(8, 8); setPressedFunc(func); setText(text, helpText, upperCase); @@ -26,7 +29,8 @@ ButtonComponent::ButtonComponent(Window* window, const std::string& text, const void ButtonComponent::onSizeChanged() { - mBox.fitTo(mSize, Vector3f::Zero(), Vector2f(-32, -32)); + auto sz = mBox.getCornerSize(); + mBox.fitTo(mSize, Vector3f::Zero(), Vector2f(-sz.x() * 2, -sz.y() * 2)); } void ButtonComponent::setPressedFunc(std::function f) From a6a2fb571428ff39bc26df2fddeb61794c756450 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 22 Feb 2020 23:45:22 +0100 Subject: [PATCH 326/336] Gamelists : Fix -> If the current gamelist.xml is corrupted, it can't be saved anymore --- es-app/src/Gamelist.cpp | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/es-app/src/Gamelist.cpp b/es-app/src/Gamelist.cpp index b5bdd6d436..f90cb75462 100644 --- a/es-app/src/Gamelist.cpp +++ b/es-app/src/Gamelist.cpp @@ -342,18 +342,14 @@ void updateGamelist(SystemData* system) { //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 node in gamelist \"" << xmlReadPath << "\"!"; - return; + root = doc.append_child("gameList"); } }else{ //set up an empty gamelist to append to From 396bd856457e28e12a7bf4c35dbfc9d84137f35d Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 23 Feb 2020 09:59:13 +0100 Subject: [PATCH 327/336] System Grouping : Add an option to select Groups to enable/disable + Import folder metadata found in the source gamelist.xml ( for . ) in the virtual folder when grouping is active. --- es-app/src/FileData.h | 8 +- es-app/src/SystemData.cpp | 41 ++++ es-app/src/SystemData.h | 5 +- .../src/guis/GuiCollectionSystemsOptions.cpp | 216 ++++++++++-------- es-app/src/guis/GuiCollectionSystemsOptions.h | 16 +- es-app/src/guis/GuiThemeInstall.cpp | 1 + es-app/src/guis/GuiThemeInstall.h | 31 --- .../src/views/gamelist/GridGameListView.cpp | 28 ++- es-core/CMakeLists.txt | 1 + es-core/src/AudioManager.cpp | 3 + es-core/src/components/MultiLineMenuEntry.h | 39 ++++ es-core/src/components/OptionListComponent.h | 28 ++- es-core/src/utils/FileSystemUtil.cpp | 3 + resources/checkbox_checked.svg | 62 ++++- resources/locale/fr/emulationstation2.po | 4 + 15 files changed, 343 insertions(+), 143 deletions(-) create mode 100644 es-core/src/components/MultiLineMenuEntry.h diff --git a/es-app/src/FileData.h b/es-app/src/FileData.h index 425f739f16..4bfaf64df6 100644 --- a/es-app/src/FileData.h +++ b/es-app/src/FileData.h @@ -128,6 +128,7 @@ class FolderData : public FileData public: FolderData(const std::string& startpath, SystemData* system, bool ownsChildrens=true) : FileData(FOLDER, startpath, system) { + mIsDisplayableAsVirtualFolder = false; mOwnsChildrens = ownsChildrens; } @@ -142,7 +143,9 @@ class FolderData : public FileData mChildren.clear(); } - inline bool isVirtualFolder() { return !mOwnsChildrens; } + inline bool isVirtualFolderDisplay() { return mIsDisplayableAsVirtualFolder && !mOwnsChildrens; } + void enableVirtualFolderDisplay(bool value) { mIsDisplayableAsVirtualFolder = value; }; + bool isVirtualFolderDisplayEnabled() { return mIsDisplayableAsVirtualFolder; }; FileData* FindByPath(const std::string& path); @@ -159,9 +162,10 @@ class FolderData : public FileData private: std::vector getFlatGameList(bool displayedOnly, SystemData* system) const; - std::vector mChildren; + bool mOwnsChildrens; + bool mIsDisplayableAsVirtualFolder; }; #endif // ES_APP_FILE_DATA_H diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 680029af30..da393f1b43 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -370,7 +370,12 @@ void SystemData::createGroupedSystems() { SystemData* sys = *it; if (!sys->isCollection() && !sys->getSystemEnvData()->mGroup.empty()) + { + if (Settings::getInstance()->getBool(sys->getSystemEnvData()->mGroup + ".ungroup")) + continue; + map[sys->getSystemEnvData()->mGroup].push_back(sys); + } } for (auto item : map) @@ -402,6 +407,8 @@ void SystemData::createGroupedSystems() std::string path = logoElem->get("path"); folder->setMetadata("image", path); folder->setMetadata("thumbnail", path); + + folder->enableVirtualFolderDisplay(true); } } @@ -855,6 +862,40 @@ void SystemData::deleteIndex() } } +bool SystemData::isGroupChildSystem() +{ + if (mEnvData != nullptr && !mEnvData->mGroup.empty()) + return !Settings::getInstance()->getBool(mEnvData->mGroup + ".ungroup"); + + return false; +} + +std::unordered_set SystemData::getAllGroupNames() +{ + std::unordered_set names; + + for (auto sys : SystemData::sSystemVector) + { + if (sys->isGroupSystem()) + names.insert(sys->getName()); + else if (sys->mEnvData != nullptr && !sys->mEnvData->mGroup.empty()) + names.insert(sys->mEnvData->mGroup); + } + + return names; +} + +std::unordered_set SystemData::getGroupChildSystemNames(const std::string groupName) +{ + std::unordered_set names; + + for (auto sys : SystemData::sSystemVector) + if (sys->mEnvData != nullptr && sys->mEnvData->mGroup == groupName) + names.insert(sys->getFullName()); + + return names; +} + SystemData* SystemData::getParentGroupSystem() { if (!isGroupChildSystem() || isGroupSystem()) diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 48db1d4fcb..888a2669bf 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -145,7 +145,7 @@ class SystemData inline bool isGameSystem() { return mIsGameSystem; }; inline bool isGroupSystem() { return mIsGroupSystem; }; - inline bool isGroupChildSystem() { return mEnvData != nullptr && !mEnvData->mGroup.empty(); } + inline bool isGroupChildSystem(); bool isVisible(); @@ -189,6 +189,9 @@ class SystemData SystemData* getParentGroupSystem(); + static std::unordered_set getAllGroupNames(); + static std::unordered_set getGroupChildSystemNames(const std::string groupName); + private: static SystemData* loadSystem(pugi::xml_node system); static void createGroupedSystems(); diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.cpp b/es-app/src/guis/GuiCollectionSystemsOptions.cpp index 30fabc0dd1..a24a2b6b38 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.cpp +++ b/es-app/src/guis/GuiCollectionSystemsOptions.cpp @@ -1,40 +1,70 @@ +#include #include "guis/GuiCollectionSystemsOptions.h" #include "components/OptionListComponent.h" #include "components/SwitchComponent.h" #include "guis/GuiSettings.h" #include "guis/GuiTextEditPopupKeyboard.h" +#include "guis/GuiTextEditPopup.h" #include "utils/StringUtil.h" #include "views/ViewController.h" #include "CollectionSystemManager.h" #include "Window.h" -#include "SystemData.h" -GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) : GuiComponent(window), mMenu(window, _("GAME COLLECTION SETTINGS")) +GuiCollectionSystemsOptions::GuiCollectionSystemsOptions(Window* window) + : GuiSettings(window, _("GAME COLLECTION SETTINGS").c_str()) { initializeMenu(); } void GuiCollectionSystemsOptions::initializeMenu() { - addChild(&mMenu); + auto groupNames = SystemData::getAllGroupNames(); + if (groupNames.size() > 0) + { + auto ungroupedSystems = std::make_shared>(mWindow, _("GROUPED SYSTEMS"), true); + for (auto groupName : groupNames) + { + std::string description; + for (auto zz : SystemData::getGroupChildSystemNames(groupName)) + { + if (!description.empty()) + description += ", "; - // get collections + description += zz; + } + + ungroupedSystems->addEx(groupName, description, groupName, !Settings::getInstance()->getBool(groupName + ".ungroup")); + } + + addWithLabel(_("GROUPED SYSTEMS"), ungroupedSystems); + addSaveFunc([this, ungroupedSystems, groupNames] + { + std::vector checkedItems = ungroupedSystems->getSelectedObjects(); + for (auto groupName : groupNames) + { + bool isGroupActive = std::find(checkedItems.cbegin(), checkedItems.cend(), groupName) != checkedItems.cend(); + if (Settings::getInstance()->setBool(groupName + ".ungroup", !isGroupActive)) + setVariable("reloadSystems", true); + } + }); + } + + // get collections addSystemsToMenu(); // add "Create New Custom Collection from Theme" - std::vector unusedFolders = CollectionSystemManager::get()->getUnusedSystemsFromTheme(); if (unusedFolders.size() > 0) { - addEntry(_("CREATE NEW CUSTOM COLLECTION FROM THEME"), ThemeData::getMenuTheme()->Text.color, true, - [this, unusedFolders] { - auto s = new GuiSettings(mWindow, _("SELECT THEME FOLDER")); - std::shared_ptr< OptionListComponent > folderThemes = std::make_shared< OptionListComponent >(mWindow, "SELECT THEME FOLDER", true); + addEntry(_("CREATE NEW CUSTOM COLLECTION FROM THEME").c_str(), true, + [this, unusedFolders] { + auto s = new GuiSettings(mWindow, _("SELECT THEME FOLDER").c_str()); + std::shared_ptr< OptionListComponent > folderThemes = std::make_shared< OptionListComponent >(mWindow, _("SELECT THEME FOLDER"), true); // add Custom Systems - for(auto it = unusedFolders.cbegin() ; it != unusedFolders.cend() ; it++ ) + for (auto it = unusedFolders.cbegin(); it != unusedFolders.cend(); it++) { ComponentListRow row; std::string name = *it; @@ -44,18 +74,14 @@ void GuiCollectionSystemsOptions::initializeMenu() }; row.makeAcceptInputHandler(createCollectionCall); - auto themeFolder = std::make_shared(mWindow, Utils::String::toUpper(name), ThemeData::getMenuTheme()->TextSmall.font, ThemeData::getMenuTheme()->TextSmall.color); + auto themeFolder = std::make_shared(mWindow, Utils::String::toUpper(name), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color); row.addElement(themeFolder, true); s->addRow(row); } - - s->updatePosition(); mWindow->pushGui(s); }); } - ComponentListRow row; - row.addElement(std::make_shared(mWindow, _("CREATE NEW CUSTOM COLLECTION"), ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); auto createCustomCollection = [this](const std::string& newVal) { std::string name = newVal; // we need to store the first Gui and remove it, as it'll be deleted by the actual Gui @@ -64,78 +90,123 @@ void GuiCollectionSystemsOptions::initializeMenu() window->removeGui(topGui); createCollection(name); }; - row.makeAcceptInputHandler([this, createCustomCollection] { - mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, _("New Collection Name"), "", createCustomCollection, false)); + addEntry(_("CREATE NEW CUSTOM COLLECTION").c_str(), true, [this, createCustomCollection] { + if (Settings::getInstance()->getBool("UseOSK")) { + mWindow->pushGui(new GuiTextEditPopupKeyboard(mWindow, _("New Collection Name"), "", createCustomCollection, false)); + } + else { + mWindow->pushGui(new GuiTextEditPopup(mWindow, _("New Collection Name"), "", createCustomCollection, false)); + } }); - mMenu.addRow(row); - - bundleCustomCollections = std::make_shared(mWindow); + std::shared_ptr bundleCustomCollections = std::make_shared(mWindow); bundleCustomCollections->setState(Settings::getInstance()->getBool("UseCustomCollectionsSystem")); - mMenu.addWithLabel(_("GROUP UNTHEMED CUSTOM COLLECTIONS"), bundleCustomCollections); + addWithLabel(_("GROUP UNTHEMED CUSTOM COLLECTIONS"), bundleCustomCollections); + addSaveFunc([this, bundleCustomCollections] + { + if (Settings::getInstance()->setBool("UseCustomCollectionsSystem", bundleCustomCollections->getState())) + setVariable("reloadAll", true); + }); - sortAllSystemsSwitch = std::make_shared(mWindow); + std::shared_ptr sortAllSystemsSwitch = std::make_shared(mWindow); sortAllSystemsSwitch->setState(Settings::getInstance()->getBool("SortAllSystems")); - mMenu.addWithLabel(_("SORT CUSTOM COLLECTIONS AND SYSTEMS"), sortAllSystemsSwitch); - - toggleSystemNameInCollections = std::make_shared(mWindow); - toggleSystemNameInCollections->setState(Settings::getInstance()->getBool("CollectionShowSystemInfo")); - mMenu.addWithLabel(_("SHOW SYSTEM NAME IN COLLECTIONS"), toggleSystemNameInCollections); + addWithLabel(_("SORT CUSTOM COLLECTIONS AND SYSTEMS"), sortAllSystemsSwitch); + addSaveFunc([this, sortAllSystemsSwitch] + { + if (Settings::getInstance()->setBool("SortAllSystems", sortAllSystemsSwitch->getState())) + setVariable("reloadAll", true); + }); - if(CollectionSystemManager::get()->isEditing()) + std::shared_ptr toggleSystemNameInCollections = std::make_shared(mWindow); + toggleSystemNameInCollections->setState(Settings::getInstance()->getBool("CollectionShowSystemInfo")); + addWithLabel(_("SHOW SYSTEM NAME IN COLLECTIONS"), toggleSystemNameInCollections); + addSaveFunc([this, toggleSystemNameInCollections] { - row.elements.clear(); - row.addElement(std::make_shared(mWindow, _("FINISH EDITING COLLECTION")+" '" + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection()) + "'", ThemeData::getMenuTheme()->Text.font, ThemeData::getMenuTheme()->Text.color), true); - row.makeAcceptInputHandler(std::bind(&GuiCollectionSystemsOptions::exitEditMode, this)); - mMenu.addRow(row); - } + if (Settings::getInstance()->setBool("CollectionShowSystemInfo", toggleSystemNameInCollections->getState())) + setVariable("reloadAll", true); + }); - mMenu.addButton(_("BACK"), _("BACK"), std::bind(&GuiCollectionSystemsOptions::applySettings, this)); - mMenu.setPosition((Renderer::getScreenWidth() - mMenu.getSize().x()) / 2, (Renderer::getScreenHeight() - mMenu.getSize().y()) / 2); -} -void GuiCollectionSystemsOptions::addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func) -{ - auto theme = ThemeData::getMenuTheme(); - std::shared_ptr font = theme->Text.font; // Font::get(FONT_SIZE_MEDIUM); + if (CollectionSystemManager::get()->isEditing()) + addEntry((_("FINISH EDITING COLLECTION") + " : " + Utils::String::toUpper(CollectionSystemManager::get()->getEditingCollection())).c_str(), false, std::bind(&GuiCollectionSystemsOptions::exitEditMode, this)); - // populate the list - ComponentListRow row; - row.addElement(std::make_shared(mWindow, name, font, theme->Text.color), true); + addSaveFunc([this] + { + std::string newAutoSettings = Utils::String::vectorToCommaString(autoOptionList->getSelectedObjects()); + std::string newCustomSettings = Utils::String::vectorToCommaString(customOptionList->getSelectedObjects()); - if(add_arrow) + bool dirty = Settings::getInstance()->setString("CollectionSystemsAuto", newAutoSettings); + dirty |= Settings::getInstance()->setString("CollectionSystemsCustom", newCustomSettings); + + if (dirty) + setVariable("reloadAll", true); + }); + + onFinalize([this] { - std::shared_ptr bracket = makeArrow(mWindow); - row.addElement(bracket, false); - } + if (getVariable("reloadSystems")) + { + Window* window = mWindow; + window->renderLoadingScreen(_("Loading...")); + + ViewController::get()->goToStart(); + delete ViewController::get(); + ViewController::init(window); + CollectionSystemManager::deinit(); + CollectionSystemManager::init(window); + SystemData::loadConfig(window); + + GuiComponent* gui; + while ((gui = window->peekGui()) != NULL) + { + window->removeGui(gui); + if (gui != this) + delete gui; + } + ViewController::get()->reloadAll(nullptr); // Avoid reloading themes a second time + window->endRenderLoadingScreen(); - row.makeAcceptInputHandler(func); + window->pushGui(ViewController::get()); + } + else if (getVariable("reloadAll")) + { + Settings::getInstance()->saveFile(); - mMenu.addRow(row); + CollectionSystemManager::get()->loadEnabledListFromSettings(); + CollectionSystemManager::get()->updateSystemsList(); + ViewController::get()->goToStart(); + ViewController::get()->reloadAll(mWindow); + mWindow->endRenderLoadingScreen(); + } + }); } -void GuiCollectionSystemsOptions::createCollection(std::string inName) { +void GuiCollectionSystemsOptions::createCollection(std::string inName) +{ std::string name = CollectionSystemManager::get()->getValidNewCollectionName(inName); SystemData* newSys = CollectionSystemManager::get()->addNewCustomCollection(name); customOptionList->add(name, name, true); + std::string outAuto = Utils::String::vectorToCommaString(autoOptionList->getSelectedObjects()); std::string outCustom = Utils::String::vectorToCommaString(customOptionList->getSelectedObjects()); updateSettings(outAuto, outCustom); + ViewController::get()->goToSystemView(newSys); Window* window = mWindow; CollectionSystemManager::get()->setEditMode(name); - while(window->peekGui() && window->peekGui() != ViewController::get()) + while (window->peekGui() && window->peekGui() != ViewController::get()) delete window->peekGui(); + return; } void GuiCollectionSystemsOptions::exitEditMode() { CollectionSystemManager::get()->exitEditMode(); - applySettings(); + close(); } GuiCollectionSystemsOptions::~GuiCollectionSystemsOptions() @@ -178,35 +249,24 @@ void GuiCollectionSystemsOptions::addSystemsToMenu() autoOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); } } - mMenu.addWithLabel(_("AUTOMATIC GAME COLLECTIONS"), autoOptionList); + addWithLabel(_("AUTOMATIC GAME COLLECTIONS"), autoOptionList); std::map customSystems = CollectionSystemManager::get()->getCustomCollectionSystems(); customOptionList = std::make_shared< OptionListComponent >(mWindow, _("SELECT COLLECTIONS"), true); // add Custom Systems - for(std::map::const_iterator it = customSystems.cbegin() ; it != customSystems.cend() ; it++ ) + for (std::map::const_iterator it = customSystems.cbegin(); it != customSystems.cend(); it++) { customOptionList->add(it->second.decl.longName, it->second.decl.name, it->second.isEnabled); } - mMenu.addWithLabel(_("CUSTOM GAME COLLECTIONS"), customOptionList); -} - -void GuiCollectionSystemsOptions::applySettings() -{ - std::string newAutoSettings = Utils::String::vectorToCommaString(autoOptionList->getSelectedObjects()); - std::string newCustomSettings = Utils::String::vectorToCommaString(customOptionList->getSelectedObjects()); - updateSettings(newAutoSettings, newCustomSettings); - delete this; + addWithLabel(_("CUSTOM GAME COLLECTIONS"), customOptionList); } void GuiCollectionSystemsOptions::updateSettings(std::string newAutoSettings, std::string newCustomSettings) { bool dirty = Settings::getInstance()->setString("CollectionSystemsAuto", newAutoSettings); dirty |= Settings::getInstance()->setString("CollectionSystemsCustom", newCustomSettings); - dirty |= Settings::getInstance()->setBool("SortAllSystems", sortAllSystemsSwitch->getState()); - dirty |= Settings::getInstance()->setBool("UseCustomCollectionsSystem", bundleCustomCollections->getState()); - dirty |= Settings::getInstance()->setBool("CollectionShowSystemInfo", toggleSystemNameInCollections->getState()); if (dirty) { @@ -216,26 +276,4 @@ void GuiCollectionSystemsOptions::updateSettings(std::string newAutoSettings, st ViewController::get()->goToStart(); ViewController::get()->reloadAll(); } -} - -bool GuiCollectionSystemsOptions::input(InputConfig* config, Input input) -{ - bool consumed = GuiComponent::input(config, input); - if(consumed) - return true; - - if(config->isMappedTo("b", input) && input.value != 0) - { - applySettings(); - } - - - return false; -} - -std::vector GuiCollectionSystemsOptions::getHelpPrompts() -{ - std::vector prompts = mMenu.getHelpPrompts(); - prompts.push_back(HelpPrompt("b", _("BACK"))); - return prompts; -} +} \ No newline at end of file diff --git a/es-app/src/guis/GuiCollectionSystemsOptions.h b/es-app/src/guis/GuiCollectionSystemsOptions.h index 6f789ce4fe..42a8b23ecf 100644 --- a/es-app/src/guis/GuiCollectionSystemsOptions.h +++ b/es-app/src/guis/GuiCollectionSystemsOptions.h @@ -3,37 +3,29 @@ #define ES_APP_GUIS_GUI_COLLECTION_SYSTEM_OPTIONS_H #include "components/MenuComponent.h" +#include "GuiSettings.h" template class OptionListComponent; class SwitchComponent; class SystemData; -class GuiCollectionSystemsOptions : public GuiComponent +class GuiCollectionSystemsOptions : public GuiSettings { public: GuiCollectionSystemsOptions(Window* window); ~GuiCollectionSystemsOptions(); - bool input(InputConfig* config, Input input) override; - - virtual std::vector getHelpPrompts() override; private: void initializeMenu(); - void applySettings(); void addSystemsToMenu(); - void addEntry(std::string name, unsigned int color, bool add_arrow, const std::function& func); + void updateSettings(std::string newAutoSettings, std::string newCustomSettings); void createCollection(std::string inName); void exitEditMode(); - std::shared_ptr< OptionListComponent > autoOptionList; std::shared_ptr< OptionListComponent > customOptionList; - - std::shared_ptr sortAllSystemsSwitch; - std::shared_ptr bundleCustomCollections; - std::shared_ptr toggleSystemNameInCollections; - MenuComponent mMenu; + SystemData* mSystem; }; diff --git a/es-app/src/guis/GuiThemeInstall.cpp b/es-app/src/guis/GuiThemeInstall.cpp index b3d63d89f6..1b2933c2ea 100644 --- a/es-app/src/guis/GuiThemeInstall.cpp +++ b/es-app/src/guis/GuiThemeInstall.cpp @@ -6,6 +6,7 @@ #include "views/ViewController.h" #include "utils/StringUtil.h" #include "components/ComponentGrid.h" +#include "components/MultiLineMenuEntry.h" #include "EsLocale.h" #include "ContentInstaller.h" diff --git a/es-app/src/guis/GuiThemeInstall.h b/es-app/src/guis/GuiThemeInstall.h index 0b0ec1064b..0f4041d0b0 100644 --- a/es-app/src/guis/GuiThemeInstall.h +++ b/es-app/src/guis/GuiThemeInstall.h @@ -23,34 +23,3 @@ class GuiThemeInstall : public GuiComponent MenuComponent mMenu; }; - - -class MultiLineMenuEntry : public ComponentGrid -{ -public: - MultiLineMenuEntry(Window* window, const std::string& text, const std::string& substring) : - ComponentGrid(window, Vector2i(1, 2)) - { - auto theme = ThemeData::getMenuTheme(); - - mText = std::make_shared(mWindow, text.c_str(), theme->Text.font, theme->Text.color); - mText->setVerticalAlignment(ALIGN_TOP); - - mSubstring = std::make_shared(mWindow, substring.c_str(), theme->TextSmall.font, theme->Text.color); - mSubstring->setOpacity(192); - - setEntry(mText, Vector2i(0, 0), true, true); - setEntry(mSubstring, Vector2i(0, 1), false, true); - - setSize(Vector2f(0, mText->getSize().y() + mSubstring->getSize().y())); - } - - virtual void setColor(unsigned int color) - { - mText->setColor(color); - mSubstring->setColor(color); - } - - std::shared_ptr mText; - std::shared_ptr mSubstring; -}; diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 569421363b..75616bdd38 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -232,7 +232,7 @@ const std::string GridGameListView::getImagePath(FileData* file) const bool GridGameListView::isVirtualFolder(FileData* file) { - return file->getType() == FOLDER && ((FolderData*)file)->isVirtualFolder(); + return file->getType() == FOLDER && ((FolderData*)file)->isVirtualFolderDisplay(); } void GridGameListView::populateList(const std::vector& files) @@ -247,6 +247,7 @@ void GridGameListView::populateList(const std::vector& files) auto top = mCursorStack.top(); std::string imagePath; + bool displayAsVirtualFolder = true; // Find logo image from original system if (mCursorStack.size() == 1 && top->getSystem()->isGroupChildSystem()) @@ -254,18 +255,33 @@ void GridGameListView::populateList(const std::vector& files) std::string startPath = top->getSystem()->getStartPath(); auto parent = top->getSystem()->getParentGroupSystem(); - for (auto child : parent->getRootFolder()->getChildren()) + + auto theme = parent->getTheme(); + if (theme) + { + const ThemeData::ThemeElement* logoElem = theme->getElement("system", "logo", "image"); + if (logoElem && logoElem->has("path")) + imagePath = logoElem->get("path"); + } + + if (imagePath.empty()) { - if (child->getPath() == startPath) + for (auto child : parent->getRootFolder()->getChildren()) { - imagePath = child->getMetadata("image"); - break; + if (child->getPath() == startPath) + { + if (child->getType() == FOLDER) + displayAsVirtualFolder = ((FolderData*)child)->isVirtualFolderDisplayEnabled(); + + imagePath = child->getMetadata("image"); + break; + } } } } FileData* placeholder = new FileData(PLACEHOLDER, "..", this->mRoot->getSystem()); - mGrid.add(". .", imagePath, "", "", false, true, !imagePath.empty(), placeholder); + mGrid.add(". .", imagePath, "", "", false, true, displayAsVirtualFolder && !imagePath.empty(), placeholder); } std::string systemName = mRoot->getSystem()->getFullName(); diff --git a/es-core/CMakeLists.txt b/es-core/CMakeLists.txt index 8aafb040ac..0a4a90740f 100644 --- a/es-core/CMakeLists.txt +++ b/es-core/CMakeLists.txt @@ -50,6 +50,7 @@ set(CORE_HEADERS ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoPlayerComponent.h ${CMAKE_CURRENT_SOURCE_DIR}/src/components/VideoVlcComponent.h + ${CMAKE_CURRENT_SOURCE_DIR}/src/components/MultiLineMenuEntry.h # Guis ${CMAKE_CURRENT_SOURCE_DIR}/src/guis/GuiDetectDevice.h diff --git a/es-core/src/AudioManager.cpp b/es-core/src/AudioManager.cpp index 307564e696..d1eccaa081 100644 --- a/es-core/src/AudioManager.cpp +++ b/es-core/src/AudioManager.cpp @@ -289,6 +289,9 @@ void AudioManager::setVideoPlaying(bool state) if (sInstance == nullptr || !sInstance->mInitialized || !Settings::getInstance()->getBool("audio.bgmusic")) return; + if (state && !Settings::getInstance()->getBool("VideoLowersMusic")) + return; + sInstance->mVideoPlaying = state; } diff --git a/es-core/src/components/MultiLineMenuEntry.h b/es-core/src/components/MultiLineMenuEntry.h new file mode 100644 index 0000000000..476a06dc46 --- /dev/null +++ b/es-core/src/components/MultiLineMenuEntry.h @@ -0,0 +1,39 @@ + +#pragma once + +#include "Window.h" +#include "components/TextComponent.h" +#include "components/ComponentGrid.h" +#include "math/Vector2i.h" +#include "math/Vector2f.h" +#include "ThemeData.h" + +class MultiLineMenuEntry : public ComponentGrid +{ +public: + MultiLineMenuEntry(Window* window, const std::string& text, const std::string& substring) : + ComponentGrid(window, Vector2i(1, 2)) + { + auto theme = ThemeData::getMenuTheme(); + + mText = std::make_shared(mWindow, text.c_str(), theme->Text.font, theme->Text.color); + mText->setVerticalAlignment(ALIGN_TOP); + + mSubstring = std::make_shared(mWindow, substring.c_str(), theme->TextSmall.font, theme->Text.color); + mSubstring->setOpacity(192); + + setEntry(mText, Vector2i(0, 0), true, true); + setEntry(mSubstring, Vector2i(0, 1), false, true); + + setSize(Vector2f(0, mText->getSize().y() + mSubstring->getSize().y())); + } + + virtual void setColor(unsigned int color) + { + mText->setColor(color); + mSubstring->setColor(color); + } + + std::shared_ptr mText; + std::shared_ptr mSubstring; +}; diff --git a/es-core/src/components/OptionListComponent.h b/es-core/src/components/OptionListComponent.h index ae8403783a..475b46c993 100644 --- a/es-core/src/components/OptionListComponent.h +++ b/es-core/src/components/OptionListComponent.h @@ -6,6 +6,8 @@ #include "Log.h" #include "Window.h" #include "EsLocale.h" +#include "ThemeData.h" +#include "components/MultiLineMenuEntry.h" //Used to display a list of options. //Can select one or multiple options. @@ -26,6 +28,8 @@ class OptionListComponent : public GuiComponent struct OptionListData { std::string name; + std::string description; + T object; bool selected; @@ -83,7 +87,10 @@ class OptionListComponent : public GuiComponent } else { - row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, color), true); + if (!it->description.empty()) + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), it->description), true); + else + row.addElement(std::make_shared(mWindow, Utils::String::toUpper(it->name), font, color), true); if (mParent->mMultiSelect) { @@ -297,6 +304,25 @@ class OptionListComponent : public GuiComponent return selected.at(0); } + void addEx(const std::string name, const std::string description, const T& obj, bool selected) + { + for (auto sysIt = mEntries.cbegin(); sysIt != mEntries.cend(); sysIt++) + if (sysIt->name == name) + return; + + OptionListData e; + e.name = name; + e.description = description; + e.object = obj; + e.selected = selected; + + e.group = mGroup; + mGroup = ""; + + mEntries.push_back(e); + onSelectedChanged(); + } + void add(const std::string& name, const T& obj, bool selected) { for (auto sysIt = mEntries.cbegin(); sysIt != mEntries.cend(); sysIt++) diff --git a/es-core/src/utils/FileSystemUtil.cpp b/es-core/src/utils/FileSystemUtil.cpp index 7a34b421d2..36eb356b53 100644 --- a/es-core/src/utils/FileSystemUtil.cpp +++ b/es-core/src/utils/FileSystemUtil.cpp @@ -681,6 +681,9 @@ namespace Utils if(!_path.length()) return _path; + if (_path.length() == 1 && _path[0] == '.') + return getGenericPath(_relativeTo); + // replace '.' with relativeTo if((_path[0] == '.') && (_path[1] == '/' || _path[1] == '\\')) return getGenericPath(_relativeTo + &(_path[1])); diff --git a/resources/checkbox_checked.svg b/resources/checkbox_checked.svg index e61e9f234d..fd39d10d67 100644 --- a/resources/checkbox_checked.svg +++ b/resources/checkbox_checked.svg @@ -1 +1,61 @@ - \ No newline at end of file + + + + + + image/svg+xml + + + + + + + + + + diff --git a/resources/locale/fr/emulationstation2.po b/resources/locale/fr/emulationstation2.po index ff5cf53a75..c3c8c4f0a2 100644 --- a/resources/locale/fr/emulationstation2.po +++ b/resources/locale/fr/emulationstation2.po @@ -82,6 +82,10 @@ msgstr "Tous les jeux" msgid "DOWNLOADING" msgstr "TÉLÉCHARGEMENT" +#: +msgid "GROUPED SYSTEMS" +msgstr "SYSTÈMES À FUSIONNER" + #: msgid "THEME INSTALLED SUCCESSFULLY" msgstr "THEME INSTALLÉ" From c22afd92ae8debc434d753f57b5f9f77c2e9e033 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 23 Feb 2020 10:34:28 +0100 Subject: [PATCH 328/336] Fix : Last played collection not working correctly... --- es-app/src/CollectionSystemManager.cpp | 22 ++++++++++++++++++++++ es-app/src/CollectionSystemManager.h | 1 + 2 files changed, 23 insertions(+) diff --git a/es-app/src/CollectionSystemManager.cpp b/es-app/src/CollectionSystemManager.cpp index 818f10284c..de71f138a9 100644 --- a/es-app/src/CollectionSystemManager.cpp +++ b/es-app/src/CollectionSystemManager.cpp @@ -14,6 +14,7 @@ #include #include #include "Gamelist.h" +#include "FileSorts.h" std::string myCollectionsName = "collections"; @@ -333,6 +334,7 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS if (name == "recent") { + sortLastPlayed(curSys); trimCollectionCount(rootFolder, LAST_PLAYED_MAX); ViewController::get()->onFileChanged(rootFolder, FILE_METADATA_CHANGED); } @@ -341,6 +343,22 @@ void CollectionSystemManager::updateCollectionSystem(FileData* file, CollectionS } } +void CollectionSystemManager::sortLastPlayed(SystemData* system) +{ + if (system->getName() != "recent") + return; + + FolderData* rootFolder = system->getRootFolder(); + system->setSortId(FileSorts::LASTPLAYED_DESCENDING); + + const FileSorts::SortType& sort = FileSorts::getSortTypes().at(system->getSortId()); + + std::vector& childs = (std::vector&) rootFolder->getChildren(); + std::sort(childs.begin(), childs.end(), sort.comparisonFunction); + if (!sort.ascending) + std::reverse(childs.begin(), childs.end()); +} + void CollectionSystemManager::trimCollectionCount(FolderData* rootFolder, int limit) { SystemData* curSys = rootFolder->getSystem(); @@ -914,7 +932,11 @@ void CollectionSystemManager::populateAutoCollection(CollectionSystemData* sysDa } } if (sysDecl.type == AUTO_LAST_PLAYED) + { + sortLastPlayed(newSys); trimCollectionCount(rootFolder, LAST_PLAYED_MAX); + } + sysData->isPopulated = true; } diff --git a/es-app/src/CollectionSystemManager.h b/es-app/src/CollectionSystemManager.h index 1e41983ed4..60cf43f045 100644 --- a/es-app/src/CollectionSystemManager.h +++ b/es-app/src/CollectionSystemManager.h @@ -141,6 +141,7 @@ class CollectionSystemManager std::vector getUserCollectionThemeFolders(); void trimCollectionCount(FolderData* rootFolder, int limit); + void sortLastPlayed(SystemData* system); bool themeFolderExists(std::string folder); From b8d79c03dd9e8f8cec809aab6515ea5f9b78809c Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 23 Feb 2020 11:22:03 +0100 Subject: [PATCH 329/336] Fix Last played auto collection : don't apply favorite first setting --- es-app/src/views/gamelist/BasicGameListView.cpp | 6 ++++-- es-app/src/views/gamelist/GridGameListView.cpp | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/es-app/src/views/gamelist/BasicGameListView.cpp b/es-app/src/views/gamelist/BasicGameListView.cpp index 90066788c5..d4db0b54dc 100644 --- a/es-app/src/views/gamelist/BasicGameListView.cpp +++ b/es-app/src/views/gamelist/BasicGameListView.cpp @@ -66,10 +66,12 @@ void BasicGameListView::populateList(const std::vector& files) std::string systemName = mRoot->getSystem()->getFullName(); mHeaderText.setText(systemName); + std::string systemShortName = mRoot->getSystem()->getName(); + bool favoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); - bool showFavoriteIcon = (systemName != "favorites"); + bool showFavoriteIcon = (systemName != "favorites" && systemShortName != "recent"); if (!showFavoriteIcon) - favoritesFirst = false; + favoritesFirst = false; if (files.size() > 0) { diff --git a/es-app/src/views/gamelist/GridGameListView.cpp b/es-app/src/views/gamelist/GridGameListView.cpp index 75616bdd38..2c64885590 100644 --- a/es-app/src/views/gamelist/GridGameListView.cpp +++ b/es-app/src/views/gamelist/GridGameListView.cpp @@ -285,9 +285,10 @@ void GridGameListView::populateList(const std::vector& files) } std::string systemName = mRoot->getSystem()->getFullName(); + std::string systemShortName = mRoot->getSystem()->getName(); bool favoritesFirst = Settings::getInstance()->getBool("FavoritesFirst"); - bool showFavoriteIcon = (systemName != "favorites"); + bool showFavoriteIcon = (systemName != "favorites" && systemShortName != "recent"); if (!showFavoriteIcon) favoritesFirst = false; From e4194aed65cc4e5cafc6c3c9b846fe2326fd89a3 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 23 Feb 2020 12:02:05 +0100 Subject: [PATCH 330/336] Fix linux build --- es-core/src/HttpReq.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-core/src/HttpReq.cpp b/es-core/src/HttpReq.cpp index 377216afb5..a23983cc99 100644 --- a/es-core/src/HttpReq.cpp +++ b/es-core/src/HttpReq.cpp @@ -4,7 +4,7 @@ #include "utils/StringUtil.h" #include "Log.h" #include - +#include #include #ifdef WIN32 From e1ca680d1723207d95b93f4213d852c08dd7224d Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sun, 23 Feb 2020 12:09:46 +0100 Subject: [PATCH 331/336] Fix linux build --- es-app/src/SystemData.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/es-app/src/SystemData.h b/es-app/src/SystemData.h index 888a2669bf..e783e41100 100644 --- a/es-app/src/SystemData.h +++ b/es-app/src/SystemData.h @@ -145,7 +145,7 @@ class SystemData inline bool isGameSystem() { return mIsGameSystem; }; inline bool isGroupSystem() { return mIsGroupSystem; }; - inline bool isGroupChildSystem(); + bool isGroupChildSystem(); bool isVisible(); From 23ba8c57a483338411f487533b14b0198e9f2e10 Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Mon, 24 Feb 2020 00:55:31 +0100 Subject: [PATCH 332/336] Fix crash when START ON SYSTEM system is not visible anymore (grouped systems/collections) --- es-app/src/guis/GuiMenu.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/es-app/src/guis/GuiMenu.cpp b/es-app/src/guis/GuiMenu.cpp index 756a878195..4c30768526 100644 --- a/es-app/src/guis/GuiMenu.cpp +++ b/es-app/src/guis/GuiMenu.cpp @@ -924,6 +924,9 @@ void GuiMenu::openUISettings() if ("retropie" != (*it)->getName() && (*it)->isVisible()) systemfocus_list->add((*it)->getName(), (*it)->getName(), Settings::getInstance()->getString("StartupSystem") == (*it)->getName()); + if (!systemfocus_list->hasSelection()) + systemfocus_list->selectFirstItem(); + s->addWithLabel(_("START ON SYSTEM"), systemfocus_list); s->addSaveFunc([systemfocus_list] { Settings::getInstance()->setString("StartupSystem", systemfocus_list->getSelected()); From a98438944a9fc18639dd5cedfbb103f2781f150b Mon Sep 17 00:00:00 2001 From: Fabrice CARUSO Date: Sat, 29 Feb 2020 11:04:34 +0100 Subject: [PATCH 333/336] Fix : Better korean / japanese / chinese fonts --- es-core/src/resources/Font.cpp | 12 ++++-------- resources/DroidSansFallbackFull.ttf | Bin 0 -> 4033420 bytes resources/NanumMyeongjo.ttf | Bin 0 -> 3808496 bytes resources/locale/fr/emulationstation2.po | 4 ++++ 4 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 resources/DroidSansFallbackFull.ttf create mode 100644 resources/NanumMyeongjo.ttf diff --git a/es-core/src/resources/Font.cpp b/es-core/src/resources/Font.cpp index f2275b2729..270a708cf8 100644 --- a/es-core/src/resources/Font.cpp +++ b/es-core/src/resources/Font.cpp @@ -257,10 +257,10 @@ std::vector getFallbackFontPaths() fontDir += "\\Fonts\\"; const char* fontNames[] = { - "glyphs.ttf", // latin + ":/glyphs.ttf", // latin + ":/DroidSansFallbackFull.ttf",// japanese, chinese, present on Debian + ":/NanumMyeongjo.ttf" // korean font "arial.ttf", // latin - "meiryo.ttc", // japanese - "simhei.ttf" // chinese }; //prepend to font file names @@ -269,11 +269,7 @@ std::vector getFallbackFontPaths() for(unsigned int i = 0; i < sizeof(fontNames) / sizeof(fontNames[0]); i++) { - std::string path = fontDir + fontNames[i]; - - if (i == 0) - path = ":/glyphs.ttf"; - + std::string path = Utils::String::startsWith(fontNames[i], ":/") ? fontNames[i] : fontDir + fontNames[i]; if (ResourceManager::getInstance()->fileExists(path)) fontPaths.push_back(path); } diff --git a/resources/DroidSansFallbackFull.ttf b/resources/DroidSansFallbackFull.ttf new file mode 100644 index 0000000000000000000000000000000000000000..89959f52fd0f487c2cb772fcfd30152546cd44b3 GIT binary patch literal 4033420 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

uxKF=d!o= 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 z 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

qZ+@jz(^acH8FjgU7yXaQ?cWD`H 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

}y1PWoZLG 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­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�^?(Lp%C0Sbj@gSp{n~4< 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 z z&@-TV{e;Qyf2W?2JLjI;UzyhC>uz=4oT%-637M;=b&ZNC&Chnb2=@hUeRRB$hG%XG zAqBXn5V|vTFdYH1{&eUvj+mHqqOGC>l}M<+jkL@lUD`g-?6ND22sdbZl?pTtTQkc? zXOJ~KKJHEO0A4T8T;}W}Im@3ye;9bEvjEj{s!p|P<+*eftM?G&jXz+#5wTaKO)}2d z(kLH#v?t7Zdx0N69-=AW-al~5pYgoUlrN6nJ%Ue>@F`c=9#Ht=U!pinr-iv#RX3j} zSO`G<<+Rd%)QqD>&@|t1=U*2^<59`q9u`Otck{$`gMB zi2*DIF2=>6tSB~(m1(w`sAxx3{;t1VY(BWi8- z^}Ceadzb=&OYcQX_T^>l_bJ%QKOXu+94k>(2QzLVOWD~L$i1->EH3vV%tU|aDnfzo+ zc0Fx@lptA|Wx;*{;=)(&MenS3a8f$xuOvbPxi_I=^4i{Ws@c%menreU6{(|(yI)4S zu04qaTnKW&z#B-eS5Q@l^SgkeNFZLpcWXk9M_T(Tf+94I=p?7f;;VW!0{9gwDAKO$9yfMpiSNH z3AcUy;KGY9zDJsKB}dGR)}?7VZ~5@zqJ46;2;BGb zx&W9XY`dn;qvX$Xu)tR!L4G#$ZPX+X(e{~x9Jjp!j6&gX=Pz@79MT|s>UaW{tQx&5@)Nx zPu_xmB`FjUDt$?mq~2ddh{dQ04ka9tfCL$U5izTQRKUAyEq@Upb#AsGXVcyLQO$1{ zhZKrSpTu%n-hPW~@QccXfFA6CeA1kCTS-@|4nnh@)8IcfJZz6OLnKVP%d(3~7c>py zZDR=n8%?oN#!+n)}Y46{vvkir;Q)+=KH zY!j#l`Yh##LXNO`f~HzOa!$KbnxpVz2M&af%_Coc8lv(usO<1`|4i8`{ilgyVLSs{ zq3eXsL4$$t?l;tmix|ncIfJ#m8!&P|#o9(eWkURA&53D+wyhG$&q#t|{qP%h4F=L| z-S4{-P7q}7y7H)!_1mN?PD}vC&M@?;tgacqNzNtbMrY1FfO}^aPug+@OJ(ORmw*GV zVkKUMpo>~2kPDf7=ghvtuEO`*&z6byy3aq?Tk{W2`ARx>=+2QD5!(Q#IMO1ypo+$b zBZ)bu*D;IWBjm^FMY~0q43sLFFwwfvY^bP(6Vq8B5PPe#Y!|auv3~GiH$+<(NEULx z+z6h&PH)DxU=Oe`bn-HcX4(^@U}dv;0O=uf40xrIAui+We*5v`R=R1IPM;hztE0yz zg-L;I`y}GtB8m@4T9Ey>eaQ#QeG|HEzh9I%p&E5H}qa}nfp9wVZYBn|zx7Pl{m zBr&wU^z@()U&M`AH+a4=BI-FD>DqKs5`jH#b2X%%y8A_YI*Ys!cpi}11r3x-?|0pb zns5($9zgyNzs%W&P;gDpN9%FJadoYr`>tmUpUW4m#S4mZp=M7}c<(f%$Uj2z5hQ0H zU=_M&WbBbCh1HtIR)W2@FA;_ZY9yqVcwiYjc+5F_wu{C(IyHFm0|38^{U*Ag!83J-PZ0#G`}Pb9{VUcK;cXMv+WWzKQQV*enarTpu^wz+ zXl20egN4Cfg1|7AZRZCVVCyskoaEfx#QkG4Z*fjInf?0_<}^+?2w0q%0*DVPvGWe9 z__9vxt4R7rb-3C9x=lH62%#8?LRMD8M#D~^`C_I!o37 z>+@sF+~PQN!13HSpbJ9f$LUHSFzFIHy=Rcyu1aT+of2>W$^_)Kn(&eU_3?dd^G%?@ zSD*#dV^m^>j2W#b-D98Q1;vPHePaq}ksFv^>ccYSrp++V&Sj zb$GZsn@*<}mzKVpwym07H#byu!?fK)lgjkcP1d9}O{g|b&2?`n5RWrU`Rm|P@=wqj zfhV_PqP#^%XFDb#y|< z8>U7J{emr6C{Jiz9!bM*pE`8NZ8pEf!xe3pY`YX_*&}dFIbF6hy%sk<&hV|VFqc{G$e+dt0OM?#2KyA9g!G+6_|X(!JNy63bPLYy=pSGr zH79fcJ-T`0jqdbxb;6sPTKR?LiLc4&>1uU4L`90w=?L2VPP+g^OT_+cGFZnT;a`Cd z^WW_)o;&WG+Fu$~#;Q(fTpKOz{zV1@bUS%;)bR@ZX9}7_jEo$Xx1p{6kQ2 zbcFUJ7*QJp>?v_2UBr=e_2mVI!sLbs?j-h%tv5EZkx)4&DfiM(D#sMr<|iNN6jp>ZDD|0=kP%(^}x zR2;`9io+3ra};^aG)nT1(>g_7ICyQ?@sF|5`0kxW+sm1Z(ELDMOi$*8PV<4HY9v#3 zQs4t55~A~ITh6ogG{0vE)I8b_f?(!&RYGogUgRaUJ*m4=ZNi?gut|g;dMl38KG*lj zV!PC*1A&ZYqXcfN>1jvS^|V^p39xwB%=)7zPS{$*DcyW3>syuX4g9n?HZG1sw4aID zpYX~iGb+sgk!8(z6eBH>j@#R3A@_J0z>0j6 zZ1%O|HddDOvY%nwA}>Lt2W>HVOy+%Q9e}dfGK0#t4*S{Kn|kj2{KSKkJ^9{yosA7$ zDx;}v<*6cn$vxOY+FhZcN_Wz->lW-$+g92us=894I?6kkzy6OwLlLNYAJ=}G zq)jA489(7U4dmbAziZd7d6dAMd-6%~!3k6z{QT17cPvj5L-5>96gSi@(vWCJ) z48%}d$P=hhJqgdefj@(C->(&84fBZ4mMJth@3=wKn6Hdp^ztnXK_Crl7tUF^bn5E zs%_bCdh$vAfv%w|S|)GSJ<+c66S6NA&{}MB^N$w@dMcakS;n-B)IJ<;)wYjPE>|f4 z_tmyM(B>rw;#c76?pq7}{ys4Y#M7rSG5%oEjrg6ezEq}>(=HJO-S_r=4O9-AcCIj) zU3J%8M`>jikXg}iT({<0?mmLX|JE#6$_fOu{X45F2awoA)|X{M_yxhUDE`HAon?B` zcR{iyI~J#qG7^>jlG2L>f#|`r7R!BUK^vf*g7wu4h)iN@lZq(}d{aQei}P63^3h^rZ^qTUpya7*dV1cd0lrOM$T%oP zM~6La{<~1S+P+`b`_OmKZW@w#qym%YDZ!OtjK7pF9feHG!n3+P0kBdm;jq1g|Z40G3 zDq4&F-rWeEKnRO`+c;_#S&^|y*j@P zfwoM}$YC%@)B0@X*bYn%7IYMdKjQzkcO2xWh0jp5RfJ%7M*;d!|6CMe+G?#(Bf}E@QLjgM$1m<);2Dp zLDNv9kztJVyX#-1$}iXia3Vmtz@Jl)iR2DRhQ7mp!yHB}0rZTC@ijP{P)2t>Z!Lv?hdN#{dl;!^RIz6Ur8K-QrL=kkyK4C}0|!sMOVVjEvYq z-b!~SQDSPi;hbiOSrOUf)Uv}se$Jb~@e1f=P}V*-@0!)n%)_~x`(mKAM~tyN9CY|| z3&nUph91G<+I`{zIYpwmW-a+^Kuh=5{3R#@PzNyEenak`AmSC(q!OfSyuc(c$&7*q z7tbkkmf<71TbsZRl}rc-Nk)gZ|cS;CG_-Ks)!*;VhFFs&uQxwE;`ykLp+%XXrjG5*7Imvg2vD8E0>8_jS8nFSu^L)Ira8$>Yim-g6sVfJg;0 zLmhn)K+v`_A-O#5_F^^KZ3hi32iuMA1=ZB{-A)X>^PhhVv-_(sx(8fIC&PO#wa4!p z?R?e!yz71*6MqDo@S~xxfItU-7F81m6at`kyz4fEa1)+saHZ7rboVRnntRTJ@~a+q z*Wx+9iOo4qH0Y=Dzjn@8g6#xe&pU3m)Rxe^-R6r8!E@?d8mQzQG&NXoQV4~Pi!M)l zxKLGmk@ip(`!gQCHX%&DX2Ma7*yR(85%mgdU3*L9#Rk{k6OAPp#=0&zb!QPu+q zti08HY{1nq;6OC#i|v9n(>v60t#MmhQyldB6;Gv_jYUA@yW#0dSuIzNYr%-rI-PXX znvoRHx(j%bHG_PPaw!GRBv4UJ!CF?*c7Uu`2VP(dimcLCu>LwkRhc#sG8m$XO_zcN ztaxTy`tAemguAxRFDO%~_L+>0m?GM7id5|Hg^6bu7TkMA?asvmRSfaieY{o?n}ExS z1R2S}|7k%M?Q;#yd?7r5W*2XB15H{4W%e~2jUXdb2b`KQWe?x)WssnA-A*T&Ht@>O z*VvR9)sjy?ns7i9e(3FEn&)S=iS**Bwq>Qi=2k!bwt8sJ?3vwXwi=j?hLk5tqa;@| z%Z}+2ZJ$uK@#%Is7)=J9iG7DF)(4ZRNGTI;HxF&~QpnegiPn-wkh(eG(&0OC+ElnK zq;ZhoOF1bIp8{nWzB=?}?BlkK2SFsN8t@Mi4KFI!<~*<1@Eimv_>mDQXSTlVOuMV4 zq&m*GC&Y}F^x8e}^8;Y}A0qfbu-JX|j}9uv%LqUJwK_2~Gx6Z~^mJR8`L-h+a5s@U z=k7~+cyk(h$@ic&4AcOq0CdbTuLb}-&-iD~T=c%+T{!c5XqxAa=JI3ifG^$&?_L|F z{9AlMGk7D$xbC<+xLmZRmo~c|0<_yWW4OK}RX{4_ftWM=F{ynEaGP6pQUX$7UNfOy zGS+CIb2j>>>%L$g)H2A~L;Ds$SYdG~TaLUVA>e)(V?guR;)GcDC1(wtKx2u!>UU=0NT@RNmoC_O7(_am`uq{Zn9<6Rw~=D??kNOFx~Wk#5KIN?m@qT-@&OT9_3%n)J5V-k z%C?+ifonmuC6bH};rRR+PLaqD#dvb?y~yvS+(Eh?y8rxGK}9L7dn7lJ&yQxaU%7Nl zmP4TefX&aZVJVYf$43BC75OsQ2eq&@$6R;9b^rL-{`~`Td*FYK+>R`O+!hW)4qr!Z zBju$O4kUG7Om68>sdWZ~RfGKv7+f?o66z7TmGLT?PywMy-KPXLuuKR?PQl0@VhjIm zDAO#&>I*n_n$fw_4_rNR#qcJTQs@`Nz;PtC2viK^+fD{4bh539HZQxLO_+|6z`X_A z7{q`<|EeyrousPpAZD^f1gn4}5*B%+u5d`2K+m2OT5MWLWPf!9n}0Xg5LU_nmvl1- zwhC6dB-w=tVdNeYptT9Ef^w{Uq&X(xS0f}EO6R0(4sweK;OlL8GuWf3n*$|ufVz9X z?&Z+n1^uhA*&2Ki`+?4E+g7U5;gml)UNN)QjeJTD#R`nzk9?T>uRGk!&~#)k8PEJK z%$zKwG8{C6nS+@IhbxmIZ=g_ABxH*4D+AG7mpDhAdCf5+Ir7x@Q|3O)6B>!_Y((1W z_d$o-4m>81x4vIwzIimdgOmeLZ&Q{J%bcB@p}H^5LuV#0Z8!l^mEqmBI7yKlK4Yg4 zW4!8RP-JPPE0P1Xw4LM1;(SGRr=hy%)5$QOECx`Op}<2h---=*3F;}O4TBxF6@QYa z?8;&@)ke=VAKc1y#76U*2ito=r>V8Wl(PB)++9BfM;UnY;84LX^;&;vh+ZmR;4_}> zJ5BS#)Up_YuTQ>7erT7r2PCZzJ%Ej;ut1HzITjw+2gu4;!+?N^(}gK@#Ge4-^r%*G zy-+QCBaga(HX^a&lsn1a1KvamEl-L?^zJkL`Wl}Em|~#2|NqsCB5BUHusGQEh6;zw zYt)Oy3Lt#F48D7yUfkz}e8#_S=Tq>eQi$$5eZGT61tXL#dgNIUi3qUexQ_x8HConY z>%J-yy*Cbtd%1#Fl!(az6J-Jqrlgc%75UukH1O#-`K7Vsh*O=fndsu>0a^r=@0qII zSzgV1xypTp$s;Q}3B~-UOt0f{e8oXC8U;J3@ED)ywJ%}bqF#GwqSKjvWV+j(dKun8 z%WS#;Q$S|5vj~CtFwVgrAf?`B$v4=?SoCB3vDOry@#O7#^a0JKrsP7RC3N%}U%$`a z<9U1beEm=%<)zH&9T~@)xNxUw-nnmvX;R-~>6WBPQ4c`UIwF}z>qAr)S(yQ?5S2DZ z>hKxeK^`%9q)c{>o(|9~rZ-R2}&as;r5YOB|#c^mC=Jk+GZs_o>s7Fa~Jdm}>V zO$pN+Un@u@tkr{Hj~{_dB^F?Wn;{)wi2{84b&6CxqPgCiXMoYqO+9+bX*EkVIEVqf zF*YVOf*C|)2!Sd68v!bUove7Wl2WQtwSxvPNw?Neob;G53^4I{Re#8|&?u+oHPNXk z^zp;2#e33mw=z3xyYAP%oK1R3<(I$p7rk5UReZ+VKx#Db$g8!*VYJb)>m;HrCROL& z+4Z{Ji3tzk1x1t4H|wG+X^!^J!-o+R-`H@wM^OIlyRsNMs*QEIhfy*~(x~c&sX2^R zxA+%Z^1y^UH)_|764LC_X4xJ_11|p)Q&Z02meXE)&)t({b8`QBNv}YdP#F*DRNGQ( zqYvB~azs;a9e1ZXF5p?*Fmy8+J2VP*<5MFe?!j8RzW1;_F<~Fx)pADm;zOT-()tR- z8!4^iXy5W&Muh%MGZ070G5`fDC(6y+_Cd7a6x(nQxh}<4ZiF`fJaca|otHWHo`i4C zQOn;EI=}Uy(CK0^C`j^IB*6dyo2ZmV>Bkh|*FNBy1tZk83ffEP-k%AhIU{?sZk6kb zfQGL|z z;)bt&0FvehP;|F7kdDkF5}hP;K)qLhDV;M=fo-j#0?q@beA=*NtfT|? z!P^;IV~ueVdfF6=2h`G$&P&DJ@a_gd3E_=dCzzqUOe;&nxS7d1I)CTsn+*D-R6#KSt#F@%cSunJsO_N+tD#ti*D z;OV?Sxzu#48-nCXS!CY-;X}K+dUY}l{Pr8CygX`V^cRn(0ftVM_s4@w^{nw`X*I)jXZnf&V3-(yaGpbYgVCvKG zJ(wV50@0=hq_NYwwXQSWBdQnY2)`LpHd3!X;|B*0treWlJi@^V;5U z+OxfD$&@IGGu*wptvlCw>$Y6wD9e(Ad{N#u%sR%iTUo zj(VBCKR>_z!BPM7UJvBbN(o%9YQ_sXFM`4i#O!S>^K)2cDtO%;{+>!CSHyQO^Ie+w zoVoe@ajZn&!3ftmL56g7Vw z#t&UH31uGwGyImJcbpnqlWZ9+IvEu~!kzeozO-i5A80nTj1P>Z`G&8kpgFf@I1f0_L;Lb)q;D60unA* zbEUdV3*L|m0f=N+0O0O6*MfoQ!~&4|Bn3^M>jnxk3^c-{u5#i@P&b5$YDTO9>6_$G zBO1i--AP3G&BcMFfw1wfEH%IZQqyL`t~ltGyv*tIX5O2Lb-|LeUY1#!XVyAj33Hl_ zcs4L9P~S9=G?sUq70!E8J%4`xCQN}m^;so#^LKc#7sX37m~iTfCdzp78AeGPgQXMk zhS3OP(~I)myjTfSwMBK*5GphJOkF#uHbXZXXJ^%gJ2p&zU=dU|MqsC@p{Cmd>O%~7 zJ;L>;hgysp7e?%5G=^_sK^hnZHN;I18O|LIhfd>;g5dIqdJQZY{i^+rd4G9XKq}X^ zgRGegQ(*+aImwHyMlk;Xn&~&_HvS9Ei*`IauTMe??%>(KsFp_#mMo6C>sRXU#nIb{kKpu^DX)gbGr_s zh8j&2h#-%C61W6jj2=_5CkJlodXOT2W+=#kIeJBXOF=O*xK#BhmkLsg)?6y|jvoDW zQh1rDe*^eo(9mX7jP-cOZ(b5O1rjjFPT-hrFZ5cdj zG_}01v{FcWDYtm<{sx4-dN9vQC-QS8B8a{*anJ%7mDKt?5oN$EPVbZv=02$sBr}`D zxIKDu2xQ$U%V;)1A2~w#*E}_E1fUH1*NB6R%;UJ&#~WqW-#VvBZG^Bj;Rz$5+03oho3^+s39P~!J6#ecB-cPw6I$T$}Ru_S+xNH`p z00d2kyhR^P313zls1uDl_FP5OL7W$q%7Im0ykc5c*E-N<&*JdXmE{3wo_6|8d(Hb= zNo~Cu1Le~|85w(5Js@9>S8{9jme$sSI1@=Lw?h`TL;TM^V8^ak5y7?$sB@u?D}?G{ z9>kkb`$KY`Fgemo+;AN8dE$wPuQoT+=vT+wFbiZY&B6tf$XP*dP&__6c-`k6ci(8mHH8g_IEDnoW&Y1 zMi|Vrvold!(~7{|CIx&cvM2tVws2TlT}Habwi<|H2uc<_gMtRQUSB>MNGNJ^a@j5j zzgs#wyX+h^kI-u)hU#^fTJasmt8{ix6njQkh>&@}(>cYdXv&ocxhSZi}L zcI-k2EAZmPdP>$JJL|r%{tu8X4q?;9qiMVS4 z{{MJ$aWP`Ru3K9xow?)ULJg$6s|J?mr$Z+N*WRA->S=$*gX1T(eKR+4=SU7kUR^=u z<9s(r6ef=To9xSJXnD6a|7E@2Q(G%2RDzWW7&SweyNK}DL2|_m)Y-CPXGPi1%)kw< zAsHZBw+U=VJ`?5?N8opgx{geWkF0%F%;Q>+n|Z}ulB(d}E@X=`FQg*8(x(wg40#D3 zk9qGB;?a0pifQCX47C%d>>|roS~7Sugi>AfSX;n$IN4H!f;3rTLImVhNBtFf1r`@- z$tfp?0xGwJoLo-0&=i}gIG#8S~x^h5cIT*+kwz}}KL>$pfSBRY}PdGe^}R7hx? z^oU5)(V*)1u01A6*$#u&sbi!1*MC&P8+q3!mmqv>v|xs$R1kn z%aT)=7iV&rOMNjL2<4oz6z9^5(CZhlV-)oD6TJ=vPMRI-vaK$rTgVq?``Je$@P@~x z&d|~q>h4zT<~^cNgm-INV4(Whe&6#tZsEn3=1Q!!AEg4`k#bmAe&@@>JnK?5#6uG! zhTsQ79-qZk9_%zg!j|CEhH`x~fYGli!u+F<$YpiyzWWg5&q(`gK;)Tg=`E{jNppHn ztlT!teb)#2VEraBbR-Q#YXwGTBJM|t=lV5^R5|9a%Sje>M+rdLx z{2>_=7!a=5R{MAN%Rb8-30sS>vUMOZS|vA?V}hr}fjZhaTbEarvsfbQ2knupvJq<+ zQ2HD)R$V?G0ecCg&ze_S#4p&nEMx~d?Qxz!xD8!S$(STlHmQfDwb>pE4+QMiXfH0v zp0@l>^|4+m1UH@BL&XVl*Ua`Ga5aFz{|4H*Sa57BO#2yQjWzbGozTh`az(vJMV1bN zvwsGAf^FM!!H$2hj2HwR+gWgj;5C#xcs$~2I zhOXm^Nud3v`-W-YdZD-G`^XI-eD@9SQ9xk?dgtO*@2Kx@Z5=)OTMY)PQaRt-7o9YW zpX(Q%M*@s2sXyfr_ADfi?(3<8j2GE87rcDpa&Qa3{k1477R83{9I=fS3Xnj>5MGUe z)AZ`^#~>@yo;$H|0{?&f5BImEN*}?CEGF>`7V+zlb(5cS@*sA-7hXS+Ms#7wh!tQ$ z$)uorzMc)locsX05Ac5{yOHVYRlPV`IG|SKSAP#62pR2HzWQIYV$*foh{_Bj0QxMW z`AgzAGrIe%GDdr~N_#ZptKb_b)h;}oIJBqm8<*sGdO*8)+sba7NtG8dnV_Q-jC)@hmbcQ z(i3z7Ns^LAxIfhTTheHNlfyi;8ajCxa{dv|E9*_WyXcK){EA+_?P(e2%C$(#2X&$xQXzu9zZhGzkW2Bk96DX4r)AcY-pD#u07_`XY<{kn&GxWUOKzVHGG481&2ESzPkIt^|19L5>UX9GiZ3*O2YMHr zCM-T4j`CkC=RKyUc$eU^6o;c%U7w6Yc>?IPI_v3D3g{BTYN? zM)0ciie$4-O)W0*yHzZ?%S1|0e{aw0^@i_S>YV}vqwAw-1M0KPw1H|zIkg<;8fp~^ z)U0TcA-De>JQv)s;hgOgk(wHKXu%YKsqKJLOGW)0jQQBMvU(O6`x%skB$X8j-Ni)D zV&>b@PqR-(yEFe!1@drah+dmOuA!l0*sqwxxP4o)F9#bjj6$t>?%bh^!DXC1hQm&3 zg_dGr&oY>oH}6^qzQ9xE-sYmtBmP|AC+kjDbJAlcQ-1XM0ou!#3}df&HdKtRF&p>Q zTG%xUb*b1J4tu@97I2YQ6u=?>4z(6ltcnXW-gN^j1qy2` zw->n9YeHmd>&QrZcu*^I!>tD{+T$unpBCQ%rb|YMBoynRF;Ufev>ow5BOaF=S}hz@ zd?9^-STbxDk|=t9hy%=@{zYFZ=gc^-x2RX=L2~uH0uR&(@jR6L6$TPg# z2=hd4M9E95U6x``ZHXPxKiC1N1~BN#;B4rH@~qkx<|HiGC2Z^8AVQ!*fAKaM7=_6W zVn~y3v9q*PAYGc&Zcu3EBOVH|J)e8P#uqHc)0a#{N(6n!OUlkfT zJIcpaZ&?VuuH68zVt1wB&85Ru#O&Tk==+4n`wYhM7r09(P?FU0?&?vV#v5r(puFf4 zjv+tB@4B_A-Di8y39=*xv$(u07yQ)@ia=2w-YO>fuQ7<*Vp~EwP9Wl)@D0J zWBIq@7`Ra2Ma}~s2A}FId%BKb?UQ()Z=mpp2aWj)@0?G{UtqDhzk;$-_(#*9FwG}_ zV0{Jfa{(&C1O?$WNm1RFq3A@@pegQn{r6erMtS=~C<@mT>m zp+l*xmkFh7nSNz6U3&1KQmr0CB-yB6RW&gMm%f7hA&{S9)m2Ko!T?}BAfm98wE%sq z==RlI8Ou*OWvW25lV%ny|F){;C>7gVK>P9%dWUK?0B2sw=j(uiDSxK>$BXW)J2!_< zi+|juRQYu{qkp?+KXQ_ZbxhXxZtt8Hn$~wZwU2ozTyVf)Hsh=Xe(Qn2 zT`NN5G3_f7ANY)8!bkMFR_Unq5vC#LQdv90l6(O6bC(H!Gg=S+!7 zCa(pFhQ7=y|;f%h{{V zWGP;e4IMaCWRQ{)`8j`x*jKDv z@`tM~;4s~LO$D|_nHMy@e)JX4q^H!UKKd`#wLG9UJQgXDcjc(t#Qsy29U1X?PFqU* z55)xYcH)Tewp=w zbhf(kS!Cl-z=Lw%$d%z#m?kRP6*=B#S#O^&Ik2765e7pZ1%*J`9EWR{nL@jOx$@rN ziGtx(gURI_P4wu?6~9~to~>6X&eqnLUe*dzxgi2iQ(Q7IpG`G8?ZbaVOuLM7BL_HyA{ynuVPrBbH0AU6gOz}Y`y8+!stC8F( z{JWi_fWv@QA>$N(G<-H@2Hcp9pUA|XZYqj3qu z4uEu>iby9iz^zi;=s20QC6gg?L3!@IV{y#1j3OY4>^cM-lU_o4absk6wYKjz^`^h`pF9h3Ve!Rt$U?v=`GA83yWn zvzOIH)a-k)H`B^WtN6GrSr^?Q&r6rCTsd^ny;ySqgGU9SRBuZS6l8t#U8en|?~@9- z(NEaTVzE?eWZLkTwD>kE@K}X4WmjF8gNXfe0fr(Ko%}Vt+oXnuR4Ipzggn#QK%TLU zsPKd&uC1IV=+n)cy76e!8&RLz1?(QF6Dl*O>Id2rnhj1gm)~TorU^chKlgvKw)+xeFFm3hJ;^f7f|~P@VQczkh%I;!#sk%?k}=qam$S9! zIN#D~dXAGm<)rU4tF5$$Cdc_m3!-oN1jF?tM)0Umc z3@mi;Ii`--ZUC6TEH(nfFD?+*&~_*xX=IfGH+6~X6s_y%<8_VOf_Fc{Rgs5khvr790+O*JjyR4wLFsIArN|Xm5wJ6jIY7v^^7B?&I@V!l<#{5{& z`r<(|+-~Mdv^G9Z}GBZZ)}1SkCinL4FQ9l1}8UO@1bU3BWtJcF8Ep@#CW z{5UzJ zuPQ4?D=ww0ZhZT1P^16b5NU$Nq#}Bp8C-biDorN=;Gb%-7AbQ7xBs>O+6VxV&E|uY zqg=qgd~MG!;Q-8W>hsI^w>VaGyzoe_kI4bV3*yM_WQ2IFIgdtg!?4zF(*-z$pe9Fm zOm_eohBP0V%Vs5)LoT6O1uA%jRUWukvppdYg;c&|X1!UE-fJr<6rW`jMLwJ!mxU}c zR>jzv?OroeOXlJc9h@1BXMjp&>%_;Y0k-qdT7ZbSUTcLqD_>wf$QIEi%m87LcENpw zj6K9zD`>J&i<}&4Em2{JY2S~NPqBqWM-xZ(F8gJAE$r4cB#U(Fxwnq5qbD{H{7`=$AwW{uy<`;=ZH*Jn2hB%}nP7M%Q?!gU zr>WWTBloxJjg*!S0h^hlJeU?C@}8>J06B&5!7_dNJo9b_AaqxN;0_l~19=%LK5lv& zQ55Y5beZK-UV(9bI-FS1Ys8OfJ1T~EoGlY1~qBp#H&+kVCw18GGNZhIOtqZPVD*c zcm-D?z=}+8A4WOR1S?85TgcRBqu_-iPpB@C})Bvm=L9fnchHID)nh3 zd7mjDBN@Q_V#Fd-{0s9>$^^Lp zFWCZe_+c0nw9qafk)swG>P#^cSF}o2>Z0**?dFLSr(Qe8K=6&&98*(y(6h5)+RrqL zXb`GcIR~5v=((}rNg*buAcgi9Ftw7x7;5Cs;=Z5bgG>%LKifFheDij#=5#I5=e-j_ z_$W1r+vh#xtejJiJ?8J**FodDtml2(L3%x7;G4?;w>PdMa>?|sy1oWLHz`JmW#vBB zKkNnyDn&*r5?r7swHB9L|NY)sS z-Di7x#;0FjcngA?N?9e3r{k(swlwDyi}S^k^yzH1x>UbNpI$v{-{;GUEk;0o$(h&f z1t4Y3BO-g2&KfNy<-n=Y=M5;#wLL_&OlLjWKmvHWDL#M{!r!%&pn=N4UrF=C`#sK1;3`DbXvXVxJv@(XV- zfI!{!4k96C_>1aC3`P);^#5&CX4g6cxrcfuRLOeig4RNR@u-LT&Jlg!@*%^S!gvXr zdd!{wsjCI07h&&7^4hde(cMVRzv;`({f92)n{#t>3k$2D4$cd8t$avDR4*_?=Z+Zf zkM{kzTf9_!7kriSq3ebe_4Ix8Ny7)W|9+SF9xCcl9 z=(;+HY8MvG!&UL5hEWnwY(96}1RSNd3=}b{g@rG!PvWImj~<;2~-d+cJUe-9qIG1u3XnVzlQN=7mMul?Erl ziSQJyAr~ULM5&l%_aD*U&|vQRn-6>96;7C5KEl%t%sqd>^& z8S!kSWl;>~x-F$)6y==jpfn%d*El40A4MmQmC3o)$Tl^*;>;-Wi4y!C&7K4E1IqSm zIe4*Fg;*aR;X3HGN$9D|qaWOv*;Q^ruHz&KME5U=L*hgxa`V?csar{Ts6!AMxL3ea zvIsER1t7qoi~czLFU5L|Wgk3@kZ9UieK33f=E$5~d*)t$Em#ukbSTRzmMo&cIJh4Gi$xkHBaIvjgnp0_Y$CSCp3IIRcwD(1f=*t{|@);GjK)p31*P3WJOsXA-6$NyRYf zy1`uNMG%gu0Xdiux1^>`4Hn z12-Ir3@b2aJU3Mq=RhU0M&6w)dis! zt5ujAWH!;&MbWMn7>iE<^AyWR@0w^Xe)P9>FVUfp5@?gKN7EIoVa0a2`4fv|3e}SA z^)vIR=o)Xxrz%#U75s`wwwHaZYC^Yq`1V zsv`Nm?)yt7%Nfxx*M&0Jwh-*G$~p8o+RPmSi4)xNp=Rjs@gmvGba-K0Nwi~Bc1C~* z1^#!=m8}_STu=dA4h02K*0Ay~!%<9-*vY=7QdeBCwAi?cFgY$CwBl2Z9ncvfx#Fvn zOz6kke zspIyY9_;!aZd)hyn%`?L`0<*AdLhh^nduz~1@!C?A>Hv>X0$GzD&qc#t+tZ~Ph6=h z*V@A3H8~y(mi4aZRzL>b9)!|w!r8}%J2x?PR&3{3}z_t41t{Rz#+V#3#mbH0bkl{b4Wux^~ zsS>EU2({t)aMfLK#~HsFL-QRU`Pwtssxx>~3ehOMp0kV4$N`8fk9PBH(nR+sEQ*9sE;HoZDpZs|cu2Y~aLSwts zJZ>VdY|Nui!e+d88W`d!*^H_n2uA@t+#2-IN|vOlZ5;mHdznAzhMSgX;L4tg$4<-Fq$nGwxcxE{ zs>PDtw9pBtDH57(Wg|MB?nkDtE?X5N81CI$`TkJ~wvb^8!x+G}YILb%wr-oBx8AXm5oM=;3M(;WpCR>Jns9g;7g}NM5)jMMLZe~ zedC0ETpP1EeAf`@#ASYdQWi|e38ksQ8B7vWe3^OZ4DteJi*($Zhs>+kU|`dT5kBAp z=gRmOJZTZmx36ctu&Cud*D+!%Fy!VMk-#i8Sff#?wA=H_VBU?*oLjRR?KT)GZwn@D z0Zl~j2te6@lMCig5kbp#ynW014sLuVy9Z)gBA zboi|?N&6tpn<(<+H@M&?y6fxSO>Z3-pU_Q6vBtIgs$z|GK-OCJJNWpwH7DvKmR}NU zAAQ7TjglL_&$8YZ9xk6F;;i@M-XN504fhq5%0zq#Y3;CVB&TGP^F7`vts^Dy1`ax$ ztALr=Svb;&>sV19#GD$}1n9O-;Sa1ggj@)ew_L^S4q(+fX5X9{SJ_-9Xm<75k@-O9 z&L;^Pp$$$$wsRb``>xxct|)9P-!ABD%$=kRwqf!W;ee@~IPp3VyNK{_o?2lmD~pPT zaLD|our?U1WXt72P_^xB?)0*2 zPZwqS#6+d@tC>O%)5Y9QJ3}03LFrdX5leD`|5Ocm2=s1(awqp7{~q)^zLEWu0gw{o zUe5os#cJ~>bUW|3CLnr&)+la?OEbWEU{+Am%DKRpzemrO)Z9-XfwoXX2M3C5__qkm znaHQ1`y56JeQpAgP(s5wxCF!;KyXqTfCfBNo_HegKT^wZkkG1%BplJ$P&d#wxp|mm zWS40n5_O6_AkhdWV#LoJp5kAVTamuU=_l>_3{v$q@B0~GrQuF==i2yKcogOG;h2{? z{KtH|ou3mVH|^-?mI9ABPU;f|a1*^IX;>UP6Wpcb4yn+eb%fhik)b>Q#DDI(PrP() zJ#K4+aZ+Q?bt?SyL>kvRPX*(Nbb& z^^)zT!6Ri$QVE}Jx*2Q;8X+1GViZN(2&Q`;lcIA8{Y9!Yg1@jMM39?AcIZi1hEqGZ zHE|lYk0_@1Yuf0Q6*U8sDhTre1>uBcV9jL#X6Yzv2)Y=+J+Wo;B@3li*1cdW1ozXr zhoYbLJE**M?#sl804RW@Msp(|LExJ*UC=;;h4me zI8hZf4-^OGqCiqYOHX1EO^Pe|wMDb}$6`tQf=f?4JroAnQn{>^G{C=V8FzoeBTFDM(8puf84mCK8@^YrNr zAjYV+Q38lMj%rB?ebqM0XV61|@OouwzJPL&9CQFo<^di!p#)#~{Wzf_u0#u$BArkY zkwu(Meknpg$e!RwgC9-;@T3EZFQdu*gui@2o!u)f^b)RM=aenSo3k zQ_;bRkR*a9uRDN7ccod?x6z*K&^+SVRId=fHFfB{KT zZW5kNMnS8J>pB?$Eg?pognsxH`G7hd)3TqjS_6A6xKs~^EzJxTJg4#;vRYg&g9k47sn8zu^CSHMk^Y@5?QVlnW;1&%P zJH?Msn}3aHrKSZjWMmLX<)kcT$j?@UMIF=V$s*!|zFG8(#dgZ;bp_o8*oBZvxgv;a z3}kN%C0%ebmow#J3qV2YWemt^+Nuru*R=d^fasS$c8skqN(<#|R_^1ba*@{^J&GKd zC%%Q{e=_~VNjH?+4+tZ1c_b9GSZV_E_!H>s3M#${J_rMz-iF-V*`1unuSiy>itN*V zXZecjme3EdKE?7F^)sCu^y;isA8i8~ol<;9)H$G$P%=_X1K1WCICWPv)XV_sV-$6w zv{J;PTDZhNfb;!7XhXOISTMcb{5)G;Ms0pJj(aHj!>3m@WHUBY7jb$z3l$`k3g>* zaYM}`7ntptUbtw10NclptGc^nQ9p%J3}VZyv|ME%n!n8?!^tx-c4{moUz@V&i{NwInB2FCJd#90twT0(<9xyH3x?K>U znD_F9Gt0IQRygDzQG;#R@yi-Qv)-w+>S@jM|DPQ_|u-2gr3h@qO z4owvK;gAWLC3ySgQfwC&50;$LnQ~Dr{#XR{8j5|TFUcie>qn6^4>Rat%!(Lts0)nz zbz;a#Sgo)dcjZEn@){yGgD2707fBMJOX#V@Qkbd~Y#i5qF#aPxdA2{UqxVqVg}8v25znENam4YmsI{GF696*h$DMW?pA(N`{8@I zQ~|kET#dY`;+;<@v9upwp?Xr7Ldw!Q_pNfMDlML-PhzZwwu0K96za~fp(a3sF5AR~ zegk$6Q-2q2IL`4p0Hxt5+Q*$a(?l4}G}{Q=pljQbdZLIjWlhm$fEpHuqL7c&2*xT^ zoBiDc=3L*_KxDk$bq8^y0wRa~fKH(M2#1Ouq{_iL&S!ymL89!JVAH<+si!;&Th~9j zrJ%8XAn1A#B`EmlZ^F`p@IgBPa=;YiSe4RWK0XVg$__h|b~44R%F9&B0t}hN~%9G476P7!m~~=ufi!RrD7vvOJU2%XJ;|B**hlc=%_1~f=}GX-Z^Ih6eaGu$z6y+zBvalF(UbSoNAmAuoHR!3~VoGR?c>F zg*n2N$OI{XZOgLM6CeAYo1=J*T zTEH}>zxZc)yQE1GTy~87U3>OGN^c^YL0i6OOiN4Jd^=)^6bc0LCy~In4*CXS+O(A* z?pEV`=Tr#(N@vM5!4{?RNF0?wO--&TH!C&hPj0{VM%baCokA_e*C<`#r?L$!-0Xoxy zhee$~Gcc)+!xlIXmh{9HB_*He28`@391FT5wrwc-Kz<$zZe@ybRkUSjFA7g1E1UrPILA@+2x^@GS~;)L(^r z796)Ts-@hrGNxtRmQ>5lR7{v>IU6_;p^!m%*VDm}3wR2OlZF!h6Z4XQ3QcgZ92n<88GXaySQSd%~vEPQ5b_A4OQv;K41L+)+Sv-||owQ@@ID0+*U(Ga}5UjmPvW&5j<>hLa

RYN4=NEUpy_%Hxl_AGuf2#~^6cT;(#nmfCBs-7oIjSM2jx z3hJNXBEo<)6_wlp)a2m`6F5scr5I;$lPuruNG0!2W?j-HBmzL+wT` zTZ=Y3Qpk?&(6K8>#Qqr^zgKyBU|PaTaR{m>| zKMY`_WzlSoz8b<%zaMo*|7q9ZBPD+K5yQ`uLGrJ7|L-_xc_&(0YBqr(Gc(g{E-Xa$ zLo5h7;a>Xm0%8c_f?y)kBP@u9rNt*CNs47zhVqVaqW*NxPothBF?P3*J)xK^l_E_M z4j^YFZ`&a=oD!Taez+nDXFeGqqz%8o^JG#@bQZT5BYqSKmgi54C84UFc29V-&_FEC zW}vE55qJ_1JwwuMUxaN`1)To$QYMEEXds4Mgo&jzEWn~DRg`P~^iM~iq)7c7&vfN-)=5tx-0vOXd{UYP3OfRP3E zx7&szUCYywyzSA`KF~p;g9VEa)1c`9j+5pII})dgCi>jF-h}{E&0)^dANYXs_Pz}7lGy~8nw-g}Y ziOn+~<%PVzkTzY^o#BE4AH==XJM0fSDs-P=#sIuVG|NIjTsOQ}F5QcSM!BV=Q>Y!t zi1v&hD^Var8dSmnxdLAdh%Nv@_eQkldf7;oC2HQspo=hJjkw zPQiHFdmIiMT?G>>Nth5faraoxdtrdKz9Hs>^drQOHu<%aRx=%6hfuzAgLv$NJg?6 zTr}#m+qm8!npl&GLz9e>enS%CzhH}X$K5~Q@6U@?7KP8zmLtrmf{8b^z)p*-h$EPG zUdhC%Hv~Z2-EeXUJ%NBGUjxt@`mC_!o`>v3&{nkSC@2YZCqE1alghAFr0<7tDj$Ko zCQ^!#?CP|kWuixfi-~`l=!@yI+v=(|Ip(Gn4cJE%XB;46-*<{ z7>Xqv#j`+8xcgaYjAy|QCRk|u1J93XAT^HSWBTLd>5R4WwEWtG?gP%}GPYO&)r?RU zH3eHLStc;OO*~t5-t?^ZPCQ#ec{>2mcK%p|RcMQ4D6G4mrj!0LM6W3Imbe6o6+gAO z_?uJ_y~k$TQ1dO%*n99io1I+%t852FXmtFngLB5h6Gdy4 zmr=hT*B@QhN$`C=*`Y+2kh}!^98HWOE;TqsbdpH6=*FIiRuW|TtgEtutHj+_J2l_7 za!SQEYkEa#_Q3nt4rQl==9}^mY`OJ=9X6DaWP8&3iFh+BuKM3CL+D=PEHmcHIS}46&1R zMI~1N4iIVoNh<4ZD{EARwV9cpAQrbz27fnF- zqabi!-M2ndQ$^CKwgTaTm!)YSj^=0 zQ-kmi0^>3%Kbz)fD7>;$o48vc(mWFBdn+P%YGJz?v60J!pn?bY532W*GM)83#mumw~hDFisDqy1V|NWPACExjbPmK!FzKyN`4R zDD?mqq^1DhChSD}=+oF_Sv^__JtgpehYb)@#s~J+ICaXncc2Y7u7Q_y%XK#(zH@lg zz)FG`k%Dz0;Flwf8~3`=ejNK zB)`)HRI7cfgyd!$Ak6g|GEb@{2(ZXN1q{GRP08d@auERXEqzHY#Bqp^Y!i$G>SY*k zl0msAWToA$zBFG%d8W`U6i{Z>TlFy$czvQBfWrb2AmF&V-jAoB+d}y_pweUx36=o2 z6G&Q8D{zV=ipDz+sUGZ2^a>y2&N-fL2IfKc#fwwW~5e(nr9>+a%Ce61xIP0zlJGCe)BZc zl7upNiSA_oX{E@|cvIAjoU7>y(*jS;gXtn71A!BEcOT%7Au4|yMQG!88qC-p1;q3s z_yYlE?mPaBd*cS8e-L>36zK0qDJQraY($mcfE8P=o*9AStZwD&b1xWG{S6?s6wm!UYB+o{X9c`J1Oi zi%;i(6gj?95B%^hLHWkZ=r=4q(gkVX3!v}oU^dRKxIVT zk$5C@MIA?J^}Q3(hp9B(lksG4lCkcLmnIs*8Yhv$Om=yaB=Y=hV}k>H|Le-=J&O_| zdPw%5o6ZS>(>Vsvek36(Ls8WgD=bp4t zt?C$|p_ZeFyk;wNRzln)!bi4iz~gj`*E3*K$5$g;6nx)waz?gu3Sd|DR8Z zriI6lGy|+HQfc%Ac8u-zF|9v>^OCAGbzjp!m1C$QR!=SPRJuD&7_=sl=_FGBAfN2ECZT!n_RqN9;}e#W zIqyERd{vTaw89 zln=rfSal`StC11u*5$O7mYa53vL&#NN?#f~BmmI`At&|nc%mg6$YZ;ZXogyu0xWG* z)evu6=xR4qqnHZ<=+L4721bTlz4Nx zG6*tYJ83Iv_v!0YDNWomS|nt1pr1$(=uc=JrXmETFruqI(dCpYrtX8}?2{&YeDB^P zo12Q8ci-n`Sp!v7_ezF5+ig6HT5W3##~=+t$uMjq*jYu(knV&T=wambK!?!+o2TNG zzl1wmUqs}3P_GX>UkXH28`nYl)kJ$>Zy*XPg8i12r{j*;6kHEg(galJOP*%?NVnVS z$RLnm2k|DVKaRFf1cwL5%qP-MU+FN@gVOsv3PEgl_%G28^o3X#$HgoDl`Ch?Jo;a6 zoj7qtFN;5wt~C`u{UNcqlp3LLWFhl?OVZ3HzJ5_rbV-vDWzwV#5Tofi*sr9!@_69V zG`Qk@5NReth)gz!yuKvW6v@(Vk$n@d*BG1A=A)nHHEHbFC_>EGKD?xl6Vn@FPQ_@n zr+*|3RjtIVd~sehLQ5*hhMuv6p3p%HW=Zc3PW}ihc#WYWZ znx?O!wNJMmG<01OY$>)BNhx8b6%{zo@b8RJ)SZkeDqDtc(^ICg#bjBPWkhEkeB&Di zDw%9l=_5~o6+*e8PPLj3a^WR-;@xR|!q23f$W(b_r*QlzGz4`t-M{S=GKuQ+p4ug< zRMC*MqJ!{p;z!e+OcG6xf90n82+8Z1qo(=@pdCQGp$gSBI&#F6WdKOZ&%aQ$3;~H} zt*?5SMXi_4v!tf$jsOc%iDTV{qujEr>qV<=c3^PK8XU8vb52(T)G%Y?k@lCtH&j+j zDuUB=TK^hRxv38GCT1s|;w+l&YeOHJ37$v524(dMSw0c_hi5v>M4EBqU7q4DLRkTb96OjZm3cq&pvBKnI8T0oB@ z>}jVB*+|vFhL}Md1eVrix~S8JWTqT|#3L@Np`u#PFDwWxe55)ZpuyaOwYsKj8TkPP z*h9Kx90C_ODCH&nsAb^r9gD5_aReRAo{KFv(Z`6%>oYS-j}5 zvHL}P`Q0!2Fn>|1O28<;W26}PS&&N4h?SNUcr^jITc)3jgi1^KG-#wv+XU4L$Z5bV zJ)6zkKu;A7mH_nr0z|(=s{vzbLOe9aL6c}DM#G7R_53^2ELY9c4M9uQQq6|dEH9`? zsyWtw`1vJ}!LYgKZynA#*`pWg1W-UM)lcK4|C|#>(@Wo#rNX*mWoeb$ACWv(!|R?0}VrAY*MIuFi^7CV61Of zf=(=2812s-oVVvQvonGv%4t^&N@jm9v!?FP^gbsdhkC#p%+`hB+pc+8*{zY;F<0ivxM$3v>{{&OkO}p(q_Sp67k8Lt_7NKD9!g)?P_$aa8)xB)4;D8?q z`1TN{i~rcZA7VkSAr@*PbBY-t_kk%f1l_p&8IY?W#7z4xvEqW*&t|T>mPSJ*@AjWW z8+Ik{6^mJvo=bTr7W2g@%1>#-=F3ysFyc%3ALGjVs3&b$jfYxomALY>g{H3~oHlRw zr6S4>-prNv9%F|qkKU7-pb`KV4i`2#6MWMkVEijb@WqYDtzWtze$-_JJAVrGCuleK zq*BEwmV?|w{-$r}i}L2?SJi6K&c|_4E#x9e@8G_hsl|^v&JCUQo}K536q3N#ab~iJ z4h)b2oNo8z9Z9aS%^dX`Z<&~6+eS@<(~H00fBVCpxC#9En%c_d>v}B&hFjh&ING2+ z^p=)VAsjGy7&*ZH46Y!GsI%8a(K<^C+330POqi&TB1QUlD1h4O52IY&s4i`9Oia}#1jgj5Hk|NbtG7gw6}EleTo@Q;j;H-z6aj^OK2M)ynSCrS**K=8d(Fr z5lU(_=_CTJKw#bSydAK8c>$3jwBrD4-(UqmGR!}TPWK(C{RC?14cMCm*a)HL&JZsz znXdazLm-xdIw-&9UGrB|PZ2gBZqLm*D6QMdd3H|p(&&b4f3Ann9auMvB{s(vGJdUV z_t(WDDz%<~3G)k3BVPmN?=;Y|{hs7SlNCyjukB#De*O=l2KvSpa6#K(YdAcEc-d2JP z8l)nYC{p?2bI(C3-g8gsgCE4!G=1hj^DZmv7sXaSS2aM7=a^_{3XIndcg?oZUPho2 z9|H^J-arSkbP#PRN)~Rbg#lgq_W!%*C$Lh)c|jES+xG-*3bM>3Lfk)n) zbT9Kg-EMRt+ zR@yA?2QZ;!rM0|;z7Z5Z;IATE6GHi_ch(!gKG>v@;G7$^e3D+=$34kE1=Gn@BY=G7y@75re?Fdy9*n@1H?c-+-kg&`95r5svpRr1xJpm8(ZG7rlPUvC#dULc)3$_Rt&K zMtU-2+A9n-z+jWj7#(EFkDPz>(N*u4yp=~D83Mlc4bcZ)^QgD6(HpS}NP%HoB-91B zC*BC(aax9*^T}ThuRDHU-3Q0rlHIgxE{bGZN_VzC&SpN{?H>0B1+Ow#s9McApPL&*4ImN?CLM0i%pm=6j}HqN&+3*}=<<9b{I!trA!Z6GxR+%Q|~e)h{2;ocHEI1=f(6H}Bn>!K=QAgGYya z7X@*0J(4^m{!emEzC77y?{qJmwxBy$`{@$GLR`0!8J;j^y0?wE}RS|!l^C=N=EhI#gisb4yrzQ7jBTCst;#|3t8 z8n0?{hQnK2Zg!4ia%S#6HHTg4eMg>ra=+(&+S~W!lSALm-$Gk#2n(eTsn4>|1z!<` zudi{QKAA84w5W=tg&)J@E0qjl)kcJIOkc^Of&{eR;VrT+c#Ga~7K7tGc!p)GdMUV2 zGRxpvD}Nb)sRWvOPpMG{S6s1}e6gX)RXiY3IwI2_!RFYLs4TXJj((!O==fuclC}@Z z^V3*@1t8FNZgv;4Y0vT=CYvKh5f?CpE#rI$48KBgp+*oVsXZ!CXCI#Iz|LBIhf2g7 zR9$hoS_Azl8@gJphK+FVfZ}0+T}U0cUz4vKNnK_O6}^n{5|BPY%O=o@(~2r;<)BzH z?i&oAV*5A?cu(r(8_Td9@QggQA4s*)gWET>ihn7ny6_gkKx!`?164nZ!rM%4E)%Ey zi7j!@J(dyX)hqMX>=$xwq2T5$Dy*OkMegQN^z(Miij9I(i=iY|nY4Spwrh|{?wq8b zs};Vq7{%1XBXzrdX<(V@_fxay$q#i}uQb~yy*koMKtX@v9gCZrD1%t!UB|CE*3uFs zN*X*Iy>JL5P!-qinjI7SOBgtR1-Fchmm+U5!=lg*^ILf;WK`46-!4M>elkKHZss*V zd@vXgTF5`>7zK1&BZOXH53{_jU%l$CtejOB`skKO<$?@?{~H@iO9N^3E0I(#tAXZs zv$go%Rq~cdRI_>g2I4_twv9G)x^t7S*su}Z2Gd8P zaJkk^^e=u76&zd>12KK`e|w89S7gg<&!bPEH+UOD_;YoMW00SRioJ!63@>H{rX7Yfg`K zoz)?5ayH-z_j>sM=v&`vemM94bN1e0j$PNCU;}tL=ZBnuS5-je9J;zX=jxo;IkTIw zNw!Ef#Vm>zmFSTyCu!`pJhr?}>$UuiY_BcbqD18;_3bzIHx9ES?MNPtMFmRMpWffO z52!+sl$J6`00nf_dpDeO?z!il^ZR8i=^SS$wM&ZBXMK6Tu2ht=AxD{3D-+2jN|M7< z28eP_R#15bbW+~MGDeYwpD`U~65~u{6_^tEf!&Q>+=I3Iy0_YA{W2i*wjdp5te$L& z$OlI@3@F3^lo%3%sd@4wb}$Wpj$V8Pps<>j)3m&%b+chKoJGGO8)m~05PyF{HjE7T zP>M!S24~m4cB_|SMT(Uto)IWiw;xb4rbJkr-a;HPgFOOCv-^H9tyZvh<0U&g3xJey z_PWvLOCU~dkF@a^z3l2rdV*oX+{QM?AxU#;Lr99iv&!QLFI$cVIPv%WpgoXw z3k^^=WgXZD4=sCj-6PQIE|mzYV}D^$NlRC)3tm` z^>o+W3mD&*5gO6)^J=_`E%|X!_BH>H{ADfERQvW<^G2@n@+gWdD@G1>Jj6u;6H81L z+mnCnDeh!zT<pAHcU zV8{j~M`dI79*o&)e#=i8vo9>!AXhAv@K-u+`y-r%_JkO5bKEq9GLb^j^NX#29`NpB zkH>Dg?yUjjg)W9@w2U$NHjrUrh+4$tkOx8b5(|Bm4f*OWBAP}XcJq_XGIy|O>(Xq3 zz!s}=s=3KnUp{)Jq-Q4H`=sN8{edU*0MeBrbt85~E)`yf1ZH_TImd-~J@{!^|KQ>ZJ@o#60=^px2s3NiaF{ z--9;a!=!)hG@`wewao3)d-mM<%>i%V)IA8?f`)5;{uutu!XDr|>t0~Xdr#K{I+}*8 zMX?%UUTW3l;J|^akGa9sv|CO4=FS5`DZS&|@;!xui_MFpcyd@!9F=qIvYW-Yxi~!f z4kT-p|F1i4xLR)*g|1#d;2k`8@$b$bNFMTzAHVcpFC4lLaXy40!8>KKcZ?(Dq5Si@ z53yors<%c1$ulOK?RO73Nea5wgESX$Agn`FMJOKEz{?KJNDLi1o7Bw`R}%QtZnv~S zo6l|4_v{}wHZ2`m;6J45$&6?vC+uc`coFZ+PJxP44<>P7gG5VEQNRp0F>e2WA`;4o zh(S(Q&9itf&A{e z7x9yd&6LlO3CL`WG5>ywZ6_(gi@aRcZ6@$5uI+T~ZCZ9yV z`|fX(UGC-#V0#N&^lVj2cu99Ca>l%jJP=IJHJs7%h@KuB+cNSHy*y`%5>8?Op|5Z$ z$rnK43?$lZvZMN;X@*t=$Ci@LApryk^u2W->o^!B2R#aiL19K>HDGz4d;a`kuu0&c zc<;8c=kWL#STJ_!G#vV_0^n;g^mI*HoIA?1_KFepapk&`wGX(<%iFgfJm@~Y?7&sr zv19pJ-vL{W(|DKrOHI5JdV>X<`TDx&am5S^qxNV!DIi+inM8JV>H$=XT)gZkiWleK zJw5tilnux@UH#AqzL-rt&&`iKih@KR)V;YeFw5D>flWvo^aLQc@`SVD*){990-hDwf3Tn;IDwX23K~36o zQ8oa}>gDkHLGc2LMU<=;DJ9UafROp;-tZgu8Ipq1YD7gH1j>Pr_lD2wZ6nYWSI~5v z<#0e2LTv$%L>++F$A5rzES{%#clKwD)s0egvFcz1DS{94w35WYv;)?j{*7!CQ)=%) zKnmv+!>%6SfEj~xKueW5oO)Vm%Tob9!#!jjH~{S9y{WN9@d4LWk@wZiV$;y3gpjce zs_!(t);6YXf5-qP4+h?ZYxe_4@|kuIe{x zOUct`c0L+EKIYqW_5StdO`x+-GORSU1FVFrd9_+J2LFrs0_ZO~jaA9V znBuvjqmgxq^Z09bCr=k0E+c-#+nYRh#3!cs`qkDyIzjcwjSPOFR@sGkB< zWl4&h)`e5s9?DJ5KKY3F5}BdvvpO+$@0mQm%vOA17vPh_~bNo(-`qq0RpU5j7D< zrn!vEGg#ge!pJMmJ&jaH-eCi&dA(xWUED~uO0A+^Y_>#zT3Euod@FZOHM1!orX*^K zvYy}Qyw7nK7M2&^M=y)-;!%&oRWwvThJf6WhpizGs6S%1>=}0Rm?Rx5qeh3IJkeGf z-|u7QL>6|VI27eKt|)ug16WDvWTF^vxkxB`TWn5&9j0%+!w0f4ZT!zg(P(tHMlXxg--%$6-U2!urP9k^dz(7Tm% zo!A1c&O1{=8o17+lPW~mi^WbvJm*i^VqF7+T$~e<2nf8t(tpJS^};^@YHjBH3w>^6m<0Zy8piJBT$@pOUn}D z;Jn3hMoZr-2551Sa2KL9HE0+?pw~X4DzYB(SLeb!Y&GHQ}Li=e)CL z_XA%Q6M-jQgBhEc_Ss9qGhPFwa3@^6%2GVF4FHX^TB>8maT985ribM z2dTs$!%QbewYEa%_E(+28NhJZp3Rg8(;bYThKSG*JHrP83K0y?8|Ne1wp~~6KTa$@=50@*L@0)cY#6- ziA0OGC5HKYT5$5S2NmUD0RVCsTiWLTaNS3lm#~UmzhXSts@3rn=;yB9b{2>?&3y3~ z&;pJ`%7%YfP#fE(dr8L%Nj%tN0MX!x`AYTJRv=h1v*<=$4qL5XR+HYZIiB+}8lGJF?Be+DwaaI;o z5>9&3H{At$Wq*nutvY709Spy91X3ljgoTuZ=I6HpjinD3csf@wd z5(s_v;n7DR%(5c5sSMbiY~^X8ofiSL>PbLfHUr*qOG4w*z)>6=D6c3h)5m& zB8kjPJsH{}6I)Bin=&5}MK3 zs)XI>6SEAfiMF(=VVYm)$94PL$x&fk3$!AVZtk}eVTekBy)pyVdmaFdJPIAKgv)AV zZDq_gi|UX;G?7o?w%^AVq3bn99Tj@JUL&oa}-OK;{$fLu8R^5%~lCMW%snMCdHFX|~xwMy0oWwunB7E^{a$bs<*5Je?E<0jL5 z+m%cKxVIWNZzRlwa7P~`EyaGtDaXGu=tySE;Z^Wlpm1J6fnz2}HU#kh024$GvZ!zo zTtS>ETduEp8GZ=1IAB)hgK*F)7)oXt$iDb*ZT!bC0uF>%xBJ@6fjxW9pLZ`@*t2K< zl@H1|Rg+}_=@0m?UT1IE>zDxaqRbgOh*-0-upzCP3;srLVZqzzFEBE%bgLso9qn#s*=77vc?LOEAq=Y>P>wGkW!(}14Q zJ*U`2<#7at;+#X743wksFWAg=%NbU>yt3BC67qY2F>z*j`NBV3SzbOfiMXLO{fg-0 zxSTxz`-6-bT#n9eeu(@yAFXEM#`Y;}Fk^4%{Ue+7`-V@rd369$7*xAwW|fU1DnYfT zp}BSmQAi+@h(3X2kj&qWk>g+&DYj2SNyaOJ6t0X$d;^R@JPwg*^YTVO!Rq?bsYaoY zj0WVQ5z6ISGGD0OE=i6dO1`S84M5*}MSvhUsi>aT2e>v}OE7Q}W6m4tus&g;jIABR}k*crVBJUO22 zbKgR&R>yeis4Bo}-sAaGrw$&x`i~b69z5}|^EI$CJEovk(AJ{EUVIEP5t-2;CW!_u zgKR{jqY740TapVyIT zV0T;-id*&gvav((ke>=Np5=m~S>9Rkj%Zh>2zkL=c7YpbgeJ0todIyB@tfr=3cfK2 zK75qd)?Mjf?9wK8twQv8KxA)X@#%W|@%9!G71*E5qBj5<@a;gO&0<5&B`2H_eGaGs z)B4!r!2zf7(PpAH*>0VLN3Ra5HIy)=1ijU&A_dBC(=n?Sk+VILkVXfu4^BKi+kd|+yTFJwbAH-IO3Sq4P$C>rpZys9 zjiU^m;qR&v&hZhvnnrk)6m50*=@#tJ1m@<(C~OsqRyHKy+HyuB+4hFDLECKFgX*x@ zLVCs4@?;{HfGM+`DWGB5DP$13nJCKWrT!Tt#n*ud2=W;m(*D~wp>D^~H>z1-l8mmA zJ&Qxn@y{Z@VT)_X6#tzbe_S+dh3|*mn9BGg>P8NrzDBU44`nKoseF}d9OnTGW5F0p zLp*3xo2qS$>0onkQRb?YLCNE3xIN`+5M(-00FRL*cCZv;Sx4GlNIPlP2*haPMFEQ+ z+n&ZvdteULYlDNlCON973aO+gqA-S$JS)lhtH_z+)st`sy<}OHUJd~@8Q{{3^ z9BaUTHqx~n3kVe$+Fgx?GoLp7VaO!m6#Zb`**M*{H+)Dk)Vm28k4}MX)mO(3?%sXn z3Qk7%?marGoXzC~9}BWZz~odzLeMHYYEHikASc=!5aK4fOA_7*6vo|&Ehv>S;FN@( znD*xH6$-bc_kGj~07+ly=QnIgj%-ODJIS5B{?J2T%9vTDmDeRpb%_f}meY!|M7dcq z^x!FEvUQ7s*4~bK(OI|1X0N?tcv{ouq|I&%KuL}Qs;pW)nIB5zi>V5>SD$S;?cfgG z0F@!aFj>&HXIk0pDpuVOy(@WZ|hzCfY)WC17u$Q76PmE@ zedxV+0ukHA>bzuu;$B}oRLbX*)jATz1F2X^=5l4MfaUH2h-J?m1|Uw|#Zt(u;YC=( zS6PlPoWh@M4i97XM`5@&wqg%wjaQLPj!i#^UkGmtFD?YZWIBF^ZQnZd7-(m@Lr5Vq4~FSG+_M%fA7vqlhnpE`Z)*wz1b`QX8mI5hx5@iRy#)Aj)= z-oGfNyp;53V50VEDow?vmno_xmq8#PITE~P52gbIj*vh)M*1$$ZYU-G4N?uu%M_Xh zhVK3@&OanBqz|4=EiWJV)S-R*6L%iU%!%8bvh4CUxSZ)$C{{9gm)O0CA>+p=#EC=1 z|1RF>AGF1GHGyD^!!nFj1+aBGi>~#bg-fdzE^vSDRp%I=>7jpH&&WVS3+S z7_Z9`SRizOlleZ#D||n#iYI*!`#cojn_&5(I(%X#H37%+eHet!GozN!G?5E8@B5<5}Ckp2*fXOsxiE5#%(U6ENOR$tSsWpBOhS(E1u z9!%VMJ#}TdG$-z;3Bj^BA8bFwpTBtEz_}^ykpN*Eq*2nX|>NEH=|?t_*cWf+mM>|)KOA)x`N#ld9KIfAq%SJ8*6lbaj6x-6lA;nSe` zaQSFRVHH`?@ic)+(&*y(gQ$MvCQ@~4sf26|Wb81pAb2teWkXBjpm0IWNfx zyupKBR^zaP28xOe&q{aGqt3jDD6ppl5RD1U!w*n(UIMPsg*VJa>~c)@iUcU?;{D~S z{LpOmE@*3}^DQp%fZhPrd28$3-0t1Z&YkPmZ*C}REdZe)@QiH9sh27U4Dr69ikS+a zsLo)<@O7Me1kuW3N{Kn(j{-QXm}B_?X1A_27uksBGDBHeHD zd3*$&C^=Wy^OCpIWl>x=On*q`$e?H7h~x_>=D0eC0`83lHQ!@l(l;`@a4 zMp0$j(qk;q05oXzC#eiL!fYCD!H zbUOMxI!Xx~{Ww>BLq}=S*7lT5<4RYfo6Q*~#{qL1w5>ypX#|nWfTY#JDY%fL-XjX|T!fPQ_yYuMm2pU5t@c%DdBNO`8M7WOn+677P_PG0*^ajUa ztR?Z#r_Ui-qQdW$j_kdDf&Ex(Nk{h`LfNfsJ;q)yU!zk{AzO0GP3M}2KsBhXb+;G^ ztT-2xTX5$f@y|i=f7n4Npz5f&zajOV1@)_l3yyWSv$tKx1kyOJ0|upo3EdHuq+?|G zvX6kLCiv)L*|*5p7p>mh%^|yo;m7#mdV&GK*lqr!<|p_bv<*Yt%Sbp z21-C#U@}_rFxD=8HDtu584?~sve?h)JkSxQT)sW5mW;ewt_}w_fYYRcFki$z7NkY z9d<%6Bjd3AkmoxsqB>Fm(zn6>NJ^mIrnX1K|2jt~D(Ip^-zP z;B~0zm#{bz@(ej3u{omY>->nHR@E}fpYpaTqop0!&v;wqpdZ!(Ncd5=G8fda0yt9>^&DR+V6Ye2%SNJ7#Lzf@qm9`cN2w9%$(C- zz?XoSv-db05Ko)avH{_%8Bs5rr ziV-9d^ZT(Sf!5NlB$}pxOJo!jPtENe_ptD#3JtNL0`yh*LGEN z5zGL^EFQ!RP<~>KkyXFJ7)NjTOr`>NX+~Rzd-g@*(>Ya)Z<2KVp^G* z8=O>@Ce3n79$0YtmgSvW-0{oO*+;ZwfS6t`i^iW0osixNCzJV9Pm(<4>2L>E417^J3 zSD`Wz2{WsdxP^xQEuycnP1aNq529eXz(R|Neo zO}i`d=P6i66MO@UT(^R@(`gby^mI*HkKXTrD&|D3|Fk#cc|${gP%1ff#QhFjFpLYE zM=@b^sQWDddy*ePX#l-THyb@>9(opUG8nK@%)Eye+ya;9>uqVgvF~1dNaZ>o&h?3X z?})^{BPU!!4ToWZ?^3eYd_w&taYfT6M!ug1PD#Z%p`qq)Bg`SrnT|1nQ$w(`g8Hwv|A_^j{N)Z?4vT)4zM#x(e~WefEny`Kc%$jW$P zwgO1X>yVWK6ai|`F(p`mDX195OC#MIQE}WGU(tu~tatu=DnQkc5*bx9JtS}H^LDAj z!-=zzD|!fx8K}qxQ)(gba)Rqyfn&?A(Lyv{YxeO#rbo&eN*OTo zZzrKmR`qo8xHqPZ%P`?kLkTT-0&1vs64A*kiOtdq6aX9im^+947z*OpbhJl~>>c(n zAfjMo$ELi3p0$gIfwseyD-z7}B5+l@Tkn&F$wLeVJ;^|C zapf7omG|r;GRy-X$ILbkZlK4bqKWOegM?TcM~25-wS<%>K-82eXPR#JJ^1WJHotV2 zSgvR`J3q!FjIDTgHKRFrMm{x|3If#+(nc}0oU}|prs$y&C5O(4=kD|_U-s^N*s)Oo zYeUMxc?S-13wvj<#(B1O2!%RzqRXL_#mC?c*-YIDvSme{y7i)1&0@a>ZKLIcMu7sK zkNGa=iW@asrMf)J8Mn=02$%KpOBrm(v_Cwwd0H9YbapHV0AXr|GxKJFtyKgje2XnE zv@gUZi8du3V6uAjB(Im036>E52A7!Nl4|0GBb)Tf5g>*ht!NwGDL*kPS=)@g%_VWy zxI4L7pHY^FU1_tjfHY=aQ1ObsBRt^fybW7y7wv;WT-A)TL@>MIE}Bzkf(zeDXCJytpWOMT*z!$-G_ z+>ZzFe@wj$AYOHG2#3_Bz5zL!$Y9h#;`l>E*)&*-E=qXUmx%*OkZ(9l*mTpH7co)8 z4>ZW#n_?Jhe-Idp-&z7*xjZ-I0)JWJ5KY{j)&L9)Sc@e*?suG51#l5*s-kB%fKdde zc{3ZmgsG+vhDup3%}{e-JDx!pT|@8Vx-U9>BIO}LW~o`Y8Fih+nR*N>9zA6*+at0o zazh(8YNt*)Gk6?K-QfQ7ShYHa$eK|z-7`~}rOX7_3^21<+~a>BcKsF`L+T5S>Z(=F zpL8gyb1&Y+T|2KtV*P=p(D>q33r0UM<2N2kYT$`$)9&Jj5Y;%}jO`9r8%qsm7Afpv z+(B9hPHyx!e#5(k$LQZ*I(F=ukiVy)rOikJP;B<%6C0XpRjC2LPE9%)1deft2Tm5+ zP5&I$+<(Dp<1?pk5>|8?7lXvFE-=L9_NFhV{;MK@;UU|0JPyg>aLYlzk{@y`&g6Z~ zT`mC(RnAOywq&5k=_}KIzx%~tS|^UD$I;avV>?ROuNgS-QyWy|O3GsZLAJc;M>+umlhs zjkGBaen|;z!*k$);(T`F#{C>v#gLq;fkRZ&zygNTIFx21SxV6Y@FybLZ6?^j5!Tz^ zV3rvEPApPAIeggOdSt_=CTqX4f4_1u!+|x<0;y_{0E-CD$Nj&-R^zfLppZy)?p{BM z>WRTAmhJpMk%LNGRf-puSJK|ttaJ+adVY?&K^8VF9Wn22A7=U&5Z#iq`}*ebE@k$c ziX3iFE57dYqwY30^0wje^9}o~Z%HVTsM%-JATcpoA_&>BvX~2kc3so1H)+ek!FhfU z9JH_WNPVXvOxz(fz2T7ayUuvK_`lqB+TF#Us!P?Ib-D5h&)y6sYm6wpm1T|n$F!E5 zu6LvRvFjICFyML}E3vW`G1oN`u8=Yi3WJRfY1Fcjn)(Bd4ce{54};XqeoGeBz<{BgL0@!)hDf>A+}BIojD z;pkCkV&VX7r=je&L7|A4If~Un;^%qUHpfhV>`GwE2kv=d(zLleR`B7pkioqvw)P#s znZ$Kj@hZ8-^&*?OcCGBT%0HiSgbROH{qTpqyYK#J*5X|G6YePHV8@Pkio9ltHBBw8 z_kbrM^=~?mu7j(YUw0k%BL*N9l1>PGgr&c->7!t6j~OCA7sIe>mpYCwSu8R7J%`Je zlu|xjSB6TXaWqv5?9x1-IIt6_N@=GygJMB_FHZe#3qlE{J{8PBap+l(vpJ!e1Dwv7 z{R|5*NBCfm$|Zz^rMyPR#tLNP9gvL}+NQ$7y*;aFS1wlR)Izig&Bv5MbRazGtl&%i z4SsA+hv`#u4WVq)QDv*B79BwSy1Cq+Avbv+2su#Jj>Gz46t6C2^en=W{r!|rPEOkm zoFuGj?Xx6RnbjnTeasVP4xBrOdNjvN`7)n6m*y827kN;UixN_a z#Q~hB4ht5Kc)=cIWo*czRzMwm9*%R-vd)x*BKGvWZ>P9%e$>w-at@S{a3vmx%*E|% z;CzRjq|$`4fs6;SV|u$5KV*|jEeqK|&icYov)y(^Mw&vqnODd|-9t1-&fbzx-f3Hi zzrVt(M|Y5vVV`Q&j@H~Mug_3N75;dR zac6d87TFU!W|DqoLlOZal1MV8tKxuO9CJ|O?Rf~djnu08K9n{J$_ZUjwJqR)mMjE0 z#u4}g^eQ)GD|v85Vc4A9^R)(e5@gQ?2w%p$1PUt?6uyqCK97aEnlkq13?OtAlw`3F zn*zJfs?bpeMa@V;DJ(k7>YaNUgNDH~>@<$73NtyYjb;BH>(ZJ-hd4+RHgIUre`NqA zRcNA-8cR7gLPLl|f$OnQxIgRl4;v_$^XI2Bu27S+e}%jZ%FLNU*`&u|%aTeXVZ;LY zW4Hwy7oG!yi;pF;KCGd@lxlToU;ec<0!w*vs{;F$B0)pmBNn7ybDlWd1RL_mNF)qK zd4*EH=jWG2+ZEj9vRK|-fmnP^&`~MmEa=cU>eCT zJps8`WujwCXU95X5Pb;jB=f#kLr*=87i6^u7oXiV11^<7nztL+S8h(Mzd)8(Z7AEEH;L5hU=4pBnz z4Fred0}t8?P-`%=@N%QhkGMC}Vydl~h@LxR@8u69_TGEi-YV?g^L`|j9}sN*%L`Br zGwGNMNUK);lms?PF@aZE#BD?vzK<}mnr4g}beoQz#?p?^WjE{x&FD#ETum_YX+O-g zo9PZMZy|)E5FDHjN_&+kH z^jWy&-HI2Nd1b72K6XJ!jp<3Pg-)Zak+cQFiVZt+5ztrINubl-eYdM=Ma5N5-nhZv zIcsLPrfg`QopCp9iX4z9hgv4LhWfdHUu6uge~zn2YhfD_l5PRYfZ zwFN^>J3GuSYZVi`_^a4ruEdS#7cnE~Lv4LB#!;}wpbZ082gZ_*=(>1W+n*bIV6dt` z0V>CY2bTL}U*BoV9I@;fkO6-dHDoFng=(e;78~N{RH}H5EX<5pHkFOa(vpJo91oFA zoKR>;zo&Ww76G7vBMaVf+eC~M_Qm+r3HVa$seOHODCw#67uELUvCa1?k0d9COoVv- znQ?zaK6mEu++V|Nz;FQ8GmV%BFKE22$W&vPw<+`4KtT%Kl=QMYO1KxBExMbyrZTB} zS1L(wPx(`AZE*J3=D9JDz)cMqDTjCF#=^F8=JcVd%ehIiTtE!5i=Z9HAtNFpNX3>A z6Su24%VouM4)P4Yfn*H1L5BixsePN}82kRJ{W4eRysUu1A#~AL?9h^@fU?`x zOURy5vVMO%`atLffd>{+S1UXDoy%o-c>QkBxxvk!;RbHCUUwS}kTOSu25w+0X{Xs& zUA+OSJPNyZl`YTjTe$%xTDuWX#23mU2wE`vUMgxu28=wcd9ec}ee1jB< z-oA-w%;!5kez(+($p9&fkLUk5aGW4878kc{`TXJ1HQoLCrjg&B#u9W1_)=6W6Yt#^ z#v19_TEMPvI6q$inkPN}`qPf{=?#N-58m5NaX$xVlTOFF9#BWh+0k}S;DJpYAYDKd zlFb$hCEallkh1}Vkxpkn1=dvhFVSyT4$h-w+KpuT0u6is^7LbNtk6BOT)Wjv5iZg#-)jsZ8fgzPb2Vx z&O^jtS8=dSRTN0@o5ueuVMoHr{xu}-I&L#zfM?p1lbl;I<&B&~xq5hf<`b!b&Df|t z--kSd@S&^oh=00?Kbh;l*nfrV>nI5U|HdM*MxcA-IUX`Jr{))(N~KV!RI129*8yDP zRVuk$u{&%7cJh?+9&`FO492IY}F)QQJm zZAzYaAS;-o+GM7!RQ*1yu-)cc+ExUi9p55W1SE(mMh@60H zZazPh-$gHZP;gW99vSh*lH<~rU)I6ty##*=KAvfo%w;TMBiJ>-zaF z2u&!)#B zBnYvWrSl=x#p_7W7R;?c{f#t8`W?uuAR4pwx{X+sPR81bLxM@L5p>Q|-onr9rrBan zBc=mWBrDVGC87=Oo4%yoV|2}O5+w~hb)G^+Eiz%=%0OE|a7VT5P)7Q3Ero2dr1_*` zCYxLsq)=rns5%x@+e&RyjZ~cv!PzF-U{{q;{0Ux5RBBq_2kt+8M;ojeb~cba9owI3 z2AMsI-W`pIY zmZ;9l#NL!rjz;1AV4mPz32R^>dOx$Oxwv$WoIzS3k*B;`_fIAyp2rvL1k#Gl6d-sc z>@$q@KrrKgywrmUrKQe6%2oJ@vAn1PYR;R zfdP`z3Q~5E^a@Pp46vcRp_FTh1rBKA*2cbcx^Ec9E{$YEn4vlFAhuy>D1qTROYkP~ z>zFp^N>_7Y6M!zPE}(81fTx85%u5jz=Qsj&!PJ}8tF`ibJP^C102RWIs&FJt?%x); zcDCv|Sb7dKfq!LXA)y4GI4xj=4Jt`FPEyE=qn5xNkrIO&E_=<%}qG-vZO=6JEJd~UyNC5UP zI^))`K?OTM<247IkER;MuhbsB${sz}Jg~u;u!nJcMSFu=Xy?~3*(+7SJ!>%rAr$`r z{1ro}0xsn3^y6017?nM_uKi}(HY$BwmQXVafvcDLxJ;JH*$DaZ7E3pK8^fvJjUq(P z*}_?7l@a?kKIc4_a=7tf#Y?_O!+A=mWB2(GF8cbqPqTd}gXCZyG7DzVdPKrSGWXP- zcWO^QIT*NJxr#Ij*%JmvgzTr?aW^HdPp7oD&^IDxb>1yQk8b|k_hCT4h$(^|b+u=b ziSVDgQPlrb;AOwK-MQCse(l$AScmv^SP;k_kvYQcR<~-= zcibAEPFpQ8=Z(P5xHu;T+=J@m62CcB-?k%fjNa9pX|=W=7sNoyVF6CAyV1EpoFCi? z%*1AbnR)gjw0AmsypZxzOPkcPRg`llQAHzWOT=++*bU>i`;Z}gX^X!7@bOx|oPkXr zIw;j;tJ|aNba7Y(diw)6nZzjx5OQjESyu=3Qm*bGfy%snQ1Ow_ z5N>E#&4rl(D|cv=XKaQ%2p#B9<~CfC9^a0}|II!EHvS!R)D0(n3!VU`Art}Ds|`9R z3UB9k&`bW0Z#W}J9Zkut9Xp&G`TR^2Md$GB7>2vvwj`gi0IlFoi3YMI+2eE0Q`n`? zjvHR;gCxl(gbJkYei()yA`I2ZAB<&)*+Lo^+Wax#fDbw1-odc}sj1gAv)>w2CZ-ZN zsSLj?q}@4pvSgJfC(2rL)SYW;l!$th!3e$t6Gpvx{0}=_y!^Ji2vpXT>P}D>v+VIX=gH)oyZ9)&_c8H56<8^`DAGD0r13%Rs zce7s39V)x-Ww$u^$@NE%uHSdZri{jyt(==$P(a8P zy5JxNo<7dM0b^tBYf$1g^xwX-Ow+J@eczRY@q%;l@MiKCU4Q1j@!3GXU4`c-2=5qY_4b@EA z-l3FZR7cq3W%q-aUNA_x$zN_ZPdCmARXBwjYOKGB?R0lJrcCHqQ829#$~Ud^q(n3) z_QMuJeqcxnod$(hhKJ7pytUo_yuB4P)TSoe$-OuXGX^kBKRHG<>Vg`YlNA%?NX*0Ui6T?{E~><+s30~1Yk_ocAX#{>BueM{L# z@|2gWD(johcIos6Jy+H$!&-S*9sPo{2_Q0ts@Tch;2SfCcr|0d(Zmc2o?H3=3&ccuFFNC^903UK-r$QfkM8 z$RiY#GYbnKeQT#mO>W?KnoVc_dTFxUtfyxBGqxM#hUcfL@KN2o)v3(cz(6MC_X+S$ zeHDksUHSj%vi~7JNM+o+QohrmtdMQnZrB?jZlSL@iCsE2plv~c{h>S0*$k^+m;6pbO zyX}75>Kag$$XBD{Xu{iI3ub4-hN10o0l(-2o!=jH)Vz@{V($q!96%uO%>h3A;)t(j zMkH>iWaI?CWy@(PT?DdG(j=hzyCL*6eU$Q!OTyP;`be5e>lOfKx}o`%gdVKcQ@;i{ zi(+v{u~-_*^BSK3!JA!7xWESV?L@}AZEAehfr#Xt2X^sEDG>9zyMGvzj;J!Y&O>C+ zFyh|Zh_}(P?gUCRX}=1QT`etG8BgZqqO*w2$C`!s0vif?jUu>*-8MESfm5t7RuPG+olu-=qG&z*&ZO*-; z2zNC*JES^#2Fdomm_`KK!)ZW(BZquZsGqYv1^nchqBJdx;)GVnDuOY=R|+zr7m#ic zQ*fN30Mj&1$r?1yv1(APB({*^8ea%wPsu?m>5GM-l6N35o1+F(wmk7-=TlT_<@Z^Z{Q1O`n2I zZ$`iEwpw$~&$Zz)X9u&{K6lCObGbrM$_3??;Rd&@F@Wt`V>JZdk8Gcw-*kwBzB?h%f}Z)9?v}py$keY|K^FA&047ULf&Tzv4Ons8KVZ6mA2VeO z=blJzB?Lv)4ey>wx`_=F;1Z~M;DsI1CjJkhos-R!3$$CX!sKKtjQFY#`t()?v_u0O z4r4(M5`W8`L>v@hEjL%eW?O4u2uQpr0!ns_u-#`nFp#W)Z?b3T3RaKq{*w79q~o2^ zx!7lk|B}7F3`~G98`(}pE0<9D*HVDH;RxG7yuN`YXEvn4!ELKQo)&?EEKai5SIU-S zHC4HiHZy=pGkHUFLx8ATcDqv^f$W85w^V|DCJHJ;ss16DJ*;GZGtpaPu&w`svumXIa8xs1msj@f=%!{ch zV@?(tSv6an5~qY*o}a;xooBs^ZOt@sQ2k8ShWrXrBqR&O8!Xnv+M{o2SCsj;{90D6 zCXNpuw6RuA$W6Zf*48HHzO}VEKQ?wPs@9_ZX&Lq&Jt8#s?;slew%m;7CtWa0IlrE& zTQQ*fT}RD1WRiId254cQK-HF8v0n++;0P61M^UL_C*dK79>A*)vO|IdCP;9)3(>Np^Cr~0GTShSn!i*%cJ+2~m<;Sp2 z-RX;q_vg{{PN;~SI|@)xcq|Vk{L)I3$#Y_32`B$8=k7lyqt&^4!c1z&VoGi`&7}#giT_9*=(ziFAcJ&= z;iq2nF&uU6PFdPjbuo?yJ7zMC)6V2J)0`jH8u*XO1PH|RdvEueuf3gXD$J?+KBkNE zKq)5HWL9zt*upz8Wf%o!AK*KY{|F;N$t46@0>DIk2crmy19lvKF%DSLtkRQanMKv` z2S_b`4Rzhqyn}r|Rzy2t8yrAlhqQfx79K`GP<>E)54cwh1cFP3arB*IiI4~x`RTtX z;rvV&06D0`I`9HW0FY0S&FLjh&Z}A}vaBd##(%;pI0*Vm1fqV5t&o%mT!jlpHllK^bYF2I$chY-d(IPD+-cjm-=r0fps=<7n~3pogng1icK|#4yo}g%#bR zpjEt;d8Epnz?36x}3atz|Mw+byH0+q1 zcxtG?;^j`)SXlS4@ZZt z{ut*kWNYRj=zAD`cIW&yHn%Z53%1bRJNCXK`*gY++`|$G8G}uA&oZFVxtzGswm0JOSqXW|C~(6IXcNHG zsfxe_OLB=sg69$7RJKXpjEgXhoY~`xSqg5(Yw6avXFEG22;F4&(2M@MI7z#-gf_4N%8_b&jQ z5c%Pe4YXYk+*bq)ZX@%{_nQG_5a}Qheh^W}3Nt*tLobNECr%{MlXq>PG@4C=I{4m@ z{-Sw8r7qZ7QF|GTOZd-;+?1S=vt`*r(RL6y529Cuk@FfDyZ)WM8rZJ3{Fvp7)0X?H60QDz0cLhO{q)a?_TpZ-zD&WQrl`-({&`|t$tl^S$)%8vfHWhqrq)v>)OH7!>WQzo;~)d^(lvbq zi7+B6;jN?u&>0_9`>p^26CSw=sxD#DA4lTU57+%Awi^&K1W;E%hM5(bK;F<(XE*9r zpDra4`e@ZWXJ-3fnvbVJ0;N?1z;@YY{Ti-wAEx#@7-n`-Nn71cD2JsHuqCMWc*RRB zsOb!X@JX&Ona&ymT>0aJgU^g3S5D2{xp_yy9=Hc1MVMV*2I#tu%`=5|yXp?~N387fF_pmHk7UbLjZXDWnOQrh;4l6KISIFyOs6CXcU+L5oI^Z-}L4<;#C6q`923B^Bkg zY(uZc`^WMT+8s%&-_9*70xD+z-l>Q*|dDL zfN)^{7c`$BYFJE+)&uO};K+0`GU7Gk=RNg6LMxgcqKT3uZbg=O)(Z?51wLHmv2CI! z+9yVQ(t(8yj8iBlMz-slhgz68D&D+=^@L7BiArf@;xP6!{LnXC zA8UCC&tJS0_HU9<6!cW!3|5B{+k@}qHJ%`7G zH(1Ba!55nD3{m^Fafz=Xoc&WT@wH-!ujx;D9!OiBx^|7JiqZd69a$S_w72elD8;MH z8pOt%yx`7J4~98=fKaQ%b*5u1E#CQbgyP@LWuZ|}@>nwO`nMk;a!mlkaXKxn z4Do3#)P>f{$F5!T$XTkt@WM+UJvR1^(Mv?@cQ59dvK8Vpz<&>(Iy3M9^{zV>!ojU1 z3Jo|bnM?vQTxci-de)~b*Q{DN>i#4SqH56tS-XIq0$-o7YumOVZ%N%Pr)>o={dNvm_a~6VjmUr# zI!FNHQi>;n!jp3DYp|jJ2r!O#1&WPY$C9kZL1UPV-WBMb$Wm%5v)&PymV;Rb1U;J) zjjWbwxKake-VAbDqV>eTWZn5_I(;sUkh`*r1Fs|CIFVUqo0@}F2D-+sV6&kg}Y zM} zU5#CfoOQkJ0SbS+*J$U)$A4pR&>cLSyKnHGA&4KD!f#_eiNOqd){`FZ4c>0;+vz%2 zmlkkdkOoY}{QT$tDvx0>#)tZ`ew>}}yAa`G1X=hxq`I4bO^xU-t=RI1Xi?X{xbIzd zA`MzR*OSj}cXAH6PhWEXPai~Z+9wM4nC^|`^%yA&Xg#lEC8Ci+Z+wVB?3`09tK%$? zkMQuBSFo0C-LTV+7$tk0%}En>RJh1-+zEFT6wN`fyJ8&C>XR11^}210FkW0D?~Q5 zbFfd3wrv0mvTf{^o24qiY=x2zlu=)?2A8J^8Tbktv>25>M&pTx4UrUzlfa`w&mr}! zc}RU?^2%EHJ2>6MnPAH2$2nUoH9!`%VXJ0Vs-XSQ{G%>H0?rFV_Oc7dP2FN68LG+^ zoqDb!*ATcYri)f3gaF+S5un5x0!e@X;69K+GSj#v>2B?Jw>{nTmkxVdjcM-}QR050 z=C02+z1kA~xLf2(@)Q4Ytohw5OxFHv{BRPif@N>!#Kw)knOmMLnEAQ+ZE5$|z=|$j zg)a8t?|XESI@~>zh;uEva*#&VV!}r4wEutO#?&U4@2;`0PKyiJXVFD3F%~w;QIbH1GiIrkMpbV zdC$Sa0pN`E;x)vifGCE)mr-~1b>;%cV3E*SnkeR%LyE9gi0B1|BBG{8@m8a5sZHCH zJ_5kGdx#B@vPEw3y_N0JzAVM!vqGJ31(h?QwU} zy>y%NKU7VFgWf1U<{L&@lSdmyp>DMM)I#F;9d|=1ZD5nUfPDya{8_iiX~MzfH6&V# zr}%>Bxz?ol=`=WP729nJHH1VkT17Pa7XJIGPRgxJmMaM0bEV?-TMwsc&k>COd|$9Y z`{x%Cm;F9g8)kl=G1r#MnZQ-}cMMOH(8 zj5#{pe5N;Cq*l~w0{|eBu#}jYhIb6ncYyvBxs>clSzu8^aH$xImg2tHelUfBZF);e z2~0c)CCIXqX^BGOd)$Eh3q344@Zj)pwOyFNpXQgTl3~z z+uO&@aMQ43h z(LV3|epup)((`y|wM#p5SQV`i2u6%i`fVI&kn=!kohyIZ<2+!r{8IriF2rIABQ;vg zX=cZ9nD*f~R2Lt)aW)qk<|a5*4@1Bx?CGrn5_M#RqFG(XzIPR(mF;2gfZiInS_V+- zlHo#N+za3mm-16?mqOc6EGdzY1DuUZTK$$`+#1a7AVe=J38ILk4#%e)m*;eiG@0xW zBhj^1ERY(fuHT7`SMvA?xuzGz>`r$?FKWQBn;WvRZP7QnhWp3)WY#?~VGS>veLJgO zuGPxh(QHAerj>M>pQ5$vGQ0DaA;2v}v7rcbUuiXF--w9m6iQ*(N8TCU6dE3J(p*9h2nmJ5L=iIy4HPXl z%#N_7Z8f@%@0mPI*WR;9u2l3LqKDXF)=O(yF98(ov;gGtgi+S=TJ06jHKbw=1*>|_ zA5RCxxkD(vN$?*iI>LsAQ=P&csaoiDBo649wHQPiRs>>(kzgC1c;V_jsfhjk*eUp1 z-LNCGFJDy0N378ubJ(`Rjp0yyGMB~CuI-$*RXBiO#fdIfS0n-Cidu0Ok|z!Zu&LS( zO=uS7Lak6yD@7fN)i&wNltFcIfsOsLqg~_aPR#q_xt?bnccN1pHBv6S6*r7{gtPe z?_=*>9+xLO7hPV}5vBtsk-dsdUW8(!*o+6_%&{XL#D3RZCXz@vaA3>e^Xs5i=#1_g zy6C%^Q+3bFh{aT*s7|?tvEpb;TElg>xD~7T`?{QP5|#oN3X;KctVB*Xt!vgytOm8=SfqU@1~LZ$Oefyn`OrMJwu( zESY*CgRu^}Zy~y701v=D;<}fdqwnZU$C|lGp^9oDKb22gzoI;J8_0^{I>eIH%Er2B z=_7toE1OWqV55HB&Hy;iH8Wb$D4^3Al_Hm&z`pnRac4H=6}MiwXQ^%%XHN$R1tui1 zOA{+DusTL^!XUG-P}raAggfX|5BZ8LDG>J$5hxciI*N9YGMc`U_BkPvI@#hLvadwYBL2PiM>&Y$9J)}Ec5+BjsH5951&b2H;TB& zocKwALB#`(k0@qE$Gl>r3@2Dqbv?MpXvNNGUP}VhQ%l}> zt*&3Y(88Xt`}d!2`ZOLtFvRJ0!o#HlCf$#1!oKOli4|cxN|#x`CV}((5XSv_LKL3`b`PC|K|YuyG`%lBz9`JOyWFzS%~RRxu3cYs zil4BZPZsy5b|kkYLG<)_WYdQpH4y+Kr1efBd^QY*VPSv2{JYZplQUxJ@?~e+w#0inKEPmVcJe%$nMX`c7XcU!2Y75IAT|2(2+s0Wz2@1J6;;M+=p|u!X+8fN9Lbhx55D9274u27pkz9}UBZh^A;42X%7z!*-{xYV|tZU`< z@a~rPyhkp7X%&Qsz+g*Q&9&-WS9&IC0pas@$GIJ^7Y#6=4D$roaX_ohX}T>bM?9L~ zDim+ZV5S<5g-WS@j7sgW`Z+w34IB|JAyhS#4e-kUcJZbblJp45=&oK@^rTQz^$0Mm zGlvg<+1`psE&vatqGeVNow1CRcGI>gxwmw-Wg)Y55)UKQLGjNlYPYC0JZOvN5nXC?{ZQ=KNvALEEi}5f|*3TM~~vz-nPPyC_=ZEm7toz}5*2*)yWZ zckIFVS587W-B~#}BFue!0TqLWwpf!~yRlE;9nRNyWly|{_-)#9d5^j~sYH=VvnbXa zBA5&-HeMUQBH&kC+RweHw;Nj4aHK6jkt<5IAXyjkJ{OZLga;A`L-1RG-GSG;G%Z=- zP|i4DHa2UFUr2K~-ya7tArj2c>StAA^$Bkdhg(gc>k_-XSgo8Ev%X zLY@SLlQa5oziQ-5YP*<{$67U;-*d#wIO8cTZi-na0sO>-SsVZJ0C``}N?T|Ev`07{ zqr2=`=7^P!AK$*sQG+a1Foc@wC7cxX8;}nORe;B^>!GE*)&(CJEPZg?^OliQQrkS= z@creBzNDZ^HUeB)Og$M>ThRbPwpZ+gk1p<9*yt`&h#@L5e%cCGrRwP@8qPSlf3 zzAruALWX99jYXm*OonQ|9xf!LBb*afM5<~Kdl>dC|C2nqn_rl%1Dm))6t||_S3cjm|nV9a|!$HtaAly7xhU#JltLUK6rX7}jPk{wHF znqvc&+=>9Z*AqH*$59fz``C?Lxj%CIma%NAB&Ly?tvPN`>p~pHl#{xG0THPV&WI%J z_fQr?(>cw7*MWyZ_|ucZqXGfpC$u{JxR2w4{3f9^^>IwcvsRGxu=JBCAe8l^fMqh= zG;j$~Io)uaWBC7{1!-|A&X+OWB$ddC`NRS@*)S?KjBRboMe$x)eal(U@9kK~_Yt)k zh@3U)wq``IXR?q{S%A@7{(cs?7;-sY(Xz6wWdPkf6KMm8thssf4HsFYZeM_bo1SKQ z)ywyL`}ccwQhUeooX=sy%t#64JvN8^geMyc{f|qzbed2KRLm04GWn!X|1(Q&*hbC@ zZoHHY%tFg9(?}}P;uIkJ3ht0Ph{=Z~1u|SdGeYsS1L8bARR*uTyltrF1bTtuB4VO1|GC7`$>k4|BLb87ujQxbV3oV8FH=If3qG zes=Fk0=7Sg-BHY&0q@WivDGk@7Rv8{QPgYDcLutyeCwps?@vxH!e9ijj~&qic?|Uj zMQ&UQ)U4e?Y47A0F}DFy-L3<3NtdDt!^s`XEE`Az_K_7wf3QX9a!G&7|J zD_J{bgvK}ioQR4Ra#=IbHl$)~l^;);emqEEG6o{Bf|`c~;wBRA$IK*$jNP0wTh&Tq z&YWi?I9GQToHJ*f1zgAlHURc2>Var<`oNb8t&niBdM96MNYJ==G#f3?pZcu$BQo^gJ?O+qw(BMZmm>ANb;M&D1YBq zuykHNpH||!G@nbTQ+5Wxij($U7%8S1uPZepCw5*G-T4vGK%2c{)>VY@6rT$)oqp`F zM>)Mkq;}ITs*|nbmrz~ZM8lfyp-fqZilBO0@+yY7Cjm432b&;8gx#!_OfiiG>I|2E z+qBbrz$q4|%g5=*8zQFpO{fpPzaHU>$HL>=CPr zfm0y)^K3xlFoy(^r|TGSKjVEyE;#vh?h7AHg6NX#%%V-Gy4Z1Ya=~|;qE>K6=jv{K z7OyW*-JQM1w#%5b-CW&P4NR|W+sMhyVg75B(JUS16egM@pbwdb^P(eT4ZWfM5`uv^ z3`t<{b--+h67>IQ8y`6F6|1xYI6350cob=&rzzoaDK1i|iJe?Lk(UcezAfjR(IZuH zj(?{)7MIL1TgU)K*z5o_emvf80xA#qoT&G}uxP|B5^@?yXZ9FFm!cR8iN<6orU%Ef zE~=S+A<8B49LmjsR00Ryu;?r<&L%K>m_Ot67pF}LlT*|TdcBIpjG^G1?BtWl7>547 z>A4RlF$kE0Z*Mgk_(JSM=Gm*TN`|vr!)`NnP{iQ_3bP@wHRf2YSzd5$fJoBo ze{{6jY_%NBq@J6Z0j%H7xC>{>((HtSgBH;1BFeEpIheJx_T)cq9liEF2&~I|(J{ zL;@tY#IcIC6R+%~lU0&N{%YJl?IZbM=$McVREjnsY>C&Nt~z~p-kkI;o7y?Z+9S~( z4uqiyKXq_z@N!<~sY+LV@P*U^?E?i4-^T;yA2nMd}e&%#L1fN2*o=R7}=z5h1~~g~ha2*@bnX zI5+={;sl?r2PBC@Soe9TF7aRF6Vs{HX@#`(TbjVEon00VmO(v9J`>YiFP;>H$VrT( zYVuo99i{X28=+YVDGTC`Vgd5}_ErA^bf(b$khKvqvJoJIRp@DMrrAuTYS)Mw&1gpQ)^b)c#nQYgCxM`7 zmNS}b6_)qz?FHyn%n)!pnwbTyhlXaFeubo)X1I#F8cR%du8|ocM zpzTs=Cel?bRHE^*O58L2sNX`>nx%~bR~Kpcf}>fJmIkT(CR7U(xK&sU!^9bx34_`z z3TWX3x@1g%;hr*M$Ij`xeM0fkZq|0a1aJ7#XhUwqMeQn8<|#%UaH8l)Ysa&w&g11? z93cip6ma(EfhX`|*uPZv5DwuPgwDg_2RxI63TFP!vH>(TdnZn?$TzP)G&fr3dF z?yCGQjS7x!8~^{x_4ZO6KBn%Vy=%{Y$JxK1Y9~Vrj|w~fg0=auX!)8fXMzT?E7~8N z6}r6j=nyF!dJpcKM7>ZCLkE|8Fn!0y=J&~%bmyc>n*lkY*nB>c0P-K-;{vZ3H{EEx4V`5pvTuq)@=F!0URNBg+(wiVz zW1HR&dGv1xr-;%H(oy^E|4c+Ps3w!HAZ94RS;VRAn4Fde;pl!Rv`93`=IxlEz>Q3!r`Z6y@rqm!1B^)>&JN5 zq$bo?z~r11t;vNw(&Pl@#V5U4U`JMq#YV$!wMwPhOL0wymWBM)1(TD3Q0a@vFM5QP zZSB0I@NP2gnkUfL0bDY$iH9#HwnOYrghw$ns*z=;kkJ5H)6#C=2WJO@~Gut76>Y$bk#{mJXA#SJ#ljY zu(to?dG;tEicBkm6tIan2p_{Z1r5XEEckbE70G2q2P$34r6LOhV^mZ4a1i=&<~Ceq zR(x2DO{y{s{(*g%E(~QYj+;01EZ=NyzG@MyKGb>T~mEu*J;62L;Md=TbW_?!@4wDBL`FpVX+X~bhxyShi(1< z1u2E?Wenvmn?*5cPP#`Uwv=>~5g=nVPjTvM0*Ej2lmfQ^)glN} zrGgR9WSor;x`zU~8^oRQEflI0bRSwjVcwMl2jbJTLByh%^UHW_aO>e2)46`9E*Q8M zKF-AqFpA=fff#RAqoQPBhCsxRV=B-Lw0GcOfcl@btXd5n-I$GHS-z66vbEM1%vz4i zz?b2Hzyh)?*j3=kM3b37PUcF91egyf6PVs<#;DP_HiFE#9+(t}J%Wm;&x7Z}Y|L5_ zWGD`3adce}6Y$lbF$LO7zkaF!{EGBysvxYc!nKK`civT;*qN9w!^5=`mX(;Dcl@bJ zw902tp6f(Qa4W6@dy^YD1jY~9Bb?1LJSjZP57xu^^l&m1hl?QApf|2}pp~yXO|!;O zsUTFN0vG3i+*6F5HWnAvqer>Dd+i2$)e5MsMjSw}{u3rAxRT2P}U>&e`cWJ9l37fE-zMFlRM~!NWcUCuB5NO=9+)e%mfZ-919A zbO9&$|ALiDXzDsoti;leq=3SDm{9Q$V0M4-#zC>aUi&AYR}gG%!#^Q&eC^-h-);Q$ zzZ3d%(S?=BHmOKm&U!l^8I6rMV(l`2uaL<`^Myz;hF-j=0iX(7vZF52W+qo05z>An zNJi5Mt}6P`gcl7A{$=#D35Ea?mpDGkNkH>770v*%k->pKuh&H#RtZ!FZ^WY4cM&Q~%zL<4J9pI3TI$2>xGL5N6QVt)e4i7Em z?Y6lBhJ9e{&WS~5X~|jq5&_3ycmSLWO+gw7-~)!OppV9>#^8ZasjM-A>RFVi3(Qgf z4#H9t6wu&!kdxfS;j&fI!4WU^*SR1x-0@>NUbY!JX8ap^f2f6#(0JYHS?H8(+hpNv z-bc`?$I7Wx{%gmc{GVl~>rDPJ)e32skI{cPIcO9V^9F_o4`>g_|E;j;Stkt7P6S{2 zbd8WqbiMsMjVOGmLd-z09C2wt5MgE;u)}YdAGfr`v;Y#0Xn=3o6=?{)f&juH_~=zu zj6^0N_BIU5vtVA>$3)v)b(Ofnqbv<0o9Oi?U!{4q%EEL_p4ohUzI8(rV&h3BHiq^j z1An2DfK~-xfwM?5AZmzr-xK`dqo%36wcPzp5%{`-Y8kPVpoqP*YRKUhZ_hgsM2Wc{;)vDb2T5 z_K}#f3gv)$Y?(_({Q#MovWd;`oE!}ads=Vib<~+klO>;C?|^*sdQNCO=FCrZ69v8ahzY> zeU1$<+H>*F-PP*+d@x~oyl3yaRp5Yw;!0Pk_j&%7}Wyw?Y%`hDmgoE}2N3-E&BCI5+yqF9@m-`;vu3 zcTXY@C)y?Ep!E}j(D%m!1Q!<)QIDSR&x~g@|%va zosrh3!=>a3ix9DcMDH^ZF{_(m`(nGL{gECSQZN?HFlw1J0-NElLiq&Q^f7V!`?vDG zDJ3LL;yt9VJTI};j%#i$pjX>6k)1WoB2Wj5oMB{Qwt|te=2d`wswFOiMsRdBAYYWS zwcB~QkPm`ZGTBaD#NQoDH9*n0V6qU)`7e!o8M4^eUVVs9iiKdHA#0R>P76_Qn zBAap4nawxl(eA+(t{g!pRf!ofOudsiQ{+dCD6|u0G^qk~h~`Nk6F*nvY`iMEGH@>K zaxK8iD6xFM&ETHZm--!{P;?t|*_gLa#LUQV z_wfux-%q@W4e4&X3>#Hdfd*Z)-7P65>lweTr~z<~e2ajA3=7nRtH#iBhr zUCqKaL4e=MHSE!)oa<9F)j1?_KMr#sbX{JDl+QjIW^c){RY+O6hnjpHro~5tVlB z^3u$fqM2P1Q8!oX=g>D<}kfmdU zAu>JflqeDoFWVIT)+doK^mhPT26+a1VJQml8go5?$tUR?@5LoCR<6hpT}e0hrsBAe zPEYP->ZU(?rx1yr!$<=&58*9Q3u=EuSF^zUHsY6H76!z|jZ#yEaNb?Aw^Nu0*1lPir zF90n>h{pi@9iY&@=NQ2JlEsz1lk()kNle93ZEM`;0SSbJF5R?wo9QSOQLIWu6cH47 z6rMrKPyASr`P;8x3sW0Lv`Wg2aZNNunO?0L=4z@Il3$_=v##5uHUcJbKbzd0A2 zS=9gT$Xa$AO}b7n%%^gg==S zfoo{Xf|9Spm`pFx09iE$3_;lU2HL}axaqIpY7C7DS|nEfuypA&&wn&~gZNh_R6yF2 zVE_@r#!t{0LOF%vz}lS{*dymqjm!dCw1?0vSvNzN@L{5d{+7;zIFP>A*X=y|N1^Rc ze+j1`v{u$35#c4Xv0ft|`H1_#1O9R{7r;7#e3T{RYrJgI!01DzOkZXVGp2LN2%;or z0^m`JCkZ9 zv79qUj7+74fYuEvOQCy*PmUxMQi9a zW#!CPe6Onjx8y<9h(Z~fB`qsf7M)dD=omt~qNw)&$0Y4BBp=SONXO41+)#H9B5YXd zFB7}on%rg6THsf78D$Kp8{bY9ojMK_;2K5X#2z5mE@tqtLR>L{@4z8@3W2NZ{L~~2 zc3TyrOPJ-0`_pA1n*_dC5Xf>Uh5-y_WXnwe-N2UH-j7W;GO1WINU8FDrZXPM>GtAs zuOZc|9Y@sxTO>JMcQ&95Ul`sz9F{|3d*pQtwL7#uQ}-l$2PObh_>Y?LeJMM}yBPH; zfTj2`OV}QzLb{5b5&|TcPafvPZHb5`=@2uBC3BJ=k;kF{FGFZT)+KY_8F&^ZXoIqW zLlcU`<>^i5(B%8eOetcX!5b2PgKQbn4t6mpiV0)i;lYQ>Zpe0I5q!XqJi(L;#Y`rh zE??l4gu;>^pYp7h83g%eb96K^vso_!PPaN9q47-_?_f&}f_l;{tA(r7;OQ79C`val z-w~j@W)AnD>!$>~P%e_=6hU#2k$@YckbQ?A0}-F^0r3~cg!}J5Mn~0`S)z(~W|#q@ zjl4$x-CHOWv)3WuvbdPde%bCE>ew$Y19ATioer+>UKrdzg^Q*VqmiB%+|`S2M6Fv{ zRUJ{s<6l3wyKPpx^Lewp3y-$Z$$R^z|N09a4ca7DCF|h!obM%1j;1Ci(&_(dclJ)& zSK*e=x85*03Fn2{pninj3gUb+iPlDoF(HUu5WNIUnyvr&DRewcOdybq|NA#vWc>Iq zPzn46!?>_AX@%dg$OJz9A<##dYNJ3&fEu~Mr>>Pn&`OuRDB2R2+`O{HURMG8^EKD~ zTCZ{n|HBy(mc^^U^F>qM7FNsiw>om>`n=R|+Mw3s;7&BjuJTy%1R zD~-2-I!8sa?M>Gq3HX;}XS01XjYozsj*NmDL}q{UJ3Yx4_fQJ(ZU6Z?3@uVJ3W^+P z(tHdpeWWD z23mdzN*uH1PackAiUsARH6u~ryo{020ltpaQvcwOuwn|S(ZEx7j;tgC6%lYxsJIu$ zN(YPQ*Xn<-rdm?#11sW+e$z?*oO|h#d()Fg#75&fO@k>U0pDaAGgj>M7R=4+K|K-{ zJ?X|D!Qx>BL}c;6Ya3WReT098wJU5(*lcK@_Wrw?ZzWSI+J>t+BQj-l6G!4((p}EN zJViN`8vn<`b$vTlUdo*R#A2jO&WWd*R7Pb zh{#)Gd`HSt-P6b7F%(rMP$+q8-$(dR#5z<&gdav%`Y+6G;X-j5T_(41;@0g`hA|HK z7}@124FE@1Z;9V3j@RpMv-!uc$mNWfDPfjS%<&q(VY{ACASCAO;7Rwq_DQS^dRWdF zyzWWb&Ep1eR8%Jbh)oU2xYC9AIZSb`y7l@WJ1tm|8F7o`$}!t#PvVQ~Px|qu+wdfd zW$f6!@szW}1IS_l1%=c@%t@OY%b%|6T1z&3lh30`00Wlu`AV!YDKtu-w=jK!OGxuT zNQhd__H5S1h*=$_>rL#5axBq+m>IUWo1fCUrQ4JI68zx)A5*tyTayn>vb zuNteFQYr8;w-~^DlP|>+cIzE?SbDu;H=V3)Ef9oVY}QIsgRg@u)O$AF4?~WbZER2A zJrrgTFVObFn4FIpYg5lai zV3`~$zOW}^bSeRAvbfpstTy!S3L7(e)RIQj5%OWsbNl_(s2k~FVub`pOy+TFm=voP z6knHP?_cWuk?RV|AHqntr>4;RSVMrhP>N5NbEfUK=TWr)>{gnfc?G~w1efp&33F{_ zIB-1wfj4$BatOHFipw`fc+aSEbpSi`?wfr2Dt`u7O?N>aLUv=rv}q7>sHGr_A;G4+$@j0UY`w;PzdN3{%FS&69<&SQBeHxy2Y~K4 zY~Kjd6Gs3J{dHFLGT6+b{4R}(r#iqz zwgwO#y9BllB0(~32J0IB?6qgu9jE zIXZe;?@{x}(Y<(``Y)!M0sdA`(zZr1h7-_@+qeJf($co$&aXP-pR$3Ai$XnfQrG=5 zX0#On#gIfhlA>gF47H_cZRY-Xe0L0cd-uQpGp4!zDcp9(_gD`$*ni1&F&3)!r{CIZ z$WPoCcHMs)F5O=str3E!3=g483kz=sTOmzOeq^`26IkzPqrqf7xutFin$3#SbE?f| z!1a1pd09aTy^?b9Y{Y4U&c8!(aVVmlt8W`*f{P*6IG?+Iit$bq$?`Uc2{#>s(5rhCK`P8 zdY~{fY3Nnd(3T3GooNMp){&ds2#=M~03Z@%aU5L@CJljilTT73gK5YPGW87H+{M4p{bE;zX{Ki5kjS%)1HU^#D<+S`bX4}ZJYMI+2(*eY&%JvzlRuK70o$%d)eEkC?6V4VGI^-k#G~}7KcS26px@Ch1Ma+2N}sw z=Xh%YxX@$-VeN&z6_5e;p}RH7N>~3OlJ7Z)Cpcoz=S=7;i+n0QTllTZ&tDDyI{*w zLa@LZKxoB8YyrjI*#hYTm;=PZf&rpJ^%YO4JDGTjpEhhbq&eg)0VQa7K8gxnj;B9E zCMv&wPhQCGaQ5!SC`MZ>t)g6F40@X;7E#=sW9pFc8MuvUmo=t?W%9dP90M?AF`Ez^ zA&0JvtnX#DtXJh5#HHBb^>`tf>iz-`S zC=R1_;U|C4m0L34H&hf)M#fPT7tkPh$+#NNe?%JLTNohq-gCX2knLR_&y~e$X2a)) zvvEO~DD7I8d1M$8t#;9bL09uX%;v=Lc+v=DAK6p@$oL`}xGif6B`!w%s-<%E#g=E< zMu6IMBPdQ^(^F?p_W1+le7+3HLSRa-vN8*W9TU6fu;@6zv&omhkC=u3*O;b|7ROvJ zV8TpOe^Qx+lPe%(_C}ap4VD z=(ZyOR>)Ui=5n%&Q5Y}9lLJ2AJSvY(5}(gaq2k+G9S5Itn;wE%BQ(-5Xh9ENyAo1s z!|R~$B-OtIjE@nwlS*|`7&VCRzx4&f_(G?%2faOy;SFQzPIP!t2#*z6ZnR=ZY||Fr z?MMgbJDrIMSCCXIaTV2q6}w_@`E=UV+Bp5Y;h0bwRb}2K%XDMAkZ;ti!6Xn}Z=e3e z?%=aBH|8Glx|;9S3wo>VTy8vbXv_dWp3x9_9w3OWNN~LrsCpGL zfk4<;49MFsc9HZ<7^b>%KY;@vt8!`;(2l9pA32{zV{%maSpB$h97S6IOY zMJ<_lfr=_YwD7cDtaF3H`g_3&+*(A{G*yfoLpz%@JNwmnG(JvL@<2aIw{Lx;Wxesh zBBBtbP@b5$bR8-L#9b6ircKe+vPT^ zR!7Pu5(Ope0?jLFrVME;s@q&Th9*?jjGl8<8-e52(9! z{;N*u6PEP}SmGaRS^do^CDBFKFo776;QlG}j|1(-h6gbdfc#N%I$;IC3}Dig=$Bf* zcmX~6Ah~kCeAfuZ$}N_4gs65KMouc7MLb(Fb657p_r^Hhs>N!Y5QR7Tzh@9^NTD4~ zQ4Kw-0XB^gzqT+wLWn%hUqB3~i zg^gtcsaZ=19)v&*qo6bw#k#kekrM%F_aoe{qLAH&;?x8AczS1SW}#b<3IpE16-baj zMFe1&oMC0RApGI6hd0jBG8-+}D|e1hOf3A$JVq0`nao&rFMavryQs+x|IcnM6bc9N zhH!Zh6#QN6j=&+4*dbxo5CN?zdK~sz(6-K`EVJW#pK?dg5Sx&;IV}v$*tSi;ly^Zr z?TE6ayuA2419B)Xf2xz+ow>_$fq)_MNu=d*pkykYA6OP)ch4^~mB^5#)9iUto=p>i z$*LfW!qsZ1!X*i6#b7CY*24kv2k0otQkqB0jBT<*jIv_b5SE2n>W5A%ZH(~?J{vT| zx(8ne%K~D8wfQYEiIRhrL^A0LrE}eEHmX~-j!lxYk^s!*6V;- zGH$^laI4#J>PJ-5*M6np%(Yiioe_}F^EPvuH z_LKUW1;I27`IVp5xHNFHQmLZc$YP=#uhee^UwbE}6IyZ$X1|EqR@tv-3i~@~f>@4JZTk5sVvl?{N+uJlJ;U=AO*k*@KF5FuORR_O=~cXeAJJ zeWZvBxd%)_UN*PZ<2%4fChlVr54XUcN3j7Cxp)wJzd#5z%61>vEF6d|lQ(^AcU@;> z)#`N<%H;e>)G{A+-He7N2;_$Fdp6+D6Bg>y;CtSTKPdd|%Gd+EkgJEtP*}P>0Z|szmN_&eT zC-=;0sGjU>lca5}6dq4gr|J6F<$mZ6hyAEO-^^f#6`JN7bpic(;|l54-q1o-NwZnL zuiV-LBxg1p=}7oZMcEJFuC{H%+*g|Xdf)&8uvD&w*C_isc3M?W=Z%hm`y{YDYJ+dX zH0f{Jhr?RC%ng?sI&+9sl?FvJ#35j2taxo@uhBRmBTacXq16zbk1`UwTr{flsvD6< z<*2Ev5o1-^<6N-sOQ0%(L#39}c7Bfv#pwlo6Gt_PYG&K*ybG{Yf~jsM63vkrAHXM4 zdDpImeKyUZzqsisoHNZRAgh}65Q>sQpY65(^?)6(c!DiO2`YTI2{aDTmLy))%$HGj znBa4css-7YZsz%_2@Q|4xsmm?O2_6+jNetVLQ2W1XwcPqRH&y0`uX$L(o)P4G5*pq8*i&|(u-ck)bn1h zj%GCp#jY5c1Nb5Q5?X@*fs$D1o52W%tGj$2gMm~;k3yVZlE%X>$6;|RYc6MkKNLBT zL3NV2Bo)|d*QCUHB#o5A$DF=(*6vRrm#(g$CrLSS#2ydOsHZ|Q$|$-AZ2Q0gad{baOaB=`@&(geK&y~x z*oe#}I2}V`UBzCyRhDl(v_)-d280ITe~>fOVr4UX(EfFV_<$CLH*_{L3D@(O@JUIL zuLU1MaSlfbP|o0rkT)pTLMOYClRsc}yUR%CcJi^3XaO=tk0?Eic@Hov+i6>;E75ig zeQ7b9kEWEml$9&ZXw?RafZL}pFG(oQL}XP(XG|;>jY|MW;W25tNI-9D`d6a1R?h z-yksY6BN;w5UQpiEISAM9=_w^-mRulzMVi#Q19V>t2L1pOQN5~2#-dMGgTks6{3J& zRMe~ph|D0Ciui!_kR>2|=CWCnzZDApDtdTbkU4rv@x?OFp)06>0UEBF)&stZu!`HT zP>hjvA6H5XVya?Frl@`Pd=2%4;ms;mr(!ctrnrPdTXV#mB|75#7`x zs_MqFSqn)S4aVV1@zm!Xh#cH}7!gFJTvJ%G(7Pg7_4sE9gbRYWJ0D8@`oBe420 zP055WbMs~^ler${FmKzv+g$4MfS{v% z&PKMXa0Ny4lTt^E=5iCo!}R5h0a)-mO*AP_b9QVBCgIo+1^6#H$X6p zndt9>>9Jka`N>M8%A*t$v-HwH6N#Zf)-W2z>C=|$RxvybUp(oG2zw~4Y3SE^8O2~a zO6gV98%E+WPT+JB8b-|6y?IQU=pS2MC__Y`<3cGSq*bZ-^n%2A24?b45ON9I<;ji= zAeINQ(a=d|T^tY=-EX&v1ehM{Xi70QCK0;#SMR7Qgm-{8>*DTfbd0zAYb%!<%dy!) zIKNv#eWbOS*dfu=9m?ELEWwo28pUFzf*?mOS1!Aa?L`caDY7pZZ4cX;l6#NfV>ar! z;U^704T2fEZW2-)9~g>&J|&--WCriyr@d8slz^2%dm|c%GkZTG}GAx4`1T~4m zp(5z?NYVbf!%eE$y^@y{5m%FhYTgzkFJ4wlG0c&1ukt8zJ30}Mp(ZCK@RIUo$*QEY zohiKV*n58NW6=`1;knGqf}noQW8dfS(`xpB?4>0<)~V(lNlwJ-D)2;X&!eYuNqQ{I z=8PgK52));RaoBnS8(|2am{ek#1r%EUg?P0oen9SojJBU_=_&BaH!UU4QIS z{`>CeefM#<++y$C$r(iedAhMT3JLAcjv^@qPQoxr{7*&2J54Je{5Sr1>;}n@T835( zfcm1wP<9b@5D%`x`~NPzz6gNm^BxrTGiVR!cCXb1taKI>wSNsn56IT+0t zoP#LCj6B|_FNC`im;_1*{gyRlV zkv&#xXY{Hl^*0<|~MUM8@wMTgh=QZ#T!LFm2Q7jN$`59MnHy&@2W2 zQB60HZoRcocsczQ!SeE4#fc~M#%lG5AWL)g4n;wH#gvYb<9K^Xo0R!(J zTA3Xb^mD&lTypmTh62(>P5SbzFln~D8NF|fs7-F^cF((6RP%g3De0#I8@E9%{lqZAqyZfd-R8E&W(2xb8GO8E>2C-y_^o!~7P zoetFDfG)zwMi~04eMah*ddhUWzNBp~wQLLhK5bnON>1r`Du)W(RF z33n3s`D@%0CvN`dOV=C)+%xQ^Is9`IkNZAYi`r61POz{Q9lZHJFLhAJUvsa2z)3kt zFKr!u1pmGay-Tw=L%I7iIv!6czxXa=ZmSt}I73<8mU!2%KeBr~9#fO(_z)z08_mG; zkRtg0rl;9MNte)#b;%KXOgrlK96&+^?XXi3;c_nu|TLnz`4E zVN!3Ky!YMd(v3GpfbkxK0=aD=KsAVqiHRK>*v0WCd+fJ2{T(=rj+VS6M#JG89`e9% zbB!CD&q$9}&zebir@+qL{BFT_54D`y3c%P*j3?D-^$y0=`P z-lOa+7$?+gxogh@$qz`8!Q5=pPEjV4CXBqi>DSqQkZUBGWnUQa@a0x$i_jEJNthmRy_S!jjzm_g3Q70w~ou+-joj-HtImeoB%%!A>U9E$Xz>lJBhmgzu%hYzxk^LxK^hQIxYgo+a= zTf3P9&f`95E6yqhv<74u3X1hEKI3~ z13`5CuyS~OcY1e+zqyj4fxxa+)5RU_51o;p&f4v)5n#eR5XPI4WXT~KCYmVN3gTe7k-1+?5dK18F?_rjULA5QDOco zXrHj4!w8QYh6X-qJ(=7ULblovw5FB&zb7`weAquZJ?F&C-wFzqzOXzxGOKheiSdGS z&UvAtSZ*?rKyY-{DwBC9} zh(|DoJRjrSU6wlT#@txEE_squw45eovdPK(31k$FPopY^4aqI+nz9{z+RsCtJU*}V z`t3#eknS!Uv(dw@`|G@1bzN_ynlj79%#3H6fH|BSA)e zBHQX8v@7ZXAP>5`Bk6?Cfjn=1W5>$mvoAbrVXi6sJUKrPha5>$htY`EaZuJ>w}HFl!qRN$6*c{*Y$NtxtZ^`@gpP0hNnMLOH9AMDs0- z>4|QB%E>v463uUtcu8BSH!;4qkY{X80jH-aO0eF21CE2~$4na*6d<#iF0g#uPEBj$ zH`N<oC+vyEG*76P-JELN=}Nec;`qycfoy8>{poVe})BiT-Z z&J1RFK8Fq|+8fvyd_dQ+R)j>3>@aSIzv&-AHps~-W81w(!Yq!7Ci=;v_E-k^XvODv z-@Eq$&_Nn=fe#nC8;>cDkO7Xtc-W4iB3u3Hrr!iTgmqtQ0v>G4NF3DHr$2<65RZGv zlg4M1nW@H{u_x_LO{?9_h#41`hLwkoe?{FnPa5eIUD#_`s`=NZk2%#_z$*h)g`A$kB^>Q=8%zZ&BY1_?FCa zAqu%v>!s}CUg*To3H;Wkw}piLGq(2dbLrFfc)~M4V2Wct0++FOo2y6`E*lFvO7gVI z+c^DV4b-f=TenS_o!N=1QKPr==gHh+bgX3Po!r@(9`Dj*3!TokSyp=&|Ep4+$2 z-J3a+T@qJI?ml^ockSnA;ydgBt?&y@9r(jDvxcj*fh>=={x z*e_`}Uf5Wk3D5TTf8YJq{kcScIk1bKV;(vzHq(b6b@C3NN_1bz7S)6uaT3Eu$nRjs zOxO<$c1Zj%Q{MY?ZRO;r7u{=1M)usZj)M7cr%p}Q^dpW!E!&sQbN-* zjCBoTGvSRwZ%{o#;_4`7pCxNhhLeZ%J?$HL+tKfM_F1RnEcy_`^7Kf6ID=J-7pNbW ze5W5WnL!rSPzsV>!+WC$`MVrqC(44m;2wl8dg;>9Wt3({ZmXqQzLLu5wJD5Rl2KE(uYF9B9aL)MDmOc=UCUKuM!vy}mgY^`g%_$G_Fglp6AQqp`qG z6roNq99{42NV(al=YNv( z$o|0`j5lK*uBgS%ed|uQ3+FvscrT{knmURjZa?p3tUTej5d%E~@kD+%jw4u)2*I&b z&AL$z8km#THOKU~aTB?YF_KT1$=rlGLK`Q^^IcH95o!ghrovH@_{TbcN)nPQNl!Xq ze7_Q%n~bHaQYw~p7w?D|F5oC7Au+y(VUv>3BvI zquYV!T2tCj*t4CAQW@FWOC)w6D-iYCGig)+VD)8$f4{fsapqCb1E9p#bmlt+M$H)>?kQiktnjd>Va_K?AIo$)9v{aDAh;qG&mJ#_NrB(i-Q5==q&&@c$ zc71lMyf;%Ri4*F~yVM+Np&A(0+T(p~`w|Tl+|79OyZF@KfJqUy?X1a?4F~m_GIl7r zdt82cPM+x(SERj%PKc#tGYZuu?;bNAj;OfwTs;08yaY^Kr&@7re|(-}qs>J@G!Bmh zh?i6EwTt=k-^Hm>@@42i;M7*Wu`{MdHEc60HP854T;F?GMO=MOcE$<;mt3FcV0rBXY_ z)tG}g#+Fk_*W0ZnGEy#x2`Nt^r;*P6UqDF1Vmp!~L$(6NW6dGyQ#=oh*F^=cQ22vG zXQBuM$MX^&TXplB)y4EabN@A$Hy>UUo|#FGQU*>+t}K>pjE|cI)Z~^VnUgAT{ayvM zj2ZUBLu;@Nk^EtQcwMMtjdtg4vCp5A4@h-6q4}Cw`tUR#WRPQs+vTcyt>R#yG9Uj{ z-oK@4We+$BGqBW#d@CC{Uywxok?okK8_`K>Vz;jjjHV*PssTaMmWL{KC`ibZA}!0d zJox^%>2xHOjTgkHFxlv!dvIal;QUkcB$^QezViCtnj^uYX zkt693EX<9TQ?SNN-STIuzN4HtAa_0W*wEH&O21r5z@YZs~O4fwBDF(}jRrPSU zjGd5I^N-+{_3YjnYyP~o|D*d}_n3QZ%a&s+@1-wyKW!LK=W<;nM~_o(GzKa4?M>f+ zQS}R^Lx+V(-^gTe=`XmGM~}LrqmMqA#L!Cl(&O)I@32ew3z^AZ=0@x}z z-H>8QOW;2c(<`2kJms1%kD|d}HtzFPbIX5!+`7{jvbKZ<>IynS+Ih#RVV5?np??5% zu7kp3mo6%Km=8^-M}zhK|Oll0}8Co-S&=!dNjc-;XiB`NVe!{=y7I$UkapxW{?!@KWlDizrE>kr8 zui(+X2LB?Il@OWu0DAwG`-tm4!d_2*Skpe-PHanVCwo35P0B1V-e2>0*5W5MvW5oi z*u^nE<(9H)shLQt2wW;cJ!Mb3ZoVQki`%z9)3Z=`Z_i3ffomwdp#RE;E0_$W+hd`i z`5vU=`w?NI#5}~)nujtp@WBz}wQlgXQ1sAUa`!;+~Y=|7w$T3O>URsF_-fW zEG68X@8^|cIk3Jr>zjJQU(E3Fc-&LcK(0FnA7`-1Z@~%;nSqf5asGw{pxPg&w8OdU zEv8X$-1vL!awXodME;#ej@*^p0kEG`otJO#K*OrN?mptN3NG*aFvL_wCQNy($$1XceiwxpfkJVV`X>c9U87xOSKX3&m<`U85{Kn90=1ua9q-k$+?{;c(C$(eC-FLsa$fq4oJ)>!>7$;jf+ZtTnD=a0!R);efZit;oSTCjB ze^?1Mr?pVOYm&oUxXP&v<>37uW#p<|$xo?WJP`8eI zlQpH**f(2q;d^SPA^W0e@d+x`z{A?O<^KjDJ)B+va>N0#O|e#X;@dlY<+%H(yC9A~ zir24kspo9_Ic;m{B>9hY)IUP3jP>fSIaknYL;IgDaS+n~r%aN|%B!*MM!IA)^a*K; zeLQ;8H*budy*2@9D(w)P2Kost-N|2ncM5}*aPpLO{1;dT1~d9!ckaY5H@lZFpK_mc zSH_-v^6ie3KC38amq)R+0KfU|flOkk4nyk=w*`5;#wWdRyPtI3Pk!>C7vU;NEuD2L zdZzgFhb8I5PuwL&k{Ff*H2E|Jte_%Ui3xrb zFhl$fOhMXODqSd1#@k!kQ`3qoj5hxj>CLAR`=(?c0?orf6^n_0Zm{p2PQvw-#)wjGu$!ov?=cv!(Th9da+(Dx3>n;j6Nb9}%WBc`r(Pp_7ymHG1YH0CbU zyV_)V_3kY2zg;ggyX;h}&gPkv<#(_$O1!=TooIy76kgaErXY#F<~)Z<(@%NHWUlO( z0kCyjEvfL912g;v<5MfaTP zR@zEr?D38?|cz;6aILEJ$Bp|=lg6jJJCTRv*%+tbq0WMe@(;us077qJuQ zx7!!LP8uIi@82%%-u1z_Y9~xpa3sr892YpXhG(dN`jvMe*ew9C(sk9f>wXV&{3xWt z_=;G6jVp*{v4DDS(dFA{Dv4b>n?YTC-aDl1a<4fXoA%A%Sq^wl2y)kvh@9*s9Ix!; zQo6@>N%740LfZMb44Ul0?A<|rg8Zk@HS4qIg$DhDqqE9#*FfG>?ajkFD#VL?beoUC zC*EaBcY1Fw1F~UM<8r3!aMu_La(}{(LQQJqc)pcYav%X4?WaG4Gzyy=C+5bOd?-?b@b)5+!U3mLmL`HgVszz;)xs3%&=3H*T3CAxx# zqdvzUK@ZSKPO=m0Iw};r^)-duL*lmsSC}@+V`t&+qSP9%s%LAE0FA$>)gL&OV}&F( z=~NO9sBusQsbPkQ3#l^j6Vy&aau)G9NY6CsQE;L_5!g*>{*jSa`}ARb7?em16ADJv zv9g`J_zH`k=<|jz$3AM76$q8*X-`B4CI@Vg=R~?6|4-c{V)wkz28runmxOqegjE}> z{-Hw`57s*ReBG}CP*gd1ig<5JsVy zLW@&(83+gdI}S+m?a70ZS~vTAhnm+?*>tJjXx7ZcO3BKd#tOfQSq!F{VOg*0)JvH} ze=CX3;$*fkFp+VBv2g@Y5%d)O00ph5MVPmCwAb@7-8930(^IK9CQ?LqE2Tl4-h?K% zhpnf#R3SNR@YY03Ps#|ZRh!b=8cy=xz~c&_qt-J5-D2uE#5G*;4y_N~f94X2W*{;3Dv`{CgSbGfUaHs^76{3M9-_Ge!4yjN~NiMuC~lfQ>}k~036 zYL>nIh06xL-1tVLoHz*b(By0+gE@DutH$bEt@+BaTdHO@mwDkuh6yVy>1(B}Nr_9w z>_pzp)WO7OsTLkC%=*3#MHwDRA){S46*NRo-&Wv<=1snPGvH&fcq@#>MAJQ(WWUG+^eMA=Exq)XU>}7 zJdUhH`0S){!k7VqN2OztGQK`Hu@^AsYHWOyj9HUQypc6a!z#;w?;yhJdH3-ao6sHK z@e-HLN%vt|6x8=5s$F%diJ5a)?4FSpNq!P_T=Ey5qx2c?+__WxYAA9wyy}eEsV<*9 z^MvQ|BJ1N`a%k^LE5m{YE8;mE0F{0y91VdP>XtF+>`Z_j1ETS}_T~d`jcxhM;_+?A zj=cy3HBcg)KOlc%2D?EnCXrT#ZK9-}DD0(DGS-4}iD7d|b1DEAT&SV`SYMX*?9j`q z4q8CNu?Niq1Bp5HytHF0I+0cP6)@|#rAjH03I+z2OxM+3>Ga1MlRT;W9#-KHsy>9Q z`UU8(LtDLV+ZLW**m`<&lyBucbn$)!-S#R{DU>1X$s*(g+LEj`tsow?`ugO7#682$ zeo6Cos;$?5ZU5zOUS2t!M)_$Fvu7+$mz1UXfrdo$m%t+^}4E%r~&(0Ntnl@avr_FJD^BwuTrxn-)Pg+H;OLjx) z2S<#>ob=|^np@f;vT3R3%6R#T&I4g#35 z5=C=MoN0^Hv2O1E+~@A^OcgsvY;Nvtm!!;};Z_Oh)VJ;2R(v8#?yaC8AYrEvOS zu-d<1ILj=LcM@l{6(j=-X2x^M|0b2M0OTC&IeoJ`?`6lW;RnaisFIS4ee-Fa z-w~DPDi+0l4v3L|AR?@_nDz(WZ+f2@$>&G#pD6(QG5YZ-WQKUI!=c2Xhu)r=I9`7? zEyV#tYQ-?K(py^E$1kR(#>EmJAMZhP{sQ4CO(Uj*NxG3_omBqD#Uk~8*;mh%rZ0Q< zd40yigNLw0)r>Ww+;`vS61p?s9n>^YhOgtz)f?0}1W=ro;#^(_zg zBIrqGFN(wdG(Y^iPPO`5QP(TQYpN1mHTUXUtX-F`S{TBcG@|ipv=6;~vBKTm-^Ajz zNWWE%7hWAeIVP_8LmF3d3tIzZk7T_z6@UjMibttW#Umrfoe?;3l``})f$b$!C3c$=>pae#OE6H>=Q_<>y z5xXxs!UqPGalKy|`hu#fJekR`Vdm%5dSJRGb*SmX)#T~+aIMB#KsJ!>&0v?mhdhxb zV#m;YOyP#eTc(6%g=#Rwv{aoM+9Fp*wV{4}-_DN)nibE+XS_}QAfK0w2cjy|OKkUD z0nMj5{+W01qzs=-$t?=~iLs8XbExX*!?5GD>v%{R(2|~p3UDEBg8ljK1^y0O*nPTI zTYLxHFSSTZuS5pkX=PqunU5L9$G{bSepIQW4>pzH;R|uyHuO^f$ z6&mr;aGR#8-`>@p+V=;y&d(h^)_3O2^DMuq#NNGQQ~u%6BJMhalGhK`@@6Fa8$Sl0 z;nSVt`#+psT|M_FH|~4;?S1>6XPGSp_W40D+!pqUHl$tR@Vp=b&91+By{~z2i??eb zg&Ff{FWoSQ{OQ}ORZx5TnTZQX@7k3iVHb&R>5Itj5cx*;+~C0DljtOgb4WQGm^!Q= zp0HE>uK#8KyyDC8V=rVY7cXL964@Sp*CCyiT~!<8+H*;3AeHl|F3L&T zBIw=_LXcqfj;fF~{JC}z*pD0k?8bw!Bi_-YFyluaPp8FLkR8DO1~kMiCekaEiduu? zVs)@Sy`TpV1j9{hM?uBFvbTHpgv0SE=AjeP`DIYaE>GQ4C~T`Hn35@~DIF=p8aTy! zS&E;a>TCcP5;C{Yg5U#%B@QiMFcD@ZXyMkt1nPO0TL&>3FTDv`*5>!}Sb0<*&RYxi zVEK^4^G^P=Qku7<8qOVvVnL33Wamu$HVoiM`83WiHl9O z5#Hbgg>L`K06YLK|Af zRQ07DU_zVXa~fs|pp0Yn;c%RULiiD$grNJv$HE?wBsafiw(q$45r5gZRs<0BPM(64 zQ1bT&Sa{wIdT80P=J-EoH<6Kn&RcVuNpje#>8bZU#yuZ^5ab7U&-x1s+uz+W`^jX& z1;_ER?Y5n@Qms;nk8wVRRUjDdkKhMTqM5UX@%W@D8qGWW)zv#LNAj6? z*1YNLnCE19?nOZPOz#a~0@zlO)R<*ixtcOkd5BUegVY0E<|jyWk?9UeGoio3d*2w} zkTSx8_rw$jL5OSrs<-bz!W`Wz9q8o7lxEPt*eNTq?9I)s&OS|FUUZz(7|9BI1{gC3 zj#kImK&Op%8^HJp=|TCMpPcz*w$opm3^o|Vxq9`0CJy$>*{OMoCl`%7YkHSCBlVHQG&*2do> z9eTw=hh1|pQOZojG4YyZ`)yvT4XaNeX(aazCd!$ac-i-OVFbzII#8y~R9<0L0$2>d z-^ZlEY$BJ~p2q--5C2}u;`cF)s8I@7iNA(={)N>!5=mP9zXq+b_tsm5bLUVreC9n} zfA8@-ys)-e6#xGV;{Ok!B5Q}QCywi?po33(6Y%0UjTzpaPSBj=l3zf=rTxMeb^VJ^ ze-ycI44nq>(n?v5Pwe^Zzkde|yo6L*kM<$>8zKRs=OHPflY=*|T4hzqOH9O5C7w=} zj8wY83bhQ6S3b{2I3F2#Fdg^jE4W6|cC}*dT@b78?eyLn`U$U1G2^L$#Efi)M2K1SR$S#{xVl>U!Fkg{!!&go;Z z`T2pzw?2)9+Ouc0z*04=7e11RNsTeJS$+;o<*ND2CseH}HPCBn-7`Cd_T;%anJt5o+w%1N zE@U@Pg9*s?e^*pzL!JcT@~2dlZ* z_U}NAzmNMMB~A|zDjQs%>@T}-*D z;`l!8WACSVF~N-K6dI^xUgCVW6}ZfEQy}8xVOjFK(B}U)EEAEWbai>x7IkHoZbBH6 z>xB<%>Xvx}z4Bgzm*q!Nuthbg;+7JXT5hCT734ZBz^jV%#LCJ@!OGNGvq@0=E0Bnv zp!^quw_QKjjwK9-4)&QqCmI2NU9k8Zv}Bp{?n8q#MjA zos}m2jj8Ga!EG*R2ef`YZ6Z{!93Ax@}WaBm@GLAhGdMr4K>t1qR`X@ z!{V{mimMw{LebW&)&AV3o|t@yj_YwSkw`ainI^o_?yO<<^sJ66j(~Z8bY~sen zlP_Lnf&oP$6ZY?7LPUs)%%kI{R=use1c%w@67wYESySKf*w0n13+WMla4znqSe+Of9 zi=_2_jI-s2F)nS$n$IVc$uDHfb9d{W{P2?~QzorpZ}HS^jm?`Q0f_Rn-MjPqKZCi5 zEJ$8DD=yGtEI^4;OlxjDYYn$bt>>4ov})Xj^Zt-Bc4i6vfI>dUxZ-fghV z!3IUMl)EXYTFO@nz1{W&1a;RyS_cT70#yyzna$2ho zZ5>E&A=JHw1{p%L3c9ea)#>dQ!Xx2puj975{tEdgNFI~q))x^Lc#suu|Wxsa2}$&#+t9L~s2Q=bbp zKTvoc4ABIq2?NN{~k;46l0|^5}NU1!O8d&|1fkPfAZ%GH#e9HDS z1uJzU)?+ehv1lY$js4!iLy23C@}2e`UkR4=&HjN?%KnNP z_spkU-L$R8p_8i)%W; zr%CTn4R266AQ%j_;h#lM^BmO~Yz)%|$*1QIOsl8-J+UR(HBSBKl>kgRJmS<^k%GQk zThz9^3@ir7(LBwU%DSe5kUgooG$QCQ%YyPqO6sgOjK4w%25Jqb7GMpFL)Cit*}@>g zB;sG#7U;kZ$r@#P*P>c@FjI5N9s^ZkzFpt%4a5?l2FtV>k^3&vxUP#gm%;pQ98M!` z3kT0pah$w@Bn6FaBWw$#Aq=#H?(Kw5{?p!eZxCGSi^gQ4oc2sV zb?v?zFWz6DnhtEQ1)vmUvin&C{z$_!t&?cA;gKRYEq{1>DZ7qcCCy&-4@%J;(!m4T zO5%VHV)v^e%B-g4TQv z(rJf2@w!yfmFX=JItvRLZsa3gV!W+)CeRnV&$;QQ@&9zEpxKp#tV`-w2!n8y^?%LD?Nuu;a*pH;hq=-kwr;E$18_yoCXt%tB|C&4}GyVLi zo~=$NmIl#9-L876<|a3tHIDH}M;}@ByeBMlXD1yKQH+m-qoV6zpD^6*OOhKK+lE7U z;J<$xY4=Zy2L-zyX*on^h?zs*7$VY@R(jM&Qp#lY@PoC!vk^(+`AANVCL)2oXv`Qn zt!Y>08S`ByG143S9@fwGW|5YX@AfqHPBFo#@y zOi7&Ozril5i|=l`aKZ5M@q%;p>h5<`qv3fCdm1^TDu%$Ys8&KpRoHa*eYj}yM#mJ( zNS{n^5^m@)5!7Jw@GE(OaFbV&(L8g;JAZy>JfH2?J>~pusiBuz>D87R<$F{o@UH^*BJVTp#*;tkX769RPjTg$w-Vn>$G}eOm&=J5VNrgFF}5)H4va z9=-$_D~K{7J|lFD$SvxAX4g1kHNs<4_}_o~tM9)1W#>)c<&5M|Y}9&l3~3+5dTE~v zzjd#w@{$;eqvqpaE3*xVmVvt^=p1!nhXW?MCZww%sXiHscldQa&+}iem?L_}EI;+s zos;mG$78co%JlT%$%u&78@u=zgt zErFm`8%!~odBspX_wmO}Bj&}_)6xWW5Y>m&LwI17&h|@TK=7FA~SM zR4aqHetv#112lR+ZMa&KCYbO+NVf^Y zsT^z~2DlNej|;blC=(d&1bNI&(Eu6@Z(Nn$fh1ICR|rMUcBE>uOl~6498IMBRvayX z`uwfKQx8qFz$`l^pFpv-8esgD;((UgMa-Wu*^SVx`ydrWRLLO_KL*Z;<1h{i67il` zlfmg$in?jdjW-fLy|{%(xAI&r0+!FXX(YDpuY!`RGLz^VPo$EA7?Q?dyDerUjadJL zd^akI7da+W>nGw;S{lmY)?~-csIFg06muTXZ2p*Q;nt#{^d{)-^SCvNuAvAJ7=!>e zcnP#7;XQnY$%j=roljKscw&bR3f&?%xXzxJ*!dIzfONhm?Ha|jpVm_WK9>l=?ttwh zqWazvudgt+yg5)YSvEhGSWP6ofq2GY)?n;->}1T7hA`z1W6#qvOI8!bb1tW6|t^rgc|FuCq5T)EcUW`--tIlI{W#eap<7Eu2Q4Fx4A^=uRMKTiUglxF7 zBU_R~bZ>DS91DexpCFqVf-k$Ut*#E=peGSQT-RIF`jwSq$8P@iP5bxn^`@uy&OAY1 zUOoeK9=#k8%z87F(T`)JD9XNur>Eo{5w754$==dO25a#fqP5f?{vrR$SG>m`cf2`0 z;hQtxoU?&i-^vSZ@X!K!i2_>WhO^K!Kf?_ZsHVFsbg0*DH*|Zade=BIJdwp|dr%+n z_=rAYjkY7>55O?wSqsy`MiKI5D`E(zkJ|j73rP~$+z&@dfjBVv-vUK^JgrO71^cXa z)Hqq-7BX$uwh|w$)4zzBlB0^mOpUs>D0}kH$ex4@A35PqZD9Ez2@3tRuAhwi4<+g= zPNG=VE2V5*A8#|177B&OY1aGD!|AQsw14q+6{LRL&gYQGa6fMHPn}z_EjLl@vrr_| z%aG#GyLk`WDKy{)DW)r8#bNNeHUf*bu8xCq-kCEO_^CGMNG6G;pyTg94Hax;ReULZRRLd2zTy)(!eldWYs8=DJ&x5{vjzZ~uPywO9JR z{$V#LTIILQtsm1K{k-S;pfYxchVptEn$)ppU5A)@$j<#O)-gc5G2GQ2q4H>V#DazH z(wYqyN|wr}LKvQA(gCxSkCzQC+CL_@ha$UF!}9MeqFh>yHAdp4;?w9omd)4W-q{XR zj8nouB*z*?4?|$n&K(nVG-<4 zvIIoSu*1f*r4ss?UH949C?=~FyaAu*TEJn{+YPCf5p)8i7Vd)I(Cs8$vzp==DXwWW zfnYrEm`S%DByz5o)$)N{0U0y8ti2a8Hayi_LQp*Io7~8vhtWmK9*}WjFeWCp_h5)< zJWGh%BCForrL~l-aEG8iG10NRVPXj%ai#GYt=&3&IFiXC{y%VFc+}VvJvP$D_Suz3 zGZ{N5yr)VfmnDsWP59{BREHSw#ZJ-9ozV-Bog_SPXlx15%6D)tui|{rc!!{CsA$Q? z!!y^iAcF0NT=_#FUH&8lTEZI`D~*86f`DmL{3I6x?}A|McHCTkwYR019C-zI0e%~dX; z>%>h-=ntUKm@)!XsqW665XY~V*r40$H6qiUlf&g#zx~y3tdhUu7+_7&w`9t-#H5yu z!e_2M{q!|93^0Ic>4;opKe>6)bUbZ&tgbglc9cRsu^&O7KY>m3Lr_F0OC&+CV2~}k z5S9f$$@r=K?~h#mZ&!|#xKc7R3b!*K$CN>{2$}VtV0STgmlDs_SUu%irIC2L?$*7) zD&VomXMY}QzDrsrbQY9XPdoR*dNLy3Xry{|G}6`s9uL$+E#9_>o>j=4!S2DFx8j8m>o%`Gl=+T2Ea33wSX;?%); z&>?7K$a>_y1LBUl3iSG1JKX`6cdEM9#TSq&rv#S!%`2<>qx#5lVzrZ>RYuZD+sPcb z^c!;?2CpqF9GIK)r`$k$nHhUs!=}Q!1Iio~gFp5fEh4)uV5~y{yt2@@`%q^io*t#E z;li*ntV!X%&`vt zfa~JyOjRE(V%6HZmrIlriHclx%HXQiB6otd7W0(wx;LFZ*N^tDyzmY{=Q@f)hDo!LDarH#0{zf^7seg84rvhABEiHFd0ZVD8yhrG zfh-z*-ekKa&%ETQX_n}BYs-7`ft{n7P40|0z zgc7C{@zkL=6`H$HVn2rwc%J2%eA%@p(D@26k@%$i}iXL=Rx$Z zt_cunD2NVrYGcXi+k8L*2dCVJnUYbi`!X=PR>{-uA1Uk|a|f0R06q_GQ7|Dc4J;Dw zxP*`YOF<+@dDSkvAnmeD7x1eK(W*c4?z{YBAG5sQ_TG4QlT&z<9XUcHOrX`NWcyQi zT*S{3CPGGfl1TeTRU;B5dUxaCULzMN1Zv~L_cfiA9*tN=g$Z&WCE59ulKx5RWr=R~oRo*`^ z^S}c$9TmogCmkZ!y+{=TQnNory(d(&YvGyr#5IKx;HO^nlRL#vA1G~7mdd;~ZBCYV z{%`DI&L4jG8DzN2IyW0*)1VJy#Vsp}vNAz)uv}2&{s2xSXcS?j(qXFF4?T}Y&+q8E zA6RUhS-c?@)$||yL7$V#qQ08i?*s*VFtzFgC3__G`oBGN<(rodzRD9OU`IflxMs?? zYxtH7>;_-tIdG`q$|2L{yBo~)+5>J>F?20@LyLXbgk8LN^yn?Oc(>kq`0%AFNG-O! zPG10tWBRI%IhD*_)ronY#wrO#L^b%gv1PPb>_45=xiO{t z`nH=-odToK&0Dm$G2bs3No6IUp@`7J7X|?*kj0OoZ#LLM@dQFB#oqLs9(*!+-B9uL z#uJslR;}J^KgSS4?43OtV=}9Z=56j-nxpD=Qf7uxb&1lH3R%Q} z<_S>#FR1*7!+FEpb$DBV&92|ppP3__&ge&m+wDGi@d;VerB<{)xl*aeJ-&ChymTOE z`-W#4@=KNms40qBtbn0QtMJ~_cFA>%t*jNnePIfOf&4UW$aiolT)dh)|c{&M&84$Vk@VB&dE4tLreQcc$x-A@?j`LD6FO6T?0^MAZGrY2c5P7@C&fVE8QF_)yJ zxBY>1dcYoMob^kwNFkbw4K0Jj44zvv4JOxAyqsUovAwP#87pU^0)UWJ}MvnH{ROGqWQc z13;GW9U*kx0|wWLN^G>q;?nf+s0Xd#IVert^^PVp=16V-KdS3j4{>4a2JdYa7v{-cM46ATgFtkFyS$WcoAp~ zegdn|V}=&m67*PW!cI$qd#hCjGXq=du$S`MWIxZyu73Tkua}-=nAcQd<52M-$odsw^(kLMMU1uogVb+V8DcpEy|dUw z@-{-{==|0GCH^j7j4Ypxl+V38l9!ryOMUOY`|c~ue@xXsiKOTm$4T}f1DkRa z$s<&IrO9yQBdxLdkoeQpx>xMLYpC9LyP117t7P(WVN6Ef;;=OM0tyn-Yd)ok?k%x>Ftk&C9#|RG>gP){LY#HRig$_PPlQr<$;j6ai2lMMA`ilt(4c z2uUhz%P(xdA*7Mqq{VFhvAS}nbb5CX_*>4O&(>lX$M=zsj6V7JpvJWUInMRZQZ~wv z${amYNFVTcJk2mzt`f+psA|a6>O#nB;`E-qVG~^&mke9R0=kbi(1Tdj?lU7RQo;3s zV}V9N$U23`hciSs^pHBZ39;$6EqV%hK}U~t|xyR1O_pm4NdgHi*Y$A7Ss6m-Y z=1ej`c;mvkB=lQZ;LXBRW~iXAbNAPxNh|;z8+wxmbJ|O+`kkdh(!v6;TEUkyd-bJY z&eA91+p;^dE4bLiB zNf^j(auApgqMahi6Xnje+{VMZM>GIWJZVYur>3TE8}*KR*?1=L;+Y9dXi4W)fFs^! zoCEP(ed+ysF^wlKA(rqU2;UXqSP%CsRB2c`5snkqGW-rP*#KG&SG)AvzwK?=a&*fZ z^yS~22jSa|5u1(1@E0Qo`KqzaJO0waF^o7G#e3Y0>O*NHQuQQ&N-E(ai8S?}@u;FU z9O`eem%>Ovys9VWy)Dxy;`e6&g)j$`<9A?s+f>TWPhC7Sly@q_II$swmDVdSN)qA_ z3B@TkDE!!N)}@N4Sd*6a<6Jrl+0+s{0e`$wK#8psJ1AER&tOW~h&trkV^%Z&Vk|%e zs+o2}t?B~YgL(PlJcSww?z?#0UdLL@>O(GKALlDjSLCG5<}&bY;U7 z&;LCDB}2F(mHg=WA(Y+c#?QDko!8LYBt3$^fHrnA!Qw1G$<@^ptmKW4W3#(vF?0-HK%^Vkss2Ms0yQ&cZ-XkMj!#n6;b7(N zu=egP6SY9pf1$_H5Z}LoS?l&@<6@)_hz$=3w<$3jB=jajpAXl&f6qHh@lz!u;j4Qi z`Ls3SHq}0J3qLvWzyrsor`ZmG5~a~NINeHlIR3@u)72!-BvBUq^oC#RM%<(tu&lqx z3Wcd$V4AA_TofBEgzdj>E0l}~2#D2zB6FhU0lSrp&2Bb8Fut(hEiN{}uV%9+cC}^2 zlYl5x^?9&3g4-#xE1yWQT5SQHbG9zb;g4aR-he}y#6wfF8k{*SDT#Y1*`augQLf@Y zk&13KA9XT^&$ZS4ML#hP?{db%`8W4IkhYQzpZ&io0HSH`BbZK&peh)atCO~;(%c)YDC(FJ`;TU0T$tA*jx!|6T@gh>?YpkRYa!dTs> z+*HE@x4dGfW>itLI!V5I7!SXW9iF~nVGT?nc2`KQL<


kGFL!>7Px@|a zaBAFJ1?0=ypP1BJ(!}J$f$niLlQGd09I%vTF}T32n4>O|(a|CY?;W8>D3W>u|9sfI zbhRDbpSU!ZQOI+sdYn0LW@gd@7(ZU}Six#}C2w->Y!VrJ|0y6;?7k$62X}M=hI(sy zaKS#H+gJvyf(azyPv8w1FlimJ3gI5%!>P1@R01^bzvE`{3vJ1-s1Zz87^>>04cm@6iP$@6B==MOZePMaT4BKgx|{-L zWuw~?F?2p+`iFgTn6T)Ika>dcy^npOlm;o~-ZtP)=_rTC8d~_e3Qz!>63l14S3OlxU^BBX%nx*d7P{}JWld{S?q zJuj_T)S35cBK@RpKWi_Qj#E$6CA75s6rpyPTvI&1ScfK__M?xy_{>LdyX{0i&)#L3 z?Gt56b&}8WPl$DTX}S+Bp-}5Ru?l4h%SOUZi8}gjUj>Tzz@}=)m~D8qjy+Y~bl__L z;^M)@7wOAOAaA!;1Jvy+lanQo@dvBcFr^<+5XAlwvH$mO_zG@#W3UyWefC*HKn8bXmkEFp?*=vBxt>wXCLMV>N4CgwTCmsm)-@k?QGNI%HsA|gGZxGt> z*&(o4-)3kq=zxhk>Y)?vAHy*50U)|Dm1be#lLeI0`XVf2rg+xOI5jstYz0RAKH!i_ zr#$ae=}2{<6fJZBpy0lZsG7F{L99XGE{L8-!ghDbRYSWMz&r@qX@^$o)(OSeI**ge z>>vQxsIDOm>Oi6?7Wi8v{OE}Y(KCVdg|6z=^t(n7Pe`F#_Xc})#?Vs=NCetm{9P^Z z;{k^By=HMM`>zp zUo-3e@a&j3j%sM}Ay7YLSQ8P#(4HOj6!U8=Q@Atv#F=Gr6o}aP%Q$1iU{puKX-+K3 znxvDN5a2L61C3^AHN+xx;U^oMFN{T-df1moX7qM@U|L^EC%HE7IZpc7%iK>}M6rHz z%2TbkfOO(c!!zKHNBy`tT6exIC(-~d6j8=ZYtXH3#<0=5r%u}pp#A8Gn(>uHF^dyb zmT9 z_1~%0!ED8Vr|1k9ZQgi&b;KI5&^}W^2amefH34PBjCO!z5V*@&zHr>n8#uS5?nzJ5 zo?*#A=86k<<*FNMVD<1zs1rY2x~6MerO7YCEAryLcK4{~Vx;u0{reYM6Q}_3^c$+> zW+LpPU^8bC^syj*U|2Db@=_tC zp&gIOxwHnjN&6e7Kw*ZE4c?+-S`dla|$sV!dMHJwC3v3kVQ$=>m z^7nt?sWjq2E7zZt(?iEvsV!*;TbM%l0ErAD-fKa9jrYzfx#)B^=pet|nW3%TxZSj1 zk2w;^QYUZC1lktq*yx#z8~2n7a@LsqRH;ni`v+zsYkM9FIkFoV>6sOgfkkUXr80P_ ziT%Y6OW-~FJMcri-QD33zI)w1Z9uDZJgO?EYH3M4GVYSkQNp+V3Ha)%MzzO1pBLzEtjeRyUu^=i9Yf?Q|2Z;ISKS z>MB$M{fRk_Vnc;kkunSS7s>&w8qab)XQ%e*87pU@dc?Ktnwq1u}C?gaFf1N^)3Ka17f_!qtty~UX_1_G|e!5?0#UDeLA)!0@Iz*N^r$(y5h zr6!ENw1d>Nl*;!_ZPqroQhQ@tpbHymy{JCtD8LpNIyz;2$F>|9z;Sdx8m6PHItnoL zz_Uq(q>*qW)sq}>Tm~^fO*9B74Z$I6pAc~OP`QZA%(}}+RAbGMX-v40w2=1$IzNfm zZzjQ#BJXA$yx!h(%Qp&`U^e(>8p_z%yNCWjum1eP?Z0UI?v~E(0+!Tu7i@q?hdH26nM>ZKW3?&0Q}fClbE16cO{PVGAkBMf z3QZ1{A=OpR2C;!9+xHrqw>E%LC_$rz*pF9*BAp96hjD61bg)Nj@EvR)d|Jpp@+f58 z5u;w`+lFU?l2a7HITgT_bFpH?NB&L9%eBhC1{8Ep8s*Nz#}g~@qF=KsX}_d`>s)a% z#sDZqWiN=&N_SVQtdMZwUqPAasMtxBad0oD;BPYCpVAapwJ<*#$r&@3bgDDMHs(`N zd(qv%SbP^|{aTxV%%5lbLuyi^XlTV^0`KjGRur9?zUlQ@gV1@7OYG4|ejt zRfaTB^ZL&au(ED>AoTuvxv?;jFSZQ%`XEftVQsEUIbgLK&HHyVtsiq1&N!2Y^b6+k z;Umt$f!SHUxQLM{xr`PCxKXZ`oxv*bh{IEco5@R*(;xR8c7@lWoEkDsccd{s1A*gE zl4}V;BNbezv_Rz^v1zy&#LXYt8Zlxmga?ZKd6O?-R+ZbBlnMZ1)%;||D0v=+@3K;a zHyTBZ!<0S+>Kxn9+)_QO>&!&d$`qXy#%p@Dv3TNIHm@i5WsmX5zBbwP4u~ej>E0T&Ypcx!M)Jno1fxXQRKc=D8aS1oi-A; zB+^aOoC-9GP^{kxWCe%>kh_PiFz8*l%aZR2Yv1L0y)@Zzv$8^7Z ztQ7G;IGo5z<@}ox-vbs#kk(DI*>WNZ=v)%}W*(7^JgjoL?A9=tZ;}D>q}LUu^h3^k zA!jeFnXp#aXtS=p$#7xO1ez9ZBAQvnN{V$w!UzK>$Fc4YV;oNDidQNvEtN{AuAo9w zG(nU>E^r`gS=o(n?FCkvE3r1legy z{LSC6gyp0ygb5Mvt_JA2T3vnk;q~nxlO+_J2>Y7zM4LLbQ{TP{0L<09)F;Nhq6XS~ zOgK_b=)*H6CT8jC);adDvy0!u(7<4R97o+71eBb)mfMWh@zqm3-ATg7jj%op(sGc% zlN4*-b+4j<+|&^#=^=E7@Y1h{i1>Q*+UPkxlv##@PjUfK3`Ml<^97 zjQa0swb2`^d@!1JQjzHgK+nY`7hbM){xIg;lm-S}HC|>XPAq24)Z$DWf=Gxm>R(8} zzwY|>VQFULwWf$J7(@8hYbFDtQ}aLQ!;AL=mmLMHYwH`M7A7hl*p)(qR{lLeu8`_? zwOGpCy&D7r#qva?{}@m?e!#6=*feV8{2{W8mhkk*5g?hz#?eIGPO=yU5DR!LOWJt6GY-DFgyd+hRZ=z;6GLibmlW^Jt0=muFQ6TL z!e~v+yOM6;kQUq^otB!l(tw2wMe@+Ti@>Wb{q4N|5_^dAhaUQpX2L6K*bL)b7|SlM8pi4vP2VL$|NR?2z_F$nrTe@j zZSPzTaofvfPTQEpm{% z?B>;LJ6g9lHZJaFLZUtQV1|K!Po2QQ%a z;LxVGU2EKOO$yR%e8V3`fk?--mhmiqi&P-%)ycbch;_P(jao_9v{ ze!cn(yU5tZi_cFGf>+Y{1X@FC+xMy3D$@BBs8BQLXGH@`*v(H~(MD&x1FV2>is+Xc zuN6kdzms>FHorxx1jun83_gkam%}s=h*tl^JK=dJPE0yZfft|46@jdaG2SQDY(PON zHLd5-+mNpid1Jfg>h+36i%SynBW&IP44cULh*h1URV4R_{`6s#0O=4hyc!EE!+CpN zJ@OlS=jZvBAei>S(x38Y^iei48{G>o_w*^bFgh9;`@mu2wqu$_w-kU3xq)PigS7ov zQ|}+$t!=XH5`dND=6?rgn=I`*KzGAFhxa4yPV6)7`-eUJ^dID^)!^8bE6A`O^(&R! z+k==dHncSOnwxP`gaK>d3%o_~>W6dRtr5^XqU?I)2*;Gb1K0+db^u${#LU_@0^c>q zn7qX>0qXyS79{d!SxPY=pxi`BaZJ~-jV(uF zz^7OXX$YWhQ@X=j4DETKQAlKQNEP_l|BNNCp$%?yEdkQimfic-6Wzg8(zbQRxrT3UspGV^?CMRxmcO|V+f3`iqhV!G%$;lnlAEz(Z{5bj^eAikq z5^CYz*iVd*yLcrrtDerUa# z-^wQw8U2^QZGz#A%q%@uF{YHx`4hMLbJFXticX!6zx6fDh|lUI21v*HZo{Qga0n#- zGR`N}wz{9+)AK?G$;qH&(RHM-L+b+26^!hWxEH%{HF4>7<*6xNmq&x;Ow$+{-mFGD zOVaA_FmMd$0$E)he#41ct^pmLlsO=09+0t0!Zi+1eZa*jak^gIPvHgW!r+FPYjG!Z zE<|c^t)AS$P0??Wcoa!nQnlfX;ID#ntz;C^Uf!%9Fst(u5!3ggz7k7BN>MZgQFe!J@8{!vyr}0r z9;D4;wsALbFjCVko;XlU+0{XU#f&8V)bOR_1HO}KW4UN{03lRe1f>1Q=841sLT)HJ z+OTk2@%)rY2!Fj{yYc&ncmx9=HEuZL`siq3LBDaz0}tbZ7fJXSCt^P@M37gNi9a`VP| zRjbvXLR#B(4~T!qfT=@`QRT9&!2hG>O4`matJ3vg!+2?osaM)m&-|GEJv%kM- zWIZ=yR@l&3Q=ZyN~HcuN{E>+lvlZI9At)U~9QwT8}VH{R^i;HvjYFiO0Fk})r1 z0(c%6{uJ^^_X7Y2E~>C??mM_J6UhtM&TcZl=hOzmoQ4973qv!~>e6b{t#R#3S{lQz zm1z7O)v|LLCZBCDFIU;4tg*6Ebqw!)$-TNr02}|Dp&Nl6H_%^F_8Seq>@xH#BX7kJ zTvIhI%dIMg4JQhfem{c3#o4o8c95K{NJ_$z z(s5aba!I+uMIr>x2iMg|#1o+$OP`3#2(u&X5q{M*%^~*mqrQ6loQ7GZFgKrQIQh+c zI_Ugae0f&dd^oY~1Ia#NVJ0v`zAo;cK*x+bMS%jwPYRFcZKPpD_$i)p{T;6 zB`R!UE&ud|r=EKH=`UUakvh;3cCw})%?-`PcB{q8+pcW`W#gI#NDy>MSGYep=`gNt zc2DB@!cy962(!b%%0e59wZjG?FYw`xN7_7k2jUinzc>jHs~6?dyb6~yK$zRl z9NRuIF|qBb96)hasa!Hzle7K7=5TmDoh6!PRbJN=SQGKc*lwX1NJG#G6MtCyDSL{a zIWw?HpR4iKgt63js=^aEGAN-JA7hD{l~)Wa_%x{Y#|K?XA#dsfQ9^4x9!JlakIs#J zGwzk`qGg!GuuQpAN=Af^A|P5&4?TBBNFKRH$iIZQfp`K!PH!O-;(HI4ER!iK9`lYL zUrDF0N>5Cx=900uxm`UnWle3C$M#*mym#-hTW4oyugH_O@|LE#V8sU+wxLE$w6G>9 zk?BGoqlrjEhha|FJF+TKx<3Vb`YklxI_MYb&Uu@(qp@w)aLHSn9ca!%_T*)^Qootyro?tKP*!8Gr0Kdwqlu@t%pMi}gm zSd#GBL+y=#rO`b7)ZEUU0MO3d>*=8RIN@`}c!{mBnHjbMM4Sgi!JwL^Ol%k*et^## z>Cz2`M~>XMp8tWxMU~-SPOXOx8Z9K_Zxi4TPw-DaZk^Fo-*AxhOT;QTKuqGhN?6anh08xME}-9mMcqedsSybv8vt zjFb?Tb{#t*9)E4?yUX0Z*|Sbynv;JpDDrtgvr7vLuWZ`1t|4_V~7i9eqQ#(^?t5p=obkerjVXJyOPbW96p@gFXcbd zFax8GIakTt-o00^E-#K& z7r>{`F9Wp^VAxMz5a3^&Lh9oG=?%XO|Jp>_O#~+41w$R!^=1M|#sP$iKxE&`k%!t- z7AgYP&b&!=?3=&yP9-&>WfGH1e)PfJJ~( zK;j1n1^Yy*la3(0AC+Sl0OC3;pRatg>U~7KzNqN}#WisUV|Ubf2W>#YJN-M1-T5pG zVh_^V(@6-N*Zfn;Uci~@CAfNCU{@;X%61vUI|1X0(3nBBfYQy;C!;zqM2kkGWAtHK zTl3Q#(frAi50uNSB0Jiok1)unfuO#xZw}NR(bkyD%<2y0^Y)=fnA9buo4y1~w${WI zZWMX;Lh*#U2sald{M#Lvj;0;O?F1JTuAOZenWJ)n-wmKkIj>`ET6K;BAZ3{-viS*WC{#x$PgB%%ur^rq&}<8F zAdfgKsI0m4U5^vD8XUB^a?_^FurppLm*%y;^5Xo-ljHyXaHLEF#EtsI$kiz>QQRs*ge`s04 zm0G(;4{eyCAJF_q-ikL1*5+uIk3_r=nyg%|8)-yBHv0bG=c*&Ih?UMp9vRNhwwYa9 z%?;)AabUics#`dJJeRb}{l%?vB$ASHS{PbWQy1Y+C=L^Z%TwV^{321p9`;axqFN(1 zuV)RrUKj>VR2WogtPly(KyDZ1M&;B;o^r6sI0b!jt;@){~YEtY~d0F+gNQ8JmBMSe1)+ASff{=ovI#8L%g#3 zP%!zMw@d16ji=OyDkV&{s@HF?)ke5&gO|m~ZrY?PaBF>|aWIb;E>#xi<8z{h(J$;Q zNh97O;O_1*PKR)GNtJd4`W(8n8M3qp8TqFFqJP4TCdT9}xTvflI8#}(Jfv7zm0Aw; znNsA)bYdrFD}L_4ruluzxPl1W!hi;o*GmS{!Gut}I}*=z_|$_z7Cht=0PH9E7+&89{~Swp z|NPpEd9dJEg<8(ZEo~+*Y|~5G-f!}o#cxE2{dWj*U+iX}hy#_zG76=)E~<1RRDf86 zGt#ZRno_oTm%Z`AWxRGEUDy}npsn%x1Q_qd1bjj_KDmsxpL%g9_jeGcrm$(el#&{q_V<%88bxJK1o8zWBHSB9{ zmiPC+kI<}0Nw+<0zCRHM^;SlaRO6&&mS?FnN~M;ciK-M8CrOouPbq3rU5S9iG(*!L zP@-lE{P%>Dz9{ebmAxw~H_83mhg~?;7{9Ivi(q zCcY1%B;RkINRIobJk{`1zeX5$f!in7D^6 z79;|_!@WeSQE_nyekT-K2>E`an?bw|{zcD5YHVY*h`M7c*RJtfZ@tFR4e^{gJL73^IcMcds|HADN-xum{Qs1_XLw}Sc_zpTH|JFM zR?ZQHPyh-!qk(Rqb8dDwyPKVulSGjeDT+}PgDr`YNHI2}M2V8e9*@T>d+eQ=zx&Lt zclTMR@!Iov#!Z+fm*uq zx5SyJyL`*ls&~7#71=0BK+F9K3W1HHfD-hauO!`0A>ef?eIXr2r7y`o9N)hr$RGoE z#2nZyL+aHhZWpVSN<#(WA!;nmYP%o|U3Q{C!UQOxm)5GLG$B`#We50_uUJCH7RpxP zz=5J;`VGMRLa|OdZdNS@P20_njufrLTs=O=6Ffxdcf5vhPMDuz@)WX!2%c>*eS<@w z17Z8lY7*&8>?>>Xom1MuOBG79&ulsIvC?O^%T8i)77^Qsk?kcNu=SbY)CPTRMRtqRO2sZ|ILcF`~+_rh`Jzf+*grRl&4OR^sRt-~d zqCc21Jf1R275#pohBgVpFAF0ZtkRRu-8zo&(2Tq?X2eIkS}t?;c)eUtp^Q+Pv^ORv zkHMHVVzz>^ynB&XZK<(>X`{C_~Pq4?C!%CyoF-pVw z;MY0T)Nem88J-DpX&Y@FAv=vXG)=-`g@*q>{m=I8;PQ3kYr14vXt%ky-FA4X(p53i z4t+b5#l^$74J<5Vpq$qh^0s4r0cAYqCSaEBVz&#>HN)RDHyW{olF%ECn3xhl#3quP zHdef^))7172 zp=h{GG0_n2{5^a}38#s$cY!vLEI3wtNp%Lji{Qg~+#ECA$?Xa`HjGn|sGpep6K(!} z`BNRVC|c?&f|V1B@S6|Ep8taIh~;kxQ}xP_yyTe?C#yH@?tE-8P{&XtW4s6Wdn=AP zxr&5I1ES5TI-&-^u2Up*(JDMX4<}`KIZHLo(~2M{P~4PY{0R((B(s*b6O3C?MPg|b z+4XaqBx>+hPxjaKJGeyr!WI5(q5!zbH|>M`)uUz(;HyatxkrS4G>|^;@d*6i&a#|G z?|srn-8M)0fdT-4kmDqvusB^Ua0SL zhtocs#Qn}lc0V2`?Tod{*ZnV#T=L0z zTPk0OLy{4a^!Hf%GO^f>qYM=v9UGD6POGdh(7xw>)6Ktx$Gc9iaq`(`-`IsvcLp(c z<%%F&Nh?+S!?wA&%J*T$l6%=FmOkIYxdGMVuyAO^0eJRJpQq$wMRoGeCyc^b(Ogt- ze9U>+0V>|Z4}Ud*qzU~u6HJ~H#dBps@E`zS8otx-SfGoMbI_x_{@1=xV%^c?&>{m| z&Yw78cRHs}oIuO%Cw}L5GXL;?{0ni*E5uq-^hIgt%@JrzJcm$j8I0PgCS zU;%>h4M)e4{NI@@CC8SDp6hN6xD)$zH(it)g~9+PH3yzm(a?{fES_}s+4jDDuk*6$ z>oV_(h6%lm{uV7M0C33Iy{RahLs_B_M8&?x6gC@G$!>=9G`pLgD6E6Ep_L)|m@u3z z{kGVO>By_56)`p_UWy(CUKkL)Qc@`?mo#a`C@9s>+WBTSmf#dGuVcC!Cf6&xCCjQN zs)Lv^1kpyf{WE}gk;LC7&)(62#kbUM5RMvM9c`8ejq(Tn*$%9FEQA%i4NgpO|T@!@4NcY#bd{AdDf7h9+kcQ z`svn;(#lO|I=l2kSB@Mx-%$;BT|TOI0{K0`HUWEvc)F=0!JdHG7ZXs>X5u{n8OcfI zep!-a(2Rt8{U->t?ZU9W&A@!fNpeuaXl`GDVVT4m^rSe<1P0Ptm86{BC2#=dPws9S zF%0-+!0q|HSuMQ;Lpu2HzqsK7 zobs?Rgi?vuvGE1Bh(=jFzVNzbzBbpoI)H13BOI^6+4#r_(f>?m^BG6Dcx?CXef!*q z30pzc9IGz|HTjZoFksvyKC3l8h{~GEBhu7ux$M6JeO28nyEw< z<{evsjbEQhB3XBbQ;6reW)wLccMgK$HJi-NPW@GPTAZ5kWMvUul+0Ur34!C=yA~I;8_>dwBGnq+$DU1 zB&wKeU$pHnjusB)4(8i&6t)@Zl+R#%!%~;GQ77A%Mz|zVd5`{?lE+(&@@X+lBhNXm z?a0NkPj@$Gy{1|7-Jqp4f|-pj3Z>uXho8JTu-nZ=VC16DG{3r90v=8t>0=6u&=pBf z6+=cNW+M9+7nm&fE%5L(Sr7~4%c2R_FXU=s_=yi@Pasz2O#u);<|^UVa0f~gOv>n)kA3)|{me7(Ij&)fF+)%BE})(UJ60IACj=F89Sqj}z-p@* z9fXI)?3SLq>TyCSqv$&DZv>2|8lI4+Zk|*QMD{lKy_d%P-aX2&Sd_c-(Y*V}BNtX8 zU+1|5>YQmvEEYCA0^qSqkwe?71DQ0&5UXi-egG$eV$0jThFe7yVO)!-hm`TJ&4v?i z{Dv)_(CuoxDMq84#>y&Z1_tU^oD>=?%2D*3qvoJoew*_?)`GSgiMm)4IiY)Q*iV8f6yVp%Y*J~ckKg`HFJKR{W4{7th2Fpp zDbIe9*rhnqr-meJM%`%7Nh5_}cjA6eT$39aV6Cj7gSo02DZe7+oT}wc8+={8;&7v? zl~IabR>Yt>JyrBgrJ}q{C4De-fZxTLF+uWNS(X>aVh_-}2h^ z4ZN>fYTH01t|XsEF*5pZSw%8MVMu3lEPUI&VS$7blLH}Vkc8OMk#t3p4m+Rh(l~MK zEPf5LSTb#(fliD$le#`RH@Ciy9_yJIG^sC)t)n~hS&&*%tzVN$KHz(0IcMdd8_7A* zp%JljfPjNvEoH78@)%Yp3DA9LVu`b9Q6ZxdTcE3&EwtqrYB^@14c}^*D9a1pMLBr3 z#M}`AGS|x3M*=63I%JphppB#XS6 zLl^)smw2In*jL;DuBoGHBJ3cc)`9-EvX$?&%0i)g(X33!!>5~L^2pGE&qfU+#btOd zGHTDu9jpG1hP)bPlVc86Iyc}G&~l=oI6@M;LgR>j4sB(zPy_ zDGUH5koVHOj`~@Y)S?S9Q0zNx`_4l(mDJe<#CNHrTIKfI@Ewqyh(9Rkati4VU- z+LPuQP#U(+B)IGTSuiVy*P`=zA(y{~c;MC20#Nh@t!>b-oEyTsMlIO zn?7aVZ8yXbJAdK){F$IUe~WPU-7lD?me$Iac|n%fOv{EDLXTZe3@x<|^cjJ2eIG(M zy=Coh1u$p<;UaK06z2_}y-g$eex0oMvbx>M>-HE1p(RnRo$q*B8HuZ``zP*euDfg3 ziCwSJ%dRIgs+oocQ*{WEya^0uJy*RX%a^KvlVbC-C~o%?Y|5KZvSD16@({nm6pU^| zDT2j7@Uv$qipdB{vl}OfGQ(&dDf^I-K&Y%cM>x>GVN5i3`(foj3%RczvgAUMZF*89Mp-O1QTCgxbNOkr+&>f0( z=7rJWM{>Dad>dU#8g-gN@aDS z-9zQ^;YvdL0*fIoV1$S7Y5k;41uU~i0(U)OI;b|d7)#m}ly%|pL}apwn<>S9m@BaT6BqfbF5cY4BEh`9@6pM1eYW z)yGxA&`tsMEGnwya^+4M*a(ajgoK@GSZxCY-bJM;KZeME2jYGkE*JY#Cx!u|Fh+$; zr5gYf6Dz|Mr)Bn^Z>OgPGw~0;BF!a`AadfjB}P)FRnQAg661BIN~Q67{h_AjaAUST z8n+Wg+mAc;vNIH*9mP-y0;E#GN-T)8A0xKdkEn#5GqjGKPh#F3Poe(NE&IR4LszaG zJb3EVmA|=q`t+egSFi5lF&zBR+;7m!SB)6rz-ub9x%iL1Q6kn63^%Id30|bxj#SM- zpkCCUFh^BJJ1rrS6_fNNQ>e?N88h6dO#z$2NGl{T%ru2b6za=C6u@UnBNFhO?Ra&7 zPk6OB0B6mqqix!rPpTzvDAj49KZhxRfxERsgz@;cZp0FxL}Z zKYTit%{o_n=x_C`>-sW2(Mj;W6LAxYA}3HL*D_kIG^r9!&w038}5;lWNA``4$+ z9nK-np7s?K2ubOc_a~;QeQo-`Osb#9{IPga+?z2tN1AkypPZb$V%u-{7_NX|g4Vnx ziBPz5_5w=-=#5Vl;JUOWyDp+S>K!#MW|1|~wB{fN>0n-` z?g)~DDr7>~C0_k8E3|8|?)v`u(F%Iph*jWBdsT?&u6^Sy=#DqchcxxM@569_OLG^5v=8#eoyIYGZsnBACbvI$vRUDzJnp zNBSLj^Mr^TD)><-sO3<`QIzO9p+@{c*y0E`K)%!BXc4qf`U+xVX>lJtJ=paro1aoD zc0QKnGIoJCK-*?2IwHh}GMS;w-Yi}YJ2_p8XLQtfe@)OV`+ituF)o)ztV}61+Jc<& zMR@|02kBy8elv~KiTW7DLk(2=e24j2-bdWXP@XWVBBG<_HLRNPZ@Bu96j5C#gW^;nlQNlw{4h zi;M1}I4KSYi!Ia&o4|q{;2nIP&WtT+ z0AND>lm|En-(WZNT@13m#vd-^$H2W%yM{iUYDwEG4{WWIPW%VAn$|@Z;DS5`dZu`V31G~X~p+dw7V98P@IeI-W#15k;cXnyTlD`c|Dd*mDS;- zkxZ?B1^AztTpW;RkZcCVmuoPVY%8t#HV5o1j6>ob0j8q@5Tqa>d*pk#&`duB6@=+3 zA7UO9$%N8YK!3~f$J@Q67!&kQ$!Gqr3*yw5zU16>*Lw(96u+_{8j2iK)%f}caFn!0 z%TTKX*PStI3P52+0;DBt>c=Se60S2+d)7l}I$S3unRp}-Ng8FR9{i#6`%ZSlSzkW{ zW7W9|G>{Dy=IQ)z(#u;YvK%bQvZ<^oHb6n~rKTuIz$*ki8sg2o52&;X3#&78Kwx5n zIADt(L*`?h<#nGqyD(8FFmeL&a;wZW%utg290PvtnD?~BpS#K%hQDa1N7}Mw$6bz# zq@rmfm8dJ#rQxBW3~xYA z2zSJ$1`(TWr(b=g%ynK{nrz6e(amLaYRQ1*8A$tM`@$q(fqCyYdArAAGhO@7k( zyz_6%mRB?i50tZNwsfj0cj|_E*i0PPmkI+@YNw)eZs6^5c#pra$Ev( z2$0C2uw}q;I%QyL(>P#RHARb4axn#C&A^Kuyxt$cwhnDoI>RsyfE6R7@?G4O)U$)aKpUl)u>_#bur^b29=7ESe!zXwa~72h|Iq;& z>)JP+PdLse*yBnT&>yYQy%U%qWEbv|<-4+40}(Y`%z!KYGx(m@r;*q;5*yG*?(D=O znL^LeYuCK{@%Xa;-;xX$wA3w^frtygs||>*xw!#);8R1&r|?*wp%`o_9HR12XZCZ- zbr2qif9G_OL#uAXQ_`Zpr=Eci;bsbYIX^oJKYvWEKPDa82nt#;|6m@$N+88a$-qoH z^BYaiOtd;lE7RVP0XCR%ooTFu%7}gdmx4k@eM~A)anB3eQATu~Yr!aK%zH{aku#>; z-*Km`?2VXCpq+X$XK^0Jh)y^g?$>N9RmFUql%-Cl)`kMivXPRzc6HamXHixYPnpTE zKan_xF3UKEyqcvC91luqu*i~Rkx+MHy-(wPF1;=ucol-V#gq^|lE|4@Sbt^$X+svB6rsPH*>m*?Y_piZ)2F zA41vPHE*Y3WWkT;=g%EJ{Bgwhr2FnjjCD;kxa-P%C*e2rawK>iG+OBffj!|y0r4=9 z1I0>6liD3gcDRfP=)g&17(<{MDzk1Wbn;b**c{YjvxiBFv|k9hgY|5CJs~TD%KBNs zzK3!>88%@Anv0c)J!YU)49&P|IX)|o@TIjp?1WLx&S;tZCHvx$+PE@Zvn$hDw|exe zC>!7rQfI$1r8#WWk`+>Le=3^6UvLd_L(>iXH$3kKUa0aZCRP z&2?!{L_?cXQaKcNE4r3O8KoJ!7oA{Fx&?a#<0&!Tx*f|PHSmEg%aUge6q0peG@mH& zSv#FXZoJi<8Lld|_6j=a_suUJ$mBW#9VaZ-o?G5#KrXZ2K(9W5ugm}jJj5Ey*U^LukK8flP7*LK? za#6UY3B1ZXiX^L?c19G>0Ptnh>KSxDf=3ocuc?`XlugK2bX{CLMo3V-nhih64$1)>2FBCr4qxWL4Vj1W|LpHQ+50=;jwnh2i=sL zQra8NUh!zUBs>>@F*Ib`LqlKXGjnaz@`F4k^g4Vb;__)h zCYWAv{v*R+mX8_6F`&nxlpAMBHk%3Almi*|OY}nC22tS#h-cW1gsk2t?_Ta@D`5JJ z%N-s(A#FBR(9-Vc*;(5j5Ho&00d&cIm)ufGu>t@@U1~Cnh_~ai*-=cnl#na~^H2Wu zbkbF=gTrFV!{{};iIA=vm1_bpy|KfTo%#n@?Qej!C$YcmctfixOySa|^`atS*g-R7KK33Ox3fJex4J zR|8dsn4nlZI0W$==&gPN3r;QBMR;>kz;lOCon+D!Zy-fXvFHSppn6g%Do6tRaTJe! zn9&A`bH}s;!bS5*NRcncoFJMe5Vi$4XM~BNAk+crgpfukf{1N)IFmrN!|&?diT}r# zSO(ba>WbV&s>v_|Gp|f*RzYwx^5{#Z!c7g=lwx~5?wD!`1?(0&iQLk`=n3FU2+Dmi z#?4`tIP@VXPfX_h7GFRqZpjkFo3U>sP`2lDlIsY|4l_D`;{1V=_}%V|^O7?cnZ7+2 znR}@LpvkI|;cJQ8Zac*nnQAOp#>aT!++Kq{kgN88;*421X4;74!kw-ln)u zA9ME4%*`E)%)c-ho7o>-LcAiDOvf?LSQgU!yS#Q%Yw&=YK}`bi1I4I$Be@W z?V!Vtk7$a|0d$n`ImpALa_GmZf7hfXl`hPGnkJ^zv{P_j(f#(Cjx((O!43b-BE z?4ZGVcnjeymiy!byZ3Bjyr35g1U@>Ae2jr8dbj&c(e@MBsAgi4Uqn#Qa0xh(AeG39 zNe4Px8Wmp$boiRwA~Hk(08_@F9h0l-GD28x3eq3CHg^uWbW0X(a$jstaYP+t8qh0J1J*y zoVVUZ!{B_dN4=6hkxbrY2YUnLCgt%v?>u}dfUuMy<$r-RTLRj8@rJb<_FkV=A`Ye} zC6$OsnIW)c!-Ygb+lf_zMtW?`sD?520Q&oLr9sg_6H9zA#=X_zNz7Qe1@P!;XGn9% z_tA5X)^hw#XF)_Woj6CyB@L~l!maV7FqB5eD%IiWrj-;OnyrN~3~R&78IeZWDS}#B z9$vF=!!!xcjI!jCgOq|#Af;6GO%Tuq%MbMI7`BU8E10Y=^vO=`WywGicaLLni@!NL zE}X&alSs-#=tVq}2j1^*qI2Fus(K#u*`dlMsm0~g0tizF^eT-FC3~$8KSbg@j1u5O znIQbNKGK_Sx}RuDO|?-}GkW(ciNA-LUqlamUD)D}*jI^~Rw4N03vj$F7P8-;jIo6sK@{=taJR4Wa-bVL~uspxuEn4PSXD7h~cEVuO%k_$_$9 z?Eb}Hq~3qudF-*TC-d+LY+P4v1{pRd;A?!88(g9W zn}hcv!4r&|cE+BLL1lgt`~Pzk;e`zuTXaRAOi}@#7`jJe-G8lQ&AeEt7mLoIIcnt0 zjGAiHlUllLH*C+Fv9l>BdrPAMQ-U(uzW`ts?UKhFvmw}-46A_liD{!9P9(jv?NWv{ zW!N=}1Bdd4zE7P#ea}rdee(8Z1XJe{-0*$(3`cxe6x`r>OVthI5IR`3Ng&kd_AyFH znV*NA3GCx8I;^Dwwta%J^+8o?CG=Fjf|C}dW9>0a=vZ5(3g zCras%u{hpT>a`QgXb`K(X#*&Bm>%j9ZuJ84us=t5Uqmn}tULtOhjuONTBPDQ><)Gg zFr#&LM)x3`tsBJ~#rVL-$1yBI)oY*HWsD<>!5P>1fhR88N*#>?D8?V*CY!cBGTa*A zh8hrZ1YY}nBz;|UKU zQ0;h0iE(>X#Z2FgVFzyh#s#!c#7+srYDGh#f%Z*!0jbn`ymwD3K|d!>dAgR5+e+5v z`C}aB=8D6ua+_~g;!DQC;T(2CL3;HUsCyB2w2g)ZX#JhoDG9Me5RgETiR)Zy{B3>W zPA@K;dS|@&2zyMI)Y8S8S$){4v}o9>)2*V-4l|{lZ^UKs)?4#>3;(K^F|CxfnuzfX zh}x-=T*Rms6i$-4Pg#L)VFLqZ|FI)theP|a-K5s9Ui@frFT~r^LhMvao<*o+SsOef zaO3jk^daQ)jy@QDC}KLp@iCE$+y;#MRfQs=AD_sC|I#4jJ+F4~%%eEC*%eoqbh_X&N^= zT1zNH(S7wA@{4NX42C9EUr|vi1Yl0b9>TnEPO8JnKox|OJ|QOzI3Pe2sWAJF0yWpb zW;DctWC8Oz8GDDFEWKrIyJ;lb$d99h5Wh1&xh)f>Oe3eJA992Op!}Q5%LYcDrubdE zTADtp-Iz?;8>WaZRJFcObJA+^W-QoDT$4~`R7!mvu`Z5RI~Jse+!)0P~iU zO!NFc%g7EXu8I)>x~scn(d~jkMMLWHusWzw)#1wna<;a% zCOcj{sqEf8~y^U5=x4nIs&0qvCb0UBwbks^WG)zp5NmbhwWj*O;Zmgg+OqbH8 zZbPaTaJYXD^{nG~8?3a2##Hqx-{IL{Wt2qNVO6qSyRMaxOgmy2V%v+z&oU$sf0$x9!>4{qxV$%ls2wN(2J?Vhl4mYMPm!NuW7{uMHcPT3kIV zNoP@E%nBoz@kGNJzS(mrtNrvdk>8FdbJihj>Tr)(=`4NiS@N`dY~`UL|AKhz z1RAQFP@?Prx{i}ns>7Cg0j>s?Nvi7lxE8`%;X1C1Yq8uZ(~|6l!0NQ2-=B(#8XQVM z`Ef{jqZ8Ou0z;2}4DR^TuCE%-3((KiU6|T2I{JBgFpcs*AVC)R;UYR5EVnf}J62Wd zLu(k~Ed@CPB?`)NolJUilYR;|qVckXr9)!6Z|nAUl(7vK0T&#i2V}Qplk`P2X{{5B z?(*{V^q%+kOieBNj_Y`G`S-E`9Q2zVhpKU3gkVD_u1PN`zWw;cz-_eG33)-wo=!sY zfgkQ*wL%1+5@ukqXIKOlKomt+q37S4&p7r1dwl67+kR=Za43JIg#RZ5y#4K-zNJc| z`E7VDGMqd{CHRyu$b`=#!k~;c`-%_8W`~=K|kG z-wtJI&#ogO-+%V*ek-uUO18*+t)Edx6<3(LD|*YF{F5&z&+Iwzhlgi_jND8vZ18i- z@q<(1+pdeEGBmf~y3{d&0eWWD2CgL#2XXQ=y@}?u5Fk5c639+wx`lR!tUqQ8g!!qD z#=IF7z<{O-`q0ydct6v%{}LE-@=--!00LD>!l0T~0qM~L>Oy8-QAXzSdlGSEsAdmv zyS$v5{X)Le$-{G2#JM6c{Y-S?OH!Q`0kwH6iKdAG#Ym-wY=cs1VU6lFxSa>EQZgmN zGn#;R#*PiU6u!GW^K^)<${%y8;QKHR64OpOVY3L(o_JW2sQ44nOA{t zJ_9?O7v!9$=MtO|ld_tJN^i~J4PMM(ZheX8BeFmZAyha0J(Qx*T$7OfX#YvAGnG#i zu=Qk%4i5<%Ku!``143`QD6{UUcZhen>D5^g{hMDb0ac5a z(Gd)v)U<`OvKi!^vK=#46tiey?4(JI2c-Ugb;JLTV18I>NkwvNeW5^`i1mc*v(q;T zqhft=i!e$bkL&th7)__@3=H6O!xwb^-2<_5o zM;LOl@eJ1qbPMfuiCCu-0L$o2A(3_qi2DHIms77|e0EwcT0W4^92>*gN5zX6=P77! z#Es`Y)c^A{81NkoLJKyzKu)%ss8q_K2v8T@!g=7~QuIeb-o@c%7c;ogD}dxRmG)A+ z`X>m%q*#!B7lK;*`iFyqAtk*Pp}@T%qQwN-mhjs;mmMMyR7AW%dWODgIUrO9v2?_9 z4QEqN@V(%-!50m(mHQrd2+o~<*rt0GsOqx$YX?z#6d_u$# zsONvF>h!yEtc05H4*ZC~86J*|41ewg6V;`*mMWyxeB?Ga&k(olpj zT7zY84>e@F>(j$ERwU%JdWvCF5D%fe8USLYAxv%+?AU&;UZmzk^=#^b zhBoJnAVdn05v?(V@TjugrT98GnAFq7hGBYx!}zPg4ARPOz@uNaCOB@9%Z^$5vj`Je z6N_mBIX#;%?d)45hyFWYmvF#tzg3F`jv!!|xx$WME09Rx4;tR5u`~OZ|u;H4Oe7@sngfChLLIK+fz^$i(z? z3>c7%73?w`53fA_Vi5xg%@W24Or%vGMKJQn$MZ-0DP`E1$$sfeufm@cl2RJ#*Ddn} ziwf53fn#N1li8D$Po+t{)WZ=XKyN8Pa8f_?)>DIQAw5YNE&!T^vK`|2g;QHR$w){ z!)h|EdHQa<;JE0OjdF^WMSr@dSBkc!q$ zaHJ$fslcdL;#~f--zp787d|@`X^&()wH-0Ca!G2udCxrsWB-1`j4Fnaj#x6VA#n2~ zmBY4dD$O7#t9Kfqkxlvom)fEsq{c}IP`flPxnt0uiBW~d?_x~P;M$UwOgV{?R`kYJ zoZz^nAV#@%-PAvTMpzJHzG=mqcEaqeR6WzlThq8UO5ZHAv^@ckXGKLnyx~JgGIG88 zC1S)-6qCmnVrhSi=|a~peD@jg{9j(?XTS6%_l`SWPbK3S@vBbf_S^0C^$!t6QX2z? zR^MQQaJmQ1rSMa4Lw*Trt+PbZCR0_A=%H8-1!Rbt$C53?+`VU=c%2zKef|ecz%TNB zwtKW`|5;mAjB?hlwU&{A#jOJ8*omtOKgI=#THMwUDS7=M27AORJ`EDf$IIF2tCsB< zKJtmW`<9`(D&S#TNkERnjy8lr9l(Hbsm?=0@QA3!ES`5UZB0bjf*`qLQ^0l46)s6h zO~>aV3HxVWxc94S5Ygx`617gOxOZ-h&r5DEB zf?P`;P1Y5p?N-#ztZ?8r#peF~Zgxx^$V~2Q@+a$QB~^XHfI*5ZGZ2;?Gan$12dp?l znKwDz8XwI;;CZCkMr6Y@6bL?Ypnt>)wZb;Y5JnIb&_C@SIWSrvp6G2lyA`U*D!#nR z-TePv5oYas@h}z41S;KE$?4hh>#ZPYWj6o=qP4ZAzCw{aVA!EbQ(;aJ$*woxbOc}z z8Z=H4mi#$pf>Z4e%NU~2oEc>JCG(1iCLvhy?3N#Lj(XKY!ouLtoN_AbCC-ebi`B$i zrKA>Q(ciTB@OXF6#>Ds=cVMOqMsjE>Zvn&{LR66SJL0gsV}MsPQ5|{=1F-r%XW#lr zG;Crgg~HMf$r9IxT*5kde`~Y0jcOl@J2nIu!->fUQ2=k+(#eytc5V#AI5#f?`tj1I z^#`0UI0z^lS@;6IEIbeqB{ic0veb4J0A5K7r^}ipV`_r7ctVy>kj!UT8u1%uaAyAu zhrols)WRcqw>r`Ex#bM&I5S7Hqi)z#>7NTO(7`k#?YtA=P#sT z!r;2J5&s0^Hbf!_J$zV%Ti4wY9H*L1($J&^XiYdB7N({(JuMR&!9~F{(p9Z|rIgWf zg@hR&nUwSHeW>VIvmYIEz_j zaOS4PN0iS*lG(rIj9-Za}fTpPx zIjlbTCuG>R2R|}-5N;3KpdJgdjSHhiECwDj9%}$)F)nQDPtWX0B=+oe9<_I^uOD%2 z=SCF!C`XR0--yDShaY|PWeiNQvg))XO{1`C78U9vn>y!Y)h5QOV3LZe?qLu)%us$E zl3Xrnii6peVaHOv6p?wr{EW%#R4%Hy^STy})Gb(F$dk;&&IJ?RDXVz^XpBnT?O?~E|~_MkzuwaRY79RA)L)^k1sJVlXoOVAoXhQ+m4Ey< z-`)qZ*7dryk{YUU<5DtddbQ7g-gpprJCc)%>4x+mhD7Jyx5|JDK&b`6CFDV?a${{s ztqh+`>qk+2T*4$Vie-ByK^KJ0nlvt^UlPmY9J3vX6?WEoF#Cd~&Xk+EkYSjS%#Dm|2P*)q6$p#WPlr(@T-H9NlnwAX1 zOLnpr&U;JvzP2M9K;0-iIFoTGQ#OoaS=NelQ$G+!%5mr4m?LhdgVBQHD6VTZN0Poj z5UkQm>3n@?$Zof5pGTt7ST_h)3SZjLM-6bnetW_`c+jRHjGH(`sy3xT)>IXM^@PgW z@4J?;BzD>mCtz5Lt1mXRGJLF&uMevC+Be!j!h7BjFd!$##zqG-n}V=8T0}m0&<;;lc$XwTP^>NH^zVw$D4WB2Hz>iwTXy~UITZR3{kqd)pY+Z`F1{p?ixV#z5KO1g^?l3H3V+|+SL z0gAEIc@8u6Fmo|Lucs}Qob=5%`L3C(ETCyDYuj0DRtYibpCK8JRu*XR09#jM2{mR< zQz#dL6?q7Ma*PBqPOy&PIPM(fM7VRDBfj5iv|)8<_a>zCf4DO22xEhspPeelrA$e* zyw+?m!)?mmitenc2Nf?Z!MHc-Mf+R4dh+Bt;C)aA4QpBLb^#iY0j!O(OSAL9fbgVv zOt4U<09?%oP@bq<<;zG-|0>1#0vnM4+V@E@gtT{V)|s7MSsowX!xu)Y7>AOH+fhMF zt}M?!?z(6#_RM0$k=2x5z^F9S;MQ#NsVRs_=}-b!a#*+37Co`U*iz@zp@|>DwkmdA zh>XyW3i%McXj~CZO&DriSw;)xu zkwR6hzaoNN<5I#!rpbi-Gc}>0M`bpy$rdPEiBe8gaJ*Sth2`o=DAY2NF-&a=8v%cW zUKR}r5QddOOvj=Yfmwt)Z0A9%c&}gRTR#5lwWw(a`FJ)N`FKR|ar-=&Zv9T0JNtE-03lubY5?3PsEgVY6*mB$G z>FpJ5*1Hb3B*&9hO7~G0Yxuq=8t86wD0u(m6f>-anO{z~3ibMob7 zJ+lVS9pReksEj1}+S=EajxNSsU&L%hUMl9aSFOZzit=0nkNvH82GXYbNV|d8xK3~d@Ppji2$0ZV;%G)cC6K3v-#x>g!+RS*B!L& z7gQi;s4t-j73iy(?7sDt9IPpg(zQ86-v179DYDpUhA(Ru$JvdBlvfW~op_wyfY@U< zP6C+j9NSRtPW+X0;A8x`gQ(-O%w?dk@amiji+f!w)R>TwB|7#(C2jeA7~6wua6Q z$pPT9gfbmPlamM?h!`N^p{n&(MY$M`5TS`^Z|ta@qB8ODUSalzDON;h>0~I$PNxIy zKv9nRj3dmN`P(%$H)ITG2LkkR;V~ElBe6$$WXTNs_@<&G+-Vh&zP72M5>_r%>e3+N z9{}D>gtlV^fF#*)HGC>Qmu&1w9~(>rPqCIQa)%ESia@9etPRl?=L!mkH>|v+5+yn?nhg}9C9tC+@(oW1p+$CV_?Z^gE125pHjGjL zWGn#=syT>K+It>-^!5uEZeMi_z4Tw~N1+^yw4GVJTeAc7$K(nFLM>%#Y939l$~6(L zg{164QLdViur?YXTQZbdSu3S5Q&>ce6{3~UEf{(!k>Nz38Q+Bw@sSZrFfg&Rw8Jdg z(N8>6Y7CoBxIrXI!Vyb!(huz|8L-5(DXAh@a&3Bg`YDVLC<@bPdJ6!j5t-;ddvZY4 zG-M?b^5-pp3!~c#t)4HWk@D4&`IK56SsP4mWWZ42w3;u%{2jz5hW#%H58-&pW!WzE zWJ^OFt+$srWJU%=2edeXKL^*@XU&8Nz&;sQ!g!r{br8cQQ!9UJ?{*qo=l%|-*?ZLG zNGPdidWfQF4Cpi}=Pwl9Uy zQJ4nWDSdnO*4V1h(+c?(w7WyOZ0nZL6F)VJ{@eYnmgcAUu9ejDh4tBvjs%@{ha(== z6ANOySR9g;6OLINPN=)|Q>LD@Qz*E>@U|4-J4MgYQ*deB024o#csPgXeFm-;AU_Zj z#MEe&hOdDju+u7o(s~iZ8dia^72?XcIdMdo%Ahi-vYIg~@DT zT@qEN>?}oNnu>95kGI20Vjwv3kgQZtO4-0kGE158gNrX=(CuZxrKpAbH| zkZwuEWV{(O(g2o!5)CE0;)hil-g}EZVs)QJdT)BG)SrD zjdD?50LCn#j-6rF*IUd=AfT;YiL192x9n*?$`FGiLaTU3VG+9@V*Tf>_-@;o7HheD zPDsM~Ra4E}L|h%3bf=~w2`BFIlNdXPbmnQy!YsTMmk`ncUKD!oO%8mm+ChSor=T_3 z0G^C0Wl=^j3{zDTN*(LG2!{R{V(o<2(mzx>IIuGLxgq7%`;roeNZr8+pSs3KT$2KQ zKbTfNX&Hqi%jU_ycVYunA4Z1~7}8?>CUw^nRb4B#u|Bh{3i z@FVq9EbByyZF#uLr7LndjiT;aW_5n&y5FU>A$bGPP775-QEY!g*BOwvnL$BQ4si8= zS{GeYHVfOg5Ei=qveyujf$g|^2dUK=1ra0oWS{=JEk-BZmOty^&gqQ$iu*Z>TT`8B zdA%|!HZrKjZ%Snh&N%Zcn|@#loQz>~>+U)R(oKE9Q4kY6qhU0I2($`Uv_!?fe)p+X z(?HY(U6+OgB9fV6`2Z|Dg18m+*i!jV7*A&AC5aNOk7k!n`YfV{m4@yT#oh(nAWJfZ zvIwNKoueRo>9f+=TNM86Fkq~+li8w#Hu|7kFsEFM=e3;k{O)A3fhl9iGC9TUe62QJ zJx4D9nh_)fUo34dE=D32pW?*Cn5vCbCJ^R(*cn&fWkgM5LInA7A~@5 zL5s%!&wdj-&{F$LuV2p%jJl?lF;RPF2AEyDcTY_%r?JMy&GhoW{wg&ArXRUNE0M62 zRjTGy37*E=CY~6>Yo<0r3DzbBP(vd#6r9A5L_eT$qKj}Sw7@8eqKA*zLQcv6aWH?t z5y}A8lHib*oT(|N%iEioC_2|lFuoM)^>`+5Xq;!h5zi(ZG*96V+n$|^JC&G=xrM{g zoJFuzyCKMb&Zd8}zyda6i!y0KA6vyo!V7oWS!z*xHE261&gJvt6i5R2eoWB4&kjyG z7Gga?!h|6tD@jaqKybmNE}nHrte+vR6k7BUyrHIG765TTU-A-3)mWYNr!*9r!_ue~ z>UDP%2)I?gQjZj>yu5$p9Vg)t@KN2~wQIyPtqQ)gjnnly$T|bW%pb5xf7_@l6dA*g zVpmxt6C}aXru3DB4}JZ+fBVP(=fb`B0vUa9?_RXQt!->LKn$@yV||cC>3F+iI0l{| zF`Y$^-e6T;lVOmA_N}*f;T_TW^`2dP_;pWKB;2PstK*!2i#&1o@Y%B`PTcl)x1Kn0 z?%a_hAkR0ol(%MQF(Iz(OQM32rT_wwZ<=t8vl!{=gWj~=~LURpY~^ksSx z#gR|FkMc{&!Av~q{h5NUneR$i--2d{3Lmi|`T}YgB&mRVD9Tz2kPmqb!i$L(!rFj( zpnwxm;G2lR?SslbhGXh48l17C1B5Mo>vth#hC593$+!mpLqGb}-)=NLverKnLv+%9 zEJer^$lHs7kk1{QwO3Zg9cO%MYMpcUWl@t=;$Q-Sx@Twc++Qk{J{1-sioj_@;R*^7 z`9@O$H(`h?tf462+7S58MKKw8fJWgMdREhr6@bqZw${FZz-cIu!s&jHsy*kjPaN!P z=-76{)&eXU11Un+&Byo8!)}lrA=VtK<1u=xqZs)Pjh!eD+Za@m&%eQ^6FlYycu-)z zrQEJ7mAvP%vFmq!T6dp5e*pV@mYHntY^{em)&E0ldDhk{vMt7&!wo#fZg*rOJI2sy z-vJIXkHR4zIq0ON+@mUmg6}tz$z~EU1?t$MjNta*2a2-{Bh@v_6hAxXkmP6+=*apJ zmmV{1kthA!_rPdNXp78!b``UvX8ve;-&0kBORC!Sm%}f-^J&K+`};NG9aGu03qn{) zLtDT%WUtjdlC&p9ZKNRu{s_h_)zOUi$_00DFAMvIKwxZ*5YN4IX5YTWddSHZ=(Rp^ z)e=Ut`Fu8;i=L06KWhMp!j?B+8NTZLCz~3O^7cch_>fimEljw}AdQbPp1K{K9FH#H zN5%CdG#MM}0}0f0@;ATuUPVgYw2mTc7~aG!zeIqFr^UM-Z2S$_8gZ z(1olI^PG&pUNivj^%=f*hvlGQBMWiwP~)zY-HOBkz7TT~ks#)2s^saekOb&S>Ih=D zinEYYa|KHNh04a%!+i_7IswHonB4A;Ss=(1;cWk*x$0N_&$QAesV?<3#O znTEqrsFnhdzrfv41deQ|KIh|3paDbf-m;BC|Bz~@Ah6+mZ%vc_$o+N4k~Nz8;7C?= za1gVYTb^WhY9(K4-tE+cXw-_?7K#;Ny<_6ME1}mVnn*l{$uXXlv|KBd!b{Tf7L$8Q zC#$XaOyfisT}=gKzf0JODz!FIj|(l_L&k-g#X?AAVks64V5$tX5C#cJzl1-8zr|ls zZl3-qjI`mL@cpBXI5s0sPFGf>sp4M#@ZNJkFdpNl_pAu$Xc6wiAawynNTbrwg4H-8 z9@t~gJS^US2M;tzOHHL<(;-@paS>*>yWs+1-HVG7FN>Uomw6RsYI0P}pjIa)6_IYh zO;B6&kHMcISB`D-CGGq+UlMQP26k#NVn?Pj>6Ql=qgaDr(K^m#9~cy?%Y!!l4*i}XGywtZ zhI3n)sGtRfOQfs&pyfJ@ZDvWay{DEo5IgNN>AlA-rdf>N7dj2XE6VZh7z5KsCkX*2= z*2pfv)LvV&CU zWW9BvDi1B|`*wp!@c8#0pm>D?_8cDjuUxTKS6@AJ{yR(#cpWJ=lx9OX4OxrL5G#}a;XU37_&7u(t z>TDD4^e;(^I80*gh|czcdbGM@3*fcA`LyQxBX&}EwBnwHhQn>@ z-Y#jdoEwlT7^Z3F20dji_9E;G8o-W!j*bM3GsY_>BtHhVU`Qj<3Z~|#6^$y5l33Y~ zkUc6vQ-!r2jB>DLy%N3Pp7?bbpvU`t7Jc-N8rK;H5?aKH@^K&Ig3H>l zQWkVuNpYi>kFPWZ%L7z)Q9_T#@bF7OM?*!Q$E8JHX_b7VFkrTX;)s2A#L~rK(bQz= zes9JFZINm7)2-7Ed#(tQf$IHwtN@w+YfQvYylhRxrrl*Xf|K|DbZz>NCs!Y>PhsLh z-KoFx^{;3C>lGko!~^N}1wC2P3Xd%5^Y2*N$Ozem7>lk|^@#f`dN9f@BN;weZrW6YhhaBG^lKrLC&JE>0GI$6I@J&)&NaKTO#Ody z97Q#T@<>+i6wIS2+BEe9kK8FD zuC7v_{q(0D;H8|r1-P}QeEf;2r6qu+cV8X>26)0M)Lq|-XKbrJJ-g8Yr&#eyN=~BP z->TIx9gunf$$otwB$;9s>?~qVYgimkmOE1D z^IXZ7AQ}W~MFbg9Ja&vh2W--RHeLuyq+v{sAg*E8Q5# zZ{PxmOybM18?%UqN1}y+VECxR9mhMC%%>><^SLW-0iCtiNg2tQzxfWnRY=xusz|R3 z>AYJ5#SsXe?pwp-z54ACuCiS6ftFtMow6m3i%Skz0|o|O0C}S2a2t)7UZD-2m$S5t zDWdB_2c-qCqAJ6*#OTlyp#nM`9OE}b|;z@Btb zBnK&iG%|W;0!_6qhTY0VDobSdUKkXrVT1to1Q-ELn2+Ljzw07QP`4YEQ@qwuTds+w zJug*Q7PlJwfl;*xkzMkNx~xUml=NwmeZ387X*4@0);=$=O0i%^!F++F$*uL)f78zxpfGYc6)|p|`&2WD9L>VQ^?(SkEi%aTmzO*B0wyK`acT z!vc?{VaTZGk10wFysLsN2r{!JMEmp#F2=M9HI%LNdeC9!y2&(3tJ1*8-gU=Ep%c6X z`AM{)Uf0e{IEhzdxO1W7GR8f{f9UsYBP~AopmXGi?mX$p^*`bcFHC2L4-dkXoAsn9X2w0 z(RJy1^`k$*c=2ij1=23s9^@+IW4huOV&00+mE?dgeW3V3=kPu<(&DT==BysD(MC+x z%mwSYnA8%0l66ZZB!VTFZYxvstTO8_)+Z+$)0SU{_*2={FF_I1hFjT>o^^E$`^e1f z^(GiU#mKrPA3{ES+Jesx#2T#}Tnm%WO(bGgA4H!uC zsMzIXKn%IO@;aK^Cr8l_I=D;QirOk4=S_%*7b>Ao?Z(4#B#X6-{Su8RPB*YSfOC(46gO@kB+gxcK?ZD$)$i6$ z!8Wkg*WtGYkhrnef_-(!T|D>TN*K6R3eagc=@TE?XRjvM{@A z(t7piJ;On0E4}0@M!EU$W1G=sRHBGNWH@?XVdJ2eqy?BP=EGXv8nZS(uB2=usdK)S z;()cFsM#38D_j7|&7}}8AB?9QfQ35qKxZzY6J9G{K*@JLQz&EuxrF*w04P}0 z_7A2n2VK+iGlDJNbJvY1@5oeu$rnPbh%=Q?8C&oL$(h0kVJ;#=LkrGit7{TB$<}0r zd4!lA+Xg09aM)oh=}J*?JHx{$8gI9Ucg0rX=)UGqBo-SNLlwQ+@biM;d8?o-d|T$y(khRvc5+Bc-8OTqYiEEP$U zBaC}RcL`UWJx(qzq7u|9H;(Rap*pFtUqEFp!wYGx)kgP$&ha12gU(cvwDkIBJl@GWMfBQ`2O1=qFvp1hi=*GLk2)bg3>7 zDkQff>DuL_i?o8B34!{M1PzvxNLdNA)qRd1E1(tL2?T@*>@-R-a24>A>$b;vcFYgr zoj4bIB9(vNj-gOCjws4|+`5~6W4Kg3QKYquWb_G#UsXNe5d&|eD2}KCoXk)(9C>a* zTHt|bIXZGR3G4*4ejqlXf<8qtL*0#;*!yJ*U9XbvT6Bq0SozaSERfpe7-Y(TQZTys zbqCT5q|oQnlderzq{xV2W!`DS<{W-pHunQlh1)NCPFgQN=oiFYPQ{cx@Qu2_(tN*X z@vpwU9!vX)lyLa5Qb8${4x%?sExm-IqtNsVsto=p9Y;0lCsJ))a{!y}2|92zba@Tn zl9bad@v?7$@DI&$sM{kB%>RnTd=<+|YD0h^Yb?RZ>=CmB-7|#bGIGh_vBF67j!BwV zq)d^IA9Rl<%$n&NlVV0FCv?!{Q+&W#QY^kTrZ_{W*UXl=A%s^Not&AO2p#2hSTo&f z=qgHzMqTdX6NzyRJ2#`C&e|~e^no~#J(gKFCk@S+ljo20OIt}-kU)%BkIKI zsq!}$vwH3YDQQhv4gl3OKoNs()6})r@Yi>a0Zp}G1x2KLMpO<#+XC413b=t$+LGGV zk-$VkkPyZh3?Re{`@#-?+AIv4DP_R$s_EJbK?KM++ZSdOA&@i;vM+e|h;;@Xmlq@+ z$X$BQ87&))38$l>-^rcMfMrFT2S$cvIB&qMk<5M0g5$jCF1+CZooE0Ic7`e>8wOxb zjJRZQfHp#?Pi@H_s)kW+4XBG5G@MH#In-IyA!|Q?|Ai(Ox~q|1MA$@l*E4_=)+5^n z!&151m2P=!Vc@fSq;Ua7**PyOlubD!@q-?)3`Sm0YPa8h!yejh)Nae1C2*R=A*rep zwo??e`)U^=soG72bw8GlxDC=svbenxUncLH7b}wTJ3ZlsXWoAMk7AmcLZm$ze{<`= z$3FHk2lYV8{W!b~^8(5c1SKs4>RS40Qd?0BWz5IXEEXR?e_&cwkA@=Pl4MXWSO8K) z|3lnFyqEAV4fv)1gy?JrF7#&qhz$}0WgA7Cg%kK4=k(^^(b>@3R!||f9dv2cI~^23 z)a&gBuK)I?t1rLpyzC_uZrj%`*PiPx9a8*$Va2eduUN=G%h9T=hryNzS{@DGh>wfjCA4OlGvY*IQrm&DJ z`}h;tik3mItC=w6*f19qBwXjzc16j_#au@mx>Fn0Uidc8fBWUnCuNyaRe(kr=|rF_ z;CZ$@4NOvEkOW2tI1vQ7YU`e^)RDjwStR49j5Ixp)CC- zfBHtsgb5N50Q%a6voG)7`<1=Btf{HJQ;*QgMWfKxnpgHnd%lPeQ5Af^=+!%TQ0<_b z9eD(!46hYfNE!qn0%7?loNW%vl=bdW_z)(iSZ1Cn?IHOv{tLrK}AoiH5?+K5^SfJt3c|{T9+>mHqhk0xXj_+Z0Uz| zE0v-z!?^j4;)yemU|Ok&HNv=^X7vc0#zG6mZqgMnaY1`&hiSBZx1HIZ&8p@9yL0R7 zcfJ;X>Y4cKNdR#rI!P)11_-d+ft$X>AjC2Xx$3*ghMO}o-b>FxLg2@n>Szc|^z5{m zPS@^MRLBnMOhD$9h|fW0>B`5Tx7cwFhl?x?JEI|n1l3C>hb2R& zDTVKjcPT#`cTT`jnqASf1%ij=&mpjl@G&QP$hUIv3+q-~P8!IhnXyj*#>Vm$UKQ#h znw->NYe#XxAZEz5efy@x817P`5Rqg4YRj!CZu4LV5sAcSM`aweX1IEcdwa8q=4b*P zmj3K|rm)b)iHGjfgZmRVAU#)dEKDuUruH&Cvtb-!%gmy(C5BHDRnO3lFkcc=YeZRB z=7e;UuJBq|?G>}N0W?y0$>s7jxm&6#*NsC_T2#pyY2~7be5%x!D{MdcNb>DbHI1;m6 zUYDHdI4I{KEfOar4FRZfd7R5?@`$vsVCHf|@s;@V-xkGhU;PwnHbI|bWIQwA3$E$& zCC^9YZKk3OYEA>i(2)I8uz*w=xlz=-agB|Zu}-%NCPcr}WL(&a+~Q>2D=HP=4uNEo zZcT1YgKE2Sf?fzhU*8p~u2q_u$O8Hz+@%j7s>Ub;sT}slTmCzs3W?j50$34@8)T=~ zJxDHS)OOTe5ZDG{7bZ_2GPzW5M%p+L+FtAiP&1ygUwrY?pZ~ml_uZd<${yR#A(CYs z+~(V}?#z1{Q*EW$I;Qh(E2D)~O)j-GE)h#;NZpFcR#0&RhKX_}1Vi{DnxgX9z!lW| z9Ncn@1QD8r9U-x9V2tkyZqq<8z)kmm=-l9DY;YnGeFL{ zMV!W}AwNE7x7#rgN8uRY2k$?NqLZu~_+C~@+m0k8d&GL{w4H@1QyUC zBmnAZ4P+*BDNocyH@_B!)*M4RANRDP4VY*VH5Q12yV*c9Jk$2Yz!szoO6cLvQ0(hB zI7W>^er+8nZyz1;!TB`GQTNxrrJfPD##&<|d4 zMU&rVCiT3Lb@Jd&@l97rHbj8%+Ra%JT^d|wW+FswonDQFpRZC4Le=2CRX&yFOd!$W zo(JF(2_C0D0D9^Q@_X3A7e-LPG!a_Df?Gg1u(b4xJSufV0CEY-X1 z77P9N)E47{to&puCgTze?h{@}TD*@itpY0YyL`)yJrZ+0SvNmpb!zFRFjn?}bDRo{ zY-fJ!SY58w4ymdpQ8HxlxBne!h&#F^3DkxeZ5i7(@(rN-ESC8c3II!|%~Wb?DwVp! zvT?${uVRloW9*QpjxIupCvMiBmH8KhfR>&v95_Eo)0rWr520u7lymCT+S;vudwFf` zl>U|Sic+sjhjsWwD>e5K_IMrywi4_tayrt?5u7MGxFZBho0p4ni3lnwlkiJiQUjE- zZItXV0qhDTQ!~;x3W8%a3vmRT@s*B*y+-cH5%>7Ww^-9K`FKRk8HoSAtn zK*CkCG64_A#`vUo>9S-kEQ~c&vS~WAX-^^SM^%>tuxm4YphgyAh(7dsYX#knf?%ZZ z0fcXwB#;RlJ7N!7{D|(SNA%`BwzPzZyOY&J*Da_8M^^+rmqhPI+YkpxBvr>oOgpba2P8tK?%uZ#(C1pcT;dU?;&)A0 zAlmj3C5xr5NlM*y>v|Kl*clsK6Pjpi8YEb;C=Wc3(=i@OfshoHZ8cP7NrgOo9O~cw z8Ea&Y;>MBF%;bXPltu8-h{FgXx+f;4!5lJ@VOl=ShB=5(UN-4zxHh7`{lRtd5~^zw zzMM!7a}}k8gnK9yGkV?(kc2vN5@+DTs~2qJ9JxOU*aYxJlV@aM3DnRssYWdOFZM7T9Jn zLrfM#n1)n9H|mX+f{uUXj49&oIcX)gBGqdmEQ~fgm_3h2!4BHMxoR~8U6TOhHXWhh z5mV1^(@@j&6uLP9YQ}rGAV7SqP6^aQrk@4zVXCRhbvf&%%kj%Ts-vXCm-bI_^+((7 zlhX;u80T^Uj%^L(x+$v~svIiiVo3>IJCO5weXz(6yqr`P0zhOLl>r}I>^OvhNG;&7Gn^8tb*WrX zk6SP^Ti- zN@RmPmHG=nI>KeBFx+^>PVlx;(t)0U*=b%DbPyxM=v|1}Z8;P@AB0oWxH8IuC&`>5 z5%qY%0x96frUK}153S-8|MppTUc-F?}BT1$DbjY4*6nJ?QId0H&Pp_ z@@=`emBoUZnnHA5&H64N67OtN*@`14wYVSVlmLb{rB)r^Zni%|m=A-#6^j)H)0RIv zn*;AaJu|Wl{7J}H$G60=kwLi=bTTvzzfc}orYc)P%A!MxG^NJSJ~1D%QIqW1Gml*v z!;tAWq}vVr6)QqPIuu&bsQ=*~KJ)p{KXc24QnM+e?mj2H`s%Q=u<*#ujduKEqScrw zXvJ4|pted~7|_AoK_HE=oW#DSB@7A7go~a|oadA^{59_BAZ37yv=^b>!%UQ5tVy&t zhiI^Ty%YS6L}im!yqRj^cTQ0*uO>hew+s5?ap)3xE|5at2q>bOsw4|0iobDP zYlRRyEi8CCy2Mb=m??Hp)N01m%g4n4(K0DKAZe z-EXhEn9+enY8++Fqu>?HDhrz6HnUB)T%u6N*VaL5qaP=UY#b7~*BFZ+}V^5zH#gjmOF)7r8VMb-ZbPy9tMpTBu z9JGzPP~4Mxow5BY)%P8+ZD0w^{u_`Y<%I(-y?_4T<+qFGh&B8yQnPV&_d)uG;sK*L zp#L1(P1{ZdA7tCQc$yTMgH(0_LD-S_NJQKy>n>Um~Wn^m#5d2C01xK5XNP(EXk6cNUa=GMzEOO1)YM?d}hmCm@I`4 z3|xxSWJn^{51+w~&Vb1~9b1iU$BtQJ>=7gS5^Emqrv6>0m$VuX1Pc-x0s+ndei&Li zramwA|LLCdNsLzA59oa*38P~I^X%*O=9y+spSGu`&#>1A{`l~1x4F08${G%~eICWx z+g?KNo`L7UeubiK$`I;22XH|}h>{KkO3-Yy3HF`w=p)IDkeAhq_C+82?%~i{xPQGm z`@PxKN87WF#%$ZJzx4XPTW@vmxMQ#T5j_kdZV^oF+9&e!vkwUQn9c< z$3Xt4+&&uEyv@(xgg$g+4<4uA1ddbj=rTQC=2b=ZaJjVNK)AALza$Y)J1ZU?EBqCz}z6i-YHu-6RTr4;I0emap2*B8JcZpxgWR?>mc6q0<%=S@i-lu>-(G%Oc*GZK5% z=XlBBQ;Mi85z4(V$AevieOnb=Gm$K0q)eK6uy00l1>-RF?*!<21>t;1z%o=#2ApxjpRS;b0Vh=hgN_rEH;~R z5LA2DM}4^N(;*I(FY3was8YJAB#-W`r;jkOMO#4qXBrqBs%Ye{P#E?6f zqaQGz2Bw5{gRlksx6&c*{qtc{9(=H^x6=y;xkdiaj5(?c$BxNYJpQWv{aD~9E~44^ zD@fp5n&8-Z6p{c15^MmYxKzpW^|;j7w!YxTRmNXUTN3&P*C6 zTOuqW3*uMMo5u_d?HJRYD^;fpJM9$|L}begvz=XG zF}H%pyp=XvE|>CRC>PFnd|9agklzE;mg*`c6Mg8)DxvI}E*>iY{C!Ny<^~r*vN7Oh z5T`4eJ|GYjggT;DLqZk|a1OQ6Y)IYov}lnnrmtOlOpyTr3(khwe}|vE>u2vj_kW!` zefr83=gO6vZkn|Cnp!bDwfM>YyE|>6q?Uk{o&Qw--Dgen1~@JGJYG&DMKlot{v9P% zy>})dm!hp#)4?v%eFwFPoPp=MwmotNBKUm@=)TJdfVf~jDoK)`v)NG2-ch2rml+>#eg6#J*B6yrvB=$&|^0@Y7czvTO(!E zO*iBPuF?@|#S!Pz?D4_~d?Y*uP7(OOpraKK`R+xO>!Dze+Q)yy;>(-+bg8$Lje|@X z)+Bo3)1d0isqTHHvGp{L2>{DB7QTP{g1hdoyBCh%KR(;K|MN@8oeK%;xRakyRk%5(iQW+3p06rU#@@5J*8A>xh0%an**Ha@V zhxE}j#7(lud<3eCMjL3YHUu1u+RzYcovXIwjMS^HRQm+(8kXgw5U^Us3y4Z)e0<2q z@FBm3ze9*{Tc$TEvyj4$BC%|KR8tH=!6pPhLO{_=_=!Yp{#X);D!^i))+)s1PI8vD zWL0^l<>2FoLNuv}$GSL?W*gJ;S+*#BpX|CCBScvS^q1Iie~y2gvaiGx z8Z`$-#;3ol*ilkn>@c}FR2$EtyznH6@0))1akXkVN@=8NpR)%>YtoFK!G*sK2)SR4_t6p?39IZ|$Gc~t5 zqm9*$yo{)e95WSU9&Vh};A*h zMpv6n8;{MHqJo_0PDwJial%KJ&CN&fvo60_2M$&-Q^*PF5OM6BiVo7@j~&kB^Myov zGNC#~)gBu96KAXIZr%DKrpH%*w3qr(VlE z){=TH38|C3lHqr@Pfud!dMntw|8|cbrZ3U&F^6vd7bS^$k(ttQ=h(6He|`Qq@~-uI zGjX2pRBx}Lwt4u@f!d|-&Zhz~=EjWbK)fw)(`V(;7r zt7K8=>*nw2XB*|~2%c0XzxYM>uDeEVy2+lKyTEQ95C8EWr~dv$@r$U*Y$rQf{Bwdh z{7Tc1iw%HR<5|>2L&@4PGX@+z=Dbij3RUn&LCdJj74fBR)Q<{h1S|vUi^$+awv`6P zP6Uh-y230UR;&MetQe&9a{xotcF73*=Ei4Z$gd@JJ`rm;ZdI$fZM8kJqcE0Dmc(MY zTvT00^VGcFYUhOZHBa_K(@bV3pm#A~=5h9p*}QAGsY&RlpkaNX6_ zty=-mfv-zTd-k|`+6>PXto)ZuD*0}?BUu44fa;L{;$4Ui5MDW$Z0LW9jGE)y+MulD zEz|L-<=+$1iY!BEZempFn|m~vmeY7PG@<^kvf@v<{Fv6bvCyP~EoTCigIXcp)*3=K zJCud$>cNhpNOF$ynwv#E^t@fAQis@zoXg57P*&&9Nz&0|u54t}m z_Uvx^TcdmS;BEzZ0RNHmQ9MR}^g$zGJI52AU5)PDM{Y(3TSgnU(hAt*R2^|3e&2K3 zno>zBTJv!u;h^U}zM`sirNN1vqK`^`SCLTPM+F~fSi%}9-ICe@E`$*x3;CTSmh{4t zTjvH?u`Vns`lzB~3Il`5MkMt*WuZw1(J$~HvT0DSe&x-Q3=3x~Wpq8D8os5aI5%+0 zv6SlHke8aik+$T$x0vbT6u%ARB4IqGay#^i_DT7itXGX%swicg=A0_b-{xmIq+pFu zW(?_YBCHrHlXCDNDvpTx%D8G+dR8B|%0BY%;kY#kIYHTn2PjVT?~0j;an0OWYd<6ONslSH65EQl#PZ=|a!TW%|-n04{y%~a{Ingad7YjIHxG(N%kim35L z_!J^g7*)Q&YEXUjdl@LqF!4LY@!{$>;dkUwjPYOw`XK_U%@!b-5%>dDIc`K=x=tT9 zk?AK1CcW=|kDrkqlW>w>K!Qup04sy*3HG*p%sz(yTVwVydwe|ewba^X!JEaki_B+Ny z^2JOd#z*fr#?`Yd%{6yz?egDVSpy*U!TB$L?Q2dnrt@ps{s|>JCXK%+%MB6r3pz$K zeQc{-l!{UsB7&5-0Q*Kv4L^pf{tCdtiP~a2xoc1ZBr4g^X!Kv)MI^q!n;jVcC8cH!p;9)(3g!C5R)A=D!*y!2Vr z9?-xy52U-P7#x5t8&gjLmdUPEN~QXRPPLlhB~AZiQbYPZFoFMo7Zh2#_y>ua?pIqr z9i7}zcSt-gtZ0OxGT8pbHB$CXua)4=I!-?jT1vyKiQSVb#}~q6R!NuTdOjHBYEuB zG%;Bwd8Q}9HZ%HWl9ER6r@e0DXxCutXum!rDQU&l%YebCw`+I|-fWK*>G6Rx9(rO@ z9^&sHW-8bvm?xz?aie6Vb6nQvvk?%JE#J`vVS2CKa8_MvC!gtL%~Z`SCDRR9oJ$xx z#d&+-M=E<_rzV+DN(zv!ZOAhl9`x+;$LF}|dmG$rfjUsv)^O>_s_AW`;^@=1e#ag9 zpx)63jS?s%0R2K8;e@&Pk6SLX3@Jf9v&#lKv-B@UuY0Xb6KKU&uBBAURr!a5=Jy2#p-iG(`f-=jDM9R%Gy=m#_q+;~{NZKrh9 zaHgb2q1cjUK@*Pbyrc!7xxRDE062Ts6Cfu8%1ug>ZnOnoH;;xXOo%*WCmrG z>clAU^BXK&6y~2~ESuQfs6Zva644fV_yR9!szL8*8EH|w$wyK}Qb`>t29nehPg~2AExA3q7rc5d z2)xV^2Q1ifi}%b{VOA=;C@mZ#RnVj&ky(G@3N@0G$kFK4Xac{IrPpnUHp5(jUrJ{0v2C z+u;=uvs`3CX<3+lGI65Cp>4vx&^xw(Ox&h=zwRCb>`&O*VJ{o&&Oy&RxMR|;IABhm z+~MqqPk=(xLJrv}TBEV;hYw%AyndeIw2G28(`^NeaJOZ{x-u~#H_|pbeoBKH25R`9X|7d0y0OM`V5h63bw6 zG%wnajb~33b}?d<0Hgw<_%Empb*#kj3v*<^o*Ivw$GNRzi#0R zD=6tlRm7K3=}b(mD4eo*1^x8sk5`MRHb)NvFwaJwf3q&iF?p5~bOU8JK;A-O9q?oo zbBm0F@+X)Q!IBcm0ugCuyD(8y7D0Dr|R@|&@;8#w;#TI z#Hjdsv&h$TNY zc0NGDSW+7gmCRA|Mlmnu0DcT3L&fZb>5|-Z+b7N(<~DVc$85i#Ftk@ttm~4P_`}|> zc{0(&u8vTMpez{xQXd!{b;ia9>@Wmv4!ztX?>LZg3NZ!gdTIDYpI7BD$Aw2tokzlA3Q@T7J|q5dz>em6UWi5{N3qeI18*OqMtq{MW=DoO5L{y zY|wao$0~AJVGnzM1X)1J$UVLH(wljGK<|7BN1C>KFTK~(t1TedvRaM*XUjQsi$*16 z0a-Sh@H9QycY)9VzeuXd6`P!E6GyM3+34@vAv`QJedHVf{u1(*SK_mFVz22FBlSrD zq5c{L@S9Udj3Q`b#G8A=d$M-V9e3Ef@wlKrOC%uFS>ZTO>?OaVGiMk*DT=CEDUH}` zJ|;^Of$oxILk&rCu&erXd@|5IN~sW-}y*fyLE=0Dn%B@v@W5RTb$_B{3q! zR2a50;13F^v>`1mEnT$}b#A9rFw)2$B||ap-QGl}PoNY!FYu^Wa*``m6r_uHa)OmY z+n%p_c0pCMV3W{76tp;HSf`W zlkKEcL^PZw!VQjm9Ka5niXz)79=}> zB)8?5D^QvXteK*u_ z2r0)hP^e*2Ez^a8pl0@80HTVD?^DQ^G0nsBcJ-fRlUAFo1O$clg%&t+IiDOE#lx}+ zh5410u`x9?#&-&8P6M5ioK8PE1LJL`KHUfizIO-MjzM$mX`UwGK7l#X5A!&R@c+k@oj+Bj|dBagx1aMWv9}0Eq|I z@If(PrjeB5z~PI74{DBE?RrB0`_Dvk!1P;I5;)+*UXxhDvfeaC|8=B_8gK zeKP5KpW$yI=(owqFR8r4%TEIo4*+VTAm=K&wvRIG6wtO5-R&$RMFv0Fitn~34y#SI zom4(xSa2#}mWiz67h67uSOraFq37`TRvcepFd^dI024_Gv|!Ukm&AtLeYiQ8Z)1v% zUMT1Gc|4W}Riz`MQz2Px0Gy}h+~uU5T(10p@1a{b4L&8DHLGZJRh-Ykb47%L_&Ri< zF3EDj)I#e_3{JN-h{`r=ad3xJfR_`1&om&hrcIy|FmkaE((*%e>3__3?8`S3>}ZrU z((W;cDUca`=RC=}G8M-kKGbxEHEno&e10CsO{ddpjg2+uv)SyW1v zT{}WoVzXg0VnWqjbe1{ly+>MvX7DavLD~l*ckwEfIj3eX{jW$d%+~Dom!mJaKmG1u z1$f48N!gyR*Ain=9{!eFdG^^?c|5k&DTIU^COs-T z#nclv--HmU&76@ghw8{`8R*}#!ilJo;9YQe8XBTl;3dFqJ|U=PE@kBEXg`s~Q>rK_ z2a?%9q;kq_2+aQm4rzj!|EK}1Z>oB8M?{1q2~tn1-+SKp&_mAAB9fyiq*nn3e!uY+ z@{{Q{`~OpG#t2Rr7<*lH*0!4FRuEa>fe)fP5)p|iP)?$KVvEUyU>s*C$#nE1C>uVr zx1#;#TV_F`pFZFTr`*pwdnve172e_0xYDsf5mL|xLS@^E`pyVHxmV=Y8 zbIKj3Pv4P2J!1-Oj{whhQ!Wa{As*{$0u@TUE!0YiC6EQ7VSWNBgs23Ma5tlY_TBS< zRH&bxf&)T>A|uk+;qs#w@+!QhrK~=(JGLAH^nW7FbFoqIikFLH+;g0?ZCeOHinL*= zHEISDO%SR1MJtI-IRv@)w60vwQcw zSu^QK_d3<}b)ffrUI7?{+|sq$E>?dyaqyTk*f$cRAH+1Ymzx zGh$1VWl2JD&nj4qq*b*_7KY&VWqD9lk)@?*fd>jRw8LVNs1TUC4{StpD1yE(sS5eJyhB~i;8nbgfUi_2D}5)1T%B`k-smEb>6O6C^Jz+1u1Q1xRP%aRD{uJfQlcW`9EHNwN(hWS*{>g1kXR;R|7@ zyGp}^0hI+%9vCq6iF9!ipgzn}MgDRWEv&P03O_oNBQy2a;`jM0OQtvph896W^Os(B zzvd2TyK_n&NgX6LfIn)Dn&Q1y?vOY?+?iK~E0vtwa>S{Ob=JQ`RV3!efH0!#*N~h} z0%!=e7b=$*f)Wl9t)QvsEkWZtN(v=(Yv{01bi0RTn9#Rc{)UI~2cbiX2EJ)>}IyW}2EZ)2z@9^e|r~mX%-JGGM zP?MRp10Ed@MltC;>^KkO@eS}o$UtUCji$k?wJCISzT^qu}qoSm(!Lt`CfnSI;+8$;vLuS#yBO4NU1Z$pT47 zCWFwcY@pTIa~;$Mr2yWl7(VWF6*XJwC`wY&3KnS6_@=2tz|jPv@uH-pKOPph<#o38G`_x_C)s>mn8EsuQ6FP1)Rnn!i_4b5rVwCLlr#crmLSgE7%$OSD=fYx$U`=BK#W;ux} zE=?A$&OZf}Zm-ahVA|&x{5C`bx=vB}?pevU2usaQsQ#S-b7+u>W6K}y0mQBGek*Yk zP-nDKQk#*-EHiiZr4eyf%!!8T4t$V3@+~w8O(oJrVO`FR2@|LGihKW%g90nSv$nC~ zo~@iXp+MPjF*%ALb4mUr?$lBS;UOE%%AAqpBtSR!fXlzqjfai0YE8&SFZg^tK_TnJ zoSG&rrYEFkI(m6`_y4u23NCG;#f^y>+~o6U&f9C*BVx{|qq8ZKMD;Tw)kuM7{Wo1e z6U9M^=@;$K-7>6tLMkVqzVxal2f$wdh#kKM^t!5KTLEVR!l~fIGu&`Cd80L*0R(^* zckT4ti{FHyCalBXz)+Jv8C5>++;W`62fHx_b=fe-<@zMnzR`PP!|?xp5RmRv*gA^P zup*QwiV$qNeq(nVpOz-}af>G$05I?%H`Uy<(L`ts^xkNNGGzTDl_q-3Om4`?B>^B~ zBxjSVoU?~qshl}K;^}JI4xv3vQw33$WR0hoh>N(zX>{a>Olcy8*^2;&r_>H}xjoaC zox)!o!O{H-9M?tmR{^b0bQSL1gfVlvR?_qTjBm9nxPRE-p!rP)@j#X9+So@&OvhC1ZjL zOmUHzZRkq-E5w^ptRQ?0ZUDtg{>5sf$d&FJXjoa2H%tQm3E79PZNQvJ&d5tb02wuM zszWHdpSP#`Sh?X@V195aB3Q06q{V&}=U1mM@7ZG^#m6s}y`DwyY`WbVh zKgS=CF96}{U3_nT@WD%G&t6)r1B(Kk!_MD0A8|etnz+A~maWtjKUl1EDM)L%7QgCC#j3UF@oo4?Bo1Lhhm}Sm5XL3SCu7JHwZ(shKD|H?y z3TSgx%HMN7=QyAH9C$LE?c1LNEdd%9QB$RWv%+f18sK_WRo{MIlFsj&mq3??1*eMK zUjtB<>YP>qC~{!SAqFWD<L2}Cxe9LrPoMsu&%VWJ<*wDi%koT{{+VZ0fbW~ ziz$N_eZ4g1*>>y{aO7{^2H2@>R+jI1!G}yjm3PI$;1Xk;aF%h z866zKu%IOKU0ko!1WK-Ud}+r-{N8PXT~f17#$H>S$Z94p=wLp+_8p%8&Z|$VJa15; zQ@$=>f8q9+VpqbH~A?nU-5l4~Z=B-HeboN25*g%BXKl_67@Eyt?$ z7|5DSVnwg<&4HK-Qu*6#xhgwKwM`fcL+8OZkeMJFw#st}&#GIzmoQHGF>{g@rx?r}@`tXoxa%tN=PXPG|sZ_?t5G(JZ1@W-;!+ z^G=%*u$9l;D4s#EIhg^?lQX0=ZP#j`&&Tn=ew_RAGAx}^HRKsW$kPi0q2Uw@=)3dK zgK5J3p(EuAa*i%}Ahc@TkLVdD>qLid3(Lx+oE6}vA{QZtU$Rm+*Uju62RCNZC=Fs8 zh~4{__-80AOvfYBH1_ZHSGX%CBqER|#){fqeyllu*t-jSOBn*Tas zu82c~w7D<6eaY<)NubI1u#q=g;J4+rtyRVc2i-Y4?ADgN%&*{G*>;Cm_#rG&KunS$0rGc^2 zt&8&FbtN^ruaaNXt;1nv8B5kP6=${N0d=YF7^s+97UA8b+DG6C?)z3~2a?oD90IP} z)vK{*pZ$_^kL%uZ&(~!f&;a+BB@id0hAtqiQRQRbV{5P_eTUQ_L>r}XneWj{0n>bXsbA!G3uU!f3W`eS!@onu{xW8` zY2 zk;n8A)FTwlvNJe%rf?)Dcfj&)!+LOogKzMBO0z*SY{Pd7re_dT%M#>!X!(tq8GuKj zw*$SvE$Tob#MCeP)ha8a?JtR zv12vnir`(5QkXS)RzK?3pVQ1YHCU6ap(j3xnZigHv8njJ*9bI;-|xBmZkx*U_U(I* zyR-@rqs6bfOK!0yW((^}&mCI;fq6;poH3`HtrPnX93MdGp%4JlGHS5?B@!DHOQI%7 zLZDmOa^r^3_ph4tiID~}VJEK@pBy>W(bws|MeG5w!yoMWQe8vN%nbhf37hLEc}GKW zmv4JXK!K`KE>;tgWe(^vJ{oT7e) zGQy&r8%|kPY7Zdt=r-7yu_cDDQzfA9yP?c>&t4!wxm zG_ZrA>9kRuR~^$qIHitzHGxz7gkfn#dC4BiVUGZ=D>m@85#pPKbVM!`i53$t7%SFe z186^}d(e|Q+>x^X`^V*HOdqYL+>^=m9g@*>+8Fp;%kH509My%fv&%;tjxs#FeK5|~ zULV!Y0-ryI?k!Gha|WQ~sL#;)5a_N8OZw=ju2WTZ6SqusB0)HQj%e#fl&{JH%7op| zuG3we>;){zIU%c$Q8TSzLuW}J%!{T(zB<{8$WPxK0(?xqW!L<_@@)6CP{+_aoyC^j zd?fa4T=CP1f*ijpxUnZjkQJ~cgbKtRwg@`23HJUQhN*Q{+Ddz3O(vpkEl=K}0Uao=MNI%Na7iTu1%A|8qaD?+e z2w#8Q`|yXq?!M(dkK4H+)^%uHDy+j%iJ*J8vw~w<7Z!87K8l5{W5Gr;99RXm%_A=i zms4uKC7M|gs2h4SYfPwP;bCEyI~XUVbU72)+X3i=dxR)~*+N6vr6|p)dIQRmj-guu zRX0TKQrL3emj433IC9qvW}N%Cf7*(|_r3?7iMgiLd}%PMdc?3z+^&AU6Mgv06qgc0@BHVOmB5)6&L+Bf@TX1m-|- zQrB$G$0h`g-ngJ`QQ#n2OjW>gO&pdRTPfm~+O0N+(uc_X0Hw4|l^ zZrOg`8VUSz+)W^%F!9RUScp9s{jX50Nfcw`@!^1H*kRa)UL`iJ1Sq+f#(uoRQ1lE0 zrgITE#A0G8r8OSTpf(yseOjna+nnbP4YgVmk1w>_Szx?AsdJKS0U@DQQ8T4^kQ9!h zyqScJ+P=VgFFDLz`3F{PL7min8jQ&0XTzeQ_0ww6AP{WBwUtb2?+oF|TA|QTz|UYT zn2Gk=$Yu^(VrHz;H`#^}!C7?~|%Ea@iW!24tDzRaF2FM0h z0VI7L7tM4^8Se^sEFgzau+N_#YU2LdIh(kh4*{p_0AdigR|#h}5vaVz8tHn43RLn! zr$;x+)-?_^uW=^d6qx6HRV(ctD(BlX0hk z)rl@K%q+uIryC7s!u|y7-yb)2Noai05w=fB{smrN2PkR)^Z*md^L0wfP&MnaA>>R6kVwZ5W%0KSGsRIs8{Ojn z0BRj|TN7w3Q_%j1qa}naK{fCmZUrn0ogWlZXt3WRX@hbpGN#tFJuzWBX*KH(rD57m znVe{V<48#rJ$_6!vT8Y9kuy%t42mg0FsmOdm(40R!jzp6Px3@gm&79I0>Cq&BGn;V z2Fz>{T}l+3A`E%z<{(zZ9{xcOr6Ce>cCfLF8>EEJZyFgbI{B@e&O#(8dV-Yyx91iH zl)=t==bZDXlNT@EhqM4M5lokDa*00#6i{v64SZV?IAJ9a=#!;vSLxyrCL?;f3DB z&2v z5@GCJ<%CTqX<>H;Z(6|oqWyp(;1~7A7l;)nmPsYk>hw)Oe=B~du^*WS=`5hDrMe1Q z9U(n6I5-i1e{yPQXeujz%(F%HzFN&j9l4W`Z=3=}AgEyYEIQpnAeIh$4xB3R(91JEbQJ$Jzqwpizi0<{8#0XmhQO8fPNth;kK7#n( zH{DzKnI{(as8<7skpVb)-+U-5`29}gbz**WTxJ@VHR*b zyI$Bi8~@0%=(f~ix`hn&Y#QLIX;;k_z6exb#W0jwTkPZ=RMhH5Swp%EfnZ;U(@t*2 zKeH5S4f;diYP(Ppo1m>%>0&fGdlasw4&-& zXS)otc59$&K8o6fPZRNc@Rt8KU|B3r)a}~0e{eHGbkqRNUwIaubqS$jgr#Jj2S3Z-7RBd zkH(J~Mpf3>$nJ8GR0WuO@dO}x4}9&w3H?8Vhd}y2QZMa3`Mu-2@;}O@(=p#o1l-!% zx6hfQra3x#0g!1gilQNDnv~acP|gS%G*`|euo0MXGmaR#B2S3oL=K5!&4C9;jhOGj z1-s6+7Jkfz7l#4kDjB^(&Y_wt!M2VdkE|8FiUms0R6(v~5`I?61zbZ0bq2zlExGXu z&~zj{6|`sKexd|m5iO;GTpm&l4kHC+PuL+D%6I#3{|T%2+@``xtX#B0y;UON?%8QF zhqUwe-8(Wiw)OXBhlhvJt~QciqnGC{*fwkh{y!fC!8SZ$ry_`)mp6UDGA0U*>25=G zC&Kr%8T8SczvBIM;f0={PFLQ(Wr45H0&4t%NZY(*+81VGe`sMy`Dj}g`))D-D}&h41O%F0*x+k zA9dZ|69O$~Klah*0hD0JJr}mNG2|P`EObR2flBjP4Q)hK8$>7^`f*@&!8&=m2&^Zg zg`A4WtmiM`-b8Lqge9jVd}|=pMzAV}iSDi-sa9BJeP-A-Drf%L zmbdzR6N}hE0!Qsy2F$q!wjrQloX{*uyx3kw@I%az_?;1tcgX8MEY2=YOWOip!tz_W zjJ7g8J(gTV9|W+@2uo_v8XO#49JCtG?6LRnKl!hZ?s);cNhpF;IHYNlVH|{24OPRj zVMybG#Khy?_RW+CFh^{rL!&`jD&R!8g zerT(Le#4C-D2PA1SVGA>yE;=b^s~5`jUp+rhh>9l=T z4h1d{bdaC0WS0eXY2Hh)wG^Ynl$qV>oJ_`wkwe>wFVVqBiV&e< z6J%NR(~k=`{JUF{+n#*Vz4OjzJpf-PUUr%fJp_cZH&w&nvn8Y-S^&8hQX^Z@&O$g; z6kPZPn|{=lGKk|iLYyJ3gFfS9Az{!j=ooAXenyTI-&>w;X%Do|!!@0Btxiz}tc}1{ zC^`_#_sM)hvX3Whs}gl#oj8|F6}6I|mr7bytAi48yLd3=TWW5rJUHR(;!h7rnT9yf zlEFni^@24P(_^Y6oAG%`Do7PhoXrIxuyjz3E&+*O({{k&z^t{zF5cuk)f9CJ zupx*BF|+yOEuX?A`ucV4o#xlAYtTjJ?5(RY24cSH51J=W+7lDU(pJ)y)}8#^oE3&& zpcpZl2=w|M%5F=O2hXNkngMzKeddfkk3?UJ>u%=1a;hkUCIoq4STX$7`vA} zCif<0t4Sme?UHRa8?$){EqInN979Eu!w)ZMTMsCyw$KC-DGFpJ4TZPW0C@PaWFWa) zkP7n2gk{AM5#Tl8Bm{C?1YLkDsC;0?jFlr#6k1?|Qb-Wd^rG8@u6KPF6C1@fYCFTWs3x?`}=H+}2P3Ye&?=WQuFp z_9ySXH-7isciHgC_U?UQjH0}jH7aqsk-ii`!X7`mfT)Hv!t*0pI>VxX-J2M3#B+6$ z_8*@J<%x8d_Gn|D$as_5Y}zV1#e3ToK(+7}^z2Pzk{$E-Bcbsaf6n=qb1u2FX-^>Q zU$3vWhm~S!Js40*&hU1e>vI!$?2-m{?mTlwHPLn>oj$#?Qs&-Nfx%qlD~Oqr?<*Rh zLk)d28YuLg2#WQrkw8&C3KFUNf}>DttXBa@2h=FSd4wPa`RNTXnidVO`xDHA<`O;W zXLvUi*#wAKy>VD~LjtuKwcR>k{2L>cO0`;_b^hMX`9`*>*Y4UaRjXH_#wOC4-r(TW z3b5360v`qGxO*+@3iqgrRwuxFJ0?-sy1)Hr*z>2mO738E_()pz-fCimm1B0!CVUb+ zo_EO5*^e+e z{Dl44pK$1o#zF#q5Ue+bHmR;I#O*eE(g3C;ajo)lHnzB3m{JC$q4Bsqod-aB?y7}W zXgCBsy~=`-S%_=mgb7?GT|=?NnC;u3zc7)99MWymr2tyQZTcJ{gA@i?K_dc%Lf#=< zoB0*`+Y$Xyf1}vXX^}Ro1ud5S|55hdL6T+HdFN#2^WLQQrmC{Mudb@@>aO-~+INrl z=)w3I3_J-C5S$1SAR$O3K?`AE2Ef3O6h*D5B{wK3#L#Z62nefKZER3MX?J7gC;@~K zr2R|#JNIQ)RSyOtl{0B7GrQ{Dd(S=hnD2aNqBuF(>&?v2TcHSRSTmfN>GdX_s-~@C zz25mit2)t7* z=_#1#v<_J}Q)erE5_4Xgg(An5;H8gaF2NP07 z5q@=&?!U@R9MQar!fT|jMq4K{>eNw+DYu6TYdxG^Ft1&B>EU$OICJ4D0O^AHSJdMJ zMPm&B=#8sV>VESpX&m+M6*A`2pF3YH;fBTM0R|++f(@?PGSuU&VN<WbadB7@%VZ+e;_dv}K z%CH%B**bN$%(vA_jf)e8vf*VnOR^g&8ZX42@{o1NR_imVaq}Fu)e^#2>L$}i>M^^hgON?OmD#?K&Yf#C&XHZadiD10D{sR3q6MfM z`2Wx+PVYShDnD;smi|-bsoI~G(_U|FV&YJ@`-U(YQITY ztqyccJ(gyOWs*zgh7Qdjygkq?HqH{Ak3Q50G*xDKMJzIJ%bn_^w$-BkU|6RmmHFH~|Unh;Vwhg0~U5mpJ?PmIO%4xccKEy+xZZe*GUpDkI3q_QiYXnqM- z)|l}Idbw$zF1wxo^WgZ1?|M#%8quU)ghrbHA->p&RqSui84;8!!9luWq6RnvPNpxb%!TS zmja3~=R$%&og8wpOyAX-xt6ipiVhneFal~BMJ_OMt!Nbr(^}gl2a+1KYEbvnDQy~QI}}8vSs$7SIZ){N5L~~zl7x+ZLh#FYu@b2>MZo*7+);}s3}BhIl9`va z$dH*?WP5x(q8HY(McPwlvWy!nMs}C@!0C=^<$RC*AYN2E0{QYz3%^H5HT@5dBCq@;BG zyLbIA+E28dpiQ$xpIi%fIrql(x)ok>Noa$;NROPFd_#;aWh9Rz>VOsb?@OI;M%2fBwyY3U=LX++5 zGS0g@%@H;2VgT@^y#el*_hqLf1C(u})cxq0`p3Zlb7X4Z(Kih{#h;w&box)ew7A_U z>vem|rYRJ>rCWs>Wg`pJeJ(aG$ip25*=jmA$U3*2ohqu|p5nAL>19Me^L%{j9!H3& zr1K6MFCriH-MhYtAtQTcJg{uZ7{{gb6i|Bg@J$R>IV9-z6Q{td9`S3)iGx2<}*W2Cg^&WZxunTyj zI4Sv)K;k+U9QR-O+T@X-Gh-jN>g(3=h2lrl?Tdjku)-Tvrvp8hb&_TX7M7dm3Li$1 z_4m(SqQKcUZHG37n`XmoT1tZdOkpTBrGKZ9Vrf#7F{d$(y)}RRa?2QH?lMIw?!Vfp z`@y6;bD)#Jgxf3qxZiKr8yvvi5)(9V)>HgDuop{D$#QGppIwiZ3g;Pf~5DhWw7$gbZO*po)aFh&utcfyH zcZk&QmEiL`GP@-+2VYL_zWW)NY{JErV4<+QMZv!182KKkz5 zbMmlCNWgLji8~z_d9tWS66a;09&Yh{6nRDAYi6huXbwz+#_&0FXKm;Vm(KPK*C>Q} zEtkuc*1M)@*5Gd`IUd=~LdD2aZJ1EH*}?^mD99}nP{Ns)<50N81aonbE(uS_Za^I| zMj0pL5^GoKtkH&6#+e*}L{((DgK5l!1#T_i<7$3ED>I7 zroaJOG-$9s2L3Lt(dyDS`HgV#MDBp%%L-iolvLvM)8)uELJ8Y)+g^0yQomWo>o4`L z`t8^=Q;Oy+QZY#l4uAxaDY4c6!yC{ekE<^ZY>W*yg3Ot0VUdeWHnN}jF;fKW9%N`( ze!Tej<5zzB%H#QOeTyYOK3|;s`fvVb`ZMY_FfJOOJm;K;E163Ut=5Du5e&O=VV%Z? zRlP($jX0r1QUz9+VKSDLB_r+&XC_y{-;+gQ)iPzhq4Y8r#hQ+SEEt~I+Ep?;o9h~m z95q?GpSB)Le|eA?O|7fzzu52_Y5Bvb6vJ)c{nE8FEm&>H zDP#ZZvBaHS=(26BRs60q?qXdk-kYRHc;4T_L53uYy}! zz#_X&8TErQmhAe+Xf!5S3adFx_XnZmP{k(@RoS7UP}Q4KWgy0ZyfKa}4dsUlV|~G% z91fGHL@iryZlKTI6Bo{S2{24kPl>1|PT7RNbdg|Ppsj8=+v`i4&iq_FnbLZ_deoYn z0QIRw#=xP0-%R0>{b~ep?yyF8W7L|LkIaI*QO4`=TRm9wXcXy3O`G&cf!`f8+7!iB zit|;1D1Hqyso~+rx${MX_9ZcRn+(nO169SmprUNznBJ!l{jU=`1P)mvPsJ_;;IVJQ zB5Gp%F%`^#_^^Dfq?A||?#}i%A7wKr8qP3l(n)stovSl%t;bzM*CI116#~5otce7j zNm@2vyT>V&waY_e8hrE2%yRy1g_Rlq>tC<_-|PJ_I(tj%u zUTkS|m*|NYpt9i%@a&@PI#j7ctijnD+602ag43ayNd*E>&iVj*#mng8vmztmtCMhW zR?DL#85vh1EQgTNRb$EDdv|mEy|F#7qK=I}{*e9NPfZ5Xm9@h!zkK|e9epRAPQ0+_ z)~BbZFFbq1ke}&u7%K&93PKcW_q04*EH|c zsRr$vx|#=1{X^DlvKpVzJh?>mno~+F>>rZc&#HSj_Y@s}b>Gb73Pi+370X>-Wo2jB zS2CR={dT8G@)`5W!0I(j&9N4yrpmjO!C)>$HJ&u)>_cX*-F_y9Mk{evf~gPM*Dut% zUbp=vX7N$gllN{v^cEHXfj#yT>H4`Sx)r=gbC%GVzt=L-BCx4*%&C-j3E{4^RTaWz zDWg=e5KabJS1y`;IC5l^ojBHD9Ii&CoEZ?PErulDlkYRQY`FVFzDx)(if%q`1{JVa zs}2bVsFwYdJCLG6J6Eu13|kEQ`ScioMYi{qO>xO$A`q8 z*cW*v#$;ti`4}~5WixGxnV}3X26l*iaK;nq@G07Zzh=svj6g?aQ0;b=OB}#P0p?{u72+z zduGju^&`iP%U!Qw_x(wltrvsdg1PW4nGWB%h}Wds#iRxTu0_>Cs1gHjOH5j>Pua~{ zu9Rv;v+RTgqp2@hKb3W^MeV!oI`$XKyDF9_+FI`VuNOJfjOk&`tgSw}+Pl&$_6tQj zDMcw9FnnY$Mz#(7wG?BYWW6uGJW?uxMHe*A>(q(rYcBpTXB5&Pccx@hJ-q(i(HW-Z z5V^<>uJ;g@IG*NUVr-sIvFR!`gIKt>Mx{(c!^5yXQv`BeuFyRo(pCwPH#N9*>`8#@ zS6i#@p+T{|#t_~ya|%>W7?nxQG{Nxk?sO31qdbP|C=ye^s&nBVZ`jyksHB)Dti}>j$$_`kr<9%G6{PhSH{NM z8|KAUzxxkJJUm7tt98(fZpE$l;z`Oi?cZuA?RwoVCG@qfIL%kBi>FTAd#@KY3T@}U z`_7!Xs9*n^;i*IO&iv{nIJvefsRsd&I7AATot=xw8zyd9bDECycr43>Oh$Sc80hZWC^7C zVz#a-*@XT>`!VOL^S5%75B*>E$~oi6En$W+Bk-sB-jtpueXRkXSGoSFgeLa>5VVTZ zh@-gh?n{OH9@1{P$*kQjuUj*i7&K-NO7%kR6b`U^9%?2V0-366Z2F}Lv`EohXu!}! z^LBDoL==pqI@AZH!n8r*Utm|LcQuGzR>6(K!|G8H40nPLO)~c6T>OEfjs#R=5Dy-9b4Qn!=1DbxD=z2mWmYMsn?5p3XTE0Ii8l{tP+%B1UIdmo zx{3sG?agD;&g}Fp5bxX+EK3}WvuO1n5EC9?!el;AR!&kH2i@-6iCiJx2Pf2OMIFB6 zqUoHQ$2iNL{tssxm`LKams#<}O4=Cu-RgYXf^y)HvAgkD`d8B}$~wpYe?GQ|Oi^V? zCm*v~ZwTJOgmu6>AE5 zSR3j}{1DqnUcjd(sAFvsidx2%{TN;Bso}>Ofw&LtQ=4*Nwwo!_^(2dTe4k`5eRUNS zRlu?n%FW+lc0>>Y5@Q|M_$8~B|W@jai29XH2JcY1c@vt40 z%aJ{>njAyJ3BS)_dLJwLJ<4 zuVo&xYtSB}J4#k}(_Cy_Ncu+ATbxfGHMh6a?VtOD7J-=PDYSSRJY?$-L>7tBYt|R>D~a#G%7bBgB8YoL>X9}6zuE6Y z7818^-#;6*XCH`Cd%^@>1IJ;M8pTjokGRw}xh?5Jo{Su`Y3%Tf;(9DnszRb7;3#u+ zU@Oj3vD;)Ete`rJ`;2F8b5F$72!G)Sz`tBI@U^CM8U^AfQdx}c+*Z?W_SdE=b_t_Hn(_t%TMM;Z(*Qr}5zz7} z(qR-aa`^91cA;XZT2eHqx3Du=E(4%83#L5!4e=62HuP}s)i{YJi^{ReS4WGhv_ZDV z)dCYc62vKsXb#WWP%79#DbV7oUHwAZq@3S<_o+CBYcACymk{LDB)Ohw8*b91n`!>T zg`@N(>o^;QWl}+_pg^}MSNuHoMZ77~eBHXuo)ZvLG&j6%() zv@vRE@Re&#(CQwPxxTjmqYfD;iH(SsRzi8k3&2~~P)UAp8%_kir4&GAK?>}tCb$~*-us@TJxBWxm zQa6~*&mYY#JUdYs9xkjFAq&-9mmm;?XkRJCyX(+`HYtX=9Cm1mKBXD<`B^P*1X{@r z7AERJrG1_xe=rLSx~nCIrPp=SED-&$?lP2L^=ZY{l{JH!A2Di-WtMAYQ)c54OOv7E zH)Og{2`-uiQBb-u>B86C2q^nc!=_XJ7w`Isk1t%hL~^^mM7LmXW?_N&{QJ7yMAG>W zJl3fNz4?{+6Y+1i7h`{JE2>7^l|L)|1b=5_*k_7E^`nqK&a99A+#Yop>f+!O{ zqPNWaF6DShsCRaEm*hrYMY`&yX(>Ncagl$+R(KUjk*dxtk4@%YHZ za8V!0hnO{Dehyg|l(*QKn}gG2Hsffp{GPSb>r+m-oRs@sKixRJ0oXRy^S66pY0fz{ zG(Fm~E!#`pTI23#j8o-}SB=KS^~h~DrtAnbj@yEzpHk%rl-Ly$2ZF-k(u`wO36ePU zne%{3E)=m*giA;`rh0gZ!pndpt83Ypl|4b13CA}+IHWrSN=X*u99hVvZ~tK>Y!6z- zWwYDsbX+PhicX;z6~D?05)4fm`(3trlXUX*Dx0_dQMWjoocf#b6^B?QOznX|>?{mB zZ8Y%Qj5oP_>&Bo*Yj4QkJXBO*fX=ofZ=Rsq#KbaS(pKP=8;j+wbUO_&h?nMj zhf9s7bW-@~2m2lfkswTS11HsbWvw1o+IGJN7~XCqU`uN8;FIV}pz`D4_{rXQJ`6t3 zA&l3=!gATzOChts*0mSMSshMyXAmPoQ*-ytG*aET;|D?ql<5{c4OiH83sjzZS% z^~=M*K_wp6+!8+{;rHns;gviHP-6QfmhS~du|Y<0iG z$`dJk$K`}JB-`8ns7zm8^KwyMp9{4HH6H6U@tT`keK86A#$g=I>FHNbzUL2W&V)Pt z`6>>n_wY&i!Hq1?qD2C0j!&~eN^hz-JZAY)b`h5rKPHXFu%1Bn@;h$;(>=9M`W zjjIj~$ur7Nd+%m?cyub>d9EzNml>s}pM5qxb?V_n0G zMSkX?vsbPp*RGvE|Mb&WzIN&M?XzyLw>pfwK|k!>i;H>+^cUImbaGKRI0v)+^5-W@ zNgp?1+44;jtCFXg8D4BDMc3T6(d?Xdgq-OHDV0Fe3|bzRIQqU^dy%V_u25~<3< zTW9n3u9dQ38F#%yH6mW36aEsM(g;*kQe7U`Aj93Sw$x*u>@qZ!h|6BE;Ji!xoW>9;0k zDoHY#Oc}tF6MfOEJ+b2V1MR4~8D7+JI)`Uy-uz3j5X8HXRn(T(np7FKcjH|+eqr1x z_aOI#3nh+Q!E87ibcw{o6ee@~VNgYsmF_@se-oYM)( zu`fv?5}`whlyDZRU^%nW+pG&-?Eu^A`1r~KY~uo84%$q0jJKeSM7?H&*;bUyQTAHY z%y>(RpAa|GYBepCbEP6B%vH0~y!!E7*fzU%cl-3|^u!5x`RY5%%iEyArvbB{>6wck z>U3DW$Ua=8UeKL2Q)_;H88wK3=MtQTvmUepLjuK_Bu|DcNkZSyKfhy1#@#~?6lSYs zQI|Pn0C(ua*ZusiC;#**Zh!jZlTY5uaWg%ATRndC!0W#O(MfzV=Kn)PF#+}Zr{++543Mer>@{Yo zs6%1{2ld+Pf2ycWq#nn5;!fTE?v58kN5xW3he@~V{ecT?Ru^h79hZVd&HyrwkL~EU+FJ7ho_2o8=JLD zUm^`?709avo}ZYxoRRm93UJ{nF&275C|rXHBaiRG@RV!P=tHXuJp#p_{i>;vhOyyg zhi{7zLCoVMTQxFMzcDx8^l864${YyQla%T$ji8((xkMHbL8MfEX4=HNEp2X&)_)|O zVuz`3+&M)*nO?m3=s%lJu}CH-XA3WZ-;O@A@D@relKww?ym*l(Z189|iVa1!wG9Uj@<#rAp@ndu(nntb8sA821 zmxFMB3n-nF1t%n#o^98I^YBLS_LtKuSDp!D>;AL(^VZayP9i2+S@(wdnblfc$=yBu ziBACgKDzLnJhY=fN|H`zxzkzdbbiz3U{9f0EBFPi03^(|99<8f({^@NhtA~Gm1FB2 za79h-EgELk>&yYP$?z<&r2hrYbFekW5oH@onn$+zXgOIftbcWg9UO;@oZ;Omga|NS z&lJFZWRO5w$s8^v`m$4d-0&JR&cM^lci+PwP2Q7CdM~yQ8MCeSwArotz52@MrsyWYt=CD@PGt1% zIqA8r<`OA&bGAWxwT&%i0t>RWAa9sCp>q_996>7z8(w>_ZH^QnWps`R9X9WwO}R7s zo9bcDbzljy&);aP$wyZjsFE-OexI zd_n~xeWjlxm$(+@!5Wg8)UPBZoj_(>Gvo54-cPiKvD`3%o7cj%f@z9l=SEv6qUgkl z6}iQAqoHYavW4YhC0|WhEK#oSP;^F8YPGLmWF`@SBpuoki+h|80Sx!IeX?Dr?~w{>bVXJ$%tuItk< z&8z)(HQ9lMKGZ)=(S7W;3t#(M?N6V|)zRekQ^&m0yfyXiV?nd~RSm)c)SFlE?Wj?w zT;CP9(k>zU!KFubi}1$i^a)z((RX$`;NTT~ZdNpM+@KVpn3343Gz$5TxXuH%!XfGk zuQB$A?s{BlxV@pvFl6|np^vr!v5m1a$0`g(%H$kliq^1q5F;tPkRdZ!{Ntevd@Ni1 z{6TYZdwXSNdHGQO{rcgRmDSb5hZo@df!E4}mK*%4i^<+9CkweKP4k4Sj7DzRoh`R% zu>~YYGuLr!e0Sc=$L6a6tn$_}>K=jbTvH%S)i6~P4)Bc&}F{4jNuG877{*ABP=SvA8I4s+>iGm?jmJx4%X8#&0b^;XZX z^ZsCaMTUqGt^HVZOZV^JKXMB04R?0lyu17S$&=5YIiq{ENs{q;ZDKljW~b4ZYc!rp zYlWJzmZz6yw94(Z(3!5!&O%AhBb^AVqjxIRbGmPob;G;5P2py1?s_p7<{ht02w-xe zk@3xCRi&RH6g&a*#|*5mqF=mZJCsKE;pj_B>_LGm zH+HO3>WQLAO_uUB5Gv1=gJ<5dqAj!}Lt#L6-g4=6)z#)}nM?Qnd+rHdUd&aZg6$Z~ z_cq+dx=;8j{0x=j2yf0b@{V4zOlLX@Uxw(P^Rm!XY)R9uCqf`bc&Wm*S8sTk`S=jVcn45KF9`>+FGIZJ0{- zQDX#fiLQDUxB(x=TpfH%$)~k7Z803kkC?Qhma{uqVjd#ZYVNCeI0Mpw(xHPBpKISk z?6AV3XkTZ}D0L`9JmBfew`7LY_K-V> zA3vk`B^HHVlR>|}2*@1%6m$~SaN}^S`So@>o9wJvB%l}VIfLNT;2wE+bawX0^lf>F zx4XXc3A0tJwV;}{Lce0y+g0N=fc);ZyJp6;pYoiO?h2Fyk!nOMcB^0@AQN@UwxNF^ zH0rT25w#w^4(CdHn&z94QfL1TX}`0sOz60T|Kl%X$o|;j3GCwzjgEE~L-ylIVaIK~ zWcjsCZ^<)St;Oa=d3c94==Z5?0wqMx?1`zSIroLYip}5wN>teQao2XsD(Ux40(K6x+1%fUSV zEm5FCwM$%@TA@Q8t!?SCc}j4ZT+chGPto7s2mqQ zGx?Cx6+|7Yl|>f4c95x&9Jbo>O14G5_kuzk=km+dVw?Qt;i0QG0JK_L_=hzr9_cVZ zPo{dycskbB>jQlOdQM{|4%~`YDNd24iS;R`bH!R)i)voIxH%aM`M~TfIoMura>Fc8 z+d*&48bH&?evk&W7Nws_-K11SZzo#aSfDT8HO&et7?!z^w&XS~_e>`$5dvJjMqlP; zCoP@uJc+VPI|QC;G^c6jf$`4gwPsay0nU1)34@0qYy{esksixnMT70t*eYEpN=?nh z-aPEVs!<4UpdGhTZR+Hid+YYf%Erde8A~NfXyG7ST-@jw>5^VGCyZZubMehLr=_>{z? z*|X3F{|XUv-XIlIA}m6{3HF+bS85Iy+hLVh83v`WD1IM^PzIuxSn6KUdv;lJY4R6k z8=(sOS9*+7^s#>)-nx}6FW*#;HQABG~ROZt*TT=9_mtNjn?aXd_|C=3Azdy&*H4=@K>>>V|jw z8SG?K4JO0kOPfMkzR5otc@J{*WI@?G-(dJmo;lpD-2dFOtHw$#->+7j79^Uf)ubDv z?R>>3)#84>?0Q!4fbC5fjZ)e)24#mDz`uPvnJD!2>*m_z^%ri(lZC!<%~+Yd@un4X zMx)2AMqu22REIx8Uy8Q*&V`j6#@&I8crbn-p^YQ$xNv`#ePF=ii3v{qNEDi@u--ON`}fncTZ)0)raR`23bB{+A#fkV9qWQx@^2F zN#>IwSe0y>+Y|9hoQq2y^h$(5a*ccdX!RLJ800zsv)B}-4OeF4q_6_rxfK3xASb9JXsLrfJ7lIvEz;+w{OR;&iJa9WTY~n(V*UVB~?r9 zu|egBaSs4ljik>sWQCKHwUD-5^iN4YB}hq#*Va86pGDrf+a~pWyj&Kj`SW0k{{uTS z!hfGdjjPz)xHL}1TF3giB*zvLm+em3Pa;RdkHyA)Rpsej|=uNAP`%xp(B-ZDD=3JKc zKg|P(+|#Oquqw}!>tM4hzghM_`&&W^C`Xa%ETAN;ba$#s6}nNEpZya4#g)Wp)x79T z-CsD;e=s(W`iZ*)aZnM1WI{Smx7_L9Sf{~CvZx++Ias}v>V(Qk3{wZ5!=U^sf#4*> z0roLliWQ?u&`|Hsk+81$%jIHqVzL?!*4<@DvAIub0o{os-!Icr*7j&r4CFr(dcjG{ zpr!|%Y^b9pQmdb z*;K~@g`Ad(dNYBQAG8FVrcqEX(t@!cwu-e$&0C>(ocpAfq?D8gezl=d971-|FJVOp z3PAz~y6U9drzT(>J6l3>4`u-s2CzE((bgB0ksz{*)zVkk_xmOYa>5}gcfL2;=#n7# z(F*H-Wo=y6chA#|%`brRD0QPh#0l&Qv6t1fhz6NUYwtgN>KBi1{?vP)d-mDqH(RbB zE&JG`#ZHo|RUvdQc>Q+p47nUTd@VH5^3h7EG=X8B6w~$B{+_SL7QUlynN&jX2u7Y6 zE^c@npuhrXMoILE*lWqq;POI~)X59QLT8!<8$JuNY2yG-z(_a9lCrkrlN6I+{0199 zzJtl7gg%=F`H{OQ#<4B&#L;vc!_X^Hi=dZJ{_5!>#DExnC8KgKDW_=!D7BP}R;K^A z^vN`R^2zAT8T{v8v85>5r(shVEyVGH$8^OkXgt61dlyzpNP1kYJzy9QoL;vF7~FY< z(D*(ZC1%tgFMzBdI-F? z+#cV1bqaoq_fEMJpYUaiydN};S9#^irqi+8_SWdRiuWP^LA-7HrllS+f27*w{V3V2 z>rs}3m9Usx+gQWz32i2>@C9(aW8J>@Ji116IGHr@?P^Oiw`W$WylTQGR++Bs%EO9D z$M(`%&+LDJx;6bd=16UT`ZKn9iF{r*;LDyQDM*jX|Gb!^Y+x}CsPz0PjUNI zIza|j)ecw?Ei|>;oY2+exPB&g&Axv^5js^!|KyF^CU%>#OA(2^ZRP{oMu(b|@kBW- z{aHWw*jn%YspYm-??&NziGKhIQk|Xy)7@w(yr>AC@;F(J^^EIjK5^Gs8@N!YwMl#C ziz1}b=Hmy@WQ(|tskFqk&WcR39u@l*sRn@6Iw@e$A1 zT6Nc-*|aX+TQP7ciEXI_z?G`kd)G*oazkCmtoOG;vg~g+NoYz{s_=9jz!#|NlWQC$EHg`A zMkA@A@x^ccnAeK_J-t!9o<4by{{0j<;;34ETlVzU?CBGff-HB}*1F4_Y@}PMWVmjc z*SDwRJD$*|S)YY3QOhFCD?8Mjy*b;9C9$)_m=Is-AXI7(%+D1MMHG?NgZ{o6k9b;2 zHcbUv6!{z~osvD`+Ei9Q4!Glz=NsGexrJh^LD}lms^O5ca&x%TRaL^?%m=I+cUQVp z2|w`uP1Ebq%(Lj0Ju}yc3w>+Y&R5#t9W{LVezLq=J<;y zD=k>9W8i9u=!J#jP%>50l;VTZA_sLNG^}?J?~H<*{9e<0qFAF{{ZY_}y1l9aX$Ol- zBp56dm_n^)ET&FyHwaOP1S~T#eAs54sG`@Pqt5FPuE+gCX^vp=uw9spov5BrlVH^8 zRYpw8NfV4ZNsfJwDgz6&Tb2*r!KM*udmeeqjz6YPy%y&PjA2 z(Ff#iC{c)k(4|z&Ln=6hn8|z%30-Fu&&+Bz@n+`Q96B7C7w(6p>V2`f?ORFMv^r_m z?*^U$=h+m;*(WRdy}3#?=RBj8p2(F81G-7{bJfB`Neid^)_S!X9of2jDv4Uf#6Jr; zb)jrLR5-JgZ$EI-I(Z>zOuGHAU(VgwqHeVcop(*Dqg;C3 z(gE6b^OT6ft!)6VOK~3CLbGkrFC!!kwL%gSqZ-y*gi&0AKPN0A%njo<*ix9ccT_Ql zdvVE3#?(ecz|;mr`^b!BWspLWiKr=!BSyejao$OA&!QliYr~J&kDuH+dek~op=2Y< zxh1>2aH>6FFLaXDiaTr^T`ZfHRV+v42aMUp2zdSOVl)I=cWQfkOIs~j6w_K?$Gq#4 zTyZr+w{_pj=@1Z+bSQOusZSM87??SG?#i4qw|2?y%M6vJ#?84vO&JLcConyS<>s9A z;Mn^i`CNPN9GLw$-8ikv|1xwKTh(E|ZX@4#U2`=uC*>|@aE#Kpg#{9Y+7muITg zxk}};G+HM4l5R%NMA7VF&en(ul?zljQ>W*}6{6Nr>b2cL&+As;lUQEv$_+E|9qMR# zg+pdWX=P=Ah!K({gZ9Hc*%GRc8Rz@n{M4;$*KWQw?$6+zhk7-sCbJXqE62e$xJNrt zajFE-k)4t!-6_Uha^o((@7woyCJz#CmFTgT;iESA^$3?3U=lZb@$~67)ULO6I9w+i zkOQ{JA^ys|HxD!(>om^KPUlyige~s&Q@^ zj?BJDC(npswuX3ecS1oRm-e&k}x>+jKCqR10Znh(n(za7SP-DAON` zdcu1vTCBO(vZ(@&)1ywzt-S>XEz=X)(Kpb}g82_y$e{`M>ZG*mlmYmI0sx+!~$v z88l{$&|W3*DBMBUB!PctmD8b3xyFida9HZFVu<7A=I6cDBTgZ-b%0IACzF&ttq-TG z08Y9Sgu;xta2!&nj#eZSvh(uGv;w(wsriwQB%t$(aruRJ2I-19(WuWDhd^gecv1W9 zZtSk(q$l<~bdb;MZF8|}r+T?T)lHPtth6$v_b0XoR?C7ZUfHixQRv!<2E$hb9V*45 z`3Yk(Pno@4Dne`sdy)usRuNvrS#6CnMB=kaCQN_~qIR;4$ynI0>YP!-8ZC3Ce6tL$ zY-G91xPKIoI5K%e9G1Hafbse+{FYU>avo3fP#PNEi*H%d=bz~73)L*SKS#^T%K}SW zSjsQhbHEbm81vVIq~=uw#9ew`D&RGI@M5nL`N7?G)h$)Xh3Zl8*0%vssl?gPH|E&VZySgd_1v>M5@Ecl+I`Uda4@%Q$% z8xuNM_~I9nt*wh&ACiZ!Jn;nh=c&q3dFb;4jGL9C^6>i)B@aFH+`oSAq0em#IXh~~ zX&s&9*c8KJa{m=sZdTmmPf@JZYq;kC?2YWT1!#l(cltCi2YDesQLl0QD)n^pQA|f+I?nFZXMAZd0GS-dF=UbtP=;JG#%t;xwxTyr2TOjbelHL}V0y_h5kDUmdoZOP^VD8a=u)s^dnmJ+ydbp$M1_@%7xmXtEKN~G|o0J7Mi!)7PQlnr24mj zR$n==*KgkSv`SKqDtapd%z~LvZFNS;m}F=fMAu|+hlY$=q8bvikZ1P0TKWSjLe^an zGV*UlFJ``>nmuLpe zh0b$!cVc27@%D<_h$iQH;F?o?&LO|sEY$%8HE8#l24>4!)oOTnmAqawa?vbR!|jP% zMLoQ=NeiO9lhbtHp++kaw%X&N?nU`xP)ti;OGs9_`S3Cf@ntFKq05v;?}um>Y8$*9 zTOoozJ2G)5IPC->ae7Q#G?wLkO*qWKO!6{Y0Ns*8Z`Ocr;qT7T7$eZZZP7U!fM4Tzr#7_bo%)PJC36AAK)qFN^FJ5$KV!=KIRU?oJ>?2#+_ACYT5SaI<7 za;jjy2+_g2VuBYB1(mT^yvdIuI(sP}VaSxc2hz7;N=i#UQ!~VDDW_5e#D_m)(lnm1 zE@$iHu*YrsRszumx1V{g*NBftjrO+NIPReOokMx+xa%H2{ts-@+14eRBim;%sC>Z) zzR8a3P0U`s!ZxBnGO`|^Jpdg;^e@axS8u;-)7Uu4 zsCb$+kLbPpl)c(;ssnAwI)FIT=yWYy`AIQ_OQk0HB#!ZDDya)MEp{Su6y(k!Q(GCiS zycR{Oww1D#D~fGm6a$d$p(I7AV>#FMs5X=|Nd$)N!?!ENX{eu8U;3HYsZc*&q-K^Rs16v@xHp%>9z z7$I>rZYJKle7Z~@FKU39XKR)TDH`WcA!?kuF8K9Hum5Gz8pbTdm9+9R zNFdCDMAgyQhOs~eJoMZ!UOLetD@M~egPK^7zLX};+ZIXZl509vm|Cu0cZHwZrT19Q z*Ov(R(b2$)HGDKF)cY>66e&&+&JQL_xnn|hu@_KLiiCMZobW+pVr8;r#`fN1mSyJm zW<68tnRv?U-QFDZ=DEi>+aFk*Y0x8wA`A-oDY$DNtye0JX;WS05Ma|+lH}O2<>i&9 zH#d(TPt(Ki3r`*bw>Wh_g?a=^;8^#{?Mh4sM~J6FLVT7UmjRlX;i7w+OgdlTA-wHI zC>M#fe-FcK?2V>-xzuswNHuiK{IG*!f>fv>$w+^6{4!7Sh1Uz$_~)fBd?DW6#&VU1 zuRIpL>CxNwwT_v`yT#$AyWP3xsmHL?hSlXpW48LY_FjE;tB38{miwVYN1UjL8`GSc zsuHAx>~@0p<<`Z)q>s?MvUDpFyC%$g|ea>fY$So8&piTKMpi0vg>VdFmSO4ic%<0tvWelzQJs&=(ejMZat+y!GZ zZEhN!e8*m>I^_;+4R=f@!P5>(-a<*~&IPa_J0i`gb+R>5q3t))o(mjJd)m`OW|Z%&zk!jgCRI;Kw>Dq?$v+^VI&x}D1 zt!xhazyJH`)YRrMp8BUm?86|5Yje#!#kB23 zPhtscWQ=2Pmdv~V`DKBFQOE&%aHnXPX3vB#zNLd*M)B&tNu{4~Wx_0Z#q>&Pln**l z)DEpEA|kFqe#M!b=sneiwDzI zg$tLiT%nUUH4k>g@1-SqAovtQR??K#1e4Y$H@_!la=MPv9tvSbd|E^X5k#@>BT2AT z&?Fx*ZvV+`ZDCMzt4_m$c`4Pp)-u5qms-Z+d1wg;yHINuO}L$0@wW&Xnv=d*c9fW*N_cqd&}Q*Zo-8y^lnPHjBHg+sCyxk< zq;Sq4eEV^&i7A?Ms&w?g7X66RoU!L$m}ql7E)&XlAR~m%qyst6QgTR$><0|>y=N+tNsY+~FdlkEps!=O zVjji;Q_+-Js5XmYHvi~BIyh6a{nP{R+S$2%EdGAwJGSrb#*;c_ew70;hsjHZSd^i z5W_j&k2NSJa}=D;`|dH*XkrcNuk|aQFKKa&s zk{eEG65)p_o9qYOFDs$_l7WdO-DR6E24%b-<6jW#NCt9 z=W{7K(l$Lbgd5YVoKBgWo8eqbZ&aud<(0(e>cWi%FKhl~pH>#1>S;TtQ-rtGZ?a+)I!=yWBZ<~bQ z{C$>gPp}c1FXYmoP=und*PDCmMz7bhrS+uc70zfAFo%bo;5FT#;}z|O0R#}i^#b70 z1+!j%pha3VOoL^IgcCgFv>c{HG^kf~x_~L_G#?YyHR~mbCqoCONfQ-x_1{j%VOeLMD_kwdFaeF7y}7Oe7K6BbZw_~#=N&fn7ApVd580ETr4nt z#ugccw~&acV`;W&kCDXE`tRXW&JyfD+~%x&V6XaGK&xyc(mr!H6zf#X*%hN!faYEx zOt6o=9?NYB=|Dh3bxFUjl~jtOAYroK$AlUFDQv$^+*3l#V5J9sEk1vRa-$sx7Ae~}mQ zsNY&m(?N^!9h`Sl(cdFZM@N#QMYVK5lecPWRKY`2OhT0Bf%&}yTkJxz= zN4w_YJqyX_R~YvA$QK?tB^M$Gvhyljt{n4n-;XEVce7XMT&(3gCHea^GkH(}fxkkR z`(F*yr=L!xcw{ck#d-~;z(QL&1?i-$W!xC9wjOBrPN%xa__wl<*0Hm z6zNm`P}AAj+4+ahKI`1LLGk3JD%#aL9l@VMt%&ZE_792=16jNWE6x=BqhZi*=~d0! z<=9zh6=?U|d6@>^Ro7jK+}R))URSJpJNG{`U(~Cv58K_;l%vh-C@#nwBUmo-F^FND zIu&pKA@*mHIMKM(V%E%4*h3y6sP+@G{|s^983@Bs=ehixy(t>DkwgYQ1`)MEr*m!Y-^ih1OM@x!n1yz{D)PSNGA~T_oa2kf)i<{RYT0Y0#L{i(#Jf z^*5(a-yEzso5Qq!!rE$F{tdkj{jy#+Ov_C*KdkFz>X>!f)EK%C;dbCQ45BaM82~dN zPbm-|vt!1g(KUNIOw0!2H@?fPs=YAgJIp3p=5{Q%vM8f!XLZ7l8Ll>>IMU;t$H5LOvlWeV9G?MGwJ%0^`!LK@!y@lY8{<&XxN z{`J71c>RQpI6zdd9r2A|!mJnls&#xytArnomr7Q_)jM$~TIxaznl~m1O#V)4q?$%? zinR_J8Ek+CNfkjN-L~{#poPvLor!XO&MeQ)&K)kA^s=<_dPQ2e7CCNy2r#?$x)Ra5 zDwWNB?`b3o*{WkG7b}XBT$UfPphPZH%RGH@A&OBLdVH~0ynOlWA6~q6ErOVL{r1+@ z($N=$jd1Cxoy%!lrfCN2G^Mjwu3k5CR|t-*5*TUFY}3Z~6~NV>-}No3@s(sEE&Er) z8HQ~>F3Qa|F;mOCgVO($KW4>!H<#oAx`Z=@`SogAIa_?}v6tz@bKi#`4ATk}8W%Ag zqe4sty;N&Exk*`0DH z>hlgE6@8Gg-Qh-!XHh2c15?eQvR%A#lD-a<)ZcW)x+(mzP$~Vofi{7iSTGNt)Sa*h*^*>YiBDZ9+SqD1ze&=aet0D!N14X z`zCHOqQ^3dg4(Yhz^d=qCS^K>YFMJQ@teyZuz~iy_bu!G($fiJeuydh0snj+#E2M1 zne3_^_9G*`b31}+W~*)}v6a6lTjtQlE1;K-(KBkPm+PB;*(5*g_iH{yycJsCEp1?v zq!2SybL|{4;K2DNbvRMQ@lt76qUB+6Na<1ti*mkvYHQA!U%T@7F~bTqn%Qi1Tl5{T zPjS^-@LU+UWBv)0-~Z^fBz0{XjEbuY#{ z#HkY?H^l0EyNE6>)*RIz(!oBJ{*ZG&W;#YYbx5ngjj?hf4E17q>(*P7$b0x){(@0o z(P3eTH|MRz#qDxZ%3b~@;U37BIn868uDcEr)NjCd592`&njifMd7R%e~#^Tv>SPL65sA`&8 zcv?ey50V54UDqh*sC)9Pr?g}-Nm97WBhQB?17_&3Xw{a%&FWx5mz!nc4aM$A!v5o3 z|MV}M5h6o->Y5pOTMF}B6gt~;_PGm?RBL4`viUj0U~zG*#0$4fH~r0OXLFkj`aMW*r%&56|()C zx#DzT%D8{Ri;Io3#^pyNK-Zy(_@(SKngvBpy;q14qXpFVcYp>-@G;X6Bh}5O2Z^~c z_(rgK_NV6${lm_sZGx@zBK5!wCuj7FbyVp*s`LjKvg5*EVh8+l{wdd^j>!_w|GfP_ zchWbj+x-v5@q?XUp7K)u^*Tt#9{|uQD>a;XC2|O`-g^h27{3gAYA;M6ub)bW{cy0V zc~g=vidV{Be%4;?P?{7hr#5l-U!caviFStZtT$fmVQ@SNq7F>)Z-vuljJ?{P&v^VTCgX{art$X zYPoJzD=gP4Y}?-^7kuDX@$1xS$hes z;(`!euaTLhiM4iV7Hsk5w;#1bqqMN*==prv=;w1*X_A@#Fhcu1)KpV#j4}VvXo5y| zxvbykQtJ#+>{`A~gR7$+#<4N&PUsj2LFt=X{n>4{#QPs!4(Dzh?gaIf)c5O%#S#ng z9l~OgrD>}f8ATFG$_-GKtWrp7CC+{FlgSG&lz;7X{{AbdzU^*#*Ev)?bFFat&2-J1 zgtar`uXLIkW?1O(OC_KA>|KAPetqrrzkc5cLxJCYH+8X%hu4gCEl$dXs$&+mz49NO zihF0$^mrE|ES;HTHwk;)i=6a=w@$N&LUt&Ot!}wmEZx1(@#fH*a1wX;=sCP!JL(C= zQUs@H64ZlI*tQ!%^Q%n?lXQBYl}onUPQqfhGO%a+QM+4-tDQDt^SIL2a#kEXJQ0el z8s(rdaHnpp43u|-=-gpSypn(wBvCTg$@gMU>^!%^u~bMZ@WJI~I>I{XIXihvF90O8 zli2p_*kyD&IrWq+c7EG6Iw#H?D7gr$L z^wQ1g$8P+JQUaT5!QYlPlHETz`XmLXLG)9^2kl@vs!#g82YOw%+k0?hV;8(e_=;WD$fBErkctY&{E(={f4Y|mxClO9-I##7GZ zA!BZqQ!5Bsko{;1f*R(x!Lh>*XcZjS*oM0?v4U?T%w23ZV@f`wl2L*WEOiE3#Uj+_ zuJ9zC^y1W|?nC3^{tNYf-@uHLFp)a^vy?)4bCqkvoCDY=>Q~%gtPRm>{H&sHo53-b zqtVEd4}f?GFxau%q&4h|i;Iws1##hIfr!|zGCeQn>iX=`J;(b76`JvKKineynB*US zIL}^q3-j}RwseUM^U6mxXg@T^Tz^EnD|EQBc_|7jObZ_Oh+5BDR@CTXK5js*@spM-Z$ z){W4t4v$z(y%ty=7cB(XGH(*&8C6H_>rT054Cue`nq?xlx!4uQ*Ws#*x~uta?8vp+ zr7ATi@Ij?HQf9Vs$T7;^H(<|+h)T+3&aYqXQzRs1RHsJz97~uUAA?| zERfx!ueGjb)`7X}%r-Xd2A8a>ndOq1SqL4^4;y-DRA-OT07|ciHWK+k#NmfH#s!R@ z34#w|07(;DrNl52lvEB)#vo?GaIKR^I!B-HeJn#%A8%IvR}8(i{0^bKs-r_?i|r7W z{Rw9|XhN?>{V?u^y`A5t5m*QKnDn0pOW1Oj!(M@&o#wY_%0(_aO}rFZTSIy%$7vmE z(pKYQp_CYCh4Qg(ktM4zkWTYVYCAT%t1bXa-%s>z6;;+o$ux{MBN0C2Pa#PeyM-X$ z#+#I@&k(aH^i8?&40rnfGxpwblAULn=v39oIiEU}bL#4@?&?q-C-vk!ni+*rm?Vv| zM%fBj#u*UN7%$1NUKIH_u^8tuiAUz+%n;d(>n@6F;FiqP)6I;L z<3J&5HB{}i8>WB=#<_x~*XtBMs~xi?0jz9Klqus_xC(G-1KqPom2m=11V!7_91Zdx3;|qnqx5$NIJ6Fp!1WW0Q9r{ z^5MgJaCSysK$sngf>e6xMQ6=%dIxFVv%J9kg}+B<$*ZvtP4>I>c!u%uz;9uvGj_B0 zxoc|&;0=r#>6U=bKCD^IM+$N*1$3oNzKZfG8^#oJe&iDiGH1(@l>ze{Se;a+MyIg+ z76|P>BW~x$QGP6)P8w{(P8x0#3(mahXVX54I6|~+xpe8vK576^YKZS(792ruyZ{uv z<%I^&DpAj2%Xu?cn5*ky3AP4dzB~)7!+|!-Z6iF*KGSLOb16s)>Sj&~8L4!&APmUF zSkZ;-_aKc{d(A=qJ9N?BARtV%fUoVgiu}e7v^xkIT0#Iab%;uCFz=$}P_bY%nc6|8AMk1FVWOA7j5O1Q>t#TPFyzV|wy2;E(GHbE6_O#XQ zwlQ*N1{DhVPC7zFF_SXVB4!$iX_RsJl3o#qu8H z;is5JqsslI# za&=C;%Fll0M@_ps#op%JUcGyFxj?<-*tMZsr2CPO54g%hp zwxc9KjYvgRGYXjhmnjGs_fEF*x3~QSQ(oj1ZNQvid-LPS6w)WyEXD1gvJoF1poei{ z!kd^VBfsE<^JUcPt8iIH=O!o5%?X7%HKgE^t$%rK+r#)|YL_85V#~*o3C5Un{J~BG zKs+${RM_wbxmM!<#{B%J$;tD8_2P0lUh`aO&Y|o`^0`lJ`P_{U`yLCv3sWA`$1<%3 zeZC_J_}D=d@i5E8`SbYv8Q$+EA4>=Et}P$?Z!WjxwcC;A&~cpQCR#^b&%tuf&7l^* zT;Xk~g^|sUA+_y!P~l(Wa}~xVR8d3(_q;ddc}wyOAJTs2Z?{7q({X(Z`KnjHuj6v- z*p3Hhf^##+xK{mujpG^&&du@#JYT@vn(_1FYq)>QzW%M_x&uS;QhPGhO2w^KO@TLD zeUQcRe~;t3f?NG2WK)b?@Rr*pCW@Ja=;oO|#wjCEq$8BtU*<>so3d{7$wuR32c-gj z4xQX-FO5Qbd_lmI*n4B!7hd2ZJqE=oLH35&uyAV@8oYg@gG;c)jM2w zd!Ca4MTnBjjF&Fhq!F=P2TApR+V&DIZrg~YFQr>arzMW&{O$IPysYm1%XbTpr*sn& zbH$%3J5Df(s6@v{K}3Y*-GgWiiK_31SFQ|!e^%2SG{EXe#))i&v`%;#OkH$cVOmlxg|Dh6CSQ1{R4mBX_@aZ6V77}^(024> z;6c>DKnW2T{RRZ6_2!i(auZh$`3U_460iHcW6m+Pq#MWX<}Mrq`yN|K8x!k_GE)Td zB4#MTx&?}o=CDKDzwIw^0OYQR>=dEi7Dh~z3gU=p9E zXB*DK_M^2=a7W>K<)NrhMx{p(NC2;yK#UYx`(9}6@8Eu9=7ADQi`=ZI*ZLyz_<$yLINworm&4<<^ti@$Vl3;lH-+ z1xBQ#Qe&k91{#wUvYa6Qw?FLVa&9HvvTB(q7apv<;2d@CyKiStZ;db?^5{`~lmh+s zU0ghc|469g!w~u-t%T2__%#BX_7q;5V;`;RDs(f?NFPxVfL9v>@rs__X z+Aonb#)vuKq+s9=22p@qLu~YaA;13pkSk-R6%?zeuK2r|bBvd6)U@R&jVae?fRNox z&Zf3CP{A`efNz2dXw93&35ik$V?lHh{=qkMJ{O54u415P+zQhrcZdMcCqAA9O({P) zIkYEou2J9hnLlc`aTJf?{)xAWC{o>&Q`BlTJ`WbqhxUkr`p?d|GcyxbfIwa;j4F6T zfU`fd?C8t#C(uOMb=rAH^|Sz3?XsV5JKcxELW!Nq*TEb=`2ES4i9`OugEN{P_>Otx z5_iX;56=4w7s}-(_@0pi;{YOMz1f^wo|!2dwP|mG4E{%9NW8x7k693X%oO;QE{tAJzil`*weYT`!C?5u?m_pc zi~kQoR(T-Oe?kRbjIYDOj1bK6S{N5>6;`0FHl)7e0ifccWE+QO__Sn60W#OdI~JRGvJP@Shcg%pPn(4krgcC^&Q^CHfJ(tFpb4?2es^fLAg zf8e3Xxj7gSlQ%6{$%7Af#i1TlzXR>X_m!8y^NwD5^C=O7rnOat86j`1#%G} zVk{I45e8&o%h-xQLaU7uK)TKUW|vcid`8SjR@%`3GF{7fd3l9P*MbUapuTD;BX_pq z=D9)thLdOjlI7pY?49GLp8(a0cqGNsDEAN-c_tz|IU2Gi@fuU#6RtPF*iw3HllAzq z34FPqblp!bExmgY)Nz?D|7+|-Z~}~+ao+fhHe9!{EwJ!cT5EWFqhn>=isrS7W_I*7 zSSUSrRI$V5gMp=YyK{Y`J@HfuoUfy{jMnD^3wu+c#GlGEH3^pMZ3=q;P>Q}*!N?i4 z7!2OwiO9`oAVSLp^RwA3ASbP8s+Y$|nntR3BbQNqsfcn~Y=CTnZ*BV`ynlA3#3c!$ zPmCHJn+DhqQdSSWYm}bPhJ0SAqnHaYMV1lq6?ZPZ{6(ksd=|E*@_6LuO{hn$IP-yz z)gUF`26aHz>%W<0{A>Mf8e}njz4Rs5{nDP? z?W#q6Fu#`=0>6{XEti0riF~20xm-Cm1kw=sv79hiq=i0;?GrbMa3jpvgc^v?6Ojbs z{#pnC`>9zz4~lSiIKYX|xxL=poV&DC!BPA{Q~;c4#sWf9$C9%nE+_$r$6Z)(OP*Z7 zW(*`Ne-S>|b##X(Y?KdfDva#BI~+!$39$UfGCStoT5Y~^Aq>~DK&9#;j_Hay2aV`B z-=}bY-@yHCIp3IuiG+!j$YB14gIU+dkL&aEw(EZJ&mHH_f9oTa3l4eF_u}6ZvBmYC zpaF0o{?%4Gz!6~gtU8WWe?M7Ok(E29o+x-+D6hF_ud}k}AeMi<%;nwSeOW4z$j-mp z(u2~jC_?Kmi25R)Lk1&{X6;1TpgNr@K4@kx4u0$VmRL0&jSG&c@D&52YK$Xp+D)BZ z1azcVC^=fbn#-xVhK*EL3Jw~wV3Ea@tU}tk#KCZzjW^G9FLSQ^+4-M4p{hN;p%%FyEEc^MEa*R4c9y5h z7G_d)0Xg1X>5^7|4j=l)wy!Wdh3p8j#Wt)5I0A|IW&tdG?EjeLB#gPxT|H-(N`ixG z!IJDj#eC(_kdl}vr?Fjsx^u>GRZ0Fo-C_|#E^!LPvEKrnw(tm|Pzt5N+f2?PIg0DP z`z23UvoWV*Hd8qzo%SbHz|#b3y&TqrUHNH*MS+)GxXNP|G+H2Sd7lpl9Kg!(9le%; z0_17QNAI8!f-)Y&!@te0paFr=qg{@rB-5?*hTDX80?^DTWRiarB6NpwTHh`M8VcYy zfhmcqBRVtaCPmQ@8p_T~=0WqM^`$Q{hN%e6DcQ_arn%8y)cJ*8Z9n{4Fx`1L+_hFIsmbV5% ztH{H;x-cteO*boK-88^XLw60>4@W#o!|qj`Knz_h<_Rq8e_;=?Pmr7zxNJ-%FV8}lfQX;T07%cvZxrV3|=7SX3CH&7M-4bgba`~8yBAr7!q*rVh5sI5%k3L#DUOJMWlhKH12MxOicDoxJ`)9b2lxHVMDB^f_pLAaF z-Hp5bke0%ULK@Jae$vepYD%RTHMEJEfQ*>1?(TPoa{a)0+n?CK|5br+d5+&{g$R@* zSF0;Goi)v~LftY$ce#rrrS{%$Z2Q^kjug)%J>|hjNAMj*YfN-wKa9o5+ayDelrE`- zreaRGf%OZrkoCB%F5Iz8HN4D^W{&UL)pDPia;NT*%D0GSww%2u=R;8rcUp^O5pdq3 zyJ%T08gO2Q(hyiO=-aF%iK+x?+Qle7TvC}3G+=C&u|w@5S1W?>j0h$U}<1mEsMaac}Mj;Fd1)m)YXq}b=%8sQ1lSDBH!f5^* z7jL`mu9ggt9oV0t0cfmzrXwqeD5k!QP*CKw(N<}xIp1oWI;UB-^uV53Wp3snh`!6X znAc%OzNMmP91XoL1n~6b%b2lTnM)5IAEwq8yr$anktiNWGcWD6>rVp5xZu=H*KJiI z%cmN_OZfQT1>aIRCe`U-Gq_+J9Z?((ah?K>3!j#vFw6s65+#XQy;KUs`ZZvHIa=E( z-F+_Wo8>N#fw`{07ED-H6qcv4s5l49Gt#DJa zTLH2s1|SzCor-AyxFfAhd4s{)kF8Gq^n?zj^?1iy?PItM9xY2*zKo&<25r zVWgKj%>4uwnctra zKicpA!~}wMKlAhekm6`Pv7@0d4JXu-d%aV+M#Jh@RtIY`5ceYM*8%+0#?7-UfaroQ zf%?J0AjwZ9tl%g#Rg!g6ht&D+68g&)p0dkx{MwK0gF?0PKPIf5&DAg^3)Qh`7wx8- z6IV^(;5p_@ZJ0JWBcHP+n}R2pxvNVE#Nc6~>P$#*2afXZ@5oZY*-6bx2!pKJi zTgo6-R^w{SXGe@v&SMlph;e$@-H@F2{wj0X`EBQL;^aM1Nf7BGvKe4CjpGnVKN6uc z>;BD};RikOyb@Hnnk1x7smIna(kMXne+4LRc)QAH-+6#{`N_A1535&nv$_`w;TVv9 zu$>3-(6OmN1(3|07}xwHfsUl$mw1=t_BlWU~BVQF<0 z5VE3vO_(^fPm=ROMG{2qaKCMtTD^H{_slNKl{|v2e~5XTo8A!ZoJ12lJ=!C^=#jWy z_WCF1HtK3Pe8HV@gO1y1=8XD^3!r+Fnl5gE;rvm2F4gXi z2|X4%By;NL0~CY-RvM1BCkG#)R`jk@y!^nF-5Nkg8W>GM>VZQvlnek`8>{YOvSkzt zmNwCB9Mi)ex9=;1RYG@AGs;aAG`kslzA7wBOM!_*ocGC^S^YFh%0;2!)DSaGrya>t zQz>8?c4QA@0+fc?#q+AL)BYBfzcJ*&mdq=j5Gx&=E%PF%rep{etT#;aqul9r6tnJ` zM-Oy=w_(iWO+#~dl>Y(9Ul6FbLqq*41=3?uWi@Oh6gLH9tWw6u9$)78BC{>`JfOuV z=fj%X&+pInwWhZ)Xf2&*Utgq}5oe!cflP{dAi`SCJ{^V3T@6G-37qdjOpcp=n{nyK z_0OKiriggP&r8f2wxma0cNV##;D>W=YnFYbb-O9S`StZ6`|AY1;Q)f?p(t99_Cy22 zGab}9lUs&eKfw5f!gVqFC5Qpi4C*PE_p!BgNMj4{PeOX4OpP z91`%&it|8F1R^C$llT9=DFS^}1)fqzLuynHesb!am-8?>bYr(=QxN!J(BMBnfs_44 z!KSU8@>V^FC6G}32j^zTx%uV;a-md0I19MeNdFhgfl&GKU+DT@{K2O%j>5AuJ-fMQ zh|;~FZQE(e!JWl9lVqUQ0Jg_1>_mcSqm~r+^J=e;hajz_At?D5i(@a(9&5Mrn_zP|WZaiw;<(gft zqslGAH87~xTPUJ4nJ|^!`Oc4+P+epZ{R61hNV>qu2qw8f??jwF9VrMFcSfWWi@=4S zb;X@JbhSLRvPPAPg=)1k^?GyBJ>%xShRl8`QUkvn8uh-Va(N8haB}Y3RrM(W)-jPP zMc1k1mbx>k{`GqzSFn{(#VOl9$xBGM#1nYn5PE7RdYo&+=0%a0jV@~iRmzy9oH_I` z8?Jfl;v(*#n<+FhtxvXE&Y_VsTj?}1ixWA=jS96rBQ~;r4`zZa1A!k!lH@)9Jx~$8 z#wG$_Ro9aimH}gqoe0i>od`RtF?B(#VO1L~hCb1BXLZFz|06zf4r%_Ik*!m?&SOAMK%XG{_YdbwIBnG7`s=;$~K` z*oIq1r?drMVT1k?B^d+3P+s?8Ki~p!8X)>Es!XS*&WXhaLQI?_Om+2pR&zP%uCLrq z1#CKuX{As`FYTmhWjSyO=>*M}ejtXc!#B zJjs{t@Qz_fVA`8mtXbuWW&reE7xRrq{<6n3_ut{kuP_fXVL|#}y6O#%h_?)?V7Kw0 z<2kKcJ2ZkOa5}f9nvTb#AnldW{662`H$VF8!v}5&f?Ezu7qr1%9A}(SzeaEfun$-l z44H4(Vn{9;GZEn6<9FCvOk|)#JnhM8M5+0?r*6cw0nHPEI=bSBSOT*HK(WmKMBNA!&EaLk*{2piPJ+zs+( zV!_IcO;R&Yb`ktoSe6d>vV*u++eY67ASaznI@J~<49xU0dZUARzKGn_m8OfTYyBpq zUw*ZhX+zxluAM6vCOl{t!C&N2wjkv(gKH<;0^g2C0QC+fAy0+D_cJN-Rj6B<{xwSr zPi_RqmdGif`Ha~_WF-aoIYo=%Rf0sZ^!v+h%Pp1gR(?+b83{#3Vl%6_vw*kZL%TVX z-;@<2emh%{vLylU=$dlT5*>VzbfhB?Z z*=u7-w8#)YBwB(;Kg0ya#UGq+wWb~jF%FgA;a6yJmP`A9R$8>gtsSZ0&06W=Bzj8LI26T;tmn zp=Hwk?E60Mjvz9=f!us7)v2U0p_iE5 z5iA&QH6;7Dd(=BB=56VQ`}xa9y``n2OTbrI+aE=9WksW$zXu)c4JCo*BAvn3ei3v* zrCza+N+=-_PnXa*pma*Mo}v|MQsv&U>bi=H?~O}Xie-~+BX6DgzH7E!0I3EkBWJilgu z+Sptwy`0bW(36uVvLeUzCOmLVR%5=oE&!FBJ+Q3vc?rd&Rw^oIDmh!0bIK=LtqXIR zd}f9RG_)%%+Boq!t~qj~K<6(8g2-%<9|fonE@9HH^C2 z#YDyS{TKVGxwc)_E1uj^OLDc9YP@WRKqoRoAAJ~E{2skYsBg5w9A?fcmK|bi_u!IW z$3?hc(T#|A!kCE#K}#UPKo^BSsXsZuSktvNv_K80G(X?YB@l|$ay3X$1 zQn@@+IZ1CXkK8+oTw1n;NR!oycAslT7lI%FxOo5r;7*tV{|r9jH~5HVdL+5I*${|P zG>?d7;n^}`4LLtcdW`sKG+t1fvD~j>1Te3xxd>&ZIUrP6LEg#nsO{fttw%s1OdY^` z{s{Y9S1}{lx4Fo>b2i6M>|9=M7S-m&>FhojDTncT2xZtVIY$%;{wHKN^+Z%sTRRS9 ztQ^Rw1GN!(Be*tBG5uv=FF85~K%hYZ$en;9?#d26^?!Tu&woBZS)K5+kF5i(qS6u_ zPkO<|T<>xj&w3co`u4`P#QoFPSt^MdN7#gvCviFC82$#`|8tjjNjD;p&e;;+1B-eH z>CgMN=_|Io6JJ?({l=Hi*tb-m2f{V@F@9h;E*!yN)|#;{6m49?{kV|tfav02dKiam zh{_09(`OGPH;_&sB$8us9OiA*e@k3^&pg*a*mLL3i|!p>bSf`fUWV%Nufa3bd$X21 zHLRAWn|9ZL)Zye79z&KHNm7~s88M4K*2gT`PSHFX}vF)Y`DYh~L0j;3n5Xx>`{ z{%0pMIXU^NAXiN^N7|Mg-8_I44RVSd$Uh|;m01jpb;wYChWV$Pfw-8llczz(usARg zJt>r5LzAqBX_RQj?agv?oes1^2USGsrJ3WJ3(;sY>(vI#BrBy#A)NK9}N3?0dA zpLwKois&6pDokRz{mcM3%zkw6uF#zkR6`4Z1e(z^enrjt-CvM6KrEu*K}jKAq?k<= z=;xvznTX`zQL!y>8meML4PUSSpPqH*+D6gH5x$(3x0{DCL$ZWfEcJs2I|%+a5;h<~ zxHQddQI@yF*9s_DmySyd?J01ykz#v4T#O2|cNvT|2ZWlSfAm>97mCOaJceEsNX(^DtDB z`teUf68sX5hSi%Tj+MV9e$7(E^Yz=&5-)LfqpqP@6)scCF6de9lT{wb*v0$mfRf3c zJU*>XuTSH+e>Rb_GAnsvTu6tB%VlI5JHKP+qyAaol`o5m85-5>#Eu+tK8Tvi)oXmo zbq%$7msz%u@o`3I4Ns@%v1AR6$v+6EKdz@D3L-0TOz}*I(sLMBgcqX<=5yOAN6(=7 z#wek=vDAn66&(GFQPN79-9*c8Mk&@j-3fJ5akCI5RI&42M4)tJMobAt9@ciUNkPWY_>CT+UQJ_FsqW6ziKP!csJnp zBAf;9ob}ixj)gSi{e)=1 zTG#-+V*P;MUHAQCuyTa$9iyVO4BctU1*1BH!m+u>CyPbYJukj%)M)%}PIPU!$!4qN zIz|))Hmn2JUU99+wj&FAgxs4q08|?r_3=#T4cVHMs^L(#xH;{A%kF<~2c0<)8htFj zJ42X;0D z#XV3Bl%Jtmj_aSX@xk|aE)bV7d)$K;@hb=d_D-1>d-o6Uub%+w3*mw#xFjMYKz5z9 zwuM-A{F=jgrU4C}RSKTkfs83*q`j%VI;qeo!a#G)Qo*r}PC>}I3dPW==kPn*{uIp5 zlzS`yNt%4;5ro=T0dD$-?(v|_eAaaO5o1-Xn!^~CD0V02fG7YokH)B&mG!WrB-wL+yq5OxA%^21qJy>)Pd}q+r%|Keg1@bs5gb_d(E;=z zo0a(Db*>%h^);fPn0en6h-3Z?IkcO4lE{^ow)NLX9vzHEJbKhK7H1S9z3$A{1nl(hmbeJ|*4y(?9*>`)e?#h-BW_h{)5Ra!M^xSd?+hriZX$ zc9QMcN{mlX)z&hSkNVQ^Qc0yAiM6$cm~rGKRx^7B)9PFWv2jxXaDti()K@w0*=HN> z0{%k*2?phOp%P*ad_F6~)RaVXNoJ1H%`C( z#Xw#?_s~Q4Bi%~nbA0-ql6FLcDTwSK`nJ`cg_)mbTLRkdRUSbB+u2`)UtKo7J)s5^b+4ds;vskN|2MuD#Tl>iD9;t+=$I1c+GhGsMz&%Ip zxgnj)k)aSL17E~4NkPU5LFl9nE=oe=B*-=*=ZsE>y@;0o+I8-9oE3kq(+PrOfeSRb zkqhXE(dfk;HK^ttJEqLCIei(kz*Dl?etX@SvvEC?!TJuno_3Nx0M9aJW14Lq44jA* zx+Z!h`ev(3@=f}U2zY)?%~6KRKwIx%qKqp_-C|c5ehVZ znK`Vghk^5jcsc@sGWxyFHo(KT_&$E!nsukAC+?q|ecU#_$fnnc*X2vB}J7qI7KI`alGUP^Qd49JF7m{lKH~g7J!zAY#3D zTFkieXiYp?H6l~#7I{+x67bY2w>Y6{o>A;dy{Q0A3WhR+;9NF4M6J>#OU5jzkGa`& z7rGAEUg99&B+2$ALk3`5b_#PrkoK$sPg=JS$By0)xJ(5gMw)`%5S|W|Zid)TJS1j? zc7p}sCK%x|O@QxN2#xM%gSTiuBzIBQh_WCg&uKS$7j^qR!g1D1enl&~b)%+o2R`h~ z^z6D`bIlqeo2`kNGWA1*>8OYD)z4W1Y6`8u2vG|!p^zLwie~YN9)?6s!o@8#pFn1$ zR-37ziVbVNw88K(a9cMi2HRK|1TI@WLj!7uF;&fOrKe9gT(PE>D^Xn;~lTEEXyWk*PrC0XK^4@+P+)we&*J0ju!1;5S zmqb2HOwO?oV6_G)N82nMWV)1SzK$@9zjaN-W6#teaO`SxvC*zdCsMf*U7?H4W?w1f zr>AO5VSW)V9_#A;g5&6}QnMo{QlL%o@L4eQ5aU{P&qi>|KSwCx1+A@No#U<%7xHN( zK^A7~Ojs}GNEQvi6_3^8xMYgt7s!BgZEWSBR32Hq`TD(87yq$0?w!qgXmCJorQr!) zDokN?xCG>lMsq$iT>GHoAW?%<4J|EayAOoO{K-Yp&+BTbm-Pt4UY>q8^!7?Ly%x4` z&o?v2nUb{xb+U+@=^iFru@2%UR+hQx8~l1%c#5OeK9T!My?JKuyDq8{uOG)nDb zyJ3{NLAtT`4FZP2wN)-|m|Kh;WBS3ZEX4cyWt(4z>NGH&786kA2q;^C>2@Sd;eQkD z$?b`J$_G-EVDWXO#McJhlmSGZ^O|T1*pY2Dfc^%MLK`?s_z+7g2nEfIvP_r`Y{YxT z<judbZi%E5H{4z^p~6FO(W4`w?)@*b$Q(jP=4Kg$@xI!hocWkYpUw72rhUBmg!O zCkT)rCX~a>(tFQfHZ3W>2t6@t?g@9u@5tJmCk%};@1XqwMWlj}b*K5k=K*Q0qNu{L z4EV+d@Y%$fnRV+R!qh2cSV9dhivLY1V^>aGwBYzS7<)jCq1{+uCzMZA!d5q~vfOmB zm}(*vwl`f-EDS}QezxOHPj|ahYw1PV!>Bfq>npyR6RJ~O z&;Uw)PgV^71=PUOq!qaUL7C>^oftL*zNzGRggryxx?={Kgd2s8=LC+WmT<)6MZb>2 zj(Zj2U^`8Xqzq893nC7HJALoj^&4f(Tg4F?UO-0dEPh?gEOc8Br(N4ykwO<0J#G{4 z6~_QXOvnK!GGzf+&Qq%%Uz45utEz72)>it;aOGxajdBKk%H8K@E10Wk$v#8Z^<-J8 zR)O{=-x%32l5|XoehoQDD!(9{u?}3Vb)uZ)I<|}}OnhagUO-T-HYkbdS4KyS4?t#= z+W@=ZkAH5S3c*YRt;h5C0!rdB=fsH#*A~?aXU+)cfm@LgWfcSRBva@f$7~x(|2@PK zcI04SYRlX*y8G)3y_^~X1m6$A~w*d0-b-=4^<{&rE43A(3fG2H{4vDBHD}y-ju4Mpj%(XrA zTcDe#tk;9~+;SG-rrM44_9fu@tJ{I=pa%IoW(7(5cC|hREEUjaK{ocOB+3-elG|JW zu{qNaTWsr%8{NX{n{IR1aXKA~H#~%yhTJTLnL0r!2{VEGw*;T|qW07Q(s&I+RS{hE z&;|flSQFLZl7PNSK*w2WjIoBJoQ9sbY1`LPKtzpuiESI-LmMYB{H-s)@S7k1_y?cV zK^ppe!=(&9&G7#Awm(Bv-u3aBApG_hFstHD(GxOXvJTzYw;E?oVWND!}_SFBvRMcAzj_CaHQ71SUamc9}K=e~?rL)V*z zF`2{YS+syS%2DB5j3WhT4lCRJ^4v$Id3G_tH@Jvpz{ugd5g z1m1H5yl5bCBP1bv81WhKGOy@nHkfaVwlj1)X#iiiMlo_MAIrTR+U8?e__zvZqvCjz z)#5X!bB}FnqI`Dx`1KA9=+5Qyzy;6eA*?J#$rC1TUi|~twcT@=T{_|5mt|zzG3JM= z3N+MglXaJai$6{QA`(3dVS}wR!iKRyrvUkcln;J>+7}=7YSnpuFMJgFhAeud+Wy^E z9#cz>i$~GB)SvuQUNU`6a?^$?_yFqEM9etE4DtmaG)Rad@-Tg9l-_IG{!5bL>Y!jO zhB1uq1$NE?+2c^76rW-0b*tLK9WmP4RB0q3+aEEcJ-ORNlzGX88AqAcQEU7#qM>fc z71eAeyK~_r4K|X{c!0$Po=D^31%N zvC{N7 zAiF>HQwPP}FHc#39}=R_@{m!K?=Kls$Y*hYkRKO-5h2W9$8v9tL&OBYj)T!GC?pn= z{~-|9&OiL{6+-*Ocn&`OU`ac6?AYZAd!hy@Sq^Ncpx5mBRZ&^N=!`kot7)ZjpCQYE zEZ>YQPz7<5+R~`(nn8($5@}hl-~{4&fH?j#6=w%vmSVqH3sYe9`}o?3I(@iVW13>f zGqYs%tu zW%w0&c8;IVM1{;ePD%lQ)c=RDeOzoh&77zt_)2CMk!}2rHGr)__^}r~K2DGkRv(9k zX&cO-?0)JF`MnE4$AA^9*upPyr}pL5x;L|1nRVWWFJsc13ijk@XZH`&qQ3CaotRP% z%*Q;;T19eFNS&c77=YbsP1%b;<&=)r17O8rx#UOv!v;D+Tp~=;lMW6qtcD6n$+gE~ zK>6g?C&(BiGGNqiW?b<{?SHT@TGqvjr&LW@pEh&3jABT7M$jP}cvvfjEKX;-T_6vm zNH-8(`YT=kt1tW-k4b1Qf~3IfY(=icm63&$E%%5uG=jc?`Wfgo598*(G0y3O!Zz}n zHQeLIv^Y9T-0i47^&9o>&1iC7O}Qv?Q#GEe@6?ydH(x55xzY14I|v=*Wn?!o4MIT^ zKeE**0ylkjBHsXIy^`d1y6`X87@Bb0jlfurMRa1Oo2MHZ(h(6=F{uOrwiyv+Zb3|P zeXa`z|5%qib8n`%aJly zKCan;k%{Hfg>D7+WP2gmHoooa<;#BsD_DSnosp0qK@*|_(^5yoLAcSjPc6GE(?Lb8 zz`#SPo87;9*_06RRxSXP7ZrC_*sYdK2gq`qEgbBFv&yFLHg@f)2WCKJ;ZzAkL8wV2 zahAsD%`wd}WjOY<#(w+wSI{T^nJBYy2mr3e!sqcUG6#H?Z0$+x z4Ew=KH(lP1KgH@l?y4v~%X;U$2$^!Dda67d8oZk+19@a;fWv~R3BMH=jAg`37&Bl@tDAp2{^|Sh z;9AWqp+Y-1p?G3NfLYQv>q_60-L72G0m zP($R^U4a1=IHOS`0Q?aY<7FHdRXUB^wP+0DB-A3w&G?Y$pppxYT?7&k2u%Y)6+&XH zgOXTB8{26=@%YU2G&1Ca!8EShz;v`h3C%XBg2zPkTan^ZPXMZ&+9}eG_~IpG?~&QZ zf4l6wT!?nXjVV4bvL?3scB z_b?llph0%G4FKL0Nettf)C$xY&1F%;^I}1?4l%N(0$Wc4(911&; z&q(*R3E2KqkRcx{A@Pkdina*zQ^ihiNeJ0VY=R*RvcZKBVqph@X7n+iq*0RXCznOa zj(Cw?TZ3zVuXxcUV4PBc>87erD|Qye!^&}0$%AO{#bZ{ZZcPGEnUk(sGS_#WfBx>K zR=Q_D7eN4(ojYtB;F?S53F#pTVxTM^)u&|n5zOE}VU2MFQNaq>i3h=iX=e`GRIY7KUgSoQWRC-F+fX?N~IjNQ@qXXpNl`AemP#ipHN?}gO z?F}mjj5gu zCW++-HWysoIgPTU_9YQ*zY(QKXJr;^s`aI4w))Vo`!u$p%!^o%8k4u9?V2#Ir z!&*WMS^tsY(u-DpTAR+>xjAk6Q&)Uh$~`NzEMQ?AK3s^@h2O9$i`wFsRY0WzcOfU| z9n*z(ivUHB*HENyp|s7^rqgatu0m~te((I3H@5vAf?shSyFp||Bu2y)FOg0eKLb0B zKq+iFqxPL;R0S0d<;&c`gKoW!!32tn8FSSxr^=rr*A?1!xZ;Gf&2&d7@D90_4cPot zdO{(iO!B;9lB6bVYI+=dECoiGDaHuyds{M371YA=(K~KCyl2nwVf?<07_lBYE8glf zdNRtc=U4Qy*Z&jW$R4wuCmmxJKBbTE@^TJ^({rdqjF6lxj+U#gZRNorq(NWb_J1%& zrp81Xb$Ad{w_!-La(eJa-1Zdr)Inr!jag#pamoq5dCLJ_FN?mM77gA`%MtLM5h6N; zLJL$I0-3=T=J0(ZLjEG3ul-Ot>LW_2V?cqoJMs)S{0WCkVZeze3EIikegR3=l%Q>V z3v@2X(*UTwK`?>nEz(x#)V5V@?QY1ZO?<9+Cgyw`9KU|KesbqTnj}icN@C;JEh6Bt ztXD?;Uv*{r(ZNzB+b@^1z*#CCqqm)^Rh9C40$&LN6h{RCYHd((jJ}94Jcn;r;1y0_ z@|lfL8Jmm}A3u46>PGAK0Dd$J;L{pG7?AuiEo^>IKO560j-849n3-Z`f7<|oDtDkp z&H%;xc#D+B9&-tP_UKVyH|6!*N`zSu(HEd@ady<;ZDVYOX5D>I@8Z8I0q$ds=ysLB z)HIvyyW9SEL_nsnvLonUKaDKzbp9>JfDck%k_0asV?jsSaxZM=4Il$1Stdz6uRM(dtLqC@R$rUQ)XZO2@ktQ(? zo##&P$JY0c%+7zvN@0P=WLH43mypupfW{*SlR}{ClGgGIz)A_TI*K!G=#2n#>t`ZF z#?innRwxyKJCjr6d~qsv<983h8VK723*vi!TG+!U4pEXh-NEk^-&#ihX+joV>8}Lc}%g zU4xw+RJ0a%4tCj{qF5Ao%zBgUf?R6WI}Iy{O12j6gx_>;QWZ&nudzo@DsTtlqKB>* zb{khPrhek&4a&!Lkmc9LAZz9DPD(2T`WYW|bt`fgmJH&vt%G&RmVnP<9z?)N2kGg08?H@3DGEIvo{y(U zN~bS}Nay53DFjo;ZKGddapl;I9d`#4vk>7C6N4wlTZp$4{~3`b{W%GC?6Uso+v~Mw zYK;R<)ikS)V_vv2SXu%m;iP*2Uth6|;gFc4PT5j#W)FOkvTgLBv9XW~iETO#fyQ<; zxvFF?;QC{YO-tH%F;F2|9D6Z#*ogDSuJ*IQ^pRb$C_75G+rZbvd3OOtmG$~5UBcO6 zVlxt;H=<-q#uyX%aUBB(gj^()$)-d4*Ii$~hEdTXSPE=aMDID$v#=;o0fFzW5ARl#-5r973%8xe{I|`F+J-(R z7Xx%QiZ6{WlSIVCLFYJ1uWjY%W$yNaC=ocYVoZL~vgU!Xjyxj}d#N>Wh6ULvGfFV2 zF@#_l774*9CX_<^8jDg8IddrJVPgTcnYLyTk|!R1`1Tubyd4!{ZWx|f@jM{Fh8}-A zVn8XBw+Gzq7)xV2Up?%owz_l44o%yE{o!!_Jm6+(e#(c!=b3z@JBgQwW5-hI1=?a)g_~dj!Ue{lj2SJ@?W1wmekSzVefIUAl>;a9DEzRs z;T@=*>6oUcdFZc2?ca)Jt;ljssVgVo`Pl30Q^40b!(Fn06sc;WA!3O)u(Nv^5^zlB z6UCuG6DvIj zgwWR!4vY=Dc(qgjmfR_hkr5FQE;~p@P>G30em^GDiSqoI<(NEy&7$1(q;d zNq}hbo4Zm-h>nKds34jhT5wF$N|~lu=YpUe zZl|~CE_fa&J>*gy0YwIU9w7k06x2H%fZY@UpLdPt4Cw@-0a_UPrjjkxM9{5RQ&qKz z*Pe=jd?Sy^eTJx4c?tH5ihel@qri12ZwH0#8htpdgj|Gc*L++rj8%1nV3tih$E)SpfSJ?zY?jQy^$Ys)L&#EVa1Mkphl;(dk1BkuY1rbQ=;c-URN6DeCJe5D^>v zxJDp1M1f>UZr&Ok$RL3Bsu%2NZm!*)nE~cUr!zmFweNzw?uPKlyWwRq%zOEG6s~-V z%i1WzLHY!99#LzMviP-&01p@E1*De@VO|8Hc>atiiaESifQ5|aQ%leVGgvC1Im}6! zG@{wZh&n}sm?R`ls(_74k*O>pPNWdX1YHm_2{);X1$E*pi4A!Cl@05Jjt2~u7+SyT zWY!$ms~HJ}Lz~vZARKTCEl@XX~aolFE9E zD-<#o>Tm!h3Y?PY6P}X;e=Dw6a{h2nQ?xAU9@J{Up3kdl*UT+vRw95Jsd^njl>!$T znNYx_v=y`BSa}pDH*gOy1Sxdb#`V3tTYwKQp5D*q9Ke5BAeFd#|uB5E=T z+t*hBs5P955GxHV8<40jM74}zIN#m&JQgk{0(%kS`zTUKaom#;$A;1+f@c>(+6;<~ zWEzf{nFZL(*kGUE(tJ!CiIH>2K*lY$`ycz5_uO+~gsveWtqEy8H7%8+A>5IB3W(6J zVu)TE)m`r}F>;NI(%6FqYbVNn7!%l@@0t3HWzC|ol+z4e(9n#cqnb3THoCqJq~qHW z*Y|Yo{8>9~rZBz6#t%CMH^2I;g-~@u~H?6$uJ*Ur1G^P&csigpOO& z;bc*L{y2I^F>5BNH^?g{t&Mm!t^k)3U*Cp6WN9IMw^jIwxZK0eIv`T2hcA{&Tvg9w zs`&gqZuO{h?nq_(_A~42mzI~0EWMZBiiP@B2|EyGg_Z|J3G7>0q_Q1lVhKMpO~Dfc z0nr_yBWJ+yR6k46lFhnQ#x5JHPV^<^GZ<@ZtpEDzYC3(;)ti?7!)^ufGQ8Dl*|*KE z0l>gR`5gwZOBg5F;L0s$aRwO=pfAwdRrp*ETtVHbAJg`*yw<=^i?Z!O)u+8w_Ry__ zt7`V~_h8^q(2?xuF{a7PRyP+Elw@8=B3+jNMgc?{d!e;rm+UnHCI#&;W_K9Ytwj3X zy$88K3t1zGgm?YrU7A-m1Kc4pC{k}qTe%>MMVF6L3@krUBv zbYmG(U>7_Tl%ex2)5yW$SM@@@@xWoLZ}cr>wagG!gOv|tO_fBH3s5HXG)fIW1G_eE zDvV*%;GHD(B#4xfz}?sb)J3e{+a$ao0v0Z;H-uR!kT^|Br~9Uc;#i8>R#oJ-Rb&-) zp|xPCFP+qn%p&V)AF&P7nRaHKy%}5%468s;thJC((>~4!yG_kJziBLz)DyHDHie-B(?`0?2d6X7mkLM6)iAiq) zE)a`Pyp`<2*b*b=W|j^KJ`f^p5=93{tH+ioean8F+~GUD9%UhWUa?qidQ#r54^&55 zYi0x4YoNZnuIX-Zt>{j9*{nB!LF4g-t1!Iaksv_vsESTH4^ZBg4LAzi|AJEmBq_#P zU|QXv2!vT0SVCwnH^9J&9l1@TjO3LFIY!bsVF)V=;fYBHyfv38W^GXnr7@Jt2vK=g zTw+KrJjBQX%-65reFd8LDEtVD$d2brP}VCEh)m!yRO78bU{ry#AbkzWrZNcnJ6rx; zd&}?tHUJnmFv?JhZD(cIRef{L290BgGiCyNs&CA=id@Oh|F@|*VOQ;7@4+<)jmw$} zDE3GWFm?V2qG%{SlYRW$0C2tiQrZw;ikL;9rsN2+2Jr=3#{%TB4D8)ga`C< zTs=GnE1{+p@|a;&(fWy0HsUOsyGYF3@oco%BH7%_j?AZ}<@Yb~od+Lu_wT>YzR!*B zXk}y`Gr3{bp-(+Xp#_rjq+&$YX=W5{@cV|t zji2CgF|mVY;xmtb$QS@3V#`0G)4sm`-~%*f7W3}27!-D;j3GvO6;nS9JyBksJ4Q>IkjDJqjfX;`AjefuN=Z`)7F!CaU;t_f)TXp0Nl`Pd*XH|7N{x(8b z482*(i>HSmA#@?R!>0`6sY&3`9zkM!^=Z?58sGrM!}tM=Cj-?puR%-4F~)JdB@UdC z1kBO#64){lVmH2ipK|Ayk0_Tb&E{z2G#03V?Qft@tKGFiFYs3|JoQk{-=B8=l;Z$$FT2nMX2fX~jsq|Ep13Bke- zXSvCqjZ&dVbQVN$q97_%5QlXksiEo%vfSJF5uCx6N>M745zebfmlKK(o_GY+?6&B=0TDVs7PVakmS*q5Elmv8v&smp)qeAX#`>~Z;Bzns^qepGn&*_VKL z1T#ggH^ZX;Ky6#ameWa9P%tH-)yJ?(?dj?5 z+oyL!NFmOsLNNj#*^*_r_b6Tp{KsnL33X{n>y>;>50CqPRT(1ah%N=>Qb7f`K@5Kg z3LX3d&Pm2aGJ=FAuWvZOa4-YJ2&1T!iQ6bejvkLd}k z@aeU|SD+4vE?R3W+S{=zxIV1=!#aOJwMN4J#TG_hk46B!TwfoJR!#9x!Iw&NuCku? zu0Vd9wlZVgd_+8TC@ibx@}DA@0}m8^e1f1$^dEau(rDR&s&@q`B}WR%!IZ`h6#!$l zI)l(B4FaJNOGGt87HzwE+r8T!#AIjkTqtZypo|ooVnG_(4~>1oFYFZyTxPhFR|#69 zBS^FvtU{FHkMI}AE)d-kxm9oa0i7Byxc%QnfCkx?D)K31abaIY%jEQw?WQ73pGIzr zv)q&*DvLWsB?8a@(DZc$Ekyw@s*mn;fK$b{#GE^|cgycr3S!|1UM~lVfoM-o)&L-B z-e5j#V;Hvr%tUzkX)SH)iX=m3pyHTUWWnSBP>f$jCq0mZ0LoNRM!oeLLA78yp@-Ah zHUpXR#=p|d6ib;433Y6t22!7-80hW$?rQp1wH&Fyjh3f!W$>n!C(k}qbC8BH_-c=9 z0cyam+oCc+Zw4T|>1S>FSq(su%D>VFg{Vyx9t7(BT+Ab}dYZNR)*nF^X)w2`l59ND zfQ+BT#K~ZxP?DAqMR0@1zmh7S$058~rL)t49Kj!4r{CZ77x9Okp5VM|}aQdoe31_*<2{g}7z7gaw!IIqa7Fe!?*shq97pFi_ z3CD)DQ4o;sBDn)Fa1xbDbRsiUNS?p*!2{A6bTqTX-$bou0Hn~&Y!@okDow@}Ed>5b z|37K(9VOR!-Unmm_TKNE-s|qn?9A-W?qUmoUGxqDAV82{0SSO0MUWJmC{m&*N+L~J zv{hm`ZjvR-Dwb`VTp7+k|E<{o; z8DeHslNxv$p?WW;W%I3VclN??q~W&7al-G%F7ah>Cp^@n=A+VcRv3# z2oAh+k@=9m29ZB8A7U-M3T0phUZsO5Aj6>(8xM4-(DO%sC+RUs$Y5GDIhiJE50l93 zQ_vBUy7#QCpn#1YqgC|FV~;5`4-OE;(DlR6UsTlmt6Fql3+bUJGcuYnkmH^AJCu9P%l9O|OlNm^dpuKGT0}IwhXcQiBHxT%DDJ_7H4L%gdX~+erUT~OG>R^ttBSufk@b3UHO;%P`Sdz_sp&+6HEuYn-RS{ZI z+C67W1;r4v*{?dt=R=+l=3uC1Plnr&ha-f52ek-Jtkn>45_vkM%h?FWr?LBFT~|dr zr0i~r3H%UAmJzBKSJ=^bn59@UwU4z#8ev|vE0mSqB;LX56LqrBHj8o^BSwJKM2S%# ziTq8HKB1UpEi@6S6z^^EeZ)NKvd1UT;P{q%@cR_t{}LvU9-xQhkt&%IC1hD8BUyU< zGoSIE$KykxZc$4Tic+EOSd|+RP^Sy{O%{1946-wzy`cESvFLvcRvQXTEde5HjrmAo zi=E7@E&APf(}>*0ltyxZQQ3HcY(ts0=U?#ey6gDy%kJ;FcOE--p|QMteC208_OSdk zRPwr;flB^m3Bt1?SHrL>SHvu+tZztFXyv+|SHS%iTv7iE5^JL{I@a#S_m0Oxw8IpQ zd7}LIGp0XLaTfEx5#4uJD}a@(N}ZY3t+j4vTF$B^x!p~)?`jX4c23Wuebn+N+;`yd zOhA!KWOhP~npE{`VwveZe43{1C_AuD3WOAu&A!6&fUg%)gmOmQ8|PJS83T>uRYHn* zVDjALg@~L2I0r^CH?X>QCeJ`I3k?*Oh4-$k*yuor$Lz*C0iBs=Mo$3gMYg#D(xgZC zAS{aqXTQ1DlWS3-A(xUQ>eFHVGJPVr~*Nccv{(WEsPq`@YQ3}&7Ji2$dhO`nP6CC9;c>G~~xcmDc71PcO-+;Nv1Lh2}B)%6bu5NpOgN6CQCnujn!ufz)A(JDfBvPZ9V?mIL~C zgDpfT^^nrGAfXef??T5Zo-E$bb9O2n4vZYT1zl#ZDr97cl|Ud(Z=C0@{(uAPE=J z@!PNrL!_VIf!VV|7Ys_`I})V@_R?gto_xCTA*M|{JVF8X_-pZBvpJ^l-97YMYWcv8jpg&C3D#>j6i@&WRX%|$laJmAwh0IBUtqQypu}@W=J_UpVeJGUQ zn{m7v4-B))#`kua!Cjdc*toT(F38pCqRYYfuZ5Eu8osQ?FEQKkr)TEF~Y((=YRV4 zr0Hb19LAGLMWFuN4()ocK0e`Uebl-l0PP zREqJ@wdk5s^Ii9K1P=wiMZbHjVwJw&x}SSnEo05>AMLY_fEM10wB=XOE;!~6T!09j z{9numhM#2Oyl|W9K9=QOPY9}29vO^$^^0G0-4p23ef+mhV5Qt`4`F`fLbq@>Q4tX- z(BqXNVG?^_s=?i%es}o)_@jF(hyzu>DY)}4FucjDP_GkZVlv;|c0Vg-DdQ@PA|>Rp zGs0v?#+WDP;{|*3S^3fl`LJr_fEAm`Rn?V(m(>b0>TFf7&8pjvE1rywU#Qb_JS{R2 zT=JT0x1#AZVvBYb&IAlzLTSpIU<*t#Z{oodb%VKQrG;46Cw+g#)20P(u*}P4DE1jR zd7#^imFEDpsd>o{{X+65-kE&nDeg&Z3q{fX4o(IQ1+}=Kh&m2T27NAQ+-xEeQ$0qp zWS##rYic&-hV!$?SOKO|70x?uTJEBiMOxGUy=_Z}c8~O#wKLL8>sA<~m3%5JNm*1V z=|&n(EMFb%_m(_wY3XIbbAcfYTt>%+6BC%dI_kerED~NgZ<^;%lN=QY8hue z70Wn1&xtAIpN8xomhnBn=TzLpFaJ{V-OnYSeJ1%dmN72&{Q^j1Lfs&$%^cwe-~49v zg?b{F=gab`FL*ug{*XiSoXhzBP3T&i!pD?z&lED{5lV%eX&@oU zi@uh(J-tIsgpIy7(>1A|;tenpKe_GmplZfi<3p18zk1`*Ji?dda{lv3ZMkyAJ%rKj z$JhTgzRocgAYP==GD?CYhPD74fA;86@vNKU5a~sF3<}ij&!epFR@XB$*g{s4Ueo^~k8CODJ!> zprYs%VDyF`7;K{d8xy7W57b6|?2M*$U7*RrG^Im%Y9su5$3kVMeW1|D|hZa!Sf;QD@N+x<#ma9bEGdu+#mIN{2cTcq-Jr!7u*@|J_LhcS>6NcJdEjm4*K&%J=vOdfEiA7 zG)%nuxxg1v{9ZRVKm{a*hDoZuC^D8MS%_`c4+S*e3%V4 zhu+cXQ|bzer>mKllp@-7RnXGznL91?;N`a+!ddWHXv=Y01RJI)c@7tc(t9MBM4#_-M_ZuksVn}vQmxIEI4W(Qa3(>0n*i(lZEXj zU7xWDl^H|O(v3hlcX`5Wu2{@wYg~7cH$AIf*KEUli4WYSTXM^qix@6<{}c+YN!263kI8P}2AhW(CV^k+P$KDf>|~Q~8h5yY?V$*_ke&e;fZOeU$;pl! zr{-i&)DE7ttg{OZEGp6B?}5eW@-ddkCPmU5*ud$cTT(|D8?ZZpajR=68}Z?3aedS- zUc@a?oWWVPo3KMXJ~ln=FkJ5Y23KG7sVn%wg(+n)d~m*bYZKGki~Ij3_>PEW9J^wx zIG!HenUNliB_2=QqhZ7bhV85#&!CrT-MA+shP7POK9eh#>vgv&zzq`h`z?1k{CAF^ z*HJQ#PEP|UBZ&7{5$9!)r$o5*BxF8?6(C%oxZgQgeH^D^`^nao=?`!zlPZ5h&4yFKB(&-1m@&@>9zEF2J5{CfWt&B1eO+O_7c@> zkPJ|sH_{@Ypp&lGxnAJ#a+97$J|$BO=q;(+4_hwA|Z2k zU?iUgpHdh++^AotmBCs-pCJ8$ev0gOA_#iMqrk~O-VxfFvjBTBGEN7$qq326rwvJU zk>q&v&Ym=boHSo7MyyBd>^Jl%Wi|mQ62X{g0hP~0j#AZIwe80w=`nlY4BP=>+^s_r z{x&vjQeqQ(1HmM0p=ca55{MOyV^qy2$)7TjDU4Cr34I9YSIK&tZ(46OGkonpKYkz; z7JWz-U=$fP>uPB|>^KiwU1Z1tr3NY0$gTEt9HaEhr87lj&XezRsxZeBwh zsqpOC098MGk;E{-*&ruuzCio;{O%>}$IHNg;-O8Z7fSHmvzQhfEc z-PV-dan{B^i*PeY$UcU5*tXOIIKJUqFQBZ9P<9mff1k!qv)n{RY1r6!wGf4-G$WL6 zJb>s666ZbN)d!_Lb=2S%%f{nK>`S>*_dJLqj`YZlWOM@)2!wzxc@>vr=ltXnF;#C| z+b^RWAk50h55#tRmZ7&Y3!h=vP^7ey1uo;It4L5tQ-c$_;N^edf?f{&jf0Rg;qKK_ zo?+==@g8Cb@d7N-HZ=C(Zm;-(k|EfVQiTNa6Q5u^fcsE6N_vQ|MJ_yVw&vVor}RZ+YIWW%W$ zG5h89oLfF52&+AF+7eIIIAtKW`sh`~d!(8htl0eM*w}o{sXeR7=>Rc$H(ODTmrFxo zP3>wYj>2;U6b=h;Q@%fARV~;#4b$^6@+O=@|9RUdn9l7n-2rR|(u2gBnEQ`;$B*M5 z`n}-!32UdS?(8BCfdNta%PYtniml(*9MIb=6e>Z&n&pamxSj3`JJoq{-!4@|Y5<~^ zHk!P(^&(*4H2|zp3UCJXiQn4xVJsulvoRPN(57M;^BXy*Arh*;*P5NR^&(f1!}_W* zSJ^3P?psc&A9kpHw@A3W&|1-=-9o<%Y9dD6oW6@Bsu)-^HZtNHY(2Y15F>a_j*UMD zhv?}%Uy^ovF(Vxu0d%UrT{0Zb`KxM`?a;+)h-rA1uc9JP9_>Yu>h{b7#npS zKT3>yI_7-w$a#e8uUt^vkPNX@MY}1L9|&uj7dSrJiikGAw+st~XS(WTwrAPqVJ0R6 z-Xs451;gniWJE{H6X6|s?|iAcP<=e$XO{HEVS8CU)RuWqn}53%+V_1^6e@rlZPZh4 zNkqbIrhqO3Weoo<82Y!-zla&1n*q3Z+23}bo=WifWt4IQjX5dVGgTACilTNL ztzW20(irawS&2WgDV_MT zoAmopv=l)PHY$Deq^LQEoEF9}L#}@xO>bUBRa=M(Mh`9cYSY|RI(s`ZY45oS?Q~FI z6BMo8Wk0tf9ziLAgWk!8=($A|WQD9tue<~+18Sg(ozSsNKiKwbyhqewV zaqDoT=V@s>`iyXJl(c36Wu4~b1@sk28bxhN6S;(B%cv?tsM2;&3LDKuWy*diqT}=# zRA&&&{STx^{rFSV1=cQ5c+so`#qA!>7r?Rg;!{-O9pE`Txyu)EJm+@cf~Wufdk}2b z*h#;s$1zbvvLW8d2b+S&?>pq&I6eE!c68*@2WIutHFTSr_YQjA!Ft9X?FLnxV`sxP zSl`rjg|S7fvrO#zjk0Fq+eY(w&j;ddB|CySlE_4SN*#H~jg+lx;@~qsW$A5hFz}xZ zVbkbu_0k0;pMi~HVFg~<_HlLq#X!X%YHVFh*N~uFh0oTFVk*{IyD)7u?`<`;X6xMZ zJ;1mly(kPT;=U^1>ItiA%NW+0?xjl~*HMOP1X{|?7}=drqkwE-I7y#Bnr<47qqb25 z?zwa{kg@h1cpc&ykEcp1=%$#89j+MVx+hL~2M;CAUl11;)Wz+YW#gcC>U?tK>fR?& z^JO9jUe$azrTe;VrV*CMt|zxSjHN$>lnuf%ftU>_wuWgVuq;w9>P>Mw<_t#7ar8Fc zLWpKWt<2HTNW+M3Qp!sL2BOR$lI+12uU75ymOXYcECU^YC%5v-E3*ag! zwze8XCWnq3x`+3+;#nhC$PjU&VV767IoYbA@p{TOB?2ra?)n~_725d3WV?esnnyrs zgLl4x#K5|BgYSu+cbgr>ys@x=(@}G>E3nfDm7y|b0MdPIMD_@lJ%-_6D-jllP>;*o(~SS1FMp@yi=#SX8Kc~@}GI;GY&Ez9XSvaqM@N3C=lVa z*2p0{m>QZBqB_C9Pe_?IlU+|moZkRd6mG>T$_wi-FWEP`bO{u0Fk&{Cti=x^ zVwE*ir?`sk_M}u079K*zx=RgUCkfaAW=~+|Q*@Nj0^k|*Fp8u=h6@osf+qkfN`!zJ zgDY(`|5GAg;L_P-uE3S$2w0>9i!9%&4w5{K&5eHF2FW{N-QL@zd-$j(Ff0o|@i zNi&g|F_kD%L~MZxHvjInUuS;%xcvmK&q$uRxk1ZO{J|F5M_g!=xE4q#(03(#_q;3K za`aAd-OtGbCADz)h-WU=8mS0TDL<3Zc2!VU6%2)4%B(n7Js=HwVN1C|@O#5w7euq6 zItrk>6&ZmnQH3>KK;;dVPIfa3ybMRGg+`WH*E1tz(`bqevq&XeeoDq5(Q3oLLBfz0 z5c(#K3u_0Ji*a)7@F0f*0#V9Od~6EHE7<^Cd0;lZk` zSB0S(YGr47EkvnTU}Oq+TVjAxyalAZYklP2gbt$mz<13PPPvI7?N35Y4aS0vXe{Oc z8a&o&srtyOv>Y9k_xf|la)UjR`&}DOfT8Vb^(3$nQQe54{t8>{*Z0zXde<)b1h9P^ z&X+0_Ot@;OD1d>NHr^=0-zLJxM$zp-T=7=D;CeYL8#yUNzDw6s@~UQ)#@jBN=%7t{ z(0!2obmCEoI`c=e{e?BpoFSxe(=i%z>y56vs5CQ`Av*nZ-q0BWjRE5K+Fh9d7)4P& z@4);nG*GbZ>bN#x^ZYfgO^y4Qj>Rsy9m)NraTK z0mz@#ysuM$@llgVEj7N2EF2H2^Z23QDkchTL55H*mowz1A1 z!zd@JI94|vBW)l(uN&)FT{+M*dgcB;bJxPc+ZPrVkF3jxdl;pl<{XKnY*$!OqpTa` z&%#zhPYWMFc{ii~GyDV=DzaCyXjtc~boXudVvSyo6_d%0HX?=_r!YyO=F{3;ccMlc zKuJ32R`pMns%Vh;e4o@AMcKiu{wm=wVb#=8x{h8wV}ZwBP(hOldc>R;b^xr27HkQ7D$ z7lD!G>`2i9*GuG2p8OMcx7$QDdWbw?w|2wrP`g#->iV2E*S~+t8E@hDw_VsO0GZ5- z=dxuH?JI&W-F}B-0dY1enTKnIA-_xOXmi;kC(vOBnKO|dnwndf3)<)puC_sF)JQY6 z?N0E?H(5xRNsU;_1{;EPqBLU(;as6eb?Sm&lmoqe0Uh3=mQioP$;H`9YasViW+8v! z4}f$c1u%0`T0q^W(mK{tHql=c4&eaVj!8I8?}COyYCowQ6KMtN+;BfI$2+YnqgS{| zXbTbI4w^t}sBulyFae9HA$6Ny*BXQK6=y05QM`a&NGJic#f%1*go;EM!9!AW(gO-g z2=}euOOTjl?wyZ9N=Tb!L@`+)c(VbDD3Q@Mkw+6%o>5W*$?rgW1p5Hx@l&7j-ucdt zea2rng;slL+`7Zlb51r0)x9+(N0xXxBXqOqGrP~-`_ZOh)Q~33^QvO;$k6gtPeps? zX>@9aUYpZIXaYc@93cY^ES$vQeQI2=Sfzzx_Z%++y(i@JeEM(Dg-blZ;8#{5lmXFZ91;E~qFf_gYg*?AhXOs5L2<|>Bm zj?8qyC|(O}v{Cf&;u#TAjq!IUKtFU$aO?_nj1q$}@{Co}_)72h{bkq`x_6RvJm)UG zbzHsS+K$-(P18bI%28#mSp^ChE~&#)M%B`rVW4=1O#d6P4ZzfC`s21A@Sl+jt^Qm2 z{Nu{=zl;LeYE$8O|y-oPEdQM~2B#~=Sn#?GqKp#iLt6rhDr$ehu%H3J;*Jg9)M zg~;G%S`v&K+S#!)507o`~F65=8tBv!(kSY)>W3P1=b|`&T5&BnAuepK-&#o}s?xv6n&}spjZyGsA zLGESKaSyD_XuZYLral9lrFuGz?@Uj7z$ggF`}P9P)~{}RAER(>yeC*8kWcjm26q-k z?|XP}?l>tCIb?YL4bCzH=R^S37k_R+Q{;T)3L0ag(Dh81wfI`0E9sTu)IS4QGeTc zxWit?&vBhnF!l#v*cb#;5GKO=5wGL~R@eKi+l6%~=NF%Q&bx5opSZ4gFv;>&`J<#`98PPsM~g=PU{YB z*BvnUsK}CPh6r3};t8Sg#peo;ktUW8GRndt=_!l)(^vx<%o*d+}6@4)-jar|`Lr!VFrjJruhztN))i4RzO zIu|KSi1lVb57#R~Z)~Wpv|hJMrZ$C=3~0zkwvj;ch?294`M^t~F`Sc1O{aF#%V+aq zG^)*w?5lq??ymF zkq zCMaT99jL+$JRt{8BQ@lAK&qQ^V7v}AZHUEqU5I_4;M`vD3x(SGS|N|075+B9mH#dD z;w}J(^DN*;Y7+3?1xlHqDmlWVvY?+)l@B|iYiN~8$syGTQ}BVQcJKMRw@5N9hZ5}n374ZD>zo)kiDy!b7BJjcqsPFv ztlP}W@n+WExwNAx-ljAMGb63z1z9=e)ra?a&M6Mw1ly?sp$pI@h>3-T8cZnkDFjbv z;WhgU2ux$_T#u(kd$V~ajF*elXv~qFz4UWtlbGg;zlGR#@n-HQZX?^OdhTfJhxq)d17?n0Aedf zKGj_tdZ$nCD`kyz@zY}A^eVbhG(*%3gTW{*zP)u z&AkP%wO8)Yk^F_W*;V;O35t`jsLW{FOQ-h9BBIV(h}3ya@YE$!L|Y3-0YInfV|Mod z=!p~)-;|! z+^E;PDA!2NDy4$*vS6%gKz%}`a9=>9#&W`_=1dfbpb9)G;7rk=*j_2t-x1qOT&;h)WB71lV!NRMAhVGM>N zeA3)A5O=hJm*Rr1!b(j$)%6ksIJMAD6-6~7+GxX8)!|od$b|&bO{mz!L`Byt$fKBa z$sNlnxT?mHTy|d2DYCG6=8eMw6gVAgfa9Mx`}}=b)#Y?qFt{nSR?L*^BK(g@yI*nm z+#Y>CzuM35O01nqEWKxuUpdb0`d6Z`j*KWqL>zf~r_S zdOLXICKej{26&?~MgpYzEXd*QP zpUsPK?(2F9KuGH#_$59U$tgC#8{kl& zp33CX>5rkFz<>#9A=ba~^~qsR+eK$RzP8&F8zJE)^*P72 zQKFZT`~4Ik;(k$bBfo*w6mM6}JR$}erIuIgx_l5Vc+_u-%Gn45*8E(cMz%FGQ$Q4n*RfMOC1RHd4M(0(J>jPQ@t{nVaCZ^w*VHp;#imGw&4`G$!?EXy;JXdHpMr+LHEQAMVSHu@q1Bt(J9 z4|~nmQSu4HS2DYrM){gyW^p!;43adYx`E04H4c-_>dm+=e&RU^n3NxDwluQ{2js?+ zO^Mj6$==20*Owa96{3&PTCH`?vM!Fcd>*lGRyiTvd!VR zuGoNa6OEIo>_bHp+RS;FhOUTk9TVRh8p0IVY8$@F+@kSnKADmvK7P7jJed>?B^JskGK%$~ULm_P)DPZYMz(Xr7og8{n07Wq7O zU4T7V+7`M4icQLtI8xPTQd4EE8O-gdLO{8FpGqZmEksjhW+ZDs0e8`VXh%xOH-wRT z2#InxmX=f8P;_MtAWKlikvW9iO=eE3aEc?ahh!ZrEkJ>MMfBZ4lqNy{K26J;i-^ep zA0I+Lglze1WMc{N8BoR}Go^^io5Zk~2HW(AKKT!Fx+YfMw<0Z_aNXt&^0ARz>WQ7J zmJBdrS+_K+4OBPam4UB#$cWR}z*it644f;Q7%|wm$p+!llieJz7wzL@JxP{rRcXHW z2{x}jm#+`ER1aO{4v9LzU@s-q2{d%8^6(L2fp77SLx;7;uiKf3wTeibP0MwqGc8rznLBEZFV<&{dET*O$X;4m0V};7J3(;6mfr``RQ+!BsYrUoVnb49&_LJ;aQMl_wKA%bq3Hu`u#@y_ zp{o^lPJ0S86H3E71|^GVXd^Wclf$I2v5Z*&dj)iPbSnWwoo={>o^~3)i9SEhjLxT& z914N&x@0@41q}NA7&IZEk?RT9qoMi>8+D0nq+lEh{xz!~MiYB6nJ|&vC&++<)I%}S zjf7kBlGaDM-Cs@A`Qf^jnk(qV<(y$Qmvd*q)(8LoZ;^9F%R;3zjzoqLqx;zQ z+#pqA2=MQXDYRNG9Ll21U$JzvfzA`AUCRKcC`=v#3gzzryctpZ^gu%OBb?*F^3!_& z)&Ur=Zi_{x`jUWKf$c?#3lkF8S(~(CZ&hXBJ&FL1&rgXrOjFVbk_Zp4m;ySN0v8Nr zgg!S1xL$A_fQShN&wwKTw+NS74ESUL0_#=t8n0%&n2ixTbvGB3v|>h-8wVPx7&Pb9 zln3zV$A(i6W7`lc&Q=5-HF>DhdYq5DumVkyv5`xq8`)%*-)^`dgnhxBa`=wli6F}aI z)N7&Hl~pWGOWj8IBbP;)clW7LbiZz zb?aAAaAnyQB#@nG=WD`HIkU91^ocf|50>^5D1<^&9HjOUkO58$cqQP5QBzbXvuLC& zz&Bsi^^At`KL=WV5jjrrOhCnukQgt*oP0f~AcO^;N(rn37;@(SWwgeRwZx&K^zTu` zbnzcF9l!2coXOV^Gc{~NT)23gm6aegXE@bbR;y9s^)EuCl8>Z;Wx+ab*?C5Zq)t)Z z!Iq+{FUk;TX~Hmc1Ribf^#B^G9v+#R-(Q6D&nb=o#Ku>KGfs1TPtG<&+~qj#LNWAz z{9PVb9CWRQmm=f9$%)ioM6pn-ce*m%z4b1OB|EcoUm>u_k2q=OHaW(rqb|--90_Cc zCDvk3cJxFvt;v%n&Pbgpvsecbcu(0vNp{qk0U=n-UO{Wqa7dZ+m2kM~E)COpC5@PO zMXkStZr+rb4QU0W#r>9BK;~Ex|7?2*bVfUeejvrcFptDmu;a1u*-O6kKHU3+I=e3( zDKD_z56 z#=;Wb1Ifq&;%-bKWq zRrgv1XjaQ=RC1=1MYAy{L~n+m+lD#qr}o21GXpb~S6ZS9KzA?jLMbYRJ?JqO{jgf_p0cQz`i+^-e;CQPWnYM9raOsI7mWvME+7u8 zO1qb6wmPALZkZ{*9zvhG6W~3{%c$c)H%iRG$*G=JE8$(5{hD&xsg=Bvm&KAyex!XM z%%@m^WNglm7vWY-&!G0hd8?^cW`xdXA%G4du<#iO)(~eItE!<5z>^VSfT@6&ptRB< zHpx<>PvH+`Dg%kon-Fk9X^E_A-c6Bi0rmtvcf0Waxs`mrkvzvYfg$~cu(Z5fO9SSf zni`oxWi2CFTCO7IP%agTuI-7)asopUKp^E(1H5|V5X#Itx?&SvwrB&S7PmI9<)fR9&^#&9igTG;NxE;&Bd@UG=p2U_`lBVH?LCPwYCfASYCc>N8qTmx*4DI~PD-u6 zhi}-W!w1olJ6GT1A40_)s3{Jj9DeN}N~}b{8wUZ=q&%X`iMwT6yYqr(=G;uGBphx9 z8s!l|@S}PbJqc|SqS!`SR&r!;5h}e@7*Eu4;uVnMq|9rQ@iRgBpaTHgI;K0&U~|0V zOz{7h|M%nm?tdmD_Y$7Zx+iw9M>;7xT}~Eu3$2Jfo=8Y;Hz^6b`F8xlQ&tkrKr6Y| zvUbku4g6pB=uQ%eplD*R z2?jKDNgN_b7o=9JO-y}^^GqwfL2QiJ8Ed>M-AMvZ5K}O^!yh%8w^rO*4Q&AvwVDVY zrs#4!Vjv!J9u2s-FSaVrK#acetmApnr3k&<5M!%&BYGiju`N*MEm8gkSDF zn>U+jJa?KutpiPI{0Vy>PciE80KuR!-QmF+r zIulQnmsw}fInGMuOtj;K%7jQedBy0RmIS6B5=Ei4lBz^LE)SvRwK3r7od^|-_jx=! za%$mueED{30J;ID%f1V=%?(N+2PS=QA+{`N*(RIaJl;onb+?O`{zn7! zjN%uOR;MDjtA*@USYeVWDu5f)L~YS+`C=3B8b~D8BrOU2bz2ohSGuGnB&~?7*gpag zUyN`SeAtIaNczd2^|%HHP;do?39we|3?G^a6kl`MLm&O^TSV}_yO>R=fJZ_*jbz>c z77E|?Q!RlNB$J**GYq9EMkPq%&+ZTYLo z8TI<)K_>`38FegFe-&mblqeua{$yBFh`;Mjm{d`ZMfI-;fefh1)K^@)s0(Hi$hkv4iyT0lBxOZFk|$6@r3AwD3FTgB zYnc?cJDE<)k33?2pa7L@W+b5F-t?*>waqV*E8UT0MFCtCJTX`+F&SuRX%8)ZSPHpH z4!ei7L-C8?SIm9`>5CeNwCto&LYub?$el9*(BU*7-r*7jy|d&{kV+4FbqBsHPOFcg_P=0Ug{xAxWiu@^M{`@y9z zFCToqx72Da^}Oa+Tyy(&f^~`y8%V{F)gRu+(hxz|6o3e4rAo6|fgG4cCpFqQ@W}8# zsy9i;>Z|EHgiED@Sh5;8Q+Gu_dqzGe-h3Y%#$Le?d|C0W3}9E0!bjOJR8sRdh?d4j z?8Zn*{EVrAL~phKk^Pr0`FG#FkJXCab66uctJc$7WU|Jpz#E_gQIqf(b_(h&u7Lqg zxR&u>Od>BLK9GCFkNiMuRG0j{E7rU{besikPM?1Ru=F#USUI=H_3{dUSkQ_&)o8#I zT!1P_r6o^ahYn8`*2bt`Yr`h|KZ);9tq34qqxl8rfokDOp|oOT(FzOZ=dBIN6_(J} z1toTVUCDVkn#>Ri`A4^HJ z@h>hPdUw3>Zu4uF+G>H>RUq_G-UNbz%*F9OmhVdH!H}62TO_&LdvEX2=MeH@9G1|538O;v7mLCL#sVSt@&CN|u7lIQl16fDWV-g-#T5D@o z6j}I@hB7gWa|mSkG;aGX)Mai2-8YVZT7=D@`?nHqnokKcykiAU7mqhT=>vjG%)rqQ zjYds$mu9>9+&^tM_bAK*bHvLgT@btZx7*&2vyEWYcWwFv zVi7v2pp27k8B2o$N%Ro^;mE^^jlIQ;i(q|WXUSy;=n-kq9maX{SOIOypfE9& z-aP@6ISv~AGw2wAFa_9n2fHiXc1B-7>?@ zsztyHLXG20PMU%@p$8mN;YlFNqGWv-Chde;Itx(*27_`Bz<4f9>IQG>385iZa=c~I z=>a|`bv8~wW^yyFL)>N1*c(_d^Cum$5(!)Bk>7fh-OsToLdM? zr+Y6F7WbrakUj&^Optgn+!V1Gpe#rbF}T~ua_}&lwIll$Go=V4HR!b zdg2J&9%lG{ciSVFGvQ;z=NS`V6TK)V{WT;y*Mxe#SoBM!YK@<<`i+d zG@$@I1MP!68#-cMP<63c*fLxba(nX^Nb#TU7R_A4YC=}GTY5KZrW#f~l~w(8=PT%U zp39=1O10C$p=?<%RO5lW{H%(2h=30)Sl$XVY+cz=s0!)sY0Ad97bzP&$nI- zp&cQ)g%^D24PwQ}3y~LF!(V!DADJy;UKAA_WgU()eL1zB-c9Rk0A}h<$Sus#WSH$w z_J8a+!pXESe$HQ3I*nwjo17VNnYxnaN?ERul?GpR^cIq5>wtP0W6qzK!t|Nt^$e^jkj=VLf3j_+hZG9k+DYM&jQGCgA8B2Ow+$Mn@cNR!mX5$f4)y$ za(bvOrN?8pp*|>)5p$ZN3e;FR=hs)6uT0$uApHD{!p2<3;4dK=Btcxl>9Mg5&KYuxFzx+E8c8r0>m zqGt+cxqee@WNTA$r-~804V6^XDQJ=mI+-@(K1pQU_v&fCa5j{LWWr1GTnzx7Wj!Oh z$k3$+5eJ|)vl0=<2+g3T#>7EO{8w1uFKuW?bT40$nym1VSrB6}Z(p{nd`>=HYq-t~dLzX+C^%Cq_v~^zXo5 z8W7}poFT*$G-artw5HhcaW>0ViP`bQ-N^MB!GsA4UdrbKc^Z4F7^o5l%Fi`mGN=`y zQTFt9@Xjni;`G}}b-7vK#ZF$WLJkM8n!gPu(ZM1i?l~DMs7oRqVP`}0+umpV_alJo z9?$_q4YYgD{WVftd`~Jhky4bG5aP{Z5&^S4o_lQy8O!HaQ^?}`r<{tC43cgyH1p0b zbrFzKZ(xt-u11oGXbRfXIb)jdMs0*FL~Q593k- z$wdwKdLFKDl^wEVv*0XXSt{6`_(rHmV2p_mNWcu8rd3TAq%$S1BRheqsWFdd-OFLsc#Su8MGb$ZYu%h$v39g^0!d7$MRAM6(i` z7I{rc{LjzFOm7Za7n;4EI~e*&^y~F=iO@4kMpMm46(DCg#?R#(CkJ@MPF^uB)OesX zSTTn%Vx!4tBZXk|3~=4sapy_vrWipW23H$vnp@<4&$~Iy3)6}rKrHhyZ_K~IuRqbeDnh2d23cC=_T&K>vURIx`XW@ zve%XZ^un8WHC;rm7@*=clsehJi6%Yh0@GP6%i4;tLg^>^-~Sy# z#$Uvu#)6AzoR$#YA+~39;E)K#u?j8XYR^LFB$QC0A8(f%?%)R|n!dZ}P~vsI=-%I) zP)r)$l_BG0CJJyj>|o3Rm^A1Fw7Quh z2=Ps71}W*%gDP@}dPW(6F*D`VdJVBBdBTilr}bP|P|{Ap&QwB6DH~sRWmJBp-E^Uz zMaU8POq_c;i57eC%>%W-1(FRaoeVER3Kyl|{M1-JO)~wO7$Ze)n4rS0fY#`_)r~># zVVSe16tVF$a(}z=PC%g<&az?T0k6BCei#K^pF{n-5v(8%X1c%eX2*Fm#YuON@zlM- zp#IjiwBs7UptL*_rYkYnb8J}DjVt!s$2bC5qwzj9Hbr1_A`6*ZrGa6@P)UO7;b?wW zKn0UR9%Xe{7Y8VNr9kH4++kRTe$O;Nn|CFY$kasH5ACS^a0*G(+SP|CIaCP;KUwz| zz3zE%gO5?Y0&nYUo_DV+&1QeTlgqLO1EOK)A!v~M z_LWLM=UHauWhrnYPpdR6qp4fBfYZ0 zUW$=X!tEt=<9e}VS*7MFKqER0=)ZOw_@#*1mTaG}#XT6s-^O|v<9tJiCW)jG_LWU` z%iCFrL?MWxxz`m+s^^vTI=Y+IKcXVNQ12ny(BeJempsFJ6P_=ne2XgfB4qhH6-yTh z^cB6C+8IwMiAA9(KqNW{(tihPhqO7>@A&`Pe@81yweg{fRUx+gacxep7)-Rqy3U3V zg%*xP345h?%r#2uH<2q7nPTPK2u-}AsJzfNj6~o8#A=s|Uh{)O80PjyQ4~fI5;*B4 z`aD66Au0s-_a4Wi)=rAiXgGV=XE?C)<_SVt#1m`#Joxvu%^9fauW!!0|FqAevsyyn z&^SyM@;ZR0ydHae>Af?u=nGl0X6EK2$DJswBFlJq0UR7ad`xz4{wGLBB639W$W*;heS#1*4?R3(i@-EODjlD}`5%49mZ z1N8E+a`15IRuhOj&Qi+~Wlo_kYWR^yK3NXWB@4OGgF)5Sje{H(f)vT;v2ii3>>|me zO_>LF+d4#g&%CB|OyiJ-UXE>|CsB6I5sr1GoqIJjqLfR>UC8GvH+ZTU+Lg8xAwPeP z!E3&ZBD<7_h83v0N4^LeLY*^EItle%_Z~u=l>R|yB5%k0Ad+-gvT=GeMuxG)rhbEZ zcSuZdGC6^PlDmwADZWDiFeb2~g#{#S*qVo23FJ)Kg{65_U05{Gj6(|$x^L*`BU{ZT zAL7b}8Mup!$hd63=pc>n=nrVP%~rwg5>630=bnWq ziwC)cz8_dk+!6i&iMhU{*#^m0(t9?CDN3Z0#Y!TMAM8CPkYSE>IM-kLC) zc1=5l$bB2#=RjV!)S98n8$wkTbVTBi2A}Mq9empD`f4gV+3=P2GCI|0x(_|my@t9Q zv=hxLo-g?!+5iTEm43iaS_%1lz$XM`%>%KUnuk92F7yDj??#D=6`XDMdc%#M+S`)by}23^&`i{vtk%zhI)`Iqjs9nH=yn5{jR7)0(SIxi{Y|oY+O^$fA?HnF|zzl4oYv5fUIu z&8ZUR%!z^|SR%S8ZlB9bxw*TK05c?m*t7|ZatmGtRN$atHum>J*xx#+VijffH?q-# z)nk^~Bs{G2L~dd`K!eP4$;KODBQSFIzL?y2yY=J9dkStXH3(E>RYJq5piMZhMTY5= z=g+GWBEk}R=EYw8o?PYP{B@rr`YVbmedjA~$uMQt7T+6??(oi^`?(K&lmlt0PHA2Y zJ@j`2st;g_1bPpQ8&}eXTRvBX(5LTs_#DzMWLi6UPbMY;nYR5{v1Pl1wvHgX>(qBT z*2YiJD&v3&$^MMP9S4M%jT}46ZUUVAlAF!0u4dQny%){gC^Ym%2Otiu_0<}+cpn_!Z2{|sBbh>WrQ7q+R^K=&0 zD(x7albkO^*}MqZM!Qgy?pU{Et!da3U8s0)@DO_O58#WCn87|CUWB4t$mei& z)8)Sq+tZJG%RsM!kFGuK0((esPuDXE4ONNUmtJ_wTYsu`(pVJvgks4l4PLw1l$Be7 z0!%*szXe7RdFRI{Yw#q|s%&-g&s2|Hbb{e9&Aj~D%p>8H<7Fe=+soYi>yU;vc;Wjn zAtd-X2%Theyd>yIDqZJB|5N`df6bp+mKPgo-)icd&uQ{pN_+akv7>jCmq#MdAFUyZ z_QbR-DTK6AhOhj;umkK>?(FJy?(CPNt~W9V0Z%+Q(f6^&KquX~Ss82XC59aWf#OaaiPyexN0*45MNYo~IveYoHo za!-;&F|9~4RrEi)?Lj0>d=tT{7y=jTi$#f;3*l#7<;WS=ikd!ZVEk_a|1G6uLsX(M zWV1L|;0ZQ1D)X5vYb3H|o!wYgW{!W+yUSbgab_!{8J|#tfkf+e^X3ybAJ~7cEOuvs zJT~1>6j#UsrWOOE6Z2s#ss;vTk{9BZogBm}OdlVC@PW&S)x40nbiqO^CP~WBB$RZ>SMT27pi3^!iV>!@U^=CRNXrSO~O0q>5n1a8!@wj3P zA7`6ht2OXU^UgsNO?}e(0*VaS(b5?9BHW79^4oMVVXUba-zP|qE4g-lO&)eS{AAKwEC0` z2a>%+o+H~lz#K?<)9hR1Kw2+bmCvBH^rIy_wj+_W)w&qFk96dQnVE()1!aH~c=BnP z*u=OPnmlf_24m+DOmy0TSPgo8-_h~h>YkJa!VyPSpus3bE3ig@jtoLvco7b58?GZA zwPY1hX-_;y@`DdYXg9ybbHu^&{EM;d`VndH+Vt>kZ|IiY1UI8iB^`$sh#t5NdN_JC zZ{-d*89fwbbV>%9=@9WRV9x(|%X~u%Bo<?7M?HrZQkyn zkfUH}1*a_4n&lBI<2LP!5~wV0?GWd(`mq?tGTmqpWwJ{ST4NNm+=Q<0RJ zIWD<{T(}!>^!o{tKFJ&*|7(omvTcDFkK`M*ag5V~V^Ig?ICy@pV%L-PnjkB@!?^{u zSmJteKbieEf@;gFUS~eVPrvyVIF^;SEO86tnLhS^Kht?vYNH!O$cH>y*(cE~v=ezu!& zJ;i(k$q11tMd5qYvpRlSzEiZ&yn~QW01cooTc9PqkxT+J#~JRfJEoHd7@O&&Dm$l* z?7mK+WzWti^Ej8^fp%x#M^s_rM5SY7!>l1%dg11qnPV{j#y5WP7tRjfG<(^_viTV3 ze}!}HpFki~_A!yDlb=U@@r=IY711hi*;^IUCK?&ngk$<4ZTt2k%RjM%m*)i4bZ%wU zURt?~QY5!kP%vS;4?U|F>spV2UB1r_=n4Mhm{g(sO~93zPJnFSH?ONJ4SF0V-bl9F=z0^=I_t7{8p>?V6T;E`>6JQY}o!J=7-wj^6h9+ zQT4nk);SAF{2vkIeBE_s0cDbj8;nTi;beU$KQFuDg0<{MTp%pEb3$b1EPXI*cWkZS zUtfH3fR~4;B^FO0C--mr^6+JK0N-pvX zQrZQIjS=Z+>a6s4Q*)Wo2(9^?vTva=i0f>&=0nzDc;~n^AEFE(y1zhJ{dE^3V~ z#h72fZok%IocD!8%CcYN^Wv(vE~bG0pFl#;QGMO!mY0vM{?v}u6RVH$fbTm+9$HZj z!w886>81qCOdUyKhJj>$!f^1gGyem0mt_SoTlSajb9_*-yk(dT?9M;Lm~aj)kGzmDC#*hUMt%J zdU*|rc}ygUxd+nn0G$oowl`uGz6be5adfs0n+%3S0?tMzJ^}SLcI`KDe$o0bRG+@9 zIrGp=Ys%F_n0!Em$vTOY%{%IBH$-P2CHqAvHEwx(DvdfJsB?bMSRtf3^{k@|Jv&8+ z=5K-?^Fy4|1OiO(qEHDpRvl8B1!%|jW(im9&d^xMX8jL9r8zeAhC>d9dfrbP=^&9p z3vgY5x26aWu^~JDwXBjU054;}qx!B_h}?8#ZvYrmk8rtrW_j zbJP8F6ir3Hn?i=={+1U+o#Q8akrTA{r;nn?GIXFPA^NDAmv$A?h(+gQ4!GmkASJ$v zyJ?$rY{Ui{GT3SIV$Sw7)b|MXx zQ@UAQU3IeA-$Qw&2(vH`XPlN^cFz+jDL1;S4xRf?L9%@xcKlYnK+rDT7UZ*O%M;;F zmLMjhM)TQQ+`jBPS+t}_(yFF$n#Q-4g5N*3Yu5pQ5&%=@PmTeV7f?c>?FIFI3hBRE zwgf$8fST;^CcxLhkQOnBZ($HGVUssQ>x`r^kXI4W-t_8p zIz64@>5lW2jB*N8^X4cWEvm2?Ka`$NyJS4Enp2?DFsT9 zssZX(WIsP&)iWhDYgQExi2Mzc{Lm4o;J?F199I;^E-_k%t;#R?1KE)~zwkPz%<+If ztuDI-J}<1g`y>F-s|nqxpVSW-iM3rPSKq&T{q*Wxf@o>$ICK*T|GJVb21UTvS(=uD zx37pNXyb9l+Am``l=~k|e8O8*iI}e$*z0IR$Dk3?|G@Y@WWo7ptC{}P6yPno-PxRm zM34XwF94Ytjq(WdUIRd?N=Q3E_{Inv=No0~=#I5heizD zL%w}lRwD4RxgFd6kXnq@H`?UH+dVm5+3-Io-E_N*7>36kKedcU?$Hy%4M3{keMwde z0HV|Ml9oocf`v|QpEY)GSF|WEs#Xhm3{A_xY2qda?Z~!ERTQFT&{Tnz`7xG+wiUo< z218_J*0i}>HkihYVX`G>!@WN0cwMGT;AK42|Bbr$P7U)xE0YNzelvUMWmz}#a%!jN zs-6!q6-CI2cg~-8kY|><1HZ1I?r{ob5Xf4RfP^lCMM9i=|1TO`{}VN_$$2ml8Yeye zls4yO__VO(t%woy>4dAdPnd^|rFShZ99p_x5KQeT)5N4yDwG{5)ud+FnwCZI zsfH&UOmD!Bk|>#^{?c-RV<9Xil56~vHhCPpV{IGRg8IOu+EApis6UPPpyaCMsU{x1 z2W2zerD#sloC{4JnBPOweHq=B#1!h|@UYi3t>{}+`He@nR@)J|QmwFqMpMGYoni{L;^palDDq;s!b8<5kh&SF|k}8OrRC>K7 z(b^gSNXSP0v8vAwYFWDy`j=$QRI{m+B$$GLR0{*>iYQk_a(R?dClzHUI^~&`pdfK3 zg<2U&v1*~58MjoFP(tsx2RlH(!ZArK0>SYRKw)FK$%p@=>H$FkKqTN5V)mFOaaAZv zi&!6NF%D~9{lk9?G(UY|!YgS7&+!Xt)tOvWC5fUY+Z7{S%=!*o(H2m_AV?K8eRSl9 z65xdKLoItb>)sBN*vNDeN;dmT^LfwOYS?6&|{o{11@twsiAn@j|~7lQ+Y}!R?F~- zuQJJHBtr2JJiezR7peyT8(wR3Wr{pbiVlI3Q%%Aq`?Yxk@P zYqH~nLL*nKXM#XO6vam;?tT1f+1fz+c+;m6137DEZw-)sr)SV#*Zn$bio7~p-N@)Q4HGzi=7Mbc zs9MbIA+t-?_8mHl12+Z{<{T8VL}&Y$k1`CC!iz0H8}k*W%n5 zAd0j_opizzmI#ma0T~vqWXeqV6X1QgZn+F+XD+ZzuOf-27$~xF;JhgUpTTj?SX;00 zb4o50QmH;7I0)in4{5_tF+bH9&nrNJ<7^*cT>1blFh;j)SrV3yi!&NVXI^7I>OrUceXzjNG0@< z#lLF}UjCZvzRW&5c}ERWo@ftx;nh#O^O4l@Eb zK?b|==h#~qEnvS`}jgk@G^3zY-D9fH&zgFIj zYcH!J)35pl(|&4wggVEwsu4}xW=k3>P%ehEQ`xo+Md%*v(6?fH2vCtXH`w6Ux6Z<*WwUQPa>RYt zyRb%7R6|XAf5MZ8q+!-2Pp)0B&>HRYYMxiCeJT`+fHn18|CDK*TxcZ=|AS2*{Aq7$ z+-WfT;@?$&^EY39`DOPR#-4fR8EhpYlbF-0EU&g`LQRxq{xwF*C&XnUacHCIt$16# z&%9FY^{TTl0`rhwe+R{dl2m-qITtjb@^t}9^s~lGjkZ1+GW3MGc--^8WiyT0sXr^C zVUYP-?q~78185m;s*myXsyLShOoMEtfLLbqwFs8kyd{f$7M7ng@4Yv5_uZ*G?qEB1 zJg=)FY!skx+6MKliyh@;sT&}3oVidKh9?}Xpv2{q9Ak@3EP4!4Oh(8T93lcsPRUu}S6x(> zGvc*VjNQSQ({jq+5GQ*Fp5-HMl@Y#`38jDnZ$H5B# zo&L%9qi_t*^Y!te+X$aN~N>~P$h@A8Wmt~m+$<$i{h@2 z|i6?czxHj)bGodD72s=BN_)54t^U;MxoYG%bp$lR z*3|xmc5Mr&I-c|wxfO#UVQUj7R~$$jnxModZ@wd^si=ud3&`nFU7=^R&@ykwjxSz3 zMFopRs!2P0qUM%%xjU0Rx)#C~Ry@*KDcy%3TNA#J=~!)kFz!^|%?-h7iIy z;GW&s=mhSE>z|Laf>&enGX=vhsgdS;Aa{x%>RmlQYXO5u9TYz!#C`?2rCMoP*;zFavApv+LROKkS zfdCqBWry_cw4hFyDK8x@_{JS4lB$Y_CBv1GmZglRr8QP@|&(p|tQ z^}iuwDXXZ$i*FKH8yhnn?hb>58}Sx*5G#QVL`hAJKSrmJuI|D+XI7TjZI7P-GD0?G zXB5j%9Y9JcwBZ(G-vBH`4f(^sJu1{sxYluWsS$w(duE{x*w@Cytr*A{CjFm~S@P3z z*w}GEx@7l!QhX8DzxCefdGCDZ{T>jM(A))S?(?EJUm=IP24pM}gg$)IZdH*OUU0Hv^^o($%~-leSW&KI4in}O?0 z09b9>@X7{yh@)*=W&bqg1l@-D`~X3BlZ#+_E|Im!A3azY-Mqh7Z@>Na6cDDo)UK&G zn)fwVtGhcFeWQHMwb{Gw_q_Y}FM=>=v&h42aG~BLt{KhtL1o}N=VM9|JOK2#H(B79 z8*UJ)c1f`IxLLDe6g0FYuI3sjMVaQcfxnBUDYs1wXR|ec z0)qeV!Lz@Ib#z{D6*~*k&?@E=B!l_mOkK>KmX8KA@P#eH}srkjZyO*3g}Z@hF2yHI5E;P(u+?OG9rE;jx##c z8gHhE(8R=Gnw7N(DJMmv%zA^#$ys>jPzx2hQ}D)hkDl&DPOe8k;H39$kly20h>ss* zm5dz`k+k+n`6G0BPkP>yPhLddJ^(zUR&YU)2W1;0e+W6|--X^rx(1E-Dwx?pD|2H!dkKx#XaOIz#65Z|Cd! zbYu&~mewrr1k2h*i`9|Oxka({L)UEoblJ1Y4MonQ_fT3hB@-Sf&pd0{H=sMinXxf! z?S*7(XSnkmN?1a^G~xAajYD?)WaYt6`XhLpV8buMp?Pn^-?4}Sn5JphnsaPvZ1d(+ zUQ;vhLhjg6o1H)>)O}&~PIL$Zgow@*08=n@yclz{ZYI-mjG$&agM1#6>ehexM@XvA zV)E0sDwZ+Wb6NA+DWxuU;LS@#R zmgm2`+i5(CvS%_9r3Am?$uKEDU_wxuHzOgQJmR!7={E6N8Rc>S!~|$^@eK(xGd$YpBC2x?$$^qFL={ z2)g5I9$KIWPBCvYyOV`=b#sWM21V~&^i7XBYe*l$x-S7D|Le$fixn>hYXEBmql_d1 zUZ7_FqOD$90ZjeQ54iW^y6-;sz=3-V0gC~hOF;$@?qx&N|09&HQ6=uGdyPsN`wk=? z-}c{w-~atjfBG}-3yi(+!f8dcuuk|lA&L_@0>uI9b_fpj z8_F^njOsV54nsfIM*r!j+3C~AT}49jp=uY-0r?0p)&t?FCwpB}i3g!b0qs1BmU@j!%6GG#STD`& z6Fl8TR~Oi;kzvU|%Tu+?V~(tvhtT-Nx>Ru*&OKflt>tfW_FTKv@H|$90Ts5U4-7i( zg&UQwZ!EAt8N({w4k(t`MYECByPj-KdoVm`Xd{V)==1m3dA2Nh${gTrxtC$bj`q9X z{fBe4+8iP~4ad2#6s`tK3~O-yU%rP_gv-hL6Fw340GSo!!;s*hT+cEii;?e4NTdS^ zL^L~my&d^IMGqLNNp%h*mU(~+h0E`%=4{Jr|m>%dV!vkSESx`fD58xqgN5_J&y z$|$TRds_LS%`VzA^F(4k>+gZ!{sQCEl88NNR9y1Y=%CXiZjkS9@bVTU2AXRiA z==~FjlHuf|E?QiP<7+)srR0r|D46n_ zXq_u+N0GDCRca<0G?l!0Q}+z^0Nd-jd-o!`gua9*17nVnhx`HDZ#7>8f+_%FZKa%1 zBZ#1>>urEc3G-a5dTQ#c7iP7Yt=ISSTka2#1}cWk@u%JR`@H9beWifPE4c`p0gTzkned> zff#g@%z6ibSb6B*Wk34S)Jre@HbYyScfb2OIuwx7V91lQJc&brx)yK2q3|uReO##x zk|t6Zx-;m6r&l7P@s*Q!!kHuth5`Y9*La6=6Z=DUtFWud<~>BaCwI}c`x9)6Mo3AE(7w9@CJ-rfWwGG&DPb5TGuqR>WI5B{J&g} zs==WW_c-NioUC)AO@&z5SbK<9f8EbOIu(`6*E=0-$L(d-MHdZvbDR~?lNWD0d%uTx zi>V&)QfXB{Ftne7-{IfG0A#L@CQP#~P{ZB4qjl6FNXyk_n?3R}&EDe@g4 zF^u3Ke24u$dnbZ1TO00Tpu)T02P>Iw8n;Kq7FEd?vhf{y7OP;!ipYyWFzI za;dJ`MFY06dw^Nt=Eu~Y;b~((i=G^oaX+O67?&8joIoE$=K&e*=g|~s^rMnNLcC`3 zcZ`y{nUD%d3RhfIPtkJ>Yn^O~p7AL@w`d24uaFN4GQ4y5z*}CPw%%v=2L)GQ(*-lgB zUeBR_GVYg=rh?u~K>*)nF6GN)`L>exMr@ia0E6csxyW-F9^O62&BRUjPGx2by zB-K{myo5+HxxGO1XYus^1qaY@gLuPkt~qU_KN5;SvV5fF&?a$(kH*;Uyw;cc^%UAn zx8GEO22V{3g&S@F0>O0= zGV8=+th5FB_Tgm&)z$Gio6Vk$D0g_*O>e3W9HNDr6Ei3_%lJmpxG|3gO^8nlR1O}E z^D$`3U|Bqo@BG|fGj*35m4I%MKQ`QllF&;!f~`tZdMnh$x_qJ zdpi1i0$M6i3Tgz%a?$Wm8*WefvXck7#oE#ju>ivys`zxJImSr9y@LC;WHRnAgb)-YiC%VuO7x`Rz3GxDFIgwl4AZnuaUZeB%COBB-r zmM>;Um>6c{oQdHkgOps(zCYr_Jkm?pg2**0MO;U|@V7K&^r3H@gz zy?~@mUxaEdiRDNKYBw4ZK2C`h`m0&zsi*EfQwz}PQqar-AoC#vGSD4V=Lph|Y%?_{ z$jC+jY=JtjRDCTfgasu>f%X`g@vbXeRou$JoQ4}QsxX*0p)YUd-oK4TtjOziQAV*7 zJo$286#Ee+Jlb#~{~QFa#X~=Y0JkA(Imv~iB7{yee+DW4-CKJz5umuNn?TCl=+6}x zyFhc_O(v@6bqny0W2hP!=ZBkKOxQEd|45EVGL5+UK}J%{rW=VxJeL<8VdzhY!bT@H z{gIobpP6Dcqx6BQUJfR<$z$$Qxa2YD_A~a;_AmePhd%VH>`}%Zee}i60Bklabl!v& zV9E_-4sDS{!>j?qUTkHvEnfkZkOzlUUJV}2A;j%=5+ywHMC9%C~QVKu|M!gNJegA;@h~<*nH$VTuy;~;K$%PYLsz!-` z#w1t*V0BX|!4S|O4@X~m`dZt*wq*8<9vvJ%gViQ$d;>1$3UcGZBssW9+R?$0m^J+C zb3M1$0}@!@od6(MFIX=X{2X*4^NfW)TCR1*oC6B|5?)9vATQVd1k?-}!UWNmhn_P8 zVFO(aAfxG>qyRVA*N-Te$Rt*Y-V>2QYjgV!Hnne!O{|sF{1IDIay1|+DY;Xbnv!ez z*#-AF8;_3T`tte1?)k%_jo=ZA+|ezIDGrcLijD!ndY7)>)%F3dP3qYVzn*PVp(u4>Hb?Q5RO+*x2MsW?6HwBT{w}4UckC4UN~jUqMF+aJVJsf%TuxAc@>E zP&{Q)TagnGj2}agg?Hc_Tww>G#boTTNr-pApG2E*E`*N{9dRb%SR@BcCY4L0SA$*7 z=fgijJs9c)9CvdSE;Ms&5E%X;svLldVxXS@c}9q%DA{midPBCR<)ue}!T47&9Bl|} zvLwmBjrtAR8;SE1iD>*?u7j2m(+FO*Cnp^g6fnu1?Np26*aHrFePwI`xV9+(nQKN$ z^mIq^H7gT_SQi8_kjV~Q2_$&o)zT-Od9Y>8j_M^HU8|9Dh~xzw)+W}9dboZVrbXPN zl2axIf)OXeioU*AN5loiYOv-5oE?r*_^KrV-KS=U7jG>a#fDQuK?M;XZuZaUWjAX3 zB<8^D$!_f;N^Ktehr)>HKB;GI6HPXBr6)n%3Ppa72kbA5rw{p@U9W&O>QC(Ee-7FvAsf5gKFXGtx1#qp8o&8V(`nF%gi?e` zs24L879o<=esfKCEORgQHP$a_Kkp1q?5r$F8Vo`itDiNr#Y2Y-n#OhfP-G}Fig z*qJ$qOiT@k6I<{Kj{~vi%g1u)3}kHXw$N#&UUQD-@zGkMkKSe@!%(gPI5@n&%gouC z{kyZ*l%2|vvNP^D`}SdzZUQY(85m2Nq!KM~p*`x<0EH)GkAJr=)G@)i`B=*Ea(me>#?(@(G%@3gyKwyh9oA?=8s-69r!#=i znqE#uB;-a_J)qh~%CWuc-B|}?y%uBrZHzUR02~4n>k7d~<5|SB@ij&YANtK{0~Q-^ zS8@l+3fsAp;0g{DDw%r9M*|lrk}AQ>r96;7=jJT9nIchgl%frvX$JXrA|u%T8}Uqv zsgrR(6}J@}t_3}s^T_DFuD1{(J|a0mV;f12APjmgh#L<~qY`KH+ZU#{0fjRx?l1WJ zJw$kd%&M64GVdKn{t>n1@o*}MlS)fy{t42d=o(nahXVW=92yR=!E7fR%>r>iwk3eq z7|mIj;bh)Eir~vv5Z_quU@>&k@#n{`JQ81D;kogx4B8pG^?IsNKX<}On<=C)xN1=B z7f_uHz97_~A8z_f9Q+iNq@H=i(EhsG%qg6vnh5tqh$Ku71`c_0u>{5;i)Q%x;SO){-<3}OiPFX&P%lG{Kr@#S|(zcFF4s&0XvAD+~Q;# z@F|EOd1r=YD>bcJE7sL%?dZ`P*ly3;z57+caB@mHfnH{y--3qI9sp4l+iLqd!LT3eV047K$w3@kQht zoh;vRJuSli9IgJ~#td;D7$5p3?`ncI?ogxw z?awzBmqzXgHMJ`;VY`=($WKVEZY+$H!#9BUF;CaG3E(DO48OcwO@pMih9%9)3Qh{sPRHEuoD z7u$cd%MhYeOSZGyL||xQEIqw1wJk;MM?5t(CFJ#LZK1Pm+lMA~L7J2dA;8H=NM3-o zGMXZ6RFT9N6vvpL@(Gk46}j;F((ZB3$|B+aC0%f!+kqhxNgtCGWjm}Z_*fkLDaK33 z&PQEg4UPK7fE-nHN5tT>q zxL7vQuSKBAx`r955H$?-sp#mUqVftNZN^QRDXoYG?N&qMvKRrv*WjC>3fh=6sKzp0 zJ=!h2*40Cv;R(|lnIU}dFr~Oxbv*PxQYLM+G(A4<_WJ-mmW^>yVoK*>m}Kk>?AO_y zJFRy6Rfs64{RpI@Gn#BGJtxQ^X(Vd(OGY$i5?0pr@V$N%#6*2nF_t7ydM#Gv^H`M_b2E;tBsYXufYbm9LVLlNMC;<4M?VD7)07@TI)Qg8bm1a9 zgLIl*Ho_>V95fu@$JVna&^xDC`6lu)(5_V~?k2N?p`EqK*o< znQ*rT`Lx%94i=IK3pIE)RZl`l46s)4ot{Og21!a$Ob{`1CK71yi+1K1TtK$n zsn^@L+NILAZKcu&6()DrksmLMsG?Qjz=4;@ys2pROrT|N_Czn7Ap6=UKZ%5bQ9{$8 zcE@uAer|DcBY6>vJKA~viI!`y{-U4p(<{+jb+_yret~??1E(-y_xqF6ZCu?Ycg(6t z{op(sM@#$OTlQv95|&Zn*wxU0Z5}zS92V%}ro8@vXA znNT%hKr~>~Vf`UB;^Sr!lfJQm;>nI_E45#Iv+Fr1>hI^_2gx+ug=Vwlx}Vo2<{^9z?^S0%>9{oGhF^ zrOvrsE)XjPzaTBn?%&O!T>4VkpIa+R#bbCKI)=bk3XSIw5X;-g2wMo*e1?@1blWuW z!g`+Nswz1P7va9&hk@L%=_bDa!=2`$64PGaZ_vgY{*Ls+Azgr#I3_a1TnTcM2#T`W z2Ar#IDcHH3?*lQgSVWt+gVcxj=T)AKnaGTsL zsS&c2Z1jUg;V05g4APD0%f_d@JK;0emOax{q?XnZi()#RF~JYARughSZi_1kkFM)B zF-6ekrk!Y!)KU@S15vSA4eYfBv6o$zvXB5xCI&Z zs8l~>QQOu-K1$`>1?Hpo>AALAx>jBoIH4A>&*^Gl=N<*VwS3bf@5<$JWu)f%=3yO$ zTvJoVB%8hf3eBOJ#&OUB#XBjalSiTz1rSy@?ck-p7%rzdl8Eqp#|9j!MpsraCt4Cm zNyguSOg5w{J_p}(x~Vd7860>~8*Wyw0IP6CiIf?`eA8}WpW;mjV^Ou8&F7FwMxS%6BNG2DDs=GDu`0WhMXF(#7NP`ykU)YfJh2r$YD5PLBvN_i&-sRpfX zI49Z_zSr?afKL5Up9Urv&lH=4zXQ|F!h44;^Bd5NwxqEb@t5CGm)He&Sy=(x3|jt# zUAH2&nHw~;sxy|yz|09@cKV3>kULqeqT{*4-!J<$+ih55mW1APK+6#vIm+cd6TM|^ zRR%_w{i1s6)E{D6 zUZ5Cwe)x$c&6!qN%}*6GcB-x4Qx?KTwXilf_YTuS*R|ZwZ?jW@3CUTyn4RgJsgTX! z^o88QLX;Q&QijjY)}>6=hh+~Set7!X>I7Ozy7ruta?*0YX^54IXh5$Qw2nL>Nl?(9 z_?Q0=Z1@=T0j_I;Qc04MMlQXc=J|_uc1l5Xw&5B88CG?0_hJZt#C4Ae@DggW?W_2) zCIT}O%9Ua6!U~d?pOWiieY3=yCK7#J<%B10rgfl`ddTaLEF>5k@j_q0Cs#53^Ihnr zQ}<7Gk%HX~?ty1g&}MJU0fX96+JuaW${&Z2Pqu8?+g2+_l(|_a)Fbu^+Fz&^jytH& zXwoT#Y}Y9dc+^M@K_-*76>Vec+Uz-9^m1ssdJHQ@*fReFvz7WQt(!eVLx02U+2IQB zXJ)-UEAE{b->e?*x<=W|HQM&>tYUj6iV+a46g(x=4F_;Q8s+6`oC8n(v;?f(FIe(j ziCdn%pRxD9e=m$8Lqc0{siP?h;B9>@hZ~xJ(8JKvT9GE#1FC5|oReuwt z%n-7OFClk6Y>a{PMx2~F*l7CDb!UfbZ(eSIJ$^m_?TM^&_Y1%EkjGxoh z@qV*8Xtn04X#F<$8$NQ?`IVeBVZwx?=htlk+PbkV+1c5-xdj&m9-jA!f>?8W2WdLT zW%IhtJ*=t)!~oKWD8G-6L=p=`)Ys542E~&f``qV)wTyLRTG1=TLZUM96O1o1EjA{f zfV_|-<yQah6HP;d1}XX5`Y0HFH+f^1z2cgJ=LMpS>v zy>=H0ja!Ha5i+G!^ze*Wgg0s~NXVZ)S*9TKo-(AZHmI_yb zn#KMpSyqR7XBfeD2w-hMc+#R5D1j$c08f@({Q42sH0>nBGWSC#+aCL&x#ori#2;XV_!}ipK#t?@=R(Q|Zrs6IsL@!} z<`>8nJ$S1I)7052YE!mQ@6h!?4p`8L8*c{O;LMaOw@@anA{Mq5MPKntCBFd}7W!vS zAzPZnTVLS8?24b64oBXf%*j;l-PS(i_Riq!So@CR}KG1vO!^ogt!6G z43P?*uOa@2#+f!6&Q_~c?VS1O}z{xP_3n;i4Ig#Kg;mqU=Fs zt}vuJg%_@^G1iBz((QP4X z$)b_gGe|}@rK+Zu2Gq|+YONk5ceewrAP67>e-F}-LU5xhGL82QY`MR>!j=PS(vRA5 z8hGm4oBrLpez%cIGX*)zYc8-Z@$zvVq9vl`i6slw2*jTuY_P;wwot@ca&da9;WirO zBjrZw%oz)4dPr&84G@zazZ4+6iAzX0w`?eJNbI(h`Z((3aJ8>rRcrWHG&Cx^fYU^T z;$|R-cN@kj;P6h0Pw;Fmi4A z;CdyWKkPU11mFue1FN#I*amYTkIwP5Hfl*x)Y{fs#X`qy7siZBg@Rbl*SE}$?N6Va z%%cjaQVf`1oZp>{?DQLr3=KC?6zonRU(q*9mza++ZF`96$RnZ2ALU0Vbt8t5Tu+6e z-NHJ6l!>#idEAnKHx~NlWTyl0p!%PGxgpHoe8(R4zyoX#20%HwM{FS+Td5WbxM%y)q`T%_bInZED0RFw7adsF)-b*^oT&d5 zmCLtnK~wVC+12H+rPX{EG>mSvxH3Dt1(nMzVj=5#LD)Ketc_NmEL=A=i^_{rnT zv6QZ)>QN2y^iFT*&ZT!Q?|j|^%*^rQp8!a9ow53nTO8+>ZT)074!_ZEOz_<>$8f*_ z{UzCr`wa~t{Rg2y?}s_{T}We=sAgP&_5iy9*@$LU{JIw*gLAZ#M3A-a3Jlmr{w(mr7Otl)LrU_Wd~Dc6Fqpb z3@1PnRGpRQEiK5mkT^&2oOPmrawb9FN>Ye0!8n?QZ~m*YylvWVD(xKen|j@uTpI|h zYfl*UjO{yFUy;)-W02_}M4HLJYAb;6C8Kp>s)BZK9suy!gY}sJxFlMxzFGt_I^pKr zz=PbEq0e*p{$cEeYX1ibiCz)by%oLBOez;d;eR)Zic<>#;vcRvq4$lM-<32=6-sXB z*=IYz0DE$ZvBKmy0$6Rhz;4Fm<4V8{k$FWW;E<$Z6M)W^SZEHvz6PPQrg-`+5ZGI6 zi7mBSOO3Pi_L=cqzVC_o#e3zM8TI~Lt_OeCPP`yoPM`#ttZ{warB1Stc9uT}QQvr% zq55!f3|?$}M9J=SANF?W`>G$UvDk{fWLVSX%1p5KPR8EZ&+jelr2^+89GI6_aimNl z;S??}xF{RlUObl`MM~u9h;3sq-*?W3Rkazh2Tx}z6}=&>QJ$ZICB%#vb_WQ zyOHCegWo|9N%%eI@=DNIdk-8R6e2?q>bu{J)8S#fBc39GNRDNOJsnX z)Y4YQV+-hs%bcb|;P`tur4zuw)#ZH>iFGal6Q{>Wio-DZDcKxarw=l5KYTl?uvIk; zzlGM5f_{ygj#_FXK&eYPS~hLqb3e_F!CP_cQWi=54B=B?2#00Y7X!DG@oI1hRe)4$ zR5NPMH~opTj&pXY4dS3QvzR7r<*}(w6ygqL@XNRn&Eun`p-~|)6xFR zY?a-^w$D3x(>#J6UXpHH@GVmWuK`vcHvE*)lYF*$^QGCjT4OMwR-mr}I>E6H?CHre zz{SwMdwR}yot9gN$UA^xy$m_=ld39+R)?>Le5r-TGMI8tX*-Ly+u_%%S^wYjqKbN# zxlYA-vX|Q)ZI7@;$N3gr9?8k!sR=`sE!JV;*iEt@>mq7+cHt}48c>9tv>Q6d=+@h9qnTP6NBhL!;e!pJtkD160X5JROE|*IZsIKL)^hD92uIBb?cabyA0Unn`WRl$ zqqXc}G_P#Sr?Y~MEE)-kU@Jm7r{pa+kVE&>cGum$osyHLpnIOe5k>|Dg_Edah-3v- zbq@7oT1I$5aHQcx<9PU#7Kl}+A<_mff?at60et^-=EXblEUT-Y+WMuIejC!_GOCuI zO;s}J3W=U3xOTek+4rN{P$rVHse!Uto6-ia=JO(w46N=UOJONt{}E;jdg;cz-qNe9 zjXBTNa(&H}=|DV)_%o@wl=MJV)d?O97MX*dhlByinS>`d__0Wd1q!DNsdI1~Ny+VQ zx)WrxXp_C{G}2>Bjta|!I`>#1T1}{*DwW*eEgS&g&gXt5=nHwvM+;7R^8^oM`UF<( z|2TU8Y~Mu>*T?7k$V~4?+QDNtyUWX~6un0wEAgPYV?v+4vt%wKdQW}azrE>RxCc0} z3WO!6CN3)DT(pKhRu$&Z3Ozt`2)i@(;Jk<=8(WvRIM(*A-d-fHJmnfc4%M0Zf%=L1 z7gHdS=!mo%o_Dm#eTYuWA2iH{=!k)D*~sg*YTHy@tF~%4)>!@O;=IEZI=dU&*_Vod=9bO3b2$rs#QokgeFoYzZ0V z#V>zRmUBW8*(2s>dT!fYK%<;KzV)8Agvz^Q3 zi9&fg+B|8^qWu8)dlH^icNUaJ-sq<>J!~O-XFhmzJMk#pnKVGI{h{ECY?BbeBsNeI zB3Y!rgEM@{BpVh?<3XdbPu^8_Rn0L_vYeHC%Mp}oO~nzr{jASwnb2@-vjiNs@)(@w z*qEugbjjDef|)ZaYUfo0o=OqrfR>8p7YJmfP{*uqb72xI8AG!nq~P(=gh=c#Rh>mo z5@6)wj!bg_4j&WbeViawoW|sbPG&_$C(S3B%P2i|QSfcKXy0a5L`UdYHIQd{Gix%| zY$oLBghY=@&l(%0M!=hbN(k&YUdA96bXdOGkwr|7e*S#gwUOn*sSBBX<+A?Y%EV4_ zamGbHvfe?xH~O-Zx9~igYl@}JQlAwx5K&jJnBA%DnApUUn&_O6&!Hp9tajNkG62Q) zZPcqr3QT19ih$zfMXHHL$xK1EOI3Ge;5-XflmYIH=;-O3*hEDt#Gno4dXf`;tR6s6 z4=E3dX?#j1bVs}v6Xjt2ge2zPr*+*uY*HyUSi6m6fll3>V~z1~$Y?e<_o`3w+FCFG z1qw7P;~LYvhJX7MIrHC))g*eE8m~Q)AzdcasZtkB=n4ddT!-x zv0)TJez_?(Bp=E*9A6F`AzSqg*B0!)J5^?SdE&ffonM;<<6RHN`ynskj<^oC$RYJ9 zrhtQv(+DInnO_|Dv4hw+)}1puPJJx7)WISTk4maH_&it(Amhkw1qvacGz6qLYI4TC zud8)e;e16}t}{S$RtP-0{z7Ott8FL%Q&VQHHl=Ix?p(F4`v4zx9!p!N~MDV6pY zk7X!5mKNwfq(>8h|I`a~jitJZ34a%ghzrQHG^)^uD$6lPeZ>pq$P=Xk09pej=$k{8_^=7%a_@2{g(Iavw&E$d}H~)Z8qQT zS^$Zhu9Ok#Jx}Tw*Ig$ZI^=e{ z4dh8?8WYl_tuW;a4%_c^wr3Yaw4z|f%7VCr`%)X&hu81RUiZGFJ`x2Q7cWYuPrLK; zlb-2k&P&UYoc7c&xb6YBy@vZb1<%+De-!ntdx(=WX%WL%67Q442^LFCG6F&l3SL|a zODlzQgIWO&B(f@A3go3ST0ZSXc)MCNbGd$DFB77Ewo*dd`OBhxPZa|Jx6A2??#GoFjQDQli8vVDLz{XOVxC*OK(Ivw%J>nafp zs>5%vro-RU*Q@^KQKynak0T_vpwpxuMP=`=ymI^#WF8^MC|9|3%HS~4IYVjR2q z=3`g>1VnG(YMQnwO3Tvx;H+Z3f z{LO>$mvaw2B-~cCU|VJ`ibdTF*|u#ylQQFgRj};?eR{ViiI1EyOP@evvaIFVLK*E9 z)&T8~-FSi@&{TyO_l}%H#sO%2E>?FjmXVxotR-na@t|l`L7~#ml@-&n6}?VqBdQkm zrc#RHWSwjZd2M;UVt2IG*p_fC$i56@9yHI9EGGNi(QzGmiD$ydLA5(SFrL4jzahHuR4ucs8w_0QI++bF%+G^zota4lmq zH{)P-&v29V8!+a}NiJccHLnvUKV3-=^D$abYlx{2UoRng-4>fIdwL4)j;g6ujK2IP zv}8l~8v5_Np;*N>S_?ScoaNVpK+o*8?Y(<>PAZ=MERq2z08&cKPU@vQ!Vjdh65Yj{ zjAtW?^P_`!e;s;wZ{ASnG!wRNyA2GXQn!9qehBe2geg=ELfQH9?9r?*PD#*xSsrk(>GSn|vt`VAH8NdnVQr64VwUTYQH7gI`)}rR1Pat88 zJc3F+>WIS6Dw;5;jouRn&(L_n&o~u+3`JfAHmSRm#UhL4K%HcT+1bk_c6#Tj2}N?n z96+6oxl=n&6Sz3lJ3WSZQdtDpIn{ZiRA@xm&b5)Th`Jn>ZfI_%9%S=oQ~-=} z)(_MoGG@?rz7CHO>+e9i-)a>wa_yjKI9Pj3&njX77EYk=Z+ZqnZVVt^ zX40d`c%fy!y3~X=|A{<$4K_>KnAx-X0L*UX1jlgG6Wi52)7kc%J~yUS`^MTcw&R(B zlbd8tIF~KYWqqMpf<92OA|(0b0K*G_k`%`m{b#HIHQu8Y7?!&5K_o*MjBaxLedDo* z2R1rhAUvB(1!((VHfb1W*H$%K6(KCD2oCNQkn0bbo-i`=lo zs<3`4Htyn7FcuQh?|^kY+>PN_Hn!u6fpBT(-O@3kBmybQP4)C#x)x>&RyvnA>P5i| z)VwR_jlA3WGKIk(%cHu-a5#X6upMy<@s$KQtWDsLO;&=hfpxAF7#ID`_%(({{V)SYvaa%!}3SCnvRj z|H4#hr?D+73u+{kQ3WTVe|KtA4hPF?5IgGvXUK|V!oRV=*>_J*_EYH14Ik|etJY3e zBB7+E`dj5~lbP;^}Y2aV$D{8i>!?QBrKM!(|mZm1d=ZzMAGVpgN&aK*wEsodgOA3Fw(@ zU>m(54Y)0qM`c_QG9jjc0bh&ju4KtksSwUyF@(Aw6XNP`?MIPWAutN)v|Y9Ibb~}w zdsZ3rGSzWqtR|+Swlo*+z0Sr_s+tz00Mx`RoPACj{zTxz;8?UIc*B8Cj+A{eX9&*c zMXu0WN$es%rE&blZqK32NCr5}vKb}Pcz81(`3QUR$&a%?V6~5)NIOym`iI#v+mHaL z9nmMZ$-DcRu|;LRCzlY7E6%@2g(Co{trx8{N~u6A1wfP31igW}kq8BqW>NA@Tk}Oj zu_Xn2N60Vt08pL-^J7&&xKPwal3ZalEg~DnK4mhw$^fyIVyI`!Ry$?WjkbNsEkvq2 zrc`e82~Ft?E0_*75IK+AQv6$k&9q*DLMIN+AuL1eT-f+bKilxo@X2#&`rsUT!?HO! z2R9acsk3hGNw$y8GI_tYGb$CeVj18ZFv8L61t1-CMMqXvkCqRR15~nIm!_zKJRg9J zOxmSH~ z!&((nWl7GcvY9d***9VPLGi+8-POY{I8F|C1WOl82Ibz#*24lCRaGEk;D$un3Bh4| zsu3xkp%&rcpFp&(5*5)uvw~Ds(+nU5EGq=#lPmdq@Tv`M5IPKC3MDcbLI9uXMY~Qf z`i?M4cr_yF#-CE=9r$mYtS2{+A-57Bos3mrE=s6mpV4!y3<+3-B0H<;9=^f8#H|(@1#(XHeSu- z>sqrY4rD+SpiNg@>3kCs5Ode6s|y)FTeol^1O5|TdeNsUbhQA@tkBq8O~&x-=osRZ z%r_bW)VI#Uty%>w5gK8OIufNSJ$0&{1zdQ$A}z}m1QD2w&Q`r|S^(xU3)PjYhG3w< zFAX6=!G1i3C$J9x2%h79poH6qmx%siqH4Tr#mnbY#e5ch)xwT8#TI@I66ZN|EYE7U zFLd1L#bhpbVlJd|!ji!&UQW8tC{uD1H(t)8Km;+2oLfam7erG5m`U0%YUM~6m(gz$ zIh{qNhSiJ=c;#p@h3S%wR^$#)dX1bGy2yaMVlD5-^XY))&!^+{4UYKr4LzynP$HxQ zcLn|xGnKEY-9jcP=~V>D@-p%{4Adtmjp2^g_;NO3Q6!@K(G;KK6&SzK*!j* zGxx~s@-jl)(_c9=?=O3cXiPIV$4K?N4c5s2g0U5wzqCDkG8wrkXycTJ%AIH>W? zB<+=;8KG$L@#3B|UeGvJ5@boX=P;5#eEPzNEzhb{4ppnwxso8tS;%^{e$~8`9~hpK z$_JM$BfH+GT1dlcFCwk-@YDoQSeo6tL@eL^d;?$Msq(oP9dccsL5`91TErk|Fz~dH zh7)UjR1t?I_$>}1%^0eWmyqJJHN#M%j|Q{=uS7=|z$A5EIa<{q%oX8Fvr6V}~XI zvFso_92j8G`C0(c6w$?pG~df`@QxM?q&ev107x|K?~+u;RJE2n^Ifw-S z7=qjWLyMblL?0OeR1)@79bXuz@0j#<-1Clb(v{+KvvDb1kfPJR$Y`*%H#Sa z!8~{F@YbwK$hRoBf=_0xCL_eFFq$x6n`CU})^P+T~%u>ZcM(Vo)EY25&f7jl%B?LF$UUDn;&l-B!1VC0}B=PXzm zqFzG!I^xd&Ua{~H7Xe06hGh5$iDSTpa=8d8^G-ICg^oN^hc7nILW-A;*F zLCX|P9@S4seBng`lwP?TymRM{9lPuA?l^MfhF5gLLew03!*m1u@BafLi6SxBKM4Qz zS0V%HM*Lh3A|$QOBgyu3{llM(eqsI#8Z=ha8x;yodmaPA-Ohn+Nx%`1=B5~$3rF$d zP>iqVEQ3!#9Q>5#;{G?id7Y`1$!yszSdj-2mP4uoN_&{Fv*Z?$PWj1d)s?DZb!Mi4 z2$hM(5Rm$A4)`rmamS=KwRCdV3Up&^78PX>jMeF-E}@G&YVooxH9eJHLs_Z|PN3$pzUF|NG|(jhpRC6TY56wR zp16zFvvmwQ#Jzui*Dzl?U|-{M9Q_&bqBAn&`-PxD60 z0THPm62`uT%0R);JoFooEFC9^%o8hrd!&kwFx9Yyl$pvC2I6`ftTXCu$XZ_K^ijF# z9B_Q|tpf&poL{YLz1-$)7oF|p$M{v1n>~5_#(G_?H(o%oInb4@14qzl0fEwK1h$Y8 zWgCEvQP7PS2omBZ7h1)4Ha)v8X9&*nN90UM0x}MjPkiiS)aDb{g+ofay<%7TM)%o= zud$}AX}jexnktgZMBGr{^eC44ulRR7zeU15hJt{LVQ4ETyPz|v-2sH}C(*d(0`pj? zH$Ru&8{`#KyX5Vjxt3W*WHKZD6>_XJp|G`Q8$iOA1Dr=b5R)Wy2_zU83sdrco>Z@PX}!_vXfQ)CA87epX|l8gGH?}9*wFs?kI za>#W=I!RhHcm{08h0TzRh?FS0L?R;C&DsEL-isbbrA$dm&$_GbxYn)OgWcD+-PU4P z3YEIZ>wd56B2PcPVTYZ2aYFY=hr(pr7Bn3C_Tuy#XWb}_8oiX!G|T-j^74nm~k=#Z2L{t+mkw-ix5EXxme%fd{i%XiTG3Wfc=J zQK&`Fi9^Xm{ZO7MPmmG?*N-3Y11+LAhSc_1o%xwS&ZcU5PpPS`w_JCfH#PMqZpNCT zTdUzDav>`2szYtS44iH;@NQ)&9b>8Zdd{c%DR$xnJLS)^bJSk<9Gm?-3|8f`DbanTIU3werS20x_TB_wb7^%AHO~8@;7ElCbS>%7f@k?-F zJU0pghEpb#`$VMkgB}dYlg%Dw*_)A)Y}Az9O0(%T^vy~I;x5?VGsc!OTcuY4A#E9& z1xx*t=UTcS~e>(-@lzEI!Jjb#bcmeWHQ!XY%Kga|ogXBf>t>kKZocen6YKUG9}; zGL>-O&7xf!@4nrvOqer;CHdM{0s7KqwxnhdWWt<7&!{vXyvb z%tU4)-B0&3e)=-vlpb0_Qv#?0C|eY77+9A&(qv2s@g{zT%rr3q7k8yPfst?N=Z*?+bneia$ces)emSQ6gl$N0 zoglCV`D6df-(ol)#dlN+Kg{tV2R<@wVwEq3Nh1TBc!xyua9E>jv|4lvzYWLaz7_3# z?_+oJD>Mpiy>SGgMIUbA>>5O3LoLApYm=o1%>wyh zaP=+d3IJ-;@uYJIf@~-ZSNVl#iXq?pSo!hy>;WVwT}>#JA~abbWLRJN_4}$vD~HQ- z$RDvS8MYQuR5QGy?FX<}4V_w2uB7HE*{Qnfyy(o7&^7!e9J zEbU9b@j&%h?O1gIFpHtDD2O<^m<5XOInR*rj4q|-oHkm8D$FM)EU z+>PE1ITUsR0;keax3;|AK(BqIPV;Q6tjB9n}$fm2DMOAn7Z`c=N;wA-oishkJ&)`nWs zI+Ll+LtUHyFz@R=l1a6S+?Ysr{uEX?EPWIm6Aq_~`XK%T&9neCjNZej7{cik19H9s zRyJ_-k%Km&%u#L_Zt@k&)c7tt!}=!GkPLFbtc-RpviUK;q@m8!D;g!%du66lp04X< z$2ALXWEb46Fr-?J$HtBwYav6R0Y1GQn@*xEHe-hVusHWAp((x^SP^0lVvbASfw(GEePtq6q4+S?lGd$X`o1c8dbsquaRO4=Nw-D6X*mk`D=T+Es zCb10j;HB3#eTJuB4e@ZP_9ia9A>QS?#otHJV7RKocaOG-rwd}du-mV&e*S;f-ULdr zt2`5|h1QNowz}V30w%xYJGqwRY z7(?5GH*7<*NElE_9(z19-E%x=#_h&7#tqoGdwR}czW=_6%q$^5KBuE9A~Pd1-n+}c z-GBN1{VV7NeZX<@PFW6$X*kI9+Om?aN%^n@SG60|mC<%#@o;Kow{+mrxE=rh2b$x_RQd(6lYh{ATvbLOo*CNIzjHNQnj)~BT zag~X>-dI7r+qlF&$u7%_ANo-7wab8PPI=NTBQ4uf+mF|ZW}*6uMHQq<1sP@q4FKT^ zS{X4FLIc=jE&=K0aDw?J!eUCvlqU$1udHPN zH{j~%8H}B=kbOq&v11}>qw-~QNjKk_F~xAt!)Qz{RQSY6gdA;61Wz_zdx!l4g~EY5 z{^IK4!&k2!zIkC`;X}Y+w!Q=hh33}K9XBO+Wo()JMRfJEBP#30oUz)#@tnptrVmt4 z9Y7bFps6W(C2V2}9ur!LIj(9XW+$04IUdv&LkJhDBZUqi0tRi<$axHb9GxSYz+&xf zD#G_q?NAY8*Tmo%|orUp^;jhdW6_NIHA00x$xn zn4dz*7iN(19mB*d|H$@In-OwPzllT3hs6fB^I| zbt4xI*QVLAW7nkO-Wk`(XS(R4AflN-lyfzdxFa*hf^3;|%Zs;aiI|9(mu}ZWS5g>= zWC_rO>G&KAz_`~zyj3_yKo~_B#VvUt;G@Atve^`ncx#EG>ZC#ks9O;4@`ZB#aRiMe zquKZ1uA!=>!qKB_SX1ihYFU_TgfQMR2U!8ltyPcRc+|TE?c-(S0>>_KN_&anB&y?( zBMBSanyeIT6peuPiJqIq;?Miitd+Z6GSN}7=^NYmBZ&^Kc65lrr^K!aXx;ks+X3(Z zh`eU7#IS^Lzo1h&WH7vo@}?zg79g58p^vjl&!vU~qd^pr2AjSgy0UK>?*$LA6Gx($? zjrGP#gmN!pJffv+0V~FRo?> zdfT1dUazBacXdDAR&=jGS)womRU2uDP6*$y4rB(TGXR4{1=;)G`!6`2ehFh+ z&!@*&_$XSq8{H9PS-a@oa5K`4+bA9j0mP|$uXz0vC;A`l+q2lFN4am29Gx2)YTbJh zUyP=;J{5dV7p05Sb2HJrbBF_ar!)VK5t@`F@43e#to*%uEyF1*>7_KzFd^KeHjc11 zz3jcR22I~G%BG<)QAd{mgIrQ+c#6M?IuIMM)jL5i2-V=8RqnM}Sb#S-9!^X*^gZYz zrWhI{9hKt@^Z4z>)}NziB&#PwH26rI_s`#b-$Sbaz@rQRK*H_B4P@?msAWDt;FRB5Iji*S*UP8BT*v~0homVg#vgz9668$ucd*Q zg$`d5I-FCC`Ze$+k?R2TL6d-ZrwTYeL@w%Ma){!$V|#O*NNCDqi8HZ2@yXWLD6jE` zSt!Jng=|496g>1ox6trG_Xe4S6Qz@R18rN)C>5m3S6f6!8o z)y5(xXk?(onk8I!hHy@xU`_0X2e;$f7Ct<(i-xEnC)V^V4~jN27E0=_GmdNocrIN}YktWt87$JLb(>w<34Nb3A+qU2@DoLr-rbDi$4<3#DqMh$v?YSWt;- zInfz1%}epp4~UPy1;iAazkD^A=B0)eM z%5H;i`WojcLW1PpB?vf7kUwd*8+6)QAaR8EHk0%l70$z7L;ky3$3_8TrGn-*LKCTE zRRf!+X;vk?;))E7XogkQpeDoU)TLU9q7fytqGl!%}>f5^{nx-4{;|DrGXc@mH>=A!$j2 z&=X1!+e^wDe*jYEGB}pQq#e;x8#56+X?AWn1YY>NP!+R{clzxiQY-6B{!rYm{DB8D z+D_%MHF?jc6+x7AG+R-0)oLqdfNBKvq3~7ppyI7IH6y+I^ftAGe>pU=(lms(%q=RM z5#QbN!uqb0{^>!#6F(lnR>gQ#e1NYXD2|>JZ^fj6&WNy#TaWY!3Bhq;G;$LNTYa8&4%>V{kcYOtrt#KUr z>~-h&H;BND4{IciP=k-8NO}k^^n^LokbgGuLCh;9FlO3zl@j#tLm-QPy3Q#}AaPuC z5S5bve*F?|*16BzH<*8Nu9sNXwx*8E#?mrOp)mY1@3}Jnz;S2Vopw-^NsbBywa4ht z2N%+vJjxcQ3vG1Lor4@AdYtePyg5W@bTow@VZwdfcIAWz8l-KaXDi}-jzGjX&86CU zzJsP+ZU^N}-OOcx4i>KhBVf;-TJ3}AOgVzNc+8MwQwnkC*js z-XB1evH+Zqs}oik9plOVi=i2hV|B>TBWv_}6Ca*wPTg^cBvzdxS~>i~pOc!REsd8E zf6F15$jbe8ISCv()e%6=C?F-&LSUCBcOh{#DJM)_74lhEJq3JOgl{|g+#)Kp$Pi4Y zwSUWwiL27qW;vT0wa`x7EleFeynR~jwfl~x*WBE}r5O++f^fg)KI=v^2O>a}LF!>@ z6%^e5N0?d@;k|A~zL8|NC@9db)jEXF75J{jP1J2*^2i+ifg9is%}O8T0eNWC?NV zMux1VRr{#ez@u}9MqJgh@P0UwYQwg`*<+eVqkm@%$U@HES)c+^A0xsAJPORb^H@@D z_{5C49_ECRLj%QgLuJGZO7bc86pRh0A265Jzx0Hiw>iB| zslXOty?a)ZkV1R~3FD?U@V0w*L+M?*U0~8 z&vS59QNtxlO5(d@8eFlJA*;ia*4dlB;aal88k45 z_q~#EXHZOLNONKeO}b>m7GD8D0G>!uG0=pVNJv0m6paLc^rJ!zV;x2!AQtUVN&#nC zC3NM7u>EBDyWoXMX9VH}+J2({_-KNKZIN+A=Mvg=P77lS*u3e{M7Fek-j>n7u8K8}9TpUYgCf=Tj)CvV@eBG|_qiD{ruwMS>02 z#eJw3N~$nXlrl2s&!SEpx%>KFM0vAW>uXdBkA|wgJ#h9<3v=00B-C?A;db8Y_(Rm! zTwUB%F*DO$Km^w;Yt-u4wg*@)sf~mNhRP36SpX6C*KkwF5VnMGGoCSafW$j(6*%)X z|4IL_FVBv&*+FU0QTyFTE*>~|QzfGd*8_O|}*vAh22 zo)a@RLY(?FT!<6Az<@9Ay2=bRj?6iC3gdPvS;NpaQ~2>Fs-{Or#@|K6W#o?R(8 z*`;p9qGojQ$j|=pkzey4_YeBg%z`%8ElpwM{j+=LM*H%qY7?1*I1+e%$df;@@8md* z^T}xQCc_RDYh*$5m@!L^SCrsFWrw)lh^P2WDodl?q*Z0g4Tsy zqmjy!XWIGP&;9sM{Oi`fV+A|6dlVqiK14FtdEPwg6;6BKLN%2mrEwB)6%-w%cz?Zb0b(i!c3h`O#5UrjL3ow`gBaT$ zt^dQn)gQ^{FA84j$O!-pBrfPD>Z|sy2hZ-_dp`1f!SSNnB_xcLS}6S^()&8>>Ui4z z8;~Ap-#gh5ng2X*7&h;v%)o$muGkf-(&!>)}5nNp_djUCoOpJys9a5W=ipW2{$QOSz)+3*8 z?Ix=-W%llA^z}jWhW92M4^>KvoYqDb_O-EA>*p6`1WOD5gnemEKLmC+cQYlP( z-;dhuJFnku;5NL$ZYOyez~4dByRc8WaKAqBh$Kc#pO_2qSw6tDb8g@Ra9d3tR1GIP z=ZJo?SQVM&J2pb=uZs1boJQ?e;fcsK+iS&=Wa$FSFG>r364_2kz&!oGh>Lvz=BtAn zhGgx<1_U?Pe@hX1-a&6#?>zCu7g%s$Vd+riQv^c$R%|sbEopZeC&V{BuNUDQKqgMm z;CZ#J|2sLASV%6&Y36u368(LYMS1D6-tzQzs*^0FN{KK+pTS@D)RN&D^@^GYew{kq z0H~7fOrc1*Bzn@){9Mk+j~AZ6M1Clny@k!QI!mOcjFtch90W<@g+F*CZ)fJ}F`FeJ zps#QfYn-%7s)ZyhUk$z{vDAtnvF`L{{Mp$Xsz3X2U!=$1!8}I1@t`;FH@u`cG}=M|=`BXgW9L>O;1}vOz~SIRHXK*flr9CM zf-=!SSYWJ)2IfhRIEJhn(4Wldxn0RaK}}7%sWgDjXH!G$$6*%vkYX@#Dlu+_+ z)x!e%%|a3uw%m)PXG)Ew(N5C;u#%$%Wc&3@%J!2{8rFH$v1V*vM$Ro@;`AdaeAdOH zy$7@9D05_sUe8yHwRAJV=B+3YB3(|_guc}fYk%l@b0LaK^=y+@l_$4+2p9wZ5gp>_ zh!^V)?BIuxqSx>L(E+!K_hZpT?RzXZoYw{HSmo0yb4}$*GmGkl`Wf}~$<^WPo=mFJ zP&^GycCr}kVOUCk5AZM&eRLLtS+ilzjplxOMt({~hTZD>x9!{aNdb99YKDV8Q8T=^ zpq`WRhsjdhvYG@(wia*^1SaA8<1RJ{VRK&UFYV?k?=^KcSA5=9}g^m25Ohj;6>K zojhQe@$X1_HCrD{voLY5Cuj7s;;pSM=aI0NNtE(RvG|UI18vy9ba+S3s~lc#4WV8m zx5qMGfx45MaIFz4AxWGGdr};Yrz7!86T8No6;F0ltDW}Gor_cnh3@l5vdi+WV$yGo z%<0B~t_Glh(*Qqm3c8I>a4zai)p8Tnr|8Uq5{jsRxy3$rD(N9k9@34+j_7FJ`GK%6 zO{<0ZLVD6p(U4-d^!&V2P1R4{d@+TQJb>J=Z(x;4G0>p}l(p_R96J@G^+mxlfG$av zlZ5$35Q%B|C~BGVNc0QRJEI`Eyo~bXmp-3G?A84EeLE_0WyfQ)VdwxkAY9yL7vLc2 zfA9Z+i2tA0(~8I50iFv^vmD1QN3`f;0^?T1MwSGEn9;cLpR_25PFu`*_!A&Vy?V2i}1sArD1ol z_SE`|Dn>SXH~;s-iT!B?v(RXEYr39y(>mZ@ad`JpSK?(Vl}x5mHfCG|Lgw+j4ky;i zj}9K3&Dt(2yfIF!1pgP|{{J?d;e^_^2v^N+kSUmhQm{5r$%s>DtWO~&h?e5#BybxM z;SdkeqM~(YzM?n0D_+g$`g1!v9YD7Cy;`l`-}y9UO&m;*!lq|j*3LI>R0jPUDO0Q8 zX!Vfr0USCQ)*&8nvFh#sF4Y0R@q{Hn55naQ+7XZ~@Sm`&ui$VpDgE3$q{&e6b)9(P zk#VN~us1t994E4+#Pxycehk zP_}I4cmDKx?3tQg#F0smQtSJ|Ay`DL$Rrp71BJ2z#&3=nc)VzQXg^^JMdWVu2Axi~ zJAIWIDxgm3(MQA?{d`_vx3R5EJEa~jx8W{qd>Rd%BmzG{(9uTHJi3ADIvOKJ+6mn~ zwH@e|H!PrIWG01v<%q+hxvU0tmyY_Mrlv4NZ7RB%hhjI!x3Chl^a z`ZoevqoK@zZX!8I1S|%DT+t1ou9s5f*@G|{@e7}VR5dst;8-)#xrCiU1TB>weAE`x zfUijdHO)mIqChLRkVr)agqTT-=p;sX*=Xe=zyqJF>6Q$$9s-2&s))5Cn53|zbuEoRFn4qzAQ@oC5T(r2Y7XVp^zyTzf=a29 zhNc||$0wTgB!mXFIj4SylRA(MNJnx9VIr6g*T!Soik)smQ#=_aX2W$V2YtQhirdwq z4-lv{fEY}-D>9{XSK18)y|}E;8|qm+&apYyotyg;cveu6sOA_B!4-6^cFKKVXLXJW zHfg$}=%-{Xs_J6NfkSI^2rY;4#=cDIcDZg;!O$K%1%rv3jY2S7j^I~6AY=yj;&{VG zM0s)0>?iXuktU(Cc?XpNuMd|Uwv&K(Qf#Ym8i6wkmo3ygNQv)sCSkIDi%a{zFI47c z9b|(dkIl;mh?-#w9)#Sc_J$0URD&QS`Ax=Z?4uh~60JB96QZ>cE3aSnvDj<<5|frR zv!zX=NFT+2hLP3aS{pQHD;JuhC+=g*ticld)g6_V)p-?uIdo@ofwStQWL-6pA1Z5S zQ7FP}W2xO<%5Ia8%LhPXuq@Suz5szoIo+EKDU1UvHo^2jWebK$LJbHXxG4cNzlZx< zIWEv9{*P~3ZMDdt+I-tsn5A3DH14Cvo(EsAEGTd;R-JNrs-;t;G;$PWTSa z90r!R)~`7!)Ktc+Td2Fa9jM4pzJ8J^cnt{5bpql7(-2g}IVH9TH`^qi8NkY^_@h0r zMfMmgVzp_vKAOJ&(xv-XLwD=8QE2aDeBKJfca2(%U?f zT+=P=Vs9`LY0Epf4BM!xDbO({p3h`1m>7tXo||lh&rIFXVDmRnaBSyJ^wEZr-Ht}U z{>S`ho>}tTWp5F-SQ~;E`&F=ciZoTue;1w)Dk*q#kO5!O>7mop%;I9b zz76SkXn6}C3}py9h#ECt4;erNquU%aU;u+A!-?guQEf(Hh&Rm^F*k-Kzf@}Y5kUW7 za|*H|8hIy|YnG@lnWO$DI;`Ybw$3s;0?rl};M+RM^4nv66#2pCAE3AEz^000NwLBM z<&Z?;78w?K-TWCJ=~`rdkl2dw%DwIQ;nUjOOk301ZMMWJrBV}}PZA4?-fWg;;XYqj zT0&|K&{*pBAzeRYyWZhhZF+c*JL}9b6tl6K=iN$OQB;^iM8uTDy6Ggt#}m%Uq-n>p zF~lfFxN#nZV6|?uTn^qVO&?iA)u>D5mbx+1uYa;sVrBfBL0VRrV@`cnYUn68vNG=U z78qZDg*%0GCpyneoBzBou-$VNUMZhiczXR?M}j(|dF2h(=f{+uw|*yF5r zauy3}`;8q0y!R}~`CD85fcr?sss+R1tMy}7yThS37_|QYcHgN}Y%Bi!y*SzaJ%RM<M^H>Q9ca9BZn|0J*=1g2N*NI4|Zx?J;7)2?wZ$gR?RMot7`2;Z6T3? zd);OcD)RRLsNBs}*v!n#DXdqXaHAf;8vGrw%E_OGS<_XCsOie1(ZYW)D%}%#zYJWl z?DW@|EmXXF?%6tH!L63Wte*yR{v%kA;^wqRWY$m82oY5zGjX&aBX#=!wA;yAB?YK| z7oHleRug8GU1qZM6+&HTA>E-LuJQT={-n`J33HvA=;Xb$zXMJoVP% zA}l@m<9MgT0qs#$IjzYZ7`g_*S*x zS%r+1rpB&T1zF)KdjRttORYf&Q!Og-GM1OtE0!LRx7VGE$KBAIO*np0U}-k0NE^-N zDur~~7(6UbA76^5?cuV}23$@V$TeIT_Y}mgFsHYG5~<|F;#^Ai8WSRHFj_S1(O4vp zHk15MGx@mTYrD{KFRp`$U)-iA;d^RD4oC}+i7!nci5a4%B{`)?mA$y+5w z?&j4hCP-bVzYAI#7ahlwqVo|a%nf3dF^ms+5^!Fd$w4;{_Rp{eTA1{vlLX4;eo=^O zHP&OufvgqV85|~A4^VYk%*v4On(u^W7K~36nTSn^v1U2Pe+{2^-ACj1Yk({=L&oBs*;tjbUBEE2`y^ zFjNy;)nvYn&c&}d>VTc82(x#uS<9>i8M_7Z+XZ^?K*uui_zW&@>ycOSHS@}(bvTK4r$;rc%3EPI z(ZyVTmZEXR+(K3g@$C1#^C_mxrcWsc{XQjKm}ax$DBbKuMJST2Ji zWgiJ}P?Tmf27WMEsI|2q4J1u2f()2}uRzu@?$#XB5o{3}Nz8!QuyvWY5Ze@=WB^ZM zW*5ob=vPTCxDGG@P7WYZ7R_`3oOSq$UlA)#Rm?}8Rjc}GJsm8hdYQZ)1_i`udrWUt zohoSWE^sSplFa0A6Yx+CcQW`JICRZXYhr?6MdFeD7SI+=FWWPVDa9breG8bjb+6-A z#iEvW0v)EAc5N1o57E7{z|srIqC);z0LRCNuPoQid@GI2hk4!s1v`8P2=`k&bV5f9 zL}Ecf+}TKLv4DieF;Q(Ms6SWSFi6xB7SLj}^ekIkTtt89pkg$DKs|%p=r(f8EG^Tz z8Q@8&j@w*Xt%Y``f{EV_I`}#!u3_cL>#(8AFjF{vVu~Ge4bt-P=Kso6!xMbzp8X}c zCe7;|#g_q=DgpU4Jc=%WAGI}r;;BEwsyEl2+yNiiZ<Bb=Ye>Gq9Hpt2)4ng1525wr+jtV9D*K zSPihzjTs?iOgZbeFpGO}amtlKc@q4&TNWp-L`dy9P#`zeFqKJEo{|t3!RnLe`x@+#=WS{LA!^g%#KH+Q)#i{dKaFEE7_a zkIyp@MZXkT9vUNNtUOwo&S!GDLH09@xxwy)kD~-z-d^+&2ihI%E0@b4!F#~uv587^ zdjr%8zO`ctA}WeUfDLFZ{1%`c3+Nq?t!W+gSYa?&cp4qsi(K4Y^Z-=#nl6Uuc#zK_ zER}J6cizQ+gw<>~$yKo`Q5n%yOqMJJm!pJHm=?X*HwZ zTnxMKW^O=`7mH4Yv?vSXWX*Mn8}aXAB?=+He~Mp89wNHD7=jY`89bKFzrut<%_tQ^ zG~BAlRKi?54+&`O9sp5VGWK$zc+g8FM5aSzY8?vjrBDWSO;n0#MO%f%oR=U}EFG2m zHsVu|Y_^%SftF)SMJ16)Bm-bBCB3?k4peGl_6|q^^2kgQ*|{_u#m3ZFuy8X1J4&Vm zD44oZKArWlUKHgXFJuSV?8+a9F4FX`DX2$&BQ*ITnwxNeMoveFVaBx(gXOH`lJ(l)-)t`MjrGWR&7d#hGzI zb`N4@C|Qeg11Kub$u72bClpU#$%YtnH?7Z$%qRguBVVoHQM*&uvjZyVc~varqzcO+ z+E0z%#(|lXVSMbzjHmkrG zHA`kIkpT*mmMO@U{QQ}NbFum}S`()A>}m!k#@q} zx$q<|#(?O_-@uBKW*yc3-n+@0m3@8hqdUz5-aJ~ep&qe4KmWV^Y^LW(;VjD?GpzZn zlW8BJK4bS`OX*}v)?kzSMln;`(uMdAd_Qqrx;FL{4zjU>gXRr&Lbl?41?23OL6t_P z-+H{-*=Cxvfnz1nvLNZ438b|%XMQ8p$`-JN-0-b$MRfWG%kgrkqw{8po+Yk2pw&B| zxyafk3y!dW+PsMX%@Vo{v7?EKh{m}!Txmq~N{g_FVf-6(u16$y0PC%X#)pb^#7+2l zHS5Ynqt!J{-|$m+-syv~1<#StH9l(vKEx`Zw1N7Iu7?o-Z!%is8$oZcOD%FA0vUXF z9izA&69L_dHlA)9(h*{0Nd~C|9`x8CwJbCaAR_nUjN%gP68OvroSPg zc-~D47m!^qWCh`@V}&BxFS`lbt;wz?o-rBP+^E9IP)ER4$|G=0*rCjAsx3LM7VY*jncPhG&hC6M3qj_m)4lW_dRaECl9FBSs+~W8 z=M!Ub?ITApHuSJZ)f>pttWZh3ETh9a*x+7hQ}VIKIB{{=D2Z93Hqr?g65nX}7pdDN z5=Uqyqw6%tL~s0ibs(*ltk7g9(5%|7OiOKPu4;x>vfn=y8iJiV<&_r~ktA@*fH-1f!Dyk@%?^wi<|Q%a z`rgLB$39<|&OMeVPoiVv)Y4P)=nTw!RF*FSEPOR!VVGUZ1l!-lP0g<`X(Ofx)U4IKdHEgIZg7>1X&Ocg( zaU(UtY;XeLg%>1yTR-V)1=Z8*Ey^%y-!E&?fa6e|L{HQ+j_oeNVo?Xk9JwZ@(^Anz zt^ocWQQShwabUgTzu!@MqS#j(ZXmV9hSbE?U*%f+k2mp7G-;+na%UpyNEz{;i8$l+ z#h5@FBoCkBWpkyaCDN#7+lgB0LOA@xx3I>%S~3CLbDIm z=U}1kfGHP8ZelBFf*mx8m+(^qZp#UgFA45U(ukDCSR#(UzTOUKFS`Z(uJ%?I>C1r@ z9169dXQ#4=tq+GSJcf@+dN2p5@5V1j?lI=-S$uJAqmKq?CY~KtRM24#`)ym!4#^6U zB#;Nj1jq1|cy#jzMAM)&CgC)gJdn{uiY|$fY$gcU<=xWkv8}$Kpnxx7H>xE#gIu&D zLn>y1`MJEW)%Ldh6VM4Zs(J)e_`D4IC+LT1IWG-?1(7 zgir?D32^(UGse~($0an&>uyy|Mep3GuW23A+-evK;7@QMHnm5U!NZ-w(0rSL#%5&;aC1KN2B3ezyACC!lG z%e2s)TDQS$$R|Ow6I;vV3?pY6kD#>329B8taeF78WQ#UXYkUj^9v87bd=3-hC4yPc zdYM=lTwKshH~$q)eA6Fnc=Y0uy4eW7|8x>cT~O^P%DZyPNXY0wodn!r(#jl8;qaP5 z2Q=t?9ZQF5MpaKZ#f#n^)^!uh`dlL2H#(nCg=UH}*-Se^H6+qyLxAEbzHKT(LqNF` zegVuFS3&O^L-A~L-;UwbYBs6^CZq_XA_5!VgGv7r9PbsRH^-%5cgE6YLm-kpxp_9E z^hp?y$s1QX#@!PGe43t*S(rb1G#G}EmPOR1+XBG4WK~^F zn-U6)jiih+pKK`N7^Ejq-IhoR0L4g}Tkp@+4Tdm-3fQ?;p2Zb$UqyW$D#1*gyEHVB z1z3k+5@1vs<%PN>$T*0jW1*Bx$SG6O0v|QZX6=-V+Gf;o%Z|0I0aJj|=QMN*yaPE1 z>p)roZGBAAm~N~vYUyL?C<;=|Db(OtRln8x!pS1qY(o?Tsq?3WJG>tGQ+VppA7hm% zK+4aP@xk$rmAcW<+a7Y-TYvoi)o|x8B$FRJ?Clxf{t-kfe)#P(r*gSmIn`f4OG&i2 z>h(TLd+Qt&VbYHX z8?TT;8|tph!ECY8mw)6`!vj$ec&Ta^*bzp_t5#`*Q%H-XsbZ!1RMqeOBPe*XY7vQ_ zc(3ppo4_p$@1#^}DTb#P_XX(WP%c+pIKhRJhlEQfaLvYDeJHhCc%w*47mHK6Cq^u% zZf*#Gp*Kh5tIZf6;u%l80gbOjbm|Ze>!uy<>&b7(qmK-++Ag#bd_ogPjYOCc zva*{9i&iG=EbC>aApr4+jtGwSn$5gl@&Sr$ym)F~-U)Ud_i}5}VpAJ*httyB051JI z#yLK8l)YY>k>6t$yiOr|%yeuD#e#3)J)RtS?e=UfnGg|U0csUHW!D6 zM{ZT>Da~V$kYcha7ZYu>lFX&%y`?SyCn~ZxpB!2zYX1E{qzJ9qWRME!=6y68`XTaWc9=Y`QdQoecK0v z%xjulhk;%1L>7R0yR7SZc)^Yma5c3A2q0q#MMI(sUtg)*f?g$Z#q~+f@Y{bdiQG;@ zIjy4cN>Vd&i5rBoYO{W!c16V@P5JFg4rqkg#;T;4VrZC4=!N9TZO?1uY$kcsOCjdj z``!f44n7+5?1RXLHM-DIX6caku6NzH&J6~=yJKD7_lkf@j(xD_LZz=p@)s8T`T1)-c&oLF3Wr+)L7hCnng=ts=c|u5*;<*CMZ2RVDq3kx9S+=#nRig3Cx!0*I}CJE%K6TD@Y=ru zReT%S4?Ll0J#zxo1=9Gxj4~3~s?|uXf5p}p*u{%%2JMP)_xrePpoJ$o!ubTVBUgvd zmwi2Aay^s$i4x|}_ug`&L}tdHZvjEH;Q7dC%sJJUmjKP(ZveB#%{Lw>mAp0!!rt+t z-L#u&!-Bj7ssAm+&cby!AS$#d0|~%5)U|uF|9)chzxx42PNP;sPl*@dsC^tr>4&y& z$GP*p(WSIUzHx7D_pCm@dU{AXrrh%K?Cf?q(=OupW>KDqW-l*yKT)Z$ z3ahf|Br+2sW_1>Yepb^C+hhPD-=Vi7&DrLx^R%uX@Q3)@tW#HwpC%zldH_N>)Mv4YrPG zk^)myip7!h*Vis~-eNr?a4B|-zkUpM%4pf&8hOFUtKFcU*y?F$4fF~;HmLDIooV-}8!TEZYc>{bVs|{`kL?b>-X(v}5-usBPmw#Dxcm{uVUQ#1q87zORtK@ewDmYXwVi(P^sgZEp(o`cmT( zy?n|Jkacfqao4>(Tn4}D?FQlvHK@m|G=Kq|T{l%G#75F=VzUq#_`7^K&r`*Qf?OK3 zxypb3M3K!%2oeOz8&9Y1WTdsvfA9a>@^MfvH_JAtmrh`i1GGr7o9beY<#MHJx`sBD zC=D;Bm0Y%%`v|f_J8ZvJRA8n!I6@p_9c=0?d{eJM9~1fv>^;iH4EVX8zVa=OrNAa@ zAg(f_(_+LW(0yGbYwlf3?OqekWYIM*9iS82GV+-+X1O62E6L7{Jy`*uo?;*n0b~Im z{Ji9+JVBkwgle@52zc+Z@8@_-38Nx}@9#hw5zu%{YBer8N8Hyuwh8E95-ZTh-Zq`; zduvE(%b6{2CR$rt%J`NSeTm5z?s1%Z@RMC?|JuUMe#XvvSjywL{wH7s&{A^2!{V-VU|(DDeE%2 z5_}7%tPP|Xk~Vyq_^z)T8f0M56e8?htVW+Fux;xcBh0Rg&vgbLnyN3l$eecANj2pP z>ena{9w2Cd8?}n;1;$=@;e|*%CLV3J#cl&a{6+}zf55#dk`vd3k4rcp0^pX5w>4hF z#YHUWdTn6qIixA>9O|v)+o zH$=Tuc;uq$3i+B%8BSNY0-@$06!4r_vtzYEk)ts+#kVIO`)xTjeK*>(E-DrR*iC;H zu3WSi*G+bG$DBU5^#-?BWX+q=?R_^T`wuR;9ec-#S>=Mb(urHM~1W&vzoyx5nr z05>I9fQK%Q1)+}pi|m?|VXBCwyo%nG1t}A{HI{9nz9DV27vJvsQb19qi+mZ0(aLbi zEn+gn2*hz?u7kXziQ&)zH3c>x^8@)wboM3rw}{*%G^<%y{Px3vUqVYe96*?pSdko4 zEmdzpqu`8|52BJ<0OSi8(z2A_Wd-U~>oyuU&+3DzdvrZK;{)tF?;5hNy12sd z`c-cfxar{HUHG=TfPWH8>_}KHT=Ghyt1sw)W|;jl8joLrZ!WrGB0*>ywd5$!i#2EU z(uj&y1}L;exiB&ru#V(q(I=HI>x(#%FBE3nG-3=lh){#_s8{lf{^RoVE7FQ*C0*OI zy|%P)Srb2tLrHVdcm2~r!Z>vJ@VPtv#@>Xqio2b}s*t0aL6*(%^^i&PZEOq2q9Q5; zrC^KvBeb$_(@{bmeM?*VZ0X2YG#kp-)=5ZZP+c7+SQ*x0lKD2UTHtR~ zlXbK;H%wrKz)PuR)P~+B{OwyYk#EDKnAF)~c{hs(%!9apoZS`sVB(|n2(u+_B=JYG z|2deoh$!TS0-A{9w8YdNavxG~Tc81_^Hz4^#L2*KM$rdyJXDiGR-tU5CQ4<|@-|>a zDf@d3eSX)?%kAh~hJ)(e3ZDDN>jK48Nl7b<@o=hNjBkw*<2|Twq(foBIR(~9+E@^gUh)(8e)g{RE1Kv|logTs)pK#%z(GlW?lQ7jnpBpqa zgJb;qm>b6+8_V}GA(Aj9I^(p4ucQDb#vw@-PNMi5T05k{dsEKTRJ%PGIMdU;-jtuq z^)h?u{J3YW}=?= zA5crWl~*KFU~5cO*cM*+I(ElV9_(bi>xSAy?Lxz2PoetB%-~y$t^Vc;`j)82tpxZlXVcSy|Daj;XfJ5Xf_;zu&C?x#*1N}(%a$!*^= zz#`rNMx}N}0V*xG0k<%=_V_};c10kAp79ZLv#*QD0PGptEbEI3zJP4}BUkFw2U0Ix zNTN_%LLIhfWRWecCS2P|>a~jFX6KQ*k#OOnOU(xw64LZnQ6#+2wSzeUNPiHDrf5c$ zzq)O;suh==(!u4eNYCHPkXfoPt7L`Hx#fKzQ_`LrVPWv`cubQG{~-kW(@oZHHyYh8 z>-8WK5qfI{EA;XiV0u{>Y%DvoFIaPk`!3^&P~UkDT=EhxzoyFNRSq~B3lH3o+sR{v zweAs(-GpOR%GD{C$`dZ(90Tzjfky9mp;tdd-9oiIEFi`b}4m6G~3$3EMbH&b;3aJW8Oc>5KsboXTrX`pYmSJ_)GIFZFQ&!Vv zWO_z1`l^d^X9dOm_@yQ0B-@En$GzdCUBu6x-e7uxCUhvQD}*B_dy#eRAt!L35n!QE4@u2n@jyLnKQy z6i{Zv)}7ykm}LIi5s%Vc_qw$e+`A%)Lk0GPi&)}+hDf5aiVPf6J;WIaKN=*0wZaX_ z!T=OHKS9P!)}mpoCzJ7E;K)63Ng(wY+d~qn~+fNTI%S<}JoI@Tx z)QZ8?iok)$D0xYRyczM8q3Lt=5JhvCCzb=ef!7Uj6L9D^+88n!KxwT-GRr21iP7Kw zK2kkr?FgZJzn}0KO9QGEnT`-K-9UK=&5&Spj~q*DxO&M1PC%%YuCa6b7xl$mxA~qY zKsvIP7@{EEs4|L&21=9`0kH_ID>G-BIs@*J!^edM0DZt`JONPs1Fj3Dh?mJP9wzgG3-ff4-<4zjIp*ciwKqxO#^zBgZZID3~_w$ZdoXvDy!D^?Py#v zm#TzLN>UXR$VAioa@REAt-HNBJGj}AbG#o6iUCB&2VlJj-lU4~zlIQx(;W#B;yUp? zTK6~6nq6;OJldno6V`-xaUL#>+@Nr>TAgn`>AKVAs!^+%J8>pq>{NghPO_4|f%yK* zKO%$}ticr+iPb(lOa^Mf$}gWSqsG%ks$uHVdBoLbaH$`MZYH-o*|pb=mi9ZCJU1yF z9Dv8x(PHfbb$I>zwi5N~^~6gtDlI4-;C@6lbR_W69pt0f00P+>;ifjX z^3J~*D{tzfGZ6?TxJsa_DW& zEyS^aLm^|WDY}4a_f(*!jbh5pOIFQKQflip z!9E_ifTBZpE4kS4oNxgxFpign zS#X3#siyP|;K%4liwIUx+lrQ%UJW*Y8dPCLA!Cl6K%WRae8@H+%;VIFM*uu%Wdy%} z2R?dT*WjP9aMeP=E~YYN%<&ShZI5xp8tVwwCw~A*aOsPkz!tYiD3Gx@)^Y1>5YLU| zA!vi1L?A7^`yB8whC{rxTFg`+y*fAh3|$e9A-xu;wQ7{65ociX>uv))j1M}_HN^CY zV4%zl6AfRIKZCxZ&uJ;g7LgK)q&&;?&%)dda6UB6oOSA_U{U`rH*dcP`o`%%4%da) zJV*uW{%x1RnnyVtIsv+0WUG}*3&GK_Qrz>E{{RVf9Nh6+WAa;=@CUYP%m*}4;(KiA zgaXIRfVt9kZlvDMp=6z(jLwHpM>;fBRLIs7sL(M=TE!?T^>(tFE6hmg1=W_5x+*2r zK~hFnu-|gkY)fV zwBYlo;(0q|G`fK7khh$|OelgzEh{H8IY`vEF%Qn`V~HA%ESiXeiBJPWLFcxJS!h-; z(D5Psht2&D&p>YOK_1u6Gy;kK3c4Q_nS#6uRS{H8ERv^Y*UAb@Q(h=A_4U+2ff=9i zj_#b(W_DaKC4|hNT=0+@4fjDGAzJ-TcOVTDi14p_j^Feg{-&sH{VUnM#ob$%0Jn+5z2w*#(St21R&7z2 z8 z=N)xs^r%!r>GZRoSuFUbdsxO{>3(y2xuYF zeRn7d!d&mBGW5Wyty{AN;mb-&3@z#l=_?`F#6a2|+cO7e3bPlSgqu``+mV8%8gjWT z8?rek4`dmp$fMu;?w0=zjmIaad9G(cSv2g;&)C8qZD^GDTgZ8F{IyK4$DS}7-Am!!v&wd8w>jPn>>{9X`bvKe!H<;#}5;*5sA`$XdSU; z*Ngdy0YF|>I>$eMTOrpGru~dz4+JMdb*SJhVIv(QQxogC;*y?4MGa8w2H-^Wp?TT1 z5Tg$bx2^~&^y$Ff88vh=vuu49TH2~}6?n+ZQ#zi-jlsQd#u%LW)#GQ*+#`1OP1#OH zshGeB$?t_RxVNE#z#K(`$5F{51t3*1bx@drA_l6F+e~2#aIcY@AXa%;oF7GDlhY1Y z^jvnuD@yMpUzA%rbd^!suBMC35 zX|6tHg;80qmugdTw}7OanvE3d-!~Q11{`l#1(Z!W(>o5#+pWMcbNf?0xhAyby3kh2 zDOAj4;8!&IChJMGBESEIvS_|w9ak|9h z*6Pkv85t(R@aRymwW+k+SeTuK4LhnR;cm1b`IMlw^yUp&bBD2nhA0@G4)jT=2O134 z_hB5d{{(f=lM3iKF2bb^tt$-!x1aD1$clz_{m(>#q-UQxdD1~}W+rGuO&UU0sph7- zCfacbIA9feE>gMKEn z0s&oK_FQ7)ss3sXW%@2ErvG=zl1D?x2_*3-R%U2gbZInnQt?T;nQ_(>%#RxCgPE z6Ap;pry+@^2SUqkCA z)K}rKS^hQDgb8==%Ibhrp5671KcH_4f{w-;P!O7_o9e1!BKg~h(6Wr+U7mvx`!`50 zA~-8f2J0CSi=coXyR=6-3pWw#;${3I^5n@16bnTW0Yj0LZsXaB|FK!h^6jC zxN#aFX5um42~iRJK^~1Ev|LB;Mmw~OG{qmU3hwWvDe1S{irh?rJ=hAC{blm%J^sD# z;K=wTh%iEi!^#q=Mj$)TWlU#_(Km0q^NE{W)hr^xGqn7mZPmWSKH>c@4WW^nHHRhN z@EY?vrH(y>ba^TtCh&a>ZaoErq8;47^kVjsQl$=(DNhuWbtNxX%)+PL+AOLEikVN=P;T2HabO;zXET{Yk;+q=0pj z{6b6085aBtMy94y0Qlv+4nHyHZ~BAB9zcZ`>cWIXRvwu%;!t)|!0uYs>5vc93k)q` zr8D|zv=CK{8y$jV{o<80s<|`Ujsi|?)fFQ|Syx%8L@c>WPA2nmS=ZFHTv+9_4k08wfKy^1Y6kFh-r>i#BHKPH(NPk?BA!rNSXa=Un8JecvS zeAt`kcnD{``6ZaOj)TshOg2i(%kW;ldgyr$iSW?-e{n_2I~H7*p^|17_Bl;Nc<<-w zg2d;s9Y+TH2EUK5Puo0cF}zs<0O%iz=35Uv^lbUuIW~_+rYw5Gyq7@Id&SM`ft@$g zeKpWhU-CZX{dsXOU<6T$lthpju*n`;%co=`vQerBk#j$$Pg)?y+bm3UB-e%*2SnQV z2|gi>z{`KfO0V_{aMgKgcCOc3cnWz0vjr`jO?Zn_PDN?B#??118YL{>fiRzt%mh$_Rtx(y zVO8Q_QfH*$=daTRmX4B^it{rammk5FI0;jf@*3YMWw`u3#dHaFB zc1%wu`GguQ+?t+r|8GCZTp6c*G#ZChjArvFB;4gH64R1^)wsg_3oo%T8QTZn-O$L!Ep-2S&UPOiBLQm?bwNcyz8=0bOo7DbUJ-Hb?ZN zZQb}D#5!L2XdS!)#`mL0PI7CHtW0#seOcFNE@hA?6ffn*du4Ze4X*Mru1irz? z$wj--9)QC2a_ieqz(0(z22`HpN|es+dFk!83MUnERE=E%lP$jF@F<##XaT;H8Hn4X zyKdG4#Ey)=ok#Z}J(Fnu z|D?Tlm}O^qC#V}v&f%VObIx@u-paWvcU5(cQYUq{vXCs=5(*$BVMz!?4k8!~ngM68 zJZrq3VC*qL2~bPk=b72{GtVscdSNkOz}arU!k0ud zV|ap)Fu&lR5I-Tal)mn58~qwRu@-ew^BX_3@3H4#XT**?G?Gebf$*_4OGe@_+2=;!6IP(UYN+RhNTQU<_d#6t+=i@dj%LuC5#FJb)3Ae~UmHm_#?VT1XlP;rZ-awl zX#m<}H|YiOSjRo@pqkG?;{7!C3%NlFXn1$yhW`~pfZ%ikr61WEJ&mXri+ns`56aMZ z6m%WUMEdn+#QpT}@W&)M%@u4(d)NJFp+(ik2GSr1EasOSy?@?4fBwLM+ir7jyY1k? z^Oc#IL(}h~moKZp!`rVBEQ7XRMMb$rPmKOoG5Y_4?oc5J8_HjHsTXz4VXUR{*gd&u zT;vJB-a-;g)hs3c-2NR@i^*uXYS@2xY_+4cW{!pHf7>rV1HB958L>Ee~jd%gA@EcFfI;x2YPS`;_f(Wb$CB; zAlcMkwTflE@#L~rtZZ|q#3x2kk~uOXJ1Ck|T$H$EP0s<Yv*eVATQ9b?(&9eiy&=j|yRNjjRC z)`Pg=_aKp|!B<$c(yPvc4M&USGfv=x5q#X1wgk@0#|oktT@+O#ISI%tD56~M$yydk zhJ|1AOtsQFX5YHp4Qb})Uw$%r!77(C831L$YR?v2pq`=24_+9}oKFz0VdA_yvrFSQ z_u>~~x33_5rCfG~k)0!DgW;oX>#1p7gcN3DGNM-r;NkLb5Ge0N+ppzdj z#j?V3Qz65NRCh{d_=Pc=xg>gb`(8n7!gj7Im4=xbR)^Ylbwyfuos-LmK9;M#RM44;PlapPg|dSTYu>RW1Pe4}9?3k8 zCC@8Wc$bRg{*2!6Rwy!5zbi0~7@kd%Z@m%q1jmnWx#gk!p?o%5IbBMpb1yr8iN%+e4qT?e*B^>guq)q#pG1GK!__ddj?d z0O2v<*lI`%M3^oTak-?#Qxx;pFdGs~Ay&fJjzAq_S_A0-W0ha)xSdV|*|GIbdTQ#^ zBO}i6eU^3K@S{^WkavIt0(3BwrFKElz_JF)6#X%N68PQd8dcMePE1sOTXD3`S!A_1 z`}YfnCV34p9ArD1^{i#IMqWts#eJvxm4T^agSlmGlINtj;&XWuU1V8*SL#DeifOJ_ zQG=DhZ7GL;S4#A3GO9lqp9py9gQb`gU0&tpV<{`GC!It}N}I^cM(wOv?28GG6^jsl zq4F;Vq8+4-!L^Ks8Pl*#f}sYuxt+i zx{E!7qbA9#HZw?$w3Ly_(AOQ58y;%c>1)?%oo5if=KNOpSah9 z4~LI(9bAgsXFos&3z1y_pSQ=7pd;DS;h2Y> zSV-eR-yu}@{G(aFor8xQKyBW7$twN^`48@3+sL-UjAk~EN?OOA?7cWP#MW$Dk!0Kr@g-c~a(d=Y*`xE5<~ zVHduC!_&VkFPNd?-;IsSmz~>g+uWSxCMGt=9;26sq4dgYGJGaB-s(UjtAih4PWS$g zfHwR-lFG|UG3|r5i|grjd+N!_c02usB-CK_)mlldTG<6{piK#|28go-U0~=;5s%zS zpQdBYg5p8p4qHW^y}sazO~Z)T)p&)Q^lj`nr1<1y3T{BeK^aagI#?WVPEjHXe{6nG z8SFX-4$O##BU$EtKt?B=a790H!+U?(74^p5sj1=NZWqwPgM;JgY<4JrkY1ipQG~Oq zAi>)L^b&Dbdcy6bfRX$d<&-vq!lN)atA7bnw@Lj9iDd5yW^1xXg7wVZ-n+G6CbV~U zC$zrIX1cDV?Rx*DJ3YO<4UEIvZ{OaYj80DOS zkqwZNj&SmX6z zu(@M zd$HbMY*xWdNNC`0SWTXQ9&md)pDqxQ@h=a)txA!BYYUI4QmaZ8e)lxPBZN>gKth|iF$?k-+Hee6w- z&xkR_9zr}d#0;GRU;sye0g`4)*8v7p&jB#tGccm06aWJxs*}5y=}A|yfpr#ve8s^B$-47F6GKrRjU~_LCVJKy4)vvT&6#s zyUL@H?DmW(`dm)rMP;y+H43dI!73C~<0Rq)Ao$xclz-k+7Z-S*X(T^$F|L(pL8il< zM@^JW*aLA{Ohci?V_+G+JCtT1=K$qPm>DXr2&l?3;+mD8w^Ta#BN%4quvI zjJZ%!;5rBHqKpiqm?Xv8FBOx>LPdH#9G#T(WLJFGaG+bs=;(#yz|%MkhJ(gDTFTbY zM0v0PJ%I|F0hEnm19TgwRPVQ7vfqbuL1!I=iOsS}Fac~nsz;&B8XVMR?Z zLU$l{X%R^PP}Kv6D3Lw8>rM_7qTRSHIfX*PXgqSyJq|q#>oI7kE;&4#8~HX zn<6(Ly?9{?rp+8MM*6rYxes*fa-Ijy+mH(AY|PoX;Vn$nOtJ)xP!`h>z$OQ?CtYP? z&p+&LBo<+!l4`)diZtpO;9#d^PlLMFxVvh#+mW$8y&zRBF@;!prV_3Ed!!B^A5Bjn z8Ap)uk^L-lmWVoY1E>Y0xZQ_6AjOqR;;gxLXptH5LynNt*bM=IPiJ(9iS%df>;wWGFZ-2{<3~g!8H@mZ`jJf=b zBmMN5GvL(Khjl0BIj4ZRTNz`Im!{41gmo%v#wsPrj$Uxx31^z43id0Ef&LFxu#_Fg8gyfwoOedl~WE4@CaxDC;#!)IIm4>rMjz#V4fqKn4E>(_ec+ zEZjdx5=JddT^eKV=WC+ie~iu`6}xK}TaIZ&R@(BdExpSge*pi)do}I7i3i$<(Vd9{ z3fSAGc!!Jqz6Y$EAYRQdql07#lO(;UJp*Iz#SCD0vE|zJhdg787Z`|vFGl0X4c(Dy zf+bbX@P`gX4j(?c&qlS)Ro?k*Cu&9V?{<}B+vJdMW`N;!jG3H{ArKxFDb-n)QWvC0 zup!V<362YPhk+l`xyCB&RUJr6At6q(_F1%+LMe=or@X`-gj$!pSk#ms=ajpVC4sqBY}YtBGCT_A9zqgveQQptcAF1 zNHc6-LP{Ya;`2yqrgYAp=GGhBSDO&3vbbKN@t#ebu2~j~tI~u}Co{7+V+i;CsZo5!`G}w0h*K93n8$!mB z>jp40^k_+R^s=GF3s$9!fXkDr{uqzA3AxpuVN8h_$ZqTeY-DVQC6N@`-A1CS-G5^& zYlk@oTie@ade=vRTi?E`fAnGdn4R3%@P_Syfxf=sVf6UvbcQ`@=&(*NR1Pj7ELqGU zjE%xhp4Y``(MTo@yyf8vDUdE418!ZwwvC@G&SK-FeFfRkma`3JOB!S>VosF@2b_U{ z$tR}<;5JIx$u|vf3ew0?Vc{SvQOm}fBDc1NpnyrPiT9z^Dhvt>nWwPDp#&r+jX|K6 zNEbY;9W^Lq9u62#V|YOWnl)S7E{Y?G)%`F0!Zt?0HR*H@87_mo32L)=A)i)VO83j)rG%EUuA~O}seEF6Ko!bC(j@3i^67<2w zp-8_-ksEh3oI2cS;`10^}{vbsg}zP3ZYagH3S5!<|w`N3hvYC zUcvn&72HdV3%$Trq<6!8sDe1g(lvk1>Rf;0UWPAmb&xLYJb1zN2<$kA7%*_7vRQK0 zqah%?O0F!!Sx+xs9wtGdDIxxd1nO8ObTkw8W_Uk)LXP`at0nPf@ra>=NZYqL=(okPGO3O^#`8P9W)fiyVyzF zG4V0Y;9JIP>1!&)gOp%CxN9qQpTPb;4*%WBjyrL}uXJ1`Ci9(cLMwEUKAVuER)UL~ zz7Rv7(WG7&wLjKHI{o;H>O!+FHlt2yekTpx5P;d(5G=Um?8a3%@*c)cmQl43`8GF2Z6@Z>h z=9M@KA0=7Qt4L?y<5sl7!dkkP9df#gP$F#=MOyHW9z(C;KS{=2Fbc_$(QsybRz+k- z92k{YvU#!&H=)^Yv{ZzoHNUNOun{CfegN~!VLp6;XNsX>?|MjwK?YgKAee-5_We+8e8v%^Rk?o&u;KFG=#Xo<;q3v+YJ z4H_F^p@f754GfIMZ-m^11|8_eATs<7$)KM|^82O~#~zz#<`av<7yuNXjv@>xHjPyG zZ97t_aGStGqQV7VMR}Z(0vNj`M4X70Pikis82s`jTSE5o8-ggC1F?KD;c-bV30!kM z-)xVhO>cNO1>?A8>8KkYqJFbFH^3o{!c#?g(9Z}tKf>n|auWRV9Z0_YF#`^zLJm4I zT}+07fHL`tiIAs&dHWgS7zF=c36^I!Z6pv}R?V7_Nq89%jWXnH(iFeu0YDw?;}pPz zkDw2>WO6b%J#xQLJqGH~nM?4-U)BfdTt$7!S0E51x)`ZRV{VY933UqDr4MYrn_iy zG73L`e`bSTUKY{6ZjjS8Igvn1LvD~yavbLYbq7>MP<{WF1r37=3LIkGh>p=5m+c`(kwges}>Hv06;nhMf5P}k%j5L%5Mk5|EBuTIa**!D<; zuW($IUjuqTnaf#-BMK-B;Ib^9XhA*x5t4L&!QdM44;pFjeR>XnnuibHemflMM~=)z z=I0SyqnBGjc=8O;K9GYx1JnpbDUmkGK?A>l)-G@SqBp!=;^a;{H{N*Q06h3N-@LVT z8hMV}(@)dO%P2Dxfl^LeJ}%~xxyE9w|3jHr7?1BwqTS42gM{T%% zm}UUT)ajt2q}d!yd0yS0qnCWg8XRo3(6MT0sMYEKth?ne(#u(%H^d3RdKmOZ>Nm|{ zGW-@%#691_qe0F)3WDu6mf4-(Zb}xB)lcY(yN1Q?UefqU!=y>=oeO=wg7h#no5>%f zm!erUQ9F@BMk8L75wnU&u#6w+MS1b~2(o*|@f>+b*B_$ZRxpqXvkx1_!~H-e-NF|T z#R@dXe+2ahwL^go6eRp#Lwl31iLyfg_(U1Nkzip+!wjJ4nBri))G* z_Y4PZR%{m~vQWI=17xuepfVDLbF%Sflpz8-UVpA3$_lSn)sa|#mTv)cXADO}Syc_k zG1OJ1PANoCToV=cdyue0@pg`{dy-PGh?RH|z19n&rSn`(C>6v4&$UFxK*)LfGDKAw zSu9(`q0gqvnQfLmUE4&3@yN(L;HG4jm`z?52kk@17+OMWj7^}V-2~A7J&Z9VPYe_k zoS<2R^<_H;GB}iM+C;eEQ!K7Q#vpx$9D^$7Gp&gJoxZ0UkYW)pu;T|U(LgPqs^qg& z=8<1DQE_HcfMdd^$2rG~d3@KTHaAzn(WsV2HgZdSRx+lEB1%3~IOA1$PEZd1SsF}l z4Rs$10O~&QJBZFjl}k(XB~^hC@W`{6F*TwNwKrfv(fVU%v^`;n1bbC+TCyX}ENNy9 z$e5*g&7F@gO>(wgdZ8H4I>jUawF|r@Opc-~xa=g0xIFn7?*0WYuN9Fm-W-tiEG#TP zmPUYK1f_UNnq9t>EEcT-ZwnJ$5tp|BXc1ue6WnE*?G9-NyGqR%aNS{o3u6=ScLxMM z1{DP%buryY<;86)NzU*8_AM~T{cr*k{0+q< zu)Ms0L&1!cFisJE*VAMM3!1QcMm7Pc?x8T-&Rzk~g0gkVu!r@mpGc`qLjfL>2G6qv z1P~iWC=Nc52=PS*09i6VTM3ka275sD6u-z8YkBNJp(<uPgk zW-Vo(YGZgNq~d$gt5dNLrY&5`L>Wssp6~s8F)?NX}gt2+y>q^G*_^EO4<%0FjS>&;q+1&3{`86 z&;h~8sFP?2Ly!0rWYt^~iSDU>#66#}6G)z83*Whj zil$?;u}`e7UM6@A+mAbTC7lN0B@#FBr3CD5QwL6&ix${+jEm`fE(s7oB$sh$A~ z8OS7}013G}{s30C7dquJxdwBMlJbMV?9X@5oG*%&lxvDW2Gx;}6-#9}MReAEREY3J zo0Ud>A;G1=-vp%o`GA;+nyNQyMT zC~o5o8r361iYsvg4;)VbpJ8eoaHfMJ>*}DyIoiKU@=(XXw_Uqpp^aO`PD1J8E4a}k z?9c=^V*My-ia+;8^~PfZxKq4y-(i4U={q>` z@)l5GZ-v;X$?OXc{BA&$$dawlq7<*)Tvz%WBi)Kr>s&)Ga>a7AGm`_zr|~*)qMb~a ztKpuljFBkfy{Q=jb-kdQ-GiG6VcHk1(9gaXHe*gHoi0l?SMzI;VmV&Xv+-;p(irtM zTY0^N`;73xeN30#CyU$g%*uLOh&@m4`RsLpmLRj5bnV^6+CjGLB^TT)HzxT0x+5v3 zifA?m%raBE6VV_G{UJ+w0r{0Vr)E@Se&!n%JZmPOMBi-yEJ&g|P8mq}0NR8uMiP{C#tm00FR!mV63m-C zTzL^PoUM|QNUB3aqoePMTT|Aw{2ush%ud!!wQ}Q<2Y|twJ=~UZbIV+H_`jlcqRqvdC?6V1T22-3?IKT2=Rb?-5m%#B zTc&OVhu+`eX}f2R5!3TvoVZOU+ree+B06;Xeq4xJ0=vbM)nPZ;zu!8k+qgmGhIQQF zlUTnbW0zQ~Jqh5w!iEc)nHM-$iEE+|)oNx<`0lX(wyWVr{+$GY0^MwwmeyEl9S z0K0@6wo`hEr4D>&l?&5Adx|GG4xzJx@a4~s(4RlIc&w|AY%D@h5^~(fF%p6t!3Bap zYxtxEJmrj-gDK>USmM>Yw+FS((p0R!CRFfO7l45CJpL+<^bgh$0j?FO+4!rm$|G>U zdP*PEcH8i;y1v|AxEF2|Te}Y{Yhwo=1w#$+Oz4I^C7t*TX&SV&7(OjEo;|CgXgjDZ zug_U>|Jo*!I(-GPiP6!?$uIWJCQRRo8>8leqCV+Z8}4a1ll#P5vFP1IGpBDy7K%U`E50+wXi0jM5F9H~r5Jy+h zx#KrcBtf7ZVJ`?8_FhGpbmO;4P{bu|6J1&ox@Bjz0@-e2wDSrYa-x%!)hhMrHBi1) z0b)$&s`cz4c&A1UiS8jMWPJBV#1s$gC$~xDQC4 zW*%!~$;ckC64|@}lq6skCgDzRATK2@^#dNy)DI$?Jttbp23PXA65p-JmN+EY+)?1L zM}&n;kuRrXQE0gYr+*X3;giVU3-CO{atJnU@~j8Mu*=?nI;gpeI9bX(9&>gQGC%qC zDoh7$kqn3vs(NCghCOQGvj2R;r!eENc!*B!wc%7|6*e~B`(CZ{)f0l2PUxvrvY{V# zvjg!t=~K=C6e~-CTTg~v-P$;1IxgvzCYb*Z0X}50f`=aM27$M67{Ih(=BM1Dp@V2}pIJ8qFbngf0Y+{XgFDDnxo2TOG`l@M!`aF&G{3O)^1<5dQY} zzHmmdQ+6tmmk;JsHEvW+#*(^Md)K>O1vRON99SafnEeev*U7^*-%N~*+}0 z?i6H4hHFZ@9r)0Dw<3Nq!EW`VvuDx2Sy#*mL_v+$k}N@i_!;`MqZrbU za4#kof*}R86)e!{8Fz6hK2S#gcBie3$y2{RhTPXi+iF+(^LBHJ-MDtc-{NqxNijZ6 zr>Og!TX&yv>tFxKgYSOl{rI(GZ0=uy@kZmjMjbu(%*^VlySzO06=%{pcFdV%ziwb$ zud-hc{c2b#nAt+(ue50_*V@{M{Hm#pIj2uMBRo;hDQx!-(Y|eOFHE-MY!&ElD;J-d zTw0RMa@=wFm6hqQI9+E^SAaz<%{g5hg8f*&zq{ctK$i^eB1kLRg(`q6t8((Gr?ls9 z7tb5{GT+eU%Ubm>9OM9x6|mVxZV(sQ0GW_eMPf<;lUB5tIG#VPJmf(~y5oxG>c!Y? zhk11{p~Mn#f2`>xCYP?pJtt`!`JAzg6x^tm=NpJC3HA^sdk`a~BvMwiN#QLVRjN3- zW?_#%8qs1=JBAGA;l4hkVpU%{yV{P=j$D~>PMmONaGgyo8ns#``!_ru6U+@3VBnBp z%C3ub4-omgiEk>@ab1j*-JFsiMBL2b+=aaDb6Gd%{@#HM8beKs2R8UaJje5{h`J=@ zC}#3vPdZB&(}{yi<=KpD{(4W3M%wH+!7R zD|b4LOi3t{<+{yy#!GD18T(G`J)lgzd7$#^x1EUSUOJY=0bOj_tuLUH;+Y~)Sd{e8 zzLrrR$GC`FeCLKQF(HGsV0O0Hb)dpo`S0qX8tag`454w?E`fFI#p!AQttc|ff1fAnz=Tg2b>8~+MpJPM}xLl`nOcNE9```E%*AR>a}ex1#J z>;3mTzkbxV4*GV+a-{Aw@0lYnJAKYbLMkYQs$$DAHx(R7;y(md%M{5TcMmpX{IArZ zg@@aTX=E29R=;7Z(VBDla<{Gb56?z0lml#ffl-Z3=$X_zdCG**e2tKiYK?$2s1Kr_irwsaZx!O0$90y9t!?{jjPDYe&Qt5?e1D zC|JS9{f@Km_`A#1Y9#N)eLX#F4kYLK-;Fm%9cR>8@gX?W=}ytiR7hz!fXfo$q?p?g z$^s@$DsJcm47LUEVi2{7|ELbm-L1PFHIYsi(aN!pLvcl9S+eEhS8Xkpvx{EUPS{9Z zu$*+QsMV_nqxU8}a@!w=a0yUb$zj$Tc_0reSdzEe(xuV)d3SRYolU%}TneGE%}r;# zpyb&uyL%ZwAuS+KU~ig?xGSsCM$O38jA3nto4gzKaFX?L_(hy@zfr3UCe7^1 zs$l`*7lR^k^J56yP~7Y#NV+H)CYPR>iLR_g>s2#b)kc*GVQw)x`R8`ak+XHHn6GB- z{Gijq5Vs&QNOWmEKEPNov7j)8;nYW_RM#&VgXPt`GUz^;{fgs1eMhEfR^~dXjFC^_ zBF8|*e+N5!}+$`avSc9JJV>)v|7J)x4yWj-PLSvPC)sI zBJxX{_}Wjef}<-26i8f;E_Sj4hS_XFY}JG^LLtp|WujXBdvh>+TF zAHw1J5vV&BdcVQ?K;3S4rx7L{9+w@S%Eh<6P5;;+pwlUfLobK#9VvCwzl}iJd>h#;(!{OIpks61V5mH zCo&uj6Cu(+b*1Jzt%);u*d?%f& zWK2^nqqKof0?!V&IE+F44ffgs?`8@_PpRL5_rl`Q0M9Q-lx=>IxHi4+cdz(4S=6t7MqrlKk=tjCYo3^8EerlW>5n{lZPl=K`9=GBi z{6~2#);a9Qcc3K_04`~?yJ`aF1BH|=)OT`-49d5-wSD{CRO+Qqr9q|gQ=j{j&Q10h zuCdO9>8fF7^VF9rnMua!u2y}3dAS@kKZ8z$xPF=~npK{WUlTQDBrSg^% z;5Y8ORMyMKYrmuEzx(coz^l$u#}kU*k5 zgtj#W)sgOk=V2ZdR(?DqHyyAB=WYv4>aFK(4~iI)DaEXkwC>au08dDV`A%N0mdh=v zUB>d918M#>xaKbM)6Pk}ere84xf4&iwc1qWEWP|zqw(yaX0v$}%K~6VotlG6OsnQsJUdf%YDI-Y;|D4LsleB)l)AoBNw@H>EFh(hIzPdG`6z>>gNPoD`N)^ zVvMB4P&LH0R36-bh$h5YJvHwxF1qMJ-^flspo)&oXC2qdd=5=kp1iG_vr4nGXpfOw zt@cU%#Hml*@uF^Id`a{H7BcOlq@FNE#nXR z092ag%he7v99u(^^k{}JYATh*AAz<*b?hv>9PA)1@pZ~FkpswZetz{RT&w_zGF_=V zi4Lq|t5d&ME+b{MZ7C@!i!^>Y?j)K{0fXNJiT?vQy3P_Cg0;XMm>x+~LieEyKVR%9 z2eU`-R*VI)cJYDk(nsb8-f1I6r;Z8+4PHJwzzjeSI1KYUP^WrHTC}0;(Dzv0=?{L; zz4K0cc^P$uU-?bvasQ|_gkydNg6`WGNx&{iy8Hyg>|Np8S)a>Dx48Mkhq9YX7n%~AGm{ETyNiN=v5~Tv2RxlU;qfrg_^@-~NPpT&w@*muu;+NLulVj6Cv9d) zy*LJ1BQhpoNpZ&A;}#w@n%5Yf#9D&ee#vvo`DW7{h}4u!v`|PM=Lc|RY{|IFSDm)K zy4shv-GbeA=GyIBfd~N1fjImq8hoE`;Ff8Sb)DTRaQF6XmP&9mA%=W5$$;&CUU8MF zj}1lAI*_gXC?NlQNoXn#sx$4!&}>99-^dC1vUA0cB%<=&i$f`Ndba0%zLAj&`CL^h z7r}Us;I2P{DnrT-Z6IM!>>_asdu~sSisMCyo!xidAHetc0zuq!(hwb$5g2#I9Sn;=YS(%^C(Gx-q~8S ze|gg3SBqz*!}(bO$zchxHiDWxy`W9!U1g+lSw3BSgO~LwoQoEpfYaABj;jgTGjzbP z>H50onk3o}gJ`}7;nLIV_o#TZg*{q7yh2*-(^D((KT7 z7M?mebRF-Jg;+1DTV~udoB_M&8;)Y5J&yLoO#|>wEWxjsMawocSMk);ImdbRb7+R+ z#*|Ok{w;MYw~5Y_hO6hsxd(8&HSmeQ4#}?+ELJZn1{~Kw^Jm$2y)*ToE1nkxzZkE| zG47bQ3~Np=qRS6*$>NE(WZ_TZ^z@C18@2NZqcD+}a6oz&u_WICBM1Txj1hzma39AM zRxWUPnVCD`|K~faTvv8ao;=Ch*|Jd^Hp^cG$E94Xn1%jfz_a=MyZ5*62TI@QDt;ZG35IOv)WS`gKz* z*}1K)oQ>3{4>zzFTOdnHc~Wr-sv(8}bC0o|#i78~?j+d5PZ2U4@&Y{WqJeb8O-co? z?u@H#bl4H1c3kghU7+;n25Nak?dqk;s+H?YSb!qfI@q>q3mj7FQ01UKCl@W%^9JQ4 zg>^Tu9b`ZfAQr18#Om!h8=%Tm~4e?jUi7n;fpz_oL- zZYTAHjHR@fmfXonsJ}DZYjN2`Cn*3LYP_W(#RkCaz|*=IcXMz|SUKu1g|Tem60eh+Ui;JYD!FsLcyNz z9@A<*B3^6Vd7F~Bma%&T@mfcc=TCF-1~y0oMFzVb-bRlRfZeFxT@S%5Ooj?5X@dorS&fvAeU1AM!YUe=Zv@$Tz!9L1vwR&=*X24^j@}()}O& zoY*;Kk3vV;!qv#}PF3zi1A!4i`;SaXFbC$!4)`^5(12Ss#_zF(EXpjF$7C;xk~Xc7 zjk?^h)`<^pR1MR8l>=O?Q*vems@ceB#_B5SDZ5T7qY*mx8V>Y_VQVw}82tlrU(BM6 zls75~r^TD799YYvATf!gOpcdqDU%s|(QTQ6r7i-%nTwB#u2QCSf_FjQk-9*DW#}#b>5PEFh#~AXx4xW|leaB{{m=L|(gT`q%b8tHw_0<}Tj}KubTS1j zrfuEj0n$oRfg|{sMQP|0RPGjoBIbq0UBH5NcBF@u9^CaAcmSd+ISlMWR>fW^=V?dTxv(C>o%lk6u}O~DadoDL*zT)~N8 zKCeC9&_H2+eE+CnmdX%!X#_DZ zk{5M9Zba}j>Cj<7_h9N8Rwt02*cwz$3mCz5QBe@^{~^z;j%O-zGvlP{YBfEvGR80F za`#G&sG_Ihxp*wnk(==Wt@U+aq*>^ypr?v?pK=E+D{5b{GJP%_`C=k7l+Y=TMuCtY zf~Nb}LX+JC|1HiZh{q2|W;_^HBYYE>gg^CxDO=;>NMq3bL8CuX&d1AwP-rNfN-R@R zYk4t|(^~oY7g1R5#!x`$TSLZ3YCN8ZYq^Z+pzVFsN2Bv3Kps)Q2I@J8b)h&Y2jxQUOk@-c0^DD6i=OBU6m{@TYLmY+1r6986y}Uq||h zrsQfhXGG|TgLO#JelMDc3C*PA`pIFWP@_2=hO!^(4f$4s)J+_uP;-YT1ha=zht5y1 zR{^CU-3ogK4)X~5uO{@;@@MI*T`&(B{IW|PC)1W+>!wFPFin-O*@ilX(rPccmP*3s|h&WQh737*U z7*KhbdqCIksV}>*Ss2$fy7)&NzSWjH&1_2>lW(ld%|()K%;)FlD=~tCaxb{Rh49|s z#4I{h$g{v~3@OcX<-C-~VjY7}`W~2AU<8LQ2P_sbuyB{~aYxg<>0R$qu3T}p zx7R&hYk%UjHia5fPX4Ont=Pp06r#Yq#@Xg99Ta&0^g!MKLgm153!x`=_!gTl(I^Sn z33_5bCK^`()-RxF@|LOn8`X@SFS~7}U!Pkop^KelaRAUVt!??sE{av8xP~ki1cOj5 zXWAz696&6|h^gmz54|!mJ+fVXc*FmGtz)dgvY}{-y~9CMJ-nn@?%HRqci{2ZV~FGa zv90E9B6@wn&fr=%LAZPe$yx-^M*t^;$Io=jpK`Kr3x;yYL?wwGAJ3cKlgh=%QZU~6 zK8VPLJmBN+LOs3`ikyo@i(JMbyTFF#*?UDuvf996NL3jQF!Zy4}zYkvI9#M zwDt5M_j{B>gyTO!-KTTF$(AFstEJ%*SJF@djH{C66h{`&uB1gd^lmEa8EGBG+_>^b z;1&2DqS`+rzWm&A)Gd}zS2CH>m#k^zR8m?cj{QNLukSI8D)O@LVj(h$34&WRZ#rJI z!pEOG8eg-2qsbSl@!_%g$>Ddr1Etw7Sx(2c)3&pao}+~(NoO$?C&l=AkB5=c)HCUs zzDaAqHW2!65JDI~oPlo|WipXOA`S>J#QvXCGUa)2>Aaq7{VxE3s*9F9?ZLM%42X`@ z=~z;oWz0nF60^=h%n)c#@=HQbVDLpdfxoDx51)IK0Px!y7JM5 zZg=X%MLRo#*0UtTEsrgb45Nq~qttPToD;p-5b^b70bPsMiK$dfE}d9%kmxey_*Jy* zZA>j?8jb0?o%MAu^QQ#3yu5$43f_}mc2`mVG!3kigpLt!Gj9Yr;dj7*$94ojc+h&A zala*IoXN?x&u>>z_$9pk?MRXNI)|KSyI|Xew30rwVwad*CnCJ_1{l1Hs}mbzr<5IN zqPswl2CAn|UY>xb1|-QUd+h0<~9AQg!ro>jMG9)DFj;WmsKq%py!T zF|oGhE-iJtvwsQLEL&A=uAtO9TNS z?9kK>9fb67colxHnVI!l>mhz=lvDP9!KsPM97y`8yG zZ-4T+weV?A0$DtMWy3_EYm$&kB^(ujcG$4 zw|pc8?9Zo+WER=8RdhqEW(T#28-d&iJhN}eW!Lag9A_ob_dqC`$RsFwMy7m1(@u<& zCb5m}_%Xa^K12^m(;oS}8DZC%+-T=6~;Zj&8)z_uSH4$(1sCN{Edd*o-+~^+p1nd$UGbj*V`w z#ek*<9au_e*bXr^w6z&Gu`B4Gn?yH^v=keCu>e#O6Tpgs-9HeW z)7K}8k_nNj=K&EHDHxeZu>`;}D65X3m9zQ0o6V*_htwn9Gtm981=1_wowpo5L0@ca z*!@`I15hLg&ps&RAw>J2{)Ubc($~W+rTwHme4dXjIS)JQ!t#xm-vT6&!-roNm5S|D zwV?@n@`;8AU@CG=YB;s8hk#BqG&M$*G&3}d!7Q;!^#=SXyDpht>mZsLV@$Xm`Cl71 zkJ{+UFl|ARP8aoLRd=G*p_v~W+rNKwte;imJ(AuqjGJKG+JmuTN^ahxz`L9>%MMUglywLW!&%ZchH_)o^C0f?g4;_n=VjTM*zZtJFdcb_yM$uATk$(mU=mNY}zz+x)rGvW325^ zF4`S?z*hQXRLl(ld3ktv>RH1q&Uf(mOFqrY<wRK3tkA)*jn zuM(B?uW6REO|^(X>&)vN18aB)%!Je*QX*18SOA0XEgbDpAU|=k_9|tx zdl$wL;$QyQ;y7exR?VJ6r2O=lJ3hYhiG5=L?m`xzooFjAaBR(#kwb!lO?o%xK_PUS zM;dG_&;sd(*T_nm3i-+yk?-Zv_y?vS{-r#=JOb=1c**^Wn)L_v4f4~-2R~Ecj6roI zJ)qRQKC>}gG5CJvHH(L91eiU^Y!0oaB*0}x?ITgxlcs}uA^{dMIBh?5?x9GS0w>X{ zJ5-FJ^>W84G!^jKy{|;UwBDB=IjG#GpgB^Wm(=8xGUBD&ex;7+gr$}M9C;j%SC1Og z<(M0b#Jk;$;Wx_;f_W+o!o-k;NH7QV2J+NUwF6OgC9E!{?7UqT5B-=Z8mSgkXver- z<63Z}E4H|xj4ZiBTGJVrQ^zG=E9e!zHOsf-vS5MP0RTxx49CnQwW}PQIQxdMIhB10 z+19)*z;=uRfFmU~(aBZtpyz}897N!TRL(s@JUCu7G)6Hy6i7Ilbh;-h=VqMg>Gh(T znL-ud`ubEsD{jsVSJY|hW8}a&kg(fryrgVrp0XcF8~*`BL5(?4d&&x%`vkKm!Rvxf z&RBsUM7`tJtT)aRj{5%NmNkMVmTfAVNvrunKm4~ti`@-WrJ1S?M@{9=P(Oq~70Ps_ zyk7wZiCj@l6Evl$oLPaTo(4t`->36sjb0fDuKvC*2_!2l}heM43ue z`St;2Gjmov<}byvde+hNPUj@Bqms$vzW>UGZ7m{?e{#IgA3!1deBYh)@(OzQnP&gVE>@X5 z`MB;jtgfJkL5TiJ=bWC{J?tDkF&(z3SY&+LyW5cx3M|m25#f?*UTGL3Gos6HpU{ur z2}|owAtr&thsSx>@RD{Wmb4SwP6O8YI*vK1qe1Mw39(5qrzASrQKu!OTVQfKX;*u= zW}unVxdNZ7eA?mj9i`GP_Q|vQ$&=?CNpld%7)oj=udaY{@mR5}*Llta-bd0m%tX@C z&Y+(>nq8f|fVf5!agARs8Usf20JfD_(D!$iJMa{-rKKCvs1V2sM|ItqXG?U;0}nWd z@MwMJ<`{p`NTRSC`KGB^Wi;ud!lrB`d~_Uazp~r{(ofywxtSymy;1A{fxetE3>W?-*n0*aP@xNg)r3lL!>ItlW>F za^#)v#fulZ&E`M`rRSO^qRAdmx<*Is?uU!xZL~^0-7Q;hG7ykAL|*|sEqy|Zi*TNl z041<{_w=frMJKAehBweni5BtNI64KN`N~Jzx|GsgwPBA;W&pY%)$F3v9kN^czed+> zE23&i-bU8dKoTrLO0KO<>`yyY11I1FB4ytLFW3v{5lnAKZBB#$5KOxcZhbu;zWDg~ zz7+Zt2?HjtL!!rz#oB}JGOB7Ss?fOUEjAh3u&Di6vmxjSBXG40Ii|c~tG? zuN@vT2J}8SFPxZ@O6%!TJgM^IaQK`CZBa%D@n;e@VPhM12h6`0+@-bOs|J1uMHvfE zj^xj=hi7fi&T1>B9! z6o@#WD}07pL&6=Qc-n}aZP+ZJ!avzpN8=x)M{{tMyFk;rYzvbMfcVgX<2vIPizzLp zsI@-?0DzTRZzH0uFV5A~#!O*|OQ5xG?g*fUoHUPCGWfSx11oV5R8LuALa5mXkcLtjadU`-0*YM5s@-mV=YE@upss{k8 z>DYMd19TZZF^2uvh3`Y{+*JU=Ts0yz5`pwi$1ZsG)(-zxIXyFDrBZ+^#Oo>eygzpv zccbD}xu(S5EVo{rgo=x2*8;bzdoe2}+Iie?88@V)U=>#7E|N`Ps0CX@C;wWX9Xsp6 zny|Uk{O^P(YDP%Nv-aJ0`w0oHKFLTUIBEwSc9F>Ru9-olF+8IR&S=%G9AyDYmbulr ztxdqsX`I26Y%ni_Qg#~yV6k_09`{7H7Vef{*b+U3=szT~M%;KffsIeZLQno3&z-{ zq-Kqbo#T^6{;SK>pt<@-BSjmLno}Mk1?UyundE8NhFw8z$b$6oUX~(m(z6-& z@^u;lJwNP;3y-%wB$c1eeh?n^jo;jwm{@q7*ETni3ZV4?$4iIz%hIrqhp~U;9wL51 zJ(*aBS137YhQT!%z&PFVr{!KerUQdT>mTnt(;TVGQ+EAQ%Ch6Y35&Y>Irq|~ORuNX zJOT`lJmT1tr2;I#WX@9Hl;x5qsX65#+?&#@JS(_0(d z!V~@6zPcuot$gyLap@b>eSRyS0_ zGIHYpr}&g@DFXuvoKVO+wFF_F^pN9V6IAn_VhKpypxqRve9Zxcmb$AL>?{Fk498dD zJz83c?n5@OCEVrAMwc(3>w6I?8?{;yDWir;O(&iEi9Jad9(dYm@;*Yl#UfrDIR|z{ z&TWz+fvhSpmbJ$P5=@?Gime9i0G@$bHjZI6K8Ow?+n-w<8$)wzJcpgb_K+hih{a@e zKp(uS^=p`YKN=Ss8#C?x`BZdDG&w(3(96t}k(uXh^oGej9gA^$9M3&8z|L?WEJA-A zW0)8l#?^b$`0z(yD!a0SzB^t;Q3O__IIuP4x)INf`*Ae-b*47@i^b|h4Uc>snI`Ay zr3MIh;4Z5M5dX&wokG&UU^K>M0Ai#4T0`B(UR9MT-409spwuza8OY4*S+2BS@K}M6 zg0W*h7$^;ohKt|6KP}#znMRKhU=RZ&x8^$Bc5*&)W@2Tf>^$K#Ha7CEZa%;{O3juM zFsKo1a?_k>HLR$GHlSXk=@%>)`Ii{UDeU9_gFPh8dgmr|OG?1OsK9lhXpw)1Ef%v0 zf^mJqZs+rr?({U)RqxOADYJdbcuKr=bS4cX7q3|CyLtrnfOXu+Au^KHB*21cNd+$X zLvaDkE?_EP8&81!{T-qa27g{{x8C3I4wl31kH%zUmo4n%*k|N40sAbDR^%z8^mQ~+8 zIs)}?I8gt{`$J8f>p&*o2jdV!;{{$|u8*Dyy5n=Ey$qiiU@vQN*A2{$BYvbg{I!}& z!zl%KZoUI!M=?b=k_23h(BvEoXsnW=Og_Gvc4L0Rh^l~m%lShePwSa3=t(zT(`3#w z;wiqFl5{DNlx+?T#gGLt0OQK#Ezlp31l(L!FqJvx6CQ=5xb5HCR{D4`3-c3PlRW5u z$AW^rDyS49bV1|3J?l$!LF*liJBZu)%wZ6GbK*VkaeTiCn`+i^z_C(B>Lw3?6Hm+_ zoRS)|?3|H(d5w7lDl_-ofexiO)9~>i4M?+1#Hb|~p&D@2C2*9$wi-cJErHo#sY1Js zJQIvA6Cg?EVZXNT1vZ5=)-H0x5>Sx<;dxD7>iF0Fy4^WV7y63PGQe`s zx;mm3(7}syjASF~t~0cF+3+K|XhzYctEL$3j(5~n|4PIa4B52NW+aY?dMp}Oc~cbh zXs0H(N7E(CX;wK-i=)>|vCZl5=qS%B&wj2&WH13E+Co1&)@0<$VBp-K9?45r(dOY#u$8)f76{+GaX!qQ9tGA3i>UCfN#S zk`cKdVj}1nMaL`Tj{$F?6HRv!e26Xz=YY`=^|FI?W(xIytDjk$ioRn?MX=S%>y?Jn zr#14!&e^kn0VG|(lNy5~3Lr}XuA4OKXw#rTRzl^VYE{x>)1anaADy0w3LcAF28S4* zAo_801ll3DKwxAy2Ma;`m-z8^7X?j5q8dT;O{$7%A!DnNN&-<|`Qxt6nS6nFjWjwj zL~_~R@*I>j&$OlnRZA*OwPq$WqOQh1mb4>k8IH1Y|LNi4p(5B0t;%_7_DO-0!xBz@|L8h?eHf9&6>1yJR#G6iVWVo0ZW*qd{4&ygsYb} zvBwCnkM#j$W^!`lx3*?n6pmNzd#^n0-gMLX^Jw|r2m2g7z*KdiBX=%kwPg(+mUZYf zWbgF!{T7yG3}DP`S;DhNa$(0tqu+Vx}4BdLv~~tZUar$)Dbw2JVAtxSkH|K zgZVnrtR-kk0<4R#?b!60(CbR%MqE{Zxfs-|kqS2U489e<{NYfJ} zi5fJ>^8j%!6O{cHP6gra5wTqtP@|EO-b^`q^qG-E$hnC8XjG?W1xK3Ah=ZT+=QF3o zVZQUkB-5$KY)TXOQ#0-&o=6Fgc4h?`z>cZ5v?hhoR$+Tl%kee?%f5z5(T$irSSB$V zIwnGG1oy-N47aqWu8c24E5iON+~WW_ABeO7_6-gai$zjMj_{+oW}#5MC8nc^EH?tT zM9x4CiIOrzwA%b4TmXnlIof!FDj@O|SI!J0chVT-)pB1vYgdaC=1|?Rtp+l&nO#H7 z{V$<|=aAb-+Fa;K*~2_Y-=u{Dr(?V?Ts+Vl(~A!+BQ16-ECnLfs!4`ZGy!ZT|{uzk2q{lfLgytI#QjhSYw_a#sK__Fo`j;@CBoyM(Y{!s|Jt za7OdMbI!yD^!~@B%)}mj;#? zjE+7}u*D9)-4isJpx+6OTky&bAadA}U0yF~+O1sLvyp^hS{#K^wPaj(Ol~9=t2d-p zE8h|8+2|x^tSQ;H)R|D~v(mzh(tlJsc@-@{Ik^C74bLT?fZH?`L#+po-l;-fQ21CB z$>yq$E-q-VDVS0VjjD7xq9sv^z-u+CcccKGiUp#MbEIQV1}$dj)Q4n@DPoU$A}QDw z;t2t(*rBU%k|4qT73ebmKVxqmCD~cs306eBecxZ~`<|JR8Ic(oxi3|fS^Hj6l}aU5 zQIe{(840u@fi?^hATS7H76A>7v5nnrw=sA;wvPulizL8}s@&Z(=XlPUKW5GtZ)0r0 z?wNCDj^=m27ZI5yW4EK~MMQ4#zU6M;U4FMvXlXe|arID5l`3eZS=Kv_*pYcPYldcE zrYtlXCsepZNL`D}H8{UA1Gwf*C%tB_WQ;XWHlacMD6eiF&7)-ue8(Rqs&`{wsMYWQT=3xMa>_=Q&Y_Mjc$zvtf< zy*!GXV+2&rL7$NeZyV_JnwFs_t;r-^W2>b%bWk>>mD7TfKb4a0yG%*I3?mAlaSg`? zfj08CF}=21n_(@98NMPUmY++x4OkpUQ@-Qv6WfhV1E~SpXYy*dm zHfDoR;Rx{;g=SDU!sKE^y==1w(R1a)O30&5hC^ACg$7f>%!Rrx3n}PU&osPPQIt63 zP$iYT$620|gf@Uv-YC8PMWDgQ5^|}UR%@Nx<|^l^dmZFy=H;a6ARor>1Lmp?hBMG} z5v^~09q7r($DuqV$|QY*ska3?-j3<Scq>-rh2RNzYK^e5bpHT%D z9aO4Suo*BkJTHkbQDdUXgeolENi^WX_>VzZQ@Ckl0#WC(Y1C>Q8qLNhl405pV=-Y(w!lJ&+bodq zeawg)RMb&}NQ!hN7LtXJh@4Sjib0Mo{{h~PIDt(Q)59e3R}XkdQ2RKV6kKWuwJycv z9bZ7c?zGS^+g#}MdJ7BA%1Q}ov7fChw3TM-!b}0lc)F~iof&{X(3KIEt)*o}G-$-1 z<8K{&3siHIxdy3}wgSc+jMkYbhE|%<11ZYrYkZG}#6}&_IgOvaJ6d+84?VAUpe^Li9g^|7J@EOAwRU`bL;M_L=gYUZNw?npwbCC~200pXgD$o0ABdM!; zz=TF*4RHv>5)N?}#yBZOXv5KPMp`2>FGE^MTEmEvX0u^!oU zfm#$)w6xa_4fMg%@0`_x^tR2Hg%zbMr4c(vUSjI5GEe@2YSl@nk^OXCfgd|nso1@4 z8krK-9J0PObq3|Nil5Ne0^P0)a79U)J8y;wCyUO9$e&Asw#jApW2B~2z5yMvm(lQW zKh0`Lt!&Y;$Rq!IL;)~QvFr_^4*VxDL_zF>lxIi>Vp-{k;}H2JkF;ceDFbE6-?_!5b(Ao-=bPsl?szkI(Lf59Ge0%M>h#0-|&nDu63@cyi2--)ze41`t+YnTU=b!AZ*C5H-j%=@k8cIqP%4r z#u=IqN+F5w>xhI}q7HEiCO-@T`Ik7I6|pZPS2Fg5Y+bCmk6pp~6OLwM$2kExev^V4 znY>_kY$8qn#EPwEN|&E)c74Ohqa-k4D~bG+R!t5*nRdBAzSo&XKR?Zx1(IHKW5del zzaoH#;WkF{vgMd@Ga&+3!W5M18F=#I^7`4kZCMo!K|m!fwn>L*KZI@B@xSj>OR!Aj zo7b2I6=|txfIhya3nGhIHRl)6_U!1d9bANmbg}gqJf(>0;89Te^7&<9=`~4e%9aI8 zZbex-hXbQhk~2Em+H021{Mj^;Pek=h_`tC=!6+arpmu06aleyQ&GM^CptKG&+*M^p zt||*|d;1wxB~jqif@T$1)j>O11G}Hz^C`F|sVXsYPcmL%)HPh@U)kD1Kli)$@5l3N zGp1|c!k!*v_WqwTOa-#_hBhc<0{)I{Cz8w{+VQ6Lu?TF(aR4Xa>u7CuX!6{&>WByH zGGP4mPzfetsP}Y)u7#8k^i!&>25YL+J&Z@(aMF76YTc^+kq`G9BrvqIt6n6J`DIKnT2m+ghN9$niC8Q zHmi}`W@{cPNa&zJ<%7=Q!zWdfU!RN5^I=v1yuVhfDyev;gyNfp+|#Qv7xzpJu$xV| zW)p^%u1NJO-SH1UXK}Zb+u8~vbCd|DD;j&xw{#& zl+MKhyiAcY@} z)qFVo#Sk$ozL`ZGr=e)UNjou(z?6iuzHS}+(At5|?OXEDO#$nz>6yzS8XBR7BBi~- z0SsPAd9o1{5+*u@LvCYJD)uSIIVGSQ5RPstlKt_ScAX1S21suc<0YrG1WU?iMU1ah@msZIhG9H|K_{L147y*`C`!ROu4%bh zOUB?&VtJ#pBP}a6fZfD-jj;gMlxSdR1R~9NN9=-PA~qPVF@CLq#g1g$@D=;<7}ZU* z@Kmckk%HhAf!oTTPBc62=HiPh3*&H*FKN^O4W0iK#eQJXOITpnqNt%Ip0jXJ-nUQg z;m0+cEH@E3A(|j%eq+~w4*}#VT7S?jJERXsKb!%P0;JB!fy`zFB8TWfaErNyZW{dV zUJ{JrD_3nE5PF(`^g%Q>gB6GZAVsqT4j~6XEC6j=gSQ#YIy5bdzJ7QLkD?KFLg#>f zmr;GrzrpF2x7P~bd~s0>UaO&aN}VNu2_Z8(JEN3NaXOO5P^%Fv4frn1f}E8d75(Z$ z{PSJjFgeAL3>^o-qFcd<)sd*(Rj8u9A;2mimPJDHkuZ%Y&uFA$BF86-=SP~7C@P{V zNG9SLAG3?{otdf+BYjeDod4hl3706elP`SbE8)90lND}SD(IDD$vJlp=^sdN(n{6W zQ71up(BXYY2Z4A&g%jiHb#q#@Dk=xdCY zz5kqi@QZuJ-iJPf^bB)9>QYM+PdwqA!K3uq&)z`AhEdzp8$w1AC;KH4Q56e{a}%d9 z!pqQEraL9%u;Nv5xQ|pS-54fXGj$oskl{eaK`m_Qf6?+tHPnKPF>g5hwY}jyocEp{ zm{aYIjr@~Ox@XQ{Zwv4~wLVp~zK4=(PBhSUE1csE)kb?oUe*l5D?3`xa_R;ev4n72 zU_A)2;lIILBZq4lCfgD=@H7(^q$+M=E+R{&IAFPfgF>c3ATwwZJMRne=$Q7FZ-*y8 zzqOG@$8NdfbexUthtVW!ukKgm_aT7@Ry`bxvbM16{Win{z7McMMH%dR|9w0AGz_?y zbnkP|J@e6Q92SJGD7;zcZTu#Vzy@^FuJ?5j`V2=m=dq6JDo83Y!301}dpgypBUhD01gbpv#%n;IsL4ZaZTPxSaZ;AJOglz8<_+GuV}VEAD2|3$zU5UkEbZRW2$p@^>j;AHgheo7fe1q& z0^9-N?~*^*?2+9@xBRCj`3pXObe%ioJnwAt-KPIG_9#kjR!GlHNK;$SJ!fCM_@8ZQ*_5VhDjLCk>|^fz_j|3B0^HqFD-i;ZUUp8SK~0>q6HdDC1c{0?aFU6t zd<%ZkLGP)f9LB#C= zVh7@03ebVGBwdksOn*NXf?@&z%+M87ah{8)d1OBiX_z<)Yd}^Ac{m_a7>|);Vev)s z!A8a^wsfQjn6{&JVap^oe$!6O$qq766bY?UU8o)ix0I9y*r{Z|ePiOFmn!%|zU}h$ zrqXH_Cjg^{${2vC=;)S(F7SY*nip3Kff~HQsk8Z*8JnxR25PuqriLauIVh@R7c=Pi zZo(9ZS=#~!dsj=MK;74onx{DEF$dkAK)GXJB?CBzK*vYHVLt^eJG&|kT2MZx*jmtR zX$DvVY$I{o0Q)PFpO!fgLDt0J_zC@RR1-=ip$&@SW4VYY*cIZ7KU@^_7i3}8$tp$P z%>Ds6LNrdz;nk75==P5)g%L24JrSED$E)YJ9Go>{s@39!XKkw!3Aq4vVUlS7&TxuuweMO zNG~?yD=BWetu_A?2x9SSXt0&Z5woG-wf`;Plv10=0ni@w>z~qYJb&s`z9%0}ZxV^c?Bu`IDb|R)Wrh zizby80A0b&^JuMu1#>c)$+VrFvjtalr7&t`e3nPu7}cG76`8j#n3A(nT>rPiB}?IS+yDl70VP1IQ~QteACLJl)ixIr646? zz95E4j{)?LD$S^R(g0c`kDMo7YM)`U^XJioG@|;iM!6G6K!2Qc{QdW<0G4D=*{HxXJ_(t*A{yv zG|On^?Lun_pW@)g??EdeeRyNWYY@m4=iZ}767^a6$eI6m#XW8XB zNeAAoqMbp0KR!8&&;PGbs{%&lVO&gwlI8$)56?jJ-!y?xQ1PC$0^h8J$L%mQYvBzI z@!-6TgNlLTCwjqGnB=F?i|+4-T?PFiY!NN)962K^{U2p=r2svx0jk*XzU6Kd@)Ib% z3v=21ul*VR+QO7SjK7Y+4kH%aady#CJ>Kc{V!0w$(&y*rzU8jDcZN76s^{D_{J9BZ z^CK{_4a@}co8_xvrNGRM+>p@n)MiogcZ~`IJ7@@teHKGqx-IldDQ4g`I3>{|7FM&=LOT%WaaAQ zY}&$xVDD*c@55q`X=`Xh3>__z2BhH;ql*X&dJn7VG~g43ctb5B@=e``URf_Rou&<#`Nd`vL)J0mk75OE6o=CyV+BT`3%o<0MkYNrysno^%wg)G zHxat!#I*cIdr-Z1LtnpGUtD?{AZ!V9oIdO$EF6xH+O?ecP!du*Lw0+BUqrj8{y?eQ z6?twDo1E-@wNUVisN|Z-8`-LhcWiinx#we8gqs@vL;`KJpq1REOX}nKN}?-!HLH~U zmOJTQvE4v^M`}OP3YSxCV1I{=p~OKp$3AWAaJTU~YL(}EnZW?L24$1`-?cPdH|uzbVz4@FzIU>Vo%8cd1lvazG3%9<8cx)@1?dEc? zqFm|uX(1+M#CAL`SWh09M~XY!-oIx27uBPIPeuhgpq1T4V*tG)TW=#sh%<2X0~L~s z&7C!aG@Lh5{MJxvCC*B3h>E-b`!1p7P8`W609o04M1L7QuR%RB*8YRYwvLL9*a7oq zb|R4-)ggBs`Gf+Lij=~|_REfuS)V{{P3i}H?&&Rj_|g>!fokKNd3C#wrT+p5f&}W% zCWyaJrSd2uQnHx|=UeXHe7=ccdAZE?>;Eej*$0QefNdL9-GO+8;PQOo8b9g;@5=V;O~HAkGctemesjY@;8AcVGr>!%#3AJga~99wX0+FPv&qv zGTRPj>@@Tm%IG-Bh;v>{PrDkrb|&3Z=+`PJ7hFkozBi#yO&py$BHlL}@-QLFLK6*u zf!|}NFSL5nY*Q2#>(t)Ud6P8ttJo!ya#&uns+e3JM4ZQNd~)IOmeLV=n-b26=EN+V z7n=Vy$hHrtHel$LtJ%<;5Yjp1L1O0}%OhvdPI zQw27a7_GTTTQjJbj~z`c@*FxX#dRy$1egxaL8?+u^rayDrknk!AQkztXT85Dgjp#Q zTu^P(_&O*r#9clHQXa_)HrPHhQ8421X}&%kM`bx~HDU1uWfd^%@kTv?qItAD)Kh#c zUTncZd*3}05H(+i`Kc~Blgn9ddTGBipO}4Hlc&~b)|>zLp7(;v36Y2nkdPW!6yllT zHPnzBo-(7M@5i}wXU?4OTXNO*mC|*zDKGf%*4&Jf#eo2jl|{xl|A;8#_h4v{fMDc~ zHO8{j6=Ex+Q^hX`g98A?(cBQOULaozfSjv2XfH4Q-q+$^{mtivJJ=8r^<&-Woj7a6;S<1DP!?O$J-!5<&H+X>o*d_ZJ&z zuoP5dneF6!WD5?JW4}U}N^M6tF9)j4hl-dugh)|vu)ZN5Eb>a|jXF66r+t`8+-xOBw0j_o~P#M<^|}HtDuqx8438vPzw6 zO}%jPJa_*?p7+op>P)9HX+r>TMDfH#j27tw`}hj-l6_4dO!PVB^#N7c(#l~72#X-V z4Xm#_&VKh$%dW1vx8fauboWCeKZP^kPlmvVqb4x;F!E6X@f4)ZR2Np~CcEdomo7QW z%VNyT!|*{9Dxo>yy>nkS+sS0UO_t9DET6QPmj%Ik>_8vzB}~Bo^`1vS1yrAQld((# z_D2jgR8bYL969xYPj*Cq<9qlb|%HV*ro)OHJ89tB^B659oRaWOPyQ!+!|;cmdc@!Rn4$AsfY z3Sxc>xWh{lN|qe!K$wb?ywk8LAHj^GQm>olw4)Dcb|D5d>TeTF;%GHkjQ6!^8z};6 zuRnRwkx{+A3D=aM{tnFV{jNOaOf}PXVanaFunbqIWFbk-;5G2lCox$POhi^}HY6K} zPwAD|hBVwz9iKv*HQ<;RiU&`)dZ+Q!l5Wjwj-_~feNu3Z7TmyY%1wJsH2bm}N8CCR zSfx}!w$O6erxTtFbMM2@%Q*hyhQr$tm656&k{y%h5d$yLEEIR9wW121mFxEAV)N+O zqm}KZ)cH@ABe>>W4L3*%kh-3*?o~6LqTZOpY6e(MGU+2{Iy=u&C`oMKC<*GIx1bWb z9^BiK0L#NK%CVBEC)_!_P>Yl9!RcDrbZe(#<9_1m{b#^`l@!k%@8Q4_qM% z3Wr8TgQ=uwDr2esQTI0Y>8?BAmV0oHrpsspwD(X@aHpoeH2vq6wL2GmJq z;_6%`m$0?zuGpSK)(6n4;DeO4d^}tDo*~cKEAHcWJ&kgoggd+1^kXj2fPhHpm&DqN zIfrp5Z~k!4m$8(TAV&)cy$cEiGvj55tx+e>^XNb@&eg{8msb4WO!3h4)P_Bg@KA{Y zL}%Yn<1;OYSQswx5H)g}E+GF5`zy}vnHINRPPfFxLdo!wmD9CjDFA`!{lk;W)Zk>J z(LhlM$cMP)MVMt#e3Km-j?rgZM}NV?OuAoG#@xbxFzIp z`Y232j>QGr!!0C{HKxvJI?O*fq-XkgT)U4>@9@xD;541>XOPHkiaGR>n;9VGbtZR= zp04|dzNUal?S3GL*>bAa>wm%B$Ye4HaN+I-*#4I=4@yuNVW*G{LXu__i+Ibd?C17# z1OLUGn91|4&QxQ~-RA;H`c)^@cObV@$IA!L8Rq#xjS&OsM~cA8aE`gif{aYi9>@TC znD&mI@D=th(#)e?CD46>_`EW+Ei85&*A8Xg)dHD!^mitDXvU3(8TzcWTsm{VIg3b2d z(}%R(r{yYEMak+_xh`~O=0b2v_9&iWxmTUwN}+^u>y+dQ!2Z^gSAsR9_N;{=uu`LlJg3Fj>ZT`T^VCr=9`=9YA-rATLaGP=FY#)cE$y z?Cf_YoXOy#X`KlzU|gl)mPt*d(@#^oObe#K2Zu?Wryqk4viAr-+kWk+(|iIEn~al@QP?3;I|iHSO6o}%AzQO;^oJ?BBYAVP^udNA zYg%mB&pfjNceci7toQgLrwD1Q#Oc7U*7$GR!56%kCB5`3U;n(1*AIR9Ywd0BzjMBVa5_V;Y^9h=}o1sPu|x^t_dosW7}& z@AWE`-ZtQdI*u^V?ULF|HH55RFmsKxrL>iAAbSweIn<{iTIZBavuK*7IEpt6)AlK? zQ~``w8>s9QcmXGoPb~uSr$RrXXSOa(aIrH6TP&R2B9zYItBJ$XUQb@i;v{{8>y1u2J$Q750;eBQUqJLJL|M_{ode>d<>gvVBTr8_93B+CJ6N~v1 z57e)FfS8$_M^Ow~qoS1{hq#i9A`Kg*>`Y?N;ekM*qJ|!@({?5mX#-4btST5i3*+$oi#YHQXdi|RzLKOau2UjX$dG(3u5Gi!Fot)omQ@cn5`})g} zQBeUr6;Cr>BV~a3GC46?O1Obg8ANn8u6!Ic%!i6nLUkA6BSS|HdhGSpsdb#3R7D4X zO7=U>N0661y@%+GV~v-<-G2lU=C@%(irX38F>8C_an>OjJUO5ir}7XrC_7 z78BICGll?<18e}X;cV-YE2`Nbne-|=0Gfq#CYCy0ih-8RFZpTYw7CQK!iBY*nVfU& zI{Xn&6r=+DL^Nme6VYL5^vUdo>~2Fo!z?3}c)^KH81)#6NKvR>C>+Y=igVclHzglg zFI&a#+fkmUV=s{|ns*-GGF^{6t>i`d4cx+GY%mLJJ3zBCQkwTgFo<7wGLO7-CS|ww62D~ncKhw`oXXnmO(sfygq;2#3^^rA zqux1iu-QfCbRSDOz9`iO%tEIi;uta*UsU>wsN!=LLAG1m{J=t$-(W&mD0=9D;5ED- z*iz_}@Gm7J6~xE%=?g#Mo-g4siwmQ99$thW54{K@ugZ{EZ;HL&m#ijmiq9M$eq#%BF(kiu7)ZX$|zRS}g{_>muu;*7<5S&ci(Xo?R zppB@|28;B?4zj`w;2y&12D<-`kL)w5@S%y0Cw<#47Q4kFlbe6eN;Lm)dAq`+QBUydaC4lNEV!geBgFBx8lw&&g~aI?Xb2My2H z4cEVT|NZEF5*PC=84c)Uakgzdv<`)p&Hg}jJ>8)sF^67rs_O#q)0&$!jVmeCtQkl_ z5y;e=1go22^h2-<@qzR z#+eJvgTv-h_K3UW`bY_woUSc|z;?g`hX+$mrJdwswtWv!tU@P~N42k*%L40 z$PjUi3;9jhU6>!;j*I}B8Qo&XM-94WbWBFrEX6ZkzW@IB6i%PEx_Bhfjy8cZ4>_yl z?X+r(N=6SSP|2Bi)%~jbzvTC~uo*2iE+Z@h1*7pO-{62FDxq7L$c_*%gFvD}<4B4U zO6I6MiRB8?GVB<}(dcM|)4qk8?RnuL77xJlC{;+9&xaJL@ zC?O9NCe2-U->&A%%A= zBWz6MlpnwmEVXO3s*AECCn@P1s$n3R_D_nv{%FZ8V=^4c!56Yfc59d2{Z$Rvbs7GooOQ% z(5q#*Wty12eFnHA0LXeeI%Q)GPJ$yJ#BN0RLgq$Xoahr{ki z6vmd1mXhrwCd8I=*D@;9av?9G07e1& zZu0Wwq@8rH0w}>uIyv+_6wwz9ov<*m&%xrY4dF5tF_y>;Gvqhg#a$%D!bFtbNTwQ% zZq(452<j5{%tT*g|T`UwDK%hSO!Sai$Byp#hjrvkAOd!YD_>N=hGA;j8`J_{2qVILelor7dET z{^t4vcE4Hn%*s#ZVb^#pEXATy2YhMpV9BIo-hl&c_yIX-0Z8hSc>&_U)iQ2Tz@bD- zC)Mk``=0mIfOOh-JE-=ubPpA%euB#ca^ROF!9;vdBGh==u+M<~s4}6%YMF6(h>m0t zkwm8jC~7#gNJWu;#^yxZxho_LX|bvYmiOefYX@Bv_i?9Bw{3tbqCsSsX)Z&C#(X!S zs)=Q4C->CFzA{+7WKURCSYP5LtAZ*R37NE^H3u=(LOMSul|*DbKtxbnhNkgQU@?YeOuJii5aAO+kw7;W1W#T(py^rL%N15M3qeDS9gW5h~nm7rDPe)j=g#kOEdPGAM1%(EN+nbp0ASkUFd{Lquku!ctPg zj}PBm1VbLNHc|a>03jnS=J(w_MxuV_*=qYkjWU$lilw0D;hdCkl!x8DuDchHSE)ti z*4CCHqnjiOn>@aC=Bln=tvY=&D`+Qw39M&>z7|lS&M0gr>Om_5iW?q5NdiQ|Eh2Of zr+dcl#-n8=#SDAI8p#S7@gB*e`a@7WVZRZ0Q{+c32p+O#8U3%{kt z`86v}J(mEAE2M4n2B#g$0=drV0^0+=84W0Hnw>`qxu7hWvTkQk1DeAxpaZm=bqXm1 z_!R{Qd#{0aseEa~yAjOf&)^-{Eg1HQCP-|DjF0yD{QLt2m27@k{T*~f&xq(CyTL7} zQ^LgA5cs0Ob>4X3fuhwx4eFBTS-kI50Z9ndQ5Ov#v9x!*`I9}rJ|4u#{Kt+GLD9xx zVTm_(CEwKhk9vGz3k|yPFx;w^OqvEC1D_^C^*ZNYxzdC`buNkQUGzcVbslvpD&&QU zUHk-c=|^W9;!Emz&B$mF-D+&B(XAtLB5hr76drsK7JLeT%D1owEP||_+6;VTynVNC zdvgz;K>LPis|Exf^xhT?B&=hN>7E}OPcL%of_fOu?s$f7HcaqC8qHZ@{`o#%ymG}^ zUA-d(s5ar8o8R1oioK5GsWH{H?CIN6zzkOQUwO2IAuy5n7kmCK%CkB^5*ek$N0<pjxkY+IJwofIk{q%2y^wd?f`Y@j7U>vZa5HE$-9 z?d&!^{b!g~88f4uB?Mks%;!t@0W9r~Ic0YJqTbdT@LcG6R@3(52-m^|(Mfpz8o8N}n^)p~Wow9Lm;^)bBI!EYVX zr_lqPyoG-O4+wSNi@?2bBVtRsuwv1zcB~cV%VOU6C?J7-B1&;fFhJ1uFYlC=oT^&W zJAJPGz(itsGgiu)DF9h-ZT%kH{D=kO@#!>j;xdkT;)LmNCXnFYX(C%lV#1h$@)77% z+x7Mid8S&q3am~8O^^T(iV`N%F{LC| zmz?AQn=i;ta03lASC^;M-tu{8m5d<7yz#iBD5I$IY|4%mM7+5rMd(2KFhKgVp~Q%^ zcCKO6ppQY0ubW`T^c+&bbGa-fi6i0ZX>JkO%ss-WzqNs$7t?tFUIVEuuLC|*1HhN)IUBsZUG*~>C+%%GKxNM}jX!#I)Kt!D_ z-7cV=5P##&xr0HEVDhF{IrKuC3-bAH;UqmJ`0)6h=rt>uK9}O|Y&w=r2sGN^qG+H- zA#NVl+Nju=#31XkUINHfU8B%5aCo#Vgf1zS&q5rH3Kz*nC!21#g4o?Lokz4CMSZXe|s8U{FBGHcT{!NT9yl%rmwil{f067PTZAj%+vJD3W1l{pcDkHdYNIk2K(3 zU!1#e|NRfdP&}M4`9%D|Dhj0zTxz+E#)MXIO~**vwIA^M1~8N7S>J`~L#C=G73FrU zC0Dfbt#*LmLK?DE#e$I$;10c&V}RHZ(P3+8H?h}L%2`3v7pN*yUCjI3z`%x<9|?5( zeGAjmiyuIb%*7&iKL?jW!jr|sLumDPp`;m_s#)p`Fehz!QnX{jtJq%jYiKm)u9{0( z*Q_pURvplBTv04>i7d`}$rc}w3?`BPGO{;ztCj?2UjZ|TaIs=0Ha4727q@GwacJX ziBCYDj9NJmzSPh-j+SgpJy;K@ITCK;?POYSDEP2)YrfP!bM4w)I2$7(B8YK~lMRVW z+?AGKKZO$T8N?o7ae(+JTH+1fK7yF*Qk8QOJW3o<>tq(*i%M%$F_q_T16p%B;ljpeCVm6-NHdCf z1ZEZNsUbN)sSL>4p$nnKyHQl8B}GRI5Ms-%4HS8C)d&UPYigcSwJu+Vb2p}nXpPDz zvWe}1(lB2)QD}^on1)%9P`?bXo};-MaDqH^7(6ww{YmJYoM0t@y()Qp8iFW9zSK`a zCRBo4K$tEv9EM3+Y$U_;d2~WyBctWVq`+g?mw#eRV49eg*g%^IQ`FxE-~mk^IQ)vq z>o93?^3VW%i7A8{U%khg@g+{>kox4zSh}XK09)*atVl8*ny5-5XIQ|3vjv4pQGqDq zX`X79b2BcgZlydtn0s9=CjyUxx*JyzLio|1tBm{p)ggo)t9~~1^=&8a3ds`(!>?Xh zSvkJEe6^;lNM#^5tgA}eU?K-m$@Z8`NrDg#*=d~in06G@M;D`4S^(%9`aYxGo9exY z_ah4;f8Tw#o;-Q0w(Pm_WiIsgEg=>c3mr5Zy?ogRg!lkyzW}K|WW#9MYdf&+9nQBG zfWGa={J4@rOznNcc@7T;pm&?v^_mVo2pqfWRR*VA#JwwD~lX;t(Z@B2>B4A5N7jZ05F z;YNk?x)~I-?EX2a`|kkvg-2Vh#bZW7Li2C*wrDYw&^lE*PMu!|oK+cYsmnIHP6EN4 zq$*WN-9QZ9!U>R$`VkT&*+xV1WU*F=0Fv%Bi%1T<^&r^S(h@o`mx^Q2AZgyW-3_@Mj4_vxCQ2M*g+_ zn0Z31W)Bb~Muzp;uL6cA61oDOr2)};l2$dRgsL?aT zm}r)?sl{H+Uk=XuV19IKe!{jn*Q#?q@O$UvnKm~8>ZY}(da=RfrH%WK0AhZ^ zDBE!AA^7X?Lz4a+d}pblf{wEENJU3$><@O_Q;}RDr?u}f+Qyp@{K(#e)`??ZhFWrE zxT!d+i^jb5#n88O_1c1YZ#E_vQW9{(>1*b~_y6#>l|O(tGMVg>)!$B~ z5Mtt(okcQ4g});J{x^xXp>citNXOK8+DOcN(7=!~GZ66l-EjA%WGhBJF5LR;dbKqKloQa|bjZ_sq zkSgzjby!A9$l2)Sf#;r6f9~hJ?QOH6I6{zkzfjUxFPuXYP+z7=-P8=D#oC|NGkho* z2|!yU2osa?H9rlrf9FuL3!R&&%pcoaqv4E($QBag1W7~~6>L?>NHB_S5v%CoMSAi{ zKpg?%&Pusc`XINaH+@|%nt8Vsno8%7X<%rugu~Lj$tbf|4_s>+WhbF*x_(ff3l%!^ zQ3}+Ta26PO`6ofA1b@lY@V`10g@#N54Ay&yCq~pn(UFd`kHXBAo!NveBKAs;<&4@| zbv(rbf~YR1Id?)@ZD?&fsrhIHe?%i&E2numwfL&D4k$hY9jar8;ngsvcx@^yfv(Z+ z4|!6O7CzHI5Bn3TL}Ug9Xc5@bT#&qAWYI_nGnB9sUxGv%^@OFSN_$zm8*Bk6qrZ!4 z*tTG6@V&7MXTUHGNc7SMjb9o2Vy^YZzkMu?4(=HTA+>J%TDu3|@G|d-at8k7$>e@| zS~A>>mRg#Wg5VoY*T%sNF!xA*I(k$;)m7$JZwHp}KHFXaO-|9e1{_rY&)d8k#{5h0=*Ch$kRU}@v3Mtp1;J$k*qomS+V8%7^Ye?rxCQ06fyJUC zh=wFD5&bt!HA`OuZqFd}WV{7uTk^ULY%$1rxg<$a5#|i@jr5tND_D({g0*4kjWJ92+cII_khM~Tp&Z! zG_UA&t)^i+Db!0wfj$mDtpb^b%r9)pFsi{knj>N)8jB_q83(jm5Op{07RYkEv-I=J zV)sFK5f}L6o%Kou=A~>~LQyn*B@?!))jBea>K^>ab?;CG`BSZXrz+Q7`^4s~GP`oS z-q@g`8^o;yQPAPL!WS}Glx1wsJqG;VXt6?ZambEeh4H>4;~L5yKrdurHDaS1=fCK0890X?`>!E$#;^GXPd^3d;^u+W~gz88l16Td1 zYUfa&*MaM|4e`&k`VaT~3TTa_dlf-f!U2xt`^kSgUb2ZmgxHo*JP$=?*q~u9BDQci ziAe89yD}Jh2gos?7u*(_lbk5r-deD2HLE0IW?afjx7xNPhMt$5nE{imv1s5W6YZXi zTV%vjBo|gW8WKYH=E)nGN&-ASFJe;yS<(+*GDSUine3g+D(G9v<)ekK$9o8fiE~q@}o$({n3=wXUssQDb;pcfE1K?V%9Hii2gz00w z`F99S>~?pOlZTLi3Fs`++oLH)yf>u6k>Q3{VoATk+IsK2-Syr46LT9*#|A!NPc4=mR) zUfNGF+ie7OM@&P9X-0|=45WQ9Dy;G_M8+5e-=SABFJ|c-5h-0e#}E7ve-oWGuvLuD zl5QDz5>LWkmd1f^x;KHx>|+2`>rs|u>6)(qg8dHXL|)0Ib+lM9mzIzL^*u_qEF?Hp zfGw>+U?}>u0t_?Y28D{^ucNDaT$+EU@wL?~EQN~|XVC98Z1Y!)HSonUyWG#jQ^ z`_b!IJV&$Y|M!0$Y4G8M!dNt5^*npz0X*g%2HeHXx`%z8n>r9a5Axg=ZyqVQ#?3e=N2$klN z^@vQd*|0CpM-$T@uI8K#J6Bm%vKuz=3*~-7+R(I(jn|#j*DdSobaH$UjU}cRGvcwf z80+l|rkbjcQ>&mJl z&vVjVRo%PybvOK7UH{z>kB0-<=%&hncz8;T^;az~^*$gC(yYg|IfN)=v;#qGOM*S~*7+FHdOKZrH_$31^M#L{3p2LV>5 zRRpn+i1^4ojfn{lDkYofblQLHZo0SPT*RrTT}m%MfWQCoaJ8x^+W+S%fFa_H3`y9C zBfrssf49_e9(Fqmm;9-R;REv1W|%oEyAMD7@b2XbVyfOvRRYaTrzV9&9~3}!GJlDp z^mj4}9F@&64Kk)c>J>vrqaBA8^RU2S*RR}h^3Tt1dY|#ukgC1%nJF(Xw1kXM|IC!| z&d+@2ZD=ZSzoOi4>{Efv&Lh5|X%aW5f$j`I(9tfDZy4rrRqV8c{+Iv;adae<4You+J|94=8)vWZ-5+MStkr%@n&f)kR7ye_G$D{04{zNc10 zxEwco9ycQ-PKT8{fW9h1^yzp@nIMpTI{>qg8b5N=To_mlG3idCVi%!LPT(jVBd>gU zzPfcNV@$ohIzKUS;8p>svc8DUv(zq`Zu2(`)Qq5IaQ@z9MUy@x`toPLm;};4YRUmM ziWqe1uSV-#43W5Gm~=7%xh*{Z7Gdb^_M3nok?fD=oGr$qFtMHCfU0GaJan;xQxX| z2~2h5%!P%D_b=7~SuoW`|F(i5)$~@pwj`lHr1bLInv0CQ`pUwjHGTf7=^(QUANmZe zJA%HaeB5#NAK61#2q@c~{2c~;^|$U3XBu9VIk%M4%FQ67#68}vWc7bPgWTh+Sm{rg zNQAT|WUVO7CE_wwhCYHdrcODd+%J4G4$V6QAB~^gl-*byR06zd{Ey;t3>c-h>{)iG zpq~Jc8E~x43D76_C(QxPLdquf)U$bmtHRIkDXt48fSRXTPM_Pfdfem&x8j^{h94{F zWsHUP`|mIo3YbxHbwsbkA2<#Uw`ix`MLuHsuHWkljc>O|Xgebr7Ri&`$k`K8KlhP7JvU31VwH)OE}#H)Hc)N54) zY5_Sm;k3au58!0=*PzHMjT~DNJq7d@gReJ+&+LRmlX+UPfpw~)2`ZWg0oI?BlNKT? z+Q$>C3-Q6dbMZ{}9Y4H_s}w$Sk*gNww%U4gskA6jY4)RxGDr28B%f)i$POOI8t85$ zpxGG!w#`#jAV6;%P!Fe(We!|r4S?U-I|era;K-@|^M;>Hy**E&a|~rrQnX`4OH5@&B-CKr0?#oRy72`&v6?$0A4$!taK>3$Z4I>u z;z|wOU)>v=VDykNR#i030GL#8D+QFBx(PV##Gd!Q`S%dSQHPp;u9%tbgo(U(*Igbe z)2q$9n;@olPE}t!=CqzgYB`mrIVBRjd+x=DQ6VV{hBuh7rESC_4cJrT&n$E?X?6Le z-{63bq&r2=$u#EV&wEM9R?+#}6>U4qCk-JG%30OG5Z?#F{NbLvhP(LIJ;Wl@D*rI) za;~)Eo>*B~x$A9!oQ~^C>)q(QTeL8+Cy>}dg-ev{JQ6oMM28d(un$oN_AxTkU>U&Z zAa54U{P_Yr7JAvv7Lk@&nx0O12}#kGmoxR6;-k0kVyo~6DCNt|$rGTQf{3hALqo3>iJP;lvZst;Vxb=%v7VSN%LV>K|C+V;SO_TGKET5ztK!qhx>-$na(= zYpf=iAs=m2qs23mM~NJHn*ks0AaC9#bzXtL!eG)>$tC@2Az|HLs9fYY9p z8w}Ma;e=?vyo!9egN6c2U*1xk05gUPc@lf|V-PaZBgT2&!~Bl%I3i&+D$gGs&p<6O zIL6_F{~1l<1oilw+v`mwRk*s+fPh^g(*Mu22hxe-;pgfTGAIr=di-+>{pu zb{fT85Lg^86C~M)ka6A!#)SO8YnA=VRslY0%dI0;l+w_sKo#z9&CEc1C6x61eC5=2 z!$Ko+N6MnBCtz{waT_0IPhmr?X`2vlCIK%E5%>;_hHP~z6{nyf*a_wX*hdhlK%Quh z#zszHX3h1H>;oO!Ks=wmjbD6n8o)NwE0&%`tP&n(vDM){qjc2e(9%gU!hJ5#z-gm~sh67FZO7SO6jBH_1y0C^4wyHx zr3j)S0Fg3lVz*}s`iu1AZv8d<)+5h8yG#HLoDZyJ(SFyi_uqDd&jpm>`(iS2mOJ>K z-?{lK-0Qu=y^j(#$xH-`V)NNF#%LnA6Hy$l|D&x~I`GHglI=Pc3aAy-gsb(%#l^2( z&B<_lNWS5v4<9i-Px<3O%mLK`cp@+?NcaCEww21#b|hppDYl&>smbn)nMRxZju5`w zP*)riV%ytv9T?^odKfs0s=12!Z@OLEZCaVfZr{-74q3gPeX!{J(+}J~k+LTDg3TU8 zZtVZVOpd6bEwxU~9g>h?TGjGT(4@vG$ zOiics0DTNoRxaZ?$rPfALC_4>>FIZJLTKkwwsI5#(HFoP=m~dDvb{3ar4=m0v9JMm z4OZw;tPnN+vRGmw1~~L53`kSVhit@5jWFLP4UT9JhvmlnM;!jFj3mIM6i*yL90|^? z>3peVpktR=t;#6Ge1mhII)}t`|C0qHw0Yf5+r8@CY^7(XJ&nBHk2BpdYNtb32}8Cc z)+6(UBs#nO4r&kGeo%S$wB#tWk4smtx*Hq*y{*~VkeFVR2ZO?qKQd7@Va{3JtdmYS z8IQmcP#H=U9Pb3_MXkA9*dGkkC1P|;l@S4>0QR17{8%jF82auh$(Lsyaa}=^(b|KW z8Em`gzXBpba?**uJ3sICzwU_{M@S^m5Z6@F=v+F*B@G5&8rG}CmZ@g9x zOdy}*&#=2@#;Js!>BLM02~ZADG@bLcz%#5g3gV#>tqjnQ(JDwnnKhF0!Xz@ZJ*roG z5k|^S5u%UsU<=WrN83x(6oxljnv}T?BM=0@A|9~LP-wWidQ3C@L_*}z>Ql7=mbDFU zOqy>=uU=a8mV;GOlPCDNoDmDAeOEr0wUKV)C|GJzdDMeFx;q^LhNMOJ#(Lr)Kmsmu zX^mR5&%bo>BJ#Zl1Ei=bO6ToU)vAHUtqZ-e2-ha21o^3zN>y0*<&~lrd3K zA|!1hXCOtZc8RtTSQ^_tLQ5Noau%^-0n-uD|G360=;>#R$dfjBG`uq%**By(0FsnV z8})&@)_{{l@S*}ANvIqJ0G2*+mgAryp#j!{MPxSr1|k`RMLt}32b&{JZrpyZtIA|h_W z=_4tUoju3{V_EJwXov*UDZPO+^^~ZDVjej(GeBidmeB!^Fkjvcqv@wO%jSkv9n?!~ zmt?_78>1eX;l!0cYpp&|M`MQRMO_=HMoyPoZP8PUll8mgCjtJZW$<6ue-9z*(9S@I zD-E=Ki&08pcx|ua?A=St+ygBTW86D*AX7WLh&4u1VI<{9Fd$rp&J>X46tLO}mffC9 zqC`5(bS1y*#w1;8i(OIdc6YmvliD(Qk7ots@<-m|hnTtg4QwR=J(BkoDgn^nBoc9l ze&t!uMt9eOnAwn8EfXcs#y7C?_P)DuGz>D9cME9i)1cqK9cJ$R|E#@tkeugvCx{MT z&foim&N=r?&-C z_YlDM^bB!!U?(nU=p4aijpi>Gphe5N@1pWd&+oMivxTBm^_Ant9lvc?m)E*>zJ5&T z$(@$$lfUo?7no#F7q^2BP@i-(kYn`XOo}IvWdczakys?dxG;SJb_50o(WkXkLGK1= z0V!(PgC`2DR{yE_X0z~nluG2vr-_<@R8E2DB6JI2A-niLW#66vsZ*;$6tJRfCUs0j z)+)$(ii(e`nbI>AS(E&nekEZtEe9Tb*3*}Ax*4O$7-sP#q@LQ8Y8g3*h0RQ+>BWP3 zE?cw&$xP_75zh$HTS@Q1lTs4Ep1x|{ez0a2cR$!guiPQL7L%SX=*co>AEAjchQdc|<#u?6j9dPiNO-cTr?DCO+WZyBFi$It`~z4qE`7cApmS-zJlCKfQrG$kU(g97YT6+-SZa+80@V`Vu!mC!&U zFe%z3H&(Z91UyX24tqvM77A{13{xdXUw>eA?6QILH*03b1a3F$*kUZ~2-!gYpoV$8 z1uhK#&By`Hm0id=rsg@LRBD7#K2GBT?6WCPM=m~1v!!ZgpR2kF#q#XLlxk_n*bbt_ zKOqA8Av^8$(bHZrZ9pon7T|KZ^2X|LI2w5d#sqz}?)eS0v71Hr*i>vmR?6jU5ACeI`T4g3R3SSx>j*lj0AYA!p!R1jIScjx z#eA+?W$d0j6s*1LA;ho0PZkK0ivjShqTyX!VKo<;(Rlz-kt86=DiBU2us|nV*i5tD z5I6YKpUQ(xaqSMqdh)1*u~gc>MYtd@CTu08BK~3Cjb3TlU>J#fcNcEi_klfZ%MTz* zotml2o)s4WktM4lOTgMkUQ)6`Xoa|<)A_pKu3%OOg6g$?8P4X{1Y(wN8E7=f~F}ysqV_C6u zQ7fgrl7^x-&lijZ+pnnAx|JHK^F6zAK-qUq$cw<;3lW#H09lYmnUd>c%9*J#p$N2$ zqKt8vh#~+4OSNS5u^7;vC_)t!L~9+_BCX$`*k_-4TYqi3DfU2muD;0O(C)Zadlh z15@-IL1s8_K$&4;CA9f|XA{6bF?CRs4WTPy_2rZII-rx6aS;?{nA&*LXe(8@?WHwy z0^yYG6S0G8-c3E*l=M!c^W_T6*J`zRsaV-(qU5lEmfg+hiw?*tGz0G5jrv~H^xDUd zQ=>VM#qlvm-`aC`==vQ+YmREmDg5_$h~2A*F{WU7QJk9EEje9tkwh*Jmrk%g=zu{@ zh+OZ8mAso)o?UW_3QBYLF1dN7(dZ%?a8h#Of~Y6Ls#*)fg4)dM0Xn-{dcLdn=Je`@ zdhmN^&{!+#Mdyrz%JPz!LOVhVR5nx+HbB_}L@)sD79g~dK=Y`L!sISGa!7=5?!4_f zHHQ=3A=uutxZ}N?n4Vh_TVh&nMo!ET%TjKYq+6Atq@%Oaj4P@t$2$r*%|r$JPcT@H zTm=tqY!u-mX%2P4yB>JNhQ6E7;l3b$$8hg* zooW(WkLXIO2dBJHfR&0Frxy^7)(~`}2{>29JfZ0Owr>Z{QO}u8V<5NhH&BHt=F#{N z0AD&eEB5Gxl|BYDlkz@3&r@uJSaA0=S}C{QNH5STBab->$~ST1Xh#)s5txjD@4;O= zZHmt{<|W%2>_PLGh)NP$x>vg60VDOo$FSO&Yt_=pL2D+j(ZjL+Msy+rMd zB*U|TFNC@4WeG6|1cVxkgA~Be_1wA(_#(n>>thF$3cg-vY@td_PWL@|KKdKh%w+=P z1a4wjecvNDl?x>+*uf7ibA(#&v(_|yzLfRAt4<^mF-CPo<2$J`-<5GVks(a@p1 zo<{W}1s!%OuPRvl(Dh=79u-k)Wd2RRhv=Eo0}sbV_5s30AO^0+#^HwwkTk`IAr+}9 z_DJ-_JAdZPT^qJgz))D*xC;}kG0L=x)BYn`>9qfdWR%qb9+qy+B88?bRSIgMJ~}EJ zk|LQ-D@ip^5927Q?B%9ir?F-p8^EI}Aj20RVahhD-3B9Wsf`te+QI|;Z)Gl z?`Rj-1>f=__P%#0=575Ls;kf^Y2;3(qrvLpE>YPivPfcj#?$9Hduashdva>aLxImgD>6$h_5!Oy~v z)zWnVu#~{!(DERvOYqhIcA|r$W_<#6Ugo-u#0UjU!ilG|BX~lz91_oBjSb>c6qs)H zHbSBh%YIpzdtw&t4A96*^#f@6;E0)r4t3CarvukY)DB_dt|=hql`x8#SV)0w^)2AG z8>LhV17y)-c*Bg|HY*4~q+)Go4hX4OOf(?B(BPW#RWg+*R`TCrf&x+sTKWJl*9qW-U=W=lBqSGQd)l8`<`UV5guyW4wRG05mnQZwYnv5utvUatd|aU@o3bf~1+8z`F$E*iSz+iN|Qc_eE?KUS612<6-t4X1umMQ?y<+_!! zwG?{`c)VzN*E(i4^UTgR?a&Tz$|Q#90;ZcIcE(mBDYad$LE##@glL!8-_rxc80Rj} z43e*yruvw5I^TbCu4cdb>RJn}l7X5KC7~r)3>`phWSB|$>zYP)pw+A_;$XnNHH|v* zLaLR+jU?o>yQU~>jg;cZiVPd@0dBQM-8Mz;wi8Dm+_kyu#P!O878cp7_zt@n;%*>^ z#$e!S>kZWga~i|HoLmm)DY_z#!f`1N+on)Bl)tHj9uZVm_&)3{EEv&jGcN0HY@ySE z+8805B;4D)1Zcuvc!z0Mo!1 zFhm=ud^5sqJV?dSYNH_qNJS9e+`%OOs0I|;4&566N+*C9T@4T5zsL8_KR!2dYi`3` zn%e|+Y$mf@uODnQG8lDlsOkoq2n|Kqh<^H}qxXt?_ZEA43k^6JPiI<>==vik_S)Bd z0f#?0QHBPBE5lt!CrS~tG)X5B@lQz!XN`4n;naMO-%bA=zaIQUem?*rz*pj;pF9Q* zSV2((a0f7~UIif`xo3?$3!S+N+Uqn^auOIURe@CwP;6jxq|EXyrmPw>xToK@PcDzN z+3`J)W*|CC3{5AWNo!69|Ggz4Pf^HwsCK&@EQyXJ%k8$9fxiAZKy08IM+{)v-wT5L zTL|e2+JkO`#eM&j46)y6b6D&4#&J151o3#XS5h=v+k$c&)7GZm?{x_LqcIMNE2c;X zeHa+g=$^m~Zdf6tu3W3AAS?b1`Jw6SAC~ z1$;*}ks4zLV`&+nC#Lx+upWYeO1a|d^4@{6uzEYfMfRo>5L0@9`vdT-p}WD7fB_Fe z)<6c(1Y0HwHm1bOsP>Wdq@fQXmvRbBZgle*JeU0wM0(LZHgQCvti=L=8m1ZzXRtt` zaw;21*5H74+Io@PiCQ8MqrjOFP6C}CfD&Lq^Ad~!e*7=Wv*PZSMWr^D8fc~_KmR-c zn5)co8%3DP0G~`E4Un7*B?~2QKo&zkU%r8?<(ro5Sja{@4CDUr3TB`d@Bti3>ktOr zCe%HdwYq9ZER@i#`6&8h(6dyOid00;*>yV7BeFK4PCx>rIDls2|L;lf)Z96D&WSy|t`Tf`8A_c@_cMkxNMsEO@H`C=TP zK_lSr#-Iins{y05h@sk=s%5q- z6%FG-gakV2@PRx9J`i%24MkLwaXbsCRH!vWBeF8mOMU#iPbsiKa^rt86+7qB2p9VrFZME!}Q zd0<%8Qp~k#p-?Q^c6Hq@my5+})xPT4`%pQ6))Szk8ksC6)*tID=+HG6@H^Ks3Z=Y- zP>TP48aSq2PM<~u({Ri*S4Naf|IQ>?W=+7IcS9AOicA`!;w=?J%Ow*G0XTdM8Hjyw z#l$y`UE}X~K~s@@Btqa)kLZDX^fA(5zq7h(6$+f3jQYcH)BYApiO?&eNvCjJCJ}SWjDI>-GPCy=;9H=s ze|7c#n`WKHnKSnv$p)o6PVT`e(b$OhLl_Vu4li~7dy=T55ERY_CN-Lu^-j0j*7MBu z%xu`5ZL0fFPX`3J4Q9*Qn*i2x#g<*U`<9YxrRLqS$2NN=~>Ed=%e2V!RZcBiK8$=eD<+g|k9s?lQ`7$tM}*%Acz{1v;?d(q^FF=aXaFJc~!JUg+EiO zJEgnM0L^RbksTR<*=VOxY4;ll-v?W1@|{G~2=PqHi${z{R0vKYuKKWY5kr`BVmS>z zEXSOZs}9vllXbiB;TIN~UvbR}(0&fL^fPa!My zYN=GZ2``v2Djdk4?0N#ez%&;^^`Cg-NKj3!V|vy^GHq$C=_CL84>ER8{-+0Tyz$&O zZ$EnUfV;48VDKz`dcrEscJxxWm)?ygSkpvJQxyPjCA=Uu6(p#BihI~4B*kcg+B-aJ zzCqFhkzqSEVDZnIqr*(hrOYtHN=93|v4p)Q7S@ve+1X5-YIM~z@cHGp1kIuHnVg47 zXGq6w$hcomJmjWERG`a5p(ao*8iAWWg^N`=%TnFX9ki)wPowTgNU zW;UGAC@JfyMx&bS4wKzhD_nC$*_q>uc@uJ)*_E_oWNOzfV?hYaWSOr8>B(22V1I}~ ziWU|?@yjhQa9n-$5jV)B6ByTi_G^A7_<$Gp63LIW@E?5;%ICjB`55?*3UTtjXw~E9 zmrUQc%%a0GS~g%;YgZO5yL9LAd3WI)%|ySC3))4U=*>a8M%bqy1qz@OmEBc1K4!^9j9=sw_1R9Zuz%;qKUiwHz1I#YfEC|?7B`e+;}(dQl)D~1Wcw`^DjdVW!p(t4N%Ru4sM>aRv zWyUV!pFfK;cpKRY(vYOZCB7J*mcYGyj;ROY=@aG&w&;}2oMM;1hwc;O(vq|EKs9gY zbfd=!kj#qLdA0)w1Gk_VS-#_lfKf$Br7;0MefaLAujz@Rmk5x|m)a&!6iu7OGac1W z6kh+N#-us)IJWZrwq4tuF6#B&r%n3`7WxP{>Tf{-H2(+E+5<7dh=xVN#oKr|`cut9 z;h@dztQEu-uen0)p+$4_$;8+G=lJ;8USnzLY@Ex7AEFB7Dug5fcT%%2kMYs2xjKEP zP{yCRbZ9QJWZj@i=b6<+t(&X>a%s0$E~8+??f~vwyDDOWWjTY%P`?NP&7=*?Pr>Mv zy%;_wkXYzWgwtg%oT!5`n%D-e;-?hi>w7(X6v?JBL#LNnH3K8Rf(r0vz3#68RtFgG zt~>x@MpBL>QyFt+GrtVjV-k(PUv)QDgj$lBkzgYE810{W38|)XA|E4B^Y@Xj{Y4|WuVJQwJlm6V4pKFRDjZ%XZF+Xw z(@}VfsRtOp^TkqK39YzS8c1Ud3e6o}>M`4F4ems7p;QLy1ZOgmfCR*x=SRo+`Z{Z= z3AGOCF&h7hx3;zuPB!Y6gyuAu9Hno_ADjN6ZY%@u$w8*Pktkh-zbwOi6WPX%w`uSUze6*7H&c$O;z^ z_M%qcI6467glTjcNoQW9_^$J%2BXpVE2`Kt#k7+*5hTkS1?1n2c`GJ*uJ}FlHSF&K zi`);=h~H*ghuq5IT&Fq$`|BKH;C*=e2%>HZmO*qB*??rBAhyr#v#|tffW?bL-{>hf z%;~~ltj-UPc-q>ZE@;-Vg>n6asFvpot##XFRWpf#E)%(Q=mg(pHSY#Q|8TEQ!U_5t zREkqLqk?@+yjDJ`2#=dM=MfJA;Q%N2X$-`?z}DA?EE5>HvTK7CQ6!#A>n}gBs4cGF zb!f3t>;Mdz8MCrl6dH=8=(yykael;+)Pl%Q54z`_!EbB^`}=~D94z)zCagQmJofs7 z1?`&UveLOU#vG^zKls51o-MHq7Ei7DKfyS}Y2|1XEkyO+0(cHzj2}t>xwRY;QkCh$-K9>-BT>I-c<|6w6=WfTHZs zE<`mzjNBMNE7FihGd}4(aPq9Te{F4l%>R$4$``y3_n(=Y!a z?dhbhz?J;%uXD9YyW-ua%=yX;>-FMtx~`S8Rzc3_wr&PuYz7@}v$-!8i?>$-9~I4h zr~qQg_O!g6#)Q8SHglO%%J)z?6qy4247kx3*#u-lU?ZPk+Ks(Y->#cw*Xn39%Iq9) z(=y)^%WRIV$v5AQbYJQL^gtwaF%~d1R5pkv1#3gsRmA2!_GS|0dA6BvtWszFP&jf> zID-6CEQX+I+K79{5>B#K5PGfue=LBdM5XaQJ}a2l2COq+e_rrt?%(Pe$o zl}`3|bQ5}><-a8aZtHvs2c;f*<&_7XYgKfb6ZjO|)E}XLM&b%)!U0jM!1axb1OJB) zJ^aFRDG?)tf!dCE{2T6<+&@`4i6Q0k>Z6aIy-*;N{W-kfkNNu%XK_cAwn}a&(du8{ z|G@iOn0K7dzrnu1{$%yMyBmvm4m3z9pU64_yL0&jen;eR+QYEL^@Ao-ot!{_)R!Pk zq?9HC6CJrbCp7b}EEWri8D$~i#Zjj3h}v(kli6Z&zSby-b8WQUWHPmbk73Z2{0p5< z3*lFb^eh#0P{WDO`C!6t{2^F;#0b$dUz%a_y|84YJm#eBn%y{yl(V4x(s1am_gHKE z{8MucyT50z4lp4dboUTs+226D5Om2j!m`C(IF+gWGK}6na^XJDB-9e;3z?EO2Y?o| zsU&mqf(h5gSaVe%eSS|g8iD6`fY#9}zUZjXl=`_!Tj^9Jsaw>@Y&i!Z^&{+P+Dy#% zG-1C<>A`ggm&=A*Gb}Iet4;-V8n3$Wpd9Bn+U;NHK(T+~g*mf(rSf&p`}#lo8Eorm z%;qG!VT#DlEeE<_7z$iDzk1I-9u?D9UV7>E&)l-`O#glwZucA(MJ+@aFFjGWa4yFv zTHQL5@u(T4znY(g1*N-d=tPIL?Et*0Ve4mDI$;OOw<>L^Qx>l@az!cS3mJ_ z-$mq;%O29`l)5dF>>)RN%h;rrD0;#qra2t^7t;xos)Q7{4g^SM9b+sjRm$aG0+{1v zNe-l}>3nBhTK>V<^sIf{+in2i&KgI)-_O8!nZ^V8c|jNhJ!z&}1jlhPUpEwob~E-kHSMZ!;lN z)k~S6XttF@ht7EiJnz7Pw*1Y_v67veELhJoptA=Hf${C@9O+?0qK$hM

NB0*||j&=8K+E$fFGwf;APO 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 z

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