Skip to content

fix(rx): consolidate slice-mute UI — single-click this slice, double-click all#3006

Merged
ten9876 merged 3 commits into
mainfrom
auto/rx-mute-doubleclick-all-slices
May 23, 2026
Merged

fix(rx): consolidate slice-mute UI — single-click this slice, double-click all#3006
ten9876 merged 3 commits into
mainfrom
auto/rx-mute-doubleclick-all-slices

Conversation

@ten9876

@ten9876 ten9876 commented May 23, 2026

Copy link
Copy Markdown
Collaborator

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_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 eventFilterMainWindow::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

…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>
@ten9876 ten9876 requested a review from jensenpat as a code owner May 23, 2026 16:01
@ten9876 ten9876 enabled auto-merge (squash) May 23, 2026 16:01
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>

@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.

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.h touched — squarely within stated scope.
  • No QSettings introduced; no new system-boundary calls added.
  • All m_muteAllBtn references in .cpp / .h removed (verified by grep).
  • Null guards on m_slice in the timer callback look correct.
  • QGraphicsOpacityEffect reuse via qobject_cast<...>(graphicsEffect()) in setSliceButtonsDimmed is 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>
@ten9876 ten9876 merged commit 4dff37c into main May 23, 2026
5 checks passed
@ten9876 ten9876 deleted the auto/rx-mute-doubleclick-all-slices branch May 23, 2026 18:04
ten9876 added a commit that referenced this pull request May 23, 2026
#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>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
…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>
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
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>
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.

1 participant