feat(gui): badge on keyboard slider steps; Enter releases slider focus#3396
Conversation
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>
There was a problem hiding this comment.
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 existingshowDragValuePopup()+linger()pair, so the timing (~450 mskDefaultLingerMs) genuinely matches the mouse-release path — good.- The
dynamic_cast<GuardedSlider*>in the lease handler is the right call given the lack ofQ_OBJECT; the inline comment explains why future readers won't reach forqobject_cast. releaseSliderShortcutLease(true)will renew (not release) ifisSliderDown()is true, which is the correct behavior for the Enter path — a stuck mouse drag shouldn't lose its lease.MeterSlider::keyPressEventonly 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 aMeterSlider-vs-GuardedSliderasymmetry 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
left a comment
There was a problem hiding this comment.
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() → focusChanged → releaseSliderShortcutLease(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
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>
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 publicflashDragValue(). Keyboard nudges for these sliders are routed throughMainWindow's shortcut-lease key handler, so that handler callsflashDragValue()after each arrow /Ctrl+arrow /PageUp/PageDown/Home/Endstep. It's reached viadynamic_castbecauseGuardedSliderhas noQ_OBJECTmacro.MeterSlider(TCI/DAX combined meter + gain fader): shows and lingers the badge directly from its ownkeyPressEvent.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:MainWindowreleases 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:MeterSlideris a plainQWidgetwithQt::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/Righttune frequency,Up/Downadjust AF gain), a matchedQShortcutconsumes the key before the focused widget'skeyPressEventruns — soMeterSlider's own arrow stepping (and the new badge) never executed.Fixes, mirroring
GuardedSliderbehavior exactly:MeterSlider:Qt::TabFocus→Qt::StrongFocus, plus an explicitsetFocus(MouseFocusReason)on left-press, so a mouse interaction hands off to the keyboard. AddsisDragging().MainWindow: the shortcut lease is generalized fromQAbstractSlider*toQWidget*soMeterSlidercan hold it (freeing the arrows from the global tune/AF shortcuts while focused).focusChangedand the mouse-press event filter begin the lease forMeterSlidertoo, and a keypress hook renews it on each step so it doesn't expire mid-adjustment. AleaseHolderBusy()helper unifiesisSliderDown()/isDragging()so the lease never times out mid-drag for either type.Applet-slider audit (gaps found)
GuardedSliderMeterSlider(TCI/DAX)QSliderThe 8 plain
QSliders have never shown a badge (mouse or keyboard):AetherDspWidgetDFNR atten/beta,StripFinalOutputPanelfreq/level×2/morse-pitch,StripWaveformPanelwindow,SpectrumOverlayMenuline-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 toGuardedSliderwould give them the mouse badge for free; left out of this PR to keep it focused — easy follow-up if wanted.Behavior summary
Testing
💻 Generated with Claude Code (Opus 4.8) with architecture by @jensenpat