diff options
Diffstat (limited to 'xbmc/guilib/GUIIncludes.cpp')
-rw-r--r-- | xbmc/guilib/GUIIncludes.cpp | 703 |
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 ¶mName) + { + 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; +} |