Skip to content

feat(theme): Phase 5 PR 2 — Theme Editor inspector mode + 505-site reverse-map sweep#3144

Merged
ten9876 merged 1 commit into
mainfrom
auto/theme-inspector
May 25, 2026
Merged

feat(theme): Phase 5 PR 2 — Theme Editor inspector mode + 505-site reverse-map sweep#3144
ten9876 merged 1 commit into
mainfrom
auto/theme-inspector

Conversation

@ten9876

@ten9876 ten9876 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Browser-devtools-style "click on the wrong-coloured part of the UI to fix it" inspector lands on top of the Phase 5 PR 1 token-tree editor. Per RFC #3076 the inspector is the primary interaction for the Theme Editor — the token tree is the secondary navigation.

New surface

View → Theme Editor → 🎯 Inspect:

  • Click the toggle, cursor becomes a cross.
  • Move over the main UI — a cyan rounded outline tracks the widget under the cursor (top-level overlay, click-through, always-on-top).
  • Click a region — token list filters to just the tokens painting it, status reads ClassName: N tokens. If exactly one matches, the colour picker opens immediately for the "click ugly thing → fix it" flow.
  • ESC cancels without picking.
  • The dialog itself is invisible to the inspector (no self-targeting).

Reverse-map coverage: 22 → 527 widgets

The Phase 2 stylesheet resolver already had a (widget → tokens) reverse-map populated by ThemeManager::applyStyleSheet(), but only 22 sites in the codebase called that API. The remaining 505 sites used the equivalent-but-untracked widget->setStyleSheet(theme.resolve(template)) pattern, leaving hundreds of widgets invisible to the inspector.

This PR sweeps all 505 sites to applyStyleSheet() via a balanced-paren-aware Python migration tool (kept under tools/ for the next sweep we need to do).

Bit-identical at runtimeapplyStyleSheet internally does the same widget->setStyleSheet(resolve(template)) call — but the widget now shows up in the reverse-map.

ThemeManager additions

  • declareWidgetTokens(widget, tokens) — explicit hook for custom-paint widgets (panadapter, waterfall, meters, slice indicators) that read tokens directly in paintEvent rather than through a stylesheet template. Same destroy-signal cleanup as applyStyleSheet.
  • reapplyAllTrackedStyleSheets() now skips entries with empty templates so paint-code-only declarations don't wipe any stylesheet the widget inherited from a parent / Theme.h helper.

ThemeInspector class

src/gui/ThemeInspector.{h,cpp}QObject lifecycle:

  • start() installs a QApplication-level event filter + shows a frameless translucent overlay (Qt::Tool | Qt::FramelessWindowHint | Qt::WindowStaysOnTopHint | Qt::WindowTransparentForInput) with WA_TransparentForMouseEvents so it never intercepts events itself. The global event filter is the authoritative event source.
  • Mouse moves → QApplication::widgetAt(globalPos) → reposition overlay (skipping editor + overlay self-hits).
  • Left-click → eats the event, emits widgetPicked(target, localPos), auto-deactivates.
  • ESC → eats the event, emits canceled(), deactivates.

The dialog walks up the parent chain from the picked widget until it finds a tracked ancestor, so a click on a deep child (a QLabel inside a QPushButton inside a themed panel) still surfaces the panel-level tokens.

Migration tool

tools/migrate_setStyleSheet_to_applyStyleSheet.py — balanced-paren walker that handles arbitrary chained receivers (m_btn->, this->m_btn->, btns[i]->, statusBar()->, parent()->widget->), multi-line adjacent-string-literal templates, and the dot-form case (menu.setStyleSheet(...)) where the receiver is a stack-allocated value and applyStyleSheet needs &obj.

Known limits (follow-up PRs)

  • Custom-paint widgets (SpectrumWidget, MeterWidget, slice indicators on the panadapter, applet headers painted in code) aren't declared yet. Clicks on the spectrum trace or meter bars fall through to the parent widget's tokens or hit "no tokens registered for this region yet." Phase 5 PR 3 will add the themeRegions() mixin from the RFC for fine-grained sub-region lookup.
  • Test theme_manager_test has pre-existing stale assertions from before the Phase 2 token realignment. Not introduced by this PR; needs its own cleanup.

