2 * Copyright (C) 2005-2018 Team Kodi
3 * This file is part of Kodi - https://kodi.tv
5 * SPDX-License-Identifier: GPL-2.0-or-later
6 * See LICENSES/README.md for more information.
9 #include "GUIIncludes.h"
11 #include "GUIInfoManager.h"
12 #include "addons/Skin.h"
13 #include "guilib/GUIComponent.h"
14 #include "guilib/guiinfo/GUIInfoLabel.h"
15 #include "interfaces/info/SkinVariable.h"
16 #include "utils/StringUtils.h"
17 #include "utils/XBMCTinyXML.h"
18 #include "utils/XMLUtils.h"
19 #include "utils/log.h"
21 using namespace KODI::GUILIB
;
23 CGUIIncludes::CGUIIncludes()
25 m_constantAttributes
.insert("x");
26 m_constantAttributes
.insert("y");
27 m_constantAttributes
.insert("width");
28 m_constantAttributes
.insert("height");
29 m_constantAttributes
.insert("center");
30 m_constantAttributes
.insert("max");
31 m_constantAttributes
.insert("min");
32 m_constantAttributes
.insert("w");
33 m_constantAttributes
.insert("h");
34 m_constantAttributes
.insert("time");
35 m_constantAttributes
.insert("acceleration");
36 m_constantAttributes
.insert("delay");
37 m_constantAttributes
.insert("start");
38 m_constantAttributes
.insert("end");
39 m_constantAttributes
.insert("center");
40 m_constantAttributes
.insert("border");
41 m_constantAttributes
.insert("repeat");
43 m_constantNodes
.insert("posx");
44 m_constantNodes
.insert("posy");
45 m_constantNodes
.insert("left");
46 m_constantNodes
.insert("centerleft");
47 m_constantNodes
.insert("right");
48 m_constantNodes
.insert("centerright");
49 m_constantNodes
.insert("top");
50 m_constantNodes
.insert("centertop");
51 m_constantNodes
.insert("bottom");
52 m_constantNodes
.insert("centerbottom");
53 m_constantNodes
.insert("width");
54 m_constantNodes
.insert("height");
55 m_constantNodes
.insert("offsetx");
56 m_constantNodes
.insert("offsety");
57 m_constantNodes
.insert("textoffsetx");
58 m_constantNodes
.insert("textoffsety");
59 m_constantNodes
.insert("textwidth");
60 m_constantNodes
.insert("spinposx");
61 m_constantNodes
.insert("spinposy");
62 m_constantNodes
.insert("spinwidth");
63 m_constantNodes
.insert("spinheight");
64 m_constantNodes
.insert("radioposx");
65 m_constantNodes
.insert("radioposy");
66 m_constantNodes
.insert("radiowidth");
67 m_constantNodes
.insert("radioheight");
68 m_constantNodes
.insert("sliderwidth");
69 m_constantNodes
.insert("sliderheight");
70 m_constantNodes
.insert("itemgap");
71 m_constantNodes
.insert("bordersize");
72 m_constantNodes
.insert("timeperimage");
73 m_constantNodes
.insert("fadetime");
74 m_constantNodes
.insert("pauseatend");
75 m_constantNodes
.insert("depth");
76 m_constantNodes
.insert("movement");
77 m_constantNodes
.insert("focusposition");
79 m_expressionAttributes
.insert("condition");
81 m_expressionNodes
.insert("visible");
82 m_expressionNodes
.insert("enable");
83 m_expressionNodes
.insert("usealttexture");
84 m_expressionNodes
.insert("selected");
87 CGUIIncludes::~CGUIIncludes() = default;
89 void CGUIIncludes::Clear()
94 m_skinvariables
.clear();
96 m_expressions
.clear();
99 void CGUIIncludes::Load(const std::string
&file
)
101 if (!Load_Internal(file
))
103 FlattenExpressions();
104 FlattenSkinVariableConditions();
107 bool CGUIIncludes::Load_Internal(const std::string
&file
)
109 // check to see if we already have this loaded
114 if (!doc
.LoadFile(file
))
116 CLog::Log(LOGINFO
, "Error loading include file {}: {} (row: {}, col: {})", file
,
117 doc
.ErrorDesc(), doc
.ErrorRow(), doc
.ErrorCol());
121 TiXmlElement
*root
= doc
.RootElement();
122 if (!root
|| !StringUtils::EqualsNoCase(root
->Value(), "includes"))
124 CLog::Log(LOGERROR
, "Error loading include file {}: Root element <includes> required.", file
);
131 LoadExpressions(root
);
135 m_files
.push_back(file
);
140 void CGUIIncludes::LoadDefaults(const TiXmlElement
*node
)
145 const TiXmlElement
* child
= node
->FirstChildElement("default");
148 const char *type
= child
->Attribute("type");
149 if (type
&& child
->FirstChild())
150 m_defaults
.insert(std::make_pair(type
, *child
));
152 child
= child
->NextSiblingElement("default");
156 void CGUIIncludes::LoadExpressions(const TiXmlElement
*node
)
161 const TiXmlElement
* child
= node
->FirstChildElement("expression");
164 const char *tagName
= child
->Attribute("name");
165 if (tagName
&& child
->FirstChild())
166 m_expressions
.insert(std::make_pair(tagName
, "[" + child
->FirstChild()->ValueStr() + "]"));
168 child
= child
->NextSiblingElement("expression");
173 void CGUIIncludes::LoadConstants(const TiXmlElement
*node
)
178 const TiXmlElement
* child
= node
->FirstChildElement("constant");
181 const char *tagName
= child
->Attribute("name");
182 if (tagName
&& child
->FirstChild())
183 m_constants
.insert(std::make_pair(tagName
, child
->FirstChild()->ValueStr()));
185 child
= child
->NextSiblingElement("constant");
189 void CGUIIncludes::LoadVariables(const TiXmlElement
*node
)
194 const TiXmlElement
* child
= node
->FirstChildElement("variable");
197 const char *tagName
= child
->Attribute("name");
198 if (tagName
&& child
->FirstChild())
199 m_skinvariables
.insert(std::make_pair(tagName
, *child
));
201 child
= child
->NextSiblingElement("variable");
205 void CGUIIncludes::LoadIncludes(const TiXmlElement
*node
)
210 const TiXmlElement
* child
= node
->FirstChildElement("include");
213 const char *tagName
= child
->Attribute("name");
214 if (tagName
&& child
->FirstChild())
216 // we'll parse and store parameter list with defaults when include definition is first encountered
217 // if there's a <definition> tag only use its body as the actually included part
218 const TiXmlElement
*definitionTag
= child
->FirstChildElement("definition");
219 const TiXmlElement
*includeBody
= definitionTag
? definitionTag
: child
;
221 // if there's a <param> tag there also must be a <definition> tag
222 Params defaultParams
;
223 bool haveParamTags
= GetParameters(child
, "default", defaultParams
);
224 if (haveParamTags
&& !definitionTag
)
225 CLog::Log(LOGWARNING
, "Skin has invalid include definition: {}", tagName
);
227 m_includes
.insert({ tagName
, { *includeBody
, std::move(defaultParams
) } });
229 else if (child
->Attribute("file"))
231 std::string file
= g_SkinInfo
->GetSkinPath(child
->Attribute("file"));
232 const char *condition
= child
->Attribute("condition");
235 { // load include file if condition evals to true
236 if (CServiceBroker::GetGUI()->GetInfoManager().Register(condition
)->Get(
237 INFO::DEFAULT_CONTEXT
))
243 child
= child
->NextSiblingElement("include");
247 void CGUIIncludes::FlattenExpressions()
249 for (auto& expression
: m_expressions
)
251 std::vector
<std::string
> resolved
= std::vector
<std::string
>();
252 resolved
.push_back(expression
.first
);
253 FlattenExpression(expression
.second
, resolved
);
257 void CGUIIncludes::FlattenExpression(std::string
&expression
, const std::vector
<std::string
> &resolved
)
259 std::string
original(expression
);
260 GUIINFO::CGUIInfoLabel::ReplaceSpecialKeywordReferences(expression
, "EXP", [&](const std::string
&expressionName
) -> std::string
{
261 if (std::find(resolved
.begin(), resolved
.end(), expressionName
) != resolved
.end())
263 CLog::Log(LOGERROR
, "Skin has a circular expression \"{}\": {}", resolved
.back(), original
);
264 return std::string();
266 auto it
= m_expressions
.find(expressionName
);
267 if (it
== m_expressions
.end())
268 return std::string();
270 std::vector
<std::string
> rescopy
= resolved
;
271 rescopy
.push_back(expressionName
);
272 FlattenExpression(it
->second
, rescopy
);
278 void CGUIIncludes::FlattenSkinVariableConditions()
280 for (auto& variable
: m_skinvariables
)
282 TiXmlElement
* valueNode
= variable
.second
.FirstChildElement("value");
285 const char *condition
= valueNode
->Attribute("condition");
287 valueNode
->SetAttribute("condition", ResolveExpressions(condition
));
289 valueNode
= valueNode
->NextSiblingElement("value");
294 bool CGUIIncludes::HasLoaded(const std::string
&file
) const
296 for (const auto& loadedFile
: m_files
)
298 if (loadedFile
== file
)
304 void CGUIIncludes::Resolve(TiXmlElement
*node
, std::map
<INFO::InfoPtr
, bool>* xmlIncludeConditions
/* = NULL */)
310 ResolveConstants(node
);
311 ResolveExpressions(node
);
312 ResolveIncludes(node
, xmlIncludeConditions
);
314 TiXmlElement
*child
= node
->FirstChildElement();
318 Resolve(child
, xmlIncludeConditions
);
319 child
= child
->NextSiblingElement();
323 void CGUIIncludes::SetDefaults(TiXmlElement
*node
)
325 if (node
->ValueStr() != "control")
328 std::string type
= XMLUtils::GetAttribute(node
, "type");
329 const auto it
= m_defaults
.find(type
);
330 if (it
!= m_defaults
.end())
332 // we don't insert <left> et. al. if <posx> or <posy> is specified
333 bool hasPosX(node
->FirstChild("posx") != nullptr);
334 bool hasPosY(node
->FirstChild("posy") != nullptr);
336 const TiXmlElement
&element
= (*it
).second
;
337 const TiXmlElement
*tag
= element
.FirstChildElement();
340 std::string value
= tag
->ValueStr();
342 if (hasPosX
&& (value
== "left" || value
== "right" || value
== "centerleft" || value
== "centerright"))
344 if (hasPosY
&& (value
== "top" || value
== "bottom" || value
== "centertop" || value
== "centerbottom"))
346 // we insert at the end of block
348 node
->InsertEndChild(*tag
);
349 tag
= tag
->NextSiblingElement();
354 void CGUIIncludes::ResolveConstants(TiXmlElement
*node
)
359 TiXmlNode
*child
= node
->FirstChild();
360 if (child
&& child
->Type() == TiXmlNode::TINYXML_TEXT
&& m_constantNodes
.count(node
->ValueStr()))
362 child
->SetValue(ResolveConstant(child
->ValueStr()));
366 TiXmlAttribute
*attribute
= node
->FirstAttribute();
369 if (m_constantAttributes
.count(attribute
->Name()))
370 attribute
->SetValue(ResolveConstant(attribute
->ValueStr()));
372 attribute
= attribute
->Next();
377 void CGUIIncludes::ResolveExpressions(TiXmlElement
*node
)
382 TiXmlNode
*child
= node
->FirstChild();
383 if (child
&& child
->Type() == TiXmlNode::TINYXML_TEXT
&& m_expressionNodes
.count(node
->ValueStr()))
385 child
->SetValue(ResolveExpressions(child
->ValueStr()));
389 TiXmlAttribute
*attribute
= node
->FirstAttribute();
392 if (m_expressionAttributes
.count(attribute
->Name()))
393 attribute
->SetValue(ResolveExpressions(attribute
->ValueStr()));
395 attribute
= attribute
->Next();
400 void CGUIIncludes::ResolveIncludes(TiXmlElement
*node
, std::map
<INFO::InfoPtr
, bool>* xmlIncludeConditions
/* = NULL */)
405 TiXmlElement
*include
= node
->FirstChildElement("include");
408 // file: load includes from specified XML file
409 const char *file
= include
->Attribute("file");
411 Load(g_SkinInfo
->GetSkinPath(file
));
413 // condition: process include if condition evals to true
414 const char *condition
= include
->Attribute("condition");
417 INFO::InfoPtr conditionID
=
418 CServiceBroker::GetGUI()->GetInfoManager().Register(ResolveExpressions(condition
));
423 value
= conditionID
->Get(INFO::DEFAULT_CONTEXT
);
425 if (xmlIncludeConditions
)
426 xmlIncludeConditions
->insert(std::make_pair(conditionID
, value
));
431 include
= include
->NextSiblingElement("include");
439 // determine which form of include call we have
440 const char *name
= include
->Attribute("content");
443 // <include content="MyControl" />
445 // <include content="MyControl">
446 // <param name="posx" value="225" />
447 // <param name="posy">150</param>
451 GetParameters(include
, "value", params
);
455 const TiXmlNode
*child
= include
->FirstChild();
456 if (child
&& child
->Type() == TiXmlNode::TINYXML_TEXT
)
458 // <include>MyControl</include>
459 // old-style includes for backward compatibility
460 tagName
= child
->ValueStr();
464 // check, whether the include exists and therefore should be replaced by its definition
465 auto it
= m_includes
.find(tagName
);
466 if (it
!= m_includes
.end())
468 const TiXmlElement
*includeDefinition
= &it
->second
.first
;
470 // combine passed include parameters with their default values into a single list (no overwrites)
471 const Params
& defaultParams
= it
->second
.second
;
472 params
.insert(defaultParams
.begin(), defaultParams
.end());
474 // process include definition
475 const TiXmlElement
*includeDefinitionChild
= includeDefinition
->FirstChildElement();
476 while (includeDefinitionChild
)
478 // insert before <include> element to keep order of occurrence in xml file
479 TiXmlElement
*insertedNode
= static_cast<TiXmlElement
*>(node
->InsertBeforeChild(include
, *includeDefinitionChild
));
482 InsertNested(node
, include
, insertedNode
);
484 // resolve parameters even if parameter list is empty (to remove param references)
485 ResolveParametersForNode(insertedNode
, params
);
487 includeDefinitionChild
= includeDefinitionChild
->NextSiblingElement();
490 // remove the include element itself
491 node
->RemoveChild(include
);
493 include
= node
->FirstChildElement("include");
497 CLog::Log(LOGWARNING
, "Skin has invalid include: {}", tagName
);
498 include
= include
->NextSiblingElement("include");
503 void CGUIIncludes::InsertNested(TiXmlElement
*controls
, TiXmlElement
*include
, TiXmlElement
*node
)
505 TiXmlElement
*target
;
506 TiXmlElement
*nested
;
508 if (node
->ValueStr() == "nested")
515 nested
= node
->FirstChildElement("nested");
521 // copy all child elements except param elements
522 const TiXmlElement
*child
= include
->FirstChildElement();
525 if (child
->ValueStr() != "param")
527 // insert before <nested> element to keep order of occurrence in xml file
528 target
->InsertBeforeChild(nested
, *child
);
530 child
= child
->NextSiblingElement();
533 target
->RemoveChild(nested
);
538 bool CGUIIncludes::GetParameters(const TiXmlElement
*include
, const char *valueAttribute
, Params
& params
)
540 bool foundAny
= false;
542 // collect parameters from include tag
543 // <include content="MyControl">
544 // <param name="posx" value="225" /> <!-- comments and other tags are ignored here -->
545 // <param name="posy">150</param>
551 const TiXmlElement
*param
= include
->FirstChildElement("param");
552 foundAny
= param
!= NULL
; // doesn't matter if param isn't entirely valid
555 std::string paramName
= XMLUtils::GetAttribute(param
, "name");
556 if (!paramName
.empty())
558 std::string paramValue
;
560 // <param name="posx" value="120" />
561 const char *value
= param
->Attribute(valueAttribute
); // try attribute first
566 // <param name="posx">120</param>
567 const TiXmlNode
*child
= param
->FirstChild();
568 if (child
&& child
->Type() == TiXmlNode::TINYXML_TEXT
)
569 paramValue
= child
->ValueStr(); // and then tag value
572 params
.insert({ paramName
, paramValue
}); // no overwrites
574 param
= param
->NextSiblingElement("param");
581 void CGUIIncludes::ResolveParametersForNode(TiXmlElement
*node
, const Params
& params
)
585 std::string newValue
;
586 // run through this element's attributes, resolving any parameters
587 TiXmlAttribute
*attribute
= node
->FirstAttribute();
590 ResolveParamsResult result
= ResolveParameters(attribute
->ValueStr(), newValue
, params
);
591 if (result
== SINGLE_UNDEFINED_PARAM_RESOLVED
&& strcmp(node
->Value(), "param") == 0 &&
592 strcmp(attribute
->Name(), "value") == 0 && node
->Parent() && strcmp(node
->Parent()->Value(), "include") == 0)
594 // special case: passing <param name="someName" value="$PARAM[undefinedParam]" /> to the nested include
595 // this usually happens when trying to forward a missing parameter from the enclosing include to the nested include
596 // 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
597 // 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
598 node
->Parent()->RemoveChild(node
);
601 else if (result
!= NO_PARAMS_FOUND
)
602 attribute
->SetValue(newValue
);
603 attribute
= attribute
->Next();
605 // run through this element's value and children, resolving any parameters
606 TiXmlNode
*child
= node
->FirstChild();
609 if (child
->Type() == TiXmlNode::TINYXML_TEXT
)
611 ResolveParamsResult result
= ResolveParameters(child
->ValueStr(), newValue
, params
);
612 if (result
== SINGLE_UNDEFINED_PARAM_RESOLVED
&& strcmp(node
->Value(), "param") == 0 &&
613 node
->Parent() && strcmp(node
->Parent()->Value(), "include") == 0)
615 // special case: passing <param name="someName">$PARAM[undefinedParam]</param> to the nested include
616 // we'll remove the offending param tag same as above
617 node
->Parent()->RemoveChild(node
);
619 else if (result
!= NO_PARAMS_FOUND
)
620 child
->SetValue(newValue
);
622 else if (child
->Type() == TiXmlNode::TINYXML_ELEMENT
||
623 child
->Type() == TiXmlNode::TINYXML_COMMENT
)
627 // save next as current child might be removed from the tree
628 TiXmlElement
* next
= child
->NextSiblingElement();
630 if (child
->Type() == TiXmlNode::TINYXML_ELEMENT
)
631 ResolveParametersForNode(static_cast<TiXmlElement
*>(child
), params
);
642 const std::map
<std::string
, std::string
>& m_params
;
643 // keep some stats so that we know exactly what's been resolved
644 int m_numTotalParams
= 0;
645 int m_numUndefinedParams
= 0;
648 explicit ParamReplacer(const std::map
<std::string
, std::string
>& params
) : m_params(params
) {}
649 int GetNumTotalParams() const { return m_numTotalParams
; }
650 int GetNumDefinedParams() const { return m_numTotalParams
- m_numUndefinedParams
; }
651 int GetNumUndefinedParams() const { return m_numUndefinedParams
; }
653 std::string
operator()(const std::string
¶mName
)
656 std::map
<std::string
, std::string
>::const_iterator it
= m_params
.find(paramName
);
657 if (it
!= m_params
.end())
659 m_numUndefinedParams
++;
664 CGUIIncludes::ResolveParamsResult
CGUIIncludes::ResolveParameters(const std::string
& strInput
, std::string
& strOutput
, const Params
& params
)
666 ParamReplacer
paramReplacer(params
);
667 if (GUIINFO::CGUIInfoLabel::ReplaceSpecialKeywordReferences(strInput
, "PARAM", std::ref(paramReplacer
), strOutput
))
668 // detect special input values of the form "$PARAM[undefinedParam]" (with no extra characters around)
669 return paramReplacer
.GetNumUndefinedParams() == 1 && paramReplacer
.GetNumTotalParams() == 1 && strOutput
.empty() ? SINGLE_UNDEFINED_PARAM_RESOLVED
: PARAMS_RESOLVED
;
670 return NO_PARAMS_FOUND
;
673 std::string
CGUIIncludes::ResolveConstant(const std::string
&constant
) const
675 std::vector
<std::string
> values
= StringUtils::Split(constant
, ",");
676 for (auto& i
: values
)
678 std::map
<std::string
, std::string
>::const_iterator it
= m_constants
.find(i
);
679 if (it
!= m_constants
.end())
682 return StringUtils::Join(values
, ",");
685 std::string
CGUIIncludes::ResolveExpressions(const std::string
&expression
) const
687 std::string
work(expression
);
688 GUIINFO::CGUIInfoLabel::ReplaceSpecialKeywordReferences(work
, "EXP", [&](const std::string
&str
) -> std::string
{
689 std::map
<std::string
, std::string
>::const_iterator it
= m_expressions
.find(str
);
690 if (it
!= m_expressions
.end())
698 const INFO::CSkinVariableString
* CGUIIncludes::CreateSkinVariable(const std::string
& name
, int context
)
700 std::map
<std::string
, TiXmlElement
>::const_iterator it
= m_skinvariables
.find(name
);
701 if (it
!= m_skinvariables
.end())
702 return INFO::CSkinVariable::CreateFromXML(it
->second
, context
);