Skip to content

feat(gui): badge on keyboard slider steps; Enter releases slider focus#3396

Merged
NF0T merged 2 commits into
aethersdr:mainfrom
jensenpat:slider-kbd-badge-enter-focus
Jun 5, 2026
Merged

feat(gui): badge on keyboard slider steps; Enter releases slider focus#3396
NF0T merged 2 commits into
aethersdr:mainfrom
jensenpat:slider-kbd-badge-enter-focus

Conversation

@jensenpat

@jensenpat jensenpat commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Summary

Two small follow-ups to the applet-slider keyboard support that landed in #3303, plus an audit so the same UX is consistent across every applet slider. Both features make the keyboard path behave like the mouse path on applet sliders (AGC level, TCI/DAX gain, etc.).

1. Value badge on keyboard steps

When you drag an applet slider with the mouse, a value badge pops up above the thumb and lingers briefly (~450 ms) after release. Adjusting the same slider with the keyboard previously showed no badge.

Now keyboard stepping flashes the identical badge with the identical linger/fade timeout, so keyboard and mouse give matching visual feedback.

  • GuardedSlider (AGC threshold, most applet sliders): gains a public flashDragValue(). Keyboard nudges for these sliders are routed through MainWindow's shortcut-lease key handler, so that handler calls flashDragValue() after each arrow / Ctrl+arrow / PageUp/PageDown / Home/End step. It's reached via dynamic_cast because GuardedSlider has no Q_OBJECT macro.
  • MeterSlider (TCI/DAX combined meter + gain fader): shows and lingers the badge directly from its own keyPressEvent.

2. Enter returns focus to the panadapter

After a mouse interaction, a slider holds a short keyboard lease (2 s) / focus, during which arrow keys nudge the slider instead of driving the global panadapter shortcuts. If you want those global shortcuts back immediately — without waiting for the lease to expire — you can now press Enter while the slider has focus.

  • GuardedSlider: MainWindow releases the shortcut lease and clears slider focus, re-enabling the global operating shortcuts.
  • MeterSlider: hides the badge and clears focus.

3. TCI/DAX (MeterSlider) — making the keyboard path actually reachable

The first pass added the badge + Enter handling to MeterSlider, but that code was dead: MeterSlider is a plain QWidget with Qt::TabFocus, so a mouse drag never left keyboard focus on it, and it never engaged the shortcut lease. Because the arrow keys are global shortcuts (Left/Right tune frequency, Up/Down adjust AF gain), a matched QShortcut consumes the key before the focused widget's keyPressEvent runs — so MeterSlider's own arrow stepping (and the new badge) never executed.

Fixes, mirroring GuardedSlider behavior exactly:

  • MeterSlider: Qt::TabFocusQt::StrongFocus, plus an explicit setFocus(MouseFocusReason) on left-press, so a mouse interaction hands off to the keyboard. Adds isDragging().
  • MainWindow: the shortcut lease is generalized from QAbstractSlider* to QWidget* so MeterSlider can hold it (freeing the arrows from the global tune/AF shortcuts while focused). focusChanged and the mouse-press event filter begin the lease for MeterSlider too, and a keypress hook renews it on each step so it doesn't expire mid-adjustment. A leaseHolderBusy() helper unifies isSliderDown()/isDragging() so the lease never times out mid-drag for either type.

Applet-slider audit (gaps found)

Slider type Count Mouse badge Keyboard badge Status
GuardedSlider 66 ✅ (this PR) covered
MeterSlider (TCI/DAX) 4 ✅ (this PR) covered
plain QSlider 8 pre-existing gap, see below

The 8 plain QSliders have never shown a badge (mouse or keyboard): AetherDspWidget DFNR atten/beta, StripFinalOutputPanel freq/level×2/morse-pitch, StripWaveformPanel window, SpectrumOverlayMenu line-width. Most live in transient dialogs/menus, not the main applet panel, and the keyboard-badge path depends on the panadapter shortcut lease that only applies in the main window. Converting them to GuardedSlider would give them the mouse badge for free; left out of this PR to keep it focused — easy follow-up if wanted.

Behavior summary

Action Before After
Keyboard-step an applet slider (incl. TCI/DAX) no badge badge flashes + lingers (same as mouse)
Press Enter with slider focused nothing; wait out the lease global panadapter shortcuts resume immediately

Testing

  • Builds clean (Ninja / RelWithDebInfo, Qt6).
  • Deployed to the test Mac for hands-on verification of badge timing, TCI/DAX keyboard stepping, and Enter-to-release behavior.

💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat

Two follow-ups to the applet-slider keyboard support landed in aethersdr#3303.

1. Keyboard stepping now flashes the same value badge that mouse
   dragging shows, with the identical ~450 ms linger/fade timeout, so
   keyboard and mouse give matching visual feedback:
   - GuardedSlider gains a public flashDragValue(); MainWindow's
     shortcut-lease key handler calls it after each arrow / Ctrl+arrow /
     PageUp/Down / Home/End nudge (reached via dynamic_cast since
     GuardedSlider has no Q_OBJECT).
   - MeterSlider (TCI/DAX gain) shows and lingers the badge from its
     own keyPressEvent.

