Skip to content

Phase 2 accessibility: live-region announcements + structural fixes for custom widgets #3288

Description

@w9fyi

Thank you to @ten9876 for merging #899 and to @jensenpat for confirming the Phase 1 work landed in release builds. Having real builds to test against made it possible to do the structured VoiceOver pass I promised in #988 — and that pass is what produced this report.

Phase 1 brought every interactive control up to the point where VoiceOver can find it and read a name. That's the right foundation. Phase 2 is about making the radio usable — announcing values, states, and changes, and fixing the controls that are structurally inaccessible regardless of how many names are set.


Critical gap: QAccessible::updateAccessibility is never called

The single highest-impact finding from the audit: QAccessible::updateAccessibility is called zero times in the entire codebase. This means that even widgets with accessible names are static labels to VoiceOver — no values, states, or live changes are ever announced. A screen reader user can navigate to the S-meter and hear "S-Meter," but they will never hear "S9" or "S9+20." They can find the VFO frequency display and hear "VFO A," but tuning the dial is completely silent.

Everything in Phase 2 flows from fixing this. The quick wins below can be done without it; the structural fixes require it.


Quick wins — setAccessibleName additions, no architecture change

These are purely additive. No new classes, no focus policy changes — just missing names on already-instantiated widgets. Each one can be a one-liner in the existing construction code.

MeterApplet (src/gui/MeterApplet.cpp):

  • m_paTempGaugesetAccessibleName(tr("PA temperature gauge"))
  • m_supplyGaugesetAccessibleName(tr("Supply voltage gauge"))
  • m_fanGaugesetAccessibleName(tr("Fan speed gauge"))

TunerApplet (src/gui/TunerApplet.cpp):

  • m_fwdGauge (line 82) → setAccessibleName(tr("Forward power gauge"))
  • m_swrGauge (line 88) → setAccessibleName(tr("SWR gauge"))
  • m_c1Bar (line 100) → setAccessibleName(tr("ATU capacitor C1 position"))
  • m_lBar (line 101) → setAccessibleName(tr("ATU inductor L position"))
  • m_c2Bar (line 102) → setAccessibleName(tr("ATU capacitor C2 position"))

AmpApplet (src/gui/AmpApplet.cpp):

  • m_fwdGauge (line 21) → setAccessibleName(tr("Amplifier forward power gauge"))
  • m_swrGauge (line 27) → setAccessibleName(tr("Amplifier SWR gauge"))
  • m_tempGauge (line 33) → setAccessibleName(tr("Amplifier temperature gauge"))

VfoWidget tab bar (src/gui/VfoWidget.cpp, lines 758–790):
The Audio/DSP/Mode/X/RIT/DAX tab labels are QLabel widgets with no accessible names. In the creation loop, add setAccessibleName(tabA11yNames[i]) for each label. This is a quick win even before the structural fix below, because it at least makes the labels discoverable by name.

SpectrumWidget (src/gui/SpectrumWidget.cpp):

  • setAccessibleName(tr("Panadapter"))
  • setAccessibleDescription(tr("Radio frequency spectrum display"))

TciApplet MeterSlider (src/gui/TciApplet.cpp, lines 93 and 126):

  • RX meters: setAccessibleName(tr("TCI RX channel %1 gain").arg(i+1))
  • TX meter: setAccessibleName(tr("TCI TX gain"))

DaxApplet MeterSlider (src/gui/DaxApplet.cpp, lines 98 and 130):

  • Apply the same pattern with DAX-appropriate names.

PhoneCwApplet HGauge note: PhoneCwApplet.cpp:138 already names its HGauge instance — these additions bring the rest of the codebase into parity with that existing pattern.


Structural fixes — the harder items

These require more than name changes.

1. Live value announcements — SMeterWidget and VfoWidget

SMeterWidget::setLevel() (src/gui/SMeterWidget.cpp, around line 232) updates the displayed value but never fires a QAccessibleValueChangeEvent. The fix:

QAccessibleValueChangeEvent ev(this, newValue);
QAccessible::updateAccessibility(&ev);

VfoWidget::updateFreqLabel() calls m_freqLabel->setText() but never fires a QAccessibleValueChangeEvent. Same pattern applies. VFO frequency changes are the most important single announcement in a radio application — a blind operator needs to hear the frequency when tuning, the same way a sighted operator reads it.

2. VFO tab bar — QLabel cannot be activated

The six tab labels in VfoWidget.cpp (lines 758–790) are QLabel widgets, not QPushButton. VoiceOver sees them as static text and has no mechanism to activate them. There are two paths:

  • Replace with QPushButton (flat style) — cleanest, role is correct automatically.
  • Alternatively: add setFocusPolicy(Qt::TabFocus) and a keyPressEvent handler (Space/Return → click) to the existing labels. This avoids a layout change but requires manual role annotation.

Either path is fine. I'd suggest the QPushButton replacement — it's more semantically correct and Qt handles the rest.

3. KeyboardMapWidget — shortcut assignment is completely inaccessible

KeyboardMapWidget (src/gui/KeyboardMapWidget.cpp, line 33) has no accessible name, no focus policy, and interaction is mouse-only via hitTest() in mousePressEvent. It is used in ShortcutDialog.cpp:56. A VoiceOver user cannot assign keyboard shortcuts at all — the entire dialog is effectively blocked.

The fix requires:

  • setFocusPolicy(Qt::StrongFocus)
  • Arrow key navigation through the key map
  • QAccessible::updateAccessibility on selection change

This is the most involved item in Phase 2, but it's also the one that most directly blocks a blind user from configuring the application at all.

4. RelayBar — wheel-only interaction

RelayBar (src/gui/HGauge.h, lines 248–303) has no accessible name and accepts interaction only via mouse wheel. TunerApplet.cpp creates C1, L, and C2 bars (lines 100–102) with no setAccessibleName. A keyboard alternative is needed — at minimum, arrow key adjustment with a QAccessibleValueChangeEvent on change.

5. PhaseKnob — hide from accessibility tree, not expose

PhaseKnob (src/gui/PhaseKnob.cpp) has no name and no focus. However, the paired ESC sliders in VfoWidget.cpp (lines 957 and 982) already cover the same values and are the correct accessible interface. PhaseKnob should be excluded from the accessibility tree rather than given independent focus — Qt::WA_AcceptTouchEvents is not the right attribute here; the correct approach is to set an empty accessible name and assign a role that screen readers will skip, so the sliders are the sole accessible path for those values.


Offer

I'm happy to submit PRs for any or all of the above in phases, the same way #899 was structured. A natural split:

  • Phase 2a: All quick-win setAccessibleName additions (MeterApplet, TunerApplet, AmpApplet, VFO tabs, SpectrumWidget, TciApplet/DaxApplet sliders). Purely additive, zero risk, reviewable fast.
  • Phase 2b: QAccessible::updateAccessibility in SMeterWidget::setLevel() and VfoWidget::updateFreqLabel(). Small targeted additions, high impact.
  • Phase 2c: VFO tab bar structural fix (QLabel → QPushButton or keyboard activation).
  • Phase 2d: KeyboardMapWidget keyboard navigation and RelayBar keyboard alternative.

If @m13v is still around and willing to review the QAccessibleInterface work, I'd welcome that — the custom widget approach described on #870 is exactly the architecture I'd follow for Phase 2d.

Let me know which phase to start with and whether there are any constraints on the tab bar widget type (in case QPushButton changes something downstream in the layout).

— AI5OS (@w9fyi)

Metadata

Metadata

Assignees

No one assigned

    Labels

    GUIUser interfaceenhancementImprovement to existing featuremaintainer-reviewRequires maintainer review before any action is taken

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions