Skip to content

feat(accessibility): screen reader support for NVDA / Narrator / VoiceOver (WIP)#10955

Open
fla-rion wants to merge 7 commits into
bambulab:masterfrom
fla-rion:feat/accessibility-nvda-screenreader
Open

feat(accessibility): screen reader support for NVDA / Narrator / VoiceOver (WIP)#10955
fla-rion wants to merge 7 commits into
bambulab:masterfrom
fla-rion:feat/accessibility-nvda-screenreader

Conversation

@fla-rion

@fla-rion fla-rion commented Jun 1, 2026

Copy link
Copy Markdown

Accessibility: make Bambu Studio usable with NVDA and other screen readers (WIP)

This PR adds MSAA/IAccessible support so keyboard-only users and people using
screen readers (NVDA, Narrator, VoiceOver) can navigate and operate Bambu Studio.

Root problems addressed

Problem 1 – Custom widgets invisible to MSAA
BambuStudio's UI widgets (Button, SwitchButton, etc.) are fully custom-drawn
wxWindow subclasses. Without explicit wxAccessible objects they have no MSAA
presence, so NVDA announces nothing.

Problem 2 – Buttons excluded from Tab order
Custom Button (→ StaticBoxwxWindow) did not add WS_TABSTOP to its
Win32 window style, so it was invisible to keyboard Tab navigation.

Problem 3 – NVDA says "wxwebview" on every Tab press
Embedded Edge/WebView2 controls (Home tab, Device tab) held WS_TABSTOP and
captured Tab focus, preventing NVDA from ever reaching real UI controls.

Changes

Core accessibility infrastructure (Accessibility.hpp/.cpp)

  • ButtonAccessible – name from label/tooltip, PUSHBUTTON role, focus/state
  • ToggleAccessible – for CheckBox and SwitchButton (CHECKBUTTON role, checked state)
  • ProgressBarAccessible – value/min/max
  • TextCtrlLabelAccessible – pairs a static label with its input field
  • TabButtonAccessible – PAGETAB role + SELECTED state for notebook tabs
  • ComboBoxAccessible, PrintOptionItemAccessible, ValueButtonAccessible,
    GLCanvasAccessible (with virtual toolbar children for gizmos)

Widget layer

  • Button: MSWGetStyle() override adds WS_TABSTOP; onSetFocus fires
    NotifyEvent(FOCUS, OBJID_CLIENT) so NVDA reads the button name on Tab
  • CheckBox / SwitchButton: NotifyEvent(STATECHANGE) on toggle; SwitchButton
    role changed from PUSHBUTTON → CHECKBUTTON
  • ProgressBar, SpinInput, TempInput, TextInput, ComboBox, Notebook: all wired
    with appropriate accessible objects

WebView Tab-order fix (WebView.cpp, MainFrame.cpp) ← new this push

  • After webView->Create() on Windows, WS_TABSTOP is stripped from the wrapper
    HWND via SetWindowLong, so Tab-order traversal skips the embedded browser.
  • MainFrame: switching to a WebView tab no longer calls SetFocus() on the
    WebView panel; focus is redirected to the topbar instead, giving NVDA a real
    accessible control to announce.

Dialog / panel names

Accessible names added to controls in: SelectMachine, StatusPanel, PrintOptions,
Preferences, AMSMaterialsSetting, AMSMappingPopup, MixedFilamentDialog,
FilamentMapDialog, CapsuleButton, ObjectList, UserPresetsDialog, and more.

Status

  • Builds and links on Windows (MSVC, wxUSE_ACCESSIBILITY=ON)
  • WebView Tab-capture bug fixed — Tab now cycles through real UI controls
  • Manual testing with NVDA still in progress
  • VoiceOver / Narrator not yet validated

How to test

  1. Build with -DwxUSE_ACCESSIBILITY=ON (already set in deps/wxWidgets/wxWidgets.cmake)
  2. Run with NVDA active
  3. Press Tab/Shift+Tab — should cycle through toolbar buttons, tab buttons
    ("Home, tab", "Prepare, tab", …), sidebar controls, etc.
  4. Activate a checkbox/switch — NVDA should announce state change
fla-rion and others added 5 commits June 1, 2026 17:23
…t (WIP)

Introduces MSAA/IAccessible support for Bambu Studio's custom wxWidgets UI
so that blind and low-vision users can operate the slicer with screen readers
(NVDA, Windows Narrator, macOS VoiceOver).

What was done:
- Enable wxUSE_ACCESSIBILITY=ON in wxWidgets build
- Add Accessibility.hpp/.cpp with wrapper classes:
    ButtonAccessible, ToggleAccessible, TabButtonAccessible,
    TextCtrlLabelAccessible, ComboBoxAccessible, ProgressBarAccessible,
    ValueButtonAccessible, PrintOptionItemAccessible, GLCanvasAccessible
- Wire accessible objects to all major custom widgets:
    Button, CheckBox, SwitchButton, ProgressBar, SpinInput, TempInput,
    TextInput, ComboBox, Notebook tabs
- Accessible names and roles in key dialogs:
    SelectMachine, StatusPanel, Preferences, PrintOptionsDialog,
    AMSMaterialsSetting, AmsMappingPopup, MixedFilamentDialog and more
- GLCanvas virtual MSAA children for 3D toolbar actions
- wxTAB_TRAVERSAL fixes for previously non-focusable panels
- accessibility.js helper for embedded web guide pages

Current status (WIP - NVDA not yet announcing reliably):
The code compiles and the app runs. However NVDA does not yet announce all
controls correctly. Root cause under investigation - likely one of:
  1. Custom wxWindow subclasses not registering IAccessible with Win32
  2. SetAccessible() called before the HWND is fully created
  3. NVDA object navigation skipping windowless children

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…idget