2. Pressing Enter while a slider holds keyboard focus hands control
   straight back to the panadapter's global shortcuts, instead of
   waiting for the 2 s lease / focus to time out:
   - GuardedSlider: MainWindow releases the shortcut lease and clears
     focus (re-enabling global shortcuts).
   - MeterSlider: hides the badge and clears focus.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Co-authored-by: Codex <noreply@openai.com>
@jensenpat jensenpat marked this pull request as ready for review June 4, 2026 22:47
@jensenpat jensenpat requested a review from a team as a code owner June 4, 2026 22:47

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

Reviewed the diff and traced the relevant context in MainWindow.cpp, GuardedSlider.h, MeterSlider.h, and DragValuePopup.h. The change is well-scoped and mirrors the established mouse-drag patterns cleanly.

A few notes from the read-through:

  • GuardedSlider::flashDragValue() reuses the existing showDragValuePopup() + linger() pair, so the timing (~450 ms kDefaultLingerMs) genuinely matches the mouse-release path — good.
  • The dynamic_cast<GuardedSlider*> in the lease handler is the right call given the lack of Q_OBJECT; the inline comment explains why future readers won't reach for qobject_cast.
  • releaseSliderShortcutLease(true) will renew (not release) if isSliderDown() is true, which is the correct behavior for the Enter path — a stuck mouse drag shouldn't lose its lease.
  • MeterSlider::keyPressEvent only handles arrow keys for stepping (no PageUp/PageDown/Home/End), which is unchanged from before — worth flagging only because the PR description's table reads like all four key classes apply on both slider types. It's a MeterSlider-vs-GuardedSlider asymmetry that pre-dates this PR.
  • The badge is shown even when the value clamps at min/max (both surfaces). Matches the mouse drag's "always update the popup" behavior, so this is the right call.

No AppSettings or RAII concerns; no boundary error-handling gaps; scope is tight to the three files described.

Thanks @jensenpat — clean follow-up to #3303.


🤖 aethersdr-agent · cost: $13.1722 · model: claude-opus-4-7

The first commit added keyboard badge + Enter handling to MeterSlider
(TCI/DAX combined meter+gain fader), but that path was unreachable:

- MeterSlider is a plain QWidget with Qt::TabFocus, so a mouse drag
  never left keyboard focus on it (unlike GuardedSlider).
- MeterSlider is not a QAbstractSlider, so it never engaged the shortcut
  lease. The arrow keys are global shortcuts (Left/Right tune frequency,
  Up/Down adjust AF gain), and a matched QShortcut consumes the key
  before the focused widget's keyPressEvent — so MeterSlider's own arrow
  stepping (and the new badge) never ran.

Fixes, mirroring the GuardedSlider behavior exactly:

- MeterSlider: Qt::TabFocus -> Qt::StrongFocus and an explicit
  setFocus(MouseFocusReason) on left-press, so a mouse interaction hands
  off to the keyboard. Adds isDragging() for the lease.
- MainWindow: generalize the shortcut lease from QAbstractSlider* to
  QWidget* so MeterSlider can hold it (frees the arrows from the global
  tune/AF shortcuts while focused). focusChanged and the mouse-press
  event filter now begin the lease for MeterSlider too, and a keypress
  hook renews it on each step so it doesn't expire mid-adjustment.
  A leaseHolderBusy() helper covers isSliderDown()/isDragging() so the
  lease never times out mid-drag for either control type.

MeterSlider keeps handling its own arrow stepping / badge / Enter in
keyPressEvent; the lease change just makes those keys reach it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Co-authored-by: Codex <noreply@openai.com>
@NF0T NF0T self-assigned this Jun 5, 2026
@NF0T NF0T enabled auto-merge (squash) June 5, 2026 17:39

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

Clean follow-up to #3303, @jensenpat — the three changes work together correctly and the implementation traces through cleanly.

Verified qApp->installEventFilter(this) at MainWindow.cpp:1648 — all widget events flow through eventFilter application-wide, so the new MeterSlider mouse branch receives events without any additional registration. leaseHolderBusy() correctly dispatches isSliderDown() vs isDragging() by type and replaces the previous calls that would have been unsafe on a non-QAbstractSlider. The dynamic_cast for GuardedSlider::flashDragValue() is the right call given the missing Q_OBJECT, and the comment explains it. MeterSlider's Enter path — clearFocus()focusChangedreleaseSliderShortcutLease(false) — reaches the same outcome as GuardedSlider's direct releaseSliderShortcutLease(true) call; both leave focus cleared, lease released, and global shortcuts restored.

One minor note for future maintainability: a short comment in MeterSlider::keyPressEvent noting that clearFocus() triggers the lease release via focusChanged would help a reader who encounters the Enter path without context from the GuardedSlider side. Not blocking.

All five CI checks green.

73,
Ryan NF0T

