524 lines
15 KiB
C++
524 lines
15 KiB
C++
/*
|
|
* 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 <algorithm>
|
|
#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<bool>());
|
|
break;
|
|
case SettingTypeInt:
|
|
return mpt::ufmt::val(as<int32>());
|
|
break;
|
|
case SettingTypeFloat:
|
|
return mpt::ufmt::val(as<double>());
|
|
break;
|
|
case SettingTypeString:
|
|
return as<mpt::ustring>();
|
|
break;
|
|
case SettingTypeBinary:
|
|
return mpt::encode_hex(mpt::as_span(as<std::vector<std::byte>>()));
|
|
break;
|
|
case SettingTypeNone:
|
|
default:
|
|
return mpt::ustring();
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
void SettingValue::SetFromString(const AnyStringLocale &newVal)
|
|
{
|
|
switch(GetType())
|
|
{
|
|
case SettingTypeBool:
|
|
value = ConvertStrTo<bool>(newVal);
|
|
break;
|
|
case SettingTypeInt:
|
|
value = ConvertStrTo<int32>(newVal);
|
|
break;
|
|
case SettingTypeFloat:
|
|
value = ConvertStrTo<double>(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<SettingPath> 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<std::byte> IniFileSettingsBackend::ReadSettingRaw(const SettingPath &path, const std::vector<std::byte> &def) const
|
|
{
|
|
std::vector<std::byte> result = def;
|
|
if(!mpt::in_range<UINT>(result.size()))
|
|
{
|
|
return result;
|
|
}
|
|
::GetPrivateProfileStruct(GetSection(path).c_str(), GetKey(path).c_str(), result.data(), static_cast<UINT>(result.size()), filename.AsNative().c_str());
|
|
return result;
|
|
}
|
|
|
|
mpt::ustring IniFileSettingsBackend::ReadSettingRaw(const SettingPath &path, const mpt::ustring &def) const
|
|
{
|
|
std::vector<TCHAR> buf(128);
|
|
while(::GetPrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::ToWin(def).c_str(), buf.data(), static_cast<DWORD>(buf.size()), filename.AsNative().c_str()) == buf.size() - 1)
|
|
{
|
|
if(buf.size() == std::numeric_limits<DWORD>::max())
|
|
{
|
|
return def;
|
|
}
|
|
buf.resize(mpt::exponential_grow(buf.size(), std::numeric_limits<DWORD>::max()));
|
|
}
|
|
return mpt::ToUnicode(mpt::winstring(buf.data()));
|
|
}
|
|
|
|
double IniFileSettingsBackend::ReadSettingRaw(const SettingPath &path, double def) const
|
|
{
|
|
std::vector<TCHAR> buf(128);
|
|
while(::GetPrivateProfileString(GetSection(path).c_str(), GetKey(path).c_str(), mpt::tfmt::val(def).c_str(), buf.data(), static_cast<DWORD>(buf.size()), filename.AsNative().c_str()) == buf.size() - 1)
|
|
{
|
|
if(buf.size() == std::numeric_limits<DWORD>::max())
|
|
{
|
|
return def;
|
|
}
|
|
buf.resize(mpt::exponential_grow(buf.size(), std::numeric_limits<DWORD>::max()));
|
|
}
|
|
return ConvertStrTo<double>(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<std::byte> &val)
|
|
{
|
|
MPT_ASSERT(mpt::in_range<UINT>(val.size()));
|
|
::WritePrivateProfileStruct(GetSection(path).c_str(), GetKey(path).c_str(), (LPVOID)val.data(), static_cast<UINT>(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<char> ReadFile(const mpt::PathString &filename)
|
|
{
|
|
mpt::ifstream s(filename, std::ios::binary);
|
|
std::vector<char> 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<const char*>(UTF16LE_BOM), 2);
|
|
inifile.write(reinterpret_cast<const char*>(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<char> data = ReadFile(filename);
|
|
if(!data.empty() && IsTextUnicode(data.data(), mpt::saturate_cast<int>(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<std::string>(data)));
|
|
}
|
|
|
|
SettingValue IniFileSettingsBackend::ReadSetting(const SettingPath &path, const SettingValue &def) const
|
|
{
|
|
switch(def.GetType())
|
|
{
|
|
case SettingTypeBool: return SettingValue(ReadSettingRaw(path, def.as<bool>()), def.GetTypeTag()); break;
|
|
case SettingTypeInt: return SettingValue(ReadSettingRaw(path, def.as<int32>()), def.GetTypeTag()); break;
|
|
case SettingTypeFloat: return SettingValue(ReadSettingRaw(path, def.as<double>()), def.GetTypeTag()); break;
|
|
case SettingTypeString: return SettingValue(ReadSettingRaw(path, def.as<mpt::ustring>()), def.GetTypeTag()); break;
|
|
case SettingTypeBinary: return SettingValue(ReadSettingRaw(path, def.as<std::vector<std::byte> >()), 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<bool>()); break;
|
|
case SettingTypeInt: WriteSettingRaw(path, val.as<int32>()); break;
|
|
case SettingTypeFloat: WriteSettingRaw(path, val.as<double>()); break;
|
|
case SettingTypeString: WriteSettingRaw(path, val.as<mpt::ustring>()); break;
|
|
case SettingTypeBinary: WriteSettingRaw(path, val.as<std::vector<std::byte> >()); 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
|