aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--cmake/treedata/common/subdirs.txt1
-rw-r--r--xbmc/GUIInfoManager.cpp28
-rw-r--r--xbmc/GUIInfoManager.h7
-rw-r--r--xbmc/addons/Skin.cpp38
-rw-r--r--xbmc/addons/Skin.h44
-rw-r--r--xbmc/addons/gui/skin/CMakeLists.txt7
-rw-r--r--xbmc/addons/gui/skin/SkinTimer.cpp98
-rw-r--r--xbmc/addons/gui/skin/SkinTimer.h109
-rw-r--r--xbmc/addons/gui/skin/SkinTimerManager.cpp249
-rw-r--r--xbmc/addons/gui/skin/SkinTimerManager.h87
-rw-r--r--xbmc/addons/gui/skin/SkinTimers.dox145
-rw-r--r--xbmc/addons/kodi-dev-kit/doxygen/Skin/skin.dox1
-rw-r--r--xbmc/application/ApplicationSkinHandling.cpp8
-rw-r--r--xbmc/guilib/guiinfo/GUIInfoLabels.h2
-rw-r--r--xbmc/guilib/guiinfo/SkinGUIInfo.cpp15
-rw-r--r--xbmc/interfaces/builtins/SkinBuiltins.cpp86
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}}};
}