accessibility: phase 2 — widget names, live announcements, VFO tabs, keyboard nav#3303
Conversation
jensenpat
left a comment
There was a problem hiding this comment.
Thanks for pushing the Phase 2 accessibility work forward. I reviewed this against latest upstream/main (73117bb7) and found a few issues to address before merge.
Findings:
-
This PR is currently non-mergeable with
main.AmpApplet.cppandTunerApplet.cppboth conflict with the recent PWR/SWR applet refactor: main now uses external value labels and meter ballistics, while this PR adds accessible names to the older inline gauge layout. The fix should preserve the new row-label/ballistics layout and add the accessibility metadata on top of it. -
VfoWidget::updateFreqLabel()now posts aQAccessibleValueChangeEventevery time the frequency label is refreshed. That method is reached from more than explicit user tuning: slice sync, collapse refresh, lock-feedback state changes, and everySliceModel::frequencyChangedemission from local, CAT, TCI, hardware, or radio status updates. The VFO wheel path alone accepts one tune step every 50 ms, so VoiceOver could be asked to speak around 20 times per second during active scrolling. Please debounce/coalesce this so accessibility gets the settled value rather than every intermediate label refresh.
A possible shape:
void VfoWidget::scheduleFrequencyAnnouncement(const QString& text)
{
if (!QAccessible::isActive()) return;
m_pendingAccessibleFrequencyText = text;
m_accessibleFrequencyTimer.start(300); // single-shot, restart on each tune
}
// timeout:
if (m_pendingAccessibleFrequencyText != m_lastAccessibleFrequencyText) {
m_lastAccessibleFrequencyText = m_pendingAccessibleFrequencyText;
QAccessibleValueChangeEvent event(m_freqLabel, m_pendingAccessibleFrequencyText);
QAccessible::updateAccessibility(&event);
}I would also consider calling this only from the tune-driven path, or passing an explicit AnnounceFrequencyChange flag into updateFreqLabel(), so sync/collapse repaint paths do not speak. LOCKED can probably announce immediately, but should still suppress repeated identical announcements while the 500 ms locked-feedback gate is active.
-
SMeterWidget::setLevel()announces the rawdbm, but S-Meter Peak mode paintsm_peakDbminstead. That can make VoiceOver announce a value that disagrees with the visible meter. The accessible value should be derived from the same displayed value/text used by paint, e.g. current S-units plus dBm for the active RX mode. -
The DAX/TCI
MeterSliders receive accessible names, butMeterSlideris still a customQWidgetwith no focus policy or keyboard handling. The PR test note says these should announce when focused, but normal keyboard focus cannot reach them yet. Either add real focus/keyboard/value accessibility forMeterSlider, or narrow this PR's claim to naming only.
git diff --check upstream/main...refs/remotes/upstream/pr-3303 is clean. I did not build because the PR does not merge cleanly yet.
3e57050 to
3a07f7f
Compare
|
Thanks for the thorough review, @jensenpat. I've rebased onto `0f4a6875` and addressed all four items: AmpApplet / TunerApplet conflicts — Both files are now based on the row-label + ballistics layout from main. Accessible names sit on the refactored widgets (`m_fwdGauge`, `m_swrGauge`, `m_idGauge` for the amp; `m_fwdGauge`, `m_swrGauge` for the tuner). `m_tempGauge` is gone — the `m_c1Bar`/`m_lBar`/`m_c2Bar` names were already present in your version of TunerApplet, so those needed no change. VFO frequency debounce — `updateFreqLabel()` now routes through `scheduleFrequencyAnnouncement()`, a 300 ms single-shot timer that restarts on each call and only fires `QAccessibleValueChangeEvent` when the settled text differs from the last announced value. LOCKED is still immediate but suppressed on repeats (the 500 ms lock-feedback gate already serialises those). S-meter displayed value — `setLevel()` now derives the accessible text from the same `displayDbm` selection that `paintEvent` uses (`m_peakDbm` in SMeterPeak mode, `m_levelDbm` otherwise) and formats it as "S7, -79 dBm" to match the painted readout exactly. MeterSlider focus + keyboard — Added `Qt::TabFocus` and a `keyPressEvent` handler (Left/Down −5%, Right/Up +5%) to `MeterSlider.h`. DAX/TCI sliders are now reachable by Tab and operable without a mouse. `git diff --check upstream/main...HEAD` is still clean. Force-pushed to the same branch so the PR URL is unchanged. |
jensenpat
left a comment
There was a problem hiding this comment.
Phase-2 feedback from the prior review is addressed. Verified locally: full build clean on macOS (merged onto current main), and CI is green on Linux, Windows, macOS, and CodeQL. Approving.
…keyboard nav (aethersdr#3303) Phase 2a/2b/2c/2d: setAccessibleName on meter/amp/tuner/spectrum/DAX/TCI widgets; debounced VFO frequency + S-meter live announcements; VFO tab bar QLabel->QPushButton; RelayBar/PhaseKnob keyboard navigation. Squashed to satisfy signed-commit branch protection; original authorship preserved (w9fyi / Justin Mann / AI5OS).
440f1db to
97ec9aa
Compare
|
Thanks for this, @w9fyi — phase 2 looks great, and I verified it builds clean on Linux, Windows, and macOS. One heads-up on how it's landing: our To speed up future merges (and skip the manual squash-and-sign), it's worth setting up SSH commit signing for GitHub on your side. If you're working with an AI agent, you can just ask it to "set up SSH commit signing for GitHub." The gist: git config --global gpg.format ssh
git config --global user.signingkey ~/.ssh/id_ed25519.pub
git config --global commit.gpgsign true…then add that same key to GitHub under Settings → SSH and GPG keys as a Signing Key (separate from an auth key). After that your commits show up as Verified and clear the merge gate automatically — no maintainer squash needed. Thanks again for the accessibility work — this is a meaningful improvement. 🙏 |
#3396) ## 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::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>
…keyboard nav (aethersdr#3303) Companion to aethersdr#3288 (Phase 2 accessibility audit), per maintainer guidance from @jensenpat. Addresses all four issues from the @jensenpat review on the previous iteration. ## What's in this PR ### Phase 2a — `setAccessibleName` on previously unnamed widgets All changes are additive, no behavioral risk. **MeterApplet** (`MeterApplet.cpp`): PA temperature, supply voltage, main fan speed gauges **AmpApplet** (`AmpApplet.cpp`): Forward power, SWR, drain current gauges — rebased onto the new row-label/ballistics layout from aethersdr#3301 **TunerApplet** (`TunerApplet.cpp`): Forward power, SWR gauges + C1/L/C2 relay bars — rebased onto the same row-label refactor **SpectrumWidget** (`SpectrumWidget.cpp`): "Panadapter spectrum display" **TciApplet / DaxApplet**: RX 1–N gain sliders, TX gain slider (named in loop) ### Phase 2b — Live accessibility announcements **VFO frequency** (`VfoWidget.cpp` / `VfoWidget.h`): - `updateFreqLabel()` now routes through `scheduleFrequencyAnnouncement()` — a 300 ms single-shot debounce timer that fires once the frequency settles, preventing VoiceOver from being asked to speak ~20 times/second during active wheel tuning - LOCKED state announces immediately (user-triggered, infrequent) but suppresses repeats while the 500 ms lock-feedback gate is active **S-meter** (`SMeterWidget.cpp`): - `setFocusPolicy(Qt::TabFocus)` added — widget is keyboard-reachable - `setLevel()` announces the *displayed* value, not raw dBm: in S-Meter Peak mode the event value is derived from `m_peakDbm` (what the needle and text readout show), not `m_levelDbm`; formatted as "S7, -79 dBm" to match the painted readout exactly **MeterSlider** (`MeterSlider.h`): - `setFocusPolicy(Qt::TabFocus)` added — DAX/TCI gain sliders are now keyboard-reachable - `keyPressEvent` handler: Left/Down −5%, Right/Up +5% — gain thumb is adjustable without a mouse ### Phase 2c — VFO tab bar: `QLabel` → `QPushButton` VFO tab labels were `QLabel` with a click event filter; VoiceOver could not activate them. Replaced with `QPushButton` throughout (`VfoWidget.cpp`, `VfoWidget.h`). ### Phase 2d — Keyboard navigation: RelayBar + PhaseKnob - `RelayBar` gains arrow-key support (Left/Right step through relay positions) - `PhaseKnob` excluded from the tab order (it is already accessible via the paired ESC sliders) ## Test notes On macOS with VoiceOver: 1. Tab to the S-meter — subsequent signal level changes should be spoken as "S7, -79 dBm" etc. 2. Tune the VFO wheel quickly — VoiceOver speaks once after tuning settles (not on every step) 3. Lock the VFO — VoiceOver speaks "LOCKED" once 4. Tab to a DAX/TCI gain slider — Left/Right arrows move the thumb 5. VFO tab bar buttons ("DSP", "USB", "X/RIT", "DAX") are activatable via VoiceOver — AI5OS (@w9fyi)
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>
Companion to #3288 (Phase 2 accessibility audit), per maintainer guidance from @jensenpat.
Addresses all four issues from the @jensenpat review on the previous iteration.
What's in this PR
Phase 2a —
setAccessibleNameon previously unnamed widgetsAll changes are additive, no behavioral risk.
MeterApplet (
MeterApplet.cpp): PA temperature, supply voltage, main fan speed gaugesAmpApplet (
AmpApplet.cpp): Forward power, SWR, drain current gauges — rebased onto the new row-label/ballistics layout from #3301TunerApplet (
TunerApplet.cpp): Forward power, SWR gauges + C1/L/C2 relay bars — rebased onto the same row-label refactorSpectrumWidget (
SpectrumWidget.cpp): "Panadapter spectrum display"TciApplet / DaxApplet: RX 1–N gain sliders, TX gain slider (named in loop)
Phase 2b — Live accessibility announcements
VFO frequency (
VfoWidget.cpp/VfoWidget.h):updateFreqLabel()now routes throughscheduleFrequencyAnnouncement()— a 300 ms single-shot debounce timer that fires once the frequency settles, preventing VoiceOver from being asked to speak ~20 times/second during active wheel tuningS-meter (
SMeterWidget.cpp):setFocusPolicy(Qt::TabFocus)added — widget is keyboard-reachablesetLevel()announces the displayed value, not raw dBm: in S-Meter Peak mode the event value is derived fromm_peakDbm(what the needle and text readout show), notm_levelDbm; formatted as "S7, -79 dBm" to match the painted readout exactlyMeterSlider (
MeterSlider.h):setFocusPolicy(Qt::TabFocus)added — DAX/TCI gain sliders are now keyboard-reachablekeyPressEventhandler: Left/Down −5%, Right/Up +5% — gain thumb is adjustable without a mousePhase 2c — VFO tab bar:
QLabel→QPushButtonVFO tab labels were
QLabelwith a click event filter; VoiceOver could not activate them. Replaced withQPushButtonthroughout (VfoWidget.cpp,VfoWidget.h).Phase 2d — Keyboard navigation: RelayBar + PhaseKnob
RelayBargains arrow-key support (Left/Right step through relay positions)PhaseKnobexcluded from the tab order (it is already accessible via the paired ESC sliders)Test notes
On macOS with VoiceOver:
— AI5OS (@w9fyi)