Test plan

  • Builds clean (--target all, including all test executables — no bot-CMake-omission regressions)
  • Visual smoke: every panel/dialog/applet renders identically (sweep is runtime-bit-identical)
  • Inspector flow on KDE Plasma X11 — outline tracking, click-to-pick, ESC cancel all working
  • Slice strip widgets (VfoWidget) inspectable: slice badge, filter-width label, freq label, band/mode buttons, mute/lock/tx
  • Title bar + status bar inspectable
  • Wayland session smoke (untested locally — relying on CI)
  • Custom-paint widgets (panadapter, waterfall) — known not-yet-instrumented; falls through to parent chain

…verse-map sweep

## Summary

Browser-devtools-style "click on the wrong-coloured part of the UI to
fix it" inspector lands on top of the Phase 5 PR 1 token-tree editor.
Per RFC #3076 the inspector is the **primary** interaction for the
Theme Editor — the token tree is the secondary navigation for users
who know token names already.

## New surface

**View → Theme Editor → 🎯 Inspect:**
- Click the toggle, cursor becomes a cross.
- Move over the main UI — a cyan rounded outline tracks the widget
  under the cursor (top-level overlay, click-through, always-on-top).
- Click a region — token list filters to just the tokens painting it,
  status reads "ClassName: N tokens." If exactly one matches, the
  colour picker opens immediately for the "click ugly thing → fix it"
  flow.
- ESC cancels without picking.
- The dialog itself is invisible to the inspector (no self-targeting).

## Reverse-map coverage

The Phase 2 stylesheet resolver already had a `(widget → tokens)`
reverse-map populated by `ThemeManager::applyStyleSheet()`, but only
22 sites in the codebase called that API.  The remaining 505 sites
used the equivalent-but-untracked
`widget->setStyleSheet(theme.resolve(template))` pattern, leaving
hundreds of widgets invisible to the inspector.

This PR sweeps all 505 sites to `applyStyleSheet()` via a balanced-
paren-aware Python migration tool (kept under `tools/` for the next
sweep we need to do).  The change is bit-identical at runtime —
`applyStyleSheet` internally does the same
`widget->setStyleSheet(resolve(template))` call — but the widget now
shows up in the reverse-map.

Coverage jumps from 22 → 527 widgets discoverable by the inspector.

## ThemeManager additions

- `declareWidgetTokens(widget, tokens)` — explicit hook for custom-
  paint widgets (panadapter, waterfall, meters, slice indicators) that
  read tokens directly in `paintEvent` rather than through a
  stylesheet template.  Same destroy-signal cleanup as
  `applyStyleSheet`.
- `reapplyAllTrackedStyleSheets()` now skips entries with empty
  templates so paint-code-only declarations don't wipe any stylesheet
  the widget inherited from a parent / `Theme.h` helper.

## ThemeInspector

New `src/gui/ThemeInspector.{h,cpp}` — QObject lifecycle:

- `start()` installs a `QApplication`-level event filter + shows a
  frameless translucent overlay (`Qt::Tool | Qt::FramelessWindowHint |
  Qt::WindowStaysOnTopHint | Qt::WindowTransparentForInput`) with
  `WA_TransparentForMouseEvents` so it never intercepts events
  itself.  The global event filter is the authoritative event source.
- Mouse moves → `QApplication::widgetAt(globalPos)` → reposition
  overlay (skipping editor + overlay self-hits).
- Left-click → eats the event, emits `widgetPicked(target, localPos)`,
  auto-deactivates.
- ESC → eats the event, emits `canceled()`, deactivates.

The dialog walks up the parent chain from the picked widget until it
finds a tracked ancestor, so a click on a deep child (a `QLabel` inside
a `QPushButton` inside a themed panel) still surfaces the panel-level
tokens.

## Migration tool

`tools/migrate_setStyleSheet_to_applyStyleSheet.py` — balanced-paren
walker that handles arbitrary chained receivers (`m_btn->`,
`this->m_btn->`, `btns[i]->`, `statusBar()->`, `parent()->widget->`),
multi-line adjacent-string-literal templates, and the dot-form case
(`menu.setStyleSheet(...)`) where the receiver is a stack-allocated
value and applyStyleSheet needs `&obj`.

## Known limits (follow-up PRs)

