Skip to content

fix: report Range control values to screen readers on Windows - #269

Open
omaribrahimsaad wants to merge 2 commits into
musescore:mainfrom
omaribrahimsaad:fix/uia-slider-value
Open

omaribrahimsaad wants to merge 2 commits into
musescore:mainfrom
omaribrahimsaad:fix/uia-slider-value

Conversation

@omaribrahimsaad

@omaribrahimsaad omaribrahimsaad commented Sep 3, 2026

Copy link
Copy Markdown

Resolves: audacity/audacity#11600

On Windows, no Range control in a muse-based application conveys its value to
screen readers. The control is reachable and interactive, but the value is never
announced — on focus NVDA reads " slider" with no value, and on each
Up/Down it reads "blank". Reproduced in Audacity 4 and in shipped MuseScore
Studio 4.7.4.

Two causes, both in accessibleiteminterface.cpp:

1. text() never handled QAccessible::Value. Qt reads a control's value
through text(QAccessible::Value) (QWindowsUiaValueProvider::get_Value).
text() handled Name, Accelerator and Description, then fell through to
default: break; returning an empty QString — the empty Value.Value. The
value data was always correct: AccessibleItem::accessibleValue() returns valid
doubles on every query, and AccessibilityController::propertyChanged() already
sends QAccessibleValueChangeEvent with the real value. Only the read path Qt
actually queries was missing. Qt Widgets does the equivalent in
QAccessibleSlider::text().

2. interface_cast advertised TextInterface for every role, so sliders
exposed UIA TextPattern. Screen readers then read the (empty) text content
instead of the value on value-change events. Scoped to exclude
QAccessible::Slider.

Before (inspect.exe, Audacity 4 track Pan slider):

Value.Value:             ""
LegacyIAccessible.Value: ""
IsTextPatternAvailable:  true
RangeValue.Value:        2.000000

After:

Value.Value:             "2"
LegacyIAccessible.Value: "2"
IsTextPatternAvailable:  false
RangeValue.Value:        2.000000   (unchanged)

RangeValuePattern is untouched, so clients reading it (JAWS, Narrator) are
unaffected. Verified text fields, buttons, checkboxes and spin boxes still
announce correctly.

Note: the TextInterface cast carries the comment "Without Action and Text
interfaces NVDA doesn't work", so it was added deliberately. I've only excluded
Slider and saw no regressions in testing, but if that cast is load-bearing for
a case I haven't hit, I'm happy to narrow the condition further.

Related: QTBUG-139296 covers the same symptom in stock Qt Quick Controls, which
this change works around rather than depends on.

Tested on Windows 10 22H2, Qt 6.10.3, NVDA 2026.2.

  • I signed the CLA as omarkhalil100
  • The title of the PR describes the problem it addresses.
  • Each commit's message describes its purpose and effects, and references the issue it resolves.
  • The code in the PR follows the coding rules.
  • I understand all aspects of the code I'm contributing and I'm able to explain it if requested.
  • The code compiles and runs on my machine, preferably after each commit individually. I have manually tested and verified that my changes fulfil their intended purpose.
  • No prior attempts to resolve this problem exist, or if they do, I listed them in my PR description and described how I avoided repeating past mistakes.
  • There are no unnecessary changes.
  • I created a unit test or vtest to verify the changes I made (if applicable).

Build configuration

audacity: audacity/audacity/master
audacity platforms: windows_x64
musescore: musescore/MuseScore/main
musescore platforms: windows_x64

@coderabbitai

coderabbitai Bot commented Sep 3, 2026

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Advanced

Run ID: 87b57795-d78d-44a7-9228-5f8a9372dfa9

📥 Commits

Reviewing files that changed from the base of the PR and between 2ac103f and 6d4e758.

📒 Files selected for processing (1)
  • framework/accessibility/internal/accessibleiteminterface.cpp

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


📝 Walkthrough

Walkthrough

The accessibility item interface now handles QAccessible::Value by returning the item's accessible value or an empty string when invalid. Slider roles no longer advertise QAccessibleTextInterface through interface_cast().

