From 2bacc9c43193ce63d5d4dc3d466f6d0f12f16227 Mon Sep 17 00:00:00 2001 From: jrassa Date: Sun, 14 May 2017 00:07:28 -0400 Subject: [PATCH] variable support for themes --- THEMES.md | 41 ++++++++++++++++- es-app/src/SystemData.cpp | 23 ++++++++-- es-app/src/views/SystemView.cpp | 40 ++++++++++------- es-core/src/ThemeData.cpp | 79 ++++++++++++++++++++++++++++----- es-core/src/ThemeData.h | 4 +- 5 files changed, 153 insertions(+), 34 deletions(-) diff --git a/THEMES.md b/THEMES.md index 1cd0f5f65..fec2e61aa 100644 --- a/THEMES.md +++ b/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 `` 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. +``` + + 8b0000 + +``` + +#### Usage in themes +Variables can be used to specify the value of a theme property: +``` +${themeColor} +``` + +or to specify only a portion of the value of a theme property: + +``` +${themeColor}c0 +./art/logo/${system.theme}.svg +```` + 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. diff --git a/es-app/src/SystemData.cpp b/es-app/src/SystemData.cpp index 41efa64cb..d41577c32 100644 --- a/es-app/src/SystemData.cpp +++ b/es-app/src/SystemData.cpp @@ -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 sysData; + sysData.insert(std::pair("system.name", getName())); + sysData.insert(std::pair("system.theme", getThemeFolder())); + sysData.insert(std::pair("system.fullName", getFullName())); + + mTheme->loadFile(sysData, path); } catch(ThemeException& e) { LOG(LogError) << e.what(); diff --git a/es-app/src/views/SystemView.cpp b/es-app/src/views/SystemView.cpp index 3252c8a5f..e4635c55d 100644 --- a/es-app/src/views/SystemView.cpp +++ b/es-app/src/views/SystemView.cpp @@ -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(logo); + std::string path = theme->getElement("system", "logo", "image")->get("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(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(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(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(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(textSelected); } diff --git a/es-core/src/ThemeData.cpp b/es-core/src/ThemeData.cpp index ff92d6028..43b76deb6 100644 --- a/es-core/src/ThemeData.cpp +++ b/es-core/src/ThemeData.cpp @@ -7,6 +7,7 @@ #include "Settings.h" #include "pugixml/src/pugixml.hpp" #include +#include #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 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 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 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(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::mapsecond) { 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::mapfileExists(path)) { std::stringstream ss; @@ -381,14 +426,25 @@ void ThemeData::parseElement(const pugi::xml_node& root, const std::map(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::getDefault() { try { - theme->loadFile(path); + std::map emptyMap; + theme->loadFile(emptyMap, path); } catch(ThemeException& e) { LOG(LogError) << e.what(); diff --git a/es-core/src/ThemeData.h b/es-core/src/ThemeData.h index e0e7bb129..a4e483b62 100644 --- a/es-core/src/ThemeData.h +++ b/es-core/src/ThemeData.h @@ -8,6 +8,7 @@ #include #include #include +#include #include #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 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& typeMap, ThemeElement& element);