Skip to content

feat(theme): toggle button namespaces with per-tribe + per-applet overrides#3198

Merged
ten9876 merged 4 commits into
aethersdr:mainfrom
nigelfenton:feat/theme-toggle-namespaces
May 27, 2026
Merged

feat(theme): toggle button namespaces with per-tribe + per-applet overrides#3198
ten9876 merged 4 commits into
aethersdr:mainfrom
nigelfenton:feat/theme-toggle-namespaces

Conversation

@nigelfenton
Copy link
Copy Markdown
Contributor

Second control-type carve-out following the slider + knob pattern from #3188. References the toggle-buttons bullet under "follow-up sweeps" in #3184.

Why tribes (and not a single namespace)

Where sliders have one canonical visual identity (track + fill), toggle buttons carry semantic colour: the "ON" state communicates meaning — enable / activate / warning / generic-mode — not just a value. A single color.toggle.background.checked token can't represent all four colour tribes that exist in the current codebase (green-success, blue-accent, amber-warning, deep-blue-panel), so the carve-out splits the checked-state tokens across three tribes:

enum class ToggleTribe { Accent, Success, Warning };

Unchecked + disabled state styling is shared across tribes. Only the Accent tribe additionally carries per-applet overrides through the v2 scope tree — Success and Warning are semantic and look identical wherever they live.

Namespaces (16 new tokens, both bundled themes)

color.toggle.background           ← {color.gray.800}   (alias to primitive)
color.toggle.foreground           ← {color.gray.200}
color.toggle.border               ← {color.gray.700}
color.toggle.background.disabled  ← {color.gray.900}
color.toggle.foreground.disabled  ← {color.gray.600}
color.toggle.border.disabled      ← {color.gray.900}

color.toggle.accent.background.checked    ← {color.blue.700}    (TX red / RX green / comp amber per applet)
color.toggle.accent.foreground.checked    ← {color.blue.500}
color.toggle.accent.border.checked        ← {color.blue.500}

color.toggle.success.background.checked   ← #006040 / #c8e8d0
color.toggle.success.foreground.checked   ← {color.green.500}
color.toggle.success.border.checked       ← {color.green.500}

color.toggle.warning.background.checked   ← #5a3a0a / #f5e8d0
color.toggle.warning.foreground.checked   ← {color.amber.500}
color.toggle.warning.border.checked       ← {color.amber.500}

