feat(theme): ship Default Light theme (Phase 4 milestone)#3129
Merged
Conversation
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
Backgrounds — light at the base, slightly darker as elevation
increases. background.0 (panel base) `#f5f5f8`,
through background.3 (highest raised) `#b0bcc8`.
background.tx becomes a warm cream `#ffeacc` to
preserve the "transmit area" feel without overwhelming
the surrounding light panels. background.spectrum
flips to white `#ffffff`.
Accents — pulled toward the darker end of each hue family for
contrast against the light canvas. Cyan accent
`#00b4d8` → `#0088b0`; warning amber
`#ffb84d` → `#d08010`; danger red
`#ff4d4d` → `#c02020`; success green
`#4dd87a` → `#1a8040`.
Text — full inversion of the dark ramp. primary
`#c8d8e8` → `#1a2a3a` (dark blue-grey on light);
secondary, label, disabled tiers shift accordingly.
Borders — light grey at the subtle tier, medium grey at strong,
accent border matches accent colour. TX border keeps
its warm cast: `#5a4a28` → `#b08840`.
Meters — semantic colours preserved (red crest, cyan rms,
amber threshold, gold gain-reduction). All darkened
for contrast on the light meter background. peak
reads dark on light (`#1a2a3a`) instead of near-white.
Spectrum — trace, peakHold, average shift to the darker tier of
their dark-theme hues. grid lightens from
`#1a2330` to `#d0d8e0` so it's visible-but-subtle
against the white spectrum canvas.
Waterfall — colormap gradients UNCHANGED across all 5 presets.
The waterfall renders OPAQUE pixels from a heatmap;
the 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 rather than 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 +1 line registering the new
theme as `:/themes/default-light.json`.
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")
with no further code changes.
## Build verified clean
All 320 targets including every test target. Both themes appear in
the bundled qrc_resources.cpp.
## Phase 4 status
Phase 4 is now complete:
✅ Slice indicators runtime themed (#3121)
✅ Waterfall colormap runtime themed (#3122)
✅ Default Light theme (this commit)
Phase 5 picks up the theme editor UI (modeless dialog with inspector
mode), import/export, and custom-theme persistence in
~/.config/AetherSDR/themes/.
73, Jeremy KK7GWY & Claude (AI dev partner)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Default-light shipped in this PR's first commit but had no user-reachable
switch — `setActiveTheme()` existed only as an API call with no UI
binding. An operator had to edit the AppSettings file by hand to load
the light theme, which defeats the whole point of bundling it.
Adds a "Theme" submenu under the View menu that:
* Lists every theme `ThemeManager::availableThemes()` knows about
(built-ins under `:/themes/`, plus user themes under
`~/.config/AetherSDR/themes/` once Phase 5's editor starts writing
there).
* Marks the currently active theme via QActionGroup.
* On click, calls `setActiveTheme(name)` which:
- swaps the token table
- persists `ActiveTheme` to AppSettings
- emits `themeChanged()`, which kicks every widget registered
through `applyStyleSheet` into a re-paint with the new tokens.
* Rebuilds itself on `themeChanged` so Phase-5 user themes appearing
in the user dir get picked up without a restart, and so the check-
mark moves to the right entry after the swap.
Same lazy-rebuild pattern as the existing Band Plan submenu so the
implementation is self-explanatory by analogy.
73, Jeremy KK7GWY & Claude (AI dev partner)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
11 tasks
This was referenced May 25, 2026
ten9876
added a commit
that referenced
this pull request
May 25, 2026
…+ Save As) (#3130) ## Summary First user-facing surface of the theming subsystem. Operators can now edit any colour token in the active theme and ship a custom theme to disk, all without touching a JSON file. **View → Theme Editor…** opens a modeless dialog with: - "Editing: \<active theme\>" header - Filter bar — type-to-match against dotted token names (e.g. \`accent\`, \`slice\`, \`meter\`) - Sorted list of every colour token with a 16×16 swatch + hex value per row - Click row → \`QColorDialog\` opens seeded with the current value - Accept → \`ThemeManager::setColor()\` → emits \`themeChanged\` → every widget registered through \`applyStyleSheet\` repaints on the next event-loop turn. **No restart**. - "Save As…" prompts for a theme name and writes the current tokens to \`~/.config/AetherSDR/themes/\<name\>.json\` via \`saveCurrentThemeAs()\`. The new theme is registered + made active immediately. ## ThemeManager additions | Method | Purpose | |---|---| | \`allTokenKeys()\` | Sorted enumeration of every loaded token — used by the editor to populate its list. | | \`setColor(token, color)\` | In-memory mutation + themeChanged. Hex round-trip so \`cssFragment()\` / \`resolve()\` pick up the new value the same way they pick up loaded tokens. No-op on a same-value write. | | \`setSizing(token, int)\` | Integer-token equivalent for panel padding / border widths. Not yet exposed in the editor UI; will land in PR 2 alongside font/sizing surface. | | \`saveCurrentThemeAs(name)\` | Rebuilds nested JSON from the flat dotted-key map (so on-disk shape matches bundled themes), writes to user themes dir, registers + activates. Handles QString/int/double/bool scalars + full \`ThemeGradient\` round-trip so the waterfall colormap and slice.dim tokens persist correctly. | ## Explicitly NOT in this PR (deferred to Phase 5 PR 2-4) 1. **Inspector mode** (click-on-widget to find tokens that paint it). \`ThemeManager::tokensForWidget()\` already exposes the reverse-map populated by \`applyStyleSheet\`; wiring is a few hundred lines once we settle on the latch/unlatch UX. Probably matches the AetherControl double-click convention (#3103). 2. **Gradient editing** for \`waterfall.colormap.{default,grayscale,fire,…}\` and \`slice.dim.{a..h}\`. The list filters them out via the \`brush().gradient()\` probe so they don't render as misleading-first-stop swatches. 3. **Font / sizing editing** — same Phase 5 PR 2 surface as the inspector mode. 4. **Import** (file picker for arbitrary JSON), drag-and-drop, theme deletion, theme renaming. ## Build verified clean - [x] AetherSDR builds clean - [x] All 320 targets including every test target ## Phase status | Phase | Item | Status | |---|---|---| | 1 | ThemeManager scaffolding + JSON loader | ✅ | | 2 | Gradient tokens + applyStyleSheet reverse-map + mass migration | ✅ | | 3 | Paint-code migration + canonical-table closeout | ✅ | | 4 | Default Light theme | ⏳ (#3129, in review) | | **5** | **Editor UI (PR 1: live colour editing + Save As)** | **this PR** | ## Test plan - [ ] CI: build / check-paths / check-windows / CodeQL - [ ] Visual smoke: - [ ] View → Theme Editor… opens the modeless dialog - [ ] Title shows "Editing: Default Dark" (or whatever's active) - [ ] Filter bar — type \`accent\` → only accent.* rows visible - [ ] Click an accent row → QColorDialog opens, pick a wild hue, accept → that colour appears immediately wherever \`color.accent\` paints (most buttons, slider handles, badges) - [ ] Save As… → name it "Test", confirm: - File written to \`~/.config/AetherSDR/themes/Test.json\` - Title bar updates to "Editing: Test" - View → Theme submenu (from #3129) now lists "Test" - [ ] Switch back to Default Dark via the View menu → original colours restored, custom edits gone (file on disk preserved) - [ ] Reopen Test from the menu → custom edits restored from the saved file 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>
G6PWY-Chris
pushed a commit
to G6PWY-Chris/AetherSDR
that referenced
this pull request
Jun 22, 2026
…+ Save As) (aethersdr#3130) ## Summary First user-facing surface of the theming subsystem. Operators can now edit any colour token in the active theme and ship a custom theme to disk, all without touching a JSON file. **View → Theme Editor…** opens a modeless dialog with: - "Editing: \<active theme\>" header - Filter bar — type-to-match against dotted token names (e.g. \`accent\`, \`slice\`, \`meter\`) - Sorted list of every colour token with a 16×16 swatch + hex value per row - Click row → \`QColorDialog\` opens seeded with the current value - Accept → \`ThemeManager::setColor()\` → emits \`themeChanged\` → every widget registered through \`applyStyleSheet\` repaints on the next event-loop turn. **No restart**. - "Save As…" prompts for a theme name and writes the current tokens to \`~/.config/AetherSDR/themes/\<name\>.json\` via \`saveCurrentThemeAs()\`. The new theme is registered + made active immediately. ## ThemeManager additions | Method | Purpose | |---|---| | \`allTokenKeys()\` | Sorted enumeration of every loaded token — used by the editor to populate its list. | | \`setColor(token, color)\` | In-memory mutation + themeChanged. Hex round-trip so \`cssFragment()\` / \`resolve()\` pick up the new value the same way they pick up loaded tokens. No-op on a same-value write. | | \`setSizing(token, int)\` | Integer-token equivalent for panel padding / border widths. Not yet exposed in the editor UI; will land in PR 2 alongside font/sizing surface. | | \`saveCurrentThemeAs(name)\` | Rebuilds nested JSON from the flat dotted-key map (so on-disk shape matches bundled themes), writes to user themes dir, registers + activates. Handles QString/int/double/bool scalars + full \`ThemeGradient\` round-trip so the waterfall colormap and slice.dim tokens persist correctly. | ## Explicitly NOT in this PR (deferred to Phase 5 PR 2-4) 1. **Inspector mode** (click-on-widget to find tokens that paint it). \`ThemeManager::tokensForWidget()\` already exposes the reverse-map populated by \`applyStyleSheet\`; wiring is a few hundred lines once we settle on the latch/unlatch UX. Probably matches the AetherControl double-click convention (aethersdr#3103). 2. **Gradient editing** for \`waterfall.colormap.{default,grayscale,fire,…}\` and \`slice.dim.{a..h}\`. The list filters them out via the \`brush().gradient()\` probe so they don't render as misleading-first-stop swatches. 3. **Font / sizing editing** — same Phase 5 PR 2 surface as the inspector mode. 4. **Import** (file picker for arbitrary JSON), drag-and-drop, theme deletion, theme renaming. ## Build verified clean - [x] AetherSDR builds clean - [x] All 320 targets including every test target ## Phase status | Phase | Item | Status | |---|---|---| | 1 | ThemeManager scaffolding + JSON loader | ✅ | | 2 | Gradient tokens + applyStyleSheet reverse-map + mass migration | ✅ | | 3 | Paint-code migration + canonical-table closeout | ✅ | | 4 | Default Light theme | ⏳ (aethersdr#3129, in review) | | **5** | **Editor UI (PR 1: live colour editing + Save As)** | **this PR** | ## Test plan - [ ] CI: build / check-paths / check-windows / CodeQL - [ ] Visual smoke: - [ ] View → Theme Editor… opens the modeless dialog - [ ] Title shows "Editing: Default Dark" (or whatever's active) - [ ] Filter bar — type \`accent\` → only accent.* rows visible - [ ] Click an accent row → QColorDialog opens, pick a wild hue, accept → that colour appears immediately wherever \`color.accent\` paints (most buttons, slider handles, badges) - [ ] Save As… → name it "Test", confirm: - File written to \`~/.config/AetherSDR/themes/Test.json\` - Title bar updates to "Editing: Test" - View → Theme submenu (from aethersdr#3129) now lists "Test" - [ ] Switch back to Default Dark via the View menu → original colours restored, custom edits gone (file on disk preserved) - [ ] Reopen Test from the menu → custom edits restored from the saved file 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
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
Wiring
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
Phase 4 status — complete
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
73, Jeremy KK7GWY & Claude (AI dev partner)
🤖 Generated with Claude Code