- Custom-paint widgets (SpectrumWidget, MeterWidget, slice indicators,
  applet headers painted in code) aren't declared yet.  Clicks on the
  spectrum trace or meter bars fall through to the parent widget's
  tokens or hit "no tokens registered for this region yet."  Phase 5
  PR 3 will add the `themeRegions()` mixin from the RFC for fine-
  grained sub-region lookup on those widgets.
- Test `theme_manager_test` has pre-existing stale assertions (from
  before the Phase 2 token realignment).  Not introduced by this PR;
  needs its own cleanup.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 requested review from a team as code owners May 25, 2026 16:53
@ten9876 ten9876 merged commit 34f219f into main May 25, 2026
4 checks passed
@ten9876 ten9876 deleted the auto/theme-inspector branch May 25, 2026 17:17
ten9876 added a commit that referenced this pull request May 25, 2026
…sion

Three review fixes from PR #3153:

1. MainWindow wiring (blocker) — HealthApplet was registered in the
   AppletPanel drawer but never given a MeterModel.  Without setMeterModel()
   the three signal connections in HealthApplet::setMeterModel never form,
   bestSnapshot() always returns invalid, and the applet renders permanently
   as IDLE / "RF IDLE".  Adds the canonical wiring pair next to MeterApplet's:
     m_appletPanel->healthApplet()->setMeterModel(&m_radioModel.meterModel());
     m_appletPanel->healthApplet()->setPowerScale(maxW, ampActive);   // updatePowerScale lambda

