mirror of
https://github.com/RetroDECK/ES-DE.git
synced 2024-11-21 21: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:
|
||||
* `[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.
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)),
|
||||
TextComponent* textSelected = new TextComponent(mWindow,
|
||||
(*it)->getName(),
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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);
|
||||
|
|
Loading…
Reference in a new issue