feat(theme): slice indicator runtime theming via ThemeManager#3121
Merged
Conversation
The 8-slot per-slice colour catalogue (A=cyan, B=magenta, C=green, …)
used by SpectrumWidget overlays, VfoWidget badges, and the RxApplet
header lived in a compile-time SliceColorEntry kSliceColors[] table.
The 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[], plus a new
`slice.dim.{a..h}` nested block carrying the dim variants used
by inactive-slice rendering.
* `src/gui/SliceColorManager.cpp` — `defaultActive(idx)` and
`defaultDim(idx)` now resolve through `ThemeManager::color()`
against `color.slice.{letter}` and `color.slice.dim.{letter}`.
The singleton's constructor subscribes to `ThemeManager::themeChanged`
and re-emits `colorsChanged()` after a cache rebuild so every
listener (panadapter slice markers, VFO badges, the rx header)
repaints with the new palette when the user switches themes.
* `src/gui/SliceColors.h` — `SliceColorEntry` and `kSliceColors[]`
deleted; only the `kSliceColorCount = 8` constant is retained
because SpectrumWidget / SliceColorManager / RadioSetupDialog
still use it to size loops and arrays. The header's 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 are unaffected — `m_customColors` and the persisted
SliceColor{0..7} app-settings paths still work as before; the theme
change just refreshes the m_hexCache so anything reading hexActive()
gets the new active hue when the override is off.
73, Jeremy KK7GWY & Claude (AI dev partner)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
ten9876
added a commit
that referenced
this pull request
May 25, 2026
) ## Summary Closes the third (and last mechanical) deferred item from the Phase 3 closeout. All five waterfall colormap presets exposed via the Spectrum Overlay → Display → \`Scheme:\` dropdown (**Default / Grayscale / Blue-Green / Fire / Plasma**) now resolve through \`ThemeManager\` against named gradient tokens. ## Why all five (not one) The user-facing dropdown stays — operators still pick from the same five presets and the choice still drives which colormap paints. **What changed is the *content* of each preset**: a future light theme can ship a brighter \"Default\" for daylight viewing, redesign \"Fire\" with a softer stop set, etc., entirely from JSON without touching code. ## JSON shape The previous flat \`color.waterfall.colormap\` gradient (never wired up — its stops had drifted from the actual compile-time defaults) is replaced by a nested object with five named gradient sub-tokens: \`\`\` color.waterfall.colormap.default (7 stops, black→…→red) color.waterfall.colormap.grayscale (2 stops, black→white) color.waterfall.colormap.blueGreen (5 stops, black→blue→teal→green→white) color.waterfall.colormap.fire (5 stops, black→red→orange→yellow→white) color.waterfall.colormap.plasma (5 stops, black→purple→magenta→orange→yellow) \`\`\` \`ThemeManager::flattenTokens()\` already routes inner objects through \`parseGradient()\` whenever they carry the \`type\` discriminator, so each sub-token resolves through \`brush()\` / \`cssFragment()\` like any other gradient. ## C++ wiring — \`src/gui/SpectrumWidget.cpp\` A file-static \`WfStopsCache\` holds five \`std::vector<WfGradientStop>\` — the converted form of each named gradient's stop list. Lazy-populated on first \`wfSchemeStops()\` call so the function works during early static-init sequences where the SpectrumWidget hasn't been constructed yet. SpectrumWidget's constructor connects to \`ThemeManager::themeChanged\`: 1. Rebuild the cache from the new theme's gradient tokens. 2. Mark the static overlay dirty. 3. \`update()\`. **Already-rendered waterfall rows keep their pre-switch colours** — same behaviour as changing the \`Scheme:\` dropdown today, since recolouring the history image would force an O(rows × cols) repaint we don't currently amortise. New rows pick up the new palette on the next push. The five compile-time \`kDefaultStops\` / \`kGrayscaleStops\` / etc. tables are deleted; JSON is now the source of truth. ## Visual continuity Default-dark's stop hex values mirror the deleted compile-time tables exactly, so the dark theme renders the same waterfall across all five presets. Only the source of truth has moved. ## Build verified clean - [x] AetherSDR builds clean - [x] All 320 targets including every test target ## Phase 3/4 status | Item | Status | |---|---| | Slice indicators runtime themed | ✅ #3121 | | Waterfall colormap runtime themed | ✅ this PR | | default-light theme | ⏳ last Phase 4 milestone | After this lands, every load-bearing colour decision in the GUI flows through the token system. Default-light becomes a pure JSON authoring exercise (plus a contrast pass). ## Test plan - [ ] CI: build / check-paths / check-windows / CodeQL - [ ] Visual smoke: open the Spectrum Overlay → Display tab and cycle through all 5 colormap presets, confirm each renders identically to current main on default-dark - [ ] Bonus: drop a custom theme JSON into \`~/.config/AetherSDR/themes/\` with a re-coloured \`color.waterfall.colormap.fire\` to confirm theme switching repaints new rows with the override 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>
12 tasks
ten9876
added a commit
that referenced
this pull request
May 25, 2026
## Summary The final Phase 4 milestone for the theming subsystem — every load-bearing colour decision in the GUI now flows through the token system, so authoring a second theme is purely a JSON exercise. ## What Default Light is Daylight-readable variant of the canonical taxonomy. Backgrounds invert (panel ramp goes light→dark for elevation), text colours invert (primary text now dark on light), accents darken to maintain contrast against the light canvas. Slice and meter colours keep semantic meaning but shift toward the darker / more-saturated end of their hue family so they read clearly on the light background. ## Design choices, by category | Category | Approach | |---|---| | **Backgrounds** | Light at the base (\`#f5f5f8\`), darker with elevation (\`#b0bcc8\` at the highest tier). \`background.tx\` warms to \`#ffeacc\` to preserve "transmit zone" feel. \`background.spectrum\` flips to pure white. | | **Accents** | Pulled toward the darker end of each hue family. Cyan \`#00b4d8\` → \`#0088b0\`; warning amber → \`#d08010\`; danger red → \`#c02020\`; success green → \`#1a8040\`. | | **Text** | Full inversion of the dark ramp. primary \`#c8d8e8\` → \`#1a2a3a\`; secondary / label / disabled shift accordingly. | | **Borders** | Light grey at subtle, medium grey at strong. TX border keeps the warm cast: \`#5a4a28\` → \`#b08840\`. | | **Meters** | Semantic colours preserved (red crest, cyan rms, amber threshold, gold GR). All darkened for contrast on the light meter background. \`peak\` reads dark on light instead of near-white. | | **Spectrum** | trace / peakHold / average shift to darker tones. grid lightens to \`#d0d8e0\` so it stays visible-but-subtle on the white canvas. | | **Waterfall** | Colormap gradients **UNCHANGED** across all 5 presets. The waterfall renders OPAQUE pixels from a heatmap; canvas underneath doesn't show through. Same Default / Grayscale / Blue-Green / Fire / Plasma in both themes. | | **Slice** | Active hues stay distinct (cyan / magenta / green / yellow / orange / teal / coral / lavender) but shift toward saturated mid-tones. Dim variants become **washed-out light pastels** instead of half-brightness saturated versions — they sit visibly above the light background without being attention-grabbing. | ## Wiring - \`resources/themes/default-light.json\` — new file, 152 lines. - \`resources/resources.qrc\` — one-line entry registering the new theme as \`:/themes/default-light.json\`. No C++ changes needed. \`ThemeManager::scanAvailableThemes()\` already auto-discovers any \`*.json\` in \`:/themes/\` at startup, so the new theme appears in \`availableThemes()\` and is settable via \`setActiveTheme(\"Default Light\")\`. All widgets that registered through \`ThemeManager::applyStyleSheet\` (50+ sites from the Phase 2/3 migration) live-re-theme automatically. Slice indicators, waterfall colormaps, FFT background fill, primary sliders — all flip on the next paint. ## Build verified clean - [x] AetherSDR builds clean - [x] All 320 targets including every test target - [x] Both themes appear in the generated qrc_resources.cpp ## Phase 4 status — complete | Item | PR | |---|---| | Slice indicators runtime themed | #3121 ✅ | | Waterfall colormap runtime themed | #3122 ✅ | | **Default Light theme** | **this PR** ✅ | Phase 5 picks up the theme editor UI (modeless dialog with inspector mode), import/export, and custom-theme persistence in \`~/.config/AetherSDR/themes/\`. ## Test plan - [ ] CI: build / check-paths / check-windows / CodeQL - [ ] Visual smoke: launch the app, open Settings → switch active theme to "Default Light", confirm: - [ ] Panels invert correctly (light backgrounds, dark text) - [ ] Spectrum widget renders with a white canvas, dark FFT trace, light grid lines - [ ] Waterfall colormap unchanged (still black→…→hot) - [ ] All 8 slice indicators readable on the light spectrum - [ ] Primary sliders (sub-page fill + light handle) still visible with darker accent fill - [ ] Strip panels' amber-sub-page sliders still TX-coloured - [ ] Bonus: switch back to "Default Dark" — every widget should snap back without restart 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>
G6PWY-Chris
pushed a commit
to G6PWY-Chris/AetherSDR
that referenced
this pull request
Jun 22, 2026
…thersdr#3122) ## Summary Closes the third (and last mechanical) deferred item from the Phase 3 closeout. All five waterfall colormap presets exposed via the Spectrum Overlay → Display → \`Scheme:\` dropdown (**Default / Grayscale / Blue-Green / Fire / Plasma**) now resolve through \`ThemeManager\` against named gradient tokens. ## Why all five (not one) The user-facing dropdown stays — operators still pick from the same five presets and the choice still drives which colormap paints. **What changed is the *content* of each preset**: a future light theme can ship a brighter \"Default\" for daylight viewing, redesign \"Fire\" with a softer stop set, etc., entirely from JSON without touching code. ## JSON shape The previous flat \`color.waterfall.colormap\` gradient (never wired up — its stops had drifted from the actual compile-time defaults) is replaced by a nested object with five named gradient sub-tokens: \`\`\` color.waterfall.colormap.default (7 stops, black→…→red) color.waterfall.colormap.grayscale (2 stops, black→white) color.waterfall.colormap.blueGreen (5 stops, black→blue→teal→green→white) color.waterfall.colormap.fire (5 stops, black→red→orange→yellow→white) color.waterfall.colormap.plasma (5 stops, black→purple→magenta→orange→yellow) \`\`\` \`ThemeManager::flattenTokens()\` already routes inner objects through \`parseGradient()\` whenever they carry the \`type\` discriminator, so each sub-token resolves through \`brush()\` / \`cssFragment()\` like any other gradient. ## C++ wiring — \`src/gui/SpectrumWidget.cpp\` A file-static \`WfStopsCache\` holds five \`std::vector<WfGradientStop>\` — the converted form of each named gradient's stop list. Lazy-populated on first \`wfSchemeStops()\` call so the function works during early static-init sequences where the SpectrumWidget hasn't been constructed yet. SpectrumWidget's constructor connects to \`ThemeManager::themeChanged\`: 1. Rebuild the cache from the new theme's gradient tokens. 2. Mark the static overlay dirty. 3. \`update()\`. **Already-rendered waterfall rows keep their pre-switch colours** — same behaviour as changing the \`Scheme:\` dropdown today, since recolouring the history image would force an O(rows × cols) repaint we don't currently amortise. New rows pick up the new palette on the next push. The five compile-time \`kDefaultStops\` / \`kGrayscaleStops\` / etc. tables are deleted; JSON is now the source of truth. ## Visual continuity Default-dark's stop hex values mirror the deleted compile-time tables exactly, so the dark theme renders the same waterfall across all five presets. Only the source of truth has moved. ## Build verified clean - [x] AetherSDR builds clean - [x] All 320 targets including every test target ## Phase 3/4 status | Item | Status | |---|---| | Slice indicators runtime themed | ✅ aethersdr#3121 | | Waterfall colormap runtime themed | ✅ this PR | | default-light theme | ⏳ last Phase 4 milestone | After this lands, every load-bearing colour decision in the GUI flows through the token system. Default-light becomes a pure JSON authoring exercise (plus a contrast pass). ## Test plan - [ ] CI: build / check-paths / check-windows / CodeQL - [ ] Visual smoke: open the Spectrum Overlay → Display tab and cycle through all 5 colormap presets, confirm each renders identically to current main on default-dark - [ ] Bonus: drop a custom theme JSON into \`~/.config/AetherSDR/themes/\` with a re-coloured \`color.waterfall.colormap.fire\` to confirm theme switching repaints new rows with the override 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
…3129) ## Summary The final Phase 4 milestone for the theming subsystem — every load-bearing colour decision in the GUI now flows through the token system, so authoring a second theme is purely a JSON exercise. ## What Default Light is Daylight-readable variant of the canonical taxonomy. Backgrounds invert (panel ramp goes light→dark for elevation), text colours invert (primary text now dark on light), accents darken to maintain contrast against the light canvas. Slice and meter colours keep semantic meaning but shift toward the darker / more-saturated end of their hue family so they read clearly on the light background. ## Design choices, by category | Category | Approach | |---|---| | **Backgrounds** | Light at the base (\`#f5f5f8\`), darker with elevation (\`#b0bcc8\` at the highest tier). \`background.tx\` warms to \`#ffeacc\` to preserve "transmit zone" feel. \`background.spectrum\` flips to pure white. | | **Accents** | Pulled toward the darker end of each hue family. Cyan \`#00b4d8\` → \`#0088b0\`; warning amber → \`#d08010\`; danger red → \`#c02020\`; success green → \`#1a8040\`. | | **Text** | Full inversion of the dark ramp. primary \`#c8d8e8\` → \`#1a2a3a\`; secondary / label / disabled shift accordingly. | | **Borders** | Light grey at subtle, medium grey at strong. TX border keeps the warm cast: \`#5a4a28\` → \`#b08840\`. | | **Meters** | Semantic colours preserved (red crest, cyan rms, amber threshold, gold GR). All darkened for contrast on the light meter background. \`peak\` reads dark on light instead of near-white. | | **Spectrum** | trace / peakHold / average shift to darker tones. grid lightens to \`#d0d8e0\` so it stays visible-but-subtle on the white canvas. | | **Waterfall** | Colormap gradients **UNCHANGED** across all 5 presets. The waterfall renders OPAQUE pixels from a heatmap; canvas underneath doesn't show through. Same Default / Grayscale / Blue-Green / Fire / Plasma in both themes. | | **Slice** | Active hues stay distinct (cyan / magenta / green / yellow / orange / teal / coral / lavender) but shift toward saturated mid-tones. Dim variants become **washed-out light pastels** instead of half-brightness saturated versions — they sit visibly above the light background without being attention-grabbing. | ## Wiring - \`resources/themes/default-light.json\` — new file, 152 lines. - \`resources/resources.qrc\` — one-line entry registering the new theme as \`:/themes/default-light.json\`. No C++ changes needed. \`ThemeManager::scanAvailableThemes()\` already auto-discovers any \`*.json\` in \`:/themes/\` at startup, so the new theme appears in \`availableThemes()\` and is settable via \`setActiveTheme(\"Default Light\")\`. All widgets that registered through \`ThemeManager::applyStyleSheet\` (50+ sites from the Phase 2/3 migration) live-re-theme automatically. Slice indicators, waterfall colormaps, FFT background fill, primary sliders — all flip on the next paint. ## Build verified clean - [x] AetherSDR builds clean - [x] All 320 targets including every test target - [x] Both themes appear in the generated qrc_resources.cpp ## Phase 4 status — complete | Item | PR | |---|---| | Slice indicators runtime themed | aethersdr#3121 ✅ | | Waterfall colormap runtime themed | aethersdr#3122 ✅ | | **Default Light theme** | **this PR** ✅ | Phase 5 picks up the theme editor UI (modeless dialog with inspector mode), import/export, and custom-theme persistence in \`~/.config/AetherSDR/themes/\`. ## Test plan - [ ] CI: build / check-paths / check-windows / CodeQL - [ ] Visual smoke: launch the app, open Settings → switch active theme to "Default Light", confirm: - [ ] Panels invert correctly (light backgrounds, dark text) - [ ] Spectrum widget renders with a white canvas, dark FFT trace, light grid lines - [ ] Waterfall colormap unchanged (still black→…→hot) - [ ] All 8 slice indicators readable on the light spectrum - [ ] Primary sliders (sub-page fill + light handle) still visible with darker accent fill - [ ] Strip panels' amber-sub-page sliders still TX-coloured - [ ] Bonus: switch back to "Default Dark" — every widget should snap back without restart 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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`
`src/gui/SliceColorManager.cpp`
`src/gui/SliceColors.h`
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
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
73, Jeremy KK7GWY & Claude (AI dev partner)
🤖 Generated with Claude Code