2. Inspector-discoverability — convert the ~8 setStyleSheet(theme.resolve())
   call sites in HealthApplet to ThemeManager::applyStyleSheet(widget, template)
   so the labels and pills register in the Phase 5 inspector reverse-map
   (PR #3144's sweep pattern).  labelStyle/pillStyle helpers now return
   unresolved {{token}} templates; new applyLabelStyle/applyPillStyle wrappers
   route through applyStyleSheet.

3. Tighten the disconnect in HealthApplet::setMeterModel — replace the broad
   disconnect(m_model, nullptr, this, nullptr) with explicit named-signal
   disconnects so any unrelated connections to the same model survive.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request May 25, 2026
…on-bar refactor (#3153)

## Summary

Rebased version of @jensenpat's #3133 onto PR #3150's customisable
button-bar architecture. Same `HealthApplet.{h,cpp}` source verbatim
from #3133 (zero changes to applet code) — this PR is purely the
AppletPanel integration delta that adapts to PR #3150's new `BarButton`
registry + favorites/drawer model.

**Depends on #3150** — currently targets `fix/button-bar`. Once #3150
merges to main, this PR's base will be retargeted to main and the diff
will collapse to just the HLTH integration touchpoints.

## What changed vs #3133

Three integration touchpoints adjusted; HealthApplet itself untouched:

1. **`makeEntry()` callsite** routes through `m_drawer` /
`m_drawerLayout` (the new drawer layout from #3150), matching every
other drawer-resident applet's pattern. PR #3150's `registerBarButton()`
inside `makeEntry()` handles bar-button registration automatically — no
manual `btnRow2` plumbing required.

2. **Default position** moved from index 4 (between TX and PHNE) to
after MTR in the drawer:
   ```cpp
   "RX", "TUN", "AMP", "TX", "PHNE", "P/CW", "EQ", "WAVE", "TXDSP",
   "CAT", "DAX", "TCI", "IQ", "MTR", "HLTH", "AG", "SS"
   ```
With the new 5-button favorites row, putting HLTH at index 4 would push
PHNE out of favorites and surprise shipping users. Operators who want
HLTH up top can drag it via the right-click Favorites picker.

3. **CMakeLists.txt** — `src/gui/HealthApplet.cpp` added to
`GUI_SOURCES`.

## Credit

Commit attribution stays with @jensenpat as the HealthApplet author.
This PR exists only so the HLTH work doesn't have to wait on the
contributor doing a manual rebase after #3150 lands — they're welcome to
keep #3133 open if they'd prefer to do the rebase themselves.

## Review of HLTH itself

The applet code is well-built — appreciated zero hardcoded colours
(every paint and stylesheet call reads from `ThemeManager`), clean
paint-method separation, visibility-gated 20 Hz timer, three-tier meter
source priority (amp → TGXL tuner → radio TX), and EWMA + recent-window
variance detection.

One small consistency note for a follow-up: HLTH uses
`widget->setStyleSheet(theme.resolve(...))` (~8 sites) instead of
`theme.applyStyleSheet(widget, ...)`. That's the pattern PR #3144 swept
across the rest of the codebase to make widgets inspector-discoverable.
A two-line conversion would let the Phase 5 inspector find HLTH labels.
Tiny follow-up — not blocking this PR.

## Test plan

- [x] Linux (x86_64) — builds clean against `fix/button-bar` (`cmake
--build --target all`)
- [ ] Live smoke: HLTH appears in the drawer after MTR; right-click bar
→ Favorites picker shows HLTH in Active column
- [ ] Once #3150 merges, retarget base to main, confirm no further diff
change

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

---------

Co-authored-by: Pat Jensen <jensenpat@users.noreply.github.com>
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
…verse-map sweep (aethersdr#3144)

## Summary

Browser-devtools-style "click on the wrong-coloured part of the UI to
fix it" inspector lands on top of the Phase 5 PR 1 token-tree editor.
Per [RFC aethersdr#3076](aethersdr#3076) the
inspector is the **primary** interaction for the Theme Editor — the
token tree is the secondary navigation.

## New surface

**View → Theme Editor → 🎯 Inspect:**

- Click the toggle, cursor becomes a cross.
- Move over the main UI — a cyan rounded outline tracks the widget under
the cursor (top-level overlay, click-through, always-on-top).
- Click a region — token list filters to just the tokens painting it,
status reads `ClassName: N tokens`. If exactly one matches, the colour
picker opens immediately for the "click ugly thing → fix it" flow.
- ESC cancels without picking.
- The dialog itself is invisible to the inspector (no self-targeting).

## Reverse-map coverage: 22 → 527 widgets

The Phase 2 stylesheet resolver already had a `(widget → tokens)`
reverse-map populated by `ThemeManager::applyStyleSheet()`, but only 22
sites in the codebase called that API. The remaining 505 sites used the
equivalent-but-untracked
`widget->setStyleSheet(theme.resolve(template))` pattern, leaving
hundreds of widgets invisible to the inspector.

This PR sweeps all 505 sites to `applyStyleSheet()` via a
balanced-paren-aware Python migration tool (kept under `tools/` for the
next sweep we need to do).

**Bit-identical at runtime** — `applyStyleSheet` internally does the
same `widget->setStyleSheet(resolve(template))` call — but the widget
now shows up in the reverse-map.

## `ThemeManager` additions

- `declareWidgetTokens(widget, tokens)` — explicit hook for custom-paint
widgets (panadapter, waterfall, meters, slice indicators) that read
tokens directly in `paintEvent` rather than through a stylesheet
template. Same destroy-signal cleanup as `applyStyleSheet`.
- `reapplyAllTrackedStyleSheets()` now skips entries with empty
templates so paint-code-only declarations don't wipe any stylesheet the
widget inherited from a parent / `Theme.h` helper.

## `ThemeInspector` class

`src/gui/ThemeInspector.{h,cpp}` — `QObject` lifecycle:

- `start()` installs a `QApplication`-level event filter + shows a
frameless translucent overlay (`Qt::Tool | Qt::FramelessWindowHint |
Qt::WindowStaysOnTopHint | Qt::WindowTransparentForInput`) with
`WA_TransparentForMouseEvents` so it never intercepts events itself. The
global event filter is the authoritative event source.
- Mouse moves → `QApplication::widgetAt(globalPos)` → reposition overlay
(skipping editor + overlay self-hits).
- Left-click → eats the event, emits `widgetPicked(target, localPos)`,
auto-deactivates.
- ESC → eats the event, emits `canceled()`, deactivates.

The dialog walks up the parent chain from the picked widget until it
finds a tracked ancestor, so a click on a deep child (a `QLabel` inside
a `QPushButton` inside a themed panel) still surfaces the panel-level
tokens.

## Migration tool

`tools/migrate_setStyleSheet_to_applyStyleSheet.py` — balanced-paren
walker that handles arbitrary chained receivers (`m_btn->`,
`this->m_btn->`, `btns[i]->`, `statusBar()->`, `parent()->widget->`),
multi-line adjacent-string-literal templates, and the dot-form case
(`menu.setStyleSheet(...)`) where the receiver is a stack-allocated
value and `applyStyleSheet` needs `&obj`.

## Known limits (follow-up PRs)

- Custom-paint widgets (`SpectrumWidget`, `MeterWidget`, slice
indicators on the panadapter, applet headers painted in code) aren't
declared yet. Clicks on the spectrum trace or meter bars fall through to
the parent widget's tokens or hit "no tokens registered for this region
yet." **Phase 5 PR 3** will add the `themeRegions()` mixin from the RFC
for fine-grained sub-region lookup.
- Test `theme_manager_test` has pre-existing stale assertions from
before the Phase 2 token realignment. Not introduced by this PR; needs
its own cleanup.

## Test plan

- [x] Builds clean (`--target all`, including all test executables — no
bot-CMake-omission regressions)
- [x] Visual smoke: every panel/dialog/applet renders identically (sweep
is runtime-bit-identical)
- [x] Inspector flow on KDE Plasma X11 — outline tracking,
click-to-pick, ESC cancel all working
- [x] Slice strip widgets (VfoWidget) inspectable: slice badge,
filter-width label, freq label, band/mode buttons, mute/lock/tx
- [x] Title bar + status bar inspectable
- [ ] Wayland session smoke (untested locally — relying on CI)
- [ ] Custom-paint widgets (panadapter, waterfall) — known
not-yet-instrumented; falls through to parent chain

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
…3150 button-bar refactor (aethersdr#3153)

## Summary

Rebased version of @jensenpat's aethersdr#3133 onto PR aethersdr#3150's customisable
button-bar architecture. Same `HealthApplet.{h,cpp}` source verbatim
from aethersdr#3133 (zero changes to applet code) — this PR is purely the
AppletPanel integration delta that adapts to PR aethersdr#3150's new `BarButton`
registry + favorites/drawer model.

**Depends on aethersdr#3150** — currently targets `fix/button-bar`. Once aethersdr#3150
merges to main, this PR's base will be retargeted to main and the diff
will collapse to just the HLTH integration touchpoints.

## What changed vs aethersdr#3133

Three integration touchpoints adjusted; HealthApplet itself untouched:

1. **`makeEntry()` callsite** routes through `m_drawer` /
`m_drawerLayout` (the new drawer layout from aethersdr#3150), matching every
other drawer-resident applet's pattern. PR aethersdr#3150's `registerBarButton()`
inside `makeEntry()` handles bar-button registration automatically — no
manual `btnRow2` plumbing required.

2. **Default position** moved from index 4 (between TX and PHNE) to
after MTR in the drawer:
   ```cpp
   "RX", "TUN", "AMP", "TX", "PHNE", "P/CW", "EQ", "WAVE", "TXDSP",
   "CAT", "DAX", "TCI", "IQ", "MTR", "HLTH", "AG", "SS"
   ```
With the new 5-button favorites row, putting HLTH at index 4 would push
PHNE out of favorites and surprise shipping users. Operators who want
HLTH up top can drag it via the right-click Favorites picker.

3. **CMakeLists.txt** — `src/gui/HealthApplet.cpp` added to
`GUI_SOURCES`.

## Credit

Commit attribution stays with @jensenpat as the HealthApplet author.
This PR exists only so the HLTH work doesn't have to wait on the
contributor doing a manual rebase after aethersdr#3150 lands — they're welcome to
keep aethersdr#3133 open if they'd prefer to do the rebase themselves.

## Review of HLTH itself

The applet code is well-built — appreciated zero hardcoded colours
(every paint and stylesheet call reads from `ThemeManager`), clean
paint-method separation, visibility-gated 20 Hz timer, three-tier meter
source priority (amp → TGXL tuner → radio TX), and EWMA + recent-window
variance detection.

One small consistency note for a follow-up: HLTH uses
`widget->setStyleSheet(theme.resolve(...))` (~8 sites) instead of
`theme.applyStyleSheet(widget, ...)`. That's the pattern PR aethersdr#3144 swept
across the rest of the codebase to make widgets inspector-discoverable.
A two-line conversion would let the Phase 5 inspector find HLTH labels.
Tiny follow-up — not blocking this PR.

## Test plan

- [x] Linux (x86_64) — builds clean against `fix/button-bar` (`cmake
--build --target all`)
- [ ] Live smoke: HLTH appears in the drawer after MTR; right-click bar
→ Favorites picker shows HLTH in Active column
- [ ] Once aethersdr#3150 merges, retarget base to main, confirm no further diff
change

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

---------

Co-authored-by: Pat Jensen <jensenpat@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant