feat(theme): toggle button namespaces with per-tribe + per-applet overrides#3198
Conversation
…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>
There was a problem hiding this comment.
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 AetherSDRalongside the slider helper, mirrors its signature, and has theif (!btn) return;null-guard. ✓ - All new
color.toggle.*aliases in both bundled themes resolve single-hop to primitives, so theresolveAlias/m_primitivesconstraint is respected. ✓ - File scope matches the PR body — no out-of-scope drive-bys. ✓
- The intentional
QPushButton:disabledaddition vs. the oldkGreenToggleis 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
…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>
|
Hey @nigelfenton — landed #3199 ahead of this and the GitHub Update-Branch API hit a conflict (most likely in 73, Jeremy KK7GWY & Claude (AI dev partner) |
…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>
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.checkedtoken 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: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)
Also adds
color.background.warningprimitive (dark#5a3a0a/ light#f5e8d0) for tribe symmetry with the existingcolor.background.success(which was added in PR #3130).All aliases resolve directly to primitives (single-hop) so the
resolveAlias()lookup againstm_primitivessucceeds — 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>.tokensalongside the existing slider + knob overrides. Single-token override (background.checkedonly) matches the slider precedent of one token per applet:What lands
resources/themes/default-dark.json,default-light.jsoncolor.background.warningprimitivesrc/core/ThemeManager.cppseedBuiltinDefaultsextension (16 token seeds + 3 applet-scope overrides) so user themes forked before this PR still resolve correctlysrc/gui/Theme.hToggleTribeenum +applyToggleButtonStyle(btn, tribe)helper + templatesrc/gui/CatControlApplet.cppkGreenToggleconstant, route through helper (Success tribe) — single source of truth going forwardtests/theme_manager_test.cppdocs/theming/toggle-button-tokens.mdWhy no global QSS rule
Unlike sliders, this PR does NOT add a
QPushButton:checkedrule toappStylesheetTemplate. Every checkable button in the codebase that currently has no explicit:checkedstyling 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/AetherSDRmain at4ee99f78)Unit tests:
#ff4d4d, rx=#4dd87a, comp=#ffb84d), success + warning stable across applet scopes,isOverriddenAtdistinguishes own override from inherited.importThemeFromFilerelated) are also present on cleanmainat 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):
color.toggle.accent.background.checkedresolvedroot#0070c0(blue, alias to{color.blue.700})applet/tx#ff4d4d(red, alias to{color.red.500}) — verified in Editor columnar viewapplet/rx#4dd87a(green) — verified in Editorapplet/comp#ffb84d(amber) — verified in Editor; columnar view shows root#0070c0/ appletinherited/ comp#ffb84dCatControlApplet helper-driven refactor:
:checked→#006040dark green background,#4dd87atext/border — identical to pre-PR.:checked→#c8e8d0soft mint background,#1a8040text/border — identical to post-cleanup(theme): swap kGreenToggle #006040 for color.background.success (#3141) #3195.applyStyleSheetregistration.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:disabledrule that the originalkGreenToggleconstant didn't have. In practice the Enable CAT button is never set disabled inCatControlApplet, so this is invisible there. The addition is intentional — gives every toggle that opts into the helper a consistent disabled appearance using thecolor.toggle.*.disabledtokens.Out of scope (follow-up sweep)
AetherDspWidget,AetherialAudioStrip,AntennaGeniusApplet,AppletPanel,ClientChainApplet,ClientCompApplet(bypass toggle),ClientCompEditor, etc. Each currently has its own inlineQPushButton:checkedstylesheet; 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.applet/txonly touchescolor.toggle.accent.background.checked;foreground.checkedandborder.checkedstay at the root blue, which on a red background reads visually inconsistent (red bg + blue border + blue text). Addingred.700/green.700/amber.700primitives, or per-applet overrides of all three checked tokens, is the next design step. Documented in the Out of scope section oftoggle-button-tokens.md.QCheckBox::indicator/QRadioButton::indicatorare a different visual primitive, separate namespace + sweep.color.toggle.panel.*tribe —AppletPanel.cpp's deep blue#0a3060for tab-like toggles doesn't fit Accent / Success / Warning cleanly. A fourthpanelormutedtribe may earn its keep in a later sweep.QPushButton:checkedrule inappStylesheetTemplate— per-site auditing first.cc @ten9876 @jensenpat @chibondking
73 Nigel G0JKN
🤖 Generated with Claude Code