diff --git a/README.md b/README.md index a3d1829eb..2fd0e5150 100644 --- a/README.md +++ b/README.md @@ -77,24 +77,27 @@ Configuring **~/.emulationstation/es_systems.cfg:** When first run, an example systems configuration file will be created at $HOME/.emulationstation/es_systems.cfg. This example has some comments explaining how to write the configuration file, and an example RetroArch launch command. See the "Writing an es_systems.cfg" section for more information. -**~/.emulationstation/es_input.cfg:** -When you first start EmulationStation, you will be prompted to configure any input devices you wish to use. The process is thus: - -1. Press a button on any device you wish to use. *This includes the keyboard.* Once you have selected all the devices you wish to configure, hold a button on the first device to continue to step 2. - -2. Press the displayed input for each device in sequence. You will be prompted for Up, Down, Left, Right, A (Select), B (Back), Menu, Select (fast select), PageUp, and PageDown. If your controller doesn't have enough buttons for everything, you can press A to skip the remaining inputs. - -3. Your config will be saved to `~/.emulationstation/es_input.cfg`. *If you wish to reconfigure, just delete this file.* - -*NOTE: If `~/.emulationstation/es_input.cfg` is present but does not contain any available joysticks or a keyboard, an emergency default keyboard mapping will be used.* - -As long as ES hasn't frozen, you can always press F4 to close the application. - - **Keep in mind you'll have to set up your emulator separately from EmulationStation!** After you launch a game, EmulationStation will return once your system's command terminates (i.e. your emulator closes). +**~/.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 things stop working, you can delete the `~/.emulationstation/es_input.cfg` file to make the input configuration screen reappear on next run.** + You can use `--help` or `-h` to view a list of command-line options. Briefly outlined here: ``` @@ -109,9 +112,17 @@ You can use `--help` or `-h` to view a list of command-line options. Briefly out --home-path [path] - use [path] instead of the "home" environment variable (useful for portable installations). ``` +As long as ES hasn't frozen, you can always press F4 to close the application. + + Writing an es_systems.cfg ========================= -The file `~/.emulationstation/es_systems.cfg` contains the system configuration data for EmulationStation, written in XML. + +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: +* `~/.emulationstation/es_systems.cfg` +* `/etc/emulationstation/es_systems.cfg` The order EmulationStation displays systems reflects the order you define them in. @@ -163,7 +174,12 @@ gamelist.xml The gamelist.xml for a system defines metadata for a system's games, such as a name, image (like a screenshot or box art), description, release date, and rating. -If a file named gamelist.xml is found in the root of a system's search directory OR within `~/.emulationstation/%NAME%/`, game metadata will be loaded from it. This allows you to define images, descriptions, and different names for files. Note that only standard ASCII characters are supported for text (if you see a weird [X] symbol, you're probably using unicode!). +ES will check three places for a gamelist.xml, in the following order: +* `[SYSTEM_PATH]/gamelist.xml` +* `~/.emulationstation/gamelists/[SYSTEM_NAME]/gamelist.xml` +* `/etc/emulationstation/gamelists/[SYSTEM_NAME]/gamelist.xml` + +This file allows you to define images, descriptions, and different names for files. Note that only standard ASCII characters are supported for text (if you see a weird [X] symbol, you're probably using Unicode!). Images will be automatically resized to fit within the left column of the screen. Smaller images will load faster, so try to keep your resolution low. An example gamelist.xml: ```xml @@ -177,12 +193,17 @@ An example gamelist.xml: ``` -The path element should be the absolute path of the ROM. Special characters SHOULD NOT be escaped. The image element is the path to an image to display above the description (like a screenshot or boxart). Most formats can be used (including png, jpg, gif, etc.). Not all elements need to be used. +The path element should be the *absolute path* of the ROM. Special characters SHOULD NOT be escaped. The image element is the path to an image to display above the description (like a screenshot or boxart). Most formats can be used (including png, jpg, gif, etc.). Not all elements need to be used. The switch `--gamelist-only` can be used to skip automatic searching, and only display games defined in the system's gamelist.xml. -The switch `--ignore-gamelist` can be used to ignore the gamelist and use the non-detailed view. +The switch `--ignore-gamelist` can be used to ignore the gamelist and force ES to use the non-detailed view. -*You can use ES's [scraping](http://en.wikipedia.org/wiki/Web_scraping) tools to avoid creating a gamelist.xml by hand.* A command-line version is also provided - just run emulationstation with `--scrape`. +*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. + +A command-line version of the scraper is also provided - just run emulationstation with `--scrape` *(currently broken)*. Themes ====== @@ -190,6 +211,7 @@ 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! 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! -Aloshi diff --git a/THEMES.md b/THEMES.md index 30a5db44c..a4741ccce 100644 --- a/THEMES.md +++ b/THEMES.md @@ -3,9 +3,41 @@ Themes EmulationStation allows each system to have its own "theme." A theme is a collection **views** that define some **elements**, each with their own **properties**. -Themes are loaded from two places. If it is not in the first, it will try the next: +The first place ES will check for a theme is in the system's `` folder, for a theme.xml file: * `[SYSTEM_PATH]/theme.xml` -* `[HOME]/.emulationstation/[SYSTEM_NAME]/theme.xml` (where `[HOME]` is the `$HOME` environment variable on Linux and `%HOMEPATH%` on Windows) + +If that file doesn't exist, ES will try to find the theme in the current **theme set**. Theme sets are just a collection of individual system themes arranged in the "themes" folder under some name. Here's an example: + +``` +... + themes/ + my_theme_set/ + snes/ + theme.xml + my_cool_background.jpg + + nes/ + theme.xml + my_other_super_cool_background.jpg + + common_resources/ + scroll_sound.wav + + another_theme_set/ + snes/ + theme.xml + some_resource.svg +``` + +The theme set system makes it easy for users to try different themes and allows distributions to include multiple theme options. Users can change the currently active theme set in the "UI Settings" menu. The option is only visible if at least one theme set exists. + +There are two places ES can load theme sets from: +* `[HOME]/.emulationstation/themes/[CURRENT_THEME_SET]/[SYSTEM_NAME]/theme.xml` +* `/etc/emulationstation/themes/[CURRENT_THEME_SET]/[SYSTEM_NAME]/theme.xml` + +If both files happen to exist, ES will pick the first one (the one located in the home directory). + +Again, the `[CURRENT_THEME_SET]` value is set in the "UI Settings" menu. If it has not been set yet or the previously selected theme set is missing, the first available theme set will be used as the default. Simple Example ============== diff --git a/src/Settings.cpp b/src/Settings.cpp index f0f750529..b3d36298b 100644 --- a/src/Settings.cpp +++ b/src/Settings.cpp @@ -47,6 +47,7 @@ void Settings::setDefaults() mIntMap["GameListSortIndex"] = 0; mStringMap["TransitionStyle"] = "fade"; + mStringMap["ThemeSet"] = ""; mScraper = std::shared_ptr(new GamesDBScraper()); } diff --git a/src/SystemData.cpp b/src/SystemData.cpp index 1748ce77b..988477071 100644 --- a/src/SystemData.cpp +++ b/src/SystemData.cpp @@ -201,19 +201,18 @@ void SystemData::populateFolder(FileData* folder) } //creates systems from information located in a config file -bool SystemData::loadConfig(const std::string& path, bool writeExample) +bool SystemData::loadConfig() { deleteSystems(); + std::string path = getConfigPath(false); + LOG(LogInfo) << "Loading system config file " << path << "..."; if(!fs::exists(path)) { - LOG(LogError) << "File does not exist!"; - - if(writeExample) - writeExampleConfig(path); - + LOG(LogError) << "es_systems.cfg file does not exist!"; + writeExampleConfig(getConfigPath(true)); return false; } @@ -222,7 +221,7 @@ bool SystemData::loadConfig(const std::string& path, bool writeExample) if(!res) { - LOG(LogError) << "Could not parse config file!"; + LOG(LogError) << "Could not parse es_systems.cfg file!"; LOG(LogError) << res.description(); return false; } @@ -330,20 +329,16 @@ void SystemData::deleteSystems() sSystemVector.clear(); } -std::string SystemData::getConfigPath() +std::string SystemData::getConfigPath(bool forWrite) { - std::string home = getHomePath(); - if(home.empty()) - { - LOG(LogError) << "Home path environment variable empty or nonexistant!"; - exit(1); - return ""; - } + fs::path path = getHomePath() + "/.emulationstation/es_systems.cfg"; + if(forWrite || fs::exists(path)) + return path.generic_string(); - return(home + "/.emulationstation/es_systems.cfg"); + return "/etc/emulationstation/es_systems.cfg"; } -std::string SystemData::getGamelistPath() const +std::string SystemData::getGamelistPath(bool forWrite) const { fs::path filePath; @@ -351,25 +346,33 @@ std::string SystemData::getGamelistPath() const if(fs::exists(filePath)) return filePath.generic_string(); - filePath = getHomePath() + "/.emulationstation/"+ getName() + "/gamelist.xml"; - return filePath.generic_string(); + filePath = getHomePath() + "/.emulationstation/gamelists/" + mName + "/gamelist.xml"; + if(forWrite) // make sure the directory exists if we're going to write to it, or crashes will happen + fs::create_directories(filePath.parent_path()); + if(forWrite || fs::exists(filePath)) + return filePath.generic_string(); + + return "/etc/emulationstation/gamelists/" + mName + "/gamelist.xml"; } std::string SystemData::getThemePath() const { - fs::path filePath; + // where we check for themes, in order: + // 1. [SYSTEM_PATH]/theme.xml + // 2. currently selected theme set - filePath = mRootFolder->getPath() / "theme.xml"; - if(fs::exists(filePath)) - return filePath.generic_string(); + // first, check game folder + fs::path localThemePath = mRootFolder->getPath() / "theme.xml"; + if(fs::exists(localThemePath)) + return localThemePath.generic_string(); - filePath = getHomePath() + "/.emulationstation/" + getName() + "/theme.xml"; - return filePath.generic_string(); + // not in game folder, try theme sets + return ThemeData::getThemeFromCurrentSet(mName).generic_string(); } bool SystemData::hasGamelist() const { - return (fs::exists(getGamelistPath())); + return (fs::exists(getGamelistPath(false))); } unsigned int SystemData::getGameCount() const @@ -380,9 +383,15 @@ unsigned int SystemData::getGameCount() const void SystemData::loadTheme() { mTheme = std::make_shared(); + + std::string path = getThemePath(); + + if(!fs::exists(path)) // no theme available for this platform + return; + try { - mTheme->loadFile(getThemePath()); + mTheme->loadFile(path); } catch(ThemeException& e) { LOG(LogError) << e.what(); diff --git a/src/SystemData.h b/src/SystemData.h index 149a3afd4..6b9887ac8 100644 --- a/src/SystemData.h +++ b/src/SystemData.h @@ -24,7 +24,7 @@ public: inline PlatformIds::PlatformId getPlatformId() const { return mPlatformId; } inline const std::shared_ptr& getTheme() const { return mTheme; } - std::string getGamelistPath() const; + std::string getGamelistPath(bool forWrite) const; bool hasGamelist() const; std::string getThemePath() const; @@ -33,9 +33,9 @@ public: void launchGame(Window* window, FileData* game); static void deleteSystems(); - static bool loadConfig(const std::string& path, bool writeExampleIfNonexistant = true); //Load the system config file at getConfigPath(). Returns true if no errors were encountered. An example can be written if the file doesn't exist. + static bool loadConfig(); //Load the system config file at getConfigPath(). Returns true if no errors were encountered. An example will be written if the file doesn't exist. static void writeExampleConfig(const std::string& path); - static std::string getConfigPath(); + 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; diff --git a/src/ThemeData.cpp b/src/ThemeData.cpp index 3594d5f1e..f7712d29a 100644 --- a/src/ThemeData.cpp +++ b/src/ThemeData.cpp @@ -4,6 +4,7 @@ #include "Sound.h" #include "resources/TextureResource.h" #include "Log.h" +#include "Settings.h" #include "pugiXML/pugixml.hpp" #include @@ -411,3 +412,54 @@ ThemeExtras::~ThemeExtras() for(auto it = mExtras.begin(); it != mExtras.end(); it++) delete *it; } + + +std::map ThemeData::getThemeSets() +{ + std::map sets; + + static const size_t pathCount = 2; + fs::path paths[pathCount] = { + "/etc/emulationstation/themes", + getHomePath() + "/.emulationstation/themes" + }; + + fs::directory_iterator end; + + for(size_t i = 0; i < pathCount; i++) + { + if(!fs::is_directory(paths[i])) + continue; + + for(fs::directory_iterator it(paths[i]); it != end; ++it) + { + if(fs::is_directory(*it)) + { + ThemeSet set = {*it}; + sets[set.getName()] = set; + } + } + } + + return sets; +} + +fs::path ThemeData::getThemeFromCurrentSet(const std::string& system) +{ + auto themeSets = ThemeData::getThemeSets(); + if(themeSets.empty()) + { + // no theme sets available + return ""; + } + + auto set = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if(set == themeSets.end()) + { + // currently selected theme set is missing, so just pick the first available set + set = themeSets.begin(); + Settings::getInstance()->setString("ThemeSet", set->first); + } + + return set->second.getThemePath(system); +} diff --git a/src/ThemeData.h b/src/ThemeData.h index 19743636f..0e23fa5c5 100644 --- a/src/ThemeData.h +++ b/src/ThemeData.h @@ -81,6 +81,14 @@ private: std::vector mExtras; }; +struct ThemeSet +{ + boost::filesystem::path path; + + inline std::string getName() const { return path.stem().string(); } + inline boost::filesystem::path getThemePath(const std::string& system) const { return path/system/"theme.xml"; } +}; + class ThemeData { public: @@ -131,6 +139,9 @@ public: static const std::shared_ptr& getDefault(); + static std::map getThemeSets(); + static boost::filesystem::path getThemeFromCurrentSet(const std::string& system); + private: static std::map< std::string, std::map > sElementMap; diff --git a/src/XMLReader.cpp b/src/XMLReader.cpp index 1dcc9b059..0aeef8f23 100644 --- a/src/XMLReader.cpp +++ b/src/XMLReader.cpp @@ -119,7 +119,7 @@ FileData* findOrCreateFile(SystemData* system, const boost::filesystem::path& pa void parseGamelist(SystemData* system) { - std::string xmlpath = system->getGamelistPath(); + std::string xmlpath = system->getGamelistPath(false); if(!boost::filesystem::exists(xmlpath)) return; @@ -227,37 +227,33 @@ void updateGamelist(SystemData* system) if(Settings::getInstance()->getBool("IgnoreGamelist")) return; - std::string xmlpath = system->getGamelistPath(); - pugi::xml_document doc; + pugi::xml_node root; + std::string xmlReadPath = system->getGamelistPath(false); - if(boost::filesystem::exists(xmlpath)) + if(boost::filesystem::exists(xmlReadPath)) { //parse an existing file first - pugi::xml_parse_result result = doc.load_file(xmlpath.c_str()); + pugi::xml_parse_result result = doc.load_file(xmlReadPath.c_str()); if(!result) { - LOG(LogError) << "Error parsing XML file \"" << xmlpath << "\"!\n " << result.description(); + 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; } }else{ //set up an empty gamelist to append to - doc.append_child("gameList"); - - //make sure the folders leading up to this path exist (or the XML file write will fail later on) - boost::filesystem::path path(xmlpath); - boost::filesystem::create_directories(path.parent_path()); + root = doc.append_child("gameList"); } - pugi::xml_node root = doc.child("gameList"); - if(!root) - { - LOG(LogError) << "Could not find node in gamelist \"" << xmlpath << "\"!"; - return; - } - //now we have all the information from the XML. now iterate through all our games and add information from there FileData* rootFolder = system->getRootFolder(); if (rootFolder != nullptr) @@ -296,9 +292,15 @@ void updateGamelist(SystemData* system) ++fit; } + //now write the file - if (!doc.save_file(xmlpath.c_str())) { - LOG(LogError) << "Error saving gamelist.xml file \"" << xmlpath << "\"!"; + + //make sure the folders leading up to this path exist (or the write will fail) + boost::filesystem::path xmlWritePath(system->getGamelistPath(true)); + boost::filesystem::create_directories(xmlWritePath.parent_path()); + + if (!doc.save_file(xmlWritePath.c_str())) { + LOG(LogError) << "Error saving gamelist.xml to \"" << xmlWritePath << "\" (for system " << system->getName() << ")!"; } }else{ LOG(LogError) << "Found no root folder for system \"" << system->getName() << "\"!"; diff --git a/src/guis/GuiMenu.cpp b/src/guis/GuiMenu.cpp index a7ad70abe..d13269b37 100644 --- a/src/guis/GuiMenu.cpp +++ b/src/guis/GuiMenu.cpp @@ -8,6 +8,7 @@ #include "GuiSettings.h" #include "GuiScraperStart.h" #include "GuiDetectDevice.h" +#include "../views/ViewController.h" #include "../components/ButtonComponent.h" #include "../components/SwitchComponent.h" @@ -120,6 +121,34 @@ GuiMenu::GuiMenu(Window* window) : GuiComponent(window), mMenu(window, "MAIN MEN s->addWithLabel("TRANSITION STYLE", transition_style); s->addSaveFunc([transition_style] { Settings::getInstance()->setString("TransitionStyle", transition_style->getSelected()); }); + // theme set + auto themeSets = ThemeData::getThemeSets(); + + if(!themeSets.empty()) + { + auto selectedSet = themeSets.find(Settings::getInstance()->getString("ThemeSet")); + if(selectedSet == themeSets.end()) + selectedSet = themeSets.begin(); + + auto theme_set = std::make_shared< OptionListComponent >(mWindow, "THEME SET", false); + for(auto it = themeSets.begin(); it != themeSets.end(); it++) + theme_set->add(it->first, it->first, it == selectedSet); + s->addWithLabel("THEME SET", theme_set); + + Window* window = mWindow; + s->addSaveFunc([window, theme_set] + { + bool needReload = false; + if(Settings::getInstance()->getString("ThemeSet") != theme_set->getSelected()) + needReload = true; + + Settings::getInstance()->setString("ThemeSet", theme_set->getSelected()); + + if(needReload) + window->getViewController()->reloadAll(); // TODO - replace this with some sort of signal-based implementation + }); + } + mWindow->pushGui(s); }); diff --git a/src/main.cpp b/src/main.cpp index d8a78ac0f..ecdf69fa1 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -162,7 +162,7 @@ int main(int argc, char* argv[]) } //try loading the system config file - if(!SystemData::loadConfig(SystemData::getConfigPath(), true)) + if(!SystemData::loadConfig()) { LOG(LogError) << "Error parsing system config file!"; return 1; diff --git a/src/views/ViewController.cpp b/src/views/ViewController.cpp index 11b4f87ed..f1a3de88b 100644 --- a/src/views/ViewController.cpp +++ b/src/views/ViewController.cpp @@ -7,6 +7,7 @@ #include "gamelist/DetailedGameListView.h" #include "gamelist/GridGameListView.h" #include "../guis/GuiMenu.h" +#include "../guis/GuiMsgBox.h" #include "../animations/LaunchAnimation.h" #include "../animations/MoveCameraAnimation.h" #include "../animations/LambdaAnimation.h" @@ -285,6 +286,37 @@ void ViewController::reloadGameListView(IGameListView* view, bool reloadTheme) } } +void ViewController::reloadAll() +{ + std::map cursorMap; + for(auto it = mGameListViews.begin(); it != mGameListViews.end(); it++) + { + cursorMap[it->first] = it->second->getCursor(); + } + mGameListViews.clear(); + + for(auto it = cursorMap.begin(); it != cursorMap.end(); it++) + { + it->first->loadTheme(); + getGameListView(it->first)->setCursor(it->second); + } + + mSystemListView.reset(); + getSystemListView(); + + // update mCurrentView since the pointers changed + if(mState.viewing == GAME_LIST) + { + mCurrentView = getGameListView(mState.getSystem()); + }else if(mState.viewing == SYSTEM_SELECT) + { + mSystemListView->goToSystem(mState.getSystem()); + mCurrentView = mSystemListView; + }else{ + goToSystemView(SystemData::sSystemVector.front()); + } +} + std::vector ViewController::getHelpPrompts() { std::vector prompts; diff --git a/src/views/ViewController.h b/src/views/ViewController.h index 6c052b310..52f9121f3 100644 --- a/src/views/ViewController.h +++ b/src/views/ViewController.h @@ -18,6 +18,7 @@ public: // If a basic view detected a metadata change, it can request to recreate // the current gamelist view (as it may change to be detailed). void reloadGameListView(IGameListView* gamelist, bool reloadTheme = false); + void reloadAll(); // Reload everything with a theme. Used when the "ThemeSet" setting changes. // Navigation. void goToNextGameList(); @@ -61,6 +62,7 @@ public: private: void playViewTransition(); + std::shared_ptr getGameListView(SystemData* system); std::shared_ptr getSystemListView();