feat(accessibility): screen reader support for NVDA / Narrator / VoiceOver (WIP)#10955
feat(accessibility): screen reader support for NVDA / Narrator / VoiceOver (WIP)#10955fla-rion wants to merge 7 commits into
Conversation
…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>
Update: Root cause identified and fixes appliedAfter further investigation, three concrete root causes were found and fixed: 1.
|
… 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>
|
This is a very good fix PR, but we need to pull it locally for testing and verification. |
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-drawnwxWindowsubclasses. Without explicitwxAccessibleobjects they have no MSAApresence, so NVDA announces nothing.
Problem 2 – Buttons excluded from Tab order
Custom
Button(→StaticBox→wxWindow) did not addWS_TABSTOPto itsWin32 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_TABSTOPandcaptured 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/stateToggleAccessible– for CheckBox and SwitchButton (CHECKBUTTON role, checked state)ProgressBarAccessible– value/min/maxTextCtrlLabelAccessible– pairs a static label with its input fieldTabButtonAccessible– PAGETAB role + SELECTED state for notebook tabsComboBoxAccessible,PrintOptionItemAccessible,ValueButtonAccessible,GLCanvasAccessible(with virtual toolbar children for gizmos)Widget layer
MSWGetStyle()override addsWS_TABSTOP;onSetFocusfiresNotifyEvent(FOCUS, OBJID_CLIENT)so NVDA reads the button name on TabNotifyEvent(STATECHANGE)on toggle; SwitchButtonrole changed from PUSHBUTTON → CHECKBUTTON
with appropriate accessible objects
WebView Tab-order fix (
WebView.cpp,MainFrame.cpp) ← new this pushwebView->Create()on Windows,WS_TABSTOPis stripped from the wrapperHWND via
SetWindowLong, so Tab-order traversal skips the embedded browser.MainFrame: switching to a WebView tab no longer callsSetFocus()on theWebView 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
How to test
-DwxUSE_ACCESSIBILITY=ON(already set indeps/wxWidgets/wxWidgets.cmake)("Home, tab", "Prepare, tab", …), sidebar controls, etc.