feat(a11y): QAccessibleInterface for VfoWidget + LevelBar (#3754)#3758
Conversation
The Qt Accessibility Static Analysis flagged two custom-painted widgets in VfoWidget that lacked a QAccessibleInterface, so screen readers couldn't read their content. Both are pre-existing; #3751 surfaced them by touching the file. - VfoWidget: custom-paints its chrome and, while collapsed, the slice-letter and TX badges (which have no child-widget equivalent). Add VfoWidgetAccessible (role Grouping) with a live text(Name) summary — slice letter, frequency, TX state, collapsed — so the flag isn't opaque to AT tools. Expanded-mode detail (frequency, dBm) is already announced via the child labels' value-change events. - LevelBar: the render-only ESC combiner level meter. Like PhaseKnob, the named ESC gain/phase sliders are the accessible interface, so the bar is decorative and returns NoRole to drop out of AT navigation. Matched by objectName since LevelBar has no Q_OBJECT of its own. Factory registered once via QAccessible::installFactory, mirroring PhaseKnob. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Accessibility check findingsThis PR touches
See Findings are warnings only — the build is not blocked. |
…3754) The a11y linter (tools/check_a11y.py) scans each changed file independently and clears a custom-painted widget only when the file itself names QAccessibleInterface or has a companion *Accessible.h. VfoWidget.cpp already satisfies this (the factory returns QAccessibleInterface*), but VfoWidget.h did not, so the header — touched by this PR — stayed flagged. Expand the accessibleSummary() doc comment to point at VfoWidgetAccessible (the QAccessibleInterface in VfoWidget.cpp). Accurate documentation for a header reader, and it clears the lint. Verified: check_a11y.py now reports 0 findings on both files. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Thanks for this, @ten9876 — clean, well-scoped a11y work that closely mirrors the existing PhaseKnob precedent. I checked the key correctness points and they hold up:
- Factory key is correct.
VfoWidgethasQ_OBJECTand lives innamespace AetherSDR, sometaObject()->className()is"AetherSDR::VfoWidget"— thevfoAccessibleFactorykey match will fire. (Matches howphaseKnobFactorykeys on"AetherSDR::PhaseKnob".) - Units are right.
SliceModel::frequency()returns MHz (SliceModel.h:38), so the", %1 MHz"summary is accurate — no Hz→MHz mismatch. - Null safety on
m_sliceinaccessibleSummary()is handled. - LevelBar matched by
objectNameis the right call given it has noQ_OBJECT;setObjectName(...)runs inbuildTabContent(), well before any AT query, so the lazy lookup resolves fine. - Single-install static guard mirrors
PhaseKnobexactly.
Two minor, optional notes (neither blocking):
-
Dead branch in
accessibleSummary().SliceModel::letter()never returns empty — it falls back to'A' + m_id(SliceModel.h:34-36). So theletter.isEmpty() ? "VFO" : "VFO slice %1"ternary's first arm is unreachable. Harmless, but you could drop it for clarity (the!m_sliceguard above already covers the no-slice case with"VFO flag"). -
Translatable text. The summary uses
QStringLiteralrather thantr(). Since this is spoken AT text,tr()would be marginally more correct for future localization — though the codebase is inconsistent here (the VfoWidget itself mixestr("Signal meter")with plain-literal accessible names), so this matches local convention either way.
LGTM. Nice fill of the collapsed-flag gap.
🤖 aethersdr-agent · cost: $3.8736 · model: claude-opus-4-8
There was a problem hiding this comment.
Reviewed the diff against the codebase. This is a clean, well-scoped accessibility addition — thanks @ten9876.
What I verified
- Follows the established precedent.
LevelBarAccessible/ the factory registration mirrorPhaseKnob.cpp(static-flag-guardedinstallFactory,NoRoleto drop a decorative render-only widget from AT navigation) almost line-for-line. Good consistency. - Frequency unit is correct.
SliceModel::frequency()returns MHz (SliceModel.h:38), soarg(..., 0, 'f', 6)yields14.225000as the description shows — not a Hz value. ✅ - No null-deref / leak risk.
accessibleSummary()guardsm_slice; theVfoWidgetfactory branch only runs on a key match (so theqobject_cast<QWidget*>is always non-null); Qt owns the returned interface. Summary is computed at query time, so it's always fresh. - No Qt-floor concern. Only long-standing APIs (
QAccessibleWidget,installFactory,NoRole, theGroupingctor) — nothing Qt 6.5+, consistent with the Linux CI floor. LevelBarobjectName tagging is safe.LevelBaris fully custom-painted (no QSS selectors keyed on objectName), sosetObjectName()has no styling side effects.
Minor, non-blocking
- Matching
LevelBarbyobjectNameis the right call given it has noQ_OBJECTof its own, but it's a slightly looser contract than the class-name key path — if anything else ever reuses"AetherSDR.LevelBar"the factory would wrap it. The namespaced constant makes a collision unlikely, so this is fine as-is; just worth a mental note. - The out-of-scope deferral of live
NameChangedre-announcement on TX-toggle/collapse is the right scoping, and you flagged it honestly. A collapsed flag's summary being correct on query/navigation already closes the reported gap.
Looks good to merge from my side. Nice, focused fix.
🤖 aethersdr-agent · cost: $5.1176 · model: claude-opus-4-8
…rites (rebased #3765) (#3771) Closes #3762. **Rebased + conflict-resolved version of #3765** by @dsocha (original authorship preserved on all three commits). Opened from a maintainer branch because the original is on a fork; #3765 should be closed in favor of this. ## What it fixes (unchanged from #3765) Two GPU-flag-sprite rendering bugs in the SmartMTR meter view (#3723): 1. **Extremes value labels never drew (#3762)** — `drawSmartMtrLabels()` gated on `m_smartMtrWidget->isVisible()`, false in GPU-sprite mode. Now gates on `!size().isEmpty()` (the same condition the sprite is drawn under), so labels track the markers in both GPU and software paths. 2. **Meter selector vanished from the sprite** — `QWidget::render()` can't rasterize a `QGraphicsEffect`. The selector now gates its underline on an explicit `m_meterMenuOpen` flag (not the hidden row's `isVisible()`), and the option rows drop the `QGraphicsOpacityEffect`s in favor of `setEnabled(false)` + `:disabled` stylesheet dimming (new `QComboBox:disabled` rule in `ComboStyle.h`), so they stay visible **and** dimmed in the sprite. ## Rebase / conflict resolution (the reason this is a new branch) The original #3765 conflicted with **#3760** ("disable identity opacity effect…"), which merged into `main` after #3765 was branched. Both target the same blank-rows-in-sprite bug, but: - **#3760** only neutralized the effect at *identity* (opacity 1.0) — i.e. for *enabled* rows. A *disabled* row still carried an active 0.45 effect, so it would **still render blank** in a sprite. - **#3765** removes the effects entirely, so both enabled and disabled rows render. **#3765 ⊇ #3760.** Resolved `syncSmartMtrSettingsState` in favor of #3765's approach and **dropped #3760's now-dead `setRowOpacity` lambda + the `m_*Fade` members**. Verified: no dangling `m_*Fade`/`kDisabledOpacity` references; the `QGraphicsOpacityEffect` on the DSP filter-level row (meant to vanish in sprites) is untouched. ## Verification - Rebased onto `main` (`437f7469`); conflict resolved; commits signed. - Composes with the merged #3752 (M3 `syncSmartMtrSettingsControls` re-seeds combos, then `syncSmartMtrSettingsState` disables the rows) and #3758 (a11y — `QAccessibleInterface` reference preserved; a11y linter passes 0 findings). - Original hardware test (macOS / FlexRadio 6000) per #3765. Credit: @dsocha. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Dariusz Socha <dariusz.socha.pl@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Closes #3754.
Implements
QAccessibleInterfacefor the two custom-painted widgets the Qt Accessibility Static Analysis flagged (both pre-existing; #3751 surfaced them by touching the file).VfoWidget →
VfoWidgetAccessible(roleGrouping)The VFO flag custom-paints its chrome and, while collapsed, the slice-letter and TX badges — which have no accessible child-widget equivalent, so a screen reader on a collapsed flag got nothing. Added a
text(QAccessible::Name)summary built from a newVfoWidget::accessibleSummary():Expanded-mode detail (frequency, dBm) is already announced via the child labels'
QAccessibleValueChangeEvents, so this fills the collapsed-mode gap and gives the group a meaningful name for navigation.LevelBar →
LevelBarAccessible(NoRole)LevelBaris the render-only ESC combiner level meter. Following the existingPhaseKnobprecedent (whose comment states "the named ESC phase/gain sliders are the accessible interface"), the bar is decorative — the ESC gain/phase sliders carry the accessible semantics — so it returnsQAccessible::NoRoleto drop out of AT navigation. Matched byobjectNamesinceLevelBaris a.cpp-local class with noQ_OBJECTof its own.Notes
QAccessible::installFactory, mirroringPhaseKnob.cpp.-fsyntax-onlycompile against Qt6; follows the established a11y pattern indocs/a11y.md.QAccessible::NameChanged) is a possible future enhancement — out of scope here; the summary is correct on query/navigation.🤖 Generated with Claude Code