Add accessibility support for wxStatusBar in wxGTK and wxOSX

Only wxMSW uses the native wxStatusBar which is recognized by screen
readers, while wxGTK and wxOSX use the generic version which is just a
window and so was not recognized by them as being what it is at all.

Add generic (but private for now) SetAccessibleElements() function
implemented for both of them and use it from wxStatusBarGeneric to let
Orca/VoiceOver know about the status bar fields and, in wxGTK, about its
role.

Assisted-by: Claude Opus 5.5
Co-authored-by: Quin Gillespie <trypsynth@gmail.com>
This commit is contained in:
Vadim Zeitlin
2026-10-01 03:01:52 +02:00
co-authored by Quin Gillespie
parent ac4cbd0c09
commit 29aa2bbd4c
4 changed files with 587 additions and 0 deletions
+68
View File
@@ -0,0 +1,68 @@
///////////////////////////////////////////////////////////////////////////////
// Name: wx/private/access.h
// Purpose: Private accessibility helpers used by the generic controls.
// Author: Vadim Zeitlin
// Created: 2026-09-30
// Copyright: (c) 2026 Vadim Zeitlin <vadim@wxwidgets.org>
// Licence: wxWindows licence
///////////////////////////////////////////////////////////////////////////////
#ifndef _WX_PRIVATE_ACCESS_H_
#define _WX_PRIVATE_ACCESS_H_
#include "wx/gdicmn.h"
#include "wx/string.h"
#include <vector>
class WXDLLIMPEXP_FWD_CORE wxWindow;
namespace wxPrivate
{
// Part of a custom drawn window, which is not a window itself, but should
// still be visible to the accessibility clients, e.g. a status bar field.
struct AccessibleElement
{
AccessibleElement(const wxString& label_, const wxRect& rect_)
: label(label_), rect(rect_)
{
}
// The text read by the screen readers.
wxString label;
// Rectangle occupied by the element in the client coordinates of the
// window it belongs to.
wxRect rect;
};
using AccessibleElements = std::vector<AccessibleElement>;
// Let the accessibility clients see the given elements as the children of this
// window, using the "static text" role.
//
// Calling this function again replaces the elements set by the previous call,
// but does nothing if they didn't change, so it's fine to call it whenever the
// elements might have changed. Passing an empty vector removes them.
//
// This function is currently implemented for wxOSX and wxGTK3 only and does
// nothing elsewhere.
#if defined(__WXOSX_COCOA__) || (defined(__WXGTK3__) && !defined(__WXGTK4__))
WXDLLIMPEXP_CORE void
SetAccessibleElements(wxWindow* win, const AccessibleElements& elements);
#else // !wxOSX && !wxGTK3
inline void
SetAccessibleElements(wxWindow* WXUNUSED(win),
const AccessibleElements& WXUNUSED(elements))
{
}
#endif // wxOSX || wxGTK3
} // namespace wxPrivate
#endif // _WX_PRIVATE_ACCESS_H_
+43
View File
@@ -23,6 +23,8 @@
#include "wx/control.h"
#endif
#include "wx/private/access.h"
#ifdef __WXGTK__
#include "wx/gtk/private.h"
#endif
@@ -44,6 +46,37 @@
// Margin between the field text and the field rect
#define wxFIELD_TEXT_MARGIN 2
// ----------------------------------------------------------------------------
// helpers
// ----------------------------------------------------------------------------
namespace
{
// Let the screen readers read the fields, which are not windows and so are
// invisible to them otherwise.
//
// This must be called whenever either the text or the position of the fields
// changes.
void UpdateAccessibleFields(wxStatusBarGeneric* statbar)
{
wxPrivate::AccessibleElements elements;
const int count = statbar->GetFieldsCount();
for ( int i = 0; i < count; ++i )
{
wxRect rect;
if ( !statbar->GetFieldRect(i, rect) )
continue;
elements.emplace_back(statbar->GetStatusText(i), rect);
}
wxPrivate::SetAccessibleElements(statbar, elements);
}
} // anonymous namespace
// ----------------------------------------------------------------------------
// GTK+ signal handler
// ----------------------------------------------------------------------------
@@ -123,6 +156,12 @@ bool wxStatusBarGeneric::Create(wxWindow *parent,
SetFieldsCount(1);
#if defined(__WXGTK3__) && !defined(__WXGTK4__)
// Let the screen readers know that this is a status bar, e.g. Orca has a
// command for reading it.
atk_object_set_role(gtk_widget_get_accessible(m_widget), ATK_ROLE_STATUSBAR);
#endif
#if defined( __WXGTK__ )
#if GTK_CHECK_VERSION(2,12,0)
if (HasFlag(wxSTB_SHOW_TIPS) && wx_is_at_least_gtk2(12))
@@ -160,6 +199,8 @@ void wxStatusBarGeneric::DoUpdateStatusText(int number)
Refresh(true, &rect);
UpdateAccessibleFields(this);
// it's common to show some text in the status bar before starting a
// relatively lengthy operation, ensure that the text is shown to the
// user immediately and not after the lengthy operation end
@@ -193,6 +234,8 @@ void wxStatusBarGeneric::DoUpdateFieldWidths()
// recompute the cache of the field widths if the status bar width has changed
m_widthsAbs = CalculateAbsWidths(width);
UpdateAccessibleFields(this);
}
bool wxStatusBarGeneric::ShowsSizeGrip() const
+406
View File
File diff suppressed because it is too large Load Diff
+70
View File
@@ -25,6 +25,7 @@
#include "wx/osx/private/datatransfer.h"
#endif
#include "wx/private/access.h"
#include "wx/private/bmpbndl.h"
#include "wx/evtloop.h"
@@ -4391,6 +4392,75 @@ void wxWidgetCocoaImpl::DoNotifyFocusEvent(bool receivedFocus, wxWidgetImpl* oth
}
}
void
wxPrivate::SetAccessibleElements(wxWindow* win, const AccessibleElements& elements)
{
NSView* const view = win->GetHandle();
if ( !view )
return;
// Check if anything has changed since the last call, as replacing the
// elements would make VoiceOver lose its position in them.
NSArray* const current = [view accessibilityChildren];
if ( current.count == elements.size() )
{
bool changed = false;
for ( size_t n = 0; n < elements.size(); ++n )
{
id const child = current[n];
if ( ![child isKindOfClass:[NSAccessibilityElement class]] )
{
changed = true;
break;
}
NSAccessibilityElement* const element = child;
const wxString label = wxCFStringRef::AsString([element accessibilityLabel]);
const NSRect frame = wxToNSRect(view, elements[n].rect);
if ( label != elements[n].label ||
!NSEqualRects([element accessibilityFrameInParentSpace], frame) )
{
changed = true;
break;
}
}
if ( !changed )
return;
}
if ( elements.empty() )
{
[view setAccessibilityChildren:nil];
return;
}
NSMutableArray* const children =
[NSMutableArray arrayWithCapacity:elements.size()];
for ( const auto& e : elements )
{
NSAccessibilityElement* const element =
[NSAccessibilityElement
accessibilityElementWithRole:NSAccessibilityStaticTextRole
frame:NSZeroRect
label:wxCFStringRef(e.label).AsNSString()
parent:view];
// Use the frame relative to the parent view and not the screen frame
// for the element to remain at the correct position if the window
// moves.
[element setAccessibilityFrameInParentSpace:wxToNSRect(view, e.rect)];
[children addObject:element];
}
// The container itself must be visible to the accessibility clients for
// them to reach its children.
[view setAccessibilityRole:NSAccessibilityGroupRole];
[view setAccessibilityChildren:children];
}
void wxWidgetCocoaImpl::SetCursor(const wxCursor& cursor)
{
if ( !wxIsBusy() )