Skip to content

feat(theme): Phase 5 PR 3 — alpha pipeline + themeRegions + glass-mode backdrop#3148

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

feat(theme): Phase 5 PR 3 — alpha pipeline + themeRegions + glass-mode backdrop#3148
ten9876 merged 1 commit into
mainfrom
auto/theme-pr3

Conversation

@ten9876

@ten9876 ten9876 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes the alpha-handling correctness gap that PR #3130's colour picker exposed, lays the inspector-coverage groundwork for custom-paint widgets, and adds a single-token "glass mode" backdrop that lets operators dial app translucency without flipping every widget at once.

Alpha pipeline fix

The colour picker returned QColor with alpha < 255, but every downstream step quietly stripped it:

Step Bug
setColor() QColor::name() defaults to #rrggbb — alpha lost at storage
gradientCssFragment() explicit QColor::HexRgb for stops
cssFragment() returned raw "#aarrggbb" — Qt SS parser ignores it
saveCurrentThemeAs() bare .name() on gradient stops on save

New helpers colorToTokenString() and colorHexToCssFragment() route all four:

  • Storage: HexArgb when alpha < 255, HexRgb otherwise (bundled-theme strings stay byte-identical)
  • Stylesheet emit: translates translucent hex to rgba(r, g, b, a) so Qt actually honours alpha
  • JSON round-trip: preserves the 8-digit form on save

Theme Editor swatch now sits over a 2×2 checkerboard so translucent colours are visually distinguishable; hex label shows the 8-digit form when alpha < 255.

themeRegions() infrastructure

Per RFC #3076:

struct ThemeRegion {
    QString  token;
    std::function<bool(QPoint localPos)> hitTest;
    QString  description;
};
void declareWidgetRegions(QWidget* widget, const QList<ThemeRegion>& regions);
QStringList tokensAtPoint(const QWidget* widget, const QPoint& localPos) const;

tokensAtPoint() returns just the tokens whose hitTest() matches the click point. Falls back to the coarse tokensForWidget() list when no region claims the point — guarantees the inspector always surfaces something useful.

ThemeEditorDialog's inspector now routes through tokensAtPoint() with proper coordinate-space mapping up the parent chain.

SpectrumWidget calls declareWidgetTokens() with its 25 painted tokens (background tiers, spectrum trace/peakHold/average/grid, all 8 slice colours + tx, waterfall.colormap, text tiers, accents). Sub-region splits between panadapter, waterfall, and slice triangles are a follow-up.

Glass-mode backdrop

A new color.background.app token paints MainWindow's full client area through a custom paintEvent override, with Qt::WA_TranslucentBackground enabled. Default is opaque (#0f0f1a dark, #f5f5f8 light) — existing installs see zero visual change.

But operators can now edit the token via Theme Editor and dial alpha down to watch the desktop show through wherever child widgets don't have explicit opaque fills. This is the unblocking architectural change for a future "glass" theme variant.

Test plan

  • Builds clean (--target all)
  • Visual regression check — color.background.app defaulted opaque; no diff vs main when theme isn't edited
  • Alpha picker: dialled color.accent down to ~50% opacity — Qt now honours rgba()
  • Glass-mode A/B: alpha on color.background.app showed compositor desktop wallpaper through unclaimed regions (KWin / KDE Plasma X11)
  • Inspector: clicks on the spectrum/waterfall area now report SpectrumWidget: 25 tokens (was: "no tokens registered")
  • Glass-mode audit: identifying the widgets that bleed desktop when they shouldn't — separate follow-up PR

Follow-ups already on the roadmap

  • Glass-mode audit — add opaque fills to widgets that don't paint their own background (separate PR, open-ended scope)
  • SpectrumWidget sub-region splits via declareWidgetRegions() (panadapter / waterfall / slice triangles)
  • Gradient editor — in-dialog stop-strip widget for waterfall.colormap and slice.dim (PR 3b)
  • Phase 5 PR 4: font + sizing pickers, draft layer, delete/rename, reset-to-default

🤖 Generated with Claude Code

…ure + glass-mode backdrop

## Summary

Closes the alpha-handling correctness gap that PR 5/1's colour picker
exposed, lays the inspector-coverage groundwork for custom-paint
widgets, and adds a single-token "glass mode" backdrop that lets
operators dial app translucency without flipping every widget at once.

## Alpha pipeline fix

QColorDialog (opened via the Theme Editor) returned QColors with
alpha < 255, but every downstream step quietly stripped it:

  * `ThemeManager::setColor()` used `QColor::name()` (defaults to
    "#rrggbb") — alpha gone at storage time.
  * `gradientCssFragment()` explicitly passed `QColor::HexRgb` for
    stop colours.
  * `cssFragment()` for scalars returned the raw string — Qt's
    stylesheet parser doesn't accept "#aarrggbb", only `rgba(...)`.
  * `saveCurrentThemeAs()` re-emitted stops via the same bare `.name()`.

New helpers `colorToTokenString()` and `colorHexToCssFragment()` route
all four sites:

  * Storage uses `HexArgb` when alpha < 255, `HexRgb` otherwise (keeps
    today's bundled-theme strings byte-identical).
  * Stylesheet emit translates translucent hex to `rgba(r, g, b, a)`
    so Qt actually honours the alpha at render time.
  * JSON round-trip preserves the 8-digit form on save.

ThemeEditorDialog updates: the swatch icon now sits over a 2x2
checkerboard so translucent colours are visually distinguishable from
opaque ones at a glance, and the hex label shows the 8-digit form
when alpha < 255.

## themeRegions() infrastructure

Adds the RFC #3076 sub-region hook for custom-paint widgets:

```cpp
struct ThemeRegion {
    QString  token;
    std::function<bool(QPoint localPos)> hitTest;
    QString  description;
};
void declareWidgetRegions(QWidget* widget, const QList<ThemeRegion>& regions);
QStringList tokensAtPoint(const QWidget* widget, const QPoint& localPos) const;
```

`tokensAtPoint()` returns just the tokens whose `hitTest()` matches
the click point.  Falls back to the coarse `tokensForWidget()` list
when no region claims the point, so the inspector always surfaces
something useful for a tracked widget.

ThemeEditorDialog's inspector now routes through `tokensAtPoint()`
with proper coordinate-space mapping up the parent chain — clicks
on a deep child whose nearest themed ancestor declared sub-regions
get the narrowed hit-list.

SpectrumWidget — the biggest paint surface in the app — calls
`declareWidgetTokens()` with its 25 painted tokens (background tiers,
spectrum trace/peakHold/average/grid, all 8 slice colours + tx,
waterfall.colormap, text tiers, accents).  Sub-region splits between
the panadapter, waterfall, and slice triangles are a follow-up that
benefits from being separately reviewable.

## Glass-mode backdrop

A new `color.background.app` token paints MainWindow's full client
area through a custom `paintEvent` override, with
`Qt::WA_TranslucentBackground` enabled.  Default value is opaque
(`#0f0f1a` dark, `#f5f5f8` light) so existing installs see no visual
change — but operators can now edit the token via Theme Editor to
dial alpha down and watch the desktop show through wherever child
widgets don't have explicit opaque fills.

This is the unblocking architectural change for a future "glass"
theme variant.  The audit work — adding explicit fills to widgets
that currently rely on MainWindow's opaque backdrop — is open-ended
and gets its own follow-up PR.

## Known follow-ups

  * Glass-mode audit: identify and add opaque fills to widgets that
    bleed desktop through (free-floating labels, header strips, etc.).
  * SpectrumWidget sub-regions: split coarse declaration into
    panadapter / waterfall / slice-triangle ThemeRegions via
    declareWidgetRegions().
  * Gradient editor: in-dialog stop-strip widget for editing
    waterfall.colormap and slice.dim block (moved to PR 3b — bigger
    UI surface, benefits from isolated review).
  * MeterWidget + SliceLabel declareWidgetTokens() coverage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 requested a review from a team as a code owner May 25, 2026 17:53

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Thanks @ten9876 — three-piece scope is well partitioned and the alpha-pipeline rationale is convincing. The bug table in the PR body matches what I see in the diff: every QColor::name() / HexRgb call on a translucent value was a silent strip, and routing all four through colorToTokenString() / colorHexToCssFragment() is the right fix. JSON round-trip stays byte-identical for opaque values (verified: c.alpha() == 255 branch returns plain #rrggbb), so bundled-theme strings won't churn.

A few things worth a look before merge:

1. WA_TranslucentBackground is unconditional (MainWindow.cpp:1416)

setAttribute(Qt::WA_TranslucentBackground, true);
setAutoFillBackground(false);

This runs for every install, not just operators experimenting with translucent color.background.app. On Linux WMs without an active compositor (raw X11, KWin in non-compositing mode, some i3/dwm setups, certain Wayland fallback paths) the attribute can produce a window with garbage pixels, a fully-transparent client area, or platform-specific rendering glitches even when the backdrop colour is opaque — because the Qt backing store now allocates with alpha and the platform window is requested as translucent regardless of what we draw into it.

The "zero visual change" claim is true for KWin-compositing (your test environment) but isn't a property of WA_TranslucentBackground itself. Two safer options:

  • Gate on token value: only flip the attribute when color.background.app has alpha < 255. This needs the QWindow to be re-realised on theme edit (hide()/show() or re-create), which is more work but keeps the "default install behaves exactly like main" guarantee.
  • Gate behind a per-user opt-in in AppSettings (e.g. Theme/AllowTranslucentBackdrop) — small UX cost, big safety win for the audit phase.

Either path defends against silent regressions on the long tail of Linux desktop configurations we don't have on the bench. Worth a follow-up issue if you'd rather not block this PR on it.

2. SpectrumWidget token list will drift (SpectrumWidget.cpp:521)

The hardcoded list of 25 tokens in the constructor is invisible to anyone adding a new ThemeManager::instance().color("color.spectrum.foo") call in the paint code — they'll silently fall out of the inspector. Not a blocker (you've already noted the sub-region split as follow-up), but consider a // MAINTAINER: keep in sync with paintXxx() token reads comment so the next person hunting for "why doesn't the inspector see my new token" finds the answer fast.

3. Minor: QPainter scope (MainWindow.cpp:5519)

QPainter p(this);
...
p.fillRect(rect(), bg.isValid() ? bg : QColor("#0f0f1a"));
QMainWindow::paintEvent(event);

QMainWindow::paintEvent is a QWidget no-op so this isn't actively broken, but the conventional pattern is to scope the painter ({ QPainter p(this); ... }) before calling the base — keeps you safe if the base ever grows a body. One-line fix.

Alpha-pipeline and themeRegions look solid otherwise. The parent-chain coordinate mapping in onInspectorPicked (mapping through global at each step) is the correct way to do that walk.


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

@ten9876 ten9876 merged commit 745e98e into main May 25, 2026
5 checks passed
@ten9876 ten9876 deleted the auto/theme-pr3 branch May 25, 2026 18:34
ten9876 added a commit that referenced this pull request May 25, 2026
…lat↔gradient conversion + frameless chrome (#3158)

## Summary

Closes the last visible gap from [RFC
#3076](#3076 Phase 5 —
every theme token can now be edited end-to-end through the canonical
frameless dialog flow.

## What's new

### Gradient editor

`src/gui/GradientEditorDialog.{h,cpp}` — modal dialog with a custom
`GradientStrip` widget:

- Live preview over a checkerboard (translucent stops stay visible)
- Triangle markers draggable to reposition; double-click strip body adds
a stop (colour sampled at the click point); right-click marker deletes
(refuses below 2 stops)
- QListWidget with `+` / `−` / `Edit colour…` buttons for
keyboard-driven editing
- Angle spinbox for linear gradients
- **`Reset to default`** button pulls from a lazy-loaded
`:/themes/default-dark.json` snapshot via new
`ThemeManager::factoryGradient()`

### Any-token flat ↔ gradient conversion

Click any colour token row → small QMenu at cursor with two options
reflecting current type:

| Current | Menu options |
|---|---|
| Flat colour | "Edit flat colour…" + "Convert to gradient…" |
| Gradient | "Edit gradient…" + "Convert to flat colour…" |

`Convert to gradient` seeds a 2-stop gradient with the scalar at both
ends — initial render is **bit-identical** to the flat output, so
opening the editor doesn't perturb anything until a deliberate edit.
`Convert to flat` seeds `QColorDialog` with the gradient's first stop.
`ThemeManager` already overwrites the other-type entry on `setColor` /
`setGradient`, so the data layer just routes through the right setter.

### Meter-bar gradient

New token `color.meter.bar.fillGradient` — linear, angle 0°
(bottom→top), 5 stops matching the original hardcoded palette (green /
lime / amber / red / deep-red).

- `ClientLevelMeter` reads via `ThemeManager::brush(token, stripBox)`,
dropping its hand-rolled `QLinearGradient` block
- `ClientCompMeter` switches to the same shared token, replacing the
`kLevelLo` / `kLevelMid` / `kLevelHi` 3-stop assembly
- Both widgets connect to `themeChanged` for live updates

Themes can recolour every level meter in the app by editing one
gradient.

### Frameless chrome

Per `docs/style/dialog-patterns.md`, both editors now subclass
`PersistentDialog`:

- **Theme Editor** — frameless title bar, 8-axis edge resize, geometry
persists under `ThemeEditorDialogGeometry`; MainWindow wireup collapsed
from a 10-line lazy-construct dance to
`showOrRaisePersistent(m_themeEditorDialog)`
- **Gradient Editor** — same chrome; inherits frameless state from
`AppSettings::FramelessWindow` at construction. Still modal so the
parent's `dlg.exec()` flow is preserved.

### Inspector pauses for modal child dialogs

The cyan tracking overlay was leaking on top of the gradient editor (and
`QColorDialog`). Fixed: `ThemeInspector::eventFilter` now early-returns
and hides its overlay whenever `QApplication::activeModalWidget()` is
non-null. The inspector resumes automatically the moment the modal
closes.

## Test plan

- [x] Builds clean (`--target all`)
- [x] `color.waterfall.colormap.fire` → "Edit gradient…" → strip editor
opens; drag/add/delete stops repaint the panadapter waterfall live;
"Reset to default" restores the bundled stops
- [x] `color.accent` → "Convert to gradient…" → strip editor opens
seeded with a 2-stop same-colour gradient; edits flow to every
`applyStyleSheet`-tracked widget
- [x] `color.meter.bar.fillGradient` → gradient editor; edits recolour
slice level meters AND compression meters together
- [x] Inspector mode + open gradient editor → overlay hides while modal
is up, resumes on close
- [x] Theme Editor frameless chrome matches the rest of the dialogs

## Follow-ups already on the roadmap

- Glass-mode audit (widgets that bleed desktop through with the PR #3148
backdrop)
- `SpectrumWidget` sub-region splits via `declareWidgetRegions()`
(panadapter / waterfall / slice triangles)
- `MeterWidget` + `SliceLabel` `declareWidgetTokens()` coverage
- Phase 5 PR 4: font + sizing pickers, draft layer, delete/rename,
reset-to-default-per-token
- Phase 6: `.aethertheme` import/export + drag-and-drop

🤖 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
…e backdrop (aethersdr#3148)

## Summary

Closes the alpha-handling correctness gap that PR aethersdr#3130's colour picker
exposed, lays the inspector-coverage groundwork for custom-paint
widgets, and adds a single-token "glass mode" backdrop that lets
operators dial app translucency without flipping every widget at once.

## Alpha pipeline fix

The colour picker returned `QColor` with `alpha < 255`, but every
downstream step quietly stripped it:

| Step | Bug |
|---|---|
| `setColor()` | `QColor::name()` defaults to `#rrggbb` — alpha lost at
storage |
| `gradientCssFragment()` | explicit `QColor::HexRgb` for stops |
| `cssFragment()` | returned raw "#aarrggbb" — Qt SS parser ignores it |
| `saveCurrentThemeAs()` | bare `.name()` on gradient stops on save |

New helpers `colorToTokenString()` and `colorHexToCssFragment()` route
all four:

- Storage: `HexArgb` when alpha < 255, `HexRgb` otherwise (bundled-theme
strings stay byte-identical)
- Stylesheet emit: translates translucent hex to `rgba(r, g, b, a)` so
Qt actually honours alpha
- JSON round-trip: preserves the 8-digit form on save

Theme Editor swatch now sits over a 2×2 checkerboard so translucent
colours are visually distinguishable; hex label shows the 8-digit form
when alpha < 255.

## `themeRegions()` infrastructure

Per [RFC aethersdr#3076](aethersdr#3076):

```cpp
struct ThemeRegion {
    QString  token;
    std::function<bool(QPoint localPos)> hitTest;
    QString  description;
};
void declareWidgetRegions(QWidget* widget, const QList<ThemeRegion>& regions);
QStringList tokensAtPoint(const QWidget* widget, const QPoint& localPos) const;
```

`tokensAtPoint()` returns just the tokens whose `hitTest()` matches the
click point. Falls back to the coarse `tokensForWidget()` list when no
region claims the point — guarantees the inspector always surfaces
something useful.

ThemeEditorDialog's inspector now routes through `tokensAtPoint()` with
proper coordinate-space mapping up the parent chain.

SpectrumWidget calls `declareWidgetTokens()` with its 25 painted tokens
(background tiers, spectrum trace/peakHold/average/grid, all 8 slice
colours + tx, waterfall.colormap, text tiers, accents). Sub-region
splits between panadapter, waterfall, and slice triangles are a
follow-up.

## Glass-mode backdrop

A new `color.background.app` token paints MainWindow's full client area
through a custom `paintEvent` override, with
`Qt::WA_TranslucentBackground` enabled. Default is opaque (`#0f0f1a`
dark, `#f5f5f8` light) — existing installs see zero visual change.

But operators can now edit the token via Theme Editor and dial alpha
down to watch the desktop show through wherever child widgets don't have
explicit opaque fills. This is the unblocking architectural change for a
future "glass" theme variant.

## Test plan

- [x] Builds clean (`--target all`)
- [x] Visual regression check — `color.background.app` defaulted opaque;
no diff vs main when theme isn't edited
- [x] Alpha picker: dialled `color.accent` down to ~50% opacity — Qt now
honours rgba()
- [x] Glass-mode A/B: alpha on `color.background.app` showed compositor
desktop wallpaper through unclaimed regions (KWin / KDE Plasma X11)
- [x] Inspector: clicks on the spectrum/waterfall area now report
`SpectrumWidget: 25 tokens` (was: "no tokens registered")
- [ ] Glass-mode audit: identifying the widgets that bleed desktop when
they shouldn't — separate follow-up PR

## Follow-ups already on the roadmap

- Glass-mode audit — add opaque fills to widgets that don't paint their
own background (separate PR, open-ended scope)
- SpectrumWidget sub-region splits via `declareWidgetRegions()`
(panadapter / waterfall / slice triangles)
- Gradient editor — in-dialog stop-strip widget for waterfall.colormap
and slice.dim (PR 3b)
- Phase 5 PR 4: font + sizing pickers, draft layer, delete/rename,
reset-to-default

🤖 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
…lat↔gradient conversion + frameless chrome (aethersdr#3158)

## Summary

Closes the last visible gap from [RFC
aethersdr#3076](aethersdr#3076 Phase 5 —
every theme token can now be edited end-to-end through the canonical
frameless dialog flow.

## What's new

### Gradient editor

`src/gui/GradientEditorDialog.{h,cpp}` — modal dialog with a custom
`GradientStrip` widget:

- Live preview over a checkerboard (translucent stops stay visible)
- Triangle markers draggable to reposition; double-click strip body adds
a stop (colour sampled at the click point); right-click marker deletes
(refuses below 2 stops)
- QListWidget with `+` / `−` / `Edit colour…` buttons for
keyboard-driven editing
- Angle spinbox for linear gradients
- **`Reset to default`** button pulls from a lazy-loaded
`:/themes/default-dark.json` snapshot via new
`ThemeManager::factoryGradient()`

### Any-token flat ↔ gradient conversion

Click any colour token row → small QMenu at cursor with two options
reflecting current type:

| Current | Menu options |
|---|---|
| Flat colour | "Edit flat colour…" + "Convert to gradient…" |
| Gradient | "Edit gradient…" + "Convert to flat colour…" |

`Convert to gradient` seeds a 2-stop gradient with the scalar at both
ends — initial render is **bit-identical** to the flat output, so
opening the editor doesn't perturb anything until a deliberate edit.
`Convert to flat` seeds `QColorDialog` with the gradient's first stop.
`ThemeManager` already overwrites the other-type entry on `setColor` /
`setGradient`, so the data layer just routes through the right setter.

### Meter-bar gradient

New token `color.meter.bar.fillGradient` — linear, angle 0°
(bottom→top), 5 stops matching the original hardcoded palette (green /
lime / amber / red / deep-red).

- `ClientLevelMeter` reads via `ThemeManager::brush(token, stripBox)`,
dropping its hand-rolled `QLinearGradient` block
- `ClientCompMeter` switches to the same shared token, replacing the
`kLevelLo` / `kLevelMid` / `kLevelHi` 3-stop assembly
- Both widgets connect to `themeChanged` for live updates

Themes can recolour every level meter in the app by editing one
gradient.

### Frameless chrome

Per `docs/style/dialog-patterns.md`, both editors now subclass
`PersistentDialog`:

- **Theme Editor** — frameless title bar, 8-axis edge resize, geometry
persists under `ThemeEditorDialogGeometry`; MainWindow wireup collapsed
from a 10-line lazy-construct dance to
`showOrRaisePersistent(m_themeEditorDialog)`
- **Gradient Editor** — same chrome; inherits frameless state from
`AppSettings::FramelessWindow` at construction. Still modal so the
parent's `dlg.exec()` flow is preserved.

### Inspector pauses for modal child dialogs

The cyan tracking overlay was leaking on top of the gradient editor (and
`QColorDialog`). Fixed: `ThemeInspector::eventFilter` now early-returns
and hides its overlay whenever `QApplication::activeModalWidget()` is
non-null. The inspector resumes automatically the moment the modal
closes.

## Test plan

- [x] Builds clean (`--target all`)
- [x] `color.waterfall.colormap.fire` → "Edit gradient…" → strip editor
opens; drag/add/delete stops repaint the panadapter waterfall live;
"Reset to default" restores the bundled stops
- [x] `color.accent` → "Convert to gradient…" → strip editor opens
seeded with a 2-stop same-colour gradient; edits flow to every
`applyStyleSheet`-tracked widget
- [x] `color.meter.bar.fillGradient` → gradient editor; edits recolour
slice level meters AND compression meters together
- [x] Inspector mode + open gradient editor → overlay hides while modal
is up, resumes on close
- [x] Theme Editor frameless chrome matches the rest of the dialogs

## Follow-ups already on the roadmap

- Glass-mode audit (widgets that bleed desktop through with the PR aethersdr#3148
backdrop)
- `SpectrumWidget` sub-region splits via `declareWidgetRegions()`
(panadapter / waterfall / slice triangles)
- `MeterWidget` + `SliceLabel` `declareWidgetTokens()` coverage
- Phase 5 PR 4: font + sizing pickers, draft layer, delete/rename,
reset-to-default-per-token
- Phase 6: `.aethertheme` import/export + drag-and-drop

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

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