Skillversitas

Accessibility

Accessibility is the app's interface to the assistive technologies - the screen reader, the magnifier, the on-screen keyboard - and in GTK it is provided by ATK (the Accessibility Toolkit), the layer that exposes every widget's state and semantics to those technologies. The model is that each widget has an AtkObject (reached through get_accessible()) that carries the widget's accessible name, description, role, and state, and the toolkit populates most of it automatically - the Label's text is the name of the control it labels, the Button's label is its accessible name. The app's job is to fill the gaps: the widgets whose meaning is not in their visible text (the icon-only button, the image, the custom widget), the descriptions that add context, and the keyboard path that reaches every control. This chapter covers the accessible object, the name and description, the role and state, the keyboard navigation, and the rules that keep a UI usable without a mouse.

The Accessible Object

import gtk

# every widget has an accessible - the AtkObject that the
# assistive technologies read:
button = gtk.Button(label="Save")
acc = button.get_accessible()
print(acc.get_name())          # "Save" - from the label
print(acc.get_role())          # the ATK_ROLE_PUSH_BUTTON
print(acc.get_description())   # None - not set

# the accessible NAME is what the screen reader announces
# when the control gets focus; the toolkit derives it from
# the widget's visible text where it exists:
#   a labeled button - the label is the name
#   a label next to an entry - the label is the entry's
#     accessible name (the label-for relationship)

# the GAP: the icon-only button has no visible text, so the
# accessible name is empty - the screen reader announces
# nothing. the app must set it:
icon_button = gtk.Button()
icon_button.set_image(gtk.Image.new_from_icon_name(
    "document-save", gtk.ICON_SIZE_BUTTON))
icon_button.get_accessible().set_name("Save the document")
# now the screen reader announces "Save the document, button"

# the tooltip is a second channel (the visual hint), and it
# does NOT become the accessible name automatically:
icon_button.set_tooltip_text("Save the document (Ctrl+S)")

The accessible object is the AtkObject behind every widget, reached by get_accessible(), and it is what the screen reader and the other assistive technologies read: the get_name (what the control is, announced on focus), the get_description (the longer context), the get_role (the control's kind - the push button, the entry, the check box), and the state. The toolkit populates most of it from the widget's visible semantics: the labeled button's name is its label, and the label adjacent to an entry becomes the entry's accessible name through the label-for relationship (the Label's set_mnemonic_widget or the label's proximity, which the toolkit wires). The gap is the widget without visible text - the icon-only button, the image, the custom drawing - whose accessible name is empty and whose screen-reader announcement is nothing. The app fills it with the set_name on the accessible, and the description (set_description) adds the context the name does not carry. The discipline: every control has an accessible name that states what it does, and where the visible text does not provide it, the app sets it explicitly.

Names, Descriptions, and the Roles

import gtk
import atk

# the description - the context the name does not carry:
entry = gtk.Entry()
entry.get_accessible().set_name("Search text")
entry.get_accessible().set_description(
    "Type part of a filename to filter the list")

# the role - the control's kind; the toolkit sets it from
# the widget type, and the custom widget sets its own:
class Dial(gtk.Widget):
    def __init__(self):
        gtk.Widget.__init__(self)
        # the role: tell the AT what this control IS
        self.get_accessible().set_role(
            atk.Role.SLIDER)
        self.get_accessible().set_name("Volume")
    def do_expose_event(self, event):
        return False

# the state - the ATK_STATE_* the AT reads:
#   SENSITIVE (enabled), FOCUSED, SELECTED, CHECKED,
#   EXPANDED, VISIBLE, SHOWING
# the toolkit maintains most of them from the widget state;
# the app's sensitivity change (set_sensitive) is reflected
# in the accessible state automatically

# the relation - the label-for: the explicit label-to-control
# link (the programmatic form of the proximity wiring):
label = gtk.Label("Volume:")
label.set_mnemonic_widget(dial)   # the label names the dial

The description is the context layer - the set_description on the accessible, the longer text the screen reader reads on demand (the "read description" key) that the name does not carry. The role is the control's kind - the atk.Role the toolkit sets from the widget type (the PUSH_BUTTON, the ENTRY, the CHECK_BOX) - and the custom widget that is a slider in behavior sets its own role (atk.Role.SLIDER) so the AT announces it as a slider, not as an unknown object. The state is the ATK_STATE set the AT reads - the SENSITIVE (the enabled), the FOCUSED, the SELECTED, the CHECKED - and the toolkit maintains most of them from the widget's state, so the set_sensitive the app calls is reflected in the accessible state without a separate call. The relation is the explicit label-for - the set_mnemonic_widget that links the label to the control it names, the programmatic form of the proximity wiring. The four together - the name, the description, the role, the state - are the semantics the AT reads, and the app's job is to make them present and correct for every control, especially the ones the toolkit cannot derive.

The Keyboard Path: No-Mouse Usability

import gtk
import gdk

# the keyboard navigation is the AT's primary input: the
# Tab order, the focus, the activator key
# the Tab order follows the widget tree by default (the
# order the widgets are added); the set_can_focus and the
# focus chain control it:

# every interactive control is focusable (the default for
# the buttons, entries, checks); the custom widget opts in:
dial.set_can_focus(True)

# the focus-visible: the focus is drawn (the focus ring)
# the toolkit's theme draws it; the custom widget that
# overrides the draw must draw the focus state itself:
def do_expose_event(self, event):
    cr = self.window.cairo_create()
    # ... the normal draw ...
    if self.has_focus():
        cr.set_source_rgb(0, 0, 0.8)
        cr.rectangle(0, 0,
                     self.allocation.width,
                     self.allocation.height)
        cr.set_line_width(2)
        cr.stroke()
    return False

# the activator: the key that activates the focused control
# (Space/Enter for the button) - the toolkit handles it for
# the standard widgets; the custom widget connects the key:
def do_key_press_event(self, event):
    if event.keyval in (gdk.KEY_Return, gdk.KEY_space):
        activate_dial()
        return True
    return False

# the mnemonics: the Alt+letter on the menu items and the
# buttons (the _ in the label, the menus chapter) - the
# keyboard path to the menu and the action buttons

The keyboard path is the no-mouse usability: every control reachable by the Tab (the focus traversal, following the widget tree by default), every action triggerable by a key (the activator - the Space/Enter on the focused button, the mnemonic on the menu and the buttons), and the focus visible (the focus ring the user sees). The standard widgets handle their own keyboard behavior (the Tab focus, the activator, the Enter in the entry), and the app's jobs are the gaps: the set_can_focus(True) on the custom widget that should be in the focus chain, the focus-state draw in the custom widget's expose (the has_focus check, the ring drawn - the custom widget that overrides the draw must draw the focus itself, or it is invisible to the keyboard user), the do_key_press_event that activates on the activator keys, and the mnemonics (the _ markers) that give the menu and the action buttons their keyboard access. The test is the one that defines the requirement: the whole UI usable with the keyboard alone - the Tab to every control, the key to every action, the focus visible at every step - and the assistive technologies ride on that same keyboard path.

The Working Rules

  • Every control has an accessible name. The visible text provides it where it exists (the labeled button, the label-for entry); the set_name on the accessible provides it where it does not (the icon-only button, the image, the custom widget) - the empty name is the control the screen reader cannot announce.
  • The description is the context, the role is the kind. The set_description adds the on-demand context; the set_role on the custom widget tells the AT what the control is (the slider, not the unknown) - the two that make the custom control semantically present.
  • The custom widget draws its focus state. The has_focus check in the expose, the ring drawn - the custom widget that overrides the draw and skips the focus is invisible to the keyboard user, and the focus-visible is the requirement.
  • The keyboard reaches everything. The set_can_focus on the interactive custom widget, the activator key handler, the mnemonics on the menu and the buttons - the UI usable with the keyboard alone is the test, and the AT rides on that path.
  • The state is maintained by the toolkit; verify the sensitivity. The set_sensitive is reflected in the accessible state automatically, and the disabled control is announced as unavailable - the state the AT reads is the widget's state, kept in sync by the toolkit.

Summary

Accessibility in GTK is the ATK layer: every widget has an AtkObject (get_accessible()) that carries the name (what the control is, announced on focus), the description (the on-demand context), the role (the control's kind), and the state (the SENSITIVE, the FOCUSED, the SELECTED), and the toolkit populates most of it from the widget's visible semantics - the labeled button's name, the label-for entry, the state from the set_sensitive. The app fills the gaps: the set_name on the icon-only and the custom control (the empty name is the unannounced control), the set_description for the context, the set_role on the custom widget that is a slider in behavior, and the set_mnemonic_widget for the explicit label-for. The keyboard path is the no-mouse usability - the Tab focus chain (the set_can_focus on the custom widget), the activator keys (the custom widget's do_key_press_event), the mnemonics, and the focus ring the custom widget must draw in its expose. The test that defines the requirement: the whole UI usable with the keyboard alone, every control named, and the focus visible at every step.