Skip to content

feat(theme): Phase 5 PR 4 — font + sizing pickers + per-token reset + theme rename/delete#3160

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

feat(theme): Phase 5 PR 4 — font + sizing pickers + per-token reset + theme rename/delete#3160
ten9876 merged 1 commit into
mainfrom
auto/theme-pr4

Conversation

@ten9876

@ten9876 ten9876 commented May 25, 2026

Copy link
Copy Markdown
Collaborator

Summary

Closes RFC #3076's Phase 5 — every editable token namespace now has a working picker in the Theme Editor, and saved user themes can be renamed or deleted from the dialog without touching the filesystem.

ThemeManager additions

  • setString(token, value) — font-family setter; mirrors setColor / setSizing
  • factoryColor() / factorySizing() / factoryString() / hasFactoryValue() — extend the factory-snapshot lookup beyond gradients
  • deleteTheme(name) — switches active theme to Default Dark before unlinking; refuses on built-ins
  • renameTheme(oldName, newName) — rewrites the JSON file's "name" field + filesystem rename in lockstep; refuses on built-ins and name collisions
  • isBuiltInTheme(name) — detected by whether the recorded path is inside the Qt resource bundle (:/themes/)

Editor changes

The token list now surfaces every editable namespace. Font-family tokens show the family name, integer tokens show "N px", gradient + scalar colour rows keep the existing swatch + descriptor.

Click-menu adapts to the token's namespace:

Namespace Menu options
color.* Existing flat ↔ gradient chooser
font.family.* "Edit font family…" (QFontDialog)
font.size.* / sizing.* "Edit size…" (QInputDialog::getInt, 0–96)

A "Reset to default" entry surfaces at the bottom of the menu whenever the token has a factory baseline. The route picks the right setter based on the token's namespace + the factory value's shape (gradient vs scalar colour vs integer vs string).

New "Theme actions ▾" dropdown at the left of the button row:

  • Rename theme… — refuses on built-ins with a friendly message pointing to Save As…
  • Delete theme… — confirms before unlinking; switches to Default Dark first so consumers don't render against a half-loaded token map

Inspector overlay fix

The cyan tracking outline was leaking on top of the row's type-chooser QMenu and the Theme actions dropdown. ThemeInspector::eventFilter now early-returns and hides its overlay whenever either QApplication::activeModalWidget() OR QApplication::activePopupWidget() is non-null — QMenus register as popups, not modals, so both checks are required to cover the full child-interaction surface. Same auto-resume on close.

Test plan

  • Builds clean (--target all)
  • font.family.ui → "Edit font family…" → QFontDialog → pick → UI re-fonts
  • font.size.normal → "Edit size…" → spinbox → labels grow
  • sizing.panel.padding → spinbox → applet panels get roomier
  • Right-edit anything → "Reset to default" → restores from :/themes/default-dark.json
  • Theme actions ▾ → Delete theme… (on a user-saved theme) → switches to Default Dark + removes JSON file
  • Save As "My Test" → Theme actions ▾ → Rename theme… → "My Renamed" → file renames + active state follows
  • Inspect on + click font row → menu shows; cyan overlay stays hidden through the QMenu and the subsequent QFontDialog

🤖 Generated with Claude Code

…heme rename/delete

## Summary

Closes Phase 5 from RFC #3076 — every editable token namespace
(`color.*`, `font.*`, `sizing.*`) now has a working picker in the
Theme Editor, and saved user themes can be renamed or deleted from
the dialog without touching the filesystem manually.

## ThemeManager additions

  * setString(token, value) — font-family setter; mirrors setColor /
    setSizing.
  * factoryColor() / factorySizing() / factoryString() /
    hasFactoryValue() — extend the factory-snapshot lookup beyond
    gradients so every Reset-to-default affordance works across all
    namespaces.
  * deleteTheme(name) — switches the active theme to Default Dark
    before unlinking so the UI doesn't render half-blank during the
    file removal; refuses on built-in themes (paths starting with
    ":/themes/").
  * renameTheme(oldName, newName) — rewrites the JSON file's "name"
    field + filesystem rename in lockstep; refuses on built-ins and
    name collisions.
  * isBuiltInTheme(name) — detected by whether the recorded path is
    inside the Qt resource bundle.