@NF0T NF0T merged commit f98cc39 into aethersdr:main Jun 5, 2026
5 checks passed
G6PWY-Chris pushed a commit to G6PWY-Chris/AetherSDR that referenced this pull request Jun 22, 2026
aethersdr#3396)

## Summary

Two small follow-ups to the applet-slider keyboard support that landed
in aethersdr#3303, plus an audit so the same UX is consistent across every applet
slider. Both features make the keyboard path behave like the mouse path
on applet sliders (AGC level, TCI/DAX gain, etc.).

### 1. Value badge on keyboard steps

When you drag an applet slider with the mouse, a value badge pops up
above the thumb and lingers briefly (~450 ms) after release. Adjusting
the **same slider with the keyboard** previously showed **no badge**.

Now keyboard stepping flashes the identical badge with the identical
linger/fade timeout, so keyboard and mouse give matching visual
feedback.

- `GuardedSlider` (AGC threshold, most applet sliders): gains a public
`flashDragValue()`. Keyboard nudges for these sliders are routed through
`MainWindow`'s shortcut-lease key handler, so that handler calls
`flashDragValue()` after each arrow / `Ctrl`+arrow / `PageUp`/`PageDown`
/ `Home`/`End` step. It's reached via `dynamic_cast` because
`GuardedSlider` has no `Q_OBJECT` macro.
- `MeterSlider` (TCI/DAX combined meter + gain fader): shows and lingers
the badge directly from its own `keyPressEvent`.

### 2. Enter returns focus to the panadapter

After a mouse interaction, a slider holds a short keyboard lease (2 s) /
focus, during which arrow keys nudge the slider instead of driving the
global panadapter shortcuts. If you want those global shortcuts back
**immediately** — without waiting for the lease to expire — you can now
press **Enter** while the slider has focus.

- `GuardedSlider`: `MainWindow` releases the shortcut lease and clears
slider focus, re-enabling the global operating shortcuts.
- `MeterSlider`: hides the badge and clears focus.

### 3. TCI/DAX (MeterSlider) — making the keyboard path actually
reachable

The first pass added the badge + Enter handling to `MeterSlider`, but
that code was **dead**: `MeterSlider` is a plain `QWidget` with
`Qt::TabFocus`, so a mouse drag never left keyboard focus on it, and it
never engaged the shortcut lease. Because the arrow keys are **global
shortcuts** (`Left`/`Right` tune frequency, `Up`/`Down` adjust AF gain),
a matched `QShortcut` consumes the key before the focused widget's
`keyPressEvent` runs — so `MeterSlider`'s own arrow stepping (and the
new badge) never executed.

Fixes, mirroring `GuardedSlider` behavior exactly:
- `MeterSlider`: `Qt::TabFocus` → `Qt::StrongFocus`, plus an explicit
`setFocus(MouseFocusReason)` on left-press, so a mouse interaction hands
off to the keyboard. Adds `isDragging()`.
- `MainWindow`: the shortcut lease is generalized from
`QAbstractSlider*` to `QWidget*` so `MeterSlider` can hold it (freeing
the arrows from the global tune/AF shortcuts while focused).
`focusChanged` and the mouse-press event filter begin the lease for
`MeterSlider` too, and a keypress hook renews it on each step so it
doesn't expire mid-adjustment. A `leaseHolderBusy()` helper unifies
`isSliderDown()`/`isDragging()` so the lease never times out mid-drag
for either type.

### Applet-slider audit (gaps found)

| Slider type | Count | Mouse badge | Keyboard badge | Status |
|---|---|---|---|---|
| `GuardedSlider` | 66 | ✅ | ✅ (this PR) | covered |
| `MeterSlider` (TCI/DAX) | 4 | ✅ | ✅ (this PR) | covered |
| plain `QSlider` | 8 | ❌ | ❌ | pre-existing gap, see below |

The 8 plain `QSlider`s have **never** shown a badge (mouse or keyboard):
`AetherDspWidget` DFNR atten/beta, `StripFinalOutputPanel`
freq/level×2/morse-pitch, `StripWaveformPanel` window,
`SpectrumOverlayMenu` line-width. Most live in transient
**dialogs/menus**, not the main applet panel, and the keyboard-badge
path depends on the panadapter shortcut lease that only applies in the
main window. Converting them to `GuardedSlider` would give them the
mouse badge for free; left out of this PR to keep it focused — easy
follow-up if wanted.

## Behavior summary

| Action | Before | After |
|---|---|---|
| Keyboard-step an applet slider (incl. TCI/DAX) | no badge | badge
flashes + lingers (same as mouse) |
| Press Enter with slider focused | nothing; wait out the lease | global
panadapter shortcuts resume immediately |

## Testing

- Builds clean (Ninja / RelWithDebInfo, Qt6).
- Deployed to the test Mac for hands-on verification of badge timing,
TCI/DAX keyboard stepping, and Enter-to-release behavior.

💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat

---------

Co-authored-by: Codex <noreply@openai.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.

2 participants