Skip to content

Runtime theming infrastructure with BYOT support (W3C Design Tokens) #2453

Description

@ten9876

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/)

class ThemeManager : public QObject {
    Q_OBJECT
public:
    static ThemeManager& instance();
    
    // Load all themes from search paths.
    void scanThemes();
    
    // Switch active theme by name.  Emits themeChanged when complete.
    bool setActiveTheme(const QString& name);
    QString activeTheme() const;
    
    // Lookup a token value by W3C-style path: "color.surface.base"
    QColor   color(const QString& path) const;
    int      dimension(const QString& path) const;
    QString  raw(const QString& path) const;
    
    // List of {name, displayName, path} for the View menu.
    QList<ThemeInfo> availableThemes() const;
    
signals:
    void themeChanged();
    void themesRescanned();
};

Storage and search paths

Themes loaded from (in priority order):

  1. 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.
  2. 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:

  1. Each applet exposes a refreshTheme() slot that rebuilds its QSS using ThemeManager::instance().color(...) lookups.
  2. ThemeManager::themeChanged connects to every refreshTheme() slot.
  3. On the global level, qApp->setStyleSheet(buildGlobalStylesheet()) re-applies the base stylesheet (Theme.h equivalent).
  4. 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)
  • Card borders with shadowing
  • Current accent / surface / text vocabulary

Bundled alongside:

Other community themes can ship via the BYOT mechanism without project commitment.

Migration plan

This is multi-phase. Suggested order:

  1. Foundation — ThemeManager + JSON loader + default-theme JSON file containing every current hardcoded color. No visible behaviour change. This is the bulk of the work.
  2. Migrate Theme.h — switch the global stylesheet builder to read from ThemeManager.
  3. Migrate applets incrementally — RxApplet, TxApplet, PanadapterApplet, then Strip* panels, then editors. Each PR migrates one or two files; refreshTheme slot added; themeChanged connected.
  4. Wire View menu — Theme submenu with switch/reload/open-folder actions.
  5. Performance pass — measure theme-switch time, add suppress-updates / defer-to-idle / cache-QSS optimizations.
  6. Document the format in docs/theming.md with the JSON shape, examples, and a how-to for users authoring custom themes.

Out of scope for this issue

  • Visual design changes (flat / minimal / etc.) — themes are the mechanism, design direction is separate.
  • Light theme — interesting future work but not required for v1; community can ship one via BYOT.

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

73, Jeremy KK7GWY & Claude (AI dev partner)

Metadata

Metadata

Assignees

No one assigned

    Labels

    GUIUser interfaceNew FeatureNew feature requestawaiting-responseWaiting for reporter to provide additional informationenhancementImprovement to existing featuremaintainer-reviewRequires maintainer review before any action is takenpriority: mediumMedium priority

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions