feat(theme): Phase 5 PR 3b — gradient editor + meter-bar gradient + flat↔gradient conversion + frameless chrome#3158
Merged
Merged
Conversation
…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>
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>
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
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 customGradientStripwidget:+/−/Edit colour…buttons for keyboard-driven editingReset to defaultbutton pulls from a lazy-loaded:/themes/default-dark.jsonsnapshot via newThemeManager::factoryGradient()Any-token flat ↔ gradient conversion
Click any colour token row → small QMenu at cursor with two options reflecting current type:
Convert to gradientseeds 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 flatseedsQColorDialogwith the gradient's first stop.ThemeManageralready overwrites the other-type entry onsetColor/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).ClientLevelMeterreads viaThemeManager::brush(token, stripBox), dropping its hand-rolledQLinearGradientblockClientCompMeterswitches to the same shared token, replacing thekLevelLo/kLevelMid/kLevelHi3-stop assemblythemeChangedfor live updatesThemes can recolour every level meter in the app by editing one gradient.
Frameless chrome
Per
docs/style/dialog-patterns.md, both editors now subclassPersistentDialog:ThemeEditorDialogGeometry; MainWindow wireup collapsed from a 10-line lazy-construct dance toshowOrRaisePersistent(m_themeEditorDialog)AppSettings::FramelessWindowat construction. Still modal so the parent'sdlg.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::eventFilternow early-returns and hides its overlay wheneverQApplication::activeModalWidget()is non-null. The inspector resumes automatically the moment the modal closes.Test plan
--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 stopscolor.accent→ "Convert to gradient…" → strip editor opens seeded with a 2-stop same-colour gradient; edits flow to everyapplyStyleSheet-tracked widgetcolor.meter.bar.fillGradient→ gradient editor; edits recolour slice level meters AND compression meters togetherFollow-ups already on the roadmap
SpectrumWidgetsub-region splits viadeclareWidgetRegions()(panadapter / waterfall / slice triangles)MeterWidget+SliceLabeldeclareWidgetTokens()coverage.aetherthemeimport/export + drag-and-drop🤖 Generated with Claude Code