aethersdr / aethersdr/AetherSDR

[Tracking] JAWS on Windows + 4O3A device accessibility — field report from DL9RAR

Open
#4,896 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

enhancement external devices GUI maintainer-review priority: high Windows
Dominant language
C++
Stars
221
Forks
117
Avg merge
2d 7h
Merged PRs (30d)
299

Description

Background

DL9RAR (Joe), a blind operator running a FlexRadio on Windows with the JAWS
screen reader and a full 4O3A stack (Power Genius XL, Tuner Genius XL, Antenna
Genius), wrote to ask what is currently accessible and what is planned. He has
offered to test with his station and provide detailed feedback.

We replied with an honest audit and committed to closing every gap found.
This issue tracks that work.

The audit found the core radio path in reasonable shape and the 4O3A path
substantially unfinished. It also found that our accessibility CI check has a
structural blind spot that let a zero-coverage panel pass clean.

Audit baseline (at time of filing, main @ v26.8.2)
Signal Count
setAccessibleName 500 calls across 63 files
setAccessibleDescription 182
QAccessible::updateAccessibility 20 sites, 14 files
QAccessibleInterface subclasses 4 (SMeter, Vfo, PhaseKnob, CrossNeedleMeter)
src/gui/*.cpp with zero accessible names 145 of 208
tools/check_a11y.py full-tree findings 7

The gap between "500 names" and "20 announcements" is the headline: most
controls identify themselves correctly but go silent when their value changes.

What is already good (do not regress)
  • VfoWidget and SMeterWidget have purpose-built QAccessibleInterface
    adapters reporting real values, with settled-value throttling.
  • RelayBar (src/gui/HGauge.h) is the reference implementation for a
    focusable, announcing, debounced custom widget — see #4565. Every task
    below that adds announcements should follow this pattern.
  • 69 rebindable keyboard actions with import/export cover frequency, mode,
    filter, AGC, RF/AF gain, MOX/TUNE/ATU, and slice selection.

Phase 0 — Make JAWS a supported target, and fix the blind spot that hid this

0.1 — Add JAWS to the supported screen-reader set.
docs/a11y.md currently names VoiceOver, NVDA, Orca, and Narrator only; JAWS
appears nowhere in the repository. Add it to the manual-verification section
with JAWS-specific navigation notes (virtual cursor vs forms mode, and the
JAWS key + F1 control-inspection idiom that testers will use to report).

0.2 — Verify the Windows UIA bridge actually ships.
Qt exposes accessibility on Windows through UI Automation. Confirm that the
accessibility bridge is present in the windeployqt payload produced by
.github/workflows/windows-installer.yml and in the MSIX package
(packaging/windows/create-msix.ps1), and add a packaging assertion so it
cannot silently drop out of a future Qt bump. A missing bridge would make every
other item in this issue invisible, so this is the first thing to check.

0.3 — Close the check_a11y.py constructor blind spot.
check_widget_constructor_names() only walks Class::Class() bodies. Applets
that build their UI in a buildUI() / setupUi() helper are skipped entirely.
16 files currently have zero accessible names AND are invisible to the check
— including AntennaGeniusApplet.cpp, which is why a completely unlabelled
panel passes CI clean today. Extend the check to follow the common build-helper
names, or to treat the class as the unit rather than the constructor.

0.4 — Add a bridge-driven a11y coverage audit.
The automation bridge's dumpTree already reports objectName,
accessibleName, enabled, and live value for every widget. A script that
walks a running instance and reports interactive widgets with no accessible
name
is strictly better evidence than regex static analysis, because it sees
the real widget tree including dynamically created children (e.g. the Antenna
Genius antenna buttons, which are built at runtime from device data and can
never be caught by a source-level linter). Land it as
tools/audit_a11y_tree.py and wire it into the existing automation test
harness.


Phase 1 — Antenna Genius (the worst gap)

src/gui/AntennaGeniusApplet.cpp has zero accessible names, zero focus
policy, and zero announcements. Antenna selection is not optional for
operating a station, so this is the highest-priority functional gap.

Widgets requiring names (from AntennaGeniusApplet.h):

  • m_deviceCombo — device selector
  • m_connectBtn — connect/disconnect (label changes with state; see note below)
  • m_statusLabel — connection status
  • m_manualIpEdit — manual IP entry (needs a description giving the expected
    format, per the input-widget rule in docs/a11y.md)
  • m_portABandLabel / m_portBBandLabel — current band per port
  • m_portAAntLabel / m_portBAntLabel — selected antenna per port
  • m_portABtns / m_portBBtnsdynamically created antenna buttons; name
    them at creation in rebuildAntennaButtons() from the antenna name plus the
    port, e.g. tr("Port A antenna: %1"), and expose checked state so a screen
    reader reports which one is active
  • m_portAAutoBtn / m_portBAutoBtn — AUTO toggles, checkable

Announcements required:

  • Antenna switched on a port (portStatusChanged) — this is the single most
    important announcement in the whole panel
  • Band followed on a port (radioBandChanged)
  • Connection established / lost / failed (connected, disconnected,
    connectionError)

Also set an accessible name on the two port container widgets
(m_portASection / m_portBSection) so the antenna button grid is announced
with its port context rather than as a bare grid of callsign-like names.

Naming rule to apply throughout: for a button whose text is its state
(OPERATE / STANDBY / BYPASS, Connect / Disconnect), the accessible
name must describe the control, not duplicate the label — otherwise a screen
reader reads the state twice and the control's identity never. Add this rule
to docs/a11y.md as part of this phase.


Phase 2 — HGauge: focus and live values across all five device panels

HGauge (src/gui/HGauge.h) is the horizontal bar used for forward power,
reflected power, SWR, drain current, and temperature. It carries an accessible
name set by its host applet, but it has no focus policy and fires no
value-change events
— so it is reachable only by object navigation, never by
Tab, and never announces a changing value. RelayBar, in the same header, has
had the full treatment since #4565; HGauge was left behind.

Fixing HGauge once fixes the gauges in five panels at once: AmpApplet
(PGXL), TunerApplet (TGXL), AcomApplet, SpeApplet, and TxApplet.

Work:

  • setFocusPolicy(Qt::TabFocus).
  • Throttled QAccessibleValueChangeEvent on setValue() /
    setValueImmediate(), gated on hasFocus() && QAccessible::isActive(),
    debounced via the existing kAccessibilityAnnouncementIntervalMs — copy
    RelayBar's timer structure verbatim.
  • focusOutEvent() must reset the dedup sentinel, exactly as RelayBar does,
    or a value that wanders away and back while unfocused is swallowed.
  • A QAccessibleWidget subclass returning text(QAccessible::Value) composed
    from the existing m_value, m_unit, and m_label members, so the value
    reads as "142 watts" rather than a bare number. Register via
    QAccessible::installFactory, following SMeterWidgetAccessible.
  • Respect setReversed() — the compression bar in PhoneCwApplet inverts the
    painted fraction, and the announced value must be the real value, not the
    painted one.

Verify against the throttling tests already in the tree
(range_slider_a11y_test, relay_bar_a11y_test) and add an equivalent
hgauge_a11y_test.


Phase 3 — Faults, warnings, and protection trips

Currently unverified end to end. For an amplifier this is a safety-adjacent
gap, not a cosmetic one: a fault that is displayed but never announced is a
fault a blind operator discovers by its consequences.

Value-change events are the wrong mechanism here — they are gated on focus,
and a fault must reach the operator whose focus is elsewhere. Use
QAccessibleAnnouncementEvent (available in Qt 6.8, which is our declared
minimum in CMakeLists.txt) with QAccessible::AnnouncementPoliteness::Assertive
for faults and Polite for routine state changes.

Sources to wire up:

  • Amplifier state transitionsFlexBackend::decodeAmplifierStatus()
    already retains the full device key/value map in AmpDelta::telemetry, so
    vendor fault keys reach the model today and only need surfacing. Announce
    OPERATE ↔ STANDBY ↔ TRANSMIT transitions, and any fault/protection key
    present in the telemetry map.
  • Tuner stateTunerApplet's OPERATE → BYPASS → STANDBY cycle, and SWR
    trips.
  • Connection lossTgxlConnection::onError /
    AntennaGeniusModel::connectionError currently log via qCWarning and
    update a label; they should also announce.
  • ATU tune failure on the radio's internal ATU.

Requirements:

  • Dedup and rate-limit, so a flapping fault does not become an announcement
    storm — same reasoning as #4565.
  • Announce the transition, not the steady state; do not re-announce an
    unchanged fault on every telemetry frame.
  • Faults must announce regardless of focus and regardless of whether the
    device panel is currently visible.

Phase 4 — Keyboard shortcuts for the 4O3A devices

There are 69 keyboard actions covering the radio and none covering the
4O3A stack. A blind operator can reach those controls by tabbing but cannot
drive them the way they drive the radio.

Add via the existing registerAction() table in
src/gui/MainWindow_Shortcuts.cpp, using new categories alongside the current
"Band" / "Mode" groupings:

  • Antenna — select antenna by index on port A and port B, cycle antenna,
    toggle AUTO per port. Antenna selection first; it is the one Joe called out
    as essential.
  • Amplifier — OPERATE / STANDBY toggle.
  • Tuner — OPERATE / BYPASS / STANDBY cycle.

All new actions inherit the existing rebinding and import/export support, which
matters here: a JAWS user will need to move bindings out of the screen reader's
way, and any default we pick will collide with something for someone.

TX gate: anything that keys the transmitter (tuner tune) must respect the
markTxKeying() / aetherTxKeying contract from #3646 and stay behind
AETHER_AUTOMATION_ALLOW_TX on the automation path.


Phase 5 — Widen live announcements on the core radio path

The 500-names-vs-20-announcements gap. Scope this phase to the controls Joe
named, rather than attempting all 500:

  • Operating mode changes (including mode changed by shortcut or by the radio)
  • Filter bandwidth / passband changes
  • AGC mode and AGC threshold
  • RF gain and AF gain
  • Slice selection and active-slice changes, including TX slice
  • TX state: MOX, TUNE, ATU tune in progress / complete

All must follow the settled-value throttling in docs/a11y.md — a mode change
is a discrete event and can announce immediately, but gain and threshold are
continuous and must debounce. Announcing too much is a regression too; #4565
was filed because a control talked too much, not too little.

Ordering note: the final priority within this phase should come from Joe's
testing, not from our guess. If frequency, mode, and antenna selection are what
matter in a contest, those land first and the rest waits.


Phase 6 — Documentation and CI

  • docs/a11y.md: JAWS section (0.1); the announcement-vs-alert distinction and
    when to use QAccessibleAnnouncementEvent (Phase 3); the "accessible name
    must not duplicate a state-carrying button label" rule (Phase 1); dynamically
    created widgets must be named at creation (Phase 1).
  • tools/check_a11y.py: constructor blind spot (0.3).
  • tools/audit_a11y_tree.py: new bridge-driven audit (0.4).
  • Tests: hgauge_a11y_test (Phase 2), fault-announcement dedup test (Phase 3).
  • Keep the CI check warning-only. It is a nudge, not a gate, and that stays.

Testing with DL9RAR

Joe has offered to test with his own FlexRadio, JAWS, and all three 4O3A
devices. We cannot reproduce that bench here, and none of the work above is
verified until he sees it.

Sequence agreed in the reply:

  1. Core radio functions with JAWS — do controls identify, do values read, does
    tabbing reach what it should, and does anything announce so aggressively it
    gets in the way.
  2. The 4O3A panels after Phase 1 and Phase 2 land — including whether our
    chosen terminology matches how an operator would describe those controls out
    loud.
  3. His judgement on priority ordering for Phase 5.

We also asked him for conventions from other accessible amateur radio software
that his muscle memory already expects, so we match what works rather than
inventing something that only makes sense to a sighted developer.

Anything he reports that is not already listed above gets filed and fixed.
Accessibility gaps are treated as bugs here, not as feature requests.


Related issues

  • #3957 — SpectrumWidget adaptive-filter markers need a QAccessibleInterface
  • #3959 — WAVE waveform scope (WaveformWidget) needs a QAccessibleInterface
  • #1853 — Screen reader / speech output for frequency and key radio state
  • #2665 — UI layout and accessibility controls (configurable font sizes, split
    AppletPanel)
  • #4358 — Keyboard and MIDI commands to navigate the spectrum
  • #4565 — RelayBar / RangeSlider announcement throttling (the reference
    implementation for every announcement task above)
  • #3288 / #3303 — Accessibility Phase 2
  • #899 — Accessibility Phase 1

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

This is a broad tracking issue, so first choose one independently scoped phase rather than attempting the whole list. For Phase 0, read docs/a11y.md, tools/check_a11y.py, .github/workflows/windows-installer.yml, and packaging/windows/create-msix.ps1, then run the existing automation test harness. Done means the selected accessibility target is implemented, covered by the named check or test, and documented without regressing existing behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
accessibility, desktop
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
32/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.