Skip to content

[feature] UI Enhancements: Customizable Slice Colors#2155

Merged
ten9876 merged 7 commits into
aethersdr:mainfrom
chibondking:feature/slice-color-customization
Apr 30, 2026
Merged

[feature] UI Enhancements: Customizable Slice Colors#2155
ten9876 merged 7 commits into
aethersdr:mainfrom
chibondking:feature/slice-color-customization

Conversation

@chibondking

Copy link
Copy Markdown
Collaborator

Summary

Adds a new UI Enhancements settings tab that lets users choose between the
built-in Aether default slice colors or a fully custom palette. Changes take
effect immediately across the entire UI — no restart required — and are
persisted across sessions.

Motivation

Slice A–H colors were previously hardcoded in SliceColors.h and scattered
throughout the rendering and widget code with no way for users to change them.
Users with color vision deficiencies, specific monitor profiles, or personal
preference had no recourse. This feature brings the slice color palette under
user control while keeping the existing Aether defaults as the out-of-the-box
experience.


What Changed

New: SliceColorManager (singleton, src/gui/SliceColorManager.h/.cpp)

The central piece of this feature. A QObject singleton that sits between the
static kSliceColors[] table and every widget that renders slice colors.

  • activeColor(int sliceId) / dimColor(int sliceId) — returns the effective
    color for a given slice, either from the Aether defaults or the user's custom
    palette.
  • hexActive(int sliceId) — returns a CSS hex string (e.g. "#00d4ff") for
    use in setStyleSheet() calls.
  • useCustomColors() / setUseCustomColors(bool) — toggles the mode.
  • customColor(int) / setCustomColor(int, QColor) — gets/sets an individual
    slot's custom color.
  • resetToDefault(int) — restores a single slot to the Aether default.
  • load() / save() — reads and writes to AppSettings. Keys:
    • SliceColorsUseCustom"True" / "False"
    • SliceColor0 through SliceColor7 — hex color strings
  • Q_SIGNAL void colorsChanged() — emitted on any effective color change.
    All consumer widgets connect to this signal to repaint/re-stylesheet
    themselves in real time.

Dim colors for custom slots are derived automatically at ~40 % of the active
color's RGB values (same relative attenuation as the built-in defaults), so
the inactive-slice shading remains readable without requiring a separate picker
for every state.


New Settings Tab: "UI Enhancements" (in Radio Setup dialog)

Added as a deferred-load tab following the same lazy-construction pattern used
by all other tabs since #1776.

Controls:

Control Behavior
Use Aether defaults radio button Switches to the built-in palette; color buttons are disabled
Custom colors radio button Enables the custom palette; color buttons become interactive
A–H color buttons Each button shows its current color as a background. Clicking opens a native QColorDialog. The change is applied and saved immediately — no "Apply" step needed.
Reset All to Defaults button Resets all eight custom color slots to the Aether defaults without switching the mode radio button

The tab also connects to SliceColorManager::colorsChanged() so that if a
color is ever programmatically changed elsewhere, the swatches stay in sync.


Consumer Widgets Updated

All direct accesses to kSliceColors[i] in rendering/widget code have been
replaced with SliceColorManager::instance() calls. Each widget also connects
colorsChanged() to refresh itself:

File What changed
SpectrumWidget.cpp sliceColor() helper delegates to manager; constructor connects colorsChangedmarkOverlayDirty() to repaint the filter-band overlay
VfoWidget.cpp Slice letter badge in both expanded and collapsed (painter) modes uses manager; constructor connects colorsChangedsyncFromSlice() + update()
RxApplet.cpp Slice selector toolbar buttons and the header badge use manager
CatControlApplet.cpp Slice letter badges and TCP-client status labels use manager

main.cpp

SliceColorManager::instance().load() is called immediately after
AppSettings::instance().load() at startup, so custom colors are active
before any widget is constructed.


CMakeLists.txt

