feat(theme): Phase 5 PR 4 — font + sizing pickers + per-token reset + theme rename/delete#3160
Merged
Conversation
…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>
6 tasks
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>
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 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; mirrorssetColor/setSizingfactoryColor()/factorySizing()/factoryString()/hasFactoryValue()— extend the factory-snapshot lookup beyond gradientsdeleteTheme(name)— switches active theme to Default Dark before unlinking; refuses on built-insrenameTheme(oldName, newName)— rewrites the JSON file's"name"field + filesystem rename in lockstep; refuses on built-ins and name collisionsisBuiltInTheme(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:
color.*font.family.*QFontDialog)font.size.*/sizing.*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:
Inspector overlay fix
The cyan tracking outline was leaking on top of the row's type-chooser QMenu and the Theme actions dropdown.
ThemeInspector::eventFilternow early-returns and hides its overlay whenever eitherQApplication::activeModalWidget()ORQApplication::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
--target all)font.family.ui→ "Edit font family…" → QFontDialog → pick → UI re-fontsfont.size.normal→ "Edit size…" → spinbox → labels growsizing.panel.padding→ spinbox → applet panels get roomier:/themes/default-dark.json🤖 Generated with Claude Code