[feature] UI Enhancements: Customizable Slice Colors#2155
Conversation
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>
…m/chibondking/AetherSDR into feature/slice-color-customization
There was a problem hiding this comment.
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 colorsChanged → markOverlayDirty() 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.
|
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.
|
@jensenpat Done.. great idea 👍 |
|
@AetherClaude changes implemented. |
There was a problem hiding this comment.
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.
|
Nice work @chibondking — the Missing
To get full live-update coverage you'd want to connect // 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 Minor: method name vs tab label The tab is now labeled Otherwise this is looking good. The dim-color derivation at 40% RGB, hex cache, and the settings persistence pattern all look correct. |
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>
…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>
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.hand scatteredthroughout 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
QObjectsingleton that sits between thestatic
kSliceColors[]table and every widget that renders slice colors.activeColor(int sliceId)/dimColor(int sliceId)— returns the effectivecolor 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") foruse in
setStyleSheet()calls.useCustomColors()/setUseCustomColors(bool)— toggles the mode.customColor(int)/setCustomColor(int, QColor)— gets/sets an individualslot's custom color.
resetToDefault(int)— restores a single slot to the Aether default.load()/save()— reads and writes toAppSettings. Keys:SliceColorsUseCustom—"True"/"False"SliceColor0throughSliceColor7— hex color stringsQ_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:
QColorDialog. The change is applied and saved immediately — no "Apply" step needed.The tab also connects to
SliceColorManager::colorsChanged()so that if acolor is ever programmatically changed elsewhere, the swatches stay in sync.
Consumer Widgets Updated
All direct accesses to
kSliceColors[i]in rendering/widget code have beenreplaced with
SliceColorManager::instance()calls. Each widget also connectscolorsChanged()to refresh itself:SpectrumWidget.cppsliceColor()helper delegates to manager; constructor connectscolorsChanged→markOverlayDirty()to repaint the filter-band overlayVfoWidget.cppcolorsChanged→syncFromSlice()+update()RxApplet.cppCatControlApplet.cppmain.cppSliceColorManager::instance().load()is called immediately afterAppSettings::instance().load()at startup, so custom colors are activebefore any widget is constructed.
CMakeLists.txtsrc/gui/SliceColorManager.cppadded to the source list.Settings Persistence
When
SliceColorsUseCustomisFalse(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
filter band, VFO badge, and RX applet badge all update immediately without
reopening the dialog.
AetherSDR.settings.