Skip to content

feat(theme): Phase 5 PR 3b — gradient editor + meter-bar gradient + flat↔gradient conversion + frameless chrome#3158

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

feat(theme): Phase 5 PR 3b — gradient editor + meter-bar gradient + flat↔gradient conversion + frameless chrome#3158
ten9876 merged 1 commit into
mainfrom
auto/theme-pr3b

Conversation

@ten9876

@ten9876 ten9876 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes the last visible gap from RFC #3076's 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

  • Builds clean (--target all)
  • 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
  • color.accent → "Convert to gradient…" → strip editor opens seeded with a 2-stop same-colour gradient; edits flow to every applyStyleSheet-tracked widget
  • color.meter.bar.fillGradient → gradient editor; edits recolour slice level meters AND compression meters together
  • Inspector mode + open gradient editor → overlay hides while modal is up, resumes on close
  • 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 feat(theme): Phase 5 PR 3 — alpha pipeline + themeRegions + glass-mode backdrop #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

…n, flat↔gradient conversion, frameless chrome

## Summary

Closes the last visible gap from RFC #3076's Phase 5 — every theme
token can now be edited end-to-end through a frameless dialog flow.
Adds a gradient stop-strip editor, makes any colour token convertible
between flat-fill and gradient form, tokenises the level-meter fill
through a new shared gradient, and converts both editor dialogs to
the canonical `PersistentDialog` chrome shape.

## GradientEditorDialog

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

  * Live gradient preview over a checkerboard so translucent stops
    stay visible.
  * Triangle markers below the strip, draggable to reposition.
  * Double-click strip body → add stop (colour sampled from the
    gradient at the click point so the addition splits the ramp
    visually).
  * Right-click marker → delete stop (refuses below 2 stops).
  * QListWidget alongside with explicit `+`, `−`, `Edit colour…`
    buttons for keyboard-driven editing.
  * Angle spinbox for linear gradients (radial defers angle).
  * `Reset to default` button (`QDialogButtonBox::RestoreDefaults`)
    pulls the canonical value from `:/themes/default-dark.json` via
    a new lazy-loaded `ThemeManager::factoryGradient()` snapshot.
    Disabled when the token has no factory baseline.

## ThemeManager API additions

  * `ThemeGradient gradient(token)` / `setGradient(token, g)` — typed
    accessors paralleling the scalar `color()` / `setColor()` path.
    `setGradient` overwrites any prior scalar entry; `setColor`
    likewise overwrites a prior gradient.  Conversion at the data
    layer is therefore free — UI just routes through the right setter.
  * `factoryGradient(token)` — reads from a one-shot snapshot of the
    bundled `:/themes/default-dark.json`.  Lazy-loaded so a missing
    resource bundle doesn't crash the singleton.

## ThemeEditorDialog wiring

  * Token list now shows gradient rows with a mini left-to-right
    preview swatch + descriptor (`linear, 0°, 5 stops`).  Scalar rows
    keep their hex label + checkerboarded swatch from PR #3148.
  * Clicking any row pops a small `QMenu` at the cursor with two
    options that always reflect the current type:
      - "Edit flat colour…" + "Convert to gradient…" for scalars
      - "Edit gradient…" + "Convert to flat colour…" for gradients
    Picking "Convert to gradient" seeds a 2-stop gradient with the
    scalar colour at both ends — initial render is bit-identical to
    the flat output so opening the editor doesn't perturb anything
    until the operator makes a deliberate edit.  Picking "Convert
    to flat" seeds `QColorDialog` with the gradient's first stop.

## ThemeInspector — pause during modal child dialogs

While the editor has spawned a modal child (`QColorDialog`,
`GradientEditorDialog`, "Save As" confirms), the inspector's global
event filter now early-returns and hides its overlay.  Without this,
the cyan tracking outline draws on top of the modal dialog because
the modal isn't recognised as a descendant of the editor's
`isAncestorOf()` chain (top-level windows don't traverse).

## Meter-bar gradient

  * New token `color.meter.bar.fillGradient` — linear, angle 0°
    (bottom→top), 5 stops: green / lime / amber / red / deep-red.
    Matches the original hardcoded palette so existing installs see
    no visual diff.
  * `ClientLevelMeter` reads the gradient via
    `ThemeManager::brush(token, stripBox)`, dropping its hand-rolled
    `QLinearGradient` block.
  * `ClientCompMeter` switches to the same shared token, replacing
    its `kLevelLo`/`kLevelMid`/`kLevelHi` 3-stop assembly.  Both
    meters now recolour from one place — themes can edit the
    gradient once and every level-style meter follows.
  * Both widgets connect to `themeChanged` for live updates.

## Frameless chrome

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

  * `ThemeEditorDialog` — frameless title bar, 8-axis edge resize,
    geometry persists under `ThemeEditorDialogGeometry`.  MainWindow
    wireup collapsed from a 10-line lazy-construct block to a single
    `showOrRaisePersistent(m_themeEditorDialog)` call; member changed
    to `QPointer<ThemeEditorDialog>`.
  * `GradientEditorDialog` — same chrome; inherits frameless state
    from `AppSettings` at construction.  Still modal so the parent's
    `dlg.exec()` flow is preserved.

## Test plan

- [x] Build clean (`--target all`)
- [x] Click `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] Click `color.accent` → "Convert to gradient…" → strip editor
      opens seeded with a 2-stop same-colour gradient; making edits
      flows to every applyStyleSheet-tracked widget
- [x] Click `color.meter.bar.fillGradient` → gradient editor; edits
      recolour the 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
      (title bar, drag-to-move, edge resize, geometry persists)

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 19:37
@ten9876 ten9876 merged commit 8ff0ee2 into main May 25, 2026
4 checks passed
@ten9876 ten9876 deleted the auto/theme-pr3b branch May 25, 2026 19:57
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