feat(theme): Phase 2 widget→tokens reverse-map + live re-theme on change#3085
Merged
Conversation
Phase 5's inspector mode ("click the wrong-coloured part of the UI to
edit its token") needs to answer "which tokens paint this widget?"
when the operator clicks a region. Today's resolve() throws away
that information after substitution.
Adds applyStyleSheet(QWidget*, const QString& template) — a wrapper
around setStyleSheet(resolve(...)) that additionally:
- records the (widget → template + tokens-it-references) reverse-map
- connects the widget's destroyed() signal so the entry vanishes
automatically when the widget is gone (no dangling pointers)
- on themeChanged, walks the recorded set and re-applies every
stylesheet with freshly resolved values — stylesheet-painted
widgets get free live-reload when the user switches themes
Public API surface:
void applyStyleSheet(QWidget*, const QString&);
void clearWidgetTracking(QWidget*);
QStringList tokensForWidget(const QWidget*) const;
static QStringList extractReferencedTokens(const QString&);
The static extractReferencedTokens() helper is callable without
state — useful for audit tooling and the Phase 5 editor's preview
of "which tokens would this template reference?" before any widget
exists.
Implementation notes:
- QHash<QWidget*, TrackedWidget> for O(1) lookup by widget pointer.
- extractReferencedTokens is order-preserving + deduplicating; the
same token referenced N times in a template only counts once in
the reverse-map.
- reapplyAllTrackedStyleSheets snapshots keys before iterating to
avoid QHash-mutation-during-iteration UB if a re-apply triggers
further widget creation/destruction.
- onTrackedWidgetDestroyed slot uses static_cast<QWidget*> on the
QObject pointer purely for hash lookup — it never dereferences
the widget (already destroyed by signal-emission time).
Test coverage: applyStyleSheet sets the stylesheet, records the
correct tokens, clearWidgetTracking detaches, destroyed-signal
cleanup works without crash, extractReferencedTokens handles
duplicates and empty placeholders.
No consumers wired yet — applyStyleSheet is foundation only. The
pilot stylesheet conversion (SliceLabel or CommonStyles) follows
this commit and will be the first call site.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…get-map # Conflicts: # src/core/ThemeManager.cpp
7 tasks
ten9876
added a commit
that referenced
this pull request
May 25, 2026
…tokens First real consumer of the RFC #3076 Phase 2 theming infrastructure. ComboStyle.h is the central styling helper for every QComboBox in AetherSDR — applyComboStyle() is called from 20+ sites (AppletPanel, RadioSetupDialog, PhoneCwApplet, RxApplet, StripEqPanel, WaveApplet, SpectrumOverlayMenu, etc.). What changed: - Hardcoded hex (#1a2a3a, #c8d8e8, #304050, #00b4d8) replaced with canonical token references: {{color.background.1}}, {{color.text.primary}}, {{color.background.2}}, {{color.accent}}. - QColor(0x8a, 0xa8, 0xc0) in the arrow PNG replaced with ThemeManager::color("color.text.secondary"). - applyComboStyle() now routes through ThemeManager::applyStyleSheet so every combo gets free live re-theme on theme change (registered in the widget-tracked reverse-map for the Phase 5 inspector). - SpectrumOverlayMenu.cpp's two direct calls to comboStyleSheet() converted to applyComboStyle() — the underlying helper is gone. - Arrow PNG cache key now includes the resolved text.secondary hex so each theme variation gets its own cached arrow without stomping. Tradeoff documented inline: applyComboStyle installs TWO connections on themeChanged — the reverse-map's automatic re-apply, plus an explicit refresh to regenerate the arrow URL (since the URL is computed at apply time, not a {{token}} placeholder). The double- apply is wasteful but small; Phase 5 could introduce a "computed token" mechanism if combos become a hot path. QPointer guards against widget destruction between the connection and the next themeChanged. Verified locally: Linux build clean (all 20+ call sites still resolve), binary launches without crash. No visible diff expected today — the resolved colours match the previous hardcoded values exactly. Visible payoff arrives when the user switches themes: every combo re-themes simultaneously without any per-call-site plumbing. Phase 4's Default Light theme will demonstrate this end-to-end. Stacked on top of #3085 (applyStyleSheet infrastructure) — when that lands the pilot rebases cleanly to a single-commit diff against main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ten9876
added a commit
that referenced
this pull request
May 25, 2026
…tokens (#3087) ## Summary First real consumer of the RFC #3076 Phase 2 theming infrastructure. Migrates [src/gui/ComboStyle.h](src/gui/ComboStyle.h) — the central styling helper for every QComboBox in AetherSDR — from hardcoded hex to canonical token references via \`ThemeManager::applyStyleSheet\`. ## Why ComboStyle as the pilot - **High leverage:** \`applyComboStyle()\` is called from 20+ sites (AppletPanel, RadioSetupDialog, PhoneCwApplet, RxApplet, StripEqPanel, WaveApplet, SpectrumOverlayMenu, …) — one function migrated themes them all. - **Small surface:** single 54-line header, easy to review and rollback. - **Covers both code paths:** stylesheet template + paint-code QColor literal (the arrow PNG), exercising both \`resolve()\` and direct \`ThemeManager::color()\` lookup. ## Migration details **Hex → tokens:** | Was | Now | |---|---| | \`#1a2a3a\` | \`{{color.background.1}}\` | | \`#c8d8e8\` | \`{{color.text.primary}}\` | | \`#304050\` | \`{{color.background.2}}\` | | \`#00b4d8\` | \`{{color.accent}}\` | | \`QColor(0x8a, 0xa8, 0xc0)\` | \`ThemeManager::color(\"color.text.secondary\")\` | **Routing:** \`applyComboStyle\` now calls \`ThemeManager::applyStyleSheet\` instead of \`combo->setStyleSheet\`. Combos get free live re-theme on theme change via the widget-tracked reverse-map (PR #3085). **Arrow PNG cache:** filename now includes the resolved \`text.secondary\` hex so each theme variation gets its own cached arrow (\`/tmp/aethersdr_combo_arrow_<hex>.png\`). **SpectrumOverlayMenu.cpp:** two direct calls to \`comboStyleSheet()\` (the underlying-now-removed helper) converted to \`applyComboStyle()\`. ## Tradeoff documented inline \`applyComboStyle\` installs **two** connections on \`themeChanged\`: 1. The reverse-map's automatic re-apply (from #3085) 2. An explicit refresh lambda that regenerates the arrow URL The double-apply is wasteful but small. The arrow URL has to be computed at apply time (it's a filesystem path that depends on the resolved colour), not a \`{{token}}\` placeholder. Phase 5 could introduce a "computed token" mechanism if combos become a hot path. QPointer guards the lambda against widget destruction between the connection and the next theme change. ## Verified locally - [x] Linux build clean (all 20+ call sites still resolve) - [x] AetherSDR binary launches without crash (offscreen QPA, 3s smoke test) - [x] No visible diff expected today — resolved colours match the previous hardcoded values exactly ## Stacking note This PR is based on \`auto/theme-phase2-widget-map\` (PR #3085) because it consumes \`ThemeManager::applyStyleSheet\` which only exists there. When #3085 merges, this rebases cleanly to a single-commit diff against main. ## Test plan - [ ] CI green on the dependent #3085 first - [ ] CI green here after rebase - [ ] Visual smoke: launch app, open Radio Setup dialog, confirm every combo box (profile picker, RCA/ACC, mic source dropdowns) renders identically to v26.5.3 - [ ] After Phase 4 Light theme lands: confirm theme switch repaints every combo simultaneously without a 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
…nge (aethersdr#3085) ## Summary Adds the widget→tokens reverse-map infrastructure that powers Phase 5's inspector mode. Phase 5's planned UX is "click the wrong-coloured part of the UI to edit its token" — that requires the manager to remember which tokens paint which widgets after \`resolve()\` substitutes them away. ## Public API additions \`\`\`cpp // Wrap setStyleSheet(resolve(...)) + record the reverse-map. void applyStyleSheet(QWidget*, const QString& template); // Stop tracking a widget (no more themeChanged re-applies). void clearWidgetTracking(QWidget*); // Inspector lookup: tokens referenced by the widget's last template. QStringList tokensForWidget(const QWidget*) const; // Stateless helper — list tokens a template references without applying. static QStringList extractReferencedTokens(const QString&); \`\`\` ## Behaviour notes - **Free live re-theme.** Stylesheet-painted widgets registered through \`applyStyleSheet\` get re-applied automatically when \`themeChanged\` fires. No consumer plumbing required. - **Automatic cleanup.** \`QObject::destroyed\` removes the widget's entry from the map — no dangling pointers. - **Iteration safety.** \`reapplyAllTrackedStyleSheets\` snapshots keys before iterating in case a re-apply triggers widget creation/destruction in side effects. - **Static \`extractReferencedTokens\`** — useful for audit tooling and the Phase 5 editor's "this template would reference these tokens" preview before any widget exists. ## Tests \`theme_manager_test\` extended to verify: - Applied stylesheet is the resolved form, not the template - Reverse-map records exactly the referenced tokens (deduplicated) - \`clearWidgetTracking\` detaches a widget - destroyed-signal cleanup happens (no crash, no leak) - \`extractReferencedTokens\` handles duplicates and empty \`{{}}\` placeholders Test target now uses \`QApplication\` (needed for QWidget signals) and links \`Qt6::Widgets\`. Runs under \`QT_QPA_PLATFORM=offscreen\` in CI. ## Behavior at runtime today Zero visible change. \`applyStyleSheet\` exists but no call site uses it yet. The pilot stylesheet conversion (likely \`SliceLabel\` or \`CommonStyles\`) is the next PR and will be the first consumer. ## Test plan - [x] Linux: theme_manager_test passes locally (worktree build, PASS) - [x] Linux: full AetherSDR target builds clean (391 link steps) - [ ] CI: build / check-paths / check-windows / CodeQL green ## Sequence note This PR is independent of aethersdr#3084 (gradient support) — they touch the same files but different functions. Whichever lands first, the other rebases trivially. 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
…tokens (aethersdr#3087) ## Summary First real consumer of the RFC aethersdr#3076 Phase 2 theming infrastructure. Migrates [src/gui/ComboStyle.h](src/gui/ComboStyle.h) — the central styling helper for every QComboBox in AetherSDR — from hardcoded hex to canonical token references via \`ThemeManager::applyStyleSheet\`. ## Why ComboStyle as the pilot - **High leverage:** \`applyComboStyle()\` is called from 20+ sites (AppletPanel, RadioSetupDialog, PhoneCwApplet, RxApplet, StripEqPanel, WaveApplet, SpectrumOverlayMenu, …) — one function migrated themes them all. - **Small surface:** single 54-line header, easy to review and rollback. - **Covers both code paths:** stylesheet template + paint-code QColor literal (the arrow PNG), exercising both \`resolve()\` and direct \`ThemeManager::color()\` lookup. ## Migration details **Hex → tokens:** | Was | Now | |---|---| | \`#1a2a3a\` | \`{{color.background.1}}\` | | \`#c8d8e8\` | \`{{color.text.primary}}\` | | \`#304050\` | \`{{color.background.2}}\` | | \`#00b4d8\` | \`{{color.accent}}\` | | \`QColor(0x8a, 0xa8, 0xc0)\` | \`ThemeManager::color(\"color.text.secondary\")\` | **Routing:** \`applyComboStyle\` now calls \`ThemeManager::applyStyleSheet\` instead of \`combo->setStyleSheet\`. Combos get free live re-theme on theme change via the widget-tracked reverse-map (PR aethersdr#3085). **Arrow PNG cache:** filename now includes the resolved \`text.secondary\` hex so each theme variation gets its own cached arrow (\`/tmp/aethersdr_combo_arrow_<hex>.png\`). **SpectrumOverlayMenu.cpp:** two direct calls to \`comboStyleSheet()\` (the underlying-now-removed helper) converted to \`applyComboStyle()\`. ## Tradeoff documented inline \`applyComboStyle\` installs **two** connections on \`themeChanged\`: 1. The reverse-map's automatic re-apply (from aethersdr#3085) 2. An explicit refresh lambda that regenerates the arrow URL The double-apply is wasteful but small. The arrow URL has to be computed at apply time (it's a filesystem path that depends on the resolved colour), not a \`{{token}}\` placeholder. Phase 5 could introduce a "computed token" mechanism if combos become a hot path. QPointer guards the lambda against widget destruction between the connection and the next theme change. ## Verified locally - [x] Linux build clean (all 20+ call sites still resolve) - [x] AetherSDR binary launches without crash (offscreen QPA, 3s smoke test) - [x] No visible diff expected today — resolved colours match the previous hardcoded values exactly ## Stacking note This PR is based on \`auto/theme-phase2-widget-map\` (PR aethersdr#3085) because it consumes \`ThemeManager::applyStyleSheet\` which only exists there. When aethersdr#3085 merges, this rebases cleanly to a single-commit diff against main. ## Test plan - [ ] CI green on the dependent aethersdr#3085 first - [ ] CI green here after rebase - [ ] Visual smoke: launch app, open Radio Setup dialog, confirm every combo box (profile picker, RCA/ACC, mic source dropdowns) renders identically to v26.5.3 - [ ] After Phase 4 Light theme lands: confirm theme switch repaints every combo simultaneously without a restart 73, Jeremy KK7GWY & Claude (AI dev partner) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds the widget→tokens reverse-map infrastructure that powers Phase 5's inspector mode. Phase 5's planned UX is "click the wrong-coloured part of the UI to edit its token" — that requires the manager to remember which tokens paint which widgets after `resolve()` substitutes them away.
Public API additions
```cpp
// Wrap setStyleSheet(resolve(...)) + record the reverse-map.
void applyStyleSheet(QWidget*, const QString& template);
// Stop tracking a widget (no more themeChanged re-applies).
void clearWidgetTracking(QWidget*);
// Inspector lookup: tokens referenced by the widget's last template.
QStringList tokensForWidget(const QWidget*) const;
// Stateless helper — list tokens a template references without applying.
static QStringList extractReferencedTokens(const QString&);
```
Behaviour notes
Tests
`theme_manager_test` extended to verify:
Test target now uses `QApplication` (needed for QWidget signals) and links `Qt6::Widgets`. Runs under `QT_QPA_PLATFORM=offscreen` in CI.
Behavior at runtime today
Zero visible change. `applyStyleSheet` exists but no call site uses it yet. The pilot stylesheet conversion (likely `SliceLabel` or `CommonStyles`) is the next PR and will be the first consumer.
Test plan
Sequence note
This PR is independent of #3084 (gradient support) — they touch the same files but different functions. Whichever lands first, the other rebases trivially.
73, Jeremy KK7GWY & Claude (AI dev partner)
🤖 Generated with Claude Code