## Editor changes

The token list now surfaces every editable namespace.  Font-family
tokens show the family name, integer tokens show "N px", gradient +
scalar colour rows keep the existing swatch + descriptor.

Click-menu adapts to the token's namespace:

  * color.*           — existing flat ↔ gradient chooser
  * font.family.*     — "Edit font family…" (QFontDialog)
  * font.size.* / sizing.* — "Edit size…" (QInputDialog::getInt)

A "Reset to default" entry surfaces at the bottom of the menu
whenever the token has a factory baseline (every bundled token
does).  Route picks the right setter based on the token's
namespace + the factory value's shape (gradient vs scalar colour
vs integer vs string).

A new "Theme actions ▾" dropdown at the left of the button row
exposes:

  * Rename theme… — refuses on built-ins with a friendly message
    pointing the operator at Save As… as the workaround.
  * Delete theme… — confirms before unlinking; switches to Default
    Dark first so consumers don't render against a half-loaded map.

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 20:13
@ten9876 ten9876 merged commit 0fa728b into main May 25, 2026
5 checks passed
@ten9876 ten9876 deleted the auto/theme-pr4 branch May 25, 2026 20:47
ten9876 added a commit that referenced this pull request May 25, 2026
)

## Summary

Closes [RFC #3076](#3076)
by adding a share-friendly file format for AetherSDR themes plus the
import / export / drag-and-drop UI that operators need to actually swap
themes around.

**Depends on #3160** — currently targets `auto/theme-pr4` so the diff
shows only Phase 6's delta. Will retarget to main once #3160 lands.

## ThemeManager additions

- `exportThemeToFile(themeName, filePath, *err)` — dumps the active
theme's live state (or another registered theme's on-disk JSON) to an
arbitrary file
- `importThemeFromFile(filePath, *err)` — validates schema, picks a
destination name from the JSON's `"name"` field (file stem fallback),
sanitises against path traversal, refuses to clobber built-ins, suffixes
`(2)` / `(3)`… on name collisions, copies into
`~/.config/AetherSDR/themes/`, makes it active. Returns the final name
on success.

## Theme Editor changes

- **"Theme actions" menu** gains an Export… / Import… pair below the
existing Rename / Delete actions
- **Drag-and-drop**: drop one or more `.aethertheme` (or `.json` theme)
files anywhere on the dialog window → each imports + activates in order
- Drop validation refuses non-local URLs and any file whose extension
isn't `aethertheme` / `json` — arbitrary files get the no-go cursor at
drag-enter
- Cosmetic: removed the manual `▾` glyph from "Theme actions" button
label (QPushButton with `setMenu()` auto-renders one; the two collided)

## File format (`.aethertheme`)

Plain JSON, identical shape to the user-dir `themes/*.json` files (same
`schemaVersion`, `name`, `author`, `version`, `description`, `tokens`).
Sharing happens via file exchange — Discord, Discussions, gist, email
attachment. No hosted marketplace.

## Format compatibility

- `schemaVersion = 1` is current; `> 1` loads with a log warning instead
of refusing — unknown tokens round-trip unchanged so a future v2 theme
survives a v1 build
- Missing tokens on import fall back to built-in defaults via the
existing `seedBuiltinDefaults()` path

## Test plan

- [x] Builds clean (`--target all`)
- [x] Save As "My Glassy Plasma" → Theme actions → Export to file… →
save as `.aethertheme`
- [x] Switch to Default Dark → Theme actions → Import from file… →
re-imports + activates
- [x] Drag the same file onto the dialog → re-imports as "My Glassy
Plasma (2)"
- [x] Drop a non-theme file → silently refused at drag-enter
- [x] Single Qt-rendered dropdown arrow on the Theme actions button (was
two)

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

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ten9876 added a commit that referenced this pull request May 25, 2026
…eq + temp widget classes