Also adds color.background.warning primitive (dark #5a3a0a / light #f5e8d0) for tribe symmetry with the existing color.background.success (which was added in PR #3130).

All aliases resolve directly to primitives (single-hop) so the resolveAlias() lookup against m_primitives succeeds — chained semantic→semantic aliases don't resolve through the current path, which would land the literal {color.background.1} string into QSS / color() callers and break rendering. Caught and fixed during test development.

Cascade — root → applet → applet/<name>

Per-applet overrides live under scopes.applet.scopes.<name>.tokens alongside the existing slider + knob overrides. Single-token override (background.checked only) matches the slider precedent of one token per applet:

"applet": {
  "scopes": {
    "tx":   { "tokens": { ..., "color.toggle.accent.background.checked": "{color.red.500}" } },
    "rx":   { "tokens": { ..., "color.toggle.accent.background.checked": "{color.green.500}" } },
    "comp": { "tokens": { ..., "color.toggle.accent.background.checked": "{color.amber.500}" } }
  }
}

What lands

File Role
resources/themes/default-dark.json, default-light.json Token defs + per-applet overrides + new color.background.warning primitive
src/core/ThemeManager.cpp seedBuiltinDefaults extension (16 token seeds + 3 applet-scope overrides) so user themes forked before this PR still resolve correctly
src/gui/Theme.h ToggleTribe enum + applyToggleButtonStyle(btn, tribe) helper + template
src/gui/CatControlApplet.cpp Drop inline kGreenToggle constant, route through helper (Success tribe) — single source of truth going forward
tests/theme_manager_test.cpp Coverage for base + per-tribe + per-applet cascade
docs/theming/toggle-button-tokens.md Following the slider-knob-tokens.md shape with the migration pattern for the next sweep

Why no global QSS rule

Unlike sliders, this PR does NOT add a QPushButton:checked rule to appStylesheetTemplate. Every checkable button in the codebase that currently has no explicit :checked styling would have suddenly acquired the Accent-tribe look — a subtle but real visual regression for ~30+ sites that aren't part of this sweep. Opt-in via the helper keeps the namespace landing isolated; the global QSS rule can land in the follow-up after per-site auditing.

Verification (Windows 11, MSVC + Ninja + Qt 6.10.3, branched from aethersdr/AetherSDR main at 4ee99f78)

Unit tests:

  • 10/10 new toggle assertions pass — base token validity, per-tribe checked values, per-applet cascade for accent (tx=#ff4d4d, rx=#4dd87a, comp=#ffb84d), success + warning stable across applet scopes, isOverriddenAt distinguishes own override from inherited.
  • The 4 pre-existing failures on this branch (lines 452/454/505/644 — importThemeFromFile related) are also present on clean main at the same source lines (398/400/451/590 there). Not introduced here. Looks like a Windows-vs-CI environment difference around temp dir handling. Happy to investigate separately if useful.

Theme Editor inspection (live cascade):

Scope color.toggle.accent.background.checked resolved
root #0070c0 (blue, alias to {color.blue.700})
applet/tx #ff4d4d (red, alias to {color.red.500}) — verified in Editor columnar view
applet/rx #4dd87a (green) — verified in Editor
applet/comp #ffb84d (amber) — verified in Editor; columnar view shows root #0070c0 / applet inherited / comp #ffb84d

CatControlApplet helper-driven refactor:

  • Default Dark, :checked#006040 dark green background, #4dd87a text/border — identical to pre-PR.
  • Default Light, :checked#c8e8d0 soft mint background, #1a8040 text/border — identical to post-cleanup(theme): swap kGreenToggle #006040 for color.background.success (#3141) #3195.
  • Docked + floating variants both verified.
  • Live theme-switch (Dark↔Light without restart) re-renders cleanly via the helper's applyStyleSheet registration.

Cross-test sanity: ran the full test suite. Other test failures (async_log_writer_test, CAT_Flex_test, CAT_TS-2000_test, ole_compound_file_test, rigctld_test) are unrelated to theme work and present on clean main — not introduced here either.

One intentional addition to flag

The new helper template includes a QPushButton:disabled rule that the original kGreenToggle constant didn't have. In practice the Enable CAT button is never set disabled in CatControlApplet, so this is invisible there. The addition is intentional — gives every toggle that opts into the helper a consistent disabled appearance using the color.toggle.*.disabled tokens.

Out of scope (follow-up sweep)

  • Per-site QSS migrationAetherDspWidget, AetherialAudioStrip, AntennaGeniusApplet, AppletPanel, ClientChainApplet, ClientCompApplet (bypass toggle), ClientCompEditor, etc. Each currently has its own inline QPushButton:checked stylesheet; multiple sites per file, each needs a tribe judgement before migration. Mirrors the punt on hardcoded slider sites in feat(theme): carve sliders + knobs into dedicated namespaces with per-applet overrides #3188.
  • Multi-token per-applet overrides — current override on applet/tx only touches color.toggle.accent.background.checked; foreground.checked and border.checked stay at the root blue, which on a red background reads visually inconsistent (red bg + blue border + blue text). Adding red.700/green.700/amber.700 primitives, or per-applet overrides of all three checked tokens, is the next design step. Documented in the Out of scope section of toggle-button-tokens.md.
  • Indicator-style togglesQCheckBox::indicator / QRadioButton::indicator are a different visual primitive, separate namespace + sweep.
  • Hover / pressed state tokens — same deferral as sliders.
  • color.toggle.panel.* tribeAppletPanel.cpp's deep blue #0a3060 for tab-like toggles doesn't fit Accent / Success / Warning cleanly. A fourth panel or muted tribe may earn its keep in a later sweep.
  • Global QPushButton:checked rule in appStylesheetTemplate — per-site auditing first.

cc @ten9876 @jensenpat @chibondking

73 Nigel G0JKN

🤖 Generated with Claude Code

…rrides

Second control-type carve-out following the slider + knob pattern from
PR aethersdr#3188. Where sliders have one canonical visual identity, toggle
buttons carry semantic colour — the carve-out splits checked-state
across three tribes (accent / success / warning); unchecked + disabled
styling is shared. The accent tribe additionally picks up per-applet
background overrides (TX red / RX green / comp amber) through the v2
scope tree, matching the slider precedent.

Also introduces the color.background.warning primitive for tribe
symmetry with the existing color.background.success.

What lands:
- 16 new color.toggle.* tokens in both bundled themes, plus per-applet
  color.toggle.accent.background.checked overrides alongside the
  existing slider+knob overrides.
- seedBuiltinDefaults extended (raw-hex root entries + per-applet
  scope-tree seeds) so user themes forked before this PR still resolve
  the canonical look + applet differentiation.
- ToggleTribe enum + applyToggleButtonStyle helper in Theme.h. Single
  source of truth for checkable QPushButton appearance; opt-in (no
  change to global QSS).
- docs/theming/toggle-button-tokens.md following the slider-knob doc
  shape, with the migration pattern for the next sweep documented.
- theme_manager_test coverage for base + per-tribe + per-applet
  cascade (tx red, rx green, comp amber for accent; success +
  warning stable across applet scopes).

Site migration in this PR (kept tight):
- CatControlApplet — drops the inline kGreenToggle constant in favour
  of applyToggleButtonStyle(btn, ToggleTribe::Success). Same visual
  result; the helper is the single source of truth going forward.

Verification on Windows 11 (MSVC + Ninja + Qt 6.10.3):
- theme_manager_test: all new toggle assertions pass (10/10).
  The 4 pre-existing failures on this branch (lines 452/454/505/644 —
  importThemeFromFile related) are also present on clean main at
  the same source lines (398/400/451/590 there). Not introduced here.
- Theme Editor cascade visible at all three applet scopes:
  applet/tx → #ff4d4d (red), applet/rx → #4dd87a (green),
  applet/comp → #ffb84d (amber). Inspector columnar view confirms
  the override at each child scope while parent + grandparent show
  "inherited".
- CatControlApplet Enable CAT button: identical visual in Default
  Dark (#006040) and Default Light (#c8e8d0), docked + floating.
  Live theme-switch (Dark↔Light without restart) re-renders cleanly
  via the helper's applyStyleSheet registration.

Out of scope (follow-up sweep):
- Per-site QSS migration in AetherDspWidget, AetherialAudioStrip,
  AntennaGeniusApplet, AppletPanel, ClientChainApplet, ClientCompApplet
  (bypass toggle), ClientCompEditor, etc. — each has its own inline
  :checked stylesheet; multiple sites per file, each needs a tribe
  judgement.
- Multi-token per-applet overrides — TX/RX/comp currently only
  override color.toggle.accent.background.checked; foreground.checked
  and border.checked stay at the root blue, which on a red/green/amber
  background reads visually inconsistent. Adding red.700/green.700/
  amber.700 primitives or per-applet overrides of all three checked
  tokens is the next design step.
- Indicator-style toggles (QCheckBox::indicator / QRadioButton::indicator)
  — different visual primitive, separate namespace + sweep.
- Hover / pressed state tokens — same deferral as sliders.
- Global QPushButton:checked rule in appStylesheetTemplate — needs
  per-site auditing of every existing checkable button first, so this
  PR keeps the namespace landing isolated to opt-in via the helper.

cc @ten9876 @jensenpat @chibondking

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nigelfenton nigelfenton requested review from a team as code owners May 26, 2026 21:58
Copy link
Copy Markdown
Contributor

@aethersdr-agent aethersdr-agent Bot left a comment

Choose a reason for hiding this comment

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

Thanks @nigelfenton — this is a clean, well-scoped PR that follows the slider/knob namespace pattern from #3188 faithfully. The carve-out of three semantic tribes (only Accent carrying per-applet overrides) reads correctly, the dual seeding (bundled JSON + seedBuiltinDefaults) is consistent with prior carve-outs so pre-PR forked themes won't regress, and the test coverage on the per-applet Accent cascade plus the "Success/Warning don't move per scope" assertions is exactly the right shape.

A few small observations, none blocking:

1. color.background.warning primitive is added but not consumed by an alias.
The new color.background.warning token (dark #5a3a0a / light #f5e8d0) is added for tribe symmetry with color.background.success, but color.toggle.warning.background.checked is set as a literal hex (matching the success precedent because of the chained-alias resolution constraint you noted in the PR body). So nothing currently aliases through color.background.warning. That's fine for symmetry/documentation, just worth flagging that the primitive is currently load-bearing only as a hook for future use.

2. Light-theme disabled-state contrast.
In default-light.json, color.toggle.foreground.disabled = {color.gray.500} (#a0b0c0) on background.disabled = {color.gray.900} (#f5f5f8) is a fairly low-contrast pair. Disabled is meant to be subtle so this may be intentional, and since CatControlApplet's Enable CAT button is never set disabled the visual won't surface from this PR — but worth a sanity check before the per-site QSS migration sweep lands more Success/Warning toggles into disabled-capable sites.

3. Hover state is tribe-agnostic.
The helper's QPushButton:hover rule paints {{color.background.2}} regardless of tribe, which matches the original kGreenToggle behaviour bit-for-bit (so no regression at the migrated CAT site) and matches the "hover/pressed tokens deferred — same as sliders" callout. Just noting that when a Success-tribe toggle is unchecked and hovered, the user sees a generic gray rather than a green-family tint. Fine for now given the explicit deferral.

Convention/safety check:

  • Helper sits in namespace AetherSDR alongside the slider helper, mirrors its signature, and has the if (!btn) return; null-guard. ✓
  • All new color.toggle.* aliases in both bundled themes resolve single-hop to primitives, so the resolveAlias/m_primitives constraint is respected. ✓
  • File scope matches the PR body — no out-of-scope drive-bys. ✓
  • The intentional QPushButton:disabled addition vs. the old kGreenToggle is called out in the body and verified invisible at the migrated site. ✓

LGTM as a foundation for the follow-up per-site migration sweep. Thanks for keeping the global QSS rule deferral explicit — that's the right call given the ~30+ existing inline :checked sites.


🤖 aethersdr-agent · cost: $10.5573 · model: claude-opus-4-7

ten9876 pushed a commit that referenced this pull request May 27, 2026
…dioSetup / FlexControl) (#3204)

Closes the second half of Pat's "both" answer to **(a)** the toggle
namespace work (landed as
[#3198](#3198)) and **(b)**
the hardcoded-hex slider sites called out in
[#3188](#3188 *Out of
scope* section.

## Background

[#3188](#3188) tagged four
sites as bypassing the `color.slider.*` namespace entirely:

> **Hardcoded-hex slider sites** in `PanadapterApplet`, `EqApplet`,
`RadioSetupDialog`, `FlexControlDialog` bypass the theme system
entirely. Each needs its local stylesheet dropped in favour of the
global QSS + per-applet override path.

Each site used inline `setStyleSheet(...)` calls full of hardcoded hex
values — so the v2 per-applet override path couldn't reach them, and
theme switches required an app restart. This PR tokenizes the colours
and routes through `ThemeManager::applyStyleSheet` so:

- the per-applet override cascade lights up automatically once a
relevant scope exists (none of the four currently sits under
`applet/tx|rx|comp`, so the immediate effect at root scope is the
canonical Wave-blue / accent-success treatment);
- live theme switching (Dark ↔ Light without restart) re-resolves the
tokens and repaints, matching the rest of the namespace-aware UI.

## Approach — Option A (tokenize colours, preserve sizes)

Site-local slider **dimensions** are preserved intentionally — the four
sites each chose bespoke handle widths / groove heights for the UX
context they live in (Panadapter's tiny 8px handle for the CW bar,
EqApplet's 10×16 vertical handle for band-column rhythm, RadioSetup's
emphasis-sized 14px handle, FlexControl's 16px bordered ring).

Homogenizing them via `applyPrimarySliderStyle` would have been a more
literal interpretation of "drop the local stylesheet" but would have
introduced visible regressions across all four sites. **Option A**
(tokenize colours, preserve sizes) achieves Pat's stated goal — sliders
honour the theme system — without the visual cost.

## Per-site notes

| File | Sliders | Token migration |
|---|---|---|
| `PanadapterApplet.cpp` | 2 pitch sliders (CW decoder bar) | `#1a2a3a`
groove → `{{color.slider.background}}`<br>`#6a8090` handle →
`{{color.slider.handle}}` |
| `EqApplet.cpp` | 8 vertical EQ band sliders | `#203040` groove →
`{{color.slider.background}}`<br>`#00b4d8` handle →
`{{color.slider.foreground}}` (the EQ idiom puts the accent colour on
the handle itself — no sub-page rule) |
| `RadioSetupDialog.cpp` | 3 filter-sharpness sliders (voice / cw /
digital) | `#1a2a3a` groove → `{{color.slider.background}}`<br>`#c8d8e8`
handle → `{{color.slider.handle}}`<br>(`lineoutSlider` + `hpSlider`
already inherit from the global QSS — no migration needed) |
| `FlexControlDialog.cpp` | 2 sliders (spin / sensitivity) inside the
dialog-wide `kFlexControlStyle` template | `#162437` groove →
`{{color.slider.background}}`<br>`#65d379` sub-page →
`{{color.accent.success}}` (preserves FlexControl green
identity)<br>`#d8e2ef` handle → `{{color.slider.handle}}`<br>`#65d379`
handle border → `{{color.accent.success}}`<br>Body widget switched to
`applyStyleSheet` so the tokens resolve + live re-theme registers |

## Verification

Windows 11 (MSVC + Ninja + Qt 6.10.3) on top of `aethersdr/AetherSDR`
main at `4ee99f78`.

| Site | Default Dark | Default Light |
|---|---|---|
| PanadapterApplet pitch sliders | ✓ dark groove + light handle | ✓
light groove + dark handle — **live re-theme adapts cleanly** |
| EqApplet band sliders | ✓ dark groove + accent-cyan handle | ✓ handle
resolves to light-theme accent (`#0088b0`) |
| RadioSetupDialog filter sliders | ✓ dark groove + light handle | ✓
light groove + dark handle — **live re-theme adapts cleanly** |
| FlexControlDialog spin + sensitivity | ✓ dark groove + green sub-page
+ bordered handle | ✓ sub-page green preserved (the green is
`color.accent.success` which is green in both bundled themes; gray
shades adapt around it) |

`theme_manager_test`: zero new failures. The 4 pre-existing
`importThemeFromFile`-related failures on clean main remain
(Windows-vs-CI env issue, not introduced here).

## Observed during testing — out of scope for this PR

While verifying in Default Light, two **broader migration debt** items
came to light that go beyond this slider sweep:

- **`RadioSetupDialog` light-theme contrast** — `kLabelStyle` (`color:
#c8d8e8`), `kValueStyle` (`color: #00c8ff`), `kGroupStyle` (border
`#304050`, title `#8aa8c0`), and `kEditStyle` (background `#1a2a3a`) all
hardcode dark-theme colours. Labels render as low-contrast pale text
against the light-theme `#f5f5f8` background, and edit fields look
misplaced. Migrating those constants needs the same `{{token}}` +
`applyStyleSheet` pattern and touches dozens of call sites across the
dialog — a separate follow-up PR.
- **`FlexControlDialog` whole-dialog migration debt** — the dialog
template hardcodes `#0b1625` button backgrounds, `#31455f` borders,
`#65d379` FlexControl-green accents throughout. The slider rules now
adapt to theme; the rest of the dialog stays dark in light theme. A
separate "FlexControl light-theme migration" PR would close this;
touching it here would have ballooned the slider-cleanup scope.

Both observations were flagged as candidates for the next cleanup sweep
that follows this one.

## Together with #3198

This PR completes Pat's "both" answer. With #3198 (toggle namespace
carve-out) and this PR (slider site cleanup) merged, the four
originally-cited problem sites from #3188's Out-of-scope section all
become theme-system-aware and the toggle button class joins the slider +
knob namespace pattern as a third carved-out widget type.

cc @ten9876 @jensenpat @chibondking

73 Nigel G0JKN

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876
Copy link
Copy Markdown
Collaborator

ten9876 commented May 27, 2026

Hey @nigelfenton — landed #3199 ahead of this and the GitHub Update-Branch API hit a conflict (most likely in tests/theme_manager_test.cpp where both PRs append tests in main()). When you have a moment, a local git rebase origin/main should resolve it mechanically. Once CI's green again I'll land it.

73, Jeremy KK7GWY & Claude (AI dev partner)

ten9876 and others added 3 commits May 27, 2026 08:09
…espaces

# Conflicts:
#	tests/theme_manager_test.cpp
The "inheritance walk picks up parent's override" test queried
colorAt("scopeB/leaf", ...) before scopeB/leaf existed as a scope
object.  lookupRaw falls back to root scope when scopeForPath returns
nullptr, so the query jumped over scopeB straight to root and returned
the root colour instead of the inherited override.

Production editor flow creates the leaf via setColor/setSizing before
querying it — this test now matches.  Pre-existing failure on main;
folded into this PR's merge per @ten9876.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 merged commit b99aaf1 into aethersdr:main May 27, 2026
4 checks passed
@nigelfenton nigelfenton deleted the feat/theme-toggle-namespaces branch May 27, 2026 17:54
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.

2 participants