Add settings object API

Documentation is in `docs/specs/Settings-Object-API.md`.
This commit is contained in:
Vasily
2020-01-08 21:18:45 +01:00
committed by username223
parent bda3cc0aa2
commit e71bc7b1b3
6 changed files with 426 additions and 2 deletions
+6 -2
View File
@@ -19,7 +19,9 @@ SOURCES += src/main.cpp\
src/openvr/overlay_utils.cpp \ src/openvr/overlay_utils.cpp \
src/keyboard_input/keyboard_input.cpp \ src/keyboard_input/keyboard_input.cpp \
src/keyboard_input/input_parser.cpp \ src/keyboard_input/input_parser.cpp \
src/settings/settings.cpp src/settings/settings.cpp \
src/settings/settings_object.cpp \
HEADERS += src/overlaycontroller.h \ HEADERS += src/overlaycontroller.h \
src/tabcontrollers/AudioTabController.h \ src/tabcontrollers/AudioTabController.h \
@@ -55,7 +57,9 @@ HEADERS += src/overlaycontroller.h \
src/settings/internal/setting_value.h \ src/settings/internal/setting_value.h \
src/settings/internal/settings_internal.h \ src/settings/internal/settings_internal.h \
src/settings/internal/settings_controller.h \ src/settings/internal/settings_controller.h \
src/settings/internal/specific_setting_value.h src/settings/internal/specific_setting_value.h \
src/settings/settings_object.h \
src/settings/internal/settings_object_data.h
win32 { win32 {
SOURCES += src/tabcontrollers/audiomanager/AudioManagerWindows.cpp \ SOURCES += src/tabcontrollers/audiomanager/AudioManagerWindows.cpp \
+120
View File
@@ -0,0 +1,120 @@
## Problem
The current method of saving logically connected variables is to use the QSettings API directly.
This leads to large amounts of code duplication since every controller has to have their own implementation.
It also leads to slow development since new features will usually have to implement their own settings system.
## Considerations
The API should be resistant to internal changes.
The API should be simple to use.
## Requirements
The API MUST allow the permanent storage of objects. Internal changes to objects MUST NOT be a breaking change.
## Public API
The public API is in the `settings_object.h` file in the `settings` namespace.
The API consists of two parts, an imperative set of functions and a virtual class that objects must implement.
The imperative functions are straightforward.
```cpp
void saveObject( const ISettingsObject& obj );
void loadObject( ISettingsObject& obj );
void saveNumberedObject( const ISettingsObject& obj, const int slot );
void loadNumberedObject( ISettingsObject& obj, const int slot );
int getAmountOfSavedObjects( ISettingsObject& obj );
```
The `numberedObject` versions can be used for saving an arbitrary amount of objects, retrievable with the same `slot`.
`getAmountOfSavedObjects` will return the amount of objects, starting from _1_, that exist contiguously.
Will return `0` if no objects have been saved as `saveNumberedObject(obj, 1)`.
The `ISettingsObject`s will have to be implemented by the object to be saved.
```cpp
class ISettingsObject
{
public:
virtual ~ISettingsObject() {}
virtual SettingsObjectData saveSettings() const = 0;
virtual void loadSettings( SettingsObjectData& obj ) = 0;
virtual std::string settingsName() const = 0;
};
```
The `settingsName()` is used internally for correctly identifying objects and it should be globally unique.
The `ISettingsObject` communicates with the API through a `SettingsObjectData` object, which contains the variables of the object in a First-In First-Out (FIFO) format.
The contents of the object can be set or retrieved through three templated functions.
```cpp
class SettingsObjectData
{
public:
template <typename Value> void addValue( const Value value );
template <typename Value>
Value getNextValueOrDefault( const Value defaultValue );
template <typename Value> bool hasValuesOfType();
}
```
The different types are stored differently, so `addValue<int>()`, `addValue<double>()`, `getNextValueOrDefault<double>()` would return the value added second, but the first double value.
An example implementation for a struct can be found below.
```cpp
struct A : settings::ISettingsObject
{
bool val1 = true;
int val2 = 1123;
double val3 = 1.0003;
double val4 = 1.0004;
double val5 = 1.0005;
std::string val6 = "val6";
std::string settingsName() const override
{
return "SettingsTestStruct";
}
void loadSettings( settings::SettingsObjectData& obj ) override
{
val1 = obj.getNextValueOrDefault( false );
val2 = obj.getNextValueOrDefault( 1 );
val3 = obj.getNextValueOrDefault( 3.0 );
val4 = obj.getNextValueOrDefault( 4.0 );
val5 = obj.getNextValueOrDefault( 5.0 );
val6 = obj.getNextValueOrDefault<std::string>( "default" );
}
settings::SettingsObjectData saveSettings() const override
{
settings::SettingsObjectData o;
o.addValue( val1 );
o.addValue( val2 );
o.addValue( val3 );
o.addValue( val4 );
o.addValue( val5 );
o.addValue( val6 );
return o;
}
};
```
Values of different types can be returned in any order, although it is recommended to save and load settings in the same order.
## Other Solutions
Simply copying the contents of an object (think direct `memcpy()`) was considered but was not chosen because every addition or removal of a struct member would be a breaking change.
Additionally, the C++ compiler is free to insert padding in the struct, meaning different compilations could interpret data differently.
+1
View File
@@ -1,5 +1,6 @@
#include "utils/setup.h" #include "utils/setup.h"
#include "settings/settings.h" #include "settings/settings.h"
#include "settings/settings_object.h"
INITIALIZE_EASYLOGGINGPP INITIALIZE_EASYLOGGINGPP
@@ -0,0 +1,75 @@
#pragma once
#include <list>
#include <easylogging++.h>
namespace settings
{
class SettingsObjectData
{
public:
template <typename Value> void addValue( const Value value )
{
std::list<Value>& list = getListObject<Value>();
list.push_back( value );
}
template <typename Value>
Value getNextValueOrDefault( const Value defaultValue )
{
Value returnedValue = defaultValue;
std::list<Value>& list = getListObject<Value>();
if ( hasValuesOfType<Value>() )
{
returnedValue = list.front();
list.pop_front();
}
return returnedValue;
}
template <typename Value> bool hasValuesOfType()
{
std::list<Value>& list = getListObject<Value>();
return !list.empty();
}
private:
template <typename Value> std::list<Value>& getListObject()
{
using std::is_same;
const auto isBool = is_same<bool, Value>::value;
const auto isInt = is_same<int, Value>::value;
const auto isDouble = is_same<double, Value>::value;
const auto isString = is_same<std::string, Value>::value;
static_assert( isBool || isInt || isDouble || isString,
"Type is not supported for the settings object." );
if constexpr ( isBool )
{
return m_boolValues;
}
else if constexpr ( isInt )
{
return m_intValues;
}
else if constexpr ( isDouble )
{
return m_doubleValues;
}
else if constexpr ( isString )
{
return m_stringValues;
}
}
std::list<bool> m_boolValues;
std::list<int> m_intValues;
std::list<double> m_doubleValues;
std::list<std::string> m_stringValues;
};
} // namespace settings
+197
View File
@@ -0,0 +1,197 @@
#include <QSettings>
#include "settings_object.h"
namespace settings
{
[[nodiscard]] QSettings& getQSettings();
}
namespace
{
template <typename Value>
std::list<Value> createListFromObject( settings::SettingsObjectData& obj )
{
std::list<Value> list;
while ( obj.hasValuesOfType<Value>() )
{
list.push_back( obj.getNextValueOrDefault( Value{} ) );
}
return list;
}
template <typename Value>
void addListToObject( std::list<Value>& list,
settings::SettingsObjectData& obj )
{
while ( !list.empty() )
{
obj.addValue( list.front() );
list.pop_front();
}
}
template <typename Value>
void saveListToDisk( std::list<Value> values,
const std::string structName,
const std::string typeName )
{
auto& s = settings::getQSettings();
s.beginGroup( structName.c_str() );
s.beginWriteArray( typeName.c_str() );
for ( int i = 0; !values.empty(); ++i )
{
s.setArrayIndex( i );
if constexpr ( std::is_same<std::string, Value>::value )
{
s.setValue( typeName.c_str(), values.front().c_str() );
}
else
{
s.setValue( typeName.c_str(), values.front() );
}
values.pop_front();
}
s.endArray();
s.endGroup();
}
template <typename Value>
std::list<Value> loadListFromDisk( const std::string structName,
const std::string typeName )
{
using std::is_same;
const auto isBool = is_same<bool, Value>::value;
const auto isInt = is_same<int, Value>::value;
const auto isDouble = is_same<double, Value>::value;
const auto isString = is_same<std::string, Value>::value;
static_assert( isBool || isInt || isDouble || isString,
"Type is not supported for the settings object." );
auto& s = settings::getQSettings();
s.beginGroup( structName.c_str() );
auto size = s.beginReadArray( typeName.c_str() );
std::list<Value> list;
for ( int i = 0; i < size; ++i )
{
s.setArrayIndex( i );
auto v = s.value( typeName.c_str() );
if constexpr ( isBool )
{
list.push_back( v.toBool() );
}
else if constexpr ( isInt )
{
list.push_back( v.toInt() );
}
else if constexpr ( isDouble )
{
list.push_back( v.toDouble() );
}
else if constexpr ( isString )
{
list.push_back( v.toString().toStdString() );
}
}
s.endArray();
s.endGroup();
return list;
}
settings::SettingsObjectData loadSettingsObject( std::string objName )
{
settings::SettingsObjectData s;
auto boolValues = loadListFromDisk<bool>( objName, "bools" );
addListToObject( boolValues, s );
auto intValues = loadListFromDisk<int>( objName, "ints" );
addListToObject( intValues, s );
auto doubleValues = loadListFromDisk<double>( objName, "doubles" );
addListToObject( doubleValues, s );
auto stringValues = loadListFromDisk<std::string>( objName, "strings" );
addListToObject( stringValues, s );
return s;
}
void saveSettingsObject( settings::SettingsObjectData& s, std::string objName )
{
auto boolValues = createListFromObject<bool>( s );
saveListToDisk( boolValues, objName, "bools" );
auto intValues = createListFromObject<int>( s );
saveListToDisk( intValues, objName, "ints" );
auto doubleValues = createListFromObject<double>( s );
saveListToDisk( doubleValues, objName, "doubles" );
auto stringValues = createListFromObject<std::string>( s );
saveListToDisk( stringValues, objName, "strings" );
}
std::string appendSlotNumberToSettingsName( const std::string name,
const int slot )
{
return name + "-" + std::to_string( slot );
}
} // namespace
namespace settings
{
void saveObject( const ISettingsObject& obj )
{
auto s = obj.saveSettings();
saveSettingsObject( s, obj.settingsName() );
}
void loadObject( ISettingsObject& obj )
{
auto s = loadSettingsObject( obj.settingsName() );
obj.loadSettings( s );
}
void saveNumberedObject( const ISettingsObject& obj, const int slot )
{
auto s = obj.saveSettings();
saveSettingsObject(
s, appendSlotNumberToSettingsName( obj.settingsName(), slot ) );
}
void loadNumberedObject( ISettingsObject& obj, const int slot )
{
auto s = loadSettingsObject(
appendSlotNumberToSettingsName( obj.settingsName(), slot ) );
obj.loadSettings( s );
}
int getAmountOfSavedObjects( ISettingsObject& obj )
{
auto& s = getQSettings();
auto groups = s.childGroups();
for ( int i = 1;; ++i )
{
const auto groupDoesNotContainSetting = !groups.contains(
appendSlotNumberToSettingsName( obj.settingsName(), i ).c_str() );
if ( groupDoesNotContainSetting )
{
return i - 1;
}
}
}
} // namespace settings
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include "internal/settings_object_data.h"
namespace settings
{
class ISettingsObject
{
public:
virtual ~ISettingsObject() {}
virtual SettingsObjectData saveSettings() const = 0;
virtual void loadSettings( SettingsObjectData& obj ) = 0;
virtual std::string settingsName() const = 0;
};
void saveObject( const ISettingsObject& obj );
void loadObject( ISettingsObject& obj );
void saveNumberedObject( const ISettingsObject& obj, const int slot );
void loadNumberedObject( ISettingsObject& obj, const int slot );
int getAmountOfSavedObjects( ISettingsObject& obj );
} // namespace settings