feat(theme): carve sliders + knobs into dedicated namespaces with per-applet overrides#3188
Merged
Conversation
…s only) First slice of carving slider and knob theming out of the generic color.accent / color.background.1 / color.text.primary categories they currently share with buttons, panels, body text, etc. Adds 12 new tokens (6 per namespace) to both bundled themes, aliased to existing primitives so the rendered output is bit-identical. Slider tokens (Default Dark / Default Light): color.slider.background = gray.800 / gray.700 color.slider.foreground = blue.500 / blue.500 color.slider.handle = gray.200 / gray.50 color.slider.background.disabled = gray.850 / gray.800 color.slider.foreground.disabled = gray.600 / gray.500 color.slider.handle.disabled = gray.500 / gray.400 Knob tokens (parallel namespace for ClientCompKnob + PhaseKnob): color.knob.background = gray.700 / gray.700 color.knob.foreground = blue.500 / blue.500 color.knob.handle = gray.200 / gray.50 color.knob.background.disabled = gray.800 / gray.800 color.knob.foreground.disabled = gray.600 / gray.500 color.knob.handle.disabled = gray.400 / gray.400 Suffix-as-state matches the existing `color.text.disabled` / `color.accent.bright` pattern. Editor groups them naturally in alphabetical order in the token list. No QSS migration yet — Theme.h still references the borrowed tokens. Step 2 (slider QSS migration) and step 3 (knob paint-code migration) follow in subsequent commits. theme_manager_test still passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ation)
Routes every QSlider rendering path through the new color.slider.*
namespace. Rendered output unchanged — the new tokens alias to the
exact primitives the borrowed tokens (color.background.1, color.accent,
color.text.primary) point at, so the visual baseline is identical.
Designers can now retint sliders without rippling into buttons,
borders, body text, or other accent-consumers.
Two QSS surfaces migrated:
1. appStylesheetTemplate() in Theme.h — the global app-wide QSS.
The previous version only had horizontal rules + no add-page
handling. Brings it up to the canonical shape:
* Both orientations (h + v).
* groove + sub-page + add-page + handle, each reading from
color.slider.{background,foreground,background,handle}.
* Disabled-state overrides via :disabled selectors that read
from color.slider.*.disabled tokens.
2. primarySliderStyleTemplate() — the per-call-site helper used by
slicePrimarySliderStyle and TX-warning sliders. The accentToken
parameter that lets call sites override the foreground (per-slice
colours, TX amber) is preserved unchanged — anything that passes
a custom token still wins.
Also migrated TitleBar's two inline slider stylesheets (master
volume + headphone) which previously hardcoded color.background.1 /
color.accent.
Out of scope here: PanadapterApplet, EqApplet, RadioSetupDialog, and
FlexControlDialog each have slider QSS that bypasses the theme system
with hardcoded hex (`#1a2a3a` / `#c8d8e8` / etc). Migrating those is
a separate sweep — they need to drop their local stylesheets entirely
and trust the global QSS to paint them.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…igration)
Routes ClientCompKnob's three knob-component colours (ring background,
indicator arc, pointer) through the new color.knob.* namespace. The
two label/value text colours stay on color.text.* — those render text
below the knob, not knob componentry.
Used by ~10 sites (comp, gate, deess, pudu, reverb, tube, eq output,
phone, ...). Per-applet container overrides (next commit) will
re-tint all of them in one go.
Knob defaults adjusted in both themes so the rendered output stays
bit-identical to pre-change:
* color.knob.background: gray.700 → gray.800
(matches color.background.1's hex — what ClientCompKnob used)
* color.knob.foreground: blue.500 → blue.700
(matches color.accent.dim — the indicator arc's previous tone)
* Disabled variants tightened to match the new defaults' shade.
PhaseKnob deferred — its paintEvent hardcodes 6 raw QColor literals
(no tm.color() lookups at all), so migrating it requires more design
judgment than a rename pass. Filed as a follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
First visible per-applet differentiation through the scope tree.
TX applet sliders + knobs render red, RX renders green, Comp renders
amber — every other slider / knob keeps the canonical blue baseline.
Bundled themes (both light + dark) gain nested-scope overrides under
`scopes.applet.scopes.{tx,rx,comp}`:
* applet/tx: color.slider.foreground = color.knob.foreground = {color.red.500}
* applet/rx: {color.green.500}
* applet/comp: {color.amber.500}
To make the QSS path scope-aware (the global QSS is resolved once at
the main-window scope, which is root), each of TxApplet / RxApplet /
ClientCompApplet now calls applyStyleSheet at its own constructor
with a slim slider override. resolveFor walks the applet's scope
chain (applet/<name> → applet → root) and picks up the foreground
override; Qt's QSS cascade applies it to every QSlider descendant.
ClientCompKnob's color helpers gain a `const QWidget*` parameter and
route through ThemeManager's widget-aware overload, so the applet/comp
scope override naturally reaches the rendered output — no per-knob
applyStyleSheet plumbing needed because knobs are paint-code, not QSS.
Visual verification:
1. Open TX applet — every slider's filled portion is red.
2. Open RX applet — every slider's filled portion is green.
3. Open the compressor applet — every knob arc is amber, sliders too.
4. Every other applet (DAX, EQ, Phone, ...) renders the canonical
blue-500 fill from root scope.
5. Open Theme Editor, pick scope `applet/tx`, see the columnar
view show color.slider.foreground overridden at `tx` column.
Closes the four-step plan for slider + knob theming. PhaseKnob
migration (hardcoded raw QColor literals → tokens) is still
outstanding — separate sweep with design judgment.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…in builtin defaults
The new color.slider.* / color.knob.* tokens weren't applying for users
whose active theme pre-dates this PR (e.g. a "My Default Dark" forked
before the namespace existed). Their theme's JSON has no slider/knob
entries AND no nested-scope overrides, so the lookups returned empty
QSS — sliders fell back to Qt's default rendering instead of carrying
the new canonical look.
Two seeds added to seedBuiltinDefaults():
1. Root-scope colour seeds for the 12 new tokens. Raw hex (not
{alias} primitives) because older user themes may not have a
primitives section. Default Dark / Default Light's JSON
re-asserts via aliases — idempotent.
2. Per-applet scope-tree seeds for applet/tx, applet/rx, applet/comp.
Creates the scopes via scopeOrCreate() and inserts
color.slider.foreground + color.knob.foreground overrides
(red / green / amber). Bundled themes' JSON re-asserts these
too; user themes without them inherit the canonical TX-is-red /
RX-is-green / comp-is-amber semantic.
Net effect: per-applet slider + knob colours now visible regardless
of whether the user is on a bundled theme or an older user-forked copy.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ound
Step 2's QSS migration changed primarySliderStyleTemplate's default
arg but missed its wrapper applyPrimarySliderStyle() — which is what
~20+ call sites actually use. The wrapper kept defaulting to
"color.accent" so every RxApplet / PhoneCwApplet / PhoneApplet /
WaveApplet slider was still resolving against the root-scope cyan,
ignoring the applet/* scope overrides.
Call sites that pass an explicit accentToken (StripFinalOutputPanel
passes color.accent.warning for TX-adjacent amber) keep working
unchanged — that path was already correct.
After this fix:
* applyPrimarySliderStyle(slider) at any depth inside RxApplet
resolves color.slider.foreground via the slider's container
chain → walks to applet/rx → returns green.
* Same for TxApplet (red) and ClientCompApplet (amber).
* All other applets fall through to root scope → blue baseline.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ange The slider tokens were stored correctly and the editor confirmed the applet/tx scope held the red override — but RF Power slider in the TX applet still rendered cyan. Root cause: applyPrimarySliderStyle is called BEFORE the slider is added to its layout (common pattern in TxApplet, RxApplet, PhoneCwApplet — configure, then addWidget). At apply time the slider has no parent, so containerPathFor walks nothing and resolves against root scope. Fix: ThemeManager installs itself as an event filter on every tracked widget. When a QEvent::ParentChange fires, the widget's template is re-resolved against the now-correct scope chain and re-applied. Cheap — ParentChange only fires during initial construction (and on rare deliberate reparents like floating panels). Not a hot path. Net effect: any helper that configures a widget pre-parent works correctly — applyPrimarySliderStyle, applyAppTheme, and the dozens of inline applyStyleSheet sites that happen during ctor bodies all get their scope chain right once the widget is finally attached. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pattern Captures the full pattern from this PR so the next control-type carve-out follows the same shape: - The two new namespaces (color.slider.*, color.knob.*) with state suffix convention matching the existing color.text.disabled pattern. - The cascade (root → applet → applet/<name>) and how it resolves at runtime, with the concrete TX-red / RX-green / Comp-amber example. - seedBuiltinDefaults's role as a compile-time safety net for user themes that pre-date the namespace. - The QSS migration surfaces (global QSS, applyPrimarySliderStyle helper, per-call-site applet stylesheets). - The knob paint-code migration (widget-aware tm.color(w, token) overload routes the cascade naturally — no per-knob applyStyleSheet plumbing). - The ParentChange event filter — the subtle bug we hit (helpers configure widgets pre-parent, resolution locks to root scope) and the generic fix (ThemeManager re-resolves on reparent for every tracked widget). Includes a "pattern for adding new control namespaces" section as the canonical recipe for follow-up sweeps (toggle buttons, spinboxes, progress bars, ...). Lists the four out-of-scope items still to do (PhaseKnob, hardcoded-hex slider sites in 4 dialogs/applets, dialog-scope knob overrides). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…e-out Addresses pre-merge review asks on #3188. Three new test blocks in theme_manager_test: 1. Slider + knob tokens resolve to the canonical Wave-blue look on Default Dark — guards against the seedBuiltinDefaults coverage we added for forked-pre-PR user themes. 2. Per-applet scope cascade resolves correctly: applet/tx → red, applet/rx → green, applet/comp → amber, applet/dax → root blue. isOverriddenAt distinguishes "overridden here" vs "inherited". 3. ParentChange re-resolution: applyStyleSheet on a parentless QLabel resolves to root blue. After setParent to a widget marked themeContainer="applet/tx", the event filter fires and re-resolves to applet/tx red. This is the direct regression guard for the "still cyan" bug that surfaced mid-PR. Code-side comment additions: - loadThemeFromPath now documents the ordering dependency between m_rootScope->children.clear() and seedBuiltinDefaults() (the seeds re-create the per-applet scope children that clear() just wiped). - seedBuiltinDefaults flags the hex-value duplication with the primitives palette in default-dark.json as KEEP IN SYNC, so a future palette shift updates both sites. - applyStyleSheet's installEventFilter call carries a perf note: the early-out keeps per-event cost to one int compare, but with N tracked widgets the cumulative wakeups in drag / paint storms is N×events; gate to "no-parent-at-apply" if profiling flags it. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 26, 2026
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
added a commit
that referenced
this pull request
May 27, 2026
…rrides (#3198) Second control-type carve-out following the slider + knob pattern from [#3188](#3188). References the toggle-buttons bullet under "follow-up sweeps" in [#3184](#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: ```cpp 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: ```json "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-#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 migration** — `AetherDspWidget`, `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 #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 toggles** — `QCheckBox::indicator` / `QRadioButton::indicator` are a different visual primitive, separate namespace + sweep. - **Hover / pressed state tokens** — same deferral as sliders. - **`color.toggle.panel.*` tribe** — `AppletPanel.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](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Jeremy Fielder <kk7gwy@aethersdr.com>
G6PWY-Chris
pushed a commit
to G6PWY-Chris/AetherSDR
that referenced
this pull request
Jun 22, 2026
…-applet overrides (aethersdr#3188) First worked example of carving a control type out of the generic theme categories (`color.accent` / `color.background.1` / `color.text.primary`) into dedicated namespaces with per-applet overrides through the v2 scope tree. Sets the pattern for follow-up sweeps (toggle buttons, spinboxes, progress bars, …). **Draft** — opening early for visibility; more polish may layer on top. ## What's in this PR ### New token namespaces (12 entries) ``` color.slider.background ← groove + add-page color.slider.foreground ← sub-page (filled portion) color.slider.handle ← thumb color.slider.background.disabled color.slider.foreground.disabled color.slider.handle.disabled color.knob.background ← ring / track color.knob.foreground ← indicator arc color.knob.handle ← center pointer / tick color.knob.background.disabled color.knob.foreground.disabled color.knob.handle.disabled ``` State suffix (`.disabled`) matches existing `color.text.disabled` / `color.accent.bright` pattern. Hover / pressed deferred — explicit design call. ### Per-applet cascade (visible result) Bundled themes ship nested-scope overrides: | Scope | `color.slider.foreground` & `color.knob.foreground` | |---|---| | `applet/tx` | `{color.red.500}` | | `applet/rx` | `{color.green.500}` | | `applet/comp` | `{color.amber.500}` | | (everywhere else) | inherits root → `{color.blue.500}` | Open the TX applet → sliders + the comp knob (if a comp lives inside) render red. Open RX → green. Open the compressor applet → amber. All other surfaces stay the canonical blue. ### The ParentChange event filter (general fix) While building this, surfaced a latent bug: helpers like `applyPrimarySliderStyle` are commonly called **before** the widget is added to its parent layout (configure → applyStyle → addWidget). At apply time the widget has no parent, so `containerPathFor` walks nothing and resolves the QSS against root scope. The applet-scope override never reaches the widget. Fix: `ThemeManager` now installs itself as an event filter on every widget tracked through `applyStyleSheet`. `QEvent::ParentChange` re-resolves the template against the now-correct scope chain. Cheap — only fires during initial construction / rare deliberate reparents. This makes **every** helper that configures a widget pre-parent work correctly (applyAppTheme, applyPrimarySliderStyle, the dozens of inline applyStyleSheet sites in ctor bodies) without per-call-site changes. ### seedBuiltinDefaults safety net Older user themes (forked before this PR exists) get: 1. Root-scope hex values for all 12 tokens (raw hex, no primitives dependency). 2. `applet/tx`, `applet/rx`, `applet/comp` scope-tree seeds via `scopeOrCreate()` with the red/green/amber overrides. Bundled themes' JSON re-asserts these via `{color.red.500}` aliases — idempotent, but the seed is what makes per-applet differentiation work on a "My Default Dark" forked last week. ## Files touched | File | Role | |---|---| | `resources/themes/default-dark.json`, `default-light.json` | Token defs + nested scope overrides | | `src/core/ThemeManager.{h,cpp}` | seedBuiltinDefaults extension + ParentChange filter | | `src/gui/Theme.h` | Global QSS migration + `applyPrimarySliderStyle` default arg | | `src/gui/TitleBar.cpp` | Master + headphone sliders read from `color.slider.*` | | `src/gui/TxApplet.cpp` / `RxApplet.cpp` / `ClientCompApplet.cpp` | Applet-level slider QSS override (catches sliders not using the helper) | | `src/gui/ClientCompKnob.cpp` | Widget-aware paint-code migration to `color.knob.*` | | `docs/theming/slider-knob-tokens.md` | Full writeup of the namespace + cascade + ParentChange pattern | ## Out of scope (follow-ups) - **`PhaseKnob`** paint code still uses 6 hardcoded `QColor` literals — needs more design judgment than a rename. - **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. - **Dialog-scope knob overrides** — available today (namespace + scope tree both support it); nothing declared yet. ## Test plan - [ ] Open TX applet — every slider's filled portion is **red**. - [ ] Open RX applet — every slider's filled portion is **green**. - [ ] Open the compressor applet — every knob arc + slider fill is **amber**. - [ ] Every other applet (DAX, EQ, Phone, …) renders the canonical **blue** fill from root scope. - [ ] Disable an AGC slider (or any slider in a disabled section) — groove/fill/handle shift to the muted disabled variants. - [ ] Open Theme Editor, pick scope `applet/tx` — the columnar view shows `color.slider.foreground` overridden at the `tx` column. - [ ] Inspector click on a slider surfaces the 6 `color.slider.*` entries; click on a comp knob surfaces the 6 `color.knob.*` entries. - [ ] Smoke-test on both Default Dark and Default Light. - [ ] Verify a user theme forked before this PR (e.g. "My Default Dark") still gets the per-applet colours via the seeded defaults. 🤖 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
…dioSetup / FlexControl) (aethersdr#3204) Closes the second half of Pat's "both" answer to **(a)** the toggle namespace work (landed as [aethersdr#3198](aethersdr#3198)) and **(b)** the hardcoded-hex slider sites called out in [aethersdr#3188](aethersdr#3188 *Out of scope* section. ## Background [aethersdr#3188](aethersdr#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 aethersdr#3198 This PR completes Pat's "both" answer. With aethersdr#3198 (toggle namespace carve-out) and this PR (slider site cleanup) merged, the four originally-cited problem sites from aethersdr#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>
G6PWY-Chris
pushed a commit
to G6PWY-Chris/AetherSDR
that referenced
this pull request
Jun 22, 2026
…rrides (aethersdr#3198) Second control-type carve-out following the slider + knob pattern from [aethersdr#3188](aethersdr#3188). References the toggle-buttons bullet under "follow-up sweeps" in [aethersdr#3184](aethersdr#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: ```cpp 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 aethersdr#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: ```json "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-aethersdr#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 migration** — `AetherDspWidget`, `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 aethersdr#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 toggles** — `QCheckBox::indicator` / `QRadioButton::indicator` are a different visual primitive, separate namespace + sweep. - **Hover / pressed state tokens** — same deferral as sliders. - **`color.toggle.panel.*` tribe** — `AppletPanel.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](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Jeremy Fielder <kk7gwy@aethersdr.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.
First worked example of carving a control type out of the generic theme categories (
color.accent/color.background.1/color.text.primary) into dedicated namespaces with per-applet overrides through the v2 scope tree. Sets the pattern for follow-up sweeps (toggle buttons, spinboxes, progress bars, …).Draft — opening early for visibility; more polish may layer on top.
What's in this PR
New token namespaces (12 entries)
State suffix (
.disabled) matches existingcolor.text.disabled/color.accent.brightpattern. Hover / pressed deferred — explicit design call.Per-applet cascade (visible result)
Bundled themes ship nested-scope overrides:
color.slider.foreground&color.knob.foregroundapplet/tx{color.red.500}applet/rx{color.green.500}applet/comp{color.amber.500}{color.blue.500}Open the TX applet → sliders + the comp knob (if a comp lives inside) render red. Open RX → green. Open the compressor applet → amber. All other surfaces stay the canonical blue.
The ParentChange event filter (general fix)
While building this, surfaced a latent bug: helpers like
applyPrimarySliderStyleare commonly called before the widget is added to its parent layout (configure → applyStyle → addWidget). At apply time the widget has no parent, socontainerPathForwalks nothing and resolves the QSS against root scope. The applet-scope override never reaches the widget.Fix:
ThemeManagernow installs itself as an event filter on every widget tracked throughapplyStyleSheet.QEvent::ParentChangere-resolves the template against the now-correct scope chain. Cheap — only fires during initial construction / rare deliberate reparents.This makes every helper that configures a widget pre-parent work correctly (applyAppTheme, applyPrimarySliderStyle, the dozens of inline applyStyleSheet sites in ctor bodies) without per-call-site changes.
seedBuiltinDefaults safety net
Older user themes (forked before this PR exists) get:
applet/tx,applet/rx,applet/compscope-tree seeds viascopeOrCreate()with the red/green/amber overrides.Bundled themes' JSON re-asserts these via
{color.red.500}aliases — idempotent, but the seed is what makes per-applet differentiation work on a "My Default Dark" forked last week.Files touched
resources/themes/default-dark.json,default-light.jsonsrc/core/ThemeManager.{h,cpp}src/gui/Theme.happlyPrimarySliderStyledefault argsrc/gui/TitleBar.cppcolor.slider.*src/gui/TxApplet.cpp/RxApplet.cpp/ClientCompApplet.cppsrc/gui/ClientCompKnob.cppcolor.knob.*docs/theming/slider-knob-tokens.mdOut of scope (follow-ups)
PhaseKnobpaint code still uses 6 hardcodedQColorliterals — needs more design judgment than a rename.PanadapterApplet,EqApplet,RadioSetupDialog,FlexControlDialogbypass the theme system entirely. Each needs its local stylesheet dropped in favour of the global QSS + per-applet override path.Test plan
applet/tx— the columnar view showscolor.slider.foregroundoverridden at thetxcolumn.color.slider.*entries; click on a comp knob surfaces the 6color.knob.*entries.🤖 Generated with Claude Code