Несколько лет назад я публиковал статью о том, как писать код не глядя.
Статья неплохо зашла, настолько неплохо, что меня пригласили поднимать с колен доступность в IDE на базе IntelliJ.
С виндой то там всё было ок, а вот для MacOS требовалось вмешательство, и я вмешался. Погнали под кат, там всё расскажу. А кто буковы не любит, внизу видео с моего доклада на SnowOne по этой теме.
IntelliJ работает у нас на основе Swing. Тот в свою очередь — часть OpenJDK, вот в неё и полезем. Но давайте по порядку.
Доступность
Accessibility, A11y в англоязычной среде
В разработке UI — это обеспечение возможности использования ПО как можно большим числом людей, включая тех, чьи способности как-либо ограничены.
Для нас это значит, что мы предоставляем альтернативный интерфейс для людей с ограниченными возможностями. В нашем случае, для незрячих. И незрячий человек должен получить достаточно информации, чтобы взаимодействовать с нашим продуктом.
Как это работает?
Рассматривать будем на примере реализации для MacOS. Потому как на MacOS существует всего один Accessibility API, и нам будет его достаточно, чтобы понять принцип.
Что у нас есть?
Клиент специальных возможностей VoiceOver;
Протокол NSAccessibility;
Графический интерфейс на Swing.
Когда мы производим какое-нибудь действие, например, при помощи клавиши Tab перемещаем фокус клавиатуры на другой элемент, внутри JVM отправляется событие, в котором объекту CAccessibility, который реализует фокус листнер, сообщается, что фокус изменился.
CAccessibility вызывает нативный метод, который отправляет NSAccessibilityFocusedUIElementChangedNotification с нативным представлением компонента, который создал событие.
Клиент специальных возможностей, который сидит в системном центре уведомлений, слышит это уведомление и у приложенного компонента вызывает accessibilityFocusedUIElement. Реализация этого селектора спрашивает через JNI в Java, какой объект теперь в фокусе, и получает нативную реализацию этого нового компонента.
После всего этого клиент доносит информацию об этом новом сфокусированном компоненте до пользователя. В случае с VO при помощи синтеза речи зачитываются значения, возвращаемые нативными селекторами accessibilityLabel, accessibilityRole и т.д. Их там навалом.
Похожим образом происходит работа с другими изменениями: выбранная строка списка, ячейка таблицы, развёрнутая/свёрнутая нода дерева, только через другой листнер и другие нативные уведомления.

NSAccessibility
Давным-давно у Apple существовал протокол на основе методов NSObject.
Он состоял из нескольких методов, которые предоставляли список доступных атрибутов, и по имени атрибута возвращали его значение, например:
NSAccessibilityTitleAttribute — имя объекта;
NSaccessibilityHelpAttribute — подсказка;
NSAccessibilityRoleAttribute — роль объекта.
Ему на смену пришёл протокол на основе ролей. Apple предоставили 18 протоколов ролей, приняв какой-нибудь из них, можно реализовать представление доступности для соответствующего объекта.
NSAccessibilityElement;
NSAccessibilityButton;
NSAccessibilityCheckBox;
NSAccessibilityRadioButton;
NSAccessibilitySwitch;
NSAccessibilityStaticText;
NSAccessibilityNavigableStaticText;
NSAccessibilityImage;
NSAccessibilityProgressIndicator;
…
Самый примечательный из них — NSAccessibilityElement, он позволит реализовать любой элемент, для роли которого не нашлось протокола.
Иерархия
Ролей у Apple больше чем протоколов, но меньше чем ролей в Java. Что же делать с остальными ролями? Игнорировать, вот так просто.
Как и в традиционном UI, в Accessibility все компоненты выстраиваются в иерархию по вложенности, но для нашего пользователя информация о рут пэйнах, вью портах и прочей тысячи панелей, которые использовал разработчик UI, чтобы вот эта кномпочка стояла точно в 39 черешнях от края окошка, не нужна, поэтому мы эти лишние роли просто опускаем за некоторым исключением.

На картинке показана иерархия javax.accessibility.AccessibleContext в сравнении с иерархией NSAccessibility элементов. Видно, что элементов в Java иерархии больше, но мы все панели опускаем для нативного представления.
В каком случае нам всё же понадобятся панели?
Когда элемент списка, ячейка таблицы или нода дерева представляет из себя сложный рендерер, например, два лейбла, как на картинке ниже.

Что дальше
Все эти изменения мы апстримили в OpenJDK, так что если вы используете Swing для создания UI, начиная с версии OpenJDK 17.0.2 доступность на MacOS на уровне JVM появится и у вас.
Но этого не достаточно, если вы хотите предоставить качественный интерфейс доступности. Вам всё же придётся делать некоторые изменения.
Пример 1
JLabel label = new JLabel("This is the second text field:");
JTextField secondTextField = new JTextField("some text 2");
JPanel panel = new JPanel();
panel.setLayout(new FlowLayout());
panel.add(label);
panel.add(secondTextField);
Создаём лэйбл и текстовое поле. Но если фокус встанет в такое поле, VO произнесёт только текст внутри, а название поля, которое в лэйбле указано, произнесено не будет.
Чтобы исправить положение, добавим setLabelFor().
JLabel label = new JLabel("This is the second text field:");
JTextField secondTextField = new JTextField("some text 2");
label.setLabelFor(secondTextField);
JPanel panel = new JPanel();
panel.setLayout(new FlowLayout());
panel.add(label);
panel.add(secondTextField);
Пример 2
Вот ещё одна распространённая ошибка. В списке кастомный рендерер:
public static class AccessibleJListTestRenderer extends JPanel implements ListCellRenderer {
private JLabel labelAJT = new JLabel("AJL");
private JLabel itemName = new JLabel();
// …
@Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
itemName.setText(((String) value));
return this;
}
}
Но элементы этого списка будут проговариваться все одинаково, потому что при отсутствии имени у компонента, обычно, проговаривается роль. Значит, панели надо задать имя.
public static class AccessibleJListTestRenderer extends JPanel implements ListCellRenderer {
private JLabel labelAJT = new JLabel("AJL");
private JLabel itemName = new JLabel();
// …
@Override
public Component getListCellRendererComponent(JList list, Object value, int index, boolean isSelected, boolean cellHasFocus) {
itemName.setText(((String) value));
getAccessibleContext().setAccessibleName(labelAJT.getText() + ", " + itemName.getText());
return this;
}
}
Вот и всё. Спасибо, что уделили время этой статье. Надеюсь, она показалась вам интересной. Как и обещал, ниже будет видео с моего доклада. Прошу прощения, мне кажется, я там был несколько косноязычен.
А теперь погнали обсуждать в комментах!