2020-09-21 17:17:34 +00:00
|
|
|
// SPDX-License-Identifier: MIT
|
2020-06-21 12:25:28 +00:00
|
|
|
//
|
2020-09-21 17:17:34 +00:00
|
|
|
// EmulationStation Desktop Edition
|
2020-06-21 12:25:28 +00:00
|
|
|
// ThemeData.cpp
|
|
|
|
//
|
2020-06-22 15:27:53 +00:00
|
|
|
// Finds available themes on the file system and loads these,
|
|
|
|
// including the parsing of individual theme components
|
|
|
|
// (includes, features, variables, views, elements).
|
2020-06-21 12:25:28 +00:00
|
|
|
//
|
|
|
|
|
2013-11-12 23:28:15 +00:00
|
|
|
#include "ThemeData.h"
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
#include "Log.h"
|
|
|
|
#include "Platform.h"
|
|
|
|
#include "Settings.h"
|
2014-01-03 14:26:39 +00:00
|
|
|
#include "components/ImageComponent.h"
|
|
|
|
#include "components/TextComponent.h"
|
2018-01-09 22:55:09 +00:00
|
|
|
#include "utils/FileSystemUtil.h"
|
2019-07-06 14:50:50 +00:00
|
|
|
#include "utils/StringUtil.h"
|
2020-09-21 17:17:34 +00:00
|
|
|
|
2018-01-29 22:50:10 +00:00
|
|
|
#include <algorithm>
|
2020-09-21 17:17:34 +00:00
|
|
|
#include <pugixml.hpp>
|
2014-01-26 22:20:21 +00:00
|
|
|
|
2021-08-17 16:41:45 +00:00
|
|
|
std::vector<std::string> ThemeData::sSupportedViews{{"all"}, {"system"}, {"basic"},
|
|
|
|
{"detailed"}, {"grid"}, {"video"}};
|
|
|
|
std::vector<std::string> ThemeData::sSupportedFeatures{
|
|
|
|
{"navigationsounds"}, {"video"}, {"carousel"}, {"z-index"}, {"visible"}};
|
|
|
|
|
|
|
|
std::map<std::string, std::map<std::string, ThemeData::ElementPropertyType>> ThemeData::sElementMap{
|
|
|
|
{"image",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"maxSize", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"rotation", FLOAT},
|
|
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
|
|
|
{"path", PATH},
|
|
|
|
{"default", PATH},
|
|
|
|
{"tile", BOOLEAN},
|
|
|
|
{"color", COLOR},
|
|
|
|
{"colorEnd", COLOR},
|
|
|
|
{"gradientType", STRING},
|
|
|
|
{"visible", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
|
|
|
{"imagegrid",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"margin", NORMALIZED_PAIR},
|
|
|
|
{"padding", NORMALIZED_RECT},
|
|
|
|
{"autoLayout", NORMALIZED_PAIR},
|
|
|
|
{"autoLayoutSelectedZoom", FLOAT},
|
|
|
|
{"gameImage", PATH},
|
|
|
|
{"folderImage", PATH},
|
|
|
|
{"imageSource", STRING},
|
|
|
|
{"scrollDirection", STRING},
|
|
|
|
{"centerSelection", BOOLEAN},
|
|
|
|
{"scrollLoop", BOOLEAN},
|
|
|
|
{"animate", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
|
|
|
{"gridtile",
|
|
|
|
{{"size", NORMALIZED_PAIR},
|
|
|
|
{"padding", NORMALIZED_PAIR},
|
|
|
|
{"imageColor", COLOR},
|
|
|
|
{"backgroundImage", PATH},
|
|
|
|
{"backgroundCornerSize", NORMALIZED_PAIR},
|
|
|
|
{"backgroundColor", COLOR},
|
|
|
|
{"backgroundCenterColor", COLOR},
|
|
|
|
{"backgroundEdgeColor", COLOR}}},
|
|
|
|
{"text",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"rotation", FLOAT},
|
|
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
|
|
|
{"text", STRING},
|
|
|
|
{"backgroundColor", COLOR},
|
|
|
|
{"fontPath", PATH},
|
|
|
|
{"fontSize", FLOAT},
|
|
|
|
{"color", COLOR},
|
|
|
|
{"alignment", STRING},
|
|
|
|
{"forceUppercase", BOOLEAN},
|
|
|
|
{"lineSpacing", FLOAT},
|
|
|
|
{"value", STRING},
|
|
|
|
{"visible", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
|
|
|
{"textlist",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"selectorHeight", FLOAT},
|
|
|
|
{"selectorOffsetY", FLOAT},
|
|
|
|
{"selectorColor", COLOR},
|
|
|
|
{"selectorColorEnd", COLOR},
|
|
|
|
{"selectorGradientType", STRING},
|
|
|
|
{"selectorImagePath", PATH},
|
|
|
|
{"selectorImageTile", BOOLEAN},
|
|
|
|
{"selectedColor", COLOR},
|
|
|
|
{"primaryColor", COLOR},
|
|
|
|
{"secondaryColor", COLOR},
|
|
|
|
{"fontPath", PATH},
|
|
|
|
{"fontSize", FLOAT},
|
|
|
|
{"scrollSound", PATH}, // For backward compatibility with old themes.
|
|
|
|
{"alignment", STRING},
|
|
|
|
{"horizontalMargin", FLOAT},
|
|
|
|
{"forceUppercase", BOOLEAN},
|
|
|
|
{"lineSpacing", FLOAT},
|
|
|
|
{"zIndex", FLOAT}}},
|
|
|
|
{"container",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"visible", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
|
|
|
{"ninepatch",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"path", PATH},
|
|
|
|
{"visible", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
|
|
|
{"datetime",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"rotation", FLOAT},
|
|
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
|
|
|
{"backgroundColor", COLOR},
|
|
|
|
{"fontPath", PATH},
|
|
|
|
{"fontSize", FLOAT},
|
|
|
|
{"color", COLOR},
|
|
|
|
{"alignment", STRING},
|
|
|
|
{"forceUppercase", BOOLEAN},
|
|
|
|
{"lineSpacing", FLOAT},
|
|
|
|
{"value", STRING},
|
|
|
|
{"format", STRING},
|
|
|
|
{"displayRelative", BOOLEAN},
|
|
|
|
{"visible", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
|
|
|
{"rating",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"rotation", FLOAT},
|
|
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
|
|
|
{"color", COLOR},
|
|
|
|
{"filledPath", PATH},
|
|
|
|
{"unfilledPath", PATH},
|
|
|
|
{"visible", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
2021-09-04 19:15:14 +00:00
|
|
|
{"badges",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"direction", STRING},
|
2021-09-05 01:40:23 +00:00
|
|
|
{"wrap", STRING},
|
|
|
|
{"justifyContent", STRING},
|
|
|
|
{"align", STRING},
|
|
|
|
{"margin", NORMALIZED_PAIR},
|
2021-09-04 19:15:14 +00:00
|
|
|
{"slots", STRING},
|
|
|
|
{"customBadgeIcon", PATH},
|
|
|
|
{"visible", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT}}},
|
2021-08-17 16:41:45 +00:00
|
|
|
{"sound", {{"path", PATH}}},
|
|
|
|
{"helpsystem",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"textColor", COLOR},
|
2021-08-22 14:43:15 +00:00
|
|
|
{"textColorDimmed", COLOR},
|
2021-08-17 16:41:45 +00:00
|
|
|
{"iconColor", COLOR},
|
2021-08-22 14:43:15 +00:00
|
|
|
{"iconColorDimmed", COLOR},
|
2021-08-17 16:41:45 +00:00
|
|
|
{"fontPath", PATH},
|
2021-08-20 15:20:05 +00:00
|
|
|
{"fontSize", FLOAT},
|
|
|
|
{"entrySpacing", FLOAT},
|
2021-08-20 15:51:36 +00:00
|
|
|
{"iconTextSpacing", FLOAT},
|
2021-08-22 15:51:19 +00:00
|
|
|
{"textStyle", STRING},
|
2021-08-23 10:56:42 +00:00
|
|
|
{"customButtonIcon", PATH}}},
|
2021-08-17 16:41:45 +00:00
|
|
|
{"navigationsounds",
|
|
|
|
{{"systembrowseSound", PATH},
|
|
|
|
{"quicksysselectSound", PATH},
|
|
|
|
{"selectSound", PATH},
|
|
|
|
{"backSound", PATH},
|
|
|
|
{"scrollSound", PATH},
|
|
|
|
{"favoriteSound", PATH},
|
|
|
|
{"launchSound", PATH}}},
|
|
|
|
{"video",
|
|
|
|
{{"pos", NORMALIZED_PAIR},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"maxSize", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"rotation", FLOAT},
|
|
|
|
{"rotationOrigin", NORMALIZED_PAIR},
|
|
|
|
{"default", PATH},
|
|
|
|
{"delay", FLOAT},
|
|
|
|
{"visible", BOOLEAN},
|
|
|
|
{"zIndex", FLOAT},
|
|
|
|
{"showSnapshotNoVideo", BOOLEAN},
|
|
|
|
{"showSnapshotDelay", BOOLEAN}}},
|
|
|
|
{"carousel",
|
|
|
|
{{"type", STRING},
|
|
|
|
{"size", NORMALIZED_PAIR},
|
|
|
|
{"pos", NORMALIZED_PAIR},
|
|
|
|
{"origin", NORMALIZED_PAIR},
|
|
|
|
{"color", COLOR},
|
|
|
|
{"colorEnd", COLOR},
|
|
|
|
{"gradientType", STRING},
|
|
|
|
{"logoScale", FLOAT},
|
|
|
|
{"logoRotation", FLOAT},
|
|
|
|
{"logoRotationOrigin", NORMALIZED_PAIR},
|
|
|
|
{"logoSize", NORMALIZED_PAIR},
|
|
|
|
{"logoAlignment", STRING},
|
|
|
|
{"maxLogoCount", FLOAT},
|
|
|
|
{"zIndex", FLOAT}}}};
|
2021-07-07 18:31:46 +00:00
|
|
|
|
2014-06-09 18:12:21 +00:00
|
|
|
#define MINIMUM_THEME_FORMAT_VERSION 3
|
2021-08-22 15:52:02 +00:00
|
|
|
#define CURRENT_THEME_FORMAT_VERSION 7
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
// Helper.
|
2020-12-16 22:59:00 +00:00
|
|
|
unsigned int getHexColor(const std::string& str)
|
2013-12-31 03:48:28 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
ThemeException error;
|
2020-12-16 22:59:00 +00:00
|
|
|
if (str == "")
|
2020-06-21 12:25:28 +00:00
|
|
|
throw error << "Empty color";
|
2013-12-31 03:48:28 +00:00
|
|
|
|
2020-12-16 22:59:00 +00:00
|
|
|
size_t len = str.size();
|
2020-06-21 12:25:28 +00:00
|
|
|
if (len != 6 && len != 8)
|
|
|
|
throw error << "Invalid color (bad length, \"" << str << "\" - must be 6 or 8)";
|
2013-12-31 03:48:28 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
unsigned int val;
|
|
|
|
std::stringstream ss;
|
|
|
|
ss << str;
|
|
|
|
ss >> std::hex >> val;
|
2013-12-31 03:48:28 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
if (len == 6)
|
|
|
|
val = (val << 8) | 0xFF;
|
2013-12-31 03:48:28 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
return val;
|
2013-12-31 03:48:28 +00:00
|
|
|
}
|
|
|
|
|
2017-05-14 04:07:28 +00:00
|
|
|
std::map<std::string, std::string> mVariables;
|
2013-12-31 03:48:28 +00:00
|
|
|
|
2020-12-16 22:59:00 +00:00
|
|
|
std::string resolvePlaceholders(const std::string& in)
|
2017-05-14 04:07:28 +00:00
|
|
|
{
|
2020-12-16 22:59:00 +00:00
|
|
|
if (in.empty())
|
|
|
|
return in;
|
2017-05-14 04:07:28 +00:00
|
|
|
|
2020-12-16 22:59:00 +00:00
|
|
|
const size_t variableBegin = in.find("${");
|
|
|
|
const size_t variableEnd = in.find("}", variableBegin);
|
2017-11-29 19:57:43 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
if ((variableBegin == std::string::npos) || (variableEnd == std::string::npos))
|
2020-12-16 22:59:00 +00:00
|
|
|
return in;
|
2017-11-29 19:57:43 +00:00
|
|
|
|
2020-12-16 22:59:00 +00:00
|
|
|
std::string prefix = in.substr(0, variableBegin);
|
|
|
|
std::string replace = in.substr(variableBegin + 2, variableEnd - (variableBegin + 2));
|
|
|
|
std::string suffix = resolvePlaceholders(in.substr(variableEnd + 1).c_str());
|
2017-11-29 19:57:43 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
return prefix + mVariables[replace] + suffix;
|
2017-05-14 04:07:28 +00:00
|
|
|
}
|
2013-12-30 23:23:34 +00:00
|
|
|
|
|
|
|
ThemeData::ThemeData()
|
2013-11-12 23:28:15 +00:00
|
|
|
{
|
2021-07-07 18:31:46 +00:00
|
|
|
// The version will be loaded from the theme file.
|
2020-06-21 12:25:28 +00:00
|
|
|
mVersion = 0;
|
2013-11-12 23:28:15 +00:00
|
|
|
}
|
|
|
|
|
2017-05-14 04:07:28 +00:00
|
|
|
void ThemeData::loadFile(std::map<std::string, std::string> sysDataMap, const std::string& path)
|
2013-11-12 23:28:15 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
mPaths.push_back(path);
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
ThemeException error;
|
|
|
|
error.setFiles(mPaths);
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
if (!Utils::FileSystem::exists(path))
|
2021-01-17 21:33:02 +00:00
|
|
|
throw error << "File does not exist";
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
mVersion = 0;
|
|
|
|
mViews.clear();
|
|
|
|
mVariables.clear();
|
2017-05-14 04:07:28 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
mVariables.insert(sysDataMap.cbegin(), sysDataMap.cend());
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
pugi::xml_document doc;
|
2021-07-07 18:31:46 +00:00
|
|
|
#if defined(_WIN64)
|
2020-07-10 16:32:23 +00:00
|
|
|
pugi::xml_parse_result res = doc.load_file(Utils::String::stringToWideString(path).c_str());
|
2021-07-07 18:31:46 +00:00
|
|
|
#else
|
2020-06-21 12:25:28 +00:00
|
|
|
pugi::xml_parse_result res = doc.load_file(path.c_str());
|
2021-07-07 18:31:46 +00:00
|
|
|
#endif
|
2020-06-21 12:25:28 +00:00
|
|
|
if (!res)
|
|
|
|
throw error << "XML parsing error: \n " << res.description();
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
pugi::xml_node root = doc.child("theme");
|
|
|
|
if (!root)
|
|
|
|
throw error << "Missing <theme> tag!";
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
// parse version
|
|
|
|
mVersion = root.child("formatVersion").text().as_float(-404);
|
|
|
|
if (mVersion == -404)
|
2021-07-07 18:31:46 +00:00
|
|
|
throw error << "<formatVersion> tag missing\n "
|
|
|
|
"It's either out of date or you need to add <formatVersion>"
|
|
|
|
<< CURRENT_THEME_FORMAT_VERSION << "</formatVersion> inside your <theme> tag.";
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
if (mVersion < MINIMUM_THEME_FORMAT_VERSION)
|
2021-07-07 18:31:46 +00:00
|
|
|
throw error << "Theme uses format version " << mVersion << ". Minimum supported version is "
|
|
|
|
<< MINIMUM_THEME_FORMAT_VERSION << ".";
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
parseVariables(root);
|
|
|
|
parseIncludes(root);
|
|
|
|
parseViews(root);
|
|
|
|
parseFeatures(root);
|
2013-11-12 23:28:15 +00:00
|
|
|
}
|
|
|
|
|
2013-12-31 03:48:28 +00:00
|
|
|
void ThemeData::parseIncludes(const pugi::xml_node& root)
|
2013-11-12 23:28:15 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
ThemeException error;
|
|
|
|
error.setFiles(mPaths);
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
for (pugi::xml_node node = root.child("include"); node; node = node.next_sibling("include")) {
|
|
|
|
std::string relPath = resolvePlaceholders(node.text().as_string());
|
|
|
|
std::string path = Utils::FileSystem::resolveRelativePath(relPath, mPaths.back(), true);
|
|
|
|
if (!ResourceManager::getInstance()->fileExists(path))
|
2021-07-07 18:31:46 +00:00
|
|
|
throw error << " -> \"" << relPath << "\" not found (resolved to \"" << path << "\")";
|
2021-02-08 19:53:39 +00:00
|
|
|
error << " -> \"" << relPath << "\"";
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
mPaths.push_back(path);
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
pugi::xml_document includeDoc;
|
2021-07-07 18:31:46 +00:00
|
|
|
#if defined(_WIN64)
|
2020-07-10 16:32:23 +00:00
|
|
|
pugi::xml_parse_result result =
|
2021-07-07 18:31:46 +00:00
|
|
|
includeDoc.load_file(Utils::String::stringToWideString(path).c_str());
|
|
|
|
#else
|
2020-06-21 12:25:28 +00:00
|
|
|
pugi::xml_parse_result result = includeDoc.load_file(path.c_str());
|
2021-07-07 18:31:46 +00:00
|
|
|
#endif
|
2020-06-21 12:25:28 +00:00
|
|
|
if (!result)
|
2021-02-08 19:53:39 +00:00
|
|
|
throw error << ": Error parsing file: " << result.description();
|
2013-12-31 03:48:28 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
pugi::xml_node theme = includeDoc.child("theme");
|
|
|
|
if (!theme)
|
2021-02-08 19:53:39 +00:00
|
|
|
throw error << "Missing <theme> tag ";
|
2013-12-30 23:23:34 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
parseVariables(theme);
|
|
|
|
parseIncludes(theme);
|
|
|
|
parseViews(theme);
|
|
|
|
parseFeatures(theme);
|
2013-12-31 03:48:28 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
mPaths.pop_back();
|
|
|
|
}
|
2013-11-12 23:28:15 +00:00
|
|
|
}
|
|
|
|
|
2017-03-10 18:49:15 +00:00
|
|
|
void ThemeData::parseFeatures(const pugi::xml_node& root)
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
ThemeException error;
|
|
|
|
error.setFiles(mPaths);
|
2017-03-10 18:49:15 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
for (pugi::xml_node node = root.child("feature"); node; node = node.next_sibling("feature")) {
|
|
|
|
if (!node.attribute("supported"))
|
|
|
|
throw error << "Feature missing \"supported\" attribute!";
|
2017-03-10 18:49:15 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
const std::string supportedAttr = node.attribute("supported").as_string();
|
2017-03-10 18:49:15 +00:00
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
if (std::find(sSupportedFeatures.cbegin(), sSupportedFeatures.cend(), supportedAttr) !=
|
|
|
|
sSupportedFeatures.cend()) {
|
2020-06-21 12:25:28 +00:00
|
|
|
parseViews(node);
|
2021-07-07 18:31:46 +00:00
|
|
|
}
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
2017-03-10 18:49:15 +00:00
|
|
|
}
|
|
|
|
|
2017-05-14 04:07:28 +00:00
|
|
|
void ThemeData::parseVariables(const pugi::xml_node& root)
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
ThemeException error;
|
|
|
|
error.setFiles(mPaths);
|
2019-08-25 15:23:02 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
pugi::xml_node variables = root.child("variables");
|
2017-05-14 04:07:28 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
if (!variables)
|
|
|
|
return;
|
2019-08-25 15:23:02 +00:00
|
|
|
|
2021-01-25 17:07:11 +00:00
|
|
|
for (pugi::xml_node_iterator it = variables.begin(); it != variables.end(); it++) {
|
2020-06-21 12:25:28 +00:00
|
|
|
std::string key = it->name();
|
|
|
|
std::string val = it->text().as_string();
|
2017-05-14 04:07:28 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
if (!val.empty())
|
|
|
|
mVariables.insert(std::pair<std::string, std::string>(key, val));
|
|
|
|
}
|
2017-05-14 04:07:28 +00:00
|
|
|
}
|
|
|
|
|
2013-12-31 03:48:28 +00:00
|
|
|
void ThemeData::parseViews(const pugi::xml_node& root)
|
2013-11-12 23:28:15 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
ThemeException error;
|
|
|
|
error.setFiles(mPaths);
|
|
|
|
|
|
|
|
// Parse views.
|
|
|
|
for (pugi::xml_node node = root.child("view"); node; node = node.next_sibling("view")) {
|
|
|
|
if (!node.attribute("name"))
|
|
|
|
throw error << "View missing \"name\" attribute!";
|
|
|
|
|
2020-12-16 22:59:00 +00:00
|
|
|
const std::string delim = " \t\r\n,";
|
2020-06-21 12:25:28 +00:00
|
|
|
const std::string nameAttr = node.attribute("name").as_string();
|
|
|
|
size_t prevOff = nameAttr.find_first_not_of(delim, 0);
|
|
|
|
size_t off = nameAttr.find_first_of(delim, prevOff);
|
|
|
|
std::string viewKey;
|
2020-07-13 18:58:25 +00:00
|
|
|
while (off != std::string::npos || prevOff != std::string::npos) {
|
2020-06-21 12:25:28 +00:00
|
|
|
viewKey = nameAttr.substr(prevOff, off - prevOff);
|
|
|
|
prevOff = nameAttr.find_first_not_of(delim, off);
|
|
|
|
off = nameAttr.find_first_of(delim, prevOff);
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
if (std::find(sSupportedViews.cbegin(), sSupportedViews.cend(), viewKey) !=
|
|
|
|
sSupportedViews.cend()) {
|
|
|
|
ThemeView& view =
|
|
|
|
mViews.insert(std::pair<std::string, ThemeView>(viewKey, ThemeView()))
|
|
|
|
.first->second;
|
2020-06-21 12:25:28 +00:00
|
|
|
parseView(node, view);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2013-11-12 23:28:15 +00:00
|
|
|
}
|
|
|
|
|
2013-12-31 03:48:28 +00:00
|
|
|
void ThemeData::parseView(const pugi::xml_node& root, ThemeView& view)
|
2013-11-12 23:28:15 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
ThemeException error;
|
|
|
|
error.setFiles(mPaths);
|
|
|
|
|
|
|
|
for (pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) {
|
|
|
|
if (!node.attribute("name"))
|
|
|
|
throw error << "Element of type \"" << node.name() << "\" missing \"name\" attribute!";
|
|
|
|
|
|
|
|
auto elemTypeIt = sElementMap.find(node.name());
|
|
|
|
if (elemTypeIt == sElementMap.cend())
|
|
|
|
throw error << "Unknown element of type \"" << node.name() << "\"!";
|
|
|
|
|
2020-12-16 22:59:00 +00:00
|
|
|
const std::string delim = " \t\r\n,";
|
2020-06-21 12:25:28 +00:00
|
|
|
const std::string nameAttr = node.attribute("name").as_string();
|
|
|
|
size_t prevOff = nameAttr.find_first_not_of(delim, 0);
|
2020-10-18 17:45:26 +00:00
|
|
|
size_t off = nameAttr.find_first_of(delim, prevOff);
|
2020-06-21 12:25:28 +00:00
|
|
|
while (off != std::string::npos || prevOff != std::string::npos) {
|
|
|
|
std::string elemKey = nameAttr.substr(prevOff, off - prevOff);
|
|
|
|
prevOff = nameAttr.find_first_not_of(delim, off);
|
|
|
|
off = nameAttr.find_first_of(delim, prevOff);
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
parseElement(
|
|
|
|
node, elemTypeIt->second,
|
|
|
|
view.elements.insert(std::pair<std::string, ThemeElement>(elemKey, ThemeElement()))
|
|
|
|
.first->second);
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
if (std::find(view.orderedKeys.cbegin(), view.orderedKeys.cend(), elemKey) ==
|
|
|
|
view.orderedKeys.cend())
|
2020-06-21 12:25:28 +00:00
|
|
|
view.orderedKeys.push_back(elemKey);
|
|
|
|
}
|
|
|
|
}
|
2013-11-12 23:28:15 +00:00
|
|
|
}
|
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
void ThemeData::parseElement(const pugi::xml_node& root,
|
2021-07-07 18:31:46 +00:00
|
|
|
const std::map<std::string, ElementPropertyType>& typeMap,
|
|
|
|
ThemeElement& element)
|
2013-12-30 23:23:34 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
ThemeException error;
|
|
|
|
error.setFiles(mPaths);
|
|
|
|
|
|
|
|
element.type = root.name();
|
|
|
|
element.extra = root.attribute("extra").as_bool(false);
|
|
|
|
|
|
|
|
for (pugi::xml_node node = root.first_child(); node; node = node.next_sibling()) {
|
|
|
|
auto typeIt = typeMap.find(node.name());
|
|
|
|
if (typeIt == typeMap.cend())
|
2021-07-07 18:31:46 +00:00
|
|
|
throw error << ": Unknown property type \"" << node.name() << "\" (for element of type "
|
|
|
|
<< root.name() << ")";
|
2020-06-21 12:25:28 +00:00
|
|
|
|
|
|
|
std::string str = resolvePlaceholders(node.text().as_string());
|
|
|
|
|
|
|
|
switch (typeIt->second) {
|
2021-07-07 18:31:46 +00:00
|
|
|
case NORMALIZED_RECT: {
|
2021-08-15 20:03:17 +00:00
|
|
|
glm::vec4 val;
|
2021-07-07 18:31:46 +00:00
|
|
|
|
|
|
|
auto splits = Utils::String::delimitedStringToVector(str, " ");
|
|
|
|
if (splits.size() == 2) {
|
2021-08-17 16:41:45 +00:00
|
|
|
val = glm::vec4{static_cast<float>(atof(splits.at(0).c_str())),
|
2021-08-15 20:03:17 +00:00
|
|
|
static_cast<float>(atof(splits.at(1).c_str())),
|
|
|
|
static_cast<float>(atof(splits.at(0).c_str())),
|
2021-08-17 16:41:45 +00:00
|
|
|
static_cast<float>(atof(splits.at(1).c_str()))};
|
2021-07-07 18:31:46 +00:00
|
|
|
}
|
|
|
|
else if (splits.size() == 4) {
|
2021-08-17 16:41:45 +00:00
|
|
|
val = glm::vec4{static_cast<float>(atof(splits.at(0).c_str())),
|
2021-08-15 20:03:17 +00:00
|
|
|
static_cast<float>(atof(splits.at(1).c_str())),
|
|
|
|
static_cast<float>(atof(splits.at(2).c_str())),
|
2021-08-17 16:41:45 +00:00
|
|
|
static_cast<float>(atof(splits.at(3).c_str()))};
|
2021-07-07 18:31:46 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
element.properties[node.name()] = val;
|
|
|
|
break;
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
2021-07-07 18:31:46 +00:00
|
|
|
case NORMALIZED_PAIR: {
|
|
|
|
size_t divider = str.find(' ');
|
|
|
|
if (divider == std::string::npos)
|
|
|
|
throw error << "invalid normalized pair (property \"" << node.name()
|
|
|
|
<< "\", value \"" << str.c_str() << "\")";
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2021-08-17 16:41:45 +00:00
|
|
|
std::string first{str.substr(0, divider)};
|
|
|
|
std::string second{str.substr(divider, std::string::npos)};
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2021-08-17 16:41:45 +00:00
|
|
|
glm::vec2 val{static_cast<float>(atof(first.c_str())),
|
|
|
|
static_cast<float>(atof(second.c_str()))};
|
2020-06-21 12:25:28 +00:00
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
element.properties[node.name()] = val;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case STRING: {
|
|
|
|
element.properties[node.name()] = str;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case PATH: {
|
|
|
|
std::string path = Utils::FileSystem::resolveRelativePath(str, mPaths.back(), true);
|
|
|
|
if (!ResourceManager::getInstance()->fileExists(path)) {
|
|
|
|
std::stringstream ss;
|
|
|
|
// "From theme yadda yadda, included file yadda yadda.
|
|
|
|
LOG(LogWarning) << error.msg << ":";
|
|
|
|
LOG(LogWarning)
|
|
|
|
<< "Could not find file \"" << node.text().get() << "\" "
|
|
|
|
<< ((node.text().get() != path) ? "which resolves to \"" + path + "\"" :
|
|
|
|
"");
|
|
|
|
}
|
2021-08-23 10:56:42 +00:00
|
|
|
|
2021-09-04 19:15:14 +00:00
|
|
|
// Special parsing instruction for recurring options.
|
|
|
|
// Store as it's attribute to prevent nodes overwriting each other.
|
2021-08-23 10:56:42 +00:00
|
|
|
if (strcmp(node.name(), "customButtonIcon") == 0) {
|
|
|
|
const auto btn = node.attribute("button").as_string("");
|
2021-08-23 18:57:11 +00:00
|
|
|
if (strcmp(btn, "") == 0)
|
2021-08-23 10:56:42 +00:00
|
|
|
LOG(LogError)
|
|
|
|
<< "<customButtonIcon> element requires the `button` property.";
|
|
|
|
else
|
|
|
|
element.properties[btn] = path;
|
|
|
|
}
|
2021-09-04 19:15:14 +00:00
|
|
|
else if (strcmp(node.name(), "customBadgeIcon") == 0) {
|
|
|
|
const auto btn = node.attribute("badge").as_string("");
|
|
|
|
if (strcmp(btn, "") == 0)
|
|
|
|
LOG(LogError) << "<customBadgeIcon> element requires the `badge` property.";
|
|
|
|
else
|
|
|
|
element.properties[btn] = path;
|
|
|
|
}
|
2021-08-23 10:56:42 +00:00
|
|
|
else
|
|
|
|
element.properties[node.name()] = path;
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
case COLOR: {
|
|
|
|
element.properties[node.name()] = getHexColor(str);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case FLOAT: {
|
|
|
|
float floatVal = static_cast<float>(strtod(str.c_str(), 0));
|
|
|
|
element.properties[node.name()] = floatVal;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case BOOLEAN: {
|
|
|
|
// 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();
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2013-11-12 23:28:15 +00:00
|
|
|
}
|
2013-12-31 03:48:28 +00:00
|
|
|
|
2017-02-25 04:19:29 +00:00
|
|
|
bool ThemeData::hasView(const std::string& view)
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
auto viewIt = mViews.find(view);
|
|
|
|
return (viewIt != mViews.cend());
|
2017-02-25 04:19:29 +00:00
|
|
|
}
|
2014-01-01 05:39:22 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
const ThemeData::ThemeElement* ThemeData::getElement(const std::string& view,
|
2021-07-07 18:31:46 +00:00
|
|
|
const std::string& element,
|
|
|
|
const std::string& expectedType) const
|
2014-01-01 05:39:22 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
auto viewIt = mViews.find(view);
|
|
|
|
if (viewIt == mViews.cend())
|
|
|
|
return nullptr; // Not found.
|
2014-01-01 05:39:22 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
auto elemIt = viewIt->second.elements.find(element);
|
2020-10-18 17:45:26 +00:00
|
|
|
if (elemIt == viewIt->second.elements.cend())
|
|
|
|
return nullptr;
|
2014-01-01 05:39:22 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
if (elemIt->second.type != expectedType && !expectedType.empty()) {
|
2021-07-07 18:31:46 +00:00
|
|
|
LOG(LogWarning) << " requested mismatched theme type for [" << view << "." << element
|
|
|
|
<< "] - expected \"" << expectedType << "\", got \"" << elemIt->second.type
|
|
|
|
<< "\"";
|
2020-06-21 12:25:28 +00:00
|
|
|
return nullptr;
|
|
|
|
}
|
2014-01-01 05:39:22 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
return &elemIt->second;
|
2014-01-01 05:39:22 +00:00
|
|
|
}
|
|
|
|
|
2014-01-03 16:40:36 +00:00
|
|
|
const std::shared_ptr<ThemeData>& ThemeData::getDefault()
|
2014-01-01 05:39:22 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
static std::shared_ptr<ThemeData> theme = nullptr;
|
|
|
|
if (theme == nullptr) {
|
|
|
|
theme = std::shared_ptr<ThemeData>(new ThemeData());
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
const std::string path =
|
|
|
|
Utils::FileSystem::getHomePath() + "/.emulationstation/es_theme_default.xml";
|
2020-06-21 12:25:28 +00:00
|
|
|
if (Utils::FileSystem::exists(path)) {
|
|
|
|
try {
|
|
|
|
std::map<std::string, std::string> emptyMap;
|
|
|
|
theme->loadFile(emptyMap, path);
|
|
|
|
}
|
2021-07-07 18:31:46 +00:00
|
|
|
catch (ThemeException& e) {
|
2020-06-21 12:25:28 +00:00
|
|
|
LOG(LogError) << e.what();
|
|
|
|
theme = std::shared_ptr<ThemeData>(new ThemeData()); // Reset to empty.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return theme;
|
2014-01-03 16:40:36 +00:00
|
|
|
}
|
2014-01-03 14:26:39 +00:00
|
|
|
|
2020-06-21 12:25:28 +00:00
|
|
|
std::vector<GuiComponent*> ThemeData::makeExtras(const std::shared_ptr<ThemeData>& theme,
|
2021-07-07 18:31:46 +00:00
|
|
|
const std::string& view,
|
|
|
|
Window* window)
|
2014-01-03 14:26:39 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
std::vector<GuiComponent*> comps;
|
|
|
|
|
|
|
|
auto viewIt = theme->mViews.find(view);
|
|
|
|
if (viewIt == theme->mViews.cend())
|
|
|
|
return comps;
|
|
|
|
|
2021-07-07 18:31:46 +00:00
|
|
|
for (auto it = viewIt->second.orderedKeys.cbegin(); // Line break.
|
|
|
|
it != viewIt->second.orderedKeys.cend(); it++) {
|
2020-06-21 12:25:28 +00:00
|
|
|
ThemeElement& elem = viewIt->second.elements.at(*it);
|
|
|
|
if (elem.extra) {
|
|
|
|
GuiComponent* comp = nullptr;
|
|
|
|
const std::string& t = elem.type;
|
|
|
|
if (t == "image")
|
|
|
|
comp = new ImageComponent(window);
|
|
|
|
else if (t == "text")
|
|
|
|
comp = new TextComponent(window);
|
|
|
|
|
2020-10-17 12:05:41 +00:00
|
|
|
if (comp) {
|
2021-08-16 16:25:01 +00:00
|
|
|
comp->setDefaultZIndex(10.0f);
|
2020-10-17 12:05:41 +00:00
|
|
|
comp->applyTheme(theme, view, *it, ThemeFlags::ALL);
|
|
|
|
comps.push_back(comp);
|
|
|
|
}
|
2020-06-21 12:25:28 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return comps;
|
2014-01-03 14:26:39 +00:00
|
|
|
}
|
|
|
|
|
2014-05-01 02:12:45 +00:00
|
|
|
std::map<std::string, ThemeSet> ThemeData::getThemeSets()
|
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
std::map<std::string, ThemeSet> sets;
|
|
|
|
|
2020-07-12 10:47:39 +00:00
|
|
|
// Check for themes first under the home directory, then under the data installation
|
2021-07-07 18:31:46 +00:00
|
|
|
// directory (Unix only) and last under the ES-DE binary directory.
|
|
|
|
|
|
|
|
#if defined(__unix__) || defined(__APPLE__)
|
2020-07-12 10:47:39 +00:00
|
|
|
static const size_t pathCount = 3;
|
2021-07-07 18:31:46 +00:00
|
|
|
#else
|
2020-06-21 12:25:28 +00:00
|
|
|
static const size_t pathCount = 2;
|
2021-07-07 18:31:46 +00:00
|
|
|
#endif
|
2020-07-12 10:47:39 +00:00
|
|
|
std::string paths[pathCount] = {
|
2020-07-03 18:23:51 +00:00
|
|
|
Utils::FileSystem::getExePath() + "/themes",
|
2021-07-07 18:31:46 +00:00
|
|
|
#if defined(__APPLE__)
|
2020-08-21 19:49:45 +00:00
|
|
|
Utils::FileSystem::getExePath() + "/../Resources/themes",
|
2021-07-07 18:31:46 +00:00
|
|
|
#elif defined(__unix__)
|
2020-07-03 18:23:51 +00:00
|
|
|
Utils::FileSystem::getProgramDataPath() + "/themes",
|
2021-07-07 18:31:46 +00:00
|
|
|
#endif
|
2020-06-21 12:25:28 +00:00
|
|
|
Utils::FileSystem::getHomePath() + "/.emulationstation/themes"
|
|
|
|
};
|
|
|
|
|
|
|
|
for (size_t i = 0; i < pathCount; i++) {
|
|
|
|
if (!Utils::FileSystem::isDirectory(paths[i]))
|
|
|
|
continue;
|
|
|
|
|
|
|
|
Utils::FileSystem::stringList dirContent = Utils::FileSystem::getDirContent(paths[i]);
|
|
|
|
|
|
|
|
for (Utils::FileSystem::stringList::const_iterator it = dirContent.cbegin();
|
2021-07-07 18:31:46 +00:00
|
|
|
it != dirContent.cend(); it++) {
|
2020-06-21 12:25:28 +00:00
|
|
|
if (Utils::FileSystem::isDirectory(*it)) {
|
2021-08-17 16:41:45 +00:00
|
|
|
ThemeSet set = {*it};
|
2020-06-21 12:25:28 +00:00
|
|
|
sets[set.getName()] = set;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return sets;
|
2014-05-01 02:12:45 +00:00
|
|
|
}
|
|
|
|
|
2018-01-29 22:50:10 +00:00
|
|
|
std::string ThemeData::getThemeFromCurrentSet(const std::string& system)
|
2014-05-01 02:12:45 +00:00
|
|
|
{
|
2020-06-21 12:25:28 +00:00
|
|
|
std::map<std::string, ThemeSet> themeSets = ThemeData::getThemeSets();
|
|
|
|
if (themeSets.empty())
|
|
|
|
// No theme sets available.
|
|
|
|
return "";
|
|
|
|
|
|
|
|
std::map<std::string, ThemeSet>::const_iterator set =
|
2021-07-07 18:31:46 +00:00
|
|
|
themeSets.find(Settings::getInstance()->getString("ThemeSet"));
|
2020-06-21 12:25:28 +00:00
|
|
|
if (set == themeSets.cend()) {
|
2021-07-07 18:31:46 +00:00
|
|
|
// Currently configured theme set is missing, so just pick the first available set.
|
2020-06-21 12:25:28 +00:00
|
|
|
set = themeSets.cbegin();
|
|
|
|
Settings::getInstance()->setString("ThemeSet", set->first);
|
|
|
|
}
|
|
|
|
|
|
|
|
return set->second.getThemePath(system);
|
2014-05-01 02:12:45 +00:00
|
|
|
}
|