mirror of
https://github.com/RetroDECK/ES-DE.git
synced 2024-11-25 23:55:38 +00:00
variable support for themes
This commit is contained in:
parent
6722c3453a
commit
2bacc9c431
41
THEMES.md
41
THEMES.md
|
@ -6,7 +6,7 @@ EmulationStation allows each system to have its own "theme." A theme is a collec
|
||||||
The first place ES will check for a theme is in the system's `<path>` folder, for a theme.xml file:
|
The first place ES will check for a theme is in the system's `<path>` folder, for a theme.xml file:
|
||||||
* `[SYSTEM_PATH]/theme.xml`
|
* `[SYSTEM_PATH]/theme.xml`
|
||||||
|
|
||||||
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:
|
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. A theme set can provide a default theme that will be used if there is no matching system theme. Here's an example:
|
||||||
|
|
||||||
```
|
```
|
||||||
...
|
...
|
||||||
|
@ -23,6 +23,7 @@ If that file doesn't exist, ES will try to find the theme in the current **theme
|
||||||
common_resources/
|
common_resources/
|
||||||
scroll_sound.wav
|
scroll_sound.wav
|
||||||
|
|
||||||
|
theme.xml (Default theme)
|
||||||
another_theme_set/
|
another_theme_set/
|
||||||
snes/
|
snes/
|
||||||
theme.xml
|
theme.xml
|
||||||
|
@ -308,6 +309,40 @@ You can now change the order in which elements are rendered by setting `zIndex`
|
||||||
* `text name="logoText"`
|
* `text name="logoText"`
|
||||||
* `image name="logo"`
|
* `image name="logo"`
|
||||||
|
|
||||||
|
### Theme variables
|
||||||
|
|
||||||
|
Theme variables can be used to simplify theme construction. There are 2 types of variables available.
|
||||||
|
* System Variables
|
||||||
|
* Theme Defined Variables
|
||||||
|
|
||||||
|
#### System Variables
|
||||||
|
|
||||||
|
System variables are system specific and are derived from the values in es_systems.cfg.
|
||||||
|
* `system.name`
|
||||||
|
* `system.fullName`
|
||||||
|
* `system.theme`
|
||||||
|
|
||||||
|
#### Theme Defined Variables
|
||||||
|
Variables can also be defined in the theme.
|
||||||
|
```
|
||||||
|
<variables>
|
||||||
|
<themeColor>8b0000</themeColor>
|
||||||
|
</variables>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Usage in themes
|
||||||
|
Variables can be used to specify the value of a theme property:
|
||||||
|
```
|
||||||
|
<color>${themeColor}</color>
|
||||||
|
```
|
||||||
|
|
||||||
|
or to specify only a portion of the value of a theme property:
|
||||||
|
|
||||||
|
```
|
||||||
|
<color>${themeColor}c0</color>
|
||||||
|
<path>./art/logo/${system.theme}.svg</path>
|
||||||
|
````
|
||||||
|
|
||||||
Reference
|
Reference
|
||||||
=========
|
=========
|
||||||
|
|
||||||
|
@ -444,8 +479,10 @@ Reference
|
||||||
- The help system style for this view.
|
- The help system style for this view.
|
||||||
* `carousel name="systemcarousel"` -ALL
|
* `carousel name="systemcarousel"` -ALL
|
||||||
- The system logo carousel
|
- The system logo carousel
|
||||||
* `image name="logo"` - PATH
|
* `image name="logo"` - PATH | COLOR
|
||||||
- A logo image, to be displayed in the system logo carousel.
|
- A logo image, to be displayed in the system logo carousel.
|
||||||
|
* `text name="logoText"` - FONT_PATH | COLOR | FORCE_UPPERCASE
|
||||||
|
- A logo text, to be displayed system name in the system logo carousel when no logo is available.
|
||||||
* `text name="systemInfo"` - ALL
|
* `text name="systemInfo"` - ALL
|
||||||
- Displays details of the system currently selected in the carousel.
|
- 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.
|
* 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.
|
||||||
|
|
|
@ -411,15 +411,24 @@ std::string SystemData::getThemePath() const
|
||||||
{
|
{
|
||||||
// where we check for themes, in order:
|
// where we check for themes, in order:
|
||||||
// 1. [SYSTEM_PATH]/theme.xml
|
// 1. [SYSTEM_PATH]/theme.xml
|
||||||
// 2. currently selected theme set
|
// 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
|
// first, check game folder
|
||||||
fs::path localThemePath = mRootFolder->getPath() / "theme.xml";
|
fs::path localThemePath = mRootFolder->getPath() / "theme.xml";
|
||||||
if(fs::exists(localThemePath))
|
if(fs::exists(localThemePath))
|
||||||
return localThemePath.generic_string();
|
return localThemePath.generic_string();
|
||||||
|
|
||||||
// not in game folder, try theme sets
|
// not in game folder, try system theme in theme sets
|
||||||
return ThemeData::getThemeFromCurrentSet(mThemeFolder).generic_string();
|
localThemePath = ThemeData::getThemeFromCurrentSet(mThemeFolder);
|
||||||
|
|
||||||
|
if (fs::exists(localThemePath))
|
||||||
|
return localThemePath.generic_string();
|
||||||
|
|
||||||
|
// not system theme, try default system theme in theme set
|
||||||
|
localThemePath = localThemePath.parent_path().parent_path() / "theme.xml";
|
||||||
|
|
||||||
|
return localThemePath.generic_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool SystemData::hasGamelist() const
|
bool SystemData::hasGamelist() const
|
||||||
|
@ -448,7 +457,13 @@ void SystemData::loadTheme()
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
mTheme->loadFile(path);
|
// build map with system variables for theme to use,
|
||||||
|
std::map<std::string, std::string> sysData;
|
||||||
|
sysData.insert(std::pair<std::string, std::string>("system.name", getName()));
|
||||||
|
sysData.insert(std::pair<std::string, std::string>("system.theme", getThemeFolder()));
|
||||||
|
sysData.insert(std::pair<std::string, std::string>("system.fullName", getFullName()));
|
||||||
|
|
||||||
|
mTheme->loadFile(sysData, path);
|
||||||
} catch(ThemeException& e)
|
} catch(ThemeException& e)
|
||||||
{
|
{
|
||||||
LOG(LogError) << e.what();
|
LOG(LogError) << e.what();
|
||||||
|
|
|
@ -42,10 +42,14 @@ void SystemView::populate()
|
||||||
|
|
||||||
// make logo
|
// make logo
|
||||||
if(theme->getElement("system", "logo", "image"))
|
if(theme->getElement("system", "logo", "image"))
|
||||||
|
{
|
||||||
|
std::string path = theme->getElement("system", "logo", "image")->get<std::string>("path");
|
||||||
|
|
||||||
|
if(!path.empty() && ResourceManager::getInstance()->fileExists(path))
|
||||||
{
|
{
|
||||||
ImageComponent* logo = new ImageComponent(mWindow, false, false);
|
ImageComponent* logo = new ImageComponent(mWindow, false, false);
|
||||||
logo->setMaxSize(Eigen::Vector2f(mCarousel.logoSize.x(), mCarousel.logoSize.y()));
|
logo->setMaxSize(Eigen::Vector2f(mCarousel.logoSize.x(), mCarousel.logoSize.y()));
|
||||||
logo->applyTheme((*it)->getTheme(), "system", "logo", ThemeFlags::PATH);
|
logo->applyTheme((*it)->getTheme(), "system", "logo", ThemeFlags::PATH | ThemeFlags::COLOR);
|
||||||
logo->setPosition((mCarousel.logoSize.x() - logo->getSize().x()) / 2,
|
logo->setPosition((mCarousel.logoSize.x() - logo->getSize().x()) / 2,
|
||||||
(mCarousel.logoSize.y() - logo->getSize().y()) / 2); // center
|
(mCarousel.logoSize.y() - logo->getSize().y()) / 2); // center
|
||||||
e.data.logo = std::shared_ptr<GuiComponent>(logo);
|
e.data.logo = std::shared_ptr<GuiComponent>(logo);
|
||||||
|
@ -56,8 +60,10 @@ void SystemView::populate()
|
||||||
logoSelected->setPosition((mCarousel.logoSize.x() - logoSelected->getSize().x()) / 2,
|
logoSelected->setPosition((mCarousel.logoSize.x() - logoSelected->getSize().x()) / 2,
|
||||||
(mCarousel.logoSize.y() - logoSelected->getSize().y()) / 2); // center
|
(mCarousel.logoSize.y() - logoSelected->getSize().y()) / 2); // center
|
||||||
e.data.logoSelected = std::shared_ptr<GuiComponent>(logoSelected);
|
e.data.logoSelected = std::shared_ptr<GuiComponent>(logoSelected);
|
||||||
|
}
|
||||||
}else{
|
}
|
||||||
|
if (!e.data.logo)
|
||||||
|
{
|
||||||
// no logo in theme; use text
|
// no logo in theme; use text
|
||||||
TextComponent* text = new TextComponent(mWindow,
|
TextComponent* text = new TextComponent(mWindow,
|
||||||
(*it)->getName(),
|
(*it)->getName(),
|
||||||
|
@ -65,14 +71,16 @@ void SystemView::populate()
|
||||||
0x000000FF,
|
0x000000FF,
|
||||||
ALIGN_CENTER);
|
ALIGN_CENTER);
|
||||||
text->setSize(mCarousel.logoSize);
|
text->setSize(mCarousel.logoSize);
|
||||||
|
text->applyTheme((*it)->getTheme(), "system", "logoText", ThemeFlags::FONT_PATH | ThemeFlags::COLOR | ThemeFlags::FORCE_UPPERCASE);
|
||||||
e.data.logo = std::shared_ptr<GuiComponent>(text);
|
e.data.logo = std::shared_ptr<GuiComponent>(text);
|
||||||
|
|
||||||
TextComponent* textSelected = new TextComponent(mWindow,
|
TextComponent* textSelected = new TextComponent(mWindow,
|
||||||
(*it)->getName(),
|
(*it)->getName(),
|
||||||
Font::get((int)(FONT_SIZE_LARGE * 1.5)),
|
Font::get((int)(FONT_SIZE_LARGE * mCarousel.logoScale)),
|
||||||
0x000000FF,
|
0x000000FF,
|
||||||
ALIGN_CENTER);
|
ALIGN_CENTER);
|
||||||
textSelected->setSize(mCarousel.logoSize);
|
textSelected->setSize(mCarousel.logoSize);
|
||||||
|
textSelected->applyTheme((*it)->getTheme(), "system", "logoText", ThemeFlags::FONT_PATH | ThemeFlags::COLOR | ThemeFlags::FORCE_UPPERCASE);
|
||||||
e.data.logoSelected = std::shared_ptr<GuiComponent>(textSelected);
|
e.data.logoSelected = std::shared_ptr<GuiComponent>(textSelected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@
|
||||||
#include "Settings.h"
|
#include "Settings.h"
|
||||||
#include "pugixml/src/pugixml.hpp"
|
#include "pugixml/src/pugixml.hpp"
|
||||||
#include <boost/assign.hpp>
|
#include <boost/assign.hpp>
|
||||||
|
#include <boost/xpressive/xpressive.hpp>
|
||||||
|
|
||||||
#include "components/ImageComponent.h"
|
#include "components/ImageComponent.h"
|
||||||
#include "components/TextComponent.h"
|
#include "components/TextComponent.h"
|
||||||
|
@ -123,7 +124,7 @@ std::map< std::string, ElementMapType > ThemeData::sElementMap = boost::assign::
|
||||||
namespace fs = boost::filesystem;
|
namespace fs = boost::filesystem;
|
||||||
|
|
||||||
#define MINIMUM_THEME_FORMAT_VERSION 3
|
#define MINIMUM_THEME_FORMAT_VERSION 3
|
||||||
#define CURRENT_THEME_FORMAT_VERSION 4
|
#define CURRENT_THEME_FORMAT_VERSION 5
|
||||||
|
|
||||||
// helper
|
// helper
|
||||||
unsigned int getHexColor(const char* str)
|
unsigned int getHexColor(const char* str)
|
||||||
|
@ -170,14 +171,34 @@ std::string resolvePath(const char* in, const fs::path& relative)
|
||||||
return path.generic_string();
|
return path.generic_string();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
std::map<std::string, std::string> mVariables;
|
||||||
|
|
||||||
|
std::string &format_variables(const boost::xpressive::smatch &what)
|
||||||
|
{
|
||||||
|
return mVariables[what[1].str()];
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string resolvePlaceholders(const char* in)
|
||||||
|
{
|
||||||
|
if(!in || in[0] == '\0')
|
||||||
|
return std::string(in);
|
||||||
|
|
||||||
|
std::string inStr(in);
|
||||||
|
|
||||||
|
using namespace boost::xpressive;
|
||||||
|
sregex rex = "${" >> (s1 = +('.' | _w)) >> '}';
|
||||||
|
|
||||||
|
std::string output = regex_replace(inStr, rex, format_variables);
|
||||||
|
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
ThemeData::ThemeData()
|
ThemeData::ThemeData()
|
||||||
{
|
{
|
||||||
mVersion = 0;
|
mVersion = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThemeData::loadFile(const std::string& path)
|
void ThemeData::loadFile(std::map<std::string, std::string> sysDataMap, const std::string& path)
|
||||||
{
|
{
|
||||||
mPaths.push_back(path);
|
mPaths.push_back(path);
|
||||||
|
|
||||||
|
@ -189,6 +210,9 @@ void ThemeData::loadFile(const std::string& path)
|
||||||
|
|
||||||
mVersion = 0;
|
mVersion = 0;
|
||||||
mViews.clear();
|
mViews.clear();
|
||||||
|
mVariables.clear();
|
||||||
|
|
||||||
|
mVariables.insert(sysDataMap.begin(), sysDataMap.end());
|
||||||
|
|
||||||
pugi::xml_document doc;
|
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());
|
||||||
|
@ -207,12 +231,12 @@ void ThemeData::loadFile(const std::string& path)
|
||||||
if(mVersion < MINIMUM_THEME_FORMAT_VERSION)
|
if(mVersion < MINIMUM_THEME_FORMAT_VERSION)
|
||||||
throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << ".";
|
throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << ".";
|
||||||
|
|
||||||
|
parseVariables(root);
|
||||||
parseIncludes(root);
|
parseIncludes(root);
|
||||||
parseViews(root);
|
parseViews(root);
|
||||||
parseFeatures(root);
|
parseFeatures(root);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void ThemeData::parseIncludes(const pugi::xml_node& root)
|
void ThemeData::parseIncludes(const pugi::xml_node& root)
|
||||||
{
|
{
|
||||||
ThemeException error;
|
ThemeException error;
|
||||||
|
@ -238,6 +262,7 @@ void ThemeData::parseIncludes(const pugi::xml_node& root)
|
||||||
if(!root)
|
if(!root)
|
||||||
throw error << "Missing <theme> tag!";
|
throw error << "Missing <theme> tag!";
|
||||||
|
|
||||||
|
parseVariables(root);
|
||||||
parseIncludes(root);
|
parseIncludes(root);
|
||||||
parseViews(root);
|
parseViews(root);
|
||||||
parseFeatures(root);
|
parseFeatures(root);
|
||||||
|
@ -265,6 +290,26 @@ void ThemeData::parseFeatures(const pugi::xml_node& root)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<std::string, std::string>(key, val));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void ThemeData::parseViews(const pugi::xml_node& root)
|
void ThemeData::parseViews(const pugi::xml_node& root)
|
||||||
{
|
{
|
||||||
ThemeException error;
|
ThemeException error;
|
||||||
|
@ -344,12 +389,12 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map<std::str
|
||||||
if(typeIt == typeMap.end())
|
if(typeIt == typeMap.end())
|
||||||
throw error << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ").";
|
throw error << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ").";
|
||||||
|
|
||||||
|
std::string str = resolvePlaceholders(node.text().as_string());
|
||||||
|
|
||||||
switch(typeIt->second)
|
switch(typeIt->second)
|
||||||
{
|
{
|
||||||
case NORMALIZED_PAIR:
|
case NORMALIZED_PAIR:
|
||||||
{
|
{
|
||||||
std::string str = std::string(node.text().as_string());
|
|
||||||
|
|
||||||
size_t divider = str.find(' ');
|
size_t divider = str.find(' ');
|
||||||
if(divider == std::string::npos)
|
if(divider == std::string::npos)
|
||||||
throw error << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")";
|
throw error << "invalid normalized pair (property \"" << node.name() << "\", value \"" << str.c_str() << "\")";
|
||||||
|
@ -363,11 +408,11 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map<std::str
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case STRING:
|
case STRING:
|
||||||
element.properties[node.name()] = std::string(node.text().as_string());
|
element.properties[node.name()] = str;
|
||||||
break;
|
break;
|
||||||
case PATH:
|
case PATH:
|
||||||
{
|
{
|
||||||
std::string path = resolvePath(node.text().as_string(), mPaths.back().string());
|
std::string path = resolvePath(str.c_str(), mPaths.back().string());
|
||||||
if(!ResourceManager::getInstance()->fileExists(path))
|
if(!ResourceManager::getInstance()->fileExists(path))
|
||||||
{
|
{
|
||||||
std::stringstream ss;
|
std::stringstream ss;
|
||||||
|
@ -381,14 +426,25 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map<std::str
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case COLOR:
|
case COLOR:
|
||||||
element.properties[node.name()] = getHexColor(node.text().as_string());
|
element.properties[node.name()] = getHexColor(str.c_str());
|
||||||
break;
|
break;
|
||||||
case FLOAT:
|
case FLOAT:
|
||||||
element.properties[node.name()] = node.text().as_float();
|
{
|
||||||
|
float floatVal = static_cast<float>(strtod(str.c_str(), 0));
|
||||||
|
element.properties[node.name()] = floatVal;
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case BOOLEAN:
|
case BOOLEAN:
|
||||||
element.properties[node.name()] = node.text().as_bool();
|
{
|
||||||
|
// 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;
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
throw error << "Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name();
|
throw error << "Unknown ElementPropertyType for \"" << root.attribute("name").as_string() << "\", property " << node.name();
|
||||||
}
|
}
|
||||||
|
@ -432,7 +488,8 @@ const std::shared_ptr<ThemeData>& ThemeData::getDefault()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
theme->loadFile(path);
|
std::map<std::string, std::string> emptyMap;
|
||||||
|
theme->loadFile(emptyMap, path);
|
||||||
} catch(ThemeException& e)
|
} catch(ThemeException& e)
|
||||||
{
|
{
|
||||||
LOG(LogError) << e.what();
|
LOG(LogError) << e.what();
|
||||||
|
|
|
@ -8,6 +8,7 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
#include <boost/variant.hpp>
|
#include <boost/variant.hpp>
|
||||||
|
#include <boost/xpressive/xpressive.hpp>
|
||||||
#include <Eigen/Dense>
|
#include <Eigen/Dense>
|
||||||
#include "pugixml/src/pugixml.hpp"
|
#include "pugixml/src/pugixml.hpp"
|
||||||
#include "GuiComponent.h"
|
#include "GuiComponent.h"
|
||||||
|
@ -111,7 +112,7 @@ public:
|
||||||
ThemeData();
|
ThemeData();
|
||||||
|
|
||||||
// throws ThemeException
|
// throws ThemeException
|
||||||
void loadFile(const std::string& path);
|
void loadFile(std::map<std::string, std::string> sysDataMap, const std::string& path);
|
||||||
|
|
||||||
enum ElementPropertyType
|
enum ElementPropertyType
|
||||||
{
|
{
|
||||||
|
@ -145,6 +146,7 @@ private:
|
||||||
|
|
||||||
void parseFeatures(const pugi::xml_node& themeRoot);
|
void parseFeatures(const pugi::xml_node& themeRoot);
|
||||||
void parseIncludes(const pugi::xml_node& themeRoot);
|
void parseIncludes(const pugi::xml_node& themeRoot);
|
||||||
|
void parseVariables(const pugi::xml_node& root);
|
||||||
void parseViews(const pugi::xml_node& themeRoot);
|
void parseViews(const pugi::xml_node& themeRoot);
|
||||||
void parseView(const pugi::xml_node& viewNode, ThemeView& view);
|
void parseView(const pugi::xml_node& viewNode, ThemeView& view);
|
||||||
void parseElement(const pugi::xml_node& elementNode, const std::map<std::string, ElementPropertyType>& typeMap, ThemeElement& element);
|
void parseElement(const pugi::xml_node& elementNode, const std::map<std::string, ElementPropertyType>& typeMap, ThemeElement& element);
|
||||||
|
|
Loading…
Reference in a new issue