summaryrefslogtreecommitdiffstats
path: root/xbmc/guilib/GUIIncludes.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/guilib/GUIIncludes.cpp')
-rw-r--r--xbmc/guilib/GUIIncludes.cpp703
1 files changed, 703 insertions, 0 deletions
diff --git a/xbmc/guilib/GUIIncludes.cpp b/xbmc/guilib/GUIIncludes.cpp
new file mode 100644
index 0000000..d950c2d
--- /dev/null
+++ b/xbmc/guilib/GUIIncludes.cpp
@@ -0,0 +1,703 @@
+/*
+ * Copyright (C) 2005-2018 Team Kodi
+ * This file is part of Kodi - https://kodi.tv
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ * See LICENSES/README.md for more information.
+ */
+
+#include "GUIIncludes.h"
+
+#include "GUIInfoManager.h"
+#include "addons/Skin.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/guiinfo/GUIInfoLabel.h"
+#include "interfaces/info/SkinVariable.h"
+#include "utils/StringUtils.h"
+#include "utils/XBMCTinyXML.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+using namespace KODI::GUILIB;
+
+CGUIIncludes::CGUIIncludes()
+{
+ m_constantAttributes.insert("x");
+ m_constantAttributes.insert("y");
+ m_constantAttributes.insert("width");
+ m_constantAttributes.insert("height");
+ m_constantAttributes.insert("center");
+ m_constantAttributes.insert("max");
+ m_constantAttributes.insert("min");
+ m_constantAttributes.insert("w");
+ m_constantAttributes.insert("h");
+ m_constantAttributes.insert("time");
+ m_constantAttributes.insert("acceleration");
+ m_constantAttributes.insert("delay");
+ m_constantAttributes.insert("start");
+ m_constantAttributes.insert("end");
+ m_constantAttributes.insert("center");
+ m_constantAttributes.insert("border");
+ m_constantAttributes.insert("repeat");
+
+ m_constantNodes.insert("posx");
+ m_constantNodes.insert("posy");
+ m_constantNodes.insert("left");
+ m_constantNodes.insert("centerleft");
+ m_constantNodes.insert("right");
+ m_constantNodes.insert("centerright");
+ m_constantNodes.insert("top");
+ m_constantNodes.insert("centertop");
+ m_constantNodes.insert("bottom");
+ m_constantNodes.insert("centerbottom");
+ m_constantNodes.insert("width");
+ m_constantNodes.insert("height");
+ m_constantNodes.insert("offsetx");
+ m_constantNodes.insert("offsety");
+ m_constantNodes.insert("textoffsetx");
+ m_constantNodes.insert("textoffsety");
+ m_constantNodes.insert("textwidth");
+ m_constantNodes.insert("spinposx");
+ m_constantNodes.insert("spinposy");
+ m_constantNodes.insert("spinwidth");
+ m_constantNodes.insert("spinheight");
+ m_constantNodes.insert("radioposx");
+ m_constantNodes.insert("radioposy");
+ m_constantNodes.insert("radiowidth");
+ m_constantNodes.insert("radioheight");
+ m_constantNodes.insert("sliderwidth");
+ m_constantNodes.insert("sliderheight");
+ m_constantNodes.insert("itemgap");
+ m_constantNodes.insert("bordersize");
+ m_constantNodes.insert("timeperimage");
+ m_constantNodes.insert("fadetime");
+ m_constantNodes.insert("pauseatend");
+ m_constantNodes.insert("depth");
+ m_constantNodes.insert("movement");
+ m_constantNodes.insert("focusposition");
+
+ m_expressionAttributes.insert("condition");
+
+ m_expressionNodes.insert("visible");
+ m_expressionNodes.insert("enable");
+ m_expressionNodes.insert("usealttexture");
+ m_expressionNodes.insert("selected");
+}
+
+CGUIIncludes::~CGUIIncludes() = default;
+
+void CGUIIncludes::Clear()
+{
+ m_includes.clear();
+ m_defaults.clear();
+ m_constants.clear();
+ m_skinvariables.clear();
+ m_files.clear();
+ m_expressions.clear();
+}
+
+void CGUIIncludes::Load(const std::string &file)
+{
+ if (!Load_Internal(file))
+ return;
+ FlattenExpressions();
+ FlattenSkinVariableConditions();
+}
+
+bool CGUIIncludes::Load_Internal(const std::string &file)
+{
+ // check to see if we already have this loaded
+ if (HasLoaded(file))
+ return true;
+
+ CXBMCTinyXML doc;
+ if (!doc.LoadFile(file))
+ {
+ CLog::Log(LOGINFO, "Error loading include file {}: {} (row: {}, col: {})", file,
+ doc.ErrorDesc(), doc.ErrorRow(), doc.ErrorCol());
+ return false;
+ }
+
+ TiXmlElement *root = doc.RootElement();
+ if (!root || !StringUtils::EqualsNoCase(root->Value(), "includes"))
+ {
+ CLog::Log(LOGERROR, "Error loading include file {}: Root element <includes> required.", file);
+ return false;
+ }
+
+ // load components
+ LoadDefaults(root);
+ LoadConstants(root);
+ LoadExpressions(root);
+ LoadVariables(root);
+ LoadIncludes(root);
+
+ m_files.push_back(file);
+
+ return true;
+}
+
+void CGUIIncludes::LoadDefaults(const TiXmlElement *node)
+{
+ if (!node)
+ return;
+
+ const TiXmlElement* child = node->FirstChildElement("default");
+ while (child)
+ {
+ const char *type = child->Attribute("type");
+ if (type && child->FirstChild())
+ m_defaults.insert(std::make_pair(type, *child));
+
+ child = child->NextSiblingElement("default");
+ }
+}
+
+void CGUIIncludes::LoadExpressions(const TiXmlElement *node)
+{
+ if (!node)
+ return;
+
+ const TiXmlElement* child = node->FirstChildElement("expression");
+ while (child)
+ {
+ const char *tagName = child->Attribute("name");
+ if (tagName && child->FirstChild())
+ m_expressions.insert(std::make_pair(tagName, "[" + child->FirstChild()->ValueStr() + "]"));
+
+ child = child->NextSiblingElement("expression");
+ }
+}
+
+
+void CGUIIncludes::LoadConstants(const TiXmlElement *node)
+{
+ if (!node)
+ return;
+
+ const TiXmlElement* child = node->FirstChildElement("constant");
+ while (child)
+ {
+ const char *tagName = child->Attribute("name");
+ if (tagName && child->FirstChild())
+ m_constants.insert(std::make_pair(tagName, child->FirstChild()->ValueStr()));
+
+ child = child->NextSiblingElement("constant");
+ }
+}
+
+void CGUIIncludes::LoadVariables(const TiXmlElement *node)
+{
+ if (!node)
+ return;
+
+ const TiXmlElement* child = node->FirstChildElement("variable");
+ while (child)
+ {
+ const char *tagName = child->Attribute("name");
+ if (tagName && child->FirstChild())
+ m_skinvariables.insert(std::make_pair(tagName, *child));
+
+ child = child->NextSiblingElement("variable");
+ }
+}
+
+void CGUIIncludes::LoadIncludes(const TiXmlElement *node)
+{
+ if (!node)
+ return;
+
+ const TiXmlElement* child = node->FirstChildElement("include");
+ while (child)
+ {
+ const char *tagName = child->Attribute("name");
+ if (tagName && child->FirstChild())
+ {
+ // we'll parse and store parameter list with defaults when include definition is first encountered
+ // if there's a <definition> tag only use its body as the actually included part
+ const TiXmlElement *definitionTag = child->FirstChildElement("definition");
+ const TiXmlElement *includeBody = definitionTag ? definitionTag : child;
+
+ // if there's a <param> tag there also must be a <definition> tag
+ Params defaultParams;
+ bool haveParamTags = GetParameters(child, "default", defaultParams);
+ if (haveParamTags && !definitionTag)
+ CLog::Log(LOGWARNING, "Skin has invalid include definition: {}", tagName);
+ else
+ m_includes.insert({ tagName, { *includeBody, std::move(defaultParams) } });
+ }
+ else if (child->Attribute("file"))
+ {
+ std::string file = g_SkinInfo->GetSkinPath(child->Attribute("file"));
+ const char *condition = child->Attribute("condition");
+
+ if (condition)
+ { // load include file if condition evals to true
+ if (CServiceBroker::GetGUI()->GetInfoManager().Register(condition)->Get(
+ INFO::DEFAULT_CONTEXT))
+ Load_Internal(file);
+ }
+ else
+ Load_Internal(file);
+ }
+ child = child->NextSiblingElement("include");
+ }
+}
+
+void CGUIIncludes::FlattenExpressions()
+{
+ for (auto& expression : m_expressions)
+ {
+ std::vector<std::string> resolved = std::vector<std::string>();
+ resolved.push_back(expression.first);
+ FlattenExpression(expression.second, resolved);
+ }
+}
+
+void CGUIIncludes::FlattenExpression(std::string &expression, const std::vector<std::string> &resolved)
+{
+ std::string original(expression);
+ GUIINFO::CGUIInfoLabel::ReplaceSpecialKeywordReferences(expression, "EXP", [&](const std::string &expressionName) -> std::string {
+ if (std::find(resolved.begin(), resolved.end(), expressionName) != resolved.end())
+ {
+ CLog::Log(LOGERROR, "Skin has a circular expression \"{}\": {}", resolved.back(), original);
+ return std::string();
+ }
+ auto it = m_expressions.find(expressionName);
+ if (it == m_expressions.end())
+ return std::string();
+
+ std::vector<std::string> rescopy = resolved;
+ rescopy.push_back(expressionName);
+ FlattenExpression(it->second, rescopy);
+
+ return it->second;
+ });
+}
+
+void CGUIIncludes::FlattenSkinVariableConditions()
+{
+ for (auto& variable : m_skinvariables)
+ {
+ TiXmlElement* valueNode = variable.second.FirstChildElement("value");
+ while (valueNode)
+ {
+ const char *condition = valueNode->Attribute("condition");
+ if (condition)
+ valueNode->SetAttribute("condition", ResolveExpressions(condition));
+
+ valueNode = valueNode->NextSiblingElement("value");
+ }
+ }
+}
+
+bool CGUIIncludes::HasLoaded(const std::string &file) const
+{
+ for (const auto& loadedFile : m_files)
+ {
+ if (loadedFile == file)
+ return true;
+ }
+ return false;
+}
+
+void CGUIIncludes::Resolve(TiXmlElement *node, std::map<INFO::InfoPtr, bool>* xmlIncludeConditions /* = NULL */)
+{
+ if (!node)
+ return;
+
+ SetDefaults(node);
+ ResolveConstants(node);
+ ResolveExpressions(node);
+ ResolveIncludes(node, xmlIncludeConditions);
+
+ TiXmlElement *child = node->FirstChildElement();
+ while (child)
+ {
+ // recursive call
+ Resolve(child, xmlIncludeConditions);
+ child = child->NextSiblingElement();
+ }
+}
+
+void CGUIIncludes::SetDefaults(TiXmlElement *node)
+{
+ if (node->ValueStr() != "control")
+ return;
+
+ std::string type = XMLUtils::GetAttribute(node, "type");
+ const auto it = m_defaults.find(type);
+ if (it != m_defaults.end())
+ {
+ // we don't insert <left> et. al. if <posx> or <posy> is specified
+ bool hasPosX(node->FirstChild("posx") != nullptr);
+ bool hasPosY(node->FirstChild("posy") != nullptr);
+
+ const TiXmlElement &element = (*it).second;
+ const TiXmlElement *tag = element.FirstChildElement();
+ while (tag)
+ {
+ std::string value = tag->ValueStr();
+ bool skip(false);
+ if (hasPosX && (value == "left" || value == "right" || value == "centerleft" || value == "centerright"))
+ skip = true;
+ if (hasPosY && (value == "top" || value == "bottom" || value == "centertop" || value == "centerbottom"))
+ skip = true;
+ // we insert at the end of block
+ if (!skip)
+ node->InsertEndChild(*tag);
+ tag = tag->NextSiblingElement();
+ }
+ }
+}
+
+void CGUIIncludes::ResolveConstants(TiXmlElement *node)
+{
+ if (!node)
+ return;
+
+ TiXmlNode *child = node->FirstChild();
+ if (child && child->Type() == TiXmlNode::TINYXML_TEXT && m_constantNodes.count(node->ValueStr()))
+ {
+ child->SetValue(ResolveConstant(child->ValueStr()));
+ }
+ else
+ {
+ TiXmlAttribute *attribute = node->FirstAttribute();
+ while (attribute)
+ {
+ if (m_constantAttributes.count(attribute->Name()))
+ attribute->SetValue(ResolveConstant(attribute->ValueStr()));
+
+ attribute = attribute->Next();
+ }
+ }
+}
+
+void CGUIIncludes::ResolveExpressions(TiXmlElement *node)
+{
+ if (!node)
+ return;
+
+ TiXmlNode *child = node->FirstChild();
+ if (child && child->Type() == TiXmlNode::TINYXML_TEXT && m_expressionNodes.count(node->ValueStr()))
+ {
+ child->SetValue(ResolveExpressions(child->ValueStr()));
+ }
+ else
+ {
+ TiXmlAttribute *attribute = node->FirstAttribute();
+ while (attribute)
+ {
+ if (m_expressionAttributes.count(attribute->Name()))
+ attribute->SetValue(ResolveExpressions(attribute->ValueStr()));
+
+ attribute = attribute->Next();
+ }
+ }
+}
+
+void CGUIIncludes::ResolveIncludes(TiXmlElement *node, std::map<INFO::InfoPtr, bool>* xmlIncludeConditions /* = NULL */)
+{
+ if (!node)
+ return;
+
+ TiXmlElement *include = node->FirstChildElement("include");
+ while (include)
+ {
+ // file: load includes from specified XML file
+ const char *file = include->Attribute("file");
+ if (file)
+ Load(g_SkinInfo->GetSkinPath(file));
+
+ // condition: process include if condition evals to true
+ const char *condition = include->Attribute("condition");
+ if (condition)
+ {
+ INFO::InfoPtr conditionID =
+ CServiceBroker::GetGUI()->GetInfoManager().Register(ResolveExpressions(condition));
+ bool value = false;
+
+ if (conditionID)
+ {
+ value = conditionID->Get(INFO::DEFAULT_CONTEXT);
+
+ if (xmlIncludeConditions)
+ xmlIncludeConditions->insert(std::make_pair(conditionID, value));
+ }
+
+ if (!value)
+ {
+ include = include->NextSiblingElement("include");
+ continue;
+ }
+ }
+
+ Params params;
+ std::string tagName;
+
+ // determine which form of include call we have
+ const char *name = include->Attribute("content");
+ if (name)
+ {
+ // <include content="MyControl" />
+ // or
+ // <include content="MyControl">
+ // <param name="posx" value="225" />
+ // <param name="posy">150</param>
+ // ...
+ // </include>
+ tagName = name;
+ GetParameters(include, "value", params);
+ }
+ else
+ {
+ const TiXmlNode *child = include->FirstChild();
+ if (child && child->Type() == TiXmlNode::TINYXML_TEXT)
+ {
+ // <include>MyControl</include>
+ // old-style includes for backward compatibility
+ tagName = child->ValueStr();
+ }
+ }
+
+ // check, whether the include exists and therefore should be replaced by its definition
+ auto it = m_includes.find(tagName);
+ if (it != m_includes.end())
+ {
+ const TiXmlElement *includeDefinition = &it->second.first;
+
+ // combine passed include parameters with their default values into a single list (no overwrites)
+ const Params& defaultParams = it->second.second;
+ params.insert(defaultParams.begin(), defaultParams.end());
+
+ // process include definition
+ const TiXmlElement *includeDefinitionChild = includeDefinition->FirstChildElement();
+ while (includeDefinitionChild)
+ {
+ // insert before <include> element to keep order of occurrence in xml file
+ TiXmlElement *insertedNode = static_cast<TiXmlElement*>(node->InsertBeforeChild(include, *includeDefinitionChild));
+
+ // process nested
+ InsertNested(node, include, insertedNode);
+
+ // resolve parameters even if parameter list is empty (to remove param references)
+ ResolveParametersForNode(insertedNode, params);
+
+ includeDefinitionChild = includeDefinitionChild->NextSiblingElement();
+ }
+
+ // remove the include element itself
+ node->RemoveChild(include);
+
+ include = node->FirstChildElement("include");
+ }
+ else
+ { // invalid include
+ CLog::Log(LOGWARNING, "Skin has invalid include: {}", tagName);
+ include = include->NextSiblingElement("include");
+ }
+ }
+}
+
+void CGUIIncludes::InsertNested(TiXmlElement *controls, TiXmlElement *include, TiXmlElement *node)
+{
+ TiXmlElement *target;
+ TiXmlElement *nested;
+
+ if (node->ValueStr() == "nested")
+ {
+ nested = node;
+ target = controls;
+ }
+ else
+ {
+ nested = node->FirstChildElement("nested");
+ target = node;
+ }
+
+ if (nested)
+ {
+ // copy all child elements except param elements
+ const TiXmlElement *child = include->FirstChildElement();
+ while (child)
+ {
+ if (child->ValueStr() != "param")
+ {
+ // insert before <nested> element to keep order of occurrence in xml file
+ target->InsertBeforeChild(nested, *child);
+ }
+ child = child->NextSiblingElement();
+ }
+ target->RemoveChild(nested);
+ }
+
+}
+
+bool CGUIIncludes::GetParameters(const TiXmlElement *include, const char *valueAttribute, Params& params)
+{
+ bool foundAny = false;
+
+ // collect parameters from include tag
+ // <include content="MyControl">
+ // <param name="posx" value="225" /> <!-- comments and other tags are ignored here -->
+ // <param name="posy">150</param>
+ // ...
+ // </include>
+
+ if (include)
+ {
+ const TiXmlElement *param = include->FirstChildElement("param");
+ foundAny = param != NULL; // doesn't matter if param isn't entirely valid
+ while (param)
+ {
+ std::string paramName = XMLUtils::GetAttribute(param, "name");
+ if (!paramName.empty())
+ {
+ std::string paramValue;
+
+ // <param name="posx" value="120" />
+ const char *value = param->Attribute(valueAttribute); // try attribute first
+ if (value)
+ paramValue = value;
+ else
+ {
+ // <param name="posx">120</param>
+ const TiXmlNode *child = param->FirstChild();
+ if (child && child->Type() == TiXmlNode::TINYXML_TEXT)
+ paramValue = child->ValueStr(); // and then tag value
+ }
+
+ params.insert({ paramName, paramValue }); // no overwrites
+ }
+ param = param->NextSiblingElement("param");
+ }
+ }
+
+ return foundAny;
+}
+
+void CGUIIncludes::ResolveParametersForNode(TiXmlElement *node, const Params& params)
+{
+ if (!node)
+ return;
+ std::string newValue;
+ // run through this element's attributes, resolving any parameters
+ TiXmlAttribute *attribute = node->FirstAttribute();
+ while (attribute)
+ {
+ ResolveParamsResult result = ResolveParameters(attribute->ValueStr(), newValue, params);
+ if (result == SINGLE_UNDEFINED_PARAM_RESOLVED && strcmp(node->Value(), "param") == 0 &&
+ strcmp(attribute->Name(), "value") == 0 && node->Parent() && strcmp(node->Parent()->Value(), "include") == 0)
+ {
+ // special case: passing <param name="someName" value="$PARAM[undefinedParam]" /> to the nested include
+ // this usually happens when trying to forward a missing parameter from the enclosing include to the nested include
+ // problem: since 'undefinedParam' is not defined, it expands to <param name="someName" value="" /> and overrides any default value set with <param name="someName" default="someValue" /> in the nested include
+ // to prevent this, we'll completely remove this parameter from the nested include call so that the default value can be correctly picked up later
+ node->Parent()->RemoveChild(node);
+ return;
+ }
+ else if (result != NO_PARAMS_FOUND)
+ attribute->SetValue(newValue);
+ attribute = attribute->Next();
+ }
+ // run through this element's value and children, resolving any parameters
+ TiXmlNode *child = node->FirstChild();
+ if (child)
+ {
+ if (child->Type() == TiXmlNode::TINYXML_TEXT)
+ {
+ ResolveParamsResult result = ResolveParameters(child->ValueStr(), newValue, params);
+ if (result == SINGLE_UNDEFINED_PARAM_RESOLVED && strcmp(node->Value(), "param") == 0 &&
+ node->Parent() && strcmp(node->Parent()->Value(), "include") == 0)
+ {
+ // special case: passing <param name="someName">$PARAM[undefinedParam]</param> to the nested include
+ // we'll remove the offending param tag same as above
+ node->Parent()->RemoveChild(node);
+ }
+ else if (result != NO_PARAMS_FOUND)
+ child->SetValue(newValue);
+ }
+ else if (child->Type() == TiXmlNode::TINYXML_ELEMENT ||
+ child->Type() == TiXmlNode::TINYXML_COMMENT)
+ {
+ do
+ {
+ // save next as current child might be removed from the tree
+ TiXmlElement* next = child->NextSiblingElement();
+
+ if (child->Type() == TiXmlNode::TINYXML_ELEMENT)
+ ResolveParametersForNode(static_cast<TiXmlElement*>(child), params);
+
+ child = next;
+ }
+ while (child);
+ }
+ }
+}
+
+class ParamReplacer
+{
+ const std::map<std::string, std::string>& m_params;
+ // keep some stats so that we know exactly what's been resolved
+ int m_numTotalParams;
+ int m_numUndefinedParams;
+public:
+ explicit ParamReplacer(const std::map<std::string, std::string>& params)
+ : m_params(params), m_numTotalParams(0), m_numUndefinedParams(0) {}
+ int GetNumTotalParams() const { return m_numTotalParams; }
+ int GetNumDefinedParams() const { return m_numTotalParams - m_numUndefinedParams; }
+ int GetNumUndefinedParams() const { return m_numUndefinedParams; }
+
+ std::string operator()(const std::string &paramName)
+ {
+ m_numTotalParams++;
+ std::map<std::string, std::string>::const_iterator it = m_params.find(paramName);
+ if (it != m_params.end())
+ return it->second;
+ m_numUndefinedParams++;
+ return "";
+ }
+};
+
+CGUIIncludes::ResolveParamsResult CGUIIncludes::ResolveParameters(const std::string& strInput, std::string& strOutput, const Params& params)
+{
+ ParamReplacer paramReplacer(params);
+ if (GUIINFO::CGUIInfoLabel::ReplaceSpecialKeywordReferences(strInput, "PARAM", std::ref(paramReplacer), strOutput))
+ // detect special input values of the form "$PARAM[undefinedParam]" (with no extra characters around)
+ return paramReplacer.GetNumUndefinedParams() == 1 && paramReplacer.GetNumTotalParams() == 1 && strOutput.empty() ? SINGLE_UNDEFINED_PARAM_RESOLVED : PARAMS_RESOLVED;
+ return NO_PARAMS_FOUND;
+}
+
+std::string CGUIIncludes::ResolveConstant(const std::string &constant) const
+{
+ std::vector<std::string> values = StringUtils::Split(constant, ",");
+ for (auto& i : values)
+ {
+ std::map<std::string, std::string>::const_iterator it = m_constants.find(i);
+ if (it != m_constants.end())
+ i = it->second;
+ }
+ return StringUtils::Join(values, ",");
+}
+
+std::string CGUIIncludes::ResolveExpressions(const std::string &expression) const
+{
+ std::string work(expression);
+ GUIINFO::CGUIInfoLabel::ReplaceSpecialKeywordReferences(work, "EXP", [&](const std::string &str) -> std::string {
+ std::map<std::string, std::string>::const_iterator it = m_expressions.find(str);
+ if (it != m_expressions.end())
+ return it->second;
+ return "";
+ });
+
+ return work;
+}
+
+const INFO::CSkinVariableString* CGUIIncludes::CreateSkinVariable(const std::string& name, int context)
+{
+ std::map<std::string, TiXmlElement>::const_iterator it = m_skinvariables.find(name);
+ if (it != m_skinvariables.end())
+ return INFO::CSkinVariable::CreateFromXML(it->second, context);
+ return NULL;
+}