Commit a7359a2d authored by Aloshi's avatar Aloshi

Themes mostly stable, documentation updated

parent 8bfde969
......@@ -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
......
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.
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:
```
<theme>
<listPrimaryColor>0000FF</listPrimaryColor>
<listSecondaryColor>00FF00</listSecondaryColor>
<component>
<type>image</type>
<path>./theme/background.png</path>
<pos>0 0</pos>
<dim>1 1</dim>
<origin>0 0</origin>
</component>
<listFont>
<path>./../all_themes/font.ttf</path>
<size>0.045</size>
</listFont>
<component>
<type>image</type>
<path>./theme/logo.png</path>
<pos>0 0</pos>
<dim>0.4 0</dim>
<origin>0 0</origin>
</component>
</theme>
<descriptionFont>
<path>./../all_themes/font.ttf</path>
<size>0.035</size>
</descriptionFont>
<!-- You can also optionally define a "basic" theme, which is used instead if ES is in the "basic" view (no box art) -->
<basicTheme>
<listPrimaryColor>0000FF</listPrimaryColor>
<listSecondaryColor>00FF00</listSecondaryColor>
<component>
<type>image</type>
<backgroundImage>
<path>./theme/background.png</path>
<pos>0 0</pos>
<dim>1 1</dim>
<origin>0 0</origin>
</component>
</basicTheme>
```
Themes can be defined with two tags: `<theme>` and `<themeBasic>`.
You can define both a normal and basic theme in the same file.
If EmulationStation is running in "basic" mode, it will try to use `<themeBasic>`. If that doesn't exist or ES is running in "detailed" mode (a gamelist.xml is present), `<theme>` 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>` - path to the image file. Most common file types are supported, and . and ~ are properly expanded.
`<pos>` - the position, as two screen percentages, at which to display the image.
`<dim>` - the dimensions, as two screen percentages, that the image will be resized to. Make one axis 0 to keep the aspect ratio.
`<origin>` - the point on the image that `<pos>` defines, as an image percentage. "0.5 0.5", the center of the image, by default.
`<tiled />` - 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 `<theme>` tree - for example, they can't be inside a component tag. They are not required.
**Game list attributes:**
`<listPrimaryColor>` - the hex font color to use for games on the GuiGameList.
`<listSecondaryColor>` - the hex font color to use for folders on the GuiGameList.
`<descColor>` - the hex font color to use for the description on the GuiGameList.
`<listSelectorColor>` - the hex color to use for the "selector bar" on the GuiGameList. Default is `000000FF`.
`<listSelectedColor>` - the hex color to use for selected text on the GuiGameList. Default is zero, which means no change.
<tile>true</tile>
</backgroundImage>
`<listLeftAlign />` - if present, the games list names will be left aligned to the value of `<listOffsetX>` + `<listTextOffsetX>`. On by default for detailed themes.
`<hideHeader />` - 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.
`<hideDividers />` - if present, the divider between games on the detailed GuiGameList won't be displayed.
`<listOffsetX>` - the percentage to offset the list by. Default is 0.5 (half the screen). **Will also move the selector bar**.
`<listTextOffsetX>` - the percentage to offset the text in the list by. Default is 0.005. Only works in combination with `<listLeftAlign />`.
**Game image attributes:**
`<gameImagePos>` - two values for the position of the game art, in the form of `[x] [y]`, as a percentage. Default is `$infoWidth/2 $headerHeight`.
`<gameImageDim>` - 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.
`<gameImageOrigin>` - 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).
`<gameImageNotFound>` - path to the image to display if a game's image is missing. '.' and '~' are expanded.
**Fast Select box attributes:**
<headerImage>
<path>./theme/logo.png</path>
<tile>false</tile>
</headerImage>
`<fastSelectColor>` - the hex color to use for the letter display on the fast select box.
<scrollSound>./../all_themes/scrollSound.wav</scrollSound>
</theme>
```
`<fastSelectFrame>` - 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.
Themes must be enclosed in a `<theme>` tag.
`<fastSelectFont>` - 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:
```
<fontTag>
<path>./path/to/font</path>
<size>0.05</size>
</fontTag>
<resourceName>
<!-- Path is optional. -->
<path>./some/path/here.ttf</path>
<!-- Size is a percentage of screen height. Optional. -->
<size>0.035</size>
</resourceName>
```
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.
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.
`<listFont>` - Default size: 0.045.
`<descriptionFont>` - Default size: 0.035.
**Font tags:**
`<listFont>` - font to use for the game list.
Colors
======
`<descriptionFont>` - font to use for description text.
Colors are defined in hex, like this:
`<fastSelectFont>` - font to use for the fast select letter.
`<resourceName>00FF00FF</resourceName>`
or
`<resourceName>00FF00</resourceName>`
(without the alpha channel specified - will assume FF)
`<listPrimaryColor>` - Default: 0000FFFF.
`<listSecondaryColor>` - Default: 00FF00FF.
`<listSelectorColor>` - Default: 000000FF.
`<listSelectedColor>` - Default: 00000000.
`<descriptionColor>` - Default: 48474DFF.
Audio
=====
Images
======
Themes can also define menu sounds. These tags go in the root of the `<theme>` tree, just like Display tags. Sounds should be in the .wav format. The relative path operator (.) and home operator (~) are properly expanded.
Images are defined like this:
```
<resourceName>
<path>./some/path/here.png</path>
<!-- Can be true or false. -->
<tile>true</tile>
</resourceName>
```
Pretty much any image format is supported.
`<menuScrollSound>` - path to the sound to play when the game list or fast select menu is scrolling.
`<backgroundImage>` - No default.
`<headerImage>` - No default.
`<menuSelectSound>` - path to the sound to play when the user selects something from the game list.
Sounds
======
`<menuBackSound>` - path to the sound to play when the user "goes up" from a folder in the game list.
Sounds are defined like this:
`<resourceName>./some/path/here.wav</resourceName>`
Only .wav files are supported.
`<menuOpenSound>` - path to the sound to play when the user opens a menu (either the "main menu" or the fast select menu).
`<scrollSound>` - No default.
`<gameSelectSound>` - No default.
`<backSound>` - No default.
`<menuOpenSound>` - 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 `<dim>$infoWidth 1</dim>`.
`$headerHeight` - height of the system name header.
`$infoWidth` - where the left of the game list begins. Will follow `<listOffsetX>`.
-Aloshi
http://www.aloshi.com
......@@ -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
......
......@@ -5,12 +5,21 @@
#include <boost/filesystem.hpp>
#include "MetaData.h"
class SystemData;
enum FileType
{
GAME = 1, // Cannot have children.
FOLDER = 2
};
enum FileChangeType
{
FILE_ADDED,
FILE_METADATA_CHANGED,
FILE_REMOVED
};
// 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<FileData*>& 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<FileData*> mChildren;
};
......@@ -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!";
......
......@@ -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<ThemeData>();
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()));
}
......
......@@ -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<std::string>& getExtensions() const { return mSearchExtensions; }
inline PlatformIds::PlatformId getPlatformId() const { return mPlatformId; }
inline const std::shared_ptr<ThemeData>& 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<std::string> mSearchExtensions;
std::string mLaunchCommand;
PlatformIds::PlatformId mPlatformId;
std::shared_ptr<ThemeData> mTheme;
void populateFolder(FileData* folder);
......
#include "ThemeData.h"
#include "Renderer.h"
#include "resources/Font.h"
#include "Sound.h"
#include "resources/TextureResource.h"
#include "Log.h"
#include <boost/filesystem.hpp>
#include <boost/assign.hpp>
#include "pugiXML/pugixml.hpp"
// Defaults
std::map<std::string, FontDef > ThemeData::sDefaultFonts = boost::assign::map_list_of
("listFont", FontDef(0.045f, ""))
("descriptionFont", FontDef(0.035f, ""));
std::map<std::string, unsigned int> ThemeData::sDefaultColors = boost::assign::map_list_of
("listPrimaryColor", 0x0000FFFF)
("listSecondaryColor", 0x00FF00FF)
("listSelectorColor", 0x000000FF)
("listSelectedColor", 0x00000000)
("descriptionColor", 0x48474DFF);
std::map<std::string, ImageDef> ThemeData::sDefaultImages = boost::assign::map_list_of
("backgroundImage", ImageDef("", true))
("headerImage", ImageDef("", false));
std::map<std::string, SoundDef> ThemeData::sDefaultSounds = boost::assign::map_list_of
("scrollSound", SoundDef(""))
("gameSelectSound", SoundDef(""))
("backSound", SoundDef(""))
("menuOpenSound", SoundDef(""));
const std::shared_ptr<ThemeData>& ThemeData::getDefault()
{
static const std::shared_ptr<ThemeData> def = std::shared_ptr<ThemeData>(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;