/* * Settings.cpp * ------------ * Purpose: 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. */ #include "stdafx.h" #include "Settings.h" #include "mpt/binary/hex.hpp" #include "../common/misc_util.h" #include "../common/mptStringBuffer.h" #include "Mptrack.h" #include "Mainfrm.h" #include #include "../common/mptFileIO.h" OPENMPT_NAMESPACE_BEGIN mpt::ustring SettingValue::FormatTypeAsString() const { if(GetType() == SettingTypeNone) { return U_("nil"); } mpt::ustring result; switch(GetType()) { case SettingTypeBool: result += U_("bool"); break; case SettingTypeInt: result += U_("int"); break; case SettingTypeFloat: result += U_("float"); break; case SettingTypeString: result += U_("string"); break; case SettingTypeBinary: result += U_("binary"); break; case SettingTypeNone: default: result += U_("nil"); break; } if(HasTypeTag() && !GetTypeTag().empty()) { result += U_(":") + mpt::ToUnicode(mpt::Charset::ASCII, GetTypeTag()); } return result; } mpt::ustring SettingValue::FormatValueAsString() const { switch(GetType()) { case SettingTypeBool: return mpt::ufmt::val(as()); break; case SettingTypeInt: return mpt::ufmt::val(as()); break; case SettingTypeFloat: return mpt::ufmt::val(as()); break; case SettingTypeString: return as(); break; case SettingTypeBinary: return mpt::encode_hex(mpt::as_span(as>())); break; case SettingTypeNone: default: return mpt::ustring(); break; } } void SettingValue::SetFromString(const AnyStringLocale &newVal) { switch(GetType()) { case SettingTypeBool: value = ConvertStrTo(newVal); break; case SettingTypeInt: value = ConvertStrTo(newVal); break; case SettingTypeFloat: value = ConvertStrTo(newVal); break; case SettingTypeString: value = newVal; break; case SettingTypeBinary: value = mpt::decode_hex(newVal); break; case SettingTypeNone: default: break; } } SettingValue SettingsContainer::BackendsReadSetting(const SettingPath &path, const SettingValue &def) const { return backend->ReadSetting(path, def); } void SettingsContainer::BackendsWriteSetting(const SettingPath &path, const SettingValue &val) { backend->WriteSetting(path, val); } void SettingsContainer::BackendsRemoveSetting(const SettingPath &path) { backend->RemoveSetting(path); } void SettingsContainer::BackendsRemoveSection(const mpt::ustring §ion) { backend->RemoveSection(section); } SettingValue SettingsContainer::ReadSetting(const SettingPath &path, const SettingValue &def) const { ASSERT(theApp.InGuiThread()); ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. auto entry = map.find(path); if(entry == map.end()) { entry = map.insert(map.begin(), std::make_pair(path, SettingState(def).assign(BackendsReadSetting(path, def), false))); } return entry->second; } bool SettingsContainer::IsDefaultSetting(const SettingPath &path) const { ASSERT(theApp.InGuiThread()); ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. auto entry = map.find(path); if(entry == map.end()) { return true; } return entry->second.IsDefault(); } void SettingsContainer::WriteSetting(const SettingPath &path, const SettingValue &val, SettingFlushMode flushMode) { ASSERT(theApp.InGuiThread()); ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. auto entry = map.find(path); if(entry == map.end()) { map[path] = val; entry = map.find(path); } else { entry->second = val; } NotifyListeners(path); if(immediateFlush || flushMode == SettingWriteThrough) { BackendsWriteSetting(path, val); entry->second.Clean(); } } void SettingsContainer::ForgetSetting(const SettingPath &path) { ASSERT(theApp.InGuiThread()); ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. map.erase(path); } void SettingsContainer::ForgetAll() { ASSERT(theApp.InGuiThread()); ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. map.clear(); } void SettingsContainer::RemoveSetting(const SettingPath &path) { ASSERT(theApp.InGuiThread()); ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. map.erase(path); BackendsRemoveSetting(path); } void SettingsContainer::RemoveSection(const mpt::ustring §ion) { ASSERT(theApp.InGuiThread()); ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. std::vector pathsToRemove; for(const auto &entry : map) { if(entry.first.GetSection() == section) { pathsToRemove.push_back(entry.first); } } for(const auto &path : pathsToRemove) { map.erase(path); } BackendsRemoveSection(section); } void SettingsContainer::NotifyListeners(const SettingPath &path) { const auto entry = mapListeners.find(path); if(entry != mapListeners.end()) { for(auto &it : entry->second) { it->SettingChanged(path); } } } void SettingsContainer::WriteSettings() { ASSERT(theApp.InGuiThread()); ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. for(auto &[path, value] : map) { if(value.IsDirty()) { BackendsWriteSetting(path, value); value.Clean(); } } } void SettingsContainer::Flush() { ASSERT(theApp.InGuiThread()); ASSERT(!CMainFrame::GetMainFrame() || (CMainFrame::GetMainFrame() && !CMainFrame::GetMainFrame()->InNotifyHandler())); // This is a slow path, use CachedSetting for stuff that is accessed in notify handler. WriteSettings(); } void SettingsContainer::SetImmediateFlush(bool newImmediateFlush) { if(newImmediateFlush) { Flush(); } immediateFlush = newImmediateFlush; } void SettingsContainer::Register(ISettingChanged *listener, const SettingPath &path) { mapListeners[path].insert(listener); } void SettingsContainer::UnRegister(ISettingChanged *listener, const SettingPath &path) { mapListeners[path].erase(listener); } SettingsContainer::~SettingsContainer() { WriteSettings(); } SettingsContainer::SettingsContainer(ISettingsBackend *backend) : backend(backend) { MPT_ASSERT(backend); } std::vector IniFileSettingsBackend::ReadSettingRaw(const SettingPath &path, const std::vector &def) const { std::vector result = def; if(!mpt::in_range(result.size())) { return result; } ::GetPrivateProfileStruct(GetSection(path).c_str(), GetKey(path).c_str(), result.data(), static_cast(result.size()), filename.AsNative().c_str()); return result; } mpt::ustring IniFileSettingsBackend::ReadSettingRaw(const SettingPath &path, const mpt::ustring &def) const { std::vector buf(128); while(::GetPrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::ToWin(def).c_str(), buf.data(), static_cast(buf.size()), filename.AsNative().c_str()) == buf.size() - 1) { if(buf.size() == std::numeric_limits::max()) { return def; } buf.resize(mpt::exponential_grow(buf.size(), std::numeric_limits::max())); } return mpt::ToUnicode(mpt::winstring(buf.data())); } double IniFileSettingsBackend::ReadSettingRaw(const SettingPath &path, double def) const { std::vector buf(128); while(::GetPrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::tfmt::val(def).c_str(), buf.data(), static_cast(buf.size()), filename.AsNative().c_str()) == buf.size() - 1) { if(buf.size() == std::numeric_limits::max()) { return def; } buf.resize(mpt::exponential_grow(buf.size(), std::numeric_limits::max())); } return ConvertStrTo(mpt::winstring(buf.data())); } int32 IniFileSettingsBackend::ReadSettingRaw(const SettingPath &path, int32 def) const { return (int32)::GetPrivateProfileInt(GetSection(path).c_str(), GetKey(path).c_str(), (UINT)def, filename.AsNative().c_str()); } bool IniFileSettingsBackend::ReadSettingRaw(const SettingPath &path, bool def) const { return ::GetPrivateProfileInt(GetSection(path).c_str(), GetKey(path).c_str(), def?1:0, filename.AsNative().c_str()) ? true : false; } void IniFileSettingsBackend::WriteSettingRaw(const SettingPath &path, const std::vector &val) { MPT_ASSERT(mpt::in_range(val.size())); ::WritePrivateProfileStruct(GetSection(path).c_str(), GetKey(path).c_str(), (LPVOID)val.data(), static_cast(val.size()), filename.AsNative().c_str()); } void IniFileSettingsBackend::WriteSettingRaw(const SettingPath &path, const mpt::ustring &val) { ::WritePrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::ToWin(val).c_str(), filename.AsNative().c_str()); if(mpt::ToUnicode(mpt::Charset::Locale, mpt::ToCharset(mpt::Charset::Locale, val)) != val) // explicit round-trip { // Value is not representable in ANSI CP. // Now check if the string got stored correctly. if(ReadSettingRaw(path, mpt::ustring()) != val) { // The ini file is probably ANSI encoded. ConvertToUnicode(); // Re-write non-ansi-representable value. ::WritePrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::ToWin(val).c_str(), filename.AsNative().c_str()); } } } void IniFileSettingsBackend::WriteSettingRaw(const SettingPath &path, double val) { ::WritePrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::tfmt::val(val).c_str(), filename.AsNative().c_str()); } void IniFileSettingsBackend::WriteSettingRaw(const SettingPath &path, int32 val) { ::WritePrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::tfmt::val(val).c_str(), filename.AsNative().c_str()); } void IniFileSettingsBackend::WriteSettingRaw(const SettingPath &path, bool val) { ::WritePrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::tfmt::val(val).c_str(), filename.AsNative().c_str()); } void IniFileSettingsBackend::RemoveSettingRaw(const SettingPath &path) { ::WritePrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), NULL, filename.AsNative().c_str()); } void IniFileSettingsBackend::RemoveSectionRaw(const mpt::ustring §ion) { ::WritePrivateProfileSection(mpt::ToWin(section).c_str(), _T("\0"), filename.AsNative().c_str()); } mpt::winstring IniFileSettingsBackend::GetSection(const SettingPath &path) { return mpt::ToWin(path.GetSection()); } mpt::winstring IniFileSettingsBackend::GetKey(const SettingPath &path) { return mpt::ToWin(path.GetKey()); } IniFileSettingsBackend::IniFileSettingsBackend(const mpt::PathString &filename) : filename(filename) { return; } IniFileSettingsBackend::~IniFileSettingsBackend() { return; } static std::vector ReadFile(const mpt::PathString &filename) { mpt::ifstream s(filename, std::ios::binary); std::vector result; while(s) { char buf[4096]; s.read(buf, 4096); std::streamsize count = s.gcount(); result.insert(result.end(), buf, buf + count); } return result; } static void WriteFileUTF16LE(const mpt::PathString &filename, const std::wstring &str) { static_assert(sizeof(wchar_t) == 2); mpt::SafeOutputFile sinifile(filename, std::ios::binary, mpt::FlushMode::Full); mpt::ofstream& inifile = sinifile; const uint8 UTF16LE_BOM[] = { 0xff, 0xfe }; inifile.write(reinterpret_cast(UTF16LE_BOM), 2); inifile.write(reinterpret_cast(str.c_str()), str.length() * sizeof(std::wstring::value_type)); } void IniFileSettingsBackend::ConvertToUnicode(const mpt::ustring &backupTag) { // Force ini file to be encoded in UTF16. // This causes WINAPI ini file functions to keep it in UTF16 encoding // and thus support storing unicode strings uncorrupted. // This is backwards compatible because even ANSI WINAPI behaves the // same way in this case. const std::vector data = ReadFile(filename); if(!data.empty() && IsTextUnicode(data.data(), mpt::saturate_cast(data.size()), NULL)) { return; } const mpt::PathString backupFilename = filename + mpt::PathString::FromUnicode(backupTag.empty() ? U_(".ansi.bak") : U_(".ansi.") + backupTag + U_(".bak")); CopyFile(filename.AsNative().c_str(), backupFilename.AsNative().c_str(), FALSE); WriteFileUTF16LE(filename, mpt::ToWide(mpt::Charset::Locale, mpt::buffer_cast(data))); } SettingValue IniFileSettingsBackend::ReadSetting(const SettingPath &path, const SettingValue &def) const { switch(def.GetType()) { case SettingTypeBool: return SettingValue(ReadSettingRaw(path, def.as()), def.GetTypeTag()); break; case SettingTypeInt: return SettingValue(ReadSettingRaw(path, def.as()), def.GetTypeTag()); break; case SettingTypeFloat: return SettingValue(ReadSettingRaw(path, def.as()), def.GetTypeTag()); break; case SettingTypeString: return SettingValue(ReadSettingRaw(path, def.as()), def.GetTypeTag()); break; case SettingTypeBinary: return SettingValue(ReadSettingRaw(path, def.as >()), def.GetTypeTag()); break; default: return SettingValue(); break; } } void IniFileSettingsBackend::WriteSetting(const SettingPath &path, const SettingValue &val) { ASSERT(val.GetType() != SettingTypeNone); switch(val.GetType()) { case SettingTypeBool: WriteSettingRaw(path, val.as()); break; case SettingTypeInt: WriteSettingRaw(path, val.as()); break; case SettingTypeFloat: WriteSettingRaw(path, val.as()); break; case SettingTypeString: WriteSettingRaw(path, val.as()); break; case SettingTypeBinary: WriteSettingRaw(path, val.as >()); break; default: break; } } void IniFileSettingsBackend::RemoveSetting(const SettingPath &path) { RemoveSettingRaw(path); } void IniFileSettingsBackend::RemoveSection(const mpt::ustring §ion) { RemoveSectionRaw(section); } IniFileSettingsContainer::IniFileSettingsContainer(const mpt::PathString &filename) : IniFileSettingsBackend(filename) , SettingsContainer(this) { return; } IniFileSettingsContainer::~IniFileSettingsContainer() { return; } DefaultSettingsContainer::DefaultSettingsContainer() : IniFileSettingsContainer(theApp.GetConfigFileName()) { return; } DefaultSettingsContainer::~DefaultSettingsContainer() { return; } OPENMPT_NAMESPACE_END