src/gui/SliceColorManager.cpp added to the source list.


Settings Persistence

~/.config/AetherSDR/AetherSDR.settings
  SliceColorsUseCustom = True
  SliceColor0 = #00d4ff
  SliceColor1 = #ff40ff
  ...
  SliceColor7 = #b080ff

When SliceColorsUseCustom is False (the default for new installs),
the custom color keys are still written on first save so the slots are
pre-populated with the Aether defaults and ready to edit.


Testing Notes

  • Verified clean build (0 errors, 0 new warnings) with Ninja/GCC on Arch Linux.
  • Open Radio Setup → UI Enhancements tab:
    • Defaults mode: color buttons disabled, spectrum/VFO badges show Aether colors.
    • Switch to Custom: buttons enable, pick a new color for slice A → spectrum
      filter band, VFO badge, and RX applet badge all update immediately without
      reopening the dialog.
    • Restart app → custom colors restore from AetherSDR.settings.
    • Reset All → reverts swatches and live UI to defaults without changing mode.
  • No changes to radio protocol, slice model, or audio path.

chibondking and others added 4 commits April 29, 2026 01:36
Introduces a new SliceColorManager singleton (src/gui/SliceColorManager.h/.cpp)
that sits between the static kSliceColors[] defaults and every widget that
renders slice markers, filter bands, or letter badges.  All consumers now call
SliceColorManager::instance().activeColor() / dimColor() / hexActive() instead
of indexing kSliceColors[] directly, so a single colorsChanged() signal
propagates color changes to the entire UI in real time.

Settings are persisted in AppSettings:
  - SliceColorsUseCustom  (True / False)
  - SliceColor0 … SliceColor7  (hex color strings, e.g. "#00d4ff")

Dim colors for custom slots are derived automatically at ~40 % luminance of
the active color so the inactive-slice shading stays readable without requiring
a separate picker.

A new "UI Enhancements" tab is added to the Radio Setup dialog (deferred-load,
same pattern as all other tabs) containing:
  - "Use Aether defaults" / "Custom colors" radio buttons
  - Eight color-swatch buttons (A–H) that open QColorDialog on click; the
    button background reflects the current color so the palette is visible at
    a glance
  - "Reset All to Defaults" button
  - colorsChanged() feedback loop keeps the swatches in sync if another widget
    were ever to trigger a color change

Widgets wired for real-time refresh:
  - SpectrumWidget  — markOverlayDirty() on colorsChanged()
  - VfoWidget       — syncFromSlice() + update() on colorsChanged()
  - RxApplet        — slice selector buttons and header badge
  - CatControlApplet — slice letter badges and TCP-client status labels

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…alog

MSVC (C3495) rejects capturing static-storage-duration variables in lambdas;
GCC/Clang allow it. Drop kBtnBase from the applyBtnColor capture list — it
remains accessible in the lambda body as a static local in the enclosing scope.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…alog

MSVC (C3495) rejects capturing static-storage-duration variables in lambdas;
GCC/Clang allow it. Drop kBtnBase from the applyBtnColor capture list — it
remains accessible in the lambda body as a static local in the enclosing scope.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

@aethersdr-agent aethersdr-agent Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice contribution, @chibondking — well-structured PR with a clean separation of concerns. The SliceColorManager singleton pattern is a solid approach for centralizing color lookups, and the live-update via colorsChanged() signal is the right Qt idiom. PR description is excellent.

A few observations:

Negative sliceId guard removed

In RxApplet::connectSlice(), the old code guarded against out-of-range sid:

const char* badgeColor = (sid >= 0 && sid < kSliceColorCount)
    ? kSliceColors[sid].hexActive : "#0070c0";