Custom Button (StaticBox->wxWindow) was invisible to NVDA because:
1. No WS_TABSTOP: Windows dialog manager and NVDA Tab-navigation skipped
   it entirely since it lacked the WS_TABSTOP Win32 window style.
2. No explicit focus notification: When wxWidgets routed focus to the
   Button via its own traversal, Windows fired EVENT_OBJECT_FOCUS for
   OBJID_WINDOW (the default HWND accessible), not OBJID_CLIENT where our
   ButtonAccessible lives. NVDA therefore got no accessible name/role.

Fixes:
- Override MSWGetStyle() to set WS_TABSTOP when canFocus==true, ensuring
  the Win32 dialog manager and NVDA Tab-mode include the button.
- Update SetCanFocus() to dynamically add/remove WS_TABSTOP via
  SetWindowLong() when focus capability changes after creation.
- Add EVT_SET_FOCUS / EVT_KILL_FOCUS handlers: onSetFocus() calls
  wxAccessible::NotifyEvent(EVENT_OBJECT_FOCUS, ..., OBJID_CLIENT) so
  NVDA is directed to the ButtonAccessible and reads the correct name
  and role. Both handlers call Refresh() for a visual focus indicator.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a CheckBox or SwitchButton is toggled, NVDA was not announcing the
new state (checked/unchecked) because no EVENT_OBJECT_STATECHANGE WinEvent
was fired. Screen readers rely on this event to re-query the IAccessible
and announce the new state (e.g. "checked" or "unchecked").

Add wxAccessible::NotifyEvent(EVENT_OBJECT_STATECHANGE) to the
wxEVT_TOGGLEBUTTON handler in both widgets so NVDA immediately
announces the new checked state after each toggle.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SwitchButton is an on/off toggle — using ROLE_SYSTEM_CHECKBUTTON
allows NVDA to announce "checked" / "not checked" correctly when
the switch is toggled. The previous ROLE_SYSTEM_PUSHBUTTON had no
concept of checked state, so NVDA announced no state information.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…Value()

TabButtonAccessible::GetState() was reporting SELECTED only when the tab
button had keyboard focus, causing NVDA to miss the selected state when
the user navigated inside a tab page and the tab button lost focus.

Fix: cast to Button* and call GetValue() (= m_selected) to determine
whether this tab is currently the active one, and report SELECTED
accordingly. Also add SELECTABLE so AT knows the tab is selectable.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@fla-rion

fla-rion commented Jun 1, 2026

Copy link
Copy Markdown
Author

Update: Root cause identified and fixes applied

After further investigation, three concrete root causes were found and fixed:

1. Button widget missing WS_TABSTOP (critical)

The custom Button (inherits StaticBoxwxWindow) had no WS_TABSTOP Win32 window style. This caused two problems:

  • Windows dialog manager and NVDA Tab-navigation completely skipped Button controls
  • When Windows fired EVENT_OBJECT_FOCUS, it used OBJID_WINDOW (the default HWND accessible) instead of OBJID_CLIENT where our ButtonAccessible lives — so NVDA got no name/role

Fix:

  • Override MSWGetStyle() in Button to add WS_TABSTOP when canFocus == true
  • Update SetCanFocus(bool) to dynamically add/remove WS_TABSTOP via SetWindowLong()
  • Add EVT_SET_FOCUS handler that calls wxAccessible::NotifyEvent(EVENT_OBJECT_FOCUS, ..., OBJID_CLIENT) — this explicitly directs NVDA to the ButtonAccessible (OBJID_CLIENT) rather than the default window accessible (OBJID_WINDOW)

2. CheckBox / SwitchButton not announcing state changes

When a checkbox or switch was toggled, NVDA was not announcing "checked"/"unchecked" because no EVENT_OBJECT_STATECHANGE WinEvent was fired.

Fix: Add wxAccessible::NotifyEvent(EVENT_OBJECT_STATECHANGE) in the wxEVT_TOGGLEBUTTON handler of both widgets.

3. SwitchButton wrong MSAA role

SwitchButton used ROLE_SYSTEM_PUSHBUTTON which has no concept of checked state. NVDA announced nothing about on/off state.

Fix: Changed to ROLE_SYSTEM_CHECKBUTTON so NVDA announces "checked"/"not checked".

4. Tab button selected-state not reported correctly

TabButtonAccessible::GetState() only reported SELECTED if the tab currently had keyboard focus. It now uses Button::GetValue() (the m_selected flag) for correct state independent of focus.


A new build is in progress and will be tested with NVDA. Will update this PR with test results.

… browser

Previously Tab/Shift+Tab on Windows would land on the embedded Edge WebView2
(wxWebView) controls, causing NVDA to repeatedly announce "wxwebview" instead
of real UI elements like buttons and tabs.

Two complementary fixes:
- WebView.cpp: after Create() on Windows, strip WS_TABSTOP from the wrapper
  HWND via SetWindowLong so Windows Tab-order traversal skips the WebView.
- MainFrame.cpp: don't call SetFocus() on WebView panels (m_webview,
  m_printer_view, m_web_device) when their tab is selected; redirect focus
  to m_topbar instead so NVDA starts on a real accessible control.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tonghao-bbl tonghao-bbl added the enhancement optimize for some feature or user interface label Jun 17, 2026
@BambulabRobot BambulabRobot requested review from XinZhangBambu and milk-pure and removed request for milk-pure and tonghao-bbl June 22, 2026 08:42
@MackBambu

Copy link
Copy Markdown
Contributor

This is a very good fix PR, but we need to pull it locally for testing and verification.

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

Labels

enhancement optimize for some feature or user interface

3 participants