Bundle the canonical 7-segment / 14-segment LCD-replica fonts every ham
rig (FlexRadio Maestro, Yaesu, Icom) uses for frequency / mode readout,
plus the matching DSEGWeather icon font.  All 13 TTFs total ~295 KB raw;
the qrc embedding bakes them into the binary so themes resolve them by
family name without depending on the system having them installed.

### Bundled assets

- third_party/dseg/DSEG7-Modern/   (6 weights: Light/LightItalic/Regular/
                                    Italic/Bold/BoldItalic)
- third_party/dseg/DSEG14-Modern/  (same six weights)
- third_party/dseg/DSEGWeather/    (single weight icon font)
- third_party/dseg/DSEG-LICENSE.txt + README.md preserved verbatim for
  attribution (SIL OFL 1.1 — explicitly permits bundling + embedding;
  not viral on application code).

### Plumbing

- resources/resources.qrc — new /fonts/ prefix exposes all 13 TTFs
- src/main.cpp — QFontDatabase::addApplicationFont() loader at startup
- ThemeManager.cpp seedBuiltinDefaults() — five new font.family tokens:
    * segment7  = "DSEG7 Modern"   (raw family)
    * segment14 = "DSEG14 Modern"  (raw family)
    * weather   = "DSEGWeather"    (raw family)
    * freq      = "DSEG7 Modern"   (widget-class — see below)
    * temp      = "DSEG7 Modern"   (widget-class — see below)
