Files
edgetx-gx12/companion/src/storage/appdata.cpp
T
2019-05-10 22:29:02 +02:00

601 lines
20 KiB
C++

/*
* Copyright (C) OpenTX
*
* Based on code named
* th9x - http://code.google.com/p/th9x
* er9x - http://code.google.com/p/er9x
* gruvin9x - http://code.google.com/p/gruvin9x
*
* License GPLv2: http://www.gnu.org/licenses/gpl-2.0.html
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*/
#include "appdata.h"
#include <QDateTime>
#include <QDir>
#include <QMetaObject>
#include <QMetaProperty>
#define SETTINGS_VERSION_KEY QStringLiteral("settings_version")
#define SETTINGS_TIMESTAMP_KEY QStringLiteral("settingsLastSave")
// Global data and storage object
AppData g;
// ** CompStoreObj class********************
CompStoreObj::CompStoreObj() :
QObject(),
m_settings(COMPANY, PRODUCT)
{ }
QString CompStoreObj::pathForKey(const QString & key, const QString & group) const
{
QString path = (group.isNull() ? settingsPath() : group);
if (!path.isEmpty() && !path.endsWith('/'))
path.append('/');
return path.append(key);
}
void CompStoreObj::load(CompStoreObj * obj, const QString & name, const QString & key, const QVariant & def, const QString & group)
{
const int idx = obj->metaObject()->indexOfProperty(qPrintable(name));
if (key.isEmpty() || idx < 0)
return;
const QMetaProperty & prop = obj->metaObject()->property(idx);
const QVariant currValue = prop.read(obj);
QVariant savedValue = m_settings.value(pathForKey(key, group), def);
if (savedValue.isValid() && savedValue.convert(currValue.userType()) && savedValue != currValue)
prop.write(obj, savedValue);
}
void CompStoreObj::store(const QVariant & newValue, const QString & key, const QString & group) const
{
m_settings.setValue(pathForKey(key, group), newValue);
m_settings.setValue(SETTINGS_TIMESTAMP_KEY, QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
}
void CompStoreObj::clear(const QString &key, const QString &group) const
{
m_settings.remove(pathForKey(key, group));
m_settings.setValue(SETTINGS_TIMESTAMP_KEY, QDateTime::currentDateTimeUtc().toString(Qt::ISODate));
}
void CompStoreObj::removeGroupIfEmpty(const QString &group) const
{
m_settings.beginGroup(group);
if (!m_settings.allKeys().size())
m_settings.remove("");
m_settings.endGroup();
}
//static void dumpMetadata(QObject *obj) {
// for (int i = obj->metaObject()->propertyOffset() /* 0 */, e = obj->metaObject()->propertyCount(); i < e; ++i)
// qDebug() << obj->metaObject()->property(i).name() << "=" << obj->metaObject()->property(i).read(obj);
// for (int i = obj->metaObject()->methodOffset() /* 0 */, e = obj->metaObject()->methodCount(); i < e; ++i) {
// QMetaMethod::Access a = obj->metaObject()->method(i).access(); QMetaMethod::MethodType t = obj->metaObject()->method(i).methodType();
// qDebug().noquote() << (a == 0 ? " private" : a == 1 ? "protected" : " public") << (t == 1 ? "signal:" : t == 2 ? "slot: " : ": ")
// << obj->metaObject()->method(i).typeName() << obj->metaObject()->method(i).methodSignature();
// }
//}
// static methods
bool CompStoreObj::initProperty(CompStoreObj * obj, const QString & name)
{
if (obj && !name.isEmpty()) {
const int initIdx = obj->metaObject()->indexOfMethod(PROP_FSIG_INIT_STR(name));
if (initIdx > -1)
return obj->metaObject()->method(initIdx).invoke(obj);
}
return false;
}
void CompStoreObj::initAllProperties(CompStoreObj * obj)
{
if (!obj)
return;
for (int i = obj->metaObject()->propertyOffset(), e = obj->metaObject()->propertyCount(); i < e; ++i)
initProperty(obj, QString(obj->metaObject()->property(i).name()));
}
bool CompStoreObj::storeProperty(CompStoreObj * obj, const QString & name)
{
if (!obj || name.isEmpty())
return false;
const QMetaProperty & prop = obj->metaObject()->property(obj->metaObject()->indexOfProperty(qPrintable(name)));
const QString key = propertyKeyName(obj, name);
QVariant currValue;
if (!prop.isValid() || !prop.isStored() || key.isEmpty() || !(currValue = prop.read(obj)).isValid())
return false;
if (currValue == propertyDefaultValue(obj, name)) {
obj->clear(key);
obj->removeGroupIfEmpty(obj->settingsPath());
}
else {
obj->store(currValue, key);
}
return false;
}
void CompStoreObj::storeAllProperties(CompStoreObj * obj)
{
if (!obj)
return;
for (int i = obj->metaObject()->propertyOffset(), e = obj->metaObject()->propertyCount(); i < e; ++i)
storeProperty(obj, QString(obj->metaObject()->property(i).name()));
}
bool CompStoreObj::resetProperty(CompStoreObj * obj, const QString & name)
{
if (obj && !name.isEmpty()) {
const QMetaProperty & prop = obj->metaObject()->property(obj->metaObject()->indexOfProperty(qPrintable(name)));
if (prop.isValid() && prop.isResettable())
return prop.reset(obj);
}
return false;
}
void CompStoreObj::resetAllProperties(CompStoreObj * obj)
{
if (!obj)
return;
for (int i = obj->metaObject()->propertyOffset(), e = obj->metaObject()->propertyCount(); i < e; ++i) {
const QMetaProperty & prop = obj->metaObject()->property(i);
if (prop.isValid() && prop.isResettable())
prop.reset(obj);
}
}
bool CompStoreObj::propertyPathIsValid(const QString & path)
{
const QPair<QString, QString> grp = splitGroupedPath(path); // {<group name>, <property key>}
return (groupHasDynamicProperties(grp.first) || propertyKeyExists(grp.first, grp.second));
}
bool CompStoreObj::propertyPathIsValidNonDefault(const QString & path, const QVariant & newVal)
{
const QPair<QString, QString> grp = splitGroupedPath(path); // {<group name>, <property key>}
return (groupHasDynamicProperties(grp.first) || (propertyKeyExists(grp.first, grp.second) && newVal != propertyKeyDefaultValue(grp.first, grp.second)));
}
bool CompStoreObj::propertyExists(CompStoreObj * obj, const QString & name)
{
if (!obj)
return false;
return obj->metaObject()->indexOfProperty(qPrintable(name)) > -1;
}
const QVariant CompStoreObj::propertyDefaultValue(CompStoreObj * obj, const QString & name)
{
QVariant ret;
if (!obj || name.isEmpty())
return ret;
const int idx = obj->metaObject()->indexOfMethod(PROP_FSIG_DFLT_STR(name));
if (idx > -1)
obj->metaObject()->method(idx).invoke(obj, Qt::DirectConnection, Q_RETURN_ARG(QVariant, ret));
//qDebug() << ret;
return ret;
}
const QString CompStoreObj::propertyKeyName(CompStoreObj * obj, const QString & name)
{
QString ret;
if (!obj || name.isEmpty())
return ret;
const int idx = obj->metaObject()->indexOfMethod(PROP_FSIG_KEY_STR(name));
if (idx > -1)
obj->metaObject()->method(idx).invoke(obj, Qt::DirectConnection, Q_RETURN_ARG(QString, ret));
//qDebug() << ret;
return ret;
}
QPair<QString, QString> CompStoreObj::splitGroupedPath(const QString & path)
{
const QStringList list = path.split('/');
const QString grp = list.size() > 1 ? list.first() : QStringLiteral("General"); // "General" is the default (no-name) group, used by AppData top-level settings.
return QPair<QString, QString>(grp, list.last());
}
QHash<QString, CompStoreObj *> & CompStoreObj::group2ObjMap()
{
static QHash<QString, CompStoreObj *> map;
return map;
}
QVector<QString> &CompStoreObj::dynamicPropGroups()
{
static QVector<QString> list;
return list;
}
const QHash<QString, QHash<QString, QString> > & CompStoreObj::keyToNameMap()
{
static QHash<QString, QHash<QString, QString> > map;
if (map.isEmpty()) {
QHashIterator<QString, CompStoreObj *> it(group2ObjMap());
while (it.hasNext()) {
it.next();
QHash<QString, QString> grpMap;
CompStoreObj * obj = it.value();
//dumpMetadata(obj);
for (int i = obj->metaObject()->propertyOffset(), e = obj->metaObject()->propertyCount(); i < e; ++i) {
const QString name = QString(obj->metaObject()->property(i).name());
const QString key = propertyKeyName(obj, name);
if (!key.isEmpty())
grpMap.insert(key, name);
}
map.insert(it.key(), grpMap);
//qDebug() << it.key() << grpMap;
}
}
return map;
}
// ** JStickData class********************
JStickData::JStickData() : CompStoreObj(), index(-1)
{
CompStoreObj::addObjectMapping(propertyGroup(), this);
}
bool JStickData::existsOnDisk()
{
return (m_settings.value(settingsPath() % stick_axe_key(), -1).toInt() > -1);
}
// ** Profile class********************
Profile::Profile() : CompStoreObj(), index(-1)
{
CompStoreObj::addObjectMapping(propertyGroup(), this);
}
// The default copy operator can not be used since the index variable would be destroyed
Profile & Profile::operator= (const Profile & rhs)
{
for (int i = metaObject()->propertyOffset(), e = metaObject()->propertyCount(); i < e; ++i) {
const QMetaProperty & prop = metaObject()->property(i);
if (!prop.isValid() || !prop.isWritable()) {
qWarning() << "Could not copy property" << QString(prop.name()) << "isValid:" << prop.isValid() << "isWritable:" << prop.isWritable();
continue;
}
prop.write(this, prop.read(&rhs));
}
return *this;
}
bool Profile::existsOnDisk()
{
return m_settings.contains(settingsPath() % "Name");
}
void Profile::resetFwVariables()
{
for (const QString & name : fwVarsList())
CompStoreObj::resetProperty(this, name);
}
// ** AppData class********************
AppData::AppData() :
CompStoreObj(),
m_sessionId(0)
{
QMetaType::registerComparators<SimulatorOptions>();
qRegisterMetaTypeStreamOperators<SimulatorOptions>("SimulatorOptions");
qRegisterMetaTypeStreamOperators<AppData::DownloadBranchType>("AppData::DownloadBranchType");
qRegisterMetaTypeStreamOperators<AppData::NewModelAction>("AppData::NewModelAction");
CompStoreObj::addObjectMapping(propertyGroup(), this);
firstUse = !hasCurrentSettings();
// Make sure the settings backup folder exists
if (firstUse && !QDir().mkpath(CPN_SETTINGS_BACKUP_DIR))
qWarning() << "Could not create settings backup path" << CPN_SETTINGS_BACKUP_DIR;
// Configure the profiles
for (int i=0; i<MAX_PROFILES; i++)
profile[i].setIndex(i);
// Configure the joysticks
for (int i=0; i<MAX_JOYSTICKS; i++)
joystick[i].setIndex(i);
}
static QString fmtHex(quint32 num)
{
return QString::number(num, 16).toUpper();
}
void AppData::init()
{
qInfo().noquote() << "Settings init with" << m_settings.organizationName() << m_settings.applicationName()
<< "Saved version:" << fmtHex(m_settings.value(SETTINGS_VERSION_KEY).toUInt()) << "Current version:" << fmtHex(CPN_SETTINGS_VERSION);
// This connection doesn't work in the constructor because AppData is created before QApplication. Globals suck like that. Compensate by using Qt::UniqueConnection because init() may be called multiple times within app lifetime.
connect(this, &AppData::idChanged, this, static_cast<void (AppData::*)(int)>(&AppData::sessionId), Qt::UniqueConnection);
// update anything that may have changed since last saved version and update the version marker if needed.
convertSettings(m_settings);
initAll();
}
void AppData::initAll()
{
// Initialize all variables. Use default values if no saved settings.
CompStoreObj::initAllProperties(this);
// Initialize the profiles
for (int i=0; i<MAX_PROFILES; i++)
profile[i].init();
// Initialize the joysticks
for (int i=0; i<MAX_JOYSTICKS; i++)
joystick[i].init();
}
void AppData::resetAllSettings()
{
resetAll();
fwRev.resetAll();
for (int i=0; i<MAX_PROFILES; i++)
profile[i].resetAll();
for (int i=0; i<MAX_JOYSTICKS; i++)
joystick[i].resetAll();
firstUse = true;
}
void AppData::storeAllSettings()
{
storeAll();
for (int i=0; i<MAX_PROFILES; i++)
profile[i].storeAll();
for (int i=0; i<MAX_JOYSTICKS; i++)
joystick[i].storeAll();
}
bool AppData::hasCurrentSettings() const
{
return m_settings.contains(SETTINGS_VERSION_KEY);
}
void AppData::sessionId(int index)
{
if (index < 0 || index >= MAX_PROFILES || index == m_sessionId)
return;
m_sessionId = index;
emit sessionIdChanged(index);
emit currentProfileChanged();
}
Profile & AppData::getProfile(int index)
{
if (index > -1 && index < MAX_PROFILES)
return profile[index];
return profile[0];
}
const Profile & AppData::getProfile(int index) const
{
if (index > -1 && index < MAX_PROFILES)
return profile[index];
return profile[0];
}
QMap<int, QString> AppData::getActiveProfiles() const
{
QMap<int, QString> active;
for (int i=0; i<MAX_PROFILES; i++) {
if (g.profile[i].existsOnDisk())
active.insert(i, g.profile[i].name());
}
return active;
}
void AppData::convertSettings(QSettings & settings)
{
quint32 savedVer = settings.value(SETTINGS_VERSION_KEY, 0).toUInt();
#if (VERSION_NUMBER < QT_VERSION_CHECK(2, 3, 0))
// convert old 2.2.x version marker; TODO: remove once v2.3 is established (because we'll always convert and clean 2.2.x settings anyway)
if (savedVer == 220)
savedVer = 0x2020000;
#endif
if (savedVer == CPN_SETTINGS_VERSION)
return;
if (savedVer > CPN_SETTINGS_VERSION) {
qWarning().noquote() << "Saved settings version is newer than current, skippig conversions. Saved:" << fmtHex(savedVer) << "Current:" << fmtHex(CPN_SETTINGS_VERSION);
return;
}
// We only want to remove old/deprecated settngs if the {major}{minor} part of the settings version number has changed.
// This preserves backwards compatiblity within minor version releases (where only revision number changes).
const unsigned savedMajMin = (savedVer >> 16);
const unsigned currMajMin = (CPN_SETTINGS_VERSION >> 16);
const bool removeUnused = (savedMajMin < currMajMin);
qInfo().noquote().nospace() << "Converting settings " << settings.applicationName()
<< " from v" << fmtHex(savedVer) << " (" << fmtHex(savedMajMin) << ") to v"
<< fmtHex(CPN_SETTINGS_VERSION) << " (" << fmtHex(currMajMin) << "). Removing unused: " << removeUnused;
if (settings.contains("useWizard")) {
if (!settings.contains(newModelAction_key()))
settings.setValue(newModelAction_key(), (settings.value("useWizard").toBool() ? MODEL_ACT_WIZARD : MODEL_ACT_EDITOR));
if (removeUnused)
settings.remove("useWizard");
}
// meaning of warningId changed during v2.2 development but value of "7" indicates old setting, removing it will restore defaults
if (savedMajMin < 0x203 && settings.contains("warningId") && settings.value("warningId").toInt() == 7)
settings.remove("warningId");
// Convert joystick settings to new style of key names
static const QString jsCalBasePath = QStringLiteral("JsCalibration/%1");
if (settings.contains(jsCalBasePath.arg("stick0_axe"))) {
const QStringList vars({"axe", "inv", "max", "med", "min"});
for (int i=0; i < MAX_JOYSTICKS; ++i) {
for (const QString & var : vars) {
const QString oldPath = jsCalBasePath.arg(QString("stick%1_%2").arg(i).arg(var)); // old format "/stick#_var"
const QVariant val = settings.value(oldPath);
if (val.isValid())
settings.setValue(jsCalBasePath.arg(QString("%1/stick_%2").arg(i).arg(var)), val); // new format "/#/stick_var"
if (removeUnused)
settings.remove(oldPath);
}
}
}
if (removeUnused)
clearUnusedSettings(settings);
// This is a version marker, used to convert settings between versions. Do NOT remove it, ever! Also see notes on CPN_SETTINGS_VERSION macro.
settings.setValue(SETTINGS_VERSION_KEY, CPN_SETTINGS_VERSION);
}
void AppData::clearUnusedSettings(QSettings & settings)
{
// Go through and clean up anything that doesn't exist or is set to default value.
foreach (const QString & key, settings.allKeys()) {
if (key == ".") // special Windows registry key, don't delete it
continue;
const QVariant newVal = settings.value(key);
// Remove key if property does not exist or is the default value.
if (!newVal.isValid() || !CompStoreObj::propertyPathIsValidNonDefault(key, newVal))
settings.remove(key);
}
}
bool AppData::findPreviousVersionSettings(QString * version) const
{
static const QStringList versList({QStringLiteral("2.2"), QStringLiteral("2.1"), QStringLiteral("2.0")});
for (const QString &ver : versList) {
const QString prod("Companion " % ver);
// make sure we do not try to import from ourselves otherwise settings WILL get corrupted
if (prod == PRODUCT)
continue;
QSettings settings(COMPANY, prod);
if (settings.contains(SETTINGS_VERSION_KEY)) {
*version = ver;
return true;
}
else {
settings.clear();
}
}
QSettings settings(COMPANY, "OpenTX Companion");
if (settings.contains(SETTINGS_VERSION_KEY)) {
*version = QStringLiteral("1.x");
return true;
}
else {
settings.clear();
}
return false;
}
bool AppData::importSettings(const QString & fromVersion)
{
QString fromProduct;
upgradeFromVersion.clear();
if (fromVersion.startsWith("2."))
fromProduct = "Companion " % fromVersion;
else if (fromVersion == "1.x")
fromProduct = "OpenTX Companion";
else
return false;
upgradeFromVersion = fromVersion;
QSettings fromSettings(COMPANY, fromProduct);
return importSettings(&fromSettings);
}
bool AppData::importSettings(QSettings * fromSettings)
{
if (!fromSettings)
return false;
// Create temporary settings because we may modify them before import.
QSettings tempSettings(COMPANY, PRODUCT % "_import");
for (const QString & key : fromSettings->allKeys())
tempSettings.setValue(key, fromSettings->value(key));
// convert settings first to simplify import process
convertSettings(tempSettings);
if (fromSettings->status() != QSettings::NoError || tempSettings.status() != QSettings::NoError) {
tempSettings.clear();
return false;
}
// import settings, just copy since they have already been converted/cleaned.
for (const QString & key : tempSettings.allKeys()) {
if (key != ".") // special Windows registry key, don't import it
m_settings.setValue(key, tempSettings.value(key));
}
tempSettings.clear();
if (m_settings.status() != QSettings::NoError)
return false;
firstUse = !hasCurrentSettings();
return true;
}
bool AppData::exportSettings(QSettings * toSettings, bool clearDestination)
{
if (!toSettings)
return false;
m_settings.sync();
if (clearDestination)
toSettings->clear();
foreach (const QString & key, m_settings.allKeys()) {
const QVariant newVal = m_settings.value(key);
// Skip export if property does not exist or is the default value.
if (newVal.isValid() && CompStoreObj::propertyPathIsValidNonDefault(key, newVal))
toSettings->setValue(key, newVal);
//else qDebug() << "SKIPPING:" << key << newVal;
}
// Write the version and timestamp to export file -- they will NOT be imported, but may be useful for converting future imports.
toSettings->setValue(SETTINGS_VERSION_KEY, m_settings.value(SETTINGS_VERSION_KEY));
toSettings->setValue(SETTINGS_TIMESTAMP_KEY, m_settings.value(SETTINGS_TIMESTAMP_KEY));
toSettings->sync();
return (toSettings->status() == QSettings::NoError);
}
bool AppData::exportSettingsToFile(const QString & expFile, QString & resultMsg)
{
QSettings toSettings(expFile, QSettings::IniFormat);
if (exportSettings(&toSettings)) {
resultMsg = tr("Application Settings have been saved to\n %1").arg(expFile);
return true;
}
resultMsg = tr("Could not save Application Settings to file \"%1\"").arg(expFile) % " ";
if (toSettings.status() == QSettings::AccessError)
resultMsg.append(tr("because the file could not be saved (check access permissions)."));
else
resultMsg.append(tr("for unknown reasons."));
return false;
}