diff --git a/CMakeLists.txt b/CMakeLists.txt
index 31fad76de..d2f7ce5c6 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -154,10 +154,11 @@ set(ES_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/src/Settings.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Sound.h
${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeData.h
${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.h
${CMAKE_CURRENT_SOURCE_DIR}/src/Window.h
${CMAKE_CURRENT_SOURCE_DIR}/src/XMLReader.h
- ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimationComponent.h
+
${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ButtonComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentListComponent.h
@@ -172,24 +173,28 @@ set(ES_HEADERS
${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextEditComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextListComponent.h
- ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ThemeComponent.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiDetectDevice.h
- ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiFastSelect.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMetaDataEd.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMsgBoxOk.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMsgBoxYesNo.h
- ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiGameList.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiGameScraper.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiInputConfig.h
- ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMenu.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiSettingsMenu.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiScraperStart.h
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiScraperLog.h
+
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.h
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBScraper.h
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/TheArchiveScraper.h
+
${CMAKE_CURRENT_SOURCE_DIR}/src/pugiXML/pugiconfig.hpp
${CMAKE_CURRENT_SOURCE_DIR}/src/pugiXML/pugixml.hpp
+
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/views/BasicGameListView.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/views/DetailedGameListView.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/views/GameListView.h
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.h
+
${CMAKE_CURRENT_SOURCE_DIR}/src/resources/Font.h
${CMAKE_CURRENT_SOURCE_DIR}/src/resources/ResourceManager.h
${CMAKE_CURRENT_SOURCE_DIR}/src/resources/TextureResource.h
@@ -215,10 +220,11 @@ set(ES_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/Settings.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Sound.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/SystemData.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/ThemeData.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/VolumeControl.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/Window.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/XMLReader.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/src/components/AnimationComponent.cpp
+
${CMAKE_CURRENT_SOURCE_DIR}/src/components/AsyncReqComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ButtonComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/ComponentListComponent.cpp
@@ -231,27 +237,31 @@ set(ES_SOURCES
${CMAKE_CURRENT_SOURCE_DIR}/src/components/SwitchComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/TextEditComponent.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/src/components/ThemeComponent.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiDetectDevice.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiFastSelect.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMetaDataEd.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMsgBoxOk.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMsgBoxYesNo.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiGameList.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiGameScraper.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiInputConfig.cpp
- ${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiMenu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiSettingsMenu.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiScraperStart.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/components/GuiScraperLog.cpp
+
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/Scraper.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/GamesDBScraper.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/scrapers/TheArchiveScraper.cpp
+
${CMAKE_CURRENT_SOURCE_DIR}/src/pugiXML/pugixml.cpp
+
${CMAKE_CURRENT_SOURCE_DIR}/src/resources/Font.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/resources/ResourceManager.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/resources/TextureResource.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/views/BasicGameListView.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/views/DetailedGameListView.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/views/GameListView.cpp
+ ${CMAKE_CURRENT_SOURCE_DIR}/src/views/ViewController.cpp
+
${CMAKE_CURRENT_SOURCE_DIR}/data/ResourceUtil.cpp
${CMAKE_CURRENT_SOURCE_DIR}/data/converted/ES_logo_16_png.cpp
${CMAKE_CURRENT_SOURCE_DIR}/data/converted/ES_logo_32_png.cpp
diff --git a/DEVNOTES.md b/DEVNOTES.md
new file mode 100644
index 000000000..7ec034477
--- /dev/null
+++ b/DEVNOTES.md
@@ -0,0 +1,35 @@
+Also known as "Really Bad Technical Documentation"
+
+These are mostly notes I create as I go along, marking potential gotchas for people who might try to extend my code.
+Some day I'll try and add an overview of the code structure, what each class does, etc.
+
+Development Environment
+=======================
+
+I personally launch ES in windowed mode with a smaller resolution than my monitor and with debug text enabled.
+
+`emulationstation --windowed --debug -w 1280 -h 720`
+
+
+Creating a new GuiComponent
+===========================
+
+You probably want to override:
+
+ `bool input(InputConfig* config, Input input);`
+ Check if some input is mapped to some action with `config->isMappedTo("a", input);`.
+ Check if an input is "pressed" with `input.value != 0` (input.value *can* be negative in the case of axes).
+
+ `void update(int deltaTime);`
+ `deltaTime` is in milliseconds.
+
+ `void render(const Eigen::Affine3f& parentTrans);`
+ You probably want to do `Eigen::Affine3f trans = parentTrans * getTransform();` to get your final "modelview" matrix.
+ Apply the modelview matrix with `Renderer::setMatrix(const Eigen::Affine3f&)`.
+ Render any children the component may have with `renderChildren(parentTrans);`.
+
+
+Creating a new GameListView Class
+=================================
+
+1. Don't allow the user to navigate to the root node's parent. If you use a stack of some sort to keep track of past cursor states this will be a natural side effect.
diff --git a/THEMES.md b/THEMES.md
index c908228c0..0adbf7858 100644
--- a/THEMES.md
+++ b/THEMES.md
@@ -1,176 +1,114 @@
Themes
======
-EmulationStation allows each system to have its own "theme." A theme is a collection of display settings and images defined in an XML document.
+EmulationStation allows each system to have its own "theme." A theme is a collection of resources defined in an XML document.
-ES will check 3 places for a theme, in the following order:
- - a theme.xml file in the root of a system's %PATH% directory.
- - $HOME/.emulationstation/%NAME%/theme.xml
- - $HOME/.emulationstation/es_theme.xml
-
-Almost all positions, dimensions, origins, etc. work in percentages - that is, they are a decimal between 0 and 1, representing the percentage of the screen on that axis to use. This ensures that themes look similar at every resolution.
-
-Colors are hex values, either 6 or 8 characters, defined as RRGGBB or RRGGBBAA. If alpha is not included, a default value of FF will be assumed (not transparent).
+Themes are loaded like this:
+1. Initialize to default values.
+2. If `$HOME/.emulationstation/es_theme_default.xml` exists, load it.
+3a. If there is a `theme.xml` present in the root of a system's `path` directory, load it.
+3b. IF NOT, If `$HOME/.emulationstation/%SYSTEMNAME%/theme.xml` exists, load it.
Example
=======
-Here's a theme that defines some colors, displays a background, and displays a logo in the top left corner:
```
0000FF
00FF00
-
- image
- ./theme/background.png
- 0 0
- 1 1
- 0 0
-
+
+ ./../all_themes/font.ttf
+ 0.045
+
-
- image
+
+ ./../all_themes/font.ttf
+ 0.035
+
+
+
+ ./theme/background.png
+ true
+
+
+
./theme/logo.png
- 0 0
- 0.4 0
- 0 0
-
+ false
+
+
+ ./../all_themes/scrollSound.wav
-
-
-
- 0000FF
- 00FF00
-
-
- image
- ./theme/background.png
- 0 0
- 1 1
- 0 0
-
-
-
```
-Themes can be defined with two tags: `` and ``.
-You can define both a normal and basic theme in the same file.
+Themes must be enclosed in a `` tag.
-If EmulationStation is running in "basic" mode, it will try to use ``. If that doesn't exist or ES is running in "detailed" mode (a gamelist.xml is present), `` will be used.
-
-
-Components
-==========
-
-A theme is made up of components, which have various types. At the moment, the only type is `image`. Components are rendered in the order they are defined - that means you'll want to define the background first, a header image second, etc.
-
-
-The "image" component
-=====================
-
-Used to display an image.
-
-`` - path to the image file. Most common file types are supported, and . and ~ are properly expanded.
-
-`` - the position, as two screen percentages, at which to display the image.
-
-`` - the dimensions, as two screen percentages, that the image will be resized to. Make one axis 0 to keep the aspect ratio.
-
-`` - the point on the image that `` defines, as an image percentage. "0.5 0.5", the center of the image, by default.
-
-`` - if present, the image is tiled instead of resized.
-
-
-Display tags
-============
-
-Display tags define some "meta" display attributes about your theme. Display tags must be at the root of the `` tree - for example, they can't be inside a component tag. They are not required.
-
-
-**Game list attributes:**
-
-`` - the hex font color to use for games on the GuiGameList.
-
-`` - the hex font color to use for folders on the GuiGameList.
-
-`` - the hex font color to use for the description on the GuiGameList.
-
-`` - the hex color to use for the "selector bar" on the GuiGameList. Default is `000000FF`.
-
-`` - the hex color to use for selected text on the GuiGameList. Default is zero, which means no change.
-
-`` - if present, the games list names will be left aligned to the value of `` + ``. On by default for detailed themes.
-
-`` - if present, the system name header won't be displayed (useful for replacing it with an image). If you're making a complete custom theme, you probably want to use this.
-
-`` - if present, the divider between games on the detailed GuiGameList won't be displayed.
-
-`` - the percentage to offset the list by. Default is 0.5 (half the screen). **Will also move the selector bar**.
-
-`` - the percentage to offset the text in the list by. Default is 0.005. Only works in combination with ``.
-
-
-
-**Game image attributes:**
-
-`` - two values for the position of the game art, in the form of `[x] [y]`, as a percentage. Default is `$infoWidth/2 $headerHeight`.
-
-`` - two values for the dimensions of the game art, in the form of `[width] [height]`, as a percentage of the screen. Default is `$infoWidth 0` (width fits within the info column). The image will only be resized if at least one axis is nonzero *and* exceeded by the image's size. You should always leave at least one axis as zero to preserve the aspect ratio.
-
-`` - two values for the origin of the game art, in the form of `[x] [y]`, as a percentage. Default is `0.5 0` (top-center of the image).
-
-`` - path to the image to display if a game's image is missing. '.' and '~' are expanded.
-
-
-
-**Fast Select box attributes:**
-
-`` - the hex color to use for the letter display on the fast select box.
-
-`` - the path to a "nine patch" image to use for the "background" of the fast select box. See the "Nine Patches" section for more info.
-
-`` - font definition to use for the fast select letter. See the "Fonts" section for more info.
+All paths automatically expand `./` to the folder containing the theme.xml.
+All paths automatically expand `~/` to the home directory ($HOME on Linux, %HOMEPATH% on Windows).
+Stuff you can define
+====================
Fonts
=====
Fonts are defined like so:
-
```
-
- ./path/to/font
- 0.05
-
+
+
+ ./some/path/here.ttf
+
+ 0.035
+
```
-You can leave off any tags you don't want to use, and they'll use the default. Size is defined as a percentage of the screen height. "." and "~" are expanded for paths.
+`` - Default size: 0.045.
+`` - Default size: 0.035.
-NOTE: If your font size is too big, it'll overrun the maximum OpenGL texture size. ES will attempt to rasterize it in progressively smaller sizes until one fits, then upscale it.
+Colors
+======
-**Font tags:**
+Colors are defined in hex, like this:
-`` - font to use for the game list.
+`00FF00FF`
+or
+`00FF00`
+(without the alpha channel specified - will assume FF)
-`` - font to use for description text.
+`` - Default: 0000FFFF.
+`` - Default: 00FF00FF.
+`` - Default: 000000FF.
+`` - Default: 00000000.
+`` - Default: 48474DFF.
-`` - font to use for the fast select letter.
+Images
+======
+Images are defined like this:
+```
+
+ ./some/path/here.png
+
+ true
+
+```
+Pretty much any image format is supported.
-Audio
-=====
+`` - No default.
+`` - No default.
-Themes can also define menu sounds. These tags go in the root of the `` tree, just like Display tags. Sounds should be in the .wav format. The relative path operator (.) and home operator (~) are properly expanded.
+Sounds
+======
-`` - path to the sound to play when the game list or fast select menu is scrolling.
+Sounds are defined like this:
+`./some/path/here.wav`
+Only .wav files are supported.
-`` - path to the sound to play when the user selects something from the game list.
-
-`` - path to the sound to play when the user "goes up" from a folder in the game list.
-
-`` - path to the sound to play when the user opens a menu (either the "main menu" or the fast select menu).
+`` - No default.
+`` - No default.
+`` - No default.
+`` - No default.
Nine Patches
@@ -178,18 +116,5 @@ Nine Patches
EmulationStation borrows the concept of "nine patches" from Android (or "9-Slices"). Currently the implementation is very simple and hard-coded to only use 48x48px images (16x16px for each "patch"). Check the `data/resources` directory for some examples (button.png, frame.png).
-
-List of variables
-=================
-
-Variables can be used in position and dimension definitions. They can be added, subtracted, multiplied, and divided. Parenthesis are valid. They are a percentage of the screen.
-
-For example, if you wanted to place an image that covered the left half of the screen, up to the game list, you could use `$infoWidth 1`.
-
-`$headerHeight` - height of the system name header.
-
-`$infoWidth` - where the left of the game list begins. Will follow ``.
-
-
-Aloshi
http://www.aloshi.com
diff --git a/src/FileData.cpp b/src/FileData.cpp
index 958ecf499..710313f85 100644
--- a/src/FileData.cpp
+++ b/src/FileData.cpp
@@ -9,8 +9,8 @@ std::string getCleanFileName(const fs::path& path)
}
-FileData::FileData(FileType type, const fs::path& path)
- : mType(type), mPath(path), mParent(NULL), metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor!
+FileData::FileData(FileType type, const fs::path& path, SystemData* system)
+ : mType(type), mPath(path), mSystem(system), mParent(NULL), metadata(type == GAME ? GAME_METADATA : FOLDER_METADATA) // metadata is REALLY set in the constructor!
{
// metadata needs at least a name field (since that's what getName() will return)
if(metadata.get("name").empty())
@@ -21,6 +21,9 @@ FileData::~FileData()
{
if(mParent)
mParent->removeChild(this);
+
+ while(mChildren.size())
+ delete mChildren.back();
}
const std::string& FileData::getThumbnailPath() const
diff --git a/src/FileData.h b/src/FileData.h
index 2ef987dc7..9d9ef0175 100644
--- a/src/FileData.h
+++ b/src/FileData.h
@@ -5,12 +5,21 @@
#include
#include "MetaData.h"
+class SystemData;
+
enum FileType
{
GAME = 1, // Cannot have children.
FOLDER = 2
};
+enum FileChangeType
+{
+ FILE_ADDED,
+ FILE_METADATA_CHANGED,
+ FILE_REMOVED
+};
+
// Used for loading/saving gamelist.xml.
const char* fileTypeToString(FileType type);
FileType stringToFileType(const char* str);
@@ -21,7 +30,7 @@ std::string getCleanFileName(const boost::filesystem::path& path);
class FileData
{
public:
- FileData(FileType type, const boost::filesystem::path& path);
+ FileData(FileType type, const boost::filesystem::path& path, SystemData* system);
virtual ~FileData();
inline const std::string& getName() const { return metadata.get("name"); }
@@ -29,6 +38,7 @@ public:
inline const boost::filesystem::path& getPath() const { return mPath; }
inline FileData* getParent() const { return mParent; }
inline const std::vector& getChildren() const { return mChildren; }
+ inline SystemData* getSystem() const { return mSystem; }
virtual const std::string& getThumbnailPath() const;
@@ -57,6 +67,7 @@ public:
private:
FileType mType;
boost::filesystem::path mPath;
+ SystemData* mSystem;
FileData* mParent;
std::vector mChildren;
};
diff --git a/src/GuiComponent.cpp b/src/GuiComponent.cpp
index 4f793713e..c4a1fde41 100644
--- a/src/GuiComponent.cpp
+++ b/src/GuiComponent.cpp
@@ -99,6 +99,9 @@ void GuiComponent::addChild(GuiComponent* cmp)
void GuiComponent::removeChild(GuiComponent* cmp)
{
+ if(!cmp->getParent())
+ return;
+
if(cmp->getParent() != this)
{
LOG(LogError) << "Tried to remove child from incorrect parent!";
diff --git a/src/SystemData.cpp b/src/SystemData.cpp
index d162a4b00..e7055d510 100644
--- a/src/SystemData.cpp
+++ b/src/SystemData.cpp
@@ -35,8 +35,8 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, con
mLaunchCommand = command;
mPlatformId = platformId;
- mRootFolder = new FileData(FOLDER, mStartPath);
- mRootFolder->metadata.set("name", "Search Root");
+ mRootFolder = new FileData(FOLDER, mStartPath, this);
+ mRootFolder->metadata.set("name", mFullName);
if(!Settings::getInstance()->getBool("PARSEGAMELISTONLY"))
populateFolder(mRootFolder);
@@ -45,6 +45,9 @@ SystemData::SystemData(const std::string& name, const std::string& fullName, con
parseGamelist(this);
mRootFolder->sort(FileSorts::SortTypes.at(0));
+
+ mTheme = std::make_shared();
+ mTheme->loadFile(getThemePath());
}
SystemData::~SystemData()
@@ -178,7 +181,7 @@ void SystemData::populateFolder(FileData* folder)
isGame = false;
if(std::find(mSearchExtensions.begin(), mSearchExtensions.end(), extension) != mSearchExtensions.end())
{
- FileData* newGame = new FileData(GAME, filePath.generic_string());
+ FileData* newGame = new FileData(GAME, filePath.generic_string(), this);
folder->addChild(newGame);
isGame = true;
}
@@ -186,7 +189,7 @@ void SystemData::populateFolder(FileData* folder)
//add directories that also do not match an extension as folders
if(!isGame && fs::is_directory(filePath))
{
- FileData* newFolder = new FileData(FOLDER, filePath.generic_string());
+ FileData* newFolder = new FileData(FOLDER, filePath.generic_string(), this);
populateFolder(newFolder);
//ignore folders that do not contain games
@@ -347,7 +350,19 @@ std::string SystemData::getGamelistPath() const
return filePath.generic_string();
}
-bool SystemData::hasGamelist()
+std::string SystemData::getThemePath() const
+{
+ fs::path filePath;
+
+ filePath = mRootFolder->getPath() / "theme.xml";
+ if(fs::exists(filePath))
+ return filePath.generic_string();
+
+ filePath = getHomePath() + "/.emulationstation/" + getName() + "/theme.xml";
+ return filePath.generic_string();
+}
+
+bool SystemData::hasGamelist() const
{
return (fs::exists(getGamelistPath()));
}
diff --git a/src/SystemData.h b/src/SystemData.h
index c0f2b0e92..ca6e5bed0 100644
--- a/src/SystemData.h
+++ b/src/SystemData.h
@@ -7,6 +7,7 @@
#include "Window.h"
#include "MetaData.h"
#include "PlatformId.h"
+#include "ThemeData.h"
class SystemData
{
@@ -21,9 +22,11 @@ public:
inline const std::string& getStartPath() const { return mStartPath; }
inline const std::vector& getExtensions() const { return mSearchExtensions; }
inline PlatformIds::PlatformId getPlatformId() const { return mPlatformId; }
+ inline const std::shared_ptr& getTheme() const { return mTheme; }
std::string getGamelistPath() const;
- bool hasGamelist();
+ bool hasGamelist() const;
+ std::string getThemePath() const;
unsigned int getGameCount() const;
@@ -43,6 +46,7 @@ private:
std::vector mSearchExtensions;
std::string mLaunchCommand;
PlatformIds::PlatformId mPlatformId;
+ std::shared_ptr mTheme;
void populateFolder(FileData* folder);
diff --git a/src/ThemeData.cpp b/src/ThemeData.cpp
new file mode 100644
index 000000000..63f35bf5a
--- /dev/null
+++ b/src/ThemeData.cpp
@@ -0,0 +1,207 @@
+#include "ThemeData.h"
+#include "Renderer.h"
+#include "resources/Font.h"
+#include "Sound.h"
+#include "resources/TextureResource.h"
+#include "Log.h"
+#include
+#include
+#include "pugiXML/pugixml.hpp"
+
+// Defaults
+std::map ThemeData::sDefaultFonts = boost::assign::map_list_of
+ ("listFont", FontDef(0.045f, ""))
+ ("descriptionFont", FontDef(0.035f, ""));
+
+std::map ThemeData::sDefaultColors = boost::assign::map_list_of
+ ("listPrimaryColor", 0x0000FFFF)
+ ("listSecondaryColor", 0x00FF00FF)
+ ("listSelectorColor", 0x000000FF)
+ ("listSelectedColor", 0x00000000)
+ ("descriptionColor", 0x48474DFF);
+
+std::map ThemeData::sDefaultImages = boost::assign::map_list_of
+ ("backgroundImage", ImageDef("", true))
+ ("headerImage", ImageDef("", false));
+
+std::map ThemeData::sDefaultSounds = boost::assign::map_list_of
+ ("scrollSound", SoundDef(""))
+ ("gameSelectSound", SoundDef(""))
+ ("backSound", SoundDef(""))
+ ("menuOpenSound", SoundDef(""));
+
+
+
+const std::shared_ptr& ThemeData::getDefault()
+{
+ static const std::shared_ptr def = std::shared_ptr(new ThemeData());
+ return def;
+}
+
+ThemeData::ThemeData()
+{
+ setDefaults();
+
+ std::string defaultDir = getHomePath() + "/.emulationstation/es_theme_default.xml";
+ if(boost::filesystem::exists(defaultDir))
+ loadFile(defaultDir);
+}
+
+void ThemeData::setDefaults()
+{
+ mFontMap.clear();
+ mImageMap.clear();
+ mColorMap.clear();
+ mSoundMap.clear();
+
+ mFontMap = sDefaultFonts;
+ mImageMap = sDefaultImages;
+ mColorMap = sDefaultColors;
+ mSoundMap = sDefaultSounds;
+}
+
+unsigned int getHexColor(const char* str, unsigned int defaultColor)
+{
+ if(!str)
+ return defaultColor;
+
+ size_t len = strlen(str);
+ if(len != 6 && len != 8)
+ {
+ LOG(LogError) << "Invalid theme color \"" << str << "\" (must be 6 or 8 characters)";
+ return defaultColor;
+ }
+
+ unsigned int val;
+ std::stringstream ss;
+ ss << str;
+ ss >> std::hex >> val;
+
+ if(len == 6)
+ val = (val << 8) | 0xFF;
+
+ return val;
+}
+
+std::string resolvePath(const char* in, const std::string& relative)
+{
+ if(!in || in[0] == '\0')
+ return in;
+
+ boost::filesystem::path relPath(relative);
+ relPath = relPath.parent_path();
+
+ boost::filesystem::path path(in);
+
+ // we use boost filesystem here instead of just string checks because
+ // some directories could theoretically start with ~ or .
+ if(*path.begin() == "~")
+ {
+ path = getHomePath() + (in + 1);
+ }else if(*path.begin() == ".")
+ {
+ path = relPath / (in + 1);
+ }
+
+ return path.generic_string();
+}
+
+void ThemeData::loadFile(const std::string& themePath)
+{
+ if(themePath.empty() || !boost::filesystem::exists(themePath))
+ return;
+
+ pugi::xml_document doc;
+ pugi::xml_parse_result result = doc.load_file(themePath.c_str());
+ if(!result)
+ {
+ LOG(LogWarning) << "Could not parse theme file \"" << themePath << "\":\n " << result.description();
+ return;
+ }
+
+ pugi::xml_node root = doc.child("theme");
+
+ // Fonts
+ for(auto it = mFontMap.begin(); it != mFontMap.end(); it++)
+ {
+ pugi::xml_node node = root.child(it->first.c_str());
+ if(node)
+ {
+ std::string path = resolvePath(node.child("path").text().as_string(it->second.path.c_str()), themePath);
+ if(!boost::filesystem::exists(path))
+ {
+ LOG(LogWarning) << "Font \"" << path << "\" doesn't exist!";
+ path = it->second.path;
+ }
+
+ float size = node.child("size").text().as_float(it->second.size);
+ mFontMap[it->first] = FontDef(size, path);
+ root.remove_child(node);
+ }
+ }
+
+ // Images
+ for(auto it = mImageMap.begin(); it != mImageMap.end(); it++)
+ {
+ pugi::xml_node node = root.child(it->first.c_str());
+ if(node)
+ {
+ std::string path = resolvePath(node.child("path").text().as_string(it->second.path.c_str()), themePath);
+ if(!boost::filesystem::exists(path))
+ {
+ LOG(LogWarning) << "Image \"" << path << "\" doesn't exist!";
+ path = it->second.path;
+ }
+
+ bool tile = node.child("tile").text().as_bool(it->second.tile);
+ mImageMap[it->first] = ImageDef(path, tile);
+ root.remove_child(node);
+ }
+ }
+
+ // Colors
+ for(auto it = mColorMap.begin(); it != mColorMap.end(); it++)
+ {
+ pugi::xml_node node = root.child(it->first.c_str());
+ if(node)
+ {
+ mColorMap[it->first] = getHexColor(node.text().as_string(NULL), it->second);
+ root.remove_child(node);
+ }
+ }
+
+ // Sounds
+ for(auto it = mSoundMap.begin(); it != mSoundMap.end(); it++)
+ {
+ pugi::xml_node node = root.child(it->first.c_str());
+ if(node)
+ {
+ std::string path = resolvePath(node.text().as_string(it->second.path.c_str()), themePath);
+ if(!boost::filesystem::exists(path))
+ {
+ LOG(LogWarning) << "Sound \"" << path << "\" doesn't exist!";
+ path = it->second.path;
+ }
+
+ mSoundMap[it->first] = SoundDef(path);
+ root.remove_child(node);
+ }
+ }
+
+ if(root.begin() != root.end())
+ {
+ std::stringstream ss;
+ ss << "Unused theme identifiers:\n";
+ for(auto it = root.children().begin(); it != root.children().end(); it++)
+ {
+ ss << " " << it->name() << "\n";
+ }
+
+ LOG(LogWarning) << ss.str();
+ }
+}
+
+void ThemeData::playSound(const std::string& identifier) const
+{
+ mSoundMap.at(identifier).get()->play();
+}
diff --git a/src/ThemeData.h b/src/ThemeData.h
new file mode 100644
index 000000000..ea4222801
--- /dev/null
+++ b/src/ThemeData.h
@@ -0,0 +1,78 @@
+#pragma once
+
+#include
+#include