aethersdr / aethersdr/AetherSDR
[Tracking] JAWS on Windows + 4O3A device accessibility — field report from DL9RAR
Nobody has claimed this yet.
- 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)
VfoWidgetandSMeterWidgethave purpose-builtQAccessibleInterface
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 selectorm_connectBtn— connect/disconnect (label changes with state; see note below)m_statusLabel— connection statusm_manualIpEdit— manual IP entry (needs a description giving the expected
format, per the input-widget rule indocs/a11y.md)m_portABandLabel/m_portBBandLabel— current band per portm_portAAntLabel/m_portBAntLabel— selected antenna per portm_portABtns/m_portBBtns— dynamically created antenna buttons; name
them at creation inrebuildAntennaButtons()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 activem_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
QAccessibleValueChangeEventonsetValue()/
setValueImmediate(), gated onhasFocus() && QAccessible::isActive(),
debounced via the existingkAccessibilityAnnouncementIntervalMs— copy
RelayBar's timer structure verbatim. focusOutEvent()must reset the dedup sentinel, exactly asRelayBardoes,
or a value that wanders away and back while unfocused is swallowed.- A
QAccessibleWidgetsubclass returningtext(QAccessible::Value)composed
from the existingm_value,m_unit, andm_labelmembers, so the value
reads as "142 watts" rather than a bare number. Register via
QAccessible::installFactory, followingSMeterWidgetAccessible. - Respect
setReversed()— the compression bar inPhoneCwAppletinverts 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 transitions —
FlexBackend::decodeAmplifierStatus()
already retains the full device key/value map inAmpDelta::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 state —
TunerApplet's OPERATE → BYPASS → STANDBY cycle, and SWR
trips. - Connection loss —
TgxlConnection::onError/
AntennaGeniusModel::connectionErrorcurrently log viaqCWarningand
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 useQAccessibleAnnouncementEvent(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:
- 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. - 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. - 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 aQAccessibleInterface - #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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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