/* * Settings.h * ---------- * Purpose: Header file for application setting handling framework. * Notes : (currently none) * Authors: Joern Heusipp * OpenMPT Devs * The OpenMPT source code is released under the BSD license. Read LICENSE for more details. */ #pragma once #include "openmpt/all/BuildSettings.hpp" #include "../common/misc_util.h" #include "mpt/mutex/mutex.hpp" #include <map> #include <set> #include <variant> OPENMPT_NAMESPACE_BEGIN enum SettingType { SettingTypeNone, SettingTypeBool, SettingTypeInt, SettingTypeFloat, SettingTypeString, SettingTypeBinary, }; // SettingValue is a variant type that stores any type that can natively be represented in a config backend. // Any other type that should be stored must provide a matching ToSettingValue and FromSettingValue. // Other types can optionally also set a type tag which would get checked in debug builds. class SettingValue { private: std::variant<std::monostate, bool, int32, double, mpt::ustring, std::vector<std::byte>> value; std::string typeTag; public: bool operator == (const SettingValue &other) const { return value == other.value && typeTag == other.typeTag; } bool operator != (const SettingValue &other) const { return !(*this == other); } SettingValue() { } SettingValue(const SettingValue &other) { *this = other; } SettingValue & operator = (const SettingValue &other) { if(this == &other) { return *this; } MPT_ASSERT(value.index() == 0 || (value.index() == other.value.index() && typeTag == other.typeTag)); value = other.value; typeTag = other.typeTag; return *this; } SettingValue(bool val) : value(val) { } SettingValue(int32 val) : value(val) { } SettingValue(double val) : value(val) { } SettingValue(const mpt::ustring &val) : value(val) { } SettingValue(const std::vector<std::byte> &val) : value(val) { } SettingValue(bool val, const std::string &typeTag_) : value(val) , typeTag(typeTag_) { } SettingValue(int32 val, const std::string &typeTag_) : value(val) , typeTag(typeTag_) { } SettingValue(double val, const std::string &typeTag_) : value(val) , typeTag(typeTag_) { } SettingValue(const mpt::ustring &val, const std::string &typeTag_) : value(val) , typeTag(typeTag_) { } SettingValue(const std::vector<std::byte> &val, const std::string &typeTag_) : value(val) , typeTag(typeTag_) { } // these need to be explicitly deleted because otherwise the bool overload will catch the pointers SettingValue(const char *val) = delete; SettingValue(const wchar_t *val) = delete; SettingValue(const char *val, const std::string &typeTag_) = delete; SettingValue(const wchar_t *val, const std::string &typeTag_) = delete; SettingType GetType() const { SettingType result = SettingTypeNone; if(std::holds_alternative<bool>(value)) { result = SettingTypeBool; } if(std::holds_alternative<int32>(value)) { result = SettingTypeInt; } if(std::holds_alternative<double>(value)) { result = SettingTypeFloat; } if(std::holds_alternative<mpt::ustring>(value)) { result = SettingTypeString; } if(std::holds_alternative<std::vector<std::byte>>(value)) { result = SettingTypeBinary; } return result; } bool HasTypeTag() const { return !typeTag.empty(); } std::string GetTypeTag() const { return typeTag; } template <typename T> T as() const { return *this; } operator bool () const { MPT_ASSERT(std::holds_alternative<bool>(value)); return std::get<bool>(value); } operator int32 () const { MPT_ASSERT(std::holds_alternative<int32>(value)); return std::get<int32>(value); } operator double () const { MPT_ASSERT(std::holds_alternative<double>(value)); return std::get<double>(value); } operator mpt::ustring () const { MPT_ASSERT(std::holds_alternative<mpt::ustring>(value)); return std::get<mpt::ustring>(value); } operator std::vector<std::byte> () const { MPT_ASSERT(std::holds_alternative<std::vector<std::byte>>(value)); return std::get<std::vector<std::byte>>(value); } mpt::ustring FormatTypeAsString() const; mpt::ustring FormatValueAsString() const; void SetFromString(const AnyStringLocale &newVal); }; template<typename T> std::vector<std::byte> EncodeBinarySetting(const T &val) { std::vector<std::byte> result(sizeof(T)); std::memcpy(result.data(), &val, sizeof(T)); return result; } template<typename T> T DecodeBinarySetting(const std::vector<std::byte> &val) { T result = T(); if(val.size() >= sizeof(T)) { std::memcpy(&result, val.data(), sizeof(T)); } return result; } template<typename T> inline SettingValue ToSettingValue(const T &val) { return SettingValue(val); } template<typename T> inline T FromSettingValue(const SettingValue &val) { return val.as<T>(); } // To support settings.Read<Tcustom> and settings.Write<Tcustom>, // just provide specializations of ToSettingsValue<Tcustom> and FromSettingValue<Tcustom>. // You may use the SettingValue(value, typeTag) constructor in ToSettingValue // and check the typeTag FromSettingsValue to implement runtime type-checking for custom types. template<> inline SettingValue ToSettingValue(const std::string &val) { return SettingValue(mpt::ToUnicode(mpt::Charset::Locale, val)); } template<> inline std::string FromSettingValue(const SettingValue &val) { return mpt::ToCharset(mpt::Charset::Locale, val.as<mpt::ustring>()); } template<> inline SettingValue ToSettingValue(const mpt::lstring &val) { return SettingValue(mpt::ToUnicode(val)); } template<> inline mpt::lstring FromSettingValue(const SettingValue &val) { return mpt::ToLocale(val.as<mpt::ustring>()); } #if !MPT_USTRING_MODE_WIDE template<> inline SettingValue ToSettingValue(const std::wstring &val) { return SettingValue(mpt::ToUnicode(val)); } template<> inline std::wstring FromSettingValue(const SettingValue &val) { return mpt::ToWide(val.as<mpt::ustring>()); } #endif template<> inline SettingValue ToSettingValue(const CString &val) { return SettingValue(mpt::ToUnicode(val)); } template<> inline CString FromSettingValue(const SettingValue &val) { return mpt::ToCString(val.as<mpt::ustring>()); } template<> inline SettingValue ToSettingValue(const mpt::PathString &val) { return SettingValue(val.ToUnicode()); } template<> inline mpt::PathString FromSettingValue(const SettingValue &val) { return mpt::PathString::FromUnicode(val); } template<> inline SettingValue ToSettingValue(const float &val) { return SettingValue(double(val)); } template<> inline float FromSettingValue(const SettingValue &val) { return float(val.as<double>()); } template<> inline SettingValue ToSettingValue(const int64 &val) { return SettingValue(mpt::ufmt::dec(val), "int64"); } template<> inline int64 FromSettingValue(const SettingValue &val) { return ConvertStrTo<int64>(val.as<mpt::ustring>()); } template<> inline SettingValue ToSettingValue(const uint64 &val) { return SettingValue(mpt::ufmt::dec(val), "uint64"); } template<> inline uint64 FromSettingValue(const SettingValue &val) { return ConvertStrTo<uint64>(val.as<mpt::ustring>()); } template<> inline SettingValue ToSettingValue(const uint32 &val) { return SettingValue(int32(val)); } template<> inline uint32 FromSettingValue(const SettingValue &val) { return uint32(val.as<int32>()); } template<> inline SettingValue ToSettingValue(const uint16 &val) { return SettingValue(int32(val)); } template<> inline uint16 FromSettingValue(const SettingValue &val) { return uint16(val.as<int32>()); } template<> inline SettingValue ToSettingValue(const uint8 &val) { return SettingValue(int32(val)); } template<> inline uint8 FromSettingValue(const SettingValue &val) { return uint8(val.as<int32>()); } template<> inline SettingValue ToSettingValue(const LONG &val) { return SettingValue(int32(val)); } template<> inline LONG FromSettingValue(const SettingValue &val) { return LONG(val.as<int32>()); } // An instance of SetttingState represents the cached on-disk state of a certain SettingPath. // The mapping is stored externally in SettingsContainer::map. class SettingState { private: SettingValue value; const SettingValue defaultValue; bool dirty; public: SettingState() : dirty(false) { return; } SettingState(const SettingValue &def) : value(def) , defaultValue(def) , dirty(false) { return; } SettingState & assign(const SettingValue &other, bool setDirty = true) { MPT_ASSERT(defaultValue.GetType() == SettingTypeNone || (defaultValue.GetType() == other.GetType() && defaultValue.GetTypeTag() == other.GetTypeTag())); if(setDirty) { if(value != other) { value = other; dirty = true; } } else { value = other; } return *this; } SettingState & operator = (const SettingValue &val) { assign(val); return *this; } SettingValue GetDefault() const { return defaultValue; } const SettingValue &GetRefDefault() const { return defaultValue; } bool IsDefault() const { return value == defaultValue; } bool IsDirty() const { return dirty; } void Clean() { dirty = false; } SettingValue GetValue() const { return value; } const SettingValue &GetRefValue() const { return value; } operator SettingValue () const { return value; } }; // SettingPath represents the path in a config backend to a certain setting. class SettingPath { private: mpt::ustring section; mpt::ustring key; public: SettingPath() { return; } SettingPath(mpt::ustring section_, mpt::ustring key_) : section(std::move(section_)) , key(std::move(key_)) { return; } mpt::ustring GetSection() const { return section; } mpt::ustring GetKey() const { return key; } const mpt::ustring &GetRefSection() const { return section; } const mpt::ustring &GetRefKey() const { return key; } int compare(const SettingPath &other) const { int cmp_section = section.compare(other.section); if(cmp_section) { return cmp_section; } int cmp_key = key.compare(other.key); return cmp_key; } mpt::ustring FormatAsString() const { return section + U_(".") + key; } }; inline bool operator < (const SettingPath &left, const SettingPath &right) { return left.compare(right) < 0; } inline bool operator <= (const SettingPath &left, const SettingPath &right) { return left.compare(right) <= 0; } inline bool operator > (const SettingPath &left, const SettingPath &right) { return left.compare(right) > 0; } inline bool operator >= (const SettingPath &left, const SettingPath &right) { return left.compare(right) >= 0; } inline bool operator == (const SettingPath &left, const SettingPath &right) { return left.compare(right) == 0; } inline bool operator != (const SettingPath &left, const SettingPath &right) { return left.compare(right) != 0; } class ISettingsBackend { public: virtual SettingValue ReadSetting(const SettingPath &path, const SettingValue &def) const = 0; virtual void WriteSetting(const SettingPath &path, const SettingValue &val) = 0; virtual void RemoveSetting(const SettingPath &path) = 0; virtual void RemoveSection(const mpt::ustring §ion) = 0; protected: virtual ~ISettingsBackend() = default; }; class ISettingChanged { public: virtual void SettingChanged(const SettingPath &changedPath) = 0; protected: virtual ~ISettingChanged() = default; }; enum SettingFlushMode { SettingWriteBack = 0, SettingWriteThrough = 1, }; // SettingContainer basically represents a frontend to 1 or 2 backends (e.g. ini files or registry subtrees) for a collection of configuration settings. // SettingContainer provides basic read/write access to individual setting. The values are cached and only flushed on destruction or explicit flushs. class SettingsContainer { public: using SettingsMap = std::map<SettingPath,SettingState>; using SettingsListenerMap = std::map<SettingPath,std::set<ISettingChanged*>>; void WriteSettings(); private: mutable SettingsMap map; mutable SettingsListenerMap mapListeners; private: ISettingsBackend *backend; private: bool immediateFlush = false; SettingValue BackendsReadSetting(const SettingPath &path, const SettingValue &def) const; void BackendsWriteSetting(const SettingPath &path, const SettingValue &val); void BackendsRemoveSetting(const SettingPath &path); void BackendsRemoveSection(const mpt::ustring §ion); void NotifyListeners(const SettingPath &path); SettingValue ReadSetting(const SettingPath &path, const SettingValue &def) const; bool IsDefaultSetting(const SettingPath &path) const; void WriteSetting(const SettingPath &path, const SettingValue &val, SettingFlushMode flushMode); void ForgetSetting(const SettingPath &path); void RemoveSetting(const SettingPath &path); void RemoveSection(const mpt::ustring §ion); private: SettingsContainer(const SettingsContainer &other); // disable SettingsContainer& operator = (const SettingsContainer &other); // disable public: SettingsContainer(ISettingsBackend *backend); void SetImmediateFlush(bool newImmediateFlush); template <typename T> T Read(const SettingPath &path, const T &def = T()) const { return FromSettingValue<T>(ReadSetting(path, ToSettingValue<T>(def))); } template <typename T> T Read(mpt::ustring section, mpt::ustring key, const T &def = T()) const { return FromSettingValue<T>(ReadSetting(SettingPath(std::move(section), std::move(key)), ToSettingValue<T>(def))); } bool IsDefault(const SettingPath &path) const { return IsDefaultSetting(path); } bool IsDefault(mpt::ustring section, mpt::ustring key) const { return IsDefaultSetting(SettingPath(std::move(section), std::move(key))); } template <typename T> void Write(const SettingPath &path, const T &val, SettingFlushMode flushMode = SettingWriteBack) { WriteSetting(path, ToSettingValue<T>(val), flushMode); } template <typename T> void Write(mpt::ustring section, mpt::ustring key, const T &val, SettingFlushMode flushMode = SettingWriteBack) { WriteSetting(SettingPath(std::move(section), std::move(key)), ToSettingValue<T>(val), flushMode); } void Forget(const SettingPath &path) { ForgetSetting(path); } void Forget(mpt::ustring section, mpt::ustring key) { ForgetSetting(SettingPath(std::move(section), std::move(key))); } void ForgetAll(); void Remove(const SettingPath &path) { RemoveSetting(path); } void Remove(mpt::ustring section, mpt::ustring key) { RemoveSetting(SettingPath(std::move(section), std::move(key))); } void Remove(const mpt::ustring §ion) { RemoveSection(section); } void Flush(); ~SettingsContainer(); public: void Register(ISettingChanged *listener, const SettingPath &path); void UnRegister(ISettingChanged *listener, const SettingPath &path); SettingsMap::const_iterator begin() const { return map.begin(); } SettingsMap::const_iterator end() const { return map.end(); } SettingsMap::size_type size() const { return map.size(); } bool empty() const { return map.empty(); } const SettingsMap &GetMap() const { return map; } }; // Setting<T> and CachedSetting<T> are references to a SettingPath below a SettingConainer (both provided to the constructor). // They should mostly behave like normal non-reference variables of type T. I.e., they can be assigned to and read from. // As they have actual reference semantics, all Setting<T> or CachedSetting<T> that access the same path consistently have the same value. // The difference between the 2 lies in the way this consistency is achieved: // Setting<T>: The actual value is not stored in an instance of Setting<T>. // Instead, it is read/written and converted on every access from the SettingContainer. // In the SettingContainer, each SettingPath is mapped to a single instance of SettingValue, so there cannot be any incoherence. // CachedSetting<T>: The value, readily converted to T, is stored directly in each instance of CachedSetting<T>. // A callback for its SettingPath is registered with SettingContainer, and on every change to this SettingPath, the value gets re-read and updated. // Setting<T> implies some overhead on every access but is generally simpler to understand. // CachedSetting<T> implies overhead in stored (the copy of T and the callback pointers). // Except for the difference in runtime/space characteristics, Setting<T> and CachedSetting<T> behave exactly the same way. // It is recommended to only use CachedSetting<T> for settings that get read frequently, i.e. during GUI updates (like in the pattern view). template <typename T> class Setting { private: SettingsContainer &conf; const SettingPath path; public: Setting(const Setting &other) = delete; Setting & operator = (const Setting &other) = delete; public: Setting(SettingsContainer &conf_, mpt::ustring section, mpt::ustring key, const T&def) : conf(conf_) , path(std::move(section), std::move(key)) { conf.Read(path, def); // set default value } Setting(SettingsContainer &conf_, const SettingPath &path_, const T&def) : conf(conf_) , path(path_) { conf.Read(path, def); // set default value } SettingPath GetPath() const { return path; } Setting & operator = (const T &val) { conf.Write(path, val); return *this; } operator T () const { return conf.Read<T>(path); } T Get() const { return conf.Read<T>(path); } bool IsDefault() const { return conf.IsDefault(path); } template<typename Trhs> Setting & operator += (const Trhs &rhs) { T tmp = *this; tmp += rhs; *this = tmp; return *this; } template<typename Trhs> Setting & operator -= (const Trhs &rhs) { T tmp = *this; tmp -= rhs; *this = tmp; return *this; } template<typename Trhs> Setting & operator *= (const Trhs &rhs) { T tmp = *this; tmp *= rhs; *this = tmp; return *this; } template<typename Trhs> Setting & operator /= (const Trhs &rhs) { T tmp = *this; tmp /= rhs; *this = tmp; return *this; } template<typename Trhs> Setting & operator %= (const Trhs &rhs) { T tmp = *this; tmp %= rhs; *this = tmp; return *this; } template<typename Trhs> Setting & operator |= (const Trhs &rhs) { T tmp = *this; tmp |= rhs; *this = tmp; return *this; } template<typename Trhs> Setting & operator &= (const Trhs &rhs) { T tmp = *this; tmp &= rhs; *this = tmp; return *this; } template<typename Trhs> Setting & operator ^= (const Trhs &rhs) { T tmp = *this; tmp ^= rhs; *this = tmp; return *this; } }; template <typename T> class CachedSetting : public ISettingChanged { private: mutable mpt::mutex valueMutex; T value; SettingsContainer &conf; const SettingPath path; public: CachedSetting(const CachedSetting &other) = delete; CachedSetting & operator = (const CachedSetting &other) = delete; public: CachedSetting(SettingsContainer &conf_, mpt::ustring section, mpt::ustring key, const T&def) : value(def) , conf(conf_) , path(std::move(section), std::move(key)) { { mpt::lock_guard<mpt::mutex> l(valueMutex); value = conf.Read(path, def); } conf.Register(this, path); } CachedSetting(SettingsContainer &conf_, const SettingPath &path_, const T&def) : value(def) , conf(conf_) , path(path_) { { mpt::lock_guard<mpt::mutex> l(valueMutex); value = conf.Read(path, def); } conf.Register(this, path); } ~CachedSetting() { conf.UnRegister(this, path); } SettingPath GetPath() const { return path; } CachedSetting & operator = (const T &val) { { mpt::lock_guard<mpt::mutex> l(valueMutex); value = val; } conf.Write(path, val); return *this; } operator T () const { mpt::lock_guard<mpt::mutex> l(valueMutex); return value; } T Get() const { mpt::lock_guard<mpt::mutex> l(valueMutex); return value; } bool IsDefault() const { return conf.IsDefault(path); } CachedSetting & Update() { { mpt::lock_guard<mpt::mutex> l(valueMutex); value = conf.Read<T>(path); } return *this; } void SettingChanged(const SettingPath &changedPath) { MPT_UNREFERENCED_PARAMETER(changedPath); Update(); } template<typename Trhs> CachedSetting & operator += (const Trhs &rhs) { T tmp = *this; tmp += rhs; *this = tmp; return *this; } template<typename Trhs> CachedSetting & operator -= (const Trhs &rhs) { T tmp = *this; tmp -= rhs; *this = tmp; return *this; } template<typename Trhs> CachedSetting & operator *= (const Trhs &rhs) { T tmp = *this; tmp *= rhs; *this = tmp; return *this; } template<typename Trhs> CachedSetting & operator /= (const Trhs &rhs) { T tmp = *this; tmp /= rhs; *this = tmp; return *this; } template<typename Trhs> CachedSetting & operator %= (const Trhs &rhs) { T tmp = *this; tmp %= rhs; *this = tmp; return *this; } template<typename Trhs> CachedSetting & operator |= (const Trhs &rhs) { T tmp = *this; tmp |= rhs; *this = tmp; return *this; } template<typename Trhs> CachedSetting & operator &= (const Trhs &rhs) { T tmp = *this; tmp &= rhs; *this = tmp; return *this; } template<typename Trhs> CachedSetting & operator ^= (const Trhs &rhs) { T tmp = *this; tmp ^= rhs; *this = tmp; return *this; } }; class IniFileSettingsBackend : public ISettingsBackend { private: const mpt::PathString filename; private: std::vector<std::byte> ReadSettingRaw(const SettingPath &path, const std::vector<std::byte> &def) const; mpt::ustring ReadSettingRaw(const SettingPath &path, const mpt::ustring &def) const; double ReadSettingRaw(const SettingPath &path, double def) const; int32 ReadSettingRaw(const SettingPath &path, int32 def) const; bool ReadSettingRaw(const SettingPath &path, bool def) const; void WriteSettingRaw(const SettingPath &path, const std::vector<std::byte> &val); void WriteSettingRaw(const SettingPath &path, const mpt::ustring &val); void WriteSettingRaw(const SettingPath &path, double val); void WriteSettingRaw(const SettingPath &path, int32 val); void WriteSettingRaw(const SettingPath &path, bool val); void RemoveSettingRaw(const SettingPath &path); void RemoveSectionRaw(const mpt::ustring §ion); static mpt::winstring GetSection(const SettingPath &path); static mpt::winstring GetKey(const SettingPath &path); public: IniFileSettingsBackend(const mpt::PathString &filename); ~IniFileSettingsBackend() override; void ConvertToUnicode(const mpt::ustring &backupTag = mpt::ustring()); virtual SettingValue ReadSetting(const SettingPath &path, const SettingValue &def) const override; virtual void WriteSetting(const SettingPath &path, const SettingValue &val) override; virtual void RemoveSetting(const SettingPath &path) override; virtual void RemoveSection(const mpt::ustring §ion) override; const mpt::PathString& GetFilename() const { return filename; } }; class IniFileSettingsContainer : private IniFileSettingsBackend, public SettingsContainer { public: IniFileSettingsContainer(const mpt::PathString &filename); ~IniFileSettingsContainer() override; }; class DefaultSettingsContainer : public IniFileSettingsContainer { public: DefaultSettingsContainer(); ~DefaultSettingsContainer() override; }; class SettingChangedNotifyGuard { private: SettingsContainer &conf; SettingPath m_Path; bool m_Registered; ISettingChanged *m_Handler; public: SettingChangedNotifyGuard(SettingsContainer &conf, const SettingPath &path) : conf(conf) , m_Path(path) , m_Registered(false) , m_Handler(nullptr) { return; } void Register(ISettingChanged *handler) { if(m_Registered) { return; } m_Handler = handler; conf.Register(m_Handler, m_Path); m_Registered = true; } ~SettingChangedNotifyGuard() { if(m_Registered) { conf.UnRegister(m_Handler, m_Path); m_Registered = false; } } }; OPENMPT_NAMESPACE_END