Preflight
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.
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
8px → 10px (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
-
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)?
-
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?
-
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?
-
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–119 — kButtonBase, 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)
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 #205070 → kBorderInteractive, 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.
Preflight
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.hplus dozens of per-componentsetStyleSheet()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
Theme.h(globalQPushButton)4pxRxApplet.cppkButtonBase3pxTxApplet.cppaction buttons (TUNE / MOX / ATU / MEM)3pxPanadapterApplet.cppCW panel buttons2pxPanadapterApplet.cpplock-pitch / lock-speed buttons2px2 · Button font-size — five values in active use
Theme.hbase inheritance13pxTxApplet.cpp,RxApplet.cppaction buttons10px boldPanadapterApplet.cppCW copy/clear buttons9px boldPanadapterApplet.cpppop-out / max buttons11pxPanadapterApplet.cppCW pitch range labels8px3 · Border color — three different values for "normal" borders
Theme.h(global)#203040RxApplet.cppkButtonBase,TxApplet.cppaction buttons#205070RxApplet.cppkInsetValueStyle,TxApplet.cppATU inset#1e2e3e4 · Slider handle size — three sizes
Theme.h(global)14px, margin-5px 0RxApplet.cpp,TxApplet.cpp10px, margin-3px 0PanadapterApplet.cppCW pitch range8px, margin-3px 05 · Dim / secondary label color — four different values
RxApplet.cppkDimLabelStyle#8090a0TxApplet.cppRF/Tune power labels#8aa8c0TxApplet.cppgauge scale labels#405060PanadapterApplet.cppCW hints, stats#6a80906 · Hover color — two values for button hover
Theme.h(globalQPushButton:hover)#203040RxApplet.cpp,TxApplet.cppaction buttons#2040607 · Panel header — gradient vs flat
PanadapterApplet.cppapplies a vertical gradient to every panadapter title bar: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.hstatus bar, menu bar,PanadapterApplet.cppCW text area) vs#0a0a18(RxApplet.cppvalue insets,TxApplet.cppATU inset).Both values serve the same semantic role and are visually indistinguishable, but create unnecessary maintenance surface.
Proposal
Proposed Solution
Introduce a design token layer in
src/gui/that centralizes every visual constant. All existingsetStyleSheet()overrides that duplicate or contradict the global theme are removed and replaced with references to the token layer. The global stylesheet inTheme.his 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 ofconstexprstring values and a generated stylesheet fragment.1.1 · Surface / Background Scale
The current three-way split (
#0f0f1a/#111120/#0a0a14/#0a0a18) collapses to three clearly-named, clearly-differentiated tiers.1.2 · Border Scale
Replaces the current
#203040/#205070/#1e2e3esplit.1.3 · Text Scale
1.4 · Semantic / State Colors
CW decode quality colors (
#00ff88,#e0e040,#ff9020,#ff4040) are already internally consistent and fall undersuccess/warning/color-dangersemantics — no change needed there.1.5 · Control Metrics
Part 2 — Updated Global Stylesheet (
Theme.h)Theme.his rewritten to reference the token values above. The key changes from the current version:QPushButtonborder1px solid #2030401px solid {border-control}QPushButtonborder-radius4px3pxQPushButton:hoverbackground#203040{surface-overlay}QPushButton:pressedbackground#00b4d8{color-accent}QComboBoxborder-radius4px3pxQSliderhandle width14px / -5px margin10px / -3px marginQGroupBoxtitle color#00b4d8{text-accent}QStatusBarbackground#0a0a14{surface-sunken}QMenuBarbackground#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 removalThe gradient is replaced with a flat
{surface-panel}background and a1px solid {border-subtle}bottom separator. This brings the panadapter header in line with every other panel in the application.RxApplet.cppkButtonBase: border color#205070→{border-interactive}, radius3px(already correct)kSliderStyle: groove#203040→{border-control}, handle10px(already correct)kDimLabelStyle: color#8090a0→{text-secondary}, font-size11px(already correct)kInsetValueStyle: background#0a0a18→{surface-sunken}, border#1e2e3e→{border-subtle}TxApplet.cppkActionButtonStyleconstant using tokens; remove the 5 per-buttonsetStyleSheet()callssetStyleSheet()insetTuning()/setTransmitting(): state change becomes asetProperty("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#8aa8c0/#405060→{text-secondary}/{text-tertiary}PanadapterApplet.cpp— CW panel8px→10px(token)#0a0a14→{surface-sunken}(already the same value, just explicit)8px/9px/11px→{ctrl-font-sm}(11px) for allVfoWidget.cpp— frequency readoutApply
{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:
Applied consistently to:
PanadapterApplettitle bar#3a4a5a → #1a2a38surface-panelAppletPanelapplet headerssurface-panelsurface-sunkensurface-sunken(no change)ConnectionPanelsurface-basePart 5 — Proposed
DesignTokens.hStructureTheme.h::darkThemeStylesheet()is updated to build itsQStringLiteralusing these values via a helper that substitutes tokens. All per-componentsetStyleSheet()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.halignment (no per-component changes)DesignTokens.hTheme.hto use tokensDesignTokens.h) +Theme.hrewrite (~75 lines)Phase 2 — Gradient removal and panel header normalization
PanadapterApplet.cpptitle bar: gradient → flatPhase 3 — RxApplet and TxApplet cleanup
setStyleSheet()calls inTxAppletTX-state transitions to use Qt dynamic property patternPhase 4 — PanadapterApplet CW panel + VfoWidget frequency readout
kFontFreqPhase 5 — Remaining applet audit
src/gui/forsetStyleSheetcalls; any remaining hardcoded hex values are replaced with token referencesTotal 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:
QVBoxLayout/QHBoxLayoutrestructuringRisk Assessment
setStyleSheet()removal causes applet to fall back to unexpected inherited valuestyle()->unpolish(widget); style()->polish(widget);is the standard Qt pattern and is well-testedNo changes to the radio control path, no changes to VITA-49 handling, no new threads.
Open Questions for Maintainer
Surface tier naming:
surface-panel(#141422) is slightly lighter thansurface-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)?CW decode quality colors: The current
#00ff88/#e0e040/#ff9020/#ff4040gradient 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 withkColorSuccess/kColorWarning/kColorDanger. Maintainer preference?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?
DesignTokens.hplacement: Proposed location issrc/gui/DesignTokens.h. If there is a preference to keep it closer toTheme.has a combined header, both can be merged into an expandedTheme.hwithout affecting the token vocabulary.References
aethersdrcurrentui.png(attached)aetherproposedui.png(attached)src/gui/Theme.h— global stylesheetsrc/gui/RxApplet.cpp:104–119—kButtonBase,kSliderStyle,kDimLabelStyle,kInsetValueStylesrc/gui/TxApplet.cpp:43–46, 164–170— local slider + action button stylessrc/gui/PanadapterApplet.cpp:31–34— gradient title barsrc/gui/VfoWidget.h— frequency display labelCLAUDE.md— autonomous agent design-change boundary (this RFC is targets the maintainer for review, not autonomous AI action)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:
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
ComboStyle.hcolor constants not covered byTheme.hComboStyle.hin Phase 5 auditInter/Segoe UIunavailable as system fontsTitleBar.cpp:97,123)kCtrlBtnHeight = 22on macOS before merging Phase 1main.cpp:254) suppresses GTK/KDE bleed-throughQRhiWidget)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 —
#205070vs#203040for a border,3pxvs4pxfor 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.hstylesheet with no separate token headerExtend the global stylesheet in
Theme.hto cover every widget state and every subclass, eliminating the need for a separateDesignTokens.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
#00b4d8to a slightly warmer blue) an error-prone find-and-replace across amulti-hundred-line string. More critically, the stylesheet layer is invisible to
QPainter-based widgets (SMeterWidget,HGauge) which need compile-timeQColorconstants. The token header bridges bothworlds: 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
.uifilesSwitch from programmatic layout construction to Qt Designer
.uifiles, which expose visual properties at design time and enforce consistency through the editor.Why rejected:
CLAUDE.mddocuments that all layouts are built programmatically in C++ andCONTRIBUTING.mdreflects this as a project convention. More fundamentally, Qt Designer.uifiles composepoorly 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
.uifile. 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
Stylesingleton, and declarative state machines.Why rejected: The application is deeply integrated with Qt Widgets —
QRhiWidgetfor GPU spectrum rendering,QSplitterfor multi-pan layout,QMainWindowfor the window shell, and dozens of customQPaintersubclasses. 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 anarchitectural 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, andQComboBoxthat 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, andQComboBoxinstantiation across 63 files with a custom subclass carries the same audit burden as the token approach but adds permanentcoupling: 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
QPushButtonand the global stylesheet applies tokens automatically.6 ·
QProxyStylesubclassSubclass
QProxyStyleto 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:
QProxyStyleworks best for wholesale visual overrides where all instances of a widget type should look identical. AetherSDR requires semantic variation within the same widget type — aTX active
QPushButtonmust look visually distinct from an idle one; an ATU-matched indicator differs from a TX power button. Expressing those distinctions throughQProxyStylerequires re-implementing thestate 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
.qssfile loaded at runtimeMove
Theme.h's embedded stylesheet into an external.qssfile loaded at application startup, allowing visual changes without recompilation.Why considered: Separation of styling from code is a legitimate design goal, and a
.qssfile 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
.qssfile. The runtime file dependency introduces errorhandling requirements (what happens if the file is missing or corrupted?). The token header solves the variable-substitution problem that external
.qssfiles cannot. If a designer-editable theme file isdesired in the future, the token header provides a cleaner foundation for it than a raw
.qssfile.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.halready covers 80% of the required surface — it needs systematization, not replacement.Implementation scope
Implementation Scope
New files
src/gui/DesignTokens.hconstexprcolor, metric, and font values; single source of truth for the entire design systemModified files — Core theme layer
src/gui/Theme.hdarkThemeStylesheet()to referenceDesignTokensvalues; normalize button radius to3px, slider handle to10px, hover color tosurface-overlaysrc/gui/ComboStyle.h#1a2a3a,#00b4d8,#203040) with token constants; do not change the temporary-PNG down-arrow generation mechanism — it is a necessaryModified files — Primary applets (highest delta from global theme)
src/gui/RxApplet.cppkButtonBase,kSliderStyle,kDimLabelStyle,kInsetValueStylemagic values with token references; border#205070→kBorderInteractive, inset background#0a0a18kSurfaceSunkensrc/gui/TxApplet.cppsetStyleSheet()calls to a singlekActionButtonStyleconstant; replace TX-state color swap (setStyleSheet()on every PTT cycle) with Qt dynamicsrc/gui/PanadapterApplet.cppkSurfacePanel+1px kBorderSubtlebottom separator; normalize CW panel button sizes (8px/9px→kFontSm)src/gui/VfoWidget.cppkFontFreq(22px,QFont::Bold) to primary frequency readout labelsrc/gui/AppletPanel.cppsetProperty("active", ...)) still resolves correctly after token substitution — see KnownModified files — Container and floating window layer
src/gui/containers/ContainerTitleBar.cppsrc/gui/containers/FloatingContainerWindow.cppsrc/gui/PanFloatingWindow.cppModified 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 theflat design system.
src/gui/AmpApplet.cppsrc/gui/AntennaGeniusApplet.cppsrc/gui/CatControlApplet.cppsrc/gui/ClientChainApplet.cppsrc/gui/ClientCompApplet.cppsrc/gui/ClientCompEditor.cppsrc/gui/ClientCompThresholdFader.cppsrc/gui/ClientDeEssApplet.cppsrc/gui/ClientDeEssEditor.cppsrc/gui/ClientEqApplet.cppsrc/gui/ClientEqEditor.cppsrc/gui/ClientEqOutputFader.cppsrc/gui/ClientEqParamRow.cppsrc/gui/ClientGateApplet.cppsrc/gui/ClientGateEditor.cppsrc/gui/ClientPuduApplet.cppsrc/gui/ClientPuduEditor.cppsrc/gui/ClientReverbApplet.cppsrc/gui/ClientReverbEditor.cppsrc/gui/ClientTubeApplet.cppsrc/gui/ClientTubeEditor.cppsrc/gui/DaxApplet.cppsrc/gui/DaxIqApplet.cppsrc/gui/EqApplet.cppsrc/gui/MeterApplet.cppsrc/gui/MqttApplet.cppsrc/gui/PhoneApplet.cppsrc/gui/PhoneCwApplet.cppsrc/gui/ShackSwitchApplet.cppsrc/gui/TciApplet.cppsrc/gui/TunerApplet.cppsrc/gui/WaveApplet.cppModified files — Panels and dialogs (Phase 5 audit)
src/gui/BandStackPanel.cppsrc/gui/ConnectionPanel.cppsrc/gui/CwxPanel.cppsrc/gui/DvkPanel.cppsrc/gui/MemoryBrowsePanel.cppsrc/gui/AetherDspDialog.cppsrc/gui/ClientDisconnectDialog.cppsrc/gui/DspParamPopup.cppsrc/gui/DxClusterDialog.cppsrc/gui/EditorFramelessTitleBar.cppsrc/gui/HelpDialog.cppsrc/gui/MemoryDialog.cppsrc/gui/MidiMappingDialog.cppsrc/gui/MultiFlexDialog.cppsrc/gui/NetworkDiagnosticsDialog.cppsrc/gui/PanLayoutDialog.cppsrc/gui/ProfileManagerDialog.cppsrc/gui/PropDashboardDialog.cppsrc/gui/RadioSetupDialog.cppsrc/gui/ShortcutDialog.cppsrc/gui/SliceTroubleshootingDialog.cppsrc/gui/SpotSettingsDialog.cppsrc/gui/SupportDialog.cppsrc/gui/WhatsNewDialog.cppModified files — Main UI shell
src/gui/TitleBar.cpplines 97, 123) are not modifiedsrc/gui/MainWindow.cppsetStyleSheet()calls on status-bar labels or indicator widgets; all platform guards for audio and CPU metrics are not modifiedsrc/gui/SpectrumOverlayMenu.cppsrc/gui/SpectrumWidget.cppFiles explicitly NOT in scope
src/core/— all filessrc/models/— all filessrc/gui/SpectrumWidget.cpprendering pipelineAppSettingskeys and defaultsCMakeLists.txtplatform guardsVirtualAudioBridge,PipeWireAudioBridge)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 inthe Phase 5 audit will be mechanical substitutions of 1–5 hex values per file. Revised estimates:
DesignTokens.h+Theme.hPanadapterApplet)RxApplet,TxAppletVfoWidget, CW panel, containerssrc/gui/auditKnown Issues
§1 —
TxApplet: full stylesheet rebuild on every PTT cycleTxApplet.cppcallssetStyleSheet()with a full inline string on every transition insetTuning()andsetTransmitting(). Qt re-parses the entire stylesheet string and triggers a full repaint of thewidget 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: