summaryrefslogtreecommitdiffstats
path: root/xbmc/addons/Skin.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'xbmc/addons/Skin.cpp')
-rw-r--r--xbmc/addons/Skin.cpp906
1 files changed, 906 insertions, 0 deletions
diff --git a/xbmc/addons/Skin.cpp b/xbmc/addons/Skin.cpp
new file mode 100644
index 0000000..4450a90
--- /dev/null
+++ b/xbmc/addons/Skin.cpp
@@ -0,0 +1,906 @@
+/*
+ * 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 "Skin.h"
+
+#include "FileItem.h"
+#include "ServiceBroker.h"
+#include "Util.h"
+#include "addons/addoninfo/AddonType.h"
+#include "dialogs/GUIDialogKaiToast.h"
+#include "filesystem/Directory.h"
+#include "filesystem/SpecialProtocol.h"
+#include "guilib/GUIComponent.h"
+#include "guilib/GUIWindowManager.h"
+#include "guilib/LocalizeStrings.h"
+#include "guilib/WindowIDs.h"
+#include "messaging/ApplicationMessenger.h"
+#include "messaging/helpers/DialogHelper.h"
+#include "settings/Settings.h"
+#include "settings/SettingsComponent.h"
+#include "settings/lib/Setting.h"
+#include "settings/lib/SettingDefinitions.h"
+#include "threads/Timer.h"
+#include "utils/FileUtils.h"
+#include "utils/StringUtils.h"
+#include "utils/URIUtils.h"
+#include "utils/Variant.h"
+#include "utils/XMLUtils.h"
+#include "utils/log.h"
+
+#include <charconv>
+
+#define XML_SETTINGS "settings"
+#define XML_SETTING "setting"
+#define XML_ATTR_TYPE "type"
+#define XML_ATTR_NAME "name"
+#define XML_ATTR_ID "id"
+
+using namespace XFILE;
+using namespace KODI::MESSAGING;
+using namespace std::chrono_literals;
+
+using KODI::MESSAGING::HELPERS::DialogResponse;
+
+std::shared_ptr<ADDON::CSkinInfo> g_SkinInfo;
+
+namespace
+{
+constexpr auto DELAY = 500ms;
+}
+
+namespace ADDON
+{
+
+class CSkinSettingUpdateHandler : private ITimerCallback
+{
+public:
+ CSkinSettingUpdateHandler(CAddon& addon)
+ : m_addon(addon), m_timer(this) {}
+ ~CSkinSettingUpdateHandler() override = default;
+
+ void OnTimeout() override;
+ void TriggerSave();
+private:
+ CAddon &m_addon;
+ CTimer m_timer;
+};
+
+bool CSkinSetting::Serialize(TiXmlElement* parent) const
+{
+ if (parent == nullptr)
+ return false;
+
+ TiXmlElement setting(XML_SETTING);
+ setting.SetAttribute(XML_ATTR_ID, name.c_str());
+ setting.SetAttribute(XML_ATTR_TYPE, GetType());
+
+ if (!SerializeSetting(&setting))
+ return false;
+
+ parent->InsertEndChild(setting);
+
+ return true;
+}
+
+bool CSkinSetting::Deserialize(const TiXmlElement* element)
+{
+ if (element == nullptr)
+ return false;
+
+ name = XMLUtils::GetAttribute(element, XML_ATTR_ID);
+
+ // backwards compatibility for guisettings.xml
+ if (name.empty())
+ name = XMLUtils::GetAttribute(element, XML_ATTR_NAME);
+
+ return true;
+}
+
+bool CSkinSettingString::Deserialize(const TiXmlElement* element)
+{
+ value.clear();
+
+ if (!CSkinSetting::Deserialize(element))
+ return false;
+
+ if (element->FirstChild() != nullptr)
+ value = element->FirstChild()->Value();
+
+ return true;
+}
+
+bool CSkinSettingString::SerializeSetting(TiXmlElement* element) const
+{
+ if (element == nullptr)
+ return false;
+
+ TiXmlText xmlValue(value);
+ element->InsertEndChild(xmlValue);
+
+ return true;
+}
+
+bool CSkinSettingBool::Deserialize(const TiXmlElement* element)
+{
+ value = false;
+
+ if (!CSkinSetting::Deserialize(element))
+ return false;
+
+ if (element->FirstChild() != nullptr)
+ value = StringUtils::EqualsNoCase(element->FirstChild()->ValueStr(), "true");
+
+ return true;
+}
+
+bool CSkinSettingBool::SerializeSetting(TiXmlElement* element) const
+{
+ if (element == nullptr)
+ return false;
+
+ TiXmlText xmlValue(value ? "true" : "false");
+ element->InsertEndChild(xmlValue);
+
+ return true;
+}
+
+CSkinInfo::CSkinInfo(const AddonInfoPtr& addonInfo,
+ const RESOLUTION_INFO& resolution /* = RESOLUTION_INFO() */)
+ : CAddon(addonInfo, AddonType::SKIN),
+ m_defaultRes(resolution),
+ m_effectsSlowDown(1.f),
+ m_debugging(false)
+{
+ m_settingsUpdateHandler.reset(new CSkinSettingUpdateHandler(*this));
+}
+
+CSkinInfo::CSkinInfo(const AddonInfoPtr& addonInfo) : CAddon(addonInfo, AddonType::SKIN)
+{
+ for (const auto& values : Type(AddonType::SKIN)->GetValues())
+ {
+ if (values.first != "res")
+ continue;
+
+ int width = values.second.GetValue("res@width").asInteger();
+ int height = values.second.GetValue("res@height").asInteger();
+ bool defRes = values.second.GetValue("res@default").asBoolean();
+ std::string folder = values.second.GetValue("res@folder").asString();
+ std::string strAspect = values.second.GetValue("res@aspect").asString();
+ float aspect = 0;
+
+ std::vector<std::string> fracs = StringUtils::Split(strAspect, ':');
+ if (fracs.size() == 2)
+ aspect = (float)(atof(fracs[0].c_str()) / atof(fracs[1].c_str()));
+ if (width > 0 && height > 0)
+ {
+ RESOLUTION_INFO res(width, height, aspect, folder);
+ res.strId = strAspect; // for skin usage, store aspect string in strId
+ if (defRes)
+ m_defaultRes = res;
+ m_resolutions.push_back(res);
+ }
+ }
+
+ m_effectsSlowDown = Type(AddonType::SKIN)->GetValue("@effectslowdown").asFloat();
+ if (m_effectsSlowDown == 0.0f)
+ m_effectsSlowDown = 1.f;
+
+ m_debugging = Type(AddonType::SKIN)->GetValue("@debugging").asBoolean();
+
+ m_settingsUpdateHandler.reset(new CSkinSettingUpdateHandler(*this));
+ LoadStartupWindows(addonInfo);
+}
+
+CSkinInfo::~CSkinInfo() = default;
+
+struct closestRes
+{
+ explicit closestRes(const RESOLUTION_INFO &target) : m_target(target) { };
+ bool operator()(const RESOLUTION_INFO &i, const RESOLUTION_INFO &j)
+ {
+ float diff = fabs(i.DisplayRatio() - m_target.DisplayRatio()) - fabs(j.DisplayRatio() - m_target.DisplayRatio());
+ if (diff < 0) return true;
+ if (diff > 0) return false;
+ diff = fabs((float)i.iHeight - m_target.iHeight) - fabs((float)j.iHeight - m_target.iHeight);
+ if (diff < 0) return true;
+ if (diff > 0) return false;
+ return fabs((float)i.iWidth - m_target.iWidth) < fabs((float)j.iWidth - m_target.iWidth);
+ }
+ RESOLUTION_INFO m_target;
+};
+
+void CSkinInfo::Start()
+{
+ if (!LoadUserSettings())
+ CLog::Log(LOGWARNING, "CSkinInfo: failed to load skin settings");
+
+ if (!m_resolutions.size())
+ { // try falling back to whatever resolutions exist in the directory
+ CFileItemList items;
+ CDirectory::GetDirectory(Path(), items, "", DIR_FLAG_NO_FILE_DIRS);
+ for (int i = 0; i < items.Size(); i++)
+ {
+ RESOLUTION_INFO res;
+ if (items[i]->m_bIsFolder && TranslateResolution(items[i]->GetLabel(), res))
+ m_resolutions.push_back(res);
+ }
+ }
+
+ if (!m_resolutions.empty())
+ {
+ // find the closest resolution
+ const RESOLUTION_INFO &target = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+ RESOLUTION_INFO& res = *std::min_element(m_resolutions.begin(), m_resolutions.end(), closestRes(target));
+ m_currentAspect = res.strId;
+ }
+}
+
+std::string CSkinInfo::GetSkinPath(const std::string& strFile, RESOLUTION_INFO *res, const std::string& strBaseDir /* = "" */) const
+{
+ if (m_resolutions.empty())
+ return ""; // invalid skin
+
+ std::string strPathToUse = Path();
+ if (!strBaseDir.empty())
+ strPathToUse = strBaseDir;
+
+ // if the caller doesn't care about the resolution just use a temporary
+ RESOLUTION_INFO tempRes;
+ if (!res)
+ res = &tempRes;
+
+ // find the closest resolution
+ const RESOLUTION_INFO &target = CServiceBroker::GetWinSystem()->GetGfxContext().GetResInfo();
+ *res = *std::min_element(m_resolutions.begin(), m_resolutions.end(), closestRes(target));
+
+ std::string strPath = URIUtils::AddFileToFolder(strPathToUse, res->strMode, strFile);
+ if (CFileUtils::Exists(strPath))
+ return strPath;
+
+ // use the default resolution
+ *res = m_defaultRes;
+
+ return URIUtils::AddFileToFolder(strPathToUse, res->strMode, strFile);
+}
+
+bool CSkinInfo::HasSkinFile(const std::string &strFile) const
+{
+ return CFileUtils::Exists(GetSkinPath(strFile));
+}
+
+void CSkinInfo::LoadIncludes()
+{
+ std::string includesPath =
+ CSpecialProtocol::TranslatePathConvertCase(GetSkinPath("Includes.xml"));
+ CLog::Log(LOGINFO, "Loading skin includes from {}", includesPath);
+ m_includes.Clear();
+ 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::ProcessTimers()
+{
+ m_skinTimerManager.Process();
+}
+void CSkinInfo::ResolveIncludes(TiXmlElement* node,
+ std::map<INFO::InfoPtr, bool>* xmlIncludeConditions /* = nullptr */)
+{
+ if(xmlIncludeConditions)
+ xmlIncludeConditions->clear();
+
+ m_includes.Resolve(node, xmlIncludeConditions);
+}
+
+int CSkinInfo::GetStartWindow() const
+{
+ int windowID = CServiceBroker::GetSettingsComponent()->GetSettings()->GetInt(CSettings::SETTING_LOOKANDFEEL_STARTUPWINDOW);
+ assert(m_startupWindows.size());
+ for (std::vector<CStartupWindow>::const_iterator it = m_startupWindows.begin(); it != m_startupWindows.end(); ++it)
+ {
+ if (windowID == (*it).m_id)
+ return windowID;
+ }
+ // return our first one
+ return m_startupWindows[0].m_id;
+}
+
+bool CSkinInfo::LoadStartupWindows(const AddonInfoPtr& addonInfo)
+{
+ m_startupWindows.clear();
+ m_startupWindows.emplace_back(WINDOW_HOME, "513");
+ m_startupWindows.emplace_back(WINDOW_TV_CHANNELS, "19180");
+ m_startupWindows.emplace_back(WINDOW_TV_GUIDE, "19273");
+ m_startupWindows.emplace_back(WINDOW_RADIO_CHANNELS, "19183");
+ m_startupWindows.emplace_back(WINDOW_RADIO_GUIDE, "19274");
+ m_startupWindows.emplace_back(WINDOW_PROGRAMS, "0");
+ m_startupWindows.emplace_back(WINDOW_PICTURES, "1");
+ m_startupWindows.emplace_back(WINDOW_MUSIC_NAV, "2");
+ m_startupWindows.emplace_back(WINDOW_VIDEO_NAV, "3");
+ m_startupWindows.emplace_back(WINDOW_FILES, "7");
+ m_startupWindows.emplace_back(WINDOW_SETTINGS_MENU, "5");
+ m_startupWindows.emplace_back(WINDOW_WEATHER, "8");
+ m_startupWindows.emplace_back(WINDOW_FAVOURITES, "1036");
+ return true;
+}
+
+void CSkinInfo::GetSkinPaths(std::vector<std::string> &paths) const
+{
+ RESOLUTION_INFO res;
+ GetSkinPath("Home.xml", &res);
+ if (!res.strMode.empty())
+ paths.push_back(URIUtils::AddFileToFolder(Path(), res.strMode));
+ if (res.strMode != m_defaultRes.strMode)
+ paths.push_back(URIUtils::AddFileToFolder(Path(), m_defaultRes.strMode));
+}
+
+bool CSkinInfo::TranslateResolution(const std::string &name, RESOLUTION_INFO &res)
+{
+ std::string lower(name); StringUtils::ToLower(lower);
+ if (lower == "pal")
+ res = RESOLUTION_INFO(720, 576, 4.0f/3, "pal");
+ else if (lower == "pal16x9")
+ res = RESOLUTION_INFO(720, 576, 16.0f/9, "pal16x9");
+ else if (lower == "ntsc")
+ res = RESOLUTION_INFO(720, 480, 4.0f/3, "ntsc");
+ else if (lower == "ntsc16x9")
+ res = RESOLUTION_INFO(720, 480, 16.0f/9, "ntsc16x9");
+ else if (lower == "720p")
+ res = RESOLUTION_INFO(1280, 720, 0, "720p");
+ else if (lower == "1080i")
+ res = RESOLUTION_INFO(1920, 1080, 0, "1080i");
+ else
+ return false;
+ return true;
+}
+
+int CSkinInfo::GetFirstWindow() const
+{
+ int startWindow = GetStartWindow();
+ if (HasSkinFile("Startup.xml"))
+ startWindow = WINDOW_STARTUP_ANIM;
+ return startWindow;
+}
+
+bool CSkinInfo::IsInUse() const
+{
+ // Could extend this to prompt for reverting to the standard skin perhaps
+ return CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SKIN) == ID();
+}
+
+const INFO::CSkinVariableString* CSkinInfo::CreateSkinVariable(const std::string& name, int context)
+{
+ return m_includes.CreateSkinVariable(name, context);
+}
+
+void CSkinInfo::OnPreInstall()
+{
+ bool skinLoaded = g_SkinInfo != nullptr;
+ if (IsInUse() && skinLoaded)
+ CServiceBroker::GetAppMessenger()->SendMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr,
+ "UnloadSkin");
+}
+
+void CSkinInfo::OnPostInstall(bool update, bool modal)
+{
+ if (!g_SkinInfo)
+ return;
+
+ if (IsInUse() || (!update && !modal &&
+ HELPERS::ShowYesNoDialogText(CVariant{Name()}, CVariant{24099}) ==
+ DialogResponse::CHOICE_YES))
+ {
+ CGUIDialogKaiToast *toast = CServiceBroker::GetGUI()->GetWindowManager().GetWindow<CGUIDialogKaiToast>(WINDOW_DIALOG_KAI_TOAST);
+ if (toast)
+ {
+ toast->ResetTimer();
+ toast->Close(true);
+ }
+ if (CServiceBroker::GetSettingsComponent()->GetSettings()->GetString(CSettings::SETTING_LOOKANDFEEL_SKIN) == ID())
+ CServiceBroker::GetAppMessenger()->PostMsg(TMSG_EXECUTE_BUILT_IN, -1, -1, nullptr,
+ "ReloadSkin");
+ else
+ CServiceBroker::GetSettingsComponent()->GetSettings()->SetString(CSettings::SETTING_LOOKANDFEEL_SKIN, ID());
+ }
+}
+
+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,
+ void* data)
+{
+ if (!g_SkinInfo)
+ return;
+
+ std::string settingValue = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ // Remove the .xml extension from the Themes
+ if (URIUtils::HasExtension(settingValue, ".xml"))
+ URIUtils::RemoveExtension(settingValue);
+ current = "SKINDEFAULT";
+
+ // There is a default theme (just defaults.xml)
+ // any other *.xml files are additional color themes on top of this one.
+
+ // add the default label
+ list.emplace_back(g_localizeStrings.Get(15109), "SKINDEFAULT"); // the standard defaults.xml will be used!
+
+ // Search for colors in the Current skin!
+ std::vector<std::string> vecColors;
+ std::string strPath = URIUtils::AddFileToFolder(g_SkinInfo->Path(), "colors");
+
+ CFileItemList items;
+ CDirectory::GetDirectory(CSpecialProtocol::TranslatePathConvertCase(strPath), items, ".xml", DIR_FLAG_DEFAULTS);
+ // Search for Themes in the Current skin!
+ for (int i = 0; i < items.Size(); ++i)
+ {
+ CFileItemPtr pItem = items[i];
+ if (!pItem->m_bIsFolder && !StringUtils::EqualsNoCase(pItem->GetLabel(), "defaults.xml"))
+ { // not the default one
+ vecColors.push_back(pItem->GetLabel().substr(0, pItem->GetLabel().size() - 4));
+ }
+ }
+ sort(vecColors.begin(), vecColors.end(), sortstringbyname());
+ for (int i = 0; i < (int) vecColors.size(); ++i)
+ list.emplace_back(vecColors[i], vecColors[i]);
+
+ // try to find the best matching value
+ for (const auto& elem : list)
+ {
+ if (StringUtils::EqualsNoCase(elem.value, settingValue))
+ current = settingValue;
+ }
+}
+
+void CSkinInfo::SettingOptionsSkinFontsFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ if (!g_SkinInfo)
+ return;
+
+ std::string settingValue = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ bool currentValueSet = false;
+ std::string strPath = g_SkinInfo->GetSkinPath("Font.xml");
+
+ CXBMCTinyXML xmlDoc;
+ if (!xmlDoc.LoadFile(strPath))
+ {
+ CLog::Log(LOGERROR, "FillInSkinFonts: Couldn't load {}", strPath);
+ return;
+ }
+
+ const TiXmlElement* pRootElement = xmlDoc.RootElement();
+ if (!pRootElement || pRootElement->ValueStr() != "fonts")
+ {
+ CLog::Log(LOGERROR, "FillInSkinFonts: file {} doesn't start with <fonts>", strPath);
+ return;
+ }
+
+ const TiXmlElement *pChild = pRootElement->FirstChildElement("fontset");
+ while (pChild)
+ {
+ const char* idAttr = pChild->Attribute("id");
+ const char* idLocAttr = pChild->Attribute("idloc");
+ if (idAttr != nullptr)
+ {
+ if (idLocAttr)
+ list.emplace_back(g_localizeStrings.Get(atoi(idLocAttr)), idAttr);
+ else
+ list.emplace_back(idAttr, idAttr);
+
+ if (StringUtils::EqualsNoCase(idAttr, settingValue))
+ currentValueSet = true;
+ }
+ pChild = pChild->NextSiblingElement("fontset");
+ }
+
+ if (list.empty())
+ { // Since no fontset is defined, there is no selection of a fontset, so disable the component
+ list.emplace_back(g_localizeStrings.Get(13278), "");
+ current = "";
+ currentValueSet = true;
+ }
+
+ if (!currentValueSet)
+ current = list[0].value;
+}
+
+void CSkinInfo::SettingOptionsSkinThemesFiller(const SettingConstPtr& setting,
+ std::vector<StringSettingOption>& list,
+ std::string& current,
+ void* data)
+{
+ // get the chosen theme and remove the extension from the current theme (backward compat)
+ std::string settingValue = std::static_pointer_cast<const CSettingString>(setting)->GetValue();
+ URIUtils::RemoveExtension(settingValue);
+ current = "SKINDEFAULT";
+
+ // there is a default theme (just Textures.xbt)
+ // any other *.xbt files are additional themes on top of this one.
+
+ // add the default Label
+ list.emplace_back(g_localizeStrings.Get(15109), "SKINDEFAULT"); // the standard Textures.xbt will be used
+
+ // search for themes in the current skin!
+ std::vector<std::string> vecTheme;
+ CUtil::GetSkinThemes(vecTheme);
+
+ // sort the themes for GUI and list them
+ for (int i = 0; i < (int) vecTheme.size(); ++i)
+ list.emplace_back(vecTheme[i], vecTheme[i]);
+
+ // try to find the best matching value
+ for (const auto& elem : list)
+ {
+ if (StringUtils::EqualsNoCase(elem.value, settingValue))
+ current = settingValue;
+ }
+}
+
+void CSkinInfo::SettingOptionsStartupWindowsFiller(const SettingConstPtr& setting,
+ std::vector<IntegerSettingOption>& list,
+ int& current,
+ void* data)
+{
+ if (!g_SkinInfo)
+ return;
+
+ int settingValue = std::static_pointer_cast<const CSettingInt>(setting)->GetValue();
+ current = -1;
+
+ const std::vector<CStartupWindow> &startupWindows = g_SkinInfo->GetStartupWindows();
+
+ for (std::vector<CStartupWindow>::const_iterator it = startupWindows.begin(); it != startupWindows.end(); ++it)
+ {
+ std::string windowName = it->m_name;
+ if (StringUtils::IsNaturalNumber(windowName))
+ windowName = g_localizeStrings.Get(atoi(windowName.c_str()));
+ int windowID = it->m_id;
+
+ list.emplace_back(windowName, windowID);
+
+ if (settingValue == windowID)
+ current = settingValue;
+ }
+
+ // if the current value hasn't been properly set, set it to the first window in the list
+ if (current < 0)
+ current = list[0].value;
+}
+
+void CSkinInfo::ToggleDebug()
+{
+ m_debugging = !m_debugging;
+}
+
+int CSkinInfo::TranslateString(const std::string &setting)
+{
+ // run through and see if we have this setting
+ for (const auto& it : m_strings)
+ {
+ if (StringUtils::EqualsNoCase(setting, it.second->name))
+ return it.first;
+ }
+
+ // didn't find it - insert it
+ CSkinSettingStringPtr skinString(new CSkinSettingString());
+ skinString->name = setting;
+
+ int number = m_bools.size() + m_strings.size();
+ m_strings.insert(std::pair<int, CSkinSettingStringPtr>(number, skinString));
+
+ return number;
+}
+
+int CSkinInfo::GetInt(int setting) const
+{
+ const std::string settingValue = GetString(setting);
+ if (settingValue.empty())
+ {
+ return -1;
+ }
+ int settingValueInt{-1};
+ std::from_chars(settingValue.data(), settingValue.data() + settingValue.size(), settingValueInt);
+ return settingValueInt;
+}
+
+const std::string& CSkinInfo::GetString(int setting) const
+{
+ const auto& it = m_strings.find(setting);
+ if (it != m_strings.end())
+ return it->second->value;
+
+ return StringUtils::Empty;
+}
+
+void CSkinInfo::SetString(int setting, const std::string &label)
+{
+ auto&& it = m_strings.find(setting);
+ if (it != m_strings.end())
+ {
+ it->second->value = label;
+ m_settingsUpdateHandler->TriggerSave();
+ return;
+ }
+
+ CLog::Log(LOGFATAL, "{}: unknown setting ({}) requested", __FUNCTION__, setting);
+ assert(false);
+}
+
+int CSkinInfo::TranslateBool(const std::string &setting)
+{
+ // run through and see if we have this setting
+ for (const auto& it : m_bools)
+ {
+ if (StringUtils::EqualsNoCase(setting, it.second->name))
+ return it.first;
+ }
+
+ // didn't find it - insert it
+ CSkinSettingBoolPtr skinBool(new CSkinSettingBool());
+ skinBool->name = setting;
+
+ int number = m_bools.size() + m_strings.size();
+ m_bools.insert(std::pair<int, CSkinSettingBoolPtr>(number, skinBool));
+ m_settingsUpdateHandler->TriggerSave();
+
+ return number;
+}
+
+bool CSkinInfo::GetBool(int setting) const
+{
+ const auto& it = m_bools.find(setting);
+ if (it != m_bools.end())
+ return it->second->value;
+
+ // default is to return false
+ return false;
+}
+
+void CSkinInfo::SetBool(int setting, bool set)
+{
+ auto&& it = m_bools.find(setting);
+ if (it != m_bools.end())
+ {
+ it->second->value = set;
+ m_settingsUpdateHandler->TriggerSave();
+ return;
+ }
+
+ CLog::Log(LOGFATAL, "{}: unknown setting ({}) requested", __FUNCTION__, setting);
+ assert(false);
+}
+
+std::set<CSkinSettingPtr> CSkinInfo::GetSkinSettings() const
+{
+ std::set<CSkinSettingPtr> settings;
+
+ for (const auto& setting : m_settings)
+ settings.insert(setting.second);
+
+ return settings;
+}
+
+CSkinSettingPtr CSkinInfo::GetSkinSetting(const std::string& settingId)
+{
+ const auto& it = m_settings.find(settingId);
+ if (it != m_settings.end())
+ return it->second;
+
+ return nullptr;
+}
+
+std::shared_ptr<const CSkinSetting> CSkinInfo::GetSkinSetting(const std::string& settingId) const
+{
+ const auto& it = m_settings.find(settingId);
+ if (it != m_settings.end())
+ return it->second;
+
+ return nullptr;
+}
+
+void CSkinInfo::Reset(const std::string &setting)
+{
+ // run through and see if we have this setting as a string
+ for (auto& it : m_strings)
+ {
+ if (StringUtils::EqualsNoCase(setting, it.second->name))
+ {
+ it.second->value.clear();
+ m_settingsUpdateHandler->TriggerSave();
+ return;
+ }
+ }
+
+ // and now check for the skin bool
+ for (auto& it : m_bools)
+ {
+ if (StringUtils::EqualsNoCase(setting, it.second->name))
+ {
+ it.second->value = false;
+ m_settingsUpdateHandler->TriggerSave();
+ return;
+ }
+ }
+}
+
+void CSkinInfo::Reset()
+{
+ // clear all the settings and strings from this skin.
+ for (auto& it : m_bools)
+ it.second->value = false;
+
+ for (auto& it : m_strings)
+ it.second->value.clear();
+
+ m_settingsUpdateHandler->TriggerSave();
+}
+
+std::set<CSkinSettingPtr> CSkinInfo::ParseSettings(const TiXmlElement* rootElement)
+{
+ std::set<CSkinSettingPtr> settings;
+ if (rootElement == nullptr)
+ return settings;
+
+ const TiXmlElement *settingElement = rootElement->FirstChildElement(XML_SETTING);
+ while (settingElement != nullptr)
+ {
+ CSkinSettingPtr setting = ParseSetting(settingElement);
+ if (setting != nullptr)
+ settings.insert(setting);
+
+ settingElement = settingElement->NextSiblingElement(XML_SETTING);
+ }
+
+ return settings;
+}
+
+CSkinSettingPtr CSkinInfo::ParseSetting(const TiXmlElement* element)
+{
+ if (element == nullptr)
+ return CSkinSettingPtr();
+
+ std::string settingType = XMLUtils::GetAttribute(element, XML_ATTR_TYPE);
+ CSkinSettingPtr setting;
+ if (settingType == "string")
+ setting = CSkinSettingPtr(new CSkinSettingString());
+ else if (settingType == "bool")
+ setting = CSkinSettingPtr(new CSkinSettingBool());
+ else
+ return CSkinSettingPtr();
+
+ if (setting == nullptr)
+ return CSkinSettingPtr();
+
+ if (!setting->Deserialize(element))
+ return CSkinSettingPtr();
+
+ return setting;
+}
+
+bool CSkinInfo::SettingsLoaded(AddonInstanceId id /* = ADDON_SETTINGS_ID */) const
+{
+ if (id != ADDON_SETTINGS_ID)
+ return false;
+
+ return !m_strings.empty() || !m_bools.empty();
+}
+
+bool CSkinInfo::SettingsFromXML(const CXBMCTinyXML& doc,
+ bool loadDefaults,
+ AddonInstanceId id /* = ADDON_SETTINGS_ID */)
+{
+ const TiXmlElement *rootElement = doc.RootElement();
+ if (rootElement == nullptr || rootElement->ValueStr().compare(XML_SETTINGS) != 0)
+ {
+ CLog::Log(LOGWARNING, "CSkinInfo: no <settings> tag found");
+ return false;
+ }
+
+ m_settings.clear();
+ m_strings.clear();
+ m_bools.clear();
+
+ int number = 0;
+ std::set<CSkinSettingPtr> settings = ParseSettings(rootElement);
+ for (const auto& setting : settings)
+ {
+ if (setting->GetType() == "string")
+ {
+ m_settings.insert(std::make_pair(setting->name, setting));
+ m_strings.insert(
+ std::make_pair(number++, std::dynamic_pointer_cast<CSkinSettingString>(setting)));
+ }
+ else if (setting->GetType() == "bool")
+ {
+ m_settings.insert(std::make_pair(setting->name, setting));
+ m_bools.insert(
+ std::make_pair(number++, std::dynamic_pointer_cast<CSkinSettingBool>(setting)));
+ }
+ else
+ CLog::Log(LOGWARNING, "CSkinInfo: ignoring setting of unknown type \"{}\"",
+ setting->GetType());
+ }
+
+ return true;
+}
+
+bool CSkinInfo::SettingsToXML(CXBMCTinyXML& doc, AddonInstanceId id /* = ADDON_SETTINGS_ID */) const
+{
+ // add the <skinsettings> tag
+ TiXmlElement rootElement(XML_SETTINGS);
+ TiXmlNode *settingsNode = doc.InsertEndChild(rootElement);
+ if (settingsNode == nullptr)
+ {
+ CLog::Log(LOGWARNING, "CSkinInfo: could not create <settings> tag");
+ return false;
+ }
+
+ TiXmlElement* settingsElement = settingsNode->ToElement();
+ for (const auto& it : m_bools)
+ {
+ if (!it.second->Serialize(settingsElement))
+ CLog::Log(LOGWARNING, "CSkinInfo: failed to save string setting \"{}\"", it.second->name);
+ }
+
+ for (const auto& it : m_strings)
+ {
+ if (!it.second->Serialize(settingsElement))
+ CLog::Log(LOGWARNING, "CSkinInfo: failed to save bool setting \"{}\"", it.second->name);
+ }
+
+ return true;
+}
+
+void CSkinSettingUpdateHandler::OnTimeout()
+{
+ m_addon.SaveSettings();
+}
+
+void CSkinSettingUpdateHandler::TriggerSave()
+{
+ if (m_timer.IsRunning())
+ m_timer.Restart();
+ else
+ m_timer.Start(DELAY);
+}
+
+} /*namespace ADDON*/