The new code passes sid straight through to SliceColorManager::hexActive(sid), which does sliceId % kSliceColorCount. While SliceModel::m_id is initialized to 0 and set from the radio protocol (so negative values shouldn't occur in practice), the modulo of a negative int in C++ is implementation-defined-negative — meaning m_hexCache[-1] would be UB. I'd suggest either:

  • Adding a clamp/abs in the manager methods: int idx = std::abs(sliceId) % kSliceColorCount;, or
  • Adding Q_ASSERT(sliceId >= 0) in the manager to catch misuse during development

This applies to activeColor(), dimColor(), hexActive(), and customColor().

SpectrumWidget::sliceColor() helper

The PR description mentions sliceColor() delegates to the manager, but the existing helper at SpectrumWidget.cpp:1148 reads kSliceColors[] directly for both active and dim colors. I can see the colorsChangedmarkOverlayDirty() connection was added, but can you confirm the sliceColor() body was also updated? (The diff I reviewed was truncated — just want to make sure the dim-color path through sliceColor() also goes through the manager, otherwise the overlay repaint triggered by colorsChanged would still render stale colors.)

VfoWidget.cpp direct accesses

Similarly — VfoWidget.cpp currently has kSliceColors[sliceId].hexActive at lines ~2115 and ~2787 on main. The PR description says these were updated. Same truncation caveat — if these are covered in the diff, disregard.

mutable on m_hexCache

m_hexCache is declared mutable but hexActive() only reads from it — the rebuild happens in non-const rebuildHexCache(). The mutable qualifier isn't needed and slightly obscures the intent. Very minor.

Singleton lifetime

s_instance = new SliceColorManager is never deleted. This is standard for app-lifetime singletons in Qt (the process teardown reclaims it), so no action needed — just noting for completeness.

Overall

Clean, well-scoped feature. The tab follows the existing deferred-load pattern, AppSettings is used correctly (not QSettings), and the signal/slot wiring is correct. The save()AppSettings::save() call on each change is fine for a settings dialog. No impact on radio protocol or audio path. Looks good pending the sliceId safety note above.

Thanks for the thorough testing notes and the excellent PR description — really helpful for review.

…ious mutable

  All six public methods (activeColor, dimColor, hexActive, customColor,
  setCustomColor, resetToDefault) previously used sliceId % kSliceColorCount
  directly. In C++, the result of % on a negative left operand is
  implementation-defined-negative, making m_customColors[idx] or m_hexCache[idx]
  undefined behaviour for any negative sliceId. While SliceModel::m_id is
  initialized to 0 and driven from the radio protocol, the public API gave no
  protection against misuse.

  Fix: route all public methods through a private safeIdx() helper that
  Q_ASSERTs sliceId >= 0 in debug builds (catching misuse early) and falls
  back to std::abs(sliceId) % kSliceColorCount in release builds.

  Also remove the mutable qualifier from m_hexCache. It was never needed —
  m_hexCache is only written in non-const rebuildHexCache(), so mutable
  served no purpose and obscured intent.

  The private defaultActive()/defaultDim() helpers are unaffected; they
  receive loop-controlled indices (0–7), not user-supplied slice IDs.
@jensenpat

jensenpat commented Apr 29, 2026

Copy link
Copy Markdown
Collaborator

CJ, Code works good. Can you rename "UI Enhancements" tab to "Themes"? This PR can form the foundation of our theme customization and we'll have plenty of room in this dialog for future color choosers.

…as the foundation for broader theme customization rather than a narrowly scoped feature, leaving room for future color choosers and visual preference controls.
@chibondking

Copy link
Copy Markdown
Collaborator Author

@jensenpat Done.. great idea 👍

@chibondking

Copy link
Copy Markdown
Collaborator Author

@AetherClaude changes implemented.

jensenpat
jensenpat previously approved these changes Apr 29, 2026

@jensenpat jensenpat left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New SliceColorManager framework to support slice indicator colors. Localized UX in Setup tab for future themes work. Tested successfully on macOS. Some future efforts will need to be done to persist slice colors for each applet control. Good work on this CJ, I hope you continue to develop around this area as customization is a big part of what makes AetherSDR special.

@aethersdr-agent

Copy link
Copy Markdown
Contributor

Nice work @chibondking — the SliceColorManager singleton is clean and the signal-driven live update pattern is solid. Looked through the full diff and have a few observations:

Missing colorsChanged() connections in two consumer widgets

SpectrumWidget and VfoWidget both connect to colorsChanged() so they repaint live when the user picks a new color — great. But CatControlApplet and RxApplet use the manager for colors without connecting to the signal. That means:

  • CatControlApplet: The slice letter badges and TCP status labels (buildUI() / updateChannelStatus()) will show stale colors until the applet is rebuilt.
  • RxApplet: The slice selector toolbar buttons (setMaxSlices()) and header badge (connectSlice()) won't update live either.

To get full live-update coverage you'd want to connect colorsChanged() in both of those widgets and re-apply the stylesheets, same pattern as VfoWidget. Something like:

// In CatControlApplet constructor or buildUI():
connect(&SliceColorManager::instance(), &SliceColorManager::colorsChanged,
        this, [this]() {
    for (int i = 0; i < kMaxChannels; ++i) {
        // re-apply badge + TCP status stylesheets
    }
});

And similarly for RxApplet.

Minor: method name vs tab label

The tab is now labeled "Themes" (good call @jensenpat) but the builder method is still buildUiEnhancementsTab() and the section comment still reads // ── UI Enhancements tab. A quick rename to buildThemesTab() would keep things consistent — not blocking, just housekeeping.

Otherwise this is looking good. The dim-color derivation at 40% RGB, hex cache, and the settings persistence pattern all look correct.

@jensenpat jensenpat enabled auto-merge (squash) April 29, 2026 17:42
Two minor cleanups from PR review:

1. Drop the now-dead `#include "SliceColors.h"` from VfoWidget.cpp,
   RxApplet.cpp, and CatControlApplet.cpp. After the SliceColorManager
   conversion these files no longer reference kSliceColors[],
   kSliceColorCount, or SliceColorEntry directly. SpectrumWidget.cpp
   keeps its include because it still uses kSliceColorCount for
   slice→letter mapping (A through H).

2. Drop the contradictory `Q_ASSERT(sliceId >= 0)` from safeIdx().
   The function name promises safety; the assert + std::abs
   combination was self-contradictory (assert fires in debug,
   abs handles it in release). Keeping just std::abs makes the
   defense unconditional and matches what the function is named
   for. Negative slice IDs occur transiently during slice teardown
   (-1 sentinel), so the safety isn't theoretical.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ten9876 ten9876 merged commit b60550e into aethersdr:main Apr 30, 2026
5 checks passed
ten9876 added a commit that referenced this pull request Apr 30, 2026
…os (#2184)

6000-series radios (FLEX-6300/6400/6500/6600/6700) don't expose the
extended firmware DSP filters that 8000-class hardware does (NRL, NRS,
RNN, NRF).  Showing them in the UI when connected to a 6000-series is
confusing and clutters the DSP panel.

Adds RadioModel::hasExtendedDspFilters() — returns true for BigBend
(8400, 8600), DragonFire (AU-, ML-, CL-, RT-) platforms.  Both the
VfoWidget DSP buttons and the SpectrumOverlayMenu DSP rows hide
those four filters when this returns false.

Carved out of PR #2178 (bot, no maintainerCanModify) which had a
diff that included unrelated -390 lines reverting PR #2155
(Customizable Slice Colors) — its branch was based on the moment
#2155 landed but it didn't include the SliceColorManager source
files, so a rebase showed them as deletions.  This carve-out is just
the substantive +47/-4 hide-filter change.

Improvement over the original PR: the bot only gated the 4 buttons
in setSlice()'s mode-change lambda.  This commit also gates them in
the syncFromSlice() path (line 2915 region) so subsequent mode
changes re-apply the gating consistently.

Fixes #2177.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants