/* * 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 #include #include #include #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 grp = splitGroupedPath(path); // {, } return (groupHasDynamicProperties(grp.first) || propertyKeyExists(grp.first, grp.second)); } bool CompStoreObj::propertyPathIsValidNonDefault(const QString & path, const QVariant & newVal) { const QPair grp = splitGroupedPath(path); // {, } 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 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(grp, list.last()); } QHash & CompStoreObj::group2ObjMap() { static QHash map; return map; } QVector &CompStoreObj::dynamicPropGroups() { static QVector list; return list; } const QHash > & CompStoreObj::keyToNameMap() { static QHash > map; if (map.isEmpty()) { QHashIterator it(group2ObjMap()); while (it.hasNext()) { it.next(); QHash 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(); qRegisterMetaTypeStreamOperators("SimulatorOptions"); qRegisterMetaTypeStreamOperators("AppData::DownloadBranchType"); qRegisterMetaTypeStreamOperators("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(&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 || 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 AppData::getActiveProfiles() const { QMap active; for (int i=0; i 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; }