diff options
Diffstat (limited to '')
-rw-r--r-- | xbmc/interfaces/info/CMakeLists.txt | 10 | ||||
-rw-r--r-- | xbmc/interfaces/info/Info.h | 19 | ||||
-rw-r--r-- | xbmc/interfaces/info/InfoBool.cpp | 25 | ||||
-rw-r--r-- | xbmc/interfaces/info/InfoBool.h | 83 | ||||
-rw-r--r-- | xbmc/interfaces/info/InfoExpression.cpp | 311 | ||||
-rw-r--r-- | xbmc/interfaces/info/InfoExpression.h | 117 | ||||
-rw-r--r-- | xbmc/interfaces/info/SkinVariable.cpp | 82 | ||||
-rw-r--r-- | xbmc/interfaces/info/SkinVariable.h | 56 |
8 files changed, 703 insertions, 0 deletions
diff --git a/xbmc/interfaces/info/CMakeLists.txt b/xbmc/interfaces/info/CMakeLists.txt new file mode 100644 index 0000000..621ecfc --- /dev/null +++ b/xbmc/interfaces/info/CMakeLists.txt @@ -0,0 +1,10 @@ +set(SOURCES InfoBool.cpp + InfoExpression.cpp + SkinVariable.cpp) + +set(HEADERS Info.h + InfoBool.h + InfoExpression.h + SkinVariable.h) + +core_add_library(info_interface) diff --git a/xbmc/interfaces/info/Info.h b/xbmc/interfaces/info/Info.h new file mode 100644 index 0000000..e454e3c --- /dev/null +++ b/xbmc/interfaces/info/Info.h @@ -0,0 +1,19 @@ +/* + * Copyright (C) 2022 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. + */ + +#pragma once + +namespace INFO +{ +/*! Default context of the INFO interface + @note when info conditions are evaluated different contexts can be passed. Context usually refers to the window ids where + the conditions are being evaluated. By default conditions, skin variables and labels are initialized using the DEFAULT_CONTEXT + value unless specifically bound to a given window. + */ +constexpr int DEFAULT_CONTEXT = 0; +} // namespace INFO diff --git a/xbmc/interfaces/info/InfoBool.cpp b/xbmc/interfaces/info/InfoBool.cpp new file mode 100644 index 0000000..65b1f44 --- /dev/null +++ b/xbmc/interfaces/info/InfoBool.cpp @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2013-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 "InfoBool.h" + +#include "utils/StringUtils.h" + +namespace INFO +{ + InfoBool::InfoBool(const std::string &expression, int context, unsigned int &refreshCounter) + : m_value(false), + m_context(context), + m_listItemDependent(false), + m_expression(expression), + m_refreshCounter(0), + m_parentRefreshCounter(refreshCounter) + { + StringUtils::ToLower(m_expression); + } +} diff --git a/xbmc/interfaces/info/InfoBool.h b/xbmc/interfaces/info/InfoBool.h new file mode 100644 index 0000000..6906276 --- /dev/null +++ b/xbmc/interfaces/info/InfoBool.h @@ -0,0 +1,83 @@ +/* + * 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. + */ + +#pragma once + +#include <memory> +#include <string> + +class CGUIListItem; + +namespace INFO +{ +/*! + \ingroup info + \brief Base class, wrapping boolean conditions and expressions + */ +class InfoBool +{ +public: + InfoBool(const std::string &expression, int context, unsigned int &refreshCounter); + virtual ~InfoBool() = default; + + virtual void Initialize() {} + + /*! \brief Get the value of this info bool + This is called to update (if dirty) and fetch the value of the info bool + \param contextWindow the context (window id) where this condition is being evaluated + \param item the item used to evaluate the bool + */ + inline bool Get(int contextWindow, const CGUIListItem* item = nullptr) + { + if (item && m_listItemDependent) + Update(contextWindow, item); + else if (m_refreshCounter != m_parentRefreshCounter || m_refreshCounter == 0) + { + Update(contextWindow, nullptr); + m_refreshCounter = m_parentRefreshCounter; + } + return m_value; + } + + bool operator==(const InfoBool &right) const + { + return (m_context == right.m_context && + m_expression == right.m_expression); + } + + bool operator<(const InfoBool &right) const + { + if (m_context < right.m_context) + return true; + else if (m_context == right.m_context) + return m_expression < right.m_expression; + else + return false; + } + + /*! \brief Update the value of this info bool + This is called if and only if the info bool is dirty, allowing it to update it's current value + */ + virtual void Update(int contextWindow, const CGUIListItem* item) {} + + const std::string &GetExpression() const { return m_expression; } + bool ListItemDependent() const { return m_listItemDependent; } +protected: + + bool m_value; ///< current value + int m_context; ///< contextual information to go with the condition + bool m_listItemDependent; ///< do not cache if a listitem pointer is given + std::string m_expression; ///< original expression + +private: + unsigned int m_refreshCounter; + unsigned int &m_parentRefreshCounter; +}; + +typedef std::shared_ptr<InfoBool> InfoPtr; +}; diff --git a/xbmc/interfaces/info/InfoExpression.cpp b/xbmc/interfaces/info/InfoExpression.cpp new file mode 100644 index 0000000..dc9aa63 --- /dev/null +++ b/xbmc/interfaces/info/InfoExpression.cpp @@ -0,0 +1,311 @@ +/* + * 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 "InfoExpression.h" + +#include "GUIInfoManager.h" +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "utils/log.h" + +#include <list> +#include <memory> +#include <stack> + +using namespace INFO; + +void InfoSingle::Initialize() +{ + m_condition = CServiceBroker::GetGUI()->GetInfoManager().TranslateSingleString(m_expression, m_listItemDependent); +} + +void InfoSingle::Update(int contextWindow, const CGUIListItem* item) +{ + // use propagated context in case this info has the default context (i.e. if not tied to a specific window) + // its value might depend on the context in which the evaluation was called + int context = m_context == DEFAULT_CONTEXT ? contextWindow : m_context; + m_value = CServiceBroker::GetGUI()->GetInfoManager().GetBool(m_condition, context, item); +} + +void InfoExpression::Initialize() +{ + if (!Parse(m_expression)) + { + CLog::Log(LOGERROR, "Error parsing boolean expression {}", m_expression); + m_expression_tree = std::make_shared<InfoLeaf>(CServiceBroker::GetGUI()->GetInfoManager().Register("false", 0), false); + } +} + +void InfoExpression::Update(int contextWindow, const CGUIListItem* item) +{ + // use propagated context in case this info expression has the default context (i.e. if not tied to a specific window) + // its value might depend on the context in which the evaluation was called + int context = m_context == DEFAULT_CONTEXT ? contextWindow : m_context; + m_value = m_expression_tree->Evaluate(context, item); +} + +/* Expressions are rewritten at parse time into a form which favours the + * formation of groups of associative nodes. These groups are then reordered at + * evaluation time such that nodes whose value renders the evaluation of the + * remainder of the group unnecessary tend to be evaluated first (these are + * true nodes for OR subexpressions, or false nodes for AND subexpressions). + * The end effect is to minimise the number of leaf nodes that need to be + * evaluated in order to determine the value of the expression. The runtime + * adaptability has the advantage of not being customised for any particular skin. + * + * The modifications to the expression at parse time fall into two groups: + * 1) Moving logical NOTs so that they are only applied to leaf nodes. + * For example, rewriting ![A+B]|C as !A|!B|C allows reordering such that + * any of the three leaves can be evaluated first. + * 2) Combining adjacent AND or OR operations such that each path from the root + * to a leaf encounters a strictly alternating pattern of AND and OR + * operations. So [A|B]|[C|D+[[E|F]|G] becomes A|B|C|[D+[E|F|G]]. + */ + +bool InfoExpression::InfoLeaf::Evaluate(int contextWindow, const CGUIListItem* item) +{ + return m_invert ^ m_info->Get(contextWindow, item); +} + +InfoExpression::InfoAssociativeGroup::InfoAssociativeGroup( + node_type_t type, + const InfoSubexpressionPtr &left, + const InfoSubexpressionPtr &right) + : m_type(type) +{ + AddChild(right); + AddChild(left); +} + +void InfoExpression::InfoAssociativeGroup::AddChild(const InfoSubexpressionPtr &child) +{ + m_children.push_front(child); // largely undoes the effect of parsing right-associative +} + +void InfoExpression::InfoAssociativeGroup::Merge(const std::shared_ptr<InfoAssociativeGroup>& other) +{ + m_children.splice(m_children.end(), other->m_children); +} + +bool InfoExpression::InfoAssociativeGroup::Evaluate(int contextWindow, const CGUIListItem* item) +{ + /* Handle either AND or OR by using the relation + * A AND B == !(!A OR !B) + * to convert ANDs into ORs + */ + std::list<InfoSubexpressionPtr>::iterator last = m_children.end(); + std::list<InfoSubexpressionPtr>::iterator it = m_children.begin(); + bool use_and = (m_type == NODE_AND); + bool result = use_and ^ (*it)->Evaluate(contextWindow, item); + while (!result && ++it != last) + { + result = use_and ^ (*it)->Evaluate(contextWindow, item); + if (result) + { + /* Move this child to the head of the list so we evaluate faster next time */ + m_children.push_front(*it); + m_children.erase(it); + } + } + return use_and ^ result; +} + +/* Expressions are parsed using the shunting-yard algorithm. Binary operators + * (AND/OR) are treated as right-associative so that we don't need to make a + * special case for the unary NOT operator. This has no effect upon the answers + * generated, though the initial sequence of evaluation of leaves may be + * different from what you might expect. + */ + +InfoExpression::operator_t InfoExpression::GetOperator(char ch) +{ + if (ch == '[') + return OPERATOR_LB; + else if (ch == ']') + return OPERATOR_RB; + else if (ch == '!') + return OPERATOR_NOT; + else if (ch == '+') + return OPERATOR_AND; + else if (ch == '|') + return OPERATOR_OR; + else + return OPERATOR_NONE; +} + +void InfoExpression::OperatorPop(std::stack<operator_t> &operator_stack, bool &invert, std::stack<InfoSubexpressionPtr> &nodes) +{ + operator_t op2 = operator_stack.top(); + operator_stack.pop(); + if (op2 == OPERATOR_NOT) + { + invert = !invert; + } + else + { + // At this point, it can only be OPERATOR_AND or OPERATOR_OR + if (invert) + op2 = (operator_t) (OPERATOR_AND ^ OPERATOR_OR ^ op2); + node_type_t new_type = op2 == OPERATOR_AND ? NODE_AND : NODE_OR; + + InfoSubexpressionPtr right = nodes.top(); + nodes.pop(); + InfoSubexpressionPtr left = nodes.top(); + + node_type_t right_type = right->Type(); + node_type_t left_type = left->Type(); + + // Combine associative operations into the same node where possible + if (left_type == new_type && right_type == new_type) + /* For example: AND + * / \ ____ AND ____ + * AND AND -> / / \ \ + * / \ / \ leaf leaf leaf leaf + * leaf leaf leaf leaf + */ + std::static_pointer_cast<InfoAssociativeGroup>(left)->Merge(std::static_pointer_cast<InfoAssociativeGroup>(right)); + else if (left_type == new_type) + /* For example: AND AND + * / \ / | \ + * AND OR -> leaf leaf OR + * / \ / \ / \ + * leaf leaf leaf leaf leaf leaf + */ + std::static_pointer_cast<InfoAssociativeGroup>(left)->AddChild(right); + else + { + nodes.pop(); + if (right_type == new_type) + { + /* For example: AND AND + * / \ / | \ + * OR AND -> OR leaf leaf + * / \ / \ / \ + * leaf leaf leaf leaf leaf leaf + */ + std::static_pointer_cast<InfoAssociativeGroup>(right)->AddChild(left); + nodes.push(right); + } + else + /* For example: AND which can't be simplified, and + * / \ requires a new AND node to be + * OR OR created with the two OR nodes + * / \ / \ as children + * leaf leaf leaf leaf + */ + nodes.push(std::make_shared<InfoAssociativeGroup>(new_type, left, right)); + } + } +} + +bool InfoExpression::Parse(const std::string &expression) +{ + const char *s = expression.c_str(); + std::string operand; + std::stack<operator_t> operator_stack; + bool invert = false; + std::stack<InfoSubexpressionPtr> nodes; + // The next two are for syntax-checking purposes + bool after_binaryoperator = true; + int bracket_count = 0; + + CGUIInfoManager& infoMgr = CServiceBroker::GetGUI()->GetInfoManager(); + + char c; + // Skip leading whitespace - don't want it to count as an operand if that's all there is + while (isspace((unsigned char)(c=*s))) + s++; + + while ((c = *s++) != '\0') + { + operator_t op; + if ((op = GetOperator(c)) != OPERATOR_NONE) + { + // Character is an operator + if ((!after_binaryoperator && (c == '!' || c == '[')) || + (after_binaryoperator && (c == ']' || c == '+' || c == '|'))) + { + CLog::Log(LOGERROR, "Misplaced {}", c); + return false; + } + if (c == '[') + bracket_count++; + else if (c == ']' && bracket_count-- == 0) + { + CLog::Log(LOGERROR, "Unmatched ]"); + return false; + } + if (!operand.empty()) + { + InfoPtr info = infoMgr.Register(operand, m_context); + if (!info) + { + CLog::Log(LOGERROR, "Bad operand '{}'", operand); + return false; + } + /* Propagate any listItem dependency from the operand to the expression */ + m_listItemDependent |= info->ListItemDependent(); + nodes.push(std::make_shared<InfoLeaf>(info, invert)); + /* Reuse operand string for next operand */ + operand.clear(); + } + + // Handle any higher-priority stacked operators, except when the new operator is left-bracket. + // For a right-bracket, this will stop with the matching left-bracket at the top of the operator stack. + if (op != OPERATOR_LB) + { + while (!operator_stack.empty() && operator_stack.top() > op) + OperatorPop(operator_stack, invert, nodes); + } + if (op == OPERATOR_RB) + operator_stack.pop(); // remove the matching left-bracket + else + operator_stack.push(op); + if (op == OPERATOR_NOT) + invert = !invert; + + if (c == '+' || c == '|') + after_binaryoperator = true; + // Skip trailing whitespace - don't want it to count as an operand if that's all there is + while (isspace((unsigned char)(c=*s))) s++; + } + else + { + // Character is part of operand + operand += c; + after_binaryoperator = false; + } + } + if (bracket_count > 0) + { + CLog::Log(LOGERROR, "Unmatched ["); + return false; + } + if (after_binaryoperator) + { + CLog::Log(LOGERROR, "Missing operand"); + return false; + } + if (!operand.empty()) + { + InfoPtr info = infoMgr.Register(operand, m_context); + if (!info) + { + CLog::Log(LOGERROR, "Bad operand '{}'", operand); + return false; + } + /* Propagate any listItem dependency from the operand to the expression */ + m_listItemDependent |= info->ListItemDependent(); + nodes.push(std::make_shared<InfoLeaf>(info, invert)); + } + while (!operator_stack.empty()) + OperatorPop(operator_stack, invert, nodes); + + m_expression_tree = nodes.top(); + return true; +} diff --git a/xbmc/interfaces/info/InfoExpression.h b/xbmc/interfaces/info/InfoExpression.h new file mode 100644 index 0000000..1a0877c --- /dev/null +++ b/xbmc/interfaces/info/InfoExpression.h @@ -0,0 +1,117 @@ +/* + * 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. + */ + +#pragma once + +#include "InfoBool.h" + +#include <list> +#include <stack> +#include <utility> +#include <vector> + +class CGUIListItem; + +namespace INFO +{ +/*! \brief Class to wrap active boolean conditions + */ +class InfoSingle : public InfoBool +{ +public: + InfoSingle(const std::string& expression, int context, unsigned int& refreshCounter) + : InfoBool(expression, context, refreshCounter) + { + } + void Initialize() override; + + void Update(int contextWindow, const CGUIListItem* item) override; + +private: + int m_condition; ///< actual condition this represents +}; + +/*! \brief Class to wrap active boolean expressions + */ +class InfoExpression : public InfoBool +{ +public: + InfoExpression(const std::string& expression, int context, unsigned int& refreshCounter) + : InfoBool(expression, context, refreshCounter) + { + } + ~InfoExpression() override = default; + + void Initialize() override; + + void Update(int contextWindow, const CGUIListItem* item) override; + +private: + typedef enum + { + OPERATOR_NONE = 0, + OPERATOR_LB, // 1 + OPERATOR_RB, // 2 + OPERATOR_OR, // 3 + OPERATOR_AND, // 4 + OPERATOR_NOT, // 5 + } operator_t; + + typedef enum + { + NODE_LEAF, + NODE_AND, + NODE_OR, + } node_type_t; + + // An abstract base class for nodes in the expression tree + class InfoSubexpression + { + public: + virtual ~InfoSubexpression(void) = default; // so we can destruct derived classes using a pointer to their base class + virtual bool Evaluate(int contextWindow, const CGUIListItem* item) = 0; + virtual node_type_t Type() const=0; + }; + + typedef std::shared_ptr<InfoSubexpression> InfoSubexpressionPtr; + + // A leaf node in the expression tree + class InfoLeaf : public InfoSubexpression + { + public: + InfoLeaf(InfoPtr info, bool invert) : m_info(std::move(info)), m_invert(invert) {} + bool Evaluate(int contextWindow, const CGUIListItem* item) override; + node_type_t Type() const override { return NODE_LEAF; } + + private: + InfoPtr m_info; + bool m_invert; + }; + + // A branch node in the expression tree + class InfoAssociativeGroup : public InfoSubexpression + { + public: + InfoAssociativeGroup(node_type_t type, const InfoSubexpressionPtr &left, const InfoSubexpressionPtr &right); + void AddChild(const InfoSubexpressionPtr &child); + void Merge(const std::shared_ptr<InfoAssociativeGroup>& other); + bool Evaluate(int contextWindow, const CGUIListItem* item) override; + node_type_t Type() const override { return m_type; } + + private: + node_type_t m_type; + std::list<InfoSubexpressionPtr> m_children; + }; + + static operator_t GetOperator(char ch); + static void OperatorPop(std::stack<operator_t> &operator_stack, bool &invert, std::stack<InfoSubexpressionPtr> &nodes); + bool Parse(const std::string &expression); + InfoSubexpressionPtr m_expression_tree; +}; + +}; diff --git a/xbmc/interfaces/info/SkinVariable.cpp b/xbmc/interfaces/info/SkinVariable.cpp new file mode 100644 index 0000000..53980b9 --- /dev/null +++ b/xbmc/interfaces/info/SkinVariable.cpp @@ -0,0 +1,82 @@ +/* + * 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 "SkinVariable.h" + +#include "GUIInfoManager.h" +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "utils/XBMCTinyXML.h" + +using namespace INFO; +using namespace KODI; + +const CSkinVariableString* CSkinVariable::CreateFromXML(const TiXmlElement& node, int context) +{ + const char* name = node.Attribute("name"); + if (name) + { + CSkinVariableString* tmp = new CSkinVariableString; + tmp->m_name = name; + tmp->m_context = context; + const TiXmlElement* valuenode = node.FirstChildElement("value"); + while (valuenode) + { + CSkinVariableString::ConditionLabelPair pair; + const char *condition = valuenode->Attribute("condition"); + if (condition) + pair.m_condition = CServiceBroker::GetGUI()->GetInfoManager().Register(condition, context); + + auto label = valuenode->FirstChild() ? valuenode->FirstChild()->ValueStr() : ""; + pair.m_label = GUILIB::GUIINFO::CGUIInfoLabel(label); + tmp->m_conditionLabelPairs.push_back(pair); + if (!pair.m_condition) + break; // once we reach default value (without condition) break iterating + + valuenode = valuenode->NextSiblingElement("value"); + } + if (!tmp->m_conditionLabelPairs.empty()) + return tmp; + delete tmp; + } + return NULL; +} + +CSkinVariableString::CSkinVariableString() = default; + +int CSkinVariableString::GetContext() const +{ + return m_context; +} + +const std::string& CSkinVariableString::GetName() const +{ + return m_name; +} + +std::string CSkinVariableString::GetValue(int contextWindow, + bool preferImage /* = false */, + const CGUIListItem* item /* = nullptr */) const +{ + for (const auto& it : m_conditionLabelPairs) + { + // use propagated context in case this skin variable has the default context (i.e. if not tied to a specific window) + // nested skin variables are supported + int context = m_context == INFO::DEFAULT_CONTEXT ? contextWindow : m_context; + if (!it.m_condition || it.m_condition->Get(context, item)) + { + if (item) + return it.m_label.GetItemLabel(item, preferImage); + else + { + return it.m_label.GetLabel(context, preferImage); + } + } + } + return ""; +} diff --git a/xbmc/interfaces/info/SkinVariable.h b/xbmc/interfaces/info/SkinVariable.h new file mode 100644 index 0000000..f00f96f --- /dev/null +++ b/xbmc/interfaces/info/SkinVariable.h @@ -0,0 +1,56 @@ +/* + * 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. + */ + +#pragma once + +#include "guilib/guiinfo/GUIInfoLabel.h" +#include "interfaces/info/InfoBool.h" + +#include <string> +#include <vector> + +class TiXmlElement; + +namespace INFO +{ +class CSkinVariableString; + +class CSkinVariable +{ +public: + static const CSkinVariableString* CreateFromXML(const TiXmlElement& node, int context); +}; + +class CSkinVariableString +{ +public: + const std::string& GetName() const; + int GetContext() const; + std::string GetValue(int contextWindow, + bool preferImage = false, + const CGUIListItem* item = nullptr) const; + +private: + CSkinVariableString(); + + std::string m_name; + int m_context; + + struct ConditionLabelPair + { + INFO::InfoPtr m_condition; + KODI::GUILIB::GUIINFO::CGUIInfoLabel m_label; + }; + + typedef std::vector<ConditionLabelPair> VECCONDITIONLABELPAIR; + VECCONDITIONLABELPAIR m_conditionLabelPairs; + + friend class CSkinVariable; +}; + +} |