Priority: ➖ Normal

Severity of issue fixed: Medium

Merge Risk: ⚪ Minimal · up to 6d4e7

The accessibility change has no confirmed merge-blocking issue.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the primary change: reporting Range control values to Windows screen readers.
Description check ✅ Passed The description is complete and directly explains the problem, root causes, implementation, testing, linked issue, and checklist status. The unit test checkbox is unchecked, but the description remain…
Linked Issues check ✅ Passed Issue #11600 requires Windows screen readers to receive slider and dial values on focus and value changes. The PR adds QAccessible::Value handling in AccessibleItemInterface::text(), which returns…
Out of Scope Changes check ✅ Passed The diff changes only Windows accessibility value reporting and text-interface exposure in framework/accessibility/internal/accessibleiteminterface.cpp. These changes directly support issue #11600. …
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 1…
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@omaribrahimsaad

Copy link
Copy Markdown
Author

I've pushed a second commit replacing the Slider exclusion with the EditableText allowlist from #284, having tested it rather than guessing.

Windows 10 22H2, NVDA 2026.2, Audacity 4 at ad2ad6b: buttons, checkboxes and spin boxes no longer report IsTextPatternAvailable and no longer read "blank" on arrow keys. Text fields are unaffected — character review, selection announcements and cursor reporting all still work.

@kryksyh this changes what you approved, so it needs another look.

Separately, a consequence of exposing the value that belongs on the Audacity side. Controls that already carry their value in accessible.name now announce it twice, and because changing the value changes the name, needsRevoicing() fires and triggerRevoicing() bounces focus to a sibling on every keypress:

Removing the value from the name fixes both:

That's 23 controls in Audacity and I'll open that PR there. The same pattern exists here at TextInputField.qml:113, where a field's accessible name is its own text, so every keystroke bounces focus to a sibling — I'll file that separately.

@LeonarddeR

Copy link
Copy Markdown

@omaribrahimsaad Thanks for the editable text allow list. While this change is definitely necessary from the framework side of things (it is the same change I proposed in #284), it will regress MuseScore when not handled carefully, in the same way as nvaccess/nvda#20825.

The score view in MuseScore is a tree hierarchy of text nodes: page > system > measure > segment > chord > note.
This means that when moving from measure to measure, a new leaf of the tree gets focus: measure > segment > chord > note are all announced by NVDA rather than only the new note. This is overly verbose when the text pattern is removed from them.

From the NVDA side of things, this is explainable. NVDA treats a static text node as content when the control has text (i.e. a label). When it doesn't have text it is treated as layout. New layout nodes in the focus ancestry are ignored for announcements, new content nodes are not.
When there is a text pattern and that pattern is empty, NVDA treats the node as layout. When there is no text pattern, NVDA falls back to the label and the node is considered content. Every node in the score tree has a label, since AccessibleItem::accessibleName in MuseScore builds one for containers as well as leaves.

Note that Static text is a leaf control type; it is never meant to contain other static text. MuseScore already models the containers correctly: Page, System, Measure, Segment and Chord all create their accessible item with the Group role. It is the framework that collapses Group, Information and ElementOnScore into QAccessible::StaticText on Windows (AccessibleItemInterface::role(), accessibleiteminterface.cpp around line 302; UserRole on other platforms). That mapping only ever worked because the empty text pattern made screen readers ignore these nodes.

So I think the right fix, alongside this PR, is on the role side:

  1. Map Group to QAccessible::Grouping on Windows, which Qt exposes as a UIA Group. Note that this will also announce these items as groupings, which is an improvement in semantic sense.
  2. Decide in MuseScore which containers should carry a name. NVDA announces a named group when focus enters it and ignores an unnamed one, so this becomes a deliberate choice: announcing "Measure 5" when crossing a measure boundary may be wanted, announcing segment and chord is not. Those two could also be dropped from the tree entirely with accessibleIgnored.

Without that, merging this PR as is will trade "blank" on every arrow key for a burst of container names on every measure change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Slider and Dial accessibility: value can be either not read, or read incorrectly by Windows screen readers.

3 participants