From 6818addf46f9d934f2e46e739b60f028e79cb13f Mon Sep 17 00:00:00 2001 From: elecpower Date: Mon, 11 May 2026 21:52:11 +1000 Subject: [PATCH 01/17] feat(cpn): component update last release info per radio profile --- companion/src/storage/appdata.cpp | 100 +++++++++++++++++++--- companion/src/storage/appdata.h | 38 ++++++-- companion/src/updates/updateinterface.cpp | 16 ++-- 3 files changed, 127 insertions(+), 27 deletions(-) diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index a1609d3fa6d..4f36b13b0f3 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -297,6 +297,12 @@ bool NamedJSData::existsOnDisk() return (m_settings.value(settingsPath() % jsName(), -1).toInt() > -1); } +// ** ComponentReleaseData class******************** + +ComponentReleaseData::ComponentReleaseData() : CompStoreObj(), index(-1) +{ + CompStoreObj::addObjectMapping(propertyGroup(), this); +} // ** Profile class******************** @@ -329,6 +335,22 @@ bool Profile::existsOnDisk() return m_settings.contains(settingsPath() % "Name"); } +ComponentReleaseData & Profile::getCompRelease(int index) +{ + if (index > -1 && index < MAX_COMPONENTS) + return compRelease[index]; + + return compRelease[0]; +} + +const ComponentReleaseData & Profile::getCompRelease(int index) const +{ + if (index > -1 && index < MAX_COMPONENTS) + return compRelease[index]; + + return compRelease[0]; +} + // ** ComponentAssetData class******************** ComponentAssetData::ComponentAssetData() : CompStoreObj(), index(-1) @@ -359,6 +381,7 @@ bool ComponentAssetData::existsOnDisk() ComponentData::ComponentData() : CompStoreObj(), index(-1) { + qRegisterMetaType("ComponentData::ReleaseChannel"); CompStoreObj::addObjectMapping(propertyGroup(), this); } @@ -381,15 +404,6 @@ bool ComponentData::existsOnDisk() return (m_settings.contains(settingsPath() % "name")); } -void ComponentData::releaseClear() -{ - releaseReset(); - releaseIdReset(); - prereleaseReset(); - dateReset(); - versionReset(); -} - ComponentAssetData & ComponentData::getAsset(int index) { if (index > -1 && index < MAX_COMPONENT_ASSETS) @@ -417,6 +431,10 @@ AppData::AppData() : CompStoreObj(), m_sessionId(0) { + qRegisterMetaType("AppData::NewModelAction"); + qRegisterMetaType("AppData::UpdateCheckFreq"); + qRegisterMetaType("AppData::SimuGenericKeysPos"); + CompStoreObj::addObjectMapping(propertyGroup(), this); firstUse = !hasCurrentSettings(); @@ -426,18 +444,27 @@ AppData::AppData() : qWarning() << "Could not create settings backup path" << CPN_SETTINGS_BACKUP_DIR; // Configure the profiles - for (int i = 0; i < MAX_PROFILES; i++) + for (int i = 0; i < MAX_PROFILES; i++) { profile[i].setIndex(i); + for (int j = 0; j < MAX_COMPONENTS; j++) { + profile[i].compRelease[j].setIndexes(i, j); + } + } + // Configure the joysticks for (int i = 0; i < MAX_JS_AXES; i++) joystick[i].setIndex(i); + for (int i = 0; i < MAX_JS_BUTTONS; i++) jsButton[i].setIndex(i); + for (int i = 0; i < MAX_NAMED_JOYSTICKS; i++) { namedJS[i].setIndex(i); + for (int a = 0; a < MAX_JS_AXES; a += 1) namedJS[i].joystick[a].setIndex(a, i); + for (int b = 0; b < MAX_JS_BUTTONS; b += 1) namedJS[i].jsButton[b].setIndex(b, i); } @@ -445,6 +472,7 @@ AppData::AppData() : // Configure the updates for (int i = 0; i < MAX_COMPONENTS; i++) { component[i].setIndex(i); + for (int j = 0; j < MAX_COMPONENT_ASSETS; j++) { component[i].asset[j].setIndexes(i, j); } @@ -543,23 +571,35 @@ 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++) + for (int i = 0; i < MAX_PROFILES; i++) { profile[i].init(); + + for (int j = 0; j < MAX_COMPONENTS; j++) { + profile[i].compRelease[j].init(); + } + } + // Initialize the joysticks for (int i = 0; i < MAX_JS_AXES; i++) joystick[i].init(); + for (int i = 0; i < MAX_JS_BUTTONS; i++) jsButton[i].init(); + for (int i = 0; i < MAX_NAMED_JOYSTICKS; i++) { namedJS[i].init(); + for (int a = 0; a < MAX_JS_AXES; a += 1) namedJS[i].joystick[a].init(); + for (int b = 0; b < MAX_JS_BUTTONS; b += 1) namedJS[i].jsButton[b].init(); } + // Initialize the updates for (int i = 0; i < MAX_COMPONENTS; i++) { component[i].init(); + for (int j = 0; j < MAX_COMPONENT_ASSETS; j++) { component[i].asset[j].init(); } @@ -570,46 +610,73 @@ void AppData::resetAllSettings() { resetAll(); fwRev.resetAll(); - for (int i = 0; i < MAX_PROFILES; i++) + + for (int i = 0; i < MAX_PROFILES; i++) { profile[i].resetAll(); + + for (int j = 0; j < MAX_COMPONENTS; j++) { + profile[i].compRelease[j].resetAll(); + } + } + for (int i = 0; i < MAX_JS_AXES; i++) joystick[i].resetAll(); + for (int i = 0; i < MAX_JS_BUTTONS; i++) jsButton[i].resetAll(); + for (int i = 0; i < MAX_NAMED_JOYSTICKS; i++) { namedJS[i].resetAll(); + for (int a = 0; a < MAX_JS_AXES; a += 1) namedJS[i].joystick[a].resetAll(); + for (int b = 0; b < MAX_JS_BUTTONS; b += 1) namedJS[i].jsButton[b].resetAll(); } + for (int i = 0; i < MAX_COMPONENTS; i++) { component[i].resetAll(); + for (int j = 0; j < MAX_COMPONENT_ASSETS; j++) { component[i].asset[j].resetAll(); } } + firstUse = true; } void AppData::storeAllSettings() { storeAll(); - for (int i = 0; i < MAX_PROFILES; i++) + + for (int i = 0; i < MAX_PROFILES; i++) { profile[i].storeAll(); + + for (int j = 0; j < MAX_COMPONENTS; j++) { + profile[i].compRelease[j].storeAll(); + } + } + for (int i = 0; i < MAX_JS_AXES; i++) joystick[i].storeAll(); + for (int i = 0; i < MAX_JS_BUTTONS; i++) jsButton[i].storeAll(); + for (int i = 0; i < MAX_NAMED_JOYSTICKS; i++) { namedJS[i].storeAll(); + for (int a = 0; a < MAX_JS_AXES; a += 1) namedJS[i].joystick[a].storeAll(); + for (int b = 0; b < MAX_JS_BUTTONS; b += 1) namedJS[i].jsButton[b].storeAll(); } + for (int i = 0; i < MAX_COMPONENTS; i++) { component[i].storeAll(); + for (int j = 0; j < MAX_COMPONENT_ASSETS; j++) component[i].asset[j].storeAll(); } @@ -829,8 +896,10 @@ bool AppData::exportSettings(QSettings * toSettings, bool clearDestination) 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. @@ -853,7 +922,9 @@ bool AppData::exportSettingsToFile(const QString & expFile, QString & resultMsg) 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 @@ -865,6 +936,7 @@ ComponentData & AppData::getComponent(int index) { if (index > -1 && index < MAX_COMPONENTS) return component[index]; + return component[0]; } @@ -872,6 +944,7 @@ const ComponentData & AppData::getComponent(int index) const { if (index > -1 && index < MAX_COMPONENTS) return component[index]; + return component[0]; } @@ -888,6 +961,7 @@ void AppData::resetUpdatesSettings() for (int i = 0; i < MAX_COMPONENTS; i++) { component[i].resetAll(); + for (int j = 0; j < MAX_COMPONENT_ASSETS; j++) component[i].asset[j].resetAll(); } diff --git a/companion/src/storage/appdata.h b/companion/src/storage/appdata.h index 3dcb7bb18f6..f0c556018b9 100644 --- a/companion/src/storage/appdata.h +++ b/companion/src/storage/appdata.h @@ -459,6 +459,33 @@ class NamedJSData: public CompStoreObj int index; }; +//! \brief ComponentReleaseData class stores release properties related to each updateable component. +class ComponentReleaseData: public CompStoreObj +{ + Q_OBJECT + public: + ComponentReleaseData & operator=(const ComponentReleaseData & rhs); + + protected: + explicit ComponentReleaseData(); + void setProfileIndex(int idx) { profileIndex = idx; } + void setIndex(int idx) { index = idx; } + void setIndexes(int profileIdx, int idx) { profileIndex = profileIdx; index = idx; } + inline QString propertyGroup() const override { return QString("Profiles/profile%1").arg(profileIndex); } + inline QString settingsPath() const override { return QString("%1/component%2/").arg(propertyGroup()).arg(index); } + friend class Profile; + friend class AppData; + + private: + PROPERTYSTRD( date, "") + PROPERTY (bool, prerelease, false) + PROPERTYSTRD( release, "unknown") + PROPERTY (int, releaseId, 0) + PROPERTYSTRD( version, "0") + + int profileIndex; + int index; +}; //! \brief Profile class stores properties related to each Radio Profile. //! \todo TODO: Remove or refactor stored radio settings system (#4583) @@ -468,6 +495,10 @@ class Profile: public CompStoreObj public: Profile & operator=(const Profile & rhs); QString getVariantFromType() const { return fwType().section("-", 1, 1); } + ComponentReleaseData & getCompRelease(int index); + const ComponentReleaseData & getCompRelease(int index) const; + + ComponentReleaseData compRelease[MAX_COMPONENTS]; public slots: bool existsOnDisk(); @@ -475,6 +506,7 @@ class Profile: public CompStoreObj protected: explicit Profile(); explicit Profile(const Profile & rhs); + void setIndex(int idx) { index = idx; } inline QString propertyGroup() const override { return QStringLiteral("Profiles"); } inline QString settingsPath() const override { return QString("%1/profile%2/").arg(propertyGroup()).arg(index); } @@ -566,7 +598,6 @@ class ComponentData: public CompStoreObj }; Q_ENUM(ReleaseChannel) - void releaseClear(); static QStringList releaseChannelsList() { return { tr("Releases"), tr("Pre-release"), tr("Nightly") } ; } inline ReleaseChannel boundedReleaseChannel() const { @@ -591,11 +622,6 @@ class ComponentData: public CompStoreObj private: PROPERTY (bool, checkForUpdate, false) PROPERTY (ReleaseChannel, releaseChannel, RELEASE_CHANNEL_STABLE) - PROPERTYSTRD( release, "unknown") - PROPERTY (int, releaseId, 0) - PROPERTY (bool, prerelease, false) - PROPERTYSTRD( version, "0") - PROPERTYSTRD( date, "") int index; diff --git a/companion/src/updates/updateinterface.cpp b/companion/src/updates/updateinterface.cpp index 91466fd53cf..c347aa1d458 100644 --- a/companion/src/updates/updateinterface.cpp +++ b/companion/src/updates/updateinterface.cpp @@ -650,7 +650,7 @@ const bool UpdateInterface::isReleaseLatest() // nightlies often have the same version so also check id if (isVersionLatest(versionCurrent(), m_repo->releases()->version()) && - g.component[m_id].releaseId() == m_repo->releases()->id()) + g.currentProfile().getCompRelease(m_id).releaseId() == m_repo->releases()->id()) return true; return false; @@ -788,13 +788,13 @@ void UpdateInterface::radioProfileChanged() void UpdateInterface::releaseClear() { - g.component[m_id].releaseClear(); + g.currentProfile().getCompRelease(m_id).resetAll(); releaseCurrent(); } const QString UpdateInterface::releaseCurrent() { - m_params->releaseCurrent = g.component[m_id].release(); + m_params->releaseCurrent = g.currentProfile().getCompRelease(m_id).release(); return m_params->releaseCurrent; } @@ -809,10 +809,10 @@ const QStringList UpdateInterface::releaseList() int UpdateInterface::releaseSettingsSave() { m_status->reportProgress(tr("Save release settings"), QtDebugMsg); - g.component[m_id].release(m_repo->releases()->name()); - g.component[m_id].version(m_repo->releases()->version()); - g.component[m_id].releaseId(m_repo->releases()->id()); - g.component[m_id].date(m_repo->releases()->date()); + g.currentProfile().getCompRelease(m_id).release(m_repo->releases()->name()); + g.currentProfile().getCompRelease(m_id).version(m_repo->releases()->version()); + g.currentProfile().getCompRelease(m_id).releaseId(m_repo->releases()->id()); + g.currentProfile().getCompRelease(m_id).date(m_repo->releases()->date()); return true; } @@ -1277,7 +1277,7 @@ bool UpdateInterface::validateFolder(QString & fldr) const QString UpdateInterface::versionCurrent() { - return g.component[m_id].version(); + return g.currentProfile().getCompRelease(m_id).version(); } // static From 6af4b018b758287efb2388d6ebc4045d66900775 Mon Sep 17 00:00:00 2001 From: elecpower Date: Tue, 12 May 2026 06:56:47 +1000 Subject: [PATCH 02/17] fix bumped settings verison revision --- companion/src/storage/appdata.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/companion/src/storage/appdata.h b/companion/src/storage/appdata.h index f0c556018b9..4e840b89463 100644 --- a/companion/src/storage/appdata.h +++ b/companion/src/storage/appdata.h @@ -41,7 +41,7 @@ //! CPN_SETTINGS_REVISION is used to track settings changes independently of EdgeTX version. It should be reset to zero whenever settings are migrated to new COMPANY or PRODUCT. //! \note !! Increment this value if properties are removed or refactored. It will trigger a conversion/cleanup of any stored settings. \sa AppData::convertSettings() -#define CPN_SETTINGS_REVISION 2 // Note: bumped for fix during 2.8 RCs +#define CPN_SETTINGS_REVISION 3 // Note: bumped for changes during 3.0 dev //! CPN_SETTINGS_VERSION is used for settings data version tracking. #define CPN_SETTINGS_VERSION ((VERSION_NUMBER << 8) | CPN_SETTINGS_REVISION) From f8c7afe04ca0b6e1fc4252a0a77408a9568eb4e5 Mon Sep 17 00:00:00 2001 From: elecpower Date: Wed, 13 May 2026 08:21:26 +1000 Subject: [PATCH 03/17] fix export nested settings --- companion/src/storage/appdata.cpp | 21 +++++++-------------- companion/src/storage/appdata.h | 31 ++++++++++++++++++------------- 2 files changed, 25 insertions(+), 27 deletions(-) diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index 4f36b13b0f3..54913502b9c 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -80,7 +80,7 @@ void CompStoreObj::removeGroupIfEmpty(const QString &group) const m_settings.endGroup(); } -//static void dumpMetadata(QObject *obj) { +// 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) { @@ -88,7 +88,7 @@ void CompStoreObj::removeGroupIfEmpty(const QString &group) const // 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 @@ -205,7 +205,8 @@ const QString CompStoreObj::propertyKeyName(CompStoreObj * obj, const QString & 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. + // grp is propertyGroup() thus why size minus 2 + const QString grp = list.size() > 1 ? list.mid(0, list.size() - 2).join("/") : QStringLiteral("General"); // "General" is the default (no-name) group, used by AppData top-level settings. return QPair(grp, list.last()); } @@ -249,7 +250,6 @@ const QHash > & CompStoreObj::keyToNameMap() JStickData::JStickData() : CompStoreObj(), index(-1) { - CompStoreObj::addObjectMapping(propertyGroup(), this); } bool JStickData::existsOnDisk() @@ -259,7 +259,6 @@ bool JStickData::existsOnDisk() JButtonData::JButtonData() : CompStoreObj(), index(-1) { - CompStoreObj::addObjectMapping(propertyGroup(), this); } bool JButtonData::existsOnDisk() @@ -269,7 +268,6 @@ bool JButtonData::existsOnDisk() NamedJStickData::NamedJStickData() : CompStoreObj(), index(-1) { - CompStoreObj::addObjectMapping(propertyGroup(), this); } bool NamedJStickData::existsOnDisk() @@ -279,7 +277,6 @@ bool NamedJStickData::existsOnDisk() NamedJButtonData::NamedJButtonData() : CompStoreObj(), index(-1) { - CompStoreObj::addObjectMapping(propertyGroup(), this); } bool NamedJButtonData::existsOnDisk() @@ -289,7 +286,6 @@ bool NamedJButtonData::existsOnDisk() NamedJSData::NamedJSData() : CompStoreObj(), index(-1) { - CompStoreObj::addObjectMapping(propertyGroup(), this); } bool NamedJSData::existsOnDisk() @@ -301,14 +297,12 @@ bool NamedJSData::existsOnDisk() ComponentReleaseData::ComponentReleaseData() : CompStoreObj(), index(-1) { - CompStoreObj::addObjectMapping(propertyGroup(), this); } // ** Profile class******************** Profile::Profile() : CompStoreObj(), index(-1) { - CompStoreObj::addObjectMapping(propertyGroup(), this); } Profile::Profile(const Profile & rhs) : CompStoreObj(), index(-1) @@ -355,7 +349,6 @@ const ComponentReleaseData & Profile::getCompRelease(int index) const ComponentAssetData::ComponentAssetData() : CompStoreObj(), index(-1) { - CompStoreObj::addObjectMapping(propertyGroup(), this); } // The default copy operator can not be used since the index variable would be destroyed @@ -382,7 +375,6 @@ bool ComponentAssetData::existsOnDisk() ComponentData::ComponentData() : CompStoreObj(), index(-1) { qRegisterMetaType("ComponentData::ReleaseChannel"); - CompStoreObj::addObjectMapping(propertyGroup(), this); } // The default copy operator can not be used since the index variable would be destroyed @@ -463,10 +455,10 @@ AppData::AppData() : namedJS[i].setIndex(i); for (int a = 0; a < MAX_JS_AXES; a += 1) - namedJS[i].joystick[a].setIndex(a, i); + namedJS[i].joystick[a].setIndexes(a, i); for (int b = 0; b < MAX_JS_BUTTONS; b += 1) - namedJS[i].jsButton[b].setIndex(b, i); + namedJS[i].jsButton[b].setIndexes(b, i); } // Configure the updates @@ -902,6 +894,7 @@ bool AppData::exportSettings(QSettings * toSettings, bool clearDestination) foreach (const QString & key, m_settings.allKeys()) { const QVariant newVal = m_settings.value(key); + //qDebug() << key << newVal << newVal.isValid() << CompStoreObj::propertyPathIsValidNonDefault(key, newVal); // Skip export if property does not exist or is the default value. if (newVal.isValid() && CompStoreObj::propertyPathIsValidNonDefault(key, newVal)) toSettings->setValue(key, newVal); diff --git a/companion/src/storage/appdata.h b/companion/src/storage/appdata.h index 4e840b89463..f6e9d36d4f8 100644 --- a/companion/src/storage/appdata.h +++ b/companion/src/storage/appdata.h @@ -345,7 +345,7 @@ class JStickData: public CompStoreObj protected: explicit JStickData(); - void setIndex(int idx) { index = idx; } + void setIndex(int idx) { index = idx; CompStoreObj::addObjectMapping(propertyGroup(), this);} inline QString propertyGroup() const override { return QStringLiteral("JsCalibration"); } inline QString settingsPath() const override { return QString("%1/%2/").arg(propertyGroup()).arg(index); } friend class AppData; @@ -375,7 +375,7 @@ class JButtonData: public CompStoreObj protected: explicit JButtonData(); - void setIndex(int idx) { index = idx; } + void setIndex(int idx) { index = idx; CompStoreObj::addObjectMapping(propertyGroup(), this);} inline QString propertyGroup() const override { return QStringLiteral("JsButton"); } inline QString settingsPath() const override { return QString("%1/%2/").arg(propertyGroup()).arg(index); } friend class AppData; @@ -396,7 +396,7 @@ class NamedJStickData: public CompStoreObj protected: explicit NamedJStickData(); - void setIndex(int idx, int nmIdx) { index = idx; namedIdx = nmIdx; } + void setIndexes(int idx, int nmIdx) { index = idx; namedIdx = nmIdx; CompStoreObj::addObjectMapping(propertyGroup(), this);} inline QString propertyGroup() const override { return QStringLiteral("NamedJSData/%1").arg(namedIdx); } inline QString settingsPath() const override { return QString("%1/JsCalibration/%2/").arg(propertyGroup()).arg(index); } friend class AppData; @@ -422,7 +422,7 @@ class NamedJButtonData: public CompStoreObj protected: explicit NamedJButtonData(); - void setIndex(int idx, int nmIdx) { index = idx; namedIdx = nmIdx; } + void setIndexes(int idx, int nmIdx) { index = idx; namedIdx = nmIdx; CompStoreObj::addObjectMapping(propertyGroup(), this);} inline QString propertyGroup() const override { return QStringLiteral("NamedJSData/%1").arg(namedIdx); } inline QString settingsPath() const override { return QString("%1/JsButton/%2/").arg(propertyGroup()).arg(index); } friend class AppData; @@ -443,7 +443,7 @@ class NamedJSData: public CompStoreObj protected: explicit NamedJSData(); - void setIndex(int idx) { index = idx; } + void setIndex(int idx) { index = idx; CompStoreObj::addObjectMapping(propertyGroup(), this);} inline QString propertyGroup() const override { return QStringLiteral("NamedJSData"); } inline QString settingsPath() const override { return QString("%1/%2/").arg(propertyGroup()).arg(index); } friend class AppData; @@ -468,9 +468,14 @@ class ComponentReleaseData: public CompStoreObj protected: explicit ComponentReleaseData(); - void setProfileIndex(int idx) { profileIndex = idx; } - void setIndex(int idx) { index = idx; } - void setIndexes(int profileIdx, int idx) { profileIndex = profileIdx; index = idx; } + //void setProfileIndex(int idx) { profileIndex = idx; } + //void setIndex(int idx) { index = idx; } + void setIndexes(int profileIdx, int idx) + { + profileIndex = profileIdx; + index = idx; + CompStoreObj::addObjectMapping(propertyGroup(), this); + } inline QString propertyGroup() const override { return QString("Profiles/profile%1").arg(profileIndex); } inline QString settingsPath() const override { return QString("%1/component%2/").arg(propertyGroup()).arg(index); } friend class Profile; @@ -507,7 +512,7 @@ class Profile: public CompStoreObj explicit Profile(); explicit Profile(const Profile & rhs); - void setIndex(int idx) { index = idx; } + void setIndex(int idx) { index = idx; CompStoreObj::addObjectMapping(propertyGroup(), this);} inline QString propertyGroup() const override { return QStringLiteral("Profiles"); } inline QString settingsPath() const override { return QString("%1/profile%2/").arg(propertyGroup()).arg(index); } friend class AppData; @@ -561,9 +566,9 @@ class ComponentAssetData: public CompStoreObj protected: explicit ComponentAssetData(); - void setCompIndex(int idx) { compIndex = idx; } - void setIndex(int idx) { index = idx; } - void setIndexes(int compIdx, int idx) { compIndex = compIdx; index = idx; } + //void setCompIndex(int idx) { compIndex = idx; } + //void setIndex(int idx) { index = idx; } + void setIndexes(int compIdx, int idx) { compIndex = compIdx; index = idx; CompStoreObj::addObjectMapping(propertyGroup(), this);} inline QString propertyGroup() const override { return QString("Components/component%1").arg(compIndex); } inline QString settingsPath() const override { return QString("%1/asset%2/").arg(propertyGroup()).arg(index); } friend class ComponentData; @@ -614,7 +619,7 @@ class ComponentData: public CompStoreObj protected: explicit ComponentData(); - void setIndex(int idx) { index = idx; } + void setIndex(int idx) { index = idx; CompStoreObj::addObjectMapping(propertyGroup(), this);} inline QString propertyGroup() const override { return QStringLiteral("Components"); } inline QString settingsPath() const override { return QString("%1/component%2/").arg(propertyGroup()).arg(index); } friend class AppData; From c5ba32208aa9ed6fed930537b3b0568b963ac5e3 Mon Sep 17 00:00:00 2001 From: elecpower Date: Wed, 13 May 2026 15:44:39 +1000 Subject: [PATCH 04/17] fix missing release defn assignment op --- companion/src/storage/appdata.cpp | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index 54913502b9c..e2cace9707a 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -299,6 +299,20 @@ ComponentReleaseData::ComponentReleaseData() : CompStoreObj(), index(-1) { } +// The default copy operator can not be used since the index variable would be destroyed +ComponentReleaseData & ComponentReleaseData::operator= (const ComponentReleaseData & 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; +} + // ** Profile class******************** Profile::Profile() : CompStoreObj(), index(-1) From 169bbec1a83a3151a191e7e2dde4ea5bb7bbfdbd Mon Sep 17 00:00:00 2001 From: elecpower Date: Wed, 13 May 2026 16:01:14 +1000 Subject: [PATCH 05/17] fix add resetting profile component release when resetting all update settings --- companion/src/storage/appdata.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index e2cace9707a..cded99656f7 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -972,4 +972,10 @@ void AppData::resetUpdatesSettings() for (int j = 0; j < MAX_COMPONENT_ASSETS; j++) component[i].asset[j].resetAll(); } + + for (int i = 0; i < MAX_PROFILES; i++) { + for (int j = 0; j < MAX_COMPONENT_ASSETS; j++) { + profile[i].compRelease[j].resetAll(); + } + } } From 729b3bd3500c0636f6333d6ea361ac6b7ddcd96a Mon Sep 17 00:00:00 2001 From: elecpower Date: Wed, 13 May 2026 16:15:22 +1000 Subject: [PATCH 06/17] fix copy profile to include component releases --- companion/src/storage/appdata.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index cded99656f7..1972cdabe05 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -335,6 +335,11 @@ Profile & Profile::operator= (const Profile & rhs) } prop.write(this, prop.read(&rhs)); } + + for (int i = 0; i < MAX_COMPONENTS; i++) { + compRelease[i] = rhs.compRelease[i]; + } + return *this; } From 80292c3ee9dbabedddc106dc8ff677c9d3c45af4 Mon Sep 17 00:00:00 2001 From: elecpower Date: Wed, 13 May 2026 16:39:05 +1000 Subject: [PATCH 07/17] fix use correct array size variable --- companion/src/storage/appdata.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index 1972cdabe05..cee28a4fa00 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -979,7 +979,7 @@ void AppData::resetUpdatesSettings() } for (int i = 0; i < MAX_PROFILES; i++) { - for (int j = 0; j < MAX_COMPONENT_ASSETS; j++) { + for (int j = 0; j < MAX_COMPONENTS; j++) { profile[i].compRelease[j].resetAll(); } } From be32f683911e86a43ad8925ffbdde9882400feac Mon Sep 17 00:00:00 2001 From: elecpower Date: Thu, 14 May 2026 07:43:35 +1000 Subject: [PATCH 08/17] fix joystick export --- companion/src/storage/appdata.cpp | 91 +++++++++++++++++-------------- companion/src/storage/appdata.h | 22 +++----- 2 files changed, 60 insertions(+), 53 deletions(-) diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index cee28a4fa00..50e0ebfdcfc 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -239,7 +239,7 @@ const QHash > & CompStoreObj::keyToNameMap() grpMap.insert(key, name); } map.insert(it.key(), grpMap); - //qDebug() << it.key() << grpMap; + qDebug() << it.key() << grpMap; } } return map; @@ -473,11 +473,11 @@ AppData::AppData() : for (int i = 0; i < MAX_NAMED_JOYSTICKS; i++) { namedJS[i].setIndex(i); - for (int a = 0; a < MAX_JS_AXES; a += 1) - namedJS[i].joystick[a].setIndexes(a, i); + for (int j = 0; j < MAX_JS_AXES; j++) + namedJS[i].joystick[j].setIndexes(j, i); - for (int b = 0; b < MAX_JS_BUTTONS; b += 1) - namedJS[i].jsButton[b].setIndexes(b, i); + for (int j = 0; j < MAX_JS_BUTTONS; j++) + namedJS[i].jsButton[j].setIndexes(j, i); } // Configure the updates @@ -493,29 +493,32 @@ AppData::AppData() : void AppData::saveNamedJS(int i) { namedJS[i].jsName(currentProfile().jsName()); - for (int a = 0; a < MAX_JS_AXES; a += 1) { - namedJS[i].joystick[a].stick_axe(joystick[a].stick_axe()); - namedJS[i].joystick[a].stick_max(joystick[a].stick_max()); - namedJS[i].joystick[a].stick_med(joystick[a].stick_med()); - namedJS[i].joystick[a].stick_min(joystick[a].stick_min()); - namedJS[i].joystick[a].stick_min(joystick[a].stick_min()); + + for (int j = 0; j < MAX_JS_AXES; j++) { + namedJS[i].joystick[j].stick_axe(joystick[j].stick_axe()); + namedJS[i].joystick[j].stick_max(joystick[j].stick_max()); + namedJS[i].joystick[j].stick_med(joystick[j].stick_med()); + namedJS[i].joystick[j].stick_min(joystick[j].stick_min()); + namedJS[i].joystick[j].stick_min(joystick[j].stick_min()); } - for (int b = 0; b < MAX_JS_BUTTONS; b += 1) { - namedJS[i].jsButton[b].button_idx(jsButton[b].button_idx()); + + for (int j = 0; j < MAX_JS_BUTTONS; j++) { + namedJS[i].jsButton[j].button_idx(jsButton[j].button_idx()); } + namedJS[i].jsLastUsed(time(NULL)); } void AppData::saveNamedJS() { - for (int i = 0; i < MAX_NAMED_JOYSTICKS; i += 1) { + for (int i = 0; i < MAX_NAMED_JOYSTICKS; i++) { if (namedJS[i].jsName() == currentProfile().jsName()) { saveNamedJS(i); return; } } - for (int i = 0; i < MAX_NAMED_JOYSTICKS; i += 1) { + for (int i = 0; i < MAX_NAMED_JOYSTICKS; i++) { if (namedJS[i].jsName() == "") { saveNamedJS(i); return; @@ -524,7 +527,7 @@ void AppData::saveNamedJS() unsigned int oldestTime = namedJS[0].jsLastUsed(); int oldestN = 0; - for (int i = 1; i < MAX_NAMED_JOYSTICKS; i += 1) { + for (int i = 1; i < MAX_NAMED_JOYSTICKS; i++) { if (namedJS[i].jsLastUsed() < oldestTime) { oldestTime = namedJS[i].jsLastUsed(); oldestN = i; @@ -535,22 +538,24 @@ void AppData::saveNamedJS() void AppData::loadNamedJS(int i) { - for (int a = 0; a < MAX_JS_AXES; a += 1) { - joystick[a].stick_axe(namedJS[i].joystick[a].stick_axe()); - joystick[a].stick_max(namedJS[i].joystick[a].stick_max()); - joystick[a].stick_med(namedJS[i].joystick[a].stick_med()); - joystick[a].stick_min(namedJS[i].joystick[a].stick_min()); - joystick[a].stick_min(namedJS[i].joystick[a].stick_min()); + for (int j = 0; j < MAX_JS_AXES; j++) { + joystick[j].stick_axe(namedJS[i].joystick[j].stick_axe()); + joystick[j].stick_max(namedJS[i].joystick[j].stick_max()); + joystick[j].stick_med(namedJS[i].joystick[j].stick_med()); + joystick[j].stick_min(namedJS[i].joystick[j].stick_min()); + joystick[j].stick_min(namedJS[i].joystick[j].stick_min()); } - for (int b = 0; b < MAX_JS_BUTTONS; b += 1) { - jsButton[b].button_idx(namedJS[i].jsButton[b].button_idx()); + + for (int j = 0; j < MAX_JS_BUTTONS; j++) { + jsButton[j].button_idx(namedJS[i].jsButton[j].button_idx()); } + namedJS[i].jsLastUsed(time(NULL)); } void AppData::loadNamedJS() { - for (int i = 0; i < MAX_NAMED_JOYSTICKS; i += 1) { + for (int i = 0; i < MAX_NAMED_JOYSTICKS; i++) { if (namedJS[i].jsName() == currentProfile().jsName()) { loadNamedJS(i); return; @@ -600,11 +605,11 @@ void AppData::initAll() for (int i = 0; i < MAX_NAMED_JOYSTICKS; i++) { namedJS[i].init(); - for (int a = 0; a < MAX_JS_AXES; a += 1) - namedJS[i].joystick[a].init(); + for (int j = 0; j < MAX_JS_AXES; j++) + namedJS[i].joystick[j].init(); - for (int b = 0; b < MAX_JS_BUTTONS; b += 1) - namedJS[i].jsButton[b].init(); + for (int j = 0; j < MAX_JS_BUTTONS; j++) + namedJS[i].jsButton[j].init(); } // Initialize the updates @@ -639,11 +644,11 @@ void AppData::resetAllSettings() for (int i = 0; i < MAX_NAMED_JOYSTICKS; i++) { namedJS[i].resetAll(); - for (int a = 0; a < MAX_JS_AXES; a += 1) - namedJS[i].joystick[a].resetAll(); + for (int j = 0; j < MAX_JS_AXES; j++) + namedJS[i].joystick[j].resetAll(); - for (int b = 0; b < MAX_JS_BUTTONS; b += 1) - namedJS[i].jsButton[b].resetAll(); + for (int j = 0; j < MAX_JS_BUTTONS; j++) + namedJS[i].jsButton[j].resetAll(); } for (int i = 0; i < MAX_COMPONENTS; i++) { @@ -678,11 +683,11 @@ void AppData::storeAllSettings() for (int i = 0; i < MAX_NAMED_JOYSTICKS; i++) { namedJS[i].storeAll(); - for (int a = 0; a < MAX_JS_AXES; a += 1) - namedJS[i].joystick[a].storeAll(); + for (int j = 0; j < MAX_JS_AXES; j++) + namedJS[i].joystick[j].storeAll(); - for (int b = 0; b < MAX_JS_BUTTONS; b += 1) - namedJS[i].jsButton[b].storeAll(); + for (int j = 0; j < MAX_JS_BUTTONS; j++) + namedJS[i].jsButton[j].storeAll(); } for (int i = 0; i < MAX_COMPONENTS; i++) { @@ -725,10 +730,12 @@ const Profile & AppData::getProfile(int index) const QMap AppData::getActiveProfiles() const { QMap active; + for (int i=0; i 0) { Profile tmpProfile(g.profile[m_sessionId]); - for (int i = m_sessionId; i > 0; i -= 1) { + + for (int i = m_sessionId; i > 0; i--) { g.profile[i] = g.profile[i - 1]; } + g.profile[0] = tmpProfile; id(0); } @@ -747,8 +756,10 @@ void AppData::moveCurrentProfileToTop() void AppData::convertSettings(QSettings & settings) { quint32 savedVer = settings.value(SETTINGS_VERSION_KEY, 0).toUInt(); + if (savedVer == CPN_SETTINGS_VERSION) return; + if (savedVer > CPN_SETTINGS_VERSION) { qWarning().noquote() << "Saved settings version is newer than current, skipping conversions. Saved:" << fmtHex(savedVer) << "Current:" << fmtHex(CPN_SETTINGS_VERSION); return; @@ -913,11 +924,11 @@ bool AppData::exportSettings(QSettings * toSettings, bool clearDestination) foreach (const QString & key, m_settings.allKeys()) { const QVariant newVal = m_settings.value(key); - //qDebug() << key << newVal << newVal.isValid() << CompStoreObj::propertyPathIsValidNonDefault(key, newVal); + qDebug() << key << newVal; // 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; + 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)); diff --git a/companion/src/storage/appdata.h b/companion/src/storage/appdata.h index f6e9d36d4f8..fbc16c829c7 100644 --- a/companion/src/storage/appdata.h +++ b/companion/src/storage/appdata.h @@ -346,8 +346,8 @@ class JStickData: public CompStoreObj protected: explicit JStickData(); void setIndex(int idx) { index = idx; CompStoreObj::addObjectMapping(propertyGroup(), this);} - inline QString propertyGroup() const override { return QStringLiteral("JsCalibration"); } - inline QString settingsPath() const override { return QString("%1/%2/").arg(propertyGroup()).arg(index); } + inline QString propertyGroup() const override { return QStringLiteral("Joysticks"); } + inline QString settingsPath() const override { return QString("%1/joystick%2/").arg(propertyGroup()).arg(index); } friend class AppData; friend class NamedJSData; @@ -376,8 +376,8 @@ class JButtonData: public CompStoreObj protected: explicit JButtonData(); void setIndex(int idx) { index = idx; CompStoreObj::addObjectMapping(propertyGroup(), this);} - inline QString propertyGroup() const override { return QStringLiteral("JsButton"); } - inline QString settingsPath() const override { return QString("%1/%2/").arg(propertyGroup()).arg(index); } + inline QString propertyGroup() const override { return QStringLiteral("JSButtons"); } + inline QString settingsPath() const override { return QString("%1/button%2/").arg(propertyGroup()).arg(index); } friend class AppData; friend class NamedJSData; @@ -397,8 +397,8 @@ class NamedJStickData: public CompStoreObj protected: explicit NamedJStickData(); void setIndexes(int idx, int nmIdx) { index = idx; namedIdx = nmIdx; CompStoreObj::addObjectMapping(propertyGroup(), this);} - inline QString propertyGroup() const override { return QStringLiteral("NamedJSData/%1").arg(namedIdx); } - inline QString settingsPath() const override { return QString("%1/JsCalibration/%2/").arg(propertyGroup()).arg(index); } + inline QString propertyGroup() const override { return QStringLiteral("NamedJSData/name%1").arg(namedIdx); } + inline QString settingsPath() const override { return QString("%1/joystick%2/").arg(propertyGroup()).arg(index); } friend class AppData; friend class NamedJSData; @@ -423,8 +423,8 @@ class NamedJButtonData: public CompStoreObj protected: explicit NamedJButtonData(); void setIndexes(int idx, int nmIdx) { index = idx; namedIdx = nmIdx; CompStoreObj::addObjectMapping(propertyGroup(), this);} - inline QString propertyGroup() const override { return QStringLiteral("NamedJSData/%1").arg(namedIdx); } - inline QString settingsPath() const override { return QString("%1/JsButton/%2/").arg(propertyGroup()).arg(index); } + inline QString propertyGroup() const override { return QStringLiteral("NamedJSData/name%1").arg(namedIdx); } + inline QString settingsPath() const override { return QString("%1/button%2/").arg(propertyGroup()).arg(index); } friend class AppData; friend class NamedJSData; @@ -445,7 +445,7 @@ class NamedJSData: public CompStoreObj explicit NamedJSData(); void setIndex(int idx) { index = idx; CompStoreObj::addObjectMapping(propertyGroup(), this);} inline QString propertyGroup() const override { return QStringLiteral("NamedJSData"); } - inline QString settingsPath() const override { return QString("%1/%2/").arg(propertyGroup()).arg(index); } + inline QString settingsPath() const override { return QString("%1/name%2/").arg(propertyGroup()).arg(index); } friend class AppData; public: @@ -468,8 +468,6 @@ class ComponentReleaseData: public CompStoreObj protected: explicit ComponentReleaseData(); - //void setProfileIndex(int idx) { profileIndex = idx; } - //void setIndex(int idx) { index = idx; } void setIndexes(int profileIdx, int idx) { profileIndex = profileIdx; @@ -566,8 +564,6 @@ class ComponentAssetData: public CompStoreObj protected: explicit ComponentAssetData(); - //void setCompIndex(int idx) { compIndex = idx; } - //void setIndex(int idx) { index = idx; } void setIndexes(int compIdx, int idx) { compIndex = compIdx; index = idx; CompStoreObj::addObjectMapping(propertyGroup(), this);} inline QString propertyGroup() const override { return QString("Components/component%1").arg(compIndex); } inline QString settingsPath() const override { return QString("%1/asset%2/").arg(propertyGroup()).arg(index); } From ee914fa5a088387bbd39d6e77238e4f2a2e77beb Mon Sep 17 00:00:00 2001 From: elecpower Date: Thu, 14 May 2026 07:51:25 +1000 Subject: [PATCH 09/17] fix js inv not saved or loaded --- companion/src/storage/appdata.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index 50e0ebfdcfc..616a680cd7c 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -499,7 +499,7 @@ void AppData::saveNamedJS(int i) namedJS[i].joystick[j].stick_max(joystick[j].stick_max()); namedJS[i].joystick[j].stick_med(joystick[j].stick_med()); namedJS[i].joystick[j].stick_min(joystick[j].stick_min()); - namedJS[i].joystick[j].stick_min(joystick[j].stick_min()); + namedJS[i].joystick[j].stick_inv(joystick[j].stick_inv()); } for (int j = 0; j < MAX_JS_BUTTONS; j++) { @@ -543,7 +543,7 @@ void AppData::loadNamedJS(int i) joystick[j].stick_max(namedJS[i].joystick[j].stick_max()); joystick[j].stick_med(namedJS[i].joystick[j].stick_med()); joystick[j].stick_min(namedJS[i].joystick[j].stick_min()); - joystick[j].stick_min(namedJS[i].joystick[j].stick_min()); + joystick[j].stick_inv(namedJS[i].joystick[j].stick_inv()); } for (int j = 0; j < MAX_JS_BUTTONS; j++) { From c49b1c226b6d0c5f46796d57adaa2959d19597e2 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Wed, 13 May 2026 23:32:28 +0000 Subject: [PATCH 10/17] =?UTF-8?q?=F0=9F=93=9D=20CodeRabbit=20Chat:=20Add?= =?UTF-8?q?=20generated=20unit=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- companion/src/tests/appdata_pr.cpp | 529 +++++++++++++++++++++++++++++ 1 file changed, 529 insertions(+) create mode 100644 companion/src/tests/appdata_pr.cpp diff --git a/companion/src/tests/appdata_pr.cpp b/companion/src/tests/appdata_pr.cpp new file mode 100644 index 00000000000..422730444a0 --- /dev/null +++ b/companion/src/tests/appdata_pr.cpp @@ -0,0 +1,529 @@ +/* + * Copyright (C) EdgeTX + * + * Based on code named + * opentx - https://github.com/opentx/opentx + * 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. + */ + +/** + * Tests for changes introduced in the PR that: + * - Added ComponentReleaseData (per-profile release info) and moved + * release/version/releaseId/date/prerelease out of ComponentData. + * - Fixed CompStoreObj::splitGroupedPath() to handle deeply-nested paths. + * - Moved addObjectMapping() calls from constructors to setIndex/setIndexes. + * - Added Profile::getCompRelease() with bounds checking. + * - Bumped CPN_SETTINGS_REVISION from 2 to 3. + * - Renamed joystick settings-path prefixes. + */ + +#include "gtests.h" +#include "storage/appdata.h" + +// --------------------------------------------------------------------------- +// Helper: expose the protected CompStoreObj::splitGroupedPath() for testing. +// --------------------------------------------------------------------------- +class SplitPathHelper : public CompStoreObj +{ +public: + QString propertyGroup() const override { return QStringLiteral("Test"); } + + static QPair split(const QString & path) + { + return CompStoreObj::splitGroupedPath(path); + } +}; + +// =========================================================================== +// CPN_SETTINGS_REVISION +// =========================================================================== + +TEST(AppDataPR, SettingsRevisionBumpedTo3) +{ + // The PR bumps the revision from 2 to 3 to signal settings migration. + EXPECT_EQ(3, CPN_SETTINGS_REVISION); +} + +// =========================================================================== +// CompStoreObj::splitGroupedPath() +// +// The function was changed to use list.mid(0, list.size() - 2).join("/") +// instead of list.first() so that deeply-nested property groups (more than +// two path components) are correctly identified. +// +// Contract: for a stored key of the form "", the +// function must return (propertyGroup(), propKey). +// settingsPath() always ends with '/', so the stored path is +// settingsPath() % propKey (no extra slash). +// =========================================================================== + +// Single element: no '/' present → group = "General". +TEST(AppDataPR, SplitPath_SingleKey) +{ + auto result = SplitPathHelper::split("someKey"); + EXPECT_EQ(QStringLiteral("General"), result.first); + EXPECT_EQ(QStringLiteral("someKey"), result.second); +} + +// Three-segment path: "Profiles/profile0/Name" +// list = ["Profiles","profile0","Name"], size=3 +// mid(0, 3-2) = mid(0,1) = ["Profiles"] → "Profiles" +// This matches Profile::propertyGroup() = "Profiles". +TEST(AppDataPR, SplitPath_ThreeSegments_ProfileProperty) +{ + auto result = SplitPathHelper::split("Profiles/profile0/Name"); + EXPECT_EQ(QStringLiteral("Profiles"), result.first); + EXPECT_EQ(QStringLiteral("Name"), result.second); +} + +// Three-segment path: "Components/component0/checkForUpdate" +// list = ["Components","component0","checkForUpdate"], size=3 +// mid(0,1) = ["Components"] → "Components" +// Matches ComponentData::propertyGroup() = "Components". +TEST(AppDataPR, SplitPath_ThreeSegments_ComponentProperty) +{ + auto result = SplitPathHelper::split("Components/component0/checkForUpdate"); + EXPECT_EQ(QStringLiteral("Components"), result.first); + EXPECT_EQ(QStringLiteral("checkForUpdate"), result.second); +} + +// Four-segment path: "Profiles/profile0/component0/release" +// list = ["Profiles","profile0","component0","release"], size=4 +// mid(0, 4-2) = mid(0,2) = ["Profiles","profile0"] → "Profiles/profile0" +// This matches ComponentReleaseData::propertyGroup() = "Profiles/profile{idx}". +TEST(AppDataPR, SplitPath_FourSegments_ComponentReleaseProperty) +{ + auto result = SplitPathHelper::split("Profiles/profile0/component0/release"); + EXPECT_EQ(QStringLiteral("Profiles/profile0"), result.first); + EXPECT_EQ(QStringLiteral("release"), result.second); +} + +// Four-segment path: "Components/component0/asset0/desc" +// list = ["Components","component0","asset0","desc"], size=4 +// mid(0,2) = ["Components","component0"] → "Components/component0" +// Matches ComponentAssetData::propertyGroup() = "Components/component{idx}". +TEST(AppDataPR, SplitPath_FourSegments_ComponentAssetProperty) +{ + auto result = SplitPathHelper::split("Components/component0/asset0/desc"); + EXPECT_EQ(QStringLiteral("Components/component0"), result.first); + EXPECT_EQ(QStringLiteral("desc"), result.second); +} + +// Five-segment path: deeply nested NamedJSData joystick property. +// "NamedJSData/name0/joystick1/stick_axe" +// list = ["NamedJSData","name0","joystick1","stick_axe"], size=4 +// mid(0,2) = ["NamedJSData","name0"] → "NamedJSData/name0" +// Matches NamedJStickData::propertyGroup() = "NamedJSData/name{idx}". +TEST(AppDataPR, SplitPath_FourSegments_NamedJStickProperty) +{ + auto result = SplitPathHelper::split("NamedJSData/name0/joystick1/stick_axe"); + EXPECT_EQ(QStringLiteral("NamedJSData/name0"), result.first); + EXPECT_EQ(QStringLiteral("stick_axe"), result.second); +} + +// Regression: with the OLD code (list.first()), a four-segment path would +// have returned only "Profiles" for "Profiles/profile0/component0/release", +// which is wrong. Verify the new behaviour is distinct from the old. +TEST(AppDataPR, SplitPath_FourSegments_NotJustFirstSegment) +{ + auto result = SplitPathHelper::split("Profiles/profile0/component0/release"); + // The old (incorrect) code would have returned "Profiles" here. + EXPECT_NE(QStringLiteral("Profiles"), result.first); + // The correct new code returns the full two-segment group. + EXPECT_EQ(QStringLiteral("Profiles/profile0"), result.first); +} + +// =========================================================================== +// ComponentReleaseData – declared default property values +// +// The static _default() methods are generated by the PROPERTY_META4 macro +// and are publicly accessible. +// =========================================================================== + +TEST(AppDataPR, ComponentReleaseData_DefaultDate) +{ + EXPECT_EQ(QStringLiteral(""), ComponentReleaseData::date_default().toString()); +} + +TEST(AppDataPR, ComponentReleaseData_DefaultPrerelease) +{ + EXPECT_EQ(false, ComponentReleaseData::prerelease_default().toBool()); +} + +TEST(AppDataPR, ComponentReleaseData_DefaultRelease) +{ + EXPECT_EQ(QStringLiteral("unknown"), ComponentReleaseData::release_default().toString()); +} + +TEST(AppDataPR, ComponentReleaseData_DefaultReleaseId) +{ + EXPECT_EQ(0, ComponentReleaseData::releaseId_default().toInt()); +} + +TEST(AppDataPR, ComponentReleaseData_DefaultVersion) +{ + EXPECT_EQ(QStringLiteral("0"), ComponentReleaseData::version_default().toString()); +} + +// =========================================================================== +// ComponentReleaseData::propertyGroup() / settingsPath() +// +// The group must follow "Profiles/profile{profileIndex}" so that +// splitGroupedPath() can correctly reverse it. +// We verify the pattern using the objects initialised by the global AppData. +// =========================================================================== + +TEST(AppDataPR, ComponentReleaseData_PropertyGroupPattern) +{ + // After AppData construction, profile[0].compRelease[0] has been set up + // with setIndexes(0, 0). Its propertyGroup() must be "Profiles/profile0". + // We can't call protected propertyGroup() directly, but we can verify the + // round-trip through splitGroupedPath: a path built as + // "" + "" must split back to (propertyGroup, key). + // + // settingsPath = "Profiles/profile0/component0/" + // key = "release" + // full path = "Profiles/profile0/component0/release" + // expected grp = "Profiles/profile0" (= propertyGroup of compRelease[0]) + const QString fullPath = QStringLiteral("Profiles/profile0/component0/release"); + auto result = SplitPathHelper::split(fullPath); + EXPECT_EQ(QStringLiteral("Profiles/profile0"), result.first); + EXPECT_EQ(QStringLiteral("release"), result.second); +} + +TEST(AppDataPR, ComponentReleaseData_PropertyGroupPatternProfile5Component3) +{ + // Verify for a non-zero profile and component index. + // settingsPath = "Profiles/profile5/component3/" + // key = "version" + const QString fullPath = QStringLiteral("Profiles/profile5/component3/version"); + auto result = SplitPathHelper::split(fullPath); + EXPECT_EQ(QStringLiteral("Profiles/profile5"), result.first); + EXPECT_EQ(QStringLiteral("version"), result.second); +} + +// =========================================================================== +// Profile::getCompRelease() – bounds checking +// +// Valid indices [0, MAX_COMPONENTS-1] must return a reference to the +// corresponding compRelease element. Out-of-range indices must clamp to +// compRelease[0]. +// =========================================================================== + +TEST(AppDataPR, GetCompRelease_ValidIndex_Zero) +{ + Profile & p = g.profile[0]; + EXPECT_EQ(&p.compRelease[0], &p.getCompRelease(0)); +} + +TEST(AppDataPR, GetCompRelease_ValidIndex_One) +{ + Profile & p = g.profile[0]; + EXPECT_EQ(&p.compRelease[1], &p.getCompRelease(1)); +} + +TEST(AppDataPR, GetCompRelease_ValidIndex_MaxMinus1) +{ + Profile & p = g.profile[0]; + const int lastIdx = MAX_COMPONENTS - 1; + EXPECT_EQ(&p.compRelease[lastIdx], &p.getCompRelease(lastIdx)); +} + +TEST(AppDataPR, GetCompRelease_OutOfRange_NegativeOne) +{ + Profile & p = g.profile[0]; + // index == -1 → must return compRelease[0] + EXPECT_EQ(&p.compRelease[0], &p.getCompRelease(-1)); +} + +TEST(AppDataPR, GetCompRelease_OutOfRange_MaxComponents) +{ + Profile & p = g.profile[0]; + // index == MAX_COMPONENTS → must return compRelease[0] + EXPECT_EQ(&p.compRelease[0], &p.getCompRelease(MAX_COMPONENTS)); +} + +TEST(AppDataPR, GetCompRelease_OutOfRange_LargePositive) +{ + Profile & p = g.profile[0]; + EXPECT_EQ(&p.compRelease[0], &p.getCompRelease(9999)); +} + +// Const variant – same bounds logic applies. +TEST(AppDataPR, GetCompRelease_Const_ValidIndex) +{ + const Profile & p = g.profile[0]; + EXPECT_EQ(&p.compRelease[0], &p.getCompRelease(0)); + EXPECT_EQ(&p.compRelease[MAX_COMPONENTS - 1], &p.getCompRelease(MAX_COMPONENTS - 1)); +} + +TEST(AppDataPR, GetCompRelease_Const_OutOfRange) +{ + const Profile & p = g.profile[0]; + EXPECT_EQ(&p.compRelease[0], &p.getCompRelease(-1)); + EXPECT_EQ(&p.compRelease[0], &p.getCompRelease(MAX_COMPONENTS)); +} + +// =========================================================================== +// ComponentReleaseData::operator= +// +// The custom assignment must copy all declared properties but must NOT copy +// the index fields (profileIndex, index), ensuring the destination object +// retains its own identity. +// =========================================================================== + +TEST(AppDataPR, ComponentReleaseData_AssignmentCopiesProperties) +{ + // Use two compRelease slots from different profiles so they have distinct + // indexes and are properly initialised. + ComponentReleaseData & src = g.profile[1].compRelease[2]; + ComponentReleaseData & dst = g.profile[2].compRelease[3]; + + // Set non-default values on the source. + src.release(QStringLiteral("v3.0.0"), false); + src.version(QStringLiteral("3.0.0"), false); + src.releaseId(42, false); + src.date(QStringLiteral("2025-01-01"), false); + src.prerelease(true, false); + + // Assign. + dst = src; + + // All property values must match. + EXPECT_EQ(QStringLiteral("v3.0.0"), dst.release()); + EXPECT_EQ(QStringLiteral("3.0.0"), dst.version()); + EXPECT_EQ(42, dst.releaseId()); + EXPECT_EQ(QStringLiteral("2025-01-01"), dst.date()); + EXPECT_EQ(true, dst.prerelease()); + + // Clean up: reset source back to defaults (no-store). + src.releaseReset(false); + src.versionReset(false); + src.releaseIdReset(false); + src.dateReset(false); + src.prereleaseReset(false); +} + +TEST(AppDataPR, ComponentReleaseData_AssignmentPreservesDestinationIdentity) +{ + // After assignment, the destination object must still be the same object + // (i.e. at the same address). The index must not change. + ComponentReleaseData & src = g.profile[3].compRelease[0]; + ComponentReleaseData & dst = g.profile[4].compRelease[5]; + + ComponentReleaseData * dstAddr = &dst; + dst = src; + + // Object identity preserved. + EXPECT_EQ(dstAddr, &dst); + // The address within Profile is still the same slot. + EXPECT_EQ(&g.profile[4].compRelease[5], &dst); +} + +TEST(AppDataPR, ComponentReleaseData_AssignmentFromDefault) +{ + // Assign from a source with default values; destination should also reflect + // those defaults afterwards. + ComponentReleaseData & src = g.profile[5].compRelease[1]; + ComponentReleaseData & dst = g.profile[6].compRelease[1]; + + // Set dst to non-default values first. + dst.release(QStringLiteral("dirty"), false); + dst.version(QStringLiteral("9.9"), false); + dst.releaseId(99, false); + + // Reset src to defaults. + src.releaseReset(false); + src.versionReset(false); + src.releaseIdReset(false); + src.dateReset(false); + src.prereleaseReset(false); + + dst = src; + + EXPECT_EQ(QStringLiteral("unknown"), dst.release()); + EXPECT_EQ(QStringLiteral("0"), dst.version()); + EXPECT_EQ(0, dst.releaseId()); +} + +// =========================================================================== +// Profile::operator= copies compRelease array +// +// When a Profile is copy-assigned, all MAX_COMPONENTS compRelease entries +// must be copied from the source profile. +// =========================================================================== + +TEST(AppDataPR, Profile_AssignmentCopiesCompReleaseArray) +{ + Profile & src = g.profile[7]; + Profile & dst = g.profile[8]; + + // Write distinct values into every compRelease slot of the source. + for (int j = 0; j < MAX_COMPONENTS; j++) { + src.compRelease[j].release(QStringLiteral("src-release-%1").arg(j), false); + src.compRelease[j].version(QStringLiteral("1.%1").arg(j), false); + } + + dst = src; + + for (int j = 0; j < MAX_COMPONENTS; j++) { + EXPECT_EQ(QStringLiteral("src-release-%1").arg(j), dst.compRelease[j].release()) + << "compRelease[" << j << "].release() mismatch"; + EXPECT_EQ(QStringLiteral("1.%1").arg(j), dst.compRelease[j].version()) + << "compRelease[" << j << "].version() mismatch"; + } + + // Clean up. + for (int j = 0; j < MAX_COMPONENTS; j++) { + src.compRelease[j].releaseReset(false); + src.compRelease[j].versionReset(false); + dst.compRelease[j].releaseReset(false); + dst.compRelease[j].versionReset(false); + } +} + +// =========================================================================== +// ComponentData – removed properties / methods +// +// The PR removes release, version, releaseId, date, prerelease from +// ComponentData (moved to ComponentReleaseData) and removes releaseClear(). +// We verify at compile time via type traits that the expected properties +// exist in ComponentReleaseData and that ComponentData no longer has them +// as normal Q_PROPERTY-backed accessors. +// =========================================================================== + +TEST(AppDataPR, ComponentReleaseData_HasExpectedProperties) +{ + // Verify the five properties declared on ComponentReleaseData are accessible + // via the generated static key accessors (created by PROPERTY_META4). + EXPECT_FALSE(ComponentReleaseData::date_key().isEmpty()); + EXPECT_FALSE(ComponentReleaseData::prerelease_key().isEmpty()); + EXPECT_FALSE(ComponentReleaseData::release_key().isEmpty()); + EXPECT_FALSE(ComponentReleaseData::releaseId_key().isEmpty()); + EXPECT_FALSE(ComponentReleaseData::version_key().isEmpty()); +} + +TEST(AppDataPR, ComponentData_HasOnlyExpectedProperties) +{ + // ComponentData should only have checkForUpdate and releaseChannel now. + // We verify via the meta-object that the number of own properties matches. + ComponentData & c = g.component[0]; + const QMetaObject * mo = c.metaObject(); + // propertyOffset() is the index of the first property defined by ComponentData + // (skipping QObject's own properties). + int ownPropertyCount = mo->propertyCount() - mo->propertyOffset(); + // checkForUpdate + releaseChannel = 2 + EXPECT_EQ(2, ownPropertyCount); +} + +// =========================================================================== +// Settings path renames for joystick classes +// +// The PR renamed settings paths: +// JStickData: "JsCalibration/{idx}/" → "Joysticks/joystick{idx}/" +// JButtonData: "JsButton/{idx}/" → "JSButtons/button{idx}/" +// NamedJStickData: "NamedJSData/{n}/JsCalibration/{i}/" → "NamedJSData/name{n}/joystick{i}/" +// NamedJButtonData:"NamedJSData/{n}/JsButton/{i}/" → "NamedJSData/name{n}/button{i}/" +// NamedJSData: "NamedJSData/{idx}/" → "NamedJSData/name{idx}/" +// +// We can't call settingsPath() directly (protected), but we verify the +// corresponding splitGroupedPath results match the new propertyGroup values, +// i.e. the paths that would be stored in QSettings for these objects. +// =========================================================================== + +// JStickData: propertyGroup = "Joysticks", settingsPath = "Joysticks/joystick{i}/" +// Stored key for stick_axe on joystick 3: "Joysticks/joystick3/stick_axe" +TEST(AppDataPR, SplitPath_JStickData_NewPath) +{ + auto result = SplitPathHelper::split("Joysticks/joystick3/stick_axe"); + EXPECT_EQ(QStringLiteral("Joysticks"), result.first); + EXPECT_EQ(QStringLiteral("stick_axe"), result.second); +} + +// JButtonData: propertyGroup = "JSButtons", settingsPath = "JSButtons/button{i}/" +TEST(AppDataPR, SplitPath_JButtonData_NewPath) +{ + auto result = SplitPathHelper::split("JSButtons/button2/button_idx"); + EXPECT_EQ(QStringLiteral("JSButtons"), result.first); + EXPECT_EQ(QStringLiteral("button_idx"), result.second); +} + +// NamedJSData: propertyGroup = "NamedJSData", settingsPath = "NamedJSData/name{i}/" +TEST(AppDataPR, SplitPath_NamedJSData_NewPath) +{ + auto result = SplitPathHelper::split("NamedJSData/name1/jsName"); + EXPECT_EQ(QStringLiteral("NamedJSData"), result.first); + EXPECT_EQ(QStringLiteral("jsName"), result.second); +} + +// NamedJStickData: propertyGroup = "NamedJSData/name{n}", settingsPath = "NamedJSData/name{n}/joystick{i}/" +TEST(AppDataPR, SplitPath_NamedJStickData_NewPath) +{ + auto result = SplitPathHelper::split("NamedJSData/name2/joystick4/stick_inv"); + EXPECT_EQ(QStringLiteral("NamedJSData/name2"), result.first); + EXPECT_EQ(QStringLiteral("stick_inv"), result.second); +} + +// NamedJButtonData: propertyGroup = "NamedJSData/name{n}", settingsPath = "NamedJSData/name{n}/button{i}/" +TEST(AppDataPR, SplitPath_NamedJButtonData_NewPath) +{ + auto result = SplitPathHelper::split("NamedJSData/name0/button7/button_idx"); + EXPECT_EQ(QStringLiteral("NamedJSData/name0"), result.first); + EXPECT_EQ(QStringLiteral("button_idx"), result.second); +} + +// =========================================================================== +// AppData – global object initialises compRelease indexes +// +// The AppData constructor must call setIndexes(profileIdx, compIdx) for every +// compRelease entry so they are properly registered. We verify that the +// getCompRelease() accessor returns distinct objects for each component index +// within the same profile. +// =========================================================================== + +TEST(AppDataPR, AppData_CompReleaseObjectsAreDistinctPerProfile) +{ + Profile & p = g.profile[0]; + for (int j = 0; j < MAX_COMPONENTS; j++) { + EXPECT_EQ(&p.compRelease[j], &p.getCompRelease(j)) + << "compRelease[" << j << "] address mismatch"; + } +} + +TEST(AppDataPR, AppData_CompReleaseObjectsAreDistinctAcrossProfiles) +{ + // Verify two profiles have separate compRelease arrays. + EXPECT_NE(&g.profile[0].compRelease[0], &g.profile[1].compRelease[0]); +} + +// =========================================================================== +// AppData::getComponent() – unchanged API, regression guard +// +// getComponent() was not changed in this PR, but its companion +// getCompRelease() was added. Verify that the parallel bounds logic holds. +// =========================================================================== + +TEST(AppDataPR, GetComponent_ValidIndex) +{ + EXPECT_EQ(&g.component[0], &g.getComponent(0)); + EXPECT_EQ(&g.component[MAX_COMPONENTS - 1], &g.getComponent(MAX_COMPONENTS - 1)); +} + +TEST(AppDataPR, GetComponent_OutOfRange) +{ + EXPECT_EQ(&g.component[0], &g.getComponent(-1)); + EXPECT_EQ(&g.component[0], &g.getComponent(MAX_COMPONENTS)); +} From c26b07a0b0c022c8b6ffb42831dc79cafe4395b4 Mon Sep 17 00:00:00 2001 From: elecpower Date: Fri, 15 May 2026 17:27:53 +1000 Subject: [PATCH 11/17] fix add joystick settings conversion --- companion/src/storage/appdata.cpp | 106 +++++++++++++++++++++++++++++- companion/src/storage/appdata.h | 8 +-- 2 files changed, 108 insertions(+), 6 deletions(-) diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index 616a680cd7c..58eb1ef9228 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -810,11 +810,113 @@ void AppData::convertSettings(QSettings & settings) } if (savedMajMin < 0x300) { - // 3.0 CloudBuild copy filter changed to cater for uf2 + // CloudBuild copy filter changed to cater for uf2 qInfo().noquote() << "Deleting CloudBuild settings to force refresh"; - static const QString path = QStringLiteral("Components/component6"); + QString path = QStringLiteral("Components/component6"); if (settings.contains(path)) settings.remove(path); + + // Joystick settings paths changed to fix export issue + qInfo().noquote() << "Reorganising joystick settings"; +/* + Old structure TODO THIS IS NOT CORRECT SO FIX IT + ------------- + JsCalibration + |_ + |_ + JsButton + |_ + |_ + NamedJSData + |_ + |_ + |_JsCalibration + |_ + |_ + |_JsButton + |_ + |_ + + New structure + ------------- + Joysticks + |_joystick + |_ + JsButtons + |_button + |_ + NamedJsData + |_name + |_ + |_joystick + |_ + |_button + |_ + */ + QString oldpath = "_JsCalibration/%1/%2"; + QString newpath = "_JsCalibration/stick%1/%2"; + QStringList keys = { "stick_axe", "stick_min", "stick_med", "stick_max", "stick_inv" }; + + for (int i = 0; i < MAX_JS_AXES; i++) { + for (int k = 0; k < keys.size(); k++) { + if (settings.contains(oldpath.arg(i).arg(keys.at(k)))) { + QVariant oldval = settings.value(oldpath.arg(i).arg(keys.at(k))); + settings.setValue(newpath.arg(i).arg(keys.at(k)), oldval); + } + } + } + + oldpath = "JsButton/%1/%2"; + newpath = "JsButtons/button%1/%2"; + keys = { "button_idx" }; + + for (int i = 0; i < MAX_JS_BUTTONS; i++) { + for (int k = 0; k < keys.size(); k++) { + if (settings.contains(oldpath.arg(i).arg(keys.at(k)))) { + QVariant oldval = settings.value(oldpath.arg(i).arg(keys.at(k))); + settings.setValue(newpath.arg(i).arg(keys.at(k)), oldval); + } + } + } + + for (int j = 0; j < MAX_NAMED_JOYSTICKS; j++) { + oldpath = "NamedJSData/%1/%2"; + newpath = "NamedJSData/name%1/%2"; + keys = { "jsName", "jsLastUsed" }; + + for (int k = 0; k < keys.size(); k++) { + if (settings.contains(oldpath.arg(j).arg(keys.at(k)))) { + QVariant oldval = settings.value(oldpath.arg(j).arg(keys.at(k))); + settings.setValue(newpath.arg(j).arg(keys.at(k)), oldval); + } + } + + oldpath = "NamedJSData/%1/JsCalibration/%2/%3"; + newpath = "NamedJSData/name%1/stick%2/%3"; + keys = { "stick_axe", "stick_min", "stick_med", "stick_max", "stick_inv" }; + + for (int i = 0; i < MAX_JS_AXES; i++) { + for (int k = 0; k < keys.size(); k++) { + if (settings.contains(oldpath.arg(j).arg(i).arg(keys.at(k)))) { + QVariant oldval = settings.value(oldpath.arg(j).arg(i).arg(keys.at(k))); + settings.setValue(newpath.arg(j).arg(i).arg(keys.at(k)), oldval); + } + } + } + + oldpath = "NamedJSData/%1/JsButton/%2/%3"; + newpath = "NamedJSData/name%1/button%2/%3"; + keys = { "button_idx" }; + + for (int i = 0; i < MAX_JS_BUTTONS; i++) { + for (int k = 0; k < keys.size(); k++) { + if (settings.contains(oldpath.arg(j).arg(i).arg(keys.at(k)))) { + QVariant oldval = settings.value(oldpath.arg(j).arg(i).arg(keys.at(k))); + settings.setValue(newpath.arg(j).arg(i).arg(keys.at(k)), oldval); + } + } + } + } } if (removeUnused) diff --git a/companion/src/storage/appdata.h b/companion/src/storage/appdata.h index fbc16c829c7..1417ad989d3 100644 --- a/companion/src/storage/appdata.h +++ b/companion/src/storage/appdata.h @@ -346,8 +346,8 @@ class JStickData: public CompStoreObj protected: explicit JStickData(); void setIndex(int idx) { index = idx; CompStoreObj::addObjectMapping(propertyGroup(), this);} - inline QString propertyGroup() const override { return QStringLiteral("Joysticks"); } - inline QString settingsPath() const override { return QString("%1/joystick%2/").arg(propertyGroup()).arg(index); } + inline QString propertyGroup() const override { return QStringLiteral("JsCalibration"); } + inline QString settingsPath() const override { return QString("%1/stick%2/").arg(propertyGroup()).arg(index); } friend class AppData; friend class NamedJSData; @@ -376,7 +376,7 @@ class JButtonData: public CompStoreObj protected: explicit JButtonData(); void setIndex(int idx) { index = idx; CompStoreObj::addObjectMapping(propertyGroup(), this);} - inline QString propertyGroup() const override { return QStringLiteral("JSButtons"); } + inline QString propertyGroup() const override { return QStringLiteral("JsButtons"); } inline QString settingsPath() const override { return QString("%1/button%2/").arg(propertyGroup()).arg(index); } friend class AppData; friend class NamedJSData; @@ -398,7 +398,7 @@ class NamedJStickData: public CompStoreObj explicit NamedJStickData(); void setIndexes(int idx, int nmIdx) { index = idx; namedIdx = nmIdx; CompStoreObj::addObjectMapping(propertyGroup(), this);} inline QString propertyGroup() const override { return QStringLiteral("NamedJSData/name%1").arg(namedIdx); } - inline QString settingsPath() const override { return QString("%1/joystick%2/").arg(propertyGroup()).arg(index); } + inline QString settingsPath() const override { return QString("%1/stick%2/").arg(propertyGroup()).arg(index); } friend class AppData; friend class NamedJSData; From e73bbc2a2e79175276905e4e1f34b3e0c99b61c6 Mon Sep 17 00:00:00 2001 From: elecpower Date: Fri, 15 May 2026 19:38:45 +1000 Subject: [PATCH 12/17] =?UTF-8?q?Revert=20"=F0=9F=93=9D=20CodeRabbit=20Cha?= =?UTF-8?q?t:=20Add=20generated=20unit=20tests"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit c49b1c226b6d0c5f46796d57adaa2959d19597e2. --- companion/src/tests/appdata_pr.cpp | 529 ----------------------------- 1 file changed, 529 deletions(-) delete mode 100644 companion/src/tests/appdata_pr.cpp diff --git a/companion/src/tests/appdata_pr.cpp b/companion/src/tests/appdata_pr.cpp deleted file mode 100644 index 422730444a0..00000000000 --- a/companion/src/tests/appdata_pr.cpp +++ /dev/null @@ -1,529 +0,0 @@ -/* - * Copyright (C) EdgeTX - * - * Based on code named - * opentx - https://github.com/opentx/opentx - * 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. - */ - -/** - * Tests for changes introduced in the PR that: - * - Added ComponentReleaseData (per-profile release info) and moved - * release/version/releaseId/date/prerelease out of ComponentData. - * - Fixed CompStoreObj::splitGroupedPath() to handle deeply-nested paths. - * - Moved addObjectMapping() calls from constructors to setIndex/setIndexes. - * - Added Profile::getCompRelease() with bounds checking. - * - Bumped CPN_SETTINGS_REVISION from 2 to 3. - * - Renamed joystick settings-path prefixes. - */ - -#include "gtests.h" -#include "storage/appdata.h" - -// --------------------------------------------------------------------------- -// Helper: expose the protected CompStoreObj::splitGroupedPath() for testing. -// --------------------------------------------------------------------------- -class SplitPathHelper : public CompStoreObj -{ -public: - QString propertyGroup() const override { return QStringLiteral("Test"); } - - static QPair split(const QString & path) - { - return CompStoreObj::splitGroupedPath(path); - } -}; - -// =========================================================================== -// CPN_SETTINGS_REVISION -// =========================================================================== - -TEST(AppDataPR, SettingsRevisionBumpedTo3) -{ - // The PR bumps the revision from 2 to 3 to signal settings migration. - EXPECT_EQ(3, CPN_SETTINGS_REVISION); -} - -// =========================================================================== -// CompStoreObj::splitGroupedPath() -// -// The function was changed to use list.mid(0, list.size() - 2).join("/") -// instead of list.first() so that deeply-nested property groups (more than -// two path components) are correctly identified. -// -// Contract: for a stored key of the form "", the -// function must return (propertyGroup(), propKey). -// settingsPath() always ends with '/', so the stored path is -// settingsPath() % propKey (no extra slash). -// =========================================================================== - -// Single element: no '/' present → group = "General". -TEST(AppDataPR, SplitPath_SingleKey) -{ - auto result = SplitPathHelper::split("someKey"); - EXPECT_EQ(QStringLiteral("General"), result.first); - EXPECT_EQ(QStringLiteral("someKey"), result.second); -} - -// Three-segment path: "Profiles/profile0/Name" -// list = ["Profiles","profile0","Name"], size=3 -// mid(0, 3-2) = mid(0,1) = ["Profiles"] → "Profiles" -// This matches Profile::propertyGroup() = "Profiles". -TEST(AppDataPR, SplitPath_ThreeSegments_ProfileProperty) -{ - auto result = SplitPathHelper::split("Profiles/profile0/Name"); - EXPECT_EQ(QStringLiteral("Profiles"), result.first); - EXPECT_EQ(QStringLiteral("Name"), result.second); -} - -// Three-segment path: "Components/component0/checkForUpdate" -// list = ["Components","component0","checkForUpdate"], size=3 -// mid(0,1) = ["Components"] → "Components" -// Matches ComponentData::propertyGroup() = "Components". -TEST(AppDataPR, SplitPath_ThreeSegments_ComponentProperty) -{ - auto result = SplitPathHelper::split("Components/component0/checkForUpdate"); - EXPECT_EQ(QStringLiteral("Components"), result.first); - EXPECT_EQ(QStringLiteral("checkForUpdate"), result.second); -} - -// Four-segment path: "Profiles/profile0/component0/release" -// list = ["Profiles","profile0","component0","release"], size=4 -// mid(0, 4-2) = mid(0,2) = ["Profiles","profile0"] → "Profiles/profile0" -// This matches ComponentReleaseData::propertyGroup() = "Profiles/profile{idx}". -TEST(AppDataPR, SplitPath_FourSegments_ComponentReleaseProperty) -{ - auto result = SplitPathHelper::split("Profiles/profile0/component0/release"); - EXPECT_EQ(QStringLiteral("Profiles/profile0"), result.first); - EXPECT_EQ(QStringLiteral("release"), result.second); -} - -// Four-segment path: "Components/component0/asset0/desc" -// list = ["Components","component0","asset0","desc"], size=4 -// mid(0,2) = ["Components","component0"] → "Components/component0" -// Matches ComponentAssetData::propertyGroup() = "Components/component{idx}". -TEST(AppDataPR, SplitPath_FourSegments_ComponentAssetProperty) -{ - auto result = SplitPathHelper::split("Components/component0/asset0/desc"); - EXPECT_EQ(QStringLiteral("Components/component0"), result.first); - EXPECT_EQ(QStringLiteral("desc"), result.second); -} - -// Five-segment path: deeply nested NamedJSData joystick property. -// "NamedJSData/name0/joystick1/stick_axe" -// list = ["NamedJSData","name0","joystick1","stick_axe"], size=4 -// mid(0,2) = ["NamedJSData","name0"] → "NamedJSData/name0" -// Matches NamedJStickData::propertyGroup() = "NamedJSData/name{idx}". -TEST(AppDataPR, SplitPath_FourSegments_NamedJStickProperty) -{ - auto result = SplitPathHelper::split("NamedJSData/name0/joystick1/stick_axe"); - EXPECT_EQ(QStringLiteral("NamedJSData/name0"), result.first); - EXPECT_EQ(QStringLiteral("stick_axe"), result.second); -} - -// Regression: with the OLD code (list.first()), a four-segment path would -// have returned only "Profiles" for "Profiles/profile0/component0/release", -// which is wrong. Verify the new behaviour is distinct from the old. -TEST(AppDataPR, SplitPath_FourSegments_NotJustFirstSegment) -{ - auto result = SplitPathHelper::split("Profiles/profile0/component0/release"); - // The old (incorrect) code would have returned "Profiles" here. - EXPECT_NE(QStringLiteral("Profiles"), result.first); - // The correct new code returns the full two-segment group. - EXPECT_EQ(QStringLiteral("Profiles/profile0"), result.first); -} - -// =========================================================================== -// ComponentReleaseData – declared default property values -// -// The static _default() methods are generated by the PROPERTY_META4 macro -// and are publicly accessible. -// =========================================================================== - -TEST(AppDataPR, ComponentReleaseData_DefaultDate) -{ - EXPECT_EQ(QStringLiteral(""), ComponentReleaseData::date_default().toString()); -} - -TEST(AppDataPR, ComponentReleaseData_DefaultPrerelease) -{ - EXPECT_EQ(false, ComponentReleaseData::prerelease_default().toBool()); -} - -TEST(AppDataPR, ComponentReleaseData_DefaultRelease) -{ - EXPECT_EQ(QStringLiteral("unknown"), ComponentReleaseData::release_default().toString()); -} - -TEST(AppDataPR, ComponentReleaseData_DefaultReleaseId) -{ - EXPECT_EQ(0, ComponentReleaseData::releaseId_default().toInt()); -} - -TEST(AppDataPR, ComponentReleaseData_DefaultVersion) -{ - EXPECT_EQ(QStringLiteral("0"), ComponentReleaseData::version_default().toString()); -} - -// =========================================================================== -// ComponentReleaseData::propertyGroup() / settingsPath() -// -// The group must follow "Profiles/profile{profileIndex}" so that -// splitGroupedPath() can correctly reverse it. -// We verify the pattern using the objects initialised by the global AppData. -// =========================================================================== - -TEST(AppDataPR, ComponentReleaseData_PropertyGroupPattern) -{ - // After AppData construction, profile[0].compRelease[0] has been set up - // with setIndexes(0, 0). Its propertyGroup() must be "Profiles/profile0". - // We can't call protected propertyGroup() directly, but we can verify the - // round-trip through splitGroupedPath: a path built as - // "" + "" must split back to (propertyGroup, key). - // - // settingsPath = "Profiles/profile0/component0/" - // key = "release" - // full path = "Profiles/profile0/component0/release" - // expected grp = "Profiles/profile0" (= propertyGroup of compRelease[0]) - const QString fullPath = QStringLiteral("Profiles/profile0/component0/release"); - auto result = SplitPathHelper::split(fullPath); - EXPECT_EQ(QStringLiteral("Profiles/profile0"), result.first); - EXPECT_EQ(QStringLiteral("release"), result.second); -} - -TEST(AppDataPR, ComponentReleaseData_PropertyGroupPatternProfile5Component3) -{ - // Verify for a non-zero profile and component index. - // settingsPath = "Profiles/profile5/component3/" - // key = "version" - const QString fullPath = QStringLiteral("Profiles/profile5/component3/version"); - auto result = SplitPathHelper::split(fullPath); - EXPECT_EQ(QStringLiteral("Profiles/profile5"), result.first); - EXPECT_EQ(QStringLiteral("version"), result.second); -} - -// =========================================================================== -// Profile::getCompRelease() – bounds checking -// -// Valid indices [0, MAX_COMPONENTS-1] must return a reference to the -// corresponding compRelease element. Out-of-range indices must clamp to -// compRelease[0]. -// =========================================================================== - -TEST(AppDataPR, GetCompRelease_ValidIndex_Zero) -{ - Profile & p = g.profile[0]; - EXPECT_EQ(&p.compRelease[0], &p.getCompRelease(0)); -} - -TEST(AppDataPR, GetCompRelease_ValidIndex_One) -{ - Profile & p = g.profile[0]; - EXPECT_EQ(&p.compRelease[1], &p.getCompRelease(1)); -} - -TEST(AppDataPR, GetCompRelease_ValidIndex_MaxMinus1) -{ - Profile & p = g.profile[0]; - const int lastIdx = MAX_COMPONENTS - 1; - EXPECT_EQ(&p.compRelease[lastIdx], &p.getCompRelease(lastIdx)); -} - -TEST(AppDataPR, GetCompRelease_OutOfRange_NegativeOne) -{ - Profile & p = g.profile[0]; - // index == -1 → must return compRelease[0] - EXPECT_EQ(&p.compRelease[0], &p.getCompRelease(-1)); -} - -TEST(AppDataPR, GetCompRelease_OutOfRange_MaxComponents) -{ - Profile & p = g.profile[0]; - // index == MAX_COMPONENTS → must return compRelease[0] - EXPECT_EQ(&p.compRelease[0], &p.getCompRelease(MAX_COMPONENTS)); -} - -TEST(AppDataPR, GetCompRelease_OutOfRange_LargePositive) -{ - Profile & p = g.profile[0]; - EXPECT_EQ(&p.compRelease[0], &p.getCompRelease(9999)); -} - -// Const variant – same bounds logic applies. -TEST(AppDataPR, GetCompRelease_Const_ValidIndex) -{ - const Profile & p = g.profile[0]; - EXPECT_EQ(&p.compRelease[0], &p.getCompRelease(0)); - EXPECT_EQ(&p.compRelease[MAX_COMPONENTS - 1], &p.getCompRelease(MAX_COMPONENTS - 1)); -} - -TEST(AppDataPR, GetCompRelease_Const_OutOfRange) -{ - const Profile & p = g.profile[0]; - EXPECT_EQ(&p.compRelease[0], &p.getCompRelease(-1)); - EXPECT_EQ(&p.compRelease[0], &p.getCompRelease(MAX_COMPONENTS)); -} - -// =========================================================================== -// ComponentReleaseData::operator= -// -// The custom assignment must copy all declared properties but must NOT copy -// the index fields (profileIndex, index), ensuring the destination object -// retains its own identity. -// =========================================================================== - -TEST(AppDataPR, ComponentReleaseData_AssignmentCopiesProperties) -{ - // Use two compRelease slots from different profiles so they have distinct - // indexes and are properly initialised. - ComponentReleaseData & src = g.profile[1].compRelease[2]; - ComponentReleaseData & dst = g.profile[2].compRelease[3]; - - // Set non-default values on the source. - src.release(QStringLiteral("v3.0.0"), false); - src.version(QStringLiteral("3.0.0"), false); - src.releaseId(42, false); - src.date(QStringLiteral("2025-01-01"), false); - src.prerelease(true, false); - - // Assign. - dst = src; - - // All property values must match. - EXPECT_EQ(QStringLiteral("v3.0.0"), dst.release()); - EXPECT_EQ(QStringLiteral("3.0.0"), dst.version()); - EXPECT_EQ(42, dst.releaseId()); - EXPECT_EQ(QStringLiteral("2025-01-01"), dst.date()); - EXPECT_EQ(true, dst.prerelease()); - - // Clean up: reset source back to defaults (no-store). - src.releaseReset(false); - src.versionReset(false); - src.releaseIdReset(false); - src.dateReset(false); - src.prereleaseReset(false); -} - -TEST(AppDataPR, ComponentReleaseData_AssignmentPreservesDestinationIdentity) -{ - // After assignment, the destination object must still be the same object - // (i.e. at the same address). The index must not change. - ComponentReleaseData & src = g.profile[3].compRelease[0]; - ComponentReleaseData & dst = g.profile[4].compRelease[5]; - - ComponentReleaseData * dstAddr = &dst; - dst = src; - - // Object identity preserved. - EXPECT_EQ(dstAddr, &dst); - // The address within Profile is still the same slot. - EXPECT_EQ(&g.profile[4].compRelease[5], &dst); -} - -TEST(AppDataPR, ComponentReleaseData_AssignmentFromDefault) -{ - // Assign from a source with default values; destination should also reflect - // those defaults afterwards. - ComponentReleaseData & src = g.profile[5].compRelease[1]; - ComponentReleaseData & dst = g.profile[6].compRelease[1]; - - // Set dst to non-default values first. - dst.release(QStringLiteral("dirty"), false); - dst.version(QStringLiteral("9.9"), false); - dst.releaseId(99, false); - - // Reset src to defaults. - src.releaseReset(false); - src.versionReset(false); - src.releaseIdReset(false); - src.dateReset(false); - src.prereleaseReset(false); - - dst = src; - - EXPECT_EQ(QStringLiteral("unknown"), dst.release()); - EXPECT_EQ(QStringLiteral("0"), dst.version()); - EXPECT_EQ(0, dst.releaseId()); -} - -// =========================================================================== -// Profile::operator= copies compRelease array -// -// When a Profile is copy-assigned, all MAX_COMPONENTS compRelease entries -// must be copied from the source profile. -// =========================================================================== - -TEST(AppDataPR, Profile_AssignmentCopiesCompReleaseArray) -{ - Profile & src = g.profile[7]; - Profile & dst = g.profile[8]; - - // Write distinct values into every compRelease slot of the source. - for (int j = 0; j < MAX_COMPONENTS; j++) { - src.compRelease[j].release(QStringLiteral("src-release-%1").arg(j), false); - src.compRelease[j].version(QStringLiteral("1.%1").arg(j), false); - } - - dst = src; - - for (int j = 0; j < MAX_COMPONENTS; j++) { - EXPECT_EQ(QStringLiteral("src-release-%1").arg(j), dst.compRelease[j].release()) - << "compRelease[" << j << "].release() mismatch"; - EXPECT_EQ(QStringLiteral("1.%1").arg(j), dst.compRelease[j].version()) - << "compRelease[" << j << "].version() mismatch"; - } - - // Clean up. - for (int j = 0; j < MAX_COMPONENTS; j++) { - src.compRelease[j].releaseReset(false); - src.compRelease[j].versionReset(false); - dst.compRelease[j].releaseReset(false); - dst.compRelease[j].versionReset(false); - } -} - -// =========================================================================== -// ComponentData – removed properties / methods -// -// The PR removes release, version, releaseId, date, prerelease from -// ComponentData (moved to ComponentReleaseData) and removes releaseClear(). -// We verify at compile time via type traits that the expected properties -// exist in ComponentReleaseData and that ComponentData no longer has them -// as normal Q_PROPERTY-backed accessors. -// =========================================================================== - -TEST(AppDataPR, ComponentReleaseData_HasExpectedProperties) -{ - // Verify the five properties declared on ComponentReleaseData are accessible - // via the generated static key accessors (created by PROPERTY_META4). - EXPECT_FALSE(ComponentReleaseData::date_key().isEmpty()); - EXPECT_FALSE(ComponentReleaseData::prerelease_key().isEmpty()); - EXPECT_FALSE(ComponentReleaseData::release_key().isEmpty()); - EXPECT_FALSE(ComponentReleaseData::releaseId_key().isEmpty()); - EXPECT_FALSE(ComponentReleaseData::version_key().isEmpty()); -} - -TEST(AppDataPR, ComponentData_HasOnlyExpectedProperties) -{ - // ComponentData should only have checkForUpdate and releaseChannel now. - // We verify via the meta-object that the number of own properties matches. - ComponentData & c = g.component[0]; - const QMetaObject * mo = c.metaObject(); - // propertyOffset() is the index of the first property defined by ComponentData - // (skipping QObject's own properties). - int ownPropertyCount = mo->propertyCount() - mo->propertyOffset(); - // checkForUpdate + releaseChannel = 2 - EXPECT_EQ(2, ownPropertyCount); -} - -// =========================================================================== -// Settings path renames for joystick classes -// -// The PR renamed settings paths: -// JStickData: "JsCalibration/{idx}/" → "Joysticks/joystick{idx}/" -// JButtonData: "JsButton/{idx}/" → "JSButtons/button{idx}/" -// NamedJStickData: "NamedJSData/{n}/JsCalibration/{i}/" → "NamedJSData/name{n}/joystick{i}/" -// NamedJButtonData:"NamedJSData/{n}/JsButton/{i}/" → "NamedJSData/name{n}/button{i}/" -// NamedJSData: "NamedJSData/{idx}/" → "NamedJSData/name{idx}/" -// -// We can't call settingsPath() directly (protected), but we verify the -// corresponding splitGroupedPath results match the new propertyGroup values, -// i.e. the paths that would be stored in QSettings for these objects. -// =========================================================================== - -// JStickData: propertyGroup = "Joysticks", settingsPath = "Joysticks/joystick{i}/" -// Stored key for stick_axe on joystick 3: "Joysticks/joystick3/stick_axe" -TEST(AppDataPR, SplitPath_JStickData_NewPath) -{ - auto result = SplitPathHelper::split("Joysticks/joystick3/stick_axe"); - EXPECT_EQ(QStringLiteral("Joysticks"), result.first); - EXPECT_EQ(QStringLiteral("stick_axe"), result.second); -} - -// JButtonData: propertyGroup = "JSButtons", settingsPath = "JSButtons/button{i}/" -TEST(AppDataPR, SplitPath_JButtonData_NewPath) -{ - auto result = SplitPathHelper::split("JSButtons/button2/button_idx"); - EXPECT_EQ(QStringLiteral("JSButtons"), result.first); - EXPECT_EQ(QStringLiteral("button_idx"), result.second); -} - -// NamedJSData: propertyGroup = "NamedJSData", settingsPath = "NamedJSData/name{i}/" -TEST(AppDataPR, SplitPath_NamedJSData_NewPath) -{ - auto result = SplitPathHelper::split("NamedJSData/name1/jsName"); - EXPECT_EQ(QStringLiteral("NamedJSData"), result.first); - EXPECT_EQ(QStringLiteral("jsName"), result.second); -} - -// NamedJStickData: propertyGroup = "NamedJSData/name{n}", settingsPath = "NamedJSData/name{n}/joystick{i}/" -TEST(AppDataPR, SplitPath_NamedJStickData_NewPath) -{ - auto result = SplitPathHelper::split("NamedJSData/name2/joystick4/stick_inv"); - EXPECT_EQ(QStringLiteral("NamedJSData/name2"), result.first); - EXPECT_EQ(QStringLiteral("stick_inv"), result.second); -} - -// NamedJButtonData: propertyGroup = "NamedJSData/name{n}", settingsPath = "NamedJSData/name{n}/button{i}/" -TEST(AppDataPR, SplitPath_NamedJButtonData_NewPath) -{ - auto result = SplitPathHelper::split("NamedJSData/name0/button7/button_idx"); - EXPECT_EQ(QStringLiteral("NamedJSData/name0"), result.first); - EXPECT_EQ(QStringLiteral("button_idx"), result.second); -} - -// =========================================================================== -// AppData – global object initialises compRelease indexes -// -// The AppData constructor must call setIndexes(profileIdx, compIdx) for every -// compRelease entry so they are properly registered. We verify that the -// getCompRelease() accessor returns distinct objects for each component index -// within the same profile. -// =========================================================================== - -TEST(AppDataPR, AppData_CompReleaseObjectsAreDistinctPerProfile) -{ - Profile & p = g.profile[0]; - for (int j = 0; j < MAX_COMPONENTS; j++) { - EXPECT_EQ(&p.compRelease[j], &p.getCompRelease(j)) - << "compRelease[" << j << "] address mismatch"; - } -} - -TEST(AppDataPR, AppData_CompReleaseObjectsAreDistinctAcrossProfiles) -{ - // Verify two profiles have separate compRelease arrays. - EXPECT_NE(&g.profile[0].compRelease[0], &g.profile[1].compRelease[0]); -} - -// =========================================================================== -// AppData::getComponent() – unchanged API, regression guard -// -// getComponent() was not changed in this PR, but its companion -// getCompRelease() was added. Verify that the parallel bounds logic holds. -// =========================================================================== - -TEST(AppDataPR, GetComponent_ValidIndex) -{ - EXPECT_EQ(&g.component[0], &g.getComponent(0)); - EXPECT_EQ(&g.component[MAX_COMPONENTS - 1], &g.getComponent(MAX_COMPONENTS - 1)); -} - -TEST(AppDataPR, GetComponent_OutOfRange) -{ - EXPECT_EQ(&g.component[0], &g.getComponent(-1)); - EXPECT_EQ(&g.component[0], &g.getComponent(MAX_COMPONENTS)); -} From 396f8e831858c412741600c81ca7d783505d2d64 Mon Sep 17 00:00:00 2001 From: elecpower Date: Fri, 15 May 2026 21:25:46 +1000 Subject: [PATCH 13/17] fix conversion typo --- companion/src/storage/appdata.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index 58eb1ef9228..90353a315ce 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -853,8 +853,8 @@ void AppData::convertSettings(QSettings & settings) |_button |_ */ - QString oldpath = "_JsCalibration/%1/%2"; - QString newpath = "_JsCalibration/stick%1/%2"; + QString oldpath = "JsCalibration/%1/%2"; + QString newpath = "JsCalibration/stick%1/%2"; QStringList keys = { "stick_axe", "stick_min", "stick_med", "stick_max", "stick_inv" }; for (int i = 0; i < MAX_JS_AXES; i++) { From 96bda64f7ebb5b33dd3bedc57fd3258f6500c3d8 Mon Sep 17 00:00:00 2001 From: elecpower Date: Fri, 15 May 2026 21:27:18 +1000 Subject: [PATCH 14/17] housekeeping --- companion/src/storage/appdata.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index 90353a315ce..8fe408778b0 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -839,8 +839,8 @@ void AppData::convertSettings(QSettings & settings) New structure ------------- - Joysticks - |_joystick + JsCalibration + |_stick |_ JsButtons |_button @@ -848,7 +848,7 @@ void AppData::convertSettings(QSettings & settings) NamedJsData |_name |_ - |_joystick + |_stick |_ |_button |_ From 2b34a0ef004471951ad7d984d6f8f16a0bef5030 Mon Sep 17 00:00:00 2001 From: elecpower Date: Sun, 17 May 2026 08:00:26 +1000 Subject: [PATCH 15/17] fix settings previous version search --- companion/src/storage/appdata.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index 8fe408778b0..b21fd6dfdfd 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -941,13 +941,17 @@ void AppData::clearUnusedSettings(QSettings & settings) bool AppData::findPreviousVersionSettings(QString * version) const { - int vmaj = VERSION_MAJOR; - int vmin = VERSION_MINOR - 1; // make sure we do not try to import from ourselves otherwise settings WILL get corrupted + // make sure we do not try to import from ourselves otherwise settings WILL get corrupted + const int vmax = 30; //abitary maximum minor version + int vmaj = VERSION_MINOR > 1 ? VERSION_MAJOR : VERSION_MAJOR - 1; + int vmin = VERSION_MINOR > 1 ? VERSION_MINOR - 1 : vmax; + + // qDebug() << "Search start version:" << vmaj << vmin; for (;(vmaj << 8 | vmin) >= (2 << 8 | 4);) { // 2.4 earliest EdgeTX version const QString ver = QString("%1.%2").arg(vmaj).arg(vmin); const QString prod = QString("Companion %1").arg(ver); - qDebug() << "Searching for previous version" << ver; + // qDebug() << "Searching for previous version" << ver; QSettings settings(COMPANY, prod); if (settings.contains(SETTINGS_VERSION_KEY)) { *version = ver; @@ -959,7 +963,7 @@ bool AppData::findPreviousVersionSettings(QString * version) const --vmin; if (vmin < 0) { - vmin = 30; // abitary maximum minor version for earlier major versions + vmin = vmax; --vmaj; } } From 6eb2a626f8f57e256d2a601341529adb1f2de15c Mon Sep 17 00:00:00 2001 From: elecpower Date: Sun, 17 May 2026 08:53:27 +1000 Subject: [PATCH 16/17] Add settings version to display func --- companion/src/storage/appdata.cpp | 25 +++++++++++++++++++------ companion/src/storage/appdata.h | 2 ++ 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index b21fd6dfdfd..a50f62a5a2d 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -231,7 +231,7 @@ const QHash > & CompStoreObj::keyToNameMap() it.next(); QHash grpMap; CompStoreObj * obj = it.value(); - //dumpMetadata(obj); + // 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); @@ -239,7 +239,7 @@ const QHash > & CompStoreObj::keyToNameMap() grpMap.insert(key, name); } map.insert(it.key(), grpMap); - qDebug() << it.key() << grpMap; + // qDebug() << it.key() << grpMap; } } return map; @@ -571,7 +571,8 @@ static QString fmtHex(quint32 num) 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); + << "Saved version:" << settingsVersionToDisplay(m_settings.value(SETTINGS_VERSION_KEY).toUInt()) + << "Current version:" << settingsVersionToDisplay(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(&AppData::sessionId), Qt::UniqueConnection); @@ -761,7 +762,9 @@ void AppData::convertSettings(QSettings & settings) return; if (savedVer > CPN_SETTINGS_VERSION) { - qWarning().noquote() << "Saved settings version is newer than current, skipping conversions. Saved:" << fmtHex(savedVer) << "Current:" << fmtHex(CPN_SETTINGS_VERSION); + qWarning().noquote() << "Saved settings version is newer than current, skipping conversions. Saved:" + << settingsVersionToDisplay(savedVer) << "Current:" + << settingsVersionToDisplay(CPN_SETTINGS_VERSION); return; } @@ -772,8 +775,9 @@ void AppData::convertSettings(QSettings & settings) 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; + << " from " << settingsVersionToDisplay(savedVer) + << " to " << settingsVersionToDisplay(CPN_SETTINGS_VERSION) + << ". Removing unused: " << removeUnused; // firmwares renamed from opentx-* to edgetx-* at 2.6 if (savedMajMin <= 0x207) { // Note: change merged post 2.6 rc 1 and version bumped to 2.7 the Nightly users also require upgrade @@ -1101,3 +1105,12 @@ void AppData::resetUpdatesSettings() } } } + +const QString AppData::settingsVersionToDisplay(const unsigned int ver) +{ + return QString("v%1.%2.%3.%4") + .arg(ver >> 24) + .arg((ver >> 16) & ((1U << 8) - 1)) + .arg((ver >> 8) & ((1U << 8) - 1)) + .arg(ver & ((1U << 8) - 1)); +} diff --git a/companion/src/storage/appdata.h b/companion/src/storage/appdata.h index 1417ad989d3..94dace50667 100644 --- a/companion/src/storage/appdata.h +++ b/companion/src/storage/appdata.h @@ -722,6 +722,8 @@ class AppData: public CompStoreObj bool exportSettings(QSettings * toSettings, bool clearDestination = true); bool exportSettingsToFile(const QString & expFile, QString & resultMsg); + const QString settingsVersionToDisplay(const unsigned int ver); + Profile profile[MAX_PROFILES]; JStickData joystick[MAX_JS_AXES]; JButtonData jsButton[MAX_JS_BUTTONS]; From 3bcaa30fc4f9cbb512bfd3cdc88ed3cc3a65a41f Mon Sep 17 00:00:00 2001 From: elecpower Date: Sun, 17 May 2026 17:12:03 +1000 Subject: [PATCH 17/17] Remove old joystick settings --- companion/src/storage/appdata.cpp | 56 ++++++++++++++++++++++--------- 1 file changed, 40 insertions(+), 16 deletions(-) diff --git a/companion/src/storage/appdata.cpp b/companion/src/storage/appdata.cpp index a50f62a5a2d..fc4164cdeb5 100644 --- a/companion/src/storage/appdata.cpp +++ b/companion/src/storage/appdata.cpp @@ -863,9 +863,13 @@ void AppData::convertSettings(QSettings & settings) for (int i = 0; i < MAX_JS_AXES; i++) { for (int k = 0; k < keys.size(); k++) { - if (settings.contains(oldpath.arg(i).arg(keys.at(k)))) { - QVariant oldval = settings.value(oldpath.arg(i).arg(keys.at(k))); - settings.setValue(newpath.arg(i).arg(keys.at(k)), oldval); + const QString opath = oldpath.arg(i).arg(keys.at(k)); + const QString npath = newpath.arg(i).arg(keys.at(k)); + + if (settings.contains(opath)) { + settings.setValue(npath, settings.value(opath)); + settings.remove(opath); + qInfo().noquote() << "Moved " << opath << " to " << npath; } } } @@ -876,9 +880,13 @@ void AppData::convertSettings(QSettings & settings) for (int i = 0; i < MAX_JS_BUTTONS; i++) { for (int k = 0; k < keys.size(); k++) { - if (settings.contains(oldpath.arg(i).arg(keys.at(k)))) { - QVariant oldval = settings.value(oldpath.arg(i).arg(keys.at(k))); - settings.setValue(newpath.arg(i).arg(keys.at(k)), oldval); + const QString opath = oldpath.arg(i).arg(keys.at(k)); + const QString npath = newpath.arg(i).arg(keys.at(k)); + + if (settings.contains(opath)) { + settings.setValue(npath, settings.value(opath)); + settings.remove(opath); + qInfo().noquote() << "Moved " << opath << " to " << npath; } } } @@ -889,9 +897,13 @@ void AppData::convertSettings(QSettings & settings) keys = { "jsName", "jsLastUsed" }; for (int k = 0; k < keys.size(); k++) { - if (settings.contains(oldpath.arg(j).arg(keys.at(k)))) { - QVariant oldval = settings.value(oldpath.arg(j).arg(keys.at(k))); - settings.setValue(newpath.arg(j).arg(keys.at(k)), oldval); + const QString opath = oldpath.arg(j).arg(keys.at(k)); + const QString npath = newpath.arg(j).arg(keys.at(k)); + + if (settings.contains(opath)) { + settings.setValue(npath, settings.value(opath)); + settings.remove(opath); + qInfo().noquote() << "Moved " << opath << " to " << npath; } } @@ -901,9 +913,13 @@ void AppData::convertSettings(QSettings & settings) for (int i = 0; i < MAX_JS_AXES; i++) { for (int k = 0; k < keys.size(); k++) { - if (settings.contains(oldpath.arg(j).arg(i).arg(keys.at(k)))) { - QVariant oldval = settings.value(oldpath.arg(j).arg(i).arg(keys.at(k))); - settings.setValue(newpath.arg(j).arg(i).arg(keys.at(k)), oldval); + const QString opath = oldpath.arg(j).arg(i).arg(keys.at(k)); + const QString npath = newpath.arg(j).arg(i).arg(keys.at(k)); + + if (settings.contains(opath)) { + settings.setValue(npath, settings.value(opath)); + settings.remove(opath); + qInfo().noquote() << "Moved " << opath << " to " << npath; } } } @@ -914,9 +930,13 @@ void AppData::convertSettings(QSettings & settings) for (int i = 0; i < MAX_JS_BUTTONS; i++) { for (int k = 0; k < keys.size(); k++) { - if (settings.contains(oldpath.arg(j).arg(i).arg(keys.at(k)))) { - QVariant oldval = settings.value(oldpath.arg(j).arg(i).arg(keys.at(k))); - settings.setValue(newpath.arg(j).arg(i).arg(keys.at(k)), oldval); + const QString opath = oldpath.arg(j).arg(i).arg(keys.at(k)); + const QString npath = newpath.arg(j).arg(i).arg(keys.at(k)); + + if (settings.contains(opath)) { + settings.setValue(npath, settings.value(opath)); + settings.remove(opath); + qInfo().noquote() << "Moved " << opath << " to " << npath; } } } @@ -933,13 +953,17 @@ void AppData::convertSettings(QSettings & settings) void AppData::clearUnusedSettings(QSettings & settings) { // Go through and clean up anything that doesn't exist or is set to default value. + qInfo().noquote() << "Tidy settings by removing redundant and default settings"; + 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)) + if (!newVal.isValid() || !CompStoreObj::propertyPathIsValidNonDefault(key, newVal)) { settings.remove(key); + qInfo().noquote() << "Removed key " << key; + } } }