diff options
-rw-r--r-- | cmake/treedata/common/subdirs.txt | 1 | ||||
-rw-r--r-- | xbmc/GUIInfoManager.cpp | 28 | ||||
-rw-r--r-- | xbmc/GUIInfoManager.h | 7 | ||||
-rw-r--r-- | xbmc/addons/Skin.cpp | 38 | ||||
-rw-r--r-- | xbmc/addons/Skin.h | 44 | ||||
-rw-r--r-- | xbmc/addons/gui/skin/CMakeLists.txt | 7 | ||||
-rw-r--r-- | xbmc/addons/gui/skin/SkinTimer.cpp | 98 | ||||
-rw-r--r-- | xbmc/addons/gui/skin/SkinTimer.h | 109 | ||||
-rw-r--r-- | xbmc/addons/gui/skin/SkinTimerManager.cpp | 249 | ||||
-rw-r--r-- | xbmc/addons/gui/skin/SkinTimerManager.h | 87 | ||||
-rw-r--r-- | xbmc/addons/gui/skin/SkinTimers.dox | 145 | ||||
-rw-r--r-- | xbmc/addons/kodi-dev-kit/doxygen/Skin/skin.dox | 1 | ||||
-rw-r--r-- | xbmc/application/ApplicationSkinHandling.cpp | 8 | ||||
-rw-r--r-- | xbmc/guilib/guiinfo/GUIInfoLabels.h | 2 | ||||
-rw-r--r-- | xbmc/guilib/guiinfo/SkinGUIInfo.cpp | 15 | ||||
-rw-r--r-- | xbmc/interfaces/builtins/SkinBuiltins.cpp | 86 |
16 files changed, 907 insertions, 18 deletions
diff --git a/cmake/treedata/common/subdirs.txt b/cmake/treedata/common/subdirs.txt index b55be8da8d..904799dd2c 100644 --- a/cmake/treedata/common/subdirs.txt +++ b/cmake/treedata/common/subdirs.txt @@ -3,6 +3,7 @@ xbmc/addons addons xbmc/addons/addoninfo addons_addoninfo xbmc/addons/binary-addons addons_binary-addons xbmc/addons/gui addons_gui +xbmc/addons/gui/skin addons_gui_skin xbmc/addons/interfaces addons_interfaces xbmc/addons/interfaces/gui addons_interfaces_gui xbmc/addons/interfaces/gui/controls addons_interfaces_gui_controls diff --git a/xbmc/GUIInfoManager.cpp b/xbmc/GUIInfoManager.cpp index dfc7071da7..dce25b51bb 100644 --- a/xbmc/GUIInfoManager.cpp +++ b/xbmc/GUIInfoManager.cpp @@ -7134,6 +7134,24 @@ const infomap fanart_labels[] = {{ "color1", FANART_COLOR1 }, /// @skinning_v20 **[New Infolabel]** \link Skin_Numeric `Skin.Numeric(settingid)`\endlink /// <p> /// } +/// \table_row3{ <b>`Skin.TimerElapsedSecs(timer)`</b>, +/// \anchor Skin_TimerElapsedSecs +/// _integer_ \, _string_, +/// @return The elapsed time in seconds for the provided `timer`. +/// @param timer - the timer name +/// <p><hr> +/// @skinning_v20 **[New Infolabel]** \link Skin_TimerElapsedSecs `Skin.TimerElapsedSecs(timer)`\endlink +/// <p> +/// } +/// \table_row3{ <b>`Skin.TimerIsRunning(timer)`</b>, +/// \anchor Skin_TimerIsRunning +/// _boolean_, +/// @return **True** if the given `timer` is active\, false otherwise. +/// @param timer - the timer name +/// <p><hr> +/// @skinning_v20 **[New Infolabel]** \link Skin_TimerIsRunning `Skin.TimerIsRunning(timer)`\endlink +/// <p> +/// } /// \table_end /// /// ----------------------------------------------------------------------------- @@ -10182,6 +10200,10 @@ int CGUIInfoManager::TranslateSingleString(const std::string &strCondition, bool return AddMultiInfo(CGUIInfo(SKIN_BOOL, CSkinSettings::GetInstance().TranslateBool(prop.param(0)))); else if (prop.name == "hastheme") return AddMultiInfo(CGUIInfo(SKIN_HAS_THEME, prop.param(0))); + else if (prop.name == "timerisrunning") + return AddMultiInfo(CGUIInfo(SKIN_TIMER_IS_RUNNING, prop.param(0))); + else if (prop.name == "timerelapsedsecs") + return AddMultiInfo(CGUIInfo(SKIN_TIMER_ELAPSEDSECS, prop.param(0))); } } else if (cat.name == "window") @@ -10570,6 +10592,12 @@ INFO::InfoPtr CGUIInfoManager::Register(const std::string &expression, int conte return *(res.first); } +void CGUIInfoManager::UnRegister(INFO::InfoPtr expression) +{ + std::unique_lock<CCriticalSection> lock(m_critInfo); + m_bools.erase(expression); +} + bool CGUIInfoManager::EvaluateBool(const std::string &expression, int contextWindow /* = 0 */, const CGUIListItemPtr &item /* = nullptr */) { INFO::InfoPtr info = Register(expression, contextWindow); diff --git a/xbmc/GUIInfoManager.h b/xbmc/GUIInfoManager.h index 5e522b54fa..b8750254fe 100644 --- a/xbmc/GUIInfoManager.h +++ b/xbmc/GUIInfoManager.h @@ -77,6 +77,13 @@ public: */ INFO::InfoPtr Register(const std::string &expression, int context = 0); + /*! \brief Unregister a boolean condition/expression + * This routine allows controls or other clients of the info manager to unregister a previously registered + * boolean condition/expression + \param expression the boolean condition or expression + */ + void UnRegister(INFO::InfoPtr expression); + /// \brief iterates through boolean conditions and compares their stored values to current values. Returns true if any condition changed value. bool ConditionsChangedValues(const std::map<INFO::InfoPtr, bool>& map); diff --git a/xbmc/addons/Skin.cpp b/xbmc/addons/Skin.cpp index 1618fe76e9..c0c5e343cc 100644 --- a/xbmc/addons/Skin.cpp +++ b/xbmc/addons/Skin.cpp @@ -282,6 +282,19 @@ void CSkinInfo::LoadIncludes() m_includes.Load(includesPath); } +void CSkinInfo::LoadTimers() +{ + const std::string timersPath = + CSpecialProtocol::TranslatePathConvertCase(GetSkinPath("Timers.xml")); + CLog::LogF(LOGINFO, "Trying to load skin timers from {}", timersPath); + m_skinTimerManager.LoadTimers(timersPath); +} + +void CSkinInfo::StartTimerEvaluation() +{ + m_skinTimerManager.Start(); +} + void CSkinInfo::ResolveIncludes(TiXmlElement *node, std::map<INFO::InfoPtr, bool>* xmlIncludeConditions /* = NULL */) { if(xmlIncludeConditions) @@ -401,6 +414,31 @@ void CSkinInfo::OnPostInstall(bool update, bool modal) } } +void CSkinInfo::Unload() +{ + m_skinTimerManager.Stop(); +} + +bool CSkinInfo::TimerIsRunning(const std::string& timer) const +{ + return m_skinTimerManager.TimerIsRunning(timer); +} + +float CSkinInfo::GetTimerElapsedSeconds(const std::string& timer) const +{ + return m_skinTimerManager.GetTimerElapsedSeconds(timer); +} + +void CSkinInfo::TimerStart(const std::string& timer) const +{ + m_skinTimerManager.TimerStart(timer); +} + +void CSkinInfo::TimerStop(const std::string& timer) const +{ + m_skinTimerManager.TimerStop(timer); +} + void CSkinInfo::SettingOptionsSkinColorsFiller(const SettingConstPtr& setting, std::vector<StringSettingOption>& list, std::string& current, diff --git a/xbmc/addons/Skin.h b/xbmc/addons/Skin.h index 3cd19e951a..1d2993df61 100644 --- a/xbmc/addons/Skin.h +++ b/xbmc/addons/Skin.h @@ -9,6 +9,7 @@ #pragma once #include "addons/Addon.h" +#include "addons/gui/skin/SkinTimerManager.h" #include "guilib/GUIIncludes.h" // needed for the GUIInclude member #include "windowing/GraphicContext.h" // needed for the RESOLUTION members @@ -166,6 +167,21 @@ public: const std::string& GetCurrentAspect() const { return m_currentAspect; } void LoadIncludes(); + + /*! \brief Load the defined skin timers + \details Skin timers are defined in Timers.xml \sa Skin_Timers + */ + void LoadTimers(); + + /*! \brief Starts evaluating timers + */ + void StartTimerEvaluation(); + + /*! \brief Called when unloading a skin, allows to cleanup specific + * skin resources. + */ + void Unload(); + void ToggleDebug(); const INFO::CSkinVariableString* CreateSkinVariable(const std::string& name, int context); @@ -216,6 +232,31 @@ public: void OnPreInstall() override; void OnPostInstall(bool update, bool modal) override; + + // skin timer methods + + /*! \brief Checks if the timer with name `timer` is running + \param timer the name of the skin timer + \return true if the given timer exists and is running, false otherwise + */ + bool TimerIsRunning(const std::string& timer) const; + + /*! \brief Get the elapsed seconds since the timer with name `timer` was started + \param timer the name of the skin timer + \return the elapsed time in seconds the given timer is running (0 if not running or if it does not exist) + */ + float GetTimerElapsedSeconds(const std::string& timer) const; + + /*! \brief Starts/Enables a given skin timer + \param timer the name of the skin timer + */ + void TimerStart(const std::string& timer) const; + + /*! \brief Stops/Disables a given skin timer + \param timer the name of the skin timer + */ + void TimerStop(const std::string& timer) const; + protected: bool LoadStartupWindows(const AddonInfoPtr& addonInfo); @@ -236,6 +277,9 @@ protected: std::vector<CStartupWindow> m_startupWindows; bool m_debugging; + /*! Manager/Owner of skin timers */ + CSkinTimerManager m_skinTimerManager; + private: std::map<int, CSkinSettingStringPtr> m_strings; std::map<int, CSkinSettingBoolPtr> m_bools; diff --git a/xbmc/addons/gui/skin/CMakeLists.txt b/xbmc/addons/gui/skin/CMakeLists.txt new file mode 100644 index 0000000000..916cd943d6 --- /dev/null +++ b/xbmc/addons/gui/skin/CMakeLists.txt @@ -0,0 +1,7 @@ +set(SOURCES SkinTimer.cpp + SkinTimerManager.cpp) + +set(HEADERS SkinTimer.h + SkinTimerManager.h) + +core_add_library(addons_gui_skin) diff --git a/xbmc/addons/gui/skin/SkinTimer.cpp b/xbmc/addons/gui/skin/SkinTimer.cpp new file mode 100644 index 0000000000..330fc4ed13 --- /dev/null +++ b/xbmc/addons/gui/skin/SkinTimer.cpp @@ -0,0 +1,98 @@ +/* + * 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. + */ +#include "SkinTimer.h" + +#include "interfaces/builtins/Builtins.h" +#include "interfaces/info/Info.h" + +CSkinTimer::CSkinTimer(const std::string& name, + const INFO::InfoPtr startCondition, + const INFO::InfoPtr resetCondition, + const INFO::InfoPtr stopCondition, + const std::string& startAction, + const std::string& stopAction, + bool resetOnStart) + : m_name{name}, + m_startCondition{startCondition}, + m_resetCondition{resetCondition}, + m_stopCondition{stopCondition}, + m_startAction{startAction}, + m_stopAction{stopAction}, + m_resetOnStart{resetOnStart} +{ +} + +void CSkinTimer::Start() +{ + if (m_resetOnStart) + { + CStopWatch::StartZero(); + } + else + { + CStopWatch::Start(); + } + OnStart(); +} + +void CSkinTimer::Reset() +{ + CStopWatch::Reset(); +} + +void CSkinTimer::Stop() +{ + CStopWatch::Stop(); + OnStop(); +} + +bool CSkinTimer::VerifyStartCondition() const +{ + return m_startCondition && m_startCondition->Get(INFO::DEFAULT_CONTEXT); +} + +bool CSkinTimer::VerifyResetCondition() const +{ + return m_resetCondition && m_resetCondition->Get(INFO::DEFAULT_CONTEXT); +} + +bool CSkinTimer::VerifyStopCondition() const +{ + return m_stopCondition && m_stopCondition->Get(INFO::DEFAULT_CONTEXT); +} + +INFO::InfoPtr CSkinTimer::GetStartCondition() const +{ + return m_startCondition; +} + +INFO::InfoPtr CSkinTimer::GetResetCondition() const +{ + return m_resetCondition; +} + +INFO::InfoPtr CSkinTimer::GetStopCondition() const +{ + return m_stopCondition; +} + +void CSkinTimer::OnStart() +{ + if (!m_startAction.empty()) + { + CBuiltins::GetInstance().Execute(m_startAction); + } +} + +void CSkinTimer::OnStop() +{ + if (!m_stopAction.empty()) + { + CBuiltins::GetInstance().Execute(m_stopAction); + } +} diff --git a/xbmc/addons/gui/skin/SkinTimer.h b/xbmc/addons/gui/skin/SkinTimer.h new file mode 100644 index 0000000000..8fe1aa7d22 --- /dev/null +++ b/xbmc/addons/gui/skin/SkinTimer.h @@ -0,0 +1,109 @@ +/* + * 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 "interfaces/info/InfoExpression.h" +#include "utils/Stopwatch.h" + +#include <memory> +#include <string> + +class TiXmlElement; + +/*! \brief Skin timers are skin objects that dependent on time and can be fully controlled from skins either using boolean + * conditions or builtin functions. This class represents the Skin Timer object. + * \sa Skin_Timers + */ +class CSkinTimer : public CStopWatch +{ +public: + /*! \brief Skin timer constructor + * \param name - the name of the timer + * \param startCondition - the boolean info expression to start the timer (may be null) + * \param resetCondition - the boolean info expression to reset the timer (may be null) + * \param stopCondition - the boolean info expression to stop the timer (may be null) + * \param startAction - the builtin function to execute on timer start (may be empty) + * \param stopAction - the builtin function to execute on timer stop (may be empty) + * \param resetOnStart - if the timer should be reset when started (i.e. start from zero if true or resumed if false) + */ + CSkinTimer(const std::string& name, + const INFO::InfoPtr startCondition, + const INFO::InfoPtr resetCondition, + const INFO::InfoPtr stopCondition, + const std::string& startAction, + const std::string& stopAction, + bool resetOnStart); + + /*! \brief Default skin timer destructor */ + virtual ~CSkinTimer() = default; + + /*! \brief Start the skin timer */ + void Start(); + + /*! \brief Resets the skin timer so that the elapsed time of the timer is 0 */ + void Reset(); + + /*! \brief stops the skin timer */ + void Stop(); + + /*! \brief Getter for the timer start boolean condition/expression + * \return the start boolean condition/expression (may be null) + */ + INFO::InfoPtr GetStartCondition() const; + + /*! \brief Getter for the timer reset boolean condition/expression + * \return the reset boolean condition/expression (may be null) + */ + INFO::InfoPtr GetResetCondition() const; + + /*! \brief Getter for the timer start boolean condition/expression + * \return the start boolean condition/expression (may be null) + */ + INFO::InfoPtr GetStopCondition() const; + + /*! \brief Evaluates the timer start boolean info expression returning the respective result. + * \details Called from the skin timer manager to check if the timer should be started + * \return true if the condition is true, false otherwise + */ + bool VerifyStartCondition() const; + + /*! \brief Evaluates the timer reset boolean info expression returning the respective result. + * \details Called from the skin timer manager to check if the timer should be reset to 0 + * \return true if the condition is true, false otherwise + */ + bool VerifyResetCondition() const; + + /*! \brief Evaluates the timer stop boolean info expression returning the respective result. + * \details Called from the skin timer manager to check if the timer should be stopped + * \return true if the condition is true, false otherwise + */ + bool VerifyStopCondition() const; + +private: + /*! \brief Called when this timer is started */ + void OnStart(); + + /*! \brief Called when this timer is stopped */ + void OnStop(); + + /*! The name of the skin timer */ + std::string m_name; + /*! The info boolean expression that automatically starts the timer if evaluated true */ + INFO::InfoPtr m_startCondition; + /*! The info boolean expression that automatically resets the timer if evaluated true */ + INFO::InfoPtr m_resetCondition; + /*! The info boolean expression that automatically stops the timer if evaluated true */ + INFO::InfoPtr m_stopCondition; + /*! The builtin function to be executed when the timer is started */ + std::string m_startAction; + /*! The builtin function to be executed when the timer is stopped */ + std::string m_stopAction; + /*! if the timer should be reset on start (or just resumed) */ + bool m_resetOnStart{false}; +}; diff --git a/xbmc/addons/gui/skin/SkinTimerManager.cpp b/xbmc/addons/gui/skin/SkinTimerManager.cpp new file mode 100644 index 0000000000..f1db0e5b88 --- /dev/null +++ b/xbmc/addons/gui/skin/SkinTimerManager.cpp @@ -0,0 +1,249 @@ +/* + * 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. + */ + +#include "SkinTimerManager.h" + +#include "GUIInfoManager.h" +#include "ServiceBroker.h" +#include "guilib/GUIComponent.h" +#include "interfaces/builtins/Builtins.h" +#include "utils/XBMCTinyXML.h" +#include "utils/log.h" + +#include <chrono> +#include <mutex> + +using namespace std::chrono_literals; + +CSkinTimerManager::CSkinTimerManager() : CThread("SkinTimers") +{ +} + +void CSkinTimerManager::Start() +{ + std::unique_lock<CCriticalSection> lock(m_timerCriticalSection); + if (!m_timers.empty()) + { + Create(); + } +} + +void CSkinTimerManager::LoadTimers(const std::string& path) +{ + CXBMCTinyXML doc; + if (!doc.LoadFile(path)) + { + CLog::LogF(LOGWARNING, "Could not load timers file {}: {} (row: {}, col: {})", path, + doc.ErrorDesc(), doc.ErrorRow(), doc.ErrorCol()); + return; + } + + TiXmlElement* root = doc.RootElement(); + if (!root || !StringUtils::EqualsNoCase(root->Value(), "timers")) + { + CLog::LogF(LOGERROR, "Error loading timers file {}: Root element <timers> required.", path); + return; + } + + const TiXmlElement* timerNode = root->FirstChildElement("timer"); + std::unique_lock<CCriticalSection> lock(m_timerCriticalSection); + while (timerNode) + { + LoadTimerInternal(timerNode); + timerNode = timerNode->NextSiblingElement("timer"); + } +} + +void CSkinTimerManager::LoadTimerInternal(const TiXmlElement* node) +{ + if ((!node->FirstChild("name") || !node->FirstChild("name")->FirstChild() || + node->FirstChild("name")->FirstChild()->ValueStr().empty())) + { + CLog::LogF(LOGERROR, "Missing required field name for valid skin. Ignoring timer."); + return; + } + + INFO::InfoPtr startInfo{nullptr}; + INFO::InfoPtr resetInfo{nullptr}; + INFO::InfoPtr stopInfo{nullptr}; + std::string timerName = node->FirstChild("name")->FirstChild()->Value(); + std::string startAction; + std::string stopAction; + bool resetOnStart{false}; + + if (m_timers.count(timerName) > 0) + { + CLog::LogF(LOGWARNING, + "Ignoring timer with name {} - another timer with the same name already exists", + timerName); + return; + } + + if (node->FirstChild("start") && node->FirstChild("start")->FirstChild() && + !node->FirstChild("start")->FirstChild()->ValueStr().empty()) + { + startInfo = CServiceBroker::GetGUI()->GetInfoManager().Register( + node->FirstChild("start")->FirstChild()->ValueStr()); + // check if timer needs to be reset after start + if (node->Attribute("reset") && StringUtils::EqualsNoCase(node->Attribute("reset"), "true")) + { + resetOnStart = true; + } + } + + if (node->FirstChild("reset") && node->FirstChild("reset")->FirstChild() && + !node->FirstChild("reset")->FirstChild()->ValueStr().empty()) + { + resetInfo = CServiceBroker::GetGUI()->GetInfoManager().Register( + node->FirstChild("reset")->FirstChild()->ValueStr()); + } + + if (node->FirstChild("stop") && node->FirstChild("stop")->FirstChild() && + !node->FirstChild("stop")->FirstChild()->ValueStr().empty()) + { + stopInfo = CServiceBroker::GetGUI()->GetInfoManager().Register( + node->FirstChild("stop")->FirstChild()->ValueStr()); + } + + if (node->FirstChild("onstart") && node->FirstChild("onstart")->FirstChild() && + !node->FirstChild("onstart")->FirstChild()->ValueStr().empty()) + { + if (!CBuiltins::GetInstance().HasCommand(node->FirstChild("onstart")->FirstChild()->ValueStr())) + { + CLog::LogF(LOGERROR, + "Unknown onstart builtin action {} for timer {}, the action will be ignored", + node->FirstChild("onstart")->FirstChild()->ValueStr(), timerName); + } + else + { + startAction = node->FirstChild("onstart")->FirstChild()->ValueStr(); + } + } + + if (node->FirstChild("onstop") && node->FirstChild("onstop")->FirstChild() && + !node->FirstChild("onstop")->FirstChild()->ValueStr().empty()) + { + if (!CBuiltins::GetInstance().HasCommand(node->FirstChild("onstop")->FirstChild()->ValueStr())) + { + CLog::LogF(LOGERROR, + "Unknown onstop builtin action {} for timer {}, the action will be ignored", + node->FirstChild("onstop")->FirstChild()->ValueStr(), timerName); + } + else + { + stopAction = node->FirstChild("onstop")->FirstChild()->ValueStr(); + } + } + + m_timers[timerName] = std::make_unique<CSkinTimer>( + CSkinTimer(timerName, startInfo, resetInfo, stopInfo, startAction, stopAction, resetOnStart)); +} + +bool CSkinTimerManager::TimerIsRunning(const std::string& timer) const +{ + std::unique_lock<CCriticalSection> lock(m_timerCriticalSection); + if (m_timers.count(timer) == 0) + { + CLog::LogF(LOGERROR, "Couldn't find Skin Timer with name: {}", timer); + return false; + } + return m_timers.at(timer)->IsRunning(); +} + +float CSkinTimerManager::GetTimerElapsedSeconds(const std::string& timer) const +{ + std::unique_lock<CCriticalSection> lock(m_timerCriticalSection); + if (m_timers.count(timer) == 0) + { + CLog::LogF(LOGERROR, "Couldn't find Skin Timer with name: {}", timer); + return 0; + } + return m_timers.at(timer)->GetElapsedSeconds(); +} + +void CSkinTimerManager::TimerStart(const std::string& timer) const +{ + std::unique_lock<CCriticalSection> lock(m_timerCriticalSection); + if (m_timers.count(timer) == 0) + { + CLog::LogF(LOGERROR, "Couldn't find Skin Timer with name: {}", timer); + return; + } + m_timers.at(timer)->Start(); +} + +void CSkinTimerManager::TimerStop(const std::string& timer) const +{ + std::unique_lock<CCriticalSection> lock(m_timerCriticalSection); + if (m_timers.count(timer) == 0) + { + CLog::LogF(LOGERROR, "Couldn't find Skin Timer with name: {}", timer); + return; + } + m_timers.at(timer)->Stop(); +} + +void CSkinTimerManager::Stop() +{ + StopThread(); + + // skintimers, as infomanager clients register info conditions/expressions in the infomanager. + // The infomanager is linked to skins, being initialized or cleared when + // skins are loaded (or unloaded). All the registered boolean conditions from + // skin timers will end up being removed when the skin is unloaded. However, to + // self-contain this component unregister them all here. + std::unique_lock<CCriticalSection> lock(m_timerCriticalSection); + for (auto const& [key, val] : m_timers) + { + const std::unique_ptr<CSkinTimer>::pointer timer = val.get(); + if (timer->GetStartCondition()) + { + CServiceBroker::GetGUI()->GetInfoManager().UnRegister(timer->GetStartCondition()); + } + if (timer->GetStopCondition()) + { + CServiceBroker::GetGUI()->GetInfoManager().UnRegister(timer->GetStopCondition()); + } + if (timer->GetResetCondition()) + { + CServiceBroker::GetGUI()->GetInfoManager().UnRegister(timer->GetResetCondition()); + } + } + m_timers.clear(); +} + +void CSkinTimerManager::StopThread(bool bWait /*= true*/) +{ + std::unique_lock<CCriticalSection> lock(m_timerCriticalSection); + m_bStop = true; + CThread::StopThread(bWait); +} + +void CSkinTimerManager::Process() +{ + while (!m_bStop) + { + for (auto const& [key, val] : m_timers) + { + const std::unique_ptr<CSkinTimer>::pointer timer = val.get(); + if (!timer->IsRunning() && timer->VerifyStartCondition()) + { + timer->Start(); + } + else if (timer->IsRunning() && timer->VerifyStopCondition()) + { + timer->Stop(); + } + if (timer->GetElapsedSeconds() > 0 && timer->VerifyResetCondition()) + { + timer->Reset(); + } + } + Sleep(500ms); + } +} diff --git a/xbmc/addons/gui/skin/SkinTimerManager.h b/xbmc/addons/gui/skin/SkinTimerManager.h new file mode 100644 index 0000000000..0a79b7804f --- /dev/null +++ b/xbmc/addons/gui/skin/SkinTimerManager.h @@ -0,0 +1,87 @@ +/* + * 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 "SkinTimer.h" +#include "threads/Thread.h" + +#include <map> +#include <memory> +#include <string> + +/*! \brief CSkinTimerManager is the container and manager for Skin timers. Its role is that of + * checking if the timer boolean conditions are valid, start or stop timers and execute the respective + * builtin actions linked to the timer lifecycle + * \sa Skin_Timers + * \sa CSkinTimer + */ +class CSkinTimerManager : public CThread +{ +public: + /*! \brief Skin timer manager constructor */ + CSkinTimerManager(); + + /*! \brief Default skin timer manager destructor */ + ~CSkinTimerManager() override = default; + + /*! \brief Loads all the skin timers + * \param path - the path for the skin Timers.xml file + */ + void LoadTimers(const std::string& path); + + /*! \brief Starts the manager */ + void Start(); + + /*! \brief Stops the manager */ + void Stop(); + + /*! \brief Checks if the timer with name `timer` is running + \param timer the name of the skin timer + \return true if the given timer exists and is running, false otherwise + */ + bool TimerIsRunning(const std::string& timer) const; + + /*! \brief Get the elapsed seconds since the timer with name `timer` was started + \param timer the name of the skin timer + \return the elapsed time in seconds the given timer is running (0 if not running or if it does not exist) + */ + float GetTimerElapsedSeconds(const std::string& timer) const; + + /*! \brief Starts/Enables a given skin timer + \param timer the name of the skin timer + */ + void TimerStart(const std::string& timer) const; + + /*! \brief Stops/Disables a given skin timer + \param timer the name of the skin timer + */ + void TimerStop(const std::string& timer) const; + + // CThread methods + + /*! \brief Start and run the main manager loop */ + void Process() override; + + /*! \brief Stop the manager thread + \param bWait - If the callee should wait for the thread to exit (default is true) + */ + void StopThread(bool bWait = true) override; + +private: + /*! \brief Loads a specific timer + * \note Called internally from LoadTimers + * \param node - the XML representation of a skin timer object + */ + void LoadTimerInternal(const TiXmlElement* node); + + /*! Container for the skin timers */ + std::map<std::string, std::unique_ptr<CSkinTimer>> m_timers; + + mutable CCriticalSection m_timerCriticalSection; +}; diff --git a/xbmc/addons/gui/skin/SkinTimers.dox b/xbmc/addons/gui/skin/SkinTimers.dox new file mode 100644 index 0000000000..9f56590ff1 --- /dev/null +++ b/xbmc/addons/gui/skin/SkinTimers.dox @@ -0,0 +1,145 @@ +/*! + +\page Skin_Timers Skin Timers +\brief **Programatic time-based objects for Skins** + +\tableofcontents + +-------------------------------------------------------------------------------- +\section Skin_Timers_sect1 Description + +Skin timers are skin objects that are dependent on time and can be fully controlled from skins either using +\link page_List_of_built_in_functions **Builtin functions**\endlink or +\link modules__infolabels_boolean_conditions **Infolabels and Boolean conditions**\endlink. One can see them +as stopwatches that can be activated and deactivated automatically depending on the value of info expressions or simply activated/deactivated +manually from builtins. +The framework was created to allow skins to control the visibility of windows (and controls) depending on +the elapsed time of timers the skin defines. Skin timers allow multiple use cases in skins, previously only available via the execution +of python scripts: +- Closing a specific window after x seconds have elapsed +- Controlling the visibility of a group (or triggering an animation) depending on the elapsed time of a given timer +- Defining a buffer time window that is kept activated for a short period of time (e.g. keep controls visible for x seconds after a player seek) +- Executing timed actions (on timer stop or timer start) +- etc + +Skin timers are defined in the `Timers.xml` file within the xml directory of the skin. The file has the following "schema": + +~~~~~~~~~~~~~{.xml} +<timers> + <timer>...</timer> + <timer>...</timer> +</timers> +~~~~~~~~~~~~~ + +see \link Skin_Timers_sect2 the examples section\endlink and \link Skin_Timers_sect3 the list of available tags\endlink for concrete details. + +\skinning_v20 Added skin timers + +-------------------------------------------------------------------------------- +\section Skin_Timers_sect2 Examples + +The following example illustrates the simplest possible skin timer. This timer is completely manual (it has to be manually started and stopped): + +~~~~~~~~~~~~~{.xml} +<timer> + <name>mymanualtimer</name> + <description>100% manual timer</description> +</timer> +~~~~~~~~~~~~~ + +This timer can be controlled from your skin by executing the \link Builtin_SkinStartTimer `Skin.TimerStart(mymanualtimer)` builtin\endlink or +\link Builtin_SkinStopTimer `Skin.TimerStop(mymanualtimer)` builtin\endlink. You can define the visibility of skin elements based on the internal +properties of the timer, such as the fact that the timer is active/running using \link Skin_TimerIsRunning `Skin.TimerIsRunning(mymanualtimer)` builtin\endlink +or depending on the elapsed time (e.g. 5 seconds) using \link Skin_TimerElapsedSecs Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(mymanualtimer),5)\endlink. + +The following timer is a variation of the previous timer but with the added ability of being automatically stopped by the skinning engine after a maximum of elapsed +5 seconds without having to issue the `Skin.TimerStop(mymanualtimer)` builtin: + +~~~~~~~~~~~~~{.xml} +<timer> + <name>mymanualautocloseabletimer</name> + <description>100% manual autocloseable timer</description> + <stop>Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(mymanualautocloseabletimer),5)</stop> +</timer> +~~~~~~~~~~~~~ + +This type of timer is particularly useful if you want to automatically close a specific window (or triggering a close animation) after x time has elapsed, +while guaranteeing the timer is also stopped. See the example below: + +~~~~~~~~~~~~~{.xml} +<?xml version="1.0" encoding="utf-8"?> +<window type="dialog" id="1109"> + <onload>Skin.TimerStart(mymanualautocloseabletimer)</onload> + ... + <controls> + <control type="group"> + <animation effect="slide" start="0,0" end="0,-80" time="300" condition="Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(mymanualautocloseabletimer),5)">Conditional</animation> + ... + </control> + </controls> +</window> +~~~~~~~~~~~~~ + +The following timer presents a notification (for 1 sec) whenever the timer is activated or deactivated: + +~~~~~~~~~~~~~{.xml} +<timer> + <name>manualtimerwithactions</name> + <description>100% manual timer with actions</description> + <onstart>Notification(skintimer, My timer was started, 1000)</onstart> + <onstop>Notification(skintimer, My timer was stopped, 1000)</onstop> +</timer> +~~~~~~~~~~~~~ + +The following timer is an example of a completely automatic timer. The timer is automatically activated or deactivated based on the value +of boolean info expressions. In this particular example, the timer is automatically started whenever the Player is playing a file (if not already running). It is stopped if +there is no file being played (and of course if previously running). Since the timer can be activated/deactivated multiple times, `reset="true"` ensures the timer is +always reset to 0 on each start operation. Whenever the timer is started or stopped, notifications are issued. + +~~~~~~~~~~~~~{.xml} +<timer> + <name>myautomatictimer</name> + <description>Player state checker</description> + <start reset="true">Player.Playing</start> + <stop>!Player.Playing</stop> + <onstart>Notification(skintimer, Player has started playing a file, 1000)</onstart> + <onstop>Notification(skintimer, Player is no longer playing a file, 1000)</onstop> +</timer> +~~~~~~~~~~~~~ + +In certain situations you might want to reset your timer without having to stop and start. For instance, if you want to stop the timer after 5 seconds +but have the timer resetting to 0 seconds if the user provides some input to Kodi. For such cases the `<reset/>` condition can be used: + +~~~~~~~~~~~~~{.xml} +<timer> + <name>windowtimer</name> + <description>Reset on idle</description> + <start reset="true">Window.IsActive(mywindow)</start> + <reset>Window.IsActive(mywindow) + !System.IdleTime(1) + Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(windowtimer), 1)</reset> + <stop>!Window.IsActive(mywindow) + Integer.IsGreaterOrEqual(Skin.TimerElapsedSecs(windowtimer), 5)</stop> + <onstop>Dialog.Close(mywindow)</onstop> +</timer> +~~~~~~~~~~~~~ + +-------------------------------------------------------------------------------- +\section Skin_Timers_sect3 Available tags + +Skin timers have the following available tags: + +| Tag | Description | +|--------------:|:--------------------------------------------------------------| +| name | The unique name of the timer. The name is used as the id of the timer, hence needs to be unique. <b>(required)</b> +| description | The description of the timer, a helper string. <b>(optional)</b> +| start | An info bool expression that the skinning engine should use to automatically start the timer <b>(optional)</b> +| reset | An info bool expression that the skinning engine should use to automatically reset the timer <b>(optional)</b> +| stop | An info bool expression that the skinning engine should use to automatically stop the timer <b>(optional)</b> +| onstart | A builtin function that the skinning engine should execute when the timer is started <b>(optional)</b> +| onstop | A builtin function that the skinning engine should execute when the timer is stopped <b>(optional)</b> + +-------------------------------------------------------------------------------- +\section Skin_Timers_sect4 See also +#### Development: + +- [Skinning](http://kodi.wiki/view/Skinning) + +*/ diff --git a/xbmc/addons/kodi-dev-kit/doxygen/Skin/skin.dox b/xbmc/addons/kodi-dev-kit/doxygen/Skin/skin.dox index 3705f3903a..e4e4fc022a 100644 --- a/xbmc/addons/kodi-dev-kit/doxygen/Skin/skin.dox +++ b/xbmc/addons/kodi-dev-kit/doxygen/Skin/skin.dox @@ -21,6 +21,7 @@ or two by adding a button, or altering the textures or layout. - \subpage skin_controls - Controls are the substance of your skin. - \subpage window_ids - List of available window names, definitions, IDs and the matching XML-file +- \subpage Skin_Timers - Programatic time-based objects for Skins */ diff --git a/xbmc/application/ApplicationSkinHandling.cpp b/xbmc/application/ApplicationSkinHandling.cpp index 84de3da8e0..12abf9c57f 100644 --- a/xbmc/application/ApplicationSkinHandling.cpp +++ b/xbmc/application/ApplicationSkinHandling.cpp @@ -135,6 +135,7 @@ bool CApplicationSkinHandling::LoadSkin(const std::string& skinID, g_localizeStrings.LoadSkinStrings(langPath, settings->GetString(CSettings::SETTING_LOCALE_LANGUAGE)); + g_SkinInfo->LoadTimers(); const auto start = std::chrono::steady_clock::now(); @@ -204,6 +205,10 @@ bool CApplicationSkinHandling::LoadSkin(const std::string& skinID, } } + // start timer manager after all windows were loaded and skin state was restored since timers might depend on + // boolean conditions that reference specific windows + g_SkinInfo->StartTimerEvaluation(); + return true; } @@ -214,6 +219,9 @@ void CApplicationSkinHandling::UnloadSkin() else if (!m_saveSkinOnUnloading) m_saveSkinOnUnloading = true; + if (g_SkinInfo) + g_SkinInfo->Unload(); + CGUIComponent* gui = CServiceBroker::GetGUI(); if (gui) { diff --git a/xbmc/guilib/guiinfo/GUIInfoLabels.h b/xbmc/guilib/guiinfo/GUIInfoLabels.h index f9d0d0d111..ea6bee515e 100644 --- a/xbmc/guilib/guiinfo/GUIInfoLabels.h +++ b/xbmc/guilib/guiinfo/GUIInfoLabels.h @@ -406,6 +406,8 @@ #define SKIN_ASPECT_RATIO 607 #define SKIN_FONT 608 #define SKIN_INTEGER 609 +#define SKIN_TIMER_IS_RUNNING 610 +#define SKIN_TIMER_ELAPSEDSECS 611 #define SYSTEM_IS_SCREENSAVER_INHIBITED 641 #define SYSTEM_ADDON_UPDATE_COUNT 642 diff --git a/xbmc/guilib/guiinfo/SkinGUIInfo.cpp b/xbmc/guilib/guiinfo/SkinGUIInfo.cpp index 0c052eeaa2..1d51b96221 100644 --- a/xbmc/guilib/guiinfo/SkinGUIInfo.cpp +++ b/xbmc/guilib/guiinfo/SkinGUIInfo.cpp @@ -72,6 +72,11 @@ bool CSkinGUIInfo::GetLabel(std::string& value, const CFileItem *item, int conte value = CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_FONT); return true; } + case SKIN_TIMER_ELAPSEDSECS: + { + value = std::to_string(g_SkinInfo->GetTimerElapsedSeconds(info.GetData3())); + return true; + } } return false; @@ -86,6 +91,11 @@ bool CSkinGUIInfo::GetInt(int& value, const CGUIListItem *gitem, int contextWind value = CSkinSettings::GetInstance().GetInt(info.GetData1()); return true; } + case SKIN_TIMER_ELAPSEDSECS: + { + value = g_SkinInfo->GetTimerElapsedSeconds(info.GetData3()); + return true; + } } return false; } @@ -119,6 +129,11 @@ bool CSkinGUIInfo::GetBool(bool& value, const CGUIListItem *gitem, int contextWi value = StringUtils::EqualsNoCase(theme, info.GetData3()); return true; } + case SKIN_TIMER_IS_RUNNING: + { + value = g_SkinInfo->TimerIsRunning(info.GetData3()); + return true; + } } return false; diff --git a/xbmc/interfaces/builtins/SkinBuiltins.cpp b/xbmc/interfaces/builtins/SkinBuiltins.cpp index 89384c1342..a256324fa4 100644 --- a/xbmc/interfaces/builtins/SkinBuiltins.cpp +++ b/xbmc/interfaces/builtins/SkinBuiltins.cpp @@ -458,6 +458,38 @@ static int SkinDebug(const std::vector<std::string>& params) return 0; } +/*! \brief Starts a given skin timer + * \param params The parameters. + * \details params[0] = Name of the timer. + * \return -1 in case of error, 0 in case of success + */ +static int SkinTimerStart(const std::vector<std::string>& params) +{ + if (params.empty()) + { + return -1; + } + + g_SkinInfo->TimerStart(params[0]); + return 0; +} + +/*! \brief Stops a given skin timer + * \param params The parameters. + * \details params[0] = Name of the timer. + * \return -1 in case of error, 0 in case of success + */ +static int SkinTimerStop(const std::vector<std::string>& params) +{ + if (params.empty()) + { + return -1; + } + + g_SkinInfo->TimerStop(params[0]); + return 0; +} + // Note: For new Texts with comma add a "\" before!!! Is used for table text. // /// \page page_List_of_built_in_functions @@ -603,27 +635,45 @@ static int SkinDebug(const std::vector<std::string>& params) /// containing `Skin.HasSetting(setting)`. /// @param[in] setting Skin setting to toggle /// } +/// \table_row2_l{ +/// <b>`Skin.TimerStart(timer)`</b> +/// \anchor Builtin_SkinStartTimer, +/// Starts the timer with name `timer` +/// @param[in] timer The name of the timer +/// <p><hr> +/// @skinning_v20 **[New builtin]** \link Builtin_SkinStartTimer `Skin.TimerStart(timer)`\endlink +/// <p> +/// } +/// \table_row2_l{ +/// <b>`Skin.TimerStop(timer)`</b> +/// \anchor Builtin_SkinStopTimer, +/// Stops the timer with name `timer` +/// @param[in] timer The name of the timer +/// <p><hr> +/// @skinning_v20 **[New builtin]** \link Builtin_SkinStopTimer `Skin.TimerStop(timer)`\endlink +/// <p> +/// } /// \table_end /// CBuiltins::CommandMap CSkinBuiltins::GetOperations() const { - return { - {"reloadskin", {"Reload Kodi's skin", 0, ReloadSkin}}, - {"unloadskin", {"Unload Kodi's skin", 0, UnloadSkin}}, - {"skin.reset", {"Resets a skin setting to default", 1, SkinReset}}, - {"skin.resetsettings", {"Resets all skin settings", 0, SkinResetAll}}, - {"skin.setaddon", {"Prompts and set an addon", 2, SetAddon}}, - {"skin.selectbool", {"Prompts and set a skin setting", 2, SelectBool}}, - {"skin.setbool", {"Sets a skin setting on", 1, SetBool}}, - {"skin.setfile", {"Prompts and sets a file", 1, SetFile}}, - {"skin.setimage", {"Prompts and sets a skin image", 1, SetImage}}, - {"skin.setcolor", {"Prompts and sets a skin color", 1, SetColor}}, - {"skin.setnumeric", {"Prompts and sets numeric input", 1, SetNumeric}}, - {"skin.setpath", {"Prompts and sets a skin path", 1, SetPath}}, - {"skin.setstring", {"Prompts and sets skin string", 1, SetString}}, - {"skin.theme", {"Control skin theme", 1, SetTheme}}, - {"skin.toggledebug", {"Toggle skin debug", 0, SkinDebug}}, - {"skin.togglesetting", {"Toggles a skin setting on or off", 1, ToggleSetting}} - }; + return {{"reloadskin", {"Reload Kodi's skin", 0, ReloadSkin}}, + {"unloadskin", {"Unload Kodi's skin", 0, UnloadSkin}}, + {"skin.reset", {"Resets a skin setting to default", 1, SkinReset}}, + {"skin.resetsettings", {"Resets all skin settings", 0, SkinResetAll}}, + {"skin.setaddon", {"Prompts and set an addon", 2, SetAddon}}, + {"skin.selectbool", {"Prompts and set a skin setting", 2, SelectBool}}, + {"skin.setbool", {"Sets a skin setting on", 1, SetBool}}, + {"skin.setfile", {"Prompts and sets a file", 1, SetFile}}, + {"skin.setimage", {"Prompts and sets a skin image", 1, SetImage}}, + {"skin.setcolor", {"Prompts and sets a skin color", 1, SetColor}}, + {"skin.setnumeric", {"Prompts and sets numeric input", 1, SetNumeric}}, + {"skin.setpath", {"Prompts and sets a skin path", 1, SetPath}}, + {"skin.setstring", {"Prompts and sets skin string", 1, SetString}}, + {"skin.theme", {"Control skin theme", 1, SetTheme}}, + {"skin.toggledebug", {"Toggle skin debug", 0, SkinDebug}}, + {"skin.togglesetting", {"Toggles a skin setting on or off", 1, ToggleSetting}}, + {"skin.timerstart", {"Starts a given skin timer", 1, SkinTimerStart}}, + {"skin.timerstop", {"Stops a given skin timer", 1, SkinTimerStop}}}; } |