Skip to content

RFC: Flat Design System — Design Token Vocabulary and GUI Stylesheet Consolidation #2294

Description

@LU5DX

Preflight

  • I have read GOVERNANCE.md and confirmed this change requires an RFC
  • I have searched existing issues and this RFC has not been proposed before
  • I have not opened a PR for this change yet

Problem

Summary

Introduce a single, coherent flat design system across the entire AetherSDR UI, eliminating the current patchwork of per-component inline style overrides and replacing them with a unified token vocabulary defined in one place. The result is a visually consistent interface that feels like a single product — consistent spacing, consistent controls, consistent panel structure — with no changes to any radio protocol behavior, control logic, or AppSettings.

The accompanying screenshots (aethersdrcurrentui.png') is the current UI. aetherproposedui.png` is an AI generated mockup to provide as a reference of the Flat UI look. It is not a fully accurate representation of what the AestherSDR Flat UI would look like with all its real UI elements.


Problem Statement

AetherSDR's UI is built from a global stylesheet in Theme.h plus dozens of per-component setStyleSheet() overrides scattered across individual applet and widget files. As components were added incrementally, the overrides diverged from the global values:

1 · Button border-radius — three values in active use

Location Radius
Theme.h (global QPushButton) 4px
RxApplet.cpp kButtonBase 3px
TxApplet.cpp action buttons (TUNE / MOX / ATU / MEM) 3px
PanadapterApplet.cpp CW panel buttons 2px
PanadapterApplet.cpp lock-pitch / lock-speed buttons 2px

2 · Button font-size — five values in active use

Location Font size
Theme.h base inheritance 13px
TxApplet.cpp, RxApplet.cpp action buttons 10px bold
PanadapterApplet.cpp CW copy/clear buttons 9px bold
PanadapterApplet.cpp pop-out / max buttons 11px
PanadapterApplet.cpp CW pitch range labels 8px

3 · Border color — three different values for "normal" borders

Location Color
Theme.h (global) #203040
RxApplet.cpp kButtonBase, TxApplet.cpp action buttons #205070
RxApplet.cpp kInsetValueStyle, TxApplet.cpp ATU inset #1e2e3e

4 · Slider handle size — three sizes

Location Handle width
Theme.h (global) 14px, margin -5px 0
RxApplet.cpp, TxApplet.cpp 10px, margin -3px 0
PanadapterApplet.cpp CW pitch range 8px, margin -3px 0

5 · Dim / secondary label color — four different values

Location Color
RxApplet.cpp kDimLabelStyle #8090a0
TxApplet.cpp RF/Tune power labels #8aa8c0
TxApplet.cpp gauge scale labels #405060
PanadapterApplet.cpp CW hints, stats #6a8090

6 · Hover color — two values for button hover

Location Hover background
Theme.h (global QPushButton:hover) #203040
RxApplet.cpp, TxApplet.cpp action buttons #204060

7 · Panel header — gradient vs flat

PanadapterApplet.cpp applies a vertical gradient to every panadapter title bar:

m_titleBar->setStyleSheet(
    "QWidget { background: qlineargradient(x1:0,y1:0,x2:0,y2:1,"
    "stop:0 #3a4a5a, stop:0.5 #2a3a4a, stop:1 #1a2a38); "
    "border-bottom: 1px solid #0a1a28; }");

This is the only gradient in the codebase. All other panels use flat colors, making the panadapter title bars stand out inconsistently.

8 · Deeply-sunken background — two near-identical values

#0a0a14 (Theme.h status bar, menu bar, PanadapterApplet.cpp CW text area) vs
#0a0a18 (RxApplet.cpp value insets, TxApplet.cpp ATU inset).
Both values serve the same semantic role and are visually indistinguishable, but create unnecessary maintenance surface.

Image

Proposal

Proposed Solution

Introduce a design token layer in src/gui/ that centralizes every visual constant. All existing setStyleSheet() overrides that duplicate or contradict the global theme are removed and replaced with references to the token layer. The global stylesheet in Theme.h is updated to use those same tokens.

This is a stylesheet-only refactor. No widget hierarchy changes, no layout changes, no signal rewiring, and no AppSettings keys are touched.


Part 1 — Design Token Vocabulary

Introduce src/gui/DesignTokens.h — a header of constexpr string values and a generated stylesheet fragment.

1.1 · Surface / Background Scale

surface-base      #0f0f1a   App background (all QWidget default)
surface-panel     #141422   Applet bodies, sidebar backgrounds
surface-sunken    #0a0a14   Insets, value readouts, deep backgrounds
surface-overlay   #1c2030   Modals, floating menus, tooltips

The current three-way split (#0f0f1a / #111120 / #0a0a14 / #0a0a18) collapses to three clearly-named, clearly-differentiated tiers.

1.2 · Border Scale

border-subtle      #1a2535   Structural panel separators
border-control     #253545   Button and control borders (default state)
border-interactive #2a4060   Button borders in active/hover region

Replaces the current #203040 / #205070 / #1e2e3e split.

1.3 · Text Scale

text-primary    #c8d8e8   Body text, control labels (keep)
text-secondary  #7a8fa0   Dim labels, secondary captions (unifies #8090a0 / #8aa8c0 / #6a8090)
text-tertiary   #404f5e   Inactive, placeholder, scale ticks (replaces #405060)
text-accent     #00b4d8   Section headers, interactive highlights (keep)

1.4 · Semantic / State Colors

color-accent    #00b4d8   Primary interaction (keep)
color-danger    #cc3030   TX active (TUNE/MOX), critical alerts
color-warning   #d08020   Warning states, SWR caution
color-success   #20a060   Good states, ATU matched, GPS locked

CW decode quality colors (#00ff88, #e0e040, #ff9020, #ff4040) are already internally consistent and fall under success / warning / color-danger semantics — no change needed there.

1.5 · Control Metrics

ctrl-radius      3px      Single border-radius for all controls (buttons, combos, inputs)
ctrl-btn-h       22px     Fixed button height
ctrl-btn-pad     4px 8px  Button padding
ctrl-slider-h    4px      Slider groove height
ctrl-handle-w    10px     Slider handle width (margin: -3px 0)
ctrl-font-sm     11px     Small labels (section hints, meter ticks)
ctrl-font-md     13px     Default / body (keep)
ctrl-font-lg     16px     Section header titles
ctrl-font-freq   22px     VFO frequency readout (see Part 3)

Part 2 — Updated Global Stylesheet (Theme.h)

Theme.h is rewritten to reference the token values above. The key changes from the current version:

Rule Current Proposed
QPushButton border 1px solid #203040 1px solid {border-control}
QPushButton border-radius 4px 3px
QPushButton:hover background #203040 {surface-overlay}
QPushButton:pressed background #00b4d8 {color-accent}
QComboBox border-radius 4px 3px
QSlider handle width 14px / -5px margin 10px / -3px margin
QGroupBox title color #00b4d8 {text-accent}
QStatusBar background #0a0a14 {surface-sunken}
QMenuBar background #0a0a14 {surface-sunken}

No new widget rules are introduced — only token-aligned values replace the existing ones.


Part 3 — Per-Component Cleanup

Each component that currently overrides the global theme with local setStyleSheet() calls is updated to either:
(a) remove the override entirely (if it now matches the global), or
(b) replace the hardcoded value with the appropriate token constant.

PanadapterApplet.cpp — title bar gradient removal

The gradient is replaced with a flat {surface-panel} background and a 1px solid {border-subtle} bottom separator. This brings the panadapter header in line with every other panel in the application.

// Before
m_titleBar->setStyleSheet(
    "QWidget { background: qlineargradient(x1:0,y1:0,x2:0,y2:1,"
    "stop:0 #3a4a5a, stop:0.5 #2a3a4a, stop:1 #1a2a38); "
    "border-bottom: 1px solid #0a1a28; }");

// After
m_titleBar->setStyleSheet(
    "QWidget { background: " + DesignTokens::kSurfacePanel + "; "
    "border-bottom: 1px solid " + DesignTokens::kBorderSubtle + "; }");

RxApplet.cpp

  • kButtonBase: border color #205070{border-interactive}, radius 3px (already correct)
  • kSliderStyle: groove #203040{border-control}, handle 10px (already correct)
  • kDimLabelStyle: color #8090a0{text-secondary}, font-size 11px (already correct)
  • kInsetValueStyle: background #0a0a18{surface-sunken}, border #1e2e3e{border-subtle}

TxApplet.cpp

  • Action button inline styles (TUNE / MOX / ATU / MEM): consolidate to a single kActionButtonStyle constant using tokens; remove the 5 per-button setStyleSheet() calls
  • setStyleSheet() in setTuning() / setTransmitting(): state change becomes a setProperty("active", true) + dynamic stylesheet rule rather than a full string replacement — this is the recommended Qt pattern for state-dependent styles and avoids the repeated full-string rebuild on every TX cycle
  • Label colors #8aa8c0 / #405060{text-secondary} / {text-tertiary}

PanadapterApplet.cpp — CW panel

  • Range slider handle 8px10px (token)
  • CW panel background #0a0a14{surface-sunken} (already the same value, just explicit)
  • Button sizes 8px / 9px / 11px{ctrl-font-sm} (11px) for all

VfoWidget.cpp — frequency readout

Apply {ctrl-font-freq} (22px, QFont::Bold) to the primary frequency label. No layout changes.


Part 4 — Panel Structure Consistency

Each major visual region currently has ad hoc internal structure. The proposal standardizes the pattern without changing layout mechanics:

┌─────────────────────────────────────┐
│  Panel Header (height: 26px)        │ ← surface-panel, border-bottom: border-subtle
│  Title [text-accent, ctrl-font-lg]  │   Icon buttons [text-secondary]
├─────────────────────────────────────┤
│                                     │
│  Panel Body (padding: 8px)          │ ← surface-panel
│  Controls use border-control        │
│                                     │
└─────────────────────────────────────┘

Applied consistently to:

Panel Current header treatment After
PanadapterApplet title bar gradient #3a4a5a → #1a2a38 flat surface-panel
AppletPanel applet headers varies per applet flat surface-panel
Bottom status bar surface-sunken surface-sunken (no change)
ConnectionPanel flat surface-base no change

Part 5 — Proposed DesignTokens.h Structure

// src/gui/DesignTokens.h
#pragma once
#include <QLatin1StringView>

namespace AetherSDR::DesignTokens {

// ── Surfaces ────────────────────────────────────────────────────────────────
constexpr QLatin1StringView kSurfaceBase    {"#0f0f1a"};
constexpr QLatin1StringView kSurfacePanel   {"#141422"};
constexpr QLatin1StringView kSurfaceSunken  {"#0a0a14"};
constexpr QLatin1StringView kSurfaceOverlay {"#1c2030"};

// ── Borders ──────────────────────────────────────────────────────────────────
constexpr QLatin1StringView kBorderSubtle      {"#1a2535"};
constexpr QLatin1StringView kBorderControl     {"#253545"};
constexpr QLatin1StringView kBorderInteractive {"#2a4060"};

// ── Text ──────────────────────────────────────────────────────────────────────
constexpr QLatin1StringView kTextPrimary   {"#c8d8e8"};
constexpr QLatin1StringView kTextSecondary {"#7a8fa0"};
constexpr QLatin1StringView kTextTertiary  {"#404f5e"};
constexpr QLatin1StringView kTextAccent    {"#00b4d8"};

// ── Semantic ──────────────────────────────────────────────────────────────────
constexpr QLatin1StringView kColorAccent  {"#00b4d8"};
constexpr QLatin1StringView kColorDanger  {"#cc3030"};
constexpr QLatin1StringView kColorWarning {"#d08020"};
constexpr QLatin1StringView kColorSuccess {"#20a060"};

// ── Control metrics (used in both stylesheets and layout code) ────────────────
constexpr int kCtrlRadius    = 3;
constexpr int kCtrlBtnHeight = 22;
constexpr int kCtrlSliderH   = 4;
constexpr int kCtrlHandleW   = 10;
constexpr int kFontSm        = 11;
constexpr int kFontMd        = 13;
constexpr int kFontLg        = 16;
constexpr int kFontFreq      = 22;

} // namespace AetherSDR::DesignTokens

Theme.h::darkThemeStylesheet() is updated to build its QStringLiteral using these values via a helper that substitutes tokens. All per-component setStyleSheet() calls that duplicate theme values are removed.


Implementation Path

Because this is a pure visual refactor with no protocol impact, it can be delivered incrementally across multiple PRs without risking regression in the radio control path.

Phase 1 — Token layer + Theme.h alignment (no per-component changes)

  • Add DesignTokens.h
  • Update Theme.h to use tokens
  • Verify the global stylesheet produces no visual regressions on all applets that rely on inheritance (those that do not override)
  • Scope: ~60 lines added (DesignTokens.h) + Theme.h rewrite (~75 lines)

Phase 2 — Gradient removal and panel header normalization

  • PanadapterApplet.cpp title bar: gradient → flat
  • Verify title bar still correctly handles pop-out / maximize / close button layout at minimum and maximum panel widths
  • Scope: ~5 lines changed

Phase 3 — RxApplet and TxApplet cleanup

  • Replace inline magic-value strings with token constants
  • Consolidate repeated setStyleSheet() calls in TxApplet TX-state transitions to use Qt dynamic property pattern
  • Scope: ~40–60 lines changed across 2 files

Phase 4 — PanadapterApplet CW panel + VfoWidget frequency readout

  • CW panel control sizes normalized
  • VFO frequency label font bump to kFontFreq
  • Scope: ~20 lines changed

Phase 5 — Remaining applet audit

  • Grep entire src/gui/ for setStyleSheet calls; any remaining hardcoded hex values are replaced with token references
  • Scope: varies; estimated 80–120 lines total across ~10 files

Total estimated scope: 250–350 lines changed across 15–20 files, zero new dependencies, zero CI impact.


What This RFC Does NOT Propose

To keep scope clearly bounded, the following are explicitly out of scope:

  • Changes to widget hierarchy or layouts — no QVBoxLayout / QHBoxLayout restructuring
  • New widgets or panels — no new UI surfaces introduced
  • AppSettings changes — no new keys, no defaults changed, no migrations
  • Protocol or audio engine touches — zero
  • Collapsible panels, drag-to-reorder changes, or applet visibility logic — unchanged
  • Color scheme changes for spectrum / waterfall — covered separately if desired
  • Light theme — not proposed here; token structure would make one easier to add later

Risk Assessment

Risk Likelihood Mitigation
Global stylesheet change breaks widget that relied on un-overridden value Low Phase 1 is stylesheet-only and can be tested visually against all panels before any per-component changes
Token value slightly changes appearance of a control Low Intentional and the point of this RFC; values are chosen to be perceptually close to the originals
setStyleSheet() removal causes applet to fall back to unexpected inherited value Low Review each removal against the inheritance chain; confirm token-based value matches semantic intent
TxApplet dynamic-property approach for TX state requires Qt stylesheet refresh Low style()->unpolish(widget); style()->polish(widget); is the standard Qt pattern and is well-tested

No changes to the radio control path, no changes to VITA-49 handling, no new threads.


Open Questions for Maintainer

  1. Surface tier naming: surface-panel (#141422) is slightly lighter than surface-base (#0f0f1a) to distinguish panel bodies from the canvas background. Is this delta appropriate, or is a strictly zero-elevation flat look preferred (all panels same color as background, separated only by borders)?

  2. CW decode quality colors: The current #00ff88 / #e0e040 / #ff9020 / #ff4040 gradient is brighter/more saturated than the proposed semantic tokens. These can stay as-is (they serve a distinct purpose and are already consistent) or be brought in line with kColorSuccess / kColorWarning / kColorDanger. Maintainer preference?

  3. Phased delivery vs single PR: The five-phase plan above allows incremental review. If a single omnibus PR is preferred for a refactor of this nature, the implementation is straightforward to merge into one. What is the preferred delivery model?

  4. DesignTokens.h placement: Proposed location is src/gui/DesignTokens.h. If there is a preference to keep it closer to Theme.h as a combined header, both can be merged into an expanded Theme.h without affecting the token vocabulary.


References

  • Current UI: aethersdrcurrentui.png (attached)
  • Proposed UI: aetherproposedui.png (attached)
  • src/gui/Theme.h — global stylesheet
  • src/gui/RxApplet.cpp:104–119kButtonBase, kSliderStyle, kDimLabelStyle, kInsetValueStyle
  • src/gui/TxApplet.cpp:43–46, 164–170 — local slider + action button styles
  • src/gui/PanadapterApplet.cpp:31–34 — gradient title bar
  • src/gui/VfoWidget.h — frequency display label
  • CLAUDE.md — autonomous agent design-change boundary (this RFC is targets the maintainer for review, not autonomous AI action)
Image Image

Cross-platform impact

No blockers — one hard fix required, five soft concerns


Hard fix required — ComboStyle.h must be included in scope

ComboStyle.h is a standalone combo-box stylesheet helper that generates a custom down-arrow PNG and applies its own color constants. It is called via applyComboStyle(combo) across many applets. Its hardcoded
hex values (#1a2a3a, #00b4d8, etc.) are not touched by the global Theme.h and will drift from the token system unless explicitly updated. This file must be part of the RFC scope — it uses the same
border/background values that the token layer would standardize.


1 · Font stack — "Inter" and "Segoe UI" are platform-specific

Theme.h declares font-family: "Inter", "Segoe UI", sans-serif. Neither Inter nor Segoe UI is a system font on macOS or Linux. On those platforms Qt falls back to the generic sans-serif (San Francisco on
macOS, whatever Fontconfig resolves on Linux — typically DejaVu Sans or Noto Sans). No Inter font is loaded via QFontDatabase::addApplicationFont anywhere in the codebase.

Impact on the RFC: The ctrl-font-freq: 22px token for the VFO frequency readout will render at the same logical size on all platforms, but the rendered weight and letterform will differ — San Francisco at
22px looks measurably different from Segoe UI at 22px. This is not a blocking problem (the size increase is the goal, not the specific font), but it is worth calling out in the RFC and noting as a candidate
for bundling Inter as an application font resource in a follow-on pass.


2 · HiDPI — not a concern; Qt6 handles it

The codebase reads from AppSettings and sets QT_SCALE_FACTOR before QApplication is constructed. Qt6 also enables high-DPI scaling unconditionally. All stylesheet pixel values are logical
pixels — a 22px button height renders as 44 physical pixels on a 2× Retina screen automatically. The token values are correct as stated and will scale on all platforms without modification.

The GPU spectrum overlay (SpectrumWidget.cpp:3025, 3321) already calls devicePixelRatioF() for texture allocation — this is unrelated to the stylesheet layer.


3 · macOS scrollbar overlay mode — pre-existing, not introduced

On macOS, "overlay scrollbars" (the system default since macOS 10.7) are drawn by the OS and appear on hover/scroll only. Qt's custom scrollbar stylesheet is partially overridden by this behavior depending
on the user's system setting ("Always show scroll bars" vs "Automatically"). The RFC does not change any scrollbar styling, so this is a pre-existing platform delta, not something the RFC introduces or
worsens.


4 · macOS title-bar menu bar placement — explicitly guarded, not affected

TitleBar.cpp has explicit guards:

  • Line 97 — #ifndef Q_OS_MAC — centers the menu bar on Linux/Windows only
  • Line 123 — #ifdef Q_OS_MAC — places the heartbeat widget differently on macOS

The RFC's gradient removal targets PanadapterApplet.cpp's title bar (the per-panadapter header, not the main TitleBar). These macOS guards are in a different class and are not touched.


5 · macOS font rendering — slightly different metrics

CoreText (macOS) renders the same logical font size at slightly different apparent weight and interline spacing compared to DirectWrite (Windows) or FreeType (Linux). Tight fixed button heights
(kCtrlBtnHeight = 22) may clip descenders or feel cramped on macOS where rendered cap-height is fractionally larger. This is a minor fit issue, not a functional one. The recommended mitigation is to test the
token values at 22px on macOS before merging Phase 1 and adjust if needed — the token layer makes this a one-line change.


6 · Linux primary — no concerns

Linux is the primary platform per README.md ("Linux is primary. macOS and Windows are courtesy"). The Fusion style is forced via QStyleFactory::create("Fusion") in main.cpp:254 on all platforms, which
suppresses GTK/KDE theme bleed-through. Fontconfig resolves the sans-serif fallback. No Linux-specific concerns for this RFC.


7 · Spectrum rendering — unrelated

SpectrumWidget uses Metal on macOS, D3D11/Vulkan on Windows, Vulkan/OpenGL on Linux (via QRhiWidget). The RFC does not touch spectrum rendering at all.

Cross-Platform Impact Summary Table

Concern Platform RFC-introduced? Action needed
ComboStyle.h color constants not covered by Theme.h All Yes — add to scope Include ComboStyle.h in Phase 5 audit
Font stack — Inter/Segoe UI unavailable as system fonts macOS, Linux Pre-existing Note in RFC; bundle Inter as a follow-on task
HiDPI — all token values are logical pixels, Qt6 scales automatically All No concern No action required
macOS overlay scrollbars partially override Qt scrollbar stylesheet macOS Pre-existing No action required
macOS title-bar menu placement (TitleBar.cpp:97,123) macOS Not affected — guards are in a different class No action required
macOS font metric tightness — CoreText renders slightly larger than DirectWrite macOS Soft risk Validate kCtrlBtnHeight = 22 on macOS before merging Phase 1
Fusion style forced globally (main.cpp:254) suppresses GTK/KDE bleed-through Linux No concern No action required
Spectrum rendering (Metal / D3D11 / Vulkan via QRhiWidget) All Not affected No action required

Alternatives considered

Alternatives Considered

1 · Incremental per-PR cleanup (no token layer)

Fix inconsistencies opportunistically as they are spotted during normal feature work, without introducing a shared vocabulary.

Why rejected: This is essentially the current approach, and it produced the documented fragmentation. Without a single source of truth, different contributors naturally reach for slightly different
values — #205070 vs #203040 for a border, 3px vs 4px for a radius — because there is nothing to consult. The inconsistencies documented in this RFC accumulated through exactly this process.
Incremental cleanup without a token layer addresses symptoms rather than the root cause and regenerates the problem faster than individual PRs can resolve it.


2 · Single expanded Theme.h stylesheet with no separate token header

Extend the global stylesheet in Theme.h to cover every widget state and every subclass, eliminating the need for a separate DesignTokens.h.

Why rejected: Qt Style Sheets have no variable substitution — every color value must be spelled out as a literal string in every rule that uses it. A stylesheet large enough to cover all 63 files in
scope would require repeating the same hex values dozens of times, making a future color change (e.g. adjusting the accent from #00b4d8 to a slightly warmer blue) an error-prone find-and-replace across a
multi-hundred-line string. More critically, the stylesheet layer is invisible to QPainter-based widgets (SMeterWidget, HGauge) which need compile-time QColor constants. The token header bridges both
worlds: it feeds the stylesheet string builder and provides typed constants for paint code. This is the closest alternative to the proposed approach — the only difference is where the values live — and the
token header is strictly more capable.


3 · Qt Designer .ui files

Switch from programmatic layout construction to Qt Designer .ui files, which expose visual properties at design time and enforce consistency through the editor.

Why rejected: CLAUDE.md documents that all layouts are built programmatically in C++ and CONTRIBUTING.md reflects this as a project convention. More fundamentally, Qt Designer .ui files compose
poorly with the application's dynamic layout requirements: drag-to-reorder applets, collapsible floating containers, slice-count-driven button generation, and runtime applet visibility toggling are all
driven by C++ logic that cannot be expressed in a .ui file. Moving to Designer would require restructuring the widget construction model entirely — a scope orders of magnitude beyond this RFC.


4 · QML / Qt Quick rewrite

Replace the Qt Widgets UI layer with QML, which has native support for property binding, design tokens via Qt's Style singleton, and declarative state machines.

Why rejected: The application is deeply integrated with Qt Widgets — QRhiWidget for GPU spectrum rendering, QSplitter for multi-pan layout, QMainWindow for the window shell, and dozens of custom
QPainter subclasses. A QML migration would be a multi-month, high-regression-risk rewrite with no radio functionality benefit. The consistency problem is a stylesheet management problem, not an
architectural one; the widget foundation is sound and does not need to be replaced.


5 · Custom widget subclasses (AetherButton, AetherSlider, etc.)

Create subclasses of QPushButton, QSlider, and QComboBox that enforce the correct appearance in their constructors, making it structurally impossible to instantiate an unstyled control.

Why considered seriously: Type-system enforcement is a genuine advantage — a contributor cannot forget to apply the right style if the constructor does it automatically.

Why rejected: Replacing every QPushButton, QSlider, and QComboBox instantiation across 63 files with a custom subclass carries the same audit burden as the token approach but adds permanent
coupling: every future control added to the codebase must use the custom type, and every developer must know the custom types exist. The token approach achieves the same consistency with lower ongoing
maintenance cost — a new developer uses a standard QPushButton and the global stylesheet applies tokens automatically.


6 · QProxyStyle subclass

Subclass QProxyStyle to intercept Qt's style engine drawing calls and enforce consistent metrics (button height, handle size, border radius) at the rendering level, below the stylesheet layer.

Why rejected: QProxyStyle works best for wholesale visual overrides where all instances of a widget type should look identical. AetherSDR requires semantic variation within the same widget type — a
TX active QPushButton must look visually distinct from an idle one; an ATU-matched indicator differs from a TX power button. Expressing those distinctions through QProxyStyle requires re-implementing the
state logic that stylesheets already handle declaratively via pseudo-states (:checked, [txActive="true"]). The stylesheet approach is a better fit for the required customization range.


7 · External .qss file loaded at runtime

Move Theme.h's embedded stylesheet into an external .qss file loaded at application startup, allowing visual changes without recompilation.

Why considered: Separation of styling from code is a legitimate design goal, and a .qss file can be edited without touching C++.

Why rejected: Qt QSS still has no variable substitution, so the fragmentation problem is not solved — it just moves from C++ string literals to a .qss file. The runtime file dependency introduces error
handling requirements (what happens if the file is missing or corrupted?). The token header solves the variable-substitution problem that external .qss files cannot. If a designer-editable theme file is
desired in the future, the token header provides a cleaner foundation for it than a raw .qss file.


8 · Third-party Qt theming library (QDarkStyle, qt-material, etc.)

Adopt an existing open-source Qt dark theme library instead of maintaining Theme.h.

Why rejected: No existing library matches AetherSDR's semantic color requirements: S-meter blue/red scale, TX danger state, spectrum heatmap palette integration, and the specific tone of the existing
dark surface. Any library would require heavy customization to match, effectively recreating the token problem inside someone else's abstraction. It also introduces an external dependency with its own
release cadence. The existing Theme.h already covers 80% of the required surface — it needs systematization, not replacement.

Implementation scope

Implementation Scope

New files

File Purpose
src/gui/DesignTokens.h Token vocabulary — all constexpr color, metric, and font values; single source of truth for the entire design system

Modified files — Core theme layer

File Change
src/gui/Theme.h Rewrite darkThemeStylesheet() to reference DesignTokens values; normalize button radius to 3px, slider handle to 10px, hover color to surface-overlay
src/gui/ComboStyle.h Replace hardcoded hex values (#1a2a3a, #00b4d8, #203040) with token constants; do not change the temporary-PNG down-arrow generation mechanism — it is a necessary
cross-platform workaround

Modified files — Primary applets (highest delta from global theme)

File Key changes
src/gui/RxApplet.cpp Replace kButtonBase, kSliderStyle, kDimLabelStyle, kInsetValueStyle magic values with token references; border #205070kBorderInteractive, inset background #0a0a18
kSurfaceSunken
src/gui/TxApplet.cpp Consolidate 5 per-button setStyleSheet() calls to a single kActionButtonStyle constant; replace TX-state color swap (setStyleSheet() on every PTT cycle) with Qt dynamic
property pattern — see Known Issues §1
src/gui/PanadapterApplet.cpp Remove gradient from panadapter title bar; replace with flat kSurfacePanel + 1px kBorderSubtle bottom separator; normalize CW panel button sizes (8px/9px
kFontSm)
src/gui/VfoWidget.cpp Apply kFontFreq (22px, QFont::Bold) to primary frequency readout label
src/gui/AppletPanel.cpp Replace scrollbar hardcoded colors with token values; verify hover dynamic property (setProperty("active", ...)) still resolves correctly after token substitution — see Known
Issues §2

Modified files — Container and floating window layer

File Key changes
src/gui/containers/ContainerTitleBar.cpp Normalize header background and button styles to tokens; ensures pop-out panels match docked panel appearance
src/gui/containers/FloatingContainerWindow.cpp Border and background token alignment
src/gui/PanFloatingWindow.cpp Token alignment for floated panadapter windows

Modified files — Device and DSP applets (Phase 5 audit)

Each of these files contains at least one setStyleSheet() call with hardcoded hex values. Most changes will be trivial (one to three value substitutions per file), but all must be visited to complete the
flat design system.

File Notes
src/gui/AmpApplet.cpp
src/gui/AntennaGeniusApplet.cpp
src/gui/CatControlApplet.cpp
src/gui/ClientChainApplet.cpp
src/gui/ClientCompApplet.cpp
src/gui/ClientCompEditor.cpp
src/gui/ClientCompThresholdFader.cpp
src/gui/ClientDeEssApplet.cpp
src/gui/ClientDeEssEditor.cpp
src/gui/ClientEqApplet.cpp
src/gui/ClientEqEditor.cpp
src/gui/ClientEqOutputFader.cpp
src/gui/ClientEqParamRow.cpp
src/gui/ClientGateApplet.cpp
src/gui/ClientGateEditor.cpp
src/gui/ClientPuduApplet.cpp
src/gui/ClientPuduEditor.cpp
src/gui/ClientReverbApplet.cpp
src/gui/ClientReverbEditor.cpp
src/gui/ClientTubeApplet.cpp
src/gui/ClientTubeEditor.cpp
src/gui/DaxApplet.cpp
src/gui/DaxIqApplet.cpp
src/gui/EqApplet.cpp
src/gui/MeterApplet.cpp
src/gui/MqttApplet.cpp
src/gui/PhoneApplet.cpp
src/gui/PhoneCwApplet.cpp
src/gui/ShackSwitchApplet.cpp
src/gui/TciApplet.cpp
src/gui/TunerApplet.cpp
src/gui/WaveApplet.cpp

Modified files — Panels and dialogs (Phase 5 audit)

File Notes
src/gui/BandStackPanel.cpp Scrollbar color tokens
src/gui/ConnectionPanel.cpp
src/gui/CwxPanel.cpp
src/gui/DvkPanel.cpp
src/gui/MemoryBrowsePanel.cpp
src/gui/AetherDspDialog.cpp
src/gui/ClientDisconnectDialog.cpp Scrollbar color tokens
src/gui/DspParamPopup.cpp
src/gui/DxClusterDialog.cpp
src/gui/EditorFramelessTitleBar.cpp
src/gui/HelpDialog.cpp
src/gui/MemoryDialog.cpp
src/gui/MidiMappingDialog.cpp
src/gui/MultiFlexDialog.cpp
src/gui/NetworkDiagnosticsDialog.cpp
src/gui/PanLayoutDialog.cpp
src/gui/ProfileManagerDialog.cpp
src/gui/PropDashboardDialog.cpp
src/gui/RadioSetupDialog.cpp
src/gui/ShortcutDialog.cpp
src/gui/SliceTroubleshootingDialog.cpp
src/gui/SpotSettingsDialog.cpp
src/gui/SupportDialog.cpp
src/gui/WhatsNewDialog.cpp

Modified files — Main UI shell

File Notes
src/gui/TitleBar.cpp Label and button color token alignment; macOS layout guards (lines 97, 123) are not modified
src/gui/MainWindow.cpp Any inline setStyleSheet() calls on status-bar labels or indicator widgets; all platform guards for audio and CPU metrics are not modified
src/gui/SpectrumOverlayMenu.cpp Overlay menu color token alignment
src/gui/SpectrumWidget.cpp Overlay label and axis label styles only; GPU rendering pipeline is not modified

Files explicitly NOT in scope

File / Subsystem Reason
src/core/ — all files Protocol, audio engine, DSP — no styling
src/models/ — all files Data models — no styling
src/gui/SpectrumWidget.cpp rendering pipeline GPU/QPainter FFT and waterfall rendering — separate concern
All AppSettings keys and defaults No settings changes in this RFC
All CMakeLists.txt platform guards Build system — not affected
All audio bridge files (VirtualAudioBridge, PipeWireAudioBridge) Platform audio — not affected

Revised effort estimate

The original RFC estimated ~20 files. The actual count of files containing setStyleSheet() calls is 63, distributed across applets, editors, panels, dialogs, and containers. The majority of changes in
the Phase 5 audit will be mechanical substitutions of 1–5 hex values per file. Revised estimates:

Phase Files Estimated lines changed
Phase 1 — DesignTokens.h + Theme.h 2 (1 new) ~130
Phase 2 — Gradient removal (PanadapterApplet) 1 ~5
Phase 3 — RxApplet, TxApplet 2 ~60
Phase 4 — VfoWidget, CW panel, containers 5 ~30
Phase 5 — Full src/gui/ audit ~55 ~250–350
Total ~65 ~475–575

Known Issues

§1 — TxApplet: full stylesheet rebuild on every PTT cycle

TxApplet.cpp calls setStyleSheet() with a full inline string on every transition in setTuning() and setTransmitting(). Qt re-parses the entire stylesheet string and triggers a full repaint of the
widget tree on each call. During rapid PTT toggling this can produce visible flicker. The recommended fix replaces the inline string swap with Qt's dynamic property pattern:

// Set a property on state change — no string rebuild
m_moxBtn->setProperty("txActive", transmitting);
m_moxBtn->style()->unpolish(m_moxBtn);
m_moxBtn->style()->polish(m_moxBtn);

// Handled once in the stylesheet via property selector:
// QPushButton[txActive="true"] { background: #cc3030; border-color: #ff4444; color: #fff; }

This is a correctness improvement independent of the token system; it is included in Phase 3.

§2 — AppletPanel: scrollbar hover uses dynamic property

AppletPanel.cpp drives scrollbar handle brightness via setProperty("active", ...) and a corresponding stylesheet property selector. After token substitution, the stylesheet property selector must be
re-verified to confirm the new token-based border colors resolve correctly under the active state. No behavioral change is expected, but it requires explicit testing.

§3 — SMeterWidget and HGauge: QPainter colors are outside the stylesheet system

Both widgets render entirely via QPainter with hardcoded QColor hex literals — they are invisible to the stylesheet layer. SMeterWidget.cpp uses QColor(0x00, 0x80, 0xd0) (blue arc), QColor(0xff, 0x44, 0x44)
(red danger zone), QColor(0xc8, 0xd8, 0xe8) (text), QColor(0x80, 0x90, 0xa0) (dim labels), and QColor(0xff, 0xaa, 0x00) (peak needle). HGauge.h uses analogous hardcoded values for the power and SWR bar
fills. These must be migrated to compile-time QColor constants derived from DesignTokens.h in a separate pass — they cannot be handled by the stylesheet audit alone. This is flagged as a follow-on task.

§4 — ComboStyle.h temporary PNG generation must be preserved

ComboStyle.h writes a small down-arrow PNG to the system temp directory to work around Qt's inconsistent cross-platform rendering of ::down-arrow pseudo-elements. This mechanism must not be removed. The RFC
scope for this file is limited to updating the color constants (#1a2a3a → kSurfacePanel, #00b4d8 → kColorAccent, etc.); the PNG path and generation logic are untouched.

Metadata

Metadata

Assignees

No one assigned

    Labels

    GUIUser interfaceenhancementImprovement to existing featuremaintainer-reviewRequires maintainer review before any action is takenpriority: lowLow priorityrfc

    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