You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add runtime theme switching to AetherSDR with bring-your-own-theme support — users drop a JSON file in their config directory and the theme appears in a View → Theme menu, ready to switch into without restarting the app.
This issue defines the architecture; #2374 surfaced the need for a token foundation but the visual flat-design direction is not what we're committing to. Default theme remains the current instrument-style look; flat / high-contrast / community themes ride on top via the BYOT mechanism.
Spec format: W3C Design Tokens Format Module
Author themes in W3C Design Tokens JSON — the de facto industry standard, also targeted by Style Dictionary, Tokens Studio, and most modern design systems.
Token references ({color.surface.base}) supported per the W3C spec.
Architecture
ThemeManager (singleton, src/core/)
classThemeManager : publicQObject {
Q_OBJECTpublic:static ThemeManager& instance();
// Load all themes from search paths.voidscanThemes();
// Switch active theme by name. Emits themeChanged when complete.boolsetActiveTheme(const QString& name);
QString activeTheme() const;
// Lookup a token value by W3C-style path: "color.surface.base"
QColor color(const QString& path) const;
intdimension(const QString& path) const;
QString raw(const QString& path) const;
// List of {name, displayName, path} for the View menu.
QList<ThemeInfo> availableThemes() const;
signals:
voidthemeChanged();
voidthemesRescanned();
};
Storage and search paths
Themes loaded from (in priority order):
User: ~/.config/AetherSDR/themes/*.json (Linux), %LOCALAPPDATA%/AetherSDR/themes/ (Windows), ~/Library/Preferences/AetherSDR/themes/ (macOS) — uses QStandardPaths::GenericConfigLocation + /AetherSDR/themes to match the path fix from fix(windows): correct settings path so UI scaling works on Windows #2443.
Bundled: <resource>/themes/*.json — built-in themes shipped with AetherSDR (default, high-contrast).
User themes override bundled themes with the same name. On first run, bundled themes are also copied into the user directory so users have a starting template to fork from.
Active theme persistence
AppSettings("ActiveTheme") stores the chosen theme name. Default "AetherSDR Default".
Stylesheet generation
Today every applet has hand-written QSS strings with hardcoded hex. Migration:
Each applet exposes a refreshTheme() slot that rebuilds its QSS using ThemeManager::instance().color(...) lookups.
ThemeManager::themeChanged connects to every refreshTheme() slot.
On the global level, qApp->setStyleSheet(buildGlobalStylesheet()) re-applies the base stylesheet (Theme.h equivalent).
QPalette is also updated for colors that propagate via palette (selected text, link colors) — palette changes propagate without per-widget restyling.
Performance considerations
Suppress repaints during switch: setUpdatesEnabled(false) on each top-level window, swap stylesheets, then setUpdatesEnabled(true).
Defer to idle: QTimer::singleShot(0, this, &::applyTheme) so pending UI events drain first.
Cache compiled stylesheets in ThemeManager — don't rebuild the QSS string on each widget; build once, distribute.
Target: theme switch < 250ms with brief acceptable flicker. Comparable to VS Code / Spotify.
UI integration
View → Theme submenu lists available themes; checkbox-style action group; switching emits the change.
View → Theme → Open Themes Folder opens the user themes directory in the file manager.
View → Theme → Reload re-scans the themes directory (so users editing a theme can iterate without restarting).
Settings → Theme tab in a future settings dialog: theme picker + small preview, optional "export current theme" button that writes the active token set as a starting JSON.
Default theme content
Default theme = the current "instrument" look, NOT flat. Preserve:
Title bar gradients (the 18 px blue gradient family)
Other community themes can ship via the BYOT mechanism without project commitment.
Migration plan
This is multi-phase. Suggested order:
Foundation — ThemeManager + JSON loader + default-theme JSON file containing every current hardcoded color. No visible behaviour change. This is the bulk of the work.
Migrate Theme.h — switch the global stylesheet builder to read from ThemeManager.
Migrate applets incrementally — RxApplet, TxApplet, PanadapterApplet, then Strip* panels, then editors. Each PR migrates one or two files; refreshTheme slot added; themeChanged connected.
Wire View menu — Theme submenu with switch/reload/open-folder actions.
cc @LU5DX — the DesignTokens.h vocabulary you proposed in #2374 maps cleanly onto this spec; if you want to take a stab at the foundation phase (item 1 above), we'd welcome it. The visual flat-design phases from #2374 aren't being committed to as the project default, but flat.json shipping as a bundled alternative theme is on the table.
Priority: medium. No specific deadline — this unlocks BYOT and accessibility but the current hardcoded look isn't broken.
Goal
Add runtime theme switching to AetherSDR with bring-your-own-theme support — users drop a JSON file in their config directory and the theme appears in a View → Theme menu, ready to switch into without restarting the app.
This issue defines the architecture; #2374 surfaced the need for a token foundation but the visual flat-design direction is not what we're committing to. Default theme remains the current instrument-style look; flat / high-contrast / community themes ride on top via the BYOT mechanism.
Spec format: W3C Design Tokens Format Module
Author themes in W3C Design Tokens JSON — the de facto industry standard, also targeted by Style Dictionary, Tokens Studio, and most modern design systems.
Sample shape:
{ "$schema": "https://design-tokens.github.io/community-group/format/", "color": { "surface": { "base": { "$value": "#0f0f1a", "$type": "color" }, "panel": { "$value": "#141422", "$type": "color" }, "sunken": { "$value": "#0a0a14", "$type": "color" } }, "border": { "subtle": { "$value": "#1a2535", "$type": "color" }, "control": { "$value": "#253545", "$type": "color" }, "interactive": { "$value": "#2a4060", "$type": "color" } }, "text": { "primary": { "$value": "#c8d8e8", "$type": "color" }, "secondary": { "$value": "#7a8fa0", "$type": "color" }, "accent": { "$value": "#00b4d8", "$type": "color" } }, "semantic": { "danger": { "$value": "#cc3030", "$type": "color" }, "warning": { "$value": "#d08020", "$type": "color" }, "success": { "$value": "#20a060", "$type": "color" } } }, "control": { "radius": { "$value": "3px", "$type": "dimension" }, "btnHeight": { "$value": "22px", "$type": "dimension" } }, "font": { "sm": { "$value": 11, "$type": "number" }, "md": { "$value": 13, "$type": "number" }, "lg": { "$value": 16, "$type": "number" }, "freq": { "$value": 22, "$type": "number" } }, "meta": { "name": "AetherSDR Default", "author": "AetherSDR", "version": "1.0.0" } }Token references (
{color.surface.base}) supported per the W3C spec.Architecture
ThemeManager(singleton, src/core/)Storage and search paths
Themes loaded from (in priority order):
~/.config/AetherSDR/themes/*.json(Linux),%LOCALAPPDATA%/AetherSDR/themes/(Windows),~/Library/Preferences/AetherSDR/themes/(macOS) — usesQStandardPaths::GenericConfigLocation+/AetherSDR/themesto match the path fix from fix(windows): correct settings path so UI scaling works on Windows #2443.<resource>/themes/*.json— built-in themes shipped with AetherSDR (default, high-contrast).User themes override bundled themes with the same name. On first run, bundled themes are also copied into the user directory so users have a starting template to fork from.
Active theme persistence
AppSettings("ActiveTheme")stores the chosen theme name. Default"AetherSDR Default".Stylesheet generation
Today every applet has hand-written QSS strings with hardcoded hex. Migration:
refreshTheme()slot that rebuilds its QSS usingThemeManager::instance().color(...)lookups.ThemeManager::themeChangedconnects to everyrefreshTheme()slot.qApp->setStyleSheet(buildGlobalStylesheet())re-applies the base stylesheet (Theme.h equivalent).QPaletteis also updated for colors that propagate via palette (selected text, link colors) — palette changes propagate without per-widget restyling.Performance considerations
setUpdatesEnabled(false)on each top-level window, swap stylesheets, thensetUpdatesEnabled(true).QTimer::singleShot(0, this, &::applyTheme)so pending UI events drain first.UI integration
Default theme content
Default theme = the current "instrument" look, NOT flat. Preserve:
Bundled alongside:
high-contrast.json— accessibility theme.flat.json— covers the design direction explored in Flat design system: Phases 1-4 token layer + UI cleanup #2374, available as an opt-in user theme.Other community themes can ship via the BYOT mechanism without project commitment.
Migration plan
This is multi-phase. Suggested order:
refreshThemeslot added;themeChangedconnected.Out of scope for this issue
References
Notes
cc @LU5DX — the DesignTokens.h vocabulary you proposed in #2374 maps cleanly onto this spec; if you want to take a stab at the foundation phase (item 1 above), we'd welcome it. The visual flat-design phases from #2374 aren't being committed to as the project default, but
flat.jsonshipping as a bundled alternative theme is on the table.Priority: medium. No specific deadline — this unlocks BYOT and accessibility but the current hardcoded look isn't broken.
73, Jeremy KK7GWY & Claude (AI dev partner)