feat(slice): radio-letter slice display mode for Multi-Flex (#2606)#2708
Conversation
Adds an opt-in display mode (Radio Setup → Themes → Slice Letter
Display) that switches slice badges from the global slot letter
('A' + sliceId) to the radio-provided per-client letter with a
1-based global-slot subscript (e.g. "A₂" for the second-client's
first slice on global slot 1).
Wolfgang (HB9RYZ) reported that secondary Multi-Flex clients see
slice C / D where SmartSDR shows A / B. The radio sends per-client
letters via `index_letter`; we now parse and surface them as a
display option without re-wiring any of the internal slice-id
plumbing (settings keys, signal routing, command building all stay
keyed on the global sliceId).
Defaults to "Global" so existing users see no behaviour change.
In RadioIndexed mode the RX Applet tab row also reorders to:
[owned slices, sorted by sliceId] [empty slots with sequential
letters] [foreign in-use slots as "—"]
So a user on a 4-slot radio with one slice and one other client
sees: [A₂] [B] [C] [—]. Colour follows the displayed letter so
the user's "A" slice stays cyan regardless of which global slot it
landed on. Spectrum marker / passband colour follows the same
rule via SliceOverlay::perClientLetter.
Also cleans up a stale RxApplet.cpp.bak that had been committed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Foreign in-use slots in the RX-applet tab row previously rendered as
the dim global letter ("A" / "B" / ...) in Global mode and "—" in
RadioIndexed mode. The dim-letter form is visually indistinguishable
from an empty available slot for colour-blind users, so the in-use
state didn't read at a glance.
Unify on "—" in both modes — the dash glyph plus the foreign-style
QSS rule reads as "taken, not yours" regardless of colour vision.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ize, tests (#2606) Addresses follow-up items raised in the PR self-review: 1. **Spectrum off-screen pill + context menus** now resolve the slice letter via SliceLabel::unicodeForm(sliceId, perClientLetter) so the displayed letter follows the same display mode as the marker colour. Previously the pill could read "C" while the marker was painted in cyan because the user's slice was A₂. 2. **Serial-shortcut RadioSetupDialog** (the Settings → Flex Control path) now also connects the sliceLetterDisplayModeChanged signal to the slice-letter refresh fan-out so toggling the mode from that dialog repaints live, not on the next slice event. 3. **RxApplet tab-row stylesheet memoization** — store the last applied colour index per button and skip setStyleSheet when nothing colour-relevant changed. updateSliceButtons fires on every slot occupancy + letter change; cheap, but no reason to re-parse the stylesheet when the palette is unchanged. 4. **slice_model_letter_test** — 6 cases covering letter() fallback, index_letter parse + letterChanged emission, no-op re-apply, letter change, emitLetterRefresh, and unrelated status messages not disturbing the letter. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks @ten9876 — this is a really thoughtful build-out for a fairly subtle Multi-Flex problem. The SliceLabel namespace is a clean place to land the mode logic, the default-off / opt-in toggle keeps existing single-client users invariant, and the tests cover both modes plus the fallbacks. Removing the stale RxApplet.cpp.bak is appreciated as a drive-by.
A few small observations, mostly minor:
1. SliceLabel::drawSliceBadge subscript size assumes a pixel-size font (src/gui/SliceLabel.cpp):
subFont.setPixelSize(std::max(7, (mainFont.pixelSize() * 7) / 10));When the caller's font was set by setPointSize() (the common case — VfoWidget::paintEvent doesn't set a pixel size on badgeFont), mainFont.pixelSize() returns -1, the expression collapses to 0, and std::max(7, 0) pins the subscript at 7 px regardless of how large the main letter is. The output is still legible, but it won't scale with the badge. Consider falling back to QFontMetrics(mainFont).height() (or pointSize()) when pixelSize() is -1.
2. Foreign-owner cleanup on hard disconnect (src/models/RadioModel.cpp):
m_foreignSliceOwners is only pruned inside handleSliceStatus (either when we receive ownership of the slot or when a removed=1 status arrives). If a foreign Multi-Flex client drops without the radio publishing a removed=1 for their slices — abrupt TCP close, network blip — the foreign markers would persist and the slot would stay dimmed in the tab row until something else cleared it. I didn't see a cleanup path in this diff; worth verifying that client / disconnect handling elsewhere drops the entry (or clear it from the per-client-handle teardown path).
3. state as const char* with qstrcmp (src/gui/RxApplet.cpp updateSliceButtons): functional but a enum class SlotState { Ours, Foreign, Empty } would read more clearly and let the compiler check the branches. Not blocking — just a small future-cleanup note.
4. Property-cache key is colourIdx only (applyStyleIfChanged): if a button transitions ours → empty (or ours → foreign) but the colour happens to stay the same, the QSS string isn't rebuilt. The slotState dynamic property + unpolish/polish handles the visual switch via the attribute selector, so this is correct as written — just calling it out because the cache key looks under-specified at first glance. A short comment confirming "QSS attribute selector handles state transitions; cache only tracks colour" would save the next reader a head-scratch.
5. setTextFormat(Qt::RichText) is set on every text update in DaxApplet / TciApplet / PanadapterApplet rather than once at label construction. Harmless, just slightly wasteful — could be moved to the label's setup site.
Nothing here blocks the feature. The default-off behaviour and the test coverage make this safe to land for users who don't care, and the architecture leaves room to deepen the per-client tracking later. Nice work.
Five non-blocking observations from the bot review, all addressed in this commit: 1. SliceLabel::drawSliceBadge now falls back to QFontMetrics(mainFont).height() when the painter's font was set via setPointSize() rather than setPixelSize() (the latter returns -1, which collapsed the subscript-size math to a fixed 7px regardless of badge size). 2. RadioModel now drops m_foreignSliceOwners entries for a client handle when the radio reports `client 0x<handle> disconnected`, and clears the whole map alongside the three other m_clientStations reset paths. Covers the abrupt-disconnect case where the radio may not echo `slice N removed=1` for the dropped client's slices. 3. RxApplet::updateSliceButtons switches from `const char*` + qstrcmp to a function-scoped `enum class SlotState`. Compiler-checked dispatch, cleaner code. 4. applyStyleIfChanged grew a comment explaining that the colour-index-only cache key is intentional — state transitions (ours/foreign/empty) at the same colour are handled by the slotState dynamic property + QSS attribute selector via unpolish/polish, not by the stylesheet rebuild. 5. setTextFormat(Qt::RichText) hoisted from per-update callsites to widget construction in DaxApplet, TciApplet, PanadapterApplet, RxApplet, and VfoWidget. Slightly less wasteful; the format is set once at the same place we set the rest of the label's style. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
Closes #2606. Adds an opt-in display mode that switches slice badges from the global slot letter (
'A' + sliceId) to the radio-provided per-client letter with a 1-based global-slot subscript (e.g.A₂for a secondary client's first slice on global slot 1).Wolfgang (HB9RYZ) reported that secondary Multi-Flex clients see slice C/D in AetherSDR where SmartSDR shows A/B. The radio sends per-client letters via the
index_letterslice-status field — we now parse it and surface it as a UI option without re-wiring any of the internal slice-id plumbing.Default behaviour unchanged
SliceLetterDisplaydefaults to"Global"inAppSettings. Existing users see no behaviour change until they flip the toggle at Radio Setup → Themes → Slice Letter Display.What's in RadioIndexed mode
A₂(radio letter + 1-based global-slot subscript).A₂)B,C, …) — previews what the radio would call the slice if claimed—(with the dim foreign styling)Architecture
SliceModel::letter()returns the radio'sindex_letter(fallback'A' + sliceId) +letterChangedsignal.RadioModelnow tracksm_foreignSliceOwnersand emitsslotOccupancyChangedinstead of dropping foreign-client slices silently.SliceLabelhelper module (richText,unicodeForm,drawSliceBadge,displayColorIndex) readsAppSettings("SliceLetterDisplay")on every call so the runtime view tracks the toggle without restart.SpectrumWidget::SliceOverlaygains aperClientLetterfield; MainWindow keeps it synced viasetSliceOverlayLetteron slice add and onletterChanged.PanadapterApplet::setSliceId(int, const QString&)takes the per-client letter; 4 MainWindow callers passslice->letter().What did NOT change
Drive-by cleanups
src/gui/RxApplet.cpp.bak— a 793-line stale backup that had been committed by accident months ago. Deleting it as part of this PR so it doesn't keep showing up in repo grep results.Follow-up commits in this PR
The first commit landed the feature; subsequent commits address review nits:
SliceLabel::unicodeFormso the displayed letter tracks the mode; secondRadioSetupDialoginstance (Settings → Flex Control) now also wiressliceLetterDisplayModeChanged;updateSliceButtonsmemoises the colour index per button to skip stylesheet rebuilds when nothing colour-relevant changed; newslice_model_letter_test(6 cases) coveringindex_letterparsing,letterChangedemission semantics, andemitLetterRefresh.Test plan
slice_label_testcovers both modes plus fallbacks (10 checks incl.displayColorIndex)slice_model_letter_test— 6 cases covering letter parsing, signal emission, refreshB, no subscript (Global default)A₃, tab row shows[A₃] [B] [C] [—]B, no subscript (Global default)🤖 Generated with Claude Code