- Both bundled themes (default-dark.json, default-light.json) get the
  same five entries under font.family so the Theme Editor's font-family
  picker (PR #3160) surfaces them automatically.

### Widget-class tokens (freq, temp)

`font.family.freq` is read by both VfoWidget's frequency label/edit
and RxApplet's frequency label/edit — one Theme Editor pick re-themes
both surfaces in lockstep via the existing applyStyleSheet reverse-map.

`font.family.temp` is registered for future temperature surfaces
(band-conditions / propagation readouts) so they can theme
independently from freq when those widgets land.  No widget paints
with it yet.

Defaults to DSEG7 Modern so a fresh install gets the segment look out
of the box.  Operators who want a proportional / fixed look can pick
any installed family via Theme Editor → click `font.family.freq` row →
QFontDialog (#3160 added that picker).

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
… theme rename/delete (aethersdr#3160)

## Summary

Closes [RFC aethersdr#3076](aethersdr#3076
Phase 5 — every editable token namespace now has a working picker in the
Theme Editor, and saved user themes can be renamed or deleted from the
dialog without touching the filesystem.

## ThemeManager additions

- `setString(token, value)` — font-family setter; mirrors `setColor` /
`setSizing`
- `factoryColor()` / `factorySizing()` / `factoryString()` /
`hasFactoryValue()` — extend the factory-snapshot lookup beyond
gradients
- `deleteTheme(name)` — switches active theme to Default Dark before
unlinking; refuses on built-ins
- `renameTheme(oldName, newName)` — rewrites the JSON file's `"name"`
field + filesystem rename in lockstep; refuses on built-ins and name
collisions
- `isBuiltInTheme(name)` — detected by whether the recorded path is
inside the Qt resource bundle (`:/themes/`)

## Editor changes

The token list now surfaces **every** editable namespace. Font-family
tokens show the family name, integer tokens show "N px", gradient +
scalar colour rows keep the existing swatch + descriptor.

Click-menu adapts to the token's namespace:

| Namespace | Menu options |
|---|---|
| `color.*` | Existing flat ↔ gradient chooser |
| `font.family.*` | "Edit font family…" (`QFontDialog`) |
| `font.size.*` / `sizing.*` | "Edit size…" (`QInputDialog::getInt`,
0–96) |

A **"Reset to default"** entry surfaces at the bottom of the menu
whenever the token has a factory baseline. The route picks the right
setter based on the token's namespace + the factory value's shape
(gradient vs scalar colour vs integer vs string).

New **"Theme actions ▾"** dropdown at the left of the button row:

- **Rename theme…** — refuses on built-ins with a friendly message
pointing to Save As…
- **Delete theme…** — confirms before unlinking; switches to Default
Dark first so consumers don't render against a half-loaded token map

## Inspector overlay fix

The cyan tracking outline was leaking on top of the row's type-chooser
QMenu and the Theme actions dropdown. `ThemeInspector::eventFilter` now
early-returns and hides its overlay whenever **either**
`QApplication::activeModalWidget()` OR
`QApplication::activePopupWidget()` is non-null — QMenus register as
popups, not modals, so both checks are required to cover the full
child-interaction surface. Same auto-resume on close.

## Test plan

- [x] Builds clean (`--target all`)
- [x] `font.family.ui` → "Edit font family…" → QFontDialog → pick → UI
re-fonts
- [x] `font.size.normal` → "Edit size…" → spinbox → labels grow
- [x] `sizing.panel.padding` → spinbox → applet panels get roomier
- [x] Right-edit anything → "Reset to default" → restores from
`:/themes/default-dark.json`
- [x] Theme actions ▾ → Delete theme… (on a user-saved theme) → switches
to Default Dark + removes JSON file
- [x] Save As "My Test" → Theme actions ▾ → Rename theme… → "My Renamed"
→ file renames + active state follows
- [x] Inspect on + click font row → menu shows; cyan overlay stays
hidden through the QMenu and the subsequent QFontDialog

🤖 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
…thersdr#3164)

## Summary

Closes [RFC aethersdr#3076](aethersdr#3076)
by adding a share-friendly file format for AetherSDR themes plus the
import / export / drag-and-drop UI that operators need to actually swap
themes around.

**Depends on aethersdr#3160** — currently targets `auto/theme-pr4` so the diff
shows only Phase 6's delta. Will retarget to main once aethersdr#3160 lands.

## ThemeManager additions

- `exportThemeToFile(themeName, filePath, *err)` — dumps the active
theme's live state (or another registered theme's on-disk JSON) to an
arbitrary file
- `importThemeFromFile(filePath, *err)` — validates schema, picks a
destination name from the JSON's `"name"` field (file stem fallback),
sanitises against path traversal, refuses to clobber built-ins, suffixes
`(2)` / `(3)`… on name collisions, copies into
`~/.config/AetherSDR/themes/`, makes it active. Returns the final name
on success.

## Theme Editor changes

- **"Theme actions" menu** gains an Export… / Import… pair below the
existing Rename / Delete actions
- **Drag-and-drop**: drop one or more `.aethertheme` (or `.json` theme)
files anywhere on the dialog window → each imports + activates in order
- Drop validation refuses non-local URLs and any file whose extension
isn't `aethertheme` / `json` — arbitrary files get the no-go cursor at
drag-enter
- Cosmetic: removed the manual `▾` glyph from "Theme actions" button
label (QPushButton with `setMenu()` auto-renders one; the two collided)

## File format (`.aethertheme`)

Plain JSON, identical shape to the user-dir `themes/*.json` files (same
`schemaVersion`, `name`, `author`, `version`, `description`, `tokens`).
Sharing happens via file exchange — Discord, Discussions, gist, email
attachment. No hosted marketplace.

## Format compatibility

- `schemaVersion = 1` is current; `> 1` loads with a log warning instead
of refusing — unknown tokens round-trip unchanged so a future v2 theme
survives a v1 build
- Missing tokens on import fall back to built-in defaults via the
existing `seedBuiltinDefaults()` path

## Test plan

- [x] Builds clean (`--target all`)
- [x] Save As "My Glassy Plasma" → Theme actions → Export to file… →
save as `.aethertheme`
- [x] Switch to Default Dark → Theme actions → Import from file… →
re-imports + activates
- [x] Drag the same file onto the dialog → re-imports as "My Glassy
Plasma (2)"
- [x] Drop a non-theme file → silently refused at drag-enter
- [x] Single Qt-rendered dropdown arrow on the Theme actions button (was
two)

🤖 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.

RFC: Theming subsystem (light + dark + user-customizable + import/export)

1 participant