feat(theme): Phase 3 PR 5/5 — spectrum + chain + waveform paint-code migration, canonical table closeout#3117
Conversation
…migration, canonical table closeout Final mechanical sweep of file-scope const QColor and remaining paint- code QColor literals across the spectrum, chain, and waveform widget families. 159 literals migrated across 11 files via the now-mature migrate_paint_colours.py tool plus a closing 50-entry canonical-table expansion. ## Files migrated (11) SpectrumWidget 40 literals (residual from #3113) ClientChainWidget 20 StripChainWidget 20 ClientRxChainWidget 16 StripRxChainWidget 16 WaveformWidget 15 StripWaveform 15 KeyboardMapWidget 8 (10 categoryColor() returns left as raw QColor — intentional, each category needs a distinct hue and no shared visual language applies) ClientCompThresholdFader 5 (gradient stops deferred from #3116) ClientCompEditorCanvas 3 ClientGateCurveWidget 1 (translucent cyan fill from #3116) ## Canonical table — 50 new close-mappings Three logical groups, each annotated with its sibling canonical token and the ΔRGB so the rationale is auditable later: * Spectrum paint code (25) — peak hold, trace overlays, axis labels, alert / overload / warning shades * Chain / waveform (11) — raised backgrounds, muted labels, clip / peak-hold markers, fallback cyan * Threshold fader / gate (6) — meter gradient stops (lo-mid-hi green → amber → red) + translucent cyan * Pre-existing repeats (8) — variations of existing entries ## Build verified clean AetherSDR + all 308 targets including every test target. Two warnings surface (sliceColor / effectiveGridStepMhz unused), both pre-existing on main — confirmed via local stash bisect. ## Skips left for follow-on (intentional) * 10 KeyboardMapWidget::categoryColor() returns — semantically distinct hues, no canonical mapping is meaningful * 1 ClientCompMeter no-go-zone tint (#3a1810) — unique meter visual * Slice indicator runtime theming — SliceColorManager still reads from compile-time `kSliceColors[]` table; converting to read from ThemeManager warrants its own PR (touches SliceColorManager, SpectrumWidget overlay rendering, VfoWidget badges, RxApplet) * Waterfall colormap runtime theming — gradient token exists in default-dark.json but the SpectrumWidget waterfall renderer hard-codes its stop set; converting warrants its own PR This closes the Phase 2/3 wrap-up at 5 PRs as constrained. Phase 4 (default-light theme + slice/waterfall runtime-theming refactor) picks up the deferred items along with the new light-theme JSON. 73, Jeremy KK7GWY & Claude (AI dev partner) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks for closing out Phase 3 — the migration tool has clearly matured and the canonical-table additions are well documented. A few concerns worth a look before merge.
Material concerns
1. Mic-ready / TX-active text becomes invisible against its own background
In ClientChainWidget.cpp (and identically in StripChainWidget.cpp and ClientRxChainWidget.cpp), three previously-distinct hex values are now collapsed onto a single canonical token:
| Role | Was | After PR resolves to |
|---|---|---|
kBgMicReady (body fill) |
#006040 (dark) |
color.accent.success = #4dd87a |
kBorderMicReady (ring) |
#00a060 (mid) |
color.accent.success = #4dd87a |
kTextMicReady (label) |
#00ff88 (bright) |
color.accent.success = #4dd87a |
So once m_micInputReady flips true, the text, border, and body are all painted with the same #4dd87a and the "MIC" label will disappear. The TX-active triple (kBorderTxActive + kTextTxActive both → color.accent.danger = #ff4d4d) and the StatusActive triple in ClientRxChainWidget (kBgStatusActive / kBorderStatusActive / kTextStatusActive all → color.accent.success) have the same problem.
The dark-theme JSON only ships a single accent.success / accent.danger value, so this can't be fixed at the resolver layer. Options that keep the migration moving:
- Leave these three-tier "status" triples as raw
QColorfor now (treat them like thecategoryColor()skips you already documented), or - Mint distinct
accent.success.bg/accent.success.fg(or.dim/.bright) tokens in the canonical table before this PR lands.
2. TX-dim breathing tint shifts to brown
kBgTxActiveDim was #3a1010 (near-black red) and is now mapped to color.background.tx = #3a2a0e (dark amber/brown). The breathing lerp in ClientChainWidget.cpp:447-457 then runs from brown → bright red instead of the previous dim red → mid red, which changes the "active transmit" mood. If a color.accent.danger.dim token would be more honest here, worth introducing it; otherwise calling this out in the audit deviations list would be enough.
Cosmetic
3. Migration tool is folding the next-line block comment onto the previous declaration
In several spots the tool has produced output like:
inline QColor kBorderGrey() { return ...color("color.background.1"); } // MIC endpoint "ready" state — matches RxApplet's SQL-button green
// so users recognise the visual language across the sidebar.
inline QColor kBgMicReady() { ... }The "MIC endpoint..." comment originally documented kBgMicReady but now reads as a trailing comment on kBorderGrey. Same pattern repeats with the "TX endpoint..." block and the "User-facing short label..." block in ClientChainWidget / StripChainWidget / ClientRxChainWidget. Worth a fix in migrate_paint_colours.py so future migrations don't reattach leading-block comments to the previous statement.
Scope / boundaries
- File list matches the PR description (11 widget files + tool table). ✓
- Skips are documented and justified (
categoryColor(), no-go-zone tint, slice/waterfall runtime work). ✓ - No new error-handling boundaries introduced; all changes are paint-code. ✓
inlineon internal-linkage helpers in unnamed namespaces is redundant but harmless.
The mechanical migration itself is fine — the issue is that this batch crosses from "background/border families" (where collapsing onto a single token is the goal) into "fg/bg pairs that need to stay visually distinct," and the canonical table doesn't yet differentiate them. Confirming with the radio + a quick screenshot of the mic-ready and TX-active states would catch (1) and (2) immediately.
🤖 aethersdr-agent · cost: $8.0290 · model: claude-opus-4-7
## Summary Wires the per-slice colour catalogue through \`ThemeManager\` so a theme switch repaints the slice markers, VFO badges, and rx headers with the new palette. Closes the second of the three deferred items from #3117's Phase 3 closeout (the other two: default-light theme, waterfall colormap runtime theming). ## What changed The 8-slot per-slice palette (A=cyan, B=magenta, C=green, D=yellow, E=orange, F=teal, G=coral, H=lavender) used to live in a compile-time \`SliceColorEntry kSliceColors[]\` table. \`default-dark.json\` had a \`color.slice.{a..h}\` block but **nothing read from it** — the JSON values were stale (different hues than the actual code) since they were never wired through. This commit makes the theme JSON authoritative: ### \`resources/themes/default-dark.json\` - Slice tokens updated to the hex values currently shipped via \`kSliceColors[]\` (cyan / magenta / green / yellow / orange / teal / coral / lavender — visually identical to what users see today). - New nested \`color.slice.dim.{a..h}\` block carrying the dim-variant hex values used by inactive-slice rendering. ### \`src/gui/SliceColorManager.cpp\` - \`defaultActive(idx)\` and \`defaultDim(idx)\` now resolve through \`ThemeManager::color()\` against \`color.slice.{letter}\` / \`color.slice.dim.{letter}\`. - The singleton's constructor subscribes to \`ThemeManager::themeChanged\`, rebuilds the hex cache, and re-emits \`colorsChanged()\` so every listener (panadapter slice markers, VFO badges, RxApplet header) repaints with the new palette. ### \`src/gui/SliceColors.h\` - \`SliceColorEntry\` and \`kSliceColors[]\` deleted (no remaining consumers). - \`kSliceColorCount = 8\` constant retained — \`SpectrumWidget\`, \`SliceColorManager\`, and \`RadioSetupDialog\` still use it to size loops and arrays. - Header comment now points readers at the theme JSON for the actual values. ## Visual continuity The new JSON values are **identical** to the previously-compile-time defaults, so the dark theme renders the same slice palette as before. Only the source of truth has moved. ## Custom-colour users Unaffected — \`m_customColors\` and the persisted \`SliceColor{0..7}\` app-settings paths still work as before. The theme change refreshes \`m_hexCache\` so anything reading \`hexActive()\` gets the new active hue when the per-user override is off. ## Build verified clean - [x] AetherSDR builds clean (236 ninja steps) - [x] All 320 targets including every test target ## Phase 4 readiness Once the default-light theme ships, it can define a brighter slice palette appropriate for daylight viewing (or just use desaturated variants of the dark theme). Operators see slice colours change live without restart. The remaining deferred item from #3117 (waterfall colormap runtime theming) is independent and can land separately. ## Test plan - [ ] CI: build / check-paths / check-windows / CodeQL - [ ] Visual smoke: open a panadapter with multiple slices and confirm slice colours render identically to current main (sub-perceptual or zero delta — the JSON values match kSliceColors exactly) - [ ] Verify dim/active states still look right when toggling slice activation 73, Jeremy KK7GWY & Claude (AI dev partner) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…migration, canonical table closeout (aethersdr#3117) ## Summary **PR 5 of 5** in the Phase 2/3 wrap-up — closes the mechanical migration sweep. 159 \`QColor\` literals migrated across 11 widget files via the now-mature \`migrate_paint_colours.py\` tool, plus a closing 50-entry canonical-table expansion. ## Files migrated (11) | File | Literals | Notes | |---|---|---| | \`SpectrumWidget\` | 40 | Residual paint code from aethersdr#3113 | | \`ClientChainWidget\` | 20 | | | \`StripChainWidget\` | 20 | | | \`ClientRxChainWidget\` | 16 | | | \`StripRxChainWidget\` | 16 | | | \`WaveformWidget\` | 15 | rx + strip-mode share the same palette | | \`StripWaveform\` | 15 | | | \`KeyboardMapWidget\` | 8 | 10 \`categoryColor()\` returns left as-is (see Skips) | | \`ClientCompThresholdFader\` | 5 | Meter gradient stops deferred from aethersdr#3116 | | \`ClientCompEditorCanvas\` | 3 | | | \`ClientGateCurveWidget\` | 1 | Translucent cyan fill deferred from aethersdr#3116 | ## Canonical-table additions — 50 new close-mappings Three logical groups in \`tools/migrate_colours.py\`, each entry annotated with its sibling canonical value and ΔRGB so the rationale is auditable later: \`\`\`python # Spectrum paint code (25) \"#80d0ff\": \"color.accent.bright\", # peak hold light cyan \"#ff5858\": \"color.accent.danger\", # bright alert red \"#ffc040\": \"color.accent.warning\", # warning amber, ΔRGB≈0 from #ffb84d # … 22 more # Chain / waveform (11) \"#587890\": \"color.text.label\", # waveform centerline \"#ffd166\": \"color.accent.warning\", # peak-hold, ΔRGB≈1 from #ffd070 # … 9 more # Threshold fader / gate gradient stops (6) \"#2f9e6a\": \"color.accent.success\", # comp meter lo green \"#f2362a\": \"color.accent.danger\", # comp meter peak red \"#50b4dc\": \"color.accent.dim\", # gate curve translucent cyan # … 3 more \`\`\` ## Build verified clean - [x] AetherSDR builds clean - [x] All 308 targets build clean (every test target) - Two warnings surface (\`sliceColor\` / \`effectiveGridStepMhz\` unused) — both pre-existing on \`main\`, confirmed via local stash bisect ## Intentional skips (deferred to Phase 4) 1. **10 \`KeyboardMapWidget::categoryColor()\` returns** — semantically distinct hues for Frequency / TX / Audio / DSP / etc. categories. No shared visual language applies; converting would homogenize them and defeat the categorization. Left as raw \`QColor\`. 2. **1 \`ClientCompMeter\` no-go-zone tint (\`#3a1810\`)** — unique meter visual, no canonical equivalent worth absorbing. 3. **Slice indicator runtime theming** — \`SliceColorManager\` still reads from the compile-time \`kSliceColors[]\` table in \`SliceColors.h\`. Default-dark.json already has \`color.slice.a\` through \`.h\` tokens, but wiring \`SliceColorManager\` to read them requires touching \`SpectrumWidget\` overlay rendering + \`VfoWidget\` badges + \`RxApplet\`. Warrants its own PR. 4. **Waterfall colormap runtime theming** — \`color.waterfall.colormap\` gradient token is already shipped in default-dark.json (validating gradient-token support added in Phase 2), but the \`SpectrumWidget\` waterfall renderer hard-codes its colormap stop set. Conversion warrants its own PR. ## Phase 2/3 closeout This closes the **5-PR Phase 2/3 wrap-up** as constrained. Recap: | PR | What | Status | |---|---|---| | aethersdr#3102 | Mass migration + token taxonomy | merged | | aethersdr#3106 | MeterSlider pilot (Phase 3 PR 2/5) | merged | | aethersdr#3113 | Paint-color tool + SpectrumWidget (PR 3/5) | merged | | aethersdr#3116 | Comp/gate/curve widgets (PR 4/5) | merged | | **#XXXX** | **Spectrum + chain + waveform closeout (PR 5/5)** | **this PR** | Phase 4 picks up: default-light theme + slice indicator runtime theming + waterfall colormap runtime theming. ## Test plan - [ ] CI: build / check-paths / check-windows / CodeQL - [ ] Visual smoke: open the app and confirm the spectrum + chain + waveform widgets render to-within-ΔRGB-of-50 of current main (the 159 migrated literals resolve to canonical token values that fall within audited deviation thresholds) 73, Jeremy KK7GWY & Claude (AI dev partner) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sdr#3121) ## Summary Wires the per-slice colour catalogue through \`ThemeManager\` so a theme switch repaints the slice markers, VFO badges, and rx headers with the new palette. Closes the second of the three deferred items from aethersdr#3117's Phase 3 closeout (the other two: default-light theme, waterfall colormap runtime theming). ## What changed The 8-slot per-slice palette (A=cyan, B=magenta, C=green, D=yellow, E=orange, F=teal, G=coral, H=lavender) used to live in a compile-time \`SliceColorEntry kSliceColors[]\` table. \`default-dark.json\` had a \`color.slice.{a..h}\` block but **nothing read from it** — the JSON values were stale (different hues than the actual code) since they were never wired through. This commit makes the theme JSON authoritative: ### \`resources/themes/default-dark.json\` - Slice tokens updated to the hex values currently shipped via \`kSliceColors[]\` (cyan / magenta / green / yellow / orange / teal / coral / lavender — visually identical to what users see today). - New nested \`color.slice.dim.{a..h}\` block carrying the dim-variant hex values used by inactive-slice rendering. ### \`src/gui/SliceColorManager.cpp\` - \`defaultActive(idx)\` and \`defaultDim(idx)\` now resolve through \`ThemeManager::color()\` against \`color.slice.{letter}\` / \`color.slice.dim.{letter}\`. - The singleton's constructor subscribes to \`ThemeManager::themeChanged\`, rebuilds the hex cache, and re-emits \`colorsChanged()\` so every listener (panadapter slice markers, VFO badges, RxApplet header) repaints with the new palette. ### \`src/gui/SliceColors.h\` - \`SliceColorEntry\` and \`kSliceColors[]\` deleted (no remaining consumers). - \`kSliceColorCount = 8\` constant retained — \`SpectrumWidget\`, \`SliceColorManager\`, and \`RadioSetupDialog\` still use it to size loops and arrays. - Header comment now points readers at the theme JSON for the actual values. ## Visual continuity The new JSON values are **identical** to the previously-compile-time defaults, so the dark theme renders the same slice palette as before. Only the source of truth has moved. ## Custom-colour users Unaffected — \`m_customColors\` and the persisted \`SliceColor{0..7}\` app-settings paths still work as before. The theme change refreshes \`m_hexCache\` so anything reading \`hexActive()\` gets the new active hue when the per-user override is off. ## Build verified clean - [x] AetherSDR builds clean (236 ninja steps) - [x] All 320 targets including every test target ## Phase 4 readiness Once the default-light theme ships, it can define a brighter slice palette appropriate for daylight viewing (or just use desaturated variants of the dark theme). Operators see slice colours change live without restart. The remaining deferred item from aethersdr#3117 (waterfall colormap runtime theming) is independent and can land separately. ## Test plan - [ ] CI: build / check-paths / check-windows / CodeQL - [ ] Visual smoke: open a panadapter with multiple slices and confirm slice colours render identically to current main (sub-perceptual or zero delta — the JSON values match kSliceColors exactly) - [ ] Verify dim/active states still look right when toggling slice activation 73, Jeremy KK7GWY & Claude (AI dev partner) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
PR 5 of 5 in the Phase 2/3 wrap-up — closes the mechanical migration sweep. 159 `QColor` literals migrated across 11 widget files via the now-mature `migrate_paint_colours.py` tool, plus a closing 50-entry canonical-table expansion.
Files migrated (11)
Canonical-table additions — 50 new close-mappings
Three logical groups in `tools/migrate_colours.py`, each entry annotated with its sibling canonical value and ΔRGB so the rationale is auditable later:
```python
Spectrum paint code (25)
"#80d0ff": "color.accent.bright", # peak hold light cyan
"#ff5858": "color.accent.danger", # bright alert red
"#ffc040": "color.accent.warning", # warning amber, ΔRGB≈0 from #ffb84d
… 22 more
Chain / waveform (11)
"#587890": "color.text.label", # waveform centerline
"#ffd166": "color.accent.warning", # peak-hold, ΔRGB≈1 from #ffd070
… 9 more
Threshold fader / gate gradient stops (6)
"#2f9e6a": "color.accent.success", # comp meter lo green
"#f2362a": "color.accent.danger", # comp meter peak red
"#50b4dc": "color.accent.dim", # gate curve translucent cyan
… 3 more
```
Build verified clean
Intentional skips (deferred to Phase 4)
10 `KeyboardMapWidget::categoryColor()` returns — semantically distinct hues for Frequency / TX / Audio / DSP / etc. categories. No shared visual language applies; converting would homogenize them and defeat the categorization. Left as raw `QColor`.
1 `ClientCompMeter` no-go-zone tint (`#3a1810`) — unique meter visual, no canonical equivalent worth absorbing.
Slice indicator runtime theming — `SliceColorManager` still reads from the compile-time `kSliceColors[]` table in `SliceColors.h`. Default-dark.json already has `color.slice.a` through `.h` tokens, but wiring `SliceColorManager` to read them requires touching `SpectrumWidget` overlay rendering + `VfoWidget` badges + `RxApplet`. Warrants its own PR.
Waterfall colormap runtime theming — `color.waterfall.colormap` gradient token is already shipped in default-dark.json (validating gradient-token support added in Phase 2), but the `SpectrumWidget` waterfall renderer hard-codes its colormap stop set. Conversion warrants its own PR.
Phase 2/3 closeout
This closes the 5-PR Phase 2/3 wrap-up as constrained. Recap:
Phase 4 picks up: default-light theme + slice indicator runtime theming + waterfall colormap runtime theming.
Test plan
73, Jeremy KK7GWY & Claude (AI dev partner)
🤖 Generated with Claude Code