fix(rx): consolidate slice-mute UI — single-click this slice, double-click all#3006
Conversation
…click all PR #2833 added a dedicated 'mute all slices' button to the RX Controls slice-tab row, which fixed the workflow but on inline (≤4-slice) radios crowded the header row past the panel width — the button sat right against the panel edge with the antenna and filter chips squeezed beside it. Remove the standalone mute-all button. Extend the existing per-slice mute icon next to the AF slider so a single click toggles the current slice (the existing behaviour) and a double click toggles every owned slice (the use case the standalone button was added for). The mute-all keyboard shortcut and MainWindow::onMuteAllSlicesToggle() entry point remain wired and unchanged — only the dedicated button goes away. Implementation: - m_muteBtn no longer setCheckable; the 🔊/🔇 icon is driven entirely from SliceModel::audioMuteChanged so the source of truth is the radio state, not the in-flight click. - Single click on m_muteBtn starts a single-shot QTimer at the platform doubleClickInterval (typically 400 ms). When the timer fires it toggles the current slice's audio_mute. - Double click on m_muteBtn cancels that timer and emits muteAllToggled via eventFilter; MainWindow::onMuteAllSlicesToggle() already handles the all-slices logic (including the RADE-slice skip). - m_muteSuppressNextClick swallows the trailing clicked() Qt fires after a MouseButtonDblClick sequence so the double-click doesn't end with a stray single-click action queued behind it. The header row's overflow path (>4-slice / 6700) gets a layout simplification as a side effect — slice tabs now sit naturally without the mute-all anchor on the right. Tooltip updated to mention the double-click affordance. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visual ack for the double-click-to-mute-all flow added in the parent commit. On every owned-slice audioMuteChanged, the RxApplet recomputes whether every owned slice is in the muted state and applies a QGraphicsOpacityEffect at 35% to each slice-tab button and to the static slice badge. The dim lifts the moment any one slice unmutes — single-click on the active slice or a re-double-click both naturally restore the bright state. Implementation: - RxApplet::setRadioModel now also wires audioMuteChanged on every slice the model exposes (existing slices at connect time + every future SliceModel from RadioModel::sliceAdded). UniqueConnection guards against double-attach on re-entry; slice destruction auto-disconnects via Qt's signal-receiver bookkeeping. - refreshAllMutedDim() walks RadioModel::slices() (already pruned to owned slices via the client_handle filter), checks for any unmuted, and calls setSliceButtonsDimmed(true/false). - setSliceButtonsDimmed iterates m_sliceBtns and m_sliceBadge, attaching a QGraphicsOpacityEffect once and adjusting its opacity thereafter. Works in both the inline ≤4-slice layout (buttons in m_headerRow) and the >4-slice overflow layout (buttons in m_sliceTabRow) because the effect lives on each button, not on the row container. - updateSliceButtons calls refreshAllMutedDim at the tail so rebuilt buttons inherit the current dim state. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks @ten9876 — the UX consolidation makes sense and the implementation is well-documented (the m_muteSuppressNextClick rationale and the click-deferral comment are exactly the kind of context that helps future readers). A few notes:
UX trade-off worth calling out
Every single mute click now incurs QApplication::doubleClickInterval() of latency (~400 ms typical) before the radio sees the toggle, plus the radio's own ack RTT before the icon flips. The test plan acknowledges the ~400 ms but it might be worth confirming this still feels acceptable for power users who mute/unmute a single slice frequently — it's a real regression on the most common single-click path. Not blocking, just a deliberate cost of the design.
Qt::UniqueConnection on a lambda is undefined behavior
In setRadioModel (around RxApplet.cpp:1611-1622):
connect(slice, &SliceModel::audioMuteChanged, this,
[this](bool) { refreshAllMutedDim(); },
Qt::UniqueConnection);Per Qt docs, Qt::UniqueConnection only deduplicates connections to non-static member function pointers — for functors/lambdas it is silently ignored and a duplicate connection is made. So the "protects against double-attaches when setRadioModel is invoked more than once" comment is inaccurate.
Today setRadioModel is typically called once, so this is latent — but if you'd like the comment's invariant to actually hold, either route through a named member slot (&RxApplet::refreshAllMutedDim) which works with UniqueConnection, or disconnect this object from the slice's audioMuteChanged signal at the top of the per-slice setup. The same applies to the per-slice connect added in the sliceAdded handler.
Keyboard activation skips the double-click path
If the user focuses m_muteBtn and presses Space/Enter, Qt fires clicked() but never generates MouseButtonDblClick, so keyboard activation always takes the deferred-single-click path (and there is no keyboard affordance for mute-all on the button itself). That's fine since the mute-all keyboard shortcut still lives on MainWindow, but worth being explicit that the double-click is mouse-only.
Scope and other checks
- Only
RxApplet.cpp/RxApplet.htouched — squarely within stated scope. - No
QSettingsintroduced; no new system-boundary calls added. - All
m_muteAllBtnreferences in.cpp/.hremoved (verified by grep). - Null guards on
m_slicein the timer callback look correct. QGraphicsOpacityEffectreuse viaqobject_cast<...>(graphicsEffect())insetSliceButtonsDimmedis correct and avoids leaks.
Happy to defer the lambda/UniqueConnection fix to a follow-up if you'd prefer to keep this PR tight.
🤖 aethersdr-agent · cost: $5.6512 · model: claude-opus-4-7
…suppress
Two bugs in the consolidated mute UX uncovered in testing:
1. The all-muted dim never appeared because the per-slice
audioMuteChanged listener was attached via a lambda paired with
Qt::UniqueConnection. Qt silently rejects that combination — the
connection is not made and a runtime warning is emitted
('unique connections require a pointer to member function of a
QObject subclass'). Switch the slot to &RxApplet::refreshAllMutedDim
(member-function pointer) which UniqueConnection accepts. Qt's
prefix-match rule lets a zero-arg slot bind to the (bool)
audioMuteChanged and (int) sliceRemoved signals.
2. The first single click after any double-click did nothing. Cause:
m_muteSuppressNextClick was set to true in the eventFilter's
MouseButtonDblClick branch on the assumption that Qt would emit a
trailing clicked() that needed swallowing — but because the
eventFilter returns true on the dblclick event,
QAbstractButton::mouseDoubleClickEvent is never invoked, the button
never re-enters pressed state on the second press, and the second
release does not emit clicked() at all. The flag just sat there
sticky from prior double-clicks, ready to eat the next genuine
single click. Drop the flag and the associated check.
Also swap the dim implementation from QGraphicsOpacityEffect to a
direct stylesheet swap. QGraphicsOpacityEffect doesn't always
compose cleanly with QSS-styled widgets on Linux X11 (silently
dropped in some configurations). The new approach caches each
button's original stylesheet on first dim and restores it on un-dim,
applying a uniform dark-gray look while in the all-muted state.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#3009) (#3015) Closes #3009. Several widgets (slice mute button, RX chain stages, waveform widgets) defer the single-click action by `QApplication::doubleClickInterval()` (~400 ms on Linux) so a double-click can override it. Power users wanted instant single-click; users with slower input wanted longer windows. This PR makes the interval a project-wide AppSettings key with a UI surface. ## Changes ### `src/gui/InteractionSettings.h` (new) Inline accessor `clickDiscriminationIntervalMs()` reads AppSettings key `ClickDiscriminationIntervalMs`, falls back to platform default if missing/malformed/negative. `0` is honoured and means "fire single-click instantly" (also disables the double-click affordances on widgets that use this gate). ### Migrated 7 call sites - `RxApplet.cpp` (slice mute button — #3006) - `StripRxChainWidget.cpp` - `ClientRxChainWidget.cpp` - `ClientChainWidget.cpp` - `StripChainWidget.cpp` - `WaveformWidget.cpp` — also dropped construction-time cache; reads at click time - `StripWaveform.cpp` — same The two waveform widgets previously called `QTimer::setInterval()` at construction. Switching to `start(clickDiscriminationIntervalMs())` at click time means a user-adjusted Radio Setup value propagates without an app restart. ### UI surface New "Single-click delay" group in **Radio Setup → Themes** tab (which is `buildUiEnhancementsTab()` internally). 0-1000 ms QSpinBox defaulted to platform value, Reset button restores platform default, descriptive label explains the 0 = instant-single-click semantics. ## Test plan - [x] Builds clean - [ ] Open Radio Setup → Themes, verify "Single-click delay" group renders with platform default - [ ] Set delay to 100, click slice mute button — verify ~100 ms perceived latency - [ ] Set delay to 0, click slice mute button — verify instant response - [ ] Set delay to 0, double-click slice mute button — verify the double-click action no longer fires (both clicks fire single-click toggles) - [ ] Click Reset, verify spinbox returns to platform default 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…click all (aethersdr#3006) ## Summary PR aethersdr#2833 added a dedicated 'mute all slices' button to the RX Controls slice-tab row. The workflow it enables (one click to mute every owned slice) is useful, but on inline (≤4-slice) radios the button crowded the header row past the panel width — the speaker icon sat right against the right edge with the antenna and filter chips squeezed beside it. Remove the standalone mute-all button. Extend the existing per-slice mute icon next to the AF slider so: - **Single click** → toggle this slice's audio mute (existing behavior) - **Double click** → toggle every owned slice (the use case aethersdr#2833 added) The mute-all keyboard shortcut and `MainWindow::onMuteAllSlicesToggle()` entry point remain wired and unchanged — only the dedicated button goes away. ## Implementation - `m_muteBtn` no longer `setCheckable`; the 🔊/🔇 icon is driven entirely from `SliceModel::audioMuteChanged` so the source of truth is the radio state, not the in-flight click. - Single click starts a single-shot `QTimer` at the platform `doubleClickInterval()` (typically 400 ms). When the timer fires, it toggles the current slice's `audio_mute`. - Double click cancels that timer and emits `muteAllToggled` via `eventFilter` — `MainWindow::onMuteAllSlicesToggle()` already handles the all-slices loop (including the RADE-slice skip). - `m_muteSuppressNextClick` swallows the trailing `clicked()` Qt fires after a `MouseButtonDblClick` sequence so the double-click doesn't end with a stray single-click action queued behind it. The header row's overflow path (>4-slice / 6700) gets a layout simplification as a side effect — slice tabs sit naturally without the mute-all anchor on the right. Tooltip updated to mention the double-click affordance. ## Test plan - [ ] Single click 🔊 → this slice mutes (~400 ms after click, icon flips to 🔇 on radio ack) - [ ] Click 🔇 again → this slice unmutes - [ ] Double click 🔊 with multiple owned slices, none muted → all muted - [ ] Double click 🔇 with all owned slices muted → all unmuted - [ ] Mute-all keyboard shortcut still works (separate code path, unchanged) - [ ] Inline ≤4-slice radio: header row no longer overflows panel width - [ ] >4-slice / 6700 overflow row: slice tabs left-aligned, no orphan mute icon on the right 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
aethersdr#3009) (aethersdr#3015) Closes aethersdr#3009. Several widgets (slice mute button, RX chain stages, waveform widgets) defer the single-click action by `QApplication::doubleClickInterval()` (~400 ms on Linux) so a double-click can override it. Power users wanted instant single-click; users with slower input wanted longer windows. This PR makes the interval a project-wide AppSettings key with a UI surface. ## Changes ### `src/gui/InteractionSettings.h` (new) Inline accessor `clickDiscriminationIntervalMs()` reads AppSettings key `ClickDiscriminationIntervalMs`, falls back to platform default if missing/malformed/negative. `0` is honoured and means "fire single-click instantly" (also disables the double-click affordances on widgets that use this gate). ### Migrated 7 call sites - `RxApplet.cpp` (slice mute button — aethersdr#3006) - `StripRxChainWidget.cpp` - `ClientRxChainWidget.cpp` - `ClientChainWidget.cpp` - `StripChainWidget.cpp` - `WaveformWidget.cpp` — also dropped construction-time cache; reads at click time - `StripWaveform.cpp` — same The two waveform widgets previously called `QTimer::setInterval()` at construction. Switching to `start(clickDiscriminationIntervalMs())` at click time means a user-adjusted Radio Setup value propagates without an app restart. ### UI surface New "Single-click delay" group in **Radio Setup → Themes** tab (which is `buildUiEnhancementsTab()` internally). 0-1000 ms QSpinBox defaulted to platform value, Reset button restores platform default, descriptive label explains the 0 = instant-single-click semantics. ## Test plan - [x] Builds clean - [ ] Open Radio Setup → Themes, verify "Single-click delay" group renders with platform default - [ ] Set delay to 100, click slice mute button — verify ~100 ms perceived latency - [ ] Set delay to 0, click slice mute button — verify instant response - [ ] Set delay to 0, double-click slice mute button — verify the double-click action no longer fires (both clicks fire single-click toggles) - [ ] Click Reset, verify spinbox returns to platform default 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Summary
PR #2833 added a dedicated 'mute all slices' button to the RX Controls slice-tab row. The workflow it enables (one click to mute every owned slice) is useful, but on inline (≤4-slice) radios the button crowded the header row past the panel width — the speaker icon sat right against the right edge with the antenna and filter chips squeezed beside it.
Remove the standalone mute-all button. Extend the existing per-slice mute icon next to the AF slider so:
The mute-all keyboard shortcut and
MainWindow::onMuteAllSlicesToggle()entry point remain wired and unchanged — only the dedicated button goes away.Implementation
m_muteBtnno longersetCheckable; the 🔊/🔇 icon is driven entirely fromSliceModel::audioMuteChangedso the source of truth is the radio state, not the in-flight click.QTimerat the platformdoubleClickInterval()(typically 400 ms). When the timer fires, it toggles the current slice'saudio_mute.muteAllToggledviaeventFilter—MainWindow::onMuteAllSlicesToggle()already handles the all-slices loop (including the RADE-slice skip).m_muteSuppressNextClickswallows the trailingclicked()Qt fires after aMouseButtonDblClicksequence so the double-click doesn't end with a stray single-click action queued behind it.The header row's overflow path (>4-slice / 6700) gets a layout simplification as a side effect — slice tabs sit naturally without the mute-all anchor on the right.
Tooltip updated to mention the double-click affordance.
Test plan
🤖 Generated with Claude Code