Skip to content

feat(theme): Phase 3 PR 5/5 — spectrum + chain + waveform paint-code migration, canonical table closeout#3117

Merged
ten9876 merged 1 commit into
mainfrom
auto/theme-phase3-pr5
May 25, 2026
Merged

feat(theme): Phase 3 PR 5/5 — spectrum + chain + waveform paint-code migration, canonical table closeout#3117
ten9876 merged 1 commit into
mainfrom
auto/theme-phase3-pr5

Conversation

@ten9876

@ten9876 ten9876 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

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 #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 #3116
`ClientCompEditorCanvas` 3
`ClientGateCurveWidget` 1 Translucent cyan fill deferred from #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

  • AetherSDR builds clean
  • 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
#3102 Mass migration + token taxonomy merged
#3106 MeterSlider pilot (Phase 3 PR 2/5) merged
#3113 Paint-color tool + SpectrumWidget (PR 3/5) merged
#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

…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>
@ten9876 ten9876 requested a review from a team as a code owner May 25, 2026 06:18

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 QColor for now (treat them like the categoryColor() 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. ✓
  • inline on 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

@ten9876 ten9876 merged commit f4e25e8 into main May 25, 2026
5 checks passed
@ten9876 ten9876 deleted the auto/theme-phase3-pr5 branch May 25, 2026 07:03
ten9876 added a commit that referenced this pull request May 25, 2026
## 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>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…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>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant