fix: persist Theme Editor overrides under pre-seeded applet scopes#3688
Conversation
There was a problem hiding this comment.
Reviewed the change against the codebase — the diagnosis and fix both hold up.
Root cause confirmed. seedBuiltinDefaults() is called before readScopeFromJson() on every load (ThemeManager.cpp:638) and seeds applet/{tx,rx,comp} via scopeOrCreate (lines 516/522/528), which materializes the top-level applet scope. The old code's into->children.emplace(name, …) is a no-op when the key already exists, so the saved applet/* subtree was parsed into a raw node that was created but never linked — every per-applet override was silently dropped. spectrum isn't seeded, which is exactly why spectrum/vfo round-tripped fine. The find-or-recurse rewrite is the right shape.
Merge semantics are correct. Recursing into the existing node funnels saved tokens through flattenTokens into into->tokens, which uses QHash::insert (overwrite) — so saved overrides win over seeded defaults, and untouched seeded tokens are preserved. The reused node keeps its name/path/parent, all consistent with how scopeOrCreate computes them, so no divergence there.
Tests are well-chosen. The four scenarios pin the regression (applet/rx font), guard the previously-working path (spectrum/vfo), prove generality (applet/comp color), and the sparse-theme case confirms seeded per-applet defaults survive when a theme doesn't mention applet/* — that last one is the case the merge logic could most plausibly have broken, so good to see it covered.
Conventions look right: no flat-key AppSettings, RAII via unique_ptr preserved, localized to one function. Nice clean-room bug report and fix — thanks @mvanhorn.
🤖 aethersdr-agent · cost: $1.7704 · model: claude-opus-4-8
There was a problem hiding this comment.
Thanks for this, @mvanhorn — clear write-up and a well-targeted fix.
I traced the root cause and it matches your diagnosis exactly. seedBuiltinDefaults() calls scopeOrCreate("applet/tx" | "applet/rx" | "applet/comp") (ThemeManager.cpp:515-532) on every load, which materializes the intermediate applet node as a child of root before the theme JSON is read. So in the old readScopeFromJson, the applet key already existed and into->children.emplace(name, …) was a no-op — the saved applet/* subtree was recursed into the freshly-constructed raw duplicate that was never linked into the tree (and then discarded at end of loop), silently dropping every per-applet override. spectrum isn't pre-seeded, which is why spectrum/vfo round-tripped fine. The find-then-merge rewrite is the right shape and stays localized to the one function.
A few things I checked that hold up:
- Merge semantics are correct. Recursing into the existing seeded node means
flattenTokensoverwrites only the keys present in the JSON and leaves the seeded floor intact — consistent with the "seeds are the floor, JSON wins where defined" contract documented at loadThemeFromPath (line 633-634). Your sparse-theme test pins this nicely. - No leak / dangling concern in either the old or new path — the old code discarded data but didn't leak; the new path links what it creates.
m_scopeByPathstays consistent —loadThemeFromPathcallsrebuildScopePathIndex()afterreadScopeFromJson(line 669), so the merged-in nodes get indexed regardless.
Conventions look good: no flat-key AppSettings, RAII unique_ptr ownership preserved, C++20 idioms intact. Tests are thorough — regression lock on applet/rx, the working-path spectrum/vfo control, a general applet/comp color case, and the sparse-theme seeded-defaults case. All six CI checks (build, macOS, Windows, CodeQL/analyze) are green.
Nothing blocking from me. Nice, surgical bug fix.
🤖 aethersdr-agent · cost: $1.9535 · model: claude-opus-4-8
ten9876
left a comment
There was a problem hiding this comment.
Approving — accurate root-cause fix and thorough tests.
The bug: readScopeFromJson always did make_unique + std::map::emplace, but emplace no-ops when the key exists — and seedBuiltinDefaults() pre-creates the applet scope, so saved applet/* overrides were read into an orphaned node never linked into the tree (while non-pre-seeded spectrum/* loaded fine). The fix correctly recurses into the existing child and merges, rather than orphaning a duplicate. Minimal and localized.
The four new scenarios genuinely lock it (applet/rx round-trip = the regression lock; spectrum/vfo guards the working path; applet/comp color proves it's general; sparse-theme confirms seeded defaults survive) — built and passed 1/1 locally. Bug fix with a clear root cause, Principle V honored. Nice work @mvanhorn.
|
Thanks for the second one, @ten9876. Persisting Theme Editor overrides under pre-seeded applet scopes closes a frustrating little gap. |
Summary
Fixes #3653. Theme Editor overrides applied under a pre-seeded applet scope (font size on the applet/RX frequency display, and any other
applet/tx,applet/rx,applet/comptoken) were dropped on restart, while the same edit on the floating VFO flag persisted.ThemeManager::readScopeFromJsonusedstd::map::emplacefor child scopes, which is a no-op when the key already exists. BecauseseedBuiltinDefaults()pre-creates the top-levelappletscope during load, the savedapplet/*subtree was read into an orphaned node that was never linked into the scope tree, so every per-applet override was silently discarded.spectrumis not pre-seeded, which is why the flag'sspectrum/vfooverride loaded fine and the applet ones did not.The fix looks up the child scope by name first: if it already exists, recurse into the existing node; otherwise create, link, and recurse as before. This merges saved overrides into the pre-seeded tree instead of writing them into a discarded duplicate. The change is localized to one function. No save-path, schema, or seeding changes are needed, and it restores persistence for every Theme-Editor override under the pre-seeded
applet/*scopes, not just fonts.Constitution principle honored
Principle V — Each Feature Owns Its Configuration As A Single Object. The theme's saved config object now round-trips intact through reload; per-applet overrides survive the save/load cycle instead of being silently lost.
Test plan
cmake --build build)Added four scenarios to
tests/theme_manager_test.cpp: a save/reload round-trip of anapplet/rxfont.size.freqoverride (regression lock for this bug), the same forspectrum/vfoto confirm the working path does not regress, anapplet/compcolor override to prove the fix is general, and a sparse theme with noapplet/*overrides to confirm seeded defaults remain intact.theme_manager_testbuilds and passes (1/1) in the project CI imageghcr.io/ten9876/aethersdr-ci:latest.Checklist
docs/COMMIT-SIGNING.md)AppSettingscalls — use nested-JSON-under-one-key (Principle V)MeterSmoother(AGENTS.md convention)AI was used for assistance.