variable support for themes

This commit is contained in:
jrassa 2017-05-14 00:07:28 -04:00
parent 6722c3453a
commit 2bacc9c431
5 changed files with 153 additions and 34 deletions

View file

@ -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:
* `[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/
scroll_sound.wav
theme.xml (Default theme)
another_theme_set/
snes/
theme.xml
@ -308,6 +309,40 @@ You can now change the order in which elements are rendered by setting `zIndex`
* `text name="logoText"`
* `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
=========
@ -444,8 +479,10 @@ Reference
- The help system style for this view.
* `carousel name="systemcarousel"` -ALL
- The system logo carousel
* `image name="logo"` - PATH
* `image name="logo"` - PATH | COLOR
- 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
- 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.

View file

@ -411,15 +411,24 @@ std::string SystemData::getThemePath() const
{
// where we check for themes, in order:
// 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
fs::path localThemePath = mRootFolder->getPath() / "theme.xml";
if(fs::exists(localThemePath))
return localThemePath.generic_string();
// not in game folder, try theme sets
return ThemeData::getThemeFromCurrentSet(mThemeFolder).generic_string();
// not in game folder, try system theme in theme sets
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
@ -448,7 +457,13 @@ void SystemData::loadTheme()
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)
{
LOG(LogError) << e.what();

View file

@ -43,21 +43,27 @@ void SystemView::populate()
// make logo
if(theme->getElement("system", "logo", "image"))
{
ImageComponent* logo = new ImageComponent(mWindow, false, false);
logo->setMaxSize(Eigen::Vector2f(mCarousel.logoSize.x(), mCarousel.logoSize.y()));
logo->applyTheme((*it)->getTheme(), "system", "logo", ThemeFlags::PATH);
logo->setPosition((mCarousel.logoSize.x() - logo->getSize().x()) / 2,
(mCarousel.logoSize.y() - logo->getSize().y()) / 2); // center
e.data.logo = std::shared_ptr<GuiComponent>(logo);
std::string path = theme->getElement("system", "logo", "image")->get<std::string>("path");
ImageComponent* logoSelected = new ImageComponent(mWindow, false, false);
logoSelected->setMaxSize(Eigen::Vector2f(mCarousel.logoSize.x() * mCarousel.logoScale, mCarousel.logoSize.y() * mCarousel.logoScale));
logoSelected->applyTheme((*it)->getTheme(), "system", "logo", ThemeFlags::PATH | ThemeFlags::COLOR);
logoSelected->setPosition((mCarousel.logoSize.x() - logoSelected->getSize().x()) / 2,
(mCarousel.logoSize.y() - logoSelected->getSize().y()) / 2); // center
e.data.logoSelected = std::shared_ptr<GuiComponent>(logoSelected);
if(!path.empty() && ResourceManager::getInstance()->fileExists(path))
{
ImageComponent* logo = new ImageComponent(mWindow, false, false);
logo->setMaxSize(Eigen::Vector2f(mCarousel.logoSize.x(), mCarousel.logoSize.y()));
logo->applyTheme((*it)->getTheme(), "system", "logo", ThemeFlags::PATH | ThemeFlags::COLOR);
logo->setPosition((mCarousel.logoSize.x() - logo->getSize().x()) / 2,
(mCarousel.logoSize.y() - logo->getSize().y()) / 2); // center
e.data.logo = std::shared_ptr<GuiComponent>(logo);
}else{
ImageComponent* logoSelected = new ImageComponent(mWindow, false, false);
logoSelected->setMaxSize(Eigen::Vector2f(mCarousel.logoSize.x() * mCarousel.logoScale, mCarousel.logoSize.y() * mCarousel.logoScale));
logoSelected->applyTheme((*it)->getTheme(), "system", "logo", ThemeFlags::PATH | ThemeFlags::COLOR);
logoSelected->setPosition((mCarousel.logoSize.x() - logoSelected->getSize().x()) / 2,
(mCarousel.logoSize.y() - logoSelected->getSize().y()) / 2); // center
e.data.logoSelected = std::shared_ptr<GuiComponent>(logoSelected);
}
}
if (!e.data.logo)
{
// no logo in theme; use text
TextComponent* text = new TextComponent(mWindow,
(*it)->getName(),
@ -65,14 +71,16 @@ void SystemView::populate()
0x000000FF,
ALIGN_CENTER);
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);
TextComponent* textSelected = new TextComponent(mWindow,
(*it)->getName(),
Font::get((int)(FONT_SIZE_LARGE * 1.5)),
Font::get((int)(FONT_SIZE_LARGE * mCarousel.logoScale)),
0x000000FF,
ALIGN_CENTER);
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);
}

View file

@ -7,6 +7,7 @@
#include "Settings.h"
#include "pugixml/src/pugixml.hpp"
#include <boost/assign.hpp>
#include <boost/xpressive/xpressive.hpp>
#include "components/ImageComponent.h"
#include "components/TextComponent.h"
@ -123,7 +124,7 @@ std::map< std::string, ElementMapType > ThemeData::sElementMap = boost::assign::
namespace fs = boost::filesystem;
#define MINIMUM_THEME_FORMAT_VERSION 3
#define CURRENT_THEME_FORMAT_VERSION 4
#define CURRENT_THEME_FORMAT_VERSION 5
// helper
unsigned int getHexColor(const char* str)
@ -170,14 +171,34 @@ std::string resolvePath(const char* in, const fs::path& relative)
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()
{
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);
@ -189,6 +210,9 @@ void ThemeData::loadFile(const std::string& path)
mVersion = 0;
mViews.clear();
mVariables.clear();
mVariables.insert(sysDataMap.begin(), sysDataMap.end());
pugi::xml_document doc;
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)
throw error << "Theme uses format version " << mVersion << ". Minimum supported version is " << MINIMUM_THEME_FORMAT_VERSION << ".";
parseVariables(root);
parseIncludes(root);
parseViews(root);
parseFeatures(root);
}
void ThemeData::parseIncludes(const pugi::xml_node& root)
{
ThemeException error;
@ -238,6 +262,7 @@ void ThemeData::parseIncludes(const pugi::xml_node& root)
if(!root)
throw error << "Missing <theme> tag!";
parseVariables(root);
parseIncludes(root);
parseViews(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)
{
ThemeException error;
@ -344,12 +389,12 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map<std::str
if(typeIt == typeMap.end())
throw error << "Unknown property type \"" << node.name() << "\" (for element of type " << root.name() << ").";
std::string str = resolvePlaceholders(node.text().as_string());
switch(typeIt->second)
{
case NORMALIZED_PAIR:
{
std::string str = std::string(node.text().as_string());
size_t divider = str.find(' ');
if(divider == std::string::npos)
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;
}
case STRING:
element.properties[node.name()] = std::string(node.text().as_string());
element.properties[node.name()] = str;
break;
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))
{
std::stringstream ss;
@ -381,14 +426,25 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map<std::str
break;
}
case COLOR:
element.properties[node.name()] = getHexColor(node.text().as_string());
element.properties[node.name()] = getHexColor(str.c_str());
break;
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;
}
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;
}
default:
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
{
theme->loadFile(path);
std::map<std::string, std::string> emptyMap;
theme->loadFile(emptyMap, path);
} catch(ThemeException& e)
{
LOG(LogError) << e.what();

View file

@ -8,6 +8,7 @@
#include <string>
#include <boost/filesystem.hpp>
#include <boost/variant.hpp>
#include <boost/xpressive/xpressive.hpp>
#include <Eigen/Dense>
#include "pugixml/src/pugixml.hpp"
#include "GuiComponent.h"
@ -111,7 +112,7 @@ public:
ThemeData();
// throws ThemeException
void loadFile(const std::string& path);
void loadFile(std::map<std::string, std::string> sysDataMap, const std::string& path);
enum ElementPropertyType
{
@ -145,6 +146,7 @@ private:
void parseFeatures(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 parseView(const pugi::xml_node& viewNode, ThemeView& view);
void parseElement(const pugi::xml_node& elementNode, const std::map<std::string, ElementPropertyType>& typeMap, ThemeElement& element);