From e71bc7b1b3bcc052d5c7fbb81797136258d8900d Mon Sep 17 00:00:00 2001 From: Vasily Date: Thu, 26 Dec 2019 11:42:30 +0100 Subject: [PATCH] Add settings object API Documentation is in `docs/specs/Settings-Object-API.md`. --- build_scripts/qt/sources.pri | 8 +- docs/specs/Settings-Object-API.md | 120 +++++++++++ src/main.cpp | 1 + src/settings/internal/settings_object_data.h | 75 +++++++ src/settings/settings_object.cpp | 197 +++++++++++++++++++ src/settings/settings_object.h | 27 +++ 6 files changed, 426 insertions(+), 2 deletions(-) create mode 100644 docs/specs/Settings-Object-API.md create mode 100644 src/settings/internal/settings_object_data.h create mode 100644 src/settings/settings_object.cpp create mode 100644 src/settings/settings_object.h diff --git a/build_scripts/qt/sources.pri b/build_scripts/qt/sources.pri index a4385e2..ff2cd93 100644 --- a/build_scripts/qt/sources.pri +++ b/build_scripts/qt/sources.pri @@ -19,7 +19,9 @@ SOURCES += src/main.cpp\ src/openvr/overlay_utils.cpp \ src/keyboard_input/keyboard_input.cpp \ src/keyboard_input/input_parser.cpp \ - src/settings/settings.cpp + src/settings/settings.cpp \ + src/settings/settings_object.cpp \ + HEADERS += src/overlaycontroller.h \ src/tabcontrollers/AudioTabController.h \ @@ -55,7 +57,9 @@ HEADERS += src/overlaycontroller.h \ src/settings/internal/setting_value.h \ src/settings/internal/settings_internal.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 { SOURCES += src/tabcontrollers/audiomanager/AudioManagerWindows.cpp \ diff --git a/docs/specs/Settings-Object-API.md b/docs/specs/Settings-Object-API.md new file mode 100644 index 0000000..0802254 --- /dev/null +++ b/docs/specs/Settings-Object-API.md @@ -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 void addValue( const Value value ); + + template + Value getNextValueOrDefault( const Value defaultValue ); + + template bool hasValuesOfType(); +} +``` +The different types are stored differently, so `addValue()`, `addValue()`, `getNextValueOrDefault()` 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( "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. diff --git a/src/main.cpp b/src/main.cpp index 34033a1..a42d240 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,5 +1,6 @@ #include "utils/setup.h" #include "settings/settings.h" +#include "settings/settings_object.h" INITIALIZE_EASYLOGGINGPP diff --git a/src/settings/internal/settings_object_data.h b/src/settings/internal/settings_object_data.h new file mode 100644 index 0000000..e2cd410 --- /dev/null +++ b/src/settings/internal/settings_object_data.h @@ -0,0 +1,75 @@ +#pragma once +#include +#include + +namespace settings +{ +class SettingsObjectData +{ +public: + template void addValue( const Value value ) + { + std::list& list = getListObject(); + + list.push_back( value ); + } + + template + Value getNextValueOrDefault( const Value defaultValue ) + { + Value returnedValue = defaultValue; + + std::list& list = getListObject(); + + if ( hasValuesOfType() ) + { + returnedValue = list.front(); + list.pop_front(); + } + + return returnedValue; + } + + template bool hasValuesOfType() + { + std::list& list = getListObject(); + + return !list.empty(); + } + +private: + template std::list& getListObject() + { + using std::is_same; + const auto isBool = is_same::value; + const auto isInt = is_same::value; + const auto isDouble = is_same::value; + const auto isString = is_same::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 m_boolValues; + std::list m_intValues; + std::list m_doubleValues; + std::list m_stringValues; +}; +} // namespace settings diff --git a/src/settings/settings_object.cpp b/src/settings/settings_object.cpp new file mode 100644 index 0000000..5ade7a1 --- /dev/null +++ b/src/settings/settings_object.cpp @@ -0,0 +1,197 @@ +#include +#include "settings_object.h" + +namespace settings +{ +[[nodiscard]] QSettings& getQSettings(); + +} + +namespace +{ +template +std::list createListFromObject( settings::SettingsObjectData& obj ) +{ + std::list list; + while ( obj.hasValuesOfType() ) + { + list.push_back( obj.getNextValueOrDefault( Value{} ) ); + } + return list; +} + +template +void addListToObject( std::list& list, + settings::SettingsObjectData& obj ) +{ + while ( !list.empty() ) + { + obj.addValue( list.front() ); + list.pop_front(); + } +} + +template +void saveListToDisk( std::list 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::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 +std::list loadListFromDisk( const std::string structName, + const std::string typeName ) +{ + using std::is_same; + const auto isBool = is_same::value; + const auto isInt = is_same::value; + const auto isDouble = is_same::value; + const auto isString = is_same::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 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( objName, "bools" ); + addListToObject( boolValues, s ); + + auto intValues = loadListFromDisk( objName, "ints" ); + addListToObject( intValues, s ); + + auto doubleValues = loadListFromDisk( objName, "doubles" ); + addListToObject( doubleValues, s ); + + auto stringValues = loadListFromDisk( objName, "strings" ); + addListToObject( stringValues, s ); + + return s; +} + +void saveSettingsObject( settings::SettingsObjectData& s, std::string objName ) +{ + auto boolValues = createListFromObject( s ); + saveListToDisk( boolValues, objName, "bools" ); + + auto intValues = createListFromObject( s ); + saveListToDisk( intValues, objName, "ints" ); + + auto doubleValues = createListFromObject( s ); + saveListToDisk( doubleValues, objName, "doubles" ); + + auto stringValues = createListFromObject( 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 diff --git a/src/settings/settings_object.h b/src/settings/settings_object.h new file mode 100644 index 0000000..6d8fc20 --- /dev/null +++ b/src/settings/settings